数据模板化概述

WPF 数据模板化模型提供极大的灵活性来定义数据的呈现。 WPF 控件具有内置功能来支持数据呈现的自定义。 本主题首先演示如何定义 DataTemplate,然后引入其他数据模板化功能,例如基于自定义逻辑选择模板,并支持显示分层数据。

先决条件

本主题重点介绍数据模板功能,而不是引入数据绑定概念。 有关基本数据绑定概念的信息,请参阅 数据绑定概述

DataTemplate 是关于数据的呈现,是 WPF 样式和模板化模型提供的许多功能之一。 有关 WPF 样式设置和模板化模型的介绍(例如如何使用 Style 设置控件的属性),请参阅 样式设置和模板化 主题。

此外,了解 Resources是非常重要的,因为它们本质上是使诸如 StyleDataTemplate 这样的对象能够被重复使用的关键因素。 有关资源的详细信息,请参阅 XAML 资源

数据模板化基础知识

为了演示为什么 DataTemplate 很重要,让我们演练一个数据绑定示例。 在本示例中,有一个绑定到 Task 对象列表的 ListBox。 每个 Task 对象都有一个 TaskName(string)、一个 Description(字符串)、一个 Priority(int)和一个类型 TaskType的属性,这是一个具有值 HomeWorkEnum

<Window x:Class="SDKSample.Window1"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:local="clr-namespace:SDKSample"
  Title="Introduction to Data Templating Sample">
  <Window.Resources>
    <local:Tasks x:Key="myTodoList"/>

</Window.Resources>
  <StackPanel>
    <TextBlock Name="blah" FontSize="20" Text="My Task List:"/>
    <ListBox Width="400" Margin="10"
             ItemsSource="{Binding Source={StaticResource myTodoList}}"/>
  </StackPanel>
</Window>

不提供 DataTemplate

在没有 DataTemplate 的情况下,ListBox 当前如下所示:

“数据模板化示例简介”窗口的屏幕截图,其中显示了“我的任务列表”列表框,而此列表框显示了每个源对象的字符串表示形式 SDKSample.Task。

发生的情况是,如果没有任何特定说明,默认情况下,ListBox 会在尝试在集合中显示对象时调用 ToString。 因此,如果 Task 对象重写了 ToString 方法,ListBox 就会显示基础集合中每个源对象的字符串表示形式。

例如,如果 Task 类以这种方式替代 ToString 方法,其中 nameTaskName 属性的字段:

public override string ToString()
{
    return name.ToString();
}
Public Overrides Function ToString() As String
    Return _name.ToString()
End Function

然后,ListBox 如下所示:

“数据模板化示例简介”窗口的屏幕截图,其中显示了“我的任务列表”列表框,而此列表框显示了任务列表。

然而,这样的方式限制了灵活性。 此外,如果要绑定到 XML 数据,将不能替代 ToString

定义简单的 DataTemplate

解决方案是定义一个 DataTemplate。 执行此操作的一种方法是将 ListBoxItemTemplate 属性设置为 DataTemplate。 在 DataTemplate 中指定的内容将成为数据对象的可视结构。 以下 DataTemplate 相当简单。 我们规定:每项显示为 StackPanel 中的三个 TextBlock 元素。 每个 TextBlock 元素都绑定到 Task 类的属性。

<ListBox Width="400" Margin="10"
         ItemsSource="{Binding Source={StaticResource myTodoList}}">
   <ListBox.ItemTemplate>
     <DataTemplate>
       <StackPanel>
         <TextBlock Text="{Binding Path=TaskName}" />
         <TextBlock Text="{Binding Path=Description}"/>
         <TextBlock Text="{Binding Path=Priority}"/>
       </StackPanel>
     </DataTemplate>
   </ListBox.ItemTemplate>
 </ListBox>

本主题中示例的基础数据是 CLR 对象的集合。 如果要绑定到 XML 数据,则基本概念相同,但语法上有轻微的差异。 例如,您可以将 XPath 设置为 @TaskName(如果 TaskName 是您的 XML 节点的属性),而不是使用 Path=TaskName

现在,ListBox 如下所示:

“数据模板化示例简介”窗口的屏幕截图,其中显示了“我的任务列表”列表框,而此列表框将任务显示为 TextBlock 元素。

将 DataTemplate 创建为资源

在以上示例中,我们定义了 DataTemplate 内联。 更常见的是在 resources 节中定义它,以便它可以是可重用的对象,如以下示例所示:

<Window.Resources>
<DataTemplate x:Key="myTaskTemplate">
  <StackPanel>
    <TextBlock Text="{Binding Path=TaskName}" />
    <TextBlock Text="{Binding Path=Description}"/>
    <TextBlock Text="{Binding Path=Priority}"/>
  </StackPanel>
</DataTemplate>
</Window.Resources>

现在,可以使用 myTaskTemplate 作为资源,如以下示例所示:

<ListBox Width="400" Margin="10"
         ItemsSource="{Binding Source={StaticResource myTodoList}}"
         ItemTemplate="{StaticResource myTaskTemplate}"/>

由于 myTaskTemplate 是一种资源,因此现在可以在具有采用 DataTemplate 类型的属性的其他控件上使用它。 如上所示,对于 ItemsControl 对象(如 ListBox),它是 ItemTemplate 属性。 对于 ContentControl 对象,它是 ContentTemplate 属性。

DataType 属性

DataTemplate 类具有与 StyleTargetType 属性非常相似的 DataType 属性。 因此,在上述示例中不需要为 DataTemplate 指定 x:Key,可以执行以下命令:

<DataTemplate DataType="{x:Type local:Task}">
  <StackPanel>
    <TextBlock Text="{Binding Path=TaskName}" />
    <TextBlock Text="{Binding Path=Description}"/>
    <TextBlock Text="{Binding Path=Priority}"/>
  </StackPanel>
</DataTemplate>

DataTemplate 会自动应用于所有 Task 对象。 请注意,在这种情况下,x:Key 是隐式设置的。 因此,如果为此 DataTemplate 分配 x:Key 值,你将替代隐式 x:Key,并且不会自动应用 DataTemplate

如果要将 ContentControl 绑定到 Task 对象的集合,ContentControl 不会自动使用上述 DataTemplate。 这是因为 ContentControl 上的绑定需要更多信息来区分是要绑定到整个集合还是单个对象。 如果 ContentControl 跟踪 ItemsControl 类型的选择,则可以将 ContentControl 绑定的 Path 属性设置为“/”,以指示你对当前项感兴趣。 有关示例,请参阅绑定到集合并基于选择显示信息。 否则,需要通过设置 ContentTemplate 属性来显式指定 DataTemplate

如果具有不同类型的数据对象的 CompositeCollectionDataType 属性尤其有用。 有关示例,请参阅实现 CompositeCollection

向 DataTemplate 添加更多内容

当前,数据显示了必要信息,但还可以显示更多信息。 让我们通过添加 BorderGrid 和一些用于描述要显示的数据的 TextBlock 元素来呈现更多信息。


<DataTemplate x:Key="myTaskTemplate">
  <Border Name="border" BorderBrush="Aqua" BorderThickness="1"
          Padding="5" Margin="5">
    <Grid>
      <Grid.RowDefinitions>
        <RowDefinition/>
        <RowDefinition/>
        <RowDefinition/>
      </Grid.RowDefinitions>
      <Grid.ColumnDefinitions>
        <ColumnDefinition />
        <ColumnDefinition />
      </Grid.ColumnDefinitions>
      <TextBlock Grid.Row="0" Grid.Column="0" Text="Task Name:"/>
      <TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding Path=TaskName}" />
      <TextBlock Grid.Row="1" Grid.Column="0" Text="Description:"/>
      <TextBlock Grid.Row="1" Grid.Column="1" Text="{Binding Path=Description}"/>
      <TextBlock Grid.Row="2" Grid.Column="0" Text="Priority:"/>
      <TextBlock Grid.Row="2" Grid.Column="1" Text="{Binding Path=Priority}"/>
    </Grid>
  </Border>
</DataTemplate>

以下屏幕快照使用此修改后的 DataTemplate 显示 ListBox

“数据模板化示例简介”窗口的屏幕截图,其中显示了包含修改后的 DataTemplate 的“我的任务列表”列表框。

我们可以在 ListBox 上将 HorizontalContentAlignment 设置为 Stretch,以确保项的宽度占据整个空间:

<ListBox Width="400" Margin="10"
     ItemsSource="{Binding Source={StaticResource myTodoList}}"
     ItemTemplate="{StaticResource myTaskTemplate}" 
     HorizontalContentAlignment="Stretch"/>

HorizontalContentAlignment 属性设置为 Stretch后,ListBox 现在如下所示:

“数据模板化示例简介”窗口的屏幕截图,其中显示了拉伸以水平适合屏幕的“我的任务列表”列表框。

