本文以一个小例子介绍了如何采用WPF原生库和CommunityToolkit.Mvvm库实现简单的MVVM模式,后者对MVVM模式的常用功能进行了进一步封装,使用更方便,代码更简洁,在大型项目中可以优先考虑使用。
除了数据和命令绑定,CommunityToolkit.Mvvm库也提供了很多其他功能,比如依赖注入、控制反转等。
对一个设计理念的理解或优秀框架的学习没有止境,后面随着自己理解的深入,我将不断更新本系列文章。
实例说明
本文以一个简单的小实例,说明如何使用WPF的MVVM模式。
窗体上有两个文本框和一个按钮,当两个文本框的内容都不为空时按钮可用,否则不可用。点击按钮后,消息框显示两个文本框里的内容。
原生库
Model
1 2 3 4 public class PersonModel { public string FirstName { get ; set ; } public string LastName { get ; set ; } }
View
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <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 >
(1)按钮的Command也是一个属性,其绑定与文本框Text属性的绑定一样。
(2)设置UpdateSourceTrigger=PropertyChanged,保证文本框内容发生变化时马上更新内存数据。
ViewModel
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 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)); } } 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); } } }
(1)当属性值发生变化时,调用RaiseCanExecuteChange()函数,触发CanExecute()函数,从而更新按钮的可用状态。
(2)用可读可写属性对命令进行包装,可实现命令的动态改变;但在大多数情况下,一个按钮的逻辑基本是固定的,这里的做法略显过度,可以声明一个只读属性。
1 2 private MyCommand _submitCommand;public MyCommand SubmitCommand => _submitCommand;
(3)这里通过CanExecute()来设置按钮的可用状态,实际上使用多值绑定的方式可以保证UI逻辑更加紧凑。
(4)ViewModel文件中的内容较杂,可以使用#region分块使代码更清晰。
在View.xaml的后台代码中绑定ViewModel
1 2 3 4 5 6 7 8 9 10 11 12 using System.Windows;using TestMvvm.ViewModels;namespace TestMvvm.Views { public partial class MainWindow : Window { public MainWindow () { InitializeComponent(); this .DataContext = new PersonViewModel(); } } }
依旧采用最经典的DataContext方式进行绑定。
使用当前比较主流的CommunityToolkit.Mvvm库改造以上实例,重点依然集中在数据绑定和命令绑定上。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 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库后代码量可以进一步下降,且程序的可读性更好,有以下几点需要重点说明下:
(1)ViewModel类继承ObservableObject类,在属性的set访问器中调用SetProperty()方法,就可以在更新属性值的同时刷新View上的绑定目标。
(2)SetProperty()方法的最后一个参数是Action类型,用于对value进行数据校验或转换。
(3)RelayCommand是一个泛型命令类,其泛型参数表示命令的参数类型,第一个参数指定命令的Execute()逻辑,第二个参数指定命令的CanExecute()逻辑。
(4)当文本框内容发生变化时,会触发属性的set访问器修改内存数据,这时手动调用命令的NotifyCanExecuteChanged()函数,触发CanExecute()逻辑,从而更新按钮的可用状态。
总结
(1)本文以一个小实例介绍了如何采用WPF原生库和CommunityToolkit.Mvvm库实现简单的MVVM模式,后者对MVVM模式的常用功能进行了封装,使用起来更方便,代码也更简洁,在大型项目中推荐使用。
(2)除了数据绑定和命令绑定外,CommunityToolkit.Mvvm库也提供了其他诸多功能,比如依赖注入、控制反转等,一切都是为了实现“高内聚低耦合”的目标。
(3)对一个设计理念的理解和优秀框架的学习没有止境,随着理解的深入,我会不断更新本系列文章。