MVVM是Model-View-ViewModel的简写,是由微软在WPF中提供的新技术,MVVM 架构使用的是数据绑定基础架构。更多介绍:http://baike.baidu.com/view/3507915.htm
MVVM架构分为三层:
• Model负责访问数据,为ViewModel提供数据。
• ViewModel连接Model层和View层. ViewModel帮助管理Model提供的数据.
• View层表现数据,通常为XAML定义的用户界面.
三层之间的调用关系:
View层不允许直接调用Model层获取数据,View层获取数据需要通过调用ViewModel提供的方法或属性简介获取Model层提供的数据。
同时为了实现双向通信,ViewModel和View会提供事件处理逐层向下反馈用户处理。
再简单的MVVM示例中Model层是被省略的,ViewModel负责全部业务逻辑。ViewModel和View之间通过Data Binding通信,其中View作为Target,ViewModel作为Source。
ViewModels 和 Data Binding
再Data Binding介绍中提到,Source必须是实现了INotifyPropertyChanged(INPC)接口的对象。
INotifyPropertyChanged接口包含一个PropertyChanged事件委托类型为
PropertyChangedEventHandler
。PropertyChangedEventHandler委托第二个参数类型为PropertyChangedEventArgs
,PropertyChangedEventArgs包含一个string 类型的PropertyName属性,根据这个属性可以判断ViewModel的哪个属性发生改变。
对Xamarin.Forms平台而言,ViewModel通常继承BindableObject
即可,ViewModel公共属性定义为BindableProperty
类型。BindableObject已经实现了INotifyPropertyChanged接口,定义为BindableProperty类型的属性改变时会自动触发PropertyChanged事件。
简单INotifyPropertyChanged接口使用示例:一个Label标签显示当前时间,每过一秒Label时间更新一次。INotifyPropertyChanged ➕ Data Binding实现。
自定义ViewModel实现INotifyPropertyChanged接口。
public class DateTimeViewModel : INotifyPropertyChanged
{
DateTime dateTime = DateTime.Now;
public event PropertyChangedEventHandler PropertyChanged;
public DateTimeViewModel()
{
Device.StartTimer(TimeSpan.FromSeconds(1), () =>
{
DateTime = DateTime.Now;
return true;
});
}
public DateTime DateTime
{
private set
{
if (dateTime != value)
{
dateTime = value;
OnPropertyChanged("DateTime");
}
}
get
{
return dateTime;
}
}
protected void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
ViewModel除实现了INotifyPropertyChanged接口外还定义了DateTime类型的DateTime属性。在DateTime属性的set操作中检查属性对应的字段与value是否相等,如不想等对dateTime赋值的同时检查PropertyChanged的值是否为null,不为null调用PropertyChanged。
OnPropertyChanged方法传入的string类型参数表示当前改变属性的字符串名,一个ViewModel内定义多个属性应确保调用OnPropertyChanged方法时传入一个正确的字符串,错误的字符串导致会使绑定失败。.Net 4.5之后的版本提供了一个特性类CallerMemberName
,表调用者名且对应参数应设为可选参数具有默认值。修改后的OnPropertyChanged方法:
protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
属性的set操作中调用OnPropertyChanged方法的代码修改为OnPropertyChanged();
。由于定义了CallerMemberNameAttribute可以省略参数传递。事实上CallerMemberNameAttribute类就是为了INotifyPropertyChanged接口使用定义的。
进一步修改代码,定义ViewModel时定义一个SetProperty通用方法,第一个参数标记为ref,表示Property对应的字段,第二个参数为set提供的value。
bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = "")
{
if (Object.Equals(storage, value))
return false;
storage = value;
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
return true;
}
如果属性的set操作中只提供简单的赋值,调用定义的SetProperty方法即可。本例中DateTime属性的示例:
public DateTime DateTime
{
private set
{
SetProperty<DateTime>(ref dateTime,value);
}
get
{
return dateTime;
}
}
《Creating Mobile Apps with Xamarin.Forms》中代码示例,可以定义一个ViewModel基类,定义ViewModel时即成ViewModelBase类即可,不再考虑INotifyPropertyChanged接口。
XAML界面定义:
<ContentPage.Resources>
<ResourceDictionary>
<local:DateTimeViewModel x:Key="dateTimeViewModel" />
</ResourceDictionary>
</ContentPage.Resources>
<Label Text="{Binding Source={StaticResource dateTimeViewModel} ,Path=DateTime }"
VerticalOptions="Center" HorizontalOptions="Center" />
运行效果:
MVVM可以使用户界面与逻辑代码分离,如修改Label为Button,只需要编辑XAML代码即可。
<Button Text="{Binding Source={StaticResource dateTimeViewModel} ,Path=DateTime }"
VerticalOptions="Center" HorizontalOptions="Center" />
ICommand 接口介绍
Data Binding 可以方便的连接View的属性和ViewModel的属性,有时候ViewModel还会定义公共方法给View调用,如ViewModel定义一个公共方法响应Button的点击事件。MVVM提供了command interface
协议绑定方法,command interface只支持极少数的几个类:Button、MenuItem、ToolbarItem、SearchBar、TextCell、ImageCell、ListView、TapGestureRecognizer。当然也可以自定义类实现command interface的支持。
通过command interface代替Button的Clicked事件示例。Button提供了Command和CommandParameter两个属性支持commanding。ViewModel需要定义一个类型为ICommand的公告属性连接Button的Command属性。ICommand定义在System.Windows.Input命名空间下。
ICommand中定义了两个方法一个事件,当Button的Command属性绑定一个实现了ICommand接口的对象时,点击按钮触发Clicked事件的同时还会调用Command属性的Excute方法,Excute方法的参数通过Button的CommandParameter属性设置。CanExecute方法会在Command属性第一次赋值时调用,如果CanExecute返回false,表示Button不可用且不会发生Excute方法的调用。Button还会监听CanExecuteChanged事件,当CanExecuteChanged事件触发时会继续调用CanExecute方法判断Button是否可用。
实际使用通常不会自己实现ICommand接口,Forms提供了ICommand的实现类Command
,同时也提供了Command<T>
类.
Command出了实现ICommand接口还提供了ChangeCanExecute
方法,调用该方法会触发CanExecuteChanged事件。
Command如上所示,构造函数提供的两个参数类型分别为Action和Func<bool>,excute表示Execute方法的调用,canExecute表示CanExecute方法的调用。
通过一个简单示例展示Commanding的用法。添加两个按钮使Label上显示的数字在+3~-3之间增加和减小,效果:
首先定义NumberViewModel类
public class NumberViewModel : INotifyPropertyChanged
{
private int number = 0;
private const int Increase_Limit = 3, Decrease_Limit = -3;
public event PropertyChangedEventHandler PropertyChanged;
public NumberViewModel()
{
IncreaseCommand = new Command(() => { Number++; }, () => { return number < Increase_Limit; });
DecreaseCommand = new Command(() => { Number--; }, () => { return number > Decrease_Limit; });
}
public int Number
{
private set
{
if (SetProperty<int>(ref number, value))
{
IncreaseCommand.ChangeCanExecute();
DecreaseCommand.ChangeCanExecute();
}
}
get
{
return number;
}
}
bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = "")
{
if (Object.Equals(storage, value))
return false;
storage = value;
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
return true;
}
#region Command
public Command IncreaseCommand
{
get;
set;
}
public Command DecreaseCommand
{
get;
set;
}
#endregion
}
NumberViewModel类不止实现了INotifyPropertyChanged类还定义了两个Command类型(实现了ICommand接口)属性IncreaseCommand和DecreaseCommand,这两个属性分别会绑定到加1按钮和减1按钮的Command属性。两个属性在构造函数中初始化时第一个参数的匿名委托中一定要直接操作Number属性而不是number字段,否则不会触发PropertyChanged事件,第二个参数根据number的值判断按钮是否可以用从而控制number的变化范围。修改Number的set代码,改变number值的同时调用Command的ChangeCanExecute方法触发CanExecuteChanged事件从而调用CanExecute方法(前文提到的第二个参数)。