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

MVVM 中的命令

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.97/5 (107投票s)

2011 年 10 月 29 日

CPOL

15分钟阅读

viewsIcon

547068

downloadIcon

16994

为 WPF、Silverlight 和 WP7 提供命令、异步命令和事件到命令的一致性方法。

目录

  1. 示例 1 - 一个简单的命令 
  2. 示例 2 - 使用 Lambda 函数的简单命令
  3. 示例 3 - 带参数的命令
  4. 示例 4 - 启用或禁用命令
  5. 示例 5 - 触发事件的命令
  6. 示例 6 - 异步命令
  7. 示例 7 - 更新 UI 的异步命令
  8. 示例 8 - 可取消的异步命令
  9. 示例 9 - 将事件绑定到命令
  10. 工作原理 - Command 类
  11. 工作原理 - AsynchronousCommand 类
  12. 工作原理 - EventBinding 类

引言

在本文中,我将描述在使用 MVVM(Model View ViewModel)的项目中命令的工作原理。我所描述的方法与您使用 WPF、Silverlight 还是 Windows Phone 7 完全相同。首先,我将展示在任何 MVVM 项目中使用命令的各种方法 - 此部分可作为命令的快速参考。然后,文章后面将详细介绍每个示例 - 我将确切地展示代码的工作原理。

使用的代码是我 Apex MVVM 库的一部分,但是由于每个部分都附有完整的代码示例,因此您可以轻松地将这些方法集成到现有项目或库中 - 或者直接添加 Apex 作为引用即可开始使用。

屏幕截图 1:WPF 中的命令

consistentmvvmcommands/WPF.png

屏幕截图 2:Silverlight 中的命令

consistentmvvmcommands/SL.png

屏幕截图 3:Windows Phone 7 中的命令

consistentmvvmcommands/WP7.png

什么是命令? 

这是 MVVM 中一个非常重要的概念。您需要了解以下内容。

命令是绑定的对象。它在用户界面和逻辑之间实现了分离。

这是核心概念。更详细地说,我们可以将命令描述为:

  1. 命令是实现 ICommand 接口的对象。
  2. 通常,它与某些代码中的函数相关联。
  3. 用户界面元素绑定到命令 - 当用户激活它们时,命令将被触发 - 这会调用关联的函数。
  4. 命令知道自己是否已启用。
  5. 函数可以禁用命令对象 - 自动禁用与之关联的任何用户界面元素。 
实际上,命令还有很多其他用途。我们可以使用命令来处理异步功能,提供可以独立于用户界面进行测试的逻辑,等等。

命令示例 

首先,我们将研究一组需要使用命令的场景。每个场景在文章后面都有一个相关的详细说明部分,介绍其工作原理。 

重要提示:本示例集中的每个 ViewModel 都包含一个名为 'Messages' 的字符串可观察集合 - 每个示例应用程序都在列表中显示这些消息,以便我们能够看到发生了什么。下面的 ViewModel 基类显示如下:

public class MainViewModel : ViewModel
{
    public MainViewModel()
    {
    }

    private ObservableCollection<string> messages = new ObservableCollection<string>();

    public ObservableCollection<string> Messages
    {
      get { return messages; }
    }
}

我的 ViewModel 继承自 'ViewModel',它实现了 INotifyPropertyChanged,但是您可以使用任何您喜欢的 ViewModel 类型或基类。

示例 1:简单的命令示例

需求:当用户界面元素被按下或激活时,我需要调用 ViewModel 的一个函数。

下面的代码展示了最简单的命令示例。首先,我们在 ViewModel 中添加一个 'Command' 对象 - 并关联一个在命令被调用时要调用的函数。

public class MainViewModel : ViewModel
{
    public MainViewModel()
    {
      //  Create the simple command - calls DoSimpleCommand.
      simpleCommand = new Command(DoSimpleCommand);
    }

    /// <summary>
    /// The SimpleCommand function.
    /// </summary>
    private void DoSimpleCommand()
    {
      //  Add a message.
      Messages.Add("Calling 'DoSimpleCommand'.");
    }

    /// <summary>
    /// The simple command object.
    /// </summary>
    private Command simpleCommand;

    /// <summary>
    /// Gets the simple command.
    /// </summary>
    public Command SimpleCommand
    {
      get { return simpleCommand; }
    }
}

现在,我们将命令绑定到按钮(或其他 UI 元素)的 'Command' 属性。

<Button Content="Simple Command" Command="{Binding SimpleCommand}" /> 

这是最简单的命令示例 - 我们将 Command 对象绑定到界面元素的 Command 依赖属性。当元素被激活时,命令将被调用。在底层,这将导致 'DoSimpleCommand' 函数被调用。

示例 1:工作原理?

示例 2:使用 Lambda 表达式的简单命令示例

