65.9K
CodeProject 正在变化。 阅读更多。
Home

MVVM(第 2 部分)- 命令

starIconstarIconstarIconstarIconstarIcon

5.00/5 (1投票)

2010年9月30日

CPOL

5分钟阅读

viewsIcon

33469

downloadIcon

293

如何在 Silverlight 中创建命令基础设施。

引言

在我上一篇文章中,我介绍了MVVM架构中缺失的命令功能。本文将通过向自建MVVM添加命令来完善上一篇文章。

命令在这里如何工作

我敢肯定Silverlight 5将对命令有更好的支持,但目前我不得不设计某种方案来支持命令。我“劫持”事件的方式是重新配置它们,以便调用命令操作。

依我之见,有些事件应该保留在代码隐藏中,因为它们更多是UI功能,而不是业务或数据相关的。但是,如果我想通过命令完成所有事情,这就需要一些操作,而且不仅仅是操作——某些事件信息我不知道如何通过XAML绑定获取。某些操作可能需要控件的引用。其他操作可能需要不相关的数据。基于以上几点,我希望我的命令能够传递3个对象——发送者、原始事件数据和一个用户(XAML绑定的)参数。我还希望能够通过非常简单的XAML行进行绑定和指定命令。无需资源等。

下面的内容并非高深莫测,但如果你之前没做过,也不算简单。如果我让你迷糊了,可以去看代码部分,它可能会让你更清楚。为了不陷入细节,以下的描述会省略和隐藏一些点——我稍后会再讲。

所以,它的工作方式是:通过XAML,我将一个属性(附加依赖属性)绑定到一个CommandBase类的实例。CommandBase类保存着命令操作和其他相关数据的引用。当视图渲染时,对该属性的绑定会触发依赖属性“注册”中指定的某个回调。附加属性是static的,但回调会传递实例。它传递控件的一个实例以及我在XAML中绑定的命令的一个实例。接下来是劫持/重连——我获取原始事件并为其注册一个本地的通用处理程序。此时,绑定活动就完成了。根据上述操作,我得到的是一个事件触发时将运行的通用处理程序。

这个处理程序接收原始事件参数和事件发送者。它还可以提取命令对象(我稍后会讲)以及用户可能指定的参数(我稍后会讲)。它将这3个对象合并成一个ParameterWrapper对象。使用命令对象,它调用命令的操作,并将ParameterWrapper对象传递给它。

CommandBase 类

此类实现ICommand接口,该接口定义了操作并检查操作是否可以在此时调用。Silverlight 4 使用它与现有的内置命令。它包含两个委托——一个用于检查操作是否可以运行,另一个是操作本身。

public class CommandBase : ICommandFromEvent
{
	private bool canExecuteLocal = false;
	private readonly Func<object, bool> isExecuteAllowed = null;
	private readonly Action<object> commandAction = null;

	public CommandBase(Action<object> commandDelegate, 
                           Func<object, bool> canExecuteDelegate = null)
   {
        commandAction = commandDelegate;
        isExecuteAllowed = canExecuteDelegate;
   } 

它还实现了一个自定义接口,允许我只调用一个方法,并且isAllowed检查和执行都会完成。

CommandBinder 类

此类将XAML声明的CommandBase绑定到一个控件和一个事件。每种事件类型都需要自己的CommandBindercommandBinder包含它将要“劫持”的事件名称。它还包含两个(static)附加属性:CommandComandParameter。每个属性都需要其相应的(static)getter和setter。Command属性还需要(static)回调。回调会实例化一个我称之为Executor的类。Executor类负责劫持事件(重连以及调用CommandBase中的操作)。这是代码的第一部分。

private const string EName = "SelectionChanged"; //the event name
public static readonly DependencyProperty CommandProperty = 
				DependencyProperty.RegisterAttached
   (
      "Command",                             //Property name
      typeof(ICommand),                      //Property type
      typeof(SelectionChangedCommandBinder), //Type of property owner
      new PropertyMetadata(CommandChanged)   //Callback invoked on property
                                                  value has changed
    );

public static void SetCommand(DependencyObject depObj, ICommand cmd)
{
    depObj.SetValue(CommandProperty, cmd);
}

public static ICommand GetCommand(DependencyObject depObj)
{
    return (ICommand)depObj.GetValue(CommandProperty);
}
//the callback
protected static void CommandChanged(DependencyObject depObj,
                    DependencyPropertyChangedEventArgs depArgs)
{ //create an Executor for a given even and event-args type
	var executor = new Executor<selectionchangedeventargs />(EName);
	executor.CommandGetter = GetCommand;
	executor.ParameterGetter = GetCommandParameter;
	executor.CommandChanged(depObj, depArgs);
}

CommandParameterProperty的代码类似(省略了回调)。CommandChanged(回调)为每次新的绑定实例化一个新的Executor。这是附加属性的static性质“停止”并为每个事件/控件/命令分配新实例的地方。在实例化Executor类时,回调会将事件名称、一个用于查找BaseCommand的委托和一个用于查找CommandParameter的委托传递给它。由于Executor是一个泛型类,回调会隐式指定事件参数类型的类型。

Executor 类

这个泛型类将一个泛型事件处理程序连接到事件。首先,它使用反射根据事件名称查找事件,然后创建一个由本地泛型处理程序组成的委托,最后将该委托添加到事件中,以便下次引发事件时,它将调用本地处理程序。

public void CommandChanged(DependencyObject depObj,
               DependencyPropertyChangedEventArgs depArgs)
{ //use reflection to add a given handler as a 
  //delegate to the given event name
   if (depObj != null)
   {
      Type type = depObj.GetType();
      var eventInfo = type.GetEvent(eventName);
      if (eventInfo != null)
      { //event name yield an event
         var delegateType = eventInfo.EventHandlerType;
         if (delegateType != null)
         { //create a delegate from the handler
            Delegate handler = Delegate.CreateDelegate(
                     delegateType, this, "TheHandler");
            if (handler != null)
            { // delegate is good, add it to the event.
               eventInfo.AddEventHandler(depObj, handler);
            }
         }
      }
      else
      {
         MessageBox.Show(string.Format(
                  "missing types for Event [{0}] in [{1}]",
                  eventName, depObj));
      }
   }
}

本地泛型处理程序使用传递进来的委托并调用命令的操作。

private void TheHandler(object sender, T e)
{
   var commander = sender as DependencyObject;
   if (commander != null)
   { //get the CommandBase
      var cmd = commandGetter(commander);
      if (cmd != null)
      { //wrap all the data by one object
         var param = new ParameterWrapper(parameterGetter(commander), 
		                             sender, e as RoutedEventArgs);
         var sCmd = cmd as ICommandFromEvent;
         if (sCmd != null)
         { //as part of this call check if the command is allowed to be executed
            sCmd.SmartExecute(param);
         }
         else
         { //Execute checking might have been done earlier
            cmd.Execute(param);
         }
      }
   }
}

命令的XAML声明

所有这些命令设计中最棒的部分是,我可以在XAML中轻松声明命令和命令参数。

<sdk:DataGrid	AutoGenerateColumns="True"
	HorizontalAlignment="Left"
	Margin="5"
	VerticalAlignment="Top"
	Grid.Row="1"			
	ItemsSource="{Binding DataToPresent}"
	cmd:SelectionChangedCommandBinder.Command="{Binding ItemSelectedCommand}"
	cmd:SelectionChangedCommandBinder.CommandParameter=
	          "{Binding SelectedItem, RelativeSource={RelativeSource self}}"
	/>

上面的XAML片段声明了DataGrid——最后两行处理命令声明。

首先,我将ItemSelectCommand(代表CommandBase的一个实例)与SelectionChangedCommandBinder类中的Command Attached Property进行绑定。

第二行使用一些XAML技巧将当前控件的SelectedItem属性绑定到SelectionChangedCommandBinder类中的CommandParameter Attached Property。

创建命令的步骤

我想要转换为命令的每个事件都需要自己的类,包含这两个附加属性。好消息是,大多数应用程序需要有限数量的事件类型。而且创建过程相当简单,但仍然是一个过程。

