.NET-Attribute effektiv anwenden

Veröffentlicht: 17. Jun 2002 | Aktualisiert: 09. Nov 2004

Von Thomas Fenske

Attribute stellen einen Mechanismus dar, wie man zusätzliche Informationen im Code ablegen kann. Diese lassen sich dann zur Laufzeit über die Reflektion auswerten. Erfahren Sie, wie Sie Attribute einsetzen können.

Auf dieser Seite

 Erfolg durch Frameworks
 ODO - Ein Framework unter .NET
 Fazit

Zur Homepage von dot.net magazin

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.

Zur Partnerübersichtsseite von MSDN Online

.NET wartet mit einer ungeheuer großen Anzahl von Neuerungen auf, die sich auf alle Bereiche der Software-Entwicklung erstrecken: Web-Services, Common Language Runtime, C#, ADO.NET, WebForms. Das alles sind Schlagworte, die seit geraumer Zeit die Autoren beschäftigen. Bei der Fülle an faszinierenden Techniken besteht allerdings die Gefahr, bedeutende Neuerungen zu übersehen. So auch bei der Reflektion.

Die enormen Gestaltungsspielräume, die sich durch die konsequente Nutzung von Reflektion und Attributen eröffnen, sind leider erst bei genauerem Hinsehen zu erkennen. In der Regel widmen die bisher erschienenen Bücher diesem Thema ein kurzes Pflichtkapitel. Zu Unrecht!

In diesem Artikel steht deswegen der Nutzen, den die Verwendung von Attributen bringen kann, im Vordergrund. Attribute stellen einen Mechanismus dar, wie man zusätzliche Informationen im Code ablegen kann. Diese lassen sich dann zur Laufzeit über die Reflektion auswerten.
Sehen wir uns dazu folgendes Beispiel zum Abwickeln von Einkäufen an.

Ein Kunde (Customer) kann nacheinander einzelne Artikel (Article) kaufen, die in einer Liste abgelegt werden. Offenkundig befinden sich in diesem Code noch Fehler. Teilweise sind Methoden auch noch gar nicht implementiert. Eben ein Beispiel mitten aus dem Leben (siehe Listing 1).

public class Customer
{
  private string name_;
  private System.Collections.ArrayList articles_=new System.Collections.ArrayList();
  public Customer(string name)
  {
    name_=name;
  }
  public void Print()
  {
    Print();
    foreach(Article A in articles_)
    {
      System.Console.Write("  ");
      A.Print();
    }
  }
  public void Buy(Article a)
  {
  }
}
public class Article
{
  private string name_="";
  private decimal price_=0;
  public Article(string name, decimal price)
  {
    name_=name;
  }
  public void Print()
  {
    System.Console.WriteLine("{0} {1:c}",name_,price_);
  }
}

Listing 2 - Beispiel mit Business-Code zur Abwicklung von Einkäufen

Um das Beseitigen dieser Missstände zu systematisieren, wählen wir einen üblichen Ansatz: Wir schreiben die Anmerkungen der Qualitätssicherung (Quality Assurance; kurz: QA) direkt in den Text.

Statt lediglich Kommentare in bestimmten Formaten zu verwenden, die wir später mit selbst geschriebenen Scripts auswerten, wollen wir uns die neuen Attribute zu Nutze machen und die Auswertung .NET überlassen. Wir legen die Latte sogar so hoch, dass die Applikation sich selbst um die Auswertung der Anmerkungen kümmert.
Doch immer schön der Reihe nach. Unser erstes Ziel ist es, den Compiler zu überreden, unsere QA-Anmerkungen als gültige Syntax zu betrachten.
Vor jede Methode wollen wir das Ergebnis unserer Qualitätsprüfung in der Form

[QACheck("Amy Herold", QACheck.ok)]
public void Foo()

oder

[QACheck("Gunnar Holl", QACheck.notImplemented)]
public void Foo()

