103

In a WPF application using MVVM, I have a usercontrol with a listview item. In run time, it will use databinding to fill the listview with a collection of objects.

What is the correct way to attach a double click event to the items in the listview so that when an item in the list view is doubleclicked, A corresponding event in the view model is fired and has a reference to the item clicked?

How can it be done in a clean MVVM way i.e. no code behind in the View?

Emad Gabriel
  • 3,677
  • 7
  • 37
  • 49

10 Answers10

85

Please, code behind is not a bad thing at all. Unfortunately, quite a lot people in the WPF community got this wrong.

MVVM is not a pattern to eliminate the code behind. It is to separate the view part (appearance, animations, etc.) from the logic part (workflow). Furthermore, you are able to unit test the logic part.

I know enough scenarios where you have to write code behind because data binding is not a solution to everything. In your scenario I would handle the DoubleClick event in the code behind file and delegate this call to the ViewModel.

Sample applications that use code behind and still fulfill the MVVM separation can be found here:

Win Application Framework (WAF) - https://github.com/jbe2277/waf

jbe
  • 6,976
  • 1
  • 43
  • 34
  • 5
    Well said, I refuse to use all that code and an extra DLL just to do a double-click! – Eduardo Molteni Oct 30 '09 at 01:51
  • 4
    This only use Binding thing is giving me a real headache. It's like being asked to code with 1 arm, 1 eye on an eye patch, and standing on 1 leg. Double click should be simple, and I don't see how all this additional code is worth it. – Echiban Apr 25 '10 at 06:31
  • 3
    I'm afraid I do not totally agree with you. If you say 'code behind is not bad', then I have a question about that: Why don't we delegate the click event for button but often using the binding (using Command property) instead? – Nam G VU Jul 18 '10 at 08:15
  • 21
    @Nam Gi VU: I would always prefer a Command Binding when it is supported by the WPF Control. A Command Binding does more than just relaying the ‘Click’ event to the ViewModel (e.g. CanExecute). But Commands are only available for the most common scenarios. For other scenarios we can use the code-behind file and there we delegate non-UI related concerns to the ViewModel or the Model. – jbe Jul 18 '10 at 20:14
  • 3
    Now I understand you more! Nice discussion with you! – Nam G VU Jul 19 '10 at 03:55
  • To add, from code-behind you can call your viewModel and execute the logic there by getting the DataContext. – CularBytes Mar 25 '16 at 13:23
  • 1
    Using MVVM based binding is no bad thing either - the problem is that so much is poorly explained and documented. – AndyUK Sep 28 '18 at 09:52
  • 1
    IMHO if you have a view model, there is no need to use code behind (and manage 2 code files). code behind should be only used in custom controls to manage the view logic. – Kux Sep 05 '19 at 17:37
79

I am able to get this to work with .NET 4.5. Seems straight forward and no third party or code behind needed.

<ListView ItemsSource="{Binding Data}">
        <ListView.ItemsPanel>
            <ItemsPanelTemplate>
                <StackPanel Orientation="Horizontal"/>
            </ItemsPanelTemplate>
        </ListView.ItemsPanel>
        <ListView.ItemTemplate>
            <DataTemplate>
                <Grid Margin="2">
                    <Grid.InputBindings>
                        <MouseBinding Gesture="LeftDoubleClick" Command="{Binding ShowDetailCommand}"/>
                    </Grid.InputBindings>
                    <Grid.RowDefinitions>
                        <RowDefinition/>
                        <RowDefinition/>
                    </Grid.RowDefinitions>
                    <Image Source="..\images\48.png" Width="48" Height="48"/>
                    <TextBlock Grid.Row="1" Text="{Binding Name}" />
                </Grid>
            </DataTemplate>
        </ListView.ItemTemplate>
    </ListView>
