3

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!
approxiblue
  • 6,982
  • 16
  • 51
  • 59
Alaor
  • 2,181
  • 5
  • 28
  • 40
  • 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 Sep 29 '11 at 06:55
  • @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 Sep 29 '11 at 12:04
  • @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 Sep 29 '11 at 22:30

7 Answers7

2

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.

John
  • 3,451
  • 2
  • 16
  • 7
1

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 :)

Chris Koenig
  • 2,736
  • 18
  • 17
  • brother, thanks for your patience and time, I really appreciate, but I'm afraid it didn't work. Sorry if it's my fault explaining, but, if you debug your project, you add an account, OK, then you add a transaction, OK, after that, if you look to the Transactions relationship inside the account object, it still don't have the transaction you just created. I took a print that may help to show where is the problem, please, take a look: http://tinyurl.com/62suzjw Thank you for your help. – Alaor Oct 03 '11 at 22:07
  • NOW I understand what you're saying. I didn't look at the nested count. Let me see what I can figure out and update my post. – Chris Koenig Oct 06 '11 at 02:41
  • That's OK, it's my fault, I have a really poor english skill. Your updated answer works, but it'd be really messy in scenarios where I'm updating one transaction, for example, I'd have to store the old account, if the user change the account I should remove it from the old one, then add the transaction in the new one, it'd be a lot more easier if Linq to SQL could manage it for me. But I'm assuming L2S doesn't offer this capability, since I couldn't find any real fix for it over internet in weeks (really bad, msft!) or identify where I'm doing it wrong. But thanks, anyway, I'll accept it! – Alaor Oct 06 '11 at 11:34
0

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.

saus
  • 2,136
  • 17
  • 15
  • Thanks for your comment, saus. It still won't work. I actually was checking the objects in the debugging, so I knew it was not the cause that I was not calling the PropertyChanged, I forgot to say it. But I've update the source in the post with the call of NotifyPropertyChanged inside my view model. – Alaor Sep 27 '11 at 23:19
0

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.

Andris
  • 1,262
  • 1
  • 15
  • 24
  • thanks for your answer, but I don't think it's realted to refreshing the UI, even debuging the code, the transaction I just created won't appear in the account.Transactions until I dispose the context and create it again. But I'll test your answer when in home at night. Thanks. – Alaor Sep 29 '11 at 12:08
0

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.

Johannes
  • 1,095
  • 6
  • 17
  • brother, thanks for your answer, but I don't think it's realted to the UI, even debuging the code the transaction don't appear in the collection account.Transactions until I dispose my context, take a look at my comment to Kirk Broadhurst. I'll test your answer when in home later, anyway. – Alaor Sep 29 '11 at 12:06
0

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?

Pleun
  • 8,856
  • 2
  • 30
  • 50
  • I don't have a DBML file, I've created all my linq classes from scretch, the Windows Phone VS do not allow the use of tool to create theses classes, I guess. May a DBML being generated behind the scenes? – Alaor Oct 01 '11 at 12:08
0

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

Community
  • 1
  • 1
Johannes Rudolph
  • 35,298
  • 14
  • 114
  • 172
  • Actually, this code was based in an example from MSDN, there they added the objects to the collection and to the database, so they didn't had to load all collections again. This is my goal. The only reason why the LoadColletion is there is because I'm trying to make the transactions appear in the account.Transactions collection. It will be changed later if it works. My troubles are because in Entity Framework I get what I want, if I add a transaction and save, it appear in the account.Transactions collection, I had hope it would work the same in L2S but don't appear do be the case. – Alaor Oct 05 '11 at 11:41
  • About the Aggreagate Roots, if I understood it correctly, there are features where I work directly with transactions, and some others that are related to accounts. You could think that transaction is in some way a "child" of an account, but in the code sometimes it's the only object that matter (for example, if I need a graph of expenses by category, I don't want to walk through accounts, I want to query transactions directly), I don't think it's the case here. – Alaor Oct 05 '11 at 11:49
  • Sometimes I'm afraid I'm not saying properly what I want to say, briefly, I want to add a transaction and after save (SubmitChanges) it should appear in the "Transactions" collection of the account object it belongs to, without need to dispose the context and create it again. It's not an UI binding problem. I'm reading the links you provided and I'll give a feedback later. – Alaor Oct 05 '11 at 11:58