web-dev-qa-db-fra.com

Signature d'événement dans .NET - Utilisation d'un «expéditeur» typé fort?

Je me rends pleinement compte que ce que je propose ne suit pas les directives .NET et, par conséquent, est probablement une mauvaise idée pour cette seule raison. Cependant, je voudrais considérer cela sous deux angles possibles:

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

(2) Est-ce un concept que les concepteurs de cadres pourraient envisager de modifier ou de mettre à jour?

Je pense à utiliser une signature d'événement qui utilise un "expéditeur" typé fort, au lieu de le taper comme "objet", qui est le modèle de conception .NET actuel. 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 très différent d'une Action <TSender, TEventArgs>, mais en utilisant le StrongTypedEventHandler, nous imposons que le TEventArgs dérive de System.EventArgs.

Ensuite, à titre d'exemple, nous pouvons utiliser 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énements de type fort qui ne nécessite pas de transtypage:

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

Je me rends bien compte que cela rompt avec le modèle standard de gestion des événements .NET; cependant, gardez à l'esprit que la contravariance permettrait à un abonné d'utiliser une signature de gestion d'événement traditionnelle si elle le souhaitait:

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

C'est-à-dire que si un gestionnaire d'événements devait s'abonner à des événements de types d'objets disparates (ou peut-être inconnus), le gestionnaire pourrait taper le paramètre `` sender '' comme `` objet '' afin de gérer toute l'étendue des objets émetteurs potentiels.

À part briser la convention (ce que je ne prends pas à la légère, croyez-moi), je ne vois aucun inconvénient à cela.

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

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

Quelqu'un d'autre peut-il penser à un problème avec cela? Ou est-ce que cela rompt tellement avec la convention qu'il fait tourner l'estomac des gens?

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

(1) Consignes de conception d'événements [MSDN 3.5]

(2) C # simple Event Raising - using "sender" vs. custom EventArgs [StackOverflow 2009]

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

Je suis intéressé par l'opinion de tout le monde à ce sujet ...

Merci d'avance,

Mike

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

Voici un exemple de travail complet qui montre que les gestionnaires d'événements de type fort et les gestionnaires d'événements standard actuels qui utilisent un paramètre "expéditeur d'objet" peuvent coexister avec cette approche. Vous pouvez copier-coller dans le code et l'exécuter:

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 elle s'applique ici. Les délégués en langage C # ont eu la covariance et la contravariance depuis si longtemps que cela semble juste "intrinsèque", mais ce n'est pas le cas. Cela pourrait même être quelque chose qui est activé dans le CLR, je ne sais pas, mais Visual Basic .NET n'a pas obtenu de capacité de covariance et de contravariance pour ses délégués avant le .NET Framework 3.0 (VB.NET 2008). Et par conséquent, Visual Basic.NET pour .NET 2.0 et les versions antérieures ne pourraient pas 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 à 100%. Mais je l'ai maintenant testé sur VB.NET 2005, juste pour être sûr, et il ne compile pas, déclarant:

La méthode 'Public Sub SomeEventHandler (expéditeur As Object, e As vbGenericEventHandling.GenericEventHandling.PublisherEventArgs)' n'a pas la même signature que le délégué 'Delegate Sub StrongTypedEventHandler (Of TSender, TEventArgs As System.EventArgs) (sender As Publisher, e As PublisherEvent) "

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

Edit: Update # 3

Ok, j'utilise cela 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;

Autre que ce changement de nom, je l'ai implémenté exactement comme indiqué ci-dessus.

Il déclenche la règle FxCop CA1009, qui stipule:

"Par convention, les événements .NET ont deux paramètres qui spécifient l'expéditeur de l'événement et les données d'événement. Les signatures du gestionnaire d'événements doivent se présenter comme suit: void MyEventHandler (object sender, EventArgs e). Le paramètre 'sender' est toujours de type System.Object, même s'il est possible d'employer un 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 délégué System.EventHandler. Les gestionnaires d'événements renvoient void pour pouvoir envoyer chaque événement à plusieurs méthodes cible. Toute valeur renvoyée par une cible serait perdue après le premier appel. "

Bien sûr, nous savons tout cela et enfreignons les règles de toute façon. (Tous les gestionnaires d'événements peuvent utiliser "l'expéditeur d'objet" standard dans leur signature s'ils le préfèrent dans tous les cas - il s'agit d'un changement incessant.)

Donc, 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. Cela fonctionne vraiment très bien.

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

Mike

105
Mike Rosenblum

Il semble que Microsoft ait repris cela car un exemple similaire est maintenant sur MSDN:

Délégués génériques

25
Bas

Ce que vous proposez a beaucoup de sens en fait, et je me demande simplement si c'est une de ces choses qui est simplement la même parce qu'elle a été conçue à l'origine avant les génériques, ou s'il y a une vraie raison à cela.

13
BFree

Windows Runtime (WinRT) introduit un TypedEventHandler<TSender, TResult> délégué, qui fait exactement ce que votre StrongTypedEventHandler<TSender, TResult> le fait, mais apparemment sans la contrainte sur le paramètre de type TResult:

public delegate void TypedEventHandler<TSender, TResult>(TSender sender,
                                                         TResult args);

La documentation MSDN est ici .

13
Pierre Arnaud

Je conteste les déclarations suivantes:

  • Je crois que les anciennes versions de Visual Basic .NET à 2005 n'ont pas de covariance et de contravariance de délégué.
  • Je me rends bien compte que cela frôle le blasphème.

