WPF模板-DataTemplate和ControlTemplate
在WPF中,ControlTemplate
和DataTemplate
是两种用于定义UI的模板。
- DataTemplate:用于定义数据对象如何显示。比如在
ListBox
、ComboBox
、ItemsControl
等控件中,程序员会直接绑定数据——这里我们可以使用DataTemplate
来定义每一项的数据如何显示。注意,DataTemplate
的目标是数据对象,它定义了数据如何在UI中呈现,比如一个字符串、一个自定义对象,或者一个集合的每一项。 - ControlTemplate:用于定义控件的外观。例如,你可以为一个
Button
控件定义一个ControlTemplate
来改变按钮的外观。注意,ControlTemplate
的目标是控件本身,比如Button
、TextBox
等。它定义了控件的可视化树结构。
数据模板
数据模板提供了一种至关重要的能力:即可以自由灵活地展示数据外观。在不使用数据模板的情况下,WPF默认展示对象时总是固定调用对象的.ToString()
方法,我们当然还可以使用绑定路径、自定义控件等方法。但是问题是,这些方案都不够灵活。
数据模板(DataTemplate) 提供的解决方案是,为控件关连的对象自动应用相应的数据模板。
DataTemplate
通常在绑定控件的ItemTemplate
、ContentTemplate
等属性中使用,用于定义数据项的显示方式。例如,定义一个ListBox
中项的显示方式:
<ListBox ItemsSource="{Binding Items}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Name}" FontWeight="Bold"/>
<TextBlock Text="{Binding Description}" FontStyle="Italic"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
数据模板可以定义在资源中,实现一定程度上的共享。数据模板中还可以包含数据触发器,以动态修改属性:
<Window.Resources>
<DataTemplate DataType="{x:Type local:Employee}">
<Border Padding="5" BorderThickness="1" BorderBrush="Gray" CornerRadius="5">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="80"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock x:Name="txtName" VerticalAlignment="Center" Text="{Binding Name}" FontSize="18" FontWeight="Black"/>
<StackPanel Grid.Column="1">
<TextBlock Text="{Binding Job}"/>
<TextBlock Text="{Binding Department}"/>
<TextBlock Text="{Binding Email}"/>
</StackPanel>
</Grid>
</Border>
<!-- 为数据模板定义数据触发器 -->
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding Gender}" Value="Male">
<Setter TargetName="txtName" Property="Foreground" Value="LightBlue"/>
</DataTrigger>
<DataTrigger Binding="{Binding Gender}" Value="Female">
<Setter TargetName="txtName" Property="Foreground" Value="LightGreen"/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</Window.Resources>
内容模板
和数据模板类似,内容模板是WPF中的一种模板技术。不过和DataTemplate
不同,DataTemplate
的TargetType
是普通数据,而ControlTemplate
的TargetType
是一个个控件:
The TargetType of a ControlTemplate must be or inherit from a Control, a Page, or a PageFunctionBase.
如果指定的TargetType
不是一个控件,就会抛出ArgumentException
,关于这块的说明,可参见微软官方文档。
例1: 通过控件模板为默认的按钮重写外观
默认的Button
是一个矩形,我们可以把ControlTemplate
将其改成椭圆形(这个示例参照的是dino623的文章):
代码如下:
<StackPanel>
<Button Content="原生控件" Margin="20,20,20,20" />
<Button Content="改写外观" Foreground="Salmon" Margin="20,20,20,20">
<Button.Template>
<ControlTemplate TargetType="Button">
<Grid>
<Ellipse Stroke="DarkOrange" StrokeThickness="3" Fill="Black"/>
<ContentPresenter Content="{TemplateBinding Content}"
Margin="10,20" HorizontalAlignment="Center" VerticalAlignment="Center" />
</Grid>
</ControlTemplate>
</Button.Template>
</Button>
</StackPanel>
这里<ContentPresenter/>
用于显示控件的内容(content),默认绑定到Content
属性上。而属性TemplateBinding
用于模板内的依赖属性绑定。
例2:通过控件模板为无头控件编写外观
编写无头控件的代码
我们还是参照dino.c的文章,定义一个简单的无头控件 MyContentControl
,可以根据使用者传入ContentTemplate
展示内容。此控件代码非常简单,只是包含一个自定义的Content
和ContentTemplate
依赖属性:
public class MyContentControl : Control
{
static MyContentControl()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(MyContentControl), new FrameworkPropertyMetadata(typeof(MyContentControl)));
}
#region Content DP
public object Content
{
get { return (object)GetValue(ContentProperty); }
set { SetValue(ContentProperty, value); }
}
// Using a DependencyProperty as the backing store for ContentProperty. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ContentProperty = DependencyProperty.Register(nameof(Content), typeof(object), typeof(MyContentControl), new PropertyMetadata(null));
#endregion
#region ContentTemplate DP
public DataTemplate ContentTemplate
{
get { return (DataTemplate)GetValue(ContentTemplateProperty); }
set { SetValue(ContentTemplateProperty, value); }
}
// Using a DependencyProperty as the backing store for ContentTemplateProperty. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ContentTemplateProperty = DependencyProperty.Register(nameof(ContentTemplate), typeof(DataTemplate), typeof(MyContentControl), new PropertyMetadata(null));
#endregion
}
其中,我们定义的Content
是任意类型;而ContentTemplate
是DataTemplate
类型。
注意,上面的代码中,并未定义如何展示我们自定义的Content
和如何使用ContentTemplate
属性。这部分可以留待使用者实现。而这个控件的编写者,往往会提供默认的样式,并在其中设置控件的默认的Template
属性(也就是ControlTemplate
):
<Style TargetType="{x:Type local:MyContentControl}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:MyContentControl}">
<ContentPresenter
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
注意:
- 这里改写了控件
Template
属性(ControlTemplate) - 这里依旧使用了
<ContentPresenter>
来展现内容,注意ContentPresenter.Content
和ContentPresenter.ContentTemplate
这两个属性的绑定。
使用无头控件
如你所见,我们之前定义Content
属性的时候,使用的是object
类型。这意味着使用的时候,我们可以传入任意对象。比如这里我们可以新建一个ViewModel
,其类型为:
internal class CourseViewModel
{
public string Name { get; set; }
public int Score { get; set; }
}
然后直接将其赋给Content
属性:
<local:MyContentControl>
<!-- 指定Content -->
<local:MyContentControl.Content>
<viewmodels:CourseViewModel>
<viewmodels:CourseViewModel.Name>Math</viewmodels:CourseViewModel.Name>
<viewmodels:CourseViewModel.Score>100</viewmodels:CourseViewModel.Score>
</viewmodels:CourseViewModel>
</local:MyContentControl.Content>
<!-- 指定ContentTemplate -->
<local:MyContentControl.ContentTemplate>
<DataTemplate DataType="{x:Type viewmodels:CourseViewModel}">
<Border Background="Green">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}"
Foreground="White" FontSize="20" Margin="20" HorizontalAlignment="Center" />
<TextBlock Text="{Binding Score}"
Foreground="White" FontSize="20" Margin="20" HorizontalAlignment="Center" />
</StackPanel>
</Border>
</DataTemplate>
</local:MyContentControl.ContentTemplate>
</local:MyContentControl>
注意,这里外部传入了两个属性:
Content
属性:是object类型,代表外部传入的“数据”。在大多数情况下,这是“ViewModel”。ContentTemplate
属性:是DataTemplate类型,代表外部传入的“数据模板”。
最终效果为: