如何手动触发事件?

31

我在C#中有以下代码:

_timer.ElapsedTick += _somefunction1;
_timer.ElapsedTick += _somefunction2;
_timer.ElapsedTick += _somefunction3;
如何在不指定_somefunction的情况下调用订阅_timer.ElapsedTick的所有方法?在这个伪代码行附近。
invoke(_timer.ElapsedTick);

您的问题不应包含自己的答案,也不要使用“UPDATE”标记:https://meta.stackexchange.com/a/127655/316262 - Neuron
11个回答

26

您无法调用属于另一个类型的事件。事件只能从声明它的类内部调用。


5
@MatthewPatrickCashatt 是的,但是你在使用反射。这可以用来绕过CLR和C#中的大量限制。总的来说,我不认为反射是“如何做X”的有效答案。它肯定是有效的。 - JaredPar
8
通常情况下,当回答一个“如何在C#中实现某个功能”的问题时,我会限制我的回答只针对使用语言本身的能力,不包括反射/ PInvoke / COM互操作。这些往往在代码库中是禁忌的,因此通常是不正确的。一旦引入了反射,就有许多可以做到但在C#中无法实现的事情,而且通常有充分的理由。例如,允许您代表我调用我的事件是危险的,它可能会轻易地破坏我的消费者所依赖的微妙保证(例如书尾事件)。 - JaredPar
2
@MatthewPatrickCashatt 这绝不是一项硬性规定,也不是普遍认可的。这是我回答问题的方法。如果你浏览我的历史记录,我相信你可以找到很多我决定提供反思答案的地方。但我通常会在回答中加上“在原始的C#中不可能,但如果你想深入研究的话”... - JaredPar
1
@JesonMartajaya 这并不总是危险的,但通常是这样。考虑一下我的类型具有“开始”和“结束”事件的情况。我提供的保证是我会按顺序引发它们,并且“开始”始终会伴随着“结束”。因为我控制事件,所以我可以做出这个保证,另一个类可以依赖于此。如果突然间你能够控制我的事件,那么就没有什么阻止你在“开始”之前引发“结束”,或者在没有结束的情况下引发“开始”。此外,你引发事件可能会绕过我的类型设置我正在进行“开始”/“结束”进程的标志。所有这些都非常糟糕。 - JaredPar
2
@MatthewPatrickCashatt 你说得对,我应该更加小心地说“它不起作用”时其实是“它确实可以工作,但你可能不应该这样做”。 - JaredPar
显示剩余11条评论

23

能否使用传统的C#实现?不能(正如先前所述)。但是,使用反射是可能的。下面是一些经过测试的代码,基于这个MSDN论坛主题的答案

class InvokeFromMe
{
    public event EventHandler RaiseMe;
}

class Program
{
    static void Main(string[] args)
    {
        var fromMe = new InvokeFromMe();

        fromMe.RaiseMe += fromMe_RaiseMe;
        fromMe.RaiseMe += fromMe_RaiseMe1;
        fromMe.RaiseMe += fromMe_RaiseMe2;

        FireEvent(fromMe, "RaiseMe", null, EventArgs.Empty);
    }

    static void fromMe_RaiseMe(object sender, EventArgs e)
    {
        Console.WriteLine("Event Handler 0 Raised");
    }
    static void fromMe_RaiseMe1(object sender, EventArgs e)
    {
        Console.WriteLine("Event Handler 1 Raised");
    }
    static void fromMe_RaiseMe2(object sender, EventArgs e)
    {
        Console.WriteLine("Event Handler 2 Raised");
    }

    public static void FireEvent(object onMe, string invokeMe, params object[] eventParams)
    {
        MulticastDelegate eventDelagate =
              (MulticastDelegate)onMe.GetType().GetField(invokeMe,
               System.Reflection.BindingFlags.Instance |
               System.Reflection.BindingFlags.NonPublic).GetValue(onMe);

        Delegate[] delegates = eventDelagate.GetInvocationList();

        foreach (Delegate dlg in delegates)
        {
            dlg.Method.Invoke(dlg.Target, eventParams);
        }
    } 

}

更新

我不熟悉System.Timer.Timer类,所以我不确定它与我提供的示例有何不同。你可以尝试像这样做:

