RoutedUICommand PreviewExecuted Bug?

elm*_*mar 5 c# wpf routed-commands

我正在使用MVVM设计模式构建应用程序,我想使用ApplicationCommands类中定义的RoutedUICommands.由于View的CommandBindings属性(读取UserControl)不是DependencyProperty,因此我们无法将ViewModel中定义的CommandBindings直接绑定到View.我通过定义一个抽象的View类来解决这个问题,该类基于ViewModel接口以编程方式绑定它,该接口确保每个ViewModel都有一个ObBableCollection的CommandBindings.这一切都很好,但是,在某些情况下我想执行在不同类(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);
}
Run Code Online (Sandbox Code Playgroud)

乍一看,前面的代码就是我想要做的,但是文档绑定到的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;
        };
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

Ale*_*x_P 3

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

  1. 对用户输入(鼠标或键盘)做出反应的UIElement调用。CommandManager.TranslateInput()
  2. 然后在不同的级别上CommandManager查找CommandBindings与输入关联的命令。
  3. 当命令被发现时,它的CanExecute()方法被调用,如果它返回,true则被Executed()调用。
  4. 如果RoutedCommand每个方法本质上执行相同的操作 - 它会在启动进程时引发一对附加事件CommandManager.PreviewCanExecuteEventCommandManager.CanExecuteEvent(或PreviewExecutedEvent和)。第一阶段就此结束。ExecutedEventUIElement
  5. 现在,已经UIElement为这四个事件注册了类处理程序,这些处理程序只需调用CommandManager.OnCanExecute()and CommandManager.CanExecute()(对于预览事件和实际事件)。
  6. 仅在此处CommandManager.OnCanExecute()和方法中调用CommandManager.OnExecute()注册的处理程序。CommandBinding如果没有找到,则将CommandManager事件传输到 的UIElement父级,并且新的循环开始,直到处理命令或到达可视化树的根。

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

PreviewExecuted(sender, e); 
e.Handled = true;
Run Code Online (Sandbox Code Playgroud)

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

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

  PreviewCanExecute(sender, e); 
  if (e.CanExecute)
  { 
    e.Handled = true; 
  }
Run Code Online (Sandbox Code Playgroud)

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

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

请注意,只有通过 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.

从表面上看——这是故意的。为什么?人们只能猜测...