Bienvenue à Blogs CodeS-SourceS Identification | Inscription | Aide

Abonnements

ObservableCollection… en mieux sans effort

La classe ObservableCollection est une des pire classes que je connaisse dans le framework .NET :

  • Limitée en nombre de méthodes : pas de AddRange par exemple
  • Catastrophique pour les perfs dès qu’on veut faire un Refresh. => Clear + n Add => n+1 events de rafraîchissement envoyés à l’UI
  • L’évènement déclenché par le Clear “oublie” de nous donner la liste des éléments supprimés alors que la définition de  l’évènement le prévoie.

Bref, cette classe fait cruellement tâche dans notre magnifique framework .NET.

Ma première idée serait de la refaire complètement. Cependant, par fainéantise (je l’avoue) et par manque de temps, je me suis dit que j’allais plutôt en hériter.

Commençant par le Reset.

Une UI WPF ou SL, s’abonne à l’évènement CollectionChanged de l’interface INotifyCollectionChanged.

Du coup, nous allons le redéfinir en explicite pour pouvoir le gérer nous-même.

    public new event NotifyCollectionChangedEventHandler CollectionChanged;
 
    event NotifyCollectionChangedEventHandler INotifyCollectionChanged.CollectionChanged
    {
        add { CollectionChanged += value; }
        remove { CollectionChanged -= value; }
    }

Ensuite, nous allons rajouter une méthode BeginEdit et une méthode EndEdit:

    public bool IsEditing { get; private set; }
 
    public void BeginEdit()
    {
        if (! IsEditing)
            IsEditing = true;
   
 
    public void EndEdit()
    {
        if (IsEditing)
        {
            IsEditing = false;
            OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
        }
   
 
    protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
    {
        base.OnCollectionChanged(e);
        if (CollectionChanged != null && ! IsEditing)
            CollectionChanged(this, e);
    }

Maintenant, quand on veut faire un Refresh, on pourra appeler BeginEdit, faire notre Clear puis notre AddRange que nous allons rajouter maintenant et ensuite appeler le EndEdit.

    public void AddRange(IEnumerable<T> items)
    {
        bool isEditing = IsEditing;
        IsEditing = true;
        foreach (T item in items)
            Add(item);
        IsEditing = isEditing;
        OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, items.ToList()));
    }

Enfin, nous allons bloquer l’évènement Reset du Clear pour envoyer un évènement avec l’ensemble des éléments. Cependant, nous allons garder une action Reset plus performance qu’une Action Remove. Le problème c’est que le code de la classe NotifyCollectionChangedEventArgs ne permet pas d’avoir des OldItems avec une action Reset.

Aussi vais-je commencer par me coder une classe MyNotifyCollectionChangedEventArgs qui va hériter de NotifyCollectionChangedEventArgs. Le problème c’est que la propriété OldItems est en read-only. Cette propriété n’est pas virtual. Il va donc falloir la redéfinir. Soit dit en passant, on pourra regretter son type IList au lieu de IEnumerable.

Là vous avez deux possibilités. Soit vous acceptez le risque que la classe NotifyCollectionChangedEventArgs change et vous pouvez utiliser la reflection sur le champ private :

public class MyNotifyCollectionChangedEventArgs : NotifyCollectionChangedEventArgs
{
    public MyNotifyCollectionChangedEventArgs(NotifyCollectionChangedAction action)
        : base(action)
    {
    }


    public MyNotifyCollectionChangedEventArgs(NotifyCollectionChangedAction action, IList items)
        : base(action, items)
    {
    }


    public new IList OldItems
    {
        get { return base.OldItems; }
        set
        {
            typeof(NotifyCollectionChangedEventArgs).GetField("_oldItems", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(this, value);
        }
    }

}

Soit vous demandez aux développeurs de faire un Cast s’ils veulent récupérer l’event arg typé et ainsi accéder aux anciens items (avant le Clear).

Soit vous faites les deux tout en ne garantissant que le Cast :

public class MyNotifyCollectionChangedEventArgs : NotifyCollectionChangedEventArgs
{
    public MyNotifyCollectionChangedEventArgs(NotifyCollectionChangedAction action)
        : base(action)
    {
    }


    public MyNotifyCollectionChangedEventArgs(NotifyCollectionChangedAction action, IList items)
        : base(action, items)
    {
        _oldItems = base.OldItems;
    }


    private IList _oldItems;
    public new IList OldItems
    {
        get { return _oldItems; }
        set
        {
            _oldItems = value;
            try
            {
                typeof(NotifyCollectionChangedEventArgs).GetField("_oldItems", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(this, value);
            }
            catch
            {
            }
        }
    }
}

Maintenant que cela est fait, on peut surcharger la méthode ClearItems :