public static void FirePublicEvent(object onMe, string invokeMe, params object[] eventParams)
{
    MulticastDelegate eventDelagate =
          (MulticastDelegate)onMe.GetType().GetField(invokeMe,
           System.Reflection.BindingFlags.Instance |
           System.Reflection.BindingFlags.Public).GetValue(onMe);

    Delegate[] delegates = eventDelagate.GetInvocationList();

    foreach (Delegate dlg in delegates)
    {
       dlg.Method.Invoke(dlg.Target, eventParams);
    }
} 

1
@JesonMartajaya - 对于 System.Timers.Timer 类,您需要像这样调用 FireEventFireEvent(fromMe, "onIntervalElapsed", this, null); - M.Babcock
1
e 参数必须为 null,因为 ElapsedEventArgs 没有公共构造函数,而 ElapsedEventArgs.Empty 返回一个 EventArgs。您也可以使用反射来实例化它,但我需要尝试一下才能确定。 - M.Babcock
2
我使用ILSpy将System.dll反编译,并逆向了事件。像Reflector、ILSpy和JustDecompile这样的工具是.NET开发者工具箱里不可或缺的。 - M.Babcock
在我看來,诉诸于“反射”(Reflection)是一种严重的代码异味(code smell)。虽然我还没有深入思考细节,但是通过对Timer的子类进行子类化并给该子类一个执行Timer事件处理程序所需操作的方法似乎会更加简洁。 - ToolmakerSteve
@ToolmakerSteve 当然,在这种特定情况下这样做是可行的,但如果事件的所有者是“sealed”呢?此外,这是很多仪式,现在子类是要维护和测试的问题。最好将事件处理程序的逻辑重构为另一个可以直接调用并完全绕过事件系统的方法。 - M.Babcock
显示剩余10条评论

8

我在搜索如何简单地启动事件时,最终来到了这里,该事件不具有任何自定义参数或其他内容。我知道OP的问题是不同的!但我想分享一下,希望能帮助到某些人。

简化版本:

 var Handler = EventNameHere;
        if (Handler != null)
        {
            Handler(this, EventArgs.Empty);
        }

简化版:

 EventNameHere?.Invoke(this, EventArgs.Empty);

2
你可以创建一个运行它们的函数:
public void RunAllFuncs()
{
   _somefunction1();
   _somefunction2();
   _somefunction3();
}

绝对不是我要找的。除了view _timer.ElapsedTick之外,我没有任何关于somefunction方法的参考。 - Jeson Martajaya

2
这是一种解决方法,你不能循环执行 Elapsed 事件。但是,可以让计时器完成这项工作来调用它们所有。此代码适用于 System.Timer,不确定你正在使用哪个计时器。
假设计时器已启用:
int interval = timer.Interval;
ElapsedEventHandler handler = null;
handler = (s,e) =>
{
    timer.Interval = interval; // put interval back to original value
    timer.Elapsed -= handler;
};
timer.Elapsed += handler;
timer.Interval = 1; // 1 millisecond, pretty much going to fire right now (as soon as you let it)

这样做会触发事件,但是原始的间隔会重新启动。如果您想保留原始的时间间隔模式,则可能需要进行一些计算。


1

我认为你可能需要使用InvokeMember:

public void GetMethod(string methodName){

            var args = new Object[] { [INSERT ARGS IF NECESSARY--SET TO NULL OTHERWISE] };
            try
            {
                var t = new [INSERT NAME OF CLASS THAT CONTAINS YOUR METHOD]();
                Type typeInfo = t.GetType();
                var result = typeInfo.InvokeMember(methodName, BindingFlags.InvokeMethod, null, t, args);
                return result;
            }
            catch (Exception ex)
            {
                return ex;
            }
}

然后像这样调用它:

_timer.ElapsedTick += GetMethod("[INSERT METHOD NAME AS STRING]");

请确保包含以下内容:

using System.Reflection;

祝你好运!


