C#-Abonnement für dynamische Ereignisse
-
09-06-2019 - |
Frage
Wie würden Sie ein C#-Ereignis dynamisch abonnieren, sodass Sie bei gegebener Objektinstanz und einem String-Namen, der den Namen des Ereignisses enthält, dieses Ereignis abonnieren und etwas tun (z. B. in die Konsole schreiben), wenn das Ereignis ausgelöst wurde?
Es scheint, dass dies mit Reflection nicht möglich ist, und ich möchte nach Möglichkeit vermeiden, Reflection.Emit verwenden zu müssen, da dies derzeit (für mich) die einzige Möglichkeit zu sein scheint.
/BEARBEITEN: Ich kenne die für die Veranstaltung benötigte Unterschrift des Delegierten nicht, das ist der Kern des Problems
/EDIT 2: Obwohl Delegierten-Kontravarianz ein guter Plan zu sein scheint, kann ich nicht davon ausgehen, dass diese Lösung erforderlich ist
Lösung
Sie können Ausdrucksbäume kompilieren, um Void-Methoden ohne Argumente als Ereignishandler für Ereignisse beliebigen Typs zu verwenden.Um andere Event-Handler-Typen zu berücksichtigen, müssen Sie die Parameter des Event-Handlers irgendwie den Ereignissen zuordnen.
using System;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
class ExampleEventArgs : EventArgs
{
public int IntArg {get; set;}
}
class EventRaiser
{
public event EventHandler SomethingHappened;
public event EventHandler<ExampleEventArgs> SomethingHappenedWithArg;
public void RaiseEvents()
{
if (SomethingHappened!=null) SomethingHappened(this, EventArgs.Empty);
if (SomethingHappenedWithArg!=null)
{
SomethingHappenedWithArg(this, new ExampleEventArgs{IntArg = 5});
}
}
}
class Handler
{
public void HandleEvent() { Console.WriteLine("Handler.HandleEvent() called.");}
public void HandleEventWithArg(int arg) { Console.WriteLine("Arg: {0}",arg); }
}
static class EventProxy
{
//void delegates with no parameters
static public Delegate Create(EventInfo evt, Action d)
{
var handlerType = evt.EventHandlerType;
var eventParams = handlerType.GetMethod("Invoke").GetParameters();
//lambda: (object x0, EventArgs x1) => d()
var parameters = eventParams.Select(p=>Expression.Parameter(p.ParameterType,"x"));
var body = Expression.Call(Expression.Constant(d),d.GetType().GetMethod("Invoke"));
var lambda = Expression.Lambda(body,parameters.ToArray());
return Delegate.CreateDelegate(handlerType, lambda.Compile(), "Invoke", false);
}
//void delegate with one parameter
static public Delegate Create<T>(EventInfo evt, Action<T> d)
{
var handlerType = evt.EventHandlerType;
var eventParams = handlerType.GetMethod("Invoke").GetParameters();
//lambda: (object x0, ExampleEventArgs x1) => d(x1.IntArg)
var parameters = eventParams.Select(p=>Expression.Parameter(p.ParameterType,"x")).ToArray();
var arg = getArgExpression(parameters[1], typeof(T));
var body = Expression.Call(Expression.Constant(d),d.GetType().GetMethod("Invoke"), arg);
var lambda = Expression.Lambda(body,parameters);
return Delegate.CreateDelegate(handlerType, lambda.Compile(), "Invoke", false);
}
//returns an expression that represents an argument to be passed to the delegate
static Expression getArgExpression(ParameterExpression eventArgs, Type handlerArgType)
{
if (eventArgs.Type==typeof(ExampleEventArgs) && handlerArgType==typeof(int))
{
//"x1.IntArg"
var memberInfo = eventArgs.Type.GetMember("IntArg")[0];
return Expression.MakeMemberAccess(eventArgs,memberInfo);
}
throw new NotSupportedException(eventArgs+"->"+handlerArgType);
}
}
static class Test
{
public static void Main()
{
var raiser = new EventRaiser();
var handler = new Handler();
//void delegate with no parameters
string eventName = "SomethingHappened";
var eventinfo = raiser.GetType().GetEvent(eventName);
eventinfo.AddEventHandler(raiser,EventProxy.Create(eventinfo,handler.HandleEvent));
//void delegate with one parameter
string eventName2 = "SomethingHappenedWithArg";
var eventInfo2 = raiser.GetType().GetEvent(eventName2);
eventInfo2.AddEventHandler(raiser,EventProxy.Create<int>(eventInfo2,handler.HandleEventWithArg));
//or even just:
eventinfo.AddEventHandler(raiser,EventProxy.Create(eventinfo,()=>Console.WriteLine("!")));
eventInfo2.AddEventHandler(raiser,EventProxy.Create<int>(eventInfo2,i=>Console.WriteLine(i+"!")));
raiser.RaiseEvents();
}
}
Andere Tipps
Es ist keine völlig allgemeine Lösung, aber wenn alle Ihre Ereignisse von der Form void foo (Objekt O, T Args) ausmachen, wobei T von EventArgs stammt, können Sie die Delegierte -Verhütung verwenden, um damit durchzukommen.So (wobei die Signatur von KeyDown nicht mit der von Click identisch ist):
public Form1()
{
Button b = new Button();
TextBox tb = new TextBox();
this.Controls.Add(b);
this.Controls.Add(tb);
WireUp(b, "Click", "Clickbutton");
WireUp(tb, "KeyDown", "Clickbutton");
}
void WireUp(object o, string eventname, string methodname)
{
EventInfo ei = o.GetType().GetEvent(eventname);
MethodInfo mi = this.GetType().GetMethod(methodname, BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic);
Delegate del = Delegate.CreateDelegate(ei.EventHandlerType, this, mi);
ei.AddEventHandler(o, del);
}
void Clickbutton(object sender, System.EventArgs e)
{
MessageBox.Show("hello!");
}
Es ist möglich, eine Veranstaltung über Reflection zu abonnieren
var o = new SomeObjectWithEvent;
o.GetType().GetEvent("SomeEvent").AddEventHandler(...);
http://msdn.microsoft.com/en-us/library/system.reflection.eventinfo.addeventhandler.aspx
Hier liegt nun das Problem, das Sie lösen müssen.Die für jeden Ereignishandler erforderlichen Delegaten verfügen über unterschiedliche Signaturen.Sie müssen lernen, diese Methoden dynamisch zu erstellen, was wahrscheinlich Reflection.Emit bedeutet, oder Sie müssen sich auf einen bestimmten Delegaten beschränken, damit Sie sie mit kompiliertem Code verarbeiten können.
Hoffe das hilft.
Probieren Sie LinFu aus – es verfügt über einen universellen Event-Handler, mit dem Sie zur Laufzeit eine Bindung zu jedem Event herstellen können.So können Sie beispielsweise einen Handler an das Click-Ereignis einer dynamischen Schaltfläche binden:
// Note: The CustomDelegate signature is defined as: // public delegate object CustomDelegate(params object[] args); CustomDelegate handler = delegate { Console.WriteLine("Button Clicked!"); return null; }; Button myButton = new Button(); // Connect the handler to the event EventBinder.BindToEvent("Click", myButton, handler);
Mit LinFu können Sie Ihre Handler an jedes Ereignis binden, unabhängig von der Signatur des Delegierten.Genießen!
Du findest es hier:http://www.codeproject.com/KB/cs/LinFuPart3.aspx
public TestForm()
{
Button b = new Button();
this.Controls.Add(b);
MethodInfo method = typeof(TestForm).GetMethod("Clickbutton",
BindingFlags.NonPublic | BindingFlags.Instance);
Type type = typeof(EventHandler);
Delegate handler = Delegate.CreateDelegate(type, this, method);
EventInfo eventInfo = cbo.GetType().GetEvent("Click");
eventInfo.AddEventHandler(b, handler);
}
void Clickbutton(object sender, System.EventArgs e)
{
// Code here
}
Ich habe kürzlich eine Reihe von Blogbeiträgen geschrieben, in denen Unit-Test-Ereignisse beschrieben werden, und eine der Techniken, die ich bespreche, beschreibt das dynamische Ereignisabonnement.Für die dynamischen Aspekte habe ich Reflektion und MSIL (Code Emitting) verwendet, aber das ist alles gut zusammengefasst.Mit der DynamicEvent-Klasse können Ereignisse wie folgt dynamisch abonniert werden:
EventPublisher publisher = new EventPublisher();
foreach (EventInfo eventInfo in publisher.GetType().GetEvents())
{
DynamicEvent.Subscribe(eventInfo, publisher, (sender, e, eventName) =>
{
Console.WriteLine("Event raised: " + eventName);
});
}
Eines der Merkmale des von mir implementierten Musters bestand darin, dass es den Ereignisnamen in den Aufruf des Ereignishandlers einfügt, sodass Sie wissen, welches Ereignis ausgelöst wurde.Sehr nützlich für Unit-Tests.
Der Blog-Artikel ist ziemlich lang, da er eine Technik zum Testen von Ereigniseinheiten beschreibt, es werden jedoch der vollständige Quellcode und die Tests bereitgestellt, und eine detaillierte Beschreibung, wie das dynamische Ereignisabonnement implementiert wurde, finden Sie im letzten Beitrag.
http://gojisoft.com/blog/2010/04/22/event-sequence-unit-testing-part-1/
Was Sie wollen, kann mit der Abhängigkeitsinjektion erreicht werden.Zum Beispiel Microsoft Composite UI-App-Block macht genau das, was du beschrieben hast
Diese Methode fügt einem Ereignis einen dynamischen Handler hinzu, der eine Methode aufruft OnRaised
, Übergabe der Ereignisparameter als Objektarray:
void Subscribe(object source, EventInfo ev)
{
var eventParams = ev.EventHandlerType.GetMethod("Invoke").GetParameters().Select(p => Expression.Parameter(p.ParameterType)).ToArray();
var eventHandler = Expression.Lambda(ev.EventHandlerType,
Expression.Call(
instance: Expression.Constant(this),
method: typeof(EventSubscriber).GetMethod(nameof(OnRaised), BindingFlags.NonPublic | BindingFlags.Instance),
arg0: Expression.Constant(ev.Name),
arg1: Expression.NewArrayInit(typeof(object), eventParams.Select(p => Expression.Convert(p, typeof(object))))),
eventParams);
ev.AddEventHandler(source, eventHandler.Compile());
}
OnRaised
hat diese Signatur:
void OnRaised(string name, object[] parameters);
Meinst du so etwas wie:
//reflect out the method to fire as a delegate
EventHandler eventDelegate =
( EventHandler ) Delegate.CreateDelegate(
typeof( EventHandler ), //type of event delegate
objectWithEventSubscriber, //instance of the object with the matching method
eventSubscriberMethodName, //the name of the method
true );
Dadurch wird kein Abonnement erstellt, sondern die aufzurufende Methode wird übergeben.
Bearbeiten:
Der Beitrag wurde nach dieser Antwort geklärt. Mein Beispiel hilft nicht, wenn Sie den Typ nicht kennen.
Alle Ereignisse in .Net sollten jedoch dem Standardereignismuster folgen. Solange Sie es befolgt haben, funktioniert dies also mit dem grundlegenden EventHandler.