Dynamic event subscription with ref / out parameter - c#

I have been using an expression tree to create a delegate and subscribe to any event with an Action<Object[]>, where all the event parameters are converted to an array of objects.
This has been working fine until now, that I need to subscribe to an event with an out/ref parameter and I need to set the value of this parameter.
Is there a way to use something similar to the expression tree and subscribe to any event, but still be able to set/return values to the object that raised the event?
Expression tree to create a delegate:
public static Delegate CreateProxyWithDynamicParameters(this EventInfo EventInfo, Action<object[]> Action)
{
var EventHandlerType = EventInfo.EventHandlerType;
var InvokeMethodInfo = EventHandlerType.GetMethod("Invoke");
var Parameters = InvokeMethodInfo.GetParameters().Select(Parameter => Expression.Parameter(Parameter.ParameterType, Parameter.Name)).ToArray();
var ConvertedParameters = Parameters.Select(Parameter => Expression.Convert(Parameter, typeof(object))).Cast<Expression>().ToArray();
var NewArrayInit = Expression.NewArrayInit(typeof(object), ConvertedParameters);
var Body = Expression.Call(Expression.Constant(Action), "Invoke", null, NewArrayInit);
var lambdaExpression = Expression.Lambda(Body, Parameters);
return Delegate.CreateDelegate(EventInfo.EventHandlerType, lambdaExpression.Compile(), InvokeMethodInfo.Name, ignoreCase: false);
}
Subscription with the delegate:
protected static void AddEvent<TMessage>(EventInfo eventInfo) where TMessage : EventArgs
{
void EventAction(object[] e)
{
// Run some event code.
}
var #delegate = eventInfo.CreateProxyWithDynamicParameters(EventAction);
DelegateDictionary[typeof(TMessage)] = #delegate;
eventInfo.AddEventHandler(x.Target, #delegate);
}
Edit:
Here is a vanilla simple. Where I need to set the HandlingCode parameter.
InventorApplication.ApplicationEvents.OnSaveDocument += ApplicationEvents_OnSaveDocument;
private void ApplicationEvents_OnSaveDocument(_Document DocumentObject, EventTimingEnum BeforeOrAfter, NameValueMap Context, out HandlingCodeEnum HandlingCode)
{
HandlingCode = HandlingCodeEnum.kEventHandled;
}

You are going to struggle to do this with Expression, because Expression likes to be able to run things via reflection for fallback, and it can't handle refs via reflection. You might be able to do it via raw IL, but... that's getting messier and messier.
Frankly, this event API is making life hard for you. If you control that API, I strongly suggest using the more idiomatic (object sender, SomeEventArgs args) where SomeEventArgs : EventArgs. Your SomeEventArgs would then have properties for all the things you're trying to pass as parameters, and the interested code can just do:
args.HandlingCode = ...
to assign a value.

Related

Attach event handler to be called only once

I am currently trying to write an extension function to be able to easily attach an action that is only used once when the event is fired, then unsubscribed.
I am trying something like this:
public static void AttachOnce<TEventArgs>([NotNull] this EventHandler<TEventArgs> me, [NotNull] Action<object, TEventArgs> action)
where TEventArgs : System.EventArgs
{
var handler = me;
EventHandler<TEventArgs> wrappedAction = null;
wrappedAction = (sender, args) =>
{
action(sender, args);
handler -= wrappedAction;
};
handler += wrappedAction;
}
But ReSharper complains on the unsubscribe that handler is "Access to modified closure".
I know what this means, so I made the local variable for the closure already, but it doesn't seem to resolve it. What is failing here?
The direct hard-coded code works. Something like this:
var file = new FileSystemWatcher("path/to/file");
FileSystemEventHandler handler = null;
handler = (sender, args) =>
{
// My action code here
file.Changed -= handler;
};
file.Changed += handler;
EDIT 1 (2018-10-09 11:43 CET):
I may just have been too fast, asking a question before thoroughly thinking it through.
You can't create extension methods on Events. At all. It's just not possible in C#. So I can't even test why ReSharper is complaining and if it is right, because a call like file.Changed.AttachOnce(action) is not valid. It says "The event 'Changed' can only appear on the left hand side of += or -=".
I have found some more sources for similar requests/questions:
http://www.hardkoded.com/blog/csharp-wishlist-extension-for-events
One time generic event call?
I've been thinking about a different but much simpler approach, using a "self-detaching" inline handler which would be used like this:
obj.Event += (s, e) =>
{
Detach(obj, nameof(obj.Event));
// ..do stuff..
};
The Detach method would look like this and could be put anywhere you like (most likely a static helper class):
public static void Detach(object obj, string eventName)
{
var caller = new StackTrace().GetFrame(1).GetMethod();
var type = obj.GetType();
foreach (var field in type.GetFields(BindingFlags.Instance | BindingFlags.NonPublic))
{
if (typeof(Delegate).IsAssignableFrom(field.FieldType))
{
var handler = (field.GetValue(obj) as Delegate)?.GetInvocationList().FirstOrDefault(m => m.Method.Equals(caller));
if (handler != null)
{
type.GetEvent(eventName).RemoveEventHandler(obj, handler);
return;
}
}
}
}
So for your example the code would look like this:
file.Changed += (s, e) =>
{
Detach(file, nameof(file.Changed));
// My action code here
};
In this case, it's okay.
ReSharper simply warns that handler is different between the time you declare and the time it is executed.
Not sure how exactly you want to design your extension method, but maybe this will get you started:
public static void OnChangedOnce(this FileSystemWatcher instance, Action<object, FileSystemEventArgs> action)
{
var file = instance;
var wrappedAction = action;
FileSystemEventHandler handler = null;
handler = (object sender, FileSystemEventArgs args) =>
{
wrappedAction(sender, args);
file.Changed -= handler;
};
file.Changed += handler;
file.EnableRaisingEvents = true; // mandatory
}
var file = new FileSystemWatcher("path/to/file");
file.OnChangedOnce((sender, args)) =>
{
// your action here
});

Creating a proxy delegate using reflection for any event

I am trying to write a library that allows the attaching of events using attributes, and I want to create a set of convenience method signatures.
For example, a button will have a Click event with a handler signature of void(object, EventArgs).
I have already mapped the methods that match this signature directly:
// the object that raises the event
Object eventSource = ...;
EventInfo evnt = ...;
// the object with the target method
Object target = ...;
MethodInfo method = ...;
// create and attach delegate
var del Delegate.CreateDelegate(evnt.EventHandlerType, target, method);
evnt.AddEventHandler(eventSource, del);
This works well as long as the methods are the same/compatible, even when detaching an event:
evnt.RemoveEventHandler(eventSource, del);
However, I would like to also be able to map parameterless methods.
Is it possible to create a delegate that will accept any arguments, but then ignore them, and invoke a desired method on an object?
For an example, in the working bit, I can do:
// the method
void MyClickMethod(object sender, EventArgs e)
{
}
// the execution
var evnt = myButton.GetType().GetEvent("Click");
var method = this.GetType().GetMethod("MyClickMethod")
AttachEvent(/*eventSource*/ myButton, /*evnt*/ evnt, /*target*/ this, /*method*/ method);
But, I would also like to be able to attach this method:
void MyClickMethod()
{
}
My logic here is that we can attach any parameterless method on any type to any event. This is often very helpful. It is important that I be able to detach any events.
What goes on in my mind, would to somehow create a delegate that does this:
eventSource.Event += (sender, e) => {
target.method();
};
Is there a way to do this cleanly?

Dynamic Event Subscription and 1 handler

