67

I'm using a WPF ListView control which displays a list of databound items.

<ListView ItemsSource={Binding MyItems}>
    <ListView.View>
        <GridView>
            <!-- declare a GridViewColumn for each property -->
        </GridView>
    </ListView.View>
</ListView>

I'm trying to obtain a behavior similar to the ListView.SelectionChanged event, only I want to also detect if the currently selected item is clicked. The SelectionChanged event does not fire if the same item is clicked again (obviously).

What would be the best (cleanest) way to approach this?

ΦXocę 웃 Пepeúpa ツ
  • 47,427
  • 17
  • 69
  • 97
scripni
  • 2,144
  • 2
  • 19
  • 25

7 Answers7

96

Use the ListView.ItemContainerStyle property to give your ListViewItems an EventSetter that will handle the PreviewMouseLeftButtonDown event. Then, in the handler, check to see if the item that was clicked is selected.

XAML:

<ListView ItemsSource={Binding MyItems}>
    <ListView.View>
        <GridView>
            <!-- declare a GridViewColumn for each property -->
        </GridView>
    </ListView.View>
    <ListView.ItemContainerStyle>
        <Style TargetType="ListViewItem">
            <EventSetter Event="PreviewMouseLeftButtonDown" Handler="ListViewItem_PreviewMouseLeftButtonDown" />
        </Style>
    </ListView.ItemContainerStyle>
</ListView>

Code-behind:

private void ListViewItem_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
    var item = sender as ListViewItem;
    if (item != null && item.IsSelected)
    {
        //Do your stuff
    }
}
ChimeraObscura
  • 1,944
  • 14
  • 9
28

You can handle the ListView's PreviewMouseLeftButtonUp event. The reason not to handle the PreviewMouseLeftButtonDown event is that, by the time when you handle the event, the ListView's SelectedItem may still be null.

XAML:

<ListView ... PreviewMouseLeftButtonUp="listView_Click"> ...

Code behind:

private void listView_Click(object sender, RoutedEventArgs e)
{
    var item = (sender as ListView).SelectedItem;
    if (item != null)
    {
        ...
    }
}
lznt
  • 2,330
  • 2
  • 22
  • 27
  • 1
    Technically, this is a correct answer, but I wouldn't do this. I would recommend not using code behind. I didn't down vote, but I would recommend setting this in your view model. – Rogala Apr 27 '15 at 00:47
  • 10
    This is not a correct answer because the user can click on empty space in the ListView and if there is a currently selected item then the action will be triggered as if the user click on the selected item. – Duncan Groenewald Aug 27 '15 at 03:43
  • 3
    @Rogala There is nothing inherently wrong with code-behind. Your business logic should not rely on your app code, and your app code should not rely on your UI code. If your business logic works with any app code, and your app code works with any UI, you have achieved MVVM. Applying absolute, artificial limitations without respect to context inherently limits code quality. Some UI behaviors are impossible without C# code, and calling ViewModel.SuperNeatCommand.Execute(param) is exactly the same as binding to it. An attached property works well here, but it is not always best because YAGNI. – Daniel Apr 23 '18 at 15:50
  • 2
    @Daniel Actually, what is inherently wrong with code behind is the tendency to add business logic to it instead of writing that business logic in a business layer like a VM or service. I would recommend not using code behind because it's so easy to muddy the water. Using something like http://www.mvvmlight.net/ allows you to create a VM that can store your properties and methods in it, and that way you can bind to them in the XAML. The other major benefit is that you can hand your project over to a UX person who can wire up the objects in Blend. – Rogala May 05 '18 at 20:29
  • 2
    @Rogala You just corroborated what I said. Failing to separate your view from your viewmodel is a flaw in the programmer, not inherent to code-behind. It is just as easy to write only view code in the view as it is to write only Person code in the Person class. If you can trust a programmer to do one, you can trust them to do the other assuming they properly understand MVVM. If you can't, you have bigger issues. I agree that code-behind should be avoided wherever XAML makes it redundant, but code-behind does not plot to steal our souls. There are many wise sayings about absolutes. – Daniel May 07 '18 at 01:28
19

These are all great suggestions, but if I were you, I would do this in your view model. Within your view model, you can create a relay command that you can then bind to the click event in your item template. To determine if the same item was selected, you can store a reference to your selected item in your view model. I like to use MVVM Light to handle the binding. This makes your project much easier to modify in the future, and allows you to set the binding in Blend.

When all is said and done, your XAML will look like what Sergey suggested. I would avoid using the code behind in your view. I'm going to avoid writing code in this answer, because there is a ton of examples out there.

