21. Jun 2021
Lesedauer 6 Min.
WPF-Trick für MVVM-Apps
Listbox und Datagrid
Wie Sie Mehrfachauswahlen in WPF-MVVM-Anwendungen handhaben.

Ein Klassiker in den FAQ zum Model-View-ViewModel-Pattern (MVVM) in der Windows Presentation Foundation (WPF) ist, wie man mit Auflistungen umgeht, die eine Mehrfachauswahl zulassen. Steuerelemente wie ListBox oder DataGrid unterstützen das zwar im User Interface (UI), bieten aber keine geeignete Schnittstelle an, um über eine einfache Datenbindung an die Auswahl zu gelangen. Dieser Beitrag zeigt eine mögliche Vorgehensweise.Steuerelemente für Auflistungen in der WPF wie beispielsweise DataGrid oder ListBox verfügen über eine Eigenschaft SelectedItem, die auf das aktuell ausgewählte Element verweist. Diese Eigenschaft lässt sich an eine Property im ViewModel binden, sodass deren Setter immer dann aufgerufen wird, wenn sich die Auswahl geändert hat. Das geht auch in der Gegenrichtung: Durch Setzen der Eigenschaft SelectedItem kann man dem Steuerelement vorgeben, welcher Eintrag zu markieren ist. Wie der Name SelectedItem schon vermuten lässt, geht es aber immer nur um genau ein Element. Wird bei den Steuerelementen die Mehrfachauswahl zugelassen (Eigenschaft SelectionMode), dann hilft SelectedItem nicht mehr weiter.Nun verfügen die betreffenden Steuerelemente zwar auch über eine Eigenschaft namens SelectedItems und diese verweist auch tatsächlich auf eine Auflistung der ausgewählten Elemente, doch ist diese leider schreibgeschützt und kann somit auch nicht gebunden werden. Auch um Elemente per C#-Code im ViewModel auswählen zu können und diese Auswahl von der Oberfläche reflektieren zu lassen, wäre das der falsche Ansatz. Was also tun?
Zaubertrick ItemContainerStyle
Die typischen WPF-Steuerelemente, die Auflistungen repräsentieren können, sind direkt oder indirekt abgeleitet von der Klasse ItemsControl. Bindet man eine Auflistung an die Eigenschaft ItemsSource, dann legt das Control für jedes Element der Auflistung ein Container-Element an und setzt dessen Content-Eigenschaft auf das jeweilige Datenobjekt. Von welchem Typ diese Container-Elemente sind, hängt vom jeweiligen Steuerelement ab. Die Basisklasse ItemsControl verwendet den Typ ContentPresenter, eine ListBox erzeugt Container vom Typ ListBoxItem und ein DataGrid DataGridRow-Objekte. Diese Container-Objekte haben eine Reihe von Eigenschaften gemein. So verfügen sie zum Beispiel über die Property IsSelected, um eine Auswahl zu repräsentieren, oder über die Property IsEnabled, um zwischen verfügbaren und gesperrten Elementen unterscheiden zu können.Doch wie kommt man an die Eigenschaften der Container, wenn diese ja automatisch erstellt werden? Der C#-Code des ViewModels soll ja keinen Zugriff auf die Objekte des Visual Tree erhalten.Die Lösung liefert die Eigenschaft ItemContainerStyle [1] der Klasse ItemsControl. Ihr weist man einen Style zu, der auf jedes der automatisch generierten Container-Elemente angewendet wird. Über die Setter des Styles hat man den Zugriff auf alle Properties des betreffenden Container-Typs.Das wiederum erfordert aber etwas Vorarbeit auf der Seite des ViewModels. Hier kann man die ursprüngliche Datenliste nicht mehr wie gewohnt durchreichen, sondern muss eine Hilfskonstruktion schaffen.Der erste Schritt besteht aus der Definition einer Hilfsklasse, welche die Eigenschaften IsSelected beziehungsweise IsEnabled sowie den Verweis auf das Datenobjekt aufweist, vergleiche Listing 1.Listing 1: Auswahl und Verfügbarkeit verknüpfen
// Listenelement für die Bindung an ListBox, <br/>// DataGrid etc. <br/><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> SelectableItem : NotificationObject <br/>{ <br/> // Wird ausgelöst, wenn IsEnabled geändert wurde <br/> <span class="hljs-keyword">public</span> event EventHandler IsEnabledChanged; <br/><br/> // Wird ausgelöst, wenn IsSelected geändert wurde <br/> <span class="hljs-keyword">public</span> event EventHandler IsSelectedChanged; <br/><br/> <span class="hljs-keyword">private</span> object <span class="hljs-keyword">data</span>; <br/><br/> // Angehängtes Datenobjekt <br/> <span class="hljs-keyword">public</span> object <span class="hljs-keyword">Data</span> <br/> { <br/> get { <span class="hljs-keyword">return</span> <span class="hljs-keyword">data</span>; } <br/> set { <span class="hljs-keyword">data</span> = <span class="hljs-keyword">value</span>; OnPropertyChanged(); } <br/> } <br/><br/> <span class="hljs-keyword">private</span> bool isEnabled = true; <br/><br/> // Gibt an, ob das Element verfügbar ist <br/> <span class="hljs-keyword">public</span> bool IsEnabled <br/> { <br/> get { <span class="hljs-keyword">return</span> isEnabled; } <br/> set <br/> { <br/> <span class="hljs-keyword">if</span> (isEnabled == <span class="hljs-keyword">value</span>) <span class="hljs-keyword">return</span>; <br/> isEnabled = <span class="hljs-keyword">value</span>; <br/> OnPropertyChanged(); <br/> IsEnabledChanged?.Invoke(this, EventArgs.Empty); <br/> } <br/> } <br/><br/> <span class="hljs-keyword">private</span> bool isSelected; <br/><br/> // Gibt an, ob das Element ausgewählt wurde <br/> <span class="hljs-keyword">public</span> bool IsSelected <br/> { <br/> get { <span class="hljs-keyword">return</span> isSelected; } <br/> set <br/> { <br/> <span class="hljs-keyword">if</span> (IsSelected == <span class="hljs-keyword">value</span>) <span class="hljs-keyword">return</span>; <br/> isSelected = <span class="hljs-keyword">value</span>; <br/> OnPropertyChanged(); <br/> IsSelectedChanged?.Invoke(this, EventArgs.Empty); <br/> } <br/> } <br/>}
In Listing 2 sehen Sie die Definitionen der ItemContainerStyle-Eigenschaften für ein DataGrid und eine ListBox. Die Setter der Styles stellen jeweils die Verbindung zwischen den Eigenschaften der Container-Objekte und denjenigen der Hilfsklassen-Instanzen her. Beachten Sie bitte, dass diese Eigenschaften in der WPF typischerweise OneWay-Bindungen zum Standard haben, sodass die Eigenschaft Mode des Bindungsausdrucks explizit auf TwoWay gesetzt werden muss, wenn, wie im Fall von IsSelected, die Auswahländerung vom UI zum Datenobjekt zurückübertragen werden soll.
Listing 2: IsSelected und IsEnabled binden
<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Window</span> <span class="hljs-attr">x:Class</span>=<span class="hljs-string">"WpfMvvmMultiselect.MainWindow"</span> </span></span><br/><span class="xml"><span class="hljs-tag"> <span class="hljs-attr">...</span> </span></span><br/><span class="xml"><span class="hljs-tag"> <span class="hljs-attr">Title</span>=<span class="hljs-string">"MVVM - Mehrfachauswahl"</span> <span class="hljs-attr">Height</span>=<span class="hljs-string">"450"</span> </span></span><br/><span class="xml"><span class="hljs-tag"> <span class="hljs-attr">Width</span>=<span class="hljs-string">"800"</span>&gt;</span> </span><br/><span class="xml"> <span class="hljs-tag">&lt;<span class="hljs-name">Window.Resources</span>&gt;</span> </span><br/><span class="xml"> <span class="hljs-tag">&lt;<span class="hljs-name">DataTemplate</span> <span class="hljs-attr">x:Key</span>=<span class="hljs-string">"PersonTemplate"</span>&gt;</span> </span><br/><span class="xml"> <span class="hljs-tag">&lt;<span class="hljs-name">StackPanel</span> <span class="hljs-attr">Orientation</span>=<span class="hljs-string">"Horizontal"</span> </span></span><br/><span class="xml"><span class="hljs-tag"> <span class="hljs-attr">DataContext</span>=<span class="hljs-string">"</span></span></span><span class="hljs-template-variable">{Binding Data}</span><span class="xml"><span class="hljs-tag"><span class="hljs-string">"</span>&gt;</span> </span><br/><span class="xml"> <span class="hljs-tag">&lt;<span class="hljs-name">TextBlock</span> <span class="hljs-attr">Text</span>=<span class="hljs-string">"</span></span></span><span class="hljs-template-variable">{Binding Vorname}</span><span class="xml"><span class="hljs-tag"><span class="hljs-string">"</span> </span></span><br/><span class="xml"><span class="hljs-tag"> <span class="hljs-attr">Width</span>=<span class="hljs-string">"100"</span>/&gt;</span> </span><br/><span class="xml"> <span class="hljs-tag">&lt;<span class="hljs-name">TextBlock</span> <span class="hljs-attr">Text</span>=<span class="hljs-string">"</span></span></span><span class="hljs-template-variable">{Binding Nachname}</span><span class="xml"><span class="hljs-tag"><span class="hljs-string">"</span> </span></span><br/><span class="xml"><span class="hljs-tag"> <span class="hljs-attr">Width</span>=<span class="hljs-string">"100"</span>/&gt;</span> </span><br/><span class="xml"> <span class="hljs-tag">&lt;<span class="hljs-name">TextBlock</span> <span class="hljs-attr">Text</span>=<span class="hljs-string">"</span></span></span><span class="hljs-template-variable">{Binding Alter}</span><span class="xml"><span class="hljs-tag"><span class="hljs-string">"</span> <span class="hljs-attr">Width</span>=<span class="hljs-string">"30"</span>/&gt;</span> </span><br/><span class="xml"> <span class="hljs-tag">&lt;/<span class="hljs-name">StackPanel</span>&gt;</span> </span><br/><span class="xml"> <span class="hljs-tag">&lt;/<span class="hljs-name">DataTemplate</span>&gt;</span> </span><br/><span class="xml"> <span class="hljs-tag">&lt;/<span class="hljs-name">Window.Resources</span>&gt;</span> </span><br/><span class="xml"> ... </span><br/><span class="xml"> <span class="hljs-tag">&lt;<span class="hljs-name">DataGrid</span> <span class="hljs-attr">ItemsSource</span>=<span class="hljs-string">"</span></span></span><span class="hljs-template-variable">{Binding Liste}</span><span class="xml"><span class="hljs-tag"><span class="hljs-string">"</span> <span class="hljs-attr">AutoGenerateColumns</span>=<span class="hljs-string">"False"</span> <span class="hljs-attr">IsReadOnly</span>=<span class="hljs-string">"False"</span> </span></span><br/><span class="xml"><span class="hljs-tag"> <span class="hljs-attr">Margin</span>=<span class="hljs-string">"5"</span> <span class="hljs-attr">CanUserAddRows</span>=<span class="hljs-string">"False"</span> <span class="hljs-attr">Grid.Row</span>=<span class="hljs-string">"1"</span> &gt;</span> </span><br/><span class="xml"> <span class="hljs-comment">&lt;!--IsSelected und IsEnabled der automatisch </span></span><br/><span class="xml"><span class="hljs-comment"> generierten DataGridRow-Objekte mit Objekt </span></span><br/><span class="xml"><span class="hljs-comment"> vom Typ SelectableItem binden--&gt;</span> </span><br/><span class="xml"> <span class="hljs-tag">&lt;<span class="hljs-name">DataGrid.ItemContainerStyle</span>&gt;</span> </span><br/><span class="xml"> <span class="hljs-tag">&lt;<span class="hljs-name">Style</span> <span class="hljs-attr">TargetType</span>=<span class="hljs-string">"DataGridRow"</span>&gt;</span><span class="xml"> </span></span><br/><span class="xml"><span class="xml"> <span class="hljs-tag">&lt;<span class="hljs-name">Setter</span> <span class="hljs-attr">Property</span>=<span class="hljs-string">"IsSelected"</span> <span class="hljs-attr">Value</span>=</span></span></span><br/><span class="xml"><span class="xml"><span class="hljs-tag"> <span class="hljs-string">"</span></span></span></span><span class="hljs-template-variable">{Binding IsSelected, Mode=TwoWay}</span><span class="xml"><span class="xml">" /&gt; </span></span><br/><span class="xml"><span class="xml"> <span class="hljs-tag">&lt;<span class="hljs-name">Setter</span> <span class="hljs-attr">Property</span>=<span class="hljs-string">"IsEnabled"</span> <span class="hljs-attr">Value</span>=</span></span></span><br/><span class="xml"><span class="xml"><span class="hljs-tag"> <span class="hljs-string">"</span></span></span></span><span class="hljs-template-variable">{Binding IsEnabled}</span><span class="xml"><span class="undefined">" /&gt; </span></span><br/><span class="xml"><span class="undefined"> </span><span class="hljs-tag">&lt;/<span class="hljs-name">Style</span>&gt;</span> </span><br/><span class="xml"> <span class="hljs-tag">&lt;/<span class="hljs-name">DataGrid.ItemContainerStyle</span>&gt;</span> </span><br/><span class="xml"> <span class="hljs-tag">&lt;<span class="hljs-name">DataGrid.Columns</span>&gt;</span> </span><br/><span class="xml"> <span class="hljs-tag">&lt;<span class="hljs-name">DataGridTextColumn</span> <span class="hljs-attr">Header</span>=<span class="hljs-string">"Vorname"</span> </span></span><br/><span class="xml"><span class="hljs-tag"> <span class="hljs-attr">Binding</span>=<span class="hljs-string">"</span></span></span><span class="hljs-template-variable">{Binding Data.Vorname}</span><span class="xml"><span class="hljs-tag"><span class="hljs-string">"</span>/&gt;</span> </span><br/><span class="xml"> <span class="hljs-tag">&lt;<span class="hljs-name">DataGridTextColumn</span> <span class="hljs-attr">Header</span>=<span class="hljs-string">"Nachname"</span> </span></span><br/><span class="xml"><span class="hljs-tag"> <span class="hljs-attr">Binding</span>=<span class="hljs-string">"</span></span></span><span class="hljs-template-variable">{Binding Data.Nachname}</span><span class="xml"><span class="hljs-tag"><span class="hljs-string">"</span>/&gt;</span> </span><br/><span class="xml"> <span class="hljs-tag">&lt;<span class="hljs-name">DataGridTextColumn</span> <span class="hljs-attr">Header</span>=<span class="hljs-string">"Alter"</span> </span></span><br/><span class="xml"><span class="hljs-tag"> <span class="hljs-attr">Binding</span>=<span class="hljs-string">"</span></span></span><span class="hljs-template-variable">{Binding Data.Alter}</span><span class="xml"><span class="hljs-tag"><span class="hljs-string">"</span>/&gt;</span> </span><br/><span class="xml"> <span class="hljs-tag">&lt;/<span class="hljs-name">DataGrid.Columns</span>&gt;</span> </span><br/><span class="xml"> <span class="hljs-tag">&lt;/<span class="hljs-name">DataGrid</span>&gt;</span> </span><br/><br/><span class="xml"> <span class="hljs-tag">&lt;<span class="hljs-name">ListBox</span> <span class="hljs-attr">ItemsSource</span>=<span class="hljs-string">"</span></span></span><span class="hljs-template-variable">{Binding Liste}</span><span class="xml"><span class="hljs-tag"><span class="hljs-string">"</span> </span></span><br/><span class="xml"><span class="hljs-tag"> <span class="hljs-attr">SelectionMode</span>=<span class="hljs-string">"Extended"</span> <span class="hljs-attr">Grid.Column</span>=<span class="hljs-string">"1"</span> </span></span><br/><span class="xml"><span class="hljs-tag"> <span class="hljs-attr">Margin</span>=<span class="hljs-string">"5"</span> <span class="hljs-attr">ItemTemplate</span>=</span></span><br/><span class="xml"><span class="hljs-tag"> <span class="hljs-string">"</span></span></span><span class="hljs-template-variable">{StaticResource PersonTemplate}</span><span class="xml"><span class="hljs-tag"><span class="hljs-string">"</span> <span class="hljs-attr">Grid.Row</span>=<span class="hljs-string">"1"</span>&gt;</span> </span><br/><span class="xml"> <span class="hljs-comment">&lt;!--IsSelected und IsEnabled der automatisch </span></span><br/><span class="xml"><span class="hljs-comment"> generierten ListBoxItem-Objekte mit Objekt </span></span><br/><span class="xml"><span class="hljs-comment"> vom Typ SelectableItem binden--&gt;</span> </span><br/><span class="xml"> <span class="hljs-tag">&lt;<span class="hljs-name">ListBox.ItemContainerStyle</span>&gt;</span> </span><br/><span class="xml"> <span class="hljs-tag">&lt;<span class="hljs-name">Style</span> <span class="hljs-attr">TargetType</span>=<span class="hljs-string">"ListBoxItem"</span>&gt;</span><span class="xml"> </span></span><br/><span class="xml"><span class="xml"> <span class="hljs-tag">&lt;<span class="hljs-name">Setter</span> <span class="hljs-attr">Property</span>=<span class="hljs-string">"IsSelected"</span> <span class="hljs-attr">Value</span>=</span></span></span><br/><span class="xml"><span class="xml"><span class="hljs-tag"> <span class="hljs-string">"</span></span></span></span><span class="hljs-template-variable">{Binding IsSelected, Mode=TwoWay}</span><span class="xml"><span class="xml">" /&gt; </span></span><br/><span class="xml"><span class="xml"> <span class="hljs-tag">&lt;<span class="hljs-name">Setter</span> <span class="hljs-attr">Property</span>=<span class="hljs-string">"IsEnabled"</span> <span class="hljs-attr">Value</span>=</span></span></span><br/><span class="xml"><span class="xml"><span class="hljs-tag"> <span class="hljs-string">"</span></span></span></span><span class="hljs-template-variable">{Binding IsEnabled}</span><span class="xml"><span class="undefined">" /&gt; </span></span><br/><span class="xml"><span class="undefined"> </span><span class="hljs-tag">&lt;/<span class="hljs-name">Style</span>&gt;</span> </span><br/><span class="xml"> <span class="hljs-tag">&lt;/<span class="hljs-name">ListBox.ItemContainerStyle</span>&gt;</span> </span><br/><span class="xml"> <span class="hljs-tag">&lt;/<span class="hljs-name">ListBox</span>&gt;</span> </span><br/><span class="xml"> ... </span><br/><span class="xml"><span class="hljs-tag">&lt;/<span class="hljs-name">Window</span>&gt;</span> </span>
Die Verknüpfung von IsSelected funktioniert dann in beide Richtungen. Setzen Sie im Hilfsobjekt die Eigenschaft auf true, dann zeigen die Steuerelemente den Eintrag als ausgewählt an. Ändern Sie die Auswahl in der Oberfläche, dann wird diese Eigenschaft von SelectableItem neu gesetzt. Die Implementierung von INotifyPropertyChanged für IsSelected sorgt dafür, dass Änderungen an andere Controls weitergegeben werden. Im Beispiel sind das DataGrid und die ListBox an dieselbe Auflistung gebunden. Änderungen im einen Control werden sofort im anderen nachgeführt – leider nur in leichtem Grau, da das jeweils andere Control nicht den Fokus hat.Wird IsEnabled auf false gesetzt, dann wird der jeweilige Eintrag ausgegraut und kann weder bearbeitet noch selektiert werden. Die Verknüpfung zu jeweils einer CheckBox, um auch IsEnabled einstellen zu können, werden wir später noch vorsehen.
Etwas mehr Automatismus
Damit man es bei der Implementierung des ViewModels etwas bequemer hat und sich nicht um die Details der SelectedItem-Hilfsobjekte kümmern muss, bietet sich der Einsatz einer weiteren Hilfsklasse für die passende Auflistung an (Listing 3). SelectableItemList ist von ObservableCollection abgeleitet und erhält eine Factory-Methode, der eine beliebige Auflistung übergeben werden kann. Für jedes Element dieser Auflistung wird eine Instanz von SelectableItem angelegt und hinzugefügt. In der Überschreibung von InsertItem wird das IsSelectedChanged-Event verknüpft, um zentral auf Auswahländerungen reagieren zu können. In RemoveItem wird dieser Handler wieder entfernt. Eine Read-only-Property SelectedItems kann ferner bereitgestellt werden, um eine Liste der ausgewählten Objekte bekannt zu machen. Bei Bedarf könnte man hier auch die Liste der betreffenden Datenobjekte durchreichen. Die Implementierung lässt sich frei gestalten und den jeweiligen Anforderungen anpassen.Listing 3: SelectableItemList
// Vereinfacht den Umgang mit bestehenden Auflistungs-<br/>// klassen und Änderungen der Auswahlliste von <br/>// SelectableItem. Hilfsklasse zum Erstellen der <br/>// Auflistung und zum Nachverfolgen der<br/>// ausgewählten Elemente <br/><br/><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> SelectableItemList : ObservableCollection&lt;SelectableItem&gt; <br/>{ <br/> // Factory-Methode zum Erstellen der Auflistung <br/> // &lt;param <span class="hljs-keyword">name</span>=<span class="hljs-string">"items"</span>&gt;Liste der Datenobjekte<br/> // &lt;/param&gt; <br/> // &lt;returns&gt;Liste der SelectableItems&lt;/returns&gt; <br/> <span class="hljs-keyword">public</span> static SelectableItemList FromItems(<br/> IEnumerable items) <br/> { <br/> var list = new SelectableItemList(); <br/> foreach (var item <span class="hljs-keyword">in</span> items) <br/> { <br/> list.Add(new SelectableItem { <span class="hljs-keyword">Data</span> = item }); <br/> } <br/> <span class="hljs-keyword">return</span> list; <br/> } <br/><br/> <span class="hljs-keyword">private</span> void Si_IsSelectedChanged(<br/> object sender, EventArgs e) <br/> { <br/> base.OnPropertyChanged( <br/> new PropertyChangedEventArgs(<br/> nameof(SelectedItems))); <br/> } <br/><br/> <span class="hljs-keyword">protected</span> override void InsertItem(<br/> <span class="hljs-built_in">int</span> <span class="hljs-built_in">index</span>, SelectableItem item) <br/> { <br/> base.InsertItem(<span class="hljs-built_in">index</span>, item); <br/> // Auf Änderungen von IsSelected reagieren <br/> item.IsSelectedChanged += Si_IsSelectedChanged; <br/> } <br/><br/> <span class="hljs-keyword">protected</span> override void RemoveItem(<span class="hljs-built_in">int</span> <span class="hljs-built_in">index</span>) <br/> { <br/> // Handler wieder entfernen <br/> this[<span class="hljs-built_in">index</span>].IsSelectedChanged += <br/> Si_IsSelectedChanged; <br/> base.RemoveItem(<span class="hljs-built_in">index</span>); <br/> } <br/><br/> // Ausgewählte Elemente <br/> <span class="hljs-keyword">public</span> IEnumerable&lt;SelectableItem&gt; SelectedItems =&gt; <br/> this.<span class="hljs-keyword">Where</span>(i =&gt; i.IsSelected).ToList(); <br/>}
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Person</span> </span>{
<span class="hljs-keyword">public</span> string Vorname { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
<span class="hljs-keyword">public</span> string Nachname { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
<span class="hljs-keyword">public</span> int Alter { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
}
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Personenliste</span> : <span class="hljs-title">List</span><<span class="hljs-title">Person</span>> </span>{
<span class="hljs-keyword">public</span> Personenliste() {
<span class="hljs-built_in">this</span>.Add(<span class="hljs-keyword">new</span> <span class="hljs-type">Person</span> { Nachname = <span class="hljs-string">"Meier"</span>,
Vorname = <span class="hljs-string">"Peter"</span>, Alter = <span class="hljs-number">55</span> });
<span class="hljs-built_in">this</span>.Add(<span class="hljs-keyword">new</span> <span class="hljs-type">Person</span> { Nachname = <span class="hljs-string">"Meier"</span>,
Vorname = <span class="hljs-string">"Petra"</span>, Alter = <span class="hljs-number">44</span> });
...
}
}
Diese Code-Zeilen zeigen die Implementierung der Beispieldaten für eine Klasse Person. Der Aufbau des ViewModels beschränkt sich dann auf das Instanzieren der Demo-Daten und das Anlegen der Hilfsstruktur. Das Ergebnis sehen Sie in Bild 1.

Der erste Ansatz steht:Die Mehrfachauswahl in DataGrid und ListBox wird mit der Datenstruktur verbunden(Bild 1)
Autor
public class ViewModel : NotificationObject {
// Liste auswählbarer Objekte
public ObservableCollection<SelectableItem> Liste {
get; set; }
...
// ctor
public ViewModel() {
// Liste aus Demodaten erstellen
Liste = SelectableItemList.FromItems(
new Personenliste());
...
}
...
}
Nachdem die Basis für den Umgang mit Mehrfachauswahlmöglichkeiten geschaffen wurde, können nun weitere Funktionalitäten in der Oberfläche genutzt werden. In Listing 4 werden zwei weitere Steuerelemente hinzugefügt (Typ ItemsControl).
Listing 4: Ausgewählte Elemente anzeigen
&lt;Window x:Class="WpfMvvmMultiselect.MainWindow" <br/> ...&gt; <br/> &lt;Grid Margin="10"&gt; <br/> ... <br/> &lt;ItemsControl Grid.Row="3" Margin="5"<br/> ItemsSource="{Binding Liste.SelectedItems}" <br/> ItemTemplate="{StaticResource PersonTemplate}" <br/> BorderBrush="blue" BorderThickness="1" /&gt; <br/><br/> &lt;ItemsControl Grid.Row="3" Grid.Column="1" <br/> Margin="5" ItemsSource="{Binding Liste}" <br/> BorderBrush="DarkGray" BorderThickness="1"&gt; <br/> &lt;ItemsControl.ItemTemplate&gt; <br/> &lt;DataTemplate&gt; <br/> &lt;StackPanel Orientation="Horizontal"&gt; <br/> &lt;CheckBox IsChecked="{Binding IsEnabled, <br/> Mode=TwoWay}" Margin="0,0,10,0"/&gt; <br/> &lt;ContentControl Content="{Binding }" <br/> ContentTemplate= <br/> "{StaticResource PersonTemplate}" /&gt; <br/> &lt;/StackPanel&gt; <br/> &lt;/DataTemplate&gt; <br/> &lt;/ItemsControl.ItemTemplate&gt; <br/> &lt;/ItemsControl&gt; <br/> ... <br/> &lt;/Grid&gt; <br/>&lt;/Window&gt;
Das erste ist an die oben beschriebene Eigenschaft SelectedItems der Hilfsklasse SelectableItemList gebunden und zeigt die Liste aller ausgewählten Personen. Das zweite ist an die gesamte Liste gebunden und zeigt für jedes Listenelement zusätzlich eine CheckBox an, deren IsChecked-Eigenschaft mit der IsEnabled-Eigenschaft der Hilfsklasse SelectableItem verknüpft ist. Änderungen in den CheckBoxen wirken sich sofort im DataGrid und in der ListBox aus (ist IsEnabled false, dann werden die Listeneinträge ausgegraut, siehe Bild 2).

Separate Listemit ausgewählten Objekten sowie Steuerung der IsEnabled-Eigenschaften der Listenobjekte(Bild 2)
Autor
Setzen von IsSelected im ViewModel
Auch das ist ein denkbares Szenario: In Bild 3 ist das Beispielprogramm um eine TextBox zur Volltextsuche erweitert worden. Im imperativen C#-Code wird bei Eingabe eines Suchtextes die gesamte Liste durchlaufen und je nach Prüfung der Texte die IsSelected-Eigenschaft der SelectableItem-Objekte gesetzt. Das Ergebnis wird sofort in der Oberfläche sichtbar. DataGrid und ListBox zeigen die Einträge als markiert an, die den vorgegebenen Suchtext enthalten. Listing 5 zeigt den XAML-Code für die Eingabe des Suchtextes und den ViewModel-Code für die Auswahl der Elemente, die dem Suchkriterium entsprechen.
Bei der Volltextsuchein diesem Beispiel wird die IsSelected-Eigenschaft der Hilfsobjekte im C#-Code gesetzt(Bild 3)
Autor
Listing 5: XAML- und ViewModel-Code für eine Volltextsuche
&lt;Window x:Class="WpfMvvmMultiselect.MainWindow" <br/> ... <br/> &lt;StackPanel Orientation="Horizontal" <br/> Grid.Row="4" Margin="5"&gt; <br/> &lt;TextBlock Text="Volltextsuche: "/&gt; <br/> &lt;TextBox Text="{Binding Suchfeld, <br/> UpdateSourceTrigger=PropertyChanged}" <br/> Width="100"/&gt; <br/> &lt;Button Content="Suchen" Command=<br/> "{Binding VolltextsucheCommand}" <br/> Margin="10,0"/&gt; <br/> &lt;/StackPanel&gt; <br/> ... <br/>&lt;/Window&gt; <br/>... <br/>public class ViewModel : NotificationObject { <br/> ... <br/> // Kommando Volltextsuche <br/> public ActionCommand VolltextsucheCommand {<br/> get; set; } <br/> // Eingabe für die Volltextsuche <br/> public string Suchfeld { get; set; } <br/> // ctor <br/> public ViewModel() <br/> { <br/> ... <br/> // Command anlegen <br/> VolltextsucheCommand = <br/> new ActionCommand(Volltextsuche); <br/> Suchfeld = "Schm"; <br/> } <br/><br/> private void Volltextsuche() { <br/> // IsSelected gemäß Suchergebnis setzen <br/> foreach (var item in Liste) <br/> { <br/> var person = (Person)item.Data; <br/> string text = <br/> $"{person.Vorname}_{person.Nachname}_<br/> {person.Alter}"; <br/> item.IsSelected = text.Contains(Suchfeld); <br/> } <br/> } <br/>}
Beide Richtungen sind also möglich. Man kann im ViewModel auf im UI geänderte Auswahlen reagieren, und man kann im ViewModel die Auswahl vorgeben und so Einfluss auf die Darstellung nehmen.
Fazit
Der Zaubertrick mit ItemContainerStyle eröffnet viele Möglichkeiten, die Eigenschaften der automatisch generierten Container mit Eigenschaften von Hilfsobjekten zu binden und so auf Änderungen im UI reagieren zu können und andererseits die Oberfläche über Änderungen der Datenstrukturen (Auswahl, Verfügbarkeit et cetera) zu informieren. Mit ein wenig Infrastruktur ist es dann vergleichsweise einfach, ohne Eingriff in den Visual Tree, nur über POCO-Strukturen, die Bezüge zwischen ViewModel und Auflistungs-Controls herzustellen. Die Beispielimplementierung soll die Zusammenhänge verdeutlichen. Nehmen Sie den Code als Anregung für Ihre eigenen Implementierungen. Der Sourcecode des Beispiels steht unter [2] zur Verfügung. Für das Beispiel wurde .NET 5 eingesetzt. Die Vorgehensweise funktioniert aber gleichermaßen in .NET 4.x.Fussnoten
- ItemsControl.ItemContainerStyle, http://www.dotnetpro.de/SL2107DataGridMultiselect1
- Beispielcode, http://www.dotnetpro.de/SL2107DataGridMultiselect2