Propriétés, accesseurs, et évènements d'une classe (1/3) - Evénements et sérialisation

Introduction

La gestion des évènements d'une classe peut-être très simple, mais peut générer des problèmes lors de la sérialisation d'objets.
J'ai mis au point une solution pour résoudre un certain nombre de ces problèmes de la manière la plus transparente possible.

Tutoriel

Prenons l'exemple d'un logiciel de gestion bancaire. Nous allons nous intéresser à deux classes :

  • La classe Compte, qui contient entre autres une liste d'opération bancaires, et une solde (solde = somme des opérations bancaires),
  • La classe Operation, qui a pour propriétés un montant, une date, un libellé, ...

Nous déclarons un évènement sur :

  • Le montant de la classe Operation,
  • Le solde de la classe Compte.

La classe Compte peut s'abonner aux évènements "montant changé" des opérations qu'elle contient. Le solde du compte peut ainsi être mise à jour en temps voulu.

Pour rappel, les évènements sont pris en compte dans la sérialisation, ainsi, lors d'une désérialisation, le comportement des classes entres elles restera inchangé.

L'interface graphique, elle, se connecte aussi à l'évènement sur le montant de l'opération (pour mettre à jour un libellé par exemple).

Seulement si elle fait ça, elle rend notre classe impossible à sérialiser !

Le moteur de sérialisation regarde l'évènement auquel s'est abonné la Form, et tente de l'embarquer le dump de sérialisation. Or, les Forms ne sont pas marquées sérialisables. D'autre part, il est inutile d'inclure dans notre fichier sérialisé des informations sur un formulaire, qui au final, n'a strictement rien à voir avec nos données métier.

La technique pour résoudre ce problème :

A chaque fois que l'on veut déclarer un évènement, on en créé 2. Par exemple, pour la classe Operation, on va créer deux évènements qui seront levés lors du changement de montant :

internal event EventHandler MontantChangedInternal;

[field: NonSerialized * 

public event EventHandler MontantChanged;

L'idée : Les objets de notre assembly qui concernent nos données (classes comptes, opérations, relevés bancaires, ...) s'abonneront à notre évènement marqué internal. Si nous séparons nos objets métiers dans une assembly séparée (ce qui semble logique), les form et autres composants ergonomiques n'auront pas d'autre choix que de s'abonner à SoldeChange. Celui-ci ayant l'attribut "NonSerialized", ils seront exclus lors de la sérialisation de nos objets métiers. Ce système résous notre problème de sérialisation impossible.

L'inconvénient de cette méthode, c'est que par mégarde, nous pouvons utiliser dans nos objets métiers l'évènement SoldedChange (et non SoldeChangedInternal). Si la classe Compte s'abonne à cet évènement, l'abonnement ne sera pas enregistré lors de la sérialisation, et sera définitivement perdu lors de la désérialisation des données. Le comportement des classes les unes vis à vis des autres sera alors corrompu.

Pour résoudre ce nouveau problème, voici une méthode :

Déclarer les deux évènements en privé :

private event EventHandler MontantChangedInternal;

[field: NonSerialized * 

private event EventHandler MontantChangedExternal;

... Puis déclarer un accesseur d'évènement avec Add et Remove :

public event EventHandler OnMontantChanged
{
    add
    {
        if(value.Target.GetType().Assembly == this.GetType().Assembly && value.Target.GetType().IsSerializable)
            MontantChangedInternal += value;
        else
            MontantChangedExternal += value;
    }

    remove
    {
        MontantChangedInternal -= value;
        MontantChangedExternal -= value;
    }
}

Explications :
Lorsqu'un objet s'abonne à l'évènement, l'accesseur choisit lui-même si cet objet sera persistant à la sérialisation ou non. Pour cela :

on regarde l'assembly de l'appelant, et on le compare avec celui- de l'objet en cours,
on regarde si l'objet est marqué sérialisable ou non.

Dans le cas ou l'objet qui s'abonne remplit toutes les conditions nécessaires, il est abonné à l'évènement sauvegardé à la sérialisation. Dans le cas inverse, c'est l'évènement non sérialisable qui est utilisé.

De l'extérieur de la classe, nous n'avons donc qu'un seul évènement auquel nous abonner, que nous soyons à l'intérieur de l'assembly ou non. C'est plus ergonomique et moins sujet aux erreurs de développement.

Dernier point, l'accesseur sur le solde ressemble à ceci :

private Single m_montant;
public Single Montant
{
    get { return m_montant; }
    set {
        if(m_montant == value) return;
        m_montant = value;
        if (MontantChangedInternal != null)
            MontantChangedInternal(this, EventArgs.Empty);

        if (MontantChangedExternal != null)
            MontantChangedExternal(this, EventArgs.Empty);
    }
}

Il est à noter que les objets internes sont privilégiés par rapport aux autres puisque même s'ils s'abonnent après une form à l'évènement, ils sont notifiés avant celle-ci... ce qui ne me choque pas plus que ça (les objets métier avant tout).

Vous noterez au passage le "if(m_solde == value) return;" qui permet de ne pas lancer d'évènements si on essaye d'affecter 3 à un montant qui vaut déjà 3.

Voici maintenant le code complet de l'accesseur et des évènements, avec au passage la création de la méthode permettant de lancer les évènements, et une méthode delegate pour spécifier le nouveau montant :

private Single m_montant;

private event MontantChanged MontantChangedInternal;

[field: NonSerialized]
private event MontantChanged MontantChangedExternal;

public delegate void MontantChanged(object sender, MontantChangedEventArgs args);

public event EventHandler OnMontantChanged
{
    add {
        if(value.Target.GetType().Assembly == this.GetType().Assembly && value.Target.GetType().IsSerializable)
            MontantChangedInternal += value;
        else
            MontantChangedExternal += value;
    }
    remove {
        MontantChangedInternal -= value;
        MontantChangedExternal -= value;
    }
}

public Single Montant
{
    get { return m_montant; }
    protected set {
        if(m_montant == value) return;
        Single oldMontant = m_montant;
        m_montant = value;
        LaunchMontantChangedEvent(m_montant, oldMontant);
    }
}

private void LaunchMontantChangedEvent(Single newvalue, Single oldvalue)
{
    MontantChangedEventArgs args = new MontantChangedEventArgs(newvalue, oldvalue);
    if (MontantChangedInternal != null)
        MontantChangedInternal(this, EventArgs.Empty);

    if (MontantChangedExternal != null)
        MontantChangedExternal(this, EventArgs.Empty);
}

... La classe MontantChangedEventArgs :

public class MontantChangedEventArgs : EventArgs
{
    public Single newval;
    public Single oldval;
    public MontantChangedEventArgs(Single newvalue, Single oldvalue)
    {
        newval = newvalue;
        oldval = oldvalue;
    }
}

Suite

Pour la suite : Gérer les annulations le blocage de propriétés sur un objet

Ce document intitulé « Propriétés, accesseurs, et évènements d'une classe (1/3) - Evénements et sérialisation » issu de CodeS SourceS (codes-sources.commentcamarche.net) est mis à disposition sous les termes de la licence Creative Commons. Vous pouvez copier, modifier des copies de cette page, dans les conditions fixées par la licence, tant que cette note apparaît clairement.