需求:当用户界面元素被按下或激活时,我需要调用 ViewModel 的一个函数。但是,这是一个非常简单的函数,我宁愿不显式地写出来 - 我可以使用 lambda 表达式吗?

对于本文中的所有示例,您可以显式创建一个函数,就像在示例 1 中一样,或者可以在行内使用 lambda 表达式。对于小型函数,lambda 表达式可以稍微整洁一些。

public MainViewModel()
{
      //  Create the lambda command, no extra function necessary.
      lambdaCommand = new Command(
        () =>
        {
          Messages.Add("Calling the Lamba Command - no explicit function necessary.");
        });
}

/// <summary>
/// The command object.
/// </summary>
private Command lambdaCommand;

/// <summary>
/// Gets the command.
/// </summary>
public Command LambdaCommand
{
  get { return lambdaCommand; }
} 

现在,我们将命令绑定到按钮(或其他 UI 元素)的 'Command' 属性。

<Button Content="Lambda Command" Command="{Binding LambdaCommand}" /> 

我们学到了什么?在任何定义命令的地方,我们都可以使用命名函数或 lambda 表达式。

示例 2:工作原理?

示例 3:带参数的命令

需求:当调用命令时,我需要传递一个由绑定设置的参数。

在任何使用 Command 对象(或稍后将看到的 AsynchronousCommand 对象)的地方,我们都可以选择使用参数调用函数。

public class MainViewModel : ViewModel
{
    public MainViewModel()
    {
      //  Create the parameterized command.
      parameterizedCommand = new Command(DoParameterisedCommand);
    }

    /// <summary>
    /// The Command function.
    /// </summary>
    private void DoParameterisedCommand(object parameter)
    {
      Messages.Add("Calling a Parameterised Command - the Parameter is '" + 
                   parameter.ToString() + "'.");
    }

    /// <summary>
    /// The command object.
    /// </summary>
    private Command parameterizedCommand;

    /// <summary>
    /// Gets the command.
    /// </summary>
    public Command ParameterisedCommand
    {
      get { return parameterizedCommand; }
    }
}

现在,我们将命令绑定到按钮(或其他 UI 元素)的 'Command' 属性,但同时也要绑定一个参数。

<Button Content="Parameterized Command" 
    Command="{Binding ParameterizedCommand}" CommandParameter={Binding SomeObject} /> 

在任何使用命令的地方,我们都可以传递一个参数。在创建命令时,我们可以使用 Action(一个不带参数的命令函数)或 Action<object>(一个接受单个 'object' 类型参数的命令函数)。如果愿意,我们可以使用 lambda 表达式定义上面的命令函数。

//  Create the parameterized command.
parameterizedCommand = new Command(
  (parameter) =>
  {
    Messages.Add("Calling a Parameterised Command - the Parameter is '" + 
                 parameter.ToString() + "'.");
  }); 

示例 3:工作原理?

示例 4:禁用或启用命令

需求:我需要能够通过代码或 XAML 禁用或启用命令。

每个命令都有 'CanExecute' 属性。当此属性设置为 true 时,命令被启用。当此属性设置为 false 时,命令被禁用,并且 UI 会相应更新 - 例如,绑定到该命令的按钮将变为禁用状态。

public class MainViewModel : ViewModel
{
    public MainViewModel()
    {
      //  Create the enable/disable command, initially disabled.
      enableDisableCommand = new Command(
          () =>
          {
            Messages.Add("Enable/Disable command called.");
          }, false);
    }

    private void DisableCommand()
    {
      //  Disable the command.
      EnableDisableCommand.CanExecute = false;
    }

    private void EnableCommand()
    {
      //  Disable the command.
      EnableDisableCommand.CanExecute = true;
    }
    
    /// <summary>
    /// The command object.
    /// </summary>
    private Command enableDisableCommand;

    /// <summary>
    /// Gets the command.
    /// </summary>
    public Command EnableDisableCommand
    {
      get { return enableDisableCommand; }
    }
}

现在,我们将命令绑定到按钮(或其他 UI 元素)的 'Command' 属性。我们还可以绑定命令的 'CanExecute' 属性。

<CheckBox IsChecked="{Binding EnableDisableCommand.CanExecute, Mode=TwoWay}" Content="Enabled" />
<Button Content="Enable/Disable Command" Command="{Binding EnableDisableCommand}" /> 

我们可以通过设置 CanExecute 属性在代码中启用或禁用命令。我们还可以将该属性绑定到 XAML 以从中操纵它。

每当我们创建命令对象时(如构造函数所示),我们都可以传递一个布尔值作为可选的第二个参数 - 这是初始的 CanExecute 值。默认情况下,它设置为 false。在上面的示例中,我们将命令设置为初始禁用状态。

示例 4:工作原理?

示例 5:触发事件的命令

需求:我需要知道命令何时执行或即将执行。

