Customizing Commands in Workflow Designer, for Custom Activities in VS

(Note: The previous article on customizing commands for a rehosted Workflow Designer is here. It is probably better to go that way if you are only shipping designers in rehosted scenarios.)

Due to another forum thread on customizing command behavior for workflow activities, I have been trying to create custom activities which override the workflow designer’s command handling in Visual Studio. And learned some WPF trivia. I started playing by adding CommandBindings on a custom activity designer, in XAML:

<sap:ActivityDesigner.CommandBindings>

    <CommandBinding

       Command="Copy" Executed="CopyCommand_Executed"

       CanExecute="CopyCommand_CanExecute"

       PreviewExecuted="CopyCommand_PreviewExecuted"

       PreviewCanExecute="CopyCommand_PreviewCanExecute"/>

    <CommandBinding

       Command="sapv:DesignerView.CopyCommand" Executed="SapvCopyCommand_Executed"

       PreviewExecuted="SapvCopyCommand_PreviewExecuted"/>

    <CommandBinding

       Command="Delete" Executed="DeleteCommand_Executed"

       PreviewExecuted="DeleteCommand_PreviewExecuted"/>

</sap:ActivityDesigner.CommandBindings>

 

This just about works. All of the PreviewExecute event handling functions get called fine, but mysteriously the Executed event handling functions never get called. I was worried that this is a workflow designer bug. Other routed events like moue click both tunnel down and bubble up, so if I don’t set Handled = true, then I expect I should get both PreviewExecute and Execute events, right?

Apparently, wrong. It turns out that this is a known weirdness of WPF. RoutedCommands used with CommandBindings are special compared to other routed events. Here’s the information I sourced from wpfwiki:

“What is more important is that CommandBinding will mark the routed event from the CommandManager as handled as soon as a handler gets executed (either PreviewExecuted or Executed).
Finally, even if your handler has a prototype that matches a delegate called ExecutedRoutedEventHandler, the Executed event from the CommandBinding is not a RoutedEvent but a normal CLR event. Setting or leaving the e.Handled flag to false will change nothing.
Therefore, as soon as an Executed or PreviewExecuted handler is invoked, the RoutedCommand will halt its routing. ” [emphasis mine]

 

Anyway, the conclusion of all that is a) it’s not workflow designer’s bug, and b) it is still possible to create an activity designer that can reject commands like Delete on itself, or blocks Delete commands from being routed to its immediate children, using CommandBinding in the activity designer, just as long as you understand how that works!

But it’s not a super-robust solution, because it doesn’t give you any power to stop things being deleted as part of group selections, where the target who receive the command may actually be something other than the activity designer you don’t want deleted.

There is another possibility, kinda hacky. We could making our activity designer replace the original CommandBindings of the top-level DesignerView. Here is an idea on how to do that. Of course what happens if every else starts doing the same thing for their custom activity? Ugh. This is why I called it hacky.

    static CommandBinding oldBinding;

 

    protected override void OnModelItemChanged(object newItem)

    {

        base.OnModelItemChanged(newItem);

        EditingContext context = ((ModelItem)newItem).GetEditingContext();

        DesignerView view = context.Services.GetService<DesignerView>();

        foreach (CommandBinding binding in view.CommandBindings)

        {

            if (binding.Command == DesignerView.CopyCommand && oldBinding == null)

            {

                oldBinding = binding;

                binding.Command = new RoutedCommand("OldCommand", typeof(DesignerView));

  CommandBinding overridebinding = new

                    CommandBinding

                    {

                        Command = DesignerView.CopyCommand,

                    };

                overridebinding.Executed += binding_Executed;

  view.CommandBindings.Add(overridebinding);

                break;

            }

        }

    }

 

    void binding_Executed(object sender, ExecutedRoutedEventArgs e)

    {

        e.Handled = true;

        MessageBox.Show("Override point!");

        ((RoutedCommand)oldBinding.Command).Execute(null, (IInputElement)e.OriginalSource);

    }

And because we invoke the dummy command which we rebound the original CommandBinding too, the original command executes too. I hope this can help the OP or at least someone, even though I haven’t figured out a few details like CanExecute yet.