65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (12投票s)

2007年12月21日

CPOL

9分钟阅读

viewsIcon

130489

downloadIcon

4670

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

Screenshot

目录

引言

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 所示。

图 1 - LINQ to SQL 类模型。

在此项目中,我们使用一个包含两个表 AlbumsSongs 的 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,并且 AlbumsSongsDataContext dc 的两个成员。第三行将查询结果链接到 ListBox。我们在这里使用数据绑定,表示 ListBox 中的项目必须用查询结果填充。如果此时运行应用程序,我们应该会得到如图 2 所示的内容

图 2 - 使用 LINQ 和数据绑定的 WPF 窗口。

这并非我们所期望: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>

正如我们之前所说,AlbumsSongs 表之间的关系被映射到对象模型中,因此 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 接口的代码类。此接口有两个方法,ConvertConvertBackConvert 将从数据库值转换为 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>

RadioButtonTag 属性存储用作 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 名称,背景为渐变色。

图 3 - 分组数据。

过滤数据

在 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 - 首次发布
© . All rights reserved.