2011-09-26 66 views
3

我遇到一个与Windows Phone(SQL Server CE)的LINQ to SQL有关的问题。LINQ to SQL关系不会更新集合

我正在开发一个个人理财应用程序,然后我有Account类和Transaction类。每个交易类都有一个对它所属账户的引用,所以账户类具有一系列交易,处于一对多关系。然后我有存储库(AccountRepository和TransactionRepository),公开方法插入,删除,findbykey,并返回每个类的所有实例。 ViewModel引用其存储库。好吧,一切正常,但是,当我创建一个事务时,它不会出现在Account.Transactions集合中,直到我停止该软件并再次运行它。

下面的代码的一些作品,首先,模型类:

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

    // {...} 
} 

然后,我有一些仓库实现返回一个ObservableCollection一个GETALL()方法。该库必须是视图模型类中创建的上下文类的引用,就像这样:

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


    } 

    // {...} 
} 

我的视图模型初始化构造函数中的资料库,然后暴露了一些很少的逻辑代码的方法来插入,删除,等等,每种类型。

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对象中的Transactions集合不会被更新。然后我试着调用LoadCollections()方法在上下文中强制一个新的查询,并试图以某种方式获得一个新的账户对象,但它仍然没有最近创建的事务。如果我停止我的应用程序并重新启动它,那么帐户是最新的并且具有我在其交易集合中上次运行时创建的所有事务。

如何在运行时更新此事务集合?

更新问题:

我只好向有关通知集合已更改UI的一些反馈。

我认为这是交易和帐户之间的关联问题。一旦我创建了一个事务,它就不会出现在它的account.Transactions集合中,直到我处理我的上下文并再次创建它。

这可能是一些通知错误,但我不认为它是,我做了一些代码来试图证明它,我现在不在我的电脑中,但我会尝试解释我的测试。

我没有证明它的代码是这样的:

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! 
+0

获取交易或与交易和帐户之间的关联存在问题吗?在线'solennsactions = transactionRepository.GetAll();'是集合中的新事务? –

+0

@Kirk Broadhurst对不起,也许我不清楚,英语不是我的第一个语言。问题在于trans和账户之间的关联。如果我添加一个事务,它将不会出现在account.Transactions集合中,直到我处理上下文并再次查询该帐户。如果我查询上下文而不处理它,它将返回该帐户而不是最近在该关联中创建的事务。我想,这不是通知UI的问题,因为如果我调试它,我无法在对象中看到新的事务,我做了一些代码来证明这一点,我将在家中更新问题。 – Alaor

+0

@Kirk Broadhurst,关于你关于transactionRepository.GetAll()的问题,是的,它在集合中。我可以在调试中观察到,当我创建事务(例如)时,它没有事务ID(当然),并且一旦我调用SubmitChanges,内存中的对象就会更新,我可以通过调试来看到它的ID ,我需要的是与关联集合类似的行为,当我调用SubmitChanges时,它不会更新集合,即使我再次查询上下文。只有在处置后,我才能得到最新的。 – Alaor

回答

2

如果可以发布更完整的代码示例来展示具体问题,那将是非常好的。

就父母 - 孩子而言:许多关系正如你期望的那样工作,这是L2S不直接实现的功能。而是在DataContext中实现。这是在Child.Parent属性设置器中完成的,让孩子将自己添加到其父实体集实例中。

只要您在运行时通过使用维护此绑定的setter生成父关系< - >子关系,此机制就会起作用。

由代码举例来说,这里是由Visual Studio中的O/R设计器生成的为一对孩子分配父实体类型的属性:一对多的关系:


[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%的设计者吐出的内容只能起作用。

-John Gallardo 开发人员,Windows Phone。

+0

完美!而已!非常感谢你,你摇滚! ;)这就是为什么我爱微软团队! – Alaor

+0

做得很好。当我为此寻求解决方案时,我应该采取这种方法。 :-) –

0

在LoadCollection()要更新的属性AllAccounts和AllTransactions,但你不通知他们已经改变了UI。您可以在设置属性后通过引发PropertyChanged事件来通知UI - 在属性设置器中是一个很好的选择。

+0

感谢您的评论,saus。它仍然无法工作。我实际上是在调试中检查对象,所以我知道这不是我调用PropertyChanged的原因,我忘了说出来。但是我在视图模型中使用NotifyPropertyChanged调用更新了帖子中的源代码。 – Alaor

0

我有类似的问题,并导致重新创建集合,就像你在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 
} 

这种方法保证,你的UI总是难免同一集合在内存中,您不需要使用任何RaisePropertyChanged(“AllTransaction”)放置在你的代码中,因为这个属性不能再被改变。 我总是使用这种方法在视图模型中定义集合。

+0

感谢您的回答,但我不认为这是刷新UI,即使调试代码,我刚刚创建的事务不会出现在account.Transactions,直到我处置上下文并再次创建它。但我会在晚上在家里测试你的答案。谢谢。 – Alaor

0

我注意到了代码中我想提出的其他内容。

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

都是公共变量。我写了一个小应用程序来测试你的问题,我很确定这些值应该是属性。如果你至少绑定了它们。我这样说是因为以下原因。

我写了一个小的应用程序与属性和公共变量:

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

public ObservableCollection<tClass> MyPublicVariable; 

一个小的测试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"/> 

这将增加东西到每个变量的方法:

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

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

我发现MyProperty更新了UI很好,但即使是wh我在MyPublicVariable上调用NotifyPropertyChanged,UI未更新。