Tout d'abord, rien de ce que vous avez fait ici n'a quoi que ce soit à voir avec la covariance ou la contravariance. ( Edit: La déclaration précédente est fausse, pour plus d'informations, veuillez consulter Covariance et Contravariance dans les délégués) Cette solution fonctionnera très bien dans toutes les versions 2.0 et supérieures du CLR (évidemment cela fonctionnera pas fonctionnera dans une application CLR 1.0 car elle utilise des génériques).

Deuxièmement, je suis fortement en désaccord avec le fait que votre idée soit à la limite du "blasphème" car c'est une idée merveilleuse.

5
Andrew Hare

J'ai jeté un coup d'œil à la façon dont cela a été géré avec le nouveau WinRT et basé sur d'autres opinions ici, et j'ai finalement décidé de le faire comme ceci:

[Serializable]
public delegate void TypedEventHandler<in TSender, in TEventArgs>(
    TSender sender,
    TEventArgs e
) where TEventArgs : EventArgs;

Cela semble être la meilleure voie à suivre compte tenu de l'utilisation du nom TypedEventHandler dans WinRT.

5
Inverness

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

2
Otávio Décio

D'après ce que je comprends, le champ "Expéditeur" est toujours censé faire référence à l'objet qui contient l'abonnement à l'événement. Si j'avais mes druthers, il y aurait également un champ contenant des informations suffisantes pour désinscrire un événement si cela devenait nécessaire (*) (considérons, par exemple, un enregistreur de modifications qui s'abonne aux événements `` collection-changed ''; il contient deux parties , dont l'un fait le travail réel et contient les données réelles, et l'autre qui fournit un wrapper d'interface publique, la partie principale peut contenir une référence faible à la partie wrapper. Si la partie wrapper est récupérée, cela signifierait il n'y avait plus personne intéressé par les données qui étaient collectées, et le change-logger devrait donc se désinscrire de tout événement qu'il reçoit).

Puisqu'il est possible qu'un objet envoie des événements au nom d'un autre objet, je peux voir une certaine utilité potentielle pour avoir un champ "expéditeur" qui est de type Object, et pour que le champ dérivé d'EventArgs contienne une référence à l'objet qui devrait être mis en œuvre. L'utilité du champ "expéditeur", cependant, est probablement limitée par le fait qu'il n'existe aucun moyen propre pour un objet de se désabonner d'un expéditeur inconnu.

(*) En fait, une manière plus propre de gérer les désabonnements serait d'avoir un type délégué multicast pour les fonctions qui retournent Boolean; si une fonction appelée par un tel délégué renvoie True, le délégué serait corrigé pour supprimer cet objet. Cela signifierait que les délégués ne seraient plus vraiment immuables, mais il devrait être possible d'effectuer un tel changement de manière thread-safe (par exemple en annulant la référence d'objet et en faisant en sorte que le code délégué de multidiffusion ignore toutes les références d'objet nul incorporées). Dans ce scénario, une tentative de publication et d'événement sur un objet supprimé peut être traitée très proprement, quel que soit l'origine de l'événement.

2
supercat

En regardant le blasphème comme la seule raison de faire de l'expéditeur un type d'objet (si pour omettre les problèmes de contravariance dans VB 2005 code, qui est une erreur IMHO de Microsoft), quelqu'un peut-il suggérer au moins un motif théorique pour clouer le deuxième argument au type EventArgs. Pour aller plus loin, y a-t-il une bonne raison de se conformer aux directives et conventions de Microsoft dans ce cas particulier?

Avoir besoin de développer un autre wrapper EventArgs pour une autre donnée que nous voulons transmettre à l'intérieur du gestionnaire d'événements semble étrange, pourquoi ne peut-il pas directement y passer ces données. 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;
}
2
Lu4

Fonce. Pour le code non basé sur les composants, je simplifie souvent les signatures d'événements pour qu'elles soient simplement

public event Action<MyEventType> EventName

MyEventType n'hérite pas de EventArgs. Pourquoi, si je n'ai jamais l'intention d'utiliser l'un des membres d'EventArgs.

1
Scott Weinstein

Je ne pense pas qu'il y ait quelque chose de mal avec ce que vous voulez faire. Pour la plupart, je soupçonne que le object sender le paramètre reste afin de continuer à prendre en charge 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 EvenArgs de base. Quelque chose comme ça:

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 celui-ci

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

Et des méthodes comme celle-ci:

private void HandleSomething(object sender, EventArgs e)

pourra toujours souscrire.

MODIFIER

Cette dernière ligne m'a fait réfléchir un peu ... Vous devriez en fait être capable d'implémenter ce que vous proposez sans casser aucune fonctionnalité extérieure car le runtime n'a pas de problème de downcasting. Je pencherais toujours vers la solution DataEventArgs (personnellement). Je le ferais, cependant en sachant qu'il est redondant, car l'expéditeur est stocké dans le premier paramètre et en tant que propriété de l'argument événement.

Un avantage de s'en tenir à DataEventArgs est que vous pouvez chaîner des événements, en changeant l'expéditeur (pour représenter le dernier expéditeur) tandis que les EventArgs conservent l'expéditeur d'origine.

1
Michael Meadows

Avec 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 click ne serait pas de type Button ou Label, mais de type Control (car l'événement est défini sur Control). Ainsi, certains événements de la classe Button auraient une cible de type Contrôle, d'autres auraient d'autres types de cible.

1
Tommy Carlier