107 votes

Signature d'événement dans .NET -- Utilisation d'un "expéditeur" fortement typé ?

Je suis pleinement conscient que ce que je propose ne respecte pas les directives .NET et que, par conséquent, c'est probablement une mauvaise idée pour cette seule raison. Cependant, j'aimerais envisager la question sous deux angles différents :

(1) Devrais-je envisager de l'utiliser pour mon propre travail de développement, qui est à 100% à des fins internes.

(2) S'agit-il d'un concept que les concepteurs du cadre pourraient envisager de modifier ou de mettre à jour ?

Je pense utiliser une signature d'événement qui utilise un "expéditeur" fortement typé, au lieu de le taper comme "objet", ce qui est le modèle de conception actuel de .NET. Autrement dit, au lieu d'utiliser une signature d'événement standard qui ressemble à ceci :

class Publisher
{
    public event EventHandler<PublisherEventArgs> SomeEvent;
}

J'envisage d'utiliser une signature d'événement qui utilise un paramètre "expéditeur" de type fort, comme suit :

Tout d'abord, définissez un "StrongTypedEventHandler" :

[SerializableAttribute]
public delegate void StrongTypedEventHandler<TSender, TEventArgs>(
    TSender sender,
    TEventArgs e
)
where TEventArgs : EventArgs;

Ce n'est pas si différent d'une Action<TSender, TEventArgs>, mais en utilisant la fonction StrongTypedEventHandler nous imposons que les TEventArgs dérivent de System.EventArgs .

Ensuite, à titre d'exemple, nous pouvons utiliser le StrongTypedEventHandler dans une classe de publication comme suit :

class Publisher
{
    public event StrongTypedEventHandler<Publisher, PublisherEventArgs> SomeEvent;

    protected void OnSomeEvent()
    {
        if (SomeEvent != null)
        {
            SomeEvent(this, new PublisherEventArgs(...));
        }
    }
}

L'arrangement ci-dessus permettrait aux abonnés d'utiliser un gestionnaire d'événement de type fort qui ne nécessiterait pas de moulage :

class Subscriber
{
    void SomeEventHandler(Publisher sender, PublisherEventArgs e)
    {           
        if (sender.Name == "John Smith")
        {
            // ...
        }
    }
}

Je suis tout à fait conscient que cela rompt avec le modèle standard de traitement des événements de .NET ; cependant, gardez à l'esprit que la contravariance permettrait à un abonné d'utiliser une signature traditionnelle de traitement des événements s'il le souhaite :

class Subscriber
{
    void SomeEventHandler(object sender, PublisherEventArgs e)
    {           
        if (((Publisher)sender).Name == "John Smith")
        {
            // ...
        }
    }
}

En d'autres termes, si un gestionnaire d'événements doit s'abonner à des événements provenant de types d'objets disparates (ou peut-être inconnus), il peut saisir le paramètre "sender" comme "object" afin de gérer toute la gamme des objets expéditeurs potentiels.

Mis à part le fait de briser les conventions (ce que je ne prends pas à la légère, croyez-moi), je ne vois pas d'inconvénients à cela.

