Dojos für Entwickler. Stefan Lieser
get; set; }
}
Die Eigenschaft ControlType enthält den Typ des zu verwendenden Controls. Genügt ein Textfeld, kann hier typeof(TextBox) gesetzt werden. In komplizierteren Fällen wird der Typ eines dafür implementierten UserControls gesetzt.
Um für den ausgewählten Generatortyp eine ColumnDefinition erzeugen zu können, habe ich eine Eigenschaft ergänzt, die eine Funktion erhält, die genau dies bewerkstelligt: Sie erzeugt eine ColumnDefinition. Dazu erhält sie als Eingangsparameter zum einen den Namen der Spalte, zum anderen das Control mit allen weiteren Angaben. Da der Typ des Controls variabel ist, wird es vom Typ object übergeben. Die Funktion muss dieses Objekt dann auf den erwarteten Typ casten.
Bei der Initialisierung des Portals wird für die verfügbaren Generatoren jeweils eine Spaltendefinition erzeugt und in die Item-Liste des Dropdown-Controls gestellt, siehe Listing 11.
Listing 11: Spalten definieren.
new SpaltenDefinition {
Bezeichnung = "Random DateTime",
ControlType = typeof(MinimumMaximum),
Columndefinition = (columnName, control) => new ColumnDefinition(columnName,
new RandomDateTimeGenerator(
DateTime.Parse(((MinimumMaximum)control).Minimum),
DateTime.Parse(((MinimumMaximum)control).Maximum)))
}
Interessant hierbei ist die Lambda Expression. Diese erhält die beiden Parameter columnName und control und erzeugt daraus eine ColumnDefinition mit dem ausgewählten Generator. Da diese Lambda Expression im Kontext einer SpaltenDefinition steht, kann das übergebene Control gefahrlos auf den Typ gecastet werden, der auch in der Eigenschaft ControlType verwendet wird. Auch hier sähe eine Lösung mit Generics sicher eleganter aus, ist aber ohne Ko-/Kontravarianz nicht möglich.
Wird nun in der Combobox ein anderer Generatortyp ausgewählt, muss das in der Spaltendefinition angegebene Control angezeigt werden. Um dynamisch die zugehörigen Controls zu finden, füge ich alle Controls, die zu einer Spalte gehören (Plus-und Minus-Button, Textfeld für den Spaltennamen, Combobox, Platzhalter für UserControl) in ein Panel ein. Um in diesem Panel später dynamisch das UserControl austauschen zu können, füge ich dieses zusätzlich in ein weiteres Panel. Dieses dient jeweils als Platzhalter für das auszutauschende Control.
In der Form sind nur wenige statische Elemente vorhanden. Den Aufbau der Form zeigt die Document Outline in Abbildung 9. Darin ist dargestellt, wie die einzelnen Controls ineinandergeschachtelt sind.
[Abb. 9] Document Outline.
Abbildung 10 zeigt, wie die Controls für eine Spaltendefinition dynamisch zur Laufzeit zusammengesetzt werden. Dabei zeigen die Pfeile an, auf welches Control gegebenenfalls die Tag-Eigenschaft verweist.
[Abb. 10] Controls im Panel.
Nun zur zweiten Herausforderung, dem dynamischen Ergänzen und Löschen von Spaltendefinitionen. Jede Spaltendefinition verfügt über die beiden Schaltflächen zum Hinzufügen und Löschen von Spaltendefinitionen. Zurzeit füge ich eine neue Spaltendefinition jeweils ans Ende an, künftig könnte diese aber auch an der betreffenden Position eingefügt werden. Daher habe ich bereits an jeder Spaltendefinition einen Plus-Button vorgesehen. Für die Aufnahme aller Spaltenbeschreibungen ist im statischen Teil der Form ein Panel zuständig. Wird mit der Minus-Schaltfläche versucht, eine Spaltenbeschreibung zu entfernen, müssen die zugehörigen Controls aus diesem Panel entfernt werden. Um dies zu vereinfachen, ist das zugehörige Panel an der Tag-Eigenschaft des Buttons gesetzt. So „weiß" der Button, zu welchem Panel er gehört und kann dieses aus dem umschließenden Panel entfernen.
Wird im Portal die Schaltfläche Generieren angeklickt, muss für jede Spaltenbeschreibung eine ColumnDefinition erzeugt werden, um dann die Testdaten zu generieren. Dazu wird die Liste der Spaltenbeschreibungen im statischen Panel durchlaufen. Darin befindet sich jeweils ein Textfeld, das den Namen der Spalte enthält. Ferner befindet sich im Platzhalterpanel ein Control, in dem die Parameter für den Generator enthalten sind. In der Dropdownliste enthält das SelectedItem eine SpaltenDefinition, aus der sich die ColumnDefinition erstellen lässt. Dazu wird aus der SpaltenDefinition die Funktion zum Erzeugen der ColumnDefinition aufgerufen.
Insgesamt hat das Erstellen des Portals knapp zwei Stunden in Anspruch genommen. Automatisierte Tests habe ich dazu fast keine erstellt. Diese würde ich allerdings in einem „echten" Projekt im Nachhinein ergänzen, da die Logik für den dynamischen Aufbau des Portals doch recht umfangreich geworden ist. Um hier bei späteren Erweiterungen Fehler auszuschließen, würde ich die typischen Bedienungsschritte eines Anwenders automatisiert testen.
Host
Am Ende benötigen wir für die gesamte Anwendung noch eine EXE-Datei, mit der die Anwendung gestartet werden kann. Aufgabe dieses Hosts ist es, die benötigten Komponenten zu beschaffen und sie den Abhängigkeiten gemäß zu verbinden. Die Abhängigkeiten sind hier in Form von Konstruktorparametern modelliert. Folglich muss der Host die Komponenten in der richtigen Reihenfolge instanzieren, im Abhängigkeitsbaum von unten nach oben, von den Blattknoten zur Wurzel. Anschließend übergibt er die Kontrolle an das Portal. Für die vorliegende Anwendung, bestehend aus einer Handvoll Komponenten, ist diese Aufgabe trivial. Bei größeren Anwendungen kostet diese Handarbeit Zeit und sollte automatisiert werden. Die Grund-idee dabei ist: Man überlässt das Instanzieren der Komponenten einem DI-Container wie StructureMap [2] oder Castle Windsor [3]. Über ein eigenes Interface identifiziert man den Startpunkt der Anwendung, und los geht's. Ein solcher Host kann dann sogar generisch sein und in allen Anwendungen verwendet werden.
Denkbare Erweiterungen
Für wiederkehrende Aufgaben wäre es sinnvoll, das Schema der Datengenerierung speichern und laden zu können. Dies kann beispielsweise mit dem Lounge Repository [4] erfolgen. In der Architektur würde dafür ein weiterer Adapter ergänzt, mit dem ein Schema gespeichert und geladen werden kann. Natürlich müssten im Portal entsprechende Anpassungen vorgenommen werden, um entsprechende Menüfunktionen zu ergänzen.
DesWeiteren wäre es denkbar, die Generatoren zur Laufzeit dynamisch zu laden. Damit könnten Entwickler ihre eigenen Generatoren implementieren und verwenden, ohne dazu die gesamte Anwendung übersetzen zu müssen. Mithilfe eines DIContainers wie StructureMap oder des Managed Extensibility Framework MEF [5] sollte auch diese Erweiterung keine große Hürde darstellen.
Fazit
Bei dieser Aufgabe stellte sich heraus, dass die Benutzerschnittstelle einer Anwendung durchaus einige Zeit in Anspruch nehmen kann. Die eigentliche Funktionalität war dagegen schnell entworfen und implementiert. Das lag maßgeblich daran, dass ich mir im Vorfeld einige Gedanken über die Architektur gemacht hatte. Danach ging die testgetriebene Entwicklung flüssig von der Hand. [ml]
[1] Stefan Lieser, Meier, Müller, Schulze..., Testdaten automatisch generieren, dotnetpro 5/2010, Seite 108ff., www.dotnetpro.de/A1005dojo
[2] http://structuremap.sourceforge.net/
[3] http://www.castleproject.org/container/
[4] http://loungerepo.codeplex.com/ und Ralf Westphal, Verflixte Sucht, dotnetpro 1 1/2009, Seite 52f. www.dotnetpro.de/A0911Sandbox