此文章由机器翻译。

MVVM

使用状态机模式的 WPF 命令

Tarquin Vaughan-Scott

Windows 演示文稿基础 (WPF) 有了强有力的指挥框架使您可以分离用户界面和命令逻辑。当您使用模型-视图-模型 (MVVM) 设计模式时,命令被暴露在 ViewModel 作为实现 ICommand 接口的属性。在视图上的控件绑定到这些属性。当用户与控件进行交互时,执行指定的命令。

一如往常,魔鬼是在细节中。真正的挑战不是在执行命令,但当 ViewModel 处于有效状态,这一行动的时候确保它执行。通常情况下,"可执行"验证执行条件表达式使用本地变量,如 IsLoading,CanDoXYZ,SomeObject! = null,等等。每个新的国家需要需要评价,所以这种策略会很快变得过于复杂的附加条件。

一个状态机解决了的"可执行"的问题,因为它限制所允许的特定状态的操作。将命令绑定到状态机直接提供这个棘手的问题的好办法。

这篇文章将说明如何设计和实现一个状态机,通过分析您的应用程序。它还会告诉你如何将命令绑定到状态机。也有一些额外的好处,例如防止可重入性的异步方法,以及显示和隐藏控件为特定的国家。

状态机

状态机来用不同的味道,但它们本质上是一种设计模式,它表示从一个状态转移到另一个过程。用户操作 (也称为触发器) 导致状态机状态之间转换。规则限制每个国家所允许的操作。

有限状态机只允许一个国家在一段时间。分层状态机允许各州和子状态。分层状态机往往更有用,因为子状态继承其超国家的所有属性。这可以减少所需的配置。

例如,请考虑用户单击搜索按钮:应用程序转换到搜索状态。在此状态下,用户应阻止执行其他操作 (与取消操作可能例外)。搜索状态会因此有一个规则,阻止或忽略除取消操作的所有操作。一些状态机实现还允许进入和退出动作。你可以有这种逻辑执行时的状态是进入和退出。

命令结构

像瓶中的精灵,命令有做他们主人的命令,但命令是什么?命令在命令行界面,要求用户键入一个指令,然后由应用程序被解释他们的起源。

这一概念已经在现代接口抽象用户的预期的行为。例如,如果用户想要复制某些文本,该命令是复制的文本。用户可以通过单击某个菜单项,右键单击鼠标按钮或甚至使用键盘控制 (CTRL + C) 达到这个。如何执行该命令取决于基础应用程序和赖以建立的框架。

WPF 实现指挥概念通过 ICommand 接口。这是 Microsoft.NET 框架的一部分。此接口有两种方法和事件:

  • 无效执行 (对象参数) — — 这执行代码,当调用该命令。
  • bool CanExecute (对象参数) — — 这就决定了是否可以调用的命令。
  • 事件 EventHandler CanExecuteChanged — — 这通知影响 CanExecute 法的条件已经发生变化的框架。这通常是由 WPF 框架处理。

基本上,例如按钮或菜单项的命令源实现 ICommandSource 接口。此接口具有一个名为类型 ICommand 的命令属性。通过将此属性绑定到 ViewModel ICommand 实现,该控件将调用 Execute 方法。您还可以启用和禁用基于 CanExecute 方法的结果。如果作为命令源的控件没有 ICommand 属性 (即,不幸的是,相当普遍),然后您可以使用命令技术的事件。

ICommand 内置于.NET 框架的实现是 RoutedCommand 和 RoutedUICommand。这些可视化树的事件路由,而且并不适合使用 MVVM 模式。约什-史密斯和棱镜框架的一部分是常用的 DelegateCommand RelayCommand 用于 MVVM 模式的实现。

本文附带的源代码中包含演示状态机的原理以及如何使用它来管理命令的 Visual Studio 2013 解决方案。示例应用程序允许用户搜寻的雇员列表,选择员工,然后显示对话框,用于编辑员工详细信息 (请参阅图 1)。

员工加载和准备好的列表进行编辑
图 1 员工加载和准备好的列表进行编辑

状态机设计

在设计过程中的第一步是定义使用流程图或状态图的应用程序的部分。图 2 显示员工管理器屏幕流程关系图。它还包括表明潜在的长期运行的流程,例如搜索的块。您需要处理这些在一种特殊方式,防止用户重新运行操作,而是忙着。创建这些中介的"忙"块是.NET Framework 中,新的异步功能控制返回给用户,他可能可以试着和运行相同的操作,再次导致折返显得尤其重要。