schreiben. Wenn Sie bereits mit COM-IDL oder Corba-IDL gearbeitet haben, kommt Ihnen die Syntax möglicherweise bekannt vor. Attribute werden in C# in eckigen Klammern vor die Elemente geschrieben, die sie attributieren. Neben den hier gezeigten Methoden können auch Assemblies, Klassen, Konstruktoren, Delegates, Aufzählungen, Ereignisse, Felder, Interfaces, Module, Structs, ja sogar Parameter und Rückgabewerte mit Attributen erweitert werden.
Attribute sind aber nicht nur einfache Texte, die man nach Belieben vor die aufgezählten Elemente schreiben kann. Es sind Objekte von Klassen, und zwar von Klassen, die von System.Attribute abgeleitet sind. D.h., für jedes Attribut müssen Sie eine entsprechende Attribut-Klasse definieren. Der Ausdruck, der innerhalb der eckigen Klammern steht, ist nichts weiter als der Aufruf eines Konstruktors dieser Klasse.
Wenn Ihnen das jetzt etwas umständlich vorkommt, sind Sie mit der Mehrzahl der Entwickler in guter Gesellschaft, mich eingeschlossen. Denn erst auf den zweiten Blick werden die Vorteile, die dieser Umstand bietet, offensichtlich: Attribute können damit nicht nur Informationen tragen, sondern auch Verhalten aufweisen. Wir werden später davon Gebrauch machen. Zunächst einmal definieren wir aber unsere Klasse QACheck (siehe Listing 2).

public enum QAState
{
  ok,
  foundError,
  missingFunction,
  violateStyleGuide,
  notImplemented
}
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Constructor)]
public class QACheckAttribute : System.Attribute
{
  private string tester_;
  private QAState state_;
  public QACheckAttribute(string tester, QAState state)
  {
    tester_=tester;
    state_=state;
  }
  public string Tester
  {
    get
    {
      return tester_;
    }
  }
  public QAState State 
  {
    get
    {
      return state_;
    }
  }
}

Listing 4 - Erster Entwurf von QACheckAttribute

Dass der Klassenname noch den Postfix Attribute besitzt, ist eine Besonderheit von .NET. Laut Dokumentation kann man den Postfix sowohl weglassen als auch bei der Benutzung der Attribut-Klasse verwenden. Nach unserer Erfahrung mit den bisher erschienenen .NET-Beta-Versionen funktioniert lediglich die hier gezeigte Variante: bei der Klassendefinition Attribute an den Namen hängen und bei der Benutzung des Attributes den Postfix weglassen.

Der zweite, möglicherweise unklare Punkt ist die Verwendung des Attributs AttributeUsage. Mit diesem Attribut wird angegeben, vor welchen Elementen das Attribut verwendet werden darf. Schreiben Sie QACheck vor eine Klasse, wirft der Compiler einen Fehler. Ansonsten ist QACheck eine einfache Klasse mit einem Konstruktor zum Setzen der Werte und zwei Properties zum Abfragen derselben. Aber wie kommen wir nun an ein Attribut-Objekt heran, um dessen Properties abfragen zu können?

Hier kommt Reflektion ins Spiel. Versteckt zwischen den vielen gelungenen Neuerungen von .NET gibt es ein kleines Türchen, durch das man eine faszinierende Welt betreten kann. Wenn Sie die Autovervollständigung in Visual Studio .NET eingeschaltet haben, kommen Sie tagtäglich einige dutzend Male daran vorbei. Die Tür heißt Object.GetType(). Wer durch sie hindurchtritt, verlässt den Bereich konkreter Instanzen wie z.B. des Objekts "Kunde Meier" und betritt die Welt der Typen.

Über den Typ eines Objektes können die Klasse und ihre Mitglieder (Fields, Properties, Methods etc.) genauer untersucht werden. Die einzelnen Informationen werden über die Klassen xxxInfo (MemberInfo, FieldInfo, …) bereitgestellt. Die Abbildung zeigt einen kleinen Ausschnitt der Metaklassen in .NET. Neben typischen Eigenschaften wie Name, Sichtbarkeit, Parameterlisten und Rückgabewerten können auch die Attribute (GetCustomAttributes()) analysiert werden. Genau dafür werden wir in diese Welt eintauchen.

