WPF ListView - 检测选中项被点击的事件

68

我正在使用 WPF ListView 控件,该控件显示了一个数据绑定的项目列表。

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

我正在尝试获得与 ListView.SelectionChanged 事件类似的行为,但我还想检测当前选定的项目是否被单击。如果再次单击相同的项目,则SelectionChanged 事件不会触发(显然)。

什么是最好(最清晰)的方法来处理这个问题?

7个回答

96

使用ListView.ItemContainerStyle属性为您的ListViewItems设置EventSetter,以处理PreviewMouseLeftButtonDown事件。然后,在处理程序中检查被点击的项是否已被选中。

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>

后端代码:

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

16
实际上,您可以直接在“ListView”上设置处理程序,不需要使用EventSetter。 - Gábor
2
item.IsSelected在第一次点击时无效,所以我必须双击。if (item!= null) {} 运行良好。 - Farrukh Waheed
@Gábor 设置处理程序不会在选择项目时触发。 - Farrukh Waheed
1
@FarrukhWaheed 我添加了另一个答案来解决你的问题。 - Stacksatty

28

您可以处理 ListView 的 PreviewMouseLeftButtonUp 事件。原因是在处理事件时,ListView 的 SelectedItem 可能仍然为 null,因此不需要处理 PreviewMouseLeftButtonDown 事件。

XAML:

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

代码后端:

private void listView_Click(object sender, RoutedEventArgs e)
{
    var item = (sender as ListView).SelectedItem;
    if (item != null)
    {
        ...
    }
}

1
从技术上讲,这是一个正确的答案,但我不会这样做。我建议不要使用代码后台。我没有投反对票,但我建议在您的视图模型中设置此项。 - Rogala
11
这不是正确答案,因为用户可以在 ListView 的空白区域进行点击,如果当前有选定的项目,则会触发与用户点击选定项目相同的操作。 - Duncan Groenewald
4
代码后台本身没有任何问题。你的业务逻辑不应该依赖于应用程序代码,而应用程序代码也不应该依赖于UI代码。如果你的业务逻辑能够与任何应用程序代码配合使用,而你的应用程序代码能够与任何UI配合使用,那么你就实现了MVVM。在不考虑上下文的情况下施加绝对的人为限制会限制代码质量。有些UI行为是无法在没有C#代码的情况下实现的,并且调用ViewModel.SuperNeatCommand.Execute(param)与绑定到它完全相同。这里可以使用附加属性,但并不总是最佳选择,因为你可能不需要它。 - Daniel
2
@Daniel 实际上,代码后台的本质问题在于倾向于将业务逻辑添加到其中,而不是在业务层(如VM或服务)中编写该业务逻辑。我建议不要使用代码后台,因为很容易混淆视听。使用类似http://www.mvvmlight.net/的东西可以让您创建一个VM,可以在其中存储属性和方法,这样您就可以在XAML中绑定它们。另一个主要好处是,您可以将项目交给UX人员,在Blend中连接对象。 - Rogala
3
@Rogala 你刚刚证实了我的说法。未能将视图与视图模型分离是程序员的缺陷,而不是代码后台固有的缺陷。在视图中仅编写视图代码与在Person类中仅编写Person代码一样容易。如果您可以信任程序员做到其中之一,那么假设他们正确理解MVVM,您也可以信任他们做另一个。如果您不能,那么您就有更大的问题。我同意应该避免使用代码后台,只要XAML使其多余,但代码后台并没有阴谋窃取我们的灵魂。关于绝对的智慧格言有很多。 - Daniel

21
这些都是很好的建议,但如果我是你,我会在你的视图模型中完成。在你的视图模型中,你可以创建一个中继命令,然后将其绑定到你的项目模板中的点击事件。为了确定是否选择了相同的项目,你可以在你的视图模型中存储对所选项目的引用。我喜欢使用MVVM Light来处理绑定。这使得你的项目在未来更容易修改,并允许你在Blend中设置绑定。
最终,你的XAML看起来会像Sergey建议的那样。我建议避免在你的视图中使用代码。我不会在这个答案中写代码,因为有很多例子可以参考。
这里是一个例子: 如何在MVVM Light框架中使用RelayCommand 如果你需要一个例子,请评论,我会添加一个。
~干杯
In your project, add MVVM Light Libraries Only. 创建一个视图类。一般来说,每个视图都有一个视图模型(例如:视图为MainWindow.xaml,视图模型为MainWindowViewModel.cs)。 以下是非常基础的视图模型代码: 所有包含的命名空间(如果它们出现在这里,我假设您已经将它们添加到引用中。MVVM Light可以在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;

现在添加一个基本的公共类:
/// <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;
    }
}

现在创建你的视图模型:
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); }
    }        
}

在您的视图模型中添加一个RelayCommand。请注意,我将其设置为异步,并传递了一个参数。
    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);
    }

在您的视图代码后台中,为您的视图创建一个视图模型属性,并将您的视图的数据上下文设置为该视图模型(请注意,还有其他方法可以实现此目的,但我正在尝试让这个例子变得简单明了)。
public partial class MainWindow : Window
{
    public MainWindowViewModel MyViewModel { get; set; }
    public MainWindow()
    {
        InitializeComponent();

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

在您的 XAML 中,您需要在代码顶部添加一些命名空间。
<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”和“Custom”。

这是ListView:

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

这是 ListView 的 ItemTemplate:
<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>

运行你的应用程序,查看输出窗口。你可以使用转换器来处理所选项目的样式。这似乎非常复杂,但是当你需要将视图与ViewModel分离(例如为多个平台开发ViewModel)时,它会让生活变得更加容易。此外,在Blend中工作会更加容易。一旦你开发了ViewModel,你就可以将其交给设计师,让他们把它变得非常艺术化 :)。MVVM Light添加了一些功能,使Blend能够识别你的ViewModel。在大多数情况下,你可以在ViewModel中做任何想做的事情来影响视图。如果有人阅读这篇文章,我希望你会觉得有帮助。如果你有问题,请让我知道。我在这个示例中使用了MVVM Light,但你也可以不使用它。

1
考虑到 OP 要求“干净”的解决方案,这应该是被接受的答案。一般来说,MVVM 是 WPF 中最常见的干净代码设计模式,使用交互触发器是将视图事件绑定到视图模型命令的方法。XAML 可能会更加复杂,但您可以使用 Blend 生成它,一旦完成,您的应用程序将具有清晰的分离。 - Daniel

6
您可以按照以下方式处理列表视图项的点击事件:
<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
你的答案是所有答案中最好的,但我会扩展一下你绑定到视图模型上的内容,因为之前的答案提到了使用代码后台。+1 - Rogala

2
"最初的回答" 翻译成英文是 "Original Answer"

这对我有效。

单击一行会触发后台代码。

XAML:

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

代码后台:

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.
}

1

我无法按照自己的意愿使得被接受的答案起作用(请参见Farrukh的评论)。

我想出了一个稍微不同的解决方案,感觉更加本地化,因为它在鼠标按钮按下时选择项目,然后当鼠标按钮释放时,您可以对其做出反应:

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>

代码后台:

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
    }
}

0
我建议在单击一个项目后取消选择它,并使用MouseDoubleClick事件。
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);
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接