MVVM(第 2 部分)- 命令





5.00/5 (1投票)
如何在 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
绑定到一个控件和一个事件。每种事件类型都需要自己的CommandBinder
。commandBinder
包含它将要“劫持”的事件名称。它还包含两个(static
)附加属性:Command
和ComandParameter
。每个属性都需要其相应的(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。
创建命令的步骤
我想要转换为命令的每个事件都需要自己的类,包含这两个附加属性。好消息是,大多数应用程序需要有限数量的事件类型。而且创建过程相当简单,但仍然是一个过程。
- 创建新的命令绑定器(我遵循命名约定,因此复制一个现有的,然后用新事件名替换旧事件名 [我清除搜索条件‘匹配整个单词’——我得到6个替换项])。
- 在命令部分的_回调_中,用正确的事件处理程序类型实例化
Executor
。
- 在命令部分的_回调_中,用正确的事件处理程序类型实例化
- 创建一个新的命令属性(我总是在
BaseViewModel
的“命令属性”区域中进行)。 - 添加一个新的操作(我总是在
BaseViewModel
的“命令操作”区域中创建一个虚拟操作)。 - 在
BaseViewModel
的Initialize
方法中实例化CommandBase
。 - 在特定的视图中,在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]"
历史
- 初稿