Selector DoubleClick Behaviour calling ViewModel ICommand






4.83/5 (7 votes)
The other day I was at work and needed to use a WPF ListView (Selector) to call a ICommand in a ViewModel. Here is what I did.
The other day I was at work and needed to use a WPF ListView
(Selector) to call an ICommand
in a ViewModel. Nown we want to be good and use nice design approaches, so I thought about using the attached command capabilities of my Cinch MVVM framework. But then I thought, ah, I only want to call the ICommand
when the user actually double clicks an Item
in the ListView
(Selector) and not when the double click occurs anywhere else, like say a header, which the ListView
certainly has.
So without further ado, I set to work and came up with the following attached Behaviour DP:
1: using System;
2: using System.Collections.Generic;
3: using System.Windows;
4: using System.Windows.Controls.Primitives;
5: using System.Windows.Input;
6: using System.Windows.Controls;
7:
8: namespace ListViewDoubleCLick
9: {
10: /// <summary>
11: /// Selector MouseDoubleClick calling ViewModel ICommand
12: /// </summary>
13: public static class SelectorDoubleClickCommandBehavior
14: {
15: #region Attached DPs
16: #region HandleDoubleClick
17:
18: /// <summary>
19: /// HandleDoubleClick Attached Dependency Property
20: /// </summary>
21: public static readonly DependencyProperty
22: HandleDoubleClickProperty =
23: DependencyProperty.RegisterAttached(
24: "HandleDoubleClick",
25: typeof(bool),
26: typeof(SelectorDoubleClickCommandBehavior),
27: new FrameworkPropertyMetadata(false,
28: new PropertyChangedCallback(
29: OnHandleDoubleClickChanged)));
30:
31: /// <summary>
32: /// Gets the HandleDoubleClick property.
33: /// </summary>
34: public static bool GetHandleDoubleClick(DependencyObject d)
35: {
36: return (bool)d.GetValue(HandleDoubleClickProperty);
37: }
38:
39: /// <summary>
40: /// Sets the HandleDoubleClick property.
41: /// </summary>
42: public static void SetHandleDoubleClick(DependencyObject d,
43: bool value)
44: {
45: d.SetValue(HandleDoubleClickProperty, value);
46: }
47:
48: /// <summary>
49: /// Hooks up a weak event against the source Selectors
50: /// MouseDoubleClick if the Selector has asked for
51: /// the HandleDoubleClick to be handled
52: ///
53: /// If the source Selector has expressed an interest
54: /// in not having its MouseDoubleClick handled
55: /// the internal reference
56: /// </summary>
57: private static void OnHandleDoubleClickChanged(
58: DependencyObject d,
59: DependencyPropertyChangedEventArgs e)
60: {
61: Selector selector = d as Selector;
62:
63:
64: if (selector != null)
65: {
66: if ((bool)e.NewValue)
67: {
68: selector.MouseDoubleClick -= OnMouseDoubleClick;
69:
70: //This will cause the MouseButtonEventHandler.Target
71: //to keep a strong reference to the source of the
72: //event, which will stop it from being GCd
73: selector.MouseDoubleClick += OnMouseDoubleClick;
74: }
75: }
76: }
77: #endregion
78:
79: #region TheCommandToRun
80:
81: /// <summary>
82: /// TheCommandToRun : The actual ICommand to run
83: /// </summary>
84: public static readonly DependencyProperty TheCommandToRunProperty =
85: DependencyProperty.RegisterAttached(
86: "TheCommandToRun",
87: typeof(ICommand),
88: typeof(SelectorDoubleClickCommandBehavior),
89: new FrameworkPropertyMetadata((ICommand)null));
90:
91: /// <summary>
92: /// Gets the TheCommandToRun property.
93: /// </summary>
94: public static ICommand GetTheCommandToRun(DependencyObject d)
95: {
96: return (ICommand)d.GetValue(TheCommandToRunProperty);
97: }
98:
99: /// <summary>
100: /// Sets the TheCommandToRun property.
101: /// </summary>
102: public static void SetTheCommandToRun(DependencyObject d,
103: ICommand value)
104: {
105: d.SetValue(TheCommandToRunProperty, value);
106: }
107: #endregion
108: #endregion
109:
110: #region Private Methods
111:
112:
113: /// <summary>
114: /// Handle Selector.MouseDoubleClick but will
115: /// only fire the associated ViewModel command
116: /// if the MouseDoubleClick occurred over an actual
117: /// ItemsControl item. This is nessecary as if we
118: /// are using a ListView we may have clicked the
119: /// headers which are not items, so do not want the
120: /// associated ViewModel command to be run
121: /// </summary>
122: private static void OnMouseDoubleClick(object sender,
123: MouseButtonEventArgs e)
124: {
125: //Get the ItemsControl and then get the item, and
126: //check there is an actual item, as if we are using
127: //a ListView we may have clicked the
128: //headers which are not items
129: ItemsControl listView = sender as ItemsControl;
130: DependencyObject originalSender =
131: e.OriginalSource as DependencyObject;
132: if (listView == null || originalSender == null) return;
133:
134: DependencyObject container =
135: ItemsControl.ContainerFromElement
136: (sender as ItemsControl,
137: e.OriginalSource as DependencyObject);
138:
139: if (container == null ||
140: container == DependencyProperty.UnsetValue) return;
141:
142: // found a container, now find the item.
143: object activatedItem =
144: listView.ItemContainerGenerator.
145: ItemFromContainer(container);
146:
147: if (activatedItem != null)
148: {
149: ICommand command =
150: (ICommand)(sender as DependencyObject).
151: GetValue(TheCommandToRunProperty);
152:
153: if (command != null)
154: {
155: if (command.CanExecute(null))
156: command.Execute(null);
157: }
158: }
159: }
160: #endregion
161: }
162:
163: }
Where we would use this in XAML:
1: <Window x:Class="ListViewDoubleCLick.Window1"
2: xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
3: xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
4: xmlns:local="clr-namespace:ListViewDoubleCLick"
5: xmlns:interactivity="clr- namespace:Microsoft.Expression.Interactivity;
6: assembly=Microsoft.Expression.Interactivity"
7: Title="Window1" Height="600" Width="800"
8: WindowStartupLocation="CenterScreen">
9: <Grid>
10:
11: <TabControl>
12: <TabItem Header="Attached DP Approach">
13: <ListView ItemsSource="{Binding People}"
14: IsSynchronizedWithCurrentItem="True"
15: local:NaiveSelectorDoubleClickCommandBehavior.HandleDoubleClick="true"
16: local:NaiveSelectorDoubleClickCommandBehavior.TheCommandToRun=
17: "{Binding Path=DoItCommand}" >
18:
19: <ListView.View>
20: <GridView>
21: <GridViewColumn Header="FirstName"
22: DisplayMemberBinding="{Binding FirstName}"
23: Width="80" />
24: <GridViewColumn Header="LastName"
25: DisplayMemberBinding="{Binding LastName}"
26: Width="80"/>
27: </GridView>
28: </ListView.View>
29: </ListView>
30: </TabItem>
31: </TabControl>
32: </Grid>
33:
34: </Window>
Or, we could get really fancy and use the Blend 3 Microsoft.Expression.Interactivity.Dll, which would look something like this:
1: using System;
2: using System.Collections.Generic;
3: using System.Windows;
4: using System.Windows.Controls.Primitives;
5: using System.Windows.Input;
6: using System.Windows.Controls;
7: using Microsoft.Expression.Interactivity;
8: using System.ComponentModel;
9:
10: namespace ListViewDoubleCLick
11: {
12: /// <summary>
13: /// Selector MouseDoubleClick calling ViewModel ICommand Behavior
14: /// using Blend3 Microsoft.Expression.Interactivity Dll
15: /// </summary>
16: public class InteractionsSelectorDoubleClickCommandAction :
17: TargetedTriggerAction<FrameworkElement>,
18: ICommandSource
19: {
20: #region DPs
21:
22: #region Command DP
23: /// <summary>
24: /// The actual Command to fire when the
25: /// EventTrigger occurs, thus firing this
26: /// InteractionsSelectorDoubleClickCommandAction
27: /// </summary>
28: [Category("Command Properties")]
29: public ICommand Command
30: {
31: get { return (ICommand)GetValue(CommandProperty); }
32: set { SetValue(CommandProperty, value); }
33: }
34:
35: public static readonly DependencyProperty CommandProperty =
36: DependencyProperty.Register(
37: "Command", typeof(ICommand),
38: typeof(InteractionsSelectorDoubleClickCommandAction),
39: new PropertyMetadata(
40: (ICommand)null, OnCommandChanged));
41:
42: private static void OnCommandChanged(DependencyObject d,
43: DependencyPropertyChangedEventArgs e)
44: {
45: var action =
46: (InteractionsSelectorDoubleClickCommandAction)d;
47: action.OnCommandChanged((ICommand)e.OldValue,
48: (ICommand)e.NewValue);
49: }
50:
51: #region Command implementation
52:
53: /// <summary>
54: /// This is a strong reference to the Command.
55: /// CanExecuteChanged event handler.
56: /// The commanding system uses a weak
57: /// reference and if we don’t enforce a
58: /// strong reference then the event
59: /// handler will be gc’ed.
60: /// </summary>
61: private EventHandler CanExecuteChangedHandler;
62:
63:
64:
65: private void OnCommandChanged(ICommand oldCommand,
66: ICommand newCommand)
67: {
68: if (oldCommand != null)
69: UnhookCommand(oldCommand);
70: if (newCommand != null)
71: HookCommand(newCommand);
72: }
73:
74: private void UnhookCommand(ICommand command)
75: {
76: command.CanExecuteChanged -=
77: CanExecuteChangedHandler;
78: UpdateCanExecute();
79: }
80:
81: private void HookCommand(ICommand command)
82: {
83: // Save a strong reference to the
84: // Command.CanExecuteChanged event handler.
85: // The commanding system uses a weak
86: // reference and if we don’t save a strong
87: // reference then the event handler will be gc’ed.
88: CanExecuteChangedHandler =
89: new EventHandler(OnCanExecuteChanged);
90: command.CanExecuteChanged
91: += CanExecuteChangedHandler;
92: UpdateCanExecute();
93: }
94:
95: private void OnCanExecuteChanged(object sender,
96: EventArgs e)
97: {
98: UpdateCanExecute();
99: }
100:
101: private void UpdateCanExecute()
102: {
103: if (Command != null)
104: {
105: RoutedCommand command =
106: Command as RoutedCommand;
107: if (command != null)
108: IsEnabled =
109: command.CanExecute(
110: CommandParameter, CommandTarget);
111: else
112: IsEnabled =
113: Command.CanExecute(CommandParameter);
114: if (Target != null && SyncOwnerIsEnabled)
115: Target.IsEnabled = IsEnabled;
116: }
117: }
118:
119: #endregion
120:
121:
122: #endregion
123:
124: #region CommandParameter DP
125: /// <summary>
126: /// For consistency with the Wpf Command pattern
127: /// </summary>
128: [Category("Command Properties")]
129: public object CommandParameter
130: {
131: get { return (object)GetValue(
132: CommandParameterProperty); }
133: set { SetValue(CommandParameterProperty, value); }
134: }
135:
136: public static readonly DependencyProperty
137: CommandParameterProperty =
138: DependencyProperty.Register(
139: "CommandParameter", typeof(object),
140: typeof(InteractionsSelectorDoubleClickCommandAction),
141: new PropertyMetadata());
142: #endregion
143:
144: #region CommandTarget DP
145: /// <summary>
146: /// For consistency with the Wpf Command pattern
147: /// </summary>
148: [Category("Command Properties")]
149: public IInputElement CommandTarget
150: {
151: get { return (IInputElement)GetValue(
152: CommandTargetProperty); }
153: set { SetValue(CommandTargetProperty, value); }
154: }
155:
156: public static readonly DependencyProperty
157: CommandTargetProperty =
158: DependencyProperty.Register(
159: "CommandTarget", typeof(IInputElement),
160: typeof(InteractionsSelectorDoubleClickCommandAction),
161: new PropertyMetadata());
162: #endregion
163:
164: #region SyncOwnerIsEnabled DP
165: /// <summary>
166: /// Allows the user to specify that the
167: /// owner element should be
168: /// enabled/disabled whenever the
169: /// action is enabled/disabled.
170: /// </summary>
171: [Category("Command Properties")]
172: public bool SyncOwnerIsEnabled
173: {
174: get { return (bool)GetValue(SyncOwnerIsEnabledProperty); }
175: set { SetValue(SyncOwnerIsEnabledProperty, value); }
176: }
177:
178: /// <summary>
179: /// When SyncOwnerIsEnabled is true
180: /// then changing
181: /// InteractionsSelectorDoubleClickCommandAction.
182: /// IsEnabled
183: /// will automatically update the owner
184: /// (Target) IsEnabled property.
185: /// </summary>
186: public static readonly DependencyProperty
187: SyncOwnerIsEnabledProperty =
188: DependencyProperty.Register(
189: "SyncOwnerIsEnabled", typeof(bool),
190: typeof(InteractionsSelectorDoubleClickCommandAction),
191: new PropertyMetadata());
192: #endregion
193:
194: #endregion
195:
196: #region Overrides
197: /// <summary>
198: /// On attached hook up our own MouseDoubleClick so we
199: /// can check we actually double click an item
200: /// </summary>
201: protected override void OnAttached()
202: {
203: base.OnAttached();
204: Selector s = this.AssociatedObject as Selector;
205: if (s != null)
206: {
207: s.MouseDoubleClick += OnMouseDoubleClick;
208: }
209: }
210:
211: /// <summary>
212: /// On attached unhook the previously
213: /// hooked MouseDoubleClick handler
214: /// </summary>
215: protected override void OnDetaching()
216: {
217: base.OnDetaching();
218: Selector s = this.AssociatedObject as Selector;
219: if (s != null)
220: {
221: s.MouseDoubleClick -= OnMouseDoubleClick;
222: }
223: }
224:
225: //Must at least implement abstract member invoke
226: protected override void Invoke(object parameter)
227: {
228: //The logic for this is done in the OnMouseDoubleClick
229: //as we only wanto fire command if we are actually on an
230: //Item in the Selector. If the Selector is a ListView we
231: //may have headers so will not want to fire associated
232: //Command when a header is double clicked
233: }
234: #endregion
235:
236: #region Private Methods
237:
238: /// <summary>
239: /// Handle Selector.MouseDoubleClick but will
240: /// only fire the associated ViewModel command
241: /// if the MouseDoubleClick occurred over an actual
242: /// ItemsControl item. This is nessecary as if we
243: /// are using a ListView we may have clicked the
244: /// headers which are not items, so do not want the
245: /// associated ViewModel command to be run
246: /// </summary>
247: private static void OnMouseDoubleClick(object sender,
248: MouseButtonEventArgs e)
249: {
250: //Get the ItemsControl and then get the item, and
251: //check there is an actual item, as if we are using
252: //a ListView we may have clicked the
253: //headers which are not items
254: ItemsControl listView = sender as ItemsControl;
255: DependencyObject originalSender =
256: e.OriginalSource as DependencyObject;
257: if (listView == null || originalSender == null) return;
258:
259: DependencyObject container =
260: ItemsControl.ContainerFromElement
261: (sender as ItemsControl,
262: e.OriginalSource as DependencyObject);
263:
264: if (container == null ||
265: container == DependencyProperty.UnsetValue) return;
266:
267: // found a container, now find the item.
268: object activatedItem =
269: listView.ItemContainerGenerator.
270: ItemFromContainer(container);
271:
272: if (activatedItem != null)
273: {
274: ICommand command =
275: (ICommand)(sender as DependencyObject).
276: GetValue(TheCommandToRunProperty);
277:
278: if (command != null)
279: {
280: if (command.CanExecute(null))
281: command.Execute(null);
282: }
283: }
284: }
285:
286: #endregion
287: }
288:
289: }
Which we could use from XAML as follows:
1: <Window x:Class="ListViewDoubleCLick.Window1"
2: xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
3: xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
4: xmlns:local="clr-namespace:ListViewDoubleCLick"
5: xmlns:interactivity="clr-namespace:Microsoft.Expression.Interactivity;
assembly=Microsoft.Expression.Interactivity"
6: Title="Window1" Height="600" Width="800"
7: WindowStartupLocation="CenterScreen">
8: <Grid>
9:
10: <TabControl>
11: <TabItem Header="Using Blend3 Interactivity Dll" >
12: <ListView ItemsSource="{Binding People}"
13: IsSynchronizedWithCurrentItem="True">
14:
15: <interactivity:Interaction.Triggers>
16: <interactivity:EventTrigger EventName="MouseDoubleClick">
17: <local:InteractionsSelectorDoubleClickCommandAction
18: Command="{Binding DoItCommand}"
19: SyncOwnerIsEnabled="True" />
20: </interactivity:EventTrigger>
21: </interactivity:Interaction.Triggers>
22:
23: <ListView.View>
24: <GridView>
25: <GridViewColumn Header="FirstName"
26: DisplayMemberBinding="{Binding FirstName}"
27: Width="80" />
28: <GridViewColumn Header="LastName"
29: DisplayMemberBinding="{Binding LastName}"
30: Width="80"/>
31: </GridView>
32: </ListView.View>
33: </ListView>
34: </TabItem>
35: </TabControl>
36:
37:
38:
39: </Grid>
40:
41: </Window>
As usual, here is a small demo project: