web-dev-qa-db-fra.com

Comment éviter de tirer plusieurs fois ObservableCollection.CollectionChanged lors du remplacement de tous les éléments ou de l'ajout d'une collection d'éléments

J'ai ObservableCollection<T> collection, et je veux remplacer tous les éléments par une nouvelle collection d'éléments, je pourrais faire:

collection.Clear(); 

OU:

collection.ClearItems();

(BTW, quelle est la différence entre ces deux méthodes?)

Je pourrais également utiliser foreach pour collection.Add un par un, mais cela se déclenchera plusieurs fois

Idem lors de l'ajout d'une collection d'éléments.

ÉDITER:

J'ai trouvé une bonne bibliothèque ici: Enhanced ObservableCollection avec possibilité de retarder ou de désactiver les notifications mais il semble qu'il ne prend pas en charge Silverlight.

42
Peter Lee

ColinE a raison avec toutes ses informations. Je veux seulement ajouter ma sous-classe de ObservableCollection que j'utilise pour ce cas spécifique.

public class SmartCollection<T> : ObservableCollection<T> {
    public SmartCollection()
        : base() {
    }

    public SmartCollection(IEnumerable<T> collection)
        : base(collection) {
    }

    public SmartCollection(List<T> list)
        : base(list) {
    }

    public void AddRange(IEnumerable<T> range) {
        foreach (var item in range) {
            Items.Add(item);
        }

        this.OnPropertyChanged(new PropertyChangedEventArgs("Count"));
        this.OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
        this.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
    }

    public void Reset(IEnumerable<T> range) {
        this.Items.Clear();

        AddRange(range);
    }
}
56
Jehof

Vous pouvez y parvenir en sous-classant ObservableCollection et en implémentant votre propre méthode ReplaceAll. L'implémentation de ces méthodes remplacerait tous les éléments de la propriété interne Items, puis déclencherait un événement CollectionChanged. De même, vous pouvez ajouter une méthode AddRange. Pour une implémentation de ceci, voir la réponse à cette question:

ObservableCollection Ne prend pas en charge la méthode AddRange, donc je suis averti pour chaque élément ajouté, en plus de ce qui concerne INotifyCollectionChanging?

La différence entre Collection.Clear et Collection.ClearItems est que Clear est une méthode d'API publique, tandis que ClearItems est protégé, c'est un point d'extension qui vous permet d'étendre/modifier le comportement de Clear.

10
ColinE

Voici ce que j'ai implémenté pour la référence d'autres personnes:

// http://stackoverflow.com/questions/13302933/how-to-avoid-firing-observablecollection-collectionchanged-multiple-times-when-r
// http://stackoverflow.com/questions/670577/observablecollection-doesnt-support-addrange-method-so-i-get-notified-for-each
public class ObservableCollectionFast<T> : ObservableCollection<T>
{
    public ObservableCollectionFast()
        : base()
    {

    }

    public ObservableCollectionFast(IEnumerable<T> collection)
        : base(collection)
    {

    }

    public ObservableCollectionFast(List<T> list)
        : base(list)
    {

    }

    public virtual void AddRange(IEnumerable<T> collection)
    {
        if (collection.IsNullOrEmpty())
            return;

        foreach (T item in collection)
        {
            this.Items.Add(item);
        }

        this.OnPropertyChanged(new PropertyChangedEventArgs("Count"));
        this.OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
        this.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
        // Cannot use NotifyCollectionChangedAction.Add, because Constructor supports only the 'Reset' action.
    }

    public virtual void RemoveRange(IEnumerable<T> collection)
    {
        if (collection.IsNullOrEmpty())
            return;

        bool removed = false;
        foreach (T item in collection)
        {
            if (this.Items.Remove(item))
                removed = true;
        }

        if (removed)
        {
            this.OnPropertyChanged(new PropertyChangedEventArgs("Count"));
            this.OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
            this.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
            // Cannot use NotifyCollectionChangedAction.Remove, because Constructor supports only the 'Reset' action.
        }
    }

    public virtual void Reset(T item)
    {
        this.Reset(new List<T>() { item });
    }

    public virtual void Reset(IEnumerable<T> collection)
    {
        if (collection.IsNullOrEmpty() && this.Items.IsNullOrEmpty())
            return;

        // Step 0: Check if collection is exactly same as this.Items
        if (IEnumerableUtils.Equals<T>(collection, this.Items))
            return;

        int count = this.Count;

        // Step 1: Clear the old items
        this.Items.Clear();

        // Step 2: Add new items
        if (!collection.IsNullOrEmpty())
        {
            foreach (T item in collection)
            {
                this.Items.Add(item);
            }
        }

        // Step 3: Don't forget the event
        if (this.Count != count)
            this.OnPropertyChanged(new PropertyChangedEventArgs("Count"));
        this.OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
        this.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
    }
}
5
Peter Lee

Je ne peux pas encore commenter les réponses précédentes, donc j'ajoute ici une adaptation RemoveRange des implémentations SmartCollection ci-dessus qui ne lèvera pas une exception C # InvalidOperationException: Collection Was Modified. Il utilise un prédicat pour vérifier si l'élément doit être supprimé, ce qui, dans mon cas, est plus optimal que la création d'un sous-ensemble d'éléments qui répondent aux critères de suppression.

public void RemoveRange(Predicate<T> remove)
{
    // iterates backwards so can remove multiple items without invalidating indexes
    for (var i = Items.Count-1; i > -1; i--) {
        if (remove(Items[i]))
            Items.RemoveAt(i);
    }

    this.OnPropertyChanged(new PropertyChangedEventArgs("Count"));
    this.OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
    this.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}

Exemple:

LogEntries.RemoveRange(i => closeFileIndexes.Contains(i.fileIndex));
0
Heather V.

Au cours des dernières années, j'utilise une solution plus générique pour éliminer trop de notifications ObservableCollection en créant une opération de modification par lots et en notifiant les observateurs avec une action de réinitialisation:

public class ExtendedObservableCollection<T>: ObservableCollection<T>
{
    public ExtendedObservableCollection()
    {
    }

    public ExtendedObservableCollection(IEnumerable<T> items)
        : base(items)
    {
    }

    public void Execute(Action<IList<T>> itemsAction)
    {
        itemsAction(Items);
        OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
    }
}

Son utilisation est simple:

var collection = new ExtendedObservableCollection<string>(new[]
{
    "Test",
    "Items",
    "Here"
});
collection.Execute(items => {
    items.RemoveAt(1);
    items.Insert(1, "Elements");
    items.Add("and there");
});

Appeler Execute générera une notification unique mais avec un inconvénient - la liste sera mise à jour dans l'interface utilisateur dans son ensemble, pas seulement les éléments modifiés. Cela le rend parfait pour les éléments.Clear () suivi par items.AddRange (newItems).

0
too