Anonymous type to dictionary using DynamicMethod

May 3, 2008 at 12:24 PMAndre Loker

C# 3.0 offers a variety of new language features like anonymous types, object and collection initializers and extension methods. With some creativity these features can be used to reduce the amount of code being written and to make the code more readable. Imaging you have a method that expects a dictionary of configuration values, like:

   1: public class MyClass {
   2:     public void DoStuff(IDictionary<string, object> dictionary) {
   3:         // .. do something
   4:     }
   5: }

A typical .NET 2 client would use the method like this:

   1: public static void DotNet2(MyClass mc) {
   2:     Dictionary<string, object> dictionary = new Dictionary<string, object>();
   3:     dictionary["date"] = new DateTime(1970, 6, 12);
   4:     dictionary["foo"] = 123;
   5:     dictionary["name"] = "Some text";
   6:     mc.DoStuff(dictionary);
   7: }

C# 3.0 adds the collection initializer syntax, which reduces the repetitive code parts a lot:

   1: public static void CollectionInitializer(MyClass mc) {
   2:     var dictionary = new Dictionary<string, object> {
   3:         {"date", new DateTime(1970, 6, 12)},
   4:         {"foo", 123},
   5:         {"name", "Some text"}
   6:     };
   7:     mc.DoStuff(dictionary);
   8: }

Currently Microsoft is working on ASP.NET MVC, an MVC implementation for ASP.NET comparable to Ruby On Rails or Castle MonoRail. To make the code more compact - especially in the HTML views - they use anonymous objects instead of dictionaries. In our example a first approach could look like this:

   1: public static void AnonymousWithReflection(MyClass mc) {
   2:     var data = new {
   3:         date = new DateTime(1970, 6, 12),
   4:         foo = 123,
   5:         name = "Some text"
   6:     };
   7:     var dictionary = ObjectHelper.TurnObjectIntoDictionary(data);
   8:     mc.DoStuff(dictionary);
   9: }

This looks a lot cleaner to me. Certainly the first question is: how do we convert the object into a dictionary? A simple implementation could look like this:

   1: public static class ObjectHelper {
   2:     public static IDictionary<string, object> TurnObjectIntoDictionary(object data) {
   3:         var attr = BindingFlags.Public | BindingFlags.Instance;
   4:         var dict = new Dictionary<string, object>();
   5:         foreach (var property in data.GetType().GetProperties(attr)) {
   6:             if (property.CanRead) {
   7:                 dict.Add(property.Name, property.GetValue(data, null));
   8:             }
   9:         }
  10:         return dict;
  11:     }
  12: }

This method uses reflection to grab the values of all readable properties and puts them into a dictionary. Not very difficult to understand, but because of the reflection certainly not the fastest thing in the world as we will see later.

Intermezzo: using extension methods to improve readability

Before we continue our journey lets see whether we can improve the API. The call to ObjectHelper.TurnObjectIntoDictionary looks quite verbose to me, so lets add an extension method to ObjectHelper:

   1: public static IDictionary<string, object> ToDictionaryR(this object obj) {
   2:     return TurnObjectIntoDictionary(obj);
   3: }

I named the method ToDictionaryR to remind me that it uses reflection. This is not meant for production code, just for our little prove of concept here. While the code is not spectacular it improves usability:

   1: public static void AnonymousWithReflection2(MyClass mc) {
   2:     var data = new {
   3:        date = new DateTime(1970, 6, 12),
   4:        foo = 123,
   5:        name = "Some text"
   6:    };
   7:     mc.DoStuff(data.ToDictionaryR());
   8: }

We could tweak the whole thing into another direction by providing an overload of DoStuff that accepts an object which is converted to a dictionary as needed. If we deal with legacy code, we might as well use extension methods again:

   1: public static class MyClassExtensions {
   2:     public static void DoStuff(this MyClass client, object data) {
   3:         if (data is IDictionary<string, object>){
   4:             client.DoStuff((IDictionary<string, object>)data);
   5:         } else {
   6:             client.DoStuff(data.ToDictionaryR());
   7:         }
   8:     }
   9: }

And here the client that uses the extension:

   1: public static void AnonymousWithReflection3(MyClass mc) {
   2:     var data = new {
   3:        date = new DateTime(1970, 6, 12),
   4:        foo = 123,
   5:        name = "Some text"
   6:    };
   7:     mc.DoStuff(data);
   8: }

Looks neat! I do like the compactness of the new API. Let us see how this performs at runtime.

Give me numbers!

As mentioned earlier this approach uses a lot of reflection. This is bad, or isn't it? Benchmarks to the rescue! Here comes the almighty benchmarking program:

   1: private static void Main(string[] args) {
   2:     var mc = new MyClass();
   3:     for (int i = 0; i < 5; ++i) {
   4:         Benchmark("DotNet2", DotNet2, mc);
   5:         Benchmark("CollectionInitializer", CollectionInitializer, mc);
   6:         Benchmark("AnonymousWithReflection3", AnonymousWithReflection3, mc);
   7:     }
   8: }
   9:  
  10: public static void Benchmark(string name, Action<MyClass> exec, MyClass mc) {
  11:     Console.Out.Write("Benchmarking {0,-30}:", name);
  12:     const int count = 100000;
  13:     var sw = new Stopwatch();
  14:     sw.Start();
  15:     for (var i = 0; i < count; ++i) {
  16:         exec(mc);
  17:     }
  18:     sw.Stop();
  19:     Console.Out.WriteLine("{0} ms", sw.ElapsedMilliseconds);
  20: }

We simply invoke the approaches presented before one million times (and that 5 times in a row to let the values settle down) and see how fast they are. Certainly this is not very representative for code in practice. Still I want to get a rough estimate how expensive all that reflection mumbo jumbo really is. So, here are the results on my machine (Intel E6750, 4GB RAM, Vista Ultimate 64):

   1: Benchmarking DotNet2                       :32 ms
   2: Benchmarking CollectionInitializer         :29 ms
   3: Benchmarking AnonymousWithReflection3      :1822 ms
   4: Benchmarking DotNet2                       :28 ms
   5: Benchmarking CollectionInitializer         :29 ms
   6: Benchmarking AnonymousWithReflection3      :1783 ms
   7: Benchmarking DotNet2                       :28 ms
   8: Benchmarking CollectionInitializer         :28 ms
   9: Benchmarking AnonymousWithReflection3      :1789 ms
  10: Benchmarking DotNet2                       :28 ms
  11: Benchmarking CollectionInitializer         :28 ms
  12: Benchmarking AnonymousWithReflection3      :1852 ms
  13: Benchmarking DotNet2                       :29 ms
  14: Benchmarking CollectionInitializer         :29 ms
  15: Benchmarking AnonymousWithReflection3      :1809 ms

The DotNet2 and the CollectionInitializer version perform equally. This was expected, as collection initializers are syntactic sugar only. But the reflection version does bad, I mean REALLY bad: it is roughly 60 times slower than the non-reflective approach.

DynamicMethod to the rescue

Luckily we can minimize the overhead. Instead of using reflection all the time we convert the object we will use reflection one time to create code on the fly that can be reused afterwards. With the DynamicMethod class, this is rather easy. You only have to understand IL (intermediate language). If you have no experience with IL, grab Reflector and poke around in existing code. You'll get the idea.

