WPF 中的智能路由命令






4.67/5 (24投票s)
为路由命令提供默认执行逻辑。
引言
本文将介绍如何为路由命令嵌入一些智能。当 UI 中没有元素知道命令执行时,这种智能可以作为备用方案。它还允许您将最常见的命令执行逻辑封装到命令本身中,从而更易于重用。
背景
WPF 中的命令系统基于 ICommand 接口。任何实现该接口的对象都可以被视为一个命令。您可以创建一个实现该接口并包含逻辑的类型,以确定命令是否可以执行以及执行时应做什么。这为 WPF 开发人员提供了一种方便的机制来封装操作,并允许 WPF 使用它。
通常,执行命令的含义可能会因运行时上下文而异;例如,某个域对象上的属性是否返回值,或者 CheckBox
是否被选中,等等。在这种情况下,将逻辑封装在命令对象中并不总是明智的,因为它可能需要对执行命令的 UI 有深入的了解。
这就是 WPF 拥有路由命令系统的原因。它允许将命令的实际执行逻辑委托给元素树中的任意节点。当执行路由命令时,通过将某个路由事件冒泡到元素树中,它允许树中的任何元素说:“嘿,我知道执行此命令时该怎么做。我来处理它。” 矛盾的是,路由命令非常有用,因为它们本身不做任何事情。它们所做的只是在执行时通过元素树发送通知,让其他元素有机会执行其命令执行逻辑的自定义版本(并允许其他元素决定命令是否可以执行)。
要了解有关 WPF 命令系统的更多信息,请阅读 SDK 中的此页面。
问题
假设我正在创建一个显示网页的命令。大多数情况下,我希望在用户的默认 Web 浏览器中显示该网页。我不想强制要求以任何特定方式显示该页面,因此该命令应该是一个路由命令。这将允许使用该命令的 UI 来确定如何显示网页,也许是在应用程序的 Frame
元素中。由于我们的命令可能会在许多 UI 甚至许多应用程序中使用,因此提供这种灵活性非常重要。
现在我们遇到了一个有趣的问题。我们希望命令是路由的,以便以灵活的方式使用它,但我们也希望提供标准的执行逻辑,该逻辑会在用户的默认浏览器中打开一个网页。如果路由命令未被树中的任何元素处理,则应使用标准执行逻辑作为备用计划。
不幸的是,RoutedCommand
类没有公开任何我们可以覆盖以指定默认执行逻辑的虚拟方法。我们不能简单地继承 RoutedCommand
,覆盖几个方法,然后就完成了。
我们如何创建一个具有“默认”行为的路由命令?
解决方案
我创建了 RoutedCommand
的一个子类,名为 SmartRoutedCommand
。该类公开了一个名为 IsCommandSink
的附加 Boolean
属性。当您在 UI 中使用 SmartRoutedCommand
时,必须在元素树的根元素上将 IsCommandSink
设置为 true
。这样做可以使 SmartRoutedCommand
子类在路由命令执行通知未被元素树处理时执行其默认执行逻辑。
WPF 路由事件系统的架构师一定吃过“来自原始丛林深处,由危地马拉精神病院囚犯种植的辣椒”。他们使路由事件如此灵活,以至于我们可以用很少的代码解决这样的难题。这是整个 SmartRoutedCommand
类。请密切注意 OnIsCommandSinkChanged
方法。
/// <summary>
/// This abstract class is a RoutedCommand which allows its
/// subclasses to provide default logic for determining if
/// they can execute and how to execute. To enable the default
/// logic to be used, set the IsCommandSink attached property
/// to true on the root element of the element tree which uses
/// one or more SmartRoutedCommand subclasses.
/// </summary>
public abstract class SmartRoutedCommand : RoutedCommand
{
#region IsCommandSink
public static bool GetIsCommandSink(DependencyObject obj)
{
return (bool)obj.GetValue(IsCommandSinkProperty);
}
public static void SetIsCommandSink(DependencyObject obj, bool value)
{
obj.SetValue(IsCommandSinkProperty, value);
}
/// <summary>
/// Represents the IsCommandSink attached property. This is readonly.
/// </summary>
public static readonly DependencyProperty IsCommandSinkProperty =
DependencyProperty.RegisterAttached(
"IsCommandSink",
typeof(bool),
typeof(SmartRoutedCommand),
new UIPropertyMetadata(false, OnIsCommandSinkChanged));
/// <summary>
/// Invoked when the IsCommandSink attached property is set on an element.
/// </summary>
/// <param name="depObj">The element on which the property was set.</param>
/// <param name="e">Information about the property setting.</param>
static void OnIsCommandSinkChanged(
DependencyObject depObj, DependencyPropertyChangedEventArgs e)
{
bool isCommandSink = (bool)e.NewValue;
UIElement sinkElem = depObj as UIElement;
if (sinkElem == null)
throw new ArgumentException("Target object must be a UIElement.");
if (isCommandSink)
{
CommandManager.AddCanExecuteHandler(sinkElem, OnCanExecute);
CommandManager.AddExecutedHandler(sinkElem, OnExecuted);
}
else
{
CommandManager.RemoveCanExecuteHandler(sinkElem, OnCanExecute);
CommandManager.RemoveExecutedHandler(sinkElem, OnExecuted);
}
}
#endregion // IsCommandSink
#region Static Callbacks
static void OnCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
SmartRoutedCommand cmd = e.Command as SmartRoutedCommand;
if (cmd != null)
{
e.CanExecute = cmd.CanExecuteCore(e.Parameter);
}
}
static void OnExecuted(object sender, ExecutedRoutedEventArgs e)
{
SmartRoutedCommand cmd = e.Command as SmartRoutedCommand;
if (cmd != null)
{
cmd.ExecuteCore(e.Parameter);
e.Handled = true;
}
}
#endregion // Static Callbacks
#region Abstract Methods
/// <summary>
/// Child classes override this method to provide logic which
/// determines if the command can execute. This method will
/// only be invoked if no element in the tree indicated that
/// it can execute the command.
/// </summary>
/// <param name="parameter">The command parameter (optional).</param>
/// <returns>True if the command can be executed, else false.</returns>
protected abstract bool CanExecuteCore(object parameter);
/// <summary>
/// Child classes override this method to provide default
/// execution logic. This method will only be invoked if
/// CanExecuteCore returns true.
/// </summary>
/// <param name="parameter">The command parameter (optional).</param>
protected abstract void ExecuteCore(object parameter);
#endregion // Abstract Methods
}
这里的基本思想是,当 IsCommandSink
附加属性在一个元素上设置时,我们将一个处理程序添加到 CommandManager
的 CanExecute
和 Executed
附加事件。当路由命令被查询以确定它是否可以执行以及何时已执行时,这些事件会分别冒泡到元素树中。
当这些事件在该元素上引发时,SmartRoutedCommand
中的事件处理方法将被调用,从而允许我们调用子类覆盖以提供其默认执行逻辑的抽象方法。这种技术就像让元素树的根将命令通知转发给它正在被使用但没有人知道如何处理,以便我们可以使用子类的默认逻辑。
演示应用程序
本文附带了一个演示应用程序,展示了如何使用 SmartRoutedCommand
。演示应用程序允许您键入一些关键字,然后单击按钮以使用这些关键字搜索 Google。它包含一个名为 OpenWebPageCommand
的命令,该命令派生自 SmartRoutedCommand
。UI 允许您打开一个自定义 Web 浏览器(由演示应用程序的 Window
创建),或者使用您的默认 Web 浏览器(由命令本身打开)。
下面是演示应用程序的屏幕截图

这是 OpenWebPageCommand
/// <summary>
/// A routed command which knows how to open a Web page in a browser.
/// </summary>
public class OpenWebPageCommand : SmartRoutedCommand
{
/// <summary>
/// Singleton instance of this class.
/// </summary>
public static readonly ICommand Instance = new OpenWebPageCommand();
private OpenWebPageCommand() { }
protected override bool CanExecuteCore(object parameter)
{
string uri = parameter as string;
if (uri == null)
return false;
bool isUriValid = Uri.IsWellFormedUriString(uri, UriKind.Absolute);
bool haveConnection = NetworkInterface.GetIsNetworkAvailable();
return isUriValid && haveConnection;
}
protected override void ExecuteCore(object parameter)
{
string uri = parameter as string;
Process.Start(uri);
}
}
下面显示了使用该命令的 Window
的 XAML。请注意 IsCommandSink
属性是如何在 Window
上设置的,这使得 OpenWebPageCommand
的默认执行逻辑得以使用的。
<Window
x:Class="SmartRoutedCommandDemo.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:SmartRoutedCommandDemo"
Title="SmartRoutedCommand Demo"
FontSize="12"
Width="300" Height="140"
WindowStartupLocation="CenterScreen"
local:SmartRoutedCommand.IsCommandSink="True"
>
<StackPanel Margin="2">
<StackPanel.Resources>
<local:KeywordsToGoogleSearchConverter x:Key="googleSearchConv" />
</StackPanel.Resources>
<StackPanel.CommandBindings>
<CommandBinding
Command="{x:Static local:OpenWebPageCommand.Instance}"
CanExecute="OnCanCmdExecute"
Executed="OnCmdExecuted"
/>
</StackPanel.CommandBindings>
<TextBlock Text="Enter Keywords:" />
<TextBox x:Name="txtKeywords" Margin="0,4" />
<Button
Command="{x:Static local:OpenWebPageCommand.Instance}"
CommandParameter="{Binding
Converter={StaticResource googleSearchConv},
ElementName=txtKeywords,
Mode=OneWay,
Path=Text}"
HorizontalAlignment="Right"
IsDefault="True"
>
Google It!
</Button>
<CheckBox
x:Name="chkUseCustomBrowser"
IsChecked="False"
Margin="0,20,0,0"
>
Use Custom Web Browser
</CheckBox>
</StackPanel>
</Window>
请注意,StackPanel
为 OpenWebPageCommand
具有 CommandBinding
。要确定 StackPanel
是否应处理命令执行,WPF 命令系统会在各种时间调用以下方法:
void OnCanCmdExecute(object sender, CanExecuteRoutedEventArgs e)
{
// Only execute this Window's custom command logic if the
// CheckBox is checked. Otherwise let the default logic
// of OpenWebPageCommand execute.
bool useCustomBrowser =
this.chkUseCustomBrowser.IsChecked.GetValueOrDefault();
if(useCustomBrowser)
{
// Assume we have an internet connection,
// just to keep this demo simple. By marking
// CanExecute as true, this element will be
// asked to execute the command.
e.CanExecute = true;
}
}
如果事件参数的 CanExecute
属性未设置为 true
,则 CanExecute
路由事件会一直冒泡到元素树,直到最终 Window
将通知转发给 OpenWebPageCommand
本身。届时将使用命令的内置逻辑。
结论
通过使用 SmartRoutedCommand
类,您可以两全其美;具有默认执行逻辑的路由命令。这种技术并不总是适用的,因为有些命令没有合理的“默认操作”。但是,如果您发现自己为同一个路由命令多次实现相同的命令执行逻辑,可以考虑使用 SmartRoutedCommand
将该逻辑整合到一个方便的位置。