head.WriteLine()

Mittwoch, März 17, 2010

TT.DOM: Views, Teil 2

Wie im ersten Teil bereits beschrieben, bietet TT.DOM ein View-Modell, das es ermöglicht beliebig viele Sichten auf eine Liste zu erzeugen. Während es zuletzt um die Möglichkeiten ging, Sortier- und Filterkriterien auf die View anzuwenden, möchte ich in diesem Post beschreiben, wie Sie die Darstellung einer View anpassen und um berechnete Spalten erweitern können.

Views anpassen

Wie hier bereit beschrieben, basiert die Anpassbarkeit der Views auf dem Interface ITypedList. Es definiert die GetItemProperties()-Methode, über die das gebundene Listensteuerelement Informationen über die Spalten der Datenquelle abfragt. Sie gibt eine Collection von PropertyDescriptor-Objekten zurück, welche die jeweiligen Eigenschaften beschreiben. Über einen PropertyDescriptor kann beispielsweise festgelegt werden, welchen Anzeigenamen eine Eigenschaft bei der Bindung annehmen soll, ob diese schreibgeschützt ist oder diese ausgeblendet werden soll. Diese Darstellungsmerkmale ermittelt die PropertyDescriptor-Klasse per Reflection aus der Datenklasse. In der Datenklasse können die Merkmale in Form von Attributen signalisiert werden:
[Browsable(false)]
public string Name { get; set; }

Hierbei können die folgenden Attribute aus dem Namespace System.ComponentModel verwendet werden:
  • Browsable bestimmt, ob die Eigenschaft angezeigt werden soll.
  • DisplayName gibt den Anzeigenamen an
  • ReadOnly steuert den Schreibschutz der Spalte
  • Description liefert eine Beschreibung der Spalte
Die folgende Abbildung zeigt die Kommunikation zwischen beteiligten Parteien:
ITypedList
Wie hier bereits erwähnt, haben Sie nicht immer Einfluss auf die Datenklasse, sodass Sie unter Umständen die genannten Attribute nicht setzen können. Hierfür bietet DataObjectView<T> die Eigenschaft ColumnAttributes. Sie enthält für jede Eigenschaft der Liste ein DisplayAttributeInfo-Objekt, über das die Darstellung gesteuert werden kann.
DataObjectView<Person> view = list.DefaultView;
view.ColumnAttributes["Id"].Browsable = false;
view.ColumnAttributes["FirstName"].DisplayName = "Vorname";
view.ColumnAttributes["LastName"].DisplayName = "Nachname";
view.ColumnAttributes["LastName"].ReadOnly = true;

Auf diese Weise können Sie die entsprechenden Anzeigeattribute programmatisch zuweisen, ohne die jeweilige Datenklasse anpassen zu müssen. Zudem kann es vorkommen, dass die Darstellung vom jeweiligen Kontext in der Anwendung abhängt.
Neben den genannten Anzeigeattributen bietet DisplayAttributeInfo über die Ordinal-Eigenschaft die Möglichkeit, die Position der Spalte für die Bindung festzulegen. Alternativ können Sie auch die SetOrdinal()-Methode der View verwenden.
list.DefaultView.SetOrdinal(
  "Id", "FirstName", "LastName");

Um diese Art der Anpassung auch dem Endanwender zu ermöglichen, bietet TT.DOM einen Standarddialog (CustomizeColumnsForm) mit dem die Darstellung zur Laufzeit angepasst werden kann.
CustomizeColumns

Berechnete Spalten

Über den ITypedList-Mechanismus stellt TT.DOM auch die Möglichkeit zu Verfügung, dynamisch berechnete Spalten zu definieren. Die Spalten werden hierbei über einen eigene PropertyDescriptor-Ableitung bereitgestellt, welche die Werte über einen Delegate ermitteln.
Die Anlage von berechneten Spalten erfolgt über die ComputedColumns-Eigenschaft von DataObjectView<T>. Hierbei geben Sie lediglich den Namen und den Typ der “virtuellen” Eigenschaft, sowie einen Delegate zur Berechnung an.
ComputedColumn col =
  new ComputedColumn("FullName", typeof(string),
    item =>
      ((Person)item).FirstName + " " +
      ((Person)item).LastName);
