Eigene Container auf die Darstellung in Oberflächen vorbereiten

Veröffentlicht: 12. Mrz 2003 | Aktualisiert: 09. Nov 2004

Von Thomas Fenske

dot.net magazin - Partner von MSDN Online

Diesen Artikel können Sie dank freundlicher Unterstützung von dot.net magazin auf MSDN Online lesen.
dot.net magazin ist ein Partner von MSDN Online.

Partner von MSDN Online

Auf dieser Seite

 Das Ziel
 Die Test-Anwendung
 ITypedList und PropertyDescriptor
 TypeConverter
 IBindingList
 Fazit

Der Bau eigener Container zur Verwaltung typisierter Instanzen erfreut sich großer Beliebtheit, da hier der Compiler mehr Prüfungen durchführen kann. Anleitung hierzu gibt unter anderem [1]. Dabei wird aber häufig übersehen, dass es auch an anderer Stelle Sinn macht, eigene Container zu entwickeln. Für die Anbindung von Containern an Oberflächen-Controls bietet das .NET Framework interessante Hilfsmittel, die eine genauere Betrachtung wert sind. Wer den Artikel über ExtenderProvider [2] gelesen hat, hat vielleicht schon einen Eindruck bekommen, was für ausgebuffte Zeitgenossen sich im Namespace System.ComponentModel herumtreiben. Bei unserem heutigen Ausflug in die Welt der schönen bunten Neuerungen des .NET Frameworks werden wir uns mit nur einem Interface nicht mehr zufrieden geben. Stattdessen spannen wir eine kleine Gang von Interfaces und Basisklassen vor unseren Karren.

Das Ziel

In .NET hat Microsoft eine konsequente Trennung von Daten und Darstellung durchgeführt. Dadurch kann man nun eine Liste von Objekten einer ListBox oder einem DataGrid als DataSource zuordnen. Der Inhalt dieser Liste wird von diesen Controls sofort angezeigt. Die Daten werden dabei nicht einfach aus der DataSource in das Control kopiert. Die zugewiesene Liste selbst ist der Inhalt des Controls. Ganz besonders deutlich wird dieser Umstand, wenn ein und dieselbe Liste mehreren Controls zugewiesen wird. In diesem Fall haben alle Controls die selbe Selektion. Wählt man in dem einen Control einen Eintrag aus, wird derselbe Eintrag auch in den anderen Controls selektiert.

Das angezeigte Ergebnis hängt sehr stark von der Beschaffenheit der DataSource ab. Der Inhalt einer DataSet-Instanz beispielsweise wird deutlich zufrieden stellender dargestellt als der Inhalt einer ArrayList. Wenn Sie aber nicht mit Datensätzen aus einer Datenbank, sondern mit Geschäftsobjekten arbeiten, können Sie mit DataSets nur bedingt etwas anfangen. Vielmehr brauchen wir einen Container, der ebenso wie ein DataSet die jeweiligen Controls mit allen notwendigen Informationen versorgt. Außerdem sollte die Darstellung von den im Container enthaltenen Objekten abhängen, sodass wir das spätere Erscheinungsbild vollständig durch die Implementierung der Geschäftsklassen steuern können. Der Bau eines solchen Containers ist das Ziel dieses Artikels.

 

Die Test-Anwendung

Bevor wir uns der eigentlichen Arbeit zuwenden, erstellen wir zunächst eine Test-Anwendung, an der wir den Fortschritt unserer Arbeit erkennen können. Außerdem können wir anhand der Ergebnisse feststellen, welche Features unserem Container noch fehlen.

Die Anwendung besteht aus einer Windows-Applikation mit zwei Geschäftsklassen Person und Country siehe Listing 1) und einem Formular mit verschiedenen Controls zum Anzeigen von Listen. Als Controls habe ich eine System.Windows.Forms.ListBox, ein System.Windows.Forms.DataGrid und - als Vertreter von Drittanbietern - ein DevExpress.XtraGrid.GridControl der Firma DevExpress gewählt.

Listing 1