fenske_netattribute_1

Abb. 1: Metaklassen in .NET

Für unsere Zwecke ist der Weg über Object.GetType() etwas zu umständlich. Vielmehr verwenden wir die statische Methode MethodBase.GetCurrentMethod(), die ebenfalls in Abbildung 1 dargestellt ist. Sie liefert die Informationen der aktuell laufenden Methode. An welcher Stelle das MethodBase-Objekt ermittelt wird, werden wir uns anschließend genauer ansehen. Gehen wir fürs Erste davon aus, dass wir das Objekt bereits haben, und wenden uns seiner Analyse zu.

public static void Check(MethodBase methodBase)
{
  object[] qaChecks =
methodBase.GetCustomAttributes(typeof(QACheckAttribute),true);
  if (qaChecks.Length>0)
  {
    QACheckAttribute qaCheck = qaChecks[0] as QACheckAttribute;
    if (qaCheck.CanRun)
    {
      if (qaCheck.state_!=QAState.ok)
        Debug.WriteLine("Warning '"+
          methodBase.DeclaringType.Name +"."+
          methodBase.Name +"': "+ qaCheck.Reason);
    }
    else
      Debug.WriteLine("Error '" + 
        methodBase.DeclaringType.Name+ "." + 
        methodBase.Name+"': "+ qaCheck.Reason);
  }
  else
  {
    Debug.WriteLine("Warning '" + 
      methodBase.DeclaringType.Name+"."+ 
      methodBase.Name+"': "+ "No QACheck defined");
  }
}
public bool CanRun
{
  get
  {
    bool result=false;
    switch(state_)
    {
      case QAState.foundError:
      case QAState.missingFunction:
      case QAState.notImplemented:
        result=false;
        break;
      case QAState.ok:
      case QAState.violateStyleGuide:
        result=true;
        break;
    }
    return result;
  }
}
public string Reason
{
  get
  {
    return state_.ToString();
  }
}

Listing 6 - Erweiterung von QACheckAttribute

Die Methode in Listing 3 ist schnell erklärt. Von dem übergebenen MethodBase-Objekt besorgen wir uns eine Liste aller QACheckAttribute. Da beim zweiten Parameter true angegeben wurde, würden auch alle Attribute in die Liste aufgenommen, die von QACheckAttribute abgeleitet sind. Ist die Liste leer, wird eine Warnmeldung ausgegeben. Ist die Liste gefüllt, beachten wir nur den ersten Eintrag. In Abhängigkeit des Qualitätssicherungsergebnisses wird entweder eine Warn- oder eine Fehlermeldung ausgegeben. Die Regeln, was als Fehler und was als Warnung interpretiert wird, sind in QACheck gekapselt. Ebenso der ausgegebene Text.
Ein letzter Schritt und wir haben unser Ziel erreicht. Am Anfang jeder (!) Methode, die wir implementieren, muss folgende Zeile stehen:

QACheckAttribute.Check(MethodInfo.GetCurrentMethod());

Die Notwendigkeit, diese Zeile in jede Methode aufnehmen zu müssen, hinterlässt bei unserem doch sehr charmanten Ansatz einen schalen Beigeschmack. Unsere ganze Qualitätsverbesserung steht und fällt mit der Disziplin des Entwicklers. Ist er unkonzentriert, steigt die Wahrscheinlichkeit, dass er diese Zeile schlicht vergisst. Aber ist nicht gerade die Unkonzentriertheit die häufigste Fehlerursache? Bedeutet dies das Scheitern des Ansatzes?
Die Antwort lautet Nein. Wir betten diese technische Lösung in ein Entwicklungsframework ein und lassen über dieses Framework sicherstellen, dass jede Zeile mit der Prüfzeile versehen ist. Was genau bringt der Einsatz eines Frameworks?

Erfolg durch Frameworks

Wohl jeder Entwickler kennt das: Nach Stunden - teilweise Tagen - des Kodierens funktioniert die Software nach dem ersten fehlerfreien Compilerlauf auf Anhieb. Mehrere hundert Zeilen Code hat man bewegt, hinzugefügt und umgeschrieben und am Ende hat man bewiesen, dass man Herr der Lage und des überaus komplexen Systems war und ist.
Man hielte sich wohl schnell für ein Genie, wäre diese Art der Erfolgserlebnisse - nennen wir sie Erfolg der Kategorie 1 - nicht überaus selten. Außerdem beschleicht einen meistens das Gefühl, wieder einmal nur Glück gehabt zu haben. Enden diese Kodiermarathons doch häufig mit einem Undo-Checkout der kompletten Arbeit.
Um wie viel erhebender ist dagegen doch das Gefühl, ein schwieriges Problem mit einer Zeile Code gelöst zu haben? Damit wären wir bei der zweiten Kategorie von Erfolgsgefühlen.
Ein kleines, triviales Beispiel soll beide Arten verdeutlichen: In einer modernen Benutzungsoberfläche ist die Möglichkeit der Größenänderung von Dialogen selbstverständlich. Unter VB 6 implementiert man Form_Resize und definiert darin Größe und Position jedes Controls in Abhängigkeit der Fenstergröße.
Um die Minimalgröße des Fensters sauber zu realisieren, ist noch ein Griff in die Win32-API notwendig und das Ergebnis sind etwa zwei Bildschirmseiten Größenberechnungen, die beim ersten Durchlauf abstürzen, weil doch irgendeine Höhen- und Breitenberechnung eine negative Zahl liefert.
Tritt dieser Fall ausnahmsweise nicht ein, ist unsere Freude der Kategorie 1 zuzuordnen. Die Benutzung der Eigenschaften Dock und Anchor unter .NET führt nach wenigen Mausklicks (in diesem Fall sogar ohne selbst getippten Code) direkt zu Kategorie 2.

Wieso war Derartiges unter VB 6 mit Bordmitteln nicht möglich? Microsoft hat bei der Entwicklung der .NET-Klassenbibliothek die Bedürfnisse der Entwickler berücksichtigt und die Funktionalität der Klassenbibliothek daran ausgerichtet. Mit anderen Worten: Microsoft hat ein Framework zur Anwendungsentwicklung aufgebaut, das uns von vielen technischen Aspekten abstrahieren lässt.
Diese Leistung bringt im Wesentlichen drei Vorteile:

  1. Da die Technik versteckt ist, können wir in diesen Bereichen keine Fehler machen. Die Qualität der Ergebnisse steigt.

  2. Die Zeit, die wir dadurch einsparen, können wir mit der Lösung unserer fachlichen Probleme verbringen.

  3. (Und das ist vielleicht der wichtigste Vorteil) Wir erleben häufiger Erfolgserlebnisse der Kategorie 2!

So groß der Lernaufwand bei .NET am Anfang auch sein mag, die Faszination, fachliche Probleme elegant zu lösen und anschließend mit einem Kategorie-2-Erfolgserlebnis belohnt zu werden, zieht einen von Anfang an in ihren Bann.
Dieser Bann ist symptomatisch für jedes gute Framework, da es die an uns gestellten Anforderungen, immer schneller und besser Software zu entwickeln, mit unserem Bedürfnis, mehr Spaß dabei zu haben, verbindet. Sind technische Lösungen in ein Framework gegossen, können wir die Details im Laufe eines Projektes getrost vergessen bzw. müssen sie erst gar nicht verstehen.

fenske_netattribute_2

Abb. 2: Framework-Bestandteile

Aus der bisherigen Darstellung könnte man annehmen, ein Framework sei ein Synonym für Klassenbibliothek. Das ist jedoch erst die halbe Miete. Um genau zu sein, ist es erst ein Drittel der Miete. Zu einem Framework gehören nämlich noch zwei weitere Bestandteile. Beide Bestandteile kamen in unserem kleinen Beispiel weiter oben schon vor, sind aber nicht besonders auffällig in Erscheinung getreten. Was übrigens ebenfalls eine Eigenschaft guter Frameworks ist!
Der zweite Bestandteil sind Codegeneratoren. Durch das Definieren der Dock- und Anchor-Eigenschaften wurde in unserer Dialogklasse entsprechender Code automatisch eingefügt. Drittens benötigen Sie auch noch ein Werkzeug, mit dem Sie alle Einstellungen und Spezifikationen vornehmen können. Diese Rolle spielt in unserem Beispiel der Dialog-Designer von Visual Studio.NET.
So schön und umfangreich .NET auch sein mag, es löst natürlich nicht alle technischen Probleme. Das erwartet auch niemand. .NET gibt einem Framework-Bauer mit der Reflektion und dem Erweiterungsmechanismus der Attribute ein mächtiges Werkzeug an die Hand.

 

ODO - Ein Framework unter .NET

Nach diesem kleinen Exkurs über den Sinn und Zweck von Frameworks, sollen Sie nun das Framework ODO kennen lernen. ODO macht intensivst von Attributen Gebrauch und an ihm lassen sich die eben gemachten Aussagen zum Thema Frameworks nachvollziehen.
Mit ODO können Sie Ihre Geschäftslogik übersichtlich und assistentengestützt in Form eines UML-Modells definieren. Um die Speicherung persistenter Objekte kümmert sich ein zweites Framework, GOMA. GOMA ist ein typischer objekt-relationaler Mapper (OR-Mapper), d.h., es übernimmt die Speicherung der Objekte in eine relationale Datenbank, die Definition des Tabellenschemas, das Handling von Transaktionen und noch einiges mehr.
Welche Aufgaben bleiben dann eigentlich für ODO übrig? Ganz einfach: die unkomplizierte und dennoch flexible Darstellung der Geschäftsobjekte in der Benutzungsschnittstelle. Da man sich bei der Entwicklung primär auf die Geschäftslogik konzentrieren möchte, versucht man, die Technik drum herum an dafür spezialisierte Komponenten zu delegieren.
Der Bereich Persistenz ist in diesem Punkt recht dankbar, da hier durch das Einziehen einer zusätzlichen Schicht gut von der Technik und der konkreten Datenbank abstrahiert werden kann. Vermutlich haben viele von Ihnen bereits Erfahrungen mit ähnlichen OR-Mappern gemacht.
Weniger verbreitet sind Automatisierungsansätze im Bereich der Benutzungsschnittstellen. Der offensichtliche Grund ist der besondere Charme, der von den Ergebnissen ausgeht. Es bleibt nur wenig Platz für Individualität und damit kaum Potenzial für einen Wettbewerbsvorteil. Oberflächengestaltung ist harte Programmierarbeit.
Genau hier hat ODO einen Zwischenweg gefunden. Sie kennen sicher die Möglichkeiten verschiedener Entwicklungsumgebungen, einzelne Dialogfelder mit Datenbankfeldern in Beziehung zu setzen. Diese sog. Data-Bound-Controls eignen sich zwar gut für Prototypen, doch leider nur für experimentelle Prototypen; besser bekannt als Wegwerf-Prototypen.
Der Grund dafür ist die Unfähigkeit dieses Ansatzes, 3-Schichten-Architekturen umzusetzen. Der Entwicklungsschwerpunkt liegt im Datenmodell bzw. in den Dialogen, also genau da, wo man ihn nicht haben will.
ODO hingegen stellt eine Reihe von Class-Bound-Controls bereit, die statt auf die Datenbank auf die Geschäftslogik zugreifen. Ob diese Klassen persistent oder transient sind, ist ODO egal. Es können also beliebig viele Schichten zwischen der Oberfläche einerseits und den persistenten Klassen andererseits liegen. Dadurch ist die Entwicklung der Geschäftslogik in der Mittelschicht keinerlei technischen Restriktionen unterlegen.
Da sauber definierte Schnittstellen das A und O einer stabilen Architektur sind, sollten sie nicht durch technische Notwendigkeiten "verunreinigt" werden. Das gilt besonders für die zentralen Komponenten in der Mittelschicht. Idealerweise stellen diese Komponenten ausschließlich Methoden zur Verfügung, die sich an den fachlichen Aufgaben orientieren.
Aus diesem Grund fügt ODO dieser Schnittstelle keine weiteren Methoden hinzu oder erzwingt die Implementierung eines bestimmten Interfaces. Alle notwendigen Informationen werden via Reflektion aus der Struktur der Geschäftsklassen gelesen. Reichen diese Informationen nicht aus, wird auf Attribute zurückgegriffen.
Beispiele dafür sind Resource-ID's für die automatische Lokalisierung der Anwendung, Informationen über Pflichteingaben (Not Null) oder die Definition von Wertemengen. Hingegen kann aus den Metadaten von .NET direkt ermittelt werden, welchen Typ die Daten haben, oder ob sie nur gelesen werden können (Eigenschaft besitzt keinen "set"-Teil).
Diese und noch unzählige weitere Einstellungen können über das UML-Werkzeug (wir verwenden hier objectiF von microTOOL) in den Klassenmodellen eingestellt werden. Abb. zeigt ein Klassendiagramm, in dem die Geschäftsklassen zur Verwaltung von Abenteuerreisen dargestellt sind.