Il peut y avoir des problèmes de conformité avec le CLS. Cela fonctionne dans Visual Basic .NET 2008 100% bien (j'ai testé), mais je crois que les anciennes versions de Visual Basic .NET jusqu'à 2005 n'ont pas la covariance et la contravariance des délégués. [Je l'ai testé depuis, et c'est confirmé : VB.NET 2005 et inférieur ne peut pas gérer cela, mais VB.NET 2008 est 100% correct. Voir "Edit #2", ci-dessous]. Il se peut que d'autres langages .NET aient également un problème avec cela, je ne peux pas en être sûr.

Mais je ne me vois pas développer pour un autre langage que C# ou Visual Basic .NET, et cela ne me dérange pas de le limiter à C# et VB.NET pour .NET Framework 3.0 et plus. (Je ne pourrais pas imaginer revenir à 2.0 à ce stade, pour être honnête).

Quelqu'un d'autre peut-il trouver un problème à ce sujet ? Ou est-ce que cela rompt simplement avec les conventions au point de retourner l'estomac des gens ?

Voici quelques liens connexes que j'ai trouvés :

(1) Directives pour la conception d'événements [MSDN 3.5].

(2) Levée d'événement simple en C# - utilisation de l'expéditeur et des EventArgs personnalisés [StackOverflow 2009].

(3) Modèle de signature d'événement en .net [StackOverflow 2008].

Je suis intéressé par l'opinion de tous et chacun à ce sujet...

Merci d'avance,

Mike

Edit #1 : Ceci est en réponse à Le message de Tommy Carlier :

Voici un exemple fonctionnel complet qui montre que les gestionnaires d'événements à typage fort et les gestionnaires d'événements standard actuels qui utilisent un paramètre " object sender " peuvent coexister avec cette approche. Vous pouvez copier-coller le code et l'essayer :

namespace csScrap.GenericEventHandling
{
    class PublisherEventArgs : EventArgs
    {
        // ...
    }

    [SerializableAttribute]
    public delegate void StrongTypedEventHandler<TSender, TEventArgs>(
        TSender sender,
        TEventArgs e
    )
    where TEventArgs : EventArgs;

    class Publisher
    {
        public event StrongTypedEventHandler<Publisher, PublisherEventArgs> SomeEvent;

        public void OnSomeEvent()
        {
            if (SomeEvent != null)
            {
                SomeEvent(this, new PublisherEventArgs());
            }
        }
    }

    class StrongTypedSubscriber
    {
        public void SomeEventHandler(Publisher sender, PublisherEventArgs e)
        {
            MessageBox.Show("StrongTypedSubscriber.SomeEventHandler called.");
        }
    }

    class TraditionalSubscriber
    {
        public void SomeEventHandler(object sender, PublisherEventArgs e)
        {
            MessageBox.Show("TraditionalSubscriber.SomeEventHandler called.");
        }
    }

    class Tester
    {
        public static void Main()
        {
            Publisher publisher = new Publisher();

            StrongTypedSubscriber strongTypedSubscriber = new StrongTypedSubscriber();
            TraditionalSubscriber traditionalSubscriber = new TraditionalSubscriber();

            publisher.SomeEvent += strongTypedSubscriber.SomeEventHandler;
            publisher.SomeEvent += traditionalSubscriber.SomeEventHandler;

            publisher.OnSomeEvent();
        }
    }
}

Edit #2 : Ceci est en réponse à Déclaration d'Andrew Hare concernant la covariance et la contravariance et comment elles s'appliquent ici. Les délégués du langage C# disposent de la covariance et de la contravariance depuis si longtemps qu'ils semblent "intrinsèques", mais ils ne le sont pas. Il se peut même que ce soit quelque chose qui soit activé dans le CLR, je ne sais pas, mais Visual Basic .NET n'a pas obtenu la capacité de covariance et de contravariance pour ses délégués avant le .NET Framework 3.0 (VB.NET 2008). Par conséquent, Visual Basic.NET pour .NET 2.0 et les versions inférieures ne sont pas en mesure d'utiliser cette approche.

Par exemple, l'exemple ci-dessus peut être traduit en VB.NET comme suit :

Namespace GenericEventHandling
    Class PublisherEventArgs
        Inherits EventArgs
        ' ...
        ' ...
    End Class

    <SerializableAttribute()> _
    Public Delegate Sub StrongTypedEventHandler(Of TSender, TEventArgs As EventArgs) _
        (ByVal sender As TSender, ByVal e As TEventArgs)

    Class Publisher
        Public Event SomeEvent As StrongTypedEventHandler(Of Publisher, PublisherEventArgs)

        Public Sub OnSomeEvent()
            RaiseEvent SomeEvent(Me, New PublisherEventArgs)
        End Sub
    End Class

    Class StrongTypedSubscriber
        Public Sub SomeEventHandler(ByVal sender As Publisher, ByVal e As PublisherEventArgs)
            MessageBox.Show("StrongTypedSubscriber.SomeEventHandler called.")
        End Sub
    End Class

    Class TraditionalSubscriber
        Public Sub SomeEventHandler(ByVal sender As Object, ByVal e As PublisherEventArgs)
            MessageBox.Show("TraditionalSubscriber.SomeEventHandler called.")
        End Sub
    End Class

    Class Tester
        Public Shared Sub Main()
            Dim publisher As Publisher = New Publisher

            Dim strongTypedSubscriber As StrongTypedSubscriber = New StrongTypedSubscriber
            Dim traditionalSubscriber As TraditionalSubscriber = New TraditionalSubscriber

            AddHandler publisher.SomeEvent, AddressOf strongTypedSubscriber.SomeEventHandler
            AddHandler publisher.SomeEvent, AddressOf traditionalSubscriber.SomeEventHandler

            publisher.OnSomeEvent()
        End Sub
    End Class
End Namespace

VB.NET 2008 peut l'exécuter sans problème. Mais je l'ai maintenant testé sur VB.NET 2005, juste pour être sûr, et il ne compile pas, indiquant :

Méthode 'Public Sub SomeEventHandler(sender As Object, e As vbGenericEventHandling.GenericEventHandling.PublisherEventArgs)' n'a pas la même signature que delegate 'Delegate Sub StrongTypedEventHandler(Of TSender, TEventArgs As System.EventArgs)(sender As Publisher, e As PublisherEventArgs)'.

Fondamentalement, les délégués sont invariants dans VB.NET versions 2005 et inférieures. J'ai en fait pensé à cette idée il y a quelques années, mais l'incapacité de VB.NET à gérer ce problème me dérangeait... Mais je suis maintenant passé solidement à C#, et VB.NET peut maintenant le gérer, donc, eh bien, d'où ce post.

Edit : Update #3

Ok, j'utilise ce système avec succès depuis un certain temps maintenant. C'est vraiment un bon système. J'ai décidé de nommer mon "StrongTypedEventHandler" comme "GenericEventHandler", défini comme suit :

[SerializableAttribute]
public delegate void GenericEventHandler<TSender, TEventArgs>(
    TSender sender,
    TEventArgs e
)
where TEventArgs : EventArgs;

À part ce changement de nom, j'ai mis en œuvre le système exactement comme indiqué ci-dessus.

Il passe outre la règle FxCop CA1009, qui stipule :

"Par convention, les événements .NET ont deux qui spécifient l'événement l'expéditeur et les données de l'événement. Les signatures des gestionnaires d'événements doivent suivre cette forme : void MyEventHandler( objet sender, EventArgs e). Le paramètre "sender" (expéditeur) est toujours de type System.Object, même même s'il est possible d'utiliser un type plus type plus spécifique. Le paramètre 'e' est toujours de type System.EventArgs. Les événements qui ne fournissent pas de données d'événement doivent utiliser le type de délégué System.EventHandler de type délégué. Les gestionnaires d'événements renvoient void afin qu'ils puissent envoyer chaque événement à plusieurs méthodes cibles. Toute valeur retournée par une cible serait perdue après le premier appel."

Bien sûr, nous savons tout cela, et nous enfreignons quand même les règles. (Tous les gestionnaires d'événements peuvent utiliser le standard 'object Sender' dans leur signature s'ils le préfèrent dans tous les cas -- il s'agit d'un changement non cassant).

Ainsi, l'utilisation d'un SuppressMessageAttribute fait l'affaire :

[SuppressMessage("Microsoft.Design", "CA1009:DeclareEventHandlersCorrectly",
    Justification = "Using strong-typed GenericEventHandler<TSender, TEventArgs> event handler pattern.")]

J'espère que cette approche deviendra la norme à un moment donné dans le futur. Elle fonctionne vraiment très bien.

Merci pour toutes vos opinions les gars, j'apprécie vraiment...

Mike

2voto

Otávio Décio Points 44200

Je pense que c'est une excellente idée et que MS n'a peut-être tout simplement pas le temps ou l'intérêt d'investir pour l'améliorer, comme par exemple lorsqu'ils sont passés d'ArrayList à des listes génériques.

2voto

supercat Points 25534

D'après ce que j'ai compris, le champ "Sender" est toujours censé faire référence à l'objet qui détient l'abonnement à l'événement. Si j'avais le choix, il y aurait également un champ contenant des informations suffisantes pour désabonner un événement si cela s'avérait nécessaire(*) (considérez, par exemple, un enregistreur de changements qui s'abonne aux événements "collection-changed" ; il contient deux parties, l'une qui fait le travail réel et contient les données réelles, et l'autre qui fournit une enveloppe d'interface publique, la partie principale pourrait contenir une référence faible à la partie enveloppe. Si la partie wrapper est mise à la poubelle, cela signifierait que plus personne n'est intéressé par les données collectées, et le change-logger devrait donc se désabonner de tout événement qu'il reçoit).

Puisqu'il est possible qu'un objet puisse envoyer des événements pour le compte d'un autre objet, je vois l'utilité potentielle d'avoir un champ "expéditeur" de type Object, et que le champ dérivé de l'EventArgs contienne une référence à l'objet sur lequel il faut agir. L'utilité du champ "sender", cependant, est probablement limitée par le fait qu'il n'y a pas de moyen propre pour un objet de se désabonner d'un expéditeur inconnu.

(*) En fait, une façon plus propre de gérer les désabonnements serait d'avoir un type de délégué multicast pour les fonctions qui renvoient des booléens ; si une fonction appelée par un tel délégué renvoie True, le délégué serait modifié pour supprimer cet objet. Cela signifierait que les délégués ne seraient plus vraiment immuables, mais il devrait être possible d'effectuer ce changement d'une manière sûre pour les fils d'exécution (par exemple en annulant la référence de l'objet et en faisant en sorte que le code du délégué multidiffusion ignore toute référence d'objet nulle intégrée). Dans ce scénario, une tentative de publication d'un événement vers un objet éliminé pourrait être traitée très proprement, quelle que soit l'origine de l'événement.

2voto

Lu4 Points 2774

Si l'on considère que le blasphème est la seule raison pour laquelle l'expéditeur est un type d'objet (si l'on veut éviter les problèmes de contravariance dans le code VB 2005, ce qui est une erreur de Microsoft, à mon avis), quelqu'un peut-il suggérer une raison au moins théorique pour attribuer le deuxième argument au type EventArgs. En allant encore plus loin, y a-t-il une bonne raison de se conformer aux directives et conventions de Microsoft dans ce cas particulier ?

