使用 LINQ to SQL 和数据绑定进行 WPF 数据可视化





5.00/5 (12投票s)
本文将展示如何使用数据绑定和样式来显示来自 Microsoft SQL 数据库的数据,该数据使用 Visual Studio 2008 中引入的新对象关系模型 LINQ to SQL,几乎无需编写代码即可实现数据的分组、排序和筛选。

目录
引言
WPF 为 .NET 开发引入了许多新功能:代码和界面的分离、数据绑定和样式是其中一些。这些新功能允许开发达到新水平用户体验的应用程序。开发人员可以专注于业务逻辑,而设计人员可以创建为用户提供最佳用户体验的 UI。
WPF 引入的另一个强大功能是数据绑定,您可以在无需代码的情况下将对象属性链接到用户界面。
本文将展示如何使用数据绑定和样式来显示来自 Microsoft SQL 数据库的数据,该数据使用 Visual Studio 2008 中引入的新对象关系模型 LINQ to SQL,几乎无需编写代码即可实现数据的分组、排序和筛选。
要求
要构建本文项目,您需要使用 Visual Studio 2008 和 .NET Framework 3.5 来使用引入的新功能:LINQ、Lambda 表达式和类型推断。您还需要安装 Microsoft SQL Server 2005,因为我们正在使用 SQL Server 数据库。
LINQ to SQL
Visual Studio 2008 引入了一项名为 LINQ(语言集成查询)的新技术,它允许以标准方式使用数据,使用相同的查询语言,无论数据来自何处。它有几种“风格”可以访问来自不同源的数据
- LINQ to Objects - 访问来自 CLR 集合的数据
- LINQ to SQL - 访问来自 Microsoft SQL 数据库的数据
- LINQ to Entities - 访问来自 ADO.NET 实体提供程序的数据。此数据访问仍处于测试阶段,应在 2008 年发布
- LINQ to Datasets - 访问来自类型化或非类型化数据集的数据
- LINQ to XML - 访问来自 XML 源的数据
使用 LINQ,您可以使用相同的语法查询任何类型的数据,例如
var query = from d in ObjectCollection
select d;
在上述命令中,查询中的 var
关键字是 C# 3.0/Visual Studio 2008 中引入的另一个新功能。这并不意味着 C# 正在成为一种松散类型语言。C# 仍然是一种强类型语言,但 query
变量的类型由编译器推断:在编译时,编译器会找出其类型并为变量类型生成中间语言 (IL)。
查询语言非常强大,您可以对数据进行筛选、排序和聚合(求和、计数、平均值等)。
LINQ to SQL 允许以面向对象的方式访问 Microsoft SQL 数据库:通过它,您可以完全访问数据库、其表、存储过程和关系,并且仍然拥有 Visual Studio 中可用的功能,如调试和 Intellisense。您还可以在 WPF 应用程序中使用数据绑定,我们将使用此功能在 WPF ListBox
中显示数据库表。
要使用 LINQ to SQL,我们必须使用“添加新项”项目上下文菜单项并选择“LINQ to SQL 类”来向项目添加模型。这将添加一个空白的 LINQ to SQL 模型。将服务器资源管理器中的表拖放到模型中将创建一个类模型,如图 1 所示。

在此项目中,我们使用一个包含两个表 Albums
和 Songs
的 Microsoft SQL Server 数据库。Albums
包含披头士专辑的名称,而 Songs
包含披头士歌曲的数据。外键将歌曲链接到其专辑。如图 2 所示,外键被映射为类之间的关系。Album
类具有一个 Songs
属性,即专辑中歌曲的列表。另一方面,Song
类具有一个指向其专辑数据的 Album
属性。
要访问此数据,我们必须创建一个 LINQ 查询。这在窗口构造函数 Window1.xaml.cs 中完成
BeatlesDataContext dc = new BeatlesDataContext();
var query = from s in dc.Songs
select s;
dataListBox.ItemsSource = query.ToList();
在第一行中,我们创建了一个 DataContext
。它负责数据库和我们查询中使用的对象之间的映射。第二行是 LINQ 查询。您会注意到,在键入时,您可以获得数据的 Intellisense,并且 Albums
和 Songs
是 DataContext dc
的两个成员。第三行将查询结果链接到 ListBox
。我们在这里使用数据绑定,表示 ListBox
中的项目必须用查询结果填充。如果此时运行应用程序,我们应该会得到如图 2 所示的内容