Here is the code that creates a delegate that will convert an object to a dictionary:

   1: public static Func<object, IDictionary<string, object>> CreateObjectToDictionaryConverter(Type itemType) {
   2:     var dictType = typeof (Dictionary<string, object>);
   3:  
   4:     // setup dynamic method
   5:     // Important: make itemType owner of the method to allow access to internal types
   6:     var dm = new DynamicMethod(string.Empty, typeof (IDictionary<string, object>), new[] {typeof (object)}, itemType);
   7:     var il = dm.GetILGenerator();
   8:  
   9:     // Dictionary.Add(object key, object value)
  10:     var addMethod = dictType.GetMethod("Add");
  11:  
  12:     // create the Dictionary and store it in a local variable
  13:     il.DeclareLocal(dictType);
  14:     il.Emit(OpCodes.Newobj, dictType.GetConstructor(Type.EmptyTypes));
  15:     il.Emit(OpCodes.Stloc_0);
  16:  
  17:     var attributes = BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy;
  18:     foreach (var property in itemType.GetProperties(attributes).Where(info => info.CanRead)) {
  19:         // load Dictionary (prepare for call later)
  20:         il.Emit(OpCodes.Ldloc_0);
  21:         // load key, i.e. name of the property
  22:         il.Emit(OpCodes.Ldstr, property.Name);
  23:  
  24:         // load value of property to stack
  25:         il.Emit(OpCodes.Ldarg_0);
  26:         il.EmitCall(OpCodes.Callvirt, property.GetGetMethod(), null);
  27:         // perform boxing if necessary
  28:         if (property.PropertyType.IsValueType) {
  29:             il.Emit(OpCodes.Box, property.PropertyType);
  30:         }
  31:  
  32:         // stack at this point
  33:         // 1. string or null (value)
  34:         // 2. string (key)
  35:         // 3. dictionary
  36:  
  37:         // ready to call dict.Add(key, value)
  38:         il.EmitCall(OpCodes.Callvirt, addMethod, null);
  39:     }
  40:     // finally load Dictionary and return
  41:     il.Emit(OpCodes.Ldloc_0);
  42:     il.Emit(OpCodes.Ret);
  43:  
  44:     return (Func<object, IDictionary<string, object>>) dm.CreateDelegate(typeof (Func<object, IDictionary<string, object>>));
  45: }

The result of this method is a delegate that expects an object and returns a dictionary. Invoking CreateObjectToDictionaryConverter is rather expensive, so lets cache the result by providing a factory/registry:

   1: /// <summary>
   2: /// Loads the values of an object's properties into a <see cref="IDictionary{String,Object}"/>
   3: /// </summary>
   4: public class ObjectToDictionaryRegistry {
   5:     private static readonly Dictionary<Type, Func<object, IDictionary<string, object>>> cache = new Dictionary<Type, Func<object, IDictionary<string, object>>>();
   6:     private static readonly ReaderWriterLockSlim rwLock = new ReaderWriterLockSlim();
   7:  
   8:     /// <summary>
   9:     /// Loads the values of an object's properties into a <see cref="IDictionary{String,Object}"/>.
  10:     /// </summary>
  11:     /// <param name="dataObject">The data object.</param>
  12:     /// <returns>If <paramref name="dataObject"/> implements <see cref="IDictionary{String,Object}"/>, 
  13:     /// the object is cast to <see cref="IDictionary{String,Object}"/> and returned.
  14:     /// Otherwise the object returned is a <see cref="System.Collections.Hashtable"/> with all public non-static properties and their respective values
  15:     /// as key-value pairs.
  16:     /// </returns>
  17:     public static IDictionary<string, object> Convert(object dataObject) {
  18:         if (dataObject == null) {
  19:             return null;
  20:         }
  21:         if (dataObject is IDictionary<string, object>) {
  22:             return (IDictionary<string, object>) dataObject;
  23:         }
  24:         return GetObjectToDictionaryConverter(dataObject)(dataObject);
  25:     }
  26:  
  27:     /// <summary>
  28:     /// Handles caching.
  29:     /// </summary>
  30:     /// <param name="item">The item.</param>
  31:     /// <returns></returns>
  32:     private static Func<object, IDictionary<string, object>> GetObjectToDictionaryConverter(object item) {
  33:         rwLock.EnterUpgradeableReadLock();
  34:         try {
  35:             Func<object, IDictionary<string, object>> ft;
  36:             if (!cache.TryGetValue(item.GetType(), out ft)) {
  37:                 rwLock.EnterWriteLock();
  38:                 // double check
  39:                 try {
  40:                     if (!cache.TryGetValue(item.GetType(), out ft)) {
  41:                         ft = CreateObjectToDictionaryConverter(item.GetType());
  42:                         cache[item.GetType()] = ft;
  43:                     }
  44:                 } finally {
  45:                     rwLock.ExitWriteLock();
  46:                 }
  47:             }
  48:             return ft;
  49:         } finally {
  50:             rwLock.ExitUpgradeableReadLock();
  51:         }
  52:     }
  53:  
  54:     private static Func<object, IDictionary<string, object>> CreateObjectToDictionaryConverter(Type itemType) {
  55:         // as seen above
  56:     }
  57: }