Le fait d'avoir à développer un autre wrapper EventArgs pour une autre donnée que nous voulons passer dans le gestionnaire d'événement semble étrange, pourquoi ne pas passer directement cette donnée là. Considérez les sections de code suivantes

[Exemple 1]

public delegate void ConnectionEventHandler(Server sender, Connection connection);

public partial class Server
{
    protected virtual void OnClientConnected(Connection connection)
    {
        if (ClientConnected != null) ClientConnected(this, connection);
    }

    public event ConnectionEventHandler ClientConnected;
}

[Exemple 2]

public delegate void ConnectionEventHandler(object sender, ConnectionEventArgs e);

public class ConnectionEventArgs : EventArgs
{
    public Connection Connection { get; private set; }

    public ConnectionEventArgs(Connection connection)
    {
        this.Connection = connection;
    }
}

public partial class Server
{
    protected virtual void OnClientConnected(Connection connection)
    {
        if (ClientConnected != null) ClientConnected(this, new ConnectionEventArgs(connection));
    }

    public event ConnectionEventHandler ClientConnected;
}

1voto

Michael Meadows Points 15277

Je ne pense pas qu'il y ait un problème avec ce que vous voulez faire. Pour la plupart, je soupçonne que le object sender reste afin de continuer à supporter le code pré 2.0.

