数据模板化概述

WPF 数据模板化模型为定义数据的表示提供了很大的灵活性。 WPF 控件具有支持自定义数据表示的内置功能。 本主题首先演示如何定义 DataTemplate,然后介绍其他数据模板化功能(例如,根据自定义逻辑选择模板和支持显示分层数据)。

先决条件

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

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

此外,务必了解 Resources,使 StyleDataTemplate 等对象能够重复使用离不开它。 有关资源的详细信息,请参阅 XAML 资源

数据模板化基础知识

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

<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 当前如下所示:

Screenshot of the Introduction to Data Templating Sample window showing the My Task List ListBox displaying the string representation SDKSample.Task for each source object.

在未提供任何特定说明的情况下,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 如下所示:

Screenshot of the Introduction to Data Templating Sample window showing the My Task List ListBox displaying a list of tasks.

但是,这会受到限制且不灵活。 此外,如果要绑定到 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 数据,基本概念都相同,只不过语法稍微不同。 例如,不使用 Path=TaskName,而是将 XPath 设置为 @TaskName(如果 TaskName 是 XML 节点的一个属性)。

现在,ListBox 如下所示:

Screenshot of the Introduction to Data Templating Sample window showing the My Task List ListBox displaying the tasks as TextBlock elements.

将 DataTemplate 创建为资源

在以上示例中,我们定义了 DataTemplate 内联。 常常在资源部分中定义它,以使其成为一个可重复使用的对象,如以下示例所示:

<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 类具有的 DataType 属性与 Style 类的 TargetType 属性非常相似。 因此,在上述示例中不需要为 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

Screenshot of the Introduction to Data Templating Sample window showing the My Task List ListBox with the modified DataTemplate.

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

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

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

Screenshot of the Introduction to Data Templating Sample window showing the My Task List ListBox stretched to fit the screen horizontally.

使用 DataTriggers 应用属性值

当前表示形式并未指出 Task 是家庭任务还是办公室任务。 请记住,Task 对象具有类型为 TaskTypeTaskType 属性(该属性是一个枚举,其值为 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>

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

Screenshot of the Introduction to Data Templating Sample window showing the My Task List ListBox with the home and office task borders highlighted in color.

在此示例中,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 现在如下所示:

Screenshot of Introduction to Data Templating Sample window showing the My Task List ListBox with the Priority 1 tasks prominently displayed with a red border.

这正是此示例要得到的结果。 有关完整示例,请参阅数据模板化示例简介

对 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 example screenshot

请注意,可以使用 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 sample screenshot

另请参阅