LINQ 教程:WPF 数据绑定与 LINQ to SQL






4.90/5 (45投票s)
本教程和应用程序介绍如何使用 WPF 数据绑定与 LINQ to SQL 类。这是关于使用 LINQ to SQL 的三部分教程的第三部分。
注意:运行此程序需要SQL Server Express 2008和.NET 3.5。
引言
这是关于使用 LINQ to SQL 的三部分系列的最后一部分
- 第一部分:将表映射到对象
- 第二部分:添加/更新/删除数据
- 第三部分:WPF数据绑定与LINQ to SQL
这些教程描述了如何手动将类映射到表(而不是使用 SqlMetal 等自动化工具),以便您可以支持 M:M 关系和对实体类的绑定。即使您选择自动生成类,理解这些技术的工作原理也将使您能够扩展代码以更好地满足应用程序的需求,并在问题出现时更好地进行故障排除。
这篇最终文章的目的是通过展示如何使您的 LINQ to SQL 类与 WPF 数据绑定配合使用,来完成对 LINQ to SQL 的介绍。
入门
本文基于:LINQ 教程:添加/更新/删除数据,为实体类添加 INotifyPropertyChanged
事件,以便它们能与 WPF 的数据绑定配合使用。请参考该文章的最新版本,了解应用程序是如何设置的。
简单的数据绑定
WPF 数据绑定允许您绑定到任何 CLR 对象,包括您使用 LINQ to SQL 将其映射到表的类。让我们从快速了解附加的《图书目录》应用程序中的用法开始。
主窗口 BookCatalogBrowser.xaml 在名为 Listing
的 ListView
中显示图书目录项列表(参见文件底部)。
<ListView Name="Listing"
ItemsSource="{Binding}"
HorizontalContentAlignment="Stretch"/>
其代码隐藏中的 DisplayList()
方法接受一个(任何类型的)项列表,并将 Listing
的 DataContext
设置为该列表。
private void DisplayList( IEnumerable dataToList ) {
Listing.DataContext = dataToList;
}
BookCatalogBrowser.xaml 为每种类型(Book
、Author
和 Category
)定义了 DataTemplate
,以确定它们的显示方式。例如,这是 Category
模板的一部分:
<!-- How to display Category listings -->
<DataTemplate DataType="{x:Type LINQDemo:Category}">
<Border Name="border" BorderBrush="ForestGreen"
BorderThickness="1" Padding="5" Margin="5">
<StackPanel>
<TextBlock Text="{Binding Path=Name}"
FontWeight="Bold" FontSize="14"/>
<ListView ItemsSource="{Binding Path=Books}"
HorizontalContentAlignment="Stretch" BorderThickness="0" >
<ListView.ItemTemplate>
<DataTemplate>
<TextBlock>
<Hyperlink Click="LoadIndividualBook"
CommandParameter="{Binding}"
ToolTip="Display book details">
<TextBlock Text="{Binding Path=Title}"/></Hyperlink>
...
这就是 Listing
列表中的每个 Category
实例将如何显示。DataTemplate
本身是根据数据类型(DataType="{x:Type LINQDemo:Category}"
)选择的,模板内的所有内容都绑定到单个 Category
实例。
它通过绑定显示该类别的相关数据
- 类别的名称:
{Binding Path=Name}
- 该类别的图书列表(用于
ListView
):{Binding Path=Books}
- 每个图书的标题(用于
ListView
中的单个图书):{Binding Path=Title}
这样就得到一个类别列表,每个类别都显示其名称和图书列表。
更新数据更改时的显示
这工作得很好……直到您尝试更新数据,而 UI 没有反映出来。即使告知数据绑定刷新也无法显示任何更改。一切似乎都坏了。
问题在于,数据绑定要求其绑定的对象在发生更改时提供通知。您可以通过在类中实现 INotifyPropertyChanged
接口来解决此问题。
实现INotifyPropertyChanged
为了让 WPF 数据绑定自动反映数据更新,它绑定的对象需要提供更改通知,以信号通知其值何时已更改。最常见的做法是让类实现 INotifyPropertyChanged
接口来报告其数据的更改。
实现接口
在 LINQ 教程(第一部分) 中,我们创建了 Book
、Author
和 Category
的类,并将它们映射到数据库表。
让我们以 Book
为例,逐步介绍如何将 INotifyPropertyChanged
接口添加到您的类中。
1. 将 INotifyPropertyChanged 接口添加到类声明中
[Table( Name = "Books" )]
public class Book : INotifyPropertyChanged
2. 添加一个公共 PropertyChangedEventHandler,调用者可以使用它来注册更改通知
public event PropertyChangedEventHandler PropertyChanged;
3. 添加 OnPropertyChanged 方法以通知调用者更改
此方法将检查 PropertyChanged
委托是否存在,如果存在,则调用该委托,并将已更改字段的名称传递给它。
private void OnPropertyChanged( string name ) {
if( PropertyChanged != null ) {
PropertyChanged( this, new PropertyChangedEventArgs( name ) );
}
}
对每个公共实体类执行此操作(在我们的示例中:Book
、Author
和 Category
)。
您可以跳过 M:M 连接类,因为它们不是公共接口的一部分,所以您可能不会绑定到它们。
为每个公共 [Column] 属性调用 OnPropetyChanged()
这些是具有 [Column]
属性的公共属性,直接将其映射到数据库列。
Book
中的 [Column]
公共属性是
标题
价格
在设置值后,立即从 set()
中调用 OnPropertyChanged
,并将更改的属性名称传递给它。
始终确保在设置字段后调用 OnPropetyChanged()
。否则,调用者将在您有机会更新字段的值之前收到通知并检查该字段。
如果您像我们一样使用了自动属性,您将不得不将其拆分为后备字段+属性。
private string _title;
[Column] public string Title {
get { return _title; }
set {
_title = value;
OnPropertyChanged( "Title" );
}
}
private decimal _price;
[Column] public decimal Price {
get { return _price; }
set {
_price = value;
OnPropertyChanged( "Price" );
}
}
对所有公共 [Column]
属性执行此操作。在 BookCatalog 中,这将是:
Author
:Name
Category
:Name
如果您有一个公共 Id
并且它是一个标识列,您应该可以跳过它,因为根据定义,它对于给定的实例不会更改。
为每个公共单引用(1:M)[Association] 属性调用 OnPropetyChanged()
这是任何 1:M 关系的单例端。例如,Book
包含单个 Category
。
在将 EntityRef
后备字段设置为新值后,立即调用 OnPropertyChanged()
。
例如,在我们的 Book
类的 Category
属性中,在设置 _category.Entity
后立即调用它。
public Category Category {
...
set {
...
// set category to the new value
_category.Entity = newCategory;
OnPropertyChanged( "Category" );
...
为每个公共集合(M:1 和 M:M)[Association] 属性调用 OnPropertyChanged()
集合关联(例如 Book.Categories
和 Book.Authors
)需要执行两项操作:
- 返回一个实现
INotifyCollectionChanged
的集合。 - 每当集合更改时调用
OnPropertyChanged()
。
WPF 在绑定到集合时使用步骤 #1 来确定已更改的内容。例如,当显示 Category
数据时,我们绑定的 Category.Books
列表。
当 WPF 绑定到您的对象以确定它何时更改时,将使用步骤 #2。例如,如果 Category.Name
更改。
M:M 引用集合
在 LINQ 教程(第二部分) 中,我们设置了 M:M 公共类(例如 Book
和 Author
)以返回 ObservableCollection
,它已经实现了 INotifyCollectionChanged
,所以步骤 #1 对 Book
已经完成(如下所示),对 Author
也一样。
public class Book : INotifyPropertyChanged
{
...
public ICollection Authors {
get {
var authors = new ObservableCollection( from ba in BookAuthors select ba.Author );
authors.CollectionChanged += AuthorCollectionChanged;
return authors;
}
}
创建集合时,您注册以接收其通知(authors.CollectionChanged += AuthorCollectionChanged
)。每当集合更改时,这将调用您的 AuthorCollectionChanged
方法。
更新 AuthorCollectionChanged
以在末尾调用 OnPropertyChanged()
。
private void AuthorCollectionChanged( object sender, NotifyCollectionChangedEventArgs e ) {
if( NotifyCollectionChangedAction.Add == e.Action ) {
foreach( Author addedAuthor in e.NewItems ) {
OnAuthorAdded( addedAuthor );
}
}
if( NotifyCollectionChangedAction.Remove == e.Action ) {
foreach( Author removedAuthor in e.OldItems ) {
OnAuthorRemoved( removedAuthor );
}
}
// Call OnPropertyChanged() after updating Authors
OnPropertyChanged( "Authors" );
}
然后,在 M:M 关系的另一侧(Author.Books
)镜像这些更改。
M:1 引用集合
这样就剩下我们的 M:1 集合——例如 Category.Books
——我们尚未将其包装在 ObservableCollection
中。
现在就做:在 Category
中,更新 Books
的 get()
以返回 ObservableCollection
,就像您为 Book.Authors
所做的那样。
public class Category : INotifyPropertyChanged
{
...
public ICollection Books {
get {
var books = new ObservableCollection<Book>( _books );
books.CollectionChanged += BookCollectionChanged;
return books;
}
并且,更新其 set()
以在赋值后调用 OnPropertyChanged()
。
set {
_books.Assign( value );
OnPropertyChanged( "Books" );
}
}
在 LINQ 教程(第二部分) 中,我们创建了两个委托方法:OnBookAdded
和 OnBookRemoved
,它们在类别图书更改时处理同步。
创建您的 BookCollectionChanged
方法。让它调用 OnBookAdded
和 OnBookRemoved
,并在最后调用 OnPropertyChanged("Books")
。
private void BookCollectionChanged( object sender, NotifyCollectionChangedEventArgs e ) {
if( NotifyCollectionChangedAction.Add == e.Action ) {
foreach( Book addedBook in e.NewItems ) {
OnBookAdded( addedBook );
}
}
if( NotifyCollectionChangedAction.Remove == e.Action ) {
foreach( Book removedBook in e.OldItems ) {
OnBookRemoved( removedBook );
}
}
OnPropertyChanged( "Books" );
}
由于您包装了后备字段(_books
),您需要处理在每次收到更改通知时更新它。
更新您的 OnBookAdded
和 OnBookRemoved
方法以相应地更新底层 _books
集合。
private void OnBookAdded( Book addedBook ) {
_books.Add( addedBook );
addedBook.Category = this;
}
private void OnBookRemoved( Book removedBook ) {
_books.Remove( removedBook );
removedBook.Category = null;
}
最后,由于 BookCollectionChanged
现在调用 OnBookAdded
和 OnBookRemoved
,因此从 _books
调用它将是多余的(并且是递归的!)。
从您的 new EntitySet
构造函数调用中删除 Action 参数。
public Category( ){
_books = new EntitySet<Book>( OnBookAdded, OnBookRemoved );
}
再次更新显示:数据更改时
附加的《图书目录》应用程序包含一个 EditDetails.xaml 用户控件,用于编辑任何 LINQ 数据类型。
我敢肯定,我在这里为了自己的利益而变得过于聪明,试图使用相同的方法和用户控件来处理 Books
、Authors
和 Categories
——因此我不建议您模仿此处的设计。 :-) 但我希望它能通过提供如何数据绑定到 LINQ to SQL 类并确保所有数据在应用程序中同步的示例来发挥其作用。
在 EditDetails.xaml.cs 中,有一个 BindDataToEditForm()
,它将 UserControl 与要编辑的 dataItem
之间的绑定设置为(这可以是 Book
、Author
或 Category
)。
private void BindDataToEditForm( ) {
Binding binding = new Binding( );
binding.Source = dataItem;
binding.Mode = BindingMode.OneWay;
binding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
EditForm.SetBinding( DataContextProperty, binding );
}
与主窗口一样,EditDetails.xaml 使用 DataTemplate
来确定如何显示编辑表单。例如,这是 Category
模板的一部分:
<!-- How to display Category details -->
<DataTemplate DataType="{x:Type LINQDemo:Category}">
...
<Border Name="border" BorderBrush="ForestGreen"
BorderThickness="1" Padding="10"
DockPanel.Dock="Right">
<StackPanel>
<DockPanel>
<TextBlock FontWeight="Bold"
DockPanel.Dock="Left"
VerticalAlignment="Center">Name:</TextBlock>
<TextBox Text="{Binding Path=Name}" Margin="5 0"
VerticalAlignment="Center" DockPanel.Dock="Right"/>
</DockPanel>
...
这与主窗口类似,只是它使用 TextBox
而不是 TextBlock
来显示数据,例如 {Binding Path=Name}
。
不同之处在于,由于 Category
现在实现了 INotifyPropertyChanged
,当您在 UI 中编辑 Name
时,它会自动更新底层的 Category
实例。
“保存”按钮调用 SaveDetails()
,后者调用我们 DataContext
上的 SubmitChanges()
。
private void SaveDetails( object sender, RoutedEventArgs e ) {
BookCatalog.SubmitChanges();
CloseDialog();
}
“取消”按钮调用 CancelUpdate()
,该方法:
- 不在
DataContext
上提交更改,因此这些更改将被丢弃,因为每次打开 EditDetails 时都会创建一个新的DataContext
(BookCatalog
)实例。 - 确实调用了我们在 LINQ 教程(第二部分) 中为 BookCatalog 添加的
CancelChanges()
方法,以取消对我们的 M:M 连接表的所有待定更改。
private void CancelUpdate( object sender, RoutedEventArgs e ){
BookCatalog.CancelChanges( );
CloseDialog( false );
}
对话框关闭时,主窗口 BookCatalogBrowser.xaml 会获得一个新的 DataContext
实例来刷新其列表,以捕获您可能保存的任何更改。
已知问题
我发现如果您选择使用单独的 DataContext
删除 M:M 连接记录,则会出现以下问题:
如果您在同一个“事务”中删除然后重新添加同一个 M:M 关系,您将收到一个 DuplicateKeyException
。
例如,如果您编辑一本书,先删除一个作者,然后在调用 SubmitChanges()
之前重新添加同一个作者。
BookCatalog bookCatalog = new BookCatalog( );
Book xpExplained = bookCatalog.Books.Single(
book => book.Title.Contains("Extreme Programming Explained") );
Author kentBeck = bookCatalog.Authors.Single( author => author.Name == "Kent Beck" );
xpExplained.Authors.Remove( kentBeck );
xpExplained.Authors.Add( kentBeck );
// This will throw a DuplicateKeyException
bookCatalog.SubmitChanges();
一种处理方法是阻止这种情况发生——在调用 SubmitChanges()
永久删除之前,不允许删除的关系被重新添加。然后,您可以毫无错误地重新添加它。
《图书目录》应用程序就是这样做的。如果您打开一本书进行编辑并删除其一位作者——在点击“保存”按钮之前,您将无法选择重新添加该作者。当编辑作者以更改他们拥有的图书时,情况也一样。
请注意,这仅适用于您使用单独的 DataContext
删除的 M:M 连接记录,如 LINQ 教程(第二部分) 中所述。例如,从类别中删除然后重新添加同一本书没有限制,因为这是一个 M:1(而不是 M:M)关系。
关于设计的说明
我特意选择让视图直接访问实体类,以便它能够尽可能清晰地说明应用程序中绑定和 DataContext
的工作原理。
显然,在实际应用程序中,您会在视图和模型之间放置一个层。您可以在那里放置您的业务逻辑。您还可以隐藏处理 DataContext
的细节(例如,何时刷新,何时调用 SubmitChanges()
),这样视图就不必了解 LINQ to SQL 的任何内容。
Model-View-ViewModel 模式是处理此问题的绝佳方法。请参阅 CodeProject 上 Sacha Barber 的 MVVM 教程。
谢谢!
如果您读到这里,哇,您应该为这个贡献获得一些新的 CodeProject 声望。我只是随便说说…… :-) 感谢您的阅读。
历史
- 2009/12/09:初始版本。