Propriétés, accesseurs, et évènements d''une classe (2/3) - Evènements avant un changement

Introduction

Ce tutoriel expose une solution pour envoyer des évènements BeforeChange lors de la modification d'une propriété dans une classe. Par exemple, on va envoyer un évènement alors que l'on s'apprête à changer le montant d'une opération bancaire. Un éventuel objet qui s'est connecté à cet évènement pourrait vouloir stopper cette modification avant qu'elle n'arrive, si jamais le nouveau montant ne répond pas à certaines règles métier. Essayons de voir une méthode pour gérer tout cela.

Ce tutoriel fait suite à un précédent opus, que vous pouvez visualiser ici

Tutoriel

Pour rappel, notre cas d'étude est un logiciel de gestion de comptes personnels. Nous nous intéressons principalement à 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é, ...

Voici le code de la propriété "Montant" de notre objet "Operation" :

private Single m_Montant
public Single Montant
{
    get { return m_Montant; }
    set {
        if(m_Montant == value) return;
        m_Montant = value;
    }
}

Dans nos règles métier, admettons que nous ayons à implémenter la possibilité de "bloquer les opérations d'un compte". Ceci signifie que la modification du montant d'une des opérations d'un compte donné n'est plus permise. Pour cela, plusieurs possibilités :

  • Intégrer une propriété "Locked" dans notre objet Operation. Dans le set de notre montant sur l'opération, on vérifiera si celui-ci n'est pas "locked" avant de mettre à jour notre variable privée "m_montant". C'est très bien, mais l'inconvénient, c'est que tous les autres objets peuvent modifier ce Locked pour débloquer l'objet Operation à n'importe quel moment. Ceci peut mettre en péril notre règle métier.
  • Dans l'accesseur sur le montant dans notre objet Operation, on peut regarder si le compte au dessus n'est pas bloqué. Si c'est le cas, on n'autorise pas la modification... C'est bien, mais ce qui me dérange, c'est que dans ce cas, l'objet Operation doit connaitre l'objet Compte...
  • Intégrer un évènement BeforeMontantChange qui sera lancé avant la modification de la valeur interne m_montant. Cet évènement permettra aux objets abonnés de spécifier s'ils veulent ou non annuler la modification du montant avant qu'elle ait lieu. C'est la solution que nous allons décrire dans ce tutoriel.

Tout d'abord, il est judicieux de déclarer un delegate et un évènement personnalisé qui vont nous permettre de spécifier la valeur actuelle du montant, ainsi que la future valeur de notre montant. En effet, si nous ne faisons pas ça, il sera impossible pour les classes s'abonnant à l'évènement de connaitre la nouvelle valeur du solde, puisque celle-ci na pas encore été mise à jour dans l'objet.

Voici la déclaration du delegate dans la classe Operation, et la classe MontantChangingEventArgs (qui hérite de EventArgs) :

public delegate void MontantChangingDelegate(object sender, MontantChangingEventArgs args);

public delegate void MontantChangingDelegate(object sender, MontantChangingEventArgs args);

public class MontantChangingEventArgs : EventArgs
{
    public Single newval;
    public Single oldval;
    public bool cancel = false;
    public MontantChangingEventArgs(Single newvalue, Single oldvalue)
    {
        newval = newvalue;
        oldval = oldvalue;
    }
}

Vous remarquerez la propriété booléenne "cancel". Celle-ci permettra aux objets qui s'abonnent à l'évènement MontantChanging d'arrêter la mise à jour de la valeur. Nous verrons ça un peu plus loin.

Dans notre classe Operation, nous déclarons maintenant un nouvel évènement :

private event MontantChangingDelegate MontantChanging;

Le code de notre accesseur sera maintenant le suivant :

private event MontantChangingDelegate MontantChanging;

public Single Montant
{
    get { return m_Montant; }
    set {
        if(m_Montant == value) return;
        if(LaunchValueChangingEvent(value, m_value))
        {
            m_Montant = value;
        }
    }
}


Et voici maintenant le code de la méthode LaunchValueChangingEvent, c'est ici que se trouve l'astuce :

private bool LaunchMontantChangingEvent(Single newvalue, Single oldvalue)
{
    MontantChangingEventArgs args = new MontantChangingEventArgs(newvalue, oldvalue);
    if (MontantChanging != null)
    {
        for (int j = 0; j < MontantChanging.GetInvocationList().Length; j++)
        {
            MontantChanging.GetInvocationList()[j].DynamicInvoke(new object[] { this, args });
            if (args.cancel) return false;
        }
    }

    return true;
}

... Dans cette méthode, on parcourt tous les objets abonnés à l'évènement, et on les invoque dynamiquement. Au premier qui dit "STOP ! On annule la modification de la valeur !", on retourne false dans notre méthode. Ainsi, si 100 objets s'abonnent à l'évènement et que le premier fait un cancel, les 99 autres ne seront pas appelés, ce qui est bien, puisqu'on n'en a pas l'utilité.

Comme en cas de stop la méthode retourne false, et bien dans l'accesseur ne met pas à jour la valeur m_montant...

Mission accomplie : nous avons la possibilité depuis notre compte, de stopper les modifications sur les opérations qu'il contient. Si on veut, on peut même s'amuser à ne pas autoriser qu'un montant d'opération soit mis à jour avec une valeur farfelue (une somme au dessus de 99 999 999€ par exemple...).

Dernier point, comme il s'agit du deuxième tutoriel, nous allons mettre à jour notre code pour tenir compte des préconisations détaillées dans le tutoriel n°1 :

Le code dans la classe Operation :

private Single m_montant;

private event MontantChanged MontantChangedInternal;
private event MontantChanging MontantChangingInternal;

[field: NonSerialized]
private event MontantChanged MontantChangedExternal;
[field: NonSerialized]
private event MontantChanging MontantChangingExternal;

public delegate void MontantChanged(object sender, MontantChangedEventArgs args);
public delegate void MontantChanging(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 event EventHandler OnMontantChanging
{
    add {
    if(value.Target.GetType().Assembly == this.GetType().Assembly && value.Target.GetType().IsSerializable)
        MontantChangingInternal += value;
    else
        MontantChangingExternal += value;
    }
    remove {
        MontantChangingInternal -= value;
        MontantChangingExternal -= value;
    }
}

public Single Montant
{
    get { return m_montant; }
    protected set {
        if(m_montant == value) return;
        if(LaunchValueChangingEvent(value, m_value))
        {
            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 (MontantChangeInternal != null)
        MontantChangeInternal(this, EventArgs.Empty);

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

private bool LaunchMontantChangingEvent(Single newvalue, Single oldvalue)
{
    MontantChangingEventArgs args = new MontantChangingEventArgs(newvalue, oldvalue);
    if (MontantChanging != null)
    {
        for (int j = 0; j < MontantChanging.GetInvocationList().Length; j++)
        {
            MontantChanging.GetInvocationList()[j].DynamicInvoke(new object[] { this, args });
            if (args.cancel) return false;
        }
    }

    return true;
}

... Les classes évènements :

public class MontantChangingEventArgs : Value_ChangeEventArgs<Single>
{
    public bool cancel = false;
    public Value_ChangingEventArgs(Single newvalue, Single oldvalue) : base(newvalue, oldvalue) { }
}

public class MontantChangedEventArgs : Value_ChangeEventArgs<Single>
{
    public Value_ChangedEventArgs(Single newvalue, Single oldvalue) : base(newvalue, oldvalue) { }
}

public class Value_ChangeEventArgs<T> : EventArgs
{
    public T newval;
    public T oldval;
    public Value_ChangeEventArgs(T newvalue, T oldvalue)
    {
        newval = newvalue;
        oldval = oldvalue;
    }
}

Suite

Pour la suite : Gérer l'historique des modifications pour gérer les commandes annuler/refaire d'un logiciel
Ca se passe ici

A voir également
Ce document intitulé « Propriétés, accesseurs, et évènements d''une classe (2/3) - Evènements avant un changement » 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.
Rejoignez-nous