Event Signature in .NET -- Using a Strong Typed 'Sender'? [closed]
Asked Answered
C

11

108

I fully realize that what I am proposing does not follow the .NET guidelines, and, therefore, is probably a poor idea for this reason alone. However, I would like to consider this from two possible perspectives:

(1) Should I consider using this for my own development work, which is 100% for internal purposes.

(2) Is this a concept that the framework designers could consider changing or updating?

I am thinking about using an event signature that utilizes a strong typed 'sender', instead of typing it as 'object', which is the current .NET design pattern. That is, instead of using a standard event signature that looks like this:

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

I am considering using an event signature that utilizes a strong-typed 'sender' parameter, as follows:

First, define a "StrongTypedEventHandler":

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

This is not all that different from an Action<TSender, TEventArgs>, but by making use of the StrongTypedEventHandler, we enforce that the TEventArgs derives from System.EventArgs.

Next, as an example, we can make use of the StrongTypedEventHandler in a publishing class as follows:

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

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

The above arrangement would enable subscribers to utilize a strong-typed event handler that did not require casting:

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

I do fully realize that this breaks with the standard .NET event-handling pattern; however, keep in mind that contravariance would enable a subscriber to use a traditional event handling signature if desired:

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

That is, if an event handler needed to subscribe to events from disparate (or perhaps unknown) object types, the handler could type the 'sender' parameter as 'object' in order to handle the full breadth of potential sender objects.

Other than breaking convention (which is something that I do not take lightly, believe me) I cannot think of any downsides to this.

