2016-04-01 6 views
-4

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

Например:

Four ListBoxes in a window

В приведенном выше окне, я просто нажал «Дубликат» кнопку, которая привязана к способу, который создает копию выбранного элемента в 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. The CompositeCollection от # 3 ListBox был приведен к ICollectionViewFactory, то метод этого объекта CreateView() называется, и результат присваивается непосредственно к ItemsSource собственности.

Другими словами: первый ListBox показывает совокупность equatable элементов, непосредственно связанных с ItemsSource, второй показывает коллекцию , не являющихся -equatable элементов, содержащихся в 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(); 
    } 
} 

Что касается в Copyable объекта элемента: при создании с нуля, объект не имеет никакого «оригинала» и счетчик копия 0. Когда создается из существующего элемента, объект своеобразен так же как оригинал существующего элемента (или этот элемент, если он является оригиналом), а его текст основан на исходном тексте и количестве сделанных копий.

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

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

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


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

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

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

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

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

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

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

+0

Какой смысл переопределять Equals для возврата ReferenceEquals? Скажем, вы делаете этот _equatableItems.Add (новый EquatableItem (_equatableItems [_equatableItems.Count - 1])); 5 раз, это оставляет вас с 5 различными equatableItems. –

+0

@ChrisWohlert: прежде всего, имейте в виду, что приведенный выше код просто _example_. Сравнение равенства проводится просто для целей примера. Мой реальный сценарий предполагает более сложное равенство. ** Но что более важно ** сравнение сравнения сравнений не выполняется по двум сравниваемым элементам, а скорее по стоимости этих двух элементов. Это просто сравнение и требует переопределения метода 'Equals()'. –

+0

Ну, я только что скопировал ваш код в новый проект, и он отлично работает при таргетинге .Net 4 и .Net 4.6.1 (все четыре 'ListBox' выглядят одинаково). Можете ли вы подтвердить, что проблема воспроизводится в «чистой» среде на вашей стороне? – Grx70

ответ

1

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

Это не по всей видимости, были сообщено о Connect, но есть родственная ошибка также с участием неправильного состояния просмотра после обновления до содержимого коллекции, Microsoft зафиксировала:

WPF: CompositeCollection causes ItemsControl.SelectedIndex to incorrectly increment if the source collection adds an item

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