Отношение LINQ to SQL не обновляет коллекции - PullRequest
3 голосов
/ 26 сентября 2011

У меня одна проблема с LINQ to SQL для Windows Phone (SQL Server CE).

Я занимаюсь разработкой приложения для личных финансов, а затем у меня есть класс Account и класс Transaction. Каждый класс Transaction имеет ссылку на учетную запись, к которой он принадлежит, поэтому класс Account имеет коллекцию транзакций в отношении «один ко многим». Затем у меня есть репозитории (AccountRepository и TransactionRepository), которые предоставляют методы для вставки, удаления, findbykey и возврата всех экземпляров каждого из этих классов. ViewModel имеет ссылки на свои репозитории. Хорошо, все работает просто отлично, но, когда я создаю транзакцию, она не появляется в коллекции Account.Transaction, пока я не остановлю программу и не запущу ее снова.

Вот некоторые фрагменты кода, во-первых, классы моделей:

[Table]
public class Transaction : INotifyPropertyChanged, INotifyPropertyChanging
{
    // {...}

    [Column]
    internal int _accountID;
    private EntityRef<Account> _account;
    [Association(Storage = "_account", ThisKey = "_accountID")]
    public Account Account
    {
        get {return _account.Entity;}
        set
        {
            NotifyPropertyChanging("Account");
            _account.Entity = value;

            if (value != null)
            {
                _accountID = value.AccountID;
            }

            NotifyPropertyChanged("Account");
        }
    }

    // {...}
}

[Table]
public class Account : INotifyPropertyChanged, INotifyPropertyChanging
{
    // {...}

    private EntitySet<Transaction> _transactions;

    [Association(Storage = "_transactions", OtherKey = "_accountID")]
    public EntitySet<Transaction> Transactions
    {
        get { return this._transactions; }
        set { this._transactions.Assign(value); }
    }

    public Account()
    {
        _transaction = new EntitySet<Transaction>(
            new Action<Transaction>(this.attach_transaction), 
            new Action<Transaction>(this.detach_transaction)
            );
    }

    private void attach_transaction(Transaction transaction)
    {
        NotifyPropertyChanging("Transactions");
        transaction.Account = this;
    }

    private void detach_transaction(Transaction transaction)
    {
        NotifyPropertyChanging("Transactions");
        transaction.Account = null;
    }

    // {...}
}

Тогда у меня есть несколько репозиториев, которые реализуют метод GetAll (), который возвращает ObservableCollection. Репозитории имеют ссылку на класс Context, который создается внутри класса ViewModel, и вот так:

public class AccountRepository
{
    private MyContext _context;

    public AccountRepository(ref MyContext context)
    {
        _context = context;
    }

    // {...}

    public ObservableCollection<Account> GetAll()
    {
        return new ObservableCollection(_context.Accounts.Where([some paramethers]).AsEnumerable());


    }

    // {...}
}

Мой ViewModel инициализирует репозитории в конструкторе, а затем предоставляет методы с несколькими логическими кодами для вставки, удаления и т. Д., Каждого из этих типов.

public class MyViewModel : INotifyPropertyChanged, INotifyPropertyChanging
{
    private MyContext _context;
    private AccountRepository accountRepository;
    private TransactionRepository transactionRepository;
    public ObservableCollection<Account> AllAccounts;
    public ObservableCollection<Transaction> AllTransactions;

    public MyViewModel(string connectionString)
    {
        _context = new MyContext("Data Source=’isostore:/mydatabase.sdf’"); if (!db.DatabaseExists()) db.CreateDatabase();

        accountRepository = new AccountRepository(ref _context);
        transactionRepository = new TransactionRepository(ref _context);

        // [Some other code]

        LoadCollections();           
    }

    // {...}

    public void LoadCollections()
    {
        AllAccounts = accountRepository.GetAll();
        NotifyPropertyChanged("AllAccounts");
        AllTransactions = transactionRepository.GetAll();
        NotifyPropertyChanged("AllTransactions");
    }

