首先,如果你想绑定一个 RoutedUICommand 很简单——只需添加到 UIElement.InputBindings 集合中:
<TextBox ...>
<TextBox.InputBindings>
<KeyBinding
Key="Q"
Modifiers="Control"
Command="my:ModelAirplaneViewModel.AddGlueCommand" />
当您尝试设置 Command="{Binding AddGlueCommand}" 以从 ViewModel 获取 ICommand 时,您的麻烦就开始了。由于 Command 不是 DependencyProperty,因此您无法在其上设置 Binding。
您的下一个尝试可能是创建一个附加属性 BindableCommand,该属性具有更新 Command 的 PropertyChangedCallback。这确实允许您访问绑定,但由于 InputBindings 集合未设置 InheritanceContext,因此无法使用 FindAncestor 查找您的 ViewModel。
显然,您可以创建一个附加属性,您可以将其应用于 TextBox,该属性将贯穿所有 InputBindings,在每个 InputBindings 上调用 BindingOperations.GetBinding 以查找命令绑定并使用显式源更新这些绑定,从而允许您执行以下操作:
<TextBox my:BindingHelper.SetDataContextOnInputBindings="true">
<TextBox.InputBindings>
<KeyBinding
Key="Q"
Modifiers="Control"
my:BindingHelper.BindableCommand="{Binding ModelGlueCommand}" />
这个附加属性很容易实现:在 PropertyChangedCallback 上,它将在 DispatcherPriority.Input 安排“刷新”并设置一个事件,以便在每次 DataContext 更改时重新安排“刷新”。然后在刚才的“刷新”代码中,只需在每个InputBinding上设置DataContext即可:
...
public static readonly SetDataContextOnInputBindingsProperty = DependencyProperty.Register(... , new UIPropetyMetadata
{
PropertyChangedCallback = (obj, e) =>
{
var element = obj as FrameworkElement;
ScheduleUpdate(element);
element.DataContextChanged += (obj2, e2) =>
{
ScheduleUpdate(element);
};
}
});
private void ScheduleUpdate(FrameworkElement element)
{
Dispatcher.BeginInvoke(DispatcherPriority.Input, new Action(() =>
{
UpdateDataContexts(element);
})
}
private void UpdateDataContexts(FrameworkElement target)
{
var context = target.DataContext;
foreach(var inputBinding in target.InputBindings)
inputBinding.SetValue(FrameworkElement.DataContextProperty, context);
}
两个附加属性的替代方法是创建一个 CommandBinding 子类,该子类接收路由命令并激活绑定命令:
<Window.CommandBindings>
<my:CommandMapper Command="my:RoutedCommands.AddGlue" MapToCommand="{Binding AddGlue}" />
...
在这种情况下,每个对象中的 InputBindings 将引用路由命令,而不是绑定。然后,此命令将向上路由到视图并映射。
CommandMapper 的代码比较简单:
public class CommandMapper : CommandBinding
{
... // declaration of DependencyProperty 'MapToCommand'
public CommandMapper() : base(Executed, CanExecute)
{
}
private void Executed(object sender, ExecutedRoutedEventArgs e)
{
if(MapToCommand!=null)
MapToCommand.Execute(e.Parameter);
}
private void CanExecute(object sender, CanExecuteRoutedEventArgs e)
{
e.CanExecute =
MapToCommand==null ? null :
MapToCommand.CanExecute(e.Parameter);
}
}
就我的口味而言,我更喜欢使用附加属性解决方案,因为它的代码不多,并且使我不必将每个命令声明两次(作为 RoutedCommand 和作为我的 ViewModel 的属性)。支持代码只出现一次,可以在您的所有项目中使用。
另一方面,如果您只做一个一次性项目并且不期望重用任何东西,那么即使是 CommandMapper 也可能是矫枉过正。正如您所提到的,可以简单地手动处理事件。