Почему моя CompositeCollection не работает корректно с сопоставимыми элементами?
Я обнаружил, что когда я использую CompositeCollection
объединить коллекции предметов, которые переопределяют Equals()
метод, и представить, что CompositeCollection
как ItemsSource
для ListBox
затем, когда элемент добавляется в одну из объединенных коллекций, элементы в ListBox
не отображаются правильно.
Например:
В приведенном выше окне я только что нажал кнопку "Дублировать", которая связана с методом, который создает копию выбранного элемента в ListBox
и добавляет его в конец. Последний элемент в каждом ListBox
был выбран, когда кнопка была нажата. ListBox
в каждом столбце настраивается по-разному, как показано ниже (слева направо):
-
ObservableCollection<T>
гдеT
это класс, в которомEquals()
был переопределен, назначен непосредственноItemsSource
имущество. -
ObservableObject<T>
гдеT
это класс, в которомEquals()
не был переопределен ссылкамиCollectionContainer
внутриCompositeCollection
, ТотCompositeCollection
назначен наItemsSource
имущество. -
ObservableCollection<T>
из № 1ListBox
ссылается наCollectionContainer
внутриCompositeCollection
, ТотCompositeCollection
назначен наItemsSource
имущество. -
CompositeCollection
от #3ListBox
был брошен в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 будут сравниваться как равные друг другу.
Реализация копий и равенства таким образом решает две задачи:
- Легко отслеживать, какие объекты являются копиями и сколько существует копий.
- Это показывает, что когда
CompositeCollection
Если исходный объект снова ошибочно отображается, в неправильном месте, даже когда новая копия этого объекта добавляется в источник CollectionContainer в CompositeCollection, тогда как новая копия вообще не отображается!
Я всегда опасаюсь обвинять OS/framework/library в наличии ошибки, в отличие от ошибки в моем собственном коде. Это не исключение. Хотя код кажется мне довольно простым, я прекрасно понимаю, что мог упустить что-то важное в том, как я должен использовать CompositeCollection
или даже так, как я переопределил Equals()
метод.
Итак, я сделал что-то не так здесь? Если так, что я сделал?
В качестве альтернативы, есть ли хорошие обходные пути? Я могу придумать как минимум три обходных пути:
- Обернуть уравниваемый объект в простой не уравниваемый объект, скрывая уравниваемость из коллекции и
ListBox
, - Повторно создавайте представление каждый раз, когда коллекция изменяется, либо вызывая
CreateView()
сам или просто создав совершенно новыйCompositeCollection
каждый раз. - Принудительно обновлять представление по умолчанию, например, вызывая
CollectionViewSource.GetDefaultView(listBox3.ItemsSource).Refresh()
,
Ни один из первых двух не кажется идеальным, но толчок приходит к успеху, либо все будет хорошо. Из этих двух, первый кажется, что он будет лучше с точки зрения производительности, но, конечно, требует достаточного количества дополнительного кода, который уродлив и подвержен ошибкам.
Третий обходной путь кажется лучшим из трех, но может не сильно отличаться по производительности от второго. Есть ли какой-нибудь лучший способ заставить представление правильно представить коллекцию после ее изменения?
Предложенный дубликат - просто ответный ответ недовольного пользователя переполнения стека.
1 ответ
Это похоже на ошибку в WPF. При использовании того же исполняемого файла запуск его в системе с установленным.NET 4.5.2 приводит к ошибке, а при запуске в системе с установленным.NET 4.6.1 - нет.
Похоже, об этом не сообщалось в Connect, но есть связанная ошибка, также связанная с неправильным состоянием просмотра после обновлений в изолированной коллекции, которые исправила Microsoft:
Представляется вероятным, что в процессе исправления этой ошибки команда также исправила эту ошибку, намеренно или по счастливой случайности.