Good to know – C#

Databinding – Datenbindung

Basics

Databinding ist, meiner Auffasung nach, die Möglichkeit verschiedene Datenspeicher miteinander zu synchronisieren. In der einfachsten Form hat man zwei verschiedene Objekte, die Quelle und das Ziel. Beide besitzen eine Eigenschaft (Property). Die Eigenschaft der Quelle und des Ziels sollen verknüpft werden. Verknüpft bedeutet in dem Fall dass sowohl Quelle als auch Ziel in der Lage sind über Änderungen der jeweils anderen Objekteigenschaft informiert zu werden.
Für diese Synchronisierung gibt es verschiedene Modi:

  • OneWay: Änderungen werden nur von der Quelle zum Ziel mitgeteilt. Das ist der default Modus.
  • TwoWay: Änderungen werden in beide Richtungen mitgeteilt – Quelle zum Ziel und Ziel zur Quelle.
  • OneWayToSource: Änderungen werden nur vom Ziel zur Quelle mitgeteilt.

Wie wird eine Datenbindung aufgebaut? In C# muss man nur darauf achten, dass das Ziel der Datenbindung ein DependencyProperty (DP) ist. Das Zielobjekt muss demnach ein DependencyObject sein. Das Quellobjekt besitzt keine weiteren Anforderungen. Gedanklich und sprachlich bindet man immer das Bindungsziel an die Bindungsquelle.
Als Beispiel nehmen wir zwei einfache Klassen: SimpleClassA und SimpleClassB. SimpleClassA dient als Quelle und enthält nur ein normales (CLR) Property. SimpleClassB dient als Ziel der Bindung und enthält die selbe Property wie SimpleClassA, jedoch als DP implementiert.

/* Non-DependencyObject 
 * Name is a normal (CLR) property
 * */
public class SimpleClassA
{
    public String Name { get; set; }

    public SimpleClassA()
    {
        this.Name = "A";
    }
}

/* This class derives from DependencyObject
 * 
 * */
public class SimpleClassB : DependencyObject
{
    public static DependencyProperty NameProperty =
        DependencyProperty.Register("Name", typeof(String), typeof(SimpleClassB));

    public String Name
    {
        get { return (String)this.GetValue(NameProperty); }
        set { this.SetValue(NameProperty, value); }
    }

    public SimpleClassB()
    {
        this.Name = "B";
    }
}

Nun soll eine Verknüpfung der Property Name zwischen beiden Klassen erstellt werden. Im Code nutzt man das Binding Objekt dazu:

SimpleClassA a = new SimpleClassA();
SimpleClassB b = new SimpleClassB();

// source: a
// target: b (has DependencyProperty)
Binding bindingName = new Binding("Name");
bindingName.Source = a;
BindingOperations.SetBinding(b, SimpleClassB.NameProperty, bindingName);

Welche Synchronisierung kann man nun von dieser Datenbindung erwarten? KEINE!
Warum? Das Quellobjekt a der Klasse SimpleClassA besitzt keinen Mechanismus, um das gebundene Ziel über Änderungen zu informieren. Die default Kommunikation OneWay (von der Quelle zum Ziel s.o.) funktioniert folglich nicht.
ACHTUNG: Dennoch nimmt das Ziel direkt nach dem Setzen der Bindung den Wert der Quelle an. Das ist generell so, bedeutet aber nicht, dass die Kommunikation von Quelle zum Ziel bereits funktioniert.
In der Gegenrichtung sieht es etwas besser aus. Das Ziel implementiert Name als DP und ist selbst von DependencyObject abgeleitet, womit es inhärent die Möglichkeit besitzt gebundene Objekte über Änderungen von Name zu informieren.
WICHTIG: Da das Ziel-Property einer Datenbindung immer ein DependencyProperty sein muss ist die Kommunikation von Änderungen vom Ziel zur Quelle immer per se gegeben.
Wieso funktioniert hier die Kommunikation vom Ziel zur Quelle dennoch nicht? Weil der Modus der Datenbindung auf OneWay (default) steht! Er muss auf TwoWay gesetzt werden, um Änderungsbenachrichtigungen an die Quelle zu ermöglichen:

bindingName.Mode = BindingMode.TwoWay;

WICHTIG: Das Binding Objekt bindingName ist nachdem die Bindung mittels BindingOperations.SetBinding(…) gesetzt wurde nicht mehr gültig/verfügbar. Der Modus muss entweder vor dem Setzen der Bindung geändert werden oder man holt sich das Binding Objekt vom Ziel nachdem die Bindung gesetzt wurde:

Binding bindingName = BindingOperations.GetBinding(b, SimpleClassB.NameProperty);

Der TwoWay Modus ermöglicht jetzt die Änderungsbenachrichtigung vom Ziel zur Quelle. Die (eigentlich fundamentale) Benachrichtigung von der Quelle zum Ziel muss generell erst ermöglicht werden. Dafür gibt es u.a. zwei Möglichkeiten.

  • Die Quelle implementiert das Interface INotifyPropertyChanged (namespace: System.ComponentModel, System.dll )
  • Die Quelle implementiert das Property als DP

Die zweite Variante ist schnell umgesetzt und funktioniert, da ein DP innerhalb eine DependencyObjects gebundene Objekte automatisch über Änderungen infomieren kann. Im obigen Beispiel könnte man das umsetzen indem die Datenbindung direkt zwischen zwei Objekten der Klasse SimpleClassB statt findet. Man bindet dann ein DP an ein DP.
Eine andere Möglichkeit ist durch das Interface INotifyPropertyChanged gegeben. Es definiert folgendes Event:

public event PropertyChangedEventHandler PropertyChanged;

Der Delegate PropertyChangedEventHandler ist wie folgt definiert:

public delegate void PropertyChangedEventHandler(object sender,
                                                 PropertyChangedEventArgs e);

Er wird mit einem Objekt vom Typ PropertyChangedEventArgs aufgerufen. Diese beinhalten wiederum ein Property welches den Namen des Property speichert das geändert wurde und für welches die Änderungsinformation weitergegeben werden soll. Möchte eine Klasse SimpleClassC nun Objekte über Änderungen via INotifyPropertyChanged informieren muss sie das Event PropertyChanged zur Verfügung stellen und dieses auslösen sobald sich ein Property der Klasse geändert hat. Der Name des betroffenen Property wird über die EventArgs an die Callback-Funktionen weitergereicht:

/* Implement INotifyPropertyChanged
 * 
 */
public class SimpleClassC : INotifyPropertyChanged
{
	private String name;
	public String Name
	{
		get { return this.name; }
		set
		{
			this.name = value;
			NotifyPropertyChanged("Name");
		}
	}

	public SimpleClassC()
	{
		this.Name = "C";
	}

	#region INotifyPropertyChanged Members

	public event PropertyChangedEventHandler PropertyChanged;

	#endregion

	private void NotifyPropertyChanged(string propertyName)
	{
		if (this.PropertyChanged != null)
		{
			this.PropertyChanged(this, 
                               new PropertyChangedEventArgs(propertyName));
		}
	}
}

Eine galante Möglichkeit den Bezeichner des Property als String automatisch zu übergeben sieht wie folgt aus:

private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")

Um [CallerMemberName] nutzen zu können muss der namespace System.Runtime.CompilerServices eingebunden werden.
Mit der Implementierung von INotifyPropertyChanged erfolgt nun auch die Komunikation von Änderungen von der Quelle zum Ziel. Die Datenbindung funktioniert jetzt in beide Richtungen.

Freezables

Databinding löst generell eine Änderungsbenachrichtigung aus wenn sich ein gebundenes Property ändert. Diese Änderung erfolgt direkt über den Set Accessor des Property. Das bedeutet ein Property ändert sich nur wenn es gänzlich neu gesetzt wird. Bei einem String Name wie im obigen Beispiel ist das ausreichend. Wie sieht es aber aus wenn der Typ des Property selbst wiederum Sub-Properties definiert? Hat man ein Property Freund vom Typ Person der wiederum zwei Properties für Vorname und Zuname (vom Typ String) speichert, so findet keine Änderung von Freund statt wenn jemand beispielweise Vorname in Freund ändert! Augenscheinlich hat sich das Objekt Freund verändert. Da der Freund aber nicht komplett neu zugewiesen wurde kommt diese Veränderung nicht als Änderungsbenachrichtigung beim Databinding an! Es ist der Unterschied zwischen folgenden beiden Anweisungen:

MeineKlasse.Freund = new Person(...); // Änderung für Bindung an Freund
MeineKlasse.Freund.Vorname = "Heiko"; // Keine Änderung

Im Umgang mit Databinding halte ich diese Erkenntnis für äußerst wichtig. Man muss hier zwischen Wert- und Referenztypen unterscheiden. Ein Werttyp wird IMMER neu zugewiesen

Delegates

Delegates sind die Grundvorraussetzung für Events in C#. Delegates sind einfach nur ein abstrahiertes Konzept (Wrapper) für Funktionszeiger. Im Gegensatz zu Funktionszeiger in C/C++ sind sie aber typsicher. Die Signatur des Delegaten muss mit der des enthaltenen Funktionszeigers identisch sein. Ein Delegate wird wie folgt definiert:

