21. Jun 2021
Lesedauer 14 Min.
Verzweigte Strukturen
WPF-TreeView und MVVM in Einklang bringen (Teil 1)
Mit einigen WPF-Tricks View-Model und TreeView sauber trennen.

So manche Steuerelemente in der Windows Presentation Foundation (WPF) können zu Kopfschmerzen führen, wenn man versucht, sie unter Einhalten des Model-View-ViewModel-Patterns einzusetzen. Das klassische TreeView-Control ist eines davon. Doch es gibt Lösungen.Als die WPF entwickelt wurde, hatte niemand einen Plan, wie die Programmierer mit der neuen Technik umgehen sollten. Vieles war an frühere Vorgehensweisen angelehnt, wie man sie aus Visual Basic oder Windows Forms kannte. Strukturiertere Ansätze folgten erst im Lauf der Zeit, zum Beispiel in Form des Model-View-ViewModel-Patterns, kurz MVVM. Dieses Pattern kam nicht von Microsoft, sondern hatte sich durch Diskussionen in diversen Communities entwickelt. Es ist auch eher als Denkmuster denn als Implementierungsvorschrift zu verstehen, und so finden sich viele unterschiedliche Umsetzungen von MVVM. Nicht nur in WPF findet man dieses Muster. Auch bei Single-Page-Webanwendungen kommt es zum Einsatz (beispielsweise bei Angular), wenngleich auch mit anderen Programmiersprachen und anderer Syntax (JavaScript, HTML et cetera).Heutzutage setzen die meisten Entwicklungsteams auf dieses Pattern, wenn sie Desktop-Anwendungen mit der WPF erstellen. Es bietet eine saubere Trennung der Gestaltung der Benutzeroberfläche von deren Logik. Da das Pattern aber erst später kam, sind viele der WPF-Steuerelemente nicht gut darauf vorbereitet. MVVM setzt auf Datenbindungen zwischen .NET-Properties. Für Methodenaufrufe oder Bindungen an Eventhandler müsste im C#- oder VB-Code auf die Instanzen der Steuerelemente zugegriffen werden, was die gewünschte Trennung wieder zunichtemachen würde.Bei einigen der Standard-Steuerelemente muss man daher tiefer in die Trickkiste greifen, um das MVVM-Pattern sinnvoll einsetzen zu können. Das trifft auch auf das TreeView-Control zu, das hier näher betrachtet werden soll.Im ersten Teil dieser zweiteiligen Serie werden die grundlegenden Konzepte für die Anwendung von TreeView-Controls in Verbindung mit MVVM beschrieben. In einem zweiten Teil geht es dann um Drag-and-drop, was eigentlich simpel klingt, in der Umsetzung aber doch recht mühsam und aufwendig ist.
Das Fundament schaffen
Das Thema ist nicht neu. Bereits vor zehn Jahren veröffentlichte die dotnetpro einen Beitrag zum Umgang mit einer TreeView in WPF und MVVM [1]. An den Konzepten hat sich seitdem wenig geändert. Wichtig ist, dass man die verschiedenen Werkzeuge, welche die WPF zur Verfügung stellt, kennt und versteht. Dazu gehören beispielsweise Styles sowie ControlTemplates und DataTemplates. Anhand eines Beispiels sollen diese Techniken im Detail erläutert werden. Als Anwendungsfall dient ein Dateiverzeichnis, das in einer TreeView dargestellt werden soll, siehe Bild 1.
Darstellung einer Verzeichnisstrukturim TreeView-Control(Bild 1)
Quelle der Icons: Icons8.de [3]
Zu sehen ist die Repräsentation der Verzeichnisstruktur in Form von hierarchisch angeordneten Knoten (hier vom Typ TreeViewItem). Ein Knoten besteht im Beispiel jeweils aus einem Miniaturbild und einem Text und kann ausgewählt oder auf- beziehungsweise zugeklappt werden sowie den Zustand Verfügbar oder Nicht verfügbar (Darstellung in Rot) aufweisen. In Umgebungen wie Windows-Forms würde man imperativ im Code Instanzen von TreeViewItem anlegen, Eigenschaften setzen und Eventhandler binden und hiermit das TreeView-Control füllen. Das würde aber das MVVM-Pattern verletzen.Zunächst zur Beschreibung des Datenmodells. Im Beispiel könnten das Klassen sein, wie in Listing 1 zu sehen.
Listing 1: Modellklassen für das Beispielprojekt
<span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">DataObjectBase</span> <br/>{ <br/> <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> Caption { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; } <br/> <span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> Level { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; } <br/>} <br/><br/><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">DirectoryDataObject</span> : <span class="hljs-title">DataObjectBase</span> { } <br/><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">FileDataObject</span> : <span class="hljs-title">DataObjectBase</span> { }
Die Basisklasse DataObjectBase hat zwei Eigenschaften (Caption und Level), die für die Repräsentation in der Baumstruktur genutzt werden können. Davon abgeleitet sind die Klassen DirectoryDataObject sowie FileDataObject, welche die Informationen zu einem Verzeichnis beziehungsweise einer Datei kapseln. Prinzipiell könnte man hier die 1:n-Beziehung zwischen einem Verzeichnis und seinen untergeordneten Verzeichnissen und Dateien direkt in Form von Auflistungen abbilden und diese Struktur auch direkt mit einer TreeView visualisieren (ein HierarchicalDataTemplate macht es möglich). Der Nachteil: Man hätte dann keinen Zugriff auf die Informationen der einzelnen TreeViewItems, also darüber, ob ein Knoten expandiert wurde oder nicht, ob er ausgewählt oder vielleicht nicht verfügbar ist. Daher empfiehlt sich die Definition von Hilfsklassen, die ihrerseits die benötigte Struktur für die Bindungen in der TreeView repräsentieren und andererseits allgemeingültig aufgebaut sind, sodass beliebige Daten angehängt werden können.Die Klasse TreeItem(Listing 2) korreliert mit einem TreeViewItem im TreeView-Control. Sie stellt Properties wie IsSelected, IsExpanded oder IsEnabled zur Verfügung, die mit den entsprechenden Eigenschaften eines TreeViewItem-Objekts über Datenbindungen verknüpft werden sollen. Ein ähnliches Konzept wird im Artikel „WPF-Trick für MVVM-Apps“ [2] in dieser dotnetpro-Ausgabe vorgestellt, wenn es um den Umgang mit DataGrids und ListBoxen geht. Die eigentlichen Daten werden typneutral als Object-Referenz (Eigenschaft Data) angehängt. Eine Property ToolTip (auch vom Typ Object) kann benutzt werden, um Daten für ein im UI darzustellendes Tooltipp-Fenster bereitzustellen. Anstelle der Object-Referenzen könnte man auch Generics verwenden und Platzhalter für die Datentypen vorsehen, in der Praxis führt das aber meist zu zusätzlichem Aufwand und neuen Problemen, die den Nutzen durch die Typisierung übersteigen.
Listing 2: Hilfsklasse für die Datenbindung
<span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> TreeItem : NotificationObject <br/>{ <br/> <span class="hljs-keyword">public</span> TreeItem() { <br/> Items = new TreeItemList(this); <br/> } <br/><br/> <span class="hljs-keyword">private</span> object <span class="hljs-keyword">data</span>; <br/><br/> // Angehängtes Model-Objekt <br/> <span class="hljs-keyword">public</span> object <span class="hljs-keyword">Data</span> { <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/> // Daten für Anzeige eines Tooltipps <br/> <span class="hljs-keyword">public</span> object ToolTip { get; set; } <br/><br/> <span class="hljs-keyword">private</span> bool isSelected; <br/><br/> // Repräsentation der IsSelected-Eigenschaft <br/> // des TreeViewItems <br/> <span class="hljs-keyword">public</span> bool IsSelected { <br/> get { <span class="hljs-keyword">return</span> isSelected; } <br/> set { isSelected = <span class="hljs-keyword">value</span>; OnPropertyChanged(); } <br/> } <br/> <span class="hljs-keyword">private</span> bool isExpanded; <br/><br/> // Repräsentation der IsExpanded-Eigenschaft <br/> // des TreeViewItems <br/> <span class="hljs-keyword">public</span> bool IsExpanded { <br/> get { <span class="hljs-keyword">return</span> isExpanded; } <br/> set { isExpanded = <span class="hljs-keyword">value</span>; OnPropertyChanged(); } <br/> } <br/><br/> <span class="hljs-keyword">private</span> bool isEnabled = true; <br/><br/> <span class="hljs-keyword">public</span> bool IsEnabled { <br/> get { <span class="hljs-keyword">return</span> isEnabled; } <br/> set { isEnabled = <span class="hljs-keyword">value</span>; OnPropertyChanged(); } <br/> } <br/><br/> // Navigation-Properties <br/> // Untergeordnete Items <br/> <span class="hljs-keyword">public</span> TreeItemList Items { get; set; } <br/><br/> // Parent dieses Items <br/> <span class="hljs-keyword">public</span> TreeItem Parent { get; set; } <br/> ... <br/>}
Für die Baumstruktur und den späteren Umgang damit sind zwei weitere Eigenschaften erforderlich: Items und Parent. Parent verweist auf den Elternknoten beziehungsweise hat den Wert null, falls keiner existiert. Items verweist auf die Auflistung der Kindelemente (Typ TreeItemList). Die Auflistung wird im Konstruktor von TreeItem instanziert, ist also auf jeden Fall vorhanden, zu Beginn aber leer.TreeItemList besteht im Wesentlichen aus einer Ableitung von der Framework-Klasse ObservableCollection, stellt also über das implementierte Interface INotifyCollectionChanged ein Ereignis bereit, das im Code und im UI verwendet werden kann, um auf Änderungen zu reagieren.
public <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">TreeItemList</span> :</span>
<span class="hljs-class"> <span class="hljs-title">ObservableCollection</span><TreeItem> </span>
<span class="hljs-class">{ </span>
<span class="hljs-class"> <span class="hljs-title">private</span> <span class="hljs-title">readonly</span> <span class="hljs-title">TreeItem</span> <span class="hljs-title">owner</span>;</span>
/<span class="hljs-regexp">/ ctor </span>
<span class="hljs-regexp"> /</span><span class="hljs-regexp">/ <param name="owner">Das TreeItem, zu dem </span>
<span class="hljs-regexp"> /</span><span class="hljs-regexp">/ diese Liste gehört</param</span>>
public TreeItemList(TreeItem owner)
{
this.owner = owner;
}
/<span class="hljs-regexp">/ Beim Hinzufügen eines neuen Items dessen </span>
<span class="hljs-regexp"> /</span><span class="hljs-regexp">/ Parent-Eigenschaft setzen </span>
<span class="hljs-regexp"> /</span><span class="hljs-regexp">/ <param name="index"></param</span>>
<span class="hljs-regexp">//</span> <param name=<span class="hljs-string">"item"</span>><<span class="hljs-regexp">/param> </span>
<span class="hljs-regexp"> protected override void InsertItem(</span>
<span class="hljs-regexp"> int index, TreeItem item) </span>
<span class="hljs-regexp"> { </span>
<span class="hljs-regexp"> base.InsertItem(index, item); </span>
<span class="hljs-regexp"> item.Parent = owner; </span>
<span class="hljs-regexp"> } </span>
<span class="hljs-regexp"> ... </span>
<span class="hljs-regexp">} </span>
Für den späteren Bedarf (Navigation innerhalb der Baumstruktur) wird die Liste noch um das Feld owner erweitert, das den übergeordneten Knoten referenziert. Das Feld wird im Konstruktor gesetzt (siehe auch Listing 2). Außerdem wird, wenn ein neues Element hinzugefügt wird, dessen Parent-Eigenschaft auf owner neu gesetzt (überschriebene Methode InsertItem).Nachdem die benötigten Infrastrukturklassen definiert worden sind, gilt es nun, im ViewModel die Datenstrukturen für das Zusammenspiel mit der Oberfläche mit Leben zu füllen. Listing 3 zeigt den grundlegenden Aufbau des ViewModels. Im Konstruktor wird eine Instanz von TreeItem angelegt (Feld rootTreeItem1) und dessen Unterelemente durch den rekursiven Aufruf von FillTree mit den Informationen aus dem Dateisystem angelegt und gefüllt. Eine Read-only-Property (Tree1) stellt die Kindelemente des Root-Items für eine Datenbindung nach außen bereit. Nun ist es an der Zeit, den XAML-Teil näher zu betrachten
Listing 3: Erzeugen der Datenstrukturen im ViewModel
public class MainViewModel : NotificationObject <br/>{ <br/> private TreeItem rootTreeItem1; <br/> public TreeItemList Tree1 { <br/> <span class="hljs-built_in">get</span> { <span class="hljs-built_in">return</span> rootTreeItem1.Items; } <br/> } <br/> ... <br/> public MainViewModel() { <br/> rootTree1 = <span class="hljs-built_in">new</span> TreeItem(); <br/> // Pfad bei Bedarf anpassen <br/> <span class="hljs-built_in">var</span> path = Directory.GetParent( <br/> Environment.CurrentDirectory).Parent.<br/> Parent.Parent.FullName; <br/> FillTree(Tree1, path,<span class="hljs-number">0</span>); <br/> ... <br/> } <br/> // Aufbau der Datenstruktur basierend auf dem <br/> // angegebenen Verzeichnis <br/> private void FillTree(TreeItemList tree, <br/> <span class="hljs-built_in">string</span> <span class="hljs-built_in">directory</span>, int level) <br/> { <br/> try { <br/> foreach (<span class="hljs-built_in">var</span> dir <span class="hljs-keyword">in</span> <br/> Directory.EnumerateDirectories(<span class="hljs-built_in">directory</span>)) <br/> { <br/> <span class="hljs-built_in">var</span> ti = <span class="hljs-built_in">new</span> TreeItem {<br/> Data = <span class="hljs-built_in">new</span> DirectoryDataObject { <br/> Caption = Path.GetFileName(dir), <br/> Level=level }, ToolTip = dir, IsEnabled= <br/> Path.GetFileName(dir) != <span class="hljs-string">"obj"</span> }; <br/> tree.Add(ti); <br/> FillTree(ti.Items, dir, level+<span class="hljs-number">1</span>); <br/> } <br/> foreach (<span class="hljs-built_in">var</span> file <span class="hljs-keyword">in</span> <br/> Directory.EnumerateFiles(<span class="hljs-built_in">directory</span>)) <br/> { <br/> <span class="hljs-built_in">var</span> ti = <span class="hljs-built_in">new</span> TreeItem { <br/> Data = <span class="hljs-built_in">new</span> FileDataObject { <br/> Caption = Path.GetFileName(file), <br/> Level=level }, ToolTip = file }; <br/> tree.Add(ti); <br/> } <br/> } <br/> <span class="hljs-built_in">catch</span> (Exception) { } <br/> } <br/> ... <br/>}
Griff in die WPF-Werkzeugkiste
Der erste Schritt besteht darin, die ItemsSource-Eigenschaft der TreeView über eine Datenbindung mit der Eigenschaft Tree1 des ViewModels zu verknüpfen. Dadurch wird schon einmal die Liste der TreeItems der obersten Ebene in der TreeView sichtbar (Bild 2). Da die TreeView nichts mit dem Datentyp TreeItem anfangen kann, zeigt sie den aus Object.ToString gelieferten Text in der Oberfläche an, also den voll qualifizierten Typnamen MVVM_Utilities.TreeItem. Im zweiten Schritt müssen Sie der TreeView daher mitteilen, wie sie einen Knoten im Baum darstellen soll. Das geschieht mithilfe eines DataTemplate(Listing 4).
Der erste Ansatz:Die TreeView zeigt die erste Ebene der Hilfsstruktur(Bild 2)
Autor
Listing 4: HierarchicalDataTemplate
<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">TreeView</span> <span class="hljs-attr">ItemsSource</span>=</span></span><span class="hljs-template-variable">{Binding Tree1}</span><span class="xml"><span class="hljs-tag"> <span class="hljs-attr">...</span> &gt;</span> </span><br/><span class="xml"> <span class="hljs-tag">&lt;<span class="hljs-name">TreeView.ItemTemplate</span>&gt;</span> </span><br/><span class="xml"> <span class="hljs-tag">&lt;<span class="hljs-name">HierarchicalDataTemplate</span> <span class="hljs-attr">ItemsSource</span>=</span></span><br/><span class="xml"><span class="hljs-tag"> <span class="hljs-string">"</span></span></span><span class="hljs-template-variable">{Binding Items}</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">ContentPresenter</span> <span class="hljs-attr">Content</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">HierarchicalDataTemplate.Triggers</span>&gt;</span> </span><br/><span class="xml"> <span class="hljs-tag">&lt;<span class="hljs-name">DataTrigger</span> <span class="hljs-attr">Binding</span>=<span class="hljs-string">"</span></span></span><span class="hljs-template-variable">{Binding IsEnabled}</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">Value</span>=<span class="hljs-string">"false"</span>&gt;</span> </span><br/><span class="xml"> <span class="hljs-tag">&lt;<span class="hljs-name">Setter</span> <span class="hljs-attr">Property</span>=<span class="hljs-string">"TreeViewItem.Foreground"</span> </span></span><br/><span class="xml"><span class="hljs-tag"> <span class="hljs-attr">Value</span>=<span class="hljs-string">"Red"</span> /&gt;</span> </span><br/><span class="xml"> <span class="hljs-tag">&lt;/<span class="hljs-name">DataTrigger</span>&gt;</span> </span><br/><span class="xml"> <span class="hljs-tag">&lt;/<span class="hljs-name">HierarchicalDataTemplate.Triggers</span>&gt;</span> </span><br/><span class="xml"> <span class="hljs-tag">&lt;/<span class="hljs-name">HierarchicalDataTemplate</span>&gt;</span> </span><br/><span class="xml"> <span class="hljs-tag">&lt;/<span class="hljs-name">TreeView.ItemTemplate</span>&gt;</span> </span><br/><span class="xml"> ... </span><br/><span class="xml"><span class="hljs-tag">&lt;/<span class="hljs-name">TreeView</span>&gt;</span> </span>
Zum Einsatz kommt hier der Typ HierarchicalDataTemplate, der sich bei Steuerelementen mit Baumstrukturen wie TreeView oder Menu anbietet. HierarchicalDataTemplate besteht im Inneren aus einem gewöhnlichen DataTemplate, über das die visuelle Repräsentation eines einzelnen Knotens (TreeViewItem) deklariert wird. Der Datenkontext für den Knoten ist jeweils ein TreeItem-Objekt aus der Hilfsstruktur. Um die Darstellung der Daten nicht einschränken zu müssen, wird ein ContentPresenter-Element an das durchgereichte Datenobjekt (Eigenschaft TreeItem.Data) gebunden. Das resultiert in der Ausgabe des für DirectoryDataObject beziehungsweise FileDataObject abgerufenen Textes von ToString. Noch nicht das, was wir wollen, aber die Datenstruktur ist schon mal erkennbar.An dieser Stelle ist es auch möglich, auf die optische Gestaltung der Knoten Einfluss zu nehmen, bei denen IsEnabled auf false gesetzt ist. Innerhalb eines DataTemplates lässt sich das über einen DataTrigger einrichten. Dieser prüft den Wert von IsEnabled und führt im Fall von false einen Setter aus, der die Eigenschaft Foreground der TreeView auf Red umschaltet. Bei Bedarf können hier auch andere Eigenschaften beeinflusst werden.Im Gegensatz zum klassischen DataTemplate hat das HierarchicalDataTemplate noch weitere Eigenschaften, insbesondere die hier verwendete Eigenschaft ItemsSource. Denn das Template beschreibt, wie die Zuordnung über TreeView.ItemTemplate erahnen lässt, nur die Visualisierung eines einzelnen Elements. Über Eigenschaft ItemsSource können Sie dem TreeViewItem nun mitteilen, woher es seine Kindelemente beziehen soll. Das ist im vorliegenden Beispiel die Eigenschaft Items der Hilfsklasse TreeItem. Und schon zeigt die TreeView die gesamte Baumstruktur an (Bild 3). Durch die Deklaration des HierarchicalDataTemplate legt die TreeView also für jedes TreeItem der Hilfsstruktur ein TreeViewItem für die Darstellung an und generiert somit den gleichen visuellen Aufbau des Baums, wie er im Hintergrund festgelegt wurde. Doch lassen sich nun die Verbindungen zwischen den Eigenschaften IsSelected, IsExpanded und IsEnabled in beiden Objektwelten herstellen? Ein TreeViewItem kann man im ViewModel ja nicht referenzieren, da es im UI erst zur Laufzeit automatisch generiert wird.

Die Baumstruktursteht, aber die Inhalte fehlen noch(Bild 3)
Autor
Die Lösung besteht in der Nutzung der Eigenschaft ItemContainerStyle der TreeView. Über diese Eigenschaft kann man einen Style vorgeben, der jedem generierten TreeViewItem zugewiesen wird.
<span class="hljs-tag"><<span class="hljs-name">TreeView</span> <span class="hljs-attr">...</span>></span>
<span class="hljs-tag"><<span class="hljs-name">TreeView.ItemContainerStyle</span>></span>
<span class="hljs-tag"><<span class="hljs-name">Style</span> <span class="hljs-attr">TargetType</span>=<span class="hljs-string">"TreeViewItem"</span>></span><span class="xml"> </span>
<span class="xml"> <span class="hljs-comment"><!--Verknüpfung der Eigenschaften eines </span></span>
<span class="xml"><span class="hljs-comment"> TreeViewItems--></span> </span>
<span class="xml"> <span class="hljs-tag"><<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 class="xml"><span class="hljs-tag"> <span class="hljs-string">"{Binding IsSelected, Mode=TwoWay}"</span> /></span> </span>
<span class="xml"> <span class="hljs-tag"><<span class="hljs-name">Setter</span> <span class="hljs-attr">Property</span>=<span class="hljs-string">"IsExpanded"</span> <span class="hljs-attr">Value</span>=</span></span>
<span class="xml"><span class="hljs-tag"> <span class="hljs-string">"{Binding IsExpanded, Mode=TwoWay}"</span> /></span> </span>
<span class="xml"> <span class="hljs-tag"><<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 class="xml"><span class="hljs-tag"> <span class="hljs-string">"{Binding IsEnabled, Mode=TwoWay}"</span> /></span> </span>
<span class="xml"> <span class="hljs-tag"><<span class="hljs-name">Setter</span> <span class="hljs-attr">Property</span>=<span class="hljs-string">"ToolTip"</span> <span class="hljs-attr">Value</span>=</span></span>
<span class="xml"><span class="hljs-tag"> <span class="hljs-string">"{Binding ToolTip}"</span> /></span> </span>
<span class="xml"> </span><span class="hljs-tag"></<span class="hljs-name">Style</span>></span>
<span class="hljs-tag"></<span class="hljs-name">TreeView.ItemContainerStyle</span>></span>
...
<span class="hljs-tag"></<span class="hljs-name">TreeView</span>></span>
Der TargetType des Styles muss dem Objekttyp entsprechen, der automatisch generiert wird. Hier ist es der Typ TreeViewItem, bei einer ListBox wäre es ListBoxItem und so weiter.Über die Setter kann im Style direkt auf die jeweiligen Properties zugegriffen werden und somit kann man ihnen, wie im Code zu sehen, auch einen Bindungsausdruck zuordnen. Dieser stellt nun die Verbindung zum Beispiel zwischen der Eigenschaft IsExpanded auf der Seite des TreeViewItem und IsExpanded auf der Seite der Hilfsstruktur TreeItem her. Zu beachten ist hier, dass die Bindungen explizit auf TwoWay gesetzt werden müssen, da sonst Änderungen im UI (Selektion, Auf-/Zuklappen) nicht zu Änderungen in der Datenstruktur im Hintergrund führen.Bild 4 veranschaulicht die Zusammenhänge noch einmal. Über ItemTemplate/HierarchicalDataTemplate wird der strukturelle Aufbau des Baums festgelegt. Die TreeView legt so für jede Instanz von TreeItem automatisch ein TreeViewItem an (blauer Rahmen). Das Template (grüner Rahmen) beschreibt die Repräsentation des Inhalts (Content). Es hat selbst aber keinen Zugriff auf die anderen Eigenschaften eines TreeViewItem – es definiert ja nur den Inhalt. Erst über ItemContainerStyle kommt man an die Eigenschaften eines so generierten TreeViewItem und kann sie via Setter-Definitionen setzen. Hier geht noch viel mehr. Über die Setter kann man beispielsweise auch jedem TreeViewItem ein Kontextmenü zuordnen (siehe auch [1]) oder farbliche Füllungen oder Schriftparameter setzen.

Komplexes Konstrukt:TreeViewItem, HierarchicalDataTemplate, ItemContainerStyle(Bild 4)
Autor
So weit steht die Infrastruktur, die sich allgemeingültig für beliebige Modellklassen benutzen lässt. Die nun noch fehlende sinnvolle Darstellung der Inhalte gelingt wieder auf dem bekannten Weg über DataTemplates, hier ausgelagert in ein eigenes ResourceDictionary(Listing 5), das seinerseits wie folgt über die App.xaml eingebunden wurde:
Listing 5: Vorlagen für die Model-Klassen
<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">ResourceDictionary</span> </span></span><br/><span class="xml"><span class="hljs-tag"> <span class="hljs-attr">xmlns</span>=<span class="hljs-string">"http://schemas.microsoft.com/winfx/</span></span></span><br/><span class="xml"><span class="hljs-tag"><span class="hljs-string"> 2006/xaml/presentation"</span> </span></span><br/><span class="xml"><span class="hljs-tag"> <span class="hljs-attr">xmlns:x</span>=<span class="hljs-string">"http://schemas.microsoft.com/winfx/</span></span></span><br/><span class="xml"><span class="hljs-tag"><span class="hljs-string"> 2006/xaml"</span> </span></span><br/><span class="xml"><span class="hljs-tag"> <span class="hljs-attr">xmlns:models</span>=</span></span><br/><span class="xml"><span class="hljs-tag"> <span class="hljs-string">"clr-namespace:WPF_TreeView_MVVM_DragDrop.Models"</span> </span></span><br/><span class="xml"><span class="hljs-tag"> <span class="hljs-attr">xmlns:mvvm</span>=<span class="hljs-string">"fuechse-online.de"</span> </span></span><br/><span class="xml"><span class="hljs-tag"> <span class="hljs-attr">xmlns:local</span>=</span></span><br/><span class="xml"><span class="hljs-tag"> <span class="hljs-string">"clr-namespace:WPF_TreeView_MVVM_DragDrop"</span>&gt;</span> </span><br/><br/><span class="xml"> <span class="hljs-tag">&lt;<span class="hljs-name">mvvm:ResourceListConverter</span> <span class="hljs-attr">x:Key</span>=<span class="hljs-string">"Images"</span>&gt;</span> </span><br/><span class="xml"> <span class="hljs-tag">&lt;<span class="hljs-name">mvvm:ResourceListConverter.Items</span>&gt;</span> </span><br/><span class="xml"> <span class="hljs-tag">&lt;<span class="hljs-name">BitmapImage</span> <span class="hljs-attr">UriSource</span>=</span></span><br/><span class="xml"><span class="hljs-tag"> <span class="hljs-string">"/Images/icons8-camera-50.png"</span>/&gt;</span> </span><br/><span class="xml"> <span class="hljs-tag">&lt;<span class="hljs-name">BitmapImage</span> <span class="hljs-attr">UriSource</span>=</span></span><br/><span class="xml"><span class="hljs-tag"> <span class="hljs-string">"/Images/icons8-kangaroo-50.png"</span>/&gt;</span> </span><br/><span class="xml"> <span class="hljs-tag">&lt;<span class="hljs-name">BitmapImage</span> <span class="hljs-attr">UriSource</span>=</span></span><br/><span class="xml"><span class="hljs-tag"> <span class="hljs-string">"/Images/icons8-bird-50.png"</span>/&gt;</span> </span><br/><span class="xml"> <span class="hljs-tag">&lt;<span class="hljs-name">BitmapImage</span> <span class="hljs-attr">UriSource</span>=</span></span><br/><span class="xml"><span class="hljs-tag"> <span class="hljs-string">"/Images/icons8-opened-folder-50.png"</span>/&gt;</span> </span><br/><span class="xml"> <span class="hljs-tag">&lt;<span class="hljs-name">BitmapImage</span> <span class="hljs-attr">UriSource</span>=</span></span><br/><span class="xml"><span class="hljs-tag"> <span class="hljs-string">"/Images/icons8-sheep-50.png"</span>/&gt;</span> </span><br/><span class="xml"> <span class="hljs-tag">&lt;/<span class="hljs-name">mvvm:ResourceListConverter.Items</span>&gt;</span> </span><br/><span class="xml"> <span class="hljs-tag">&lt;/<span class="hljs-name">mvvm:ResourceListConverter</span>&gt;</span> </span><br/><br/><span class="xml"> <span class="hljs-comment">&lt;!--Templates für die Darstellung der Model-Objekte </span></span><br/><span class="xml"><span class="hljs-comment"> (hier im Beispiel Directory/FileDataObject--&gt;</span> </span><br/><span class="xml"> <span class="hljs-tag">&lt;<span class="hljs-name">DataTemplate</span> <span class="hljs-attr">DataType</span>=</span></span><br/><span class="xml"><span class="hljs-tag"> <span class="hljs-string">"</span></span></span><span class="hljs-template-variable">{x:Type models:DirectoryDataObject}</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">StackPanel</span> <span class="hljs-attr">Orientation</span>=<span class="hljs-string">"Horizontal"</span>&gt;</span> </span><br/><span class="xml"> <span class="hljs-tag">&lt;<span class="hljs-name">Image</span> <span class="hljs-attr">Source</span>= <span class="hljs-string">"</span></span></span><span class="hljs-template-variable">{Binding Level, </span><br/><span class="hljs-template-variable"> Converter={StaticResource Images}</span><span class="xml"><span class="hljs-tag"><span class="hljs-string">}"</span> <span class="hljs-attr">Width</span>=<span class="hljs-string">"15"</span> </span></span><br/><span class="xml"><span class="hljs-tag"> <span class="hljs-attr">Margin</span>=<span class="hljs-string">"0,0,5,0"</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 Caption}</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">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">DataTemplate</span> <span class="hljs-attr">DataType</span>=</span></span><br/><span class="xml"><span class="hljs-tag"> <span class="hljs-string">"</span></span></span><span class="hljs-template-variable">{x:Type models:FileDataObject}</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">StackPanel</span> <span class="hljs-attr">Orientation</span>=<span class="hljs-string">"Horizontal"</span>&gt;</span> </span><br/><span class="xml"> <span class="hljs-tag">&lt;<span class="hljs-name">Image</span> <span class="hljs-attr">Source</span>=<span class="hljs-string">"/Images/icons8-sheep-50.png"</span> </span></span><br/><span class="xml"><span class="hljs-tag"> <span class="hljs-attr">Width</span>=<span class="hljs-string">"15"</span> <span class="hljs-attr">Margin</span>=<span class="hljs-string">"0,0,5,0"</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 Caption}</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">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">ResourceDictionary</span>&gt;</span> </span>
<span class="hljs-tag"><<span class="hljs-name">Application</span> <span class="hljs-attr">x:Class</span>=</span>
<span class="hljs-tag"> <span class="hljs-string">"WPF_TreeView_MVVM_DragDrop.App"</span> </span>
<span class="hljs-tag"> <span class="hljs-attr">xmlns</span>=<span class="hljs-string">"http://schemas.microsoft.com/</span></span>
<span class="hljs-tag"><span class="hljs-string"> winfx/2006/xaml/presentation"</span> </span>
<span class="hljs-tag"> <span class="hljs-attr">xmlns:x</span>=<span class="hljs-string">"http://schemas.microsoft.com/winfx/2006/xaml"</span> </span>
<span class="hljs-tag"> <span class="hljs-attr">StartupUri</span>=<span class="hljs-string">"MainWindow.xaml"</span>></span>
<span class="hljs-tag"><<span class="hljs-name">Application.Resources</span>></span>
<span class="hljs-tag"><<span class="hljs-name">ResourceDictionary</span>></span>
<span class="hljs-tag"><<span class="hljs-name">ResourceDictionary.MergedDictionaries</span>></span>
<span class="hljs-tag"><<span class="hljs-name">ResourceDictionary</span> <span class="hljs-attr">Source</span>=</span>
<span class="hljs-tag"> <span class="hljs-string">"/DataRepresentation.xaml"</span>/></span>
<span class="hljs-tag"></<span class="hljs-name">ResourceDictionary.MergedDictionaries</span>></span>
<span class="hljs-tag"></<span class="hljs-name">ResourceDictionary</span>></span>
<span class="hljs-tag"></<span class="hljs-name">Application.Resources</span>></span>
<span class="hljs-tag"></<span class="hljs-name">Application</span>></span>
Für die Klassen DirectoryDataObject und FileDataObject wird hier jeweils eine allgemeingültige Vorlage beschrieben, die immer dann zum Einsatz kommt, wenn WPF Objekte des betreffenden Typs rendern soll und nicht explizit etwas anderes vorgegeben wird. Im vorliegenden Fall nutzen Templates also automatisch das im ContentPresenter(Listing 4) durchgeschleifte Datenobjekt. Der Kontext der Bindungen ist somit ein Datenobjekt, das durch die Ableitung von DataObjectBase (siehe Listing 1) die Eigenschaften Caption und Level aufweist. Caption wird in beiden Fällen direkt an die Text-Eigenschaft eines TextBlocks gebunden, Level im Fall von DirectoryDataObject an die Source-Eigenschaft eines Image-Controls. Bild 5 veranschaulicht noch einmal den Zusammenhang zwischen DataTemplate und der resultierenden Darstellung.Die Versuchung ist groß, Objekte wie Brushes oder Images, die für die Darstellung benötigt werden, vom ViewModel an die Oberfläche durchzureichen. Das ist jedoch aus verschiedenen Gründen ungünstig. Erstens würden WPF-spezifische Datentypen im C#-Code benötigt, was man ja eigentlich vermeiden wollte, und zweitens würde ein Aspekt der UI-Gestaltung in den C#-Code verlagert. Ein später hinzugerufener Designer wird Probleme haben, die Herkunft eines bestimmten Brushes oder eines Images oder vielleicht auch nur eines Ressourcen-Pfads wiederzufinden. Deswegen gehören solche Dinge immer in den XAML-Code. Es gibt verschiedene Wege, die Verbindung zwischen den Daten (hier die Eigenschaft Level) und den WPF-Objekten herzustellen.

Repräsentation des Models(Bild 5)
Autor
Im Beispiel geschieht dies durch den Einsatz einer Converter-Klasse (ResourceListConverter, Listing 6). Der Converter definiert eine öffentliche Property namens Items vom Typ List<object>. Im XAML-Code können ihr beliebige Objekte zugewiesen werden, wie im Beispiel von Listing 5 die BitmapImage-Objekte. Der C#-Code muss bei dieser Konstruktion keinerlei Kenntnisse vom Typ dieser Objekte haben, sondern muss nur auf Basis des übergebenen Indexes ein Element aus dieser Liste auswählen. So bleibt der Algorithmus für die Auswahl Sache des C#-Codes und das Bereitstellen infrage kommender Ressourcen Aufgabe des XAML-Codes.
Listing 6: ResourceListConverter
// Brücke zwischen einer Auflistung im XAML-Code und // einem über eine Datenbindung übergebenen <span class="hljs-built_in">Index</span> <br/><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> ResourceListConverter : IValueConverter <br/>{ <br/> // Liste der verfügbaren Objekte <br/> <span class="hljs-keyword">public</span> List&lt;object&gt; Items { get; set; } = <br/> new List&lt;object&gt;(); <br/><br/> <span class="hljs-keyword">public</span> object Convert(object <span class="hljs-keyword">value</span>, <br/> <span class="hljs-keyword">Type</span> targetType, object <span class="hljs-keyword">parameter</span>, <br/> CultureInfo culture) <br/> { <br/> // Erwartet wird ein <span class="hljs-built_in">Index</span> vom Typ Int32 <br/> <span class="hljs-built_in">int</span> <span class="hljs-built_in">index</span> = (<span class="hljs-built_in">int</span>)<span class="hljs-keyword">value</span>; <br/> <span class="hljs-keyword">if</span> (<span class="hljs-built_in">index</span> &gt;= Items.<span class="hljs-built_in">Count</span>) <span class="hljs-built_in">index</span> = <span class="hljs-number">0</span>; <br/> <span class="hljs-keyword">return</span> Items[<span class="hljs-built_in">index</span>]; // Bei Bedarf Fehler <br/> } // abfangen<br/><br/> <span class="hljs-keyword">public</span> object ConvertBack(object <span class="hljs-keyword">value</span>, <br/> <span class="hljs-keyword">Type</span> targetType, object <span class="hljs-keyword">parameter</span>, <br/> CultureInfo culture) <br/> { <br/> throw new NotImplementedException(); <br/> } <br/>}
Statt die Zuordnung von Styles und Templates wie oben beschrieben vorzunehmen, kann alternativ auch der Weg über Selector-Klassen eingeschlagen werden. Je nach Control werden Eigenschaften wie ContentTemplateSelector, ItemTemplateSelector oder ItemContainerStyleSelector unterstützt. Ihnen weist man eine Referenz auf ein Objekt zu, dessen Typ von TemplateSelector beziehungsweise StyleSelector abgeleitet ist. In diesen abgeleiteten Klassen überschreibt man dann die Methode SelectTemplate beziehungsweise SelectStyle und kann so wiederum über C#-Code eine Auswahl treffen, die im UI zum Tragen kommt. Verschiedene Dritthersteller von WPF-Controls nutzen gerne diesen Weg. Aber auch hier sollte man – entgegen manchen Beispielen aus der Microsoft-Dokumentation und im Web – darauf achten, im C#-Code keine WPF-Objekte, auch keine Ressourcen-Keys zu generieren, die man später im XAML-Code vergeblich sucht.
Lohn der Mühe
Nach diesem Ausflug in die Welt der Zaubertricks von WPF zeigt das TreeView-Beispiel nun endlich die gewünschte Darstellung aus Bild 1. Auch wenn der Aufwand vergleichsweise groß erscheint, besteht ein erheblicher Teil des Codes aus wiederverwendbarer Infrastruktur. Letztere lässt sich noch um ein paar praktische Funktionen ergänzen. Eine davon wäre das vollständige Auf- oder Zusammenklappen des gesamten Baums oder eines Teilbaums. Hierzu muss man lediglich die IsExpanded-Eigenschaft eines vorgegebenen Knotens setzen und dasselbe anschließend für alle Kindelemente rekursiv durchführen.Eine Ergänzung von TreeItemList erfüllt die Aufgabe. ExpandOrCollapseAll nimmt einen booleschen Wert entgegen, der angibt, ob auf- oder zugeklappt werden soll.
public class TreeItemList : ObservableCollection<TreeItem>
{
...
// Auf- oder Zuklappen aller Knoten dieser <span class="hljs-literal">und</span>
// aller darunter liegenden Ebenen
// <param name=<span class="hljs-string">"expand"</span>><span class="hljs-literal">true</span>: aufklappen,
// <span class="hljs-literal">false</span>: zuklappen</param>
public void ExpandOrCollapseAll(bool <span class="hljs-built_in">expand</span>)
{
foreach (<span class="hljs-built_in">var</span> item <span class="hljs-keyword">in</span> this)
{
item.IsExpanded = <span class="hljs-built_in">expand</span>;
item.Items.ExpandOrCollapseAll(
<span class="hljs-built_in">expand</span>);
}
}
}
Für alle Kindelemente wird IsExpanded in einer Schleife gesetzt und über den rekursiven Aufruf derselben Methode der Zustand auch an die Kindelemente weitergegeben.Im ViewModel kann man dann Commands bereitstellen, die für das Root-Element die Methode aufrufen und so dem Anwender ermöglichen, mit einem Klick den gesamten Baum aufzuklappen oder wieder auf die oberste Ebene einzuschränken:
public MainViewModel() {
...
ExpandAllCommand = <span class="hljs-keyword">new</span> ActionCommand(<span class="hljs-function"><span class="hljs-params">()</span> =></span>
Tree1.ExpandOrCollapseAll(<span class="hljs-literal">true</span>));
CollapseAllCommand = <span class="hljs-keyword">new</span> ActionCommand(<span class="hljs-function"><span class="hljs-params">()</span> =></span>
Tree1.ExpandOrCollapseAll(<span class="hljs-literal">false</span>));
...
}
Noch ein paar Buttons mit den Commands verknüpft, und schon sieht es aus wie in Bild 6.

Das Implementierenvon „Alles auf“ und „Alles zu“ ist jetzt ein Klacks(Bild 6)
Autor
Vielleicht möchten Sie ein bestimmtes TreeItem auswählen und alle Elternknoten aufklappen, sodass es zu sehen ist? Die Suche wäre Aufgabe des ViewModels (Listing 7), das Auswählen und Aufklappen kann TreeItem übernehmen:
Listing 7: Suche nach einem Knoten
public class MainViewModel : NotificationObject { <br/> ... <br/> // Suche nach dem ersten Vorkommen von App.xaml <br/> private void OpenAppXaml() { <br/> <span class="hljs-keyword">var</span> item = FindCaption(<span class="hljs-string">"App.xaml"</span>, Tree1); <br/> <span class="hljs-keyword">if</span>(item == <span class="hljs-literal">null</span>) <span class="hljs-keyword">return</span>; <br/><br/> item.IsSelected = <span class="hljs-literal">true</span>; // TreeItem auswählen <br/> item.ExpandAncestors(<span class="hljs-literal">true</span>); // Alle Vorgänger-<br/> } // knoten aufklappen <br/><br/> // Rekursive Suche nach einem Verzeichnis-<br/> // oder Dateinamen <br/> private TreeItem FindCaption(<br/> string caption, TreeItemList <span class="hljs-keyword">list</span>) <br/> { <br/> <span class="hljs-keyword">foreach</span> (<span class="hljs-keyword">var</span> item <span class="hljs-keyword">in</span> <span class="hljs-keyword">list</span>) { <br/> <span class="hljs-keyword">if</span> (((DataObjectBase)item.Data).Caption == <br/> caption) <span class="hljs-keyword">return</span> item; <br/> <span class="hljs-keyword">var</span> subitem = <br/> FindCaption(caption, item.Items); <br/> <span class="hljs-keyword">if</span> (subitem != <span class="hljs-literal">null</span>) <span class="hljs-keyword">return</span> subitem; <br/> } <br/> <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>; <br/> } <br/>}
<span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">TreeItem</span> : <span class="hljs-title">NotificationObject</span> {
...
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">ExpandAncestors</span>(</span>
<span class="hljs-function"><span class="hljs-params"> <span class="hljs-keyword">bool</span> expand</span>) </span>
<span class="hljs-function"> </span>{
<span class="hljs-keyword">if</span> (Parent != <span class="hljs-literal">null</span>) {
Parent.IsExpanded = expand;
Parent.ExpandAncestors(expand);
}
}
}
ExpandAncestors setzt die IsExpanded-Eigenschaft des Elternknotens, sofern dieser existiert, und ruft die Methode für diesen rekursiv auf. So werden vom ausgehenden TreeItem alle anderen oberhalb expandiert. Im Beispiel wird nach dem ersten Vorkommen des Dateinamens App.xaml gesucht. Das Ergebnis zeigt Bild 7. Allerdings soll an dieser Stelle ein Problem nicht verschwiegen werden: Wenn vor der Suche der gesamte Baum aufgeklappt war, wird der gewünschte Knoten zwar selektiert, aber unter Umständen liegt er außerhalb des Bereichs, der von der TreeView angezeigt wird. Um ihn sichtbar zu machen, müsste man entweder die TreeView anweisen, die passenden Scrollbar-Einstellungen vorzunehmen, oder alle anderen Knoten schließen. Ersteres könnte man über die Methode FrameworkElement.BringIntoView versuchen. Einige Experimente hierzu lieferten aber kein zufriedenstellendes Ergebnis. Vermutlich müsste man sich in diesem Fall doch einen genaueren Einblick in das Rendering der TreeView verschaffen.

Selektion und Aufklappeneines bestimmten Knotens per Knopfdruck(Bild 7)
Autor
TreeItem und TreeItemList sind keine Einbahnstraßen. Über ihre Eigenschaften kann man vom C#-Code aus das Verhalten in der Oberfläche beeinflussen. Andererseits werden die Setter der Properties ausgeführt, wenn der Anwender etwas in der TreeView auswählt oder aufklappt.Zunutze machen kann man sich das, indem man in den Settern zusätzliche Events auslöst und diese nach außen bereitstellt. So lassen sich spezifische Ereignisse im ViewModel beobachten und auswerten. Die vorliegende Implementierung sieht bislang lediglich eine Behandlung via INotifyPropertyChanged vor. Aber die Infrastrukturklassen können ja beliebig erweitert werden. Um die aktuelle Auswahl nachverfolgen zu können, kann zum Beispiel in TreeItem ein Event deklariert werden:
<span class="hljs-keyword">public</span> <span class="hljs-keyword">event</span> EventHandler
IsSelectedChanged;
Der Event wird im Setter von IsSelected gefeuert, sobald sich der Zustand geändert hat:
<span class="hljs-keyword">if</span> (isSelected == <span class="hljs-keyword">value</span>) <span class="hljs-keyword">return</span>;
isSelected = <span class="hljs-keyword">value</span>;
OnPropertyChanged();
IsSelectedChanged?.Invoke(<span class="hljs-keyword">this</span>, EventArgs.Empty);
Der Event wird dann bei Änderung der Auswahl zweimal ausgelöst, einmal für das zuvor selektierte TreeItem, danach für das neu selektierte. Im ViewModel kann man den Event abonnieren und bleibt somit stets auf dem Laufenden, was die aktuelle Auswahl in der TreeView betrifft.
Noch ein bisschen aufräumen
Nicht zuletzt für den kommenden zweiten Teil, der durch den Einbau von Drag-and-drop sehr Event-lastig wird, sollten Sie den Code aufteilen und alle Infrastrukturklassen in eine eigenständige Klassenbibliothek packen. Die oben beschriebenen Klassen TreeItem, TreeItemList und ResourceListConverter und die hier nicht weiter dokumentierten Klassen NotificationObject und ActionCommand können einfach nach Anpassen der Namensräume in die Bibliothek verschoben werden.Für die TreeView bietet sich eine Kapselung an, in der die beschriebenen Strukturen (HierarchicalDataTemplate, ItemContainerStyle) umgesetzt werden, damit man sie nicht bei jeder TreeView erneut explizit setzen muss. Eigentlich wäre das ein guter Einsatzfall für ein CustomControl, dessen ControlTemplate später ausgetauscht werden könnte. Allerdings ist, gerade im Hinblick auf das bevorstehende Eventhandling, damit leider auch ein sehr großer Aufwand verbunden. Daher soll hier der Weg unter Verwendung eines UserControls eingeschlagen werden. Der XAML-Code des UserControls kapselt zunächst nur die darzustellende TreeView mit ihren Definitionen für ItemContainerStyle und ItemTemplate(Listing 8). Zur Erinnerung: Das Template für die Darstellung der Model-Klassen gehört zum anwendungsspezifischen Hauptprogramm. Listing 9 zeigt den Code-behind des UserControls. Die Verbindung zur Außenwelt erfolgt über dabei eine Dependency-Property namens Items. Sie ist vom Typ TreeItemList. Wird sie von außen gesetzt, resultiert das in einem Aufruf von OnItemsChanged. Dort wird die ItemsSource-Eigenschaft der TreeView gesetzt.Listing 8: Kapseln in einem UserControl
<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">UserControl</span> <span class="hljs-attr">x:Class</span>=</span></span><br/><span class="xml"><span class="hljs-tag"> <span class="hljs-string">"MVVM_Utilities.ExtendedTreeView"</span> <span class="hljs-attr">...</span> &gt;</span> </span><br/><span class="xml"> <span class="hljs-tag">&lt;<span class="hljs-name">Grid</span>&gt;</span> </span><br/><span class="xml"> <span class="hljs-tag">&lt;<span class="hljs-name">TreeView</span> <span class="hljs-attr">Name</span>=<span class="hljs-string">"_tv_"</span> <span class="hljs-attr">...</span>&gt;</span> </span><br/><span class="xml"> <span class="hljs-tag">&lt;<span class="hljs-name">TreeView.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">"TreeViewItem"</span>&gt;</span><span class="undefined"> </span></span><br/><span class="xml"><span class="undefined"> ... </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">TreeView.ItemContainerStyle</span>&gt;</span> </span><br/><span class="xml"> <span class="hljs-tag">&lt;<span class="hljs-name">TreeView.ItemTemplate</span>&gt;</span> </span><br/><span class="xml"> <span class="hljs-tag">&lt;<span class="hljs-name">HierarchicalDataTemplate</span> <span class="hljs-attr">ItemsSource</span>=</span></span><br/><span class="xml"><span class="hljs-tag"> <span class="hljs-string">"</span></span></span><span class="hljs-template-variable">{Binding Items}</span><span class="xml"><span class="hljs-tag"><span class="hljs-string">"</span>&gt;</span> </span><br/><span class="xml"> ... </span><br/><span class="xml"> <span class="hljs-tag">&lt;/<span class="hljs-name">HierarchicalDataTemplate</span>&gt;</span> </span><br/><span class="xml"> <span class="hljs-tag">&lt;/<span class="hljs-name">TreeView.ItemTemplate</span>&gt;</span> </span><br/><span class="xml"> <span class="hljs-tag">&lt;/<span class="hljs-name">TreeView</span>&gt;</span> </span><br/><span class="xml"> <span class="hljs-tag">&lt;/<span class="hljs-name">Grid</span>&gt;</span> </span><br/><span class="xml"><span class="hljs-tag">&lt;/<span class="hljs-name">UserControl</span>&gt;</span> </span>
Listing 9: CodeBehind des UserControls
using System; <br/>using System.Diagnostics; <br/>using System.Threading.Tasks; <br/>using System.Windows; <br/>using System.Windows.Controls; <br/>using System.Windows.Input; <br/>using System.Windows.Threading; <br/><br/>namespace MVVM_Utilities <br/>{ <br/> // Interaction logic for ExtendedTreeView.xaml <br/> public partial class ExtendedTreeView : UserControl <br/> { <br/> public ExtendedTreeView() { <br/> InitializeComponent(); <br/> } <br/><br/> public TreeItemList Items { <br/> get { <br/> return (<br/> TreeItemList)GetValue(ItemsProperty); <br/> } <br/> set {<br/> SetValue(ItemsProperty, value); <br/> } <br/> } <br/> // Using a DependencyProperty as the <br/> // backing store for Items. This enables <br/> // animation, styling, binding, et cetera. <br/> public static readonly DependencyProperty <br/> ItemsProperty = DependencyProperty.Register(<br/> "Items", typeof(TreeItemList), <br/> typeof(ExtendedTreeView), <br/> new FrameworkPropertyMetadata(OnItemsChanged)); <br/><br/> private static void OnItemsChanged( <br/> DependencyObject sender, <br/> DependencyPropertyChangedEventArgs e) <br/> { <br/> var uc = sender as ExtendedTreeView; <br/> // Datenstruktur an TreeView durchreichen <br/> uc._tv_.ItemsSource = uc.Items; <br/> } <br/>}
Um das Einbinden von Elementen einer Klassenbibliothek im XAML-Code zu vereinfachen, empfiehlt es sich, die verwendeten Namensräume über Assembly-Attribute bekannt zu machen:
using System.Windows.Markup;
[assembly: XmlnsDefinition(
"fuechse-online.de", nameof(MVVM_Utilities))]
[assembly: XmlnsPrefix("fuechse-online.de","mvvm")]
/* Verwendung im XAML-Code:
<Window xmlns:mvvm="fuechse-online.de"> ...
<mvvm:ExtendedTreeView Items="{Binding ...}">
*/
Außerhalb der Bibliothek können dann die Aliasnamen für die Deklaration der Namespaces verwendet und auf die Angabe der Assembly verzichtet werden.
Fazit und Ausblick
Es wurde die Vorgehensweisen beschrieben, die mithilfe einiger Infrastrukturklassen und einiger WPF-Tricks eine saubere Trennung zwischen der Welt des View-Models und der Darstellung in einer TreeView ermöglichen. Über Templates, Styles und Datenbindungen lassen sich viele Anwendungsfälle abdecken. Änderungen der Datenstrukturen im View-Model führen zu Änderungen im UI und vice versa. Für die beschriebenen Konstruktionen müssen keine Eventhandler verknüpft und keine Methoden der Steuerelemente aufgerufen werden. Das wird sich im kommenden zweiten Teil ändern, wenn es darum geht, Drag-and-drop umzusetzen. Unter [4] finden Sie den Code der Beispielanwendung.Fussnoten
- Joachim Fuchs, Hierarchien bändigen, dotnetpro 1/2011, Seite 64 ff., http://www.dotnetpro.de/A1101Hierarchic
- Joachim Fuchs, WPF-Trick für MVVM-Apps, dotnetpro 7/2021, Seite 26 ff., http://www.dotnetpro.de/A2107DataGridMultiselect
- Quelle der Icons, https://icons8.de
- Sourcecode, http://www.dotnetpro.de/SL2107TreeViewMVVM1