5

我正在使用 MVVM 设计模式构建应用程序,并且我想使用 ApplicationCommands 类中定义的 RoutedUICommands。由于 View 的 CommandBindings 属性(读取 UserControl)不是 DependencyProperty,我们无法将 ViewModel 中定义的 CommandBindings 直接绑定到 View。我通过定义一个抽象的 View 类来解决这个问题,该类以编程方式绑定它,基于 ViewModel 接口,该接口确保每个 ViewModel 都有一个 CommandBindings 的 ObservableCollection。这一切都很好,但是,在某些情况下,我想执行在不同类(视图和视图模型)相同命令中定义的逻辑。例如,保存文档时。

在 ViewModel 中,代码将文档保存到磁盘:

private void InitializeCommands()
{
    CommandBindings = new CommandBindingCollection();
    ExecutedRoutedEventHandler executeSave = (sender, e) =>
    {
        document.Save(path);
        IsModified = false;
    };
    CanExecuteRoutedEventHandler canSave = (sender, e) => 
    {
        e.CanExecute = IsModified;
    };
    CommandBinding save = new CommandBinding(ApplicationCommands.Save, executeSave, canSave);
    CommandBindings.Add(save);
}

乍一看,前面的代码就是我想要做的,但是文档绑定到的 View 中的 TextBox 只有在失去焦点时才更新它的 Source。但是,我可以通过按 Ctrl+S 来保存文档而不会失去焦点。这意味着文档在源中更新的更改之前保存,有效地忽略了更改。但是由于出于性能原因将 UpdateSourceTrigger 更改为 PropertyChanged 不是一个可行的选项,因此必须在保存之前强制进行更新。所以我想,让我们使用 PreviewExecuted 事件在 PreviewExecuted 事件中强制更新,如下所示:

//Find the Save command and extend behavior if it is present
foreach (CommandBinding cb in CommandBindings)
{
    if (cb.Command.Equals(ApplicationCommands.Save))
    {
        cb.PreviewExecuted += (sender, e) =>
        {
            if (IsModified)
            {
                BindingExpression be = rtb.GetBindingExpression(TextBox.TextProperty);
                be.UpdateSource();
            }
            e.Handled = false;
        };
    }
}

但是,将处理程序分配给 PreviewExecuted 事件似乎完全取消了该事件,即使我明确地将 Handled 属性设置为 false。所以我在前面的代码示例中定义的 executeSave 事件处理程序不再执行。请注意,当我将 cb.PreviewExecuted 更改为 cb.Executed 时,两段代码都会执行,但顺序不正确。

我认为这是 .Net 中的一个错误,因为您应该能够向 PreviewExecuted 和 Executed 添加一个处理程序,并让它们按顺序执行,前提是您没有将事件标记为已处理。

任何人都可以确认这种行为吗?还是我错了?这个错误有解决方法吗?

4

2 回答 2

3

编辑2:从查看源代码看来,它在内部的工作方式是这样的:

  1. 响应用户输入(鼠标或键盘)的UIElement调用。CommandManager.TranslateInput()
  2. 然后CommandManagerCommandBindings不同的层次上寻找与输入相关的命令。
  3. 当命令被找到时,它的方法CanExecute()被调用,如果它返回则被调用。trueExecuted()
  4. 如果RoutedCommand每种方法本质上都做同样的事情 - 它会引发一对附加事件CommandManager.PreviewCanExecuteEventCommandManager.CanExecuteEvent(或PreviewExecutedEventExecutedEventUIElement启动该过程。第一阶段到此结束。
  5. 现在UIElement已经为这四个事件注册了类处理程序,这些处理程序只需调用CommandManager.OnCanExecute()CommandManager.CanExecute()(对于预览和实际事件)。
  6. 仅在此处调用注册的处理程序的CommandManager.OnCanExecute()方法。如果没有找到,则将事件传输到的父级,并开始新的循环,直到处理命令或到达可视树的根。CommandManager.OnExecute()CommandBindingCommandManagerUIElement

如果您查看 CommandBinding 类源代码,则有 OnExecuted() 方法负责调用您通过 CommandBinding 为 PreviewExecuted 和 Executed 事件注册的处理程序。那里有一点:

PreviewExecuted(sender, e); 
e.Handled = true;

这会将事件设置为在 PreviewExecuted 处理程序返回后立即处理,因此不会调用 Executed。

编辑 1:查看 CanExecute 和 PreviewCanExecute 事件有一个关键区别:

  PreviewCanExecute(sender, e); 
  if (e.CanExecute)
  { 
    e.Handled = true; 
  }

在这里将 Handled 设置为 true 是有条件的,因此由程序员决定是否继续使用 CanExecute。只需不要在 PreviewCanExecute 处理程序中将 CanExecuteRoutedEventArgs 的 CanExecute 设置为 true,就会调用 CanExecute 处理程序。

至于ContinueRoutingPreview 事件的属性 - 当设置为 false 时,它​​会阻止 Preview 事件进一步路由,但它不会以任何方式影响以下主要事件。

请注意,它仅在通过 CommandBinding 注册处理程序时才以这种方式工作。

如果您仍然希望同时运行 PreviewExecuted 和 Executed,您有两个选择:

  1. 您可以Execute()从 PreviewExecuted 处理程序中调用路由命令的方法。想一想——在 PreviewExecuted 完成之前调用 Executed 处理程序时,您可能会遇到同步问题。对我来说,这看起来不是一个好方法。
  2. CommandManager.AddPreviewExecutedHandler()您可以通过静态方法单独注册 PreviewExecuted 处理程序。这将直接从 UIElement 类调用,不会涉及 CommandBinding。EDIT 2: Look at the point 4 at the beginning of the post - these are the events we're adding the handlers for.

从它的外观来看 - 它是故意这样做的。为什么?只能猜测...

于 2010-02-22T16:22:03.610 回答
1

我构建了以下解决方法,以获得缺少的 ContinueRouting 行为:

foreach (CommandBinding cb in CommandBindings)
{
    if (cb.Command.Equals(ApplicationCommands.Save))
    {
        ExecutedRoutedEventHandler f = null;
        f = (sender, e) =>
        {
            if (IsModified)
            {
                BindingExpression be = rtb.GetBindingExpression(TextBox.TextProperty);
                be.UpdateSource();

                // There is a "Feature/Bug" in .Net which cancels the route when adding PreviewExecuted
                // So we remove the handler and call execute again
                cb.PreviewExecuted -= f;
                cb.Command.Execute(null);
            }
        };
        cb.PreviewExecuted += f;
    }
}
于 2010-02-23T10:12:04.947 回答