Почему моя CompositeCollection не работает корректно с сопоставимыми элементами?

Я обнаружил, что когда я использую CompositeCollection объединить коллекции предметов, которые переопределяют Equals() метод, и представить, что CompositeCollection как ItemsSource для ListBox затем, когда элемент добавляется в одну из объединенных коллекций, элементы в ListBox не отображаются правильно.

Например:

В приведенном выше окне я только что нажал кнопку "Дублировать", которая связана с методом, который создает копию выбранного элемента в ListBox и добавляет его в конец. Последний элемент в каждом ListBox был выбран, когда кнопка была нажата. ListBox в каждом столбце настраивается по-разному, как показано ниже (слева направо):

  1. ObservableCollection<T> где T это класс, в котором Equals() был переопределен, назначен непосредственно ItemsSource имущество.
  2. ObservableObject<T> где T это класс, в котором Equals() не был переопределен ссылками CollectionContainer внутри CompositeCollection, Тот CompositeCollection назначен на ItemsSource имущество.
  3. ObservableCollection<T> из № 1 ListBox ссылается на CollectionContainer внутри CompositeCollection, Тот CompositeCollection назначен на ItemsSource имущество.
  4. CompositeCollection от #3 ListBox был брошен в ICollectionViewFactory тогда этот объект CreateView() метод вызывается, а результат присваивается непосредственно ItemsSource имущество.

Другими словами: первый ListBox показывает коллекцию сопоставимых предметов, непосредственно связанных с ItemsSource вторая показывает коллекцию неэквивалентных предметов, содержащихся в CompositeCollection который в свою очередь связан с ItemsSource третий объединяет два метода, используя экземпляр коллекции из первого ListBox но положить его внутрь CompositeCollection как во втором ListBox а четвертый просто берет CompositeCollection от третьего и напрямую создает ICollectionView коллекции для показа.


Для этого конкретного теста равенство элементов реализовано так, что два элемента будут сравниваться как равные, если они оба имеют один и тот же базовый текст, т.е. без номера копии в конце (число в скобках, если оно есть). Я сделал это так, чтобы у меня были объекты, которые сравнивались как равные, но при этом я мог бы визуально идентифицировать их как разные экземпляры.

Обратите внимание, что в первых двух ListBox столбцы, последний элемент показан как скопированный, как и ожидалось, но в третьем ListBox столбец, а не новый элемент, отображаемый в конце списка, его исходный источник отображается дважды, причем ни один экземпляр не находится в конце списка.

Как ни странно, хотя CompositeCollection в третьем ListBox не отображается правильно, ICollectionView создан прямо из него! То есть, по крайней мере, когда он впервые отображается. это ICollectionView терпит неудачу так же, как и исходная коллекция, если исходная коллекция изменяется после создания представления (что имеет смысл, так как ICollectionView на самом деле, как ListBox отображает CompositeCollection на первом месте).

XAML:

<Window x:Class="TestCompositeCollectionObservable.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:l="clr-namespace:TestCompositeCollectionObservable"
        Title="MainWindow" Height="350" Width="525">
  <Window.Resources>
    <DataTemplate DataType="{x:Type l:Item}">
      <TextBlock Text="{Binding Text}"/>
    </DataTemplate>
  </Window.Resources>
  <StackPanel>
    <Grid>
      <Grid.ColumnDefinitions>
        <ColumnDefinition/>
        <ColumnDefinition/>
        <ColumnDefinition/>
        <ColumnDefinition/>
      </Grid.ColumnDefinitions>
      <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="Auto"/>
      </Grid.RowDefinitions>      
      <TextBlock Text="Equatable Items in ObservableCollection" Grid.Column="0" TextWrapping="Wrap"/>
      <TextBlock Text="Plain Items in CompositeCollection" Grid.Column="1" TextWrapping="Wrap"/>
      <TextBlock Text="Equatable Items in CompositeCollection" Grid.Column="2" TextWrapping="Wrap"/>
      <TextBlock Text="Equatable Items in Explicit View" Grid.Column="3" TextWrapping="Wrap"/>
      <ListBox x:Name="listBox1" Grid.Row="1"/>
      <ListBox x:Name="listBox2" Grid.Column="1" Grid.Row="1"
               SelectedIndex="{Binding SelectedIndex, ElementName=listBox1}"/>
      <ListBox x:Name="listBox3" Grid.Column="2" Grid.Row="1"
               SelectedIndex="{Binding SelectedIndex, ElementName=listBox1}"/>
      <ListBox x:Name="listBox4" Grid.Column="3" Grid.Row="1"
               SelectedIndex="{Binding SelectedIndex, ElementName=listBox1}"/>
    </Grid>
    <Button Content="Duplicate" Click="Button_Click" HorizontalAlignment="Left"/>
  </StackPanel>
</Window>

MainWindow.cs:

public partial class MainWindow : Window
{
    private static readonly string[] _textValues =
    {
        "item 1",
        "item 2",
        "item 3"
    };

