WPF MVVM 的使用

2023年11月2日 1386点热度 0人点赞 0条评论
内容纲要

CommunityToolkit.Mvvm 主要用于代码生成,能够为用户减少编写大量的代码,在 WPF 中可以实现 MVVM 设计模式,降低代码复杂度。

引入项目包:

    <PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.1 "/>

要学习 MVVM,需要关注以下类型:

下面来介绍常用类型的使用方法。

ObservableObject 和 ObservableProperty

ObservableObject 这过实现INotifyPropertyChangedINotifyPropertyChanging接口可观察的对象的基类,它可以用作需要支持属性更改通知的各种对象的起点。

类型 ObservableProperty 是一个属性,允许从批注字段生成可观察属性。 其用途是大大减少定义可观测属性所需的样本量。

首先定义一个 ViewModel 类。

    // ViewModel 类需要继承 ObservableObject
    public partial class DashboardViewModel : ObservableObject
    {
        // 写一个字段,下划线开头、驼峰命名
        [ObservableProperty]
        private int _counter = 0;

        public DashboardViewModel()
        {
            DownloadTextCommand = new AsyncRelayCommand(DownloadText);
        }
    }

在窗口中定义一个 ViewModel 属性。

笔者演示的是 Page 页面模式,读者使用 Window 窗口也是一样的。

file

        public DashboardViewModel ViewModel { get; }

属性的名称可以随意,不一定使用 ViewModel。

然后在 xaml 文件中绑定属性:

        <TextBlock
            Grid.Column="1"
            Margin="12,0,0,0"
            VerticalAlignment="Center"
            Text="{Binding ViewModel.Counter, Mode=OneWay}" />

我们定义的是 _counter,但是 MVVM 框架会自动给我们生成一些代码。

下面只是简化示例代码,实际情况复杂得多。

public int Counter
{
    get => _counter;
    set
    {
        if (!EqualityComparer<int?>.Default.Equals(_counter, value))
        {
            OnCounterChanging(value);
            OnCounterChanging(default, value);
            _counter = value;
            OnCounterChanged(value);
            OnCounterChanged(default, value);
        }
    }
}

partial void OnCounterChanging(int value);
partial void OnCounterChanging(int value);

file

MVVM 会自动为我们创建这个属性的分步类方法的定义,因此我们可以在该属性变化前后出来变化的值。

file

命令

如果要在点击按钮之后,触发事件,按照 WPF 的写法需要定义一个方法。如果使用 MVVM,按钮事件可以看作一个命令,整个过程会更加简单。

在 ViewModel 中随便定义一个函数,然后加上 [RelayCommand] 即可。

    public partial class DashboardViewModel : ObservableObject
    {
        // 写一个字段,下划线开头、驼峰命名
        [ObservableProperty]
        private int _counter = 0;

        // 依然写 private
        [RelayCommand]
        private void OnCounterIncrement()
        {
            // 注意,要使用生成的属性,而不是使用 _counter;
            Counter++;
        }
    }

MVVM 会自动生成一个名为 CounterIncrementCommand 的命令函数。

然后 xaml 中绑定命令:

        <ui:Button
            Grid.Column="0"
            Command="{Binding ViewModel.CounterIncrementCommand, Mode=OneWay}"
            Content="Click me!"
            Icon="Fluent24" />
        <TextBlock
            Grid.Column="1"
            Margin="12,0,0,0"
            VerticalAlignment="Center"
            Text="{Binding ViewModel.Counter, Mode=OneWay}" />

file

也可以写成异步方法。

        [RelayCommand]
        private async Task OnCounterIncrement()
        {
            Counter++;
            await Task.CompletedTask;
        }

另外 RelayCommand 属性可以设置启用禁用、是否可以并发等配置。

此外,也可以使用手动设置的命令属性绑定方法:

    public partial class DashboardViewModel : ObservableObject
    {
        [ObservableProperty]
        private int _counter = 0;

        public DashboardViewModel()
        {
            UpdateCounterCommand = new RelayCommand(UpdateCounter);
        }

        public ICommand UpdateCounterCommand { get; }

        private void UpdateCounter()
        {
            Counter++;
        }
    }
        public DashboardViewModel()
        {
            UpdateCounterCommand = new AsyncRelayCommand(UpdateCounter);
        }

        public ICommand UpdateCounterCommand { get; }

        private async Task UpdateCounter()
        {
            Counter++;
        }

xaml 中绑定命令:

        <ui:Button
            Grid.Column="0"
            Command="{Binding ViewModel.UpdateCounterCommand, Mode=OneWay}"
            Content="Click me!"
            Icon="Fluent24" />
        <TextBlock
            Grid.Column="1"
            Margin="12,0,0,0"
            VerticalAlignment="Center"
            Text="{Binding ViewModel.Counter, Mode=OneWay}" />

属性变化通知

MVVM 中有个 [NotifyPropertyChangedFor] 特性,到底有什么用呢?笔者翻了很久都没有找到什么资料,只能自己一点点测试。

[NotifyPropertyChangedFor] 可以给你减少编写通知代码使用的。

