Skip to Content
Week 08Day 2 - DataTemplate

Day 2 - DataTemplate

建议用时:210-240 分钟

你将学会什么

  • 知道 DataTemplate 是“数据对象的显示模板”。
  • 会用 ListBox.ItemTemplate 自定义列表项显示。
  • 会在模板里绑定当前商品的 NamePriceEnabledText
  • 会使用 DataTemplate x:DataType="vm:ProductItem"
  • 会分清模板内部的绑定对象和外层 ViewModel。

本页只讲 DataTemplate。它负责“每一条数据怎么显示”,不负责按钮样式,也不负责业务保存逻辑。

本页固定顺序

  1. 先学第一部分:弄懂今天最小、最重要的知识,并运行短例子。
  2. 再学第二部分:把刚学的知识组合成一个完整例子。
  3. 然后做第三部分:自己跟着敲,再完成重复训练和每日小测。
  4. 最后做第四部分:先独立完成作业,再用完整答案检查。

学习衔接

上一页学习的是“样式体系”,今天继续学习“DataTemplate”。先使用上一页已经会的写法,再只增加今天这个新知识点;如果前置内容还不能独立敲出,先回上一页复习,不要硬跳。

今天的最低通过线

第一次学习不要求背完整页。完成下面 3 项,就可以继续:

  • 能用自己的话说明“DataTemplate”解决什么问题。
  • 把第一部分的短例子亲手敲完,并确认每个例子都能运行。
  • 不看完整答案完成第三部分至少前 3 个例子,再主动改一个值观察结果。

第一部分:先学原理和最小知识

这一部分从最小知识开始。先读解释,再把紧跟着的短例子敲一遍。DataTemplate 的核心是:数据和显示结构分开。

1. 没有 DataTemplate 会怎样

如果 ListBox 显示的是字符串,没问题:

public ObservableCollection<string> Products { get; } = new() { "Keyboard", "Mouse" };

但如果显示对象:

new ProductItem { Name = "Keyboard", Price = 199 }

界面需要知道这个对象应该怎么显示。是只显示名称,还是名称和价格都显示?这就是 DataTemplate 要解决的问题。

2. DataTemplate 是什么

DataTemplate 是一条数据的显示规则。

<DataTemplate x:DataType="vm:ProductItem"> <StackPanel> <TextBlock Text="{Binding Name}" /> <TextBlock Text="{Binding Price}" /> </StackPanel> </DataTemplate>

它的意思是:每个 ProductItem 都按这个结构显示。

3. 模板内部绑定的是谁

外层窗口的 x:DataType 是:

x:DataType="vm:MainWindowViewModel"

所以外层绑定:

ItemsSource="{Binding Products}"

找的是 MainWindowViewModel.Products

但模板内部写:

<DataTemplate x:DataType="vm:ProductItem">

所以模板内部绑定:

Text="{Binding Name}"

找的是当前 ProductItem.Name,不是 MainWindowViewModel.Name

4. 为什么要写 x:DataType

x:DataType="vm:ProductItem" 明确告诉 XAML:模板内部的数据类型是 ProductItem

好处:

  • 属性名写错更容易发现。
  • 读代码时知道模板绑定的是哪种对象。
  • compiled bindings 下更稳定。

如果不写,简单情况也可能显示,但错误更隐蔽。

5. DataTemplate 和 Style 的区别

概念负责什么
Style控件外观属性,比如颜色、字体、边距
DataTemplate数据对象显示成哪些控件

例子:

  • 按钮统一背景色:用 Style
  • 商品显示名称、价格、状态:用 DataTemplate

6. DataTemplate 不负责什么

DataTemplate 只负责显示一条数据,不负责:

  • 保存商品。
  • 删除商品。
  • 校验价格。
  • 从数据库加载数据。

这些逻辑仍然放在 ViewModel 或 Service。

DataTemplate 常用写法速查

需求写法说明
列表数据源ItemsSource="{Binding Products}"绑定集合
项模板ListBox.ItemTemplate定义每一项怎么显示
模板类型x:DataType="vm:ProductItem"编译期检查绑定
绑定字段Text="{Binding Name}"读取当前项属性
多字段显示StackPanel + 多个 TextBlock一项显示多个字段
空状态IsVisible="{Binding IsEmpty}"没数据时提示

最小模板:

<ListBox ItemsSource="{Binding Products}"> <ListBox.ItemTemplate> <DataTemplate x:DataType="vm:ProductItem"> <TextBlock Text="{Binding Name}" /> </DataTemplate> </ListBox.ItemTemplate> </ListBox>

第二部分:把知识组合成完整例子

今天要做一个商品列表:每条商品不是只显示一行文字,而是显示名称、价格、状态三行。

ViewModels/MainWindowViewModel.cs

