简化WPF开发:CommunityToolkit属性绑定与命令声明实战
CommunityToolKit的作用是简化了WPF中依赖属性,路由命令的声明,提高了编码效率。
该框架只支持.NET 4.6及以后的版本。
在Nuget中搜索CommunityToolkit.Mvvm并安装。
依赖属性
依赖属性的写法在CommunityToolKit中被简化为了一个特性ObservableProperty,值得注意的是使用这个特性以及后面的特性的前提是需要将类设置为部分类,并继承ObservableObject,添加部分类是因为框架会自动生成一个同样的类。
public partial class Employee : ObservableObject
{[ObservableProperty]public string name = "张三";
}
这样在XAML中就可以直接拿来使用。
<TextBlock Text="{Binding Name, StringFormat='姓名:{0}'}"></TextBlock>
我们可以看到在XAML中的Name是大写字母,而声明的字段却是小写的,这是因为该控价自动生成了一个Name的属性,并封装了很多方法。
路由命令
路由命令的声明也只需声明一个特性RelayCommand即可。
[RelayCommand]public void ChangeName(){Name = "李四";}
XAML:
<Button Content="修改名字" Command="{Binding ChangeNameCommand}"></Button>
在XAML中绑定的名字也需要注意,在声明的方法后面自动添加一个Command。
属性联动
有时候当界面中的一个属性发生改变时会影响其他控件的状态,这时候就会用到属性联动,如果使用的是Winform框架,就需要在控件的Change事件中编写相关代码,而使用该框架只需要添加一个NotifyPropertyChangedFor特性。
[ObservableProperty][NotifyPropertyChangedFor(nameof(msg))]public string name = "张三";[RelayCommand]public void ChangeName(){Name = "李四";}[ObservableProperty]public string sex = "男";public string msg => $"姓名:{Name},性别:{Sex}";
上面的ChangeName方法修改了Name,如果不使用NotifyPropertyChangedFor特性,msg不会跟着变化,它的参数是需要通知的属性的名称。
XAML:
<TextBlock Text="{Binding Name, StringFormat='姓名:{0}'}"></TextBlock>
<TextBlock Text="{Binding Sex, StringFormat='性别:{0}'}"></TextBlock>
<TextBlock Text="{Binding msg}"></TextBlock>
<Button Content="修改名字" Command="{Binding ChangeNameCommand}"></Button>
路由命令扩展
绑定了路由命令的控件可以在不能使用的时候变为不可用状态,这个在CommunityToolKit中也得到了支持,只需使用NotifyCanExecuteChangedFor和RelayCommand(CanExecute = flag),一个是在修改了该属性后给指定的方法发出通知,一个是控制该方法是否可执行。
[ObservableProperty]public string sex = "男";[ObservableProperty][NotifyCanExecuteChangedFor(nameof(ChangeSexCommand))]public bool canChangeSex = false;[RelayCommand(CanExecute = nameof(CanChangeSex))]public void ChangeSex(){Sex = "女";}
XAML:
<TextBlock Text="{Binding Sex, StringFormat='性别:{0}'}"></TextBlock>
<CheckBox IsChecked="{Binding CanChangeSex}">是否可以修改性别</CheckBox>
<Button Content="修改性别" Command="{Binding ChangeSexCommand}"></Button>
规则验证
规则验证在开发过程中经常会用到,比如在填写一个表单的时候常常会给各种控件添加验证条件,WPF的规则控件使用起来很复杂,在CommunityToolKit中简化了写法。
比如在验证年龄的只需添加Required和Range注解就可以给这个字段添加上验证逻辑,错误提示信息。
[ObservableProperty][Required(ErrorMessage = "请输入年龄")][Range(0, 200, ErrorMessage = "年龄必须在0到200之间")]public string? _age;[ObservableProperty]public string? errorMsg;[RelayCommand]void Submit(){ValidateAllProperties();ErrorMsg = string.Empty;if (HasErrors){ErrorMsg = string.Join(Environment.NewLine, GetErrors());return;}}
errorMsg是错误信息,Submit是提交按钮的路由命令,HasErrors是ObservableValidator的字段用来标记是否出现了错误,GetErrors()是用来获取错误信息的方法,这里用换行符隔开了每条信息。
XAML:
<StackPanel Orientation="Horizontal"><TextBlock Text="年龄:"></TextBlock><TextBox Text="{Binding Age}" Width="200"></TextBox>
</StackPanel><TextBlock Text="{Binding ErrorMsg}"></TextBlock>
<Button Content="提交" Command="{Binding SubmitCommand}"></Button>
在XAML中对年龄控件,错误信息文本,提交按钮进行了绑定。
如果需要进行复杂的验证,只需使用RegularExpression进行正则运算即可;还有就是默认的方式是在提交的时候才会响应验证,如果希望在控件失去焦点时立即验证,需要像这样新建一个部分方法。
partial void OnAgeChanged(string? value){ValidateProperty(value, nameof(Age));}
信使
信使(Messenger)用于在不同类之间进行数据传递,实现了松耦合的消息通信,常用于ViewModel之间的通信,ViewModel与View的通信,跨层数据传递。
当一个类需要修改另一个类的属性时,信使可通过消息传递机制实现间接操作,避免直接引用依赖。
一般我们使用的是弱引用机制(WeakReferenceMessenger),因为使用完后内存会自动回收,无需手动注销。
为了防止消息冲突,可以使用令牌Token标记不同的通道类型。
单向通信
要使用信道,同样需要继承ObservableObject并将类改为分部类,使用WeakReferenceMessenger的Send方法即可发送消息。
public partial class MessageSender : ObservableObject{[ObservableProperty]public string? _message = "发送的消息";[RelayCommand]public void Send(){// 发送消息WeakReferenceMessenger.Default.Send<ValueChangedMessage<string>>(new ValueChangedMessage<string>(_message));}}
下面是接受消息的类,接收消息时使用的是WeakReferenceMessenger的Register方法,注意这里是写在构造方法中。
public partial class MessageReceiver : ObservableObject
{[ObservableProperty]public string? _message = "接收的消息";public MessageReceiver(){// 接收消息WeakReferenceMessenger.Default.Register<ValueChangedMessage<string>> (this, (m, n) => {this.Message = n.Value;});}
}
XAML:
<StackPanel Orientation="Horizontal"><StackPanel Width="400"><TextBlock Text="发送消息:"></TextBlock><TextBox Text="{Binding Sender.Message}"></TextBox><Button Content="发送" Command="{Binding Sender.SendCommand}"></Button></StackPanel><StackPanel Width="400"><TextBlock Text="接收消息:"></TextBlock><TextBlock Width="400" Text="{Binding Receiver.Message}"></TextBlock></StackPanel>
</StackPanel>
以上是使用了ValueChangedMessage来发送简单的消息。为了避免消息的冲突还可以使用Token,由于消息采用的是多播机制,这样可以避免接收到无关的消息。
带Token的通信
[RelayCommand]public void SendWithToken(){// 发送消息WeakReferenceMessenger.Default.Send<ValueChangedMessage<string>, string>(new ValueChangedMessage<string>(_message), "token1");}
在构造方法里的接收:
// 接收带token的消息,只有当发送和接收方的token一致时,消息才会被接收WeakReferenceMessenger.Default.Register<ValueChangedMessage<string>, string>(this, "token1", (m, n) => {this.Message = n.Value;});
XAML:
<StackPanel Orientation="Horizontal"><StackPanel Width="400"><TextBlock Text="发送消息:"></TextBlock><TextBox Text="{Binding Sender.Message}"></TextBox><Button Content="发送带Token的消息" Command="{Binding Sender.SendWithTokenCommand}"></Button></StackPanel><StackPanel Width="400"><TextBlock Text="接收消息:"></TextBlock><TextBlock Width="400" Text="{Binding Receiver.Message}"></TextBlock></StackPanel>
</StackPanel>
双向通信
如果还需要响应消息就需要用到RequestMessage,但这个类无法将消息内容带过去,所以我自定义了一个类去继承它,并添加了一个Content属性来承载发送的消息。
public class MyRequestMessage<T> : RequestMessage<T>
{public MyRequestMessage(string content){this.Content = content;}public string? Content { get; set; }
}
发送消息:
[RelayCommand]public void SendAndResponse(){var response = WeakReferenceMessenger.Default.Send(new MyRequestMessage<string>(this.Message??""));if (response.HasReceivedResponse){this.Message = response.Response;}}
接受消息:
// 接收消息并回复WeakReferenceMessenger.Default.Register<MyRequestMessage<string>>(this, (m, n) => {this.Message = n.Content;n.Reply($"消息[{n.Content?.ToString()}]已收到");});
XAML:
<StackPanel Orientation="Horizontal"><StackPanel Width="400"><TextBlock Text="发送消息:"></TextBlock><TextBox Text="{Binding Sender.Message}"></TextBox><Button Content="发送有会有回复的消息" Command="{Binding Sender.SendAndResponseCommand}"></Button></StackPanel><StackPanel Width="400"><TextBlock Text="接收消息:"></TextBlock><TextBlock Width="400" Text="{Binding Receiver.Message}"></TextBlock></StackPanel>
</StackPanel>
属性发生变更时通信
最后是在属性发生变更时发送消息,用到的是PropertyChangedMessage,它需要与目标属性的部分方法OnPropertyChanged进行联动,这样依赖只要这个属性绑定的控件的值发生改变,就会立即发送消息。
[ObservableProperty]public string? _message = "发送的消息";partial void OnMessageChanged(string? value){WeakReferenceMessenger.Default.Send<PropertyChangedMessage<string>>(new PropertyChangedMessage<string>(this, nameof(Message), default, value));}
接收消息
// 接收属性变化后发送的消息WeakReferenceMessenger.Default.Register<PropertyChangedMessage<string>>(this, (m, n) => {this.Message = n.NewValue;});
XAML:
<StackPanel Orientation="Horizontal"><StackPanel Width="400"><TextBlock Text="发送消息:"></TextBlock><TextBox Text="{Binding Sender.Message}"></TextBox></StackPanel><StackPanel Width="400"><TextBlock Text="接收消息:"></TextBlock><TextBlock Width="400" Text="{Binding Receiver.Message}"></TextBlock></StackPanel>
</StackPanel>