public class Person 
{ 
        private string firstName_; 
        private string lastName_; 
        private DateTime dateOfBirth_; 
        private Country country_; 
        public Person(string firstName, string lastName, DateTime dateOfBirth, Country country) 
        { 
                firstName_ = firstName; 
                lastName_ = lastName; 
                dateOfBirth_ = dateOfBirth; 
                country_ = country; 
        } 
        public string FirstName 
        { 
                get 
                { 
                        return firstName_; 
                } 
                set 
                { 
                        firstName_ = value; 
                } 
        } 
        public string LastName 
        { 
                get 
                { 
                        return lastName_; 
                } 
                set 
                { 
                        lastName_ = value; 
                } 
        } 
        public DateTime DateOfBirth 
        { 
                get 
                { 
                        return dateOfBirth_; 
                } 
                set 
                { 
                        dateOfBirth_ = value; 
                } 
        } 
        public Country Country 
        { 
                get 
                { 
                        return country_; 
                } 
                set 
                { 
                        country_ = value; 
                } 
        } 
} 
public class Country 
{ 
        private string name_; 
        private string shortName_; 
        private static System.Collections.Hashtable instances_ = new System.Collections.Hashtable(); 
        public static Country Create(string name, string shortName) 
        { 
                Country retval = instances_[shortName] as Country; 
                if (retval == null) 
                { 
                        retval = new Country(name, shortName); 
                        instances_.Add(shortName, retval); 
                } 
                return retval; 
        } 
        private Country(string name, string shortName) 
        { 
                name_ = name; 
                shortName_ = shortName; 
        } 
        public string Name 
        { 
                get 
                { 
                        return name_; 
                } 
        } 
        public string ShortName 
        { 
                get 
                { 
                        return shortName_; 
                } 
        } 
        public static Country GetCountry(string shortName) 
        { 
                return instances_[shortName] as Country; 
        } 
}

Die beiden Klassen Person und Country sind unspektakulär. Eine Person besitzt Vor- und Nachname, ein Geburtsdatum und eine Beziehung zu dem Land, in dem sie wohnt. Ein Land besteht aus einem Namen und einer Kurzbezeichnung. Die Klasse Country hat darüber hinaus noch ein paar statische Member (instances_, Create() und GetCountry()) zur Verwaltung ihrer Instanzen.

Hinter den Knöpfen verbirgt sich der in Listing 2 aufgeführte Code. Wir werden diesen Code während der ganzen Entwicklung nicht ändern. Lediglich die erste Zeile der Methode OnCreateListClick wird beim ersten Durchlauf collection_ = new ArrayList() heißen. Erst wenn wir mit der Entwicklung eines Containers begonnen haben, wird die Zeile auf die abgebildete Version geändert. Das Feld collection_ ist vom Typ System.Collections.IList. In der Methode InitializeComponent() werden über Country.Create() Instanzen für die Länder Deutschland, Österreich und Schweiz angelegt. Schließlich und endlich wird die Ausgabe von System.Console.Error auf die Textbox errorTextBox_ umgeleitet.

Listing 2

private void OnCreateListClick(object sender, System.EventArgs e) 
{ 
        collection_ = new TypedCollection(typeof(Person)); 
        collection_.Add(new Person("Peter", "Müller", 
          new DateTime(1967, 5, 10, 0, 0, 0, 0), Country.GetCountry("D"))); 
        collection_.Add(new Person("Karsten", "Schmidt", 
          new DateTime(1971, 9, 23, 0, 0, 0, 0), Country.GetCountry("CH"))); 
        collection_.Add(new Person("Ulrike", "Kretschmer", 
          new DateTime(1962, 3, 5, 0, 0, 0, 0), Country.GetCountry("A"))); 
        listBox1.DataSource = collection_; 
        dataGrid1.DataSource = collection_; 
        gridControl1.DataSource = collection_; 
        gridControl1.MainView.PopulateColumns(); 
} 
private void OnAddElementClick(object sender, System.EventArgs e) 
{ 
        try 
        { 
                collection_.Add(new Person("Bernd", "Herrmann", 
                  new DateTime(1979, 12, 10, 0, 0, 0, 0), Country.GetCountry("D"))); 
        } 
        catch(Exception exception) 
        { 
                Console.Error.WriteLine(exception.Message); 
        } 
} 
private void OnRemoveElementClick(object sender, System.EventArgs e) 
{ 
        try 
        { 
                int index = listBox1.SelectedIndex; 
                collection_.RemoveAt(index); 
        } 
        catch(Exception exception) 
        { 
                Console.Error.WriteLine(exception.Message); 
        } 
} 
private void OnChangeElementClick(object sender, System.EventArgs e) 
{ 
        try 
        { 
                int index = listBox1.SelectedIndex; 
                collection_[index] = new Person("Beate", "Schulze", 
                  new DateTime(1987, 9, 18, 0, 0, 0, 0), Country.GetCountry("CH")); 
        } 
        catch(Exception exception) 
        { 
                Console.Error.WriteLine(exception.Message); 
        } 
}