_persons.DefaultView.ComputedColumns.Add(col);

Daraufhin erscheint die Spalte automatisch im gebundenen Listensteuerelement. Das sich der Wert der Spalte automatisch aktualisiert, wenn sich die abhängigen Eigenschaften ändern, brauche ich wohl nicht zu erwähnen ;)
Auch bei dieser Technik besteht der Vorteil vor allem darin, berechnete Spalten bereitzustellen, ohne die zugrunde liegende Datenklasse anpassen zu müssen.

Labels:

Dienstag, März 16, 2010

TT.DOM: Views, Teil 1

Neben der normalen Datenbindungsfunktionalität bietet TT.DOM ein View-Modell, das es ermöglicht mehrere unterschiedliche Sichten auf die selben Daten zu erstellen. So können Sie beispielsweise verschiedene Filter- und Sortierkriterien, oder Anzeigefelder je Sicht definieren.
Hierbei kommen eine Reihe von Standardinterfaces zum Einsatz, die vom jeweiligen Tabellensteuerelement zur Interaktion mit der Datenquelle verwendet werden.
View-Konzept
Eine View wird durch die Klasse DataObjectView<T> repräsentiert. Sie implementiert die folgenden Interfaces:
  • IBindingListView stellt Member für das Filtern und Sortieren bereit. Anders als bei LINQ, werden diese jedoch in Form von Zeichenketten bereitgestellt, was die dynamische Erstellung zur Laufzeit ermöglicht.
  • ITypedList wird von der Datenbindungsinfrastruktur verwendet, um die anzuzeigenden Eigenschaften der Datenquelle abzufragen. Hierzu stellt die Datenquelle PropertyDescriptor-Objekte zu Verfügung, die um Anzeigeattribute, wie DisplayName, ReadOnly oder Browsable angereichert werden können. Zudem können Sie über diesen Mechanismus berechnete Spalten bereit stellen, die dynamisch erzeugt werden und nicht in Form von Eigenschaften von der Datenklasse bereitgestellt werden müssen.
  • IListSource wird von der Listenklasse implementiert und verweist auf die Default View. Auf diese Weise wird beim Binden der Liste auf die Default View verwiesen. Dadurch wirken sich Änderungen der Default View direkt auf die Anzeige aus, auch wenn nicht explizit an diese gebunden wurde.

Views erstellen

Für den Zugriff auf eine View stellt DataObjectList<T> zwei Möglichkeiten zu Verfügung:
  1. DefaultView liefert die Standardsicht der Liste. Wie oben bereits erwähnt wird diese durch das IListSource-Interface standardmäßig bei der Binding verwendet.
  2. CreateView() erzeugt eine neue View. Auf diese Weise können Sie beliebig viele Views für die Datenquelle erstellen, wobei Änderungen der Daten – sowohl in der View, auch in der Originalliste – automatisch synchronisiert werden.
ListViewRelation

Liste sortieren

Für das Sortieren von Daten stellen alle Tabellensteuerelemente entsprechende Funktionen bereit. Es macht jedoch durchaus Sinn diese Funktionalität auch über das Objektmodell anzubieten. So müssen Sie nicht direkt mit dem Tabellensteuerelement kommunizieren, was die Kapselung der Logik und die Austauschbarkeit der Controls erhöht. Zudem stellt .NET mit dem Interface IBindingListView einen Mechanismus bereit, der eine Synchronisation zwischen der Liste und dem Anzeigesteuerelement ermöglicht. Auf diese Weise passt sich die Anzeige automatisch der programmatisch zugewiesenen Sortierung an und umgekehrt.
Die Zuweisung des Sortierkriteriums vollzieht sich bei TT.DOM nach dem gleichen Prinzip wie bei der guten, alten DataView-Klasse: Es wird eine Zeichenfolge mit den Sortierfeldern und optional “ASC” oder “DESC” für aufsteigende bzw. absteigende Reihenfolge festgelegt.
list.DefaultView.Sort = "FirstName ASC, LastName DESC";
Wie bereits erwähnt, bietet die String-basierte Zuweisung den Vorteil, diese zur Laufzeit zuweisen und ändern zu können. Auf diese Weise kann auch der Anwender von der Funktionalität profitieren. Hierfür stellt TT.DOM den Dialog SortForm bereit.
SortDialog

