8

It seems like I should have everything that I need here, but the particulars of making it happen are driving me crazy.

I have a static utility method which takes a web service client object, extracts a designated EventInfo from it, and is supposed to add some handlers to that event, essentially callbacks for when the web service invocation completes. The problem is, as any given event essentially has its own handler overload (the WCF generated code provides a unique SomeMethodCompletedEventArgs for each method and corresponding event), I cannot figure out how to make this happen.

I have two handlers I want to attach, and the first one is just a lambda function:

(obj, args) => task.Complete()

So what I'd like to do is just this simple:

eventInfo.AddEventHandler(client, new EventHandler((obj, args) => task.Complete()));

This, however, generates a runtime InvalidCastException, because the eventInfo is expecting an EventHandler<SomeMethodCompletedEventArgs>, not a plain EventHandler. I believe this means that I need to dynamically create the EventHandler delegate using eventInfo.EventHandlerType somehow, but I have not figured out to combine that with the lambda function, or otherwise make a receiver that really does not care what particular flavor of EventArgs is being used.

The only workaround that I've found is to create a generic template argument with which to pass in the particular event arguments type. This enables me to go:

eventInfo.AddEventHandler(client, new EventHandler<E>(...));

Where E is the parameter in question. This is obviously clunky however, and it just seems wrong to have to pass this in when the extracted eventInfo should tell us all we need to know.

It is worth noting that I am using a slightly constrained PCL Framework for Xamarin, which apparently does not include the static Delegate.CreateDelegate() method that I've seen mentioned in related problems. I do have access to Activator, though, which should cover most of the same bases.

Peter Duniho
  • 68,759
  • 7
  • 102
  • 136
Metameta
  • 93
  • 5
  • Note that it's generally not recommended to attach lambdas to event handlers, as they can only be removed by a rather complicated process of reflection. – Oblivious Sage Apr 15 '15 at 16:33
  • 1
    @ObliviousSage: _"they can only be removed by a rather complicated process of reflection"_ -- that's incorrect. First, in many cases there is never a need to remove the event handler. Second, in scenarios where one needs to, all that's needed is to retain the delegate instance reference in a variable somewhere, and use that value to unsubscribe the handler. No reflection needed, and in many cases this variable can simply be a captured local variable. – Peter Duniho Apr 15 '15 at 17:53
  • @PeterDuniho Yes, event handlers often don't need to be removed; that's not the same as saying you should assume it won't need to be removed. You are correct that a delegate can be removed if you've saved a reference to it, but a pure lambda (for example, `myObject.SomeEvent += (a, b) => Foo();`) cannot be removed without the previously mentioned reflection. That the disadvantages of using a lambda as an event handler can be mitigated doesn't mean those disadvantages don't exist, nor does it mean that using lambdas as event handlers is a good habit to get into. – Oblivious Sage Apr 15 '15 at 18:00
  • 1
    @ObliviousSage: _"but a pure lambda cannot be removed without the previously mentioned reflection"_ -- I guess you'll need to define "pure lambda" then. Because in your example, all you need to do is assign the lambda to a variable first, and then subscribe the variable to the event. That in no way suggests that using lambdas for event handlers is in some way harmful or should be avoided. _"nor does it mean that using lambdas as event handlers is a good habit to get into"_ -- it's not a habit to avoided though. It works well in a broad range of scenarios. – Peter Duniho Apr 15 '15 at 18:05
  • @ObliviousSage: That is a fair point. As it stands, provided I can get this issue resolved in the first place, it should be pretty trivial to use the delegate instance for cleanup, as Peter noted. – Metameta Apr 15 '15 at 22:55

2 Answers2

6

In the example you've provided, you should be able to just remove the explicit delegate constructor:

eventInfo.AddEventHandler(client, (obj, args) => task.Complete());

By letting C# infer the delegate type for you, it should create exactly the correct delegate type needed for the parameter.

If that does not address your specific concern, please provide a good, minimal, complete code example that reliably reproduces the problem, along with a clear, precise explanation of why the above approach doesn't help.


As an aside, it's hard to tell from what little code you posted, but it is unusual to have an explicit AddEventHandler() method. Normally, a class would simply expose an event, and you would use the += syntax to subscribe an event handler.


EDIT:

From your comments, I understand that you are required by the API to comply with a dynamically provided event signature. Personally, I think this kind of design is goofy, but I assume you are stuck with it, presumably due to the design of the Xamarin framework.

Taking the stated goal strictly — that is, given an EventHandler instance, produce a new delegate instance, the type of which matches a run-time-provided Type instance — the following method should work for you:

static Delegate CreateDelegate(Type type, EventHandler handler)
{
    return (Delegate)type.GetConstructor(new [] { typeof(object), typeof(IntPtr) })
        .Invoke(new [] { handler.Target, handler.Method.MethodHandle.GetFunctionPointer() });
}

Example usage:

eventInfo.AddEventHandler(client,
    CreateDelegate(eventInfo.EventHandlerType, (obj, args) => task.Complete());

You could write the above as an extension method to simplify invocation (you didn't say what type client is, so I just made it object for the example):

public static void AddEventHandler(this EventInfo eventInfo, object client, EventHandler handler)
{
        object eventInfoHandler = eventInfo.EventHandlerType
            .GetConstructor(new[] { typeof(object), typeof(IntPtr) })
            .Invoke(new[] { handler.Target, handler.Method.MethodHandle.GetFunctionPointer() });

        eventInfo.AddEventHandler(client, (Delegate)eventInfoHandler);
}

Example usage:

eventInfo.AddEventHandler(client, (obj, args) => task.Complete());

Give the extension method a different name if you are worried that at some point the API's own AddEventHandler() method could change in a way that causes the C# compiler to select its implementation instead of the extension method, or of course if it does so today (the above will work assuming just a single AddEventHandler() method overload with the second parameter as Delegate, but again…lacking a good, minimal, complete code example I cannot guarantee that it will work in your own code; using a unique name would guarantee that it will, at the cost of exposing a bit of the "magic" :) ).


Finally, having identified a working solution, I was then able to search Stack Overflow for similar questions and found this one, of which you could argue your own is a duplicate: Using reflection to specify the type of a delegate (to attach to an event)?

I decided to go ahead and edit my own answer here rather than just proposing to close your question, because the answer to the other question doesn't really provide what I think is as elegant or easy-to-use an example.

Community
  • 1
  • 1
Peter Duniho
  • 68,759
  • 7
  • 102
  • 136
  • The line you offered would ideal, but it generates a compiler exception: "Cannot convert lambda expression to 'System.Delegate' because it is not a delegate type. Normally, I would use the `+=` syntax, however this particular situation precludes it. WCF generates a unique `event` for each async call that can be made, but what I am trying to create is a generic utility to both invoke the method, and attach callbacks upon completion. I'll look into getting a reproducing code sample. – Metameta Apr 15 '15 at 22:30
  • @Metameta: That means that the `AddEventHandler()` method uses `Delegate` as the parameter type, rather than whatever specific delegate type it is eventually trying to cast the parameter to. So the question is: what makes you believe that given an `eventInfo` object, you (never mind the compiler) can tell what type of delegate is expected)? Please refer to my request that you provide **a good, _minimal_, _complete_ code example**. Only with that is it possible to truly understand the question and provide an answer that will work for you. – Peter Duniho Apr 15 '15 at 22:33
  • The type of delegate that should be used is actually known via EventInfo, which has an EventHandlerType property containing the Type that the delegate should conform to at run-time. All I'd like to do is create code to respond to the event, as I'm not even concerned with the event arguments; this should mean I just want to create a handler of the offered type, with my own code. – Metameta Apr 15 '15 at 22:52
  • A sample of things as they stand can be found here: https://www.dropbox.com/s/h1gixyqkw4musmb/EventHandlerProject.zip?dl=0. This exhibits the exception in question, though why it is an issue is quite straightforward, as you say; there is a mismatch between the Delegate I'm passing and the type it eventually expects. What is critically missing is the means to use that EventHandlerType to provide the correct type of Delegate. – Metameta Apr 15 '15 at 22:52
  • @Metameta: it's an unfortunate and weird API design you are working with. However, please see my edit above for an alternate approach that should work in your scenario. (By the way, please do not use links to external references to support the question; the question should be entirely self-contained, to ensure it remains useful in the future even if external references become out-of-date or disappear altogether). – Peter Duniho Apr 16 '15 at 00:06
  • I want to thank you for the valiant effort; I had actually found the question you'd linked in my research, though I agree that your implementation is much more elegant. The main difference between my situation and the one described (which I should probably edit in to my question) is that within Xamarin/Mono, I do not have access to MethodInfo.MethodHandle, and its associated function pointer. – Metameta Apr 20 '15 at 23:45
1

It turns out this is not so difficult, but does require some amount of code with a little bit of reflection.

The basic idea is to wrap the handler in a generic class parameterized by the event type

HandlerFor<T> : IDisposable where T : EventArgs. 

This class then has a private member function matching the required event handler signature:

void Handle(object sender, T eventArgs) 

that it will register on construction, unregister on disposal and that will invoke the given Action supplied to it in its constructor whenever the event occurs.

To hide the implementation details, and expose only an IDisposable as a handle for controlled event handler life cycle scope and unregistration, I wrapped this in an EventHandlerRegistry class. This will also allow adding improvements in future if required, such as building a factory delegate instead of repeatedly calling Activator.CreateInstance() to build the HandleFor instances.

It looks like this:

public class EventHandlerRegistry : IDisposable
{
    private ConcurrentDictionary<Type, List<IDisposable>> _registrations;

    public EventHandlerRegistry()
    {
        _registrations = new ConcurrentDictionary<Type, List<IDisposable>>();
    }

    public void RegisterHandlerFor(object target, EventInfo evtInfo, Action eventHandler)
    {
        var evtType = evtInfo.EventHandlerType.GetGenericArguments()[0];
        _registrations.AddOrUpdate(
            evtType,
            (t) => new List<IDisposable>() { ConstructHandler(target, evtType, evtInfo, eventHandler) },
            (t, l) => { l.Add(ConstructHandler(target, evtType, evtInfo, eventHandler)); return l; });
    }

    public IDisposable CreateUnregisteredHandlerFor(object target, EventInfo evtInfo, Action eventHandler)
    {
        var evtType = evtInfo.EventHandlerType.GetGenericArguments()[0];
        return ConstructHandler(target, evtType, evtInfo, eventHandler);
    }

    public void Dispose()
    {
        var regs = Interlocked.Exchange(ref _registrations, null);
        if (regs != null)
        {
            foreach (var reg in regs.SelectMany(r => r.Value))
                reg.Dispose();
        }
    }

    private IDisposable ConstructHandler(object target, Type evtType, EventInfo evtInfo, Action eventHandler)
    {
        var handlerType = typeof(HandlerFor<>).MakeGenericType(evtType);
        return Activator.CreateInstance(handlerType, target, evtInfo, eventHandler) as IDisposable;
    }

    private class HandlerFor<T> : IDisposable where T : EventArgs
    {
        private readonly Action _eventAction;
        private readonly EventInfo _evtInfo;
        private readonly object _target;
        private EventHandler<T> _registeredHandler;

        public HandlerFor(object target, EventInfo evtInfo, Action eventAction)
        {
            _eventAction = eventAction;
            _evtInfo = evtInfo;
            _target = target;
            _registeredHandler = new EventHandler<T>(this.Handle);
            _evtInfo.AddEventHandler(target, _registeredHandler);
        }

        public void Unregister()
        {
            var registered = Interlocked.Exchange(ref _registeredHandler, null);
            if (registered != null)
                // Unregistration is awkward: 
                // doing `RemoveEventHandler(_target, registered);` won't work.
                _evtInfo.RemoveEventHandler(_target, new EventHandler<T>(this.Handle));
        }

        private void Handle(object sender, T EventArgs)
        {
            if (_eventAction != null)
                _eventAction();
        }

        public void Dispose()
        {
            Unregister();
        }
    }
}

It supports clean adding and removing of event handlers in a pretty transparent manner. Note: this does not yet implement IDisposable in the recommended manner. You will have to add finalizers and Dispose(bool isFinalizing) yourself.

This shows an example of its usage:

public class MyArgs1 : EventArgs
{
    public string Value1;
}

public class MyEventSource
{
    public event EventHandler<MyArgs1> Args1Event;

    public EventInfo GetEventInfo()
    {
        return this.GetType().GetEvent("Args1Event");
    }

    public void FireOne()
    {
        if (Args1Event != null)
            Args1Event(this, new MyArgs1() { Value1 = "Bla " });
    }
}

class Program
{
    public static void Main(params string[] args)
    {
        var myEventSource = new MyEventSource();
        using (var handlerRegistry = new EventHandlerRegistry())
        {
            handlerRegistry.RegisterHandlerFor(
                myEventSource,
                myEventSource.GetEventInfo(),
                () => Console.WriteLine("Yo there's some kinda event goin on"));
            handlerRegistry.RegisterHandlerFor(
                myEventSource,
                myEventSource.GetEventInfo(),
                () => Console.WriteLine("Yeah dawg let's check it out"));

            myEventSource.FireOne();
        }
        myEventSource.FireOne();
    }
}

When run, it will give the below output:

output

Alex
  • 13,024
  • 33
  • 62