    protected override void ClearItems()
    {
        var removedItems = this.ToList();
        bool isEditing = IsEditing;
        IsEditing = true;
        base.ClearItems();
        IsEditing = isEditing;
        OnCollectionChanged(new MyNotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset) { OldItems = removedItems });
    }

Et c’est tout ! Notre classe MyObservableCollection<T> sera ainsi plus fonctionnelle et plus performante :

public class MyObservableCollection<T> : ObservableCollection<T>, INotifyCollectionChanged
{
    public MyObservableCollection()
    {
    }
    public MyObservableCollection(IEnumerable<T> items)
        :base(items)
    {
   
 
    public void AddRange(IEnumerable<T> items)
    {
        bool isEditing = IsEditing;
        IsEditing = true;
        foreach (T item in items)
            Add(item);
        IsEditing = isEditing;
        OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, items.ToList()));
   
 
    protected override void ClearItems()
    {
        var removedItems = this.ToList();
        bool isEditing = IsEditing;
        IsEditing = true;
        base.ClearItems();
        IsEditing = isEditing;
        OnCollectionChanged(new MyNotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset) { OldItems = removedItems });
   
 
    public bool IsEditing { get; private set; } 
 
    public void BeginEdit()
    {
        if (! IsEditing)
            IsEditing = true;
   
 
    public void EndEdit()
    {
        if (IsEditing)
        {
            IsEditing = false;
            OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
        }
   
 
    protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
    {
        base.OnCollectionChanged(e);
        if (CollectionChanged != null && ! IsEditing)
            CollectionChanged(this, e);
   
 
    public new event NotifyCollectionChangedEventHandler CollectionChanged; 
 
    event NotifyCollectionChangedEventHandler INotifyCollectionChanged.CollectionChanged
    {
        add { CollectionChanged += value; }
        remove { CollectionChanged -= value; }
    }
}

Ce post vous a plu ? Ajoutez le dans vos favoris pour ne pas perdre de temps à le retrouver le jour où vous en aurez besoin :

Publié vendredi 2 avril 2010 00:12 par Matthieu MEZIL

Classé sous : , , ,

Commentaires

# re: ObservableCollection… en mieux sans effort @ vendredi 2 avril 2010 08:01

•Limitée en nombre de méthodes : pas de AddRange par exemple

-&gt; Facilement résolu avec une méthode d'extension

•Catastrophique pour les perfs dès qu’on veut faire un Refresh. =&gt; Clear + n Add =&gt; n+1 events de rafraîchissement envoyés à l’UI

-&gt; Chaque ObservableCollection est associée à une CollectionViewSource sur laquelle tu as une méthode "DefferChanges" pour agréger les evenements (qu'ils ne remontent pas jusqu'a l'UI) puis faire un update massif (avec le moins d'evenements differents possible)

•L’évènement déclenché par le Clear “oublie” de nous donner la liste des éléments supprimés alors que la définition de  l’évènement le prévoie.

-&gt; D'apres la doc de NotifyCollectionChangedEventArgs, OldItems n'est renseigné que sur Remove et Replace. Le Clear est notifié par une Action "Reset" qui dit simplement "Vide tout".

simon ferquel

# re: ObservableCollection… en mieux sans effort @ vendredi 2 avril 2010 09:31

"Chaque ObservableCollection est associée à une CollectionViewSource sur laquelle tu as une méthode "DefferChanges" pour agréger les evenements (qu'ils ne remontent pas jusqu'a l'UI) puis faire un update massif (avec le moins d'evenements differents possible)"

  Merci pour l'info, je ne savais pas. Je creuserai quand j'aurai un peu de temps.

"D'apres la doc de NotifyCollectionChangedEventArgs, OldItems n'est renseigné que sur Remove et Replace. Le Clear est notifié par une Action "Reset" qui dit simplement "Vide tout"."

  Certes mais si on sort du contexte UI, dans certains cas, on souhaite travailler avec une "EventList". Si jamais tu veux savoir quels sont les éléments supprimés, tu ne peux pas utiliser l'ObservableCollection car le Clear ne te les donnera pas. Maintenant si tu sais que l'eventArg n'est pas "qu'un simple" NotifyCollectionChangedEventArgs, tu peux le caster en MyNotifyCollectionChangedEventArgs et tu peux ainsi accéder aux éléments supprimés (y compris par un Clear).

Matthieu MEZIL

# re: ObservableCollection… en mieux sans effort @ vendredi 2 avril 2010 10:14

Dangereux tous ces "new" dans le code. Du coup, on ne peut plus utiliser la classe de base normalement. ObservableCollection n'a manifestement pas été prévue pour être vraiment surchargée, même si elle n'est pas sealed.

Si j'étais toi, je referrais la classe, c'est plus simple et plus propre (c'est ce qu'on a fait pour CodeFluent).

D'autre part, si tu veux être complet, il peut être intéressant d'implémenter aussi IRaiseItemChangedEvents, ICancelAddNew et IBindingList qui te permettent de t'interfacer aussi avec d'autres composants (winforms, tierces parties genre DevExpress, etc...)

smo

# re: ObservableCollection… en mieux sans effort @ vendredi 2 avril 2010 10:41

@ Simon :

"Si j'étais toi, je referrais la classe, c'est plus simple et plus propre"

  Entièrement d'accord avec toi. // C'est d'ailleurs ce que j'avais fait pour un client par le passé avec en plus l'implémentation de IBindingList que tu évoques plus bas.

"ObservableCollection n'a manifestement pas été prévue pour être vraiment surchargée, même si elle n'est pas sealed"

  Là par contre je ne suis que moyennement d'accord avec toi. Je trouve que cette classe est juste mal codée (au risque de paraitre prétentieux). En effet, comme tu le dis elle n'a pas vraiment été pensée surcharge mais, au delà du fait qu'elle n'est pas sealed, elle a tout de même beaucoup de méthodes virtual.

En fait, le but initial de ce post était de montrer comment en redéfinissant l'interface explicitement, on pouvait faire en sorte que l'UI s'abonne à notre event et non à celui de la classe du framework. Et puis j'ai dérivé...

Matthieu MEZIL

# re: ObservableCollection… en mieux sans effort @ vendredi 21 mai 2010 19:58

J'avais écrit une classe comme ça pour la lib Dvp.NET il y a quelques temps... J'ai hérité directement de ObservableCollection, et il n'a pas été nécessaire de masquer les méthodes de la classe de base par des "new". En fait, j'ai juste ajouté des méthodes AddRange/RemoveRange etc, qui déclenchent une seule notification pour tout le range (et pas de BeginEdit/EndEdit)

Le code est dispo ici :

http://projets.developpez.com/projects/dvp-net/repository/entry/trunk/src/Developpez.Dotnet.Windows/Collections/RangeObservableCollection.cs

Ca marche nickel quand on l'utilise sans lien avec l'IHM, par contre j'ai eu une mauvaise surprise quand j'ai voulu tester dans une vraie appli WPF : les notifications pour plusieurs items NE SONT PAS GEREES par WPF :(

https://connect.microsoft.com/WPF/feedback/details/514922/range-actions-not-supported-in-collectionview

tomlev

# re: ObservableCollection… en mieux sans effort @ jeudi 1 mars 2012 10:08

Bonjour,

je suis entierement d'accord avec toutes les critiques concernant l'ObservableCollection, par contre je suis un peu septique sur la solution proposé:

le "new" sur l'évenement crée en fait un evenement parallele qui masque celui de la classe de base (il ne s'agit pas d'une surcharge!!) et ne sera pris en compte que sur les instances castées en MyObservableCollection.

Pour tout ce qui n'utilise que INotifyCollectionChanged (ce qui est le cas des contrôles WPF par ex), l'evenement de MyObservableCollection ne sera pas pris en compte, c'est à l'évenement de la classe de base auquel l'utilisateur s'abonne...

Je crois comme smo que ObservableCollection n'est pas prévue pour évoluer de cette façon et il me semble plus propre de réimplémenter un INotifyCollectionChanged.

Pour ce qui est de la solution de la méthode d'extension, elle ne résoud pas pour moi le pb de performance -&gt; sont implementation sera de toute facon un clear() + add() n fois qui notifiera un changemenent à chaque element ajouté.

Charles HETIER

Les commentaires anonymes sont désactivés

Les 10 derniers blogs postés

- Merci par Blog de Jérémy Jeanson le 10-01-2019, 20:47

- Office 365: Script PowerShell pour auditer l’usage des Office Groups de votre tenant par Blog Technique de Romelard Fabrice le 04-26-2019, 11:02

- Office 365: Script PowerShell pour auditer l’usage de Microsoft Teams de votre tenant par Blog Technique de Romelard Fabrice le 04-26-2019, 10:39

- Office 365: Script PowerShell pour auditer l’usage de OneDrive for Business de votre tenant par Blog Technique de Romelard Fabrice le 04-25-2019, 15:13

- Office 365: Script PowerShell pour auditer l’usage de SharePoint Online de votre tenant par Blog Technique de Romelard Fabrice le 02-27-2019, 13:39

- Office 365: Script PowerShell pour auditer l’usage d’Exchange Online de votre tenant par Blog Technique de Romelard Fabrice le 02-25-2019, 15:07

- Office 365: Script PowerShell pour auditer le contenu de son Office 365 Stream Portal par Blog Technique de Romelard Fabrice le 02-21-2019, 17:56

- Office 365: Script PowerShell pour auditer le contenu de son Office 365 Video Portal par Blog Technique de Romelard Fabrice le 02-18-2019, 18:56

- Office 365: Script PowerShell pour extraire les Audit Log basés sur des filtres fournis par Blog Technique de Romelard Fabrice le 01-28-2019, 16:13

- SharePoint Online: Script PowerShell pour désactiver l’Option IRM des sites SPO non autorisés par Blog Technique de Romelard Fabrice le 12-14-2018, 13:01