如何知道在 ListView 中点击了哪个元素?

How to know which element is tapped in ListView?

我有一个 ListView 和这样一个 DataTemplate,使用 MVVM 模式

<ListView ItemsSource="{Binding Source}"
          IsItemClickEnabled="True"
          commands:ItemsClickCommand.Command="{Binding ItemClickedCommand}">
          <ListView.ItemTemplate>
               <DataTemplate>
                   <StackPanel>
                       <TextBlock Text="{Binding A}" />
                       <Button Content="{Binding B}" />
                   </StackPanel>
               </DataTemplate>
          </ListView.ItemTemplate>
</ListView>

ItemsClickCommand 是这样定义的

public static class ItemsClickCommand
{
    public static readonly DependencyProperty CommandProperty = DependencyProperty.RegisterAttached("Command", typeof(BindableCommand), typeof(ItemsClickCommand), new PropertyMetadata(null, OnCommandPropertyChanged));
    public static void SetCommand(DependencyObject d, BindableCommand value)
    {
        d.SetValue(CommandProperty, value);
    }
    public static BindableCommand GetCommand(DependencyObject d)
    {
        return (BindableCommand)d.GetValue(CommandProperty);
    }
    private static void OnCommandPropertyChanged(DependencyObject d,
        DependencyPropertyChangedEventArgs e)
    {
        var control = d as ListViewBase;
        if (control != null)
            control.ItemClick += OnItemClick;
    }
    private static void OnItemClick(object sender, ItemClickEventArgs e)
    {
        var control = sender as ListViewBase;
        var command = GetCommand(control);
        if (command != null && command.CanExecute(e.OriginalSource))
            command.ExecuteWithMoreParameters(e.OriginalSource, e.ClickedItem);
    }
}

我要问的是我如何知道用户是否点击了 TextBlock 或 Button。 我尝试在 ViewModel 中以这种方式处理 ItemClickCommand 事件以在 VisualTree 中搜索控件(这是最好的解决方案吗?),但是转换为 DependencyObject 不起作用(returns 始终为 null)

public void ItemClicked(object originalSource, object clickedItem)
    {
        var source = originalSourceas DependencyObject;
        if (source == null)
            return;            
    }

想到了一些解决方案

解决方案 1

<ListView 
    x:Name="parent"
    ItemsSource="{Binding Source}" 
    IsItemClickEnabled="True" 
    Margin="20">
    <ListView.ItemTemplate>
        <DataTemplate>
            <StackPanel>
                <TextBlock Text="{Binding A}" />
                <Button 
                    Content="{Binding B}" 
                    Command="{Binding DataContext.BCommand, ElementName=parent}"
                    CommandParameter="{Binding}" />
            </StackPanel>
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>

请注意 ListView 如何将名称设置为 "parent",属性为:x:Name="parent",以及按钮命令的绑定如何使用它。另请注意,该命令将提供一个参数,该参数是对被单击元素的数据源的引用。

此页面的视图模型如下所示:

public class MainViewModel : MvxViewModel
{
    public ObservableCollection<MySource> Source { get; private set; }

    public MvxCommand<MySource> BCommand { get; private set; }

    public MainViewModel()
    {
        Source = new ObservableCollection<MySource>()
        {
            new MySource("e1", "b1"),
            new MySource("e2", "b2"),
            new MySource("e3", "b3"),
        };

        BCommand = new MvxCommand<MySource>(ExecuteBCommand);
    }

    private void ExecuteBCommand(MySource source)
    {
        Debug.WriteLine("ExecuteBCommand. Source: A={0}, B={1}", source.A, source.B);
    }
}

'MvxCommand' 只是 ICommand 的一个特定实现。我在我的示例代码中使用了 MvvMCross,但你不必这样做——你可以使用你需要的任何 MvvM 实现。

如果处理命令的责任在于包含列表的页面的视图模型,则此解决方案是合适的。

解决方案 2

在包含列表的页面的视图模型中处理命令可能并不总是合适的。您可能希望将代码中的逻辑移动到更靠近被单击元素的位置。在这种情况下,将元素的数据模板隔离在它自己的用户控件中,创建一个与该用户控件背后的逻辑相对应的视图模型 class 并在该视图模型中实现该命令。代码如下所示:

ListView 的 XAML:

<ListView 
    ItemsSource="{Binding Source}" 
    IsItemClickEnabled="True" 
    Margin="20">
    <ListView.ItemTemplate>
        <DataTemplate>
            <uc:MyElement DataContext="{Binding Converter={StaticResource MySourceToMyElementViewModelConverter}}" />
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>

代表一个元素的用户控件XAML:

<Grid>
    <StackPanel>
        <TextBlock Text="{Binding Source.A}" />
        <Button Content="{Binding Source.B}" Command="{Binding BCommand}" />
    </StackPanel>
</Grid>

MySourceToMyElementViewModelConverter 的源代码:

public class MySourceToMyElementViewModelConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, string language)
    {
        return new MyElementViewModel((MySource)value);
    }

    public object ConvertBack(object value, Type targetType, object parameter, string language)
    {
        throw new NotSupportedException();
    }
}

主页的视图模型:

public class MainViewModel : MvxViewModel
{
    public ObservableCollection<MySource> Source { get; private set; }

    public MainViewModel()
    {
        Source = new ObservableCollection<MySource>()
        {
            new MySource("e1", "b1"),
            new MySource("e2", "b2"),
            new MySource("e3", "b3"),
        };
    }
}

表示列表中一个元素的用户控件的视图模型:

public class MyElementViewModel : MvxViewModel
{
    public MySource Source { get; private set; }
    public MvxCommand BCommand { get; private set; }

    public MyElementViewModel(MySource source)
    {
        Source = source;
        BCommand = new MvxCommand(ExecuteBCommand);
    }

    private void ExecuteBCommand()
    {
        Debug.WriteLine("ExecuteBCommand. Source: A={0}, B={1}", Source.A, Source.B);
    }
}

解决方案 3

您的示例假设主页的视图模型公开了一个数据模型元素列表。像这样:

public ObservableCollection<MySource> Source { get; private set; }

可以更改主页的视图模型,以便改为公开视图模型元素列表。像这样:

public ObservableCollection<MyElementViewModel> ElementViewModelList { get; private set; }

ElementViewModelList 中的每个元素都对应于 Source 中的一个元素。如果 Source 的内容在 运行 时间发生变化,此解决方案可能会稍微复杂一些。主页的视图模型将需要观察 Source 并相应地更改 ElementViewModelList。沿着这条路走得更远,您可能想要抽象集合映射器的概念(类似于 ICollectionView)并为此提供一些通用代码。

对于此解决方案,XAML 将如下所示:

<ListView 
    ItemsSource="{Binding ElementViewModelList}" 
    IsItemClickEnabled="True" 
    Margin="20">
    <ListView.ItemTemplate>
        <DataTemplate>
            <StackPanel>
                <TextBlock Text="{Binding A}" />
                <Button Content="{Binding B}" Command="{Binding BCommand}" />
            </StackPanel>
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>

解决方案 1、2 和 3 的注释

我看到您的原始示例没有将命令与元素内部的按钮相关联,而是与整个元素相关联。这就提出了一个问题:你打算用内部按钮做什么?您是否会遇到用户可以单击元素或内部按钮的情况?就 UI/UX 而言,这可能不是最佳解决方案。请注意这一点。作为练习,为了更接近您的原始示例,如果您想要将命令与整个元素相关联,可以执行以下操作。
使用自定义样式将整个元素包裹在一个按钮中。该样式将修改视觉上处理点击的方式。最简单的形式是让点击不产生任何视觉效果。此更改应用于 解决方案 1(它也可以很容易地应用于 解决方案 2解决方案 3 ) 看起来像这样:

<ListView 
    x:Name="parent"
    ItemsSource="{Binding Source}" 
    IsItemClickEnabled="True" 
    Margin="20">
    <ListView.ItemTemplate>
        <DataTemplate>
            <Button 
                Command="{Binding DataContext.BCommand, ElementName=parent}"
                CommandParameter="{Binding}" 
                Style="{StaticResource NoVisualEffectButtonStyle}">
                <StackPanel>
                    <TextBlock Text="{Binding A}" />
                    <TextBlock Text="{Binding B}" />
                </StackPanel>
            </Button>
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>

在这种情况下,您必须编写 NoVisualEffectButtonStyle,但这是一项简单的任务。您还需要决定要将哪种命令与内部按钮相关联(否则为什么会有内部按钮)。或者,您更有可能将内部按钮转换为文本框之类的东西。

解决方案 4

使用行为。

首先,添加对 "Behaviors SDK". 的引用。然后修改你的XAML代码:

...
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
xmlns:core="using:Microsoft.Xaml.Interactions.Core"
...

<Grid>

    <ListView ItemsSource="{Binding Source}" IsItemClickEnabled="True" Margin="20">

        <interactivity:Interaction.Behaviors>
            <core:EventTriggerBehavior EventName="ItemClick">
                <core:InvokeCommandAction 
                    Command="{Binding BCommand}" 
                    InputConverter="{StaticResource ItemClickedToMySourceConverter}" />
            </core:EventTriggerBehavior>
        </interactivity:Interaction.Behaviors>

        <ListView.ItemTemplate>
            <DataTemplate>
                <StackPanel>
                    <TextBlock Text="{Binding A}" />
                    <TextBlock Text="{Binding B}" />
                </StackPanel>
            </DataTemplate>
        </ListView.ItemTemplate>
    </ListView>

</Grid>

ItemClickedToMySourceConverter 只是一个普通的值转换器:

public class ItemClickedToMySourceConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, string language)
    {
        return (MySource)(((ItemClickEventArgs)value).ClickedItem);
    }

    public object ConvertBack(object value, Type targetType, object parameter, string language)
    {
        throw new NotSupportedException();
    }
}

视图模型将如下所示:

public class Main4ViewModel : MvxViewModel
{
    public ObservableCollection<MySource> Source { get; private set; }

    public MvxCommand<MySource> BCommand { get; private set; }

    public Main4ViewModel()
    {
        Source = new ObservableCollection<MySource>()
        {
            new MySource("e1", "b1"),
            new MySource("e2", "b2"),
            new MySource("e3", "b3"),
        };

        BCommand = new MvxCommand<MySource>(ExecuteBCommand);
    }

    private void ExecuteBCommand(MySource source)
    {
        Debug.WriteLine("ExecuteBCommand. Source: A={0}, B={1}", source.A, source.B);
    }
}