Week 07 - 阶段检查
这一页用来确认 Week 7 的 MVVM 能力是否真正跑通。检查方式是重做三个窗口:属性绑定、命令验证、集合绑定。
本周学习地图
| 阶段 | 先掌握什么 | 最终能写什么 |
|---|---|---|
| 分层 | View、ViewModel、Model | 能说清每层职责 |
| 属性 | [ObservableProperty]、通知刷新 | 能让界面自动显示状态 |
| 绑定 | OneWay、TwoWay、计算属性 | 能让输入框和属性互通 |
| 命令 | [RelayCommand]、CanExecute | 能用按钮触发 ViewModel 方法 |
| 验证 | 字段错误、保存按钮禁用 | 能阻止错误输入保存 |
| 集合 | ObservableCollection、选中项 | 能做增删改查列表 |
本周自测项目:MVVM 商品管理页面
从 Avalonia MVVM 模板开始写一个页面,至少包含:
MainWindow.axaml只写控件和绑定。MainWindowViewModel.cs保存输入、列表、选中项、错误消息。- 新增商品、删除商品、清空表单。
- 名称为空、价格非法时保存按钮不可执行或显示错误。
- 列表变化后空状态自动更新。
过关标准:能完整解释 TextBox -> ProductName -> SaveCommand -> Products -> ListBox 的数据流。
上位机阶段验收:MVVM 设备管理页面
本周目标: 把设备状态、集合、验证和操作命令移入 ViewModel。
必须亲手完成:
- 属性绑定后数据变化自动刷新
- 新增、删除、连接使用 Command
- 无效设备名和端口阻止保存
过关标准: View 后台代码不再承担主要业务,ViewModel 可以脱离窗口单独测试。
不要只看答案。新建一个空项目重做一次,运行成功后再故意改坏一处并自己排错。
本周流程图
TextBox 输入
-> TwoWay Binding 写入 ViewModel 属性
-> SaveCommand 执行 ViewModel 方法
-> Service 校验业务规则
-> ObservableCollection 更新列表
-> ListBox 自动刷新
-> ErrorMessage / Message 显示状态第一部分:本周原理地图
Week 7 的主线是:把页面状态和页面动作放进 ViewModel,让 View 通过绑定和命令使用它们。
本周能力拆解
| 能力 | 你要能做到什么 | 对应页面 |
|---|---|---|
| MVVM 分层 | 能说清 View、ViewModel、Model 职责 | Day 1 |
| 绑定转换 | 能处理 OneWay、TwoWay、计算属性 | Day 2 |
| 命令机制 | 能写 RelayCommand 和 CanExecute | Day 3 |
| 表单验证 | 能显示字段错误并阻止错误保存 | Day 4 |
| 集合绑定 | 能绑定列表、选中项、新增删除 | Day 5 |
| CRUD 项目 | 能把增删改查串成一个页面 | Day 6 |
必须掌握的关键词
| 关键词 | 完整解释 |
|---|---|
DataContext | View 绑定时使用的数据对象 |
x:DataType | 告诉 XAML 当前绑定的 ViewModel 类型 |
[ObservableProperty] | 自动生成属性和变化通知 |
[NotifyPropertyChangedFor] | 一个属性变化时通知计算属性刷新 |
[RelayCommand] | 自动生成可绑定命令 |
CanExecute | 控制命令当前能不能执行 |
ObservableCollection | 新增删除时能通知界面刷新 |
SelectedItem | 当前选中的列表项 |
Week 7 必须会用的 MVVM 写法
| 主题 | 必须会的写法 |
|---|---|
| ViewModel 属性 | [ObservableProperty] private string name = ""; |
| 计算属性联动 | [NotifyPropertyChangedFor(nameof(CanSave))] |
| 普通命令 | [RelayCommand] private void Save() |
| 异步命令 | [RelayCommand] private async Task LoadAsync() |
| 文本双向绑定 | Text="{Binding Name, Mode=TwoWay}" |
| 命令绑定 | Command="{Binding SaveCommand}" |
| 按钮可用状态 | IsEnabled="{Binding CanSave}" |
| 列表绑定 | ItemsSource="{Binding Products}" |
| 选中项绑定 | SelectedItem="{Binding SelectedProduct}" |
| 可刷新集合 | ObservableCollection<T> |
| 错误提示 | ErrorMessage |
本周最重要的数据流
ViewModel 暴露属性和命令
-> View 用 Binding 读取或修改属性
-> Button 用 Command 调用 ViewModel 方法
-> ViewModel 做验证、更新集合、更新消息
-> View 自动显示新状态第二部分:阶段检查 1 - 属性绑定
目标:确认 View 能显示 ViewModel 属性,输入框能把用户输入写回 ViewModel。
ViewModels/MainWindowViewModel.cs 完整答案
using CommunityToolkit.Mvvm.ComponentModel;
namespace ProductApp.ViewModels;
public partial class MainWindowViewModel : ViewModelBase
{
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(DisplayName))]
private string productName = "Keyboard";
public string DisplayName =>
string.IsNullOrWhiteSpace(ProductName) ? "未填写商品名称" : ProductName.Trim();
}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="280"
Title="属性绑定检查">
<StackPanel Margin="20"
Spacing="10">
<TextBlock Text="商品名称" />
<TextBox Text="{Binding ProductName, Mode=TwoWay}" />
<TextBlock Text="{Binding DisplayName}" />
</StackPanel>
</Window>必须能解释
| 代码 | 解释 |
|---|---|
Mode=TwoWay | 输入框变化会写回 ViewModel |
DisplayName | 根据 ProductName 算出来的显示属性 |
NotifyPropertyChangedFor | ProductName 变化时通知 DisplayName 刷新 |
第三部分:阶段检查 2 - 命令和验证
目标:确认按钮能绑定命令,输入错误时按钮不可执行。
ViewModels/MainWindowViewModel.cs 完整答案
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace ProductApp.ViewModels;
public partial class MainWindowViewModel : ViewModelBase
{
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(SaveCommand))]
private string productName = "";
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(SaveCommand))]
private string priceText = "199";
[ObservableProperty]
private string message = "请输入商品信息。";
private bool CanSave()
{
return !string.IsNullOrWhiteSpace(ProductName) &&
decimal.TryParse(PriceText, out decimal price) &&
price > 0;
}
[RelayCommand(CanExecute = nameof(CanSave))]
private void Save()
{
decimal price = decimal.Parse(PriceText.Trim());
Message = $"保存成功:{ProductName.Trim()},价格 {price}";
}
}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="540"
Height="340"
Title="命令验证检查">
<StackPanel Margin="20"
Spacing="10">
<TextBlock Text="商品名称" />
<TextBox Text="{Binding ProductName, Mode=TwoWay}" />
<TextBlock Text="商品价格" />
<TextBox Text="{Binding PriceText, Mode=TwoWay}" />
<Button Content="保存"
Command="{Binding SaveCommand}" />
<TextBlock Text="{Binding Message}"
TextWrapping="Wrap" />
</StackPanel>
</Window>必须验证
| 输入 | 应该看到 |
|---|---|
| 名称空 | 保存按钮不可点击 |
价格 abc | 保存按钮不可点击 |
价格 0 | 保存按钮不可点击 |
名称 Keyboard,价格 199 | 保存按钮可点击,保存后显示成功 |
第四部分:阶段检查 3 - 集合绑定
目标:确认 ObservableCollection 能绑定列表,新增后界面自动刷新。
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 nextNumber = 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))]
private ProductItem? selectedProduct;
public string SelectedName => SelectedProduct?.Name ?? "未选择商品";
public MainWindowViewModel()
{
SelectedProduct = Products[0];
}
[RelayCommand]
private void AddProduct()
{
var product = new ProductItem
{
Name = $"Product {nextNumber}",
Price = nextNumber * 100
};
nextNumber++;
Products.Add(product);
SelectedProduct = product;
}
}
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>
<TextBlock Text="{Binding Name}"
FontWeight="Bold" />
<TextBlock Text="{Binding Price}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<StackPanel Grid.Column="1"
Spacing="10">
<TextBlock Text="当前商品"
FontSize="20"
FontWeight="Bold" />
<TextBlock Text="{Binding SelectedName}" />
<Button Content="新增商品"
Command="{Binding AddProductCommand}" />
</StackPanel>
</Grid>
</Window>必须能解释
| 代码 | 解释 |
|---|---|
ObservableCollection<ProductItem> | 新增删除时通知界面 |
ItemsSource="{Binding Products}" | 列表显示商品集合 |
DataTemplate x:DataType="vm:ProductItem" | 定义每个商品怎么显示 |
SelectedItem="{Binding SelectedProduct, Mode=TwoWay}" | 界面选中项和 ViewModel 当前项同步 |
第五部分:常见错误完整修法
| 现象 | 原因 | 修法 |
|---|---|---|
| 绑定属性找不到 | XAML 绑定名和 ViewModel 属性名不同 | 对照属性名大小写 |
| 输入框改了但 TextBlock 不变 | 缺少 NotifyPropertyChangedFor | 给源字段加通知 |
| 命令找不到 | 方法没有 [RelayCommand] | 给方法加 [RelayCommand] |
| 按钮状态不刷新 | 缺少 NotifyCanExecuteChangedFor | 给影响 CanSave 的字段加通知 |
| 列表不刷新 | 使用普通 List<T> | 改用 ObservableCollection<T> |
| 列表项绑定报错 | DataTemplate 没写类型 | 写 x:DataType="vm:ProductItem" |
| 当前项为空时报错 | 没处理 SelectedProduct 为 null | 使用 SelectedProduct?.Name ?? "未选择商品" |
第六部分:通过标准
达到下面标准,再进入 Week 8。
- 能解释 View 和 ViewModel 的区别。
- 能设置并检查
DataContext。 - 能用
[ObservableProperty]写可绑定属性。 - 能用
Mode=TwoWay绑定输入框。 - 能用
[RelayCommand]写按钮命令。 - 能用
CanExecute禁用按钮。 - 能用
decimal.TryParse做价格验证。 - 能用
ObservableCollection绑定列表。 - 能用
SelectedItem表示当前选中项。 - 能重做一个内存版 MVVM CRUD 页面。
通过标准不是把每个特性背下来,而是能用它们完成一个清晰的数据流:输入、验证、命令、列表、反馈。