I want to subscribe to any event existing on a given object, where I don't know the type in advance. I'm generatig event handler at run-time using System.Linq.Expressions.Expression.
The code I have (see below) almost works... But there are unfortunately some events that don't follow the sender, EventArgs pattern, and I have no control over it. Some of those have ref parameters, and trying to write them back (which I unfortunately need to do) causes System.ArgumentException: 'Cannot bind to the target method because its signature or security transparency is not compatible with that of the delegate type.' at lambda.Compile(). An alternative would be to rewrite it to emit IL, but I'd really rather avoid it.
The event:
public delegate void SomeEventDelegate(ref int a, int b);
public event SomeEventDelegate SomeEvent;
Dynamically generated lambda:
(ref int a, int b) =>
{
var __values__ = new[] { a, b };
Action<string, string[], object[]>.Invoke("SomeEvent", new[] { "a", "b" }, __values__);
a = (int)__values__[0]; //this causes an exception in `lambda.Compile()`
}
Full example:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using AgileObjects.ReadableExpressions; //optional, converts generated expression to more readable string
class PseudoControl
{
public delegate void SomeEventDelegate(ref int a, int b);
public event SomeEventDelegate SomeEvent;
public void RaiseEvents()
{
var a = 0;
var b = 0;
SomeEvent?.Invoke(ref a, b);
}
}
public class ControlExtender
{
public object ExtendedControl { get; set; }
public void SubscribeToAllEvents()
{
foreach (var ei in ExtendedControl.GetType().GetEvents(BindingFlags.FlattenHierarchy | BindingFlags.Instance | BindingFlags.Public))
{
SubscribeToEvent(ei);
}
}
private void SubscribeToEvent(EventInfo ei)
{
var handlerType = ei.EventHandlerType;
var eventParams = handlerType.GetMethod("Invoke").GetParameters();
//lambda: (p1, p2, ...) => RaiseObjectEvent(ei.Name, new[] { pname1, pname2, ... }, new[] { pval1, pval2, ...});
var parameters = eventParams
.Select(p => Expression.Parameter(p.ParameterType, p.Name)).ToArray();
var action = new Action<string, string[], object[]>(RaiseObjectEvent);
var constEventName = Expression.Constant(ei.Name);
var arrPNames = Expression.NewArrayInit(typeof(string), parameters.Select(p => Expression.Constant(p.Name)));
var arrPValues = Expression.NewArrayInit(typeof(object), parameters.Select(p => Expression.Convert(p, typeof(object))));
var statements = new List<Expression>();
var varPValues = Expression.Variable(typeof(object[]), "__values__");
statements.Add(Expression.Assign(varPValues, arrPValues));
statements.Add(Expression.Call(Expression.Constant(action), action.GetType().GetMethod("Invoke"), constEventName, arrPNames, varPValues));
//handle `ref` parameters being written into
for (int i = 0; i < parameters.Length; i++)
{
var p = parameters[i];
if (p.IsByRef)
{
var value = Expression.ArrayAccess(varPValues, Expression.Constant(i));
var unbox = Expression.Convert(value, p.Type);
var assign = Expression.Assign(p, unbox); //<------------- causes the error
statements.Add(assign);
}
}
var body = Expression.Block(new[] { varPValues }, statements);
var lambda = Expression.Lambda(body, parameters);
//string readable = lambda.ToReadableString(); //debug help, needs the nuget package
var del = Delegate.CreateDelegate(handlerType, lambda.Compile(), "Invoke", false);
ei.AddEventHandler(ExtendedControl, del);
}
public void RaiseObjectEvent(string name, string[] pNames, object[] pValues)
{
Console.WriteLine("RaiseObjectEvent: " + name);
for (int i = 0; i < pNames.Length; i++)
{
var pname = pNames[i];
var pval = pValues[i];
Console.WriteLine($" {pname}: {pval}");
}
if (name == "SomeEvent")
{
pValues[0] = 1;
pValues[1] = 2;
}
}
}
static class Test
{
static void Main()
{
var ce = new ControlExtender();
var control = new PseudoControl();
ce.ExtendedControl = control;
ce.SubscribeToAllEvents();
//control.SomeEvent += (ref int a, int b) => { a = 1; b = 2; };
control.RaiseEvents();
}
}
refat all