Skip to Content
Week 07Day 3 - 命令机制

Day 3 - 命令机制

建议用时:240-270 分钟

你将学会什么

  • 知道 Command 为什么比 Click 更适合 MVVM。
  • 会用 [RelayCommand] 生成 SaveCommand
  • 会用 CanExecute 控制按钮什么时候可以点击。
  • 会用 [NotifyCanExecuteChangedFor] 在输入变化时刷新按钮状态。
  • 会写带参数的命令。
  • 会把保存、重置、选择分类这些动作放到 ViewModel。

本页不手写复杂 ICommand 类,而是使用 Avalonia MVVM 模板自带的 CommunityToolkit.Mvvm。先把真实项目里最常用的 [RelayCommand] 写熟。

本页固定顺序

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

学习衔接

上一页学习的是“绑定与转换”,今天继续学习“命令机制”。先使用上一页已经会的写法,再只增加今天这个新知识点;如果前置内容还不能独立敲出,先回上一页复习,不要硬跳。

今天的最低通过线

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

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

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

这一部分从最小知识开始。先读解释,再把紧跟着的短例子敲一遍。命令机制解决的是“按钮点击后执行什么动作,以及什么时候允许执行”。

1. Click 和 Command 的区别

Week 6 的按钮写法:

<Button Content="保存" Click="OnSaveClick" />

它会执行 MainWindow.axaml.cs 里的方法。

MVVM 的按钮写法:

<Button Content="保存" Command="{Binding SaveCommand}" />

它会执行 ViewModel 里的命令。

写法动作放在哪里适合阶段
ClickView 的 code-behind刚开始理解事件
CommandViewModelMVVM 页面

进入 MVVM 后,按钮动作优先写成 Command。

2. Command 是什么

Command 可以理解成“按钮能绑定的一种动作对象”。

按钮不直接知道保存逻辑。按钮只知道:

我绑定了 SaveCommand 用户点我时,我让 SaveCommand 执行

保存逻辑写在 ViewModel:

[RelayCommand] private void Save() { }

3. [RelayCommand] 做了什么

你写:

[RelayCommand] private void Save() { }

工具会生成:

SaveCommand

所以 XAML 里绑定:

Command="{Binding SaveCommand}"

命名规则:

方法名生成命令名
SaveSaveCommand
ResetFormResetFormCommand
SelectCategorySelectCategoryCommand

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()。

第四部分:作业完整答案

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

作业要求

做一个商品命令页面:

  1. 商品名称为空时,保存按钮不可点击。
  2. 输入商品名称后,保存按钮可以点击。
  3. 点击分类按钮时,更新当前分类。
  4. 点击保存时,显示商品名称和分类。
  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>

必须验证的操作

操作应该看到
启动窗口保存按钮不可点击
输入 Keyboard保存按钮可以点击
点击 虚拟商品分类文字变成 虚拟商品
点击保存显示 保存成功:Keyboard,分类:虚拟商品
点击重置名称清空,分类恢复为 普通商品

如果报错,按这个顺序检查

  1. 如果 SaveCommand 找不到,检查 Save 方法是否写了 [RelayCommand]
  2. 如果保存按钮状态不变,检查 productName 字段是否写了 [NotifyCanExecuteChangedFor(nameof(SaveCommand))]
  3. 如果按钮一直不可点,检查 CanSave() 是否返回了正确条件。
  4. 如果分类按钮没反应,检查 CommandParameter 是否传了字符串。
  5. 如果 SelectCategoryCommand 找不到,检查方法名是不是 SelectCategory

今天真正要掌握的闭环

View 绑定 SaveCommand -> RelayCommand 让 Save 方法变成可绑定命令 -> CanSave 决定按钮是否可执行 -> ProductName 改变时通知 SaveCommand 重新判断 -> 点击按钮后执行 Save -> Save 修改 Message -> View 自动显示新 Message

命令机制的重点不是少写一个 Click,而是让页面动作回到 ViewModel,方便维护、测试和复用。