Wenn wir die Anwendung starten und auf den Knopf LISTE ERZEUGEN drücken, erhalten wir das in Abbildung 1 dargestellte Ergebnis.

Fenske_TypedCollections_1

Abb. 1: Test unter Verwendung einer ArrayList

Die Darstellung in den Grid-Controls ist auf den ersten Blick gar nicht so schlecht. Die ListBox enttäuscht allerdings. Doch hier können wir durch Überschreiben der Methode Person.ToString() schnell Abhilfe schaffen (siehe Abb. 2):

public override string ToString() 
{ 
        return LastName + ", " + FirstName; 
}

Fenske_TypedCollections_2

Abb. 2: Nach dem Überschreiben von Person.ToString

Bei den Grid-Controls ist die merkwürdige und scheinbar beliebige Reihenfolge der Spalten am auffälligsten. Auch die Beschriftung der Spaltenköpfe ist zu technisch, da sie einfach die Namen der Eigenschaften der Klasse Person tragen. Der unschöne Inhalt der Spalte Country könnte zwar ebenfalls durch Überschreiben von ToString() behoben werden, aber das ignorieren wir vorerst. Hier kommt später eine andere Technik zum Einsatz.

 

ITypedList und PropertyDescriptor

Bevor wir überhaupt eines der genannten Probleme angehen können, müssen wir die bisher verwendete ArrayList durch einen eigenen Container ersetzen. Listing 3 zeigt die erste Version unserer TypedCollection. Auch wenn der Codeumfang zunächst beeindrucken mag, handelt es sich hierbei lediglich um eine einfache Klasse, die das IList-Interface (sowie transitiv ICollection und IEnumerable) erfüllt und alle Aufgaben an das private Feld objects_ delegiert.

Derartige Implementierungen finden sich zuhauf im Internet und in Tutorials, die den Bau einer eigenen Collection erklären. Ich habe hier eine recht simple Implementierung gewählt. Zur Lösung komplexerer Aufgabenstellungen könnte man statt einer ArrayList auch andere Container wie Hashtable, Stack oder Ähnliches wählen. Dieser Umstand spielt für die weitere Betrachtung aber keine Rolle. Damit wir den neuen Container besser wieder verwenden können, erstellen wir ein eigenes Projekt vom Typ Klassenbibliothek, anstatt ihn im Projekt der Test-Anwendung anzulegen.

Bis hierher verhält sich die TypedCollection wie eine ArrayList. Das Ablaufen der Test-Anwendung führt zum selben Ergebnis wie in Abbildung 2. Der einzige Unterschied besteht darin, dass wir uns auf einen Element-Typ festlegen müssen, den wir beim Konstruktor mit übergeben. In den Methoden Insert(), Add() und CopyTo() sowie beim Indexer prüfen wir den Typ der hinzuzufügenden Objekte und werfen gegebenenfalls eine InvalidTypeException. Diese Ausnahme ist ebenfalls Teil unserer neuen Klassenbibliothek. Die Implementierung ist so langweilig, dass ich sie hier nicht zeige.

Listing 3

