数据模板化概述
WPF 数据模板化模型为定义数据的表示提供了很大的灵活性。 WPF 控件具有支持自定义数据表示的内置功能。 本主题首先演示如何定义 DataTemplate,然后介绍其他数据模板化功能(例如,根据自定义逻辑选择模板和支持显示分层数据)。
先决条件
本主题重点介绍数据模板化功能,不介绍数据绑定概念。 有关基本数据绑定概念的信息,请参阅数据绑定概述。
DataTemplate 与数据呈现有关,它是 WPF 样式设置和模板化模型提供的众多功能之一。 有关 WPF 样式设置和模板化模型的介绍(例如,如何使用 Style 设置控件的属性),请参阅样式设置和模板化主题。
此外,务必了解 Resources
,使 Style 和 DataTemplate 等对象能够重复使用离不开它。 有关资源的详细信息,请参阅 XAML 资源。
数据模板化基础知识
为了说明 DataTemplate 为什么这么重要,让我们演示一个数据绑定示例。 在本示例中,有一个绑定到 Task
对象列表的 ListBox。 每个 Task
对象都有 TaskName
(string)、Description
(string)、Priority
(int) 和 TaskType
类型的属性(它是一个 Enum
,其值为 Home
和 Work
)。
<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 当前如下所示:
在未提供任何特定说明的情况下,ListBox 在尝试显示集合中的对象时会默认调用 ToString
。 因此,如果 Task
对象替代 ToString
方法,ListBox 会以字符串形式显示基础集合中的每个源对象。
例如,如果 Task
类以这种方式重写 ToString
方法,其中 name
是 TaskName
属性的字段:
public override string ToString()
{
return name.ToString();
}
Public Overrides Function ToString() As String
Return _name.ToString()
End Function
然后,ListBox 如下所示:
但是,这会受到限制且不灵活。 此外,如果要绑定到 XML 数据,将不能替代 ToString
。
定义简单的 DataTemplate
解决方案是定义一个 DataTemplate。 为此,一种方法是将 ListBox 的 ItemTemplate 属性设置为 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 如下所示:
将 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。
如果具有不同类型的数据对象的 CompositeCollection,DataType 属性尤其有用。 有关示例,请参阅实现 CompositeCollection。
向 DataTemplate 添加更多信息
当前,数据显示了必要信息,但还可以显示更多信息。 让我们通过添加 Border、Grid 和一些用于描述要显示的数据的 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:
我们可以在 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
属性(该属性是一个枚举,其值为 Home
和 Work
)。
在以下示例中,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 设置属性值。 触发器类还具有 EnterActions 和 ExitActions 属性,使用这些属性可开始一组操作(例如动画操作)。 此外,还有一个 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>
若要使用模板选择器资源,请将其分配到 ListBox 的 ItemTemplateSelector 属性。 ListBox 为基础集合中的每一项调用 TaskListDataTemplateSelector
的 SelectTemplate 方法。 该调用会将数据对象作为项参数传递。 然后将方法返回的 DataTemplate 应用于该数据对象。
<ListBox Width="400" Margin="10"
ItemsSource="{Binding Source={StaticResource myTodoList}}"
ItemTemplateSelector="{StaticResource myDataTemplateSelector}"
HorizontalContentAlignment="Stretch"/>
使用模板选择器后,ListBox 现在如下所示:
这正是此示例要得到的结果。 有关完整示例,请参阅数据模板化示例简介。
对 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>
下面是该示例在呈现时的屏幕快照:
请注意,可以使用 ItemTemplateSelector,而不是使用 ItemTemplate。 请参考上一节中的示例。 同样,可以选择使用 ItemContainerStyleSelector,而不是使用 ItemContainerStyle。
未在此处显示的 ItemsControl 的其他两个与样式相关的属性是 GroupStyle 和 GroupStyleSelector。
对分层数据的支持
到目前为止,我们仅讨论了如何绑定到并显示单个集合。 某些时候,具有的集合包含其他集合。 HierarchicalDataTemplate 类专用于 HeaderedItemsControl 类型以显示此类数据。 在以下示例中,ListLeagueList
是 League
对象的列表。 每个 League
对象都有一个 Name
和 Division
对象的集合。 每个 Division
都有一个 Name
和 Team
对象的集合,并且每个 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,可以轻松显示包含其他列表的列表数据。 下面是该示例的一个屏幕快照。