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 的类包括: ButtonBaseMenuItem 和 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就完成了,当我们输入完毕用户名和密码,则按钮自动启用,然后点击登录就搞定了。

原文地址:https://www.cnblogs.com/ListenFly/p/2409387.html