Databinding und deklarative Programmierung
Also, eine saubere Wpf-Anwendung basiert strikt auf Databinding, d.h., im Xaml-Code werden die Controls organisiert, und ans Viewmodel gebunden.
Control-Ereignisse zu verarbeiten verstößt gleichma gegen die "reine Lehre", nach der das Gui rein deklarativ zu implementieren ist, und prozeduraler Code gehört nicht ins Gui, sondern in den Viewmodel-Bereich.
Hier ein ausgezeichneter Artikel zur Begründung des MVVM-Patterns - von Josh Smith - (den Namen lohnt es sich zu merken)
Beispiel-"Anwendung"
Auf einen button-Click hin wird ein Datei-Ordner im Treeview angezeigt:
Hier mal die Dateien, wie sie in der Solution organisiert sind
Infrastruktur-Dll
Zu beachten der Verweis auf die GalaSoft.Mvvm - Library, welche einiges an "InfrastrukturCode" enthält, der unglücklicherweise vom PresentationFramework nicht bereitgestellt wird.
Derartige Infrastruktur-Dlls gibt es viele (Google: "mvvm toolkit") - ich habe mich für die Galasoft MVVM-Toolkit entschieden, weil das relativ klein ist, und weil deren Entwickler, Laurent Burion, auch ausgezeichnete Tutorials macht.
Das wichtigste Element dieser Lib, die ViewmodelBase, wird von diese App garnicht genutzt, weil sie mit der Framework-ObeservableCollection auskommt, die bereits INotifyPropertyChanged implementiert.
Hier wird nur das GalaSoft.RelayCommand genutzt - die zweitwichtigste Klasse.
Beides könnte man auch selber schreiben, aber wozu? - Besser als das GalaSoft-Teil wird mans nur unter Schwierigkeiten hinkriegen, schlechter jedoch mit Leichtigkeit
Auf mehrfache Nachfragen, ob diese Dll wirklich nötig sei:
Ja - steht doch in obigem Absatz, dass ich das GalaSoft.RelayCommand benötige.
Natürlich würde man lieber mit .Net-Bordmitteln auskommen, aber das PresentationFramework ist einfach in manchen Punkten ungenügend - deshalb gibts ja so viele mvvm-toolkits.
Code
Wie gesagt: Wpf trennt strikt zw. Daten und Anzeige. Es werden 2 ganz unterschiedliche Sprachen verwendet, nämlich für die Daten Code, für die Anzeige Xaml.
Das MainModel:
MainModel.Root ist also das einzige MainModel, und alles, was' gibt, wird da angebunden.
In diesem MainModel gibts nur eine Property zum Anbinden, nämlich den FileSystemTree:
FileSystemTree macht 2 Properties verfügbar, an die im Xaml-Code gebunden werden kann:
Müssemermal den DirectoryNode angugge, um die Bedeutung von DirectoryNode.LoadRecursive() zu verstehen:
Polymorphie
Tatsächlich handelt es sich um 2 Klassen, nämlich DirectoryNode erbt von einer Klasse FileSystemNode.
(Achtung - im folgenden nicht verwechseln: FileSystemInfo, FileInfo, DirectoryInfo sind Klassen aussm System.IO-Namespace, hingegen FileSystemNode und DirectoryNode sind selbst gecodet)
FileSystemNode enthält nix weiter als ein FileSystemInfo, der Basisklasse sowohl von System.IO.DirectoryInfo als auch von System.IO.FileInfo.
Die Property FileSystemNode.Item As FileSystemInfo ist also polymorph - kann sowohl ein DirectoryInfo darstellen als auch ein FileInfo.
DirectoryNode beerbt nun FileSystemNode, und erweitert es um die Property Childs As ObservableCollection(Of FileSystemNode). Diese Collection ist also ebenfalls polymorph - kann sowohl FileSystemNodes als auch DirectoryNodes enthalten, in bunter Mischung.
Das ist ja genau unser Ziel: Wir wollen im Treeview zweierlei Arten von Treenode darstellen: welche für Directories, und annere für Files.
Xaml
StartObjekt ist die Application.Xaml - die macht folgendes:
setzt also derzeit nur das Start-Fenster: "Views\MainWindow.xaml".
Bleibt noch zu gugge, wie und welche Controls im MainWindow an welche Code-Objekte gebunden werden:
Zunächstmal wird der <my>-Namespace klargemacht - diese Umständlichkeit bleibt keiner Xaml-Datei erspart, die auf selbstgecodete Klassen zugreift.
Dann wird der DataContext des gesamten Windows festgelegt:
DataContext="{x: Static my:MainModel.Root}" - Mit dieser komischen Syntax:
"{x: Static ...}" wird auf das Public Shared Root - Feld der MainModel-Klasse zugegriffen. Die Syntax wird nicht durch Intellisense unterstützt, und der Compiler meckert das auch an, bis man explizit kompiliert.
Dennoch ist diese Vorgehensweise die einfachste Möglichkeit, das Viewmodel ins Xaml zu holen.
Soweit jetzt nix anneres angegeben wird, beziehen sich jetzt alle Bindings, die im folgenden eingerichtet werden, auf genau diesen einen DataContext.
Aufbau des Windows
Alle anderen Controls des Windows sind hier auffm <Grid>-Control angebracht, welches hat 2 Rows, deren erste auf Height="Auto" eingestellt ist, sodaß diese Zeile ihre Höhe nach dem darin enthaltenen Control richtet.
Das erste Control ist ein Menu mit einem <MenuItem> darin, letzteres beschriftet mit dem sinnigen Text "Load", und sein Command ist an die Load-Property der FileSystemTree-Property des Root-MainModels (DataContext!) gebunden:
<MenuItem Header="Load" Command="{Binding Path=FileSystemTree.Load}" />
Was folgt ist der TreeView, und dem ist Grid.Row="1" zugeteilt, also die 2.Zeile des Grids (die das gesamte restliche Window ausfüllt).
ItemsSource des Treeviews - also wo er seine Nodes hernimmt - ist gebunden an FileSystemTree.Roots (ihr erinnert euch: das ist die ObservableCollection, die mit maximal einem DirectoryNode befüllt wurde)
Ja, das wars im wesentlichen - wären da nicht noch die Resourcen des TreeViews.
Da haben wir nämlich 2 DataTemplates, eines davon sogar ein <HierarchicalDataTemplate> - und damit wären wir beim Thema DataTemplating:
So ein DataTemplate gibt zunächst an, für welchen Typ es zuständig ist, und dann werden da Controls hineingebastelt und an Eigenschaften dieses Typs gebunden.
Da habe ich jetzt in beide Templates eiglich dasselbe hineingebastelt, nämlich ein <Stackpanel> mit einem <Image> und einem <Textblock>. Der Text ist in beiden Fällen gebunden an die Item.Name-Property des zuständigen Typs. Ihr erinnert euch: FileSystemNode hat eine Item-Property vom Typ System.IO.FilesystemInfo, und DirectoryNode hat diese Property natürlich auch, nämlich von FileSystemNode geerbt. Und ein System.IO.FilesystemInfo hat einen Name, und den wollemer anzeige.
Zu beachten ist, dass innerhalb des DataTemplates nicht der DataContext des Windows gilt, sondern die Typ-angabe des DataTemplates gibt hier den lokalen DataContext vor.
Noch das Spezifikum eines HierarchicalDataTemplate erläutert: Beim HierarchicalDataTemplate kannich (ganz ähnlich wie beim Treeview) eine ItemsSource-Property binden, und damit ist dem HierarchicalDataTemplate mitgeteilt, wie an untergeordnete Nodes zu kommen ist: Das ist natürlich die Childs-Property eines DirectoryNodes, mit der man an seine Kind-Knoten kommt - habterja bereits weiter oben gesehen
Wars das jetzt - keine Treenodes adden und Zeugs?
Jepp - den Rest erledigt das DataTemplating für uns: Der Treeview holt sich Daten-Nodes aus seiner gebundenen ItemsSource, und für jeden Daten-Node schaut er in seine Resourcen. Hat er einen DirectoryNode erwischt, wendet er das HierarchicalDataTemplate an, um das gebundene TreeviewItem zu erstellen.
Nämlich weil die Typ-Angabe dieses HierarchicalDataTemplate so schön auf DirectoryNode matcht.
Hat er einen FileSystemNode geholt, so nimmterhalt das annere DataTemplate, welches - als wäre es Absicht! - auf FileSystemNode matcht.
Und weil das DirectoryNode-Datatemplate halt hierarchical ist, schauter beim Expandieren des DirectoryNode-TreeviewItems auch in dessen Itemssource, sodaß - rekursiv - alle Daten-Nodes angezeigt werden.
Zusammenfassung
So, das war jetzt ein ziemlicher Crashkurs, odr?
Also, eine saubere Wpf-Anwendung basiert strikt auf Databinding, d.h., im Xaml-Code werden die Controls organisiert, und ans Viewmodel gebunden.
Control-Ereignisse zu verarbeiten verstößt gleichma gegen die "reine Lehre", nach der das Gui rein deklarativ zu implementieren ist, und prozeduraler Code gehört nicht ins Gui, sondern in den Viewmodel-Bereich.
Hier ein ausgezeichneter Artikel zur Begründung des MVVM-Patterns - von Josh Smith - (den Namen lohnt es sich zu merken)
Beispiel-"Anwendung"
Auf einen button-Click hin wird ein Datei-Ordner im Treeview angezeigt:
Hier mal die Dateien, wie sie in der Solution organisiert sind
Infrastruktur-Dll
Zu beachten der Verweis auf die GalaSoft.Mvvm - Library, welche einiges an "InfrastrukturCode" enthält, der unglücklicherweise vom PresentationFramework nicht bereitgestellt wird.
Derartige Infrastruktur-Dlls gibt es viele (Google: "mvvm toolkit") - ich habe mich für die Galasoft MVVM-Toolkit entschieden, weil das relativ klein ist, und weil deren Entwickler, Laurent Burion, auch ausgezeichnete Tutorials macht.
Das wichtigste Element dieser Lib, die ViewmodelBase, wird von diese App garnicht genutzt, weil sie mit der Framework-ObeservableCollection auskommt, die bereits INotifyPropertyChanged implementiert.
Hier wird nur das GalaSoft.RelayCommand genutzt - die zweitwichtigste Klasse.
Beides könnte man auch selber schreiben, aber wozu? - Besser als das GalaSoft-Teil wird mans nur unter Schwierigkeiten hinkriegen, schlechter jedoch mit Leichtigkeit
Auf mehrfache Nachfragen, ob diese Dll wirklich nötig sei:
Ja - steht doch in obigem Absatz, dass ich das GalaSoft.RelayCommand benötige.
Natürlich würde man lieber mit .Net-Bordmitteln auskommen, aber das PresentationFramework ist einfach in manchen Punkten ungenügend - deshalb gibts ja so viele mvvm-toolkits.
Code
Wie gesagt: Wpf trennt strikt zw. Daten und Anzeige. Es werden 2 ganz unterschiedliche Sprachen verwendet, nämlich für die Daten Code, für die Anzeige Xaml.
Das MainModel:
VB.NET-Quellcode
In diesem MainModel gibts nur eine Property zum Anbinden, nämlich den FileSystemTree:
VB.NET-Quellcode
- Public Class FileSystemTree : Inherits ViewModelBase
- Private _Browser As New FolderBrowserDialog _
- With {.SelectedPath = (New DirectoryInfo("..\..")).FullName}
- Public Property Roots As New ObservableCollection(Of DirectoryNode)
- Public Property Load As New GalaSoft.MvvmLight.Command.RelayCommand( _
- Sub()
- If _Browser.ShowDialog <> DialogResult.OK Then Return
- Dim root As New DirectoryNode _
- With {.Item = New DirectoryInfo(_Browser.SelectedPath)}
- root.LoadRecursive()
- Roots.Clear()
- Roots.Add(root)
- End Sub)
- End Class
FileSystemTree macht 2 Properties verfügbar, an die im Xaml-Code gebunden werden kann:
- die Roots-Collection für die Root-Nodes des Treeviews (es wird aber nur ein einziger DirectoryNode eingefüllt)
- ein Command für den Load-Button (beachte: es ist eine Property!): die auszuführende Methode ist gleich als anonyme Methode hineingecodet
Müssemermal den DirectoryNode angugge, um die Bedeutung von DirectoryNode.LoadRecursive() zu verstehen:
VB.NET-Quellcode
- Public Class FileSystemNode
- Public Property Item As FileSystemInfo
- End Class
- Public Class DirectoryNode : Inherits FileSystemNode
- Public Property Childs As New ObservableCollection(Of FileSystemNode)
- Public Sub LoadRecursive()
- Childs.Clear()
- Dim di = DirectCast(MyBase.Item, DirectoryInfo)
- For Each d In di.GetDirectories
- Dim nd = New DirectoryNode With {.Item = d}
- nd.LoadRecursive()
- Childs.Add(nd)
- Next
- For Each f In di.GetFiles
- Childs.Add(New FileSystemNode With {.Item = f})
- Next
- End Sub
- End Class
Polymorphie
Tatsächlich handelt es sich um 2 Klassen, nämlich DirectoryNode erbt von einer Klasse FileSystemNode.
(Achtung - im folgenden nicht verwechseln: FileSystemInfo, FileInfo, DirectoryInfo sind Klassen aussm System.IO-Namespace, hingegen FileSystemNode und DirectoryNode sind selbst gecodet)
FileSystemNode enthält nix weiter als ein FileSystemInfo, der Basisklasse sowohl von System.IO.DirectoryInfo als auch von System.IO.FileInfo.
Die Property FileSystemNode.Item As FileSystemInfo ist also polymorph - kann sowohl ein DirectoryInfo darstellen als auch ein FileInfo.
DirectoryNode beerbt nun FileSystemNode, und erweitert es um die Property Childs As ObservableCollection(Of FileSystemNode). Diese Collection ist also ebenfalls polymorph - kann sowohl FileSystemNodes als auch DirectoryNodes enthalten, in bunter Mischung.
Das ist ja genau unser Ziel: Wir wollen im Treeview zweierlei Arten von Treenode darstellen: welche für Directories, und annere für Files.
Xaml
StartObjekt ist die Application.Xaml - die macht folgendes:
XML-Quellcode
- <Application x:Class="Application"
- xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
- xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
- StartupUri="Views\MainWindow.xaml">
- <Application.Resources>
- <!--hier könnten alle möglichen Objekte global verfügbar gemacht werden-->
- </Application.Resources>
- </Application>
Bleibt noch zu gugge, wie und welche Controls im MainWindow an welche Code-Objekte gebunden werden:
XML-Quellcode
- <Window x:Class="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"
- xmlns:my="clr-namespace:WpfFilesystemTreeView"
- DataContext="{x:Static my:MainModel.Root}">
- <Grid>
- <Grid.RowDefinitions>
- <RowDefinition Height="Auto" />
- <RowDefinition />
- </Grid.RowDefinitions>
- <Menu>
- <MenuItem Header="Load" Command="{Binding Path=FileSystemTree.Load}" />
- </Menu>
- <TreeView Grid.Row="1" ItemsSource="{Binding Path=FileSystemTree.Roots}" >
- <TreeView.Resources>
- <HierarchicalDataTemplate DataType="{x:Type my:DirectoryNode}"
- ItemsSource="{Binding Path=Childs}">
- <StackPanel Orientation="Horizontal">
- <Image Width="16" Height="16" Source="/WpfFilesystemTreeView;component/Icons/Folder.ico" />
- <TextBlock Margin="2 1 0 1" Text="{Binding Path=Item.Name}" />
- </StackPanel>
- </HierarchicalDataTemplate>
- <DataTemplate DataType="{x:Type my:FileSystemNode}" >
- <StackPanel Orientation="Horizontal">
- <Image Width="16" Height="16" Source="/WpfFilesystemTreeView;component/Icons/File.ico" />
- <TextBlock Margin="2 1 0 1" Text="{Binding Path=Item.Name}" />
- </StackPanel>
- </DataTemplate>
- </TreeView.Resources>
- </TreeView>
- </Grid>
- </Window>
Dann wird der DataContext des gesamten Windows festgelegt:
DataContext="{x: Static my:MainModel.Root}" - Mit dieser komischen Syntax:
"{x: Static ...}" wird auf das Public Shared Root - Feld der MainModel-Klasse zugegriffen. Die Syntax wird nicht durch Intellisense unterstützt, und der Compiler meckert das auch an, bis man explizit kompiliert.
Dennoch ist diese Vorgehensweise die einfachste Möglichkeit, das Viewmodel ins Xaml zu holen.
Soweit jetzt nix anneres angegeben wird, beziehen sich jetzt alle Bindings, die im folgenden eingerichtet werden, auf genau diesen einen DataContext.
Aufbau des Windows
Alle anderen Controls des Windows sind hier auffm <Grid>-Control angebracht, welches hat 2 Rows, deren erste auf Height="Auto" eingestellt ist, sodaß diese Zeile ihre Höhe nach dem darin enthaltenen Control richtet.
Das erste Control ist ein Menu mit einem <MenuItem> darin, letzteres beschriftet mit dem sinnigen Text "Load", und sein Command ist an die Load-Property der FileSystemTree-Property des Root-MainModels (DataContext!) gebunden:
<MenuItem Header="Load" Command="{Binding Path=FileSystemTree.Load}" />
Was folgt ist der TreeView, und dem ist Grid.Row="1" zugeteilt, also die 2.Zeile des Grids (die das gesamte restliche Window ausfüllt).
ItemsSource des Treeviews - also wo er seine Nodes hernimmt - ist gebunden an FileSystemTree.Roots (ihr erinnert euch: das ist die ObservableCollection, die mit maximal einem DirectoryNode befüllt wurde)
Ja, das wars im wesentlichen - wären da nicht noch die Resourcen des TreeViews.
Da haben wir nämlich 2 DataTemplates, eines davon sogar ein <HierarchicalDataTemplate> - und damit wären wir beim Thema DataTemplating:
So ein DataTemplate gibt zunächst an, für welchen Typ es zuständig ist, und dann werden da Controls hineingebastelt und an Eigenschaften dieses Typs gebunden.
Da habe ich jetzt in beide Templates eiglich dasselbe hineingebastelt, nämlich ein <Stackpanel> mit einem <Image> und einem <Textblock>. Der Text ist in beiden Fällen gebunden an die Item.Name-Property des zuständigen Typs. Ihr erinnert euch: FileSystemNode hat eine Item-Property vom Typ System.IO.FilesystemInfo, und DirectoryNode hat diese Property natürlich auch, nämlich von FileSystemNode geerbt. Und ein System.IO.FilesystemInfo hat einen Name, und den wollemer anzeige.
Zu beachten ist, dass innerhalb des DataTemplates nicht der DataContext des Windows gilt, sondern die Typ-angabe des DataTemplates gibt hier den lokalen DataContext vor.
Noch das Spezifikum eines HierarchicalDataTemplate erläutert: Beim HierarchicalDataTemplate kannich (ganz ähnlich wie beim Treeview) eine ItemsSource-Property binden, und damit ist dem HierarchicalDataTemplate mitgeteilt, wie an untergeordnete Nodes zu kommen ist: Das ist natürlich die Childs-Property eines DirectoryNodes, mit der man an seine Kind-Knoten kommt - habterja bereits weiter oben gesehen
Wars das jetzt - keine Treenodes adden und Zeugs?
Jepp - den Rest erledigt das DataTemplating für uns: Der Treeview holt sich Daten-Nodes aus seiner gebundenen ItemsSource, und für jeden Daten-Node schaut er in seine Resourcen. Hat er einen DirectoryNode erwischt, wendet er das HierarchicalDataTemplate an, um das gebundene TreeviewItem zu erstellen.
Nämlich weil die Typ-Angabe dieses HierarchicalDataTemplate so schön auf DirectoryNode matcht.
Hat er einen FileSystemNode geholt, so nimmterhalt das annere DataTemplate, welches - als wäre es Absicht! - auf FileSystemNode matcht.
Und weil das DirectoryNode-Datatemplate halt hierarchical ist, schauter beim Expandieren des DirectoryNode-TreeviewItems auch in dessen Itemssource, sodaß - rekursiv - alle Daten-Nodes angezeigt werden.
Zusammenfassung
So, das war jetzt ein ziemlicher Crashkurs, odr?
- Viewmodel: Zunächst wurde gezeigt, wie mit den Klassen
Mainmodel
,DirectoryTree
,DirectoryNode
undFileSystemNode
ein Gesamt-Modell entsteht, was überhaupt geeignet ist, das Datei-System einer Festplatte auch abzubilden. In diesem Modell kommen keinerlei Controls vor - aber es gibt Public Properties, die entscheidend sind. - DataContext: Xaml bindet nun an diese Public Properties an. Dabei kommt das DataContext-Konzept zum Tragen, also einen DataContext zu setzen/binden macht das gesetzte Viewmodel für alle eingeschachtelten Xaml-Elemente zugreifbar.
Wohlgemerkt nur für die eingeschachtelten, und er wirkt auch nur dann in sie hinein, solange lokal kein abweichender DataContext gesetzt ist.
So kann man sehr differenziert bestimmen, was wo wie gebunden werden kann.
(Übrigens kann auch anders gebunden werden, und nicht nur an Viewmodels oder DataContexte. Aber der DataContext-Mechanismus ist wegen seines Vererbungs-Konzepts besonders hervorzuheben) - Templates: Auch gezeigt wurden DataTemplates, die mit ihrer DataType-Angabe sowohl ihren lokalen DataContext definieren, aber gleichzeitig auch, auf welche DatenElemente sie angewendet werden.
Templating ist ein fundamentaler Ansatz: Ich erzeuge keine Treenodes und fülle sie in den Treeview, sondern ich binde den Treeview an Daten, und stelle passende Templates bereit - befüllen tut der Treeview dann sich selbst, und ist dabei clever genug, die Elemente auch jeweisl mit dem passenden Template zu präsentieren.
(Übrigens sind DataTemplates nicht die einzige Form von Templating in Wpf: Styles und ControlTemplates setzen ebenfallsTemplating-Konzepte um, aber mit anderen Eigenheiten.)
Dieser Beitrag wurde bereits 6 mal editiert, zuletzt von „ErfinderDesRades“ ()