Day 1 - MVVM 入门
建议用时:240-270 分钟
你将学会什么
- 知道 MVVM 为什么要把界面和状态分开。
- 知道
View、ViewModel、Model各自负责什么。 - 会使用 Avalonia 模板自带的
ViewModelBase。 - 会用
CommunityToolkit.Mvvm的[ObservableProperty]自动生成可通知界面的属性。 - 会用
[RelayCommand]自动生成按钮可绑定的命令。 - 会让
TextBox绑定到 ViewModel,并点击按钮执行 ViewModel 方法。
本页开始正式从 code-behind 事件过渡到 MVVM。目标不是马上写复杂架构,而是先跑通一条最小链路:ViewModel 有属性,View 绑定属性,按钮绑定命令。
本页固定顺序
- 先学第一部分:弄懂今天最小、最重要的知识,并运行短例子。
- 再学第二部分:把刚学的知识组合成一个完整例子。
- 然后做第三部分:自己跟着敲,再完成重复训练和每日小测。
- 最后做第四部分:先独立完成作业,再用完整答案检查。
学习衔接
上一页学习的是“复盘与巩固”,今天继续学习“MVVM 入门”。先使用上一页已经会的写法,再只增加今天这个新知识点;如果前置内容还不能独立敲出,先回上一页复习,不要硬跳。
今天的最低通过线
第一次学习不要求背完整页。完成下面 3 项,就可以继续:
- 能用自己的话说明“MVVM 入门”解决什么问题。
- 把第一部分的短例子亲手敲完,并确认每个例子都能运行。
- 不看完整答案完成第三部分至少前 3 个例子,再主动改一个值观察结果。
第一部分:先学原理和最小知识
这一部分从最小知识开始。先读解释,再把紧跟着的短例子敲一遍。MVVM 的难点不是语法,而是职责分开。
1. 为什么要从 code-behind 转到 MVVM
Week 6 里我们把按钮事件写在 MainWindow.axaml.cs:
按钮 Click
-> MainWindow.axaml.cs 读取控件
-> MainWindow.axaml.cs 校验
-> MainWindow.axaml.cs 修改界面这种写法直观,但页面一复杂,问题会出现:
- 窗口文件越来越长。
- 保存逻辑和控件细节混在一起。
- 很难单独测试保存逻辑。
- 换界面时,逻辑容易跟着被改坏。
MVVM 的目标是把这条线改成:
View 负责显示
-> ViewModel 负责状态和动作
-> Model 负责业务数据2. View 是什么
View 就是界面。Avalonia 里常见的 View 文件是:
Views/MainWindow.axaml
Views/MainWindow.axaml.cs本页里,MainWindow.axaml 负责:
- 摆控件。
- 写布局。
- 写绑定。
- 说明按钮要执行哪个命令。
View 不应该写大量业务规则。比如“商品名称不能为空”这种规则,应该放到 ViewModel 或更后面的业务服务里。
3. ViewModel 是什么
ViewModel 是给 View 使用的数据和动作。
本页里,MainWindowViewModel 负责:
- 保存
ProductName。 - 保存
Message。 - 提供
SaveCommand。 - 在保存时检查名称是否为空。
ViewModel 不应该直接依赖具体控件。不要在 ViewModel 里写 NameBox.Text,因为 NameBox 是 View 里的控件。
4. Model 是什么
Model 是业务数据本身。例如后面可以有:
public class Product
{
public string Name { get; set; } = "";
public decimal Price { get; set; }
}本页先不引入完整 Model,是因为第一天先把 View 和 ViewModel 的连接跑通。后面会把 Product 独立出来。
5. Binding 是什么
绑定就是把界面属性和 ViewModel 属性连接起来。
<TextBox Text="{Binding ProductName, Mode=TwoWay}" />意思是:
TextBox.Text显示ProductName。- 用户修改输入框时,
ProductName也跟着变。 Mode=TwoWay表示双向同步。
显示文字也可以绑定:
<TextBlock Text="{Binding Message}" />意思是:Message 变了,界面文字也跟着变。
6. Command 是什么
命令是 MVVM 里按钮触发动作的方式。
Week 6 写法:
<Button Content="保存" Click="OnSaveClick" />Week 7 MVVM 写法:
<Button Content="保存" Command="{Binding SaveCommand}" />区别是:
Click调用 View 的 code-behind 方法。Command调用 ViewModel 里的命令。
7. [ObservableProperty] 做了什么
本页写的是字段:
[ObservableProperty]
private string productName = "Keyboard";CommunityToolkit.Mvvm 会在编译时帮你生成属性:
public string ProductName
{
get;
set;
}真正重要的是:生成的属性会在值变化时通知界面刷新。
所以 XAML 可以绑定:
Text="{Binding ProductName, Mode=TwoWay}"注意命名规则:
| 字段 | 自动生成的属性 |
|---|---|
productName | ProductName |
message | Message |
priceText | PriceText |
8. [RelayCommand] 做了什么
本页写的是方法:
[RelayCommand]
private void Save()
{
}CommunityToolkit.Mvvm 会自动生成一个可绑定的命令属性:
SaveCommand所以 XAML 里写:
Command="{Binding SaveCommand}"命名规则:
| 方法 | 自动生成的命令 |
|---|---|
Save | SaveCommand |
Delete | DeleteCommand |
ResetForm | ResetFormCommand |
9. x:DataType 是什么
模板里的 MainWindow.axaml 会写:
x:DataType="vm:MainWindowViewModel"它告诉 Avalonia:这个页面绑定的数据类型是 MainWindowViewModel。
好处是:
- 绑定属性写错时更容易在编译阶段发现。
- 编辑器更容易提示属性名。
- 绑定更明确。
如果你写了:
Text="{Binding ProductNam}"少了一个 e,就应该回头检查 ViewModel 里真正的属性名是不是 ProductName。
10. DataContext 是什么
绑定要能工作,View 必须知道自己的 ViewModel 是谁。这个关系叫 DataContext。
Avalonia MVVM 模板通常在 App.axaml.cs 里设置:
desktop.MainWindow = new MainWindow
{
DataContext = new MainWindowViewModel(),
};意思是:MainWindow 这个 View 使用 MainWindowViewModel 作为数据来源。
如果界面显示不出绑定数据,先检查 DataContext 是否设置。
MVVM 常用写法速查
| 需求 | 写法 | 作用 |
|---|---|---|
| View | MainWindow.axaml | 只负责界面结构 |
| ViewModel | MainWindowViewModel.cs | 保存界面数据和操作 |
| 文本属性 | [ObservableProperty] private string name = ""; | 自动生成 Name |
| 数字属性 | [ObservableProperty] private decimal price; | 自动通知界面 |
| 命令 | [RelayCommand] private void Save() | 自动生成 SaveCommand |
| 文本绑定 | Text="{Binding Name, Mode=TwoWay}" | 输入框和属性互通 |
| 命令绑定 | Command="{Binding SaveCommand}" | 按钮调用方法 |
| 列表绑定 | ObservableCollection<T> | 列表变化自动刷新 |
MVVM 最小闭环:
TextBox 输入
-> 绑定到 ViewModel 属性
-> Button 触发 Command
-> Command 修改属性或集合
-> 界面自动刷新第二部分:把知识组合成完整例子
今天要做一个 MVVM 版本的商品保存窗口:
MainWindow.axaml只写界面和绑定。MainWindowViewModel.cs保存界面状态和按钮命令。- 点击保存后,不再写
Click="OnSaveClick",而是写Command="{Binding SaveCommand}"。
ViewModels/MainWindowViewModel.cs
把下面内容放到 ViewModels/MainWindowViewModel.cs。
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace ProductApp.ViewModels;
public partial class MainWindowViewModel : ViewModelBase
{
[ObservableProperty]
private string productName = "Keyboard";
[ObservableProperty]
private string message = "请输入商品名称,然后点击保存。";
[RelayCommand]
private void Save()
{
string name = ProductName.Trim();
if (name.Length == 0)
{
Message = "商品名称不能为空。";
return;
}
Message = $"保存成功:{name}";
}
}Views/MainWindow.axaml
把下面内容放到 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="MVVM 入门">
<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>先用一句话理解
TextBox 改的是 ProductName,Button 执行的是 SaveCommand,Save 方法修改的是 Message,界面自动显示新的 Message。
MVVM 文件关系和数据流
先把文件关系看成一条线:
Views/MainWindow.axaml
-> 通过 Binding 读取和修改 ViewModel
-> ViewModels/MainWindowViewModel.cs
-> 属性保存界面状态
-> 命令处理按钮动作
-> Model 或 Service 处理业务数据今天这个例子的真实数据流是:
用户在 TextBox 输入 Keyboard
-> Text="{Binding ProductName, Mode=TwoWay}"
-> ProductName 变成 Keyboard
-> 用户点击 Button
-> Command="{Binding SaveCommand}"
-> 执行 Save()
-> Save() 修改 Message
-> TextBlock 自动显示新的 Message对应文件不要混:
| 文件 | 写什么 | 不写什么 |
|---|---|---|
MainWindow.axaml | 控件、布局、绑定 | 复杂业务规则 |
MainWindow.axaml.cs | 初始化窗口 | 商品保存逻辑 |
MainWindowViewModel.cs | 属性、命令、界面状态 | 具体控件对象 |
Product.cs | 商品数据结构 | 按钮点击逻辑 |
第三部分:跟着敲代码
从这里开始动手。每个例子都按真实文件来写。
例子 1:只绑定一个只读标题
先跑通最简单的绑定:ViewModel 提供一个标题,View 显示它。
ViewModels/MainWindowViewModel.cs
namespace ProductApp.ViewModels;
public partial class MainWindowViewModel : ViewModelBase
{
public string Title { get; } = "商品管理";
}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="240"
Title="绑定标题">
<TextBlock Text="{Binding Title}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="24"
FontWeight="Bold" />
</Window>运行后,窗口中间应该显示 商品管理。
例子 2:绑定输入框和消息
这一步加入 [ObservableProperty],让输入框和 ViewModel 属性同步。
ViewModels/MainWindowViewModel.cs
using CommunityToolkit.Mvvm.ComponentModel;
namespace ProductApp.ViewModels;
public partial class MainWindowViewModel : ViewModelBase
{
[ObservableProperty]
private string productName = "Keyboard";
[ObservableProperty]
private string message = "修改输入框后,下面会显示当前值。";
partial void OnProductNameChanged(string value)
{
Message = $"当前输入:{value}";
}
}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="绑定输入">
<StackPanel Margin="20"
Spacing="10">
<TextBlock Text="商品名称" />
<TextBox Text="{Binding ProductName, Mode=TwoWay}" />
<TextBlock Text="{Binding Message}"
TextWrapping="Wrap" />
</StackPanel>
</Window>这里的关键是:
partial void OnProductNameChanged(string value)这是 [ObservableProperty] 生成属性时提供的变化回调。ProductName 变化后,这个方法会执行。
例子 3:按钮绑定命令
这一步加入 [RelayCommand],让按钮不再使用 Click。
ViewModels/MainWindowViewModel.cs
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace ProductApp.ViewModels;
public partial class MainWindowViewModel : ViewModelBase
{
[ObservableProperty]
private string productName = "Keyboard";
[ObservableProperty]
private string message = "请输入商品名称,然后点击保存。";
[RelayCommand]
private void Save()
{
string name = ProductName.Trim();
if (name.Length == 0)
{
Message = "商品名称不能为空。";
return;
}
Message = $"保存成功:{name}";
}
}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="MVVM 命令">
<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>运行后做两次验证:
- 输入框清空,点击保存,应该显示
商品名称不能为空。。 - 输入
Mouse,点击保存,应该显示保存成功:Mouse。
例子 4:补一个重置命令
这一步加第二个命令,确认命令命名规则。
ViewModels/MainWindowViewModel.cs
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace ProductApp.ViewModels;
public partial class MainWindowViewModel : ViewModelBase
{
[ObservableProperty]
private string productName = "Keyboard";
[ObservableProperty]
private string message = "请输入商品名称,然后点击保存。";
[RelayCommand]
private void Save()
{
string name = ProductName.Trim();
if (name.Length == 0)
{
Message = "商品名称不能为空。";
return;
}
Message = $"保存成功:{name}";
}
[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="520"
Height="340"
Title="MVVM 入门">
<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。ResetForm生成ResetFormCommand。- XAML 里不能写成
ResetCommand。
常见错误和修法
| 错误 | 为什么错 | 修法 |
|---|---|---|
| Binding 属性名拼错 | 界面不会更新 | 对照 ViewModel 里的真实属性名 |
| 没有设置 DataContext | View 找不到 ViewModel | 检查模板里的 ViewLocator 或窗口 DataContext |
[ObservableProperty] 字段命名错 | 生成的属性名不符合预期 | 字段用小驼峰,例如 productName |
| 命令名写错 | Save 会生成 SaveCommand | XAML 绑定 Command="{Binding SaveCommand}" |
| ViewModel 直接访问控件 | MVVM 分层被破坏 | ViewModel 只暴露属性和命令 |
小白重复敲写训练
MVVM 第一天只练“属性变化后界面跟着变”。
训练 1:最小 ViewModel
public partial class MainWindowViewModel : ObservableObject
{
[ObservableProperty]
private string _message = "Hello MVVM";
}<TextBlock Text="{Binding Message}" />改动任务:把初始文字改三次,每次重新运行。
训练 2:输入框双向绑定
[ObservableProperty]
private string _name = "";<StackPanel>
<TextBox Text="{Binding Name}" />
<TextBlock Text="{Binding Name}" />
</StackPanel>输入文字,观察下方文本是否同步。
训练 3:计算属性
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(Total))]
private decimal _price;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(Total))]
private int _count;
public decimal Total => Price * Count;第三遍在 XAML 中绑定 Price、Count 和 Total。
每日小测
做完本页后,用这 5 题检查是否真的掌握。
1. 判断题
本页的目标不是只把代码运行起来,还要能说清楚“为什么这样写”。
答案:对。能运行只是第一步,能解释原理、常用操作和常见错误,才说明本页内容进入了可复用能力。
2. 填空题
本页主题是:MVVM 入门。今天至少要掌握的 3 个点是:
1. 知道 MVVM 为什么要把界面和状态分开。
2. 知道 `View`、`ViewModel`、`Model` 各自负责什么。
3. 会使用 Avalonia 模板自带的 `ViewModelBase`。答案:以上 3 点必须能用自己的代码跑通,不能只停留在阅读。
3. 流程题
遇到本页相关功能时,先按什么顺序处理?
答案:先看完整例子,确认最终效果;再读原理和名词;然后跟着第三部分从空项目敲代码;最后对照作业答案检查。
4. 找错误题
如果本页代码运行失败,第一步应该做什么?
答案:先看终端或 IDE 里的第一条错误,找到文件名和行号;不要同时改很多地方。再回到本页的“常见错误和修法”表格,对照错误类型逐项排查。
5. 改需求题
在本页完整例子跑通后,至少改一个小需求。
可选改法:
- 改一个字段名称。
- 多加一个校验条件。
- 多输出一行结果。
- 把固定数据改成用户输入。
- 把一次处理改成多条数据处理。
答案标准:修改后能重新运行,并能说明这次修改影响了哪一段逻辑。重点检查:知道 MVVM 为什么要把界面和状态分开。。
上位机专项练习
MVVM 把界面显示和业务状态分开。先让 ViewModel 提供设备名称、状态和按钮动作。
下面 3 个例子都要亲手敲。先运行原代码,再完成每个例子后面的改动任务。
专项例子 1:ViewModel 提供设备状态
public class MainWindowViewModel
{
public string DeviceName { get; } = "PLC-01";
public string StatusText { get; } = "在线";
}运行结果或界面效果:
ViewModel 保存界面要显示的数据改动任务: 增加 Temperature 属性。
专项例子 2:View 绑定属性
<StackPanel Margin="20">
<TextBlock Text="{Binding DeviceName}" FontSize="28" />
<TextBlock Text="{Binding StatusText}" />
</StackPanel>运行结果或界面效果:
界面显示 PLC-01 和在线改动任务: 绑定 Temperature 并显示单位。
专项例子 3:设置 DataContext
public MainWindow()
{
InitializeComponent();
DataContext = new MainWindowViewModel();
}运行结果或界面效果:
Window 从 MainWindowViewModel 取得绑定数据改动任务: 在 ViewModel 中修改设备名后重启观察。
第四部分:作业完整答案
这一部分给出当天作业的完整答案。先照第三部分敲一遍,再用这里检查结果。
作业要求
做一个 MVVM 商品输入窗口:
TextBox绑定ProductName。TextBlock绑定Message。- 保存按钮绑定
SaveCommand。 - 重置按钮绑定
ResetFormCommand。 - 保存时名称不能为空。
- 重置时清空名称,并显示
表单已重置。。
ViewModels/MainWindowViewModel.cs 完整答案
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace ProductApp.ViewModels;
public partial class MainWindowViewModel : ViewModelBase
{
[ObservableProperty]
private string productName = "Keyboard";
[ObservableProperty]
private string message = "请输入商品名称,然后点击保存。";
[RelayCommand]
private void Save()
{
string name = ProductName.Trim();
if (name.Length == 0)
{
Message = "商品名称不能为空。";
return;
}
Message = $"保存成功:{name}";
}
[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="520"
Height="340"
Title="MVVM 入门">
<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>App.axaml.cs 检查点
模板生成的 App.axaml.cs 里应该有这段:
desktop.MainWindow = new MainWindow
{
DataContext = new MainWindowViewModel(),
};如果绑定完全不显示,先检查这里。
必须验证的操作
| 操作 | 应该看到 |
|---|---|
| 启动窗口 | 输入框默认显示 Keyboard |
| 点击保存 | 显示 保存成功:Keyboard |
| 清空输入框后点击保存 | 显示 商品名称不能为空。 |
输入 Mouse 后点击保存 | 显示 保存成功:Mouse |
| 点击重置 | 输入框清空,消息显示 表单已重置。 |
如果报错,按这个顺序检查
- 如果
ProductName绑定报错,检查字段是不是[ObservableProperty] private string productName。 - 如果
SaveCommand找不到,检查方法是否写了[RelayCommand] private void Save()。 - 如果
ResetFormCommand找不到,检查方法名是不是ResetForm,不是Reset。 - 如果绑定不显示,检查
App.axaml.cs有没有设置DataContext = new MainWindowViewModel()。 - 如果
ObservableProperty找不到,检查文件顶部有没有using CommunityToolkit.Mvvm.ComponentModel;。 - 如果
RelayCommand找不到,检查文件顶部有没有using CommunityToolkit.Mvvm.Input;。
今天真正要掌握的闭环
MVVM 的第一条闭环是:
ViewModel 暴露 ProductName 和 Message
-> View 用 Binding 显示和修改 ProductName
-> Button 用 Command 触发 SaveCommand
-> Save 方法修改 Message
-> View 自动显示新的 Message这条线跑通后,后面再加入价格、列表、服务类,结构才不会乱。