一般情况下,WPF 控件直接绑定属性是无效的。

    public partial class DashboardViewModel : ObservableObject
    {
        [ObservableProperty]
        private int _counter = 0;

        public int MapValue
        {
            get { return _counter; }
        }

        [RelayCommand]
        private void OnCounterIncrement()
        {
            Counter++;
            MapValue++;
        }

        /*
        // 或者下面的代码:
        public int MapValue
        {
            get { return _counter; }
        }

        [RelayCommand]
        private void OnCounterIncrement()
        {
            Counter++;
        }
        */
    }

        <ui:Button
            Grid.Column="0"
            Command="{Binding ViewModel.CounterIncrementCommand, Mode=OneWay}"
            Content="Click me!"
            Icon="Fluent24" />

        <TextBlock
            Grid.Column="1"
            Margin="12,0,0,0"
            VerticalAlignment="Center"
            Text="{Binding ViewModel.Counter, Mode=OneWay}" />

        <Label Grid.Column="2" Margin="60,0,0,0" Content="{Binding ViewModel.MapValue, Mode=OneWay}" />

file

可以看到,右边的标签并不会因为值变化而反馈到界面上。

但是改成:

    public partial class DashboardViewModel : ObservableObject
    {
        [NotifyPropertyChangedFor(nameof(MapValue))]
        [ObservableProperty]
        private int _counter = 0;

        public int MapValue
        {
            get { return _counter; }
        }

        [RelayCommand]
        private void OnCounterIncrement()
        {
            Counter++;
        }
    }

file

Counter 属性变化的时候,会通知绑定了 MapValue 的属性值也变化。

这种适合用于 MapValue 只用于显示,不需要被界面直接修改的场景。

比如说,输入值,计算生成 x 的平方。

        [NotifyPropertyChangedFor(nameof(Result))]
        [ObservableProperty]
        private int _x = 0;

        public int Result
        {
            get { return _x * _x; }
        }

当 x 值变化时,会通知控件更新 Result 的值,即重新调用 get 。对于 Result ,set 是没有意义的。

多个属性也可以 [NotifyPropertyChangedFor] 到同一个属性。

        [NotifyPropertyChangedFor(nameof(MapValue))]
        [ObservableProperty]
        private int _x = 0;

        [NotifyPropertyChangedFor(nameof(MapValue))]
        [ObservableProperty]
        private int _y = 0;

        public int MapValue
        {
            get { return _x * _y; }
        }

当 X 或 Y 变化时,界面会重新计算结果。

另外,可以在属性变化时,使用 [NotifyCanExecuteChangedFor] 自动调用命令。

但是下面代码不会有任何效果,TestCommand 不会被执行。

    public partial class DashboardViewModel : ObservableObject
    {
        [NotifyCanExecuteChangedFor(nameof(TestCommand))]
        [ObservableProperty]
        private int _counter = 0;

        [RelayCommand]
        private void OnCounterIncrement()
        {
            Counter++;
        }

        // TestCommand => testCommand ??= new RelayCommand(OnTest);
        [RelayCommand]
        private void OnTest()
        {
            Counter++;
        }

因为从官网以及其他地方的资料,实在有限,笔者也不知道到底怎么使用,在官方的 CommunityToolkit / MVVM-Samples 仓库里面也没有 [NotifyCanExecuteChangedFor] 的用法。

但是看 MVVM 生成的代码,理论上是会执行 TestCommand 命令的。既然无效,也找不到资料,那就作罢。

file

ObservableValidator

进行模型验证的类型,因为 ObservableValidator 也是抽象类,因此不能跟 ObservableObject 混用。

定义一个新的 ViewModel:

    public partial class FormViewModel: ObservableValidator
    {
        private string name;

        [Required]
        [MinLength(2)]
        [MaxLength(5)]
        public string Name
        {
            get => name;
            set => SetProperty(ref name, value, true);
        }

        [RelayCommand]
        private void Check()
        {
            ValidateAllProperties();

            if (HasErrors)
            {
                return;
            }
        }
    }
    public partial class DashboardPage : INavigableView<DashboardViewModel>
    {
        public DashboardViewModel ViewModel { get; }

        public FormViewModel Form { get; }

        public DashboardPage(DashboardViewModel viewModel)
        {
            ViewModel = viewModel;
            DataContext = this;
            Form = new FormViewModel();
            InitializeComponent();
        }
    }

在界面上使用:

        <ui:Button
            Grid.Column="0"
            Command="{Binding Form.CheckCommand, Mode=OneWay}"
            Content="Click me!"
            Icon="Fluent24" />

        <TextBox
            Grid.Column="2"
            Margin="12,0,0,0"
            VerticalAlignment="Center"
            Width="200"
            Height="30"
            BorderThickness="1,1,1,1"
            Background="Blue"
            Text="{Binding Form.Name, Mode=TwoWay}" />

file

如果点击后,进行模型验证失败了,那么对应的输入框会有一些样式,例如红色边框。但是大家往往需要定制样式,因此还需要做大量工作。

痴者工良

高级程序员劝退师

文章评论