using System.Collections.ObjectModel; namespace ProductApp.ViewModels; public partial class MainWindowViewModel : ViewModelBase { public ObservableCollection<ProductItem> Products { get; } = new() { new ProductItem { Name = "Keyboard", Price = 199, Enabled = true }, new ProductItem { Name = "Mouse", Price = 99, Enabled = true }, new ProductItem { Name = "Old Monitor", Price = 399, Enabled = false } }; } public sealed class ProductItem { public string Name { get; set; } = ""; public decimal Price { get; set; } public bool Enabled { get; set; } public string EnabledText => Enabled ? "启用" : "停用"; }

Views/MainWindow.axaml

<Window xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:vm="using:ProductApp.ViewModels" x:Class="ProductApp.Views.MainWindow" x:DataType="vm:MainWindowViewModel" Width="620" Height="420" Title="DataTemplate"> <ListBox Margin="20" ItemsSource="{Binding Products}"> <ListBox.ItemTemplate> <DataTemplate x:DataType="vm:ProductItem"> <Border BorderBrush="#DDDDDD" BorderThickness="1" Padding="12" Margin="0,0,0,8"> <StackPanel Spacing="4"> <TextBlock Text="{Binding Name}" FontSize="18" FontWeight="Bold" /> <TextBlock Text="{Binding Price}" /> <TextBlock Text="{Binding EnabledText}" /> </StackPanel> </Border> </DataTemplate> </ListBox.ItemTemplate> </ListBox> </Window>

先用一句话理解

Products 里有几条商品,ListBox 就生成几次模板;每次模板内部的绑定对象都是当前那一条 ProductItem

第三部分:跟着敲代码

从这里开始动手。每个例子都按真实文件来写。

例子 1:先显示对象列表

ViewModels/MainWindowViewModel.cs

using System.Collections.ObjectModel; namespace ProductApp.ViewModels; public partial class MainWindowViewModel : ViewModelBase { public ObservableCollection<ProductItem> Products { get; } = new() { new ProductItem { Name = "Keyboard", Price = 199 }, new ProductItem { Name = "Mouse", Price = 99 } }; } public sealed class ProductItem { public string Name { get; set; } = ""; public decimal Price { get; set; } }

Views/MainWindow.axaml

<Window xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:vm="using:ProductApp.ViewModels" x:Class="ProductApp.Views.MainWindow" x:DataType="vm:MainWindowViewModel" Width="520" Height="320" Title="DataTemplate 基础"> <ListBox Margin="20" ItemsSource="{Binding Products}"> <ListBox.ItemTemplate> <DataTemplate x:DataType="vm:ProductItem"> <StackPanel Spacing="4"> <TextBlock Text="{Binding Name}" FontWeight="Bold" /> <TextBlock Text="{Binding Price}" /> </StackPanel> </DataTemplate> </ListBox.ItemTemplate> </ListBox> </Window>

例子 2:加状态显示

ViewModels/MainWindowViewModel.cs

using System.Collections.ObjectModel; namespace ProductApp.ViewModels; public partial class MainWindowViewModel : ViewModelBase { public ObservableCollection<ProductItem> Products { get; } = new() { new ProductItem { Name = "Keyboard", Price = 199, Enabled = true }, new ProductItem { Name = "Old Monitor", Price = 399, Enabled = false } }; } public sealed class ProductItem { public string Name { get; set; } = ""; public decimal Price { get; set; } public bool Enabled { get; set; } public string EnabledText => Enabled ? "启用" : "停用"; }

Views/MainWindow.axaml

<Window xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:vm="using:ProductApp.ViewModels" x:Class="ProductApp.Views.MainWindow" x:DataType="vm:MainWindowViewModel" Width="560" Height="360" Title="商品状态模板"> <ListBox Margin="20" ItemsSource="{Binding Products}"> <ListBox.ItemTemplate> <DataTemplate x:DataType="vm:ProductItem"> <Border BorderBrush="#DDDDDD" BorderThickness="1" Padding="12" Margin="0,0,0,8"> <StackPanel Spacing="4"> <TextBlock Text="{Binding Name}" FontWeight="Bold" /> <TextBlock Text="{Binding Price}" /> <TextBlock Text="{Binding EnabledText}" /> </StackPanel> </Border> </DataTemplate> </ListBox.ItemTemplate> </ListBox> </Window>

例子 3:加选中详情

ViewModels/MainWindowViewModel.cs