使用 DataTriggers 应用属性值

目前的演示文稿不告诉我们 Task 是家庭任务还是办公室任务。 请记住,Task 对象具有 TaskType类型的 TaskType 属性,该属性是一个枚举,其值 HomeWork

在以下示例中,DataTrigger 将名为 border 的元素的 BorderBrush 设置为 Yellow(如果 TaskType 属性为 TaskType.Home)。

<DataTemplate x:Key="myTaskTemplate">
<DataTemplate.Triggers>
  <DataTrigger Binding="{Binding Path=TaskType}">
    <DataTrigger.Value>
      <local:TaskType>Home</local:TaskType>
    </DataTrigger.Value>
    <Setter TargetName="border" Property="BorderBrush" Value="Yellow"/>
  </DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>

应用程序现在如下所示。 家庭任务显示为黄色边框,办公室任务以水绿色边框显示:

数据模板化简介示例窗口的屏幕截图,其中显示了“我的任务列表列表框”,其中主页和办公室任务边框以颜色突出显示。

在此示例中,DataTrigger 使用 Setter 来设置属性值。 触发器类还具有 EnterActionsExitActions 属性,可用于启动一组操作,例如动画。 此外,还有一个 MultiDataTrigger 类,可用于基于多个数据绑定属性值应用更改。

实现相同效果的另一种方法是将 BorderBrush 属性绑定到 TaskType 属性,并使用值转换器基于 TaskType 值返回颜色。 使用转换器创建上述效果在性能方面略有效率。 此外,创建自己的转换器可以提供更大的灵活性,因为你可以提供自己的逻辑。 最终,你选择哪种技术取决于你的方案和偏好。 有关如何编写转换器的信息,请参阅 IValueConverter

DataTemplate 应该包含哪些内容?

在前面的示例中,我们使用 DataTemplate.Triggers 属性将触发器置于 DataTemplate 中。 触发器的 Setter 设置 DataTemplate 中元素(即 Border 元素)的属性值。 但是,如果 Setters 涉及的属性不是当前 DataTemplate中的元素的属性,则使用 ListBoxItem 类的 Style 设置属性可能更合适(如果绑定的控件是 ListBox)。 例如,如果希望 Trigger 在鼠标指向某个项时对项的 Opacity 值进行动画效果,则可以在 ListBoxItem 样式中设置触发器。 有关示例,请参阅样式设置和模板化示例简介

一般情况下,需要注意的是,DataTemplate 将应用于每个生成的 ListBoxItem(有关实际应用方式及其实际应用位置的详细信息,请参阅 ItemTemplate 页)。 DataTemplate 仅与数据对象的呈现和外观有关。 在大多数情况下,所有其他与呈现相关的方面,如项目在被选中时的外观或 ListBox 的布局方式,并不属于 DataTemplate的定义。 有关示例,请参阅对 ItemsControl 进行样式设置和模板化一节。

根据数据对象的属性选择 DataTemplate

DataType 属性 部分中,我们讨论了可以为不同的数据对象定义不同的数据模板。 这在你拥有不同类型的 CompositeCollection 或拥有包含不同类型的项的集合时尤其有用。 在使用 DataTrigger 应用属性值部分中,我们演示了如果拥有相同类型的数据对象集合,可以创建 DataTemplate,然后使用触发器根据每个数据对象的属性值来应用更改。 但是,触发器允许应用属性值或启动动画,但它们不会让你灵活地重新构造数据对象的结构。 某些方案可能需要为相同类型但具有不同属性的数据对象创建不同的 DataTemplate

例如,当 Task 对象的 Priority 值为 1 时,可能需要为它指定完全不同的外观,以给予你自己一个提醒。 在这种情况下,请创建一个 DataTemplate 用于显示高优先级 Task 对象。 将以下 DataTemplate 添加到资源部分:

<DataTemplate x:Key="importantTaskTemplate">
  <DataTemplate.Resources>
    <Style TargetType="TextBlock">
      <Setter Property="FontSize" Value="20"/>
    </Style>
  </DataTemplate.Resources>
  <Border Name="border" BorderBrush="Red" BorderThickness="1"
          Padding="5" Margin="5">
    <DockPanel HorizontalAlignment="Center">
      <TextBlock Text="{Binding Path=Description}" />
      <TextBlock>!</TextBlock>
    </DockPanel>
  </Border>
</DataTemplate>

此示例使用 DataTemplate.Resources 属性。 在该节中定义的资源由 DataTemplate中的元素共享。

