Avalonia 学习笔记03. View Model Basics(视图模型基础)(转载)
原文链接:Avalonia 学习笔记03. View Model Basics(视图模型基础) - simonoct - 博客园
前置准备:在开始前,请确保已经通过 NuGet 包管理器为你的项目添加了 CommunityToolkit.Mvvm 包。
这一节的核心目标是引入 MVVM 设计模式中的 ViewModel(视图模型)层,实现UI(视图)与数据和逻辑(视图模型)的分离,并通过数据绑定(Data Binding)将它们连接起来。
核心概念:什么是 MVVM?
MVVM 是一种软件架构模式,旨在将用户界面(View)的开发与业务逻辑和数据(Model)的开发分离开来。
- View (视图):用户所看到的界面。在 Avalonia 中,这通常是 .axaml 文件。它只负责“显示”数据和将用户的操作(如点击)传递出去,自身不包含任何业务逻辑。
- Model (模型):应用程序的核心数据和业务逻辑。
- ViewModel (视图模型):作为 View 和 Model 之间的桥梁。它从 Model 中获取数据,并将其转换为 View 可以直接显示的格式。同时,它包含 View 所需的命令(Commands),用于响应用户的操作。View "绑定" 到 ViewModel 的属性和命令上。
这种模式最大的好处是数据驱动UI。我们只需要在 ViewModel 中改变数据,UI 就会自动更新,反之亦然。这使得代码更易于维护、测试和扩展。
3.1 创建基础 ViewModel
为了代码的整洁和复用,我们首先创建一个所有 ViewModel 的基类。
- 在项目中创建一个名为 ViewModels 的新文件夹。
- 在该文件夹下,创建一个名为 ViewModelBase.cs 的类。
ViewModels/ViewModelBase.cs
using CommunityToolkit.Mvvm.ComponentModel; namespace AvaloniaApplication2.ViewModels; // 这个类将作为项目中所有 ViewModel 的基类 public class ViewModelBase : ObservableObject { }
- ObservableObject 是 CommunityToolkit.Mvvm 包提供的一个核心类。
- 任何继承自 ObservableObject 的类,都自动获得了“通知”能力。当它的某个属性发生变化时,它能够主动发出通知。UI界面上绑定了该属性的元素在收到通知后,就会自动更新其显示内容。
- 这背后实现的是一个叫做 INotifyPropertyChanged 的接口,但 ObservableObject 已经为我们处理了所有复杂的细节。
3.2 创建主窗口的 ViewModel
接下来,我们为 MainView 创建一个专属的 ViewModel。
- 在 ViewModels 文件夹下,创建一个名为 MainViewModel.cs 的类。
(注意:您的笔记框架中文件名为 ViewModelModel.cs,根据类名 MainViewModel,这里应该是 MainViewModel.cs)
ViewModels/MainViewModel.cs
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; namespace AvaloniaApplication2.ViewModels; // 'partial' 关键字是必须的,它允许源代码生成器在编译时为这个类添加代码 public partial class MainViewModel : ViewModelBase { // [ObservableProperty] 是一个“特性”,它会自动为一个私有字段生成一个公共属性 // 在这里,它会为 _sideMenuExpanded 字段生成一个名为 SideMenuExpanded 的公共属性 // 当 SideMenuExpanded 属性的值被改变时,它会自动发出通知 [ObservableProperty] private bool _sideMenuExpanded = true; // [RelayCommand] 特性将一个方法包装成一个可以被UI绑定的命令 // 在这里,它会创建一个名为 SideMenuResizeCommand 的命令 [RelayCommand] private void SideMenuResize() { // 这行代码会切换布尔值的状态(true -> false, false -> true) SideMenuExpanded = !SideMenuExpanded; } }
- partial class: 这个关键字允许一个类的定义被分割到多个文件中。在这里,它允许 CommunityToolkit 的源代码生成器(Source Generator)在编译过程中“注入”代码到 MainViewModel 类中,比如自动生成 SideMenuExpanded 属性和 SideMenuResizeCommand 命令的完整实现。
- [ObservableProperty]: 这是一个极其方便的特性。你只需在私有字段(如 _sideMenuExpanded)上标记它,它就会自动生成一个公共属性(首字母大写,即 SideMenuExpanded),并包含所有必要的通知逻辑。我们不再需要手动编写 get 和 set 以及调用通知事件。
- [RelayCommand]: 这个特性将一个普通方法(如 SideMenuResize)转换成一个实现了 ICommand 接口的命令对象(SideMenuResizeCommand)。在 MVVM 中,我们倾向于使用命令而不是事件来处理用户交互,因为这能让 ViewModel 与 View 完全解耦(ViewModel 不知道也不关心是哪个按钮触发了这个命令)。
3.3 将 View 与 ViewModel 连接起来
现在我们有了 View 和 ViewModel,需要将它们“绑定”在一起。
3.3.1 在应用启动时设置 DataContext
我们在应用启动时,将 MainViewModel 的一个实例指定为 MainView 的“数据上下文”(DataContext)。DataContext 就像是为这个 View 指定了一个专属的“数据管家”。
App.axaml.cs
using Avalonia; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; // 引入 ViewModels 命名空间 using AvaloniaApplication2.ViewModels; namespace AvaloniaApplication2; public partial class App : Application { public override void Initialize() { AvaloniaXamlLoader.Load(this); } public override void OnFrameworkInitializationCompleted() { if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { desktop.MainWindow = new MainView { // 为主窗口(MainView)设置数据上下文(DataContext) // 这样 MainView 及其所有子控件都能访问到 MainViewModel 里的数据 DataContext = new MainViewModel() }; } base.OnFrameworkInitializationCompleted(); } }
3.3.2 在 XAML 中进行数据绑定
设置好 DataContext后,我们就可以在 MainView.axaml 中使用 {Binding} 语法来访问 MainViewModel 中的属性和命令了。
MainView.axaml,由MainWindow.axaml改名而来。
按照惯例,你通常可以把视图模型命名为 MainWindowViewModel,或者你也可以把 MainWindow 重命名为 MainView(通常不带 "Window" 后缀)。这个教程作者习惯叫MainViewModel,所以把MainWindow.axaml改名为MainView.axaml实现命名上统一。
首先,我们需要在 Window 标签中声明 ViewModel 的命名空间和数据类型,这能为我们提供更好的智能提示和编译时类型检查。
<Window ... xmlns:vm="clr-namespace:AvaloniaApplication2.ViewModels" x:DataType="vm:MainViewModel" ...> <!-- 这个 DataContext 只在设计器(预览窗口)中生效,让我们可以在不运行程序的情况下看到绑定效果 --> <Design.DataContext> <vm:MainViewModel></vm:MainViewModel> </Design.DataContext> <!-- ... 省略部分代码 ... -->
<!-- ... --> <StackPanel Spacing="12"> <!-- 将 PointerPressed 事件连接到代码隐藏文件中的处理方法 --> <!-- IsVisible 属性绑定到 SideMenuExpanded。当其为 true 时,图片可见 --> <Image PointerPressed="InputElement_OnPointerPressed" Source="{SvgImage Assets/Images/logo.svg}" Width="220" IsVisible="{Binding SideMenuExpanded}"></Image> <!-- IsVisible 属性绑定到 SideMenuExpanded 的反值(!)。当其为 false 时,图片可见 --> <Image PointerPressed="InputElement_OnPointerPressed" Source="{SvgImage Assets/Images/icon.svg}" Width="22" IsVisible="{Binding !SideMenuExpanded}"></Image> <!-- ... --> <Button HorizontalAlignment="Stretch"> <StackPanel Orientation="Horizontal"> <Label Classes="icon" Content=""></Label> <!-- 这个 Label 的可见性同样由 SideMenuExpanded 控制 --> <Label Classes="akko" Content="Home" IsVisible="{Binding SideMenuExpanded}"></Label> </StackPanel> </Button> <!-- ... 其他按钮的 Label 也做同样绑定 ... --> </StackPanel> <!-- ... -->
- xmlns:vm="...": 定义一个XML命名空间别名 vm,指向我们的 ViewModels 文件夹。
- x:DataType="vm:MainViewModel": 明确告诉编译器和设计器,这个 View 的 DataContext 是 MainViewModel 类型的。
- d:DataContext: d: 代表 design-time(设计时)。这个设置仅用于 IDE 的预览窗口,它让预览器也创建一个 MainViewModel 实例,这样我们就能实时看到绑定后的UI效果。
- {Binding SideMenuExpanded}: 这是核心的数据绑定语法。它将 IsVisible 属性与 MainViewModel 中的 SideMenuExpanded 公共属性连接起来。
- {Binding !SideMenuExpanded}: Avalonia 的绑定引擎支持简单的逻辑运算,! 表示取反。所以这个图片会在 SideMenuExpanded 为 false 时显示。
3.3.3 连接用户操作与 ViewModel 命令
最后,我们需要将用户的双击操作,连接到 MainViewModel 中的 SideMenuResizeCommand 命令。这是在 MainView.axaml.cs(代码隐藏文件)中完成的。
MainView.axaml.cs
using Avalonia.Controls; using Avalonia.Input; using AvaloniaApplication2.ViewModels; namespace AvaloniaApplication2; public partial class MainView : Window { public MainView() { InitializeComponent(); } // 这个方法在 XAML 中通过 PointerPressed="InputElement_OnPointerPressed" 绑定 private void InputElement_OnPointerPressed(object? sender, PointerPressedEventArgs e) { // e.ClickCount 可以获取鼠标点击次数,我们只关心双击 if (e.ClickCount != 2) return; // (DataContext as MainViewModel) 尝试将 DataContext 转换成 MainViewModel 类型 // ?. 是空条件运算符,如果转换失败(DataContext不是MainViewModel),则不执行后面的代码,避免程序崩溃 // .Execute(null) 执行 ViewModel 中的命令 (DataContext as MainViewModel)?.SideMenuResizeCommand.Execute(null); } }
虽然我们尽量避免在代码隐藏文件中写逻辑,但这种将 View 的特定事件(如 PointerPressed)“翻译”成调用 ViewModel 命令的代码是完全可以接受的。它充当了一个干净的“粘合剂”,并且没有将任何业务逻辑耦合到 View 中。
总结
通过以上步骤,我们完成了一个完整的 View -> ViewModel -> View 的数据流动闭环:
- 用户操作: 用户双击图片 (View)。
- 事件触发: PointerPressed 事件在 MainView.axaml.cs 中被捕获。
- 命令执行: 代码调用 DataContext (即 MainViewModel) 的 SideMenuResizeCommand。
- 数据更新: MainViewModel 中的 SideMenuResize 方法被执行,SideMenuExpanded 属性的值发生改变。
- 变更通知: 因为 SideMenuExpanded 是一个 ObservableProperty,它会自动发出“我变了”的通知。
- UI 自动更新: MainView.axaml 中所有绑定到 SideMenuExpanded 的UI元素(图片和文字)收到通知,并自动更新它们的 IsVisible 状态,从而实现了菜单的折叠/展开效果。

浙公网安备 33010602011771号