WPF Commands Everywhere

April 14, 2009

tags: , , ,
16 comments

It has been a long time since I’ve written something in my blog and there are many items in my stack. Unfortunately, I don’t have the time to pop all, so I’ll try to peek few of my best thoughts.

In the last few weeks I’ve started to work on a very interesting UI infrastructure project, based on WPF and Prism. I can’t give many details since it’s kind of confidential, but I can say that I work with very talented people, and Lior Rozner from Microsoft Israel is one of them.

So enough about what I’m doing, let’s talk about a very interesting topic in WPF & Prism: Commands. If you’ve had the opportunity to work with WPF you probably wonder why only few controls: ButtonBase, Hyperlink and MenuItem implement the ICommandSource interface.

So the question is “What could be done in order to extend other controls such as Selector to execute both WPF and Prism (custom) commands”?

To answer this question lets define the problem and provide functional requirements.

Working with either composite or monolithic UI applications, it is reasonable to treat application domain actions triggered by the user interface as commands, so that they can be handled by the Presentation Model or the Controller and be bound to an availability state. For example, having a ListBox, we want it to execute a command to fetch the rest of the item details from a service on item selection. The usual way to achieve this in WPF is to register the Selector.SelectionChanged routed event via XAML, handle it in the C# behind XAML file and delegate the call to the Presentation Model or Controller. This approach is not only convoluted but also inappropriate by design. But I’ll leave the design for another discussion.

Extracting at least one requirement from the example above, we want the Selector control to be able to execute a command when the Selector.SelectionChanged routed event is being fired.

There are many ways to solve this issue, each has its pros and cons.

Custom Control

A straight forward approach is to create a new custom control, for example: CommandListBox, implement the ICommandSource and execute the command on item selection.

Although this solution is fairly reasonable, it requires creation of a custom control for each type of control that doesn’t natively support command.

So let’s look at another approach recommended by Lior Rozner.

Attached Properties

If you’re familiar with the outstanding WPF Attached Property mechanism you could solve this as follows:

<ListBox
    local:CommandProvider.Command="{x:Static local:CommonCommands.Do}"
    local:CommandExtender.Handler="{x:Static
        local:CommandHandlers.SelectorSelect}"
    local:CommandExtender.Parameter="{Binding Path=/}" />

In this case, the CommandProvider is a static class, it provides commanding services via attached properties, where:

- Command is the command instance to execute

- Handler is an instance of a custom type which provides the execute behavior, such as “execute the command on Selector.SelectionChanged event”

- Parameter is the command parameter

As you can see, this approach is much more flexible and extensible since it can be used on any kind of UIElement, and without the unnecessary creation of a custom control.

Multiple Commands

Since both the custom implementation of ICommandSource and the attached properties approaches support only one command at a time, being executed by only one behavior, I’ve decided to extend the attached properties approach to support more than one command to be executed by more than one behavior.

<ListView x:Name="list"
          ItemsSource="{Binding Emails}"
          IsSynchronizedWithCurrentItem="True">
    
    <ts:CommandSource.Trigger>
        <ts:CommandTriggerGroup>

            <ts:EventCommandTrigger
                RoutedEvent="UIElement.PreviewMouseLeftButtonUp"
                Command="{Binding Path=DownloadEmail}"
                CustomParameter="{Binding ElementName=list,
                                          Path=SelectedValue}" />

            <ts:EventCommandTrigger
                RoutedEvent="UIElement.PreviewMouseRightButtonUp"
                Command="{Binding Path=MarkAsRead}"
                CustomParameter="{Binding ElementName=list,
                                          Path=SelectedValue}" />

            <ts:EventCommandTrigger
                RoutedEvent="UIElement.PreviewMouseLeftButtonDown"
                Command="{Binding Path=OpenEmail}"
                CustomParameter="{Binding ElementName=list,
                                          Path=SelectedValue}" />

        </ts:CommandTriggerGroup>
    </ts:CommandSource.Trigger>
    
</ListView>

<Expander IsExpanded="{Binding Path=DummyProperty}"
          Header="Contact">
    
    <ts:CommandSource.Trigger>
        <ts:PropertyCommandTrigger
            Property="Expander.IsExpanded"
            Value="True"
            CustomParameter="{Binding}"
            Command="{Binding Path=DownloadContact,
                              RelativeSource={RelativeSource
                              Mode=FindAncestor,
                              AncestorType=Window}}" />
    </ts:CommandSource.Trigger>
    
    ...
    
</Expander>

image image image

I’ve replaced the CommandProvider with CommandSource in the markup code above and the three attached properties with only one: Trigger. The real difference here is that the Trigger attached property is of type ICommandTrigger.

The ICommandTrigger interface is implemented by three classes: EventCommandTrigger, PropertyCommandTrigger and CommandTriggerGroup.

EventCommandTrigger – executes a command when a routed event is being fired.

PropertyCommandTrigger – executes a command when a dependency property is being changed, and a specific value is met.

CommandTriggerGroup – represents a collection of commands. Using this class as shown above, you can attach more than one command trigger.

Note that both the EventCommandTrigger and PropertyCommandTrigger derive from the WPF Freezable type. This provides an option to be bound to elements in the visual tree. As for the CommandTriggerGroup I’ve used the FreezableCollection as its base class.

Command and Command Parameter

Since CommandTrigger translates routed events and dependency property values into Command, there should be an easy way to have both the routed event and property value, and another user parameter as one parameter of the command. To handle this situation I’ve created the CommandParameter types.

 

OpenEmail = new RoutedCommand();
CommandBinding cmdBinding3 = new CommandBinding(OpenEmail);