这并非我们所期望:WPF 不知道要显示什么,所以它显示了 Song
类的 ToString()
方法的结果,每首歌曲一个。改变这一点并显示专辑名称的一种方法是使用 DisplayMemberPath ListBox
属性来指定我们想要显示的内容
<ListBox x:Name="listBox1" Margin="10" DisplayMemberPath="Name"/>
这样做只会显示列表中歌曲的名称,但我们想要的更多:我们希望为每首歌曲显示两行,一行是歌曲名称和时长,另一行是专辑名称。我们可以通过为列表中的项目使用模板来实现这一点
<Window.Resources>
<!-- Data template for the listbox items -->
<DataTemplate x:Key="SongsTemplate">
<StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=Name}" FontWeight="Bold" />
<TextBlock Margin="5,0,0,0" Text="{Binding Path=Duration}"/>
</StackPanel>
<TextBlock Text="{Binding Path=Album.Name}" />
</StackPanel>
</DataTemplate>
</Window.Resources>
正如我们之前所说,Albums
和 Songs
表之间的关系被映射到对象模型中,因此 Song
类有一个 Album
成员,包含专辑数据。我们使用此功能在列表中显示专辑名称,使用 <TextBlock Text="{Binding Path=Album.Name}" />
。我们必须指定 ListBox
的项目模板是我们创建的那个
<ListBox HorizontalAlignment="Stretch" Margin="5" Name="listBox1"
VerticalAlignment="Stretch" HorizontalContentAlignment="Stretch"
ItemTemplate="{StaticResource SongsTemplate}"/>
应用程序中的样式
到目前为止,我们已经介绍了一些新的 LINQ 概念,但 WPF 还有更多功能。我们可以通过 style
更改 ListBox
项目的外观
<!-- Style for the Listbox items - Show border and content -->
<Style TargetType="ListBoxItem" x:Key="SongsItemContainerStyle">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<Border x:Name="outsideBorder" Background="#FDF356"
Margin="2" CornerRadius="3" Padding="5"
BorderBrush="Black" BorderThickness="1" >
<ContentPresenter Margin="2" RecognizesAccessKey="True"
HorizontalAlignment="Stretch"/>
</Border>
<!-- Trigger when item is selected - change border stroke and background -->
<ControlTemplate.Triggers>
<Trigger Property="IsSelected" Value="True" >
<Setter TargetName="outsideBorder" Property="Background" Value="#FBA23A"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
此 style
在项目周围显示一个边框,并带有一个关联的触发器:当项目被选中时,边框颜色和背景会改变,为用户提供视觉反馈。我们必须使用 ItemContainerStyle
属性将 style
分配给 ListBox
<ListBox x:Name="dataListBox" Grid.Row="1" Margin="5"
ItemTemplate="{StaticResource SongsTemplate}"
ItemContainerStyle="{StaticResource SongsItemContainerStyle}" />
我们还有更多数据要显示,最好的方法是添加一个工具提示,以便当鼠标悬停在某个项目上时显示额外信息。工具提示也是可定制的,我们可以为工具提示创建一个模板,其中包含我们想要显示的数据
<!-- Style for the tooltip - Show border and content -->
<Style TargetType="{x:Type ToolTip}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ToolTip}">
<Border Background="#698390" Opacity="0.95" Margin="2"
CornerRadius="3" Padding="5" BorderBrush="Black"
BorderThickness="1" >
<ContentPresenter Margin="10,5,10,5"
HorizontalAlignment="Center"
VerticalAlignment="Center"
TextBlock.Foreground="Black" TextBlock.FontSize="12"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Setter Property="FontStyle" Value="Italic" />
<Setter Property="Foreground" Value="White" />
<Setter Property="Placement" Value="Top" />
<Setter Property="HorizontalOffset" Value="20" />
</Style>
使用此模板,工具提示将以圆角显示。工具提示的内容在列表项的数据模板中分配
<!-- Template for the tooltip - Show album image and extra data -->
<StackPanel.ToolTip>
<StackPanel Orientation="Horizontal">
<Border CornerRadius="2" BorderBrush="#FFFCF7" Padding="2"
BorderThickness="2">
<Image Width="117" Height="117"
Source="{Binding Path=Album.Cover,
Converter={StaticResource CoverConvert}}" />
</Border>
<StackPanel Margin="5" MaxWidth="600">
<StackPanel Margin="5" Orientation="Horizontal">
<TextBlock FontWeight="Bold"
Text="{Binding Path=Album.Name}" />
<TextBlock Margin="5,0,0,0" Text="(" />
<TextBlock Text="{Binding Path=Album.Year}"
HorizontalAlignment="Right" />
<TextBlock Text=")" />
</StackPanel>
<TextBlock Text="{Binding Path=Recording}"/>
<TextBlock Text="{Binding Path=RecordingPlace}"/>
<TextBlock Text="{Binding Path=Details,
Converter={StaticResource DetailConvert}}"
TextWrapping="Wrap"/>
</StackPanel>
</StackPanel>
</StackPanel.ToolTip>
在此工具提示中,我们显示了大量数据:专辑封面图像、专辑名称和年份、录制数据、地点和歌曲详情,但有一个问题:数据库只存储封面名称,而物理文件还包含文件夹名称和扩展名。为了允许封面名称绑定到图像源,我们必须创建一个转换器,它将获取数据库中的封面名称并返回文件的有效 URI。Converter
是实现 IConvertValue
接口的代码类。此接口有两个方法,Convert
和 ConvertBack
。Convert
将从数据库值转换为 URI,而 ConvertBack
将不需要。类的实现如下
{
#region IValueConverter Members
public object Convert(object value, Type targetType,
object parameter, System.Globalization.CultureInfo culture)
{
return new Uri("..\\AlbumsBeatles\\" + value.ToString().Trim() +
"-A.jpg", UriKind.Relative);
}
public object ConvertBack(object value, Type targetType,
object parameter, System.Globalization.CultureInfo culture)
{
throw new NotSupportedException();
}
#endregion
}
由于我们不需要从 URI 转换为封面名称,因此我们对 ConvertBack
方法抛出异常。我们必须添加一个新的 XAML 命名空间来使用 converter
并在资源部分声明它
"xmlns:src="clr-namespace:Views"
...
<!-- Converter for the cover name to Image URI -->
<src:NametoURIConverter x:Key="CoverConvert" />
Details
字段是一个 string
字段,其中制表符 (#9 - \t) 分隔行。我们必须创建一个 converter
,它将把制表符更改为换行符。这是通过以下 converter
完成的
public class TabToNewLineConverter : IValueConverter
{
#region IValueConverter Members
public object Convert(object value, Type targetType,
object parameter, System.Globalization.CultureInfo culture)
{
return value.ToString().Replace('\t','\n').Trim();
}
public object ConvertBack(object value, Type targetType,
object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
#endregion
}
它使用此 XAML 片段声明
<!-- Converter for the details - converts tabs to new lines -->
<src:TabToNewLineConverter x:Key="DetailConvert" />
数据的排序和分组
对数据进行排序的一种方法是更改 LINQ 查询以添加 OrderBy
子句
BeatlesDataContext dc = new BeatlesDataContext();
var query = from s in dc.Songs
orderby s.Album.Name
select s;
dataListBox.ItemsSource = query.ToList();
或
BeatlesDataContext dc = new BeatlesDataContext();
var query = from s in dc.Songs
select s;
dataListBox.ItemsSource = query.OrderBy(s =>s.Album.Name).ToList();
表达式 s => s.Album.Name
是 C# 3.0 中引入的一项新功能,称为 Lambda 表达式。它可以理解为:给定一个参数 s
(类型为 Song
,由编译器推断),返回该歌曲的专辑名称。
更改后的查询将需要对数据库进行新的查询,并以新顺序显示数据,但这并非 WPF 中执行此操作的最佳方式。WPF 具有 Views
的概念:一旦您拥有一组数据,就可以在其上创建视图并对其进行排序、分组甚至筛选,而无需再次查询数据库。要对视图进行排序,我们将 SortDescription
添加到视图的 SortDescriptions
属性中
ICollectionView view = CollectionViewSource.GetDefaultView(dataListBox.ItemsSource);
view.SortDescriptions.Clear();
view.SortDescriptions.Add(new SortDescription(
(sender as RadioButton).Tag.ToString(), ListSortDirection.Ascending));
这足以对列表进行排序。排序顺序由点击所需顺序的 RadioButton
确定
<StackPanel Orientation="Horizontal" Grid.Row="1"
Background="Transparent">
<TextBlock Text="Sort by: " Margin="10,5" Foreground="Yellow"
FontWeight="Bold" />
<RadioButton x:Name="radioButton1" Tag="Name"
Click="RadioButton_Click" Content="Name" />
<RadioButton x:Name="radioButton2" Tag="Album.Name"
Click="RadioButton_Click" Content="Album" />
<RadioButton x:Name="radioButton3" Tag="Duration"
Click="RadioButton_Click" Content="Duration" />
<RadioButton x:Name="radioButton4" Tag="Album.Name"
Click="RadioButton_Click" Content="Grouped" />
</StackPanel>
RadioButton
的 Tag
属性存储用作 SortDescription
构造函数第一个参数的排序顺序。排序代码在 RadioButton_Click
事件处理程序中执行。
分组与排序几乎相同:您必须创建一个新的 GroupDescription
并将其添加到视图的 GroupDescriptions
属性中
view.GroupDescriptions.Clear();
if (sender == radioButton4)
{
view.GroupDescriptions.Add(
new PropertyGroupDescription("Album.Name"));
view.SortDescriptions.Add(new SortDescription("Name",
ListSortDirection.Ascending));
}
如果我们现在执行代码,分组不会显示。我们还必须向 ListBox
添加一个 GroupStyle
<ListBox.GroupStyle>
<GroupStyle HeaderTemplate="{StaticResource GroupTemplate}" />
</ListBox.GroupStyle>
组的模板是
<!-- Data template for the group -->
<DataTemplate x:Key="GroupTemplate">
<Border Background="{StaticResource Brush_GroupBackground}"
CornerRadius="10" Height="Auto" Padding="10" Margin="5">
<StackPanel Orientation="Horizontal">
<Border CornerRadius="2" BorderBrush="#FFFCF7" Padding="2"
BorderThickness="2">
<Image Width="117" Height="117"
Source="{Binding Path=Items[0].Album.Cover,
Converter={StaticResource CoverConvert}}" />
</Border>
<TextBlock Text="{Binding Name}" Foreground="White"
FontFamily="Tahoma" FontSize="18" FontWeight="Bold"
VerticalAlignment="Center" Margin="5,0,0,0"/>
</StackPanel>
</Border>
</DataTemplate>
我们显示了 Album
封面和 Album
名称,背景为渐变色。

过滤数据
在 WPF 中筛选数据也非常简单:我们只需将 Lambda 表达式传递给视图的 Filter
属性。此表达式必须为应显示的记录返回布尔值 true
。为了筛选数据,我们将使用 TextBox
,并且当用户在框中键入时,会生成一个新筛选器。TextChanged
事件的处理程序如下
private void filterBox_TextChanged(object sender, TextChangedEventArgs e)
{
ICollectionView view =
CollectionViewSource.GetDefaultView(dataListBox.ItemsSource);
view.Filter = m =>
((Song)m).Name.ToLower().Contains(filterBox.Text.ToLower());
}
如果歌曲名称包含 TextBox
中输入的文本,则表达式将返回 true
。这样,当用户在 TextBox
中输入内容时,将只显示包含该文本的歌曲。
结论
WPF 为 .NET 开发引入了强大的新功能:数据绑定和样式只是其中一些。有了这两个功能,您可以为用户带来新的用户体验水平,彻底改变数据的显示方式,并几乎无需代码隐藏即可将演示文稿与数据链接。这可以通过排序、分组和筛选得到极大的增强。
除了这些功能,LINQ 还带来了一个对象关系模型,它与 WPF 数据绑定模型完美契合:通过这两种技术的结合,开发与数据库数据交互的应用程序变得非常容易。
历史
- 2007/12/21 - 添加目录
- 2007/12/21 - 首次发布