CommandTree - DelegateCommands 的改进 CommandMap





5.00/5 (3投票s)
将 DelegateCommands 有组织地分层放置在一个 ViewModel 对象中 - 一个 Style 可以从中生成一个合适的菜单。您还可以为按钮或菜单项设置单独的 CommandBindings。
引言
几天前,我接触到了CommandMap
类,它由 Robert Winkler 在此处发布。它使用了TypeDescriptionProviderAttribute
以及一些稍微繁琐的技巧,来实现 XAML 将 DelegateCommand
s 视为可绑定属性,尽管它们不是属性,只是存在于 Dictionary
中。
我采纳了这个基本思想,即一个集合可以将其每个元素发布为不同的可绑定属性。然后我开发了自己的 CommandHelpers
。
概念
我扩展了(希望)大家熟知的 Delegate-Command 类,增加了 Header
、ID
和 Children
等 ViewModel 风格的属性。我将新类命名为 TreeCommand
。
现在,一个 Button
或 MenuItem
可以简单地将其 Header
(或 Button 的 Content
)绑定到它所绑定的 TreeCommand
的 Header
。在 Xaml 设计器中,您可以指定一个 ID
,该 ID 用于寻址 CommandTree
中的特定 Command
。("CommandTree
" 我命名了这个 TreeCommand
s 的容器类)
更方便的是,MenuItem
的 ItemsSource
属性可以绑定到 Commands 的 Children。
实际上,在 XAML 中,您不再需要设置任何 MenuItem
。只需将 Menu
整体绑定到 CommandTree
,BindingSystem
就会根据 CommandTree
对象中定义的层次结构,设置完整的 MenuItem
层次结构。
注意:这是两种独立的方式来访问 TreeCommand
s:要么让 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];
那个例子显示了在单行中添加 ChildCommand
s 的四种不同方法。请注意,最后一个索引执行的不是流程接口,而是*真正的*索引,用于检索第二个 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
即可完成,例如,在第一个 TreeCommand
(0
)中的第三个 TreeCommand
(2
)中的 CommandTree
(Commands
)。
<MenuItem Command="{Binding Commands.02}"/>
不用担心 ID 不会透明化,而且您不知道实际寻址的命令。如前所述,Xaml-Editor
的 Visual-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()
方法,并检索一组 PropertyDescriptor
s(每个属性一个描述符)。数据绑定会获取这些 PropertyDescriptor
s,并使用它们来访问属性(即 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
如何收集其所有 TreeCommands
的 PropertyDescriptors
,作为 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
并用上述所有东西来增强它。
也许不是 - 我还没有尝试过。一个(已实现的)目标是支持在单个代码语句中配置命令到方法。
我不知道这是否可能通过组合模式实现 - 我没有尝试过。
所以,请将本文视为介绍和概念验证,而不是一个永恒不变的完美设计解决方案 ;) 。