Here is one: How to use RelayCommand with the MVVM Light framework

If you require an example, please comment, and I will add one.

~Cheers

I said I wasn't going to do an example, but I am. Here you go.

  1. In your project, add MVVM Light Libraries Only.

  2. Create a class for your view. Generally speaking, you have a view model for each view (view: MainWindow.xaml && viewModel: MainWindowViewModel.cs)

  3. Here is the code for the very, very, very basic view model:

All included namespace (if they show up here, I am assuming you already added the reference to them. MVVM Light is in Nuget)

using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.CommandWpf;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

Now add a basic public class:

/// <summary>
/// Very basic model for example
/// </summary>
public class BasicModel 
{
    public string Id { get; set; }
    public string Text { get; set; }

    /// <summary>
    /// Constructor
    /// </summary>
    /// <param name="text"></param>
    public BasicModel(string text)
    {
        this.Id = Guid.NewGuid().ToString();
        this.Text = text;
    }
}

Now create your viewmodel:

public class MainWindowViewModel : ViewModelBase
{
    public MainWindowViewModel()
    {
        ModelsCollection = new ObservableCollection<BasicModel>(new List<BasicModel>() {
            new BasicModel("Model one")
            , new BasicModel("Model two")
            , new BasicModel("Model three")
        });
    }

    private BasicModel _selectedBasicModel;

    /// <summary>
    /// Stores the selected mode.
    /// </summary>
    /// <remarks>This is just an example, may be different.</remarks>
    public BasicModel SelectedBasicModel 
    {
        get { return _selectedBasicModel; }
        set { Set(() => SelectedBasicModel, ref _selectedBasicModel, value); }
    }

    private ObservableCollection<BasicModel> _modelsCollection;

    /// <summary>
    /// List to bind to
    /// </summary>
    public ObservableCollection<BasicModel> ModelsCollection
    {
        get { return _modelsCollection; }
        set { Set(() => ModelsCollection, ref _modelsCollection, value); }
    }        
}

In your viewmodel, add a relaycommand. Please note, I made this async and had it pass a parameter.

    private RelayCommand<string> _selectItemRelayCommand;
    /// <summary>
    /// Relay command associated with the selection of an item in the observablecollection
    /// </summary>
    public RelayCommand<string> SelectItemRelayCommand
    {
        get
        {
            if (_selectItemRelayCommand == null)
            {
                _selectItemRelayCommand = new RelayCommand<string>(async (id) =>
                {
                    await selectItem(id);
                });
            }

            return _selectItemRelayCommand;
        }
        set { _selectItemRelayCommand = value; }
    }

    /// <summary>
    /// I went with async in case you sub is a long task, and you don't want to lock you UI
    /// </summary>
    /// <returns></returns>
    private async Task<int> selectItem(string id)
    {
        this.SelectedBasicModel = ModelsCollection.FirstOrDefault(x => x.Id == id);
        Console.WriteLine(String.Concat("You just clicked:", SelectedBasicModel.Text));
        //Do async work

        return await Task.FromResult(1);
    }

In the code behind for you view, create a property for you viewmodel and set the datacontext for your view to the viewmodel (please note, there are other ways to do this, but I am trying to make this a simple example.)

public partial class MainWindow : Window
{
    public MainWindowViewModel MyViewModel { get; set; }
    public MainWindow()
    {
        InitializeComponent();

        MyViewModel = new MainWindowViewModel();
        this.DataContext = MyViewModel;
    }
}

In your XAML, you need to add some namespaces to the top of your code

<Window x:Class="Basic_Binding.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
    xmlns:Custom="clr-namespace:GalaSoft.MvvmLight;assembly=GalaSoft.MvvmLight"
    Title="MainWindow" Height="350" Width="525">

I added "i" and "Custom."

Here is the ListView:

<ListView 
        Grid.Row="0" 
        Grid.Column="0" 
        HorizontalContentAlignment="Stretch"
        ItemsSource="{Binding ModelsCollection}"
        ItemTemplate="{DynamicResource BasicModelDataTemplate}">
    </ListView>

Here is the ItemTemplate for the ListView:

<DataTemplate x:Key="BasicModelDataTemplate">
        <Grid>
            <TextBlock Text="{Binding Text}">
                <i:Interaction.Triggers>
                    <i:EventTrigger EventName="MouseLeftButtonUp">
                        <i:InvokeCommandAction 
                            Command="{Binding DataContext.SelectItemRelayCommand, 
                                RelativeSource={RelativeSource FindAncestor, 
                                        AncestorType={x:Type ItemsControl}}}"
                            CommandParameter="{Binding Id}">                                
                        </i:InvokeCommandAction>
                    </i:EventTrigger>
                </i:Interaction.Triggers>
            </TextBlock>
        </Grid>
    </DataTemplate>