I have seen several answers already but somehow I can't get mine to work. I want to dynamically use any of the events of various controls (textbox, checkbox, button, etc) and, preferably, assign them to one event handler. The handler should be assigned at runtime. Furthermore, I want to know in the handler which event triggered the handler.
I got this to work partially. Using a lambda expression I call my handler (EventAssistant) and pass an extra parameter (command) which contains the name of the event. It works for events that use type EventHandler. However, it won't work for events that expect a different handler such as type MouseEventHandler. It will fail to subscribe at AddEventHandler.
private void RegisterEventHandlers(Control ctl)
{
foreach (Command command in CommandList)
{
EventInfo eventInfo = ctl.GetType().GetEvent(command.Name);
EventHandler handler = (sender, args) =>
{
EventAssistant(sender, args, command);
};
eventInfo.AddEventHandler(ctl, handler);
}
}
public void EventAssistant(object sender, EventArgs e, Command c)
{
//do lots of other fun stuff
}
Based on C# passing extra parameters to an event handler?
As an alternative I tried to solve the problem with an Expression Tree as shown here: Why am I getting an Argument exception when creating event handler dynamically?
Apparently, the EventHandlerType can be retrieved from the EventInfo and used in a lambda expression.
But, whatever I do I always get an InvalidOperationException "Lambda Parameter not in scope".
private void RegisterEventHandlers(Control ctl)
{
foreach (Command command in CommandList)
{
EventInfo eventInfo = ctl.GetType().GetEvent(command.Name);
var sender = Expression.Parameter(typeof(object), "sender");
var e = Expression.Parameter(typeof(EventArgs), "e");
var c = Expression.Parameter(typeof(Command), "command");
Expression[] arg = new Expression[] { sender, e, c };
MethodInfo mi = this.GetType().GetMethod("EventAssistant");
var body = Expression.Call(Expression.Constant(this), mi, arg);
var lambda = Expression.Lambda(eventInfo.EventHandlerType, body, sender, e);
eventInfo.AddEventHandler(ctl, lambda.Compile());
}
}
What am I doing wrong with the Expression Tree?
Also, the first piece of code looks a lot more clean. Is it possible to get what I want using the first code sample?
In your second attempt, the variable c shouldn't be a ParameterExpression, but a ConstantExpression with the value set to the current command instead. With the current code, you are creating a handler, which essentially looks like this:
(_sender, _e) => this.EventAssistant(_sender, _e, _c)
// The expression expects "_c" to be a parameter of the lambda, which is why
// you're getting that exception
However, if you change
var c = Expression.Parameter(typeof(Command), "command");
to
var c = Expression.Constant(command);
the generated code will look (and work, of course) as expected:
(_sender, _e) => this.EventAssistant(_sender, _e, command)

Pass an event as a parameter to a method [duplicate]

This question already has answers here:
Closed 11 years ago.
Possible Duplicate:
How to pass an event to a method?
Is it possible to pass an event as a parameter to a method?
For example, the following method subscribes to the event, does work, and unsubscribes from the event:
void SubscribeDoAndUnsubscribe<TElement, TEventArgs>(
IEnumerable<TElement> elements,
??? elementEvent)
where TEventArgs: EventArgs
{
EventHandler<TEventArgs> handler = (sender, e) => { /* Handle an event */ };
foreach (var element in elements)
{
// Subscribe somehow
element.elementEvent += handler
}
// Do things
foreach (var element in elements)
{
// Unsubscribe somehow
element.elementEvent -= handler
}
}
Client code:
var elements = new [] { new Button(), new Button() };
SubscribeDoAndUnsubscribe(elements, ??? /* e => e.Click */);
If it's not possible, how do I achieve the similar logic in other ways? Shall I pass pair of delegates for subscribe/unsubscribe methods?
You have in fact discovered that events are not "first class" in C#; you cannot pass around an event as data. You can pass around a delegate to a method associated with a receiver as a first-class object by making a delegate. You can pass around a reference to any variable as a (mostly) first-class object. (I say "mostly" because references to variables cannot be stored in fields, stored in arrays, and so on; they are highly restricted compared to other kinds of data.) You can pass around a type by obtaining its Type object and passing that around.
But there is no way to directly pass around as data an event, property, indexer, constructor or destructor associated with a particular instance. The best you can do is to make a delegate (or pair of delegates) out of a lambda, as you suggest. Or, obtain the reflection object associated with the event and pass that around, along with the instance.
No, unfortunately not.
If you look at Reactive Extensions, that suffers from a similar problem. Three options they use (IIRC - it's been a while since I've looked):
Pass in the corresponding EventInfo and call it with reflection
Pass in the name of the event (and the target if necessary) and call it with reflection
Pass in delegates for subscription and unsubscription
The call in the latter case would be something like:
SubscribeAndDoUnsubscribe(elements,
handler => e.Click += handler,
handler => e.Click -= handler);
and the declaration would be:
void SubscribeDoAndUnsubscribe<TElement, TEventArgs>(
IEnumerable<TElement> elements,
Action<EventHandler<TEventArgs>> subscription,
Action<EventHandler<TEventArgs>> unsubscription)
where TEventArgs: EventArgs
You're trying to get around type safety, and you can't do so without using reflection. I'll show you an even simpler example of what you're trying to do.
void DoSomethingOnSomethingElse(T obj, Action method)
{
obj.method();
}
C# doesn't work this way. How does the compiler know that all Ts have the method method? It doesn't, and can't. Similarly, not every TElement in your code will have an event Click for example.
It sounds like you just want to set a single use event handler on a set of objects. You can do this quite easily...
EventHandler handler = null;
handler = (s,e) =>
{
DoSomething(e);
var b = (Button) s;
b.Click -= handler;
}
foreach (var button in buttons)
{
button.Click += handler;
}
This, obviously, only works with buttons, but as I write this, I see Jon Skeet has shown you a more general solution, so I'll end here.

