Перенаправление на динамический метод из универсального обработчика событий
Я пытаюсь написать класс, который будет использоваться для запуска вызова метода из произвольного события, но я застрял, поскольку просто не могу найти способ ссылаться на "это" из испущенного кода 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);
(Обратите внимание, что приведенный выше пример совершенно бессмыслен. Я просто пытаюсь описать то, что я ищу. Моя конечная цель здесь - иметь возможность инициировать вызов произвольного метода всякий раз, когда происходит произвольное событие. Я буду использовать это в проекте WPF, где я пытаюсь использовать 100% MVVM, но я наткнулся на одну из (казалось бы) классических критических точек.)
В любом случае, код "работает" настолько, насколько он успешно вызвал "invokedMethod", когда происходит произвольное событие, но "this" кажется пустым объектом (_parameter всегда равен нулю). Я провел некоторые исследования, но просто не могу найти хороших примеров, когда "this" правильно передается методу, вызываемому из динамического метода, подобного этому.
Самый близкий пример, который я нашел, это ЭТА СТАТЬЯ, но в этом примере "this" можно принудительно применить к динамическому методу, так как он вызывается из кода, а не из произвольного обработчика событий.
Любые предложения или советы будут очень признательны.
4 ответа
Я собираюсь ответить на свой вопрос здесь. Решение было очень простым, когда я понял, в чем реальная проблема: указать экземпляр / цель обработчика событий. Это делается путем добавления аргумента в 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());
}
}
Итак, спасибо за ваши комментарии и помощь. Надеюсь, кто-то чему-то научился. Я знаю, что сделал.
ура
Из-за того, как дисперсия делегатов работает в.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
, Просто создайте делегат правильного типа, который ссылается на наш метод, и подпишитесь на событие.
Если вы уверены, что хотите пойти по пути 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 его класс.
Вот моя собственная версия / для моих собственных нужд:
/// <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