fenske_netattribute_3

Abb. 3: Struktur der Geschäftsklassen

Bis auf das private Feld Data enthalten die Klassen keinerlei technische, sondern nur fachliche Member. Das ODO-Framework bietet zum komfortablen Einstellen erweiterter Eigenschaften eine Reihe von speziellen Dialogen und Assistenten an. Exemplarisch enthält Abb. einen Ausschnitt des ODO-Eigenschafts-Dialoges für die Klasse Stage. Wie Sie sehen, können hier Einstellungen wie die Resource-ID (Bezeichnung) oder Schreibschutz (nur lesen) für einzelne Properties gesetzt werden.

fenske_netattribute_4

Abb. 4: Definition der Geschäftsklasse Stage

Das Ergebnis der Codegenerierung können Sie sich in Listing 4 näher ansehen. Bis auf die Implementierung der transienten Property Description ist der Code vollständig generiert und komplett lauffähig.

[ODO.Attributes.Caption("Stage");
GOMA.Attributes.PersistentClass("Stage",typeof(GOMA.InstanceTable)),
GOMA.Attributes.Relationship("StageDestinationCamp",typeof(busCamp),typeof(busStage)),
GOMA.Attributes.Relationship("StageStartCamp",typeof(busStage),typeof(busCamp)),
GOMA.Attributes.Relationship("AdventureTourStages",typeof(busAdventureTour),typeof(busStage)),
GOMA.Attributes.Table("Stage",typeof(busStage))]
[System.Runtime.InteropServices.ClassInterface(
System.Runtime.InteropServices.ClassInterfaceType.AutoDual)]
[System.Runtime.InteropServices.Guid("5CC9237A-5250-4778-BBF8-FEB786304658")]
public class busStage
{
  [ODO.Attributes.Field(ODO.eVisibility.vtPublic),
  ODO.Attributes.SemanticKey(),
  ODO.Attributes.Caption("Bezeichnung")]
  public String Description
  {
    get
    {
      string result="";
      result=(StartCamp==null) ? "<<???>" : StartCamp.Name;
      result+=" -> ";
      result+=(DestinationCamp==null) ? "<<???>" :
        DestinationCamp.Name;
      return result;
    }
  }
  [ODO.Attributes.Field(ODO.eVisibility.vtDetailed),
  ODO.Attributes.Caption("Distance"),
  GOMA.Attributes.Column("Distance"),
  GOMA.Attributes.PersistentField("Distance")]
  public Int32 Distance
  {
    get
    {
      System.Int32 retval;
      retval=(Int32)data_["Distance"];
      return retval;
    }
    set
    {
      System.Int32 newValue = value;
      data_["Distance"]=newValue;
    }
  }
…
}

Listing 8 - Ausschnitt aus der Geschäftsklasse Stage

