RoutedUICommand PreviewExecuted 错误?

发布于 2024-08-22 02:22:41 字数 1964 浏览 7 评论 0原文

我正在使用 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 时,两段代码都会执行,但顺序不正确。

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

任何人都可以证实这种行为吗?还是我错了?这个 Bug 有解决方法吗?

I'm building an application using the MVVM design pattern and I want to make use of the RoutedUICommands defined in the ApplicationCommands class. Since the CommandBindings property of a View (read UserControl) isn't a DependencyProperty we can't bind CommandBindings defined in a ViewModel to the View directly. I solved this by defining an abstract View class which binds this programmatically, based on a ViewModel interface which ensures every ViewModel has an ObservableCollection of CommandBindings. This all works fine, however, in some scenarios I want to execute logic which is defined in different classes (the View and ViewModel) same command. For instance, when saving a document.

In the ViewModel the code saves the document to disk:

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

At first sight the previous code is all I wanted to do, but the TextBox in the View to which the document is bound, only updates its Source when it loses its focus. However, I can save a document without losing focus by pressing Ctrl+S. This means the document is saved before the changes where Updated in the source, effectively ignoring the changes. But since changing the UpdateSourceTrigger to PropertyChanged isn't a viable option for performance reasons, something else must force an update before saving. So I thought, lets use the PreviewExecuted event to force the update in the PreviewExecuted event, like so:

//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;
        };
    }
}

However, assigning an handler to the PreviewExecuted event seems to cancel the event altogether, even when I explicitly set the Handled property to false. So the executeSave eventhandler I defined in the previous code sample isn't executed anymore. Note that when I change the cb.PreviewExecuted to cb.Executed both pieces of code do execute, but not in the correct order.

I think this is a Bug in .Net, because you should be able to add a handler to PreviewExecuted and Executed and have them be executed in order, provided you don't mark the event as handled.

Can anyone confirm this behavior? Or am I wrong? Is there a workaround for this Bug?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

樱花坊 2024-08-29 02:22:41

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

  1. UIElement 调用 CommandManager.TranslateInput() 以响应用户输入(鼠标或键盘)。
  2. 然后,CommandManager 在不同级别上遍历 CommandBindings,查找与输入关联的命令。
  3. 找到命令后,将调用其 CanExecute() 方法,如果返回 true,则调用 Executed()
  4. 对于 RoatedCommand,每个方法本质上都执行相同的操作 - 它引发一对附加事件 CommandManager.PreviewCanExecuteEventCommandManager.CanExecuteEvent code> (或 PreviewExecutedEventExecutedEvent)在启动该进程的 UIElement 上。第一阶段到此结束。
  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 是有条件的,因此由程序员决定是否继续执行 CanExecute。只需不要在 PreviewCanExecute 处理程序中将 CanExecuteRoulatedEventArgs 的 CanExecute 设置为 true,就会调用 CanExecute 处理程序。

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

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

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

  1. 您可以从 PreviewExecuted 处理程序中调用路由命令的 Execute() 方法。只是想一想 - 当您在 PreviewExecuted 完成之前调用 Executed 处理程序时,您可能会遇到同步问题。对我来说,这看起来不是一个好方法。
  2. 您可以通过CommandManager.AddPreviewExecutedHandler()静态方法单独注册PreviewExecuted处理程序。这将直接从 UIElement 类调用,不会涉及 CommandBinding。 编辑 2:查看帖子开头的第 4 点 - 这些是我们要为其添加处理程序的事件。

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

EDIT 2: From looking at the source code it seems that internally it works like that:

  1. The UIElement calls CommandManager.TranslateInput() in reaction to user input (mouse or keyboard).
  2. The CommandManager then goes through CommandBindings on different levels looking for a command associated with the input.
  3. When the command is found its CanExecute() method is called and if it returns true the Executed() is called.
  4. In case of RoutedCommand each of the methods does essencially the same thing - it raises a pair of attached events CommandManager.PreviewCanExecuteEvent and CommandManager.CanExecuteEvent (or PreviewExecutedEvent and ExecutedEvent) on the UIElement that initiated the process. That concludes the first phase.
  5. Now the UIElement has class handlers registered for those four events and these handlers simply call CommandManager.OnCanExecute() and CommandManager.CanExecute() (for both preview and actual events).
  6. It is only here in CommandManager.OnCanExecute() and CommandManager.OnExecute() methods where the handlers registered with CommandBinding are invoked. If there are none found the CommandManager transfers the event up to the UIElement's parent, and the new cycle begins until the command is handled or the root of the visual tree is reached.

If you look at the CommandBinding class source code there is OnExecuted() method that is responsible for calling the handlers you register for PreviewExecuted and Executed events through CommandBinding. There is that bit there:

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

this sets the event as handled right after your PreviewExecuted handler returns and so the Executed is not called.

EDIT 1: Looking at CanExecute & PreviewCanExecute events there is a key difference:

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

setting Handled to true is conditional here and so it is the programmer who decides whether or not to proceed with CanExecute. Simply do not set the CanExecuteRoutedEventArgs's CanExecute to true in your PreviewCanExecute handler and the CanExecute handler will be called.

As to ContinueRouting property of Preview event - when set to false it prevents the Preview event from further routing, but it does not affect the following main event in any way.

Note, that it only works this way when handlers are registered through CommandBinding.

If you still want to have both PreviewExecuted and Executed to run you have two options:

  1. You can can call Execute() method of the routed command from within PreviewExecuted handler. Just thinking about it - you might run into sync issues as you're calling Executed handler before the PreviewExecuted is finished. To me this doesn't look like a good way to go.
  2. You can register PreviewExecuted handler separately through CommandManager.AddPreviewExecutedHandler() static method. This will be called directly from UIElement class and will not involve CommandBinding. EDIT 2: Look at the point 4 at the beginning of the post - these are the events we're adding the handlers for.

From the looks of it - it was done this way on purpose. Why? One can only guess...

陌若浮生 2024-08-29 02:22:41

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

I build the following workaround, to obtain the missing ContinueRouting behavior:

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;
    }
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文