WPF MVVM系统入门-上
Models:存放数据的模型,实体对象
Views:可视化界面
ViewModels:业务逻辑。ViewModels与Models的联系会更紧密,而Views页面会主动绑定ViewModels中的数据,原则上ViewModels不要直接去操作Views,被动的被Views来获取数据即可。
一般遵循MVVM模式的项目下,都会有Models、Views、ViewModels
三个文件夹来存放不同的代码工程。
案例入门
实现一个加法计算器,输入两个值,进行相加,并返回结果。
Model
一共需要三个double类型,两个输入,一个输出。
1 2 3 4 5 6
| public class MainModel { public double Value1 { get; set; } public double Value2 { get; set; } public double Value3 { get; set; } }
|
ViewModel
编写相加的逻辑代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| public class MainViewModel { public MainModel mainModel { get; set; } = new MainModel(); public void Add() { mainModel.Value3 = mainModel.Value2 + mainModel.Value1; } public MainViewModel() { Task.Factory.StartNew(() => { Task.Delay(3000).Wait(); Add(); }); } }
|
View
view中,利用一个Textbox和一个slider作为输入分别绑定Value1和value2,使用另一个TextBox绑定value3
1 2 3 4 5 6 7 8 9 10 11 12 13
| <Window.DataContext> <vm:MainViewModel /> </Window.DataContext> <Grid> <StackPanel> <TextBox Text="{Binding mainModel.Value1, UpdateSourceTrigger=PropertyChanged}" /> <Slider Maximum="100" Minimum="0" Value="{Binding mainModel.Value2}" /> <TextBox Text="{Binding mainModel.Value3}" /> </StackPanel> </Grid>
|
这样等待3秒,下面的TextBox中的数值一直没有更新

造成这样的原因是,Value3虽然更新了,但是并没有通知绑定他的控件,所以Value3作为输出,需要在更新时触发一个事件,让该事件的订阅者(也就是绑定该值得控件)进行响应。
改进Model
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| public class MainModel:INotifyPropertyChanged { public double Value1 { get; set; } public double Value2 { get; set; }
private double _value3;
public double Value3 { get { return _value3; } set { _value3 = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Value3")); } } public event PropertyChangedEventHandler? PropertyChanged; }
|

在本案例中,只有三个属性,但是属性很多,难道都需要这样触发事件吗?有没有简单的方式?
PropertyChanged.Fody插件
PropertyChanged.Fody插件可让用户简化事件的通知,Nuget安装PropertyChanged.Fody
1 2 3 4 5 6 7
| [AddINotifyPropertyChangedInterface] public class MainModel { public double Value1 { get; set; } public double Value2 { get; set; } public double Value3 { get; set; } }
|
只需要这样就可以完成上面的工作,它的原理是在编译的时候,把所有的属性都会使用PropertyChanged.Invoke
来触发事件。但有时有些属性不需要进行触发,比如本案例的Value1和Value2,所以可以这样
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| [AddINotifyPropertyChangedInterface] public class MainModel { [DoNotNotify] public double Value1 { get; set; } [DoNotNotify] public double Value2 { get; set; } public double Value3 { get; set; } }
|
命令Command
在上面案例中是使用了构造函数调用了Add
方法,但是如果我想增加一个按钮,在点击的时候才执行Add
方法要怎么办,当然可以绑定按钮的Click事件,但是这样的话Click事件要放置在View的后台类中,不能很好的利用绑定与ViewModel建立联系,所以引出了命令Command
。
命令的用途
- 将调用命令的对象与执行命令的逻辑分开,这允许多个源调用相同的命令逻辑
- 可以指示命令是否可用,如登陆时,用户名为空则登陆按钮不可用
命令要实现ICommand接口,该接口中包含
- CanExecute:是否可执行方法
- Execute:主要执行逻辑
- CanExecuteChanged:触发检查是否可执行
案例入门
- 定义一个类实现ICommand接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public class CommandBase : ICommand { public event EventHandler? CanExecuteChanged;
public bool CanExecute(object? parameter) { return true; }
public Action<object> DoExecute { get; set; } public void Execute(object? parameter) { DoExecute?.Invoke(parameter); } }
|
- 在ViewModel中定义一个CommandBase属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public class MainViewModel { public MainModel mainModel { get; set; } = new MainModel();
public void Add(object obj) { mainModel.Value3 = mainModel.Value2 + mainModel.Value1; }
public CommandBase BtnCommand { get; set; } public MainViewModel() { BtnCommand = new CommandBase() {DoExecute = new Action<object>(Add) }; } }
|
- 页面增加一个Button按键并进行绑定
<Button Command="{Binding BtnCommand}" Content="Ok" />

检测是否可执行
上面的CommandBase中,直接将CanExecute返回为true,其实可以利用这个方法来实现检测是当前按钮是否可以使用
将上面的CommandBase改为
1 2 3 4 5
| public Func<object,bool> DoCanExecute { get; set; } public bool CanExecute(object? parameter) { return DoCanExecute?.Invoke(parameter) == true; }
|
更改ViewModel
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public class MainViewModel { public MainModel mainModel { get; set; } = new MainModel();
public void Add(object obj) { mainModel.Value3 = mainModel.Value2 + mainModel.Value1; }
public bool CanCal(object obj) { return mainModel.Value1 != 0; }
public CommandBase BtnCommand { get; set; } public MainViewModel() { BtnCommand = new CommandBase() { DoExecute = new Action<object>(Add), DoCanExecute = new Func<object, bool>(CanCal) }; } }
|
运行后可以看出OK按钮为不可用状态,但是将上面的文本框改为非零状态,仍然是不可用,这是因为在更改value1后,并没有触发检测事件CanExecuteChanged
也就是说此时并没有再次执行CanExecute方法。所以需要触发CanExecuteChanged事件
,框架会自动执行CanExecute方法。

因为事件必须在本类中进行触发,所以在CommandBase中增加DoCanExecuteChanged方法并触发CanExecuteChanged事件。
1 2 3 4
| public void DoCanExecuteChanged() { CanExecuteChanged?.Invoke(this, EventArgs.Empty); }
|
因为需要在Value1赋值的时候,触发 CanExecuteChanged事件,所以需要将Model中定义一个CommandBase。并将ViewModel中的CommandBase删除。
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
| public class MainModel:INotifyPropertyChanged { private double _value1;
public double Value1 { get { return _value1; } set { _value1 = value; BtnCommand.DoCanExecuteChanged(); } }
public double Value2 { get; set; } private double _value3; public double Value3 { get { return _value3; } set { _value3 = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Value3")); } } public event PropertyChangedEventHandler? PropertyChanged;
public CommandBase BtnCommand { get; set; }
public MainModel() { BtnCommand = new CommandBase() { DoExecute = new Action<object>(Add), DoCanExecute = new Func<object, bool>(CanCal) }; }
public void Add(object obj) { Value3 =Value2 + Value1; }
public bool CanCal(object obj) { return Value1 != 0; } }
|
View中的Button更换command绑定<Button Command="{Binding mainModel.BtnCommand}" Content="Ok" />

但是这样的话就必须将Command定义在Model中。那Command如何定义在ViewModel中,请看MVVM系统入门-下。