Skip to Content
Week 07Day 1 - MVVM 入门

Day 1 - MVVM 入门

建议用时:240-270 分钟

你将学会什么

  • 知道 MVVM 为什么要把界面和状态分开。
  • 知道 ViewViewModelModel 各自负责什么。
  • 会使用 Avalonia 模板自带的 ViewModelBase
  • 会用 CommunityToolkit.Mvvm[ObservableProperty] 自动生成可通知界面的属性。
  • 会用 [RelayCommand] 自动生成按钮可绑定的命令。
  • 会让 TextBox 绑定到 ViewModel,并点击按钮执行 ViewModel 方法。

本页开始正式从 code-behind 事件过渡到 MVVM。目标不是马上写复杂架构,而是先跑通一条最小链路:ViewModel 有属性,View 绑定属性,按钮绑定命令。

本页固定顺序

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

学习衔接

上一页学习的是“复盘与巩固”,今天继续学习“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}"

注意命名规则:

字段自动生成的属性
productNameProductName
messageMessage
priceTextPriceText

8. [RelayCommand] 做了什么

本页写的是方法:

[RelayCommand] private void Save() { }

CommunityToolkit.Mvvm 会自动生成一个可绑定的命令属性:

SaveCommand

所以 XAML 里写:

Command="{Binding SaveCommand}"

命名规则:

方法自动生成的命令
SaveSaveCommand
DeleteDeleteCommand
ResetFormResetFormCommand

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 常用写法速查

需求写法作用
ViewMainWindow.axaml只负责界面结构
ViewModelMainWindowViewModel.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 改的是 ProductNameButton 执行的是 SaveCommandSave 方法修改的是 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>

运行后做两次验证:

  1. 输入框清空,点击保存,应该显示 商品名称不能为空。
  2. 输入 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 里的真实属性名
没有设置 DataContextView 找不到 ViewModel检查模板里的 ViewLocator 或窗口 DataContext
[ObservableProperty] 字段命名错生成的属性名不符合预期字段用小驼峰,例如 productName
命令名写错Save 会生成 SaveCommandXAML 绑定 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 中绑定 PriceCountTotal

每日小测

做完本页后,用这 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 商品输入窗口:

  1. TextBox 绑定 ProductName
  2. TextBlock 绑定 Message
  3. 保存按钮绑定 SaveCommand
  4. 重置按钮绑定 ResetFormCommand
  5. 保存时名称不能为空。
  6. 重置时清空名称,并显示 表单已重置。

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
点击重置输入框清空,消息显示 表单已重置。

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

  1. 如果 ProductName 绑定报错,检查字段是不是 [ObservableProperty] private string productName
  2. 如果 SaveCommand 找不到,检查方法是否写了 [RelayCommand] private void Save()
  3. 如果 ResetFormCommand 找不到,检查方法名是不是 ResetForm,不是 Reset
  4. 如果绑定不显示,检查 App.axaml.cs 有没有设置 DataContext = new MainWindowViewModel()
  5. 如果 ObservableProperty 找不到,检查文件顶部有没有 using CommunityToolkit.Mvvm.ComponentModel;
  6. 如果 RelayCommand 找不到,检查文件顶部有没有 using CommunityToolkit.Mvvm.Input;

今天真正要掌握的闭环

MVVM 的第一条闭环是:

ViewModel 暴露 ProductName 和 Message -> View 用 Binding 显示和修改 ProductName -> Button 用 Command 触发 SaveCommand -> Save 方法修改 Message -> View 自动显示新的 Message

这条线跑通后,后面再加入价格、列表、服务类,结构才不会乱。