Rushui Guan
  • 3,023
  • 1
  • 24
  • 26
  • 3
    Doesn't seem to work for the entire area, e.g. I do this on a dock panel and it only works where there is something within the dock panel (e.g. textblock, image) but not the blank space. – Stephen Drew Jun 16 '14 at 11:41
  • 3
    OK - this old chestnut again...need to set the background to transparent to receive mouse events, as per http://stackoverflow.com/questions/7991314/mouse-event-on-transparent-background – Stephen Drew Jun 16 '14 at 11:44
  • 9
    I was scratching my head trying to figure out why it was working for all of you and not for me. I suddenly realised that within the context of the item template the data context is the current item from the itemssource and not the main window's view model. So I used the following to get it working In my case the EditBandCommand is the command on the page's viewmodel not on the bound entity. – naskew Jan 06 '15 at 09:20
  • naskew had the secret sauce I needed with MVVM Light, getting a command parameter being the model object in the double-clicked listboxitem, and the data context of the window is set to the view model that exposes the command: – MC5 Mar 20 '15 at 17:33
  • Just want to add that `InputBindings` are available from .NET 3.0 and are _not_ available in Silverlight. – Martin Dec 08 '15 at 14:40
  • I think I prefer this answer to using `EventToCommand`. Having to add a DLL because of ... – afaolek Apr 18 '17 at 14:13
  • Thanks, I just added the explicitly set Background="Transparent" to my Grid container and it worked perfectly – luis_laurent Nov 28 '19 at 02:24
43

I like to use Attached Command Behaviors and Commands. Marlon Grech has a very good implementation of the Attached Command Behaviors. Using these, we could then assign a style to the ListView's ItemContainerStyle property that will set the command for each ListViewItem.

Here we set the command to be fired on the MouseDoubleClick event, and the CommandParameter, will be the data object that we click on. Here I'm traveling up the visual tree to get the command that I'm using, but you could just as easily create application wide commands.

<Style x:Key="Local_OpenEntityStyle"
       TargetType="{x:Type ListViewItem}">
    <Setter Property="acb:CommandBehavior.Event"
            Value="MouseDoubleClick" />
    <Setter Property="acb:CommandBehavior.Command"
            Value="{Binding ElementName=uiEntityListDisplay, Path=DataContext.OpenEntityCommand}" />
    <Setter Property="acb:CommandBehavior.CommandParameter"
            Value="{Binding}" />
</Style>

For the commands, you can either implement an ICommand directly, or use some of the helpers like those that come in the MVVM Toolkit.

rmoore
  • 15,162
  • 4
  • 59
  • 59
  • 1
    +1 I've found this to be my preferred solution when working with Composite Application Guidance for WPF (Prism). – Travis Heseman Feb 01 '10 at 18:26
  • 1
    What does the namespace 'acb:' stand for in your code sampleabove? – Nam G VU Jul 18 '10 at 08:44
  • @NamGiVU `acb:` = AttachedCommandBehavior. The code can be found in the first link in the answer – Rachel Oct 14 '11 at 11:58
  • i tried just that and getting null pointer exception from class CommandBehaviorBinding line 99. variable "strategy" is null. whats wrong? – etwas77 Jul 10 '14 at 08:12
14

I have found a very easy and clean way to do this with the Blend SDK Event triggers. Clean MVVM, reusable and no code-behind.

You probably already have something like this:

<Style x:Key="MyListStyle" TargetType="{x:Type ListViewItem}">

Now include a ControlTemplate for the ListViewItem like this if you don't already use one:

<Setter Property="Template">
  <Setter.Value>
    <ControlTemplate TargetType="{x:Type ListViewItem}">
      <GridViewRowPresenter Content="{TemplateBinding Content}"
                            Columns="{TemplateBinding GridView.ColumnCollection}" />
    </ControlTemplate>
  </Setter.Value>
 </Setter>

The GridViewRowPresenter will be the visual root of all elements "inside" making up a list row element. Now we could insert a trigger there to look for MouseDoubleClick routed events and call a command via InvokeCommandAction like this:

<Setter Property="Template">
  <Setter.Value>
    <ControlTemplate TargetType="{x:Type ListViewItem}">
      <GridViewRowPresenter Content="{TemplateBinding Content}"
                            Columns="{TemplateBinding GridView.ColumnCollection}">
        <i:Interaction.Triggers>
          <i:EventTrigger EventName="MouseDoubleClick">
            <i:InvokeCommandAction Command="{Binding DoubleClickCommand}" />
          </i:EventTrigger>
        </i:Interaction.Triggers>
      </GridViewRowPresenter>
    </ControlTemplate>
  </Setter.Value>
 </Setter>

If you have visual elements "above" the GridRowPresenter (probalby starting with a grid) you can also put the Trigger there.

Unfortunately MouseDoubleClick events are not generated from every visual element (they are from Controls, but not from FrameworkElements for example). A workaround is to derive a class from EventTrigger and look for MouseButtonEventArgs with a ClickCount of 2. This effectively filters out all non-MouseButtonEvents and all MoseButtonEvents with a ClickCount != 2.

class DoubleClickEventTrigger : EventTrigger
{
    protected override void OnEvent(EventArgs eventArgs)
    {
        var e = eventArgs as MouseButtonEventArgs;
        if (e == null)
        {
            return;
        }
        if (e.ClickCount == 2)
        {
            base.OnEvent(eventArgs);
        }
    }
}

Now we can write this ('h' is the Namespace of the helper class above):

<Setter Property="Template">
  <Setter.Value>
    <ControlTemplate TargetType="{x:Type ListViewItem}">
      <GridViewRowPresenter Content="{TemplateBinding Content}"
                            Columns="{TemplateBinding GridView.ColumnCollection}">
        <i:Interaction.Triggers>
          <h:DoubleClickEventTrigger EventName="MouseDown">
            <i:InvokeCommandAction Command="{Binding DoubleClickCommand}" />
          </h:DoubleClickEventTrigger>
        </i:Interaction.Triggers>
      </GridViewRowPresenter>
    </ControlTemplate>
  </Setter.Value>
 </Setter>
Gunter
  • 282
  • 3
  • 6
  • As i found out if you put the Trigger directly on the GridViewRowPresenter there could be a problem. The empty spaces between the colums probably don't receive mouse events at all (probably a workaround would be to style them with alignment stretch). – Gunter Jun 09 '11 at 16:39
  • In this case it is probably better to put an empty grid around the GridViewRowPresenter and put the trigger there. This seems to work. – Gunter Jun 09 '11 at 16:40
  • 1
    Note that you lose the default style for the ListViewItem if you replace the template like this. It didn't matter for the application i was working on as it was using a heavily customized styling anyway. – Gunter Jun 10 '11 at 07:01
6

I realize that this discussion is a year old, but with .NET 4, are there any thoughts on this solution? I absolutely agree that the point of MVVM is NOT to eliminate a code behind file. I also feel very strongly that just because something is complicated, doesn't mean it's better. Here is what I put in the code behind:

    private void ButtonClick(object sender, RoutedEventArgs e)
    {
        dynamic viewModel = DataContext;
        viewModel.ButtonClick(sender, e);
    }
Aaron
  • 61
  • 1
  • 1
  • 12
    You should viewmodel should have names representing the actions you can perform in your domain. What is a "ButtonClick" action in your domain? ViewModel represents the logic of the domain in a view-friendly context, its not just a helper to the view. So: ButtonClick should never be on the viewmodel, use viewModel.DeleteSelectedCustomer or whatever this action actually represents instead. – Marius Nov 18 '10 at 08:01
4

You can use Caliburn's Action feature to map events to methods on your ViewModel. Assuming you have an ItemActivated method on your ViewModel, then corresponding XAML would look like:

<ListView x:Name="list" 
   Message.Attach="[Event MouseDoubleClick] = [Action ItemActivated(list.SelectedItem)]" >

For further details you can examine Caliburn's documentation and samples.

idursun
  • 6,261
  • 1
  • 37
  • 51
4

I am finding it simpler to link the command when the view is created:

