【问题标题】:Equivalent of VB's custom RaiseEvent blocks in C#?等效于 C# 中 VB 的自定义 RaiseEvent 块?
【发布时间】:2012-02-18 15:39:49
【问题描述】:

(我知道标题听起来很简单,但请稍等——这可能不是您认为的问题。)

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


}

但是自定义加注的能力在哪里?

【问题讨论】:

  • 我开始注意到人们在阅读问题内容之前会做可能重复的事情(请这样做!)。 RaiseEvent 关键字可以定义两种截然不同的事物。

标签: c# events


【解决方案1】:

其他答案告诉我我不能直接在 C# 中执行此操作,但不是我为什么不能以及为什么我不想这样做的理由。与 VB.NET 相比,我花了一段时间才了解 C# 事件是如何工作的。所以这个解释是为了让其他没有很好理解这一点的人开始沿着正确的路线思考。

老实说,我已经习惯了样板文件 OnTestEvent 格式,以至于我不太喜欢让它与其他辅助方法不同的想法。 :-) 但现在我明白了其中的原理,我发现它实际上是放置这些东西的最佳位置。


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

在 C# 中,没有 RaiseEvent。引发事件基本上只不过是调用委托。当您为提升它所做的一切就是调用委托时,无法无缝调用自定义 RaiseEvent 部分。因此对于 C#,自定义事件就像骨架,实现事件的添加和删除,但没有实现引发它们的能力。这就像必须用自定义 RaiseEvent 部分中的代码替换所有 RaiseEvent TestEvent(sender, e)

对于正常事件,加注大致类似于NormalEvent(sender, e)。但是一旦您放入自定义添加和删除,您必须使用您在添加和删除中使用的任何变量,因为编译器不再这样做了。这就像 VB.NET 中的自动属性:一旦手动输入 getter 和 setter,就必须声明和处理自己的局部变量。所以不要使用TestEvent(sender, e),而是使用testEventDelegate(sender, e)。这就是您重新路由事件代表的地方。


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

现在,如果您真的想(或需要)以某种方式模仿 VB.NET 的 RaiseEvent 细节隐藏调用 SomeDelegate(sender, e) 并让魔法发生,您可以简单地隐藏细节-第二个代表内部的坚韧不拔:

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

现在您可以致电NiceTestEvent(sender, e)。不过,您将无法致电TestEvent(sender, e)TestEvent 仅用于添加和删除外部代码,Visual Studio 会告诉你。

【讨论】:

  • +1 花时间写下你的发现。如果您想更深入一点,也许您也对.NET events special methods 感兴趣。基本上,CLR 支持不同的 raise 事件访问器方法(正如您可能从 VB.NET 处理自定义事件的方式中猜到的那样),但显然它在实践中很少使用。 VB.NET 似乎确实 使用了这个访问器。 AFAIK,C# 不支持它。
  • 啊,我在想。我仍然不太了解微软 C# 和 VB.NET 部门之间的脱节。
【解决方案2】:

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

这是一个工作示例。在 C# 版本中,您甚至不需要使用 add 和 remove 块——您可以使用默认实现,只需创建一个自定义 raise 方法来引发您的事件。

下面是一个工作程序(表单只是一个带有单个按钮的 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());
        }
    }
}

【讨论】:

    【解决方案3】:

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

    【讨论】:

      【解决方案4】:

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

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

      (以上代码未经测试,语法可能略有偏差。我会尽快对其进行测试。)


      粗略但有效的例子:

      以下是上述的具体例子。我不相信以下是好的、可靠的代码,也不相信它在所有情况下都可以工作(例如多线程等)......但是,它是:

      class Foo
      {
          public Foo(SynchronizationContext context)
          {
              this.context = context ?? new SynchronizationContext();
              this.someEventHandlers = new Dictionary<EventHandler, EventHandler>();
          }
      
          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<EventHandler, EventHandler> 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 {} 语法。)

      【讨论】:

      • 如何删除委托?有第二个 lambda?
      • @jnm2,好点。您可能必须保留 Dictionary 以将委托映射到 lambda 函数,以便您可以从事件中删除 lambda。
      • 即使没有字典,Me.Invoking 每个委托单独是很多开销。不过你给了我一个好主意。我应该保留委托的非 Me.Invoked 副本并从中添加和删除,并将 testEventDelegate 设置为新的 lambda。
      • @jnm2,可能值得测试开销是否真的是一个问题(尤其是如果您认为它可能会解决您的问题)。
      • @jnm2,关于您的(现已删除)问题评论:我相信这是 JMarsch 和 svick 回答的要点......因为您只能在声明的类型中引发事件,如果如果您希望该事件可以公开提出,那么您需要一种公开的方法来做到这一点。另请参阅我的代码示例中的最后一点。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-07-01
      • 1970-01-01
      • 1970-01-01
      • 2017-11-11
      • 1970-01-01
      相关资源
      最近更新 更多