2012-10-26 8 views
5

Ich habe ein WPF-Steuerelement mit sehr schlechter Dokumentation.Dynamische Bindung an C# -Ereignisse mit Reflektion

Im Codebehind möchte ich über die Ereignisse reflektieren, die das Steuerelement unter Verwendung GetType().GetEVents() auslöst und einen Handler jedem hinzufügen, der einfach den Namen des Ereignisses ausgibt.

Dies würde mir erlauben zu sehen, was die Interaktion mit der Steuerung tatsächlich macht.

Bisher habe ich:

foreach (var e in GetType().GetEvents()) 
{ 
    var name = e.Name; 
    var handler = new Action<object,object>((o1,o2) =>Console.WriteLine(name)); 

    try 
    { 
     e.AddEventHandler(
        this, 
        Delegate.CreateDelegate(
           e.EventHandlerType, 
           handler.Target, 
           handler.Method 
           )); 
    } 
    catch (Exception ex) 
    { 
     Console.WriteLine("Failed to bind to event {0}", e.Name); 
    } 
} 

Welche scheint zu funktionieren, wenn die Ereignissignatur (object,EventArgs) ist aber, wenn sie auf bestimmte andere Ereignisse zu binden, schlägt fehl.

Gibt es eine Möglichkeit, dies zu tun, ohne unbedingt die Signatur des Ereignisses zu kennen?

Antwort

6

Sie könnten die Klasse System.Linq.Expressions.Expression verwenden, um dynamische Handler zu generieren, die der Signatur des Ereignisses entsprechen und in die Sie einfach einen Anruf an Console.WriteLine tätigen.

Die Expression.Lambda-Methode (Sie haben einen Link zu der spezifischen Überladung bereitgestellt, die Sie benötigen) kann verwendet werden, um einen Func<> oder, wahrscheinlicher, Action<> vom richtigen Typ zu generieren.

Sie die Delegattyp des Ereignisses widerspiegeln (Rupfen es ist Invoke Methode, wie durch @Davio erwähnt) alle Argumente zu ziehen und ParameterExpression s für jede dieser erstellen, um die Lambda-Verfahren zu liefern.

Hier ist eine komplette Lösung, die Sie in einem Standard-Unit-Test einfügen können, die ich in einem Follow-up bearbeiten später erklären werde:

public class TestWithEvents 
{ 
    //just using random delegate signatures here 
    public event Action Handler1; 
    public event Action<int, string> Handler2; 

    public void RaiseEvents(){ 
    if(Handler1 != null) 
     Handler1(); 
    if(Handler2 != null) 
     Handler2(0, "hello world"); 
    } 
} 

public static class DynamicEventBinder 
{ 
    public static Delegate GetHandler(System.Reflection.EventInfo ev) { 
    string name = ev.Name; 
    // create an array of ParameterExpressions 
    // to pass to the Expression.Lambda method so we generate 
    // a handler method with the correct signature. 
    var parameters = ev.EventHandlerType.GetMethod("Invoke").GetParameters(). 
     Select((p, i) => Expression.Parameter(p.ParameterType, "p" + i)).ToArray(); 

    // this and the Compile() can be turned into a one-liner, I'm just 
    // splitting it here so you can see the lambda code in the Console 
    // Note that we use the Event's type for the lambda, so it's tightly bound 
    // to that event. 
    var lambda = Expression.Lambda(ev.EventHandlerType, 
     Expression.Call(typeof(Console).GetMethod(
     "WriteLine", 
     BindingFlags.Public | BindingFlags.Static, 
     null, 
     new[] { typeof(string) }, 
     null), Expression.Constant(name + " was fired!")), parameters); 

    //spit the lambda out (for bragging rights) 
    Console.WriteLine(
     "Compiling dynamic lambda {0} for event \"{1}\"", lambda, name); 
    return lambda.Compile(); 
    } 

    //note - an unsubscribe might be handy - which would mean 
    //caching all the events that were subscribed for this object 
    //and the handler. Probably makes more sense to turn this type 
    //into an instance type that binds to a single object... 
    public static void SubscribeAllEvents(object o){ 
    foreach(var e in o.GetType().GetEvents()) 
    { 
     e.AddEventHandler(o, GetHandler(e)); 
    } 
    } 
} 

[TestMethod] 
public void TestSubscribe() 
{ 
    TestWithEvents testObj = new TestWithEvents(); 
    DynamicEventBinder.SubscribeAllEvents(testObj); 
    Console.WriteLine("Raising events..."); 
    testObj.RaiseEvents(); 
    //check the console output 
} 

Eine Skizze - wir mit einer Art beginnen, die einige Ereignisse hat (Ich benutze Action, aber es sollte mit irgendetwas funktionieren), und hat eine Methode, die wir verwenden können, um alle diese Ereignisse, die Abonnenten haben, zu testen.

dann zu der DynamicEventBinder Klasse, die zwei Methoden hat: GetHandler - um einen Handler für ein bestimmtes Ereignis für einen bestimmten Typ zu erhalten; und , die alle diese Ereignisse für eine bestimmte Instanz dieses Typs bindet - die einfach über alle Ereignisse eine Schleife macht, indem sie AddEventHandler aufruft und GetHandler aufruft, um den Handler zu erhalten.

Die GetHandler Methode ist, wo das Fleisch und Knochen ist - und tut genau, wie ich im Umriss vorschlage.

Ein Delegattyp verfügt über einen Invoke Member, der vom Compiler kompiliert wird und die Signatur eines beliebigen Handlers widerspiegelt, an den er gebunden werden kann. Also, wir reflektieren diese Methode und erhalten alle Parameter, die es hat, und erstellen Linq ParameterExpression Instanzen für jeden. Die Benennung des Parameters ist ein Feingefühl, es kommt auf den Typ an.

Dann bauen wir einen einzeiligen Lambda, dessen Körper im Grunde:

Console.WriteLine("[event_name] was fired!"); 

(hier beachten, dass der Name des Ereignisses in den dynamischen Code gezogen wird und in eine konstante String bis zum Code betreffen)

Wenn wir das Lambda bauen, wir uns die Art der Delegierten beabsichtigen zu bauen (gebunden direkt an den Delegattyp der Veranstaltung), und indem man das ParameterExpression Array, das wir vorher erstellt erzählen die Expression.Lambda Methode auch , wird es eine Methode erzeugen, die t hat Hat viele Parameter. Wir verwenden die Compile Methode, um den dynamischen Code tatsächlich zu kompilieren, was uns eine Delegate gibt, die wir dann als Argument für AddEventHandler verwenden können.

Ich hoffe aufrichtig, dass dies erklärt, was wir getan haben - wenn Sie noch nicht mit Expressions und dynamischem Code gearbeitet haben, bevor es sich um wirklich krasses Zeug handelt. Manche Leute, mit denen ich arbeite, nennen das einfach Voodoo.

+0

Das klingt interessant, aber ich habe Mühe zu implementieren! – Nick

+0

Ich werde einen Test mit Typ + und Ereignis für Sie zusammenstellen ... könnte eine halbe Stunde dauern ... –

+0

Ich habe zu dem Teil, wo ich AddHandler auf dem EventInfo-Objekt aufrufen und ich bekomme: * "Objekt des Typs 'System.Action'2 [System.Object, System.Windows.Input.MouseButtonEventArgs]' kann nicht in den Typ 'System.Windows.Input.MouseButtonEventHandler' konvertiert werden." * – Nick