Issuing, Handling, and Intercepting Commands in a WF4 Rehosted Designer application.
Background on Commands in WPF
I thought I understood Commands, but when I found myself having difficulties explaining things to a customer last week I realized I had really just been muddling along without understanding commands at all.
The first concept to understand is the basic .net command pattern. There is an interface contract for a command, ICommand:
public interface ICommand
event EventHandler CanExecuteChanged;
bool CanExecute(object parameter);
void Execute(object parameter);
There are two capabilities guaranteed by the contract:
Invoke a command.
Query to see if the command is ready to be invoked. Hereby you test whether to enable/disable buttons etc.
Now you can write custom controls which call these Execute() and CanExecute() functions directly. And of course you could pass in whatever parameter you like, as long as you think the command recipient will understand it. And of course you are free to create your own command recipient, which implements ICommand and understands the parameter you pass it. But I am speak theoretically here because I've never actually used ICommand that way.
The reason is that in WPF, it's easier to just bind a command to a button or menu. WPF provides a bunch of standard commands which you can bind to for this purpose, such as ApplicationCommands.Copy. These standard commands derive from RoutedCommand, and result in a routed event being raised to handle the command. (If you don't know about routed events yet, it could pay to read up, they are all over the place in WPF and in workflow designer.)
So now that you wish to setup such a handler for such a RoutedCommand (invoked by someone else), how to proceed?
Option 1: Register to handle the routed event directly…
…by using CommandManager to register event handlers for the corresponding routed event:
void ExecuteHandler(object sender, ExecutedRoutedEventArgs e)
Note, this style of command handler will receive ALL routed commands. Any command from anyone to anything. This means you will be writing messy command handler code with lots of if statements to check if the command actually is the one you are interested in.
Option 2: Use CommandBindings to set up your event handling
This is a simple solution to set up a plain command handler. UIElement.CommandBindings is easy to set in XAML or in code:
Title="MainWindow" Height="350" Width="525">
<CommandBinding Command="ApplicationCommands.Copy" Executed="CopyCommandExecutedHandler"/>
Because the WPF routed event model is a bubble-down bubble-up model in addition to an Executed handler, it is possible to set a PreviewExecuted event handler and use it to intercept commands which are actually issued targetting our children. (This is important to grasp. One of the key distinctions or capabiltiy that RoutedCommand class adds (compared to ICommand) is the idea of a command target. Every routed event has to have a target, and routed commands are no exception to this rule. The command binding we created on our Window above only has a chance to act if the command target is a UIElement inside of the window, or to be more precise a descendent of the window.)
Linkedy-Link-Link: At this point you might want to jump to Kushal’s Undo-Redo – Programmatically and see if it makes sense.
Commands in WorkflowDesigner
When you rehost WorkflowDesigner it similarly uses RoutedCommands to handle keyboard shortcuts (Ctrl+X), and context menus. There are a couple important points to remember when trying to issue or intercept designer commands:
- DesignerView defines its own new commands instead of reusing the commands in ApplicationCommands - e.g. DesignerView.CopyCommand.
- WorkflowDesigner.View is not actually the DesignerView. It just (by default) contains a DesignerView somewhere.
Scenario – issuing commands to the DesignerView
OK. Here’s a scenario. It’s educational (but not necessarily better) to take the same Undo-Redo – Programmatically Sample above, and figure out how to do it a different way - by issuing commands to the designer instead of calling the APIs UndoEngine.Undo()/Redo() directly.
The things needing doing are:
- Create something to issue the Command. Set that command to be DesignerView.UndoCommand/RedoCommand
- Target that command to go to the DesignerView, where there are CommandBindings already defined.
The first bit is pretty easy, and looks a lot like Kushal’s sample - but using DesignerView Commands.
<Button Command="sapv:DesignerView.UndoCommand" x:Name="undoButton">Undo</Button>
<Button Command="sapv:DesignerView.RedoCommand" x:Name="redoButton">Redo</Button>
With just that code these commands will be raised. BUT if the buttons are outside of the DesignerView, the command is never routed to the DesignerView. This is fixable by setting up a CommandTarget. This is easiest in code, just because it’s hard to get a reference to your DesignerView in the XAML.
this.Loaded += new RoutedEventHandler(MainWindow_Loaded);
void MainWindow_Loaded(object sender, RoutedEventArgs e)
designerView = des.Context.Services.GetService<DesignerView>();
undoButton.CommandTarget = designerView;
redoButton.CommandTarget = designerView;
Scenario – intercepting commands meant for the DesignerView
A more interesting scenario. Suppose we want to implement our own Copy command which overrides the designer’s Copy behavior. How can we do that? Since it is a RoutedCommand it is easy. We just add our own CommandBinding making our Window CopyCommand-aware, and handle PreviewExecuted to do the intercept.
private void CopyCommandPreviewExecuted(object sender, ExecutedRoutedEventArgs e)
//do our custom copy logic
MessageBox.Show("Copying something in designer");
e.Handled = true;
One oddity - I don't know why, but you do need to define CommandBinding.Executed in order for CommandBinding.PreviewExecuted to ever be called.
[P.S.] One more tip! I had someone ask me today how one invokes designer commands from an arbitrary place in the C# code, as opposed to using WPF command bindings. Here is a code snippet to do it.
DesignerView dView = des.Context.Services.GetService<DesignerView>();