实例说明
本文以一个简单的小实例,说明如何使用WPF的MVVM模式。
窗体上有两个文本框和一个按钮,当两个文本框都不为空时使按钮可用,否则不可用。点击按钮后,消息框显示两个文本框里的内容。
原生库
(1)Model
public class PersonModel {
public string FirstName { get; set; }
public string LastName { get; set; }
}
(2)View
<Window x:Class="TestMvvm.Views.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:TestMvvm.Views"
mc:Ignorable="d"
Title="MainWindow" SizeToContent="WidthAndHeight">
<Grid>
<StackPanel Margin="10">
<TextBox Text="{Binding FirstName,UpdateSourceTrigger=PropertyChanged}" Margin="0 0 0 5" Width="150" Height="30"/>
<TextBox Text="{Binding LastName,UpdateSourceTrigger=PropertyChanged}" Margin="0 0 0 5" Width="150" Height="30"/>
<Button Content="提交" Command="{Binding SubmitCommand}" Width="150" Height="30"/>
</StackPanel>
</Grid>
</Window>
- 按钮命令的绑定和文本框文本的绑定类似。
- 设置UpdateSourceTrigger=PropertyChanged,保证文本框内容发生改变马上更新ViewModel中的数据。
(3)ViewModel
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Input;
using TestMvvm.Models;
namespace TestMvvm.ViewModels {
public class PersonViewModel : INotifyPropertyChanged {
public PersonViewModel() {
_person = new PersonModel();
_submitCommand = new MyCommand(ShowSubmitMessage, EnableSubmit);
}
#region 界面绑定数据定义
private PersonModel _person;
public string FirstName {
get { return _person.FirstName; }
set {
if (_person.FirstName != value) {
_person.FirstName = value;
OnPropertyChanged();
SubmitCommand.RaiseCanExecuteChange();
}
}
}
public string LastName {
get { return _person.LastName; }
set {
if (_person.LastName != value) {
_person.LastName = value;
OnPropertyChanged();
SubmitCommand.RaiseCanExecuteChange();
}
}
}
#endregion
#region 命令定义
private MyCommand _submitCommand;
public MyCommand SubmitCommand {
get {
return _submitCommand;
}
set {
_submitCommand = value;
OnPropertyChanged();
}
}
#endregion
#region 业务处理逻辑,程序的核心
public void ShowSubmitMessage(object parameter) {
MessageBox.Show($"FirstName: {FirstName},LastName: {LastName}");
}
public bool EnableSubmit(object parameter) {
if (FirstName != null && !string.IsNullOrWhiteSpace(FirstName)
&& LastName != null && !string.IsNullOrWhiteSpace(LastName))
return true;
return false;
}
#endregion
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string propertyName = null) {
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
/// <summary>
/// 自定义命令类
/// </summary>
public class MyCommand : ICommand {
private Action<object> execute;
private Func<object, bool> canExecute;
public MyCommand(Action<object> execute, Func<object, bool> canExecute = null) {
this.execute = execute;
this.canExecute = canExecute;
}
public event EventHandler CanExecuteChanged;
public bool CanExecute(object parameter) {
if (canExecute != null)
return canExecute(parameter);
return true;
}
public void Execute(object parameter) {
execute?.Invoke(parameter);
}
public void RaiseCanExecuteChange() {
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
}
}
- 当属性值发生变化时,调用RaiseCanExecuteChange()函数,触发CanExecute()函数,从而更新按钮的可用状态。
- 用属性对命令进行包装,可实现命令绑定和动态变化;在大多数情况下,一个按钮的逻辑基本是固定的,这里的做法可能略显过度,可以声明一个只读属性。
private MyCommand _submitCommand;
public MyCommand SubmitCommand => _submitCommand;
- 这里为了演示CanExecute()的作用,实际上可以使用多值绑定的方式更新按钮的可用状态,这样可以保证UI逻辑更加紧凑。
- ViewModel文件中的内容较杂,可以使用#region来分块整理。
(4)在View.xaml的后台代码中绑定ViewModel
using System.Windows;
using TestMvvm.ViewModels;
namespace TestMvvm.Views {
public partial class MainWindow : Window {
public MainWindow() {
InitializeComponent();
// 绑定ViewModel
this.DataContext = new PersonViewModel();
}
}
}
- 依旧采用最经典的DataContext方式进行绑定。
CommunityToolkit.Mvvm库
下面使用比较主流的CommunityToolkit.Mvvm库改造上面的例子,重点依然集中在数据和命令绑定上。
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System.Windows;
using TestMvvm.Models;
namespace TestMvvm.ViewModels {
public class PersonViewModel : ObservableObject {
public PersonViewModel() {
_person = new PersonModel();
}
#region 界面绑定数据定义
private PersonModel _person;
public string FirstName {
get => _person.FirstName;
set {
SetProperty(_person.FirstName, value, _person, (u, n) => u.FirstName = n);
SubmitCommand.NotifyCanExecuteChanged();
}
}
public string LastName {
get => _person.LastName;
set {
SetProperty(_person.LastName, value, _person, (u, n) => u.LastName = n);
SubmitCommand.NotifyCanExecuteChanged();
}
}
#endregion
#region 命令定义
private RelayCommand<object> _submitCommand;
public RelayCommand<object> SubmitCommand {
get {
if (_submitCommand == null) {
_submitCommand = new RelayCommand<object>(ShowSubmitMessage, (p) => {
if (FirstName != null && !string.IsNullOrWhiteSpace(FirstName) &&
LastName != null && !string.IsNullOrWhiteSpace(LastName))
return true;
return false;
});
}
return _submitCommand;
}
}
#endregion
#region 业务处理逻辑,程序的核心
public void ShowSubmitMessage(object parameter) {
MessageBox.Show($"FirstName: {FirstName},LastName: {LastName}");
}
#endregion
}
}
从以上代码可以看到,相比采用原生的数据和命令绑定,使用CommunityToolkit.Mvvm库后代码量可以进一步下降,且程序的可读性更好,有以下几点需要重点说明下:
- ViewModel类继承自ObservableObject类,在属性set方法中直接调用SetProperty()方法更新属性值,就可以通知View上的绑定目标刷新。
- 本文中SetProperty()方法的最后一个参数是Action类型,主要用于将value赋值给属性时的数据转换或者验证。
- RelayCommand是一个泛型命令类,其泛型参数表示命令的参数类型,第一个参数指定命令的Execute()逻辑,第二个参数指定命令的CanExecute()逻辑。
- 当文本框内容发生变化时,会自动更新内存数据,这时调用SubmitCommand.NotifyCanExecuteChanged(),可以触发执行CanExecute()逻辑,从而更新按钮的可用状态。
总结
(1)本文以一个小例子介绍了如何采用WPF原生库和CommunityToolkit.Mvvm库实现简单的MVVM模式,后者对MVVM模式的常用功能进行了进一步封装,使用更方便,代码更简洁,在大型项目中可以优先考虑使用。
(2)除了数据和命令绑定,CommunityToolkit.Mvvm库也提供了很多其他功能,比如依赖注入、控制反转等。
(3)对一个设计理念的理解或优秀框架的学习没有止境,后面随着自己理解的深入,我将不断更新本系列文章。