    public void InsertTransaction(Transaction transaction)
    {
        AllTransactions.Add(transaction);
        transactionRepository.Add(transaction);

        LoadCollections(); // Tried this to update the Accounts with newer values, but don't work...
    }

    // {...}
}

Когда пользователь создает транзакцию, страница вызывает метод InsertTransaction (транзакция транзакции) в модели представления, которая передает объект транзакции в хранилище. Но коллекция транзакций в объекте Account не обновляется. Затем я попытался вызвать метод LoadCollections (), чтобы вызвать новый запрос в контексте, и попытаться каким-то образом получить новый объект учетной записи, но он все еще без недавно созданной транзакции. Если я остановлю свое приложение и запустлю его снова, Учетные записи будут обновлены и в моей коллекции транзакций будут все транзакции, которые я создал за последний запуск.

Как я могу обновить эту коллекцию транзакций во время выполнения?

Обновление вопроса:

У меня были некоторые отзывы относительно уведомления пользовательского интерфейса об изменении коллекции.

Мне кажется, это проблема ассоциации транзакций и аккаунта. После создания транзакции она не будет отображаться в ее учетной записи. Коллекция транзакций, пока я не распорядюсь своим контекстом и не создам ее снова.

Это может быть ошибка уведомления, но я не думаю, что это так, и я сделал некоторый код, чтобы попытаться доказать это, я сейчас не на своем ПК, но я попытаюсь объяснить свой тест. *

Код, который я сделал, чтобы доказать, что-то вроде этого:

Account c1 = context.Accounts.Where(c => c.AccountID == 1).SingleOrDefault();

Transaction t1 = new Transaction() { Account = c1, {...} };

context.Transactions.InsertOnSubmit(t1);
context.SaveChanges();

c1 = context.Accounts.Where(c => c.AccountID == 1).SingleOrDefault();

// The transaction IS NOT in the c1.Transactions collection right NOW.

context.Dispose();

context = new MyContext({...});

c1 = context.Accounts.Where(c => c.AccountID == 1).SingleOrDefault();

// The transaction IS in the c1.Transactions after the dispose!

Account c2 = context.Where(c => c.AccountID == 2).SingleOrDefault();

t1 = context.Transactions.Where(t => t.TransactionID == x).SingleOrDefault();

t1.Account = c2;

context.SubmitChanges();

c1 = context.Accounts.Where(c => c.AccountID == 1).SingleOrDefault();

// The transaction still in the c1 collection!!!

c2 = context.Accounts.Where(c => c.AccountID == 2).SingleOrDefault();

// It should be in the c2 collection, but isn't yet...

context.Dispose();

context = new MyContext({...});

c1 = context.Accounts.Where(c => c.AccountID == 1).SingleOrDefault();

// The transaction is not in the c1.Transaction anymore!

c2 = context.Accounts.Where(c => c.AccountID == 2).SingleOrDefault();

// The transaction IS in the c2.Transactions now!

Ответы [ 7 ]

2 голосов
/ 07 октября 2011

Было бы замечательно, если бы был опубликован более полный пример кода, демонстрирующий конкретную проблему.

Что касается Parent-> Child one: многие отношения работают так, как вы ожидаете, это функция, не реализованная непосредственно L2S. Скорее это реализовано в DataContext. Это делается в установщике свойств Child.Parent путем добавления дочернего элемента к его родительскому экземпляру EntitySet.

Этот механизм работает до тех пор, пока вы генерируете родительские <-> дочерние отношения во время выполнения с помощью установщика, который поддерживает эту привязку.

В качестве примера кода здесь приведено свойство, сгенерированное конструктором O / R в Visual Studio для назначения родительского типа сущности для потомка отношения один: много:


[global::System.Data.Linq.Mapping.AssociationAttribute(Name="Account_Transaction", Storage="_Account", ThisKey="AccountId", OtherKey="AccountId", IsForeignKey=true)]
        public Account Account
        {
            get
            {
                return this._Account.Entity;
            }
            set
            {
                Account previousValue = this._Account.Entity;
                if (((previousValue != value) 
                            || (this._Account.HasLoadedOrAssignedValue == false)))
                {
                    this.SendPropertyChanging();
                    if ((previousValue != null))
                    {
                        this._Account.Entity = null;
                        previousValue.Transactions.Remove(this);
                    }
                    this._Account.Entity = value;
                    if ((value != null))
                    {
                        value.Transactions.Add(this);
                        this._AccountId = value.AccountId;
                    }
                    else
                    {
                        this._AccountId = default(long);
                    }
                    this.SendPropertyChanged("Account");
                }
            }
        }

Обратите внимание, что элементы Transaction.Add и Transaction.Remove ... Это управление EntitySet <> для родительского элемента - это не происходит автоматически в слое L2S.

Да, это много клея, чтобы написать. Сожалею. Я бы порекомендовал, что если у вас сложная модель данных с множеством взаимосвязей, смоделируйте ее, используя DBML, и VS предоставит вам контекст данных. Есть несколько целевых изменений, которые необходимо внести, чтобы удалить некоторые конструкторы в этом объекте DataContext, но 99% того, что выпало из конструктора, просто сработает.

-Джон Галлардо Разработчик, Windows Phone.

1 голос
/ 03 октября 2011

2011-10-07 - ОБНОВЛЕНИЕ:

Это действительно беспокоило меня с тех пор, как вы указали, насколько грязным было решение, даже если оно работало, поэтому я углубился в решение и придумалновая :) или, скорее, модифицированная версия вашей оригинальной, которая, я думаю, делает то, что вы ищете.Я не уверен, что поместить здесь в ответе, но я вызову пару областей, а затем обновлю пример кода в своем блоге до последней версии, как только у меня возникнут проблемы с FTP.

Класс учетной записи - вставьте свой код обратно для методов onAttach и onDetatch (я сделал лямбда-выражения, но в основном это одно и то же).

public Account()
{
    _transactions = new EntitySet<Transaction>(
        (addedTransaction) =>
        {
            NotifyPropertyChanging("Account");
            addedTransaction.Account = this;
        },
        (removedTransaction) =>
        {
            NotifyPropertyChanging("Account");
            removedTransaction.Account = null;
        });
}

Класс учетной записи - обновлен атрибут Association для EntitySet:

[Association(
    Storage = "_transactions",
    ThisKey = "AccountId",
    OtherKey = "_accountId")]

Класс транзакции - обновлен атрибут Association для EntityRef для добавления отсутствующего ключа и атрибута IsForeignKey:

[Association(
    Storage = "_account",
    ThisKey = "_accountId",
    OtherKey = "AccountId",
    IsForeignKey = true)]

Наконец, вот методы обновления, с которыми я тестирую:

// test method
public void AddAccount()
{
    Account c1 = new Account() { Tag = DateTime.Now };
    accountRepository.Add(c1);
    accountRepository.Save();
    LoadCollections();
}

// test method
public void AddTransaction()
{
    Account c1 = accountRepository.GetLastAccount();
    c1.Transactions.Add(new Transaction() { Tag = DateTime.Now });
    accountRepository.Save();
    LoadCollections();
}

Обратите внимание, что я добавляю Transaction к Account - не устанавливая значение Account для Transaction при сохранении.Я думаю, что это в сочетании с добавлением параметра IsForeignKey - это то, чего не хватало в вашей первоначальной попытке решения.Попробуйте это и посмотрите, будет ли это работать лучше для вас.

2011-10-05 - ОБНОВЛЕНИЕ:

ОК - похоже, я что-то упустил в своем первоначальном ответе.Основываясь на комментарии, я думаю, что проблема связана с причудой, связанной с Linq to SQL.Когда я внес следующие изменения в свой исходный проект, он, похоже, сработал.

public void AddTransaction()
{
    Account c1 = accountRepository.GetLastAccount();
    Transaction t1 = new Transaction() { Account = c1, Tag = DateTime.Now };
    c1.Transactions.Add(t1);
    transactionRepository.Add(t1);
    accountRepository.Save();
    transactionRepository.Save();
    LoadCollections();
}