    private readonly ObservableCollection<Item> _items;
    private readonly ObservableCollection<EquatableItem> _equatableItems;

    public MainWindow()
    {
        InitializeComponent();

        _items = new ObservableCollection<Item>(_textValues.Select(text => new Item(text)));
        _equatableItems = new ObservableCollection<EquatableItem>(_textValues.Select(text => new EquatableItem(text)));

        _items.Add(new Item(_items[_items.Count - 1]));
        _equatableItems.Add(new EquatableItem(_equatableItems[_equatableItems.Count - 1]));

        listBox1.ItemsSource = _equatableItems;
        listBox2.ItemsSource = new CompositeCollection
        {
            new CollectionContainer { Collection = _items },
        };
        listBox3.ItemsSource = new CompositeCollection
        {
            new CollectionContainer { Collection = _equatableItems },
        };
    }

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        int index = listBox1.SelectedIndex;

        _items.Add(new Item(_items[index]));
        _equatableItems.Add(new EquatableItem(_equatableItems[index]));

        listBox4.ItemsSource = ((System.ComponentModel.ICollectionViewFactory)listBox3.ItemsSource).CreateView();
    }
}

Item.cs:

class Item
{
    public string Text { get; private set; }
    public Item Original { get { return _original ?? this; } }

    protected readonly Item _original;
    private int _copyCount;

    public Item(string text)
    {
        Text = text;
    }

    public Item(Item item)
    {
        Item original = item.Original;

        Text = string.Format("{0} ({1})", original.Text, ++original._copyCount);
        _original = original;
    }

}

EquatableItem.cs:

class EquatableItem : Item
{
    public EquatableItem(string text) : base(text) { }
    public EquatableItem(EquatableItem item) : base(item) { }

    public override bool Equals(object obj)
    {
        EquatableItem other = obj as EquatableItem;

        if (other == null)
        {
            return false;
        }

        return object.ReferenceEquals(Original, other.Original);
    }

    public override int GetHashCode()
    {
        return Original.Text.GetHashCode();
    }
}

Относительно объекта копируемого предмета: при создании с нуля у объекта нет "оригинала", а количество копий равно 0. При создании из существующего предмета оригинал объекта совпадает с оригиналом существующего предмета (или того предмета, если он оригинал), а его текст основан на оригинальном тексте и количестве сделанных копий.

Для EquatableItem В производном классе все копии исходного объекта item будут сравниваться как равные друг другу.

Реализация копий и равенства таким образом решает две задачи:

  1. Легко отслеживать, какие объекты являются копиями и сколько существует копий.
  2. Это показывает, что когда CompositeCollection Если исходный объект снова ошибочно отображается, в неправильном месте, даже когда новая копия этого объекта добавляется в источник CollectionContainer в CompositeCollection, тогда как новая копия вообще не отображается!


Я всегда опасаюсь обвинять OS/framework/library в наличии ошибки, в отличие от ошибки в моем собственном коде. Это не исключение. Хотя код кажется мне довольно простым, я прекрасно понимаю, что мог упустить что-то важное в том, как я должен использовать CompositeCollection или даже так, как я переопределил Equals() метод.

Итак, я сделал что-то не так здесь? Если так, что я сделал?

В качестве альтернативы, есть ли хорошие обходные пути? Я могу придумать как минимум три обходных пути:

  1. Обернуть уравниваемый объект в простой не уравниваемый объект, скрывая уравниваемость из коллекции и ListBox,
  2. Повторно создавайте представление каждый раз, когда коллекция изменяется, либо вызывая CreateView() сам или просто создав совершенно новый CompositeCollection каждый раз.
  3. Принудительно обновлять представление по умолчанию, например, вызывая CollectionViewSource.GetDefaultView(listBox3.ItemsSource).Refresh(),

Ни один из первых двух не кажется идеальным, но толчок приходит к успеху, либо все будет хорошо. Из этих двух, первый кажется, что он будет лучше с точки зрения производительности, но, конечно, требует достаточного количества дополнительного кода, который уродлив и подвержен ошибкам.

Третий обходной путь кажется лучшим из трех, но может не сильно отличаться по производительности от второго. Есть ли какой-нибудь лучший способ заставить представление правильно представить коллекцию после ее изменения?

Предложенный дубликат - просто ответный ответ недовольного пользователя переполнения стека.

1 ответ

Решение

Это похоже на ошибку в WPF. При использовании того же исполняемого файла запуск его в системе с установленным.NET 4.5.2 приводит к ошибке, а при запуске в системе с установленным.NET 4.6.1 - нет.

Похоже, об этом не сообщалось в Connect, но есть связанная ошибка, также связанная с неправильным состоянием просмотра после обновлений в изолированной коллекции, которые исправила Microsoft:

WPF: CompositeCollection заставляет ItemsControl.SelectedIndex неправильно увеличиваться, если исходная коллекция добавляет элемент

Представляется вероятным, что в процессе исправления этой ошибки команда также исправила эту ошибку, намеренно или по счастливой случайности.

Другие вопросы по тегам