public class TypedCollection : IList 
{ 
        private ArrayList objects_ = new ArrayList(); 
        private Type objectType_; 
        public TypedCollection(Type objectType) 
        { 
                objectType_ = objectType; 
        } 
        public void RemoveAt(int index) 
        { 
                objects_.RemoveAt(index); 
        } 
        public void Insert(int index, object value) 
        { 
                if (value == null || value.GetType().Equals(objectType_)) 
                        throw new InvalidTypeException(objectType_, value); 
                objects_.Insert(index, value); 
        } 
        public void Remove(object value) 
        { 
                int index = objects_.IndexOf(value); 
                objects_.Remove(value); 
        } 
        public bool Contains(object value) 
        { 
                return objects_.Contains(value); 
        } 
        public void Clear() 
        { 
                objects_.Clear(); 
        } 
        public int IndexOf(object value) 
        { 
                return objects_.IndexOf(value); 
        } 
        public int Add(object value) 
        { 
                if (value == null || !(value.GetType()).Equals(objectType_)) 
                        throw new InvalidTypeException(objectType_, value); 
                int index = objects_.Add(value); 
                return index; 
        } 
        public bool IsReadOnly 
        { 
                get 
                { 
                        return objects_.IsReadOnly; 
                } 
        } 
        public object this[int index] 
        { 
                get 
                { 
                        return objects_[index]; 
                } 
                set 
                { 
                        if (value == null || !value.GetType().Equals(objectType_)) 
                                throw new InvalidTypeException(objectType_, value); 
                        objects_[index] = value; 
                } 
        } 
        public bool IsFixedSize 
        { 
                get 
                { 
                        return objects_.IsFixedSize; 
                } 
        } 
        public void CopyTo(System.Array array, int index) 
        { 
                bool throwException = false; 
                foreach(object obj in array) 
                { 
                        if (array == null) 
                        { 
                                throwException = true; 
                                break; 
                        } 
                        if (!(obj.GetType()).Equals(objectType_)) 
                        { 
                                throwException = true; 
                                break; 
                        } 
                } 
                if (throwException) 
                        throw new InvalidTypeException(objectType_); 
                objects_.CopyTo(array, index); 
        } 
        public bool IsSynchronized 
        { 
                get 
                { 
                        return objects_.IsSynchronized; 
                } 
        } 
        public int Count 
        { 
                get 
                { 
                        return objects_.Count; 
                } 
        } 
        public object SyncRoot 
        { 
                get 
                { 
                        return objects_.SyncRoot; 
                } 
        } 
        public System.Collections.IEnumerator GetEnumerator() 
        { 
                return objects_.GetEnumerator(); 
        } 
}

Mit der ersten Erweiterung wollen wir den Controls nähere Informationen über die Spalten unseres Containers geben. Das .NET Framework hält dazu das Interface System.ComponentModel.ITypedList parat, das erfreulicherweise lediglich zwei Methoden (GetListName() und GetItemProperties()) umfasst. Die Implementierung von GetListName ist nicht weiter schwierig. Da wir den Typ der verwalteten Elemente kennen, geben wir einfach den Klassennamen zurück:

string ITypedList.GetListName(System.ComponentModel.PropertyDescriptor[] listAccessors) 
{ 
        return objectType_.Name; 
}

Leider ist die Methode GetItemProperties() nicht ganz so einfach zu implementieren. Zwar können wir den Parameter wie in GetListName() ignorieren, aber dafür müssen wir eine PropertyDescriptionCollection zurückgeben.

Was zum Teufel ist ein PropertyDescriptor? Ein PropertyDescriptor stellt eine Beschreibung für die Eigenschaft einer Klasse dar. Über einen PropertyDescriptor kann man erfahren, ob man eine Eigenschaft verändern kann (IsReadOnly), welchen Rückgabetyp sie besitzt (PropertyType), welcher Namen auf der Oberfläche erscheinen soll (DisplayName) und noch einiges Nützliche mehr.

Der eine oder andere von Ihnen fühlt sich vielleicht in diesem Moment an die Klasse System.Reflection.PropertyInfo erinnert und meint ein kleines Déjà-vu zu erleben. Waren das nicht genau die Aufgaben einer PropertyInfo? Ja und nein. Eine PropertyInfo repräsentiert die Metainformationen der Implementierung einer Eigenschaft. Sie können sich zwar die PropertyInfo einer Eigenschaft ansehen, aber Sie können keine eigene PropertyInfo-Instanz anlegen, um damit das Erscheinungsbild einer Eigenschaft nach außen hin zu verändern. Dies ist Aufgabe der PropertyDescriptor-Klasse. Während also eine PropertyInfo eine Eigenschaft beschreibt, wie sie technisch beschaffen ist, können Sie mit einem PropertyDescriptor Eigenschaften nach außen hin so erscheinen lassen, wie Sie es möchten. Und genau das werden wir gleich tun.

Mit der Implementierung eines eigenen PropertyDescriptors wollen wir drei Dinge erreichen:

  1. Die Spalten in den Grid-Controls sollen die Reihenfolge der implementierten Eigenschaften in den Geschäftsklassen widerspiegeln.

  2. Die Überschriften der Spalten sollen bei der Implementierung der Geschäftsklasse frei bestimmbar sein.

  3. Spalten sollen auf schreibgeschützt gesetzt werden können, um beispielsweise das Ändern des Geburtsdatums zu verhindern.