There may be some CLS compliance issues here. This does run in Visual Basic .NET 2008 100% fine (I've tested), but I believe that the older versions of Visual Basic .NET through 2005 do not have delegate covariance and contravariance. [Edit: I have since tested this, and it is confirmed: VB.NET 2005 and below cannot handle this, but VB.NET 2008 is 100% fine. See "Edit #2", below.] There may be other .NET languages that also have a problem with this, I can't be sure.

But I do not see myself developing for any language other than C# or Visual Basic .NET, and I do not mind restricting it to C# and VB.NET for .NET Framework 3.0 and above. (I could not imagine going back to 2.0 at this point, to be honest.)

Can anyone else think of a problem with this? Or does this simply break with convention so much that it makes people's stomachs turn?

Here are some related links that I've found:

(1) Event Design Guidelines [MSDN 3.5]

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

(3) Event signature pattern in .net [StackOverflow 2008]

I am interested in anyone's and everyone's opinion on this...

Thanks in advance,

Mike

Edit #1: This is in response to Tommy Carlier's post :

Here's a full working example that shows that both strong-typed event handlers and the current standard event handlers that use a 'object sender' parameter can co-exist with this approach. You can copy-paste in the code and give it a run:

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: This is in response to Andrew Hare's statement regarding covariance and contravariance and how it applies here. Delegates in the C# language have had covariance and contravariance for so long that it just feels "intrinsic", but it's not. It might even be something that is enabled in the CLR, I don't know, but Visual Basic .NET did not get covariance and contravariance capability for its delegates until the .NET Framework 3.0 (VB.NET 2008). And as a result, Visual Basic.NET for .NET 2.0 and below would not be able to utilize this approach.

For example, the above example can be translated into VB.NET as follows:

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 can run it 100% fine. But I've now tested it on VB.NET 2005, just to be sure, and it does not compile, stating:

Method 'Public Sub SomeEventHandler(sender As Object, e As vbGenericEventHandling.GenericEventHandling.PublisherEventArgs)' does not have the same signature as delegate 'Delegate Sub StrongTypedEventHandler(Of TSender, TEventArgs As System.EventArgs)(sender As Publisher, e As PublisherEventArgs)'

Basically, delegates are invariant in VB.NET versions 2005 and below. I actually thought of this idea a couple of years ago, but VB.NET's inability to deal with this bothered me... But I've now moved solidly to C#, and VB.NET can now handle it, so, well, hence this post.

Edit: Update #3

Ok, I have been using this quite successfully for a while now. It really is a nice system. I decided to name my "StrongTypedEventHandler" as "GenericEventHandler", defined as follows:

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

Other than this renaming, I implemented it exactly as discussed above.

It does trip over FxCop rule CA1009, which states:

"By convention, .NET events have two parameters that specify the event sender and event data. Event handler signatures should follow this form: void MyEventHandler( object sender, EventArgs e). The 'sender' parameter is always of type System.Object, even if it is possible to employ a more specific type. The 'e' parameter is always of type System.EventArgs. Events that do not provide event data should use the System.EventHandler delegate type. Event handlers return void so that they can send each event to multiple target methods. Any value returned by a target would be lost after the first call."

Of course, we know all this, and are breaking the rules anyway. (All event handlers can use the standard 'object Sender' in their signature if preferred in any case -- this is a non-breaking change.)

So the use of a SuppressMessageAttribute does the trick:

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

I hope that this approach becomes the standard at some point in the future. It really works very nicely.

Thanks for all your opinions guys, I really appreciate it...

Mike

Cobb answered 25/6, 2009 at 20:21 Comment(7)
Do it. (Don't think this justifies an answer.)Ladylove
My arguments were not really pointed at you: of course you should do this in your own projects. They were arguments why it might not work in the BCL.Filia
Gotcha, thanks Tommy. (But I think it would work broadly, at least with any .NET language that had delegate contravariance.)Cobb
Man, I wish my project had done this from the start, I hate casting the sender.Feliciafeliciano
Now THIS is a question. See, folks? Not one of these tweet-sized oh hi this my hom work solve it plz :code dump: questions, but a question we learn from.Janeljanela
Another suggestion, just name it EventHandler<,> than GenericEventHandler<,>. There is already generic EventHandler<> in BCL which is named just EventHandler. So EventHandler is a more common name and delegates supports type overloadsSanferd
This new delegate discourages the usage of closures in lambda expression event handlers. Instead of capturing sender from somewhere outside the lambda (potentially forcing the compiler to create a display class), we can just get it from the handler's argument list. Also, since sender doesn't have to be casted anymore, this also decreases the likelihood of an unhandled InvalidCastException.Luedtke
M
25

It seems Microsoft has picked up on this as a similar example is now on MSDN:

Generic Delegates

Milton answered 25/11, 2010 at 17:24 Comment(1)
+1 Ah, excellent. They have picked up on this indeed. This is good. I hope, though, that they make this a recognized pattern within the VS IDE, because, as it is now, it is more awkward to use this pattern in terms of IntelliSense, etc.Cobb
C
14

The Windows Runtime (WinRT) introduces a TypedEventHandler<TSender, TResult> delegate, which does exactly what your StrongTypedEventHandler<TSender, TResult> does, but apparently without the constraint on the TResult type parameter:

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

The MSDN documentation is here.

Catchfly answered 15/3, 2012 at 10:25 Comment(7)
Ah, good to see that there is progress... I wonder why TResult is not restricted to inherit from the 'EventArgs' class. The 'EventArgs' base class is bascially empty; maybe they are moving away from this restriction?Cobb
It might be an oversight from the design team; who knows.Catchfly
well, events work fine without using EventArgs, it's just a convention thingWeiman
It specifically states in the TypedEventHandler documentation that args will be null if there is no event data, so it does appear that they are getting away from using an essentially empty object by default. I'm guessing that the original idea was that a method with a second parameter of type EventArgs could handle any event because the types would always be compatible. They are probably realising now that being able to handle multiple different events with one method is not all that important.Wozniak
Good point: rather have a specialized method with an argument e constrained to EventArgs, let the developer do whatever is most meaningful in the context.Catchfly
It doesn't look like an oversight. The constraint was remove from the System.EventHandler<TEventArgs> delegate as well. referencesource.microsoft.com/#mscorlib/system/…Helpmate
Why on earth they didn't just define it as EventHandler<TSender, TResult>(...) I have no idea...Propane
C
13

What you're proposing does make alot of sense actually, and I just wonder if this is one of those things that's simply the way it is because it was originally designed before generics, or if there's a real reason for this.

Chibcha answered 25/6, 2009 at 20:26 Comment(1)
I'm sure that this is exactly the reason. However, now that the newer versions of the language have contravariance to handle this, it looks like they should be able to handle this in a backwards-compatible manner. Previous handlers that use a 'sender object' would not break. But this is not true for older languages and might not be true for some current .NET languages, I'm not sure.Cobb
M
6

I took a peek at how this was handled with the new WinRT and based on other opinions here, and finally settled on doing it like this:

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

This seems to be the best way forward considering the use of the name TypedEventHandler in WinRT.

Mukund answered 6/7, 2012 at 21:45 Comment(1)
Why add the generic restriction on TEventArgs? It was removed from EventHandler<> and TypedEventHandler<,> because it didn't really make sense.Propane
A
5

I take issue with the following statements:

  • I believe that the older versions of Visual Basic .NET through 2005 do not have delegate covariance and contravariance.
  • I do fully realize that this verges on blasphemy.

First of all, nothing you have done here has anything to do with covariance or contravariance. (Edit: The previous statement is wrong, for more information please see Covariance and Contravariance in Delegates) This solution will work just fine in all CLR versions 2.0 and up (obviously this will not work in a CLR 1.0 application as it uses generics).

Secondly, I strongly disagree that your idea verges on "blasphemy" as this is a wonderful idea.

Atelier answered 25/6, 2009 at 20:25 Comment(4)
Hi Andrew, thanks for the thumbs up! Considering your reputation level, this really means a lot to me... On the covariance/contravariance issue: if the delegate provided by the subscriber does not match the signature of the publisher's event exactly, then covariance and contravariance are involved. C# has had delegate covariance and contravariance since forever, so it feels intrinsic, but VB.NET did not have delegate covariance and contravariance until .NET 3.0. Therefore, VB.NET for .NET 2.0 and below would not be able to use this system. (See the code example I added in "Edit #2", above.)Cobb
@Mike - My apologies, you are 100% correct! I have edited my answer to reflect your point :)Atelier
Ah, interesting! It seems that delegate covariance/contravariance is part of the CLR, but (for reasons I don't know) it was not exposed by VB.NET before the most recent version. Here's an article by Francesco Balena that shows how delegate variance can be achieved using Reflection, if not enabled by the language itself: dotnet2themax.com/blogs/fbalena/….Cobb
@Mike - It is always interesting to learn the things that the CLR supports yet are not supported in any of the .NET languages.Atelier
D
2

I think it is a great idea and MS might simply not have the time or interest to invest in making this better as for example when they moved from ArrayList to generic based lists.

Dilworth answered 25/6, 2009 at 20:28 Comment(1)
You might be right... On the other hand, I think it's just a "standard", and possibly not a technical issue at all. That is, this capability might be present in all current .NET languages, I don't know. I do know that C# and the VB.NET can handle this. However, I'm not sure how broadly this works in all current .NET languages... But since it works in C# and VB.NET, and everyone here is so supportive, I think I am very likely to do this. :-)Cobb
I
2

From what I understand, the "Sender" field is always supposed to refer to the object which holds the event subscription. If I had my druthers, there would also be a field holding information sufficient to unsubscribe an event should it become necessary(*) (consider, for example, a change-logger which subscribes to 'collection-changed' events; it contains two parts, one of which does the actual work and holds the actual data, and the other of which provides a public interface wrapper, the main part could hold a weak reference to the wrapper part. If the wrapper part gets garbage-collected, that would mean there was no longer anybody interested in the data that was being collected, and the change-logger should thus unsubscribe from any event it receives).

Since it's possible that an object may send events on behalf of another object, I can see some potential usefulness for having a "sender" field which is of Object type, and for having the EventArgs-derived field contain a reference to the object which should be acted upon. The usefuless of the "sender" field, however, is probably limited by the fact that there's no clean way for an object to unsubscribe from an unknown sender.

(*) Actually, a cleaner way of handling unsubscriptions would be to have a multicast delegate type for functions which return Boolean; if a function called by such a delegate returns True, the delegate would be patched to remove that object. This would mean that delegates would no longer be truly immutable, but it should be possible to effect such change in thread-safe manner (e.g. by nulling out the object reference and having the multicast delegate code ignore any embedded null object references). Under this scenario, an attempt to publish and event to a disposed object could be handled very cleanly, no matter where the event came from.

Indecorous answered 31/10, 2010 at 17:24 Comment(0)
T
2

Looking back to blasphemy as the only reason for making sender an object type (if to omit problems with contravariance in VB 2005 code, which is a Microsoft's blunder IMHO), can anyone suggest at least theoretical motive for nailing the second argument to EventArgs type. Going even further, is there a good reason to conform with Microsoft's guidelines and conventions in this particular case?

Having need to develop another EventArgs wrapper for another data that we want to pass inside event handler seems odd, why can't straightly pass that data there. Consider the following sections of code

[Example 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;
}

[Example 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;
}
Tautonym answered 11/11, 2010 at 11:52 Comment(3)
Yes, creating a separate class that inherits from System.EventArgs can seem unintuitive and does represent extra work, but there is a very good reason for it. If you never need to change your code, then your approach is fine. But the reality is that you might need to increase the functionality of the event in a future version and add properties to the event args. In your scenario, you would have to add extra overloads, or optional parameters to the event handler's signature. This is an approach used in VBA and legacy VB 6.0, which is workable, but a bit ugly in practice.Cobb
By inheriting from EventArgs, however, a future version could inherit from your older event arguments class and augment it. All older callers could still work exactly as is, by operating against the base class of your new event arguments class. Very clean. More work for you, but cleaner for any callers that depend on your library.Cobb
It doesn't even need to inherit it, you can just add the additional functionality directly into your event args class and it will continue to work fine. That said, the restriction pinning args to eventargs was removed because it didn't make much sense for lots of scenarios, ie. when you know you will never need to expand functionality of a particular event or when all you need is a value type arg in very performance sensitive applications.Propane
D
1

I don't think there's anything wrong with what you want to do. For the most part, I suspect that the object sender parameter remains in order to continue to support pre 2.0 code.

If you really want to make this change for a public API, you might want to consider creating your own base EvenArgs class. Something like this:

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; } }
}