每个 Command 都有两个事件 - Executed,在命令执行后调用;Executing,在命令即将执行前调用。Executing 事件允许取消命令。

重要提示:在某个特定场景下,以下事件非常有用。想象一下,您想在命令即将运行时弹出一个消息框,询问用户是否要继续 - 您应该在哪里执行此操作?在命令代码中?这是一个糟糕的主意 - 它迫使您在 ViewModel 中编写 UI 代码,这会使命令混乱,并且也意味着它无法通过单元测试运行。您应该在视图中执行此操作 - 借助这些事件,您可以毫无问题地做到这一点。再举一个例子,如果您想在命令执行后将焦点设置到特定控件怎么办?您无法在 ViewModel 中完成此操作,它没有控件的访问权限,但是通过订阅视图中的 Executed 事件,我们就可以毫无问题地做到这一点。

public class MainViewModel : ViewModel
{
    public MainViewModel()
    {
      //  Create the events command.
      eventsCommand = new Command(
          () => 
                {
                    Messages.Add("Calling the Events Command.");
                });

现在,我们将命令绑定到按钮(或其他 UI 元素)的 'Command' 属性。

<Button Content="Events Command" Command="{Binding EventsCommand}" /> 

到目前为止,与第一个示例没有什么不同。但是,我们现在将订阅视图中的一些事件。注意:在我的视图中,我的 DataContext 被命名为 'viewModel'。

/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        viewModel.EventsCommand.Executing += 
          new Apex.MVVM.CancelCommandEventHandler(EventsCommand_Executing);
        viewModel.EventsCommand.Executed += 
          new Apex.MVVM.CommandEventHandler(EventsCommand_Executed);
    }

    void EventsCommand_Executed(object sender, Apex.MVVM.CommandEventArgs args)
    {
        viewModel.Messages.Add("The command has finished - this is the View speaking!");
    }

    void EventsCommand_Executing(object sender, Apex.MVVM.CancelCommandEventArgs args)
    {
        if (MessageBox.Show("Cancel the command?", 
                 "Cancel?", 
                 MessageBoxButton.YesNo) == MessageBoxResult.Yes)
            args.Cancel = true;
    }
}

我们开始感受到这个 Command 实现的强大之处。我们可以订阅视图(甚至其他 ViewModel 或其他对象)中的 ExecutedExecuting 事件,并知道它们何时触发。Executing 事件传递一个 CancelCommandEventArgs 对象 - 它有一个名为 'Cancel' 的属性。如果将其设置为 true,则命令不会执行。CommandEventArgsCancelCommandEventArgs 都有一个附加属性 - Parameter。这是传递给 Command 的参数(如果存在)。

示例 5:工作原理?

示例 6:异步命令

需求:我的命令函数将花费很长时间 - 我需要它异步运行,而不是阻塞 UI 线程。

在这种情况下,我们通常需要做一些类似的事情,例如创建一个后台工作线程并在命令函数中运行它。但我们遇到了问题:

  • 如果我们想在线程函数中更新 ViewModel 怎么办?我们不能,除非在 UI 线程上调用操作。
  • 我们如何确保命令不会在短时间内被按下两次,从而导致线程运行多次?
  • 当有许多必须在线程中运行的命令时,如何保持 ViewModel 的整洁?
  • 如何保持 WP7 和 Silverlight 的一致性,因为它们在线程方面有不同的选项?

AsynchronousCommand 对象(它继承自 Command)将处理所有这些问题 - 以及更多。以下是如何使用它:

public class MainViewModel : ViewModel
{
    public MainViewModel()
    {
      //  Create the async command.
      asyncCommand1 = new AsynchronousCommand(
          () =>
          {
            for (int i = 1; i <= 10; i++)
            {
              //  Report progress.
              asyncCommand1.ReportProgress(() => { Messages.Add(i.ToString()); });

              System.Threading.Thread.Sleep(200);
            }
          });
    }
    
    /// <summary>
    /// The command object.
    /// </summary>
    private AsynchronousCommand asyncCommand1;

    /// <summary>
    /// Gets the command.
    /// </summary>
    public AsynchronousCommand AsyncCommand1
    {
      get { return asyncCommand1; }
    }
}

现在,我们将异步命令绑定到按钮(或其他 UI 元素)的 'Command' 属性。

<Button Content="Asynchronous Command" Command="{Binding AsyncCommand1}" /> 

一旦命令被调用,我们将提供的函数(通过构造函数中的 lambda 表达式)就会被调用 - 在一个新线程上(来自线程池)。

如果我们有任何与 ViewModel 对象(可能绑定到 UI 元素)交互的需求,我们可以通过 ReportProgress 函数来完成。

asyncCommand1.ReportProgress(() => { Messages.Add(i.ToString()); });

ReportProgress 将确保传递给它的代码在正确的线程(UI 线程)上运行。这为我们的命令提供了非常简单的方式来在运行时更新 UI。

