Search code examples
c#lambdaexpression-trees

How to call method with EventHandler parameter using expression trees?


Consider this simple piece of code. How can this be done using expression trees?

ErrorsChangedEventManager.AddHandler(obj, obj.SomeHandler);

Here's a small sample illustrating what I'm trying to accomplish. (Add a reference to WindowBase to make it compile.)

class Program : INotifyDataErrorInfo
{
    public int Id { get; set; }
    static void Main(string[] args)
    {
        var p1 = new Program { Id = 1 };
        var p2 = new Program { Id = 2 };

        // Here is the root of the problem.
        // I need to do this INSIDE the expression from a given instance of Program.
        EventHandler<DataErrorsChangedEventArgs> handler = p1.OnError;
        var handlerConstant = Expression.Constant(handler);

        var mi = typeof(ErrorsChangedEventManager).GetMethod(nameof(ErrorsChangedEventManager.AddHandler), 
            BindingFlags.Public | BindingFlags.Static);

        var source = Expression.Parameter(typeof(INotifyDataErrorInfo), "source");
        var program = Expression.Parameter(typeof(Program), "program");

        // This will work, but the OnError method will be invoked on the wrong instance.
        // So, I need to get the expression to perform what would otherwise be easy in code...
        // E.g. AddHandler(someObject, p2.OnError);
        var call = Expression.Call(mi, source, handlerConstant);

        var expr = Expression.Lambda<Action<INotifyDataErrorInfo, Program>>(call, source, program);
        var action = expr.Compile();
        action.DynamicInvoke(p1, p2);

        p1.ErrorsChanged.Invoke(p1, new DataErrorsChangedEventArgs("Foo"));
    }

    void OnError(object sender, DataErrorsChangedEventArgs e)
    {
        if (sender is Program p)
        {
            Console.WriteLine($"OnError called for Id={Id}. Expected Id=2");
        }
    }

    public IEnumerable GetErrors(string propertyName) => Enumerable.Empty<string>();
    public bool HasErrors => false;
    public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
}

Obviously, it doesn't work. I somehow need to provide the OnError handler as a parameter to the call.


Solution

  • It seems the easiest thing to do is to create a lambda which creates the EventHandler<DataErrorsChangedEventArgs> for you, and then use Expression.Invoke to call it:

    public class Program : INotifyDataErrorInfo
    {
        public int Id { get; set; }
    
        public static void Main()
        {
            var p1 = new Program { Id = 1 };
            var p2 = new Program { Id = 2 };
    
            var mi = typeof(ErrorsChangedEventManager).GetMethod(nameof(ErrorsChangedEventManager.AddHandler),
                BindingFlags.Public | BindingFlags.Static);
    
            var source = Expression.Parameter(typeof(INotifyDataErrorInfo), "source");
            var program = Expression.Parameter(typeof(Program), "program");
    
            Expression<Func<Program, EventHandler<DataErrorsChangedEventArgs>>> createDelegate = p => p.OnError;
            var createDelegateInvoke = Expression.Invoke(createDelegate, program);
    
            var call = Expression.Call(mi, source, createDelegateInvoke);
            var expr = Expression.Lambda<Action<INotifyDataErrorInfo, Program>>(call, source, program);
            var action = expr.Compile();
    
            action(p1, p2);
    
            p1.ErrorsChanged.Invoke(p1, new DataErrorsChangedEventArgs("Foo"));
        }
    
        public void OnError(object sender, DataErrorsChangedEventArgs e)
        {
            if (sender is Program p)
            {
                Console.WriteLine($"OnError called for Id={Id}. Expected Id=2");
            }
        }
    
        public IEnumerable GetErrors(string propertyName) => Enumerable.Empty<string>();
        public bool HasErrors => false;
        public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
    }
    

    If you look at the DebugView for createDelegate, you can see that the compiler created:

    .Lambda #Lambda1<System.Func`2[Program,System.EventHandler`1[System.ComponentModel.DataErrorsChangedEventArgs]]>(Program $p)
    {
        (System.EventHandler`1[System.ComponentModel.DataErrorsChangedEventArgs]).Call .Constant<System.Reflection.MethodInfo>(Void OnError(System.Object, System.ComponentModel.DataErrorsChangedEventArgs)).CreateDelegate(
            .Constant<System.Type>(System.EventHandler`1[System.ComponentModel.DataErrorsChangedEventArgs]),
            $p)
    }
    

    You could construct this expression yourself if you wanted, by getting the MethodInfo for OnError, then calling CreateDelegate on it.


    All of that said, you can just use a lambda to do all of that:

    Expression<Action<INotifyDataErrorInfo, Program>> test = (source, program) =>
        ErrorsChangedEventManager.AddHandler(source, program.OnError);