如何解决在高度设置为自动的网格单元中使用ListView SelectionChanged时,为什么行为不正确?
上下文
Xaml:
<Window x:Class="WpfApp2.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
xmlns:local="clr-namespace:WpfApp2"
xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
Title="MainWindow" Height="350" Width="400">
<Window.DataContext>
<local:ViewModel/>
</Window.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="The ListView:"/>
<ListView
Grid.Row="1"
ItemsSource="{Binding ItemCollection}"
SelectionMode="Single">
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged">
<i:InvokeCommandAction
Command="{Binding Path=ProcessChangeCommand}"
CommandParameter="{Binding Path=SelectedItem,RelativeSource={RelativeSource FindAncestor,AncestorType={x:Type ItemsControl}}}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</ListView>
</Grid>
</Window>
后面的代码:
public partial class MainWindow : Window {
public MainWindow () {
InitializeComponent ();
}
}
public class ViewModel : INotifyPropertyChanged {
bool colorList;
string[] colors = { "blue","yellow","green","orange","black" };
string[] towns = { "Dakar","Berlin","Toronto" };
private ObservableCollection<string> itemCollection;
public event PropertyChangedEventHandler PropertyChanged;
public ObservableCollection<string> ItemCollection {
get => itemCollection;
set {
itemCollection = value;
RaisePropertyChanged (nameof (ItemCollection));
}
}
public ICommand ProcessChangeCommand { get; private set; }
public ViewModel () {
ProcessChangeCommand = new RelayCommand<string> (ProcessChange);
itemCollection = new ObservableCollection<string> (colors);
}
public void ProcessChange (string parameter) {
if (parameter == null) return;
Debug.WriteLine ($"Selected: {parameter}");
ItemCollection = new ObservableCollection<string> (colorList ? colors : towns);
RaisePropertyChanged (nameof (ItemCollection));
colorList = !colorList;
}
void RaisePropertyChanged ([CallerMemberName] string propertyName = null) {
PropertyChanged?.Invoke (this,new PropertyChangedEventArgs (propertyName));
}
}
public class RelayCommand<T> : ICommand {
readonly Action<T> _execute = null;
public event EventHandler CanExecuteChanged {
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public RelayCommand (Action<T> excute) { _execute = excute; }
public bool CanExecute (object parameter) => true;
public void Execute (object parameter) => _execute ((T) parameter);
}
使用.Net Framework 4.8。
将软件包Microsoft.Xaml.Behaviors.Wpf添加到项目中。
ListView
显示一个列表。完成选择后,其值将显示在控制台上并交换列表(交替显示两个列表)。
问题
在“颜色”列表是比“镇”列表更长,并且当orange
或black
被选择,被示出在控制台上之后选择和列表被交换(正常),触发了城镇列表的第一项Dakar
(意外)。调试时,单击orange
后,ProcessChange
被调用4次:
- 带有参数
orange
(预期), - 带有参数
null
(代码中出乎意料,但可以理解和丢弃。该调用是在ProcessChange
处理orange
时发生的可重入调用) - 带有参数
Dakar
(意外和错误), - 具有null(与第二个项目符号相同,也可重入,在处理意外的
Dakar
调用时发生)
结果控制台输出:
观察:如果以此方式设置网格行,则此双重事件异常消失:
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition Height="*"/> <!-- height set to * -->
</Grid.RowDefinitions>
(或者如果在事件处理程序中引入了100ms延迟或设置了断点)。
问题是:
Dakar
之后orange
出现在控制台上的原因是什么?
解决方法
您无需考虑更改列表后重置选择并在不完成当前执行的情况下再次触发命令的情况。
在命令执行期间必须排除其重复执行。 要重新检查命令,您需要从代码中调用其事件。 这要求该命令的实现稍有不同。
基类:
using System;
using System.Windows;
using System.Windows.Input;
namespace Common
{
#region Delegates for WPF Command Methods
/// <summary>Delegate of the executive team method.</summary>
/// <param name="parameter">Command parameter.</param>
public delegate void ExecuteHandler(object parameter);
/// <summary>Command сan execute method delegate.</summary>
/// <param name="parameter">Command parameter.</param>
/// <returns><see langword="true"/> if command execution is allowed.</returns>
public delegate bool CanExecuteHandler(object parameter);
#endregion
#region Class commands - RelayCommand
/// <summary>A class that implements the ICommand interface for creating WPF commands.</summary>
public class RelayCommand : ICommand
{
private readonly CanExecuteHandler _canExecute;
private readonly ExecuteHandler _onExecute;
private readonly EventHandler _requerySuggested;
public event EventHandler CanExecuteChanged;
/// <summary>Command constructor.</summary>
/// <param name="execute">Executable command method.</param>
/// <param name="canExecute">Method allowing command execution.</param>
public RelayCommand(ExecuteHandler execute,CanExecuteHandler canExecute = null)
{
_onExecute = execute;
_canExecute = canExecute;
_requerySuggested = (o,e) => Invalidate();
CommandManager.RequerySuggested += _requerySuggested;
}
public void Invalidate()
=> Application.Current.Dispatcher.BeginInvoke
(
new Action(() => CanExecuteChanged?.Invoke(this,EventArgs.Empty)),null
);
public bool CanExecute(object parameter) => _canExecute == null ? true : _canExecute.Invoke(parameter);
public void Execute(object parameter) => _onExecute?.Invoke(parameter);
}
#endregion
}
不带参数的命令:
namespace Common
{
#region Delegates for WPF Parameterless Command Methods
/// <summary>Delegate to the execution method of a command without a parameter.</summary>
public delegate void ExecuteActionHandler();
/// <summary>Command state method delegate without parameter.</summary>
/// <returns><see langword="true"/> - if command execution is allowed.</returns>
public delegate bool CanExecuteActionHandler();
#endregion
/// <summary>Class for commands without parameters.</summary>
public class RelayActionCommand : RelayCommand
{
/// <summary>Command constructor.</summary>
/// <param name="execute">Command execution method.</param>
/// <param name="canExecute">Method allowing command execution.</param>
public RelayActionCommand(ExecuteActionHandler execute,CanExecuteActionHandler canExecute = null)
: base(_ => execute(),_ => canExecute()) { }
}
}
键入参数命令
using System.ComponentModel;
namespace Common
{
#region Delegates for WPF Command Methods
/// <summary>Delegate of the executive team method.</summary>
/// <param name="parameter">Command parameter.</param>
public delegate void ExecuteHandler<T>(T parameter);
/// <summary>Command сan execute method delegate.</summary>
/// <param name="parameter">Command parameter.</param>
/// <returns><see langword="true"/> if command execution is allowed.</returns>
public delegate bool CanExecuteHandler<T>(T parameter);
#endregion
/// <summary>Class for typed parameter commands.</summary>
public class RelayCommand<T> : RelayCommand
{
/// <summary>Command constructor.</summary>
/// <param name="execute">Executable command method.</param>
/// <param name="canExecute">Method allowing command execution.</param>
public RelayCommand(ExecuteHandler<T> execute,CanExecuteHandler<T> canExecute = null)
: base
(
p => execute
(
p is T t
? t
: TypeDescriptor.GetConverter(typeof(T)).IsValid(p)
? (T)TypeDescriptor.GetConverter(typeof(T)).ConvertFrom(p)
: default
),p =>
canExecute == null
|| (p is T t
? canExecute(t)
: TypeDescriptor.GetConverter(typeof(T)).IsValid(p) && canExecute((T)TypeDescriptor.GetConverter(typeof(T)).ConvertFrom(p))
)
)
{}
}
}
ViewModel
using Common;
using System;
using System.Collections.ObjectModel;
using System.Diagnostics;
namespace WpfApp2
{
public class ViewModel
{
bool isColorList;
string[] colors = { "blue","yellow","green","orange","black" };
string[] towns = { "Dakar","Berlin","Toronto" };
private bool isExecutedProcessChange = false;
public ObservableCollection<string> ItemCollection { get; }
= new ObservableCollection<string>();
public RelayCommand ProcessChangeCommand { get; }
public ViewModel()
{
ProcessChangeCommand = new RelayCommand<string>(ProcessChange,CanProcessChange);
Array.ForEach(colors,color => ItemCollection.Add(color));
}
private bool CanProcessChange(string parameter)
=>! isExecutedProcessChange;
public void ProcessChange(string parameter)
{
isExecutedProcessChange = true;
ProcessChangeCommand.Invalidate();
if (parameter != null)
{
Debug.WriteLine($"Selected: {parameter}");
ItemCollection.Clear();
Array.ForEach(isColorList ? colors : towns,color => ItemCollection.Add(color));
isColorList = !isColorList;
}
isExecutedProcessChange = false;
ProcessChangeCommand.Invalidate();
}
}
}
XAML不需要更改。
要找到重复选择的原因,我在方法中添加了一个延迟:
public async void ProcessChange(string parameter)
{
isExecutedProcessChange = true;
ProcessChangeCommand.Invalidate();
if (parameter != null)
{
Debug.WriteLine($"Selected: {parameter}");
ItemCollection.Clear();
Array.ForEach(isColorList ? colors : towns,color => ItemCollection.Add(color));
isColorList = !isColorList;
await Task.Delay(500);
}
isExecutedProcessChange = false;
ProcessChangeCommand.Invalidate();
}
双重选择几乎结束了。 但是有时候还是会发生。 显然,这是由于鼠标按钮的弹跳所致。 即,有时会接收到两个信号,而不是一个从鼠标按下键的信号。 设置一个小的延迟(500毫秒)就能过滤掉其中一些双重事件。 该过滤器还有助于避免两次运行该命令,但仍会选择新列表中的项目。 并且您无法再次选择它。 为了使命令生效,您必须选择另一个元素。
解决方案可能是增加延迟并在更改列表之前将其推迟。 检查在这种情况下使用起来有多方便:
public async void ProcessChange(string parameter)
{
isExecutedProcessChange = true;
ProcessChangeCommand.Invalidate();
if (parameter != null)
{
Debug.WriteLine($"Selected: {parameter}");
await Task.Delay(1000); // It is necessary to select the optimal delay time
ItemCollection.Clear();
Array.ForEach(isColorList ? colors : towns,color => ItemCollection.Add(color));
isColorList = !isColorList;
}
isExecutedProcessChange = false;
ProcessChangeCommand.Invalidate();
}
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。