По сути, при добавлении объекта Transaction мне пришлось добавить новый Transaction в коллекцию Transactionsоригинального Account объекта.Я не думал, что ты должен был сделать это, но, похоже, это сработало.Дайте мне знать, если это не сработало, и я попробую что-нибудь еще.

Оригинальный ответ:

Я считаю, что это странная привязка данных.Я создал пример, который вы можете загрузить из моего блога, но самое большое изменение, которое я внес в предоставленный вами код, заключалось в замене полей ObservableCollection свойствами:

private ObservableCollection<Account> _accounts = new ObservableCollection<Account>();

public ObservableCollection<Account> Accounts
{
    get { return _accounts; }

    set
    {
        if (_accounts == value)
            return;
        _accounts = value;
        NotifyPropertyChanged("Accounts");
    }
}

private ObservableCollection<Transaction> _transactions = new ObservableCollection<Transaction>();

public ObservableCollection<Transaction> Transactions
{
    get { return _transactions; }

    set
    {
        if (_transactions == value)
            return;
        _transactions = value;
        NotifyPropertyChanged("Transactions");
    }
}

Я также удалил код присоединения / отсоединенияпоскольку это действительно не нужно.Вот мой конструктор учетной записи:

public Account()
{
    _transactions = new EntitySet<Transaction>();
}

Я не могу сказать из вашего примера, но убедитесь, что в каждой таблице определен PK:

// Account table
[Column(
    AutoSync = AutoSync.OnInsert,
    DbType = "Int NOT NULL IDENTITY",
    IsPrimaryKey = true,
    IsDbGenerated = true)]
public int AccountID
{
    get { return _AccountID; }
    set
    {
        if (_AccountID == value)
            return;
        NotifyPropertyChanging("AccountID");
        _AccountID = value;
        NotifyPropertyChanged("AccountID");
    }
}

// Transaction table
[Column(
    AutoSync = AutoSync.OnInsert,
    DbType = "Int NOT NULL IDENTITY",
    IsPrimaryKey = true,
    IsDbGenerated = true)]
public int TransactionID
{
    get { return _TransactionID; }
    set
    {
        if (_TransactionID == value)
            return;
        NotifyPropertyChanging("TransactionID");
        _TransactionID = value;
        NotifyPropertyChanged("TransactionID");
    }
}

Вы можете загрузить мою версиюэто приложение от http://chriskoenig.net/upload/WP7EntitySet.zip - просто убедитесь, что вы добавили учетную запись, прежде чем добавлять транзакции:)

0 голосов
/ 05 октября 2011

На самом деле, поведение, которое вы видите, имеет смысл, когда вы смотрите на него технически.

LinqToSql, как и большинство других ORM, использует IdentityMap для отслеживания загруженных объектов и изменений в них. Это одна из причин, по которой модели L2S по умолчанию реализуют INotifyPropertyChanged, поэтому механизм L2S может подписаться на эти события и действовать соответствующим образом.

Надеюсь, это проясняет, что происходит, когда вы меняете свойство объекта. Но что происходит, когда вы хотите добавить объект в базу данных? Существует два варианта, по которым L2S может знать, что вы хотите добавить его: вы указываете это явно (через DataContext.Table.InsertOnSubmit) или присоединяете его к существующему объекту, который L2S отслеживает с помощью IdentityMap.

Я рекомендую вам прочитать Состояния объектов и отслеживание изменений на MSDN, так как это прояснит ваше понимание.

public void LoadCollections()
{
    AllAccounts = accountRepository.GetAll();
    NotifyPropertyChanged("AllAccounts");
    AllTransactions = transactionRepository.GetAll();
    NotifyPropertyChanged("AllTransactions");
}

public void InsertTransaction(Transaction transaction)
{
    AllTransactions.Add(transaction);
    transactionRepository.Add(transaction);

    LoadCollections(); // Tried this to update the Accounts with newer values, but don't work...
}

