Search code examples
c#silverlightlinq-to-sqlwindows-phone-7sql-server-ce

LINQ to SQL relationship doesn't update collections


I'm having one issue with LINQ to SQL for Windows Phone (SQL Server CE).

I'm developing an personal finance app, and then I have the Account class and the Transaction class. Each Transaction class have a reference to the account it belongs to, so the Account class have a collection of transactions, in a one-to-many relationship. Then I have the repositories (AccountRepository and TransactionRepository) that expose methods to insert, delete, findbykey, and return all instances of each of this classes. The ViewModel have references to its repositories. OK, everything works just well, but, when I create a transaction, it doesn't appear in the Account.Transactions collection untill I stop the software and run it again.

Here's some pieces of the code, first, the model classes:

[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;
    }

    // {...}
}

Then I have some repositories that implement a GetAll() method that returns an ObservableCollection. The repositories have a reference to the Context class that is created inside the ViewModel class and, like this:

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());


    }

    // {...}
}

My ViewModel initialize the repositories in the constructor and then expose methods with some few logic code to insert, delete, etc., each of this types.

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...
    }

    // {...}
}

When the user create a Transaction, the page calls the InsertTransaction (Transaction transaction) method in the view model, that pass the transaction object to the repository. But the Transactions collection in the Account object doesn't get updated. Then I tried to call the LoadCollections() method to force a new query in the context, and try to somehow get a fresh account object, but it still without the recently created transaction. If I stop my app and start it again, the Accounts are up to date and have all transactions I've created in the last run within its transactions collection.

How can I update this Transactions collection at runtime?

Updating the question:

I had some feedback regarding to notifying the UI that the collection was changed.

I think it's a problem with the association between transactions and account. Once I create a transaction, it won't appear in it's account.Transactions collection until I dispose my context and create it again.

It may be some notify fault, but I DON'T think it is and I did some code to try to prove it, I'm not in my PC right now but I'll try to explain my test.

The code I did to prove it was something like this:

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!

Solution

  • It would be great if a more complete code sample could be posted demonstrating the specific problem.

    As far as getting Parent->Child one:many relationships working as you expect, this is a feature not implemented directly by L2S. Rather it is implemented in the DataContext. This is done in the Child.Parent property setter by having the child add itself to its parent EntitySet instance.

    This mechanism works as long as you generate parent<->child relationships at run time by using the setter which maintains this binding.

    By way of a code example, here is the property that is generated by the O/R designer in Visual Studio for assigning a parent entity type on the child for a one:many relationship:

    
    [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");
                    }
                }
            }
    

    Note that Transaction.Add and Transaction.Remove pieces... That is the management of the EntitySet<> on the parent item - it doesn't happen automatically within the L2S layer.

    Yes, this is a lot of glue to write. Sorry. My recommendation would be that if you have a complex data model with lots of relationships, to model it using DBML and have VS spit out a data context for you. There are a couple of targetted changes that will need to be made to remove some of the constructors on that DataContext object, but 99% of what is spit out by the designer will just work.

    -John Gallardo Developer, Windows Phone.