var r = new MyView();
r.MouseDoubleClick += (s, ev) => ViewModel.MyCommand.Execute(null);
BindAndShow(r, ViewModel);

In my case BindAndShow looks like this (updatecontrols+avalondock):

private void BindAndShow(DockableContent view, object viewModel)
{
    view.DataContext = ForView.Wrap(viewModel);
    view.ShowAsDocument(dockManager);
    view.Focus();
}

Though the approach should work with whatever method you have of opening new views.

default
  • 11,485
  • 9
  • 66
  • 102
Timothy Pratley
  • 10,586
  • 3
  • 34
  • 63
  • It seems to me that this is the simplest solution, rather than trying to make it work in XAML only. – Mas Jul 12 '11 at 14:12
2

I succeed to make this functionality with .Net 4.7 framework by using the interactivity library, first of all make sure of declaring the namespace in the XAML file

xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"

Then set the Event Trigger with his respective InvokeCommandAction inside the ListView like below.

View:

<ListView x:Name="lv" IsSynchronizedWithCurrentItem="True" 
          ItemsSource="{Binding Path=AppsSource}"  >
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="MouseDoubleClick">
            <i:InvokeCommandAction CommandParameter="{Binding ElementName=lv, Path=SelectedItem}"
                                   Command="{Binding OnOpenLinkCommand}"/>
        </i:EventTrigger>
    </i:Interaction.Triggers>
    <ListView.View>
        <GridView>
            <GridViewColumn Header="Name" DisplayMemberBinding="{Binding Name}" />
            <GridViewColumn Header="Developed By" DisplayMemberBinding="{Binding DevelopedBy}" />
        </GridView>
    </ListView.View>
</ListView>

Adapting the code above should be enough to make the double click event work on your ViewModel, however I added you the Model and View Model class from my example so you can have the full idea.

Model:

public class ApplicationModel
{
    public string Name { get; set; }

    public string DevelopedBy { get; set; }
}

View Model:

public class AppListVM : BaseVM
{
        public AppListVM()
        {
            _onOpenLinkCommand = new DelegateCommand(OnOpenLink);
            _appsSource = new ObservableCollection<ApplicationModel>();
            _appsSource.Add(new ApplicationModel("TEST", "Luis"));
            _appsSource.Add(new ApplicationModel("PROD", "Laurent"));
        }

        private ObservableCollection<ApplicationModel> _appsSource = null;

        public ObservableCollection<ApplicationModel> AppsSource
        {
            get => _appsSource;
            set => SetProperty(ref _appsSource, value, nameof(AppsSource));
        }

        private readonly DelegateCommand _onOpenLinkCommand = null;

        public ICommand OnOpenLinkCommand => _onOpenLinkCommand;

        private void OnOpenLink(object commandParameter)
        {
            ApplicationModel app = commandParameter as ApplicationModel;

            if (app != null)
            {
                //Your code here
            }
        }
}

In case you need the implementation of the DelegateCommand class.

luis_laurent
  • 784
  • 1
  • 12
  • 32
1

I saw the solution from rushui with the InuptBindings but I was still unable to hit the area of the ListViewItem where there was no text - even after setting the background to transparent, so I solved it by using different templates.

This template is for when the ListViewItem has been selected and is active:

<ControlTemplate x:Key="SelectedActiveTemplate" TargetType="{x:Type ListViewItem}">
   <Border Background="LightBlue" HorizontalAlignment="Stretch">
   <!-- Bind the double click to a command in the parent view model -->
      <Border.InputBindings>
         <MouseBinding Gesture="LeftDoubleClick" 
                       Command="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=DataContext.ItemSelectedCommand}"
                       CommandParameter="{Binding}" />
      </Border.InputBindings>
      <TextBlock Text="{Binding TextToShow}" />
   </Border>
</ControlTemplate>

This template is for when the ListViewItem has been selected and is inactive:

