【问题标题】:RoutedUICommand PreviewExecuted Bug?RoutedUICommand PreviewExecuted 错误?
【发布时间】:2010-02-17 15:18:42
【问题描述】:

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

在 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);
}

乍一看前面的代码就是我想要做的,但是文档绑定到的视图中的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 时,两段代码 do 都会执行,但顺序不正确。

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

谁能确认这种行为?还是我错了?这个 Bug 有解决方法吗?

【问题讨论】:

    标签: c# wpf routed-commands


    【解决方案1】:

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

    1. UIElement 调用 CommandManager.TranslateInput() 以响应用户输入(鼠标或键盘)。
    2. 然后CommandManager 在不同级别通过CommandBindings 寻找与输入关联的命令。
    3. 找到命令后,将调用其CanExecute() 方法,如果返回true,则调用Executed()
    4. RoutedCommand 的情况下,每个方法的作用本质上是相同的——它在启动的UIElement 上引发一对附加事件CommandManager.PreviewCanExecuteEventCommandManager.CanExecuteEvent(或PreviewExecutedEventExecutedEvent)过程。第一阶段到此结束。
    5. 现在UIElement 已为这四个事件注册了类处理程序,这些处理程序只需调用CommandManager.OnCanExecute()CommandManager.CanExecute()(用于预览和实际事件)。
    6. 只有在CommandManager.OnCanExecute()CommandManager.OnExecute() 方法中,使用CommandBinding 注册的处理程序才会被调用。如果没有找到,CommandManager 将事件向上传输到UIElement 的父级,然后新的循环开始,直到命令被处理或到达可视树的根。

    如果您查看 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 处理程序。

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

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

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

    1. 您可以在 PreviewExecuted 处理程序中调用路由命令的 Execute() 方法。想一想——在 PreviewExecuted 完成之前调用 Executed 处理程序时,您可能会遇到同步问题。对我来说,这似乎不是一个好方法。
    2. 可以通过CommandManager.AddPreviewExecutedHandler()静态方法单独注册PreviewExecuted handler。这将直接从 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.

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

    【讨论】:

    • 情节变厚了...所以我查看了您提到的源代码,他们在 OnCanExecute 和 PreviewCanExecute 中做同样的事情。但是,来自 OnCanExecute 的 CanExecuteRoutedEventArgs 与来自 OnExecuted 的 ExecutedRoutedEventArgs 之间存在重要区别。正如您所期望的那样,CanExecuteRoutedEventArgs 包含一个 ContinueRouting 属性,它完全可以做到这一点,但由于某种原因,ExecutedRoutedEventArgs 不得不这样做。我真的无法理解微软的这个选择。
    • 我认为 ContinueRouting 不参与该过程 - 请参阅我在帖子中的 EDIT 2。至于他们为什么这样做......看看 CommandBinding.OnExecuted() 方法的两个部分,它们几乎完全相同 - 它可能是复制/粘贴的经典案例 :) 然后它是一个错误.不过说真的,我认为情况并非如此。我真的很想知道他们背后的原因是什么。
    【解决方案2】:

    我构建了以下解决方法,以获得缺少的 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;
        }
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2017-06-16
      • 2014-07-19
      • 2014-02-03
      • 1970-01-01
      • 2010-12-07
      • 1970-01-01
      • 2012-02-26
      • 2014-03-02
      相关资源
      最近更新 更多