Punkt 1 werden wir dadurch erfüllen, dass wir die PropertyDescriptors in der Reihenfolge herausreichen, in der wir die PropertyInfos von Type.GetProperties() erhalten. Für die Punkte 2. und 3. greifen wir auf die Technik der Attributierung in .NET zurück, um die fehlenden Informationen spezifizieren zu können. Dazu erstellen wir eine Attribut-Klasse Caption, der wir als Parameter einen String mitgeben, sowie eine parameterlose Attribut-Klasse ReadOnly. Die Eigenschaft unserer Klasse Person versehen wir nun mit den neuen Attributen:

using DotnetMagazin.Collections.Attributes; 
… 
public class Person 
{ 
        … 
        [Caption("Vorname")] 
        public string FirstName 
        {…} 
        [Caption("Nachname")] 
        public string LastName 
        {…} 
        [Caption("Geburtsdatum"), ReadOnly] 
        public DateTime DateOfBirth 
        {…} 
        [Caption("Land")] 
        public Country Country 
        {…} 
}

Innerhalb des Konstruktors von TypedCollection wird nun die neue private Methode AnalyseObjectType() aufgerufen, die alle PropertyInfos zu den Eigenschaften der Geschäftsklasse ermittelt und in die Liste properties_ schreibt. Auf diese Liste wird innerhalb von GetItemProperties() zurückgegriffen, um die Property-Deskriptoren zu erzeugen (siehe Listing 4).

Listing 4

private void AnalyseObjectType() 
{ 
        foreach(PropertyInfo propertyInfo in objectType_.GetProperties()) 
        { 
                properties_.Add(propertyInfo); 
        } 
} 
System.ComponentModel.PropertyDescriptorCollection 
   ITypedList.GetItemProperties(System.ComponentModel.PropertyDescriptor[] listAccessors) 
{ 
        System.Collections.ArrayList descriptors = new System.Collections.ArrayList(); 
        foreach(PropertyInfo propertyInfo in properties_) 
        { 
                descriptors.Add(new PropertyDescriptor(propertyInfo.Name, propertyInfo)); 
        } 
        System.ComponentModel.PropertyDescriptor[] propertyDescriptors = 
          new System.ComponentModel.PropertyDescriptor[descriptors.Count]; 
        descriptors.CopyTo(propertyDescriptors, 0); 
        return new PropertyDescriptorCollection(propertyDescriptors); 
}

Die Implementierung enthält keine großen Überraschungen. In einer Schleife über alle PropertyInfos werden der Reihe nach Property-Deskriptoren angelegt. Die etwas umständlich wirkenden Zeilen nach der Schleife ergeben sich aus der Tatsache, dass der Konstruktor von PropertyDescriptorCollection unbedingt ein "echtes" PropertyDescriptor[]-Array haben möchte. Die eigentliche Arbeit wird auf die Instanzen vom Typ DotnetMagazin.Collections.PropertyDescriptor verlagert, dessen Code in Listing 5 dargestellt ist.

Listing 5

public class PropertyDescriptor : System.ComponentModel.PropertyDescriptor 
{ 
        private PropertyInfo propertyInfo_; 
        private System.ComponentModel.PropertyDescriptor origPropertyDescriptor_; 
        internal PropertyDescriptor(string name, PropertyInfo propertyInfo) : base(name, null) 
        { 
                propertyInfo_ = propertyInfo; 
                origPropertyDescriptor_ = FindOrigPropertyDescriptor(propertyInfo_); 
        } 
        public override object GetValue(object component) 
        { 
                return origPropertyDescriptor_.GetValue(component); 
        } 
        public override void SetValue(object component, object value) 
        { 
                origPropertyDescriptor_.SetValue(component, value); 
        } 
        public override bool IsReadOnly 
        { 
                get 
                { 
                        bool isReadonly = 
                        propertyInfo_.GetCustomAttributes(typeof( 
                          DotnetMagazin.Collections.Attributes.ReadOnlyAttribute), true).Length != 0; 
                        return origPropertyDescriptor_.IsReadOnly || isReadonly; 
                } 
        } 
        public override System.Type PropertyType 
        { 
                get 
                { 
                        return origPropertyDescriptor_.PropertyType; 
                } 
        } 
        public override bool CanResetValue(object component) 
        { 
                return origPropertyDescriptor_.CanResetValue(component); 
        } 
        public override System.Type ComponentType 
        { 
                get 
                { 
                        return origPropertyDescriptor_.ComponentType; 
                } 
        } 
        public override void ResetValue(object component) 
        { 
                origPropertyDescriptor_.ResetValue(component); 
        } 
        public override bool ShouldSerializeValue(object component) 
        { 
                return origPropertyDescriptor_.ShouldSerializeValue(component); 
        } 
        private static System.ComponentModel.PropertyDescriptor 
                FindOrigPropertyDescriptor(PropertyInfo propertyInfo) 
        { 
                foreach(System.ComponentModel.PropertyDescriptor 
                  propertyDescriptor in 
                  System.ComponentModel.TypeDescriptor.GetProperties(propertyInfo.DeclaringType)) 
                { 
                        if(propertyDescriptor.Name.Equals(propertyInfo.Name)) 
                                return propertyDescriptor; 
                } 
                return null; 
        } 
        public override string DisplayName 
        { 
                get 
                { 
                        object[] attributes = 
                                 propertyInfo_.GetCustomAttributes(typeof(CaptionAttribute), true); 
                        if (attributes.Length == 1) 
                        { 
                                CaptionAttribute caption = (CaptionAttribute) attributes[0]; 
                                return caption.Value; 
                        } 
                        return origPropertyDescriptor_.DisplayName; 
                } 
        } 
}

Auch hier gilt wieder, dass sich einige wenige interessante Zeilen (fett hervorgehoben) zwischen vielen eher langweiligen verstecken. Der Grund dafür ist das Fehlen einer umfassenden Standard-Implementierung. Die für Erweiterungen vorgesehene Klasse System.ComponentModel.PropertyDescriptor besitzt fast ausschließlich abstrakte Member und die Klasse System.ComponentModel.ReflectPropertyDescriptor, von der die Instanzen stammen, die FindOrigPropertyDescriptor ermittelt, ist leider internal. Sie steht damit nicht als Basisklasse zur Verfügung. Also schreiben wir eine Klasse, die fast alles an origPropertyDescriptor_ delegiert. Lediglich bei IsReadOnly und DisplayName wird überprüft, ob entsprechende Attribute in den Geschäftsklassen abweichende Informationen liefern.

Wenn wir nun erneut die Test-Anwendung laufen lassen, stellen wir fest, dass die drei angestrebten Ziele - Spaltenreihenfolge, Spaltenbeschriftung und Bearbeitungssteuerung - erfüllt sind (siehe Abb. 3).

Fenske_TypedCollections_3

Abb. 3: Ergebnis nach Einführung von ITypedList und PropertyDescriptor

Doch halt! Nur die Spaltenbeschriftungen des XtraGrids sind richtig. Das DataGrid von Microsoft hat hier offensichtlich einen Fehler. Erst wenn man auch die Eigenschaft Name von PropertyDescriptor überschreibt, werden die Spaltenköpfe richtig beschriftet. Unterlassen wir dagegen das Überschreiben von DisplayName, sind die Spaltenbeschriftungen des XtraGrids falsch.

 

TypeConverter

Zunächst die gute Nachricht: Den schwierigsten Teil haben wir hinter uns. Dennoch können wir mit der Darstellung der Spalte Land nicht zufrieden sein. Ich hatte bisher bewusst auf die oben schon gezeigte Lösung mit ToString() verzichtet, weil diese nur das Darstellungsproblem löst. Was aber, wenn eine Person in ein anders Land umzieht und wir diese Änderung eingeben möchten?

Um das Umwandeln eines Typen in einen andern Typen zu steuern, besitzt das .NET Framework die Basisklasse System.ComponentModel.TypeConverter. Ein TypeConverter stellt für einen bestimmten Typen Konvertierungsdienste von diesem und in diesen bereit. In unserem Fall benötigen wir einen TypeConverter für die Klasse Country.

Zur Konvertierungssteuerung verwenden wir die vier Methoden CanConvertTo(), CanConvertFrom(), ConvertTo() und ConvertFrom(). Obwohl diese Methoden mehrfach überladen sind, muss bzw. kann nur jeweils eine Version überschrieben werden. Wie die Namen der Methoden nahe legen, kann ein TypeConverter gefragt werden, ob er eine Konvertierung in bzw. von einem gegebenen Typ durchführen kann (CanConvert…). Die eigentliche Konvertierung geschieht in den Methoden ConvertFrom() und ConvertTo(). Da wir nur Konvertierung von und zur Klasse string benötigen, ist die Implementierung recht übersichtlich siehe Listing 6).

Listing 6

public class CountryTypeConverter : TypeConverter 
{ 
        public override bool CanConvertTo(System.ComponentModel.ITypeDescriptorContext context, 
               System.Type destinationType) 
        { 
                if (destinationType == typeof(string)) 
                        return true; 
                return false; 
        } 
        public override bool CanConvertFrom(System.ComponentModel.ITypeDescriptorContext context, 
               System.Type sourceType) 
        { 
                if (sourceType == typeof(string)) 
                        return true; 
                return false; 
        } 
        public override object ConvertTo(System.ComponentModel.ITypeDescriptorContext context, 
               System.Globalization.CultureInfo culture, object value, System.Type destinationType) 
        { 
                if (destinationType == typeof(string)) 
                { 
                        Country country = (Country) value; 
                        return country.Name; 
                } 
                else 
                        return null; 
        } 
        public override object ConvertFrom(System.ComponentModel.ITypeDescriptorContext 
               context, System.Globalization.CultureInfo culture, object value) 
        { 
                if (value.GetType() == typeof(string)) 
                { 
                        return Country.GetCountry((string)value); 
                } 
                else 
                        return null; 
        } 
}

Die Methode ConvertTo() macht genau das, was wir gegebenenfalls auch in einer Methode Country.ToString() hätten implementieren können. Dagegen verdient die Methode ConvertFrom() eine genauere Betrachtung. Hier macht es sich bezahlt, dass wir alle Instanzen der Klasse Country an einer zentralen Stelle verwalten. Denn so können wir mit der übergebenen Zeichenkette sehr leicht das passende Land ermitteln. Beachten Sie aber, dass die Methode GetCountry() nach der Kurzbezeichnung sucht. Um also Herrn Müller von Deutschland nach Österreich umziehen zu lassen, müssen wir ein A in der Spalte Land eintragen.

Das funktioniert aber erst, wenn die Klasse Country weiß, wer für ihre Konvertierungen verantwortlich ist. Diese Zuordnung erreichen wir durch das Attribut TypeConverter vor der Klasse Country:

[TypeConverter(typeof(CountryTypeConverter))] 
public class Country 
{…}

Nach einem erneuten Lauf unserer Test-Anwendung stellen wir fest, dass dieses Mal das XtraGrid einen Fehler hat (siehe Abb. 4). Denn es verwendet nur beim Setzen neuer Werte den TypeConverter. Bei der Anzeige wird stur aber leider falsch die Methode ToString() aufgerufen. Immerhin: Auch wenn man im XtraGrid die Kurzbezeichnung eines Landes eingibt, wird das Land korrekt gewechselt.

Fenske_TypedCollections_4

Abb. 4: Der CountryTypeConverter ermöglicht einen einfachen Wechsel der Landeszuordnung

 

IBindingList

Wofür sind eigentlich die andern Knöpfe auf dem Formular da, wenn wir bisher immer nur den ersten zum Testen verwendet haben? Drücken Sie doch einmal die anderen Knöpfe. Sie werden zunächst keine Änderung feststellen. Offensichtlich bekommen die Controls Änderungen an der Liste nicht mit. Wer weiß, wie wir dieses Problem lösen können? Richtig, mit IBindingList. Sie haben doch nicht etwa bei der Zwischenüberschrift abgeguckt? Natürlich liegt auch dieses Interface wieder in System.ComponentModel.

Also frisch ans Werk: Wir fügen der Interface-Liste von TypedCollection den Eintrag IBindingList hinzu, wählen in der Klassenansicht des Visual Studio die Funktion SCHNITTSTELLE IMPLEMENTIEREN und füllen die … oh je, das ist aber eine ganze Menge von Methoden! Keine Sorge. Für unsere Zwecke brauchen wir das Interface nicht weiter zu implementieren. Es sind nur einige wenige Änderungen durchzuführen: Die Eigenschaften AllowRemove, AllowEdit und SupportsChangeNotification müssen true liefern. Alle andern Eigenschaften liefern false. In erster Linie brauchen wir dieses Interface wegen des Ereignisses ListChanged. Denn wenn ein Container dieses Interface erfüllt und zusätzlich SupportsChangeNotification mit true beantwortet, melden sich die Controls bei diesem Ereignis an. Alles, was wir noch tun müssen, ist, das Ereignis ListChanged zu feuern, wenn ein Element der Liste hinzugefügt bzw. entnommen wird. Listing 7 zeigt die notwendigen Änderungen an TypedCollection fett hervorgehoben.

Listing 7

public void RemoveAt(int index) 
{ 
        objects_.RemoveAt(index); 
        if (ListChanged != null) 
                ListChanged(this, new ListChangedEventArgs(ListChangedType.ItemDeleted, index, 0)); 
} 
public void Insert(int index, object value) 
{ 
        if (value == null || value.GetType().Equals(objectType_)) 
                throw new InvalidTypeException(objectType_, value); 
        objects_.Insert(index, value); 
        if (ListChanged != null) 
                ListChanged(this, new ListChangedEventArgs(ListChangedType.ItemAdded, index, 0)); 
} 
public void Remove(object value) 
{ 
        int index = objects_.IndexOf(value); 
        objects_.Remove(value); 
        if (ListChanged != null) 
                ListChanged(this, new ListChangedEventArgs(ListChangedType.ItemDeleted, index, 0)); 
} 
public int Add(object value) 
{ 
        if (value == null || !(value.GetType()).Equals(objectType_)) 
                throw new InvalidTypeException(objectType_, value); 
        int index = objects_.Add(value); 
        if (ListChanged != null) 
                ListChanged(this, new ListChangedEventArgs(ListChangedType.ItemAdded, index, 0)); 
        return index; 
} 
public object this[int index] 
{ 
        get 
        { 
                return objects_[index]; 
        } 
        set 
        { 
                if (value == null || !value.GetType().Equals(objectType_)) 
                        throw new InvalidTypeException(objectType_, value); 
                objects_[index] = value; 
                if (ListChanged != null) 
                        ListChanged(this, new ListChangedEventArgs(ListChangedType.ItemChanged, index, 0)); 
        } 
}

Geschafft! Wenn wir nun ein letztes Mal die Test-Anwendung laufen lassen, führt die Verwendung der übrigen Knöpfe zu sichtbaren Ergebnissen (siehe Abb. 5).

Fenske_TypedCollections_5

Abb. 5: Bis auf den Fehler im XtraGrid verhalten sich die Controls wie gewünscht

 

Fazit

Auch wenn etwas mehr zu tun war, als ein Interface zu implementieren, haben wir mit relativ wenig Aufwand einen Container erhalten, der für die Darstellung beliebiger Objekte geeignet ist und dabei nur unwesentlich mehr Speicherplatz und Laufzeit erfordert als eine einfache ArrayList. Darüber hinaus ist dieser Container universell einsetzbar. Individuelle Darstellungsmerkmale können bei den Element-Klassen über Objekte eingestellt werden und bedürfen keiner Änderung am Container.

Microsofts Verdienst bei der ganzen Sache ist, hier eine Palette von Standard-Interfaces und Standard-Klassen bereitgestellt zu haben, die den Entwickler unabhängig von den jeweils eingesetzten Controls machen. Der Austausch eines Grid-Controls beispielsweise sollte dadurch deutlich einfacher werden. Bisherige Oberflächenanwendungen, denen derartige Standard-Schnittstellen nicht zur Verfügung stehen, sind häufig sehr stark auf die verwendeten Controls abgestimmt. Ein Austausch einzelner Komponenten führt nicht selten zur Neuentwicklung weiter Teile der Oberfläche.

Dass auch hier mal wieder Theorie und Praxis ein wenig auseinander driften, zeigen die vorhandenen Fehler bzw. Unregelmäßigkeiten bei den getesteten Grid-Controls. Allerdings haben diese in der Regel erst den ersten Release-Zyklus hinter sich, sodass man wohl ein wenig Nachsicht walten lassen kann.

Ich hoffe, Sie neugierig auf die vorgestellten Techniken und Schnittstellen im .NET Framework gemacht zu haben. Mir war es wichtig, dieses Mal mehr in die Breite als in die Tiefe zu gehen, da meiner Erfahrung nach bei isolierter Betrachtung der Nutzen vieler Techniken nur schwer zu erkennen ist.

Links & Literatur
[1] Marcel Gnoth, "Alternative Datenhaltung, Datenstrukturen im .NET Framework - Teil 2, dot.net magazin 4.2002
[2] Thomas Fenske, "Entdeckungsreisen, Die Schnittstelle IExtenderProvider", dot.net magazin 6.2002