示例 6:工作原理?

示例 7:在异步命令运行时更改 UI

需求:我的异步命令需要一段时间才能运行,我想在 UI 中显示一些进度。

AsynchronousCommand 有一个名为 'IsExecuting' 的属性。如果此属性设置为 true,则表示命令正在运行。AsynchronousCommand 实现 INotifyPropertyChanged,这意味着我们可以绑定到此属性 - 并在命令运行时使我们的 UI 保持完全最新。

public class MainViewModel : ViewModel
{
    public MainViewModel()
    {
      //  Create the async command.
      asyncCommand2 = new AsynchronousCommand(
          () =>
          {
            for (char c = 'A'; c <= 'Z'; c++)
            {
              //  Report progress.
              asyncCommand2.ReportProgress(() => { Messages.Add(c.ToString()); });

              System.Threading.Thread.Sleep(100);
            }
          });
    }
    
    /// <summary>
    /// The command object.
    /// </summary>
    private AsynchronousCommand asyncCommand2;

    /// <summary>
    /// Gets the command.
    /// </summary>
    public AsynchronousCommand AsyncCommand2
    {
      get { return asyncCommand2; }
    }
}

现在,我们将异步命令绑定到按钮(或其他 UI 元素)的 'Command' 属性。我们还将其他 UI 元素绑定到命令的 IsExecuting 属性。

<Button Content="Asynchronous Command" Command="{Binding AsyncCommand2}" 
        Visibility="{Binding AsyncCommand2.IsExecuting, 
          Converter={StaticResource BooleanToVisibilityConverter}, 
          ConverterParameter=Invert}" />

<StackPanel Visibility="{Binding AsyncCommand2.IsExecuting, 
         Converter={StaticResource BooleanToVisibilityConverter}}">
  <TextBlock Text="The command is running!" />
  <ProgressBar Height="20" Width="120" IsIndeterminate="True" />
</StackPanel>

在此示例中,一旦命令开始运行,按钮就会消失,并且文本块和进度条会出现。请注意,我们正在绑定到命令的 IsExecuting 属性。

asyncCommand1.ReportProgress(() => { Messages.Add(i.ToString()); });

ReportProgress 将确保传递给它的代码在正确的线程(UI 线程)上运行。这为我们的命令提供了非常简单的方式来在运行时更新 UI。

注意:'Invert' 参数可以传递给 BooleanToVisilityConverter,因为它是在 Apex.Converters 中定义的标准 BooleanToVisibilityConverter 的扩展版本。它反转结果,在这种情况下非常有用。

示例 7:工作原理?

示例 8:允许用户取消 AsynchronousCommand

需求:我需要让用户取消异步命令。

我们可以使用 AsynchronousCommand 的一些功能。每个 AsynchronousCommand 对象都包含另一个 Command - 名为 CancelCommand。它可以绑定到 UI 元素(例如,名为“Cancel”的按钮)或通过代码调用。当调用此命令时,AsynchronousCommand 的 'IsCancellationRequested' 属性被设置为 true(请注意,这是一个使用 INotifyPropertyChanged 的属性,因此您也可以绑定到它)。只要您定期调用 CancelIfRequested 函数并在其返回 true 时返回,那么取消功能就已启用。

public class MainViewModel : ViewModel
{
    public MainViewModel()
    {
       //  Create the cancellable async command.
       cancellableAsyncCommand = new AsynchronousCommand(
         () => 
           {
             for(int i = 1; i <= 100; i++)
             {
               //  Do we need to cancel?
               if(cancellableAsyncCommand.CancelIfRequested())
                 return;

               //  Report progress.
               cancellableAsyncCommand.ReportProgress( () => { Messages.Add(i.ToString()); } );

               System.Threading.Thread.Sleep(100);
             }
           });
    }
    
    /// <summary>
    /// The command object.
    /// </summary>
    private AsynchronousCommand cancellableAsyncCommand;

    /// <summary>
    /// Gets the command.
    /// </summary>
    public AsynchronousCommand CancellableAsyncCommand
    {
      get { return cancellableAsyncCommand; }
    }
}

现在,我们将异步命令绑定到按钮(或其他 UI 元素)的 'Command' 属性。我们还将其他 UI 元素绑定到命令的 IsExecuting 属性。

<Button Content="Cancellable Async Command" 
    Command="{Binding CancellableAsyncCommand}" 
    Visibility="{Binding CancellableAsyncCommand.IsExecuting, 
             Converter={StaticResource BooleanToVisibilityConverter},
    ConverterParameter=Invert}" />