Das ODO.Attributes.Caption-Attribut trägt die Resource-ID für die Klasse bzw. die Properties. Die GOMA-Attribute spiegeln vereinfacht gesagt den Inhalt des Klassendiagramms mit seinen Beziehungen wider, die ODO-Attribute die Informationen des Einstellungsdialogs. Da Description transient ist, fehlen ihm die GOMA- Attribute. Zusätzlich versieht ODO jede Klasse auf Wunsch mit einer COM-Schnittstelle (System.Runtime.InteropServices-Attribute).
Vermissen Sie den Code zum Schreiben in die Datenbank und zum Befüllen der Dialog-Controls? Wir auch nicht. ODO baut zu jeder Geschäftsklasse ein MetaClass-Objekt und zu jeder Property ein MetaField-Objekt auf (Abb. ). Diese Meta-Objekte rufen, parametrisiert mit dem jeweiligen Geschäftsobjekt, die semantischen Methoden der Geschäftsklassen auf. metaField.GetValue(object) sieht zwar nicht so schön aus wie customer.Name, aber da der Code sich in der ODO-Klassenbibliothek befindet, braucht sich der Entwickler daran nicht zu stören. Im Gegenteil: Im Ergebnis wird der Code in den Geschäftsklassen massiv reduziert.

fenske_netattribute_5

Abb. 5: ODO-Metaklassen

Damit haben wir alle Bestandteile eines Frameworks (vgl. Abb. ) - und somit von ODO - gestreift: die Modellierungskomponente (objectiF + ODO-spezifische Dialoge), Code-Generatoren und die Klassenbibliothek, die die Geschäftsklassen analysiert und zum Leben erweckt.
Es wird Zeit, dass wir ODO in Aktion sehen. Nachdem die Geschäftsklassen definiert und generiert sind, muss dazu lediglich ein passender Dialog erzeugt werden. In Abb. sehen Sie unsere kleine Anwendung bei der Buchung von Abenteuerreisen in Aktion. Links befindet sich eine Liste der bereits gebuchten Reisen. Rechts davon stehen die Detailinformationen der selektierten Reise mit ihrem Namen, ihren Etappen und den Kunden, die diese Reise gebucht haben. Unter der Liste der Etappen befindet sich eine Detailansicht zu der jeweils ausgewählten Klasse.

fenske_netattribute_6

Abb. 6: Dialog zur Buchung von Abenteuerreisen

Wenn man den Dialog aus einer objektorientierten Sicht betrachtet, sind drei Objekte mit ihren Eigenschaften zu identifizieren:

  • Das Applikationsobjekt, das die Liste aller Reisen verwaltet (Application.AdventureTours)

  • Die aktuell ausgewählte Reise (AdventureTour) mit den Eigenschaften Name, Stages und Customers sowie die Knöpfe zum Anlegen, Speichern und Löschen von Reisen

  • Die aktuell ausgewählte Etappe (Stage) mit den übrigen angezeigten Eigenschaften und Knöpfen

Zwischen den Listen und den Detailinformationen bestehen Abhängigkeiten. Die aktuelle Selektion einer Tabelle beeinflusst den Inhalt der dazugehörigen Detailinformation.
Die eben beschriebene Struktur des Dialoges spiegelt der Code in Listing 5 vollständig (!) wider. Neben dem Ziehen der Controls auf den Dialog ist die Strukturdefinition innerhalb des Konstruktors das Einzige, was zu tun ist, um den Dialog voll funktionsfähig zu machen.