流程图显示过程的员工管理器屏幕
图 2 流程图显示过程的员工管理器屏幕

在设计过程中的第二步是定义允许用户与应用程序交互的任何命令。在员工管理器屏幕中,用户可以搜索、 编辑员工,然后结束编辑。用户也能推动选择和取消选择员工的过程,然而,这些并不作为命令进行管理。他们是作为在 DataGrid 控件的数据绑定处理。这些命令现在映射到工作流箭头 (或控制流) 在相关点,如中所示图 2

实现状态机

你不必从零开始,创建一个状态机框架有很多自由可用的库。在.NET 框架内唯一的选项是工作流的基础 (WF) 状态机活动。这是过于复杂,我想在这里,解决这个问题,但是它非常适合长时间运行持久性的工作流。经过一些初步的研究,我解决无国籍状态机库,可用作 NuGet 程序包 bit.ly/ZL58MG

现在,我可以使用我在设计阶段创建触发器 (流程图中的每个箭头) 以及各国 (流程图中的每个块) 列表中创建的流程图。无状态使用泛型类型对于国家和触发器,所以我要往前走,两者使用的枚举:

public enum States
{
  Start, Searching, SearchComplete, Selected, NoSelection, Editing
}
public enum Triggers
{
  Search, SearchFailed, SearchSucceeded, Select, DeSelect, Edit, EndEdit
}

一旦定义了枚举,我需要配置每个状态在使用流畅的界面。重要的选项是:

  • SubstateOf (TState 状态) — — 表明一个国家具有超级-­状态和将继承它的配置。
  • 许可证 TState targetState TTrigger 触发器) — — 允许过渡到目标状态通过触发器状态。
  • 忽略 (TTrigger 触发器) — — 如果忽略该触发器的状态会导致其被解雇。
  • OnEntry (行动 entryAction) — — 导致进入状态时要执行的操作。
  • 请将 OnExit (行动 exitAction) — — 导致操作时的状态被退出,要执行的。

将状态机的异常,如果在一个国家没有有效的配置触发的触发器。这是搜索状态的配置:

Configure(States.Searching)
  .OnEntry(searchAction)
  .Permit(Triggers.SearchSucceeded, States.SearchComplete)
  .Permit(Triggers.SearchFailed, States.Start)
  .Ignore(Triggers.Select)
  .Ignore(Triggers.DeSelect);

OnEntry 行动执行搜索过程和许可证允许有关触发器的触发。忽略防止发射选择视图,然后取消选择触发器,当 DataGrid 控件绑定到基础数据源 (与 WPF 中的大多数列表控件发生烦恼)。

状态机也暴露出两种重要方法:

  • 无效火 (TTrigger) — — 这过渡状态机使用以前的配置。
  • bool CanFire (触发器触发器) — — 此方法返回 true,如果当前状态允许触发器将被解雇。

这些都是创建命令所需的关键方法。他们执行的执行,并且可以执行逻辑。

将命令绑定到状态机

MVVM 模式公开在 ViewModel 上实现 ICommand 接口的属性。创建此命令属性现在是一个简单的将其执行和 CanExecute 的方法分别绑定到状态机火和 CanFire 的方法,问题。创建一个扩展方法来集中这种逻辑:

public static ICommand CreateCommand<TState, TTrigger>(
  this StateMachine<TState, TTrigger> stateMachine, TTrigger trigger)
    {
      return new RelayCommand
        (
          () => stateMachine.Fire(trigger),
          () => stateMachine.CanFire(trigger)
        );
    }

一旦此扩展方法是在的地方,在 ViewModel 上创建 ICommand 属性 (回指中的流图图 2 在分析阶段确定的命令):

SearchCommand = StateMachine.CreateCommand(Triggers.Search);
EditCommand = StateMachine.CreateCommand(Triggers.Edit);
EndEditCommand = StateMachine.CreateCommand(Triggers.EndEdit);

将视图绑定到视图命令

对于作为命令源的控制,你可以有其命令属性绑定到视图模型的 ICommand 属性。示例应用程序有两个按钮和菜单项绑定到的命令属性在 ViewModel 上:

<Button ToolTip="Search" VerticalAlignment="Center" 
  Style="{StaticResource ButtonStyle}"
  Command="{Binding SearchCommand}">
  <Image Source="Images\Search.png"></Image>
</Button>

现在该命令是直接绑定到状态机。它将触发配置的触发器执行时,但更重要的是,它将被禁用如果该触发器不允许当前状态。这种呈现的棘手的护理可以执行逻辑,并在状态机中都整齐地配置。图 3 显示员工管理器屏幕,而是忙着寻找。注意搜索命令被禁用作为搜索触发器所不允许的搜索状态。

忙动画在搜索对话框
图 3 忙动画在搜索对话框

其他优点

一旦你已经实现了状态机,也可以绑定到其状态的其他视觉元素。当搜索功能正忙于执行 (并保持铭记,这可能是一个长时间运行的操作,在异步方法中),是可取的以向用户显示一个繁忙的指标。图 3 显示动画的图像,仅当状态机是在搜索状态。做到这一点通过创建一个状态机状态转换为可见性值的自定义转换器:

public class StateMachineVisibilityConverter : IValueConverter
  {
    public object Convert(object value, Type targetType,
      object parameter, CultureInfo culture)
    {
      string state = value != null ? 
        value.ToString() : String.Empty;
      string targetState = parameter.ToString();
      return state == targetState ? 
        Visibility.Visible : Visibility.Collapsed;
    }
 }

动画的图像然后绑定到使用自定义转换器的状态机状态:

<local:AnimatedGIFControl Visibility="{Binding StateMachine.State,
                          Converter={StaticResource StateMachineConverter},
                          ConverterParameter=Searching}"/>

您可以对多在员工管理器屏幕中所示的编辑对话框应用同样的原则图 4。当状态机处于编辑状态时,对话框中将变为可见,用户可以编辑所选的员工的详细信息。

编辑对话框状态
图 4 编辑对话框状态

心态

状态机模式不仅解决了与命令逻辑相关的问题,而且还会创建一种允许更好地应用分析的心理状态。很容易想到的命令作为执行某种形式的逻辑的控件。什么往往被忽视,这确定何时允许要执行的命令。命令状态机模式解决了这些问题早就在设计过程中。他们自然状态机配置的一部分。国家机器已经简化了我的 ViewModel 代码,并能做为您的应用程序相同的。

其他参考

  • 计算机状态: 这是出奇地难以找到精心撰写的简介状态机,但游戏开发商鲍勃奈斯特龙写了一个非常好 (bit.ly/1uGxVv6)。
  • WPF 使用 MVVM: WPF 使用 MVVM 约什-史密斯的"父亲"提供了 ICommand 接口,RelayCommand,在 2009 年 2 月执行 MSDN 杂志 条 (msdn.microsoft.com/magazine/dd419663)。
  • 命令: WPF 指挥官方 MSDN 文档讨论了路由的命令和事件是.NET 框架的一部分 (bit.ly/1mRCOTv)。
  • 路由的命令: 布莱恩 · 诺伊斯涵盖路由的命令在 2008 年 9 月 MSDN 杂志条。它是一个很好的起点,了解路由的命令和 MVVM 命令模式之间的区别 (bit.ly/1CihBVZ)。
  • 命令、 RelayCommands 和 EventToCommand: Laurent Bugnion 2013 年 5 月文章介绍如何将事件转换为命令,这对于不实现 ICommandSource 接口的控件很有用 (msdn.microsoft.com/magazine/dn237302)。
  • 棱镜框架: 棱镜框架的官方文档讨论 MVVM 模式和 DelegateCommand (bit.ly/1k2Q6sY)。
  • NuGet 无国籍的包装: NuGet Web 站点上的无国籍的软件包都带有下载说明 (bit.ly/1sXBQl2)。

Tarquin Vaughan-Scott 是在 InfoVest 的开发组长 (infovest.co.za),在开普敦,南非,造成金融业的数据管理解决方案的基础。他主要的兴趣是数据库的设计和体系结构 — —­尤其是事务性之间的差异和数据仓库系统。联系到他在 tarquin@infovest.co.za

衷心感谢以下 Microsoft 技术专家对本文的审阅:尼古拉斯 · Blumhardt
尼古拉斯 · Blumhardt (无状态) 是微软的前雇员,作为 MEF 团队的一部分