<StackPanel Visibility="{Binding CancellableAsyncCommand.IsExecuting, 
      Converter={StaticResource BooleanToVisibilityConverter}}">
  <TextBlock Margin="4" Text="The command is running!" />
  <ProgressBar Margin="4" Height="20" 
     Width="120" IsIndeterminate="True" />
  <Button Margin="4" Content="Cancel" 
      Command="{Binding CancellableAsyncCommand.CancelCommand}" />
</StackPanel>

在此示例中,当命令执行时,我们显示一个名为“Cancel”的按钮。此按钮绑定到 CancellableAsyncCommand.CancelCommand 属性。由于我们在异步函数中使用 CancelIfRequested 函数,因此我们可以优雅地取消异步命令。

注意:当异步命令被取消时 - Executed不会被调用。而是调用一个名为 Cancelled 的事件,该事件接受与 Executed 相同的参数。

示例 8:工作原理?

示例 9:将事件绑定到命令

需求:我有一个必须调用的命令,但是应该调用它的 UI 元素没有 'Command' 属性 - 它只有一个事件。

在强制事件触发命令的情况下,我们可以使用一个名为 'EventBindings' 的附加属性。此属性和关联的类位于 Apex.Commands 命名空间中。EventBindings 接受一个 EventBindingCollection,它是一个简单的 EventBinding 对象集合。每个 EventBinding 对象都包含事件名称和要触发的命令。

public class MainViewModel : ViewModel
{
    public MainViewModel()
    {
      //  Create the event binding.
      EventBindingCommand = new Command(
        () =>
        {
          Messages.Add("Command called by an event.");
        });
    }
    
    /// <summary>
    /// The command object.
    /// </summary>
    private Command eventBindingCommand;

    /// <summary>
    /// Gets the command.
    /// </summary>
    public Command EventBindingCommand
    {
      get { return eventBindingCommand; }
    }
}

现在我们可以像下面这样通过事件绑定到此命令:

<Border Margin="20" Background="Red">

  <!-- Bind the EventBindingCommand to the MouseLeftButtonDown event. -->
  <apexCommands:EventBindings.EventBindings>
     <apexCommands:EventBindingCollection>
        <apexCommands:EventBinding EventName="MouseLeftButtonDown" 
            Command="{Binding EventBindingCommand}" />
     </apexCommands:EventBindingCollection>
  </apexCommands:EventBindings.EventBindings>
  
  <TextBlock VerticalAlignment="Center" 
     HorizontalAlignment="Center" Text="Left Click on Me" 
     FontSize="16" Foreground="White" />

</Border> 

通过使用 EventBindings 附加属性,我们可以将任何事件绑定到命令。

示例 9:工作原理?

它是如何工作的?

以上每个示例都有一个关于其工作原理的关联说明。

Command 类:用于示例 1、2、3、4 和 5

这些示例使用 'Command' 类,该类将在下面逐节描述。

