(1 item) |
|
(1 item) |
|
(5 items) |
|
(1 item) |
|
(1 item) |
|
(2 items) |
|
(2 items) |
|
(4 items) |
|
(1 item) |
|
(6 items) |
|
(2 items) |
|
(4 items) |
|
(1 item) |
|
(4 items) |
|
(2 items) |
|
(1 item) |
|
(1 item) |
|
(1 item) |
|
(1 item) |
|
(1 item) |
|
(1 item) |
|
(1 item) |
|
(1 item) |
|
(2 items) |
|
(2 items) |
|
(5 items) |
|
(3 items) |
|
(1 item) |
|
(1 item) |
|
(1 item) |
|
(3 items) |
|
(1 item) |
|
(1 item) |
|
(2 items) |
|
(8 items) |
|
(2 items) |
|
(7 items) |
|
(2 items) |
|
(2 items) |
|
(1 item) |
|
(2 items) |
|
(1 item) |
|
(2 items) |
|
(4 items) |
|
(1 item) |
|
(5 items) |
|
(1 item) |
|
(3 items) |
|
(2 items) |
|
(2 items) |
|
(8 items) |
|
(7 items) |
|
(3 items) |
|
(7 items) |
|
(6 items) |
|
(1 item) |
|
(2 items) |
|
(5 items) |
|
(5 items) |
|
(7 items) |
|
(3 items) |
|
(7 items) |
|
(16 items) |
|
(10 items) |
|
(27 items) |
|
(15 items) |
|
(15 items) |
|
(13 items) |
|
(16 items) |
|
(15 items) |
For a long time I've been aware that Avalon has a commanding architecture, but I hadn't got around to looking at it in detail. It's one of those areas where at first glance, there seem to be slightly more pieces than you might expect. I've finally got around to getting my head around it. Here's what I've found.
(This applies to the March 2005 CTP. Things may change.)
RoutedCommand represents some operation the user can invoke. This is typically the kind of thing
that might be performed using a menu item, a toolbar button, or a keyboard shortcut. For example, clipboard operations like
Cut, Copy, and Paste are represented as routed commands.
Avalon defines a whole load of standard commands, such as clipboard operations, file commands like Open, New,
Save, and Print, text commands like Bold, Italic and Underline, and application commands like Refresh and Stop. These are
all provided as properties of the CommandLibrary. Some of the built-in controls like RichTextBox
understand and respond to some of these commands automatically.
You can also define your own commands. This simply involves creating a new RoutedCommand object.
To handle a RoutedCommand, you need a CommandBinding. A CommandBinding
associates a particular RoutedCommand with a particular method. A given binding is associated with a particular element.
For example, you could decide to handle the CommandLibrary.New command when it hits a Grid
element in your application window. A command binding can be per instance, in which case it is stored in the UIElement.CommandBindings
property. Alternatively, you can set up a class-level binding using CommandManager.RegisterClassCommandBinding.
A CommandBinding offers four events: Execute, PreviewExecute,
QueryEnabled, and PreviewQueryEnabled. These use the same style of routing as normal input events:
there is a tunnelling Preview event followed by a bubbling main event. So when a RoutedCommand is invoked on a particular
target element, both the PreviewExecute and Execute events will be raised on all of the relevant
CommandBinding objects. Bindings for the command for any element in the tree from the root down to the target are all
fired. The PreviewExecute fires first, starting with the root of the UI tree and working down to the target. Then
the Execute is fired, starting from the target and working up to the root. Any handler may set the Handled flag
at any stage to suppress further handling of the event. Routing for the [Preview]QueryEnabled events works the same way.
A command may be invoked or queried by calling its Execute or QueryEnabled method. (A command would be queried to find out if it can be invoked right now. This is used for greying out of options.) However, you
would not normally write code to do this directly. There are two ways in which you would normally make use of a command: either
through 'input gestures' or by telling another control to invoke the command.
An 'input gesture' is either a keyboard shortcut, represented by a KeyGesture or a particular motion made with the
mouse or stylus, represented by a MouseGesture. A command may have a number of gestures associated with it when
it is created. For example, the built-in CommandLibrary.Copy command is associated with two keyboard gestures: the
Ctrl+C and the Ctrl+Insert shortcuts.
You can add further associations between input gestures and commands on a particular user interface element through
the use of InputBinding objects. You can do this at a class level with CommandManager.RegisterClassInputBinding,
or at an instance level with the InputBindings property.
Let's walk through an example, to see how it all hangs together.
The user is typing into a RichTextBox and hits Ctrl+V. The Avalon input system checks to see if this is an input gesture
associated with a command. It must look in the class and instance InputBindings for all the elements in the UI tree from the
root down to the RichTextBox. It must also look at the set of input gestures associated with any commands registered
for handling at the instance or element level on any of the same elements.
In this example, the RichTextBox understands many of the built-in commands, including the CommandLibrary.Paste
command. It will therefore have registered a class-level CommandBinding for that class. Avalon will notice that the Paste
is handled by the rich text box, and that the Paste command recognizes Ctrl+V as one of its input gestures. Avalon will therefore
execute the Paste command. Since the focus is in the RichTextBox, that will be the target of the command.
The Paste command will first raise its PreviewExecute event. Preview events are always 'tunnelling' events.
This means Avalon will start at the root of the UI tree, and will work its way down to the RichTextBox, examining the
CommandBindings (both instance-level and class-level) for each of the elements. If it finds any CommandBinding objects
for the Paste command, it will call any handlers attached to that binding's PreviewExecute event. If any of these
handlers set the Handled flag, command processing will stop.
Note that there may very well be no handlers for the PreviewExecute event. In which case the whole of the previous paragraph
is effectively a no-op.
If the PreviewExecute handling completes without the Handled flag getting set, the Paste command
will then go on to raise the Execute event. This is a bubbling event, so it starts at the target, which in this case is our
RichTextBox. It has registered a class-level handler for the Paste event, which will now proceed to run. This
will execute the actual paste functionality. It will also set the Handled event to indicate that the operation has now been handled.
This prevents the event from bubbling up any further. And we're done.
The previous section contained a load of detail that's only really interesting if you want to know exactly how the command works its way through the system. This is useful if you want to intercept command handling, or write your own commands. But in a lot of cases you just don't need to care. So here's the simple version:
The user hits Ctrl+V, the RichTextBox pastes the clipboard contents into the text. No code is required to make this work.
If you want, you can create a MenuItem or perhaps a Button on a ToolBar with the
Command property set to CommandLibrary.Paste. If the user clicks on that menu item or button, the command
routing behaviour in Avalon means that the RichTextBox will again just perform the paste operation. (Assuming the focus was
in the RichTextBox at the time - by default, focus determines the target for a command.)
In short, Avalon's commanding means that keyboard shortcuts just work like you want them to, and it's easy to add menus and buttons that provide other ways of invoking the same functionality.
What I'm not yet clear on is how you get a MenuItem to display the shortcut for the command it represents automatically.