ItemDoubleClickTrigger and InvokeItemCommandAction
When working with ItemsControls in WPF, a common requirement is to invoke some sort of action or command when an item is double-clicked. Apart from manually adding MouseDoubleClick event triggers to every item container, there is no built-in API for doing this sort of thing. The naïve approach that I’ve seen all too often is to do something like this:
1: <ListBox MouseDoubleClick="OnListBoxMouseDoubleClick">
2: <!-- ... -->
1: private void OnListBoxMouseDoubleClick(object sender, MouseButtonEventArgs e)
2: { 3: var listBox = (ListBox)sender; 4: var item = listBox.SelectedItem; 5: 6: if (item != null)
7: // Invoke some action on the selected item.
8: }Can you see the problem? The action will be invoked any time the ListBox is double-clicked—regardless of whether a child item was double-clicked or not. If you double-click on a ScrollBar or in the empty background, the action will be invoked for the selected item. It’s also possible that the user could rapidly click on one item and then another, causing a double-click event to be raised on the ListBox even though the two clicks corresponded to separate items. Well, are these problems that bad? You should know—I'm sure you’ve seen this dialog before:
How many of you have accidentally double-clicked clicked on the empty space right below the debugger instance you wanted? What happens when you do this? The currently selected instance is used instead. Damn. I must have done this a hundred times or more, and it drives me crazy. So do your user a favor and don’t half-ass this behavior :).
Here are a few of the steps involved in one of many potential correct implementations:
- Add a MouseDoubleClick routed event handler to the parent ItemsControl, which will be invoked for events from any child item.
- In your handler, ensure that the original event source is, in fact, a child item container or one of its descendants.
- Keep in mind that the item itself could be the container, or the container could have been generated.
- Check whether or not the event has already been handled and act accordingly.
Writing this same boilerplate code over and over again can be a chore, and it’s easy to overlook one of the little nuances. This use case is an ideal candidate for a custom trigger. While custom triggers are not directly supported in WPF, an alternative API is exposed by the System.Windows.Interactivity assembly. Using these APIs, I have implemented an ItemDoubleClickTrigger which can be attached to any ItemsControl. As the name implies, it fires whenever a child item is double-clicked. I have also written a custom TriggerAction called InvokeItemCommandAction, which I often pair up with this trigger. The InvokeItemCommandAction makes use of a simple interface I wrote to look up a command to invoke for a given item:
1: /// <summary>
2: /// An interface intended to provide a command and, optionally, a command prameter
3: /// for a given item (e.g. an item in an <see cref="System.Windows.Controls.ItemsControl"/>).
4: /// </summary>
5: public interface IItemCommandProvider
6: { 7: /// <summary>
8: /// Gets the command for the specified item.
9: /// </summary>
10: /// <param name="item">The item.</param>
11: /// <param name="parameter">The command parameter that should be used when
12: /// invoking the command.</param>
13: /// <returns>The command for <paramref name="item"/>.</returns>
14: ICommand GetCommandForItem(object item, out object parameter);
15: }Here’s a simple example showing how to use these classes together:
1: public partial class MainWindow
2: { 3: private readonly DelegateCommand<object> _itemCommand;
4: 5: public MainWindow()
6: { 7: _itemCommand = new DelegateCommand<object>(
8: o => MessageBox.Show(string.Format("You double-clicked '{0}'.", o)));
9: 10: this.DataContext = new WindowViewModel
11: { 12: ItemCommandProvider = new DelegatingItemCommandProvider(
13: getCommandCallback: o => _itemCommand, 14: getParameterCallback: o => o) 15: }; 16: 17: InitializeComponent(); 18: } 19: }1: <ListBox>
2: <i:Interaction.Triggers>
3: <cdi:ItemDoubleClickTrigger>
4: <cdi:InvokeItemCommandAction ItemCommandProvider="{Binding ItemCommandProvider}" />
5: </cdi:ItemDoubleClickTrigger>
6: </i:Interaction.Triggers>
7: <!-- ... -->
8: </ListBox>
Of course, the ItemDoubleClickTrigger can be used with any TriggerAction(s). There are also properties on ItemDoubleClickTrigger which you can use to specify whether or not previously handled events should be ignored and whether or not a MouseDoubleClick event should be marked as handled when the trigger fires.
Download the Code
The source code for ItemDoubleClickTrigger and InvokeItemCommandAction, as well as some example code, are available here. Use them however you like.


2 Comments:
Note that this trigger may not work perfectly with every ItemsControl. For instance, if an ItemsControl's template doesn't follow certain standard conventions, the behavior could change slightly.
There is also an interesting edge case with the WPF DataGrid: double-clicking a column header will cause the trigger to fire. One way to deal with this issue would be to analyze the source item in your TriggerAction. For instance, if using the InvokeItemCommandAction, one could return a 'null' command if the item isn't of the expected type. If the DataGrid items are of type 'Customer' and the user double-clicks a column header, the GetCommandForItem method will be passed the column header (probably a string). Returning 'null' in these cases will prevent any command from being invoked.
@Here are a few of the steps involved in one of many potential correct implementations:
I think you should've emphasized "potential" rather than "correct", since all of those seem gross to me.
If we're going to emphasize correct, we need a correctness criterion. e.g. "The event handler should not have to care what object sent the message, since that creates a bidirectional dependency between the server and client. Any bidirectional dependencies should be modeled structurally using a Registry object to decouple the observer from the subject and vice versa." This is how we improve system design: by measuring improvements against criteria.
Cheers,
Z-Bo
Post a Comment
Subscribe to Post Comments [Atom]
<< Home