Background
The controls that inherit from ItemsControl (such as ListBox, ListView, and so on) expose an ItemTemplateSelector property that you can use instead of the ItemTemplate property to specify a DataTemplateSelector. The DataTemplateSelector enables you to specify your own custom logic that determines which DataTemplate should be applied to each item.
There are a number of blog posts out there already that give good instructions on how to implement a DataTemplateSelector, typically for tasks such as selected item highlighting or alternate background colours for list boxes, so I won't go into the details there. The Queen of Data Binding: Bea Costa, has a great post on the subject: How do I display items in an ItemsControl using different templates?
The Problem
However, there seem to be very few resources (none that I could find) that show you how to implement a custom view mode for the ListView control that implements that same template selection functionality, which brings me to the purpose of this post.
In Titan, the main files and folders view is created by using a ListView control that uses a custom view mode (like GridView, but this one does a Windows Explorer style tile view). The problem I had was that I wanted to always use the same icon for a folder, but to use a different icon for a file depending on its file extension. I managed to achieve this initially by using triggers, but then I realised that I wanted to provide different commands for files and folders.
The available commands are bound onto the ContextMenu for each item and the triggers approach just wasn’t working for trying to change the context menu, so I came to the realisation that what I actually wanted was a separate DataTemplate for files and folders. The problem was that because I was using a custom view mode, I couldn’t use the ItemTemplateSelector on the ListView itself. Looking at the implementation of the GridView I realised that I could do what I wanted by implementing template selection on my custom view mode, which is what the GridView does by using the ColumnHeaderTemplateSelector.
The Solution
1. Implement a Custom View Mode
To implement a custom view mode, you create a class that inherits from the ViewBase class. For more information on custom view modes, see How to: Create a Custom View Mode for a ListView on the MSDN web site.
There are three important additional steps to take to enable template selection for your custom view mode:
- Add a new dependency property to expose the DataTemplateSelector.
- Change the dependency property definition for the existing
ItemTemplate
dependency property to use a PropertyChangedCallback that checks to see if both the ItemTemplate
and ItemTemplateSelector
have been specified. - Override the PrepareItem method. This is called for each item in advance of its display, which gives you the opportunity to call SelectTemplate on the supplied DataTemplateSelector.
The following code example shows the relevant parts of my custom view mode:
using System.Diagnostics;
using System.Windows;
using System.Windows.Controls;
namespace DerekLakin.Libraries.Controls
{
public class TileView : ViewBase
{
#region Dependency Properties
...
public static readonly DependencyProperty ItemTemplateProperty =
DependencyProperty.Register("ItemTemplate",
typeof(DataTemplate), typeof(TileView),
new FrameworkPropertyMetadata(
new PropertyChangedCallback(TileView.OnItemTemplateChanged)));
public DataTemplate ItemTemplate
{
get { return (DataTemplate)GetValue(ItemTemplateProperty); }
set { SetValue(ItemTemplateProperty, value); }
}
public static readonly DependencyProperty ItemTemplateSelectorProperty =
DependencyProperty.Register("ItemTemplateSelector",
typeof(DataTemplateSelector), typeof(TileView),
new FrameworkPropertyMetadata(
new PropertyChangedCallback
(TileView.OnItemTemplateSelectorChanged)));
public DataTemplateSelector ItemTemplateSelector
{
get { return (DataTemplateSelector)GetValue(ItemTemplateSelectorProperty); }
set { SetValue(ItemTemplateSelectorProperty, value); }
}
public static readonly DependencyProperty ItemWidthProperty =
WrapPanel.ItemWidthProperty.AddOwner(typeof(TileView));
public double ItemWidth
{
get { return (double)GetValue(ItemWidthProperty); }
set { SetValue(ItemWidthProperty, value); }
}
#endregion
#region Overrides
...
protected override void PrepareItem(ListViewItem item)
{
base.PrepareItem(item);
object selector = this.ReadLocalValue(TileView.ItemTemplateSelectorProperty);
object template = this.ReadLocalValue(TileView.ItemTemplateProperty);
if (((null != selector) &&
(DependencyProperty.UnsetValue != selector)) &&
((null != template) &&
(DependencyProperty.UnsetValue != template)))
{
item.ContentTemplate = this.ItemTemplate;
}
else if ((null != selector) &&
(DependencyProperty.UnsetValue != selector))
{
item.ContentTemplate =
this.ItemTemplateSelector.SelectTemplate(item, this);
}
else
{
item.ContentTemplate = this.ItemTemplate;
}
}
#endregion
#region Property Changed Handlers
private static void OnItemTemplateChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
TileView view = (TileView)d;
object selector = d.ReadLocalValue(TileView.ItemTemplateSelectorProperty);
if ((null != selector) &&
(DependencyProperty.UnsetValue != selector))
{
Trace.WriteLine("Cannot specify ItemTemplate and
ItemTemplateSelector for a TileView. Using ItemTemplate.");
}
}
private static void OnItemTemplateSelectorChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
TileView view = (TileView)d;
object template = d.ReadLocalValue(TileView.ItemTemplateProperty);
if ((null != template) &&
(DependencyProperty.UnsetValue != template))
{
Trace.WriteLine("Cannot specify ItemTemplate and
ItemTemplateSelector for a TileView. Using ItemTemplate.");
}
}
#endregion
}
}
2. Implement the DataTemplateSelector
To implement your DataTemplateSelector, you need to provide properties for the templates that the user can choose from. In my case, there are two options: the FileDataTemplate
and the FolderDataTemplate
. You also need to override the SelectTemplate method to provide the logic that determines which template to return.
The following code example shows my DataTemplateSelector:
using System.Windows;
using System.Windows.Controls;
using DerekLakin.Libraries.Synchronization;
namespace DerekLakin.Applications.Titan.Controls
{
public class FilesAndFoldersDataTemplateSelector: DataTemplateSelector
{
public DataTemplate FileDataTemplate { get; set; }
public DataTemplate FolderDataTemplate { get; set; }
public override DataTemplate SelectTemplate(object item,
DependencyObject container)
{
ListViewItem targetItem = (ListViewItem)item;
if (targetItem.DataContext is VirtualFile)
{
return FileDataTemplate;
}
else
{
return FolderDataTemplate;
}
}
}
}
3. Bringing it All Together
To bring it all together, you need to create data templates for the selector to choose from. Then you need to create an instance of the DataTemplateSelector. Finally, you need to specify the template selector on the custom view mode (I'm assuming you already have an instance of the custom view mode and the ListView that uses it).
The following XAML code example shows the relevant parts:
<DataTemplate x:Key="filesTemplate">
...
</DataTemplate>
<DataTemplate x:Key="foldersTemplate">
...
</DataTemplate>
<c:FilesAndFoldersDataTemplateSelector x:Key="filesAndFoldersTemplateSelector"
FileDataTemplate=
"{StaticResource filesTemplate}"
FolderDataTemplate=
"{StaticResource foldersTemplate}" />
<controls:TileView x:Key="tileView"
ItemTemplateSelector=
"{StaticResource filesAndFoldersTemplateSelector}"
ItemWidth="128" />
I'm planning (eventually) to evolve my TileView
into something that enables the user to change the size of the tiles in the same way that Windows Explorer does, although it’s a way off for now. When I do, though, I'll release it into the wild for everyone to enjoy and it will, of course, support template selection.