若要提供逻辑以根据数据对象的 Priority 值选择要使用的 DataTemplate,需要创建 DataTemplateSelector 的子类并替代 SelectTemplate 方法。 在下面的示例中,SelectTemplate 方法提供了基于 Priority 属性值返回相应模板的逻辑。 要返回的模板位于包围 Window 元素的资源中。

using System.Windows;
using System.Windows.Controls;

namespace SDKSample
{
    public class TaskListDataTemplateSelector : DataTemplateSelector
    {
        public override DataTemplate
            SelectTemplate(object item, DependencyObject container)
        {
            FrameworkElement element = container as FrameworkElement;

            if (element != null && item != null && item is Task)
            {
                Task taskitem = item as Task;

                if (taskitem.Priority == 1)
                    return
                        element.FindResource("importantTaskTemplate") as DataTemplate;
                else
                    return
                        element.FindResource("myTaskTemplate") as DataTemplate;
            }

            return null;
        }
    }
}

Namespace SDKSample
    Public Class TaskListDataTemplateSelector
        Inherits DataTemplateSelector
        Public Overrides Function SelectTemplate(ByVal item As Object, ByVal container As DependencyObject) As DataTemplate

            Dim element As FrameworkElement
            element = TryCast(container, FrameworkElement)

            If element IsNot Nothing AndAlso item IsNot Nothing AndAlso TypeOf item Is Task Then

                Dim taskitem As Task = TryCast(item, Task)

                If taskitem.Priority = 1 Then
                    Return TryCast(element.FindResource("importantTaskTemplate"), DataTemplate)
                Else
                    Return TryCast(element.FindResource("myTaskTemplate"), DataTemplate)
                End If
            End If

            Return Nothing
        End Function
    End Class
End Namespace

然后,我们可以将 TaskListDataTemplateSelector 声明为资源:

<Window.Resources>
<local:TaskListDataTemplateSelector x:Key="myDataTemplateSelector"/>
</Window.Resources>

若要使用模板选择器资源,请将其分配给 ListBoxItemTemplateSelector 属性。 对基础集合中的每个项,ListBox 调用 TaskListDataTemplateSelectorSelectTemplate 方法。 调用时,将数据对象作为项参数传递。 然后,该方法返回的 DataTemplate 将应用于该数据对象。

<ListBox Width="400" Margin="10"
         ItemsSource="{Binding Source={StaticResource myTodoList}}"
         ItemTemplateSelector="{StaticResource myDataTemplateSelector}"
         HorizontalContentAlignment="Stretch"/>

使用模板选择器后,ListBox 现在如下所示:

“数据模板化示例简介”窗口的屏幕截图,其中显示了“优先级 1”任务以红色边框突出显示的“我的任务列表”列表框。

最后,我们对此示例进行了讨论。 有关完整示例,请参阅 数据模板化示例简介

对 ItemsControl 进行样式设置和模板化

尽管 ItemsControl 并不是唯一可以使用 DataTemplate 的控件类型,但将 ItemsControl 绑定到集合是很常见的方案。 在 DataTemplate 中有哪些内容部分中,我们讨论了 DataTemplate 定义应当仅与数据呈现相关。 为了知道何时不适合使用DataTemplate,了解ItemsControl提供的各种样式和模板属性是非常重要的。 以下示例旨在说明其中每个属性的函数。 此示例中的 ItemsControl 绑定到与上一示例中相同的 Tasks 集合。 出于演示目的,此示例中的样式和模板都以内联方式声明。

