3
votes

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!
7
Is the problem with getting the transactions, or with the association between transactions and accounts? On the line AllTransactions = transactionRepository.GetAll(); is the new transaction in the collection? - Kirk Broadhurst
@Kirk Broadhurst Sorry, maybe I was not clear, english isn't my first lang. The problem is the association between trans and accounts. If I add a transaction, it won't appear in the account.Transactions collection until I dispose the context and query the account again.If I query the context without disposing it, it will return the account WITHOUT the recently created transaction in the association. It's NOT a problem of notifying the UI, I guess, because if I debug it I can't see the new transactions in the objects neither, I did some code to proof that, I'll update the question when in home. - Alaor
@Kirk Broadhurst, about your question regarding the transactionRepository.GetAll(), yes, it is in the collection. I can observe in the debug that when I create the transaction (for example), it don't have a transactionID (of course) and as soon as I call SubmitChanges the object in memory is updated and I can see it's ID through the debug, what I need is a similar behavior with the associations collection, when I call SubmitChanges it's NOT updating the collections, even if I query the context again. Only after a dispose I get it up to date. - Alaor

7 Answers

2
votes

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.

1
votes

2011-10-07 - UPDATE:

This has really been bothering me since you pointed out how messy the solution was, even though it worked, so I dug further into a solution and have come up with a new one :) or rather, a modified version of your original one that I think does what you're looking for. I'm not sure what to put here in the answer, but I'll call out a couple of areas and then update the code sample on my blog with the latest version once I get my FTP issues worked out.

Account class - put your code back in for the onAttach and onDetatch methods (I made lambdas, but it's basically the same thing).

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

Account class - updated the Association attribute for the EntitySet:

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

Transaction class - updated the Association attribute for the EntityRef to add the missing key and the IsForeignKey attribute:

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

Lastly, here are the update methods I'm testing with:

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

Note that I'm adding a Transaction to an Account - not setting the Account value of a Transaction when I save it. I think this, combined with adding the IsForeignKey setting is what was missing from your original solution attempt. Try this out and see if it works any better for you.

2011-10-05 - UPDATE:

OK- it looks like I missed something with my original answer. Based on the comment, I think that the issue has to do with a quirk related to Linq to SQL. When I made the following changes to my original project, it seemed to work.

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

Basically, when adding a Transaction object, I had to add the new Transaction to the Transactions collection of the original Account object. I didn't think you had to do this, but it seemed to work. Let me know if this didn't work and I'll try something else.

Original answer:

I believe that this is a data binding quirk. I built out a sample that you can download from my blog, but the biggest change I made to the code you provided was to replace the ObservableCollection fields with properties:

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

I also removed the attach/detatch code as it's not really needed. Here's my Account constructor now:

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

I couldn't tell from your sample, but make sure that each table has a PK defined:

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

You can download my version of this app from http://chriskoenig.net/upload/WP7EntitySet.zip - just make sure you add an account before you add transactions :)

0
votes

In LoadCollection() you are updating the properties AllAccounts and AllTransactions, but you are not notifying the UI that they have been changed. You can notify the UI by raising the PropertyChanged event after setting the properties - in the property setter is a good place to do this.

0
votes

I had similar problem, and cause of that was recreating collections, like you do in LoadCollections(). However you call NotifyPropertyChanged(..collectionname..) which must ensure UI refreshing, but maybe just try to make like this:

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
}

This approach guarantee, that you UI always bound to same collection in memory and you not need use RaisePropertyChanged("AllTransaction") in any place in you code, since this property cannot be changed anymore. I always use this approach for defining collection in View Models.

0
votes

I noticed something else in the code that I want to raise.

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

Are both public variables. I wrote a small application to just test your question and I am pretty sure that those values should be properties instead. If you bind to them at least. I say this because of the following.

I wrote a small app with a property and a public variable:

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

public ObservableCollection<tClass> MyPublicVariable;

With a small test UI

<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"/>

And a method that will add stuff to each of the variables:

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

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

I found that the MyProperty updated the UI just fine but that even when I call NotifyPropertyChanged on the MyPublicVariable, the UI was not updated.

Hence, try to make AllAccounts and AllTransactions properties instead.

0
votes

I've had similar issues tha happened because the DBML is not in sync with your database. Have you tried to delete your DBML and recreate it again?

0
votes

Actually, the behavior you are seeing makes sense when you look at it technically.

LinqToSql, like most other ORMs, employs an IdentityMap to track loaded entities and changes to them. This is one of the reasons the default L2S models implement INotifyPropertyChanged, so the L2S engine can subscribe to these events and act accordingly.

I hope this clarifies what happens when you change a property of an entity. But what happens when you want to add an entity to the database? There are two options L2S could know you want to add it: You tell it explicitly (via DataContext.Table.InsertOnSubmit) or you attach it to an existing Entity that L2S tracks withs its IdentityMap.

I recommend you read the Object States and Change Tracking article on MSDN, as this will clarify your understanding.

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

Albeit inefficient and probably not the best design, this can be made to work (it depends on the implementation of transactionRepository.Add). When you add a new Transaction to AllTransactions, L2S can't possibly figure out it needs to insert that object because it isn't in the tracked object graph. Correctly, you then call transactionRepository.Add, which probably calls InsertOnSubmit(transaction) and SubmitChanges() (the last part is important). If you now call LoadCollections (and your databinding is setup correctly!) you should see AllTransactions containing the new transaction.

As a final note, you should check out the concept of Aggregate Roots. In general, you have one Repository Implementation per Aggregate (Transactions and Accounts are fine).