WPF 命令无处不在






4.85/5 (18投票s)
本文介绍了在不支持命令的控件中使用 WPF 命令的技巧。
引言
在过去的几周里,我开始着手一个基于 WPF 和 Prism 的非常有趣的 UI 基础结构项目。由于项目有些保密,我无法透露太多细节,但我希望分享一个关于 WPF 命令的出色技术。命令是一个在 WPF 中非常有趣的话题,尤其是在处理 WPF 组合应用程序(也称为 Prism)时。
如果你有机会使用 WPF,你可能会想,为什么只有少数控件:ButtonBase、Hyperlink 和 MenuItem 实现 ICommandSource
接口。
那么问题来了:“如何才能扩展其他控件,例如 Selector,来执行 WPF 和 Prism(自定义)命令?”
为了回答这个问题,让我们来定义问题并提供功能需求。
问题
无论是处理组合 UI 应用程序还是单体 UI 应用程序,将用户界面触发的应用程序域操作视为命令是合理的,这样它们就可以被 Presentation Model 或 Controller 处理,并绑定到可用状态。例如,对于一个 ListBox
,我们希望在选择某个项时,它能执行一个命令从服务获取该项的其余详细信息。在 WPF 中实现这一目标的常用方法是通过 XAML 注册 Selector.SelectionChanged
路由事件,在 XAML 后面的 C# 文件中处理它,并将调用委托给 Presentation Model 或 Controller。这种方法不仅复杂,而且在设计上也不合适。但设计问题我们留到以后讨论。
从上面的例子中提取至少一个需求,我们希望 Selector 控件在 Selector.SelectionChanged
路由事件触发时能够执行一个命令。
解决这个问题有很多方法,每种方法都有其优点和缺点。
解决方案
自定义控件
一种直接的方法是创建一个新的自定义控件,例如:CommandListBox
,实现 ICommandSource
并在项选择时执行命令。
尽管这个解决方案相当合理,但它需要为每种不原生支持命令的控件类型创建一个自定义控件。
附加属性
因此,让我们看看另一种方法。如果你熟悉出色的 WPF 附加属性机制,你可以按以下方式解决这个问题:
<ListBox
local:CommandProvider.Command="{x:Static local:CommonCommands.Do}"
local:CommandExtender.Handler="{x:Static
local:CommandHandlers.SelectorSelect}"
local:CommandExtender.Parameter="{Binding Path=/}" />
在这种情况下,CommandProvider
是一个 static
类,它通过附加属性提供命令服务,其中:
- Command 是要执行的命令实例
- Handler 是一个自定义类型实例,它提供执行行为,例如“在
Selector.SelectionChanged
事件上执行命令” - Parameter 是命令参数
如你所见,这种方法更加灵活和可扩展,因为它可用于任何 UIElement
,并且无需创建不必要的自定义控件。
多个命令
由于 ICommandSource
的自定义实现和附加属性方法都一次只支持一个命令,并且只由一种行为执行,因此我决定扩展附加属性方法以支持执行一种以上行为的多种命令。
<ListView x:Name="list" ItemsSource="{Binding Emails}"
IsSynchronizedWithCurrentItem="True">
<ts:CommandSource.Trigger>
<ts:CommandTriggerGroup>
<ts:EventCommandTrigger
RoutedEvent="UIElement.PreviewMouseLeftButtonUp"
Command="{Binding Path=DownloadEmail}"
CustomParameter="{Binding ElementName=list,
Path=SelectedValue}" />
<ts:EventCommandTrigger
RoutedEvent="UIElement.PreviewMouseRightButtonUp"
Command="{Binding Path=MarkAsRead}"
CustomParameter="{Binding ElementName=list,
Path=SelectedValue}" />
<ts:EventCommandTrigger
RoutedEvent="UIElement.PreviewMouseLeftButtonDown"
Command="{Binding Path=OpenEmail}"
CustomParameter="{Binding ElementName=list,
Path=SelectedValue}" />
</ts:CommandTriggerGroup>
</ts:CommandSource.Trigger>
</ListView>
...
<Expander IsExpanded="{Binding Path=DummyProperty}" Header="Contact">
<ts:CommandSource.Trigger>
<ts:PropertyCommandTrigger
Property="Expander.IsExpanded"
Value="True"
CustomParameter="{Binding}"
Command="{Binding Path=DownloadContact,
RelativeSource={RelativeSource
Mode=FindAncestor, AncestorType=Window}}" />
</ts:CommandSource.Trigger>
...
</Expander>



我在上面的标记代码中用 CommandSource
替换了 CommandProvider
,并将三个附加属性简化为一个:Trigger
。真正的区别在于 Trigger
附加属性的类型是 ICommandTrigger
。
ICommandTrigger
接口由三个类实现:EventCommandTrigger
、PropertyCommandTrigger
和CommandTriggerGroup
。EventCommandTrigger
– 在路由事件触发时执行命令。PropertyCommandTrigger
– 当依赖属性更改且满足特定值时执行命令。CommandTriggerGroup
– 代表一个命令集合。使用上面所示的类,你可以附加多个命令触发器。
请注意,EventCommandTrigger
和 PropertyCommandTrigger
都派生自 WPF 的 Freezable 类型。这提供了一个选项,可以将其绑定到视觉树中的元素。至于 CommandTriggerGroup
,我使用了 FreezableCollection
作为其基类。
命令和命令参数
由于 CommandTrigger
将路由事件和依赖属性值转换为命令,因此应该有一种简单的方法将路由事件和属性值以及另一个用户参数作为一个命令的参数。为了处理这种情况,我创建了 CommandParameter
类型。
OpenEmail = new RoutedCommand();
CommandBinding cmdBinding3 = new CommandBinding(OpenEmail);
cmdBinding3.Executed += (s, e) =>
{
var parameter = EventCommandParameter<EmailMessage,
MouseButtonEventArgs>.Cast(e.Parameter);
if (parameter.EventArgs.ClickCount == 2)
{
parameter.CustomParameter.MarkAsRead();
MessageBox.Show(parameter.CustomParameter.Content,
parameter.CustomParameter.Subject);
}
};
cmdBinding3.CanExecute += (s, e) =>
{
e.CanExecute = true;
};
CommandBindings.Add(cmdBinding3);
EventCommandParameter
和 PropertyCommandParameter
类型都派生自 CommandParameter
类型。你可以将这些类型视为对路由事件或依赖属性和自定义参数的简单包装。从上面的示例中,你可以看到每种 CommandParameter
类型都有一个特殊的 Cast<T1, T2>
辅助方法。这简化了自定义参数和路由事件参数或依赖属性值的显式转换操作。
结论
现在我们可以随处使用命令了,我们可以只使用 Data Templates 作为 Presentation Model 的视图。这种机制特别适用于组合应用程序,因为演示者通常布局在区域中,而 Data Templates 则生成视图。
你可以从 这里 下载完整代码。
历史
- 2009 年 4 月 23 日:初次发布