cmdBinding3.Executed += (s, e) =>
{
    var parameter = EventCommandParameter<EmailMessage, MouseButtonEventArgs>.Cast(e.Parameter);
    if (parameter.EventArgs.ClickCount == 2)
    {
        parameter.CustomParameter.MarkAsRead();
        MessageBox.Show(
            parameter.CustomParameter.Content,
            parameter.CustomParameter.Subject);
    }
};

cmdBinding3.CanExecute += (s, e) =>
{
    e.CanExecute = true;
};

CommandBindings.Add(cmdBinding3);
 

Both the EventCommandParameter and PropertyCommandParameter types derive from the CommandParameter type. You can think of these types as simple wrappers around the Routed Event or Dependency Property and Custom Parameter. From the sample above you can see that each type of the CommandParameter type has a special Cast<T1, T2> helper method. This simplifies the explicit casting operations of both the custom parameter and routed event argument or dependency property value.

 

Now that we can use commands anywhere we can use only Data Templates as views for our Presentation Model. This mechanism comes in handy especially in Composite Applications where presenters are usually laid out in regions and Data Templates generate the view.

You may download the full code from here.

Add comment
facebook linkedin twitter email

Leave a Reply

Your email address will not be published.

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

16 comments

  1. Edwin FohApril 20, 2009 ב 07:45

    Very helpful and insightful article. This is exactly what people out there need to leverage controls that do not implement ICommandSource, and overcomes the limitations behind using that approach.

    Good stuff! thanks for sharing.

    Ed

    Reply
  2. Avi ShilonApril 21, 2009 ב 14:49

    Hey, great solution Tomer!

    P.S.
    Tell Lior I say hi :)

    Reply
  3. Julian DominguezApril 22, 2009 ב 03:16

    Very nice. I like the approach of having freezables as attached properties… didn’t realize that was the way for binding to the DataContext to work!
    I’m also intrigued on the Prism work you are doing :)
    Looking forward to reading some more.

    Reply
  4. GerMay 11, 2009 ב 11:00

    Hi,
    Thanks for a very helpful article. I am using this EventCommandTrigger for PreviewMouseLeftButtonUp event. The problem I have is that the event gets also fired when the header of the List view is clicked or the scrollbar is moved or the column width is changed which is not desired. Could you please suggest me something to avoid it. Thanks
    -Ger

    Reply
  5. Tomer ShamamMay 11, 2009 ב 11:14

    Hi Ger,

    What exactly you do with the preview event?

    Btw, you can always check in the event args that the e.Source is ListBox or something like that.

    Reply
  6. GerMay 11, 2009 ב 11:29

    Sorry for a stupid question. Selector.SelectionChanged just solves my problem.
    -Thanks

    Reply
  7. JurajJune 24, 2009 ב 13:57

    Hi. Nice, thanks. But it is not working for me. I want to respond to ‘Sorting’ event in Datagrid (http://www.codeplex.com/wpf). My xaml looks like this:

    <ts:CommandSource.Trigger>

                   <ts:CommandTriggerGroup>

                       <ts:EventCommandTrigger RoutedEvent=”dg:DataGrid.Sorting”

    Command=”{Binding Path=Sorting}” />

                   </ts:CommandTriggerGroup>

    </ts:CommandSource.Trigger>

    I get error: ‘Attached event field missing’.

    With <ts:EventCommandTrigger RoutedEvent=”dg:DataGrid.PreviewMouseLeftButtonUp”

    Command=”{Binding Path=Sorting}” /> it works. Any idea?

    Reply
  8. Tomer ShamamJune 30, 2009 ב 21:07

    Hi Juraj,

    Does Sorting is routed event?

    Reply
  9. Tomasz KubackiJuly 23, 2009 ב 16:49

    I’ve found that there are problems with EventCommandTrigger when used inside template (for example instance treeviewItem template) any is there ay place where i can send bug request ? ;-)
    Command simply is not executed.

    Reply
  10. Tomer ShamamJuly 25, 2009 ב 09:52

    Hi Tomasz, you can place bug request here :-)

    What exactly the problem is?

    Reply
  11. FRSeptember 15, 2009 ב 14:50

    I really like this approach and want to make use of this code. But unfortunatly I have a similar problem. I am using DataTemplates for most parts of my XAML code and it seems the EventCommandTrigger cannot establish the Binding. I got the error message: “System.Windows.Data Error: 2 : Cannot find governing FrameworkElement or FrameworkContentElement for target element. BindingExpression:Path=FilterRangeCommand.Command; DataItem=null; target element is ‘EventCommandTrigger’ (HashCode=3429838); target property is ‘Command’ (type ‘ICommand’)”. If I move the code outside of the DataTemplate it is working fine.
    I am going to find the error, but was wondering if this problem is already solved

    Reply
  12. Tomer ShamamSeptember 15, 2009 ב 15:06

    Hi Guys,

    Sorry for the inconvenience, it’s probably a bug.
    I’ll look at that later and provide a fix.

    Thanks.

    Reply
  13. Tomer ShamamSeptember 15, 2009 ב 15:15

    FR, could you please provide a code snippet of whar you’re trying to do, since in my demo I uses DataTemplate for the EmailMessage and it works just fine.

    Tomer

    Reply
  14. FRSeptember 23, 2009 ב 11:50

    Tomer, you can reproduce the same error message that I get in your example. Just put a TabControl around your DockPanel and put the DockPanel into a second Tab:




    …your content

    Now, when you start the app and switch to tab2, you get the same error message that I have. I tried to Debug it, but I am not sure what exactly the problem is. Some help would be great.

    Reply