Contents
I am still getting to grips with WPF, and last night, as part of a larger article that I am still working on, I wanted to create a simple (basic version) of an explorer tree, which shows drives and folders. I wanted to display a drive image if the TreeViewItem
is a drive, and a folder image otherwise. Sounds easy right. Wrong, it turned out to be quite tricky, well at least it was for me. So I thought that as the big article where this technique is used is still being written, I would break out the tree view implementation into a smaller article (this one). I think it's probably going to be a fairly common requirement to display different images for the current TreeViewItem
based on some condition. So that's what this article is all about.
The finished product looks like this:
Really simple, isn't it.
So How did I Get the WPF TreeView to do that
The first step is to get it to display the correct tree, which is really down to the following two methods.
private void Window_Loaded(object sender, RoutedEventArgs e)
{
foreach (string s in Directory.GetLogicalDrives())
{
TreeViewItem item = new TreeViewItem();
item.Header = s;
item.Tag = s;
item.FontWeight = FontWeights.Normal;
item.Items.Add(dummyNode);
item.Expanded += new RoutedEventHandler(folder_Expanded);
foldersItem.Items.Add(item);
}
}
void folder_Expanded(object sender, RoutedEventArgs e)
{
TreeViewItem item = (TreeViewItem)sender;
if (item.Items.Count == 1 && item.Items[0] == dummyNode)
{
item.Items.Clear();
try
{
foreach (string s in Directory.GetDirectories(item.Tag.ToString()))
{
TreeViewItem subitem = new TreeViewItem();
subitem.Header = s.Substring(s.LastIndexOf("\\") + 1);
subitem.Tag = s;
subitem.FontWeight = FontWeights.Normal;
subitem.Items.Add(dummyNode);
subitem.Expanded += new RoutedEventHandler(folder_Expanded);
item.Items.Add(subitem);
}
}
catch (Exception) { }
}
}
That's enough to get us the drive/folder hierarchy for the TreeView
. Next step, I wanted images for the individual TreeViewItem
s.
By default, the WPF TreeView
control does NOT display images, for example the image below shows what the WPF control looks like out of the box (Note I am using Vista, so it may look slightly different on XP)
This isn't what I wanted. So I started to look around to see if there was an Image
property or something like that on the TreeViewItem
, and guess what, there isn't. But of course WPF lets us change the look and feel of controls using Styles/Templates. So that's a good place to start, to maybe develop a Style/Template. Note: I would recommend not using Expression Blend for this task, as it creates about 200 lines of XAML the minute you decide to start editing the WPF TreeView
control using Expression Blend, and that's before you've even changed it. So it will likely be more. Don't get me wrong. Expression Blend is handy but for some things like Style/Template editing VS2005/VS2008 and hard crafted code are the way to go, you get much less code to do the job.
Ok rant over, so we need to create some sort of Style for the WPF TreeView
control, so I started going down that road and ended up with the following:
<TreeView x:Name="foldersItem"
SelectedItemChanged="foldersItem_SelectedItemChanged"
Width="Auto" Background="#FFFFFFFF"
BorderBrush="#FFFFFFFF"
Foreground="#FFFFFFFF">
<TreeView.Resources>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="HeaderTemplate">
<Setter.Value>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Image Name="img"
Width="20"
Height="20"
Stretch="Fill"
Source="Images/diskdrive.png"/>
<TextBlock Text="{Binding}" Margin="5,0" />
</StackPanel>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
</TreeView.Resources>
</TreeView>
This style ends up with the following, where we now have some images against our TreeViewItem
, which is cool. We're getting there. But all the images are the same. But that's because this style is using a fixed path for all the Image Source
properties. So it's bound not to work. Grrr. Maybe there's something more that can be done in the style. As it happens that's exactly what is done. Let's see.
I'll just include the part of the Style that is different from that shown above.
<Image Name="img" Width="20" Height="20" Stretch="Fill"
Source="{Binding RelativeSource={RelativeSource Mode=FindAncestor,
AncestorType={x:Type TreeViewItem}},
Path=Header,
Converter={x:Static local:HeaderToImageConverter.Instance}}"
/>
Now I have to say this is probably the most complicated bit of binding code that I've ever written in XAML. So what it does then, eh?
Well basically it sets the Image Source
property to be bound to the TreeViewItem
s Header
property (The Header
property, is the one that holds the text shown on the rendered TreeView
control, so it would hold strings like c:\\
, Program Files
, Windows
etc. etc.
But what use is that, these c:\\
, Program Files
, Windows
string values aren't Image Source
Uri
's, are they. They aren't even close, an Image Source
Uri
, should be something like C:\Windows\Web\Azul.jpg or something shouldn't it?
Well yeah they should actually. But WPF Databinding has one last trick up its very long and vacuumous (is that a word, it should be, I reckon) sleeve, Value Converters. Value Converters allow us to create a class that will use the original DataBound value and return a different object that will be used as the final binding value.
This is the trick that I use to get the image source to point to the correct location. Basically in the Image Source
binding shown above, I also specify a converter called HeaderToImageConverter
which I use to check whether the actual TreeViewItem
s Header
property contains a \
character. And if it does, I consider that TreeViewItem
to be a diskdrive, so I return a diskdrive Image Source
Uri
, otherwise I return a folder Image Source
Uri
. This may become clearer once you see the actual converter.
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
using System.Windows.Media.Imaging;
namespace WPF_Explorer_Tree
{
#region HeaderToImageConverter
[ValueConversion(typeof(string), typeof(bool))]
public class HeaderToImageConverter : IValueConverter
{
public static HeaderToImageConverter Instance =
new HeaderToImageConverter();
public object Convert(object value, Type targetType,
object parameter, CultureInfo culture)
{
if ((value as string).Contains(@"\"))
{
Uri uri = new Uri
("pack://application:,,,/Images/diskdrive.png");
BitmapImage source = new BitmapImage(uri);
return source;
}
else
{
Uri uri = new Uri("pack://application:,,,/Images/folder.png");
BitmapImage source = new BitmapImage(uri);
return source;
}
}
public object ConvertBack(object value, Type targetType,
object parameter, CultureInfo culture)
{
throw new NotSupportedException("Cannot convert back");
}
}
#endregion
}
So it can be seen that the HeaderToImageConverter
accepts a string and returns an object. Basically the value parameter coming in is the TreeViewItem
s Header
property, which we specified in the original binding. We don't care about the other parameters, but the Convert
and ConvertBack
method signature are dictated by the IValueConverter
interface, so we must have the correct method signature, despite which parameters we actually end up using.
Anyway so the value parameter = the TreeViewItem
s Header
property, that's all we care about right now. The next step was to see if this value (the TreeViewItem
s Header
) contains a \
character, and if it does return the diskdrive Image Source
Uri
, otherwise I return a folder Image Source
Uri
. So that's pretty easy now, we just do a little string test, and then create the appropriate Uri
. The only tricky bit here is that because the images are actually set to have a build action in Visual Studio of "Resource", we need to get the image path out of the application.
"Where does that awful tripple comma syntax come from?
"pack://application:,,,/Images/diskdrive.png"
What the heck does that mean.
The pack URI format is part of the XML Paper Specification (XPS), which can be found at http://www.microsoft.com/whdc/xps/default.mspx
The specified format is pack://packageURI/partPath
The packageURI is actually a URI within a URI, so its encoded by converting its forward slashes into commas. This packageURI could point to an XPS document, such as file:///C:/Document.xps encoded as file:,,,c:,Documenr.xps, Or, in WPF programs it can be one of two URIs treated specially by the platform
- siteOfOrigin:/// (encoded as siteOfOrigin:,,,)
- application:/// (encoded as application:,,,)
Therefore, the tripple commas are actually encoded forward slashes bit place holders for original parameters. (Note that these can also be specified with two slashes/commas rather than three).
The application:/// package is implicitly used by all the resource references that don't use siteOfOrigin. In other words, the following URI specified in XAML:
logo.jpg
is really just shorthand notation for
pack://application:,,,/logo.jpg
and this URI
MyDll;Component/logo.jpg
is shorthand notation for:
pack://application:,,,/MyDll;Component/logo.png
You could use these longer and more explicit URIs in XAML, but there's no good reason to."
Windows Presentation Foundation Unleashed. Adam Nathan, Sams. 2007
I hope this helps anyone that wants to create a better fully functional, explorer tree in WPF. This one satisfied my requirements.
I would just like to ask, if you liked the article please vote for it, and leave some comments, as it lets me know if the article was at the right level or not, and whether it contained what people need to know.
- v1.0 09/11/07: Initial issue