RoutedUICommand PreviewExecuted Bug?

我正在使用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); } 

乍一看,前面的代码是我想要做的,但是文档绑定到的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中的一个Bug,因为你应该能够为PreviewExecuted和Executed添加一个处理程序并让它们按顺序执行,前提是你没有将事件标记为已处理。

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

编辑2:从查看源代码看起来内部它的工作原理如下:

  1. UIElement响应用户输入(鼠标或键盘)调用CommandManager.TranslateInput() )。
  2. 然后, CommandManager在不同级别上通过CommandBindings查找与输入关联的命令。
  3. 找到命令后,将调用其CanExecute()方法,如果返回true ,则调用Executed()
  4. RoutedCommand情况下,每个方法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处理程序中调用routed命令的Execute()方法。 考虑一下 – 在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.

从它的外观 – 它是故意这样做的。 为什么? 人们只能猜到……

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