NET 中的事件签名——使用强类型的“发件人”? ?

我充分认识到,我提出的建议不符合。NET 指导原则,因此,仅仅因为这个原因,可能是一个糟糕的想法。不过,我想从两个可能的角度来考虑这个问题:

(1)我是否应该考虑将其用于我自己的开发工作,这是100% 的内部目的。

(2)这是框架设计人员可以考虑更改或更新的概念吗?

我正在考虑使用一个利用强类型“ sender”的事件签名,而不是将其输入为当前的“ object”。NET 设计模式。也就是说,不使用标准的事件签名,如下所示:

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

我正在考虑使用一个使用强类型“ sender”参数的事件签名,如下所示:

首先,定义一个“ StrongTypedEventHandler”:

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

这与 Action < TSender,TEventArgs > 没有什么不同,但是通过使用 StrongTypedEventHandler,我们强制 TEventArgs 派生自 System.EventArgs

接下来,作为一个示例,我们可以在发布类中使用 StrongTypedEventHandler,如下所示:

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


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

上述安排将使订阅者能够利用不需要强制转换的强类型事件处理程序:

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

我完全明白这违反了标准。NET 事件处理模式; 但是,请记住,如果需要,逆变将使订阅者能够使用传统的事件处理签名:

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

也就是说,如果事件处理程序需要订阅来自不同(或未知)对象类型的事件,处理程序可以将“ sender”参数键入为“ object”,以便处理潜在发送方对象的全部宽度。

除了打破常规(相信我,这是我不会掉以轻心的) ,我想不出这有什么不好的地方。

这里可能存在一些 CLS 遵从性问题。这在 VisualBasic 中运行。NET2008100% 没问题(我已经测试过了) ,但是我相信老版本的 VisualBasic。NET 至2005年没有委派反变。可能还有其他。NET 语言也有这个问题,我不能肯定。

但是除了 C # 或 VisualBasic 之外,我没有看到自己在开发任何语言。NET,并且我不介意将它限制在 C # 和 VB.NET 中。NET Framework 3.0及以上版本。(老实说,我无法想象在这个时候回到2.0。)

还有人能想出什么问题吗?或者这仅仅是打破了惯例,以至于让人们反胃?

以下是我发现的一些相关链接:

(1) 活动设计指引[ MSDN 3.5]

(2) C # 简单事件引发-使用“ sender”与自定义 EventArgs [ StackOverflow 2009]

(3) Net 中的事件签名模式[ StackOverflow 2008]

我对每个人的意见都很感兴趣。

先说声谢谢,

麦克

编辑 # 1: 这是对 Tommy Carlier 的岗位的回应:

下面是一个完整的工作示例,它显示了强类型事件处理程序和使用“ object sender”参数的当前标准事件处理程序可以与此方法共存。你可以复制粘贴代码并运行它:

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

编辑 # 2: 这是对 Andrew Hare 的证词关于反变及其应用的回应。C # 语言中的代表已经有了很长一段时间的反变,以至于让人感觉它是“内在的”,但事实并非如此。它甚至可能是在 CLR 中启用的,我不知道,但是 VisualBasic。NET 的委托反变直到。NET Framework 3.0(VB.NET 2008).因此,VisualBasic.NET 用于。NET 2.0及以下版本不能使用这种方法。

例如,上面的例子可以翻译成 VB.NET 如下:

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

NET 2008可以100% 正常运行。但我现在已经在 VB.NET 2005上进行了测试,以确保它不会编译,并声明:

方法 SomEventHandler (sender As Object,e 就像 VbGenericEventProcessing. GenericEventProcessing. PublisherEventArgs)’ 不具有与 代表代表小组 StrongTypeEventHandler (属于 TSender, 作为系统 作为出版商 PublisherEventArgs)’

基本上,委托在 VB.NET 2005及以下版本中是不变的。事实上,我几年前就想到了这个主意,但是 VB.NET 无法处理这个问题让我很困扰... ... 但是我现在已经坚定地转向 C # ,而且 VB.NET 现在可以处理它,所以,好吧,所以我写了这篇文章。

编辑: 更新 # 3

好的,我已经很成功地使用了一段时间了。这个系统真不错。我决定将我的“ StrongTypedEventHandler”命名为“ GenericEventHandler”,定义如下:

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

除了这个重命名之外,我完全按照上面讨论的方式实现了它。

它的确触犯了 FxCop 规则 CA1009,其中规定:

”按照惯例,. NET 事件有两个 指定事件的参数 事件处理程序 签名须依照下列表格填写: Void MyEventHandler (对象发送器, EventArgs e)。“ sender”参数 类型总是 System.Object,甚至 如果有可能雇用更多 特定类型。‘ e’参数是 类型总是 System.EventArgs。 不提供事件数据的事件 应该使用 System.EventHandler 事件处理程序返回 使他们可以发送每个事件 多个目标方法。任何值 一个目标返回将会丢失 在第一通电话之后”

当然,我们知道这一切,并打破了规则反正。(如果在任何情况下首选,所有事件处理程序都可以在其签名中使用标准的‘ object Sender’——这是一个非破坏性更改。)

因此,使用 SuppressMessageAttribute就可以解决这个问题:

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

我希望这种方法在将来的某个时候成为标准。

谢谢你们的意见,伙计们,我真的很感激..。

麦克

13280 次浏览

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.

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.

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.

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.

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.

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.

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.

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

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

Generic Delegates

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.

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.