<ItemsControl Margin="10"
              ItemsSource="{Binding Source={StaticResource myTodoList}}">
  <!--The ItemsControl has no default visual appearance.
      Use the Template property to specify a ControlTemplate to define
      the appearance of an ItemsControl. The ItemsPresenter uses the specified
      ItemsPanelTemplate (see below) to layout the items. If an
      ItemsPanelTemplate is not specified, the default is used. (For ItemsControl,
      the default is an ItemsPanelTemplate that specifies a StackPanel.-->
  <ItemsControl.Template>
    <ControlTemplate TargetType="ItemsControl">
      <Border BorderBrush="Aqua" BorderThickness="1" CornerRadius="15">
        <ItemsPresenter/>
      </Border>
    </ControlTemplate>
  </ItemsControl.Template>
  <!--Use the ItemsPanel property to specify an ItemsPanelTemplate
      that defines the panel that is used to hold the generated items.
      In other words, use this property if you want to affect
      how the items are laid out.-->
  <ItemsControl.ItemsPanel>
    <ItemsPanelTemplate>
      <WrapPanel />
    </ItemsPanelTemplate>
  </ItemsControl.ItemsPanel>
  <!--Use the ItemTemplate to set a DataTemplate to define
      the visualization of the data objects. This DataTemplate
      specifies that each data object appears with the Proriity
      and TaskName on top of a silver ellipse.-->
  <ItemsControl.ItemTemplate>
    <DataTemplate>
      <DataTemplate.Resources>
        <Style TargetType="TextBlock">
          <Setter Property="FontSize" Value="18"/>
          <Setter Property="HorizontalAlignment" Value="Center"/>
        </Style>
      </DataTemplate.Resources>
      <Grid>
        <Ellipse Fill="Silver"/>
        <StackPanel>
          <TextBlock Margin="3,3,3,0"
                     Text="{Binding Path=Priority}"/>
          <TextBlock Margin="3,0,3,7"
                     Text="{Binding Path=TaskName}"/>
        </StackPanel>
      </Grid>
    </DataTemplate>
  </ItemsControl.ItemTemplate>
  <!--Use the ItemContainerStyle property to specify the appearance
      of the element that contains the data. This ItemContainerStyle
      gives each item container a margin and a width. There is also
      a trigger that sets a tooltip that shows the description of
      the data object when the mouse hovers over the item container.-->
  <ItemsControl.ItemContainerStyle>
    <Style>
      <Setter Property="Control.Width" Value="100"/>
      <Setter Property="Control.Margin" Value="5"/>
      <Style.Triggers>
        <Trigger Property="Control.IsMouseOver" Value="True">
          <Setter Property="Control.ToolTip"
                  Value="{Binding RelativeSource={x:Static RelativeSource.Self},
                          Path=Content.Description}"/>
        </Trigger>
      </Style.Triggers>
    </Style>
  </ItemsControl.ItemContainerStyle>
</ItemsControl>

下面是呈现示例时的屏幕截图:

ItemsControl 示例屏幕截图

请注意,可以使用 ItemTemplateSelector,而不是使用 ItemTemplate。 有关示例,请参阅上一部分。 同样,可以选择使用 ItemContainerStyleSelector,而不是使用 ItemContainerStyle

此处未显示的 ItemsControl 的另外两个样式相关属性 GroupStyleGroupStyleSelector

对分层数据的支持

到目前为止,我们只查看了如何绑定到和显示单个集合。 某些时候,具有的集合包含其他集合。 HierarchicalDataTemplate 类旨在与 HeaderedItemsControl 类型一起使用以显示此类数据。 在以下示例中,ListLeagueListLeague 对象的列表。 每个 League 对象都有一个 NameDivision 对象的集合。 每个 Division 都有一个 NameTeam 对象的集合,每个 Team 对象都有一个 Name

<Window x:Class="SDKSample.Window1"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  Title="HierarchicalDataTemplate Sample"
  xmlns:src="clr-namespace:SDKSample">
  <DockPanel>
    <DockPanel.Resources>
      <src:ListLeagueList x:Key="MyList"/>

      <HierarchicalDataTemplate DataType    = "{x:Type src:League}"
                                ItemsSource = "{Binding Path=Divisions}">
        <TextBlock Text="{Binding Path=Name}"/>
      </HierarchicalDataTemplate>

      <HierarchicalDataTemplate DataType    = "{x:Type src:Division}"
                                ItemsSource = "{Binding Path=Teams}">
        <TextBlock Text="{Binding Path=Name}"/>
      </HierarchicalDataTemplate>

      <DataTemplate DataType="{x:Type src:Team}">
        <TextBlock Text="{Binding Path=Name}"/>
      </DataTemplate>
    </DockPanel.Resources>

    <Menu Name="menu1" DockPanel.Dock="Top" Margin="10,10,10,10">
        <MenuItem Header="My Soccer Leagues"
                  ItemsSource="{Binding Source={StaticResource MyList}}" />
    </Menu>

    <TreeView>
      <TreeViewItem ItemsSource="{Binding Source={StaticResource MyList}}" Header="My Soccer Leagues" />
    </TreeView>

  </DockPanel>
</Window>

该示例演示如何使用 HierarchicalDataTemplate,可以轻松地显示包含其他列表的列表数据。 下面是示例的屏幕截图。

HierarchicalDataTemplate 示例屏幕截图

另请参阅