如何知道在 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);
}
}
我有一个 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".
...
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);
}
}