为了更清晰明确,我想手动触发 _timer.ElapsedTick 事件。根据您提出的解决方案,您认为我可以调用 GetMethod("_timer.ElapsedTick") 吗? - Jeson Martajaya
这很接近但还不够。你的代码直接运行了一个方法。你需要先获取表示类型上事件的字段作为MulticastDelegate,然后获取它的调用列表,从那里你可以动态地单独调用每个方法。这个 MSDN问题的答案展示了如何做到这一点。 - M.Babcock
你可以使用“GetMethod”调用任何你喜欢的方法,但是请看看@JaredPar的评论,因为他有一些保留意见。我一直在成功地使用GetMethod,但是很难忽视一个拥有178k声望的人警告它。祝好运! - Matt Cashatt
@MatthewPatrickCashatt - 是的,你说得对,你可以使用你提供的代码直接调用任何方法,但这不是问题所在。 - M.Babcock
@M.Babcock- 我看到你非常擅长对我的答案进行反驳(而我的答案是有效的)。也许你可以提供自己的答案,而不是指出我的答案是不正确的/没有回答问题?我们是软件开发人员--不是在争论语义的律师。让我们帮助这个人,好吗? - Matt Cashatt
@M.Babcock- 很好 - 看起来你已经快抓住他了。 干杯! - Matt Cashatt

0

在我之前的回答基础上,以下方法适用于我:

EventNameHere(null, EventArgs.Empty);

0

使用.NET 6(也可能适用于其他版本),以下通用方法可能会有所帮助。与其他答案类似,它使用反射来调用EventHandler:

private void InvokeEvent<T, TEvent>(T target, string eventName, TEvent @event)
{
   // some reflection magic to fire an event from outside the target class:
   if(target == null)
   {
      throw new ArgumentNullException(nameof(target));
   }
   Type targetType = target.GetType();
   FieldInfo? fi = targetType.GetField(eventName, BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance);
   if(fi != null)
   {
      EventHandler<TEvent>? eventHandler = fi.GetValue(target) as EventHandler<TEvent>;
      if(eventHandler != null)
      {
         eventHandler.Invoke(this, @event);
      }
   }
}

然后可以像这样调用它:

const string eventName = nameof(Algorithm.Received);
DomainEvent @event = new DomainEvent(payload);
InvokeEvent(targetObject, eventName, @event);

注意事项:

  • 如果使用+=将多个方法添加到事件处理程序中,则上述代码也适用。
  • 如果您想在产品中使用此代码,您可能需要考虑更多的错误处理,例如参数检查或如果找不到特定的事件处理程序则抛出异常。
  • 随着每个主要版本的.NET,使用反射的代码可能需要进行更改。由于.NET 6是跨平台运行在Windows、Linux、MacOS等操作系统上,我认为破坏上述代码的变化可能会减少,但不为零。
  • 我们正在我们的自动化测试套件中使用此代码。如果使用反射的代码实际上发生了更改,我们只需要更改这个方法。

0

基于上面M.Babcock的答案,进行了一些轻微的反射方法更新。

GetTypeInfoGetDeclaredField替换了GetField,用dlg.GetMethodInfo替换了dlg.Method

    using System.Reflection;

    ...

    public static void FireEvent(this object onMe, string invokeMe, params object[] eventParams)
    {
        TypeInfo typeInfo = onMe.GetType().GetTypeInfo();
        FieldInfo fieldInfo = typeInfo.GetDeclaredField(invokeMe);
        MulticastDelegate eventDelagate = (MulticastDelegate)fieldInfo.GetValue(onMe);

        Delegate[] delegates = eventDelagate.GetInvocationList();

        foreach (Delegate dlg in delegates)
        {
            dlg.GetMethodInfo().Invoke(dlg.Target, eventParams);
        }
    }

-1

根据 ElapsedTick 事件的签名,您可以通过在声明该事件的类内部像调用方法一样调用它来触发事件。例如:

ElapsedTick(this, new EventArgs());

1
EventArgs.Empty 很好,因为它避免了不必要的分配。此外,您不能像那样从类的外部调用事件。 - Ed S.
我忘了提到它是从类内部这样触发的。此外,使用 EventArgs.Empty 是一个不错的选择。 - Bernard
1
我尝试了以下代码: _timer.ElapsedTick(this, ElapsedEventArgs.Empty); 但是它会出现编译时错误:“System.Timers.Timer”不包含“ElapsedTick”的定义,也没有接受类型为“System.Timers.Timer”的第一个参数的扩展方法“ElapsedTick”可以找到(您是否缺少使用指令或程序集引用?) - Jeson Martajaya
我不明白这怎么回答问题(尽管我没有对答案进行负面评价)。OP并没有在类内部编写代码(在这种情况下,是类Timer)。那么他如何利用这个建议呢?[回答我的问题:也许通过子类化Timer,并添加一个包含此行代码的公共方法。这个答案需要展示如何做到这一点。] - ToolmakerSteve

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接