24.2 Auflistungen binden
Die Bindung eines einzelnen Objekts an ein Steuerelement haben Sie nun gesehen. Meistens hat man jedoch nicht mit einzelnen Objekten, sondern mit Listen zu tun, die viele Objekte enthalten. Der recht allgemeine Begriff »Liste« kann durch viele spezielle Mengen ersetzt werden: Es kann sich dabei um ein herkömmliches Array handeln, um eine Collection oder gar um die Tabelle einer Datenbank.
Das Binden einer Liste an ein Steuerelement ist nicht mehr ganz so einfach wie das Binden eines einzelnen Objekts. Auch die Anforderungen, die an die bindungsfähigen Listensteuerelemente gestellt werden, sind komplexer. Wir müssen uns daher zuerst einmal ansehen, wie wir Listen an die entsprechenden Steuerelemente binden können.
24.2.1 Allgemeine Gesichtspunkte
Allzu viele bindungsfähige Steuerelemente hat die WPF nicht zu bieten. Mit ComboBox, ListBox, ListView und DataGrid sind bereits alle wesentlichen genannt. Diese Elemente sind alle von der gemeinsamen Basisklasse ItemsControl abgeleitet, die den Steuerelementen die Fähigkeit verleiht, Listen anzubinden.
Auch wenn Ihnen im ersten Moment die Auswahl spärlich erscheint, sollten Sie berücksichtigen, dass auch das Layout der bindungsfähigen Listenelemente mit Templates an die unterschiedlichsten Anforderungen angepasst werden kann. Natürlich werden wir auch darauf noch zu sprechen kommen.
Wenn wir uns über die Bindung von Listen unterhalten, sind es immer drei Eigenschaften, die eine entscheidende Rolle spielen. Diese sind Tabelle 24.1 zu entnehmen.
An ItemsControl-Steuerelemente lassen sich alle möglichen Auflistungen binden. Es kann sich ebenso um einfache Arrays handeln wie um spezialisierte Auflistungen. Die einzige Bedingung ist die Unterstützung durch das Interface IEnumerable. Es sei bereits an dieser Stelle darauf hingewiesen, dass zum Ändern oder Hinzufügen von Listenelementen noch weitere Gesichtspunkte eine wichtige Rolle spielen.
Eigenschaft | Beschreibung |
Diese Eigenschaft beschreibt einen Pfad zu einem Wert im Objekt. Der Wert wird zur Anzeige verwendet. |
|
Diese Eigenschaft verweist auf ein Objekt vom Typ IEnumerable. Dabei handelt es sich um das Listenobjekt, das im Steuerelement angezeigt wird. |
|
Diese Eigenschaft definiert ein DataTemplate für die im Steuerelement enthaltenen Objekte. |
24.2.2 Anbindung an eine »ListBox«
Im Beispiel ChangeNotificationSample hatten wir eine Person an die Controls im Fenster gebunden. Im nächsten Beispiel verwenden wir wieder dieselbe Klasse, möchten uns aber eine Liste von Personen in einer ListBox anzeigen lassen. Darüber hinaus sollen die Daten der aktuell in der ListBox selektierten Person in Textboxen angezeigt werden. Das Window zur Laufzeit können Sie in Abbildung 24.2 sehen.
Abbildung 24.2 Ausgabe des Beispiels »ListBoxBindingSample«
Sehen wir uns zuerst den gesamten XAML- und C#-Code des Beispiels an.
// Beispiel: ..\Kapitel 24\ListBoxBindingSample
<Window ...>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<ListBox Name="listBox1"></ListBox>
<Grid DataContext="{Binding ElementName=listBox1, Path=SelectedItem}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Label>Name:</Label>
<Label>Alter:</Label>
<Label>Adresse:</Label>
<TextBox Text="{Binding Path=Name}"></TextBox>
<TextBox Text="{Binding Path=Alter}"></TextBox>
<TextBox Text="{Binding Path=Adresse}"></TextBox>
</Grid>
</Grid>
</Window>
Der C#-Code in der Code-Behind-Datei:
public partial class MainWindow : Window {
public MainWindow() {
InitializeComponent();
listBox1.ItemsSource = GetPersons();
listBox1.DisplayMemberPath = "Name";
listBox1.SelectedIndex = 0;
listBox1.Focus();
}
private List<Person> GetPersons() {
List<Person> liste = new List<Person>();
liste.Add(new Person { Name = "Schulze", Alter = 56,
Adresse = "Bremen" });
[...]
return liste;
}
}
Listing 24.11 Programmcode des Beispiels »ListBoxBindingSample«
Die Bindung der ListBox an die Daten, die in diesem Beispiel durch die Methode GetPersons geliefert werden, erfolgt im Konstruktor mit der Methode ItemsSource. Jedes ListBoxItem-Element beschreibt genau eine Person mit ihren insgesamt drei Eigenschaften. Wir müssen deshalb mit der Eigenschaft DisplayMemberPath die anzuzeigende Eigenschaft spezifizieren, da wir ansonsten nur den voll qualifizierten Klassenbezeichner in der ListBox sehen.
Das aktuell in der ListBox selektierte ListBoxItem und das sich dahinter befindende Person-Objekt wird im zweiten, untergeordneten Grid mit dessen Eigenschaft DataContext gebunden:
<Grid DataContext="{Binding ElementName=listBox1, Path=SelectedItem}">
[...]
</Grid>
Die Textboxen, die sich innerhalb dieses Grid befinden, können sich ihrerseits an die durch den Datenkontext beschriebene Datenquelle (also das selektierte ListBoxItem) binden, müssen aber mit Path die Eigenschaft angeben, die von der Property Text angezeigt werden soll:
<TextBox Text="{Binding Path=Name}"></TextBox>
Wenn Sie dieses Beispiel zur Laufzeit ausprobieren, werden Sie feststellen, dass Änderungen, die Sie in den Textboxen eintragen, sofort im Person-Objekt gespeichert sind. Sie brauchen dazu nur innerhalb der Liste zu einem anderen Eintrag und wieder zurück zum edierten Element zu navigieren, um das bewiesen zu sehen. Dieses Verhalten wird durch den voreingestellten Two-Way-Modus der TextBox sichergestellt.
24.2.3 Änderungen der Collection an die bindenden Elemente weiterleiten
In Abschnitt 24.1.3 haben Sie gesehen, dass an eine Klasse besondere Anforderungen gestellt werden, damit sie Änderungen an die bindenden Elemente weiterleitet. Zur Erinnerung: Dazu war die Implementierung des Interfaces INotifyPropertyChanged notwendig. Das gilt sehr ähnlich auch für eine Collection. Hier lautet die Forderung, dass die Auflistung neben INotifyPropertyChanged eine weitere Schnittstelle implementieren muss, nämlich INotifyCollectionChanged. Erst mit dieser Schnittstelle ist es möglich, bindende Elemente zu informieren, wenn der Auflistung ein neues Objekt hinzugefügt oder ein Auflistungsmitglied gelöscht worden ist.
Sie brauchen aber nicht selbst eine solche Klasse zu entwickeln, denn mit der generischen Klasse ObservableCollection<> im Namespace System.Collections.ObjectModel stellt Ihnen .NET bereits eine passende zur Verfügung. Diese Collection kann ähnlich benutzt werden wie eine herkömmliche Auflistung. Damit wird es sehr einfach, eine Liste mehrerer Objekte vom Typ Person zu beschreiben:
ObservableCollection<Person> liste = new ObservableCollection<Person>();
Damit steht uns ein Auflistungsobjekt zur Verfügung, das wir zum Füllen von Steuerelementen wie der ComboBox oder der ListBox einsetzen können.
Erweiterung des Beispiels »ListBoxBindingSample«
Es bietet sich an dieser Stelle natürlich an, das Beispiel aus dem letzten Abschnitt (ListBoxBindingSample) zu ändern und es um Fähigkeiten zu ergänzen, Änderungen am Inhalt der Collection den bindenden Elementen automatisch mitzuteilen. Das Layout des Fensters soll so gestaltet werden, wie in Abbildung 24.3 zu sehen ist.
Abbildung 24.3 Fenster des Beispiels »ObservableCollectionSample«
Klickt der Anwender auf die Schaltfläche Hinzufügen, werden die drei Textboxen geleert, und der Anwender kann die Daten der neuen Person eintragen. Gleichzeitig werden auch die beiden Schaltflächen Speichern und Abbrechen aktiviert, die nach dem Starten der Anwendung zunächst deaktiviert sind. Das Löschen aus der Liste bezieht sich immer auf die in dem Moment in der ListBox ausgewählte Person.
Als Erstes muss die Methode GetPersons statt eines Objekts vom Typ List<Person> ein Objekt vom Typ ObservableCollection<Person> zurückliefern:
private ObservableCollection<Person> GetPersons()
{
liste.Add(new Person { Name = "Schulze", Alter = 56,
Adresse = "Bremen" });
[...]
return liste;
}
Listing 24.12 Die Anpassung der Methode »GetPersons«
Die Instanziierung des Collection-Objekts erfolgt sinnvollerweise auf Klassenebene, damit auch andere Methoden Zugriff auf das Objekt haben.
Nach der Ergänzung des Window um die entsprechenden Schaltflächen offenbaren sich bei der Entwicklung schnell zwei Probleme:
- Wir müssen den Inhalt der TextBox, die das Alter der Person als Zeichenfolge angibt, in einen null-fähigen Integer konvertieren.
- Nicht minder schwer ist ein anderes Problem, das im Zusammenhang mit der Datenbindung der Textboxen auftaucht. Im Grunde genommen soll ja erst in dem Moment die Collection und auch die Anzeige in der ListBox aktualisiert werden, wenn auf die Schaltfläche Speichern geklickt wird. Bedingt durch die Datenbindung wird aber schon beim Verlassen jeder TextBox die ListBox aktualisiert – also zu einem zu frühen Zeitpunkt.
Lassen Sie uns zunächst das erstgenannte Problem lösen. Dieses tritt auf, wenn auf die Schaltfläche Hinzufügen geklickt wird und die Inhalte der Textboxen dazu verwendet werden, das neue Person-Objekt mit den entsprechenden Eigenschaftswerten zu erzeugen. Das Alter kann per Definition in der Klasse null sein. Also muss der Inhalt der TextBox, also eine Zeichenfolge, in den Typ int? konvertiert werden. Da die üblichen Wege der Typkonvertierung versagen, muss ein anderer Weg beschritten werden.
Eine relativ einfache Lösung bietet sich über die statische Methode ChangeType der Klasse Convert an, die wie folgt definiert ist:
public static Object ChangeType(Object, Type)
Dem ersten Parameter wird ein Objekt übergeben, das die Schnittstelle IConvertible implementiert. Ein Blick in die Dokumentation verrät uns, dass String dieses Interface unterstützt. Der zweite Parameter gibt den Typ des zurückzugebenden Objekts an. Da der Rückgabewert der Methode ChangeType vom Typ Object ist, muss am Ende nur noch eine entsprechende Typkonvertierung durchgeführt werden.
Es bietet sich an, für die endgültige Konvertierung eines String in int? eine Methode zu definieren, die als Erweiterungsmethode der Klasse String bereitgestellt wird:
static class ExtensionMethods {
public static T To<T>(this string text) {
return (T)Convert.ChangeType(text, typeof(T));
}
}
Listing 24.13 Erweiterungsmethode zur Typkonvertierung
Damit ist das erste der beiden Probleme gelöst. Kommen wir nun zum zweiten.
Eine einmal eingestellte Datenbindung ist immer aktiv. Nehmen wir an, der Anwender möchte eine neue Person hinzufügen. Er klickt also die entsprechende Hinzufügen-Schaltfläche an und trägt als Erstes den Namen der neuen Person ein. Wird die TextBox verlassen, wird der Wert sofort in die ListBox eingetragen, ohne dass das neue Objekt tatsächlich schon angelegt ist. Das ist der Preis der Datenbindung – in diesem Moment allerdings ein Preis, der nicht vorteilhaft ist.
Die Lösung dieses Problems ist recht einfach. Wir müssen die Bindung mit C#-Programmcode aufheben, wenn der Anwender die Schaltfläche Hinzufügen anklickt. Die Bindung muss allerdings aktiviert werden, wenn der Anwender auf eine der beiden Schaltflächen Abbrechen oder Speichern geklickt hat.
// Beispiel: ..\Kapitel 24\ObservableCollectionSample
private void btnAdd_Click(object sender, RoutedEventArgs e) {
txtName.Text = ""; txtAddress.Text = ""; txtAge.Text = "";
txtName.Focus();
// Bindung lösen
BindingOperations.ClearBinding(txtName, TextBox.TextProperty);
BindingOperations.ClearBinding(txtAddress, TextBox.TextProperty);
BindingOperations.ClearBinding(txtAge, TextBox.TextProperty);
[...]
}
private void btnSave_Click(object sender, RoutedEventArgs e) {
Person person = new Person { Name = txtName.Text,
Adresse = txtAddress.Text };
if (txtAge.Text == "") person.Alter = null;
else person.Alter = (txtAge.Text).To<int>();
liste.Add(person);
// Bindung erstellen
SetBinding();
[...]
}
private void btnCancel_Click(object sender, RoutedEventArgs e) {
// Bindung erstellen
SetBinding();
listBox1.SelectedIndex = 0;
[...]
}
private void SetBinding() {
// Bindungen neu festlegen
Binding binding = new Binding("Name");
txtName.SetBinding(TextBox.TextProperty, binding);
binding = new Binding("Adresse");
txtAddress.SetBinding(TextBox.TextProperty, binding);
binding = new Binding("Alter");
txtAge.SetBinding(TextBox.TextProperty, binding);
}
private void btnDelete_Click(object sender, RoutedEventArgs e) {
int index = listBox1.SelectedIndex;
liste.RemoveAt(index);
if (liste.Count == 0) btnDelete.IsEnabled = false;
else listBox1.SelectedIndex = 0;
}
Listing 24.14 C#-Code aus dem Beispiel »ObservableCollectionSample«
Ihre Meinung
Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an kommunikation@rheinwerk-verlag.de.