There is nothing fancy going on. This implementation already has added synchronization for thread-safety which adds a little complexity. Note by the way that I am using the new ReaderWriterLockSlim. I expect much more reads from the cache than writes to it so this seems to be a reasonable choice. I also added the check to see whether the object provided to Convert already is a IDictionary<string, object> in case of which it is simply cast.

Given this new implementation we change the usage a bit:

   1: // modified version of the MyClass extension method
   2: public static class MyClassExtensions {
   3:     public static void DoStuff(this MyClass client, object data) {
   4:         client.DoStuff(ObjectToDictionaryRegistry.Convert(data));
   5:     }
   6: }
   7:  
   8: // our final testcase using ObjectToDictionaryRegistry
   9: public static void Final(MyClass mc) {
  10:     mc.DoStuff(new {
  11:            date = new DateTime(1970, 6, 12),
  12:            foo = 123,
  13:            name = "Some text"
  14:        });
  15: }

So, let us see how this performs. Adding the Final method to our benchmarking set, we get the following results:

   1: Benchmarking DotNet2                       :41 ms
   2: Benchmarking CollectionInitializer         :33 ms
   3: Benchmarking AnonymousWithReflection2      :1961 ms
   4: Benchmarking Final                         :60 ms
   5: Benchmarking DotNet2                       :27 ms
   6: Benchmarking CollectionInitializer         :28 ms
   7: Benchmarking AnonymousWithReflection2      :1791 ms
   8: Benchmarking Final                         :58 ms
   9: Benchmarking DotNet2                       :31 ms
  10: Benchmarking CollectionInitializer         :29 ms
  11: Benchmarking AnonymousWithReflection2      :1886 ms
  12: Benchmarking Final                         :50 ms
  13: Benchmarking DotNet2                       :30 ms
  14: Benchmarking CollectionInitializer         :28 ms
  15: Benchmarking AnonymousWithReflection2      :1914 ms
  16: Benchmarking Final                         :53 ms
  17: Benchmarking DotNet2                       :30 ms
  18: Benchmarking CollectionInitializer         :28 ms
  19: Benchmarking AnonymousWithReflection2      :1827 ms
  20: Benchmarking Final                         :54 ms

Wow, this is cool: the version using DynamicMethod takes only two times longer than the dictionary version. That's highly acceptable given the fact that:

  • We have to do synchronization at the registry.
  • The method actually being called currently does nothing where in practice it will most likely take more time to execute compared to the cost of conversion. The difference between the DynamicMethod approach and the dictionary approach will very soon shrink a lot.

What we gain and what we lose

Personally I think that the syntax using anonymous types is quite nice. We need less (disturbing) characters like quotes, braces etc. to express what we want. The costs are neglectable if we use DynamicMethod and caching.

However, not everyone is happy with this approach. Jeffrey Palermo for example finds that MS makes "a huge mistake" by providing this kind of API for ASP.NET MVC. I think that this kind of API has its place but should - as always - only be used where appropriate. If you now in advance which keys to expect in the dictionary, you'd most likely be better of using a strongly typed object with an object initializer. When the keys cannot be foreseen the API can gain readability by using anonymous types. You certainly do have a problematic API when the parameters provided are that dynamic, but this is not the fault of the anonymous type. Providing string keys plus object values in a dictionary is not typesafe nor intuitive in the first place. Anonymous types can only help to improve the problematic situation.

Posted in: C# | Snippets

Tags: , ,

C# 3.0 - Auto Properties

March 19, 2008 at 11:38 AMAndre Loker

C# 3.0 bietet als Neuerung so genannte Auto Properties. Das sind einfache Properties, für der Compiler automatisch ein Backing Field erstellt. Beispiel:

   1: class MyClass {
   2:     // Auto property 
   3:     public int Position { get; set; }
   4: }

Der compiler erstellt ein implizites Backing Field für Position. Möchte man den Setter vor Zugriffen von außen schützen, ist das auch kein Problem:

   1: class MyClass {
   2:     // Auto property
   3:     public int Position { get; private set; }
   4: }

Der Vorteil dieser Syntax ist die kompakte Schreibweise für triviale Properties. Zudem ist es Problemlos möglich, bei Bedarf die Auto Property in eine vollständige Property zu upgraden bei Beibehaltung der Binärkompatibilität.

Ich stelle allerdings nach einiger Zeit fest, dass diese Syntax für den Leser des Quellcodes verwirrend ist. Ich finde es als Entwickler übersichtlicher, wenn alle Felder zu Beginn der Klasse angegeben sind. Wenn ich Auto Properties verwende, fehlt mir die sofortige Orientierung bzgl. der verwendeten Felder, besonders bei der Vermischung von Auto Properties und expliziten Backing Fields. Da Auto Properties nicht gesondert gekennzeichnet werden, muss ich ggf. über alle Properties schauen, um zu sehen, was wirklich in der Klasse vor sich geht. Es ist übrigens nicht möglich, readonly Felder als Auto Properties darzustellen. Bleibt noch zu resümieren, dass ich Auto Properties - außer bei trivialen Klassen - in Zukunft meiden werde.

Posted in: C#

Tags:

Delegates vs. Events

February 28, 2008 at 11:22 AMAndre Loker

Manche Fragen tauchen mit schöner Regelmäßigkeit in Foren etc. auf. Eine davon lautet:

Was ist der Unterschied zwischen Ereignissen (Events) und Delegaten (Delegates)?

Beispiel:

   1: delegate void MyDelegate( string message );
   2:  
   3: class MyClass {
   4:     public MyDelegate MyDelegate;
   5:     public event MyDelegate MyEvent;
   6: }

MyClass.MyDelegate ist ein Delegate, MyClass.MyEvent entsprechend ein Event. Der Unterschied zwischen den beiden besteht hauptsächlich in den Zugriffsrechten außerhalb der Klasse MyClass.

Wir können drei Operationen festhalten, die für delegates bzw. events wichtig sind:

  • Hinzufügen/Entfernen von Event-Handlern (mit den += und -= Operatoren)
  • Direktes Zuweisen eines Wertes (mit dem = Operator)
  • Auslösen des Events/aufrufen der Event-Handler (mit dem ()-Operator)

Der Unterschied zwischen Event und Delegate lässt sich mit folgender Tabelle leicht veranschaulichen:

Operation Event Delegate
Innerhalb MyClass    
  Hinzufügen/Entfernen mit += und -= erlaubt erlaubt
  Zuweisen mit = erlaubt erlaubt
  Auslösen mit () erlaubt erlaubt
Außerhalb MyClass    
  Hinzufügen/Entfernen mit += und -= erlaubt erlaubt
  Zuweisen mit = nicht erlaubt erlaubt
  Auslösen mit () nicht  erlaubt erlaubt

 

Mit "innerhalb" MyClass sind Methoden gemeint, die MyClass selbst definiert. Von MyClass abgeleitete Klassen werden in dieser Tabelle wie "außerhalb MyClass" behandelt.

Code-Beispiel:

   1: using System;
   2:  
   3: delegate void MyDelegate( string message );
   4:  
   5: class MyClass {
   6:     public MyDelegate MyDelegate;
   7:     public event MyDelegate MyEvent;
   8: }
   9:  
  10: class Program {
  11:     static void Main() {
  12:         MyClass mc = new MyClass();
  13:  
  14:         // DELEGATES
  15:         // Subscribe
  16:         mc.MyDelegate += HandleTheEvent; // OK
  17:         // Unsubscribe
  18:         mc.MyDelegate -= HandleTheEvent; // OK
  19:         // Clear all subscribers
  20:         mc.MyDelegate = null; // OK
  21:         // Invoke subscribers
  22:         mc.MyDelegate( "Hello, world!" ); // OK
  23:  
  24:         // EVENTS
  25:         // Subscribe
  26:         mc.MyEvent += HandleTheEvent; // OK
  27:         // Unsubscribe
  28:         mc.MyEvent -= HandleTheEvent; // OK
  29:         // Clear all subscribers
  30:         mc.MyEvent = null; // NOT ALLOWED, Cpmpiler error
  31:         // Invoke subscribers
  32:         mc.MyEvent( "Hello, world!" ); // NOT ALLOWED, Compiler error
  33:     }
  34:  
  35:     private static void HandleTheEvent( string message ) {
  36:         Console.WriteLine( message );
  37:     }
  38: }

Zusammengefasst:

Der wesentliche Unterschied bei der Handhabung von Delegates und Events besteht darin, dass außerhalb der Klasse, die das Event bzw. den Delegaten definiert, das direkte Zuweisen und das Auslösen eines Events nicht erlaubt ist, während beides bei einem Delegaten gesstattet ist.

Oft wird nun gefragt, was man nehmen sollte: Events oder Delegates. Wenn man Ereignisse anbieten möchte, wird man verständlicherweise zu Events greifen. Ein öffentlicher Delegat bricht die Kapselung der Klasse viel zu sehr auf. Ein Event hingegen bietet eine wohldefinierte begrenzte Schnittstelle zur Außenwelt an. Zudem erlauben Events mehr Feinkontrolle, wie der nächste Abschnitt zeigt.

Details

Es gibt - gerade in technischer Hinsicht - natürlich einen größeren Unterschied zwischen Events und Delegaten. Dazu muss man wissen, wie ein Event für die CLR aussieht. Bei der Definition des Events

   1: public event MyDelegate MyEvent;

erzeugt der C# Compiler in etwa folgenden Code:

   1: private MyDelegate _MyEvent;
   2:  
   3: public void add_MyEvent(MyDelegate value)
   4: {
   5:     this.MyEvent = (MyDelegate) Delegate.Combine(this.MyEvent, value);
   6: }
   7:  
   8: public void remove_MyEvent(MyDelegate value)
   9: {
  10:     this.MyEvent = (MyDelegate) Delegate.Remove(this.MyEvent, value);
  11: }

Hinweis: In Wirklichkeit haben das Feld und das Event den selben Namen (MyEvent). Zur Veranschaulichung verwende ich aber _MyEvent für das Feld und MyEvent für das Event.

In Wahrheit erzeugt die Definition des Events also ein privates Feld vom Typ des Delegaten und zwei öffentliche Wrapper-Methoden, um sich bei dem Delegaten an- bzw. abzumelden. Die += und -= Operator für das Event werden vom Compiler zu Aufrufen der add_MyEvent und remove_MyEvent Methoden kompiliert. Das erklärt, warum dies die einzigen beiden Operationen sind, die außerhalb der MyClass-Klasse erlaubt sind. Da _MyEvent private ist, erklärt sich auch, warum selbst von MyClass abgeleitete Klassen lediglich += und -= verwenden dürfen.

Auch das Auslösen des Events ist zunächst syntaktischer Zucker:

   1: MyEvent( "foo" );

wird zu

   1: _MyEvent( "foo" );

In manchen Fällen möchte man das Standardverhalten des C#-Compilers (erstellen eines privaten Feldes des Delegaten-Types und Erzeugen der einfachen add und remove-Methoden) überschreiben. In der .NET Klassenbibliothek wird das z.B. bei den Windows-Forms oft gemacht, da ein Control viele Events hat, die bei weitem aber nicht alle verwendet werden. Würde man hier das Standardverhalten verwenden, würde für jedes Event ein Feld angelegt, das entsprechend Platz pro Instanz belegt.