using System.Collections.ObjectModel; using CommunityToolkit.Mvvm.ComponentModel; namespace ProductApp.ViewModels; public partial class MainWindowViewModel : ViewModelBase { public ObservableCollection<ProductItem> Products { get; } = new() { new ProductItem { Name = "Keyboard", Price = 199, Enabled = true }, new ProductItem { Name = "Mouse", Price = 99, Enabled = true }, new ProductItem { Name = "Old Monitor", Price = 399, Enabled = false } }; [ObservableProperty] [NotifyPropertyChangedFor(nameof(SelectedSummary))] private ProductItem? selectedProduct; public string SelectedSummary => SelectedProduct is null ? "未选择商品" : $"{SelectedProduct.Name} / {SelectedProduct.Price}"; public MainWindowViewModel() { SelectedProduct = Products[0]; } } public sealed class ProductItem { public string Name { get; set; } = ""; public decimal Price { get; set; } public bool Enabled { get; set; } public string EnabledText => Enabled ? "启用" : "停用"; }

Views/MainWindow.axaml

<Window xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:vm="using:ProductApp.ViewModels" x:Class="ProductApp.Views.MainWindow" x:DataType="vm:MainWindowViewModel" Width="720" Height="420" Title="DataTemplate + SelectedItem"> <Grid ColumnDefinitions="320,*" ColumnSpacing="16" Margin="20"> <ListBox Grid.Column="0" ItemsSource="{Binding Products}" SelectedItem="{Binding SelectedProduct, Mode=TwoWay}"> <ListBox.ItemTemplate> <DataTemplate x:DataType="vm:ProductItem"> <Border BorderBrush="#DDDDDD" BorderThickness="1" Padding="12" Margin="0,0,0,8"> <StackPanel Spacing="4"> <TextBlock Text="{Binding Name}" FontWeight="Bold" /> <TextBlock Text="{Binding Price}" /> <TextBlock Text="{Binding EnabledText}" /> </StackPanel> </Border> </DataTemplate> </ListBox.ItemTemplate> </ListBox> <StackPanel Grid.Column="1" Spacing="8"> <TextBlock Text="当前选中" FontSize="20" FontWeight="Bold" /> <TextBlock Text="{Binding SelectedSummary}" TextWrapping="Wrap" /> </StackPanel> </Grid> </Window>

常见错误和修法

错误为什么错修法
ListBox 显示类型名没有定义显示模板给列表写 DataTemplate
模板里字段名拼错绑定失败对照列表项类型的属性名
模板放太多控件列表性能和可读性下降只显示关键字段
数据和界面结构混在一起后期难改数据放模型,显示方式放模板
不处理空列表没数据时页面空白加空状态提示

小白重复敲写训练

DataTemplate 每次只增加一个显示字段。

训练 1:只显示名称

<ListBox ItemsSource="{Binding Products}"> <ListBox.ItemTemplate> <DataTemplate> <TextBlock Text="{Binding Name}" /> </DataTemplate> </ListBox.ItemTemplate> </ListBox>

训练 2:名称和价格两列

<DataTemplate> <Grid ColumnDefinitions="*,Auto"> <TextBlock Text="{Binding Name}" /> <TextBlock Grid.Column="1" Text="{Binding Price, StringFormat='{}{0:F2}'}" /> </Grid> </DataTemplate>

改动任务:增加库存列。

训练 3:根据库存显示提示

<StackPanel Orientation="Horizontal" Spacing="8"> <TextBlock Text="{Binding Name}" /> <TextBlock Text="缺货" Foreground="Red" IsVisible="{Binding IsOutOfStock}" /> </StackPanel>

第三遍准备一条有库存和一条缺货数据进行验证。

每日小测

做完本页后,用这 5 题检查是否真的掌握。

1. 判断题

本页的目标不是只把代码运行起来,还要能说清楚“为什么这样写”。

答案:对。能运行只是第一步,能解释原理、常用操作和常见错误,才说明本页内容进入了可复用能力。

2. 填空题

本页主题是:DataTemplate。今天至少要掌握的 3 个点是:

1. 知道 `DataTemplate` 是“数据对象的显示模板”。 2. 会用 `ListBox.ItemTemplate` 自定义列表项显示。 3. 会在模板里绑定当前商品的 `Name`、`Price`、`EnabledText`。

答案:以上 3 点必须能用自己的代码跑通,不能只停留在阅读。

3. 流程题

遇到本页相关功能时,先按什么顺序处理?

答案:先看完整例子,确认最终效果;再读原理和名词;然后跟着第三部分从空项目敲代码;最后对照作业答案检查。

4. 找错误题

如果本页代码运行失败,第一步应该做什么?

答案:先看终端或 IDE 里的第一条错误,找到文件名和行号;不要同时改很多地方。再回到本页的“常见错误和修法”表格,对照错误类型逐项排查。

5. 改需求题

在本页完整例子跑通后,至少改一个小需求。

可选改法:

  • 改一个字段名称。
  • 多加一个校验条件。
  • 多输出一行结果。
  • 把固定数据改成用户输入。
  • 把一次处理改成多条数据处理。

