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
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 :
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; } }
Pour la suite : Gérer l'historique des modifications pour gérer les commandes annuler/refaire d'un logiciel
Ca se passe ici