Я обнаружил, что, когда я использую CompositeCollection
объединить коллекции элементов, которые переопределить метод Equals()
, и представить, что CompositeCollection
как ItemsSource
для ListBox
, а затем, когда элемент добавляется к одному из комбинированных коллекций, элементы в ListBox
отображаются неправильно.Почему мой CompositeCollection не работает правильно с равнодушными элементами?
Например:
В приведенном выше окне, я просто нажал «Дубликат» кнопку, которая привязана к способу, который создает копию выбранного элемента в ListBox
и добавляет его до конца. Последний элемент в каждом ListBox
был выбран при нажатии кнопки. ListBox
в каждых колонках настроен по-разному, следующим образом (слева направо):
ObservableCollection<T>
гдеT
это класс, в которомEquals()
были преодолен назначаются непосредственно кItemsSource
собственности.ObservableObject<T>
гдеT
класс, в которомEquals()
имеет не подменен ссылается наCollectionContainer
внутриCompositeCollection
. То, чтоCompositeCollection
присвоено собственностиItemsSource
.ObservableCollection<T>
от № 1ListBox
ссылается наCollectionContainer
внутриCompositeCollection
. То, чтоCompositeCollection
присвоено собственностиItemsSource
.- The
CompositeCollection
от # 3ListBox
был приведен к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
все копии исходного предмета будут сравниваться как равные друг другу.
Реализация копий и равенство таким образом выполняет две вещи:
- Легко отслеживать, какие объекты являются копиями и сколько существует экземпляров.
- Это показывает, что, когда используется
CompositeCollection
, исходный объект ошибочно отображается снова, в другом месте, даже если новая копия этого объекта добавляется в источник CollectionContainer в CompositeCollection, в то время как новая копия не отображается на все!
Я всегда настороженно относятся к обвиняя OS/рамки/библиотека иметь ошибку, а не там быть ошибка в моем коде. Это не исключение. Хотя код кажется мне довольно простым, я прекрасно понимаю, что я мог упустить что-то важное в том, как я должен использовать CompositeCollection
, или даже в том, как я переопределил метод Equals()
.
Итак, что, я сделал что-то не так? Если да, то что я сделал?
В качестве альтернативы, существуют ли какие-либо хорошие рабочие условия? Я думаю, что, по крайней мере, три обходные:
- Оберните equatable объект в простом, не equatable объекта, скрывая equatable-Несс из коллекции и
ListBox
. - Повторно создавайте представление каждый раз, когда коллекция изменяется, либо путем вызова
CreateView()
самостоятельно, либо просто создавая совершенно новоеCompositeCollection
каждый раз. - Включите представление по умолчанию, чтобы обновить себя, например. по телефону
CollectionViewSource.GetDefaultView(listBox3.ItemsSource).Refresh()
.
Ни один из первых двух не кажется идеальным, но толчок приходит в себя, либо будет работать нормально. Из этих двух первых кажется, что это было бы лучше с точки зрения производительности, но, конечно, требует много дополнительного кода, который является уродливым и подверженным ошибкам.
Третий рабочий ход кажется лучшим из трех, но может быть не так много отличается от второго. Есть ли лучший способ обмануть представление, чтобы правильно представить коллекцию после ее изменения?
Предлагаемый дубликат - это всего лишь ответный ответ недовольного пользователя Stack Overflow.
Какой смысл переопределять Equals для возврата ReferenceEquals? Скажем, вы делаете этот _equatableItems.Add (новый EquatableItem (_equatableItems [_equatableItems.Count - 1])); 5 раз, это оставляет вас с 5 различными equatableItems. –
@ChrisWohlert: прежде всего, имейте в виду, что приведенный выше код просто _example_. Сравнение равенства проводится просто для целей примера. Мой реальный сценарий предполагает более сложное равенство. ** Но что более важно ** сравнение сравнения сравнений не выполняется по двум сравниваемым элементам, а скорее по стоимости этих двух элементов. Это просто сравнение и требует переопределения метода 'Equals()'. –
Ну, я только что скопировал ваш код в новый проект, и он отлично работает при таргетинге .Net 4 и .Net 4.6.1 (все четыре 'ListBox' выглядят одинаково). Можете ли вы подтвердить, что проблема воспроизводится в «чистой» среде на вашей стороне? – Grx70