RoutedUICommand PreviewExecuted Bug?

时间:2010-02-17 15:18:42

标签: 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);
}

乍一看,前面的代码就是我想要做的,但是文档绑定到的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 个答案:

答案 0 :(得分:3)

  

编辑2:从查看源代码看,内部似乎就是这样:

     
      
  1. UIElement调用CommandManager.TranslateInput()以响应用户输入(鼠标或键盘)。
  2.   
  3. CommandManager然后在不同级别上查看CommandBindings,查找与输入相关联的命令。
  4.   
  5. 当找到命令时,会调用其CanExecute()方法,如果它返回true,则会调用Executed()
  6.   
  7. 如果RoutedCommand每个方法基本上都是相同的 - 它会引发一对附加事件CommandManager.PreviewCanExecuteEventCommandManager.CanExecuteEvent(或PreviewExecutedEvent和{{1启动该过程的ExecutedEvent。第一阶段到此结束。
  8.   
  9. 现在UIElement已为这四个事件注册了类处理程序,这些处理程序只需调用UIElementCommandManager.OnCanExecute()(对于预览和实际事件)。
  10.   
  11. 仅在CommandManager.CanExecute()CommandManager.OnCanExecute()方法中调用向CommandManager.OnExecute()注册的处理程序。如果没有找到,则CommandBinding将事件传递给CommandManager的父级,并且新的循环开始,直到处理命令或到达可视树的根。
  12.   

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

UIElement

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

  

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

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

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

     

对于Preview事件的 PreviewCanExecute(sender, e); if (e.CanExecute) { e.Handled = true; } 属性 - 当设置为false时,它会阻止Preview事件进一步路由,但它不会以任何方式影响以下主事件。

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

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

  1. 您可以在PreviewExecuted处理程序中调用路由命令的ContinueRouting方法。只是考虑一下 - 在PreviewExecuted完成之前,您可能会遇到同步问题,因为您正在调用Executed处理程序。对我而言,这看起来不是一个好方法。
  2. 您可以通过Execute()静态方法单独注册PreviewExecuted处理程序。这将直接从UIElement类调用,不涉及CommandBinding。 CommandManager.AddPreviewExecutedHandler()
  3. 从它的外观来看 - 它是故意这样做的。为什么?人们只能猜测......

答案 1 :(得分: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;
    }
}