Day 3 - 命令机制
建议用时:240-270 分钟
你将学会什么
- 知道
Command为什么比Click更适合 MVVM。 - 会用
[RelayCommand]生成SaveCommand。 - 会用
CanExecute控制按钮什么时候可以点击。 - 会用
[NotifyCanExecuteChangedFor]在输入变化时刷新按钮状态。 - 会写带参数的命令。
- 会把保存、重置、选择分类这些动作放到 ViewModel。
本页不手写复杂 ICommand 类,而是使用 Avalonia MVVM 模板自带的 CommunityToolkit.Mvvm。先把真实项目里最常用的 [RelayCommand] 写熟。
本页固定顺序
- 先学第一部分:弄懂今天最小、最重要的知识,并运行短例子。
- 再学第二部分:把刚学的知识组合成一个完整例子。
- 然后做第三部分:自己跟着敲,再完成重复训练和每日小测。
- 最后做第四部分:先独立完成作业,再用完整答案检查。
学习衔接
上一页学习的是“绑定与转换”,今天继续学习“命令机制”。先使用上一页已经会的写法,再只增加今天这个新知识点;如果前置内容还不能独立敲出,先回上一页复习,不要硬跳。
今天的最低通过线
第一次学习不要求背完整页。完成下面 3 项,就可以继续:
- 能用自己的话说明“命令机制”解决什么问题。
- 把第一部分的短例子亲手敲完,并确认每个例子都能运行。
- 不看完整答案完成第三部分至少前 3 个例子,再主动改一个值观察结果。
第一部分:先学原理和最小知识
这一部分从最小知识开始。先读解释,再把紧跟着的短例子敲一遍。命令机制解决的是“按钮点击后执行什么动作,以及什么时候允许执行”。
1. Click 和 Command 的区别
Week 6 的按钮写法:
<Button Content="保存"
Click="OnSaveClick" />它会执行 MainWindow.axaml.cs 里的方法。
MVVM 的按钮写法:
<Button Content="保存"
Command="{Binding SaveCommand}" />它会执行 ViewModel 里的命令。
| 写法 | 动作放在哪里 | 适合阶段 |
|---|---|---|
Click | View 的 code-behind | 刚开始理解事件 |
Command | ViewModel | MVVM 页面 |
进入 MVVM 后,按钮动作优先写成 Command。
2. Command 是什么
Command 可以理解成“按钮能绑定的一种动作对象”。
按钮不直接知道保存逻辑。按钮只知道:
我绑定了 SaveCommand
用户点我时,我让 SaveCommand 执行保存逻辑写在 ViewModel:
[RelayCommand]
private void Save()
{
}3. [RelayCommand] 做了什么
你写:
[RelayCommand]
private void Save()
{
}工具会生成:
SaveCommand所以 XAML 里绑定:
Command="{Binding SaveCommand}"命名规则:
| 方法名 | 生成命令名 |
|---|---|
Save | SaveCommand |
ResetForm | ResetFormCommand |
SelectCategory | SelectCategoryCommand |
4. Execute 是什么
Execute 是命令真正执行的动作。
用 [RelayCommand] 时,不需要自己写 Execute 方法。你写的普通方法就是执行内容:
private void Save()
{
Message = "保存成功";
}按钮点击后,最终执行的就是这个方法。
5. CanExecute 是什么
CanExecute 是“现在能不能执行”。
比如商品名称为空时,不应该允许保存:
private bool CanSave()
{
return !string.IsNullOrWhiteSpace(ProductName);
}再把它接到命令:
[RelayCommand(CanExecute = nameof(CanSave))]
private void Save()
{
}这样按钮会根据 CanSave() 的结果启用或禁用。
6. 为什么需要 NotifyCanExecuteChangedFor
CanSave() 依赖 ProductName。
当用户修改输入框时,ProductName 变了,保存按钮的可点击状态也应该重新判断。
所以要写:
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(SaveCommand))]
private string productName = "";意思是:ProductName 改变时,通知 SaveCommand 重新检查 CanSave()。
如果少了这句,按钮状态可能不会及时更新。
7. CommandParameter 是什么
有些命令需要参数。比如点击不同按钮选择不同分类:
<Button Content="普通商品"
Command="{Binding SelectCategoryCommand}"
CommandParameter="普通商品" />ViewModel 方法写:
[RelayCommand]
private void SelectCategory(string category)
{
Category = category;
}按钮把 CommandParameter 传给命令方法。
8. 命令里要不要再做校验
即使按钮已经用 CanExecute 禁用了,保存方法里仍然可以保留关键校验。
原因是:
- 以后保存方法可能被别的地方调用。
- 界面禁用不能代替业务规则。
- 防御性校验能减少隐藏错误。
简单页面可以只靠 CanExecute,真实业务最好在保存逻辑里也检查关键规则。
9. 命令该放哪里
| 动作 | 放哪里 |
|---|---|
| 保存表单 | ViewModel 命令 |
| 重置表单 | ViewModel 命令 |
| 选择分类 | ViewModel 命令 |
| 打开文件对话框 | 可以由 View 调用服务或后面学习交互服务 |
| 复杂业务保存 | ViewModel 调用 Service |
ViewModel 负责页面动作,但不应该把数据库、文件、网络细节全部塞进去。复杂业务后面交给 Service。
命令机制常用写法速查
| 需求 | 写法 | 作用 |
|---|---|---|
| 普通命令 | [RelayCommand] private void Save() | 生成 SaveCommand |
| 异步命令 | [RelayCommand] private async Task LoadAsync() | 生成 LoadCommand |
| 按钮绑定 | Command="{Binding SaveCommand}" | 点击执行 |
| 命令参数 | CommandParameter="{Binding}" | 把当前项传进去 |
| 防重复点击 | IsBusy + CanSave | 保存中禁用 |
| 刷新可执行状态 | [NotifyCanExecuteChangedFor] | 条件变化后更新按钮 |
命令命名规则:
Save 方法 -> SaveCommand
LoadAsync 方法 -> LoadCommand
DeleteProduct 方法 -> DeleteProductCommand第二部分:把知识组合成完整例子
今天要做一个 MVVM 命令窗口:
- 输入框为空时,保存按钮不能点击。
- 输入商品名称后,保存按钮可以点击。
- 点击保存后显示保存成功。
- 点击重置后清空输入。
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]
private string message = "请输入商品名称。";
private bool CanSave()
{
return !string.IsNullOrWhiteSpace(ProductName);
}
[RelayCommand(CanExecute = nameof(CanSave))]
private void Save()
{
Message = $"保存成功:{ProductName.Trim()}";
}
[RelayCommand]
private void ResetForm()
{
ProductName = "";
Message = "表单已重置。";
}
}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="340"
Title="命令机制">
<StackPanel Margin="20"
Spacing="10">
<TextBlock Text="商品名称" />
<TextBox Text="{Binding ProductName, Mode=TwoWay}"
PlaceholderText="请输入商品名称" />
<StackPanel Orientation="Horizontal"
Spacing="8">
<Button Content="保存"
Command="{Binding SaveCommand}" />
<Button Content="重置"
Command="{Binding ResetFormCommand}" />
</StackPanel>
<TextBlock Text="{Binding Message}"
TextWrapping="Wrap" />
</StackPanel>
</Window>先用一句话理解
Save 是真正执行保存的方法,SaveCommand 是自动生成给按钮绑定的命令,CanSave 决定保存按钮现在能不能点。
第三部分:跟着敲代码
从这里开始动手。每个例子都按真实文件来写。
例子 1:最简单的 SaveCommand
ViewModels/MainWindowViewModel.cs
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace ProductApp.ViewModels;
public partial class MainWindowViewModel : ViewModelBase
{
[ObservableProperty]
private string message = "还没有保存。";
[RelayCommand]
private void Save()
{
Message = "保存命令已经执行。";
}
}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="260"
Title="最简单命令">
<StackPanel Margin="20"
Spacing="10">
<Button Content="保存"
Command="{Binding SaveCommand}" />
<TextBlock Text="{Binding Message}" />
</StackPanel>
</Window>运行后点击保存,文字应该变成 保存命令已经执行。。
例子 2:加入 CanExecute
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]
private string message = "输入名称后,保存按钮才可以点击。";
private bool CanSave()
{
return !string.IsNullOrWhiteSpace(ProductName);
}
[RelayCommand(CanExecute = nameof(CanSave))]
private void Save()
{
Message = $"保存成功:{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="300"
Title="CanExecute">
<StackPanel Margin="20"
Spacing="10">
<TextBlock Text="商品名称" />
<TextBox Text="{Binding ProductName, Mode=TwoWay}"
PlaceholderText="请输入商品名称" />
<Button Content="保存"
Command="{Binding SaveCommand}" />
<TextBlock Text="{Binding Message}"
TextWrapping="Wrap" />
</StackPanel>
</Window>运行后观察:
- 输入框为空时,保存按钮不可点击。
- 输入
Keyboard后,保存按钮可以点击。
例子 3:加入重置命令
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]
private string message = "请输入商品名称。";
private bool CanSave()
{
return !string.IsNullOrWhiteSpace(ProductName);
}
[RelayCommand(CanExecute = nameof(CanSave))]
private void Save()
{
Message = $"保存成功:{ProductName.Trim()}";
}
[RelayCommand]
private void ResetForm()
{
ProductName = "";
Message = "表单已重置。";
}
}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="320"
Title="多个命令">
<StackPanel Margin="20"
Spacing="10">
<TextBlock Text="商品名称" />
<TextBox Text="{Binding ProductName, Mode=TwoWay}"
PlaceholderText="请输入商品名称" />
<StackPanel Orientation="Horizontal"
Spacing="8">
<Button Content="保存"
Command="{Binding SaveCommand}" />
<Button Content="重置"
Command="{Binding ResetFormCommand}" />
</StackPanel>
<TextBlock Text="{Binding Message}"
TextWrapping="Wrap" />
</StackPanel>
</Window>重置命令不需要 CanExecute,因为它随时都可以执行。
例子 4:带参数的命令
ViewModels/MainWindowViewModel.cs
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace ProductApp.ViewModels;
public partial class MainWindowViewModel : ViewModelBase
{
[ObservableProperty]
private string category = "未选择分类";
[ObservableProperty]
private string message = "请选择一个分类。";
[RelayCommand]
private void SelectCategory(string category)
{
Category = category;
Message = $"当前分类:{category}";
}
}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="300"
Title="命令参数">
<StackPanel Margin="20"
Spacing="10">
<TextBlock Text="{Binding Category}" />
<StackPanel Orientation="Horizontal"
Spacing="8">
<Button Content="普通商品"
Command="{Binding SelectCategoryCommand}"
CommandParameter="普通商品" />
<Button Content="虚拟商品"
Command="{Binding SelectCategoryCommand}"
CommandParameter="虚拟商品" />
<Button Content="服务商品"
Command="{Binding SelectCategoryCommand}"
CommandParameter="服务商品" />
</StackPanel>
<TextBlock Text="{Binding Message}" />
</StackPanel>
</Window>点击不同按钮,命令方法会收到不同的 category 参数。
例子 5:完整商品命令页面
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]
private string category = "普通商品";
[ObservableProperty]
private string message = "请输入商品名称。";
private bool CanSave()
{
return !string.IsNullOrWhiteSpace(ProductName);
}
[RelayCommand(CanExecute = nameof(CanSave))]
private void Save()
{
Message = $"保存成功:{ProductName.Trim()},分类:{Category}";
}
[RelayCommand]
private void ResetForm()
{
ProductName = "";
Category = "普通商品";
Message = "表单已重置。";
}
[RelayCommand]
private void SelectCategory(string category)
{
Category = category;
Message = $"已选择分类:{category}";
}
}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="380"
Title="命令机制">
<StackPanel Margin="20"
Spacing="10">
<TextBlock Text="商品名称" />
<TextBox Text="{Binding ProductName, Mode=TwoWay}"
PlaceholderText="请输入商品名称" />
<TextBlock Text="{Binding Category}" />
<StackPanel Orientation="Horizontal"
Spacing="8">
<Button Content="普通商品"
Command="{Binding SelectCategoryCommand}"
CommandParameter="普通商品" />
<Button Content="虚拟商品"
Command="{Binding SelectCategoryCommand}"
CommandParameter="虚拟商品" />
<Button Content="服务商品"
Command="{Binding SelectCategoryCommand}"
CommandParameter="服务商品" />
</StackPanel>
<StackPanel Orientation="Horizontal"
Spacing="8">
<Button Content="保存"
Command="{Binding SaveCommand}" />
<Button Content="重置"
Command="{Binding ResetFormCommand}" />
</StackPanel>
<TextBlock Text="{Binding Message}"
TextWrapping="Wrap" />
</StackPanel>
</Window>常见错误和修法
| 错误 | 为什么错 | 修法 |
|---|---|---|
按钮仍用 Click | 逻辑留在 View 里 | MVVM 中优先绑定 Command |
CanExecute 不刷新 | 按钮状态不会变 | 用 [NotifyCanExecuteChangedFor] |
异步命令不返回 Task | 异常和等待不好处理 | 异步命令写 async Task |
| 保存中还能重复点击 | 没有禁用条件 | CanSave 加上 !IsSaving |
| 命令里写太多业务 | ViewModel 过重 | 复杂规则放 Service |
小白重复敲写训练
命令要反复练“按钮 -> Command -> 方法”这条线。
训练 1:最小命令
[RelayCommand]
private void SayHello()
{
Message = "Hello";
}<Button Content="打招呼" Command="{Binding SayHelloCommand}" />
<TextBlock Text="{Binding Message}" />训练 2:命令接收参数
[RelayCommand]
private void SelectProduct(string? name)
{
Message = $"选中了 {name}";
}<Button Content="Keyboard"
Command="{Binding SelectProductCommand}"
CommandParameter="Keyboard" />第二遍增加鼠标按钮。
训练 3:控制命令是否可用
private bool CanSave() => !string.IsNullOrWhiteSpace(Name);
[RelayCommand(CanExecute = nameof(CanSave))]
private void Save()
{
Message = $"已保存 {Name}";
}第三遍让 Name 改变时通知 SaveCommand 重新判断。
每日小测
做完本页后,用这 5 题检查是否真的掌握。
1. 判断题
本页的目标不是只把代码运行起来,还要能说清楚“为什么这样写”。
答案:对。能运行只是第一步,能解释原理、常用操作和常见错误,才说明本页内容进入了可复用能力。
2. 填空题
本页主题是:命令机制。今天至少要掌握的 3 个点是:
1. 知道 `Command` 为什么比 `Click` 更适合 MVVM。
2. 会用 `[RelayCommand]` 生成 `SaveCommand`。
3. 会用 `CanExecute` 控制按钮什么时候可以点击。答案:以上 3 点必须能用自己的代码跑通,不能只停留在阅读。
3. 流程题
遇到本页相关功能时,先按什么顺序处理?
答案:先看完整例子,确认最终效果;再读原理和名词;然后跟着第三部分从空项目敲代码;最后对照作业答案检查。
4. 找错误题
如果本页代码运行失败,第一步应该做什么?
答案:先看终端或 IDE 里的第一条错误,找到文件名和行号;不要同时改很多地方。再回到本页的“常见错误和修法”表格,对照错误类型逐项排查。
5. 改需求题
在本页完整例子跑通后,至少改一个小需求。
可选改法:
- 改一个字段名称。
- 多加一个校验条件。
- 多输出一行结果。
- 把固定数据改成用户输入。
- 把一次处理改成多条数据处理。
答案标准:修改后能重新运行,并能说明这次修改影响了哪一段逻辑。重点检查:知道 Command 为什么比 Click 更适合 MVVM。。
上位机专项练习
Command 把按钮动作放进 ViewModel,避免所有点击逻辑都堆在窗口后台代码里。
下面 3 个例子都要亲手敲。先运行原代码,再完成每个例子后面的改动任务。
专项例子 1:定义连接命令
public partial class MainWindowViewModel : ObservableObject
{
[ObservableProperty] private string statusText = "未连接";
[RelayCommand]
private void Connect() => StatusText = "已连接";
}运行结果或界面效果:
执行 ConnectCommand 后状态变成已连接改动任务: 增加 DisconnectCommand。
专项例子 2:按钮绑定命令
<Button Content="连接设备" Command="{Binding ConnectCommand}" />
<TextBlock Text="{Binding StatusText}" />运行结果或界面效果:
点击按钮执行 ViewModel 的 Connect 方法改动任务: 增加断开按钮。
专项例子 3:根据状态禁用命令
[RelayCommand(CanExecute = nameof(CanConnect))]
private void Connect() => IsConnected = true;
private bool CanConnect() => !IsConnected;运行结果或界面效果:
连接后连接命令不可再次执行改动任务: 状态改变时调用 ConnectCommand.NotifyCanExecuteChanged()。
第四部分:作业完整答案
这一部分给出当天作业的完整答案。先照第三部分敲一遍,再用这里检查结果。
作业要求
做一个商品命令页面:
- 商品名称为空时,保存按钮不可点击。
- 输入商品名称后,保存按钮可以点击。
- 点击分类按钮时,更新当前分类。
- 点击保存时,显示商品名称和分类。
- 点击重置时,清空名称,分类恢复为
普通商品。
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]
private string category = "普通商品";
[ObservableProperty]
private string message = "请输入商品名称。";
private bool CanSave()
{
return !string.IsNullOrWhiteSpace(ProductName);
}
[RelayCommand(CanExecute = nameof(CanSave))]
private void Save()
{
Message = $"保存成功:{ProductName.Trim()},分类:{Category}";
}
[RelayCommand]
private void ResetForm()
{
ProductName = "";
Category = "普通商品";
Message = "表单已重置。";
}
[RelayCommand]
private void SelectCategory(string category)
{
Category = category;
Message = $"已选择分类:{category}";
}
}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="380"
Title="命令机制">
<StackPanel Margin="20"
Spacing="10">
<TextBlock Text="商品名称" />
<TextBox Text="{Binding ProductName, Mode=TwoWay}"
PlaceholderText="请输入商品名称" />
<TextBlock Text="{Binding Category}" />
<StackPanel Orientation="Horizontal"
Spacing="8">
<Button Content="普通商品"
Command="{Binding SelectCategoryCommand}"
CommandParameter="普通商品" />
<Button Content="虚拟商品"
Command="{Binding SelectCategoryCommand}"
CommandParameter="虚拟商品" />
<Button Content="服务商品"
Command="{Binding SelectCategoryCommand}"
CommandParameter="服务商品" />
</StackPanel>
<StackPanel Orientation="Horizontal"
Spacing="8">
<Button Content="保存"
Command="{Binding SaveCommand}" />
<Button Content="重置"
Command="{Binding ResetFormCommand}" />
</StackPanel>
<TextBlock Text="{Binding Message}"
TextWrapping="Wrap" />
</StackPanel>
</Window>必须验证的操作
| 操作 | 应该看到 |
|---|---|
| 启动窗口 | 保存按钮不可点击 |
输入 Keyboard | 保存按钮可以点击 |
点击 虚拟商品 | 分类文字变成 虚拟商品 |
| 点击保存 | 显示 保存成功:Keyboard,分类:虚拟商品 |
| 点击重置 | 名称清空,分类恢复为 普通商品 |
如果报错,按这个顺序检查
- 如果
SaveCommand找不到,检查Save方法是否写了[RelayCommand]。 - 如果保存按钮状态不变,检查
productName字段是否写了[NotifyCanExecuteChangedFor(nameof(SaveCommand))]。 - 如果按钮一直不可点,检查
CanSave()是否返回了正确条件。 - 如果分类按钮没反应,检查
CommandParameter是否传了字符串。 - 如果
SelectCategoryCommand找不到,检查方法名是不是SelectCategory。
今天真正要掌握的闭环
View 绑定 SaveCommand
-> RelayCommand 让 Save 方法变成可绑定命令
-> CanSave 决定按钮是否可执行
-> ProductName 改变时通知 SaveCommand 重新判断
-> 点击按钮后执行 Save
-> Save 修改 Message
-> View 自动显示新 Message命令机制的重点不是少写一个 Click,而是让页面动作回到 ViewModel,方便维护、测试和复用。