Перенаправление на динамический метод из универсального обработчика событий - PullRequest
1 голос
/ 25 октября 2011

Я пытаюсь написать класс, который будет использоваться для запуска вызова метода из произвольного события, но я застрял, потому что просто не могу найти способ ссылаться на 'this' из испущенного кода MSIL.

Этот пример должен описывать то, что я ищу:

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

(Обратите внимание, что приведенный выше пример довольно бессмысленный. Я просто пытаюсь описать то, что я ищу. Моя конечная цель здесь - иметь возможность инициировать вызов произвольного метода всякий раз, когда происходит произвольное событие. I ' Я буду использовать это в проекте WPF, где я пытаюсь использовать 100% MVVM, но я наткнулся на одну из [казалось бы] классических точек разрыва.)

В любом случае, код «работает» настолько, насколько он успешно вызвал «invokedMethod», когда происходит произвольное событие, но «this» кажется пустым объектом (_parameter всегда равен нулю). Я провел некоторое исследование, но просто не могу найти хороших примеров, когда «this» правильно передается методу, вызываемому из динамического метода, подобного этому.

Самый близкий пример, который я нашел, это ЭТА СТАТЬЯ , но в этом примере 'this' может быть принудительно применен к динамическому методу, так как он вызывается из кода, а не из произвольного обработчика событий.

Любые предложения или советы будут очень признательны.

Ответы [ 4 ]

2 голосов
/ 25 октября 2011

Так как в .Net работает дисперсия делегатов, вы можете написать код на C # без использования codegen:

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,Просто создайте делегат правильного типа, который ссылается на наш метод, и подпишитесь на событие.

1 голос
/ 26 октября 2011

Если вы уверены, что хотите пойти по пути codegen, возможно, потому что вы хотите поддерживать нестандартные события, вы можете сделать это следующим образом:

Когда вы хотите присоединиться к событию, создайте класс, у которого есть метод, соответствующий типу делегата события. Тип также будет иметь поле, содержащее переданный параметр. (Ближе к вашему дизайну будет поле, которое содержит ссылку на this экземпляр MyEventTriggeringClass, но я думаю, что в этом есть смысл.) Это поле устанавливается в конструкторе.

Метод вызовет 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)) из него.

Существует (по крайней мере) одна возможная оптимизация: не создавайте новый тип каждый раз, а кешируйте их. Когда вы пытаетесь присоединиться к событию с той же подписью, что и предыдущая, используйте use его класс.

0 голосов
/ 14 января 2016

Вот моя собственная версия / для моих нужд:

    /// <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
0 голосов
/ 26 октября 2011

Я собираюсь ответить на свой вопрос здесь. Решение было очень простым, когда я понял, в чем реальная проблема: указать экземпляр / цель обработчика событий. Это делается путем добавления аргумента в MethodInfo.CreateDelegate ().

Если вам интересно, вот простой пример, который вы можете вырезать и вставить в консольное приложение и попробовать:

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

Итак, спасибо за ваши комментарии и помощь. Надеюсь, кто-то чему-то научился. Я знаю, что сделал.

Приветствия

...