Run your application, and check out the output window. You can use a converter to handle the styling of the selected item.

This may seem really complicated, but it makes life a lot easier down the road when you need to separate your view from your ViewModel (e.g. develop a ViewModel for multiple platforms.) Additionally, it makes working in Blend 10x easier. Once you develop your ViewModel, you can hand it over to a designer who can make it look very artsy :). MVVM Light adds some functionality to make Blend recognize your ViewModel. For the most part, you can do just about everything you want to in the ViewModel to affect the view.

If anyone reads this, I hope you find this helpful. If you have questions, please let me know. I used MVVM Light in this example, but you could do this without MVVM Light.

Nimantha
  • 6,405
  • 6
  • 28
  • 69
Rogala
  • 2,679
  • 25
  • 27
  • 1
    Seeing that the OP asked for a "clean" solution, this should be the accepted answer. In general, MVVM is the most common clean-code design pattern for WPF, and using interactive triggers is the way bind view events to view-model commands. The XAML may be a bit more complicated but you can use Blend to generate it, and once done, you'll have a clean separation in your application. – Daniel Jul 24 '20 at 19:04
6

You can handle click on list view item like this:

<ListView.ItemTemplate>
  <DataTemplate>
     <Button BorderBrush="Transparent" Background="Transparent" Focusable="False">
        <i:Interaction.Triggers>
                <i:EventTrigger EventName="Click">
                    <i:InvokeCommandAction Command="{Binding DataContext.MyCommand, ElementName=ListViewName}" CommandParameter="{Binding}"/>
                </i:EventTrigger>
        </i:Interaction.Triggers>
      <Button.Template>
      <ControlTemplate>
         <Grid VerticalAlignment="Stretch" HorizontalAlignment="Stretch">
    ...
  • 1
    Your answer is the best of all the answers, but I would expand on your view model that you are binding to, since the previous answers mention the use of code behind. +1 – Rogala Apr 27 '15 at 00:45
2

This worked for me.

Single-clicking a row triggers the code-behind.

XAML:

<ListView x:Name="MyListView" MouseLeftButtonUp="MyListView_MouseLeftButtonUp">
    <GridView>
        <!-- Declare GridViewColumns. -->
    </GridView>
</ListView.View>

Code-behind:

private void MyListView_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
    System.Windows.Controls.ListView list = (System.Windows.Controls.ListView)sender;
    MyClass selectedObject = (MyClass)list.SelectedItem;
    // Do stuff with the selectedObject.
}
Cesar Daniel
  • 707
  • 1
  • 9
  • 25
1

I couldn't get the accepted answer to work the way I wanted it to (see Farrukh's comment).

I came up with a slightly different solution which also feels more native because it selects the item on mouse button down and then you're able to react to it when the mouse button gets released:

XAML:

<ListView Name="MyListView" ItemsSource={Binding MyItems}>
<ListView.ItemContainerStyle>
    <Style TargetType="ListViewItem">
        <EventSetter Event="PreviewMouseLeftButtonDown" Handler="ListViewItem_PreviewMouseLeftButtonDown" />
        <EventSetter Event="PreviewMouseLeftButtonUp" Handler="ListViewItem_PreviewMouseLeftButtonUp" />
    </Style>
</ListView.ItemContainerStyle>

Code behind:

private void ListViewItem_PreviewMouseLeftButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
    MyListView.SelectedItems.Clear();

    ListViewItem item = sender as ListViewItem;
    if (item != null)
    {
        item.IsSelected = true;
        MyListView.SelectedItem = item;
    }
}

private void ListViewItem_PreviewMouseLeftButtonUp(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
    ListViewItem item = sender as ListViewItem;
    if (item != null && item.IsSelected)
    {
        // do stuff
    }
}
Stacksatty
  • 191
  • 2
  • 13
0

I would also suggest deselecting an item after it has been clicked and use the MouseDoubleClick event

private void listBox_MouseDoubleClick(object sender, MouseButtonEventArgs e)
{
    try {
        //Do your stuff here
        listBox.SelectedItem = null;
        listBox.SelectedIndex = -1;
    } catch (Exception ex) {
        System.Diagnostics.Debug.WriteLine(ex.Message);
    }
}
Ivan Silkin
  • 381
  • 1
  • 7
  • 15