public AdventureToursForm (ODO.GUI.BusinessCase businessCase)
{
  InitializeComponent();
  businessCase_=businessCase;
  businessCase_.BeginConfigure();
  businessCase_.AttachControlManager(applicationManager_, "TourContainer");
  businessCase_.AttachControlManager(tourManager_, "ActualTour");
  businessCase_.AttachControlManager(stageManager_, "ActualStage");
  businessCase_.AttachBusinessCaseActor(toursGrid_, "ActualTour");
  businessCase_.AttachBusinessCaseActor(stagesGrid_, "ActualStage");
  businessCase_.AttachBusinessCaseActor(startEdit_, "StartCamp");
  applicationManager_.Attach(toursGrid_,"AdventureTours");
  tourManager_.Attach(nameEdit_, "Name", nameLabel_);
  tourManager_.Attach(stagesGrid_, "Stages", stagesBox_);
  tourManager_.Attach(customersGrid_, "Customers", customersBox_);
  stageManager_.Attach(descriptionEdit_, "Description", descriptionLabel_);
  stageManager_.Attach(difficultyEdit_, "DegreeOfDifficulty", difficultyLabel_);
  stageManager_.Attach(distanceEdit_, "Distance", distanceLabel_);
  stageManager_.Attach(startEdit_, "StartCamp", startLabel_);
  stageManager_.Attach(destinationEdit_, "DestinationCamp", destinationLabel_);
  stageManager_.Attach(guidianceEdit_, "WithGuidiance");
  tourDelete_.ControlManager=tourManager_;
  tourSave_.ControlManager=tourManager_;
  tourNew_.ControlManager=tourManager_;
  stageDelete_.ControlManager=stageManager_;
  stageSave_.ControlManager=stageManager_;
  stageNew_.ControlManager=stageManager_;
  businessCase_.EndConfigure();
}

Listing 10 - Typischer Konstruktor eines ODO-Dialoges

Alle Aktivitäten finden im Rahmen eines Geschäftsvorfalles (BusinessCase) statt. Dieser BusinessCase kennt alle beteiligten Objekte und weiß, in welcher Rolle sie auftreten (TourContainer, ActualTour und ActualStage). Da mehrere Controls an der Darstellung eines Objektes beteiligt sind, werden sie durch einen ControlManager zusammengefasst. Er übernimmt die Kommunikation mit dem BusinessCase und führt über die Knöpfe (z.B. tourDelete_, tourSave_, tourNew_) Operationen auf seinem vom BusinessCase zugewiesenen Objekt aus.
Die einzelnen Anzeigefelder werden dem ControlManager mit dem Namen der Property zugeweisen, deren Inhalt sie anzeigen sollen. Ob es sich dabei um einen atomaren Wert oder um eine Liste von Objekten handelt, ist aus Sicht des ControlManager-Objektes egal. Wichtig ist nur, dass das jeweilige Control mit den Daten etwas anfangen kann.
Dies wird von jedem Control zur Laufzeit innerhalb der Methode ControlManager.Attach entschieden. So stehen Anzahl, Reihenfolge und Überschriften eines Grid-Controls zur Compilezeit nicht fest. Diese Informationen werden erst später aus den Meta-Informationen der Geschäftsklassen gelesen.
Wird jetzt die Rolle eines BusinessCase-Objektes mit einem konkreten Geschäftsobjekt belegt, wird der zuständige ControlManager benachrichtigt (vgl. Abb. ). Der wiederum informiert darüber seine Controls. Ändert daraufhin eines der Controls eine andere Rollenbelegung des BusinessCase-Objektes, geht das Ganze für eine andere Rolle von vorne los. Dieser Vorgang kann sich nun über beliebig viele Ebenen und Dialoge fortsetzten, bis alle Controls aktualisiert sind.

fenske_netattribute_7

Abb. 7: Aktualisierung eines ControlManagers ausgelöst durch die Rollenbelegung des BusinessCases

 

Fazit

Wie wir sehen, hat der konsequente Einsatz von Reflektion und Attributen geholfen, den applikationsspezifischen Code, sei er nun generiert oder handgeschrieben, stark zu reduzieren. Im Bereich der Mittelschicht steht die Definition der Geschäftslogik im Vordergrund. Bei der Oberflächengestaltung dreht sich alles um die fachliche Strukturierung der Geschäftsvorfälle.
Wie kommen die Daten aus der Datenbank? Kommen sie überhaupt aus der Datenbank? Wie werden sie dargestellt? Dürfen die Daten verändert werden? In welcher Landessprache soll die Oberfläche dargestellt werden? Wie werden ungültige Operationen verständlich an den Benutzer gemeldet? Wie wird sichergestellt, dass sich ein Geschäftsobjekt in jedem Dialog gleich verhält? Alle diese Fragen nimmt das Framework dem Entwickler ab und schafft damit Freiräume für spannendere Themen und für viele Erfolgserlebnisse der Kategorie 2.