包 CommunityToolkit.Mvvm
主要用于代码生成,能够为用户减少编写大量的代码,在 WPF 中可以实现 MVVM 设计模式,降低代码复杂度。
引入项目包:
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.1 "/>
要学习 MVVM,需要关注以下类型:
- CommunityToolkit.Mvvm.ComponentModel
- CommunityToolkit.Mvvm.DependencyInjection
- CommunityToolkit.Mvvm.Input
- CommunityToolkit.Mvvm.Messaging
- CommunityToolkit.Mvvm.Messaging.Messages
下面来介绍常用类型的使用方法。
ObservableObject 和 ObservableProperty
ObservableObject
这过实现INotifyPropertyChanged
和INotifyPropertyChanging
接口可观察的对象的基类,它可以用作需要支持属性更改通知的各种对象的起点。
类型 ObservableProperty
是一个属性,允许从批注字段生成可观察属性。 其用途是大大减少定义可观测属性所需的样本量。
首先定义一个 ViewModel 类。
// ViewModel 类需要继承 ObservableObject
public partial class DashboardViewModel : ObservableObject
{
// 写一个字段,下划线开头、驼峰命名
[ObservableProperty]
private int _counter = 0;
public DashboardViewModel()
{
DownloadTextCommand = new AsyncRelayCommand(DownloadText);
}
}
在窗口中定义一个 ViewModel 属性。
笔者演示的是 Page 页面模式,读者使用 Window 窗口也是一样的。
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);
MVVM 会自动为我们创建这个属性的分步类方法的定义,因此我们可以在该属性变化前后出来变化的值。
命令
如果要在点击按钮之后,触发事件,按照 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}" />
也可以写成异步方法。
[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}" />
可以看到,右边的标签并不会因为值变化而反馈到界面上。
但是改成:
public partial class DashboardViewModel : ObservableObject
{
[NotifyPropertyChangedFor(nameof(MapValue))]
[ObservableProperty]
private int _counter = 0;
public int MapValue
{
get { return _counter; }
}
[RelayCommand]
private void OnCounterIncrement()
{
Counter++;
}
}
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 命令的。既然无效,也找不到资料,那就作罢。
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}" />
如果点击后,进行模型验证失败了,那么对应的输入框会有一些样式,例如红色边框。但是大家往往需要定制样式,因此还需要做大量工作。
文章评论