Building a folder picker in WPF
The last time I blogged about TreeView, I was still using the control I personally implemented. Now that WPF actually has a TreeView, lets talk about how to use it.
The first thing you’ll notice is that the API for what we shipped is pretty identical to what I blogged. (Guess who wrote the spec for WPF TreeView.) Thankfully I had a brilliant developer that implemented the tough stuff like selection and keyboard navigation.
You may notice that TreeView seems to be missing things you’ve grown to expect: explicit support for checkboxes and images. This was not an oversight. We felt that it was actually limited to provide these features explicitly when it’s so easy to add them directly. It’s the same reason Button in WPF doesn’t have Text, Image, TextAlign, ImageAlign properties. Button has a Content property that can be anything. Put in your own Panel with your own elements. We don’t need to hardwire them for you.
So enough background. What are we building?
Three things to look at:
1) Data.cs
You’ll notice a couple of classes: LocalDrives and SelectableDirectory. Both implement INotifyPropertyChanged. Both expose a bunch of properties. If you dig into SelectableDirectory, you’ll notice a bunch of wiring to handle and propagate changes to IsSelected. If a nested directory becomes selected the change bubbles up the directory hierarchy through recursive listeners to PropertyChanged events. These allow each level of the tree to maintain the ChildSelection property for SelectableDirectory and SelectedDirectories collection for SelectableDirectory and LocalDrives.
Important thing here: These classes are purely data. There are no references to UI concepts. Just logic to instantiate data structures against the file system and properties and events to the data structures in sync.
2) Window1.xaml
A couple things here. First, the ObjectDataProvider in the resources section. This maps to the LocalDrives object we defined in Data.cs. Second, take a look at the UI. A TreeView and a ListBox in a Grid (which is in a ViewBox). The Grid has a DataContext defined that points to the ObjectDataProvider. The TreeView is bound to the Drives property. The ListBox is bound to the SelectedDirectories property. (They both inherit the LocalDrives DataContext defined in Grid.)
Notice that both Drives and SelectedDirectories are collections of SelectableDirectory. By default, both TreeView and ListBox would just display these items by wrapping them in a TextBlock and with the result of ToString() as the Text. Instead, you see that both define an ItemTemplate. Where are these ItemTemplates defined? Take a look at MyApp.xaml.
3) MyApp.xaml
There are a bunch of entries in the Resources section of MyApp.xaml. First, two DataTemplates.
The first of the DataTemplates is for the ListBox and it’s pretty straight forward. It’s defined for SelectableDirectory and simply contains a TextBlock that binds to the Path property of SelectableDirectory. No rocket science here.
The second of the DataTemplates is for the TreeView. This is a special DataTemplate--a HierarchialDataTemplate. I first discussed HDT in May. I’m doing pretty much the same thing here, but this time I get a bit more sophisticated. Besides having a TextBlock bound to the Name property, I also create a CheckBox is bound to the IsSelectedProperty. The rest of the magic happens in the Triggers Section. I set up Triggers to change the FontStyle and FontWeight of the TextBlock to reflect the ChildSelection of each directory. I also use a Trigger to swap out the Template used to draw the icon Control.
Why is this all cool?
First, completely separated business logic and UI. My code handles all of the state for my directories and drives without worrying about the UI implementation. You could have thrown the data layer over the fence as a DLL to a designer and she could have developed the UI.
Second, you don’t need checkbox or image support in TreeView. You want two checkboxes? Cool. You want three images? Fine. Want to change their orientation? No problem. Build what you want, use our controls, stay away from OnRender/OnPaint/OwnerDraw.
Sound good?