Filter definieren

Das Filtern der Liste erfolgt über die Filter-Eigenschaft der DataObjectView<T>-Klasse.
view.Filter = "LastName LIKE N*";
Hierbei wird der Ausdruck dynamisch via CodeDom nach C# konvertiert, kompiliert und auf die Liste angewendet. Die folgende Abbildung zeigt diesen Prozess:
Filtern
Bei der Syntax orientiert sich die Funktionalität an den Filterausdrücken, die DataView bietet. Die folgenden Ausdrücke sind hierbei möglich:
Funktion Beschreibung
Lower() Wandelt den Text in Kleinbuchstaben.
Upper() Wandelt den Text in Großbuchstaben.
Str() Wandelt einen Ausdruck in eine Zeichenfolge.
Trim() Entfernt Leerzeichen an Anfang und Ende einer Zeichenfolge.
IsNull() Gibt true zurück, wenn ein Ausdruck den Wert null enthält.
Round() Rundet eine Zahl auf eine bestimmte Anzahl an Nachkommastellen.
Date() Wandelt eine Zeichenfolge in ein Datum.
Substring() Extrahiert eine Teilzeichenfolge an der angegebenen Stelle.
Len() Gibt die Länge einer Zeichenkette zurück.
Year() Gibt das Jahr eines Datums zurück.
Month() Gibt den Monat eines Datums zurück.
Day() Gibt den Tag eines Datums zurück.
Hour() Gibt die Stunde eines Datums zurück.
Minute() Gibt die Minuten eines Datums zurück.
Second() Gibt die Sekunden eines Datums zurück.
Millisecond() Gibt die Millisekunden eines Datums zurück.
Damit auch der Benutzer von der Funktionalität profitieren kann, stellt TT.DOM zwei Standarddialoge zu Verfügung – einen für die einfache, Spalten-basierte Filterung und einen Expression Editor für Power User.
FilterColumnsDialog
FilterDialog
Ich könnte mir vorstellen den Filterprozess zukünftig durch Scripting (via dynamic in .NET 4.0) zu ersetzen. Dies würde den komplexen Konvertierungsvorgang überflüssig machen und würde zudem einen reichhaltigeren Sprachumfang bieten.
Wenn sich jemand dazu berufen fühlt, einfach bei mir melden! :)

Labels:

Montag, März 15, 2010

TT.DOM: Transaktionen

Wie hier bereit beschrieben, stellt TT.DOM Unterstützung für Undo/Redo-Transaktionen bereit. Darüber hinaus können DataObject-Ableitungen auch an lokalen oder verteilten Transaktionen teilnehmen – ohne dass Sie dafür etwas tun müssen!
Hierfür setzt TT.DOM auf der Funktionalität von System.Transactions auf. So implementiert z.B. die Basisklasse DataObject das Interface IEnlistmentNotification (Prepare, Commit, Rollback, …). Zudem wird sowohl das implizite, als auch das explizite Programmiermodell von System.Transactions unterstützt.
Beispiel für das explizite Enlistment:
Person p = new Person
  { FirstName = "Jörg", LastName = "Neumann" };
CommittableTransaction tran =
  new CommittableTransaction();
tran.EnlistVolatile(p);
p.FirstName = "Joerg";
tran.Commit();

Beispiel für das implizite Enlistment:
Person p = new Person
  { FirstName = "Jörg", LastName = "Neumann" };
using (TransactionScope tx = new TransactionScope())
{
  p.FirstName = "Joerg";
  tx.Complete();
}

Das Ganze kann natürlich auch für auf Listenebene vollzogen und mit anderen transaktionsfähigen Objekten kombiniert werden.

Labels:

Freitag, März 12, 2010

TT.DOM in der aktuellen dotnetpro

In der aktuellen Ausgabe der dotnetpro (4/2010) ist der erste Teil meiner vierteiligen Artikelserie über TT.DOM erschienen. Hierin geht es zunächst um die Grundlagen von Datenbindung und Eingabevalidierung in Windows Forms, WPF und Silverlight.
Aus dem Abstract:
In verteilten Anwendungen spielen Datenklassen eine zentrale Rolle. Doch um diese zu erstellen, an die Oberfläche zu binden und zwischen den Schichten zu transferieren, müssen einige Hürden genommen werden. Hierbei spielen Datenbindung, Änderungsverfolgung und Transaktionsverwaltung eine zentrale Rolle. Haben Ihnen in der Vergangenheit DataSet und Co. den größten Teil der Arbeit abgenommen, müssen Sie sich bei der Entwicklung eigener Datenklassen um Vieles selbst kümmern. dotnetpro beleuchtet in dieser Artikelserie die wichtigsten Grundlagen und stellt ein Objektmodell vor, das ihnen einen Großteil der Arbeit abnimmt.
dnp_042010

Labels:

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:

Mittwoch, März 10, 2010

TT.DOM: Change Tracking

Wie im letzten Post bereits beschrieben, stellt TT.DOM mit der Basisklasse DataObject eine Reihe von Datenbindungsfunktionen bereit.
Darüber hinaus ist DataObject aber auch in der Lage, automatisch die Originalwerte der geänderten Eigenschaften eines Objekts zu speichern. Dies ist vor allem für eine spätere Datenbankaktualisierung hilfreich. Die Änderungsverfolgung muss jedoch explizit aktiviert werden, um zu vermeiden, dass Änderungen bereits beim Befüllen der Objekte protokolliert werden. Hierfür bietet DataObject und DataObjectList<T> die Methode BeginEdit().
List<Person> persons = service.GetPersons();
var list = persons.ToDataObjectList("Id");
list.BeginEdit();

Zudem kann die Änderungsverfolgung auch durch Setzen der Mode-Eigenschaft auf Objektebene festgelegt werden.
Um festzustellen, welche Datenobjekte geändert wurden, können Sie die State-Eigenschaft abfragen. Diese enthält je nach Änderung den Wert Unchanged, Insert, Update oder Delete.
Über die Methode GetChanges() können die geänderten Objekte abgefragt werden.
List<Person> changes = list.GetChanges();
Alternativ können auch Änderungen eines bestimmten Typs abgefragt werden.
List<Person> updates = 
  list.GetChanges(DataObjectStateType.Update);

Wollen Sie die Änderungen an einen WCF-Service übertragen, können Sie die Methode SaveChanges() nutzen. Diese nimmt drei Function Delegates für die Methoden Update Insert und Delete entgegen.
list.SaveChanges<Customer>(
    c => service.UpdateCustomer(c),
    c => service.InsertCustomer(c),
    c => service.DeleteCustomer(c));

Hierbei wird davon ausgegangen, dass ein WCF-Service Proxy mit folgender Signatur existiert:
public Person UpdatePerson(Person p);
public Person InsertPerson(Person p);
public void DeletePerson(Person p);

Alternativ können Sie auch alle Änderungen an den Service übertragen und serverseitig auf den jeweiligen Änderungstyp filtern.
List<Person> changes = list.GetChanges();
service.UpdatePersons(changes);

Auf Serverseite können Sie daraufhin den Status der Objekte, sowie deren Originalwerte ermitteln.
public List<Person> UpdatePersons(List<Person> changes)
{
  foreach (Person p in changes)
  {
    switch (p.State)
    {
      case DataObjectStateType.Update:
        var orgValue = p.GetOriginalValue("Name");
        // Datensatz in DB aktualisieren
      …
    }
  }
}

Hierbei sollte die Service-Methode nicht nur eine Liste der Objekte entgegen nehmen, sondern auch zurückgeben. Dies ist erforderlich, da auf Datenbankseite z.B. Identity-Werte oder Timestamps vergeben werden, die an den Client zurück synchronisiert werden müssen.
Hierfür können Sie auf Clientseite die Methode MergeItems() aufrufen und die lokale Liste mit der serverseitigen synchronisieren.
List<Person> changes = list.GetChanges();
List<Person> refrehedList =
  service.UpdatePersons(changes);
list.MergeItems(refreshedList);

Wenn Sie jedoch oben genannte SaveChanges()-Methode aufrufen, können Sie sich diesen separaten Schritt sparen, da dieser automatisch durchgeführt wird.

Undo/Redo

Neben dem Speichern der Originalwerte, kann DataObject und DataObjectList<T> auch beliebige Änderungen rückgängig machen bzw. wiederholen. Beide Klassen bieten hierfür die Methoden Undo() und Redo(). Darüber hinaus können Sie auch einen benannten Undo-Punkt definieren, zu dem zurückgesprungen werden soll. Hierfür legen Sie zunächst über die Methode SetUndoPoint() einen Punkt an und geben diesem einen beliebigen Namen. Einer Überladung der Undo()-Methode können Sie daraufhin den Namen übergeben und alle bis dahin durchgeführten Änderungen werden entsprechen zurückgerollt.
list.SetUndoPoint("MyUndoPoint");
list[0].Name += "*";
list.Add(new Person() { Name = "Bob" };
list.Remove(1);
list.Undo("MyUndoPoint");

Wenn Sie mitbekommen wollen, ob sich die Undo- oder Redo-Liste geändert hat, können Sie sich auf das UndoListChanged bzw. RedoListChanged-Event anmelden.

Labels:

Dienstag, März 09, 2010

TT.DOM: Datenbindung

Wie hier bereits erwähnt, unterstützt Thinktecture.DataObjectModel (TT.DOM) die Entwicklung flexibler Datenklassen. In den folgenden Posts werde ich die wichtigsten Features des Frameworks vorstellen. Den Anfang mache ich mit dem Thema Datenbindung.
Damit ein Objekt optimal an die Oberfläche gebunden werden kann, sollte die entsprechende Klasse eine Reihe von Interfaces implementieren. Über diese kommuniziert die Datenbindungsinfrastruktur mit den gebundenen Objekten um z.B. Synchronisation, Transaktionssteuerung oder Eingabevalidierung zu realisieren. Die folgende Abbildung zeigt diese Kommunikation:
Neumann_Datenklassen2
  • INotifyPropertyChanged dient zur Synchronisation zwischen Datenquelle und Ziel. Auf diese Weise werden Änderungen, die programmatisch an den Objekten durchgeführt werden, an das gebundene Steuerelement gemeldet, sodass dieses die Anzeige entsprechend aktualisieren kann.
  • INotifyPropertyChanging dient zur Signalisierung von Änderungen, bevor diese durchgeführt werden. Dies Interface hat keine direkten Auswirkungen für die Bindung, ist jedoch für Change Tracking und Transaktionssteuerung wichtig.
  • IEditableObject stellt ein transaktionales Verhalten für die Datenbindung bereit. So können Sie beispielsweise mehrere Zellen in einem Grid geändert und per ESC wieder zurück gerollt werden.
  • IDataErrorInfo ist für die clientseitige Validierung zuständig. So kann der Benutzer bereits während der Eingabe auf fehlerhafte Werte aufmerksam gemacht werden.
Wie Sie sehen, gibt es eine Menge zu tun um eine Datenklassen für die Datenbindung zu optimieren.
Hier kommt TT.DOM ins Spiel. Es definiert die abstrakte Basisklasse DataObject, welche die oben genannten Interfaces implementiert. Sie können nun Ihre Datenklassen von DataObject ableiten und haben fast automatisch die entsprechenden Funktionalitäten. Das Einzige was Sie tun müssen, ist Änderungen an Ihren Properties an die Basisklasse zu melden.
public class Person : DataObject
{
  private string _name;
  public string Name
  {
    get { return _name; }
    set

      {
      base.OnPropertyChanging("Name");
      _name = value;
      base.OnPropertyChanged(“Name”); }
  }
}

Wollen Sie zusätzlich Validierungsregeln im Stile von IDataErrorInfo in der Datenklasse hinterlegen, überschreiben Sie zusätzlich den Indexer der Basisklasse und hinterlegen die Validierungsregeln nach folgendem Muster.
public class Person : DataObject
{
  ...
  public override string this[string columnName]
  {
    get
    {
      if ((columnName == "" || columnName == "Name") &&
          string.IsNullOrEmpty(this.FirstName))
      {
        return "\"Name\" ist ein Pflichtfeld!";
      }
      return string.Empty;
    }
  }
}

Auf diese Weise haben Sie Ihre Klasse ohne viel Aufwand mit Synchronisation, Transaktionssteuerung und Eingabevalidierung ausgestattet.
Wie Sie später noch sehen werden, bietet TT.DOM auch die Möglichkeit die Validierungsregeln programmatisch zur Laufzeit zu hinterlegen.

Listenbindung

Auch auf Listenebene müssen Sie für eine Synchronisation zwischen Datenquelle und Ziel sorgen. In Windows Forms und WPF können Sie hierfür das Interface IBindingList bzw. dessen Standardimplementierung BindingList<T> verwenden. Darüber hinaus stellt TT.DOM die Klasse DataObjectList<T> bereit. Diese leitet von BindingList<T> ab und stellt neben der Datenbindungsfunktionalität Einiges mehr zu Verfügung.
Um das Erstellen und Befüllen von DataObjectList<T> zu vereinfachen, stellt TT.DOM die Extension Method .ToDataObjectList() zu Verfügung. Sie kann auf alle Listen vom Typ IEnumerable angewendet werden.
List<Person> persons = service.GetPersons();
var list = persons.ToDataObjectList("Id");

Als Parameter wird der oder die Eigenschaften angegeben, die ein Objekt eindeutig identifizieren. Dies ist für eine spätere Synchronisation mit einem Middle Tier-Service notwendig, doch dazu  mehr in einem separaten Post ;)

Labels:

Sonntag, März 07, 2010

Just Released: Thinktecture.DataObjectModel

Bei der Entwicklung verteilter Anwendungen steht man als Entwickler häufig zwischen den Stühlen. Denn je nach Schicht ist man mit teilweise gegenläufige Anforderungen konfrontiert. Dies betrifft vor allem das Domänenmodell:
  • Aus Sicht der Middle Tier sollten Datenklassen dem POCO- bzw. DTO-Ansatz folgen, um eine möglichst hohe Interoperabilität zu gewährleisten.
  • Die Datenbankseite interessiert sich hingegen in erster Linie für die Originalwerte der geänderten Objekte, um die Datenkonsistenz bei parallelen Zugriffen gewährleisten zu können. Zudem ist auch Transaktionsunterstützung von Vorteil.
  • Auf Clientseite sollen die Objekte möglichst einfach gebunden werden können. Darüber hinaus müssen Änderungen verfolgt werden können, um eine spätere Aktualisierung zu gewährleisten. Der Anwender freut sich hingegen über Inline-Validierung und Undo/Redo-Support. Auch das dynamische Erstellten von Sortier- und Filterregeln zur Laufzeit ist sehr beliebt.
Das Problem ist nur, dass die vorhandenen Frameworks (WCF/WPF/EF) keine durchgängige Story für Datenklassen bieten, die allen Seiten gerecht wird.
Daher habe ich in den letzten Monaten an einem Framework gearbeitet, das diesen Missstand beheben soll: Das Thinktecture.DataObjectModel steht nun in einer ersten Betaversion auf CodePlex zu Verfügung.
Die wichtigsten Features:
  • Support für Data Binding
    (inkl. Validierung, Change Tracking und Undo/Redo-Support)
  • Dynamische Attributierung von Properties
    (DisplayName, ReadOnly, Browsable, Ordinal, …)
  • Implementierung von Views
    (inkl. Sortieren, Filtern, Computed Columns, etc.)
  • Automatische Speicherung von Originalwerten für die Behandlung von Konkurrenzsituationen auf Datenbankseite.
  • Übertragen von Objekten, inkl. Änderungen an WCF-Services
  • Unterstützung von System.Transactions. So können alle Objekte automatisch an lokalen oder verteilten Transaktionen teilnehmen.
Für die Verwendung kann die entsprechende Datenklasse entweder von einer Basisklasse ableiten, oder sie wird zur Laufzeit über einen Runtime Proxy mit der beschriebenen Funktionalität ausgestattet. Auf diese Weise können zum Beispiel auch Modelle vom Entity Framework oder anderen O/R-Mappern flexibel erweitert werden.
Weitere Informationen finden Sie auf http://dataobjectmodel.codeplex.com. Hier gibt’s nicht nur die Sourcen, sondern auch einige Beispiele für Windows Forms und WPF.
Darüber hinaus erscheint in den nächsten Monaten eine vierteilige Artikelserie in der dotnetpro, in der ich das Framework detailliert beschreiben werde.
Falls Sie Fragen oder Anregungen haben, geben Sie Feedback über das Forum der Codeplex-Seite oder per Mail (joerg neumann at thinktecture de)!

Labels:

Freitag, März 05, 2010

WPF: ICO vs. PNG

Viele Entwickler verwenden in ihren WPF-Anwendungen noch immer die guten, alten Windows Icons. Diese werden von WPF jedoch nicht optimal gerendert, wie das folgende Beispiel zeigt:

IcoVsPng

Neben der allgemeinen Unschärfe beim Rendern von Icons, sorgt ein weiterer Umstand für unschöne Ergebnisse: Wenn die Icon-Datei mehrere Icons unterschiedlicher Größe enthält, wählt WPF standardmäßig das Erste aus (meist 16x16 Pixel) und vergrößert es entsprechend.

Stattdessen sollten Sie besser PNG-Bitmaps verwenden. Diese werden nicht nur “sauberer” dargestellt, sondern bieten auch die von Icons bekannte Transparenz.

Für die Konvertierung von ICO nach PNG benötigen Sie jedoch einen leistungsfähigen (und daher meist kostenpflichtigen) Icon-Editor. Haben Sie den nicht, können Sie alternativ auch Power Point verwenden. Und das geht wie folgt:

  • Konvertieren Sie zunächst die ICO-Datei mit Paint nach PNG.
  • Dann öffnen Sie diese in Power Point
  • Klicken Sie im Ribbon “Format” den Punkt “Neu einfärben” und dann “Transparente Farbe bestimmen” an.

PPTTransparent

  • Daraufhin selektieren Sie den Hintergrund des Icons und speichern das ganze wieder als PNG-Datei ab.

Problematisch kann es jedoch werden, wenn die Hintergrundfarbe ebenfalls im Symbol selbst verwendet wird, da die entsprechenden Bereich dann ebenfalls transparent dargestellt werden. Daher sollten Sie den Hintergrund ggf. vorher mit einer anderen Farbe füllen.

So richtig schön ist das ganze Verfahren zwar nicht, dafür aber kostenlos ;)

Dienstag, März 02, 2010

WPF Threading ohne Intellisense

In meinen WPF-Schulungen kommt es immer wieder zu Verwirrungen bzgl. einiger Methoden, die im Zusammenhang mit Threading zum Einsatz kommen. Daher habe die Problematik hier noch einmal zusammengefasst.

Die Klasse DispatcherObject bietet mit CheckAccess() und VerifyAccess() zwei Methoden, mit denen abgefragt werden kann, ob der Zugriff auf ein Objekt aus dem aktuellen Thread aus möglich ist.

Das Problem ist nur, dass beide Methoden nicht über Intellisense zugreifbar sind! Dies führt dazu, dass viele Entwickler sie nicht kennen. Warum die Methoden nicht angezeigt werden, offenbart ein Blick in den Source Code:

[EditorBrowsable(EditorBrowsableState.Never)]
public bool CheckAccess()
{
  …
}

Hier sorgt das EditorBrowsable-Attribut dafür, dass die entsprechende Methode weder in Intellisense, noch im Object Browser angezeigt werden.

Das gleiche gilt übrigens auch für die gleichnamigen Methoden der Dispatcher-Klasse. Darüber hinaus werden alle Überladungen der Methoden Dispatcher.Invoke() und Dispatcher.BeginInvoke() nicht angezeigt, die einen Parameter vom Typ DispatcherPriority definieren.

Interessant ist an dieser Stelle, das die genannten Methoden den Anschein erwecken, als wären sie nicht vorhanden, jedoch ausführlich (inkl. Beispielen) in der MSDN beschrieben werden.

Verstehen muss man das nicht, oder?