WPF MVVM系统入门-上
本文详细讲解WPF,MVVM开发,实现UI与逻辑的解耦。
Models:存放数据的模型,实体对象
Views:可视化界面
ViewModels:业务逻辑。ViewModels与Models的联系会更紧密,而Views页面会主动绑定ViewModels中的数据,原则上ViewModels不要直接去操作Views,被动的被Views来获取数据即可。
一般遵循MVVM模式的项目下,都会有Models、Views、ViewModels
三个文件夹来存放不同的代码工程。
案例入门
实现一个加法计算器,输入两个值,进行相加,并返回结果。
Model
一共需要三个double类型,两个输入,一个输出。
publicclassMainModel
{
publicdoubleValue1 { get; set; }
publicdoubleValue2 { get; set; }
publicdoubleValue3 { get; set; }
}
ViewModel
编写相加的逻辑代码
publicclassMainViewModel
{
//声明一个Model类型的属性
publicMainModelmainModel { get; set; } =newMainModel();
//业务逻辑
publicvoidAdd()
{
mainModel.Value3=mainModel.Value2+mainModel.Value1;
}
publicMainViewModel()
{
//业务逻辑的调用
//为了便于观察,延迟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>
<TextBoxText="{Binding mainModel.Value1, UpdateSourceTrigger=PropertyChanged}"/>
<Slider
Maximum="100"
Minimum="0"
Value="{Binding mainModel.Value2}"/>
<TextBoxText="{Binding mainModel.Value3}"/>
</StackPanel>
</Grid>
这样等待3秒,下面的TextBox中的数值一直没有更新
造成这样的原因是,Value3虽然更新了,但是并没有通知绑定他的控件,所以Value3作为输出,需要在更新时触发一个事件,让该事件的订阅者(也就是绑定该值得控件)进行响应。
改进Model
publicclassMainModel:INotifyPropertyChanged
{
publicdoubleValue1 { get; set; }
publicdoubleValue2 { get; set; }
privatedouble_value3;
publicdoubleValue3
{
get { return_value3; }
set
{
_value3=value;
PropertyChanged?.Invoke(this, newPropertyChangedEventArgs("Value3"));
}
}
publiceventPropertyChangedEventHandler?PropertyChanged;
}
在本案例中,只有三个属性,但是属性很多,难道都需要这样触发事件吗?有没有简单的方式?
PropertyChanged.Fody插件
PropertyChanged.Fody插件可让用户简化事件的通知,Nuget安装PropertyChanged.Fody
[AddINotifyPropertyChangedInterface]
publicclassMainModel
{
publicdoubleValue1 { get; set; }
publicdoubleValue2 { get; set; }
publicdoubleValue3 { get; set; }
}
只需要这样就可以完成上面的工作,它的原理是在编译的时候,把所有的属性都会使用PropertyChanged.Invoke
来触发事件。但有时有些属性不需要进行触发,比如本案例的Value1和Value2,所以可以这样
[AddINotifyPropertyChangedInterface]
publicclassMainModel
{
[DoNotNotify]
publicdoubleValue1 { get; set; }
[DoNotNotify]
publicdoubleValue2 { get; set; }
publicdoubleValue3 { get; set; }
}
//其他的特性
/// AlsoNotifyFoAttribute 实现通知的时候,同时通知其属性
/// DoNotNotify 指定不需要通知相关的代码
/// DependsOn 指定哪些属性变化的时候,通知当前属性变化
/// DoNotCheckEquality 强制不做旧值比对(默认情况会自动添加比对代码)
命令Command
在上面案例中是使用了构造函数调用了Add
方法,但是如果我想增加一个按钮,在点击的时候才执行Add
方法要怎么办,当然可以绑定按钮的Click事件,但是这样的话Click事件要放置在View的后台类中,不能很好的利用绑定与ViewModel建立联系,所以引出了命令Command
。
命令的用途
- 将调用命令的对象与执行命令的逻辑分开,这允许多个源调用相同的命令逻辑
- 可以指示命令是否可用,如登陆时,用户名为空则登陆按钮不可用
命令要实现ICommand接口,该接口中包含
- CanExecute:是否可执行方法
- Execute:主要执行逻辑
- CanExecuteChanged:触发检查是否可执行
案例入门
- 定义一个类实现ICommand接口
publicclassCommandBase : ICommand
{
publiceventEventHandler?CanExecuteChanged;
publicboolCanExecute(object?parameter)
{
returntrue;//返回true表示命令可用
}
//这样做可以在外部给DoExecute委托赋值,根据不同的逻辑业务赋予不同的值
publicAction<object>DoExecute { get; set; }
publicvoidExecute(object?parameter)
{
DoExecute?.Invoke(parameter);
}
}
- 在ViewModel中定义一个CommandBase属性
publicclassMainViewModel
{
publicMainModelmainModel { get; set; } =newMainModel();
publicvoidAdd(objectobj)
{
mainModel.Value3=mainModel.Value2+mainModel.Value1;
}
publicCommandBaseBtnCommand { get; set; }//命令
publicMainViewModel()
{
BtnCommand=newCommandBase() {DoExecute =newAction<object>(Add) };
}
}
- 页面增加一个Button按键并进行绑定
<Button Command="{Binding BtnCommand}" Content="Ok" />
检测是否可执行
上面的CommandBase中,直接将CanExecute返回为true,其实可以利用这个方法来实现检测是当前按钮是否可以使用
将上面的CommandBase改为
publicFunc<object,bool>DoCanExecute { get; set; }
publicboolCanExecute(object?parameter)
{
returnDoCanExecute?.Invoke(parameter) ==true;
}
更改ViewModel
publicclassMainViewModel
{
publicMainModelmainModel { get; set; } =newMainModel();
publicvoidAdd(objectobj)
{
mainModel.Value3=mainModel.Value2+mainModel.Value1;
}
publicboolCanCal(objectobj)
{
returnmainModel.Value1!=0;
}
publicCommandBaseBtnCommand { get; set; }//命令
publicMainViewModel()
{
BtnCommand=newCommandBase() {
DoExecute=newAction<object>(Add),
DoCanExecute=newFunc<object, bool>(CanCal) };
}
}
运行后可以看出OK按钮为不可用状态,但是将上面的文本框改为非零状态,仍然是不可用,这是因为在更改value1后,并没有触发检测事件CanExecuteChanged
也就是说此时并没有再次执行CanExecute方法。所以需要触发CanExecuteChanged事件
,框架会自动执行CanExecute方法。
因为事件必须在本类中进行触发,所以在CommandBase中增加DoCanExecuteChanged方法并触发CanExecuteChanged事件。
publicvoidDoCanExecuteChanged()
{
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
因为需要在Value1赋值的时候,触发 CanExecuteChanged事件,所以需要将Model中定义一个CommandBase。并将ViewModel中的CommandBase删除。
publicclassMainModel:INotifyPropertyChanged
{
privatedouble_value1;
publicdoubleValue1
{
get { return_value1; }
set {
_value1=value;
BtnCommand.DoCanExecuteChanged();//在更改时触发CanExecuteChanged事件,该事件触发后会执行CanExecute方法
}
}
publicdoubleValue2 { get; set; }
privatedouble_value3;
publicdoubleValue3
{
get { return_value3; }
set
{
_value3=value;
PropertyChanged?.Invoke(this, newPropertyChangedEventArgs("Value3"));
}
}
publiceventPropertyChangedEventHandler?PropertyChanged;
publicCommandBaseBtnCommand { get; set; }//命令
publicMainModel()
{
BtnCommand=newCommandBase()
{
DoExecute=newAction<object>(Add),
DoCanExecute=newFunc<object, bool>(CanCal)
};
}
publicvoidAdd(objectobj)
{
Value3=Value2+Value1;
}
publicboolCanCal(objectobj)
{
returnValue1!=0;
}
}
View中的Button更换command绑定<Button Command="{Binding mainModel.BtnCommand}" Content="Ok" />
但是这样的话就必须将Command定义在Model中。那Command如何定义在ViewModel中,请看MVVM系统入门-下。