Si vous souhaitez vraiment effectuer cette modification pour une API publique, vous pouvez envisager de créer votre propre classe de base EvenArgs. Quelque chose comme ceci :

public class DataEventArgs<TSender, TData> : EventArgs
{
    private readonly TSender sender, TData data;

    public DataEventArgs(TSender sender, TData data)
    {
        this.sender = sender;
        this.data = data;
    }

    public TSender Sender { get { return sender; } }
    public TData Data { get { return data; } }
}

Ensuite, vous pouvez déclarer vos événements comme ceci

public event EventHandler<DataEventArgs<MyClass, int>> SomeIndexSelected;

Et des méthodes comme celle-ci :

private void HandleSomething(object sender, EventArgs e)

pourront toujours s'abonner.

EDIT

Cette dernière ligne m'a fait réfléchir un peu... Vous devriez être en mesure d'implémenter ce que vous proposez sans briser aucune fonctionnalité extérieure puisque le runtime n'a aucun problème à downcasting les paramètres. Je pencherais quand même pour la méthode DataEventArgs solution (personnellement). Je le ferais, sachant toutefois que c'est redondant, puisque l'expéditeur est stocké dans le premier paramètre et comme une propriété des args de l'événement.

L'un des avantages de s'en tenir à la DataEventArgs est que vous pouvez enchaîner les événements, en changeant l'expéditeur (pour représenter le dernier expéditeur) tandis que l'EventArgs conserve l'expéditeur original.

1voto

Tommy Carlier Points 3954

Dans la situation actuelle (l'expéditeur est un objet), vous pouvez facilement attacher une méthode à plusieurs événements :

button.Click += ClickHandler;
label.Click += ClickHandler;

void ClickHandler(object sender, EventArgs e) { ... }

Si l'expéditeur était générique, la cible de l'événement de clic ne serait pas de type Bouton ou Étiquette, mais de type Contrôle (car l'événement est défini sur le Contrôle). Ainsi, certains événements de la classe Button auraient une cible de type Control, d'autres auraient d'autres types de cibles.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X