答案标准:修改后能重新运行,并能说明这次修改影响了哪一段逻辑。重点检查:知道 DataTemplate 是“数据对象的显示模板”。。

上位机专项练习

DataTemplate 决定一条设备、一条报警或一条记录怎样显示,数据集合不需要知道界面细节。

下面 3 个例子都要亲手敲。先运行原代码,再完成每个例子后面的改动任务。

专项例子 1:设备卡片模板

<DataTemplate DataType="local:DeviceItem"> <Border Padding="12" Margin="0,0,0,8"> <StackPanel> <TextBlock Text="{Binding Name}" FontWeight="Bold" /> <TextBlock Text="{Binding Status}" /> </StackPanel> </Border> </DataTemplate>

运行结果或界面效果:

每个 DeviceItem 显示成两行卡片

改动任务: 增加实时温度。

专项例子 2:报警记录模板

<ListBox ItemsSource="{Binding Alarms}"> <ListBox.ItemTemplate> <DataTemplate> <TextBlock Text="{Binding Message}" Foreground="DarkRed" /> </DataTemplate> </ListBox.ItemTemplate> </ListBox>

运行结果或界面效果:

每条报警使用相同模板

改动任务: 同时绑定 OccurredAt。

专项例子 3:根据类型选择模板

<ContentControl Content="{Binding SelectedItem}"> <ContentControl.DataTemplates> <DataTemplate DataType="local:DeviceItem"><local:DeviceDetailView /></DataTemplate> <DataTemplate DataType="local:AlarmItem"><local:AlarmDetailView /></DataTemplate> </ContentControl.DataTemplates> </ContentControl>

运行结果或界面效果:

选中不同对象显示不同详情视图

改动任务: 增加 ReadingItem 模板。

第四部分:作业完整答案

这一部分给出当天作业的完整答案。先照第三部分敲一遍,再用这里检查结果。

作业要求

做一个商品列表模板:

  1. ViewModel 有 ObservableCollection<ProductItem>
  2. ProductItem 至少包含 NamePriceEnabled
  3. ProductItem 提供 EnabledText
  4. ListBox.ItemsSource 绑定 Products
  5. ListBox.ItemTemplate 使用 DataTemplate x:DataType="vm:ProductItem"
  6. 每一项显示名称、价格、状态。

ViewModels/MainWindowViewModel.cs 完整答案

using System.Collections.ObjectModel; namespace ProductApp.ViewModels; public partial class MainWindowViewModel : ViewModelBase { public ObservableCollection<ProductItem> Products { get; } = new() { new ProductItem { Name = "Keyboard", Price = 199, Enabled = true }, new ProductItem { Name = "Mouse", Price = 99, Enabled = true }, new ProductItem { Name = "Old Monitor", Price = 399, Enabled = false } }; } public sealed class ProductItem { public string Name { get; set; } = ""; public decimal Price { get; set; } public bool Enabled { get; set; } public string EnabledText => Enabled ? "启用" : "停用"; }

Views/MainWindow.axaml 完整答案

<Window xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:vm="using:ProductApp.ViewModels" x:Class="ProductApp.Views.MainWindow" x:DataType="vm:MainWindowViewModel" Width="620" Height="420" Title="DataTemplate"> <ListBox Margin="20" ItemsSource="{Binding Products}"> <ListBox.ItemTemplate> <DataTemplate x:DataType="vm:ProductItem"> <Border BorderBrush="#DDDDDD" BorderThickness="1" Padding="12" Margin="0,0,0,8"> <StackPanel Spacing="4"> <TextBlock Text="{Binding Name}" FontSize="18" FontWeight="Bold" /> <TextBlock Text="{Binding Price}" /> <TextBlock Text="{Binding EnabledText}" /> </StackPanel> </Border> </DataTemplate> </ListBox.ItemTemplate> </ListBox> </Window>

必须验证的结果

项目应该看到
Keyboard显示名称、价格、启用
Mouse显示名称、价格、启用
Old Monitor显示名称、价格、停用

如果模板不显示,按这个顺序检查

  1. 检查 ItemsSource="{Binding Products}"
  2. 检查 DataTemplate x:DataType="vm:ProductItem"
  3. 检查 ProductItem 里是否真的有 NamePriceEnabledText
  4. 检查 XAML 顶部是否有 xmlns:vm="using:ProductApp.ViewModels"
  5. 检查绑定属性名大小写是否一致。

今天真正要掌握的闭环

Products 提供多条 ProductItem -> ListBox 绑定 Products -> DataTemplate 定义每条 ProductItem 的显示结构 -> 模板内部 Binding 绑定当前 ProductItem 的属性

DataTemplate 的重点是“当前项”。模板内部不是绑定整个 ViewModel,而是绑定当前那一条数据。