public delegate string StringFuncDelegate(string message);

Jede Funktion, die einen String als Argument erhält und einen String zurückgibt kann durch diesen Delegaten repräsentiert werden. Der Name des Delegaten definiert jetzt einen Typ von dem Objekte angelegt werden können (delegate type). Um den Delegaten zu nutzen, muss man ihn nun noch instanziieren und ihm eine solche Funktion übergeben (delegate instance). Das kann eine tatsächlich definierte Funktion oder eine Anonyme Funktion (die direkt als Argument in der Delegatinstanziierung übergeben wird und nur dort existiert) sein.

public static string print(string message)
{
    System.Console.WriteLine(message);
}

// Delegate instanziieren und zuweisen, stringDel ist jetzt die Instanz eines Delegaten
StringFuncDelegate stringDel = print;

// Delegate aufrufen
stringDel("Hello World!");

// Achtung: Delegates können auf verschiedene Weisen instanziiert werden, 
// oben ist die Kurzform seit C# 2.0 gezeigt die direkt die Funktion zuweist
// außerdem gibt es folgende Möglichkeiten:

// klassische Variante
StringFuncDelegate stringDel = new StringFuncDelegate(print);

// Lambda Variante mit anonymer Funktion
StringFuncDelegate stringDel = msg  => { // put the anonymous function content here };

Da der Delegate selbst ein Objekt ist kann er ganz normal als Parameter an Methoden übergeben werden. Die können ihn wiederum aufrufen, was als Callback Funktion bekannt ist. ACHTUNG: Interessant ist dabei, dass die aufrufenden Funktion nicht wissen muss zu welchem Objekt/Klasse die Methode gehört, da sie direkt über den Funktionszeiger aufgerufen wird, d.h. hier ist ein gewisser Entkopplungsgrad möglich, weshalb Delegate auch teilweise an Stelle von Interfaces verwendet werden.

Der Operator += (addition assignment) ist bei Delegates wichtig: Intern verfügt ein Delegate über eine Invocation List in der alle Funktionen gespeichert sind, die er aufrufen soll. Er kann somit mehrere Funktionszeiger speichern (Multicasting):

StringFuncDelegate stringDel1 = method1;
StringFuncDelegate stringDel2 = method2;
StringFuncDelegate stringDelAll = stringDel1 + stringDel2; // += stringDel1 auch möglich

Mit den Operatoren -/-= können Funktionen von der Invocation List entfernt werden. Auf die Invocation List kann man zugreifen z.bsp. mit:

int invocationCount = d1.GetInvocationList().GetLength(0);

Aber: Delegates sind immutable. Sie werden durch die Operationen +/- nicht verändert. Der Compiler generiert eine neue kombinierte Delegate Instanz (mit der gewünschten Liste von Funktionen) und gibt diese zurück.

Events:

Events und Delegates ähneln sich in C# stark und werden vor allem auf eine ähnliche Weise benutzt. Dennoch gibt es wichtig Unterschiede. Ein Event ist keine Instanz eines Delegaten (was man fälschlich denken könnte), aber sie nutzt intern eine solche Instanz.
Events sind gekapselte Delegateninstanzen, bei denen der Zugriff bzw. das Abonieren des Events über Add/Remove Methoden erfolgt. Man kann ein Event wie folgt deklarieren:

public event EventHandler MyEvent;

Das ist die typische Kurzform der Deklaration. Der Compiler erzeugt hier im Hintergrund ein Event und eine Delegateninstanz als Feld. Das Event ist von außen (wie ein Property) ansprechbar und Anfragen das Event zu abonieren werden an die Add Methode weitergeleitet, die wiederum die Delegegateninstanz um die gegebene Callbackfunktion erweitert. Folgendes Beispiel aboniert das Event und hinterlegt die Callbackfunktion OnMyEvent():

MyEvent += new EventHandler(MyOnEvent);

public void MyOnEvent(object sender, EventArgs e)
{
	Console.WriteLine("the event");
}

Man sieht hier schön, dass das Event genauso genutzt werden kann wie eine Delegateninstanz, weshalb Event und Delegate so ähnlich erscheinen. Der Delegatentyp EventHandler ist in C# grundlegend definiert und repräsentiert den meistgenutzten Typ einer Funktion, die durch ein Event aufgerufen werden soll:

public delegate void EventHandler(object sender, EventArgs e);

Das Event kann nun ausgelöst werden (raise):

MyEvent(this, new EventArgs());

Intern werden jetzt alle in der Delegateninstanz hinterlegten Funktionszeiger mit den gegebenen Parametern aufgerufen. Achtung: Wenn das Event nicht aboniert wurde, ist es null. Deswegen wird vor dem Auslösen meist auf null geprüft.

Neben der Kurzform zur Deklaration von Events gibt es außerdem eine ausführliche (explizite) Variante, bei der der Compiler nichts automatisch generiert:

// define an event explicitely
public event EventHandler MyEvent
{
	add
	{
		Console.WriteLine ("add operation");
	}

	remove
	{
		Console.WriteLine ("remove operation");
	}
}

Diese Form ist nicht sehr üblich, zeigt aber wie das Methodenpaar definiert wird. Man sieht hier, dass ein Event definiert werden kann, ohne dass in den Add/Remove Methoden tatsächlich Funktionen hinzugefügt/entfernt werden. Daher würde mit der Anweisung MyEvent += new EventHandler(…) keine Callback Funktion beim Event registriert werden. Um MyEvent sinnvoll nutzen zu können muss es wie folgt erweitert werden:

private EventHandler myEventField;
public event EventHandler MyEvent
{
	add
	{
		myEventField += value;
	}

	remove
	{
		myEventField -= value;
	}
}

Callbackfunktionen können jetzt intern dem Feld myEventField (Delegateninstanz) hinzugefügt werden. Jedoch kann MyEvent nicht wie gewohnt ausgelöst werden. Man muss hier direkt die Delegateninstanz „auslösen“.

myEventField.(this, new EventArgs());

Wird ein Event implizit deklariert (Kurzform) wird im Hintergrund ebenfalls das Auslösen des Events weitergeleitet an die Delegateninstanz des Events:

MyEvent(this, EventArgs.Empty); // is translated to ...
myEventField(this, EventArgs.Empty);

Quelle

http://csharpindepth.com/Articles/Chapter2/Events.aspx
http://msdn.microsoft.com/en-us/magazine/cc163533.aspx

Dependency Properties (Abhängigkeitseigenschaften):

Dependency Properties (DPs) sind eine Erweiterung der normalen (CLR) Properties mit ihren Get/Set Methoden. Sie sind die Grundlage für einige WPF Funktionalitäten wie Datenbindung, Animation, Styles etc.
Der Wert einer DP wird nicht (nur) direkt aus dem Wert eines lokal gespeicherten backing fields gebildet sondern dynamisch in Abhängigkeit von anderen Einflüssen ermittelt. Dabei werden z.Bsp. berücksichtigt: Der default Wert der DP. Ein Wert der eventuell vererbt wurde (Nicht OOP sondern an Children im XAML Baum). Ein Wert aus einer eventuell vorhandenen Animation. Ein Wert aus einer Datenbindung. etc. Der zuletzt ermittelte Wert kann zusätzlich automatisch validiert werden.

Im Gegensatz zu CLR Properties sind DPs statisch in einer Klasse d.h. keine Membervariable sondern eine Klassenvariable. Möchte man eine DP in einer Klasse definieren muss diese Klasse von DependencyObject erben!
Eine DP RadiusProperty in der Klasse Circle wird wie folgt definiert:

public class Circle : DependencyObject
{
  public static readonly DependencyProperty RadiusProperty = 
         DependencyProperty.Register("Radius", typeof(double), typeof(Circle));
}

Das Schlüsselwort static (zwingend erforderlich) zeigt an, dass die DP für die gesamte Klasse (und alle abgeleiteten Klassen) nur einmal exisitiert und gemeinsam genutzt wird. Das Attribut readonly (zwingend erforderlich) bedeutet, dass die DP (ähnlich wie eine Konstante) spätestens im statischen Konstruktor der Klasse initialisiert werden muss und ihren Wert danach behält. Das heißt NICHT, dass der Wert den sie speichert nicht geändert werden kann! Im obigen Beispiel wird die DP direkt initialisiert und beim DP Subsystem via Register registriert.
Oben fehlt noch die Möglichkeit die DP RaidusProperty bequem mit dem Zuweisungsoperator abfragen/setzen zu können, so wie es normal für CLR Properties mit Getter/Setter geht. Dazu wird eine CLR Property ergänzt:

public int Radius
{
get { return (int)GetValue(RadiusProperty); }
set { SetValue(RadiusProperty, value); }
}

WICHTIG die Namenskonvention: Die DP bekommt immer den Namen der CLR Property (im obigen Fall Radius) plus den Suffix „Property“. Die DP ist auch ohne die CLR Property voll nutzbar. Die CLR Property definiert selbst kein backing field hier, da keine Auto Property! Man sollte keinen (wichtigen) Code in die Getter/Setter packen, da XAML diese nicht aufruft sondern direkt über GetValue/SetValue geht.

Die Methoden GetValue/SetValue in der CLR Property wurden von DependencyObject geerbt und wissen über das Argument RadiusProperty welche Eigenschaft angesprochen wird.
DPs können den Speicherbedarf einer Anwendung wesentlich reduzieren. Bei WindowsForms hat jedes Steuerelement eine Vielzahl von Eigenschaften die Speicherplatz brauchen obwohl sie in den meisten Fällen vom Nutzer nicht geändert werden und ihren Default Wert behalten. DPs sind dagegen static und ihr default Wert wird für die gesamte Klasse nur einmal gespeichert. Erst wenn einer Instanz ein eigener Wert zugewiesen wird, wird dieser lokal hinterlegt und Speicherplatz dafür benötigt. Dafür wird aber kein einfaches backing field verwendet sondern ein Dictionary, dass jedes Objekt einer von DependencyObject abgeleiteten Klassen hat. Als key wird der Name der Property verwendet und der value ist dann der lokal gespeicherte Wert. Nur mit dem Dictionary kann lokal der Wert dynamisch gespeichert werden, ohne dass ständig Speicherplatz dafür reserviert wurde.

Jedes DP hat inherent die Möglichkeit Objekte über Änderungen seines Wertes zu informieren. Das läuft über ein Callback welches z.Bsp bei den MetaDaten des DP mit angegeben werden kann:

private static void OnMyPropertyChanged(DependencyObject source, 
        DependencyPropertyChangedEventArgs e) {}

ACHTUNG, die Callbackmethode wird als statische Methode implementiert. Das Objekt für welches die Eigenschaft tatsächlich geändert wurde wird in source übergeben. Der Name der geänderten Eigenschaft kommt in e. Dieser Mechanismus wird im Databinding verwendet. Das Ziel welches dort gebunden wird ist IMMER eine DP! Von daher ist die Änderungsbenachrichtigung vom Ziel zur Quelle inherent immer schon mit dabei und wird vom Binding Objekt genutzt/weitergeleitet. Die Eigenschaften aller Controls sind daher hautpsächlich DPs. Wird im View (Eingabemaske) etwas geändert werden die Daten dadurch sofort zum Model weitergeleitet und aktualisiert. Beim Databinding ist also die umgekehrte Richtung der Kommunikation von Haus aus schon implementiert: vom Ziel (View) zur Quelle (Model).

Es gibt zwei generelle Regeln zur Anwendung/Nichtanwendung von DPs:

  • For XAML controls, use dependency properties;
  • For data (which you bind to in the interface), use INotifyPropertyChanged.

Quellen:

http://wpftutorial.net/DependencyProperties.html
http://openbook.galileocomputing.de/visual_csharp_2012/1997_26_001.html#dodtp5591376e-61df-4e77-a551-681b9cc62287

Viewport Overlay for Events

Mouseevents wie MouseDown, MouseLeave, MouseMove werden in einem Viewport3D Element nur generiert und ausgelöst wenn sich der Mauszeiger aktuell über einem 3D Objekt der Szene befindet. Für Kamerasteuerungen ist das wenig wünschenswert. Von daher wird über den Viewport ein transparentes Objekt gelegt, dass die selbe Fläche einnimmt und die Events generiert. Möglich sind z.Bsp: <Border> und <Canvas>. WICHTIG: Dem gewählten Overlay muss eine Hintergrundfarbe zugewiesen werden, sonst werden die Events ebenfalls nur bei Überfahren von Objekten generiert. Weiß ist als Farbe völlig ausreichend:

<Border Background="White">... take some events ... <Viewport3D></Viewport3D></Border>

Binding mit RelativeSource Self

Einfach aber wichtig: Nutzt man folgendes Konstrukt zur Datenbindung in XAML, bindet man nicht an das aktuelle Window Object sondern an das XAML Tag/Element in welchem man die Bindung gerade definiert.

{Binding RelativeSource={RelativeSource Self}}

Damit kann man die Eigenschaften eines XAML Elements an andere Eigenschaften desselben Elements binden. Um jedoch an eine Eigenschaft des aktuellen Window zu binden kann eine der folgenden Varianten genutzt werden:

{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=Window}, Path=...}
{Binding ElementName=myWindow, Path=...}    // the <Window> must be named with x:Name=myWindow