Tim Pohlmann
Tim Pohlmann

Reputation: 4430

Why is the ComboBox losing its SelectedItem when sorting the ItemsSource?

Consider this simple example:

MainWindow.xaml

<Window x:Class="WPF_Sandbox.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow"
        x:Name="ThisControl">
    <StackPanel>
        <ComboBox ItemsSource="{Binding Collection, ElementName=ThisControl}" SelectedItem="a" />
        <Button x:Name="SortButton">Sort</Button>
    </StackPanel>
</Window>

MainWindow.xaml.cs

using System.Collections.Generic;
using System.Collections.ObjectModel;

namespace WPF_Sandbox
{
    public partial class MainWindow
    {
        public ObservableCollection<string> Collection { get; } = new ObservableCollection<string>(new [] { "b", "a", "c" });

        public MainWindow()
        {
            InitializeComponent();

            SortButton.Click += (s, e) => Sort(Collection);
        }

        public static void Sort<T>(ObservableCollection<T> collection)
        {
            var sortableList = new List<T>(collection);
            sortableList.Sort();

            for (var i = 0; i < sortableList.Count; i++)
                collection.Move(collection.IndexOf(sortableList[i]), i);
        }
    }
}

When starting the program, a is selected. On pressing Sort the selection doesn't change but the list gets sorted (still as expected).
If you a) press Sort again or b) select b or c before sorting, the ComboBox loses its selection and SelectedItem becomes null.

I pinpointed the issue down to the ObservableCollection.Move method. It appears that whenever you call Move(i, i) (so you do not actually move anything) with i being the SelectedItem, the selection goes to hell.

I'm not looking for a solution. Obvious workaround would be to not sort the ObservableCollection at all and use a CollectionViewSource or adjusting the Sort method to only call Move when the two indices actually differ.

The question I have is, why is this happening in the first place? There is no indication in documentation for the Move method that you must not pass the same parameter twice. Also there is no hint why this would not work in the documentation for the CollectionChanged event or the CollectionChangedEventArgs class. Is this a bug in WPF?

Upvotes: 2

Views: 621

Answers (1)

Tim Pohlmann
Tim Pohlmann

Reputation: 4430

I believe this to be a bug in the implementation of the ItemControl's event handling. Take a look here:

case NotifyCollectionChangedAction.Move:
    // items between New and Old have moved.  The direction and
    // exact endpoints depends on whether New comes before Old.
    int left, right, delta;
    if (e.OldStartingIndex < e.NewStartingIndex)
    {
        left = e.OldStartingIndex + 1;
        right = e.NewStartingIndex;
        delta = -1;
    }
    else
    {
        left = e.NewStartingIndex;
        right = e.OldStartingIndex - 1;
        delta = 1;
    }

    foreach (ItemInfo info in list)
    {
        int index = info.Index;
        if (index == e.OldStartingIndex)
        {
             info.Index = e.NewStartingIndex;
        }
        else if (left <= index && index <= right)
        {
             info.Index = index + delta;
        }
    }
break;

Source

The if statement does not seem to expect e.OldStartingIndex and e.NewStartingIndex to be of the same value which results in delta being 1 which then causes some unintended index manipulation inside of the foreach loop. I'm surprised it "only" deselects the item and not completely ruins the whole collection.

Upvotes: 2

Related Questions