Um das Verhalten zu überschreiben, kann man in C# für ein Event explizit die add- und remove-Methoden angeben. Hier ein komplexeres Beispiel, wie man mehrere Events zentral handhaben kann, indem man mit Hilfe eines Dictionary-Feldes die Buchführung für Events selber übernimmt:

   1: class MyClass {
   2:     // keys to identify events in the dictionary
   3:     private static readonly object Event1Key = new object();
   4:     private static readonly object Event2Key = new object();
   5:     private static readonly object Event3Key = new object();
   6:     private static readonly object Event4Key = new object();
   7:     // stores all active events
   8:     private readonly Dictionary<object, Delegate> events = new Dictionary<object, Delegate>();
   9:  
  10:     public event EventHandler Event1 {
  11:         add { AddHandler( Event1Key, value ); }
  12:         remove { RemoveHandler( Event1Key, value ); }
  13:     }
  14:  
  15:     public event EventHandler Event2 {
  16:         add { AddHandler( Event2Key, value ); }
  17:         remove { RemoveHandler( Event2Key, value ); }
  18:     }
  19:  
  20:     public event EventHandler Event3 {
  21:         add { AddHandler( Event3Key, value ); }
  22:         remove { RemoveHandler( Event3Key, value ); }
  23:     }
  24:  
  25:     public event EventHandler Event4 {
  26:         add { AddHandler( Event4Key, value ); }
  27:         remove { RemoveHandler( Event4Key, value ); }
  28:     }
  29:  
  30:     // add a handler using the dictionary
  31:     private void AddHandler( object key, Delegate value ) {
  32:         Delegate old;
  33:         if ( events.TryGetValue( key, out old ) ) {
  34:             // Handler exists, combine old with new
  35:             events[ key ] = Delegate.Combine( old, value );
  36:         } else {
  37:             // Handler does not exist
  38:             events[ key ] = value;
  39:         }
  40:     }
  41:  
  42:     // remove a handler using the dictionary
  43:     private void RemoveHandler( object key, Delegate value ) {
  44:         Delegate old;
  45:         if ( events.TryGetValue( key, out old ) ) {
  46:             // Handler exists, remove it
  47:             events[ key ] = Delegate.Remove( old, value );
  48:         }
  49:     }
  50:  
  51:     // raises a specific event
  52:     protected void Raise( object key, object sender, EventArgs e ) {
  53:         Delegate handler;
  54:         if ( events.TryGetValue( key, out handler ) ) {
  55:             // assume we use a delegate format similar to EventHandler, 
  56:             // i.e.: it expects (object sender, EventArgs e) as arguments
  57:             handler.DynamicInvoke( sender, e );
  58:         }
  59:     }
  60:  
  61:     // Some method that raises an event
  62:     public void SomeMethod() {
  63:         // ...
  64:         // Raise Event1
  65:         Raise( Event1Key, this, EventArgs.Empty );
  66:         // Raise Event3
  67:         Raise( Event3Key, this, EventArgs.Empty );
  68:     }
  69: }

Wichtig hierbei:

  • Es macht Sinn, das Hinzufügen/Entfernen der Handler zum/vom Dictionary in eigene Methoden auszulagern (AddHandler/RemoveHandler)
  • Man kann Events, die eigene add/remove-Methoden implementieren nicht mehr über EventName() auslösen. Statt dessen muss hier der entsprechende Delegat im Verzeichnis gefunden und aufgerufen werden. Da das Verzeichnis hier sehr allgemeingültig ist (die Delegaten werden als Delegate-Referenzen gespeichert), verwende ich hier Delegate.DynamicInvoke. Wäre der Typ des Delegaten klar definiert (z.b. EventHandler), könnte auch der normale Aufruf mittels () verwendet werden. Die Variante mit DynamicInvoke hat den Vorteil, dass unterschiedlichste Eventhandler-Delegaten verwendet werden, solange sie alle vergleichbare Parameter haben. Das erklärt übrigens auch, warum Microsoft das Format HandlerDelegate(object sender, EventArgs e) forciert.
  • Die Verwendung von Dictionary ist sicherlich nicht die platzsparendste Variante, dazu hat die Klasse einfach zu viel verwaltungstechnischen Overhead. Das .NET Framework bietet für diese Zwecke eine sehr schlanke Klasse an: System.ComponentModel.EventHandlerList (Beispiel). Die Variante mit Dictionary soll nur veranschaulichen, wie das Konzept grundsätzlich funktioniert.

Die Verwendung der Events ändert sich dadurch aber nicht:

   1: class Program {
   2:     private static void Main() {
   3:         MyClass mc = new MyClass();
   4:         // use as always
   5:         mc.Event1 += MyClassHandler;
   6:         mc.Event2 += MyClassHandler;
   7:         mc.Event3 += MyClassHandler;
   8:         mc.Event4 += MyClassHandler;
   9:         mc.SomeMethod();
  10:     }
  11:  
  12:     private static void MyClassHandler( object sender, EventArgs e ) {
  13:         Console.WriteLine( "Event raised" ); 
  14:     }
  15: }

Posted in: C# | FAQ

Tags: , ,