Day 2 - DataTemplate
建议用时:210-240 分钟
你将学会什么
- 知道
DataTemplate是“数据对象的显示模板”。 - 会用
ListBox.ItemTemplate自定义列表项显示。 - 会在模板里绑定当前商品的
Name、Price、EnabledText。 - 会使用
DataTemplate x:DataType="vm:ProductItem"。 - 会分清模板内部的绑定对象和外层 ViewModel。
本页只讲 DataTemplate。它负责“每一条数据怎么显示”,不负责按钮样式,也不负责业务保存逻辑。
本页固定顺序
- 先学第一部分:弄懂今天最小、最重要的知识,并运行短例子。
- 再学第二部分:把刚学的知识组合成一个完整例子。
- 然后做第三部分:自己跟着敲,再完成重复训练和每日小测。
- 最后做第四部分:先独立完成作业,再用完整答案检查。
学习衔接
上一页学习的是“样式体系”,今天继续学习“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 模板。
第四部分:作业完整答案
这一部分给出当天作业的完整答案。先照第三部分敲一遍,再用这里检查结果。
作业要求
做一个商品列表模板:
- ViewModel 有
ObservableCollection<ProductItem>。 ProductItem至少包含Name、Price、Enabled。ProductItem提供EnabledText。ListBox.ItemsSource绑定Products。ListBox.ItemTemplate使用DataTemplate x:DataType="vm:ProductItem"。- 每一项显示名称、价格、状态。
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 | 显示名称、价格、停用 |
如果模板不显示,按这个顺序检查
- 检查
ItemsSource="{Binding Products}"。 - 检查
DataTemplate x:DataType="vm:ProductItem"。 - 检查
ProductItem里是否真的有Name、Price、EnabledText。 - 检查 XAML 顶部是否有
xmlns:vm="using:ProductApp.ViewModels"。 - 检查绑定属性名大小写是否一致。
今天真正要掌握的闭环
Products 提供多条 ProductItem
-> ListBox 绑定 Products
-> DataTemplate 定义每条 ProductItem 的显示结构
-> 模板内部 Binding 绑定当前 ProductItem 的属性DataTemplate 的重点是“当前项”。模板内部不是绑定整个 ViewModel,而是绑定当前那一条数据。