如何在.NET/C#中通过反射引发事件?

34

我有一个由文本框和按钮(DevExpress ButtonEdit控件)组成的第三方编辑器。我想让特定的按键组合(Alt+下箭头)模拟点击按钮。为了避免反复编写代码,我想创建一个通用的KeyUp事件处理程序来触发ButtonClick事件。不幸的是,该控件似乎没有一个能够触发ButtonClick事件的方法,因此...

如何通过反射从外部函数触发该事件?

9个回答

42

这里有一个使用泛型的演示(省略了错误检查):

using System;
using System.Reflection;
static class Program {
  private class Sub {
    public event EventHandler<EventArgs> SomethingHappening;
  }
  internal static void Raise<TEventArgs>(this object source, string eventName, TEventArgs eventArgs) where TEventArgs : EventArgs
  {
    var eventDelegate = (MulticastDelegate)source.GetType().GetField(eventName, BindingFlags.Instance | BindingFlags.NonPublic).GetValue(source);
    if (eventDelegate != null)
    {
      foreach (var handler in eventDelegate.GetInvocationList())
      {
        handler.Method.Invoke(handler.Target, new object[] { source, eventArgs });
      }
    }
  }
  public static void Main()
  {
    var p = new Sub();
    p.Raise("SomethingHappening", EventArgs.Empty);
    p.SomethingHappening += (o, e) => Console.WriteLine("Foo!");
    p.Raise("SomethingHappening", EventArgs.Empty);
    p.SomethingHappening += (o, e) => Console.WriteLine("Bar!");
    p.Raise("SomethingHappening", EventArgs.Empty);
    Console.ReadLine();
  }
}

2
我注意到你创建了一个名为eventInfo的本地变量,但是你从未对它进行任何操作。这是只有我这样认为还是第一行可以被删除? - Nick
1
你不需要获取eventInfo,它是无用的。 否则,这是一个很好的示例! - henon
3
调用 GetInvocationList() 并不是必要的,只需要直接调用 eventDelegate 即可。 - HappyNomad
eventDelegate.DynamicInvoke(args) 就像 @HappyNomad 所说的那样完美地工作。 - kevindaub
1
你不使用eventInfo。 - Christo S. Christov

14
一般而言,您不能这样做。将事件视为基本上是AddHandler/RemoveHandler方法对(因为这基本上就是它们的作用)。它们的实现取决于类。大多数WinForms控件使用EventHandlerList作为其实现,但是如果开始获取私有字段和密钥,则代码将非常脆弱。 ButtonEdit控件是否公开了可调用的OnClick方法?
注:实际上,事件可以具有“raise”成员,因此有EventInfo.GetRaiseMethod。但是,C#从不填充此内容,我认为它在框架中通常也不存在。

11

通常情况下,您无法引发另一个类的事件。 事件实际上作为私有委托字段存储,并具有两个访问器(add_event和remove_event)。

通过反射完成此操作,您只需要找到私有委托字段,获取它,然后调用它即可。


10

我编写了一个扩展类,实现了INotifyPropertyChanged接口并注入了RaisePropertyChange<T>方法,这样我就可以像这样使用它:

this.RaisePropertyChanged(() => MyProperty);

该方法并未在任何基类中实现。对于我的使用来说,它速度太慢了,但也许源代码可以帮助某些人。

因此,在这里提供源代码:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq.Expressions;
using System.Reflection;
using System.Globalization;

namespace Infrastructure
{
    /// <summary>
    /// Adds a RaisePropertyChanged method to objects implementing INotifyPropertyChanged.
    /// </summary>
    public static class NotifyPropertyChangeExtension
    {
        #region private fields

        private static readonly Dictionary<string, PropertyChangedEventArgs> eventArgCache = new Dictionary<string, PropertyChangedEventArgs>();
        private static readonly object syncLock = new object();

        #endregion

        #region the Extension's

