In WPF Daten an eine ListBox zu binden und das ausgewählte Element in Formularfelder bearbeiten zu können ist zwar etwas kompliziert, aber Voodoo und Magie ist dafür nicht notwendig.
Das folgende Vorgehen habe ich mit .NET Framework 4.0 erstellt, ich weiß nicht, ob es mit anderen (insbesondere älteren) Versionen funktioniert.
Beschrieben wird:
- Das Binden einer Liste von Objekten an eine ListBox. (Das Binden ist im Code-Behind programmiert.)
- Eingabefelder werden an das ausgewählte Element der Liste gebunden um den Eintrag zu bearbeiten.
- Der Listeneintrag verändert sich, sobald das entsprechende Eingabefeld für den Titel geändert wird.
- Die Eingabefelder deaktivieren sich, wenn kein Element ausgewählt ist (oder die Liste leer ist).
- Listeneinträge können hinzugefügt, entfernt und verschoben werden.

Dieses Beispiel als komplettes Visual Studio 2010 Projekt:
ListBoxBinding.zip (13,89 kb)
Schritt 1: Neues WPF-Projekt erstellen
Eine neue WPF Anwendung erstellen (WPF Application).
In dem Fenster müssen ein paar Elemente plaziert werden. Die Liste, Eingabefelder und Buttons.
<Window x:Class="ListBoxBinding.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525" Background="WhiteSmoke">
<Grid Margin="10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="3*" />
<ColumnDefinition Width="4*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ListBox Grid.Column="0" Grid.Row="0" Name="lbMain" Margin="3">
</ListBox>
<StackPanel Name="FormPanel" Margin="6,3,3,3"
Grid.Column="1" Grid.Row="0" Grid.RowSpan="2">
<CheckBox Name="cbEnabled" Content="Aktiviert" />
<Label Content="Titel:" />
<TextBox Name="tbTitle" />
<Label Content="Weitere Daten:" />
<TextBox Name="tbData1"
AcceptsReturn="True" MinLines="4" TextWrapping="Wrap" />
</StackPanel>
<GridSplitter HorizontalAlignment="Left" ResizeDirection="Columns" Width="3"
Grid.Column="1" Grid.RowSpan="2" />
<WrapPanel Orientation="Horizontal" FlowDirection="RightToLeft"
Grid.Column="0" Grid.Row="1" >
<Button Name="bDown" Padding="3" Content="Runter" Click="bDown_Click" />
<Button Name="bUp" Padding="3" Content="Hoch" Click="bUp_Click" />
<Button Name="bAdd" Padding="3" Content="Neu" Click="bAdd_Click" />
<Button Name="bRemove" Padding="3" Content="Löschen" Click="bRemove_Click" />
</WrapPanel>
</Grid>
</Window>
Im XAML befindet sich keinerlei Datenbindung, das mache ich alles im Code(-Behind).
Schritt 2: Klassen mit unseren Daten
Als erstes überlege ich mir, welche Daten in der Liste gespeichert werden sollen. Hier wird die "Nutzlast" für die Liste verstaut.
using System;
namespace ListBoxBinding
{
public class ListBoxData
{
public String Title { get; set; }
public String Data1 { get; set; }
public Boolean Enabled { get; set; }
public ListBoxData()
{
this.Title = String.Empty;
this.Data1 = String.Empty;
this.Enabled = true;
}
}
}
Schritt 3: Usere Hilfsklasse für den Listeneintrag
Ich erstelle zusätzlich eine Klasse, die als Darstellungsbehälter für den Listeneintrag dient und ein paar Datenbindungsaufgaben übernimmt. Ich lass von ListBoxItem erben, damit ich die ganzen Formatierungsoptionen davon benutzen kann. (Wird in diesem Beispiel aber nicht benutzt.)
using System;
using System.Windows.Controls;
using System.Collections.ObjectModel;
using System.Windows.Data;
using System.Windows;
using System.Windows.Media;
namespace ListBoxBinding
{
public class ListBoxView : ListBoxItem
{
private ObservableCollection<ListBoxView> ParentList;
public ListBoxData Data { get; private set; }
public ListBoxView(ObservableCollection<ListBoxView> inParentList,
String inTitle, String inData1)
{
this.Data = new ListBoxData();
this.Data.Title = inTitle;
this.Data.Data1 = inData1;
this.SetBinding(
ListBoxItem.ContentProperty,
new Binding { Source = this.Data, Path = new PropertyPath("Title") });
this.ParentList = inParentList;
}
public Boolean CanUp
{
get { return this.ParentList.IndexOf(this) > 0; }
}
public Boolean CanDown
{
get { return this.ParentList.IndexOf(this) < this.ParentList.Count - 1; }
}
public Boolean Present { get { return true; } }
}
}
Schritt 4: Liste mit Daten befüllen
Im Code-Behind unserer Fenster-Klasse erzeugen ich eine Eigenschaft (DataList), die unsere Daten für die Liste speichert. Wichtig dabei ist der Type ObservableCollection, weil nur dadurch eine Zwei-Wege-Datenbindung (TwoWay) mit "ItemsSource" (siehe unten) möglich ist.
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Collections.ObjectModel;
namespace ListBoxBinding
{
public partial class MainWindow : Window
{
private ObservableCollection<ListBoxView> DataList = new ObservableCollection<ListBoxView>();
public MainWindow()
{
InitializeComponent();
this.DataList.Add(new ListBoxView(this.DataList, @"Erster", @"Daten eins"));
this.DataList.Add(new ListBoxView(this.DataList, @"Zweiter", @"Daten drei"));
this.DataList.Add(new ListBoxView(this.DataList, @"Dritter", @"Daten fünf"));
this.InitializeBinding();
}
// [...] Siehe unten
}
}
Schritt 5: Datenbindungen einrichten
In meinem Beispiel wird die Datenbindung komplett im Code-Behind eingerichtet. Ich finde das übersichtlicher als in der xaml-Datei. Es kann natürlich aber auch dort erledigt werden.
Die Folgende Funktion gehört in die Klasse MainWindow.
public void InitializeBinding()
{
this.lbMain.ItemsSource = this.DataList;
this.lbMain.IsSynchronizedWithCurrentItem = true;
this.tbTitle.SetBinding(
TextBox.TextProperty,
new Binding {
Source = this.lbMain,
Path = new PropertyPath("SelectedItem.Data.Title"),
Mode = BindingMode.TwoWay,
UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged });
this.tbTitle.SetBinding(
TextBox.IsEnabledProperty,
new Binding {
Source = this.lbMain,
Path = new PropertyPath("SelectedItem.Data.Enabled"),
FallbackValue = false });
this.tbData1.SetBinding(
TextBox.TextProperty,
new Binding { Source = this.lbMain,
Path = new PropertyPath("SelectedItem.Data.Data1") });
this.tbData1.SetBinding(
TextBox.IsEnabledProperty,
new Binding {
Source = this.lbMain,
Path = new PropertyPath("SelectedItem.Data.Enabled"),
FallbackValue = false });
this.cbEnabled.SetBinding(
CheckBox.IsCheckedProperty,
new Binding {
Source = this.lbMain,
Path = new PropertyPath("SelectedItem.Data.Enabled"),
FallbackValue = false });
this.cbEnabled.SetBinding(
CheckBox.IsEnabledProperty,
new Binding {
Source = this.lbMain,
Path = new PropertyPath("SelectedItem.Present"),
FallbackValue = false });
this.bDown.SetBinding(
TextBox.IsEnabledProperty,
new Binding {
Source = this.lbMain,
Path = new PropertyPath("SelectedItem.CanDown"),
FallbackValue = false });
this.bUp.SetBinding(
TextBox.IsEnabledProperty,
new Binding {
Source = this.lbMain,
Path = new PropertyPath("SelectedItem.CanUp"),
FallbackValue = false });
this.bRemove.SetBinding(
TextBox.IsEnabledProperty,
new Binding {
Source = this.lbMain,
Path = new PropertyPath("SelectedItem.Data.Enabled"),
FallbackValue = false });
}
Die Hauptarbeit ist damit erledigt. Es können jetzt Elemente in der Liste ausgwählt werden und die Eingabefelder werden dadurch mit den Daten des Eintrages befüllt. Wenn die Texte in den Eingabefelder verändert werden, werden diese automatisch in dem Listenelement gespeichert. Beim Ändern des Titels wird auch der Anzegeigte Text in der Liste aktualisiert.
Anmerkung: Das Binden an den Pfad in SelectedItem.Data wirft keine Exception, wenn SelectedItem == null ist. Durch FallbackValue = false sind die Eingabefelder dann nicht bearbeitbar.
Als spezielle Funktionalität ist mit der Datenbindung folgendes Verhalten eingebaut:
- Es können nur Aktive Einträge gelöscht werden (Checkbox "Aktiviert")
- Der Button "Hoch" ist für das erste Element in der Liste ausgegraut.
- Der Button "Runter" ist für das letzte Element in der Liste ausgegraut.
- Wenn kein Eintrag in der Liste ausgewählt oder die Liste leer ist, sind die Eingabefelder und die Buttons "Löschen", "Hoch" und "Runter" deaktiviert.
- Wenn ein Eintrag in der Liste deaktiviert ist (Checkbox "Aktiviert" nicht gesetzt), dann werden die Eingabefelder deaktiviert.
- Die Datenbindung der Checkbox "Activiert"-Eigenschaft "IsEnabled" auf ListBoxView.Present bewirkt, dass die Checkbox nicht ausgegraut wird, wenn das Listenelement deaktiviert ist, sondern, wenn es nicht vorhanden ist (kein Element ausgewählt oder Leere Liste).
Schritt 6: Liste manipulieren
Das Manipulieren von Listeneinträgen wird durch Events der entsprechenden Buttons ausgelöst. (Das hat ja an sich nichts mehr mit Datenbindung zu tun.)
Die Folgenden Funktionen gehören in die Klasse MainWindow.
private void bDown_Click(object sender, RoutedEventArgs e)
{
Int32 SelectedIndex = this.lbMain.SelectedIndex;
if (SelectedIndex != -1)
{
ListBoxView ListItem = this.DataList[SelectedIndex] as ListBoxView;
this.DataList.RemoveAt(SelectedIndex);
this.DataList.Insert(SelectedIndex + 1, ListItem);
this.lbMain.SelectedIndex = SelectedIndex + 1;
}
}
private void bUp_Click(object sender, RoutedEventArgs e)
{
Int32 SelectedIndex = this.lbMain.SelectedIndex;
if (SelectedIndex != -1)
{
ListBoxView ListItem = this.DataList[SelectedIndex] as ListBoxView;
this.DataList.RemoveAt(SelectedIndex);
this.DataList.Insert(SelectedIndex - 1, ListItem);
this.lbMain.SelectedIndex = SelectedIndex - 1;
}
}
private void bAdd_Click(object sender, RoutedEventArgs e)
{
this.DataList.Add(new ListBoxView(this.DataList, @"Neuer Eintrag", String.Empty));
this.lbMain.SelectedIndex = this.DataList.Count - 1;
this.tbTitle.Focus();
this.tbTitle.SelectAll();
}
private void bRemove_Click(object sender, RoutedEventArgs e)
{
Int32 SelectedIndex = this.lbMain.SelectedIndex;
if (SelectedIndex != -1)
{
this.DataList.RemoveAt(SelectedIndex);
if (this.DataList.Count > 0)
this.lbMain.SelectedIndex = Math.Min(SelectedIndex, this.DataList.Count - 1);
}
}
Es ist wichtig, dass ein neuer Eintrag (in bAdd_Click) nicht mit einem leeren Titel gesetzt wird (@"Neuer Eintrag" statt String.Empty), andernfalls wird der neue Eintrag fehlerhaft eingefügt und ist nicht auswähltbar. Später kann der Titel mit dem Eingabefeld gerne auf eine leere Zeichenkette gesetzt werden, das macht keine Probleme mehr.
Raum für Verbesserungen
- Wenn ein Eintrag in der ListBox verschoben wird, dann wird die Checkbox "Aktiviert" kurz entfernt und wieder gesetzt. Das geschieht, weil kurzfristig kein Element in der Liste ausgewählt ist. Die Eingabefelder sollten während der Verschiebeaktion kurzfristig vom Neuzeichnen ausgeschlossen werden. Ich weiß aber nicht, wie das in WPF funktioniert.
Eine Erweiterung, die sich nur mit einer Dependency Property einbauen lässt ist folgende: ListBoxItems, die deaktiviert sind (per Checkbox "Aktiviert") sollen in rot dargestellt werden.
Dazu wird in der Klasse ListBoxView folgendes hinzugefügt:
public static readonly DependencyProperty DataEnabledProperty =
DependencyProperty.Register(
@"DataEnabled",
typeof(Boolean),
typeof(ListBoxView),
new PropertyMetadata(true, ListBoxView.DataEnabledPropertyChanged));
private Boolean DataEnabled
{
get { return (Boolean)GetValue(DataEnabledProperty); }
set { SetValue(DataEnabledProperty, value); }
}
private static void DataEnabledPropertyChanged(Object Sender,
DependencyPropertyChangedEventArgs e)
{
if (((Boolean)e.NewValue) == true)
(Sender as ListBoxView).Foreground = ListBoxView.ActiveBrush;
else
(Sender as ListBoxView).Foreground = ListBoxView.InactiveBrush;
}
private static Brush ActiveBrush = new SolidColorBrush(Colors.Black);
private static Brush InactiveBrush = new SolidColorBrush(Colors.Red);
Und eine Zusätzliche Bindung wird im Konstruktor hinzugefügt.
this.SetBinding(
ListBoxView.DataEnabledProperty,
new Binding { Source = this.Data, Path = new PropertyPath("Enabled") });