C#中的RaiseEvent

(我知道标题听起来很容易,但坚持 – 这可能不是你认为的问题。)

在VB.NET中,我能够编写自定义事件。 举个例子,我有一个单独的线程会定期引发事件,在那个事件上需要更新GUI。 我不希望繁忙的线程打扰UI计算,我不想把Me.Invoke(Sub()…)放在事件处理程序中,因为它也是从GUI线程调用的。

我想出了这个非常有用的代码。 GUI线程将设置EventSyncInvoke = Me(主窗体)。 然后线程可以像往常一样简单地引发事件TestEvent,没有特殊代码,它将在GUI线程上无缝执行:

Private TestEventDelegate As EventHandler Public EventSyncInvoke As System.ComponentModel.ISynchronizeInvoke Public Custom Event TestEvent As EventHandler AddHandler(value As EventHandler) TestEventDelegate = [Delegate].Combine(TestEventDelegate, value) End AddHandler RemoveHandler(value As EventHandler) TestEventDelegate = [Delegate].Remove(TestEventDelegate, value) End RemoveHandler RaiseEvent(sender As Object, e As System.EventArgs) If EventSyncInvoke IsNot Nothing Then EventSyncInvoke.Invoke(TestEventDelegate, {sender, e}) Else TestEventDelegate.Invoke({sender, e}) End If End RaiseEvent End Event 

现在在C#中我可以做到这么多:

 public event EventHandler TestEvent add { testEventDelegate = (EventHandler)Delegate.Combine(testEventDelegate, value); } remove { testEventDelegate = (EventHandler)Delegate.Remove(testEventDelegate, value); } } 

但是,定制加油的能力在哪里?

编辑:
其他答案告诉我,我不能直接在C#中这样做,但不能解释为什么我不能这样做以及为什么我不想这样做。 我花了一些时间来理解C#事件与VB.NET相比如何工作。 我把自己的解释留给了那些对这个没有很好把握的人开始按照正确的方向思考。

老实说,我已经习惯了样板OnTestEvent格式,我不太喜欢让它与其他辅助方法不同的想法。 :-)既然我理解了基本原理,我发现它实际上是放置这些东西的最佳位置。

VB.NET允许您隐藏使用RaiseEvent关键字调用委托的背景细节。 RaiseEvent为自定义事件调用事件委托或自定义RaiseEvent部分。

在C#中,没有RaiseEvent。 举办活动基本上只不过是打电话给代表。 当你所做的一切都是调用委托时,没有自定义的RaiseEvent部分可以无缝调用。 因此对于C#,自定义事件就像骨架,实现事件的添加和删除,但没有实现提升它们的能力。 这就像必须用自定义RaiseEvent部分中的代码替换所有RaiseEvent TestEvent(sender,e)。

对于正常事件,提升看起来大致像NormalEvent(发件人,e)。 但是只要你自定义添加和删除,就必须使用你在添加和删除中使用的任何变量,因为编译器不再执行它了。 这就像VB.NET中的自动属性:一旦手动输入getter和setter,就必须声明并处理自己的局部变量。 因此,使用testEventDelegate(sender,e)代替TestEvent(sender,e)。 这是您重新路由事件代表的地方。

我比较了从VB.NET到C#的转换,必须用你的自定义RaiseEvent代码替换你的每个RaiseEvents。 RaiseEvent代码部分基本上是一个事件和一个辅助函数。 实际上,标准是在受保护的OnTestEvent方法中只在VB.NET或C#中有一个RaiseEvent实例,并调用该方法来引发事件。 这允许任何有权访问受保护(或私有或公共)OnTestEvent的代码引发事件。 对于您想要做的事情,只需将其放入方法中就会更容易,更简单并且性能稍好一些。 这是最佳做法。

现在,如果你真的想要(或需要)以某种方式模仿VB.NET的RaiseEvent细节隐藏调用SomeDelegate(发送者,e)并让魔法发生,你可以简单地隐藏第二个委托内的细节:

NiceTestEvent = (sender, e) => eventSyncInvoke.Invoke(testEventDelegate, new object[] { sender, e });

现在你可以调用NiceTestEvent(发件人,e)。 但是,您将无法调用TestEvent(sender,e)。 正如Visual Studio将告诉您的那样,TestEvent仅用于外部代码的添加和删除。

在C#中 ,没有任何RaiseEvent块。 你可以通过创建一个提升事件的方法来做同样的事情。

编辑

这是一个有效的例子。 在C#版本中,您甚至不需要使用添加和删除块 – 您可以使用默认实现,只需创建一个引发事件的自定义引发方法。

下面是一个工作程序(表单只是一个Windows窗体,上面有一个按钮)。

 // Here is your event-raising class using System; using System.ComponentModel; namespace ClassLibrary1 { public class Class1 { public ISynchronizeInvoke EventSyncInvoke { get; set; } public event EventHandler TestEvent; private void RaiseTestEvent(EventArgs e) { // Take a local copy -- this is for thread safety. If an unsubscribe on another thread // causes TestEvent to become null, this will protect you from a null reference exception. // (The event will be raised to all subscribers as of the point in time that this line executes.) EventHandler testEvent = this.TestEvent; // Check for no subscribers if (testEvent == null) return; if (EventSyncInvoke == null) testEvent(this, e); else EventSyncInvoke.Invoke(testEvent, new object[] {this, e}); } public void Test() { RaiseTestEvent(EventArgs.Empty); } } } // Here is a form that tests it -- if you run it, you will see that the event is marshalled back to // the main thread, as desired. using System; using System.Threading; using System.Windows.Forms; namespace ClassLibrary1 { public partial class Form1 : Form { public Form1() { InitializeComponent(); this.TestClass = new Class1(); this.TestClass.EventSyncInvoke = this; this.TestClass.TestEvent += new EventHandler(TestClass_TestEvent); Thread.CurrentThread.Name = "Main"; } void TestClass_TestEvent(object sender, EventArgs e) { MessageBox.Show(this, string.Format("Event. Thread: {0} Id: {1}", Thread.CurrentThread.Name, Thread.CurrentThread.ManagedThreadId)); } private Class1 TestClass; private void button1_Click(object sender, EventArgs e) { // You can test with an "old fashioned" thread, or the TPL. var t = new Thread(() => this.TestClass.Test()); t.Start(); //Task.Factory.StartNew(() => this.TestClass.Test()); } } } 

你根本做不到。 但由于事件只能从声明它们的类型内部引发,因此您可以创建一个执行特定引发代码的辅助方法。 然后确保不要直接在该方法之外引发事件。

在C#中不存在如VB.NET中的AFAIK自定义事件引发。 但是,您可以将实际的事件处理程序委托(传递给作为value add )包装在lambda中,并将lambda订阅到事件而不是原始委托:

 add { testEventDelegate = Delegate.Combine(testEventDelegate, (s, e) => { ... } ) } 

(上面的代码未经测试,语法可能略有偏差。我会在测试后立即修复它。)


原油,但工作的例子:

以下是上述具体示例。 我不相信自己以下是好的,可靠的代码,也不会在所有情况下都能工作(例如multithreading等)……然而,这里是:

 class Foo { public Foo(SynchronizationContext context) { this.context = context ?? new SynchronizationContext(); this.someEventHandlers = new Dictionary(); } private readonly SynchronizationContext context; // ^ could also use ISynchronizeInvoke; I chose SynchronizationContext // for this example because it is independent from, but compatible with, // Windows Forms. public event EventHandler SomeEvent { add { EventHandler wrappedHandler = (object s, EventArgs e) => { context.Send(delegate { value(s, e); }, null); // ^ here is where you'd call ISynchronizeInvoke.Invoke(). }; someEvent += wrappedHandler; someEventHandlers[value] = wrappedHandler; } remove { if (someEventHandlers.ContainsKey(value)) { someEvent -= someEventHandlers[value]; someEventHandlers.Remove(value); } } } private EventHandler someEvent = delegate {}; private Dictionary someEventHandlers; public void RaiseSomeEvent() { someEvent(this, EventArgs.Empty); // if this is actually the only place where you'd invoke the event, // then you'd have far less overhead if you moved the ISynchronize- // Invoke.Invoke() here and forgot about all the wrapping above...! } } 

(请注意,为简洁起见,我使用了C#2匿名delegate {}语法。)