        /// <summary>
        /// Verifies the name of the property for the specified instance.
        /// </summary>
        /// <param name="bindableObject">The bindable object.</param>
        /// <param name="propertyName">Name of the property.</param>
        [Conditional("DEBUG")]
        public static void VerifyPropertyName(this INotifyPropertyChanged bindableObject, string propertyName)
        {
            bool propertyExists = TypeDescriptor.GetProperties(bindableObject).Find(propertyName, false) != null;
            if (!propertyExists)
                throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture,
                    "{0} is not a public property of {1}", propertyName, bindableObject.GetType().FullName));
        }

        /// <summary>
        /// Gets the property name from expression.
        /// </summary>
        /// <param name="notifyObject">The notify object.</param>
        /// <param name="propertyExpression">The property expression.</param>
        /// <returns>a string containing the name of the property.</returns>
        public static string GetPropertyNameFromExpression<T>(this INotifyPropertyChanged notifyObject, Expression<Func<T>> propertyExpression)
        {
            return GetPropertyNameFromExpression(propertyExpression);
        }

        /// <summary>
        /// Raises a property changed event.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="bindableObject">The bindable object.</param>
        /// <param name="propertyExpression">The property expression.</param>
        public static void RaisePropertyChanged<T>(this INotifyPropertyChanged bindableObject, Expression<Func<T>> propertyExpression)
        {
            RaisePropertyChanged(bindableObject, GetPropertyNameFromExpression(propertyExpression));
        }

        #endregion

        /// <summary>
        /// Raises the property changed on the specified bindable Object.
        /// </summary>
        /// <param name="bindableObject">The bindable object.</param>
        /// <param name="propertyName">Name of the property.</param>
        private static void RaisePropertyChanged(INotifyPropertyChanged bindableObject, string propertyName)
        {
            bindableObject.VerifyPropertyName(propertyName);
            RaiseInternalPropertyChangedEvent(bindableObject, GetPropertyChangedEventArgs(propertyName));
        }

        /// <summary>
        /// Raises the internal property changed event.
        /// </summary>
        /// <param name="bindableObject">The bindable object.</param>
        /// <param name="eventArgs">The <see cref="System.ComponentModel.PropertyChangedEventArgs"/> instance containing the event data.</param>
        private static void RaiseInternalPropertyChangedEvent(INotifyPropertyChanged bindableObject, PropertyChangedEventArgs eventArgs)
        {
            // get the internal eventDelegate
            var bindableObjectType = bindableObject.GetType();

            // search the base type, which contains the PropertyChanged event field.
            FieldInfo propChangedFieldInfo = null;
            while (bindableObjectType != null)
            {
                propChangedFieldInfo = bindableObjectType.GetField("PropertyChanged", BindingFlags.Instance | BindingFlags.NonPublic);
                if (propChangedFieldInfo != null)
                    break;

                bindableObjectType = bindableObjectType.BaseType;
            }
            if (propChangedFieldInfo == null)
                return;

            // get prop changed event field value
            var fieldValue = propChangedFieldInfo.GetValue(bindableObject);
            if (fieldValue == null)
                return;

            MulticastDelegate eventDelegate = fieldValue as MulticastDelegate;
            if (eventDelegate == null)
                return;

            // get invocation list
            Delegate[] delegates = eventDelegate.GetInvocationList();

            // invoke each delegate
            foreach (Delegate propertyChangedDelegate in delegates)
                propertyChangedDelegate.Method.Invoke(propertyChangedDelegate.Target, new object[] { bindableObject, eventArgs });
        }

        /// <summary>
        /// Gets the property name from an expression.
        /// </summary>
        /// <param name="propertyExpression">The property expression.</param>
        /// <returns>The property name as string.</returns>
        private static string GetPropertyNameFromExpression<T>(Expression<Func<T>> propertyExpression)
        {
            var lambda = (LambdaExpression)propertyExpression;

            MemberExpression memberExpression;

            if (lambda.Body is UnaryExpression)
            {
                var unaryExpression = (UnaryExpression)lambda.Body;
                memberExpression = (MemberExpression)unaryExpression.Operand;
            }
            else memberExpression = (MemberExpression)lambda.Body;

            return memberExpression.Member.Name;
        }

        /// <summary>
        /// Returns an instance of PropertyChangedEventArgs for the specified property name.
        /// </summary>
        /// <param name="propertyName">
        /// The name of the property to create event args for.
        /// </param>
        private static PropertyChangedEventArgs GetPropertyChangedEventArgs(string propertyName)
        {
            PropertyChangedEventArgs args;

            lock (NotifyPropertyChangeExtension.syncLock)
            {
                if (!eventArgCache.TryGetValue(propertyName, out args))
                    eventArgCache.Add(propertyName, args = new PropertyChangedEventArgs(propertyName));
            }

            return args;
        }
    }
}

我删除了原始代码的一些部分,所以扩展程序应该可以正常工作,而不需要引用我的库的其他部分。但是它并没有经过真正的测试。

顺便说一句,代码的某些部分是借鉴别人的。很抱歉,我忘记了从哪里得到它。 :(


非常感谢您。事实上,您的答案是这里唯一进行继承树调查和适当的空值检查的答案。 - firegurafiku
为什么要缓存事件参数?在我看来,所需的负担、字典和锁定远远超过了任何潜在的性能提升。我是否遗漏了什么(共享状态等)? - skataben
你应该在这里查看 解决方案1:https://stackoverflow.com/a/54789191/7108481 - Jogge

8

看起来,Wiebe Cnossen的被接受的答案中的代码可以简化为以下内容:

private void RaiseEventViaReflection(object source, string eventName)
{
    ((Delegate)source
        .GetType()
        .GetField(eventName, BindingFlags.Instance | BindingFlags.NonPublic)
        .GetValue(source))
        .DynamicInvoke(source, EventArgs.Empty);
}

7

来自 通过反射引发事件,虽然我认为在 VB.NET 中的答案即这个帖子的前两篇将为您提供通用方法(例如,我会参考VB.NET中关于引用不在同一类中的类型的灵感):

 public event EventHandler<EventArgs> MyEventToBeFired;

    public void FireEvent(Guid instanceId, string handler)
    {

        // Note: this is being fired from a method with in the same
        //       class that defined the event (that is, "this").

        EventArgs e = new EventArgs(instanceId);

        MulticastDelegate eventDelagate =
              (MulticastDelegate)this.GetType().GetField(handler,
               System.Reflection.BindingFlags.Instance |
               System.Reflection.BindingFlags.NonPublic).GetValue(this);

        Delegate[] delegates = eventDelagate.GetInvocationList();

        foreach (Delegate dlg in delegates)
        {
            dlg.Method.Invoke(dlg.Target, new object[] { this, e });
        }
    }

    FireEvent(new Guid(),  "MyEventToBeFired");

1
这个例子给了我很大的帮助。我需要在我的 WPF 应用程序中的 Windows 应用程序对象中引发一个 CLR 事件,并且需要通过反射引用它。因此,在 FireEvent 方法中,我使用了 Application.Current,而非其他方法。非常好的建议,根据需要修改 FireEvent 方法的绑定标志。 - Tore Aurstad

6
事实证明,我可以做到这一点,而我没有意识到:
buttonEdit1.Properties.Buttons[0].Shortcut = new DevExpress.Utils.KeyShortcut(Keys.Alt | Keys.Down);

但如果我不能这样做,我将不得不深入源代码并找到触发事件的方法。

感谢所有人的帮助。


5
如果您知道控件是一个按钮,您可以调用它的PerformClick()方法。我对其他事件(如OnEnterOnExit)也有类似的问题。如果我不想为每种控件类型派生新类型,我就无法触发这些事件。

“执行点击”?你一定在开玩笑,这太棒了! - Sandeep Datta

1

对一些现有答案和评论的进一步改进。

这也考虑到委托字段可能在继承类中定义。

public static void RaiseEvent<TEventArgs>(this object source, string eventName, TEventArgs eventArgs)
    where TEventArgs : EventArgs
{
    // Find the delegate and invoke it.
    var delegateField = FindField(source.GetType(), eventName);
    var eventDelegate = delegateField?.GetValue(source) as Delegate;
    eventDelegate?.DynamicInvoke(source, eventArgs);

    // This local function searches the class hierarchy for the delegate field.
    FieldInfo FindField(Type type, string name)
    {
        while (true)
        {
            var field = type.GetField(name, BindingFlags.Instance | BindingFlags.NonPublic);
            if (field != null)
            {
                return field;
            }

            var baseType = type.BaseType;
            if (baseType == null)
            {
                return null;
            }

            type = baseType;
        }
    }
}

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