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 的基类。

  1. 在项目中创建一个名为 ViewModels 的新文件夹。
  2. 在该文件夹下,创建一个名为 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。

  1. 在 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 的数据流动闭环:

  1. 用户操作: 用户双击图片 (View)。
  2. 事件触发: PointerPressed 事件在 MainView.axaml.cs 中被捕获。
  3. 命令执行: 代码调用 DataContext (即 MainViewModel) 的 SideMenuResizeCommand。
  4. 数据更新: MainViewModel 中的 SideMenuResize 方法被执行,SideMenuExpanded 属性的值发生改变。
  5. 变更通知: 因为 SideMenuExpanded 是一个 ObservableProperty,它会自动发出“我变了”的通知。
  6. UI 自动更新: MainView.axaml 中所有绑定到 SideMenuExpanded 的UI元素(图片和文字)收到通知,并自动更新它们的 IsVisible 状态,从而实现了菜单的折叠/展开效果。
posted @ 2025-09-10 08:49  Gordon管  阅读(38)  评论(0)    收藏  举报