Then you can declare your events like this

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

And methods like this:

private void HandleSomething(object sender, EventArgs e)

will still be able to subscribe.

EDIT

That last line made me think a bit... You should actually be able to implement what you propose without breaking any outside functionality since the runtime has no problem downcasting parameters. I would still lean toward the DataEventArgs solution (personally). I would do so, however knowing that it is redundant, since the sender is stored in the first parameter and as a property of the event args.

One benefit of sticking with the DataEventArgs is that you can chain events, changing the sender (to represent the last sender) while the EventArgs retains the original sender.

Dockery answered 25/6, 2009 at 20:28 Comment(1)
Hey Michael, this is a pretty neat alternative. I like it. As you mention, though, it is redundant to have the 'sender' parameter effectively passed in twice. A similar approach is discussed here: #810109, and the consensus seems to be that it is too non-standard. This is why I was hesitant to suggest the strong-typed 'sender' idea here. (Seems to be well received though, so I'm pleased.)Cobb
F
1

With the current situation (sender is object), you can easily attach a method to multiple events:

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

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

If sender would be generic, the target of the click-event would not be of type Button or Label, but of type Control (because the event is defined on Control). So some events on the Button-class would have a target of type Control, others would have other target types.

Filia answered 25/6, 2009 at 20:34 Comment(2)
Tommy, you can do exactly this same thing with the system that I'm proposing. You can still use a standard event handler that has a 'object sender' parameter to handle these strong-typed events. (See the code example that I've now added to the original post.)Cobb
Yes, I agree, this is the good thing about standard .NET events, accepted!Tautonym
C
1

Go for it. For non component based code, I often simplify Event signatures to be simply

public event Action<MyEventType> EventName

where MyEventType does not inherit from EventArgs. Why bother, if I never intend to use any of the members of EventArgs.

Crosson answered 26/6, 2009 at 0:6 Comment(2)
Agree! Why should we feel ourselves monkeys?Tautonym
+1-ed, this is what I too use. Sometimes simplicity wins! Or even event Action<S, T>, event Action<R, S, T> etc. I have an extension method to Raise them thread-safely :)Sanferd

© 2022 - 2024 — McMap. All rights reserved.