MVVM之Event and Command
Event:
在Silverlight和WPF中没有使用.net的LCR事件,而是使用Routed路由事件,根本原因是因为Silverlight控件的节点树。
一个简单的示例:
public static readonly RoutedEvent MyRoutedEvent =EventManager.RegisterRoutedEvent("MyEvent", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(MyClass));
是不是很熟悉,没错和定义附加属性(依赖属性)的方式类似,解释下参数:
public static RoutedEvent RegisterRoutedEvent(
string name,
RoutingStrategy routingStrategy,
Type handlerType,
Type ownerType
)
Name:第一个就是事件的名字(也就是一个public,类型和handlerType一致的属性),这个对于同一个类是唯一的;
routingStrategy:指示路由事件的路由策略。枚举值
Tunnel:路由事件使用隧道策略,以便事件实例通过树向下路由(从根到源元素)。
Bubble:路由事件使用冒泡策略,以便事件实例通过树向上路由(从事件元素到根)。
Direct: 路由事件不通过元素树路由,但支持其他路由事件功能,例如类处理、EventTrigger 或 EventSetter。
handlerType:事件的类型,例子为RoutedEventHandler。
ownerType:事件所属的类,通常就是当前类。
限制:Silverlight和WPF的路由事件的一个最大的限制,就是需要把代码写在后置代码中,这样就无法在其他类中进行操作。
这个例子最能说明问题
<TextBox Text="{Binding Source={StaticResource myDomainObject}, Path=StringProperty}"
TextChanged="TextBox_TextChanged" />
这样的代码经常去写,给TextBox添加TextChanged事件,然后在事件中去更新什么东西,可是如果这么做了就打破了MVVM的完整性。都知道默认的Binding更新数据源是在LostFocus的时候去提交,可以通过UpdateSourceTrigger(枚举)去设置,修改后如下:
<TextBox Text=”{Binding Source={StaticResource myDomainObject}, Path=StringProperty,
UpdateSourceTrigger=PropertyChanged}” />
没错,修改默认的UpdateSoureTrigger为PropertyChanged即值改变后立马提交
public string StringProperty
{
get { return _stringProperty; }
set
{
_stringProperty = value;
ProcessNewStringProperty(_stringProperty);
}
}
绑定的属性则调用INotifyPropertyChanged接口的PropertyChanged事件进行更新通知。
Command:
ICommandSource :定义了解如何调用命令的对象,WPF 中可实现 ICommandSource 的类包括: ButtonBase、MenuItem 和 Hyperlink。
属性:
Command 获取将在调用命令源时执行的命令。
CommandParameter 表示可在执行命令时传递给该命令的用户定义的数据值。
CommandTarget 将在其上执行命令的对象。
说了这么多,主要是为了引出ICommand接口,也就是Command的类型。
ICommand:定义一个命令。
属性:
CanExecuteChanged 当出现影响是否应执行该命令的更改时发生。
函数:
CanExecute 定义用于确定此命令是否可以在其当前状态下执行的方法。(根据绑定方法的逻辑,来控制按钮是否禁用状态)
Execute 定义在调用此命令时调用的方法。
接下来看张图,RouteEvent的实现关系
RoutedCommand执行的调用的方法CommandManager,然后搜索元素树,找出匹配的连接一个ICommand和Handler的CommandBinding。这CommandBinding阶级作为一个关联类多对多类的关系,同事出现在ICommand间发生和他们的Handler。
看下自定义Command的实现设计图:
我们主要是在创建一个Command,同时实现Execute方法,然后绑定到Target去,这样就完成了一个完整的触发之定义Command。
定义Commander要求:
1.ViewModel(调用者)和Command(执行者)在不同的ViewModel和Command(自定义Command类)
2.Command作为ViewModel的成员(公有)
3.Command的Hander应该和其在同一个类(在同一个ViewModel完成Commande初始化和绑定)
4.CanExecute方法必须被实现,以完成禁用(启用)Target
5.CanExecute该参数是可选的,即可以不指定此委托
看个Command的示例:
public class RelayCommand:ICommand
{
private readonly Action<object> _execute;
private readonly Predicate<object> _canExecute;
public RelayCommand(Action<object> execute)
: this(execute, null)
{
}
public RelayCommand(Action<object> execute,Predicate<object> canExecute)
{
if(execute==null)
throw new ArgumentNullException("execute");
_canExecute = canExecute;
_execute = execute;
}
public bool CanExecute(object parameter)
{
return _canExecute == null ? true : _canExecute(parameter);
}
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public void Execute(object parameter)
{
_execute(parameter);
}
}
可以看到自定义的Command有两个属性,类型分别为Action<object>和 Predicate<object>,前者为一个委托,封装一个方法,参数就是类型,当前为object;后者为表示定义一组条件并确定指定对象是否符合这些条件的方法,也是一个委托,返回值为bool。
同时我们的自定义Command还有两个方法Execute和CanExecute ,前者就是Command的主要方法,执行绑定的函数,参数为object;后者也提到了是用来实现控件是否禁用。
其中还有一个很重要的属性就是 CanExecuteChanged,它用来监听用户界面的改变,光标从一个Control移动到另一个Control这样的改变,来确定Element的状态。
写完了Commande实现,还需实现ViewModel的代码:
public class LogInViewModel
{
private LogInModel _logInModel;
private RelayCommand _logInCommand;
public string UserName
{
get;
set;
}
public string Password
{
get;
set;
}
public RelayCommand LogInCommand
{
get
{
return _logInCommand;
}
}
public LogInViewModel()
{
_logInModel = new LogInModel();
_logInCommand = new RelayCommand(param=>this.AttemptLogIn(),param=> this.CanAttemptLogIn());
}
private void AttemptLogIn()
{
_logInModel.LogIn(UserName, Password);
}
private bool CanAttemptLogIn()
{
return !string.IsNullOrWhiteSpace(UserName) && !string.IsNullOrWhiteSpace(Password);
}
}
其实代码也很好理解,在ViewModel中有四个属性,一个Model,一个Command,两个用户界面要输入的属性。
在构造函数中实例化Model和Command,在实例化Command时参数很奇怪,使用了lambda表达式,这样在实例化Command的时候param表示一个对方法的引用而不是去执行方法。AttemptLogIn方法在Command被触发的时候执行,所以要实现View的代码部分:
<Window x:Class="View.Wpf.LogInView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:model="clr-namespace:ViewModel;assembly=ViewModel"
Title="LogInView" Height="300" Width="300">
<Window.Resources>
<model:LogInViewModel x:Key="loginModel"></model:LogInViewModel>
</Window.Resources>
<Grid DataContext="{StaticResource ResourceKey=loginModel}">
<Grid.RowDefinitions>
<RowDefinition Height="auto"></RowDefinition>
<RowDefinition Height="auto"></RowDefinition>
<RowDefinition Height="auto"></RowDefinition>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
</Grid.ColumnDefinitions>
<Label Content="UserName:" Grid.Row="0" Grid.Column="0" Margin=" 0 5 0 0"></Label>
<TextBox Grid.Row="0" Grid.Column="1" x:Name="txtUserName" Text="{Binding Path=UserName,UpdateSourceTrigger=PropertyChanged}"
Margin=" 0 5 0 0"></TextBox>
<Label Content="Password:" Grid.Row="1" Grid.Column="0" Margin=" 0 5 0 0"></Label>
<TextBox Grid.Row="1" Grid.Column="1" x:Name="txtPassword" Text="{Binding Path=Password, UpdateSourceTrigger=PropertyChanged}" Margin=" 0 5 0 0" ></TextBox>
<Button Content="LogIn" Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="2" Height="25" Width="200" Margin=" 0 5 0 0"
Command="{Binding Path=LogInCommand}" />
</Grid>
</Window>
Xaml代码也是很简单,两个文本框分别表示用户名和密码,一个按钮用来触发Command,绑定两个TextBox的Text分别为UserName和Password,然后绑定按钮的Command为LogInCommand,这样一个完整的自定义Command就完成了,当我们输入完毕用户名和密码,则按钮自动启用,然后点击登录就搞定了。