  1. 创建新的命令绑定器(我遵循命名约定,因此复制一个现有的,然后用新事件名替换旧事件名 [我清除搜索条件‘匹配整个单词’——我得到6个替换项])。
    • 在命令部分的_回调_中,用正确的事件处理程序类型实例化Executor
  2. 创建一个新的命令属性(我总是在BaseViewModel的“命令属性”区域中进行)。
  3. 添加一个新的操作(我总是在BaseViewModel的“命令操作”区域中创建一个虚拟操作)。
  4. BaseViewModelInitialize方法中实例化CommandBase
  5. 在特定的视图中,在XAML中绑定命令。

步骤 1

public class KeyDownCommandBinder
{
   private const string EName = "KeyDown";
   public static readonly DependencyProperty 
                 CommandParameterProperty = DependencyProperty.RegisterAttached
      (
      "CommandParameter",                            //Property name
      typeof(object),                                //Property type
      typeof(KeyDownCommandBinder),                  //Type of property owner
      new PropertyMetadata(CommandParameterChanged)  //Callback invoked on 
	                                                 //property value has change
      );

   public static void SetCommandParameter(DependencyObject depObj, object param)
   {
      depObj.SetValue(CommandParameterProperty, param);
   }

   public static object GetCommandParameter(DependencyObject depObj)
   {
      return depObj.GetValue(CommandParameterProperty);
   }

   private static void CommandParameterChanged(DependencyObject depObj, 
                              DependencyPropertyChangedEventArgs depArgs)
   {
   }

   public static readonly DependencyProperty 
                     CommandProperty = DependencyProperty.RegisterAttached
      (
      "Command",                             //Property name
      typeof(ICommand),                      //Property type
      typeof(KeyDownCommandBinder),          //Type of property owner
      new PropertyMetadata(CommandChanged)   //Callback invoked on 
	                                         //property value has change
      );

   public static void SetCommand(DependencyObject depObj, ICommand cmd)
   {
      depObj.SetValue(CommandProperty, cmd);
   }

   public static ICommand GetCommand(DependencyObject depObj)
   {
      return (ICommand)depObj.GetValue(CommandProperty);
   }

   public static void CommandChanged(DependencyObject depObj, 
                                    DependencyPropertyChangedEventArgs depArgs)
   {
      var executor = new Executor(EName);
      executor.CommandGetter = GetCommand;
      executor.ParameterGetter = GetCommandParameter;
      executor.CommandChanged(depObj, depArgs);
   }
}

第二步

private ICommand keyDownCommand = null;
public ICommand KeyDownCommand
{
	get { return keyDownCommand; }
	set
	{
		keyDownCommand = value;
		OnPropertyChanged("KeyDownCommand");
	}
}

步骤 3

protected virtual void KeyDownAction(object param)
{
	MessageBox.Show("KeyDown Action is not ready");
}

步骤 4

KeyDownCommand = new CommandBase(KeyDownAction);

步骤 5

cmd:KeyDownCommandBinder.Command="{Binding KeyDownCommand}"
cmd:KeyDownCommandBinder.CommandParameter=
		"{Binding Text, RelativeSource={RelativeSource self}}"

或者

cmd:LoadedCommandBinder.Command="{Binding LoadedCommand}"
cmd:LoadedCommandBinder.CommandParameter="[ClassicView]"

历史

  • 初稿
© . All rights reserved.