Хотя неэффективный и, вероятно, не лучший дизайн, это можно заставить работать (это зависит от реализации транзакцииRepository.Add). Когда вы добавляете новый Transaction к AllTransactions, L2S не может понять, что ему нужно вставить этот объект, потому что его нет в графе отслеживаемых объектов. Правильно, тогда вы звоните transactionRepository.Add, что, вероятно, вызывает InsertOnSubmit(transaction) и SubmitChanges() (последняя часть важна). Если вы теперь вызываете LoadCollections (и ваша привязка данных настроена правильно!), Вы должны увидеть AllTransactions с новой транзакцией.

В качестве последнего замечания вам следует ознакомиться с понятием Совокупные корни . В общем, у вас есть одна Реализация репозитория на агрегат (транзакции и счета в порядке).

0 голосов
/ 01 октября 2011

У меня были подобные проблемы, которые произошли, потому что DBML не синхронизирован с вашей базой данных.Вы пытались удалить свой DBML и воссоздать его снова?

0 голосов
/ 29 сентября 2011

Я заметил кое-что еще в коде, который я хочу вызвать.

 public ObservableCollection<Account> AllAccounts;
 public ObservableCollection<Transaction> AllTransactions;

Обе публичные переменные.Я написал небольшое приложение, чтобы просто проверить ваш вопрос, и я почти уверен, что эти значения должны быть свойствами.Если вы связываетесь с ними по крайней мере.Я говорю это из-за следующего.

Я написал небольшое приложение со свойством и общедоступной переменной:

public ObservableCollection<tClass> MyProperty { get; set; }

public ObservableCollection<tClass> MyPublicVariable;

С небольшим пользовательским интерфейсом теста

<ListBox ItemsSource="{Binding MyProperty}" DisplayMemberPath="Name" Grid.Column="0"/>
        <Button Content="Add" Grid.Column="1" Click="Button_Click" Height="25"/>
        <ListBox ItemsSource="{Binding MyPublicVariable}" Grid.Column="2" DisplayMemberPath="Name"/>

И метод, который будет добавлять вещи к каждой из переменных:

public void AddTestInstance()
{
    tClass test = new tClass() { Name = "Test3" };

    MyProperty.Add(test);
    MyPublicVariable.Add(test);
    NotifyPropertyChanged("MyPublicVariable");
}

Я обнаружил, что MyProperty обновил пользовательский интерфейс просто отлично, но даже когда я вызываю NotifyPropertyChanged на MyPublicVariable, пользовательский интерфейс былне обновляется.

Следовательно, вместо этого попробуйте задать свойства AllAccounts и AllTransactions.

0 голосов
/ 29 сентября 2011

У меня была похожая проблема, и причиной этого было воссоздание коллекций, как вы делаете в LoadCollections (). Однако вы вызываете NotifyPropertyChanged (.. collectionname ..), который должен обеспечить обновление пользовательского интерфейса, но, возможно, просто попробуйте сделать так:

private readonly ObservableCollection<Transaction> _allTransactions = new ObservableCollection<Transaction>();
public ObservableCollection<Transaction> AllTransactions 
{ get { return _allTransactions; } }
// same for AllAccounts

public void LoadCollections()
{
   // if needed AllTransactions.Clear();
   accountRepository.GetAll().ToList().ForEach(AllTransactions.Add);
   // same for other collections
}

Этот подход гарантирует, что ваш пользовательский интерфейс всегда связан с одной и той же коллекцией в памяти, и вам не нужно использовать RaisePropertyChanged ("AllTransaction") в любом месте вашего кода, поскольку это свойство больше не может быть изменено. Я всегда использую этот подход для определения коллекции в View Models.

0 голосов
/ 27 сентября 2011

В LoadCollection () вы обновляете свойства AllAccounts и AllTransactions, но не уведомляете пользовательский интерфейс об их изменении.Вы можете уведомить пользовательский интерфейс, вызвав событие PropertyChanged после установки свойств - в установщике свойств это хорошее место для этого.

...