因此,请尝试使AllAccountsAllTransactions属性代替。

+0

兄弟,谢谢你的回答,但我不认为这是真实的用户界面,甚至调试代码的交易不会出现在收集account.Transactions直到我处置我的上下文,看看我对柯克的评论布罗德赫斯特。无论如何,我会在晚些时候在家里测试你的答案。 – Alaor

0

我遇到过类似的问题,因为DBML与您的数据库不同步。你有没有试图删除你的DBML并重新创建它?

+0

我没有一个DBML文件,我已经创建了所有我的linq类从scretch,Windows Phone VS不允许使用工具创建这些类,我猜。可能在后台生成DBML? – Alaor

+0

我不熟悉Windows手机部分,只能用Linq2sql .. – Pleun

1

2011-10-07 - 更新:

这确实一直困扰着我,因为你指出了解决方案如何凌乱了,即使它的工作,所以我进一步挖成一个解决方案,并已拿出新的:)或更确切地说,你原来的一个修改版本,我认为你正在寻找。我不知道该在答案中放什么,但是我会调出几个区域,然后在我的FTP问题得到解决后,用最新版本更新我的博客上的代码示例。

账户类 - 把你的代码放回onAttach和onDetatch方法(我做了lambdas,但它基本上是一样的)。

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

Account类 - 更新了EntitySet的公会属性:

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

交易类 - 更新了的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添加到收藏TransactionsAccount对象。我不认为你必须这样做,但它似乎工作。让我知道如果这不起作用,我会尝试别的。

原始回答:

我相信这是一个数据绑定的怪癖。我建了,你可以从我的博客下载示例,但我给你提供的代码所做的最大的变化是与属性更换的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"); 
    } 
} 

我还删除了附加/ detatch代码,因为它是不是真的需要。这里是我的帐户构造函数现在:

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

兄弟,谢谢你的耐心和时间,我真的很感激,但恐怕它没有用。对不起,如果这是我的错误解释,但是,如果你调试你的项目,你添加一个帐户,确定,然后你添加一个交易,那么,如果你看到帐户对象内的交易关系,它仍然没有有你刚刚创建的交易。我拍了一张可能有助于显示问题出在哪里的照片,请查看:http://tinyurl.com/62suzjw谢谢您的帮助。 – Alaor

+0

现在我明白你在说什么了。我没有看到嵌套计数。让我看看我能弄清楚并更新我的文章。 –

+0

没关系,这是我的错,我的英语技能很差。您的更新答案有效,但在更新一个交易的场景中会非常麻烦,例如,我必须存储旧帐户,如果用户更改了帐户,我应该将其从旧帐户中移除,然后将这个事务添加到新的事务中,如果Linq to SQL可以为我管理它,事情会变得更加容易。但是我假设L2S不提供这种功能,因为在几周内我无法通过互联网找到任何真正的修复(真的很糟糕,MSFT!),或者确定我做错了什么。但是,谢谢,无论如何,我会接受它! – Alaor

0

其实,从技术角度看,您所看到的行为是有道理的。

LinqToSql与大多数其他ORM一样,使用IdentityMap来跟踪加载的实体以及对它们的更改。这是默认的L2S模型实现INotifyPropertyChanged的原因之一,因此L2S引擎可以订阅这些事件并相应地执行操作。

我希望这可以澄清当您更改实体的属性时会发生什么。但是当你想将一个实体添加到数据库时会发生什么?有两个选项可以让L2S知道你想添加它:你明确地告诉它(通过DataContext.Table。InsertOnSubmit),或者将其附加到L2S跟踪其IdentityMap的现有实体。

我建议您阅读MSDN上的Object States and Change Tracking article,因为这会澄清您的理解。

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

虽然效率低下,可能不是最好的设计,但可以使其工作(取决于transactionRepository.Add的实现)。当您将新的Transaction添加到AllTransactions时,L2S不可能知道需要插入该对象,因为它不在被跟踪的对象图中。正确地,您然后拨打transactionRepository.Add,这可能会调用InsertOnSubmit(transaction)SubmitChanges()(最后一部分很重要)。如果您现在调用LoadCollections(并且您的数据绑定设置正确!),您应该看到包含新事务的AllTransactions

作为最后一点,您应该查看Aggregate Roots的概念。一般来说,你有一个仓库实施每个聚合(交易和帐户都可以)。

+0

实际上,这段代码基于MSDN的一个例子,它们将对象添加到集合和数据库中,因此它们不必再次加载所有集合。这是我的目标。 LoadColletion存在的唯一原因是因为我试图使交易出现在account.Transactions集合中。如果它能够工作,它将在以后更改。我的烦恼是因为在实体框架中我得到了我想要的东西,如果我添加一个事务并保存,它会出现在account.Transactions集合中,我希望它在L2S中的工作方式是一样的,但不会出现这种情况。 – Alaor

+0

关于Aggreagate Roots,如果我理解正确,那么有些功能会直接处理交易,还有一些与帐户相关的其他功能。您可以认为交易在某种程度上是账户的“孩子”,但在代码中有时它是唯一重要的对象(例如,如果我需要按类别划分费用图表,我不想穿过账户,我想直接查询交易),我不认为这是事实。 – Alaor

+0

有时候我恐怕没有正确地说我想说的话,简而言之,我想添加一个事务,保存后(SubmitChanges)它应该出现在它所属的账户对象的“Transactions”集合中,而没有需要处理上下文并再次创建它。这不是一个UI绑定问题。我正在阅读您提供的链接,稍后我会提供反馈。 – Alaor