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