При очистке ObservableCollection в e.OldItems нет элементов
У меня есть кое-что, что действительно застает меня врасплох.
У меня есть ObservableCollection T, которая заполнена элементами. У меня также есть обработчик события, прикрепленный к событию CollectionChanged.
Когда вы очищаете коллекцию, она вызывает событие CollectionChanged с e.Action, для которого установлено значение NotifyCollectionChangedAction.Reset. Хорошо это нормально Но что странно, так это то, что ни в e.OldItems, ни в e.NewItems ничего нет. Я ожидаю, что e.OldItems будет заполнен всеми элементами, которые были удалены из коллекции.
Кто-нибудь еще видел это? И если так, как они обошли это?
Немного предыстории: я использую событие CollectionChanged для присоединения и отсоединения от другого события и, следовательно, если я не получу никаких элементов в e.OldItems... я не смогу отсоединиться от этого события.
ПОЯСНЕНИЕ: я знаю, что в документации не указано, что она должна вести себя так. Но для любого другого действия, оно уведомляет меня о том, что оно сделало. Итак, я предполагаю, что это скажет мне... и в случае сброса / сброса.
Ниже приведен пример кода, если вы хотите воспроизвести его самостоятельно. Сначала от xaml:
<Window
x:Class="ObservableCollection.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1"
Height="300"
Width="300"
>
<StackPanel>
<Button x:Name="addButton" Content="Add" Width="100" Height="25" Margin="10" Click="addButton_Click"/>
<Button x:Name="moveButton" Content="Move" Width="100" Height="25" Margin="10" Click="moveButton_Click"/>
<Button x:Name="removeButton" Content="Remove" Width="100" Height="25" Margin="10" Click="removeButton_Click"/>
<Button x:Name="replaceButton" Content="Replace" Width="100" Height="25" Margin="10" Click="replaceButton_Click"/>
<Button x:Name="resetButton" Content="Reset" Width="100" Height="25" Margin="10" Click="resetButton_Click"/>
</StackPanel>
</Window>
Далее код позади:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Collections.ObjectModel;
namespace ObservableCollection
{
/// <summary>
/// Interaction logic for Window1.xaml
/// </summary>
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
_integerObservableCollection.CollectionChanged += new System.Collections.Specialized.NotifyCollectionChangedEventHandler(_integerObservableCollection_CollectionChanged);
}
private void _integerObservableCollection_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
case System.Collections.Specialized.NotifyCollectionChangedAction.Add:
break;
case System.Collections.Specialized.NotifyCollectionChangedAction.Move:
break;
case System.Collections.Specialized.NotifyCollectionChangedAction.Remove:
break;
case System.Collections.Specialized.NotifyCollectionChangedAction.Replace:
break;
case System.Collections.Specialized.NotifyCollectionChangedAction.Reset:
break;
default:
break;
}
}
private void addButton_Click(object sender, RoutedEventArgs e)
{
_integerObservableCollection.Add(25);
}
private void moveButton_Click(object sender, RoutedEventArgs e)
{
_integerObservableCollection.Move(0, 19);
}
private void removeButton_Click(object sender, RoutedEventArgs e)
{
_integerObservableCollection.RemoveAt(0);
}
private void replaceButton_Click(object sender, RoutedEventArgs e)
{
_integerObservableCollection[0] = 50;
}
private void resetButton_Click(object sender, RoutedEventArgs e)
{
_integerObservableCollection.Clear();
}
private ObservableCollection<int> _integerObservableCollection = new ObservableCollection<int> { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 };
}
}
20 ответов
Хорошо, хотя я все еще хочу, чтобы ObservableCollection вела себя так, как мне хотелось... код, приведенный ниже, - это то, что я в итоге сделал. По сути, я создал новую коллекцию T с именем TrulyObservableCollection и переопределил метод ClearItems, который затем использовал для вызова события Clearing.
В коде, который использует эту TrulyObservableCollection, я использую это событие Clearing для циклического перебора элементов , которые все еще находятся в коллекции на тот момент, чтобы отсоединить событие, от которого я хотел отсоединиться.
Надеюсь, что этот подход помогает кому-то еще.
public class TrulyObservableCollection<T> : ObservableCollection<T>
{
public event EventHandler<EventArgs> Clearing;
protected virtual void OnClearing(EventArgs e)
{
if (Clearing != null)
Clearing(this, e);
}
protected override void ClearItems()
{
OnClearing(EventArgs.Empty);
base.ClearItems();
}
}
Он не претендует на включение старых элементов, потому что сброс не означает, что список был очищен
Это означает, что произошла какая-то драматическая вещь, и стоимость разработки добавления / удаления, скорее всего, превысила бы стоимость простого повторного сканирования списка с нуля... так что вы должны это сделать.
MSDN предлагает пример полной сортировки коллекции в качестве кандидата для сброса.
Повторить. Сброс не означает ясно, это означает, что ваши предположения о списке теперь недействительны. Рассматривайте это, как будто это совершенно новый список. Ясно, что это один из примеров, но вполне могут быть и другие.
Некоторые примеры:
У меня был такой список с большим количеством элементов, и он был связан с WPF ListView
для отображения на экране.
Если вы очистите список и поднимите .Reset
событие, производительность в значительной степени мгновенно, но если вы вместо этого поднимите много отдельных .Remove
В некоторых случаях производительность ужасна, поскольку WPF удаляет элементы по одному. Я также использовал .Reset
в моем собственном коде, чтобы указать, что список был пересортирован, а не выдавать тысячи отдельных Move
операции. Как и в случае с Clear, при повышении количества отдельных событий происходит значительное снижение производительности.
У нас была такая же проблема здесь. Действие Reset в CollectionChanged не включает OldItems. У нас был обходной путь: вместо этого мы использовали следующий метод расширения:
public static void RemoveAll(this IList list)
{
while (list.Count > 0)
{
list.RemoveAt(list.Count - 1);
}
}
В итоге мы не поддержали функцию Clear() и создали исключение NotSupportedException в событии CollectionChanged для действий сброса. RemoveAll вызовет действие Remove в событии CollectionChanged с соответствующими OldItems.
Хорошо, я знаю, что это очень старый вопрос, но я нашел хорошее решение проблемы и подумал, что поделюсь. Это решение черпает вдохновение во многих замечательных ответах, но имеет следующие преимущества:
- Нет необходимости создавать новый класс и переопределять методы из ObservableCollection
- Не вмешивается в работу NotifyCollectionChanged (поэтому не стоит возиться с Reset)
- Не использует отражение
Вот код:
public static void Clear<T>(this ObservableCollection<T> collection, Action<ObservableCollection<T>> unhookAction)
{
unhookAction.Invoke(collection);
collection.Clear();
}
Этот метод расширения просто занимает Action
который будет вызван до очистки коллекции.
Другой вариант - заменить событие Reset одним событием Remove, которое имеет все очищенные элементы в свойстве OldItems следующим образом:
public class ObservableCollectionNoReset<T> : ObservableCollection<T>
{
protected override void ClearItems()
{
List<T> removed = new List<T>(this);
base.ClearItems();
base.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removed));
}
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
if (e.Action != NotifyCollectionChangedAction.Reset)
base.OnCollectionChanged(e);
}
// Constructors omitted
...
}
Преимущества:
Нет необходимости подписываться на дополнительное событие (как того требует принятый ответ)
Не генерирует событие для каждого удаленного объекта (некоторые другие предложенные решения приводят к нескольким удаленным событиям).
Подписчик должен только проверять NewItems & OldItems для любого события, чтобы добавлять / удалять обработчики событий по мере необходимости.
Недостатки:
Нет сброса события
Небольшие (?) Накладные расходы при создании копии списка.
???
РЕДАКТИРОВАТЬ 2012-02-23
К сожалению, при привязке к элементам управления на основе списка WPF очистка коллекции ObservableCollectionNoReset с несколькими элементами приведет к исключению "Действия диапазона не поддерживаются". Для использования с элементами управления с этим ограничением я изменил класс ObservableCollectionNoReset на:
public class ObservableCollectionNoReset<T> : ObservableCollection<T>
{
// Some CollectionChanged listeners don't support range actions.
public Boolean RangeActionsSupported { get; set; }
protected override void ClearItems()
{
if (RangeActionsSupported)
{
List<T> removed = new List<T>(this);
base.ClearItems();
base.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removed));
}
else
{
while (Count > 0 )
base.RemoveAt(Count - 1);
}
}
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
if (e.Action != NotifyCollectionChangedAction.Reset)
base.OnCollectionChanged(e);
}
public ObservableCollectionNoReset(Boolean rangeActionsSupported = false)
{
RangeActionsSupported = rangeActionsSupported;
}
// Additional constructors omitted.
}
Это не так эффективно, когда RangeActionsSupported имеет значение false (по умолчанию), потому что для каждого объекта в коллекции создается одно уведомление об удалении
Я нашел решение, которое позволяет пользователю извлекать выгоду из эффективности добавления или удаления множества элементов одновременно, при этом запуская только одно событие, и удовлетворять потребности UIElements в получении аргументов события Action.Reset, в то время как все остальные пользователи как список элементов, добавленных и удаленных.
Это решение включает переопределение события CollectionChanged. Когда мы запускаем это событие, мы можем фактически посмотреть на цель каждого зарегистрированного обработчика и определить их тип. Поскольку только классы ICollectionView требуют NotifyCollectionChangedAction.Reset
Аргументы при изменении более чем одного элемента, мы можем выделить их и предоставить всем остальным правильные аргументы событий, которые содержат полный список удаленных или добавленных элементов. Ниже приведена реализация.
public class BaseObservableCollection<T> : ObservableCollection<T>
{
//Flag used to prevent OnCollectionChanged from firing during a bulk operation like Add(IEnumerable<T>) and Clear()
private bool _SuppressCollectionChanged = false;
/// Overridden so that we may manually call registered handlers and differentiate between those that do and don't require Action.Reset args.
public override event NotifyCollectionChangedEventHandler CollectionChanged;
public BaseObservableCollection() : base(){}
public BaseObservableCollection(IEnumerable<T> data) : base(data){}
#region Event Handlers
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
if( !_SuppressCollectionChanged )
{
base.OnCollectionChanged(e);
if( CollectionChanged != null )
CollectionChanged.Invoke(this, e);
}
}
//CollectionViews raise an error when they are passed a NotifyCollectionChangedEventArgs that indicates more than
//one element has been added or removed. They prefer to receive a "Action=Reset" notification, but this is not suitable
//for applications in code, so we actually check the type we're notifying on and pass a customized event args.
protected virtual void OnCollectionChangedMultiItem(NotifyCollectionChangedEventArgs e)
{
NotifyCollectionChangedEventHandler handlers = this.CollectionChanged;
if( handlers != null )
foreach( NotifyCollectionChangedEventHandler handler in handlers.GetInvocationList() )
handler(this, !(handler.Target is ICollectionView) ? e : new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
#endregion
#region Extended Collection Methods
protected override void ClearItems()
{
if( this.Count == 0 ) return;
List<T> removed = new List<T>(this);
_SuppressCollectionChanged = true;
base.ClearItems();
_SuppressCollectionChanged = false;
OnCollectionChangedMultiItem(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removed));
}
public void Add(IEnumerable<T> toAdd)
{
if( this == toAdd )
throw new Exception("Invalid operation. This would result in iterating over a collection as it is being modified.");
_SuppressCollectionChanged = true;
foreach( T item in toAdd )
Add(item);
_SuppressCollectionChanged = false;
OnCollectionChangedMultiItem(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, new List<T>(toAdd)));
}
public void Remove(IEnumerable<T> toRemove)
{
if( this == toRemove )
throw new Exception("Invalid operation. This would result in iterating over a collection as it is being modified.");
_SuppressCollectionChanged = true;
foreach( T item in toRemove )
Remove(item);
_SuppressCollectionChanged = false;
OnCollectionChangedMultiItem(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, new List<T>(toRemove)));
}
#endregion
}
Я занялся этим немного по-другому, так как хотел зарегистрироваться на одно событие и обрабатывать все добавления и удаления в обработчике событий. Я начал с переопределения события изменения коллекции и перенаправления действий сброса на действия удаления со списком элементов. Все это пошло не так, когда я использовал наблюдаемую коллекцию в качестве источника элементов для представления коллекции и получил "Действия диапазона не поддерживаются".
Наконец, я создал новое событие CollectionChangedRange, которое действует так, как я ожидал от встроенной версии.
Я не могу себе представить, почему это ограничение будет разрешено, и надеюсь, что этот пост, по крайней мере, помешает другим выйти из тупика, что я и сделал.
/// <summary>
/// An observable collection with support for addrange and clear
/// </summary>
/// <typeparam name="T"></typeparam>
[Serializable]
[TypeConverter(typeof(ExpandableObjectConverter))]
public class ObservableCollectionRange<T> : ObservableCollection<T>
{
private bool _addingRange;
[field: NonSerialized]
public event NotifyCollectionChangedEventHandler CollectionChangedRange;
protected virtual void OnCollectionChangedRange(NotifyCollectionChangedEventArgs e)
{
if ((CollectionChangedRange == null) || _addingRange) return;
using (BlockReentrancy())
{
CollectionChangedRange(this, e);
}
}
public void AddRange(IEnumerable<T> collection)
{
CheckReentrancy();
var newItems = new List<T>();
if ((collection == null) || (Items == null)) return;
using (var enumerator = collection.GetEnumerator())
{
while (enumerator.MoveNext())
{
_addingRange = true;
Add(enumerator.Current);
_addingRange = false;
newItems.Add(enumerator.Current);
}
}
OnCollectionChangedRange(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, newItems));
}
protected override void ClearItems()
{
CheckReentrancy();
var oldItems = new List<T>(this);
base.ClearItems();
OnCollectionChangedRange(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, oldItems));
}
protected override void InsertItem(int index, T item)
{
CheckReentrancy();
base.InsertItem(index, item);
OnCollectionChangedRange(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, index));
}
protected override void MoveItem(int oldIndex, int newIndex)
{
CheckReentrancy();
var item = base[oldIndex];
base.MoveItem(oldIndex, newIndex);
OnCollectionChangedRange(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Move, item, newIndex, oldIndex));
}
protected override void RemoveItem(int index)
{
CheckReentrancy();
var item = base[index];
base.RemoveItem(index);
OnCollectionChangedRange(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item, index));
}
protected override void SetItem(int index, T item)
{
CheckReentrancy();
var oldItem = base[index];
base.SetItem(index, item);
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, oldItem, item, index));
}
}
/// <summary>
/// A read only observable collection with support for addrange and clear
/// </summary>
/// <typeparam name="T"></typeparam>
[Serializable]
[TypeConverter(typeof(ExpandableObjectConverter))]
public class ReadOnlyObservableCollectionRange<T> : ReadOnlyObservableCollection<T>
{
[field: NonSerialized]
public event NotifyCollectionChangedEventHandler CollectionChangedRange;
public ReadOnlyObservableCollectionRange(ObservableCollectionRange<T> list) : base(list)
{
list.CollectionChangedRange += HandleCollectionChangedRange;
}
private void HandleCollectionChangedRange(object sender, NotifyCollectionChangedEventArgs e)
{
OnCollectionChangedRange(e);
}
protected virtual void OnCollectionChangedRange(NotifyCollectionChangedEventArgs args)
{
if (CollectionChangedRange != null)
{
CollectionChangedRange(this, args);
}
}
}
Вот как работает ObservableCollection, вы можете обойти это, сохранив свой собственный список за пределами ObservableCollection (добавление в список, когда действие - Добавить, удаление, когда действие - Удалить и т. Д.), Затем вы можете получить все удаленные элементы (или добавленные элементы).) когда действие сбрасывается путем сравнения вашего списка с ObservableCollection.
Другой вариант - создать свой собственный класс, который реализует IList и INotifyCollectionChanged, затем вы можете присоединять и отсоединять события из этого класса (или устанавливать OldItems на Clear, если хотите) - это на самом деле не сложно, но это много печатает.
Для сценария присоединения и отсоединения обработчиков событий к элементам ObservableCollection также существует решение "на стороне клиента". В коде обработки событий вы можете проверить, находится ли отправитель в ObservableCollection с помощью метода Contains. Pro: вы можете работать с любой существующей коллекцией ObservableCollection. Минусы: метод Contains работает с O(n), где n - количество элементов в ObservableCollection. Так что это решение для небольших ObservableCollections.
Другое решение на стороне клиента - использовать обработчик событий посередине. Просто зарегистрируйте все события в обработчике посередине. Этот обработчик событий в свою очередь уведомляет реальный обработчик события через обратный вызов или событие. Если происходит сброс, удалите обратный вызов или событие, создайте новый обработчик событий посередине и забудьте о старом. Этот подход также работает для больших ObservableCollections. Я использовал это для события PropertyChanged (см. Код ниже).
/// <summary>
/// Helper class that allows to "detach" all current Eventhandlers by setting
/// DelegateHandler to null.
/// </summary>
public class PropertyChangedDelegator
{
/// <summary>
/// Callback to the real event handling code.
/// </summary>
public PropertyChangedEventHandler DelegateHandler;
/// <summary>
/// Eventhandler that is registered by the elements.
/// </summary>
/// <param name="sender">the element that has been changed.</param>
/// <param name="e">the event arguments</param>
public void PropertyChangedHandler(Object sender, PropertyChangedEventArgs e)
{
if (DelegateHandler != null)
{
DelegateHandler(sender, e);
}
else
{
INotifyPropertyChanged s = sender as INotifyPropertyChanged;
if (s != null)
s.PropertyChanged -= PropertyChangedHandler;
}
}
}
Ну, я решил запачкаться этим сам.
Microsoft приложила немало усилий, чтобы убедиться, что NotifyCollectionChangedEventArgs не имеет никаких данных при вызове сброса. Я предполагаю, что это было решение производительности / памяти. Если вы сбрасываете коллекцию с 100 000 элементов, я предполагаю, что они не хотели дублировать все эти элементы.
Но, поскольку в моих коллекциях никогда не бывает более 100 элементов, я не вижу в этом проблемы.
В любом случае я создал унаследованный класс с помощью следующего метода:
protected override void ClearItems()
{
CheckReentrancy();
List<TItem> oldItems = new List<TItem>(Items);
Items.Clear();
OnPropertyChanged(new PropertyChangedEventArgs("Count"));
OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
NotifyCollectionChangedEventArgs e =
new NotifyCollectionChangedEventArgs
(
NotifyCollectionChangedAction.Reset
);
FieldInfo field =
e.GetType().GetField
(
"_oldItems",
BindingFlags.Instance | BindingFlags.NonPublic
);
field.SetValue(e, oldItems);
OnCollectionChanged(e);
}
Интерфейс ObservableCollection, а также интерфейс INotifyCollectionChanged четко написаны с особым вниманием: построение пользовательского интерфейса и его конкретные характеристики производительности.
Если вы хотите получать уведомления об изменениях коллекции, то обычно вас интересуют только события Add и Remove.
Я использую следующий интерфейс:
using System;
using System.Collections.Generic;
/// <summary>
/// Notifies listeners of the following situations:
/// <list type="bullet">
/// <item>Elements have been added.</item>
/// <item>Elements are about to be removed.</item>
/// </list>
/// </summary>
/// <typeparam name="T">The type of elements in the collection.</typeparam>
interface INotifyCollection<T>
{
/// <summary>
/// Occurs when elements have been added.
/// </summary>
event EventHandler<NotifyCollectionEventArgs<T>> Added;
/// <summary>
/// Occurs when elements are about to be removed.
/// </summary>
event EventHandler<NotifyCollectionEventArgs<T>> Removing;
}
/// <summary>
/// Provides data for the NotifyCollection event.
/// </summary>
/// <typeparam name="T">The type of elements in the collection.</typeparam>
public class NotifyCollectionEventArgs<T> : EventArgs
{
/// <summary>
/// Gets or sets the elements.
/// </summary>
/// <value>The elements.</value>
public IEnumerable<T> Items
{
get;
set;
}
}
Я также написал свою собственную перегрузку Collection, где:
- ClearItems повышает удаление
- InsertItem поднимает Добавлено
- RemoveItem поднимает удаление
- SetItem поднимает удаление и добавление
Конечно, AddRange также может быть добавлен.
Глядя на NotifyCollectionChangedEventArgs, выясняется, что OldItems содержит только элементы, измененные в результате действия Replace, Remove или Move. Это не означает, что в Clear будет содержаться что-либо. Я подозреваю, что Clear запускает событие, но не регистрирует удаленные элементы и вообще не вызывает код удаления.
Я просто просматривал некоторые кодовые диаграммы в наборах инструментов Silverlight и WPF и заметил, что они также решили эту проблему (в некотором роде)... и я подумал, что я опубликую их решение.
По сути, они также создали производную коллекцию ObservableCollection и переопределили ClearItems, вызывая удаление для каждого очищаемого элемента.
Вот код:
/// <summary>
/// An observable collection that cannot be reset. When clear is called
/// items are removed individually, giving listeners the chance to detect
/// each remove event and perform operations such as unhooking event
/// handlers.
/// </summary>
/// <typeparam name="T">The type of item in the collection.</typeparam>
public class NoResetObservableCollection<T> : ObservableCollection<T>
{
public NoResetObservableCollection()
{
}
/// <summary>
/// Clears all items in the collection by removing them individually.
/// </summary>
protected override void ClearItems()
{
IList<T> items = new List<T>(this);
foreach (T item in items)
{
Remove(item);
}
}
}
Для простоты, почему бы вам не переопределить метод ClearItem и не делать там что хотите, т.е. отсоединять элементы от события.
public class PeopleAttributeList : ObservableCollection<PeopleAttributeDto>, {
{
protected override void ClearItems()
{
Do what ever you want
base.ClearItems();
}
rest of the code omitted
}
Просто, чисто и содержите в коде коллекции.
Это горячая тема... потому что, на мой взгляд, Microsoft не выполнила свою работу должным образом... еще раз. Не поймите меня неправильно, мне нравится Microsoft, но они не идеальны!
Я прочитал большинство предыдущих комментариев. Я согласен со всеми, кто считает, что Microsoft неправильно запрограммировала Clear().
По моему мнению, по крайней мере, нужен аргумент, чтобы можно было отделить объекты от события... но я также понимаю влияние этого. Затем я придумал это предложенное решение.
Я надеюсь, что это сделает всех счастливыми, или, по крайней мере, большинство всех...
Эрик
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Reflection;
namespace WpfUtil.Collections
{
public static class ObservableCollectionExtension
{
public static void RemoveAllOneByOne<T>(this ObservableCollection<T> obsColl)
{
foreach (T item in obsColl)
{
while (obsColl.Count > 0)
{
obsColl.RemoveAt(0);
}
}
}
public static void RemoveAll<T>(this ObservableCollection<T> obsColl)
{
if (obsColl.Count > 0)
{
List<T> removedItems = new List<T>(obsColl);
obsColl.Clear();
NotifyCollectionChangedEventArgs e =
new NotifyCollectionChangedEventArgs
(
NotifyCollectionChangedAction.Remove,
removedItems
);
var eventInfo =
obsColl.GetType().GetField
(
"CollectionChanged",
BindingFlags.Instance | BindingFlags.NonPublic
);
if (eventInfo != null)
{
var eventMember = eventInfo.GetValue(obsColl);
// note: if eventMember is null
// nobody registered to the event, you can't call it.
if (eventMember != null)
eventMember.GetType().GetMethod("Invoke").
Invoke(eventMember, new object[] { obsColl, e });
}
}
}
}
}
У меня была такая же проблема, и это было мое решение. Вроде работает. Кто-нибудь видит какие-либо потенциальные проблемы с этим подходом?
// overriden so that we can call GetInvocationList
public override event NotifyCollectionChangedEventHandler CollectionChanged;
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
NotifyCollectionChangedEventHandler collectionChanged = CollectionChanged;
if (collectionChanged != null)
{
lock (collectionChanged)
{
foreach (NotifyCollectionChangedEventHandler handler in collectionChanged.GetInvocationList())
{
try
{
handler(this, e);
}
catch (NotSupportedException ex)
{
// this will occur if this collection is used as an ItemsControl.ItemsSource
if (ex.Message == "Range actions are not supported.")
{
handler(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
else
{
throw ex;
}
}
}
}
}
}
Вот некоторые другие полезные методы в моем классе:
public void SetItems(IEnumerable<T> newItems)
{
Items.Clear();
foreach (T newItem in newItems)
{
Items.Add(newItem);
}
NotifyCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
public void AddRange(IEnumerable<T> newItems)
{
int index = Count;
foreach (T item in newItems)
{
Items.Add(item);
}
NotifyCollectionChangedEventArgs e = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, new List<T>(newItems), index);
NotifyCollectionChanged(e);
}
public void RemoveRange(int startingIndex, int count)
{
IList<T> oldItems = new List<T>();
for (int i = 0; i < count; i++)
{
oldItems.Add(Items[startingIndex]);
Items.RemoveAt(startingIndex);
}
NotifyCollectionChangedEventArgs e = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, new List<T>(oldItems), startingIndex);
NotifyCollectionChanged(e);
}
// this needs to be overridden to avoid raising a NotifyCollectionChangedEvent with NotifyCollectionChangedAction.Reset, which our other lists don't support
new public void Clear()
{
RemoveRange(0, Count);
}
public void RemoveWhere(Func<T, bool> criterion)
{
List<T> removedItems = null;
int startingIndex = default(int);
int contiguousCount = default(int);
for (int i = 0; i < Count; i++)
{
T item = Items[i];
if (criterion(item))
{
if (removedItems == null)
{
removedItems = new List<T>();
startingIndex = i;
contiguousCount = 0;
}
Items.RemoveAt(i);
removedItems.Add(item);
contiguousCount++;
}
else if (removedItems != null)
{
NotifyCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removedItems, startingIndex));
removedItems = null;
i = startingIndex;
}
}
if (removedItems != null)
{
NotifyCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removedItems, startingIndex));
}
}
private void NotifyCollectionChanged(NotifyCollectionChangedEventArgs e)
{
OnPropertyChanged(new PropertyChangedEventArgs("Count"));
OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
OnCollectionChanged(e);
}
Я нашел другое "простое" решение, основанное на ObservableCollection, но оно не очень элегантно, потому что использует Reflection... Если вам нравится, вот мое решение:
public class ObservableCollectionClearable<T> : ObservableCollection<T>
{
private T[] ClearingItems = null;
protected override void OnCollectionChanged(System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
case System.Collections.Specialized.NotifyCollectionChangedAction.Reset:
if (this.ClearingItems != null)
{
ReplaceOldItems(e, this.ClearingItems);
this.ClearingItems = null;
}
break;
}
base.OnCollectionChanged(e);
}
protected override void ClearItems()
{
this.ClearingItems = this.ToArray();
base.ClearItems();
}
private static void ReplaceOldItems(System.Collections.Specialized.NotifyCollectionChangedEventArgs e, T[] olditems)
{
Type t = e.GetType();
System.Reflection.FieldInfo foldItems = t.GetField("_oldItems", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
if (foldItems != null)
{
foldItems.SetValue(e, olditems);
}
}
}
Здесь я сохраняю текущие элементы в поле массива в методе ClearItems, затем перехватываю вызов OnCollectionChanged и перезаписываю приватное поле e._oldItems (через Reflections) перед запуском base.OnCollectionChanged
Вы можете переопределить метод ClearItems и вызвать событие с помощью Remove action и OldItems .
public class ObservableCollection<T> : System.Collections.ObjectModel.ObservableCollection<T>
{
protected override void ClearItems()
{
CheckReentrancy();
var items = Items.ToList();
base.ClearItems();
OnPropertyChanged(new PropertyChangedEventArgs("Count"));
OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, items, -1));
}
}
Часть System.Collections.ObjectModel.ObservableCollection<T>
реализация:
public class ObservableCollection<T> : Collection<T>, INotifyCollectionChanged, INotifyPropertyChanged
{
protected override void ClearItems()
{
CheckReentrancy();
base.ClearItems();
OnPropertyChanged(CountString);
OnPropertyChanged(IndexerName);
OnCollectionReset();
}
private void OnPropertyChanged(string propertyName)
{
OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
}
private void OnCollectionReset()
{
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
private const string CountString = "Count";
private const string IndexerName = "Item[]";
}
Пожалуйста, прочитайте эту документацию с открытыми глазами и включенным мозгом. Microsoft все сделала правильно. Вы должны повторно отсканировать свою коллекцию, когда она выдаст вам сообщение Reset. Вы получаете уведомление "Сброс", потому что добавление / удаление каждого элемента (удаление из коллекции и ее добавление обратно в коллекцию) слишком дорого.
Орион Эдвардс совершенно прав (уважаемый человек). Пожалуйста, подумайте шире, читая документацию.
Если твой ObservableCollection
не становится понятным, тогда вы можете попробовать этот код ниже. это может помочь вам:
private TestEntities context; // This is your context
context.Refresh(System.Data.Objects.RefreshMode.StoreWins, context.UserTables); // to refresh the object context