<ControlTemplate x:Key="SelectedInactiveTemplate" TargetType="{x:Type ListViewItem}">
   <Border Background="Lavender" HorizontalAlignment="Stretch">
      <TextBlock Text="{Binding TextToShow}" />
   </Border>
</ControlTemplate>

This is the Default style used for the ListViewItem:

<Style TargetType="{x:Type ListViewItem}">
   <Setter Property="Template">
      <Setter.Value>
         <ControlTemplate>
            <Border HorizontalAlignment="Stretch">
               <TextBlock Text="{Binding TextToShow}" />
            </Border>
         </ControlTemplate>
      </Setter.Value>
   </Setter>
   <Style.Triggers>
      <MultiTrigger>
         <MultiTrigger.Conditions>
            <Condition Property="IsSelected" Value="True" />
            <Condition Property="Selector.IsSelectionActive" Value="True" />
         </MultiTrigger.Conditions>
         <Setter Property="Template" Value="{StaticResource SelectedActiveTemplate}" />
      </MultiTrigger>
      <MultiTrigger>
         <MultiTrigger.Conditions>
            <Condition Property="IsSelected" Value="True" />
            <Condition Property="Selector.IsSelectionActive" Value="False" />
         </MultiTrigger.Conditions>
         <Setter Property="Template" Value="{StaticResource SelectedInactiveTemplate}" />
      </MultiTrigger>
   </Style.Triggers>
</Style>

What I don't like is the repetition of the TextBlock and its text binding, I don't know I I can get around declaring that in just the one location.

I hope this helps someone!

user3235445
  • 155
  • 1
  • 10
  • This is a great solution and I use a similar one, but you really only need one control template. If a user is going to be double clicking a `listviewitem`, they probably do not care if it is already selected or not. Also it is important to note the highlight effect might also need to be tweaked to match the `listview` style. Up-voted. – David Bentley Dec 01 '17 at 16:15
1

Here's a behavior that gets that done on both ListBox and ListView.

public class ItemDoubleClickBehavior : Behavior<ListBox>
{
    #region Properties
    MouseButtonEventHandler Handler;
    #endregion

    #region Methods

    protected override void OnAttached()
    {
        base.OnAttached();

        AssociatedObject.PreviewMouseDoubleClick += Handler = (s, e) =>
        {
            e.Handled = true;
            if (!(e.OriginalSource is DependencyObject source)) return;

            ListBoxItem sourceItem = source is ListBoxItem ? (ListBoxItem)source : 
                source.FindParent<ListBoxItem>();

            if (sourceItem == null) return;

            foreach (var binding in AssociatedObject.InputBindings.OfType<MouseBinding>())
            {
                if (binding.MouseAction != MouseAction.LeftDoubleClick) continue;

                ICommand command = binding.Command;
                object parameter = binding.CommandParameter;

                if (command.CanExecute(parameter))
                    command.Execute(parameter);
            }
        };
    }

    protected override void OnDetaching()
    {
        base.OnDetaching();
        AssociatedObject.PreviewMouseDoubleClick -= Handler;
    }

    #endregion
}

Here's the extension class used to find the parent.

public static class UIHelper
{
    public static T FindParent<T>(this DependencyObject child, bool debug = false) where T : DependencyObject
    {
        DependencyObject parentObject = VisualTreeHelper.GetParent(child);

        //we've reached the end of the tree
        if (parentObject == null) return null;

        //check if the parent matches the type we're looking for
        if (parentObject is T parent)
            return parent;
        else
            return FindParent<T>(parentObject);
    }
}

Usage:

xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
xmlns:coreBehaviors="{{Your Behavior Namespace}}"


<ListView AllowDrop="True" ItemsSource="{Binding Data}">
    <i:Interaction.Behaviors>
       <coreBehaviors:ItemDoubleClickBehavior/>
    </i:Interaction.Behaviors>

    <ListBox.InputBindings>
       <MouseBinding MouseAction="LeftDoubleClick" Command="{Binding YourCommand}"/>
    </ListBox.InputBindings>
</ListView>
Prince Owen
  • 1,225
  • 12
  • 20