Click here to Skip to main content
15,868,016 members
Articles / Desktop Programming / WPF

Selector DoubleClick Behaviour calling ViewModel ICommand

Rate me:
Please Sign up or sign in to vote.
4.83/5 (7 votes)
8 Sep 2009CPOL 55.9K   15   17
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:

C#
  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:

XML
 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:

C#
  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:

XML
 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:

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
United Kingdom United Kingdom
I currently hold the following qualifications (amongst others, I also studied Music Technology and Electronics, for my sins)

- MSc (Passed with distinctions), in Information Technology for E-Commerce
- BSc Hons (1st class) in Computer Science & Artificial Intelligence

Both of these at Sussex University UK.

Award(s)

I am lucky enough to have won a few awards for Zany Crazy code articles over the years

  • Microsoft C# MVP 2016
  • Codeproject MVP 2016
  • Microsoft C# MVP 2015
  • Codeproject MVP 2015
  • Microsoft C# MVP 2014
  • Codeproject MVP 2014
  • Microsoft C# MVP 2013
  • Codeproject MVP 2013
  • Microsoft C# MVP 2012
  • Codeproject MVP 2012
  • Microsoft C# MVP 2011
  • Codeproject MVP 2011
  • Microsoft C# MVP 2010
  • Codeproject MVP 2010
  • Microsoft C# MVP 2009
  • Codeproject MVP 2009
  • Microsoft C# MVP 2008
  • Codeproject MVP 2008
  • And numerous codeproject awards which you can see over at my blog

Comments and Discussions

 
GeneralArticle Pin
zdebyman25-Oct-12 2:09
zdebyman25-Oct-12 2:09 
QuestionWhat do you mean by 'DoItCommand'? Pin
Bob Brady12-Sep-12 10:24
Bob Brady12-Sep-12 10:24 
AnswerRe: What do you mean by 'DoItCommand'? Pin
Sacha Barber12-Sep-12 19:27
Sacha Barber12-Sep-12 19:27 
AnswerRe: What do you mean by 'DoItCommand'? Pin
Sacha Barber12-Sep-12 19:30
Sacha Barber12-Sep-12 19:30 
GeneralRe: What do you mean by 'DoItCommand'? Pin
Bob Brady13-Sep-12 4:12
Bob Brady13-Sep-12 4:12 
QuestionMicrosoft.Expression.Interactivity Pin
brettles2-Sep-12 21:46
brettles2-Sep-12 21:46 
BugPotential Memory Leak Pin
Clifford Nelson7-Jun-12 8:18
Clifford Nelson7-Jun-12 8:18 
GeneralRe: Potential Memory Leak Pin
Sacha Barber7-Jun-12 9:33
Sacha Barber7-Jun-12 9:33 
QuestionImplemented in Cinch? Pin
aaj2318-Apr-12 3:01
aaj2318-Apr-12 3:01 
AnswerRe: Implemented in Cinch? Pin
Sacha Barber18-Apr-12 3:09
Sacha Barber18-Apr-12 3:09 
GeneralDon't see the benefit anymore with so much code for just a doubleclick... Pin
gvdpeer24-Mar-10 10:32
gvdpeer24-Mar-10 10:32 
GeneralRe: Don't see the benefit anymore with so much code for just a doubleclick... Pin
Sacha Barber24-Mar-10 12:00
Sacha Barber24-Mar-10 12:00 
GeneralReally complex for "just" an DoubleClick Pin
ChrDressler13-Dec-09 8:12
ChrDressler13-Dec-09 8:12 
GeneralRe: Really complex for "just" an DoubleClick Pin
Sacha Barber13-Dec-09 10:16
Sacha Barber13-Dec-09 10:16 
Of course there is a native ListView.DoubleClick, but that would be code behind which is the enemy of MVVM as code behind requires the UI to run, so its not testable.

So one must run command in the VM which is testable via Unit tests, you get me.

Sacha Barber
  • Microsoft Visual C# MVP 2008/2009
  • Codeproject MVP 2008/2009
Your best friend is you.
I'm my best friend too. We share the same views, and hardly ever argue

My Blog : sachabarber.net

GeneralPassing the double clicked item Pin
Matthew Searles29-Oct-09 16:08
Matthew Searles29-Oct-09 16:08 
AnswerRe: Passing the double clicked item Pin
Matthew Searles29-Oct-09 17:17
Matthew Searles29-Oct-09 17:17 
GeneralRe: Passing the double clicked item Pin
Sacha Barber29-Oct-09 20:42
Sacha Barber29-Oct-09 20:42 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.