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

CommandTree - DelegateCommands 的改进 CommandMap

starIconstarIconstarIconstarIconstarIcon

5.00/5 (3投票s)

2016年1月14日

CPOL

7分钟阅读

viewsIcon

13497

downloadIcon

215

将 DelegateCommands 有组织地分层放置在一个 ViewModel 对象中 - 一个 Style 可以从中生成一个合适的菜单。您还可以为按钮或菜单项设置单独的 CommandBindings。

引言

几天前,我接触到了CommandMap类,它由 Robert Winkler 在此处发布。它使用了TypeDescriptionProviderAttribute以及一些稍微繁琐的技巧,来实现 XAML 将 DelegateCommands 视为可绑定属性,尽管它们不是属性,只是存在于 Dictionary 中。

我采纳了这个基本思想,即一个集合可以将其每个元素发布为不同的可绑定属性。然后我开发了自己的 CommandHelpers

概念

我扩展了(希望)大家熟知的 Delegate-Command 类,增加了 HeaderIDChildren 等 ViewModel 风格的属性。我将新类命名为 TreeCommand
现在,一个 ButtonMenuItem 可以简单地将其 Header(或 Button 的 Content)绑定到它所绑定的 TreeCommandHeader。在 Xaml 设计器中,您可以指定一个 ID,该 ID 用于寻址 CommandTree 中的特定 Command。("CommandTree" 我命名了这个 TreeCommands 的容器类)

更方便的是,MenuItemItemsSource 属性可以绑定到 Commands 的 Children。
实际上,在 XAML 中,您不再需要设置任何 MenuItem。只需将 Menu 整体绑定到 CommandTreeBindingSystem 就会根据 CommandTree 对象中定义的层次结构,设置完整的 MenuItem 层次结构。

注意:这是两种独立的方式来访问 TreeCommands:要么让 XAML 生成完整的 MenuItem 树,要么通过 ID 单独寻址它们。

在示例源代码中,我还将 CommandTree 绑定到了一个通用的 Treeview - 这稍微说明了层次化 ID(TreeNode 左侧的“数字”)的内部逻辑。

层次化 ID

ID 是一个 string(不是数字!),它表示节点的索引。每个 char 代表一个层级。例如,“13”寻址树中的第二个节点,然后是该节点的第四个子节点(标题为“Command1”)。
为了使超过 10 个节点可访问,字母也有效,例如,'a' 寻址第 10 个子节点,'z' 寻址第 36 个子节点。

我不相信会有人创建菜单,其中单个项目包含超过 36 个子菜单,但这没问题:逻辑并没有在 'z' 处停止,而是继续向上直到 char.MaxValue
这意味着:单个父 TreeCommand 中直接 SubCommand 的数量限制为 65438 - 更多项目将会超出当前实现的 ID 生成能力。
注意:此“限制”仅指*直接*子项 - 嵌套子项不在此列。

从方法名派生标题

我创建了许多重载,以使 CommandTree 的构建尽可能简单。一些重载允许省略 Command-Header 的指定 - 在这种情况下,它会自动从 DelegateCommand 将要执行的方法名派生出标题 - 示例将在“flow-interface”部分中给出。
题外话:对于匿名方法,它不是那么容易表示,但至少是有效的。)

高级标题

我将 TreeCommand.Header 属性设计为 Object 类型。通常,它会包含一个 string,但您可以放入任何您想要的内容。而 Xaml 样式和模板为发挥创意提供了巨大的空间。
(但我很佩服:我还没有亲自尝试过。)

流程接口设计

通常,当您使一个 setter 方法返回它自身的对象时,就会出现流程接口设计。请参见 StringBuilder 类:Append/Insert/Replace 方法都返回所操作的 StringBuilder 本身,因此您可以在一行中连接多个操作。

sb.AppendLine("Hello folks!").Append("my name is ").AppendLine
(System.Environment.MachineName).AppendLine("I am the third line.");

(您可以这样做,但并非必须;))

同样的原理也适用于我的 TreeCommand

root.AddFlow(LoadDataset).AddFlow(SaveDataset, () => myDataset.HasChanges());

这会设置两个命令,第二个命令包含一个匿名的 canExecute-Delegate

但我将它发挥到了极致,通过滥用重载的索引器作为流程设计添加方法,从而摆脱了输入方法名的需要。

var secondCommand = root[LoadDataset]["_ChildMenus..."][UpdateEmployees, 
() => lineCount() > 2]["Save", SaveDataset, () => lineCount() > 4][1];

那个例子显示了在单行中添加 ChildCommands 的四种不同方法。请注意,最后一个索引执行的不是流程接口,而是*真正的*索引,用于检索第二个 TreeCommand(标记为 "_ChildMenus...")。

无论您是否喜欢,如果不喜欢,您都可以删除实现(或“犯下”)此功能的代码 ;) 。

可重用样式

如果您编写(或复制)一些可重用的样式,就可以大大简化到 CommandTree 的绑定,这些样式将承担大部分(样板)工作。

<Style TargetType="MenuItem">
   <!-- derive Header from Command -->
   <Setter Property="Header" Value="{Binding Command.Header, 
    RelativeSource={RelativeSource Self},FallbackValue=##Err##}"/>
</Style>

上面的代码自动将 MenuItem-Header 绑定到 TreeCommand-Header,此外,它还会显示 GUI 上的 Binding-Mismatches
由于这在 DesignTime(设计时)就已经有效,当您尝试绑定到无效 ID 时,您会在 Visual-Preview 中获得直接反馈。

在上述 MenuItem-Style 的基础上,一个 MenuItem 只需绑定其 Command 即可完成,例如,在第一个 TreeCommand0)中的第三个 TreeCommand2)中的 CommandTreeCommands)。

 <MenuItem  Command="{Binding Commands.02}"/>

不用担心 ID 不会透明化,而且您不知道实际寻址的命令。如前所述,Xaml-EditorVisual-Preview 会显示命令的 Header
(您可以尝试任意 ID - 预览都会忠实地跟随着您。)

(有一个相应的按钮样式,此处不再赘述。)

接下来是设置完整的 Menu 与您的 CommandTree 的样式。

<Style x:Key="commandTreeMenu" TargetType="Menu">
  <Setter Property="ItemContainerStyle" >
    <Setter.Value>
      <Style TargetType="MenuItem">
        <!-- request Header from TreeCommand -->
        <Setter Property="Header" Value="{Binding Command.Header, 
         RelativeSource={RelativeSource Self},FallbackValue=##Err##}"/>
        <!-- request ItemsSource (SubMenuItems) from TreeCommand -->
        <Setter Property="ItemsSource" 
         Value="{Binding Command, RelativeSource={RelativeSource Self}}"/>
      </Style>
    </Setter.Value>
  </Setter>
</Style>

现在,这个单行代码就设置好了完整的 Menu。

<Menu ItemsSource="{Binding Commands}" Style="{StaticResource commandTreeMenu}"/>

我再说一遍:您不再需要触碰这个 Menu。ViewModel 中对 CommandTree 的所有更改都会同时转换为 XAML 中 Menu 的更改(除非重命名 CommandTree-Property 本身)。

PropertyDescription 魔术

CommandTrees 的职责之一是派生 CustomTypeDescriptor,覆盖 GetProperties() 方法,并检索一组 PropertyDescriptors(每个属性一个描述符)。数据绑定会获取这些 PropertyDescriptors,并使用它们来访问属性(即 TreeCommands)。请参阅提到的 Override

public class CommandTree : CustomTypeDescriptor, IEnumerable<TreeCommand> {
   //...

   /// <summary> for internal use, to support Databinding </summary>
   [EditorBrowsable(EditorBrowsableState.Never)]
   public override PropertyDescriptorCollection GetProperties() {
      //unconventional, but efficient: the PropertyDescriptors are Fields of the TreeCommand-Instances
      return new PropertyDescriptorCollection(SetupIDs().Select(cmd => cmd.Descriptor).ToArray());
   }

嗯——那么 PropertyDescriptor 看起来是怎样的呢?
同样,存在一个 abstract 基类,我可以从中派生,并且只需要进行绝对必要的覆盖。

publicclass DelegatePropertyDescriptor : PropertyDescriptor {

   private Func<string> _GetName; private Func<object> _GetValue;
   
   public DelegatePropertyDescriptor(Func<string> getName, Func<object> getValue)
      : base(getName(), null) {
      if (getName == null || getValue == null) throw new ArgumentNullException();
      _GetName = getName; _GetValue = getValue;
   }
   public override bool IsReadOnly { get { return true; } }
   public override bool CanResetValue(object component) { return false; }
   public override Type ComponentType { get { throw new NotImplementedException(); } }
   public override object GetValue(object component) { return _GetValue(); }
   public override string Name { get { return _GetName(); } }
   public override Type PropertyType { get { return _GetValue().GetType(); } }
   public override void ResetValue(object component) { throw new NotImplementedException(); }
   public override void SetValue(object component, object value) { throw new NotImplementedException(); }
   public override bool ShouldSerializeValue(object component) { return false; }
}

如果您仔细看,就会发现这个 PropertyDescriptor 对它所描述的属性一无所知! :wtf
它只包含两个委托(第 3 行),并在相关的覆盖 (#13-#15) 中,它将 Binding-System 的请求重定向到其他地方。

“其他地方”的意思是:指向拥有自身 DelegatePropertyDescriptor 实例的特定 TreeCommand 实例 - 请参见 TreeCommand 构造函数。

public TreeCommand(object header, Action executeMethod, Func<bool> canExecuteMethod = null)
   : base(executeMethod, canExecuteMethod) {
   // ...
   Descriptor = new DelegatePropertyDescriptor(() => ID, () => this);
}
public readonly DelegatePropertyDescriptor Descriptor;

() => ID, () => this - 这就是魔术的精髓。

它说明:“亲爱的描述符,请告诉绑定系统,存在一个属性,并将我的 ID 作为其名称。
该属性的值就是我自己。

很奇怪,不是吗?
请记住,在上面两个列表之前,CommandTree 如何收集其所有 TreeCommandsPropertyDescriptors,作为 GetProperties() 覆盖的返回值。
这意味着,拥有所有这些属性的是*CommandTree*。

这就是为什么 BindingSystem 接受这些 ID 作为 Binding-Path 的原因。 :)

PropertyDescription 的缺点

常规属性在 Xaml-Editor-PropertyGrid 和 IntelliSense 中是已知的,这非常有帮助:当您设置绑定时,您可以遵循 IntelliSense 的建议,或者直接从“CreateBindings”对话框中选择绑定。
不幸的是,这不支持 PropertyDescriptor 的“PseudoProperties” - Xaml-Designer 不知道它们。

但尤其是对于 CommandTree,这种不足并非那么严重:首先,CommandTree-Object 本身作为一个常规的 Viewmodel-Property 是可见的,因此完全绑定 Menu 不受影响。

即使您想绑定到特定的 ID,ID 的“类似数字”的逻辑也提供了可行的方向。

顺便说一句,这个缺陷从一开始(在检查原始 CommandMap 时)就让我恼火,为了展示区别,我编写了一个 CommandMap2,其中包含两个硬编码的命令。打开 Databinding-Editor 时,CommandMap2 可以展开以检查其子属性,而 CommandMap 则不可展开 - 见图。

最后,有人给了我链接,上面是如何在 MSDN 的注释 中记录的。

结论

尤其是对于标准菜单,CommandTree-Class 提供了一种“新外观和感觉”,因为控件完全转移到了 Viewmodel 中,而在 XAML 中,则无需再做任何事情了。
但即使在设计特定寻址命令时,它也并非毫无用处 ;) 。
请注意,这些类尚未完美实现 - 我看到还有一些进一步的需求即将到来。

  • 首先是支持 CommandParameters,甚至可能是类型化的 Params。
  • 图标
  • 工具提示

另一个可能是,我的代码设计总体上可以改进。也许“组合优于继承”的模式可以作为一种更好的方法来应用,而不是派生 DelegateCommand 并用上述所有东西来增强它。

也许不是 - 我还没有尝试过。一个(已实现的)目标是支持在单个代码语句中配置命令到方法。
我不知道这是否可能通过组合模式实现 - 我没有尝试过。

所以,请将本文视为介绍和概念验证,而不是一个永恒不变的完美设计解决方案 ;) 。

© . All rights reserved.