/// <summary>
/// The ViewModelCommand class - an ICommand that can fire a function.
/// </summary>
public class Command : ICommand
{
    /// <summary>
    /// Initializes a new instance of the <see cref="Command"/> class.
    /// </summary>
    /// <param name="action">The action.</param>
    /// <param name="canExecute">if set to <c>true</c> [can execute].</param>
    public Command(Action action, bool canExecute = true)
    {
        //  Set the action.
        this.action = action;
        this.canExecute = canExecute;
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="Command"/> class.
    /// </summary>
    /// <param name="parameterizedAction">The parameterized action.</param>
    /// <param name="canExecute">if set to <c>true</c> [can execute].</param>
    public Command(Action<object> parameterizedAction, bool canExecute = true)
    {
        //  Set the action.
        this.parameterizedAction = parameterizedAction;
        this.canExecute = canExecute;
    }

首先,我们有两个创建命令的构造函数 - 传入 ActionAction<object>Action<object> 用于带参数的命令。

接下来是我们的 action(或参数化 action)以及 'canExecute' 标志。请注意,当 canExecute 标志更改时,我们必须调用 canExecuteChanged

/// <summary>
/// The action (or parameterized action) that will be called when the command is invoked.
/// </summary>
protected Action action = null;
protected Action<object> parameterizedAction = null;

/// <summary>
/// Bool indicating whether the command can execute.
/// </summary>
private bool canExecute = false;

/// <summary>
/// Gets or sets a value indicating whether this instance can execute.
/// </summary>
/// <value>
///     <c>true</c> if this instance can execute; otherwise, <c>false</c>.
/// </value>
public bool CanExecute
{
    get { return canExecute; }
    set
    {
        if (canExecute != value)
        {
            canExecute = value;
            EventHandler canExecuteChanged = CanExecuteChanged;
            if (canExecuteChanged != null)
                canExecuteChanged(this, EventArgs.Empty);
        }
    }
}

在此之后,我们实现了 ICommand 接口:

/// <summary>
/// Defines the method that determines whether the command can execute in its current state.
/// </summary>
/// <param name="parameter">Data used by the command.
///  If the command does not require data to be passed,
///  this object can be set to null.</param>
/// <returns>
/// true if this command can be executed; otherwise, false.
/// </returns>
bool ICommand.CanExecute(object parameter)
{
    return canExecute;
}

/// <summary>
/// Defines the method to be called when the command is invoked.
/// </summary>
/// <param name="parameter">Data used by the command.
///  If the command does not require data to be passed,
///  this object can be set to null.</param>
void ICommand.Execute(object parameter)
{
    this.DoExecute(parameter);

}

我们还没有看到 'DoExecute' 函数 - 我们将最后介绍它。

/// <summary>
/// Occurs when can execute is changed.
/// </summary>
public event EventHandler CanExecuteChanged;

/// <summary>
/// Occurs when the command is about to execute.
/// </summary>
public event CancelCommandEventHandler Executing;

/// <summary>
/// Occurs when the command executed.
/// </summary>
public event CommandEventHandler Executed; 

还记得那些带事件的示例吗?好吧,在这里 - 以及 ICommand 所需的 CanExecuteChanged 事件。

在继续介绍 DoExecute 函数之前,我们为每个事件创建一个 Invoke 函数,以便我们可以在派生类中调用它们。

protected void InvokeAction(object param)
{
    Action theAction = action;
    Action<object> theParameterizedAction = parameterizedAction;
     if (theAction != null)
        theAction();
    else if (theParameterizedAction != null)
        theParameterizedAction(param);
}

protected void InvokeExecuted(CommandEventArgs args)
{
    CommandEventHandler executed = Executed;

    //  Call the executed event.
    if (executed != null)
        executed(this, args);
}

protected void InvokeExecuting(CancelCommandEventArgs args)
{
    CancelCommandEventHandler executing = Executing;

    //  Call the executed event.
    if (executing != null)
        executing(this, args);
}

请注意,InvokeAction 需要一点技巧 - 我们调用 action 或参数化 action,取决于哪个被设置。最后,我们有了 DoExecute

/// <summary>
/// Executes the command.
/// </summary>
/// <param name="param">The param.</param>
public virtual void DoExecute(object param)
{
    //  Invoke the executing command, allowing the command to be cancelled.
    CancelCommandEventArgs args = 
       new CancelCommandEventArgs() { Parameter = param, Cancel = false };
    InvokeExecuting(args);

    //  If the event has been cancelled, bail now.
    if (args.Cancel)
        return;

    //  Call the action or the parameterized action, whichever has been set.
    InvokeAction(param);

    //  Call the executed function.
    InvokeExecuted(new CommandEventArgs() { Parameter = param });
}

DoExecute 非常简单 - 它调用适当的事件(如果需要,还会取消!)就是这样。

此类完全实现了 ICommand,并提供了示例 1、2、3、4 和 5 中所述的功能。

AsynchronousCommand 类:用于示例 6、7 和 8

示例 6、7 和 8 使用 AsynchronousCommand 类,该类继承自 Command,并在下面逐一介绍。

首先,我们将定义类和构造函数(它们调用基类)。

/// <summary>
/// The AsynchronousCommand is a Command that runs on a thread from the thread pool.
/// </summary>
public class AsynchronousCommand : Command, INotifyPropertyChanged
{
    /// <summary>
    /// Initializes a new instance of the <see cref="AsynchronousCommand"/> class.
    /// </summary>
    /// <param name="action">The action.</param>
    /// <param name="canExecute">if set
    /// to <c>true</c> the command can execute.</param>
    public AsynchronousCommand(Action action, bool canExecute = true) 
      : base(action, canExecute)
    { 
      //  Initialise the command.
      Initialise();
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="AsynchronousCommand"/> class.
    /// </summary>
    /// <param name="parameterizedAction">The parameterized action.</param>
    /// <param name="canExecute">if set to <c>true</c> [can execute].</param>
    public AsynchronousCommand(Action<object> parameterizedAction, bool canExecute = true)
      : base(parameterizedAction, canExecute) 
    {

      //  Initialise the command.
      Initialise(); 
    }

我们实现 INotifyPropertyChanged,以便在 'IsExecuting' 变量更改时进行通知。两个构造函数都调用 'Initialise',让我们现在看一下:

/// <summary>
/// The cancel command.
/// </summary>
private Command cancelCommand;

/// <summary>
/// Gets the cancel command.
/// </summary>
public Command CancelCommand
{
  get { return cancelCommand; }
}

/// <summary>
/// Gets or sets a value indicating whether this instance is cancellation requested.
/// </summary>
/// <value>
///     <c>true</c> if this instance is cancellation requested; otherwise, <c>false</c>.
/// </value>
public bool IsCancellationRequested
{
  get
  {
    return isCancellationRequested;
  }
  set
  {
    if (isCancellationRequested != value)
    {
      isCancellationRequested = value;
      NotifyPropertyChanged("IsCancellationRequested");
    }
  }
}

/// <summary>
/// Initialises this instance.
/// </summary>
private void Initialise()
{
  //  Construct the cancel command.
  cancelCommand = new Command(
    () =>
    {
      //  Set the Is Cancellation Requested flag.
      IsCancellationRequested = true;
    }, true);
}

这里的内容有点多。我们有一个 Command 对象(就像示例中的一样),它所做的只是将 'IsCancellationRequested' 标志设置为 true。Initialise 创建这个对象,我们有一个属性来访问它。我们还有 'IsCancellationRequested' 属性,它会在更改时进行通知。由于我们也想知道何时正在执行,因此让我们为此添加一个属性:

/// <summary>
/// Flag indicating that the command is executing.
/// </summary>
private bool isExecuting = false;

/// <summary>
/// Gets or sets a value indicating whether this instance is executing.
/// </summary>
/// <value>
///     <c>true</c> if this instance is executing; otherwise, <c>false</c>.
/// </value>
public bool IsExecuting
{
  get
  {
    return isExecuting;
  }
  set
  {
    if (isExecuting != value)
    {
      isExecuting = value;
      NotifyPropertyChanged("IsExecuting");
    }
  }
}

不算太糟。由于我们允许取消,因此我们需要一个 Cancelled 事件以及 PropertyChanged 事件(来自 INotifyPropertyChanged)。

/// <summary>
/// The property changed event.
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;

/// <summary>
/// Occurs when the command is cancelled.
/// </summary>
public event CommandEventHandler Cancelled; 

除了标准的 NotifyPropertyChanged 和取消标志的连接之外,真正有趣的是 DoExecute

/// <summary>
/// Executes the command.
/// </summary>
/// <param name="param">The param.</param>
public override void DoExecute(object param)
{
  //  If we are already executing, do not continue.
  if (IsExecuting)
    return;

  //  Invoke the executing command, allowing the command to be cancelled.
  CancelCommandEventArgs args = 
     new CancelCommandEventArgs() { Parameter = param, Cancel = false };
  InvokeExecuting(args);

  //  If the event has been cancelled, bail now.
  if (args.Cancel)
    return;

  //  We are executing.
  IsExecuting = true;

如果命令已在执行,我们则不运行它。在此之后,我们允许取消命令并设置执行标志。

//  Store the calling dispatcher.
#if !SILVERLIGHT
      callingDispatcher = Dispatcher.CurrentDispatcher;
#else
      callingDispatcher = System.Windows.Application.Current.RootVisual.Dispatcher;
#endif

我们必须存储执行命令的调度程序。为什么?因为当我们报告进度时,我们可以在适当的调度程序上报告。我们必须小心如何存储调用调度程序,因为它在 Silverlight 和 WPF 之间的工作方式略有不同。

// Run the action on a new thread from the thread pool
// (this will therefore work in SL and WP7 as well).
ThreadPool.QueueUserWorkItem(
    (state) =>
    {
      //  Invoke the action.
      InvokeAction(param);

      //  Fire the executed event and set the executing state.
      ReportProgress(
        () =>
        {
          //  We are no longer executing.
          IsExecuting = false;

          //  If we were cancelled,
          //  invoke the cancelled event - otherwise invoke executed.
          if(IsCancellationRequested)
            InvokeCancelled(new CommandEventArgs() { Parameter = param });
          else
            InvokeExecuted(new CommandEventArgs() { Parameter = param });

          //  We are no longer requesting cancellation.
          IsCancellationRequested = false;
        }
      );
    }
  );
}

现在是精彩的部分。使用线程池,我们对 InvokeAction 函数进行排队 - 这将调用命令函数(在新线程上)。现在请记住 - ReportProgress 在调用调度程序上运行,因此我们必须在那里更改属性并调用 Executed 等。因此,在调用调度程序上(一旦 action 完成),我们将清除 'IsExecuting' 标志,并根据事件是否被取消来调用 CancelledExecuted。就是这样。我们只剩下 ReportProgress

/// <summary>
/// Reports progress on the thread which invoked the command.
/// </summary>
/// <param name="action">The action.</param>
public void ReportProgress(Action action)
{
  if (IsExecuting)
  {
    if (callingDispatcher.CheckAccess())
      action();
    else
      callingDispatcher.BeginInvoke(((Action)(() => { action(); })));
  }
} 

使用 ReportProgress,我们可以在调用调度程序上调用提供的 action,或者如果我们就是调用调度程序,则直接调用它。

关于单元测试和 AsynchronousCommand 的说明

如果您在单元测试 AsynchronousCommand 对象时遇到任何困难,那是因为在单元测试中,没有 Dispatcher 帧。这里有一个关于如何处理此问题的说明:consistentmvvmcommands.aspx?msg=4084944#xx4084944xx

EventBindings:用于示例 9

EventBindings 代码由于 WPF 和 Silverlight 之间的差异而包含一些额外的复杂性。在 WPF 中,EventBindingsCollection 是一个 FreezableCollection。这样,集合的子项(事件绑定)就可以继承父元素的数据上下文。在 Silverlight 中,我们没有 FreezableCollection,因此我们必须自己设置每个事件绑定的数据上下文。这是 EventBindings 属性的定义方式:

public static class EventBindings
{
  /// <summary>
  /// The Event Bindings Property.
  /// </summary>
    private static readonly DependencyProperty EventBindingsProperty =
      DependencyProperty.RegisterAttached("EventBindings", 
      typeof(EventBindingCollection), typeof(EventBindings),
      new PropertyMetadata(null, new PropertyChangedCallback(OnEventBindingsChanged)));

    /// <summary>
    /// Gets the event bindings.
    /// </summary>
    /// <param name="o">The o.</param>
    /// <returns></returns>
    public static EventBindingCollection GetEventBindings(DependencyObject o)
    {
        return (EventBindingCollection)o.GetValue(EventBindingsProperty);
    }

    /// <summary>
    /// Sets the event bindings.
    /// </summary>
    /// <param name="o">The o.</param>
    /// <param name="value">The value.</param>
    public static void SetEventBindings(DependencyObject o, 
                       EventBindingCollection value)
    {
        o.SetValue(EventBindingsProperty, value);
    }

    /// <summary>
    /// Called when event bindings changed.
    /// </summary>
    /// <param name="o">The o.</param>
    /// <param name="args">The <see
    /// cref="System.Windows.DependencyPropertyChangedEventArgs"/>
    /// instance containing the event data.</param>
    public static void OnEventBindingsChanged(DependencyObject o, 
           DependencyPropertyChangedEventArgs args)
    {
        //  Cast the data.
        EventBindingCollection oldEventBindings = 
          args.OldValue as EventBindingCollection;
        EventBindingCollection newEventBindings = 
          args.NewValue as EventBindingCollection;

        //  If we have new set of event bindings, bind each one.
        if (newEventBindings != null)
        {
            foreach (EventBinding binding in newEventBindings)
            {
                binding.Bind(o);
#if SILVERLIGHT
                //  If we're in Silverlight we don't inherit the
                //  data context so we must set this helper variable.
                binding.ParentElement = o as FrameworkElement;
#endif
            }
        }
    }
}

正如我们所见,唯一真正有趣的功能是 EventBinding.Bind,它列在下面:

public void Bind(object o)
{
    try
    {
        //  Get the event info from the event name.
        EventInfo eventInfo = o.GetType().GetEvent(EventName);

        //  Get the method info for the event proxy.
        MethodInfo methodInfo = GetType().GetMethod("EventProxy", 
                   BindingFlags.NonPublic | BindingFlags.Instance);

        //  Create a delegate for the event to the event proxy.
        Delegate del = Delegate.CreateDelegate(eventInfo.EventHandlerType, this, methodInfo);

        //  Add the event handler. (Removing it first if it already exists!)
        eventInfo.RemoveEventHandler(o, del);
        eventInfo.AddEventHandler(o, del);
    }
    catch (Exception e)
    {
        string s = e.ToString();
    }
}

这是满足此要求的标准方法,我在其他文章中也看到过。但是,我们必须在事件代理(这我以前没见过)中更加小心。

/// <summary>
/// Proxy to actually fire the event.
/// </summary>
/// <param name="o">The object.</param>
/// <param name="e">The <see
///    cref="System.EventArgs"/> instance
///    containing the event data.</param>
private void EventProxy(object o, EventArgs e)
{   
#if SILVERLIGHT

    //  If we're in Silverlight, we have NOT inherited the data context
    //  because the EventBindingCollection is not a framework element and
    //  therefore out of the logical tree. However, we can set it here 
    //  and update the bindings - and it will all work.
    DataContext = ParentElement != null ? ParentElement.DataContext : null;
    var bindingExpression = GetBindingExpression(EventBinding.CommandProperty);
    if(bindingExpression != null)
        bindingExpression.UpdateSource();
    bindingExpression = GetBindingExpression(EventBinding.CommandParameterProperty);
    if (bindingExpression != null)
        bindingExpression.UpdateSource();

#endif

    if (Command != null)
        Command.Execute(CommandParameter);
}

最终想法

在我提供的示例应用程序中,ViewModel 类在每个项目中都是相同的 - 这段代码在 WPF、Silverlight 和 WP7 中工作方式完全相同。

希望您觉得本文很有用,如果您发现任何问题,请通过评论部分告知我。

要及时了解 Apex 的最新开发动态,您可以访问我的博客:www.dwmkerr.com 或 Apex CodePlex 页面:apex.codeplex.com

© . All rights reserved.