跳到主要内容

5.2 数据模板与控件模板

学习目标​

你将为任务对象定义显示方式,并自定义一个保留内容和基本状态的按钮。前提:已理解 资源与样式、绑定及 DataContext;项目命名空间为 WpfBasics。

模板分别回答什么问题​

DataTemplate 回答“一个数据对象如何显示”,用于列表项、内容区等;ControlTemplate 回答“一个控件的视觉树如何组成”,用于按钮、文本框等。前者近似 Web 页面中渲染 DTO 的视图片段,后者更接近替换组件内部结构。Style 主要统一属性,也可以通过设置 Template 使用一个控件模板。

数据模板内的 DataContext 通常就是正在显示的数据项,所以绑定 Title 会读当前行。控件模板需要读取被模板化控件自己的属性,常用 TemplateBinding;它是针对模板父级的高效单向绑定。复杂路径或需要其他绑定能力时,使用 RelativeSource TemplatedParent。把控件模板里的所有绑定都写成普通 {Binding ...},很容易误读外部 ViewModel。

用数据模板呈现任务​

新增完整文件;这是用于显示的不可变数据,没有编辑需求。若随后要直接修改标题或完成状态,需要可通知的行 ViewModel,见 列表、表格与筛选。

TodoCard.cs
namespace WpfBasics;

public sealed record TodoCard(int Id, string Title, bool IsCompleted);

给 MainWindow.xaml 的根元素添加 xmlns:local="clr-namespace:WpfBasics",在 Window.Resources 中加入下列模板片段。使用类型模板时不要同时随意添加显式键。

MainWindow.xaml:Window.Resources 内片段
<DataTemplate DataType="{x:Type local:TodoCard}">
<Border Padding="12" Margin="0,0,0,8" BorderThickness="1"
BorderBrush="#CBD5E1" CornerRadius="6">
<StackPanel>
<TextBlock Text="{Binding Title}" FontWeight="SemiBold" />
<TextBlock Text="{Binding IsCompleted, StringFormat='已完成:{0}'}"
Margin="0,6,0,0" />
</StackPanel>
</Border>
</DataTemplate>

把窗口内容替换为 <ContentControl Content="{Binding}" />,并在已有构造函数的 InitializeComponent() 后设置以下数据。这里是单对象演示,不需要完整页面 ViewModel。

MainWindow.xaml.cs:构造函数内片段
DataContext = new TodoCard(1, "区分两种模板", false);

用控件模板呈现按钮​

将下列显式样式加入同一个 Window.Resources。此模板用 ContentPresenter 保留按钮内容,并把 Padding、背景和内容对齐接回按钮。没有连接这些属性,外部设置相应值可能看不到效果。

MainWindow.xaml:Window.Resources 内的按钮样式
<Style x:Key="RoundedButtonStyle" TargetType="{x:Type Button}">
<Setter Property="Background" Value="#2563EB" />
<Setter Property="Foreground" Value="White" />
<Setter Property="Padding" Value="16,8" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Border x:Name="Chrome" CornerRadius="6"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Padding="{TemplateBinding Padding}">
<ContentPresenter
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
ContentStringFormat="{TemplateBinding ContentStringFormat}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
RecognizesAccessKey="True" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Chrome" Property="Opacity" Value="0.9" />
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="Chrome" Property="Opacity" Value="0.75" />
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="Chrome" Property="Opacity" Value="0.45" />
</Trigger>
<Trigger Property="IsKeyboardFocused" Value="True">
<Setter TargetName="Chrome" Property="BorderBrush" Value="#F59E0B" />
<Setter TargetName="Chrome" Property="BorderThickness" Value="2" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

在窗口布局中用 Style="{StaticResource RoundedButtonStyle}" 应用它,并设置 Content="_保存"。控件仍为 Button,命令与键盘行为来自控件本身;模板负责让状态可见。RecognizesAccessKey 保留访问键呈现,焦点视觉也需要检查。生产界面还应验证高对比度、系统颜色和屏幕阅读器使用情况。

常见错误​

用 DataTemplate 尝试改变 Button 内部边框,会改变内容呈现而非完整按钮结构。替换文本框等控件模板时遗漏必要的命名部件,可能破坏编辑、滚动等行为;不要把上述按钮模板照搬到所有控件。覆盖模板也会移除原模板的视觉状态,需要自己补齐悬停、按下、禁用、选中和焦点等适用状态。

练习与验收​

显示两种标题长度的任务,并给按钮增加图标和文本组成的内容。验收:数据模板显示对应项;按钮能显示复杂内容;Tab 可以聚焦、Space 可以触发点击;禁用与按下状态可区分。改变按钮 Padding 后边缘距离应跟着改变,证明模板仍尊重外部属性。

官方资料​