Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

RoutedUICommand PreviewExecuted 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?

like image 358
elmar Avatar asked Oct 14 '22 09:10

elmar


2 Answers

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...

like image 69
Alex_P Avatar answered Nov 03 '22 19:11

Alex_P


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;
    }
}
like image 42
elmar Avatar answered Nov 03 '22 21:11

elmar