head.WriteLine()

Donnerstag, März 11, 2010

TT.DOM: Runtime Proxies

Bei TT.DOM steckt der Hauptteil der Logik in der Basisklasse DataObject. Von dieser leitet die jeweilige Datenklasse ab und hat dadurch Zugriff auf die Funktionalität. Doch was ist, wenn die Datenklasse bereits von einer anderen Basisklasse ableitet? Dies kann beispielsweise der Fall sein, wenn die Datenklassen in Form eines Entity Framework Modells vorliegen. Vielleicht wollen Sie auf Severseite auch nicht von einer Basisklasse ableiten, um Interoperabilität zu gewährleisten. Ein anderer Fall ist, wenn Sie sich WCF Proxy-Klassen von Visual Studio generieren lassen. In diesem Fall haben Sie keinen Einfluss auf die Basisklasse der Data Contracts.
Für solche Fälle bietet TT.DOM die Möglichkeit, dynamische Runtime Proxies für die Objekte zu erzeugen. Hierbei handelt es sich um Typen, die per Reflection.Emit zur Laufzeit erzeugt werden und einen Wrapper um die Datenklasse bilden.
Ein Runtime Proxy-Typ leitet von DataObject ab und enthält die selben Eigenschaften wie die jeweilige Datenklasse. In den Gettern und Settern der Eigenschaft werden die Anfragen an das Datenobjekt delegiert, welches zuvor im Konstruktor übergeben wurde. Das Ganze sieht dann in etwa wie folgt aus:
DataObjectProxy
Das folgende Beispiel zeigt den Code einer Proxyklasse für die fiktive Klasse Person.
public class Person_Proxy : DataObject
{
  private Person _originalObject;

  public Person_Proxy(Person person1)
  {
    this._originalObject = person1;
  }

  public string FirstName
  {
    get { return this._originalObject.FirstName; }
    set
    {
      base.OnPropertyChanging("FirstName");
      this._originalObject.FirstName = value;
      base.OnPropertyChanged("FirstName");
    }
  }
  ...
}

Proxyliste erzeugen

Die einfachste Art Runtime Proxies zu erzeugen, ist über die Extension Method .ToDataObjectListProxy(), welche auf alle Listen vom Typ IEnumerable angewendet werden kann. Hierbei wird ein Runtime Proxy für den jeweiligen Objekttyp erstellt und die Objekte in eine Liste vom Typ DataObjectList<T> gefüllt. Da T an dieser stelle den jeweiligen Proxy-Typ repräsentiert, können Sie die Liste nicht direkt verwenden (da ja der Proxy-Typ zur Entwurfszeit noch nicht existiert). Stattdessen gibt .ToDataObjectListProxy() eine Instanz von IDataObjectProxyList zurück, über die Sie die Member von DataObjectList<T> ansprechen können.
List<Person> results = personService.GetPersons();
IDataObjectProxyList list = results.ToDataObjectListProxy("Id");
list.BeginEdit();
...

Proxies manuell erzeugen

Zudem besteht auch die Möglichkeit Runtime Proxies manuell zu erzeugen. Hierfür stellt TT.DOM die Klasse ProxyFactory zu Verfügung. Diese stellt Methoden zur Erzeugung von Proxytypen, Objekten und Listen bereit.
// Proxytyp erstellen
ProxyFactory factory = new ProxyFactory();
Type proxyType =
  factory.CreateProxyType(typeof(Person), attributes);

// Proxyobjekt erstellen
DataObject obj = (DataObject)
  factory.CreateProxy(proxyType, person1);

// Proxyliste erstellen
IDataObjectProxyList proxyList =
  factory.CreateProxyCollection(proxyType, sourceList);

// Objekt der Liste hinzufügen
Person p = new Person() { FirstName = "Jörg" };
factory.Add(proxyList, p, typeof(Person));

Bei der Erstellung eines Proxytyps können Sie zusätzliche Anzeigeattribute, wie Browsable, ReadOnly, oder DisplayName angeben, mit denen die Eigenschaften des Proxytyps entsprechend dekoriert werden. Hierdurch können Sie Einfluss auf die Datenbindung nehmen (wie hier beschrieben). Zudem können Sie programmatisch Validierungsregeln einbringen, die von der Proxyklasse über das Interface IDataErrorInfo abgebildet werden, doch dazu mehr in einem separaten Post.

Verwendung der Proxyobjekte

Die Verwendung der Runtime Proxies bietet sich vor allem in Szenarien an, in denen die Objekte vor allem an die Oberfläche gebunden werden. Beim programmatischen Zugriff ergeben sich einige Besonderheiten. Da der Proxytyp nicht von der Datenklasse ableitet, können Sie nicht auf diese casten, was zur Folge hat, dass Sie nicht typisiert auf dessen Eigenschaften zugreifen können. Stattdessen bieten DataObject und IDataObjectProxyList entsprechende Zugriffsmethoden an.
DataObject.GetValue(string propertyName);
DataObject.SetValue(string propertyName, object value);

Alternativ können Sie sich aber auch das zugehörige Datenobjekt ermitteln.
Person p = proxyList.GetDataObject(0);
string firstName = p.FirstName;

Dies bietet sich besonders beim lesenden Zugriff an. Der schreibende Zugriff sollte jedoch immer über die SetValue()-Methode durchgeführt werden, da nur in diesem Fall die Änderung vom Change Tracking bzw. von der Undo-/Redo-Funktionalität berücksichtigt wird.
Das ist der Preis, den Sie für die Flexibilität zahlen müssen ;)

Ableitung vs. Delegation

Das TT.DOM-Proxyverfahren unterscheidet sich von den üblichen Proxy-Generierungskonzepten. So leiten z.B. die Proxies von EF4 direkt von der jeweiligen Datenklasse ab und delegieren die Änderungen an einen zentralen State Manager weiter. Dies hat den Vorteil, dass Sie die Proxies auf den Typ Ihrer Datenklasse casten und somit typisiert auf dessen Eigenschaften zugreifen können.
Damit dieses Verfahren jedoch funktioniert, muss die Datenklasse sämtliche Eigenschaften als virtual definieren. Da dies jedoch nicht immer vorausgesetzt werden kann und auch nicht vom WCF Proxy Generator unterstützt wird, habe ich mich bei meiner Implementierung dagegen entschieden. Zudem ist es auch beim EF4-Ansatz zwingend erforderlich neue Objektinstanzen über eine Factory zu erzeugen, um Change Tracking zu gewährleisten.

Labels: