I've been eager to try working with WPF. Most of my application development experience has centered around web applications. Desktop apps have always seemed very difficult and time-consuming to develop anything advanced, and with so many limitations on the design. WPF seems to have adopted some of the best principles from web application development and hammered them in to work at the desktop level. All the new concepts introduced seem daunting at first until it all clicks. XAML brings in the HTML strengths; helping you keep your visual logic separate and easy to share with designers using other tools. Dependency properties help make the flexible XAML possible and eliminate more reasons for needing to write code to handle your UI. Styles are obvious - drill down to an element anywhere in the visual tree and change something about it and see the difference right away, again with no code. Routed events are also obvious, event bubbling is core to web development. With all these 'new' concepts, WPF has made windows development a lot more interesting.
My first 'learning' project is going to be doing the one thing you absolutely cannot and will never be able to do in web applications - the local folder tree. I know it's been done and I should probably name it 'Yet Another...' whatever, but I want to create one of my own that I can reuse in other projects and with the added feature allowing the intuitive selection of a set of paths (which may make this project at least a little unique).
I'm going to build this project iteratively, with a blog post following each iteration as my publicly shared notes on WPF development.
Goals for Part 1:
- Create a reusable control project.
- Follow the MVVM (Model-View-ViewModel) design pattern.
- Get the control up and running with a simple tree of drives and folders.
- Use lazy-loading for performance.
Getting Started
New Project.
WPF User Control Library. Wait, WPF Custom Control Library. Which one? The descripton displayed is useless. The default project name is either WpfCustomControlLibrary1 or WpfControlLibrary1. Am I making a custom control or a user control?
After this stumbling block not even out of the gate, I figured out that pretty much I want to make a user control. The custom control project template provides a class that extends directly from Control, and a XAML resource dictionary with a style definition. The user control project template gives you simply a XAML file that extends from UserControl. UserControl extends from Control and the real answer is buried in the MSDN docs. A user control is a new control that encapsulates existing controls and logic. A custom control is a custom UI element that defines it's own control template. All I really need is a TreeView with a lot of logic so user control it is.The User Control
Starting off with the simplest base functionality to get a structure in place (and since I'm new to all this). I'll follow the MVVM pattern:
Model
The model is really just the local storage devices in this case. I want to show a top level list of drives and then all the folders and subfolders for each drive, so my model will be System.IO.DriveInfo and System.IO.DirectoryInfo. I'm going to make my own classes to encapsulate the DriveInfo and DirectoryInfo instances, but the only reason for doing that is to store information for the view, so that's going to be part of the view model.
View
At this point, the view is going to be nothing more than a WPF TreeView. The TreeView is a an ItemsControl where each item is a TreeViewItem. Each TreeViewItem is a HeaderedItemsControl where the header is the tree node and the items are the sub-nodes that expand and collapse. Most of the time populating a tree view control involves implementing some sort of tree data model, providing a set of root nodes and then filling in the method that gets called to find the children nodes. There's really no other way to establish a tree of data, but WPF provides a more open approach. There is no expected model type or interface or anything, instead you use Hierarchical data templates. Hierarchical data templates serve two purposes to make this work. First, they extend the normal data template so they define the way the 'header' or the current node is going to be displayed. Second, they define their own ItemsSource property which defines where to find the collection of children from the context of the current node. Instead of being limited to one binding point for the whole tree, you can define the bindings for each level of the hierarchy.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | <UserControl x:Class="GeekJ.FolderTreeControl.FolderTree" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="300" Loaded="FolderTree_Loaded"> <Grid> <TreeView ItemsSource="{Binding Path=Drives}"> <TreeView.ItemTemplate> <HierarchicalDataTemplate ItemsSource="{Binding Path=Folders}"> <TextBlock Text="{Binding Path=Label}" /> </HierarchicalDataTemplate> </TreeView.ItemTemplate> </TreeView> </Grid> </UserControl> |
The TreeView binds to 'Drives' for the root level, and the ItemTemplate is the HierarchialDataTemplate, bound to 'Folders' for the levels below root and defining a basic TextBlock, bound to 'Label' to display each node of the tree. The recursive behavior of the HierarchialDataTemplate is built-in. The Drive objects provided by Drives will define a collection of Folders, and if each Folder also defines it's own collection of Folders, the template will automatically recurse.
These bindings just need something to map to. From the context of a user control I'm not sure if I'm supposed to expect the client application to provide the DataContext, but this UserControl inherits the DataContext property so I'm going to go ahead and define it here. I'll wire up the Loaded event to to set the DataContext of the control to my view model class.
View Model
The View Model will tie everything together. From the bindings I've already established, we're going to need a collection of Drives containing collections of Folders containing collections of Folders... all pulling from DriveInfo and DirectoryInfo.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | namespace GeekJ.FolderTreeControl.Model { public class Drive : FolderTreeItem { private DriveInfo _driveInfo; public DriveInfo DriveInfo { get { return _driveInfo; } set { _driveInfo = value; } } public Drive(DriveInfo driveInfo) { DriveInfo = driveInfo; } public string Label { get { return DriveInfo.Name; } } private ObservableCollection<Folder> _folders; public ObservableCollection<Folder> Folders { get { if (_folders == null) { _folders = new ObservableCollection<Folder>(); _folders.Add(new Folder()); } return _folders; } } public override void LoadChildren() { Folders.Clear(); foreach (var child in Folder.LoadSubFolders(DriveInfo.RootDirectory)) { Folders.Add(child); } } } } |
The FolderTreeViewModel (which was instantiated as the DataContext for the UserControl) uses DriveInfo.GetDrives() to populate the Drives collection, which is bound to the root tree nodes. Each Drive object encapsulates the DriveInfo instance and exposes a label and a collection of Folder objects. Each folder object encapsulates a DirectoryInfo instance and exposes its own label and a collection of subfolders.
Both Drive and Folder define LoadChildren() to populate the next level down, but nothing calls these yet. The only thing that's accessed is the Folders collection. If I were to have to the getter for Folders invoke LoadChildren(), it would work, but then the TreeView control is going to access all the nested Folders recursively up front, resulting in non-functional performance. The solution is to lazy-load the nested Folders.
Lazy-Loading the Subfolders
Lazy-Loading just means making sure the LoadChildren() isn't invoked until the parent node is expanded. The first requirement is to make sure that the parent nodes can be expanded in the first place. If the Folders collection is empty, there will be no expand handle on the node. To work around this, the Folders collection will be initialized with a single placeholder Folder object. When the node is expanded, a blank row will appear for a split second as LoadChildren() is called.
The next step is getting LoadChildren() to be called at the right time. There are two common approaches to this, use the TreeViewItem.Expanded event or bind to the IsExpanded property. Either way works just fine, I prefer to use the event. To me, the event says 'This node was just expanded, you can respond now', and a property binding says something more like 'Just marking you so you know you've been expanded'. I actually like to have both in my code - the event receiver to do the work and the property binding just so I have that tiny piece of information readily available on every node.
1 2 3 4 5 6 7 8 9 10 11 12 | <TreeView ItemsSource="{Binding Path=Drives}" TreeViewItem.Expanded="FolderTree_Expanded"> <TreeView.ItemContainerStyle> <Style TargetType="TreeViewItem"> <Setter Property="IsExpanded" Value="{Binding Path=IsExpanded, Mode=TwoWay}" /> </Style> </TreeView.ItemContainerStyle> <TreeView.ItemTemplate> <HierarchicalDataTemplate ItemsSource="{Binding Path=Folders}"> <TextBlock Text="{Binding Path=Label}" /> </HierarchicalDataTemplate> </TreeView.ItemTemplate> </TreeView> |
The TreeViewItem.Expanded event will trigger my FolderTree_Expanded method, and the Header property of the TreeViewItem that's the original source of the event is my actual Drive or Folder object (generalized as FolderTreeItem). Then I just call LoadChildren and it's done. Since the Folder collections are ObservableCollections, the UI will automatically update when the collection is populated.
Type-specific Data Templates
At this point the tree control is going to be fully functional, but I want to incorporate one more improvement before finishing off this part. I've got two types of objects in my tree right now: Drives and Folders. The hierarchy is pretty fixed, but what if I decide to get more complex and include other types like 'RemovableDrive' or 'Junction'. I would want different visual logic in those cases, so I'm going to refactor the data templates into resources keyed by type.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | <UserControl x:Class="GeekJ.FolderTreeControl.FolderTree" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:model="clr-namespace:GeekJ.FolderTreeControl.Model" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="300" Loaded="FolderTree_Loaded" DataContextChanged="FolderTree_DataContextChanged"> <UserControl.Resources> <HierarchicalDataTemplate DataType="{x:Type model:Drive}" ItemsSource="{Binding Path=Folders}"> <TextBlock Text="{Binding Path=Label}"/> </HierarchicalDataTemplate> <HierarchicalDataTemplate DataType="{x:Type model:Folder}" ItemsSource="{Binding Path=Folders}"> <TextBlock Text="{Binding Path=Label}"/> </HierarchicalDataTemplate> </UserControl.Resources> <Grid> <TreeView ItemsSource="{Binding Path=Drives}" TreeViewItem.Expanded="FolderTree_Expanded"> <TreeView.ItemContainerStyle> <Style TargetType="TreeViewItem"> <Setter Property="IsExpanded" Value="{Binding Path=IsExpanded, Mode=TwoWay}" /> </Style> </TreeView.ItemContainerStyle> </TreeView> </Grid> </UserControl> |
This refactoring is strictly in the XAML. I removed the whole TreeView.ItemTemplate chunk from the TreeView and added two HierarchicalDataTemplates to the resource dictionary. The TreeView still binds to the Drives collection, and when WPF encounters the Drive objects, it picks up the HierarchicalDataTemplate out of the resource dictionary and knows to use that for the item template. I really like this approach better because the templates are now cleanly defined all at the same level and each one defines how that type of item should be displayed and how to find it's children, no matter where the item is found in the hierarchy. Right now I'm not taking any advantage of that with the current simplicity of the control, but I wanted to get that in place up front.
Testing it Out
There's no way to launch a user control by itself, so I will add a Demo project to my solution with a simple window to display the control.
This part is beautifully simple. All I need to do as add a reference to my control project and the register a namespace for that reference in my window XAML and then a simple tag will instantiate my folder control in the window.
1 2 3 4 5 6 7 8 9 | <Window x:Class="Demo.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:ftc="clr-namespace:GeekJ.FolderTreeControl;assembly=GeekJ.FolderTreeControl" Title="MainWindow" Height="350" Width="525"> <Grid> <ftc:FolderTree /> </Grid> </Window> |
[[posterous-content:pid___0]]
All my code snippets are brought to you by GitHub Gist, but the whole project is also hosted on GitHub for you to download, follow or fork.
https://github.com/jasonw754/FolderTreeControl
Download the code just for part 1
Posted via email from Geek J
No comments:
Post a Comment