Is there a way to accomplish the equivalent of passing an "event" by reference?

I put "event" in quotes because I realize that it's a bit of syntax sugar, rather than a true type.
I have some events which are simply chained to matching events in another class. So when the event is raised, the passage is like
Raiser -> Proxy -> Subscriber
So in the Proxy class I have a common pattern like this:
Raiser.SomeEvent +=
(_, args) =>
{
if (this.SomeEvent != null)
this.SomeEvent(this, args);
};
To tidy up my code I wanted to move this out to another method that returns a new delegate that wraps the above event-calling code:
public static EventHandler GetHandlerDelegate(EventHandler handler, Object sender)
{
return
(_, args) =>
{
if (handler != null)
handler(sender, args);
};
}
And then in Proxy I can just do:
Raiser.SomeEvent += GetHandlerDelegate(this.SomeEvent, this);
Which is much neater.
Well this is fine as long as Subscriber doesn't decide to subscribe to Proxy.SomeEvent after the above call. Unfortunately I'm not passing the "event" around by reference as I'd hoped; I now understand that I'm just passing the invocation list, so when OtherClass.SomeEvent happens and that anonymous method is called and invokes the "event" (delegate) it was given, only the delegates that had been added to that event at the time I called GetHandlerDelegate() will be called. While that would actually suffice for my current situation, it's really not acceptable to code it that way.
I've read some other SO questions and I gather there is something called Reactive Extensions that might help, but at this time I'm looking for a simpler solution if there is one. (If not, I just won't do this.)
Is there another way I can accomplish what I'm trying to do, without said drawback?
If this question is unclear, please see my answer which hopefully helps clarify it.
EDIT: Okay, I think I get the point now. It's actually quite simple. You should be able to write the proxy to just have an event, and then make the proxy itself subscribe to the Raiser's event, like this (just for EventHandler - I'll come to that later on):
Proxy proxy = new Proxy();
raiser.SomeEvent += Proxy.Handler;
// Then in the subscriber...
proxy.ProxiedEvent += (whatever)
// And the proxy class...
public class Proxy
{
public event EventHandler ProxiedEvent;
public void Handler(object sender, EventArgs e)
{
EventHandler proxied = ProxiedEvent;
if (proxied != null)
{
// Or pass on the original sender if you want to
proxied(this, e);
}
}
}
Now, the difficulty here is getting it to work generically. I can't currently think of any way of doing that, although I'm somewhat distracted right now.
Is this the sort of thing you were thinking of, or does it at least help you think about things differently?
Since my original goal of doing:
Raiser.SomeEvent += GetHandlerDelegate(this.SomeEvent, this);
is impossible, I've compromised and come up with this:
Raiser.SomeEvent += (_, args) => RaiseEvent(this.SomeEvent, this, args);
Whereas GetHandlerDelegate() would return a delegate which raises the event, RaiseEvent() simply (you guessed it) raises the event.
public static void RaiseEvent(EventHandler _event, Object sender, EventArgs args)
{
if (_event != null)
_event(sender, args);
}
And to support events using custom EventArgs:
public static void RaiseEvent<TArgs>(EventHandler<TArgs> _event, Object sender, TArgs args)
where TArgs : EventArgs
{
if (_event != null)
_event(sender, args);
}
I've put these methods in a static helper class, so the actual call is slightly uglier; here's an example:
ViewControl.OpenFilesetClick += (_, args) => EventHelper.Raise(OpenFilesetClick, this, args);
(I also renamed the method to Raise() and dropped the optional this from the event name being passed).
But I'm not entirely convinced if this is worthwhile, considering the alternative was arguably easier to read:
ViewControl.OpenFilesetClick += (_, args) =>
{
if (OpenFilesetClick != null)
OpenFilesetClick(this, args);
};
Anyway, it was an interesting way to learn more about how events and delegates work (or how they don't work).

Categories

Resources