Day 5 - 集合绑定
建议用时:240-270 分钟
你将学会什么
- 知道
ObservableCollection<T>为什么适合绑定列表。 - 会把
ObservableCollection<ProductItem>绑定到ListBox.ItemsSource。 - 会用
DataTemplate控制每一项怎么显示。 - 会用
SelectedItem绑定当前选中商品。 - 会写新增命令和删除命令。
- 会在选中项变化时刷新右侧详情。
本页重点是“集合变化通知界面”。新增和删除时不要重新创建整个窗口,也不要手动刷新控件;直接修改 ObservableCollection。
本页固定顺序
- 先学第一部分:弄懂今天最小、最重要的知识,并运行短例子。
- 再学第二部分:把刚学的知识组合成一个完整例子。
- 然后做第三部分:自己跟着敲,再完成重复训练和每日小测。
- 最后做第四部分:先独立完成作业,再用完整答案检查。
学习衔接
上一页学习的是“表单验证”,今天继续学习“集合绑定”。先使用上一页已经会的写法,再只增加今天这个新知识点;如果前置内容还不能独立敲出,先回上一页复习,不要硬跳。
今天的最低通过线
第一次学习不要求背完整页。完成下面 3 项,就可以继续:
- 能用自己的话说明“集合绑定”解决什么问题。
- 把第一部分的短例子亲手敲完,并确认每个例子都能运行。
- 不看完整答案完成第三部分至少前 3 个例子,再主动改一个值观察结果。
第一部分:先学原理和最小知识
这一部分从最小知识开始。先读解释,再把紧跟着的短例子敲一遍。集合绑定的关键是“列表本身变化时,界面怎么知道”。
1. 为什么普通 List 不够
普通 List<T> 可以保存多条数据:
var products = new List<string>();
products.Add("Keyboard");但它不会主动通知界面:
我新增了一项
我删除了一项
我清空了列表如果界面绑定的是普通 List<T>,数据变了以后界面不一定自动刷新。
2. ObservableCollection 是什么
ObservableCollection<T> 是能通知界面集合变化的集合。
public ObservableCollection<ProductItem> Products { get; } = new();当你调用:
Products.Add(product);
Products.Remove(product);绑定到它的 ListBox 会知道集合发生了变化。
3. ItemsSource 是什么
ItemsSource 是列表控件的数据来源。
<ListBox ItemsSource="{Binding Products}" />意思是:ListBox 要显示 ViewModel 里的 Products 集合。
ListBox 不负责创建商品,它只负责显示 Products 里的商品。
4. DataTemplate 是什么
如果列表里是对象,界面要知道每个对象怎么显示。
本页的商品对象:
public sealed class ProductItem
{
public string Name { get; set; } = "";
public decimal Price { get; set; }
}DataTemplate 告诉 ListBox:每个 ProductItem 显示名称和价格。
<DataTemplate x:DataType="vm:ProductItem">
<StackPanel>
<TextBlock Text="{Binding Name}" />
<TextBlock Text="{Binding Price}" />
</StackPanel>
</DataTemplate>5. SelectedItem 是什么
SelectedItem 是当前选中的那一项。
SelectedItem="{Binding SelectedProduct, Mode=TwoWay}"意思是:
- 用户在界面选中一项,ViewModel 的
SelectedProduct会变化。 - ViewModel 改
SelectedProduct,界面选中项也会变化。
列表详情页面基本都离不开 SelectedItem。
6. 为什么 SelectedProduct 要可空
列表可能被删空。删空后没有当前商品。
所以写:
private ProductItem? selectedProduct;右侧详情也要处理没有选中项的情况:
public string SelectedName => SelectedProduct?.Name ?? "未选择商品";不要假设永远有选中项。
7. 删除命令为什么需要 CanExecute
没有选中商品时,删除按钮不应该能执行。
private bool CanRemoveSelected()
{
return SelectedProduct is not null;
}并把它接到命令:
[RelayCommand(CanExecute = nameof(CanRemoveSelected))]
private void RemoveSelected()当 SelectedProduct 改变时,要通知删除命令重新判断:
[NotifyCanExecuteChangedFor(nameof(RemoveSelectedCommand))]8. ProductCountText 为什么要手动通知
ProductCountText 是根据 Products.Count 算出来的。
新增或删除后,Products.Count 变了,但 ViewModel 的属性通知不会自动知道 ProductCountText 变了。
所以新增和删除后写:
OnPropertyChanged(nameof(ProductCountText));这句话告诉界面:ProductCountText 要重新读取。
9. 集合变化和对象属性变化不是一回事
这点很重要:
| 变化 | ObservableCollection 会通知吗 |
|---|---|
| 新增一项 | 会 |
| 删除一项 | 会 |
| 清空列表 | 会 |
修改某一项的 Name | 不一定 |
本页只做新增删除。后面如果要编辑列表项本身,需要让列表项也具备属性通知能力。
集合绑定常用写法速查
| 需求 | 写法 | 说明 |
|---|---|---|
| 可刷新列表 | ObservableCollection<T> | 增删时界面更新 |
| 列表绑定 | ItemsSource="{Binding Products}" | 显示数据源 |
| 选中项绑定 | SelectedItem="{Binding SelectedProduct}" | 当前选中对象 |
| 新增 | Products.Add(item) | 界面自动出现 |
| 删除 | Products.Remove(item) | 界面自动移除 |
| 清空 | Products.Clear() | 清空界面列表 |
| 空状态 | IsEmpty = Products.Count == 0 | 控制提示显示 |
注意:
ObservableCollection 负责集合增删通知
集合里单个对象属性变化,还需要对象自己支持属性通知第二部分:把知识组合成完整例子
今天要做一个 MVVM 列表页面:
- 左侧
ListBox显示商品集合。 - 每个商品显示名称和价格。
- 选中商品后,右侧显示详情。
- 点击新增,列表自动增加一条。
- 点击删除,删除当前选中商品。
ViewModels/MainWindowViewModel.cs
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace ProductApp.ViewModels;
public partial class MainWindowViewModel : ViewModelBase
{
private int nextProductNumber = 4;
public ObservableCollection<ProductItem> Products { get; } = new()
{
new ProductItem { Name = "Keyboard", Price = 199 },
new ProductItem { Name = "Mouse", Price = 99 },
new ProductItem { Name = "Monitor", Price = 899 }
};
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(SelectedName))]
[NotifyPropertyChangedFor(nameof(SelectedPriceText))]
[NotifyCanExecuteChangedFor(nameof(RemoveSelectedCommand))]
private ProductItem? selectedProduct;
[ObservableProperty]
private string message = "请选择一个商品。";
public string SelectedName => SelectedProduct?.Name ?? "未选择商品";
public string SelectedPriceText => SelectedProduct is null ? "-" : SelectedProduct.Price.ToString();
public string ProductCountText => $"商品数量:{Products.Count}";
public MainWindowViewModel()
{
SelectedProduct = Products[0];
}
[RelayCommand]
private void AddProduct()
{
var product = new ProductItem
{
Name = $"New Product {nextProductNumber}",
Price = nextProductNumber * 100
};
nextProductNumber++;
Products.Add(product);
SelectedProduct = product;
Message = $"已新增:{product.Name}";
OnPropertyChanged(nameof(ProductCountText));
}
private bool CanRemoveSelected()
{
return SelectedProduct is not null;
}
[RelayCommand(CanExecute = nameof(CanRemoveSelected))]
private void RemoveSelected()
{
if (SelectedProduct is null)
{
Message = "请先选择要删除的商品。";
return;
}
ProductItem removed = SelectedProduct;
Products.Remove(removed);
SelectedProduct = Products.Count > 0 ? Products[0] : null;
Message = $"已删除:{removed.Name}";
OnPropertyChanged(nameof(ProductCountText));
}
}
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="760"
Height="460"
Title="集合绑定">
<Grid RowDefinitions="Auto,*"
ColumnDefinitions="260,*"
RowSpacing="12"
ColumnSpacing="16"
Margin="20">
<TextBlock Grid.Row="0"
Grid.Column="0"
Grid.ColumnSpan="2"
Text="{Binding ProductCountText}"
FontSize="24"
FontWeight="Bold" />
<Border Grid.Row="1"
Grid.Column="0"
BorderBrush="#DDDDDD"
BorderThickness="1"
Padding="12">
<ListBox ItemsSource="{Binding Products}"
SelectedItem="{Binding SelectedProduct, Mode=TwoWay}">
<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>
</Border>
<StackPanel Grid.Row="1"
Grid.Column="1"
Spacing="10">
<TextBlock Text="当前商品"
FontSize="20"
FontWeight="Bold" />
<TextBlock Text="{Binding SelectedName}" />
<TextBlock Text="{Binding SelectedPriceText}" />
<StackPanel Orientation="Horizontal"
Spacing="8">
<Button Content="新增商品"
Command="{Binding AddProductCommand}" />
<Button Content="删除选中"
Command="{Binding RemoveSelectedCommand}" />
</StackPanel>
<TextBlock Text="{Binding Message}"
TextWrapping="Wrap" />
</StackPanel>
</Grid>
</Window>先用一句话理解
Products 是列表数据源,ListBox.ItemsSource 显示它;SelectedProduct 是当前选中项;新增和删除直接修改 ObservableCollection,界面会收到集合变化通知。
第三部分:跟着敲代码
从这里开始动手。每个例子都按真实文件来写。
例子 1:只显示字符串集合
ViewModels/MainWindowViewModel.cs
using System.Collections.ObjectModel;
namespace ProductApp.ViewModels;
public partial class MainWindowViewModel : ViewModelBase
{
public ObservableCollection<string> Products { get; } = new()
{
"Keyboard",
"Mouse",
"Monitor"
};
}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="480"
Height="300"
Title="字符串集合">
<ListBox Margin="20"
ItemsSource="{Binding Products}" />
</Window>例子 2:新增字符串
ViewModels/MainWindowViewModel.cs
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.Input;
namespace ProductApp.ViewModels;
public partial class MainWindowViewModel : ViewModelBase
{
private int nextNumber = 4;
public ObservableCollection<string> Products { get; } = new()
{
"Keyboard",
"Mouse",
"Monitor"
};
[RelayCommand]
private void AddProduct()
{
Products.Add($"Product {nextNumber}");
nextNumber++;
}
}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="480"
Height="340"
Title="新增集合项">
<StackPanel Margin="20"
Spacing="10">
<Button Content="新增商品"
Command="{Binding AddProductCommand}" />
<ListBox ItemsSource="{Binding Products}" />
</StackPanel>
</Window>点击新增后,列表会自动多一项。
例子 3:对象集合和 DataTemplate
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 },
new ProductItem { Name = "Monitor", Price = 899 }
};
}
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="360"
Title="对象集合">
<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>例子 4:选中项和右侧详情
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 },
new ProductItem { Name = "Mouse", Price = 99 },
new ProductItem { Name = "Monitor", Price = 899 }
};
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(SelectedName))]
[NotifyPropertyChangedFor(nameof(SelectedPriceText))]
private ProductItem? selectedProduct;
public string SelectedName => SelectedProduct?.Name ?? "未选择商品";
public string SelectedPriceText => SelectedProduct is null ? "-" : SelectedProduct.Price.ToString();
public MainWindowViewModel()
{
SelectedProduct = Products[0];
}
}
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="700"
Height="400"
Title="选中项">
<Grid ColumnDefinitions="260,*"
ColumnSpacing="16"
Margin="20">
<ListBox Grid.Column="0"
ItemsSource="{Binding Products}"
SelectedItem="{Binding SelectedProduct, Mode=TwoWay}">
<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>
<StackPanel Grid.Column="1"
Spacing="8">
<TextBlock Text="当前商品"
FontSize="20"
FontWeight="Bold" />
<TextBlock Text="{Binding SelectedName}" />
<TextBlock Text="{Binding SelectedPriceText}" />
</StackPanel>
</Grid>
</Window>例子 5:新增和删除
这一段就是本页完整版本。
常见错误和修法
| 错误 | 为什么错 | 修法 |
|---|---|---|
集合用 List 绑定 | 新增删除界面不自动刷新 | 用 ObservableCollection<T> |
| 修改集合后不通知空状态 | 空提示不更新 | 集合变化后通知 IsEmpty |
| 选中项没判空 | 删除或编辑会出错 | 操作前检查 SelectedItem |
| 列表项显示类型名 | 没有 DataTemplate 或显示字段 | 用模板显示名称、价格等字段 |
| 直接替换集合实例 | 绑定可能断开或状态丢失 | 优先清空后重新添加,或通知属性变化 |
小白重复敲写训练
集合绑定先练新增,再练删除和选中。
训练 1:绑定字符串集合
public ObservableCollection<string> Products { get; } =
new() { "Keyboard", "Mouse" };<ListBox ItemsSource="{Binding Products}" />训练 2:命令新增一项
[RelayCommand]
private void AddProduct()
{
Products.Add($"Product {Products.Count + 1}");
}<Button Content="新增" Command="{Binding AddProductCommand}" />连续点击三次,观察列表自动更新。
训练 3:删除选中项
[ObservableProperty]
private string? _selectedProduct;
[RelayCommand]
private void DeleteSelected()
{
if (SelectedProduct is not null)
Products.Remove(SelectedProduct);
}第三遍在 XAML 中补上 SelectedItem 绑定和删除按钮。
每日小测
做完本页后,用这 5 题检查是否真的掌握。
1. 判断题
本页的目标不是只把代码运行起来,还要能说清楚“为什么这样写”。
答案:对。能运行只是第一步,能解释原理、常用操作和常见错误,才说明本页内容进入了可复用能力。
2. 填空题
本页主题是:集合绑定。今天至少要掌握的 3 个点是:
1. 知道 `ObservableCollection<T>` 为什么适合绑定列表。
2. 会把 `ObservableCollection<ProductItem>` 绑定到 `ListBox.ItemsSource`。
3. 会用 `DataTemplate` 控制每一项怎么显示。答案:以上 3 点必须能用自己的代码跑通,不能只停留在阅读。
3. 流程题
遇到本页相关功能时,先按什么顺序处理?
答案:先看完整例子,确认最终效果;再读原理和名词;然后跟着第三部分从空项目敲代码;最后对照作业答案检查。
4. 找错误题
如果本页代码运行失败,第一步应该做什么?
答案:先看终端或 IDE 里的第一条错误,找到文件名和行号;不要同时改很多地方。再回到本页的“常见错误和修法”表格,对照错误类型逐项排查。
5. 改需求题
在本页完整例子跑通后,至少改一个小需求。
可选改法:
- 改一个字段名称。
- 多加一个校验条件。
- 多输出一行结果。
- 把固定数据改成用户输入。
- 把一次处理改成多条数据处理。
答案标准:修改后能重新运行,并能说明这次修改影响了哪一段逻辑。重点检查:知道 ObservableCollection<T> 为什么适合绑定列表。。
上位机专项练习
ObservableCollection 适合设备列表和报警列表,增删项目时界面会自动刷新。
下面 3 个例子都要亲手敲。先运行原代码,再完成每个例子后面的改动任务。
专项例子 1:准备设备集合
public ObservableCollection<DeviceItem> Devices { get; } =
[
new("PLC-01", "在线"),
new("PLC-02", "离线")
];运行结果或界面效果:
集合中有两台设备改动任务: 增加温控器-01。
专项例子 2:列表绑定集合
<ListBox ItemsSource="{Binding Devices}" SelectedItem="{Binding SelectedDevice}">
<ListBox.ItemTemplate>
<DataTemplate><TextBlock Text="{Binding Name}" /></DataTemplate>
</ListBox.ItemTemplate>
</ListBox>运行结果或界面效果:
列表显示每台设备名称改动任务: 同时显示 Status。
专项例子 3:命令添加设备
[RelayCommand]
private void AddDevice()
{
int number = Devices.Count + 1;
Devices.Add(new DeviceItem($"PLC-{number:00}", "离线"));
}运行结果或界面效果:
每点击一次就新增一台设备改动任务: 增加删除 SelectedDevice 的命令。
第四部分:作业完整答案
这一部分给出当天作业的完整答案。先照第三部分敲一遍,再用这里检查结果。
作业要求
做一个集合绑定页面:
- 使用
ObservableCollection<ProductItem>保存商品。 - 使用
ListBox.ItemsSource绑定商品集合。 - 使用
DataTemplate显示每个商品的名称和价格。 - 使用
SelectedItem绑定当前选中商品。 - 点击新增后,集合增加一条,界面自动刷新。
- 点击删除后,删除当前选中商品。
- 删除按钮在没有选中项时不可执行。
ViewModels/MainWindowViewModel.cs 完整答案
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace ProductApp.ViewModels;
public partial class MainWindowViewModel : ViewModelBase
{
private int nextProductNumber = 4;
public ObservableCollection<ProductItem> Products { get; } = new()
{
new ProductItem { Name = "Keyboard", Price = 199 },
new ProductItem { Name = "Mouse", Price = 99 },
new ProductItem { Name = "Monitor", Price = 899 }
};
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(SelectedName))]
[NotifyPropertyChangedFor(nameof(SelectedPriceText))]
[NotifyCanExecuteChangedFor(nameof(RemoveSelectedCommand))]
private ProductItem? selectedProduct;
[ObservableProperty]
private string message = "请选择一个商品。";
public string SelectedName => SelectedProduct?.Name ?? "未选择商品";
public string SelectedPriceText => SelectedProduct is null ? "-" : SelectedProduct.Price.ToString();
public string ProductCountText => $"商品数量:{Products.Count}";
public MainWindowViewModel()
{
SelectedProduct = Products[0];
}
[RelayCommand]
private void AddProduct()
{
var product = new ProductItem
{
Name = $"New Product {nextProductNumber}",
Price = nextProductNumber * 100
};
nextProductNumber++;
Products.Add(product);
SelectedProduct = product;
Message = $"已新增:{product.Name}";
OnPropertyChanged(nameof(ProductCountText));
}
private bool CanRemoveSelected()
{
return SelectedProduct is not null;
}
[RelayCommand(CanExecute = nameof(CanRemoveSelected))]
private void RemoveSelected()
{
if (SelectedProduct is null)
{
Message = "请先选择要删除的商品。";
return;
}
ProductItem removed = SelectedProduct;
Products.Remove(removed);
SelectedProduct = Products.Count > 0 ? Products[0] : null;
Message = $"已删除:{removed.Name}";
OnPropertyChanged(nameof(ProductCountText));
}
}
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="760"
Height="460"
Title="集合绑定">
<Grid RowDefinitions="Auto,*"
ColumnDefinitions="260,*"
RowSpacing="12"
ColumnSpacing="16"
Margin="20">
<TextBlock Grid.Row="0"
Grid.Column="0"
Grid.ColumnSpan="2"
Text="{Binding ProductCountText}"
FontSize="24"
FontWeight="Bold" />
<Border Grid.Row="1"
Grid.Column="0"
BorderBrush="#DDDDDD"
BorderThickness="1"
Padding="12">
<ListBox ItemsSource="{Binding Products}"
SelectedItem="{Binding SelectedProduct, Mode=TwoWay}">
<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>
</Border>
<StackPanel Grid.Row="1"
Grid.Column="1"
Spacing="10">
<TextBlock Text="当前商品"
FontSize="20"
FontWeight="Bold" />
<TextBlock Text="{Binding SelectedName}" />
<TextBlock Text="{Binding SelectedPriceText}" />
<StackPanel Orientation="Horizontal"
Spacing="8">
<Button Content="新增商品"
Command="{Binding AddProductCommand}" />
<Button Content="删除选中"
Command="{Binding RemoveSelectedCommand}" />
</StackPanel>
<TextBlock Text="{Binding Message}"
TextWrapping="Wrap" />
</StackPanel>
</Grid>
</Window>必须验证的操作
| 操作 | 应该看到 |
|---|---|
| 启动窗口 | 左侧有 3 个商品,右侧显示第一个商品 |
点击 Mouse | 右侧详情变成 Mouse |
| 点击新增商品 | 左侧新增一项,并自动选中新商品 |
| 点击删除选中 | 当前商品从列表中移除 |
| 删到没有商品 | 右侧显示未选择商品,删除按钮不可执行 |
如果报错,按这个顺序检查
- 如果
Products绑定报错,检查 ViewModel 是否有ObservableCollection<ProductItem> Products。 - 如果列表项绑定报错,检查
DataTemplate x:DataType="vm:ProductItem"。 - 如果右侧详情不更新,检查
SelectedProduct是否有NotifyPropertyChangedFor。 - 如果删除按钮状态不更新,检查
SelectedProduct是否有NotifyCanExecuteChangedFor(nameof(RemoveSelectedCommand))。 - 如果商品数量不刷新,检查新增和删除后是否调用了
OnPropertyChanged(nameof(ProductCountText))。
今天真正要掌握的闭环
ObservableCollection 保存多条数据
-> ListBox.ItemsSource 绑定集合
-> DataTemplate 控制每一项显示
-> SelectedItem 绑定当前选中项
-> 新增命令 Products.Add
-> 删除命令 Products.Remove
-> 集合变化后界面自动更新集合绑定不是手动刷新控件,而是让集合和界面之间建立通知关系。