【问题标题】:Redirecting to a dynamic method from a generic event handler从通用事件处理程序重定向到动态方法
【发布时间】:2011-12-14 18:58:48
【问题描述】:

我正在尝试编写一个用于从任意事件触发对方法的调用的类,但我被困住了,因为我根本无法从发出的 MSIL 代码中找到引用“this”的方法。

这个例子应该描述我在寻找什么:

class MyEventTriggeringClass
{ 
    private object _parameter;

    public void Attach(object source, string eventName, object parameter)
    {
        _parameter = parameter;
        var e = source.GetType().GetEvent(eventName);
        if (e == null) return;
        hookupDelegate(source, e);
    }

    private void hookupDelegate(object source, EventInfo e)
    {
        var handlerType = e.EventHandlerType;
        // (omitted some validation here)
        var dynamicMethod = new DynamicMethod("invoker",
                  null,
                  getDelegateParameterTypes(handlerType), // (omitted this method in this exmaple)
                  GetType());
        var ilgen = dynamicMethod.GetILGenerator();
        var toBeInvoked = GetType().GetMethod(
            "invokedMethod", 
            BindingFlags.NonPublic | BindingFlags.Instance);
        ilgen.Emit(OpCodes.Ldarg_0); // <-- here's where I thought I could push 'this' (failed)
        ilgen.Emit(OpCodes.Call, toBeInvoked);
        ilgen.Emit(OpCodes.Ret);
        var sink = dynamicMethod.CreateDelegate(handlerType);
        e.AddEventHandler(source, sink);
    }

    private void invokedMethod()
    {
        Console.WriteLine("Value of _parameter = " + _parameter ?? "(null)"); 
        // output is always "(null)"
    }
}

以下是我设想使用的类的示例:

var handleEvent = new MyEventTriggeringClass();
handleEvent.Attach(someObject, "SomeEvent", someValueToBePassedArround);

(请注意,上面的例子毫无意义。我只是试图描述我在寻找什么。我的最终目标是能够在任意事件触发时触发对任意方法的调用。我'将在我尝试使用 100% MVVM 的 WPF 项目中使用它,但我偶然发现了 [看似] 经典的断点之一。)

无论如何,代码“有效”,只要它在任意事件触发时成功调用“invokedMethod”,但“this”似乎是一个空对象(_parameter 始终为空)。我已经进行了一些研究,但根本找不到任何好的示例,其中“this”被正确传递给从这样的动态方法中调用的方法。

我找到的最接近的示例是 THIS ARTICLE,但在该示例中,可以强制使用动态方法,因为它是从代码调用的,而不是任意事件处理程序。

任何建议或提示将不胜感激。

【问题讨论】:

  • 不要使用全局 _parameter 并将自定义 EventArgs 作为参数传递给您的方法。
  • 如果您仔细观察,您会发现目标方法('invokedMethod')将从动态构建的任意事件处理程序中调用。因此,事件处理程序可以是诸如“MouseDown”或“SelectedChanged”之类的任何东西。事件处理程序将被传递 EventArgs (或其某些后代),这在这种情况下是无关紧要且毫无用处的。我只需要在触发事件时收到通知,然后使用一堆值(本例中为“参数”),根本无法将其传递到调用链中。这就是为什么我需要目标方法中的“this”值。
  • 您是想在 .NET 中创建事件作为一等公民,还是尝试创建某种消息总线?
  • 我真的有两个目标。主要目标是能够捕获在 WPF 用户界面中触发的任何事件,并使用它来将值写入底层视图模型数据上下文。第二个目标是学习。
  • 你能用C#(不使用IL)编写事件处理程序的代码吗?为什么要在 Attach 方法中分配 _parameter 值?您可以将 cmets 添加到您的代码中吗?谢谢

标签: c# this cil il ilgenerator


【解决方案1】:

由于 .Net 中委托的变化方式,您可以在不使用 codegen 的情况下用 C# 编写代码:

private void InvokedMethod(object sender, EventArgs e)
{
    // whatever
}

private MethodInfo _invokedMethodInfo =
    typeof(MyEventTriggeringClass).GetMethod(
        "InvokedMethod", BindingFlags.Instance | BindingFlags.NonPublic);

private void hookupDelegate(object source, EventInfo e)
{
    Delegate invokedMethodDelegate = 
        Delegate.CreateDelegate(e.EventHandlerType, this, _invokedMethodInfo);
    e.AddEventHandler(source, invokedMethodDelegate);
}

为了解释,假设你有一些遵循标准事件模式的事件,即返回类型是void,第一个参数是object,第二个参数是EventArgs或从EventArgs派生的一些类型.如果你有这个并且InvokeMethod定义如上,你可以写someObject.theEvent += InvokedMethod。这是允许的,因为它是安全的:您知道第二个参数是可以充当EventArgs 的某种类型。

和上面的代码基本相同,除了在给定事件EventInfo时使用反射。只需创建一个引用我们方法的正确类型的委托并订阅该事件。

【讨论】:

  • 非常好的答案(投票赞成),你当然是对的:只要有问题的事件遵循默认签名(对象,EventArgs)它应该可以正常工作。老实说,我不知道不同 .NET 框架中的 all 事件是否遵循此签名,但我必须假设非 .NET 框架中的某些事件没有,所以我仍然需要挂钩动态构建的事件处理程序。
  • 我从未遇到过不遵循标准模式的事件,但当然可以创建它们。但我认为你不必在意它们,除非你真的遇到了。
  • 老实说,我认为我可以在我计划的至少 95% 的场景中通过,甚至可能 100%,但我仍然想知道如何通过以下方式调用实例成员伊利诺伊州。我只是觉得很难不遗余力,觉得我需要掌握 codegen。还是谢谢。
【解决方案2】:

如果您确定要使用 codegen 方式,可能是因为您也想支持非标准事件,您可以这样做:

当你想附加到一个事件时,创建一个类,它的方法与事件的委托类型相匹配。该类型还将有一个包含传入参数的字段。 (更接近您的设计的是一个包含对MyEventTriggeringClassthis 实例的引用的字段,但我认为这样更有意义。)该字段在构造函数中设置。

该方法将调用invokedMethod,将parameter 作为参数传递。 (这意味着invokedMethod 必须是公开的并且可以设为静态,如果您没有其他理由保持非静态。)

当我们创建完类后,创建它的一个实例,为方法创建一个委托并将其附加到事件中。

public class MyEventTriggeringClass
{
    private static readonly ConstructorInfo ObjectCtor =
        typeof(object).GetConstructor(Type.EmptyTypes);

    private static readonly MethodInfo ToBeInvoked =
        typeof(MyEventTriggeringClass)
            .GetMethod("InvokedMethod",
                       BindingFlags.Public | BindingFlags.Static);

    private readonly ModuleBuilder m_module;

    public MyEventTriggeringClass()
    {
        var assembly = AppDomain.CurrentDomain.DefineDynamicAssembly(
            new AssemblyName("dynamicAssembly"),
            AssemblyBuilderAccess.RunAndCollect);

        m_module = assembly.DefineDynamicModule("dynamicModule");
    }

    public void Attach(object source, string @event, object parameter)
    {
        var e = source.GetType().GetEvent(@event);
        if (e == null)
            return;
        var handlerType = e.EventHandlerType;

        var dynamicType = m_module.DefineType("DynamicType" + Guid.NewGuid());

        var thisField = dynamicType.DefineField(
            "parameter", typeof(object),
            FieldAttributes.Private | FieldAttributes.InitOnly);

        var ctor = dynamicType.DefineConstructor(
            MethodAttributes.Public, CallingConventions.HasThis,
            new[] { typeof(object) });

        var ctorIL = ctor.GetILGenerator();
        ctorIL.Emit(OpCodes.Ldarg_0);
        ctorIL.Emit(OpCodes.Call, ObjectCtor);
        ctorIL.Emit(OpCodes.Ldarg_0);
        ctorIL.Emit(OpCodes.Ldarg_1);
        ctorIL.Emit(OpCodes.Stfld, thisField);
        ctorIL.Emit(OpCodes.Ret);

        var dynamicMethod = dynamicType.DefineMethod(
            "Invoke", MethodAttributes.Public, typeof(void),
            GetDelegateParameterTypes(handlerType));

        var methodIL = dynamicMethod.GetILGenerator();
        methodIL.Emit(OpCodes.Ldarg_0);
        methodIL.Emit(OpCodes.Ldfld, thisField);
        methodIL.Emit(OpCodes.Call, ToBeInvoked);
        methodIL.Emit(OpCodes.Ret);

        var constructedType = dynamicType.CreateType();

        var constructedMethod = constructedType.GetMethod("Invoke");

        var instance = Activator.CreateInstance(
            constructedType, new[] { parameter });

        var sink = Delegate.CreateDelegate(
            handlerType, instance, constructedMethod);

        e.AddEventHandler(source, sink);
    }

    private static Type[] GetDelegateParameterTypes(Type handlerType)
    {
        return handlerType.GetMethod("Invoke")
                          .GetParameters()
                          .Select(p => p.ParameterType)
                          .ToArray();
    }

    public static void InvokedMethod(object parameter)
    {
        Console.WriteLine("Value of parameter = " + parameter ?? "(null)");
    }
}

不过,这仍然不能处理所有可能的事件。那是因为事件的委托可以有一个返回类型。这意味着为生成的方法提供返回类型并从中返回一些值(可能是default(T))。

有(至少)一种可能的优化:不要每次都创建新类型,而是缓存它们。当您尝试附加到与前一个具有相同签名的事件时,请使用其类。

【讨论】:

  • 作为另一个答案发布,因为这个解决方案与之前的完全不同。
  • svick:抱歉,我们好像发过帖了。我刚才在回答我自己的问题时错过了你的回复。问题是我在没有意识到的情况下创建了一个静态委托。如果您只是传递动态创建的委托的目标,一切正常。动态方法的第一个参数(由 Ldarg_0 加载)然后是“this”值。如果您有兴趣,请查看我自己的答案。感谢您的帮助。
【解决方案3】:

我将继续在这里回答我自己的问题。一旦我意识到真正的问题是什么,解决方案就非常简单:指定事件处理程序的实例/目标。这是通过向 MethodInfo.CreateDelegate() 添加一个参数来完成的。

如果您有兴趣,这里有一个简单的示例,您可以将'n'paste 剪切到控制台应用程序中并尝试一下:

class Program
{
    static void Main(string[] args)
    {
        var test = new MyEventTriggeringClass();
        var eventSource = new EventSource();
        test.Attach(eventSource, "SomeEvent", "Hello World!");
        eventSource.RaiseSomeEvent();
        Console.ReadLine();
    }
}

class MyEventTriggeringClass
{
    private object _parameter;

    public void Attach(object eventSource, string eventName, object parameter)
    {
        _parameter = parameter;
        var sink = new DynamicMethod(
            "sink",
            null,
            new[] { typeof(object), typeof(object), typeof(EventArgs) },
            typeof(Program).Module);

        var eventInfo = typeof(EventSource).GetEvent("SomeEvent");

        var ilGenerator = sink.GetILGenerator();
        var targetMethod = GetType().GetMethod("TargetMethod", BindingFlags.Instance | BindingFlags.Public, null, new Type[0], null);
        ilGenerator.Emit(OpCodes.Ldarg_0); // <-- loads 'this' (when sink is not static)
        ilGenerator.Emit(OpCodes.Call, targetMethod);
        ilGenerator.Emit(OpCodes.Ret);

        // SOLUTION: pass 'this' as the delegate target...
        var handler = (EventHandler)sink.CreateDelegate(eventInfo.EventHandlerType, this);
        eventInfo.AddEventHandler(eventSource, handler);
    }

    public void TargetMethod()
    {
        Console.WriteLine("Value of _parameter = " + _parameter);
    }
}

class EventSource
{
    public event EventHandler SomeEvent;

    public void RaiseSomeEvent()
    {
        if (SomeEvent != null)
            SomeEvent(this, new EventArgs());
    }
}

所以,感谢您的 cmets 和帮助。希望有人学到了一些东西。我知道我做到了。

干杯

【讨论】:

  • 您(间接地)使用这样一个事实,即在为这样的静态方法创建委托时,您可以将其视为第一个参数是“实例”的实例方法。很有趣。
【解决方案4】:

这是我自己的版本/满足我自己的需要:

    /// <summary>
    /// Corresponds to 
    ///     control.Click += new EventHandler(method);
    /// Only done dynamically, and event arguments are omitted.
    /// </summary>
    /// <param name="objWithEvent">Where event resides</param>
    /// <param name="objWhereToRoute">To which object to perform execution to</param>
    /// <param name="methodName">Method name which to call. 
    ///  methodName must not take any parameter in and must not return any parameter. (.net 4.6 is strictly checking this)</param>
    private static void ConnectClickEvent( object objWithEvent, object objWhereToRoute, string methodName )
    {
        EventInfo eventInfo = null;

        foreach (var eventName in new String[] { "Click" /*WinForms notation*/, "ItemClick" /*DevExpress notation*/ })
        {
            eventInfo = objWithEvent.GetType().GetEvent(eventName);
            if( eventInfo != null )
                break;
        }

        Type objWhereToRouteObjType = objWhereToRoute.GetType();
        var method = eventInfo.EventHandlerType.GetMethod("Invoke");
        List<Type> types = method.GetParameters().Select(param => param.ParameterType).ToList();
        types.Insert(0, objWhereToRouteObjType);

        var methodInfo = objWhereToRouteObjType.GetMethod(methodName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, new Type[0], null);
        if( methodInfo.ReturnType != typeof(void) )
            throw new Exception("Internal error: methodName must not take any parameter in and must not return any parameter");

        var dynamicMethod = new DynamicMethod(eventInfo.EventHandlerType.Name, null, types.ToArray(), objWhereToRouteObjType);

        ILGenerator ilGenerator = dynamicMethod.GetILGenerator(256);
        ilGenerator.Emit(OpCodes.Ldarg_0);
        ilGenerator.EmitCall(OpCodes.Call, methodInfo, null);
        ilGenerator.Emit(OpCodes.Ret);

        var methodDelegate = dynamicMethod.CreateDelegate(eventInfo.EventHandlerType, objWhereToRoute);
        eventInfo.AddEventHandler(objWithEvent, methodDelegate);
    } //ConnectClickEvent

【讨论】:

    猜你喜欢
    • 2014-10-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-05-11
    • 1970-01-01
    相关资源
    最近更新 更多