0

Context

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>

Code behind:

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

Using .Net Framework 4.8.
Add package Microsoft.Xaml.Behaviors.Wpf to the project.

The ListView displays a list. When a selection is done, its value is shown on the console and the list is swapped (there are two lists shown alternatively).

Problem

The "color" list is longer than the "town" list, and when orange or black are selected, after the selection is shown on the console and the list is swapped (normal), the first item of the town list, Dakar, is triggered (unexpected). When debugging, after clicking orange, ProcessChange is invoked 4 times:

  • with parameter orange (expected),
  • with parameter null (unexpected but understandable and discarded in the code. The call is a reentrant call happenning while ProcessChange is processing orange)
  • with parameter Dakar (unexpected and wrong),
  • with null(same as second bullet, also reentrant, occurring while processing unexpected Dakar call)

The resulting console output:

enter image description here enter image description here


Observation: This double event anomaly disappears if the grid rows are set this way:

<Grid.RowDefinitions>
    <RowDefinition Height="auto"/>
    <RowDefinition Height="*"/>         <!-- height set to * -->
</Grid.RowDefinitions>

(or if a 100ms delay is introduced, or a breakpoint is set, in the event handler).


And the question is:

What is the reason Dakar appears on the console after orange?

mins
  • 6,478
  • 12
  • 56
  • 75
  • This might be an artifact of virtualization. Try ``. Note that you don't need a ListView when you don't set its View property. – Clemens Aug 18 '20 at 05:37
  • @Clemens: Thanks, no it doesn't fix the repeated selection. Note the problem is the same with `ListBox` (and likely all `Selector`) – mins Aug 18 '20 at 12:41
  • @mins: Are you saying that the command and the `ProcessChange` method is not invoked at all? Or is the `parameter` null? – mm8 Aug 18 '20 at 14:29
  • @mins: Sorry, can't reproduce/understand. – mm8 Aug 18 '20 at 14:48
  • @mins - Do you see the same behaviour if you clear the array and add the new values in observable collection instead of reinitialising the array and raising property change? – user1672994 Aug 19 '20 at 09:02
  • @user1672994: Yes it's the same. – mins Aug 20 '20 at 07:20

1 Answers1

0

You do not take into account that after changing the list, the selection is reset and the command is triggered a second time without completing the current execution.

It is necessary to exclude its repeated execution during the execution of the command. To recheck a command, you need to call its event from the code. This requires a slightly different implementation of the command.

Base class:

    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

    }

Command without parameter:

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()) { }

    }
}

Typed Parameter Command

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 does not require changes.

To find the reasons for double selection, I added a delay to the method:

    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();
    }

The double choice is almost over. But sometimes it happens anyway. Apparently, this is due to the bounce of the mouse button. That is, instead of one signal of pressing a key from the mouse, sometimes two are received. Setting a small delay (500ms) was able to filter out some of these double events. The filter also helps not to run the command twice, but the item in the new list is still selected. And you cannot select it again. For the command to work, you have to select another element.

The solution may be to increase the delay and postpone it before changing the list. Check how convenient it will be to use in this case:

    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();
    }
EldHasp
  • 6,079
  • 2
  • 9
  • 24
  • Unless I miss something when copy-pasting your code, it doesn't fix the problem of double selection. Can you confirm when you click on "orange" item it doesn't trigger an additional "Dakar" selection (and redisplay the same "color list"? – mins Aug 18 '20 at 13:13
  • I do not understand you. According to the logic of your command, when you select an item from one list, the list automatically changes to another. And I did just that, remove only some bugs. But if you need different logic, then explain a little more clearly with more details. – EldHasp Aug 18 '20 at 13:48
  • Here in this line you have a change in the list with each selection. `ItemCollection = new ObservableCollection (colorList ? colors : towns);` Do you need it? – EldHasp Aug 18 '20 at 13:50
  • Yes, I need this line. I added some clarification in the question. The SelectionChanged event is sometimes triggered twice (not counting the null selection you mentioned -- and which is already rejected in my question code). To be specific: When I click "orange", instead of selecting only "orange" and swapping the list, my code (and yours as well) selects "orange", swaps the list, selects "Dakar", swaps the lists again. This occurs only when selecting "orange" or "black", not "blue" or the other values. – mins Aug 18 '20 at 14:19
  • As it seems to me, I figured out the cause of the problem. Read the updated answer. – EldHasp Aug 19 '20 at 07:19
  • The timer effect was mentioned in the question. "*Apparently, this is due to the bounce of the mouse button*": It doesn't happen for the first three colors, only for the items having an index larger than the maximum in the town list. So the mouse theory is unlikely. – mins Aug 19 '20 at 19:23
  • If you set a small delay after updating the list (as in the first option), sometimes an element from the new list is immediately selected without calling the command. The effect is episodic and does not always appear. Therefore, I think it is still connected with the operation of the mouse, with the operation its keys. – EldHasp Aug 20 '20 at 02:51