WPF MVVM系统入门-上
Models:存放数据的模型,实体对象
Views:可视化界面
ViewModels:业务逻辑。ViewModels与Models的联系会更紧密,而Views页面会主动绑定ViewModels中的数据,原则上ViewModels不要直接去操作Views,被动的被Views来获取数据即可。
一般遵循MVVM模式的项目下,都会有Models、Views、ViewModels
三个文件夹来存放不同的代码工程。
案例入门
实现一个加法计算器,输入两个值,进行相加,并返回结果。
Model
一共需要三个double类型,两个输入,一个输出。
public class MainModel
{
public double Value1 { get; set; }
public double Value2 { get; set; }
public double Value3 { get; set; }
}
ViewModel
编写相加的逻辑代码
public class MainViewModel
{
//声明一个Model类型的属性
public MainModel mainModel { get; set; } = new MainModel();
//业务逻辑
public void Add()
{
mainModel.Value3 = mainModel.Value2 + mainModel.Value1;
}
public MainViewModel()
{
//业务逻辑的调用
//为了便于观察,延迟3s后调用方法
Task.Factory.StartNew(() =>
{
Task.Delay(3000).Wait();
Add();
});
}
}
View
view中,利用一个Textbox和一个slider作为输入分别绑定Value1和value2,使用另一个TextBox绑定value3
<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
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
[AddINotifyPropertyChangedInterface]
public class MainModel
{
public double Value1 { get; set; }
public double Value2 { get; set; }
public double Value3 { get; set; }
}
只需要这样就可以完成上面的工作,它的原理是在编译的时候,把所有的属性都会使用PropertyChanged.Invoke
来触发事件。但有时有些属性不需要进行触发,比如本案例的Value1和Value2,所以可以这样
[AddINotifyPropertyChangedInterface]
public class MainModel
{
[DoNotNotify]
public double Value1 { get; set; }
[DoNotNotify]
public double Value2 { get; set; }
public double Value3 { get; set; }
}
//其他的特性
/// AlsoNotifyFoAttribute 实现通知的时候,同时通知其属性
/// DoNotNotify 指定不需要通知相关的代码
/// DependsOn 指定哪些属性变化的时候,通知当前属性变化
/// DoNotCheckEquality 强制不做旧值比对(默认情况会自动添加比对代码)
命令Command
在上面案例中是使用了构造函数调用了Add
方法,但是如果我想增加一个按钮,在点击的时候才执行Add
方法要怎么办,当然可以绑定按钮的Click事件,但是这样的话Click事件要放置在View的后台类中,不能很好的利用绑定与ViewModel建立联系,所以引出了命令Command
。
命令的用途
- 将调用命令的对象与执行命令的逻辑分开,这允许多个源调用相同的命令逻辑
- 可以指示命令是否可用,如登陆时,用户名为空则登陆按钮不可用
命令要实现ICommand接口,该接口中包含
- CanExecute:是否可执行方法
- Execute:主要执行逻辑
- CanExecuteChanged:触发检查是否可执行
案例入门
- 定义一个类实现ICommand接口
public class CommandBase : ICommand
{
public event EventHandler? CanExecuteChanged;
public bool CanExecute(object? parameter)
{
return true;//返回true表示命令可用
}
//这样做可以在外部给DoExecute委托赋值,根据不同的逻辑业务赋予不同的值
public Action<object> DoExecute { get; set; }
public void Execute(object? parameter)
{
DoExecute?.Invoke(parameter);
}
}
- 在ViewModel中定义一个CommandBase属性
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改为
public Func<object,bool> DoCanExecute { get; set; }
public bool CanExecute(object? parameter)
{
return DoCanExecute?.Invoke(parameter) == true;
}
更改ViewModel
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事件。
public void DoCanExecuteChanged()
{
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
因为需要在Value1赋值的时候,触发 CanExecuteChanged事件,所以需要将Model中定义一个CommandBase。并将ViewModel中的CommandBase删除。
public class MainModel:INotifyPropertyChanged
{
private double _value1;
public double Value1
{
get { return _value1; }
set {
_value1 = value;
BtnCommand.DoCanExecuteChanged();//在更改时触发CanExecuteChanged事件,该事件触发后会执行CanExecute方法
}
}
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系统入门-下。