Introduction
This article examines a small WPF program that allows the user to choose the amount of detail to view about each data item in a list. The concept behind this feature is very similar to the “Views” feature in Windows Explorer on Vista, as seen below:
Background
Very often, a user interface provides too little or too much information for a particular user’s needs. It is impossible for developers and designers to create the perfect UI for all users of an application, especially if it has many users. The next best option is to allow the user to decide what information he/she wants to view. Applications often provide this by allowing the user to select which fields to view in a data grid, or having an ‘Advanced’ screen in an Options dialog, etc. This article shows how to implement this feature by allowing the user to adjust a Slider
control to decide how much information to view about each item in a list.
The Demo App
At the top of this page, you can download the sample program that accompanies the article. When you run that application, and decrease its height, it looks like this:
The UI essentially consists of an ItemsControl
and a Slider
control. The ItemsControl
displays a collection of Person
objects, and the Slider
determines what level of detail displays about each person. If you were to move Slider
a bit to the right, the UI would then look like this:
Now each person’s age appears in parentheses next to his/her name. Moving the Slider
even further to the right would result in a UI like this:
At this point, we can see each person’s name, age, and gender. The Slider
still has more room to move, so let’s see what happens if we slide it all the way to the right and resize the Window
a bit.
The UI changes considerably when the program is displaying the highest level of detail. The same display text appears for each person, but we now see his or her photo and either a blue or pink background color, based on the person’s gender.
Pushing the WPF Envelope
A WPF programming problem like this has many solutions, each with its relative merits. It turns out that the approach I consider best is not something that WPF supports very well at all. I believe that I have found a border case scenario that WPF should support, but it instead introduces an artificial limitation that required a workaround. I have been working with WPF long enough to know the “WPF way” of doing things, and in this situation, I believe WPF does not have proper support for the WPF way of solving this problem. Perhaps I just need to get out more…
The Ideal Implementation
There is a Slider
and an ItemsControl
. The ItemsControl
displays information about people, and the Slider
’s value determines how much information displays for each person. Each available level of detail requires that a certain DataTemplate
render the Person
objects. We must apply the appropriate DataTemplate
to the ItemTemplate
property of the ItemsControl
, based on the value of the Slider
. There is a direct relationship between the ItemsControl
’s ItemTemplate
property and the Slider
’s Value
property. Therefore, those two properties should be bound to each other, via WPF data binding. As we will see later, WPF does not make it easy to implement this approach because there is no supported way for a value converter to perform a resource lookup.
Implementing this entire feature should require just one binding. Sure, we could easily throw some code into the Window
’s code-behind file, handle the ValueChanged
event of the Slider
, figure out which template to use by looking at the Slider
’s Value
, and then assign the correct DataTemplate
to the ItemsControl
. To me, that is a terrible option. That is treating the Window
’s code-behind as a dumping ground for code that does not belong there. By that line of reasoning, we should also put an application’s data access logic, business logic, and grandma’s kitchen sink in there as well! We certainly can, and will, do better than that.
How It Works
I created a simple Person
class and, in the Window
’s constructor, assigned an array of them to the ItemsControl
’s ItemsSource
property. That code is below:
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
_personList.ItemsSource = new Person[]
{
new Person("Buster Wankstonian", 101, 'M', "buster.jpg"),
new Person("Pam van Slammenfield", 18, 'F', "pam.jpg"),
new Person("Peter Bonklemeister", 42, 'M', "peter.jpg"),
new Person("Sarah Pilgrimissimo", 26, 'F', "sarah.jpg"),
new Person("Teresa McPuppy", 72, 'F', "teresa.jpg"),
new Person("Zorkon McMuffin", 30, 'M', "zorkon.jpg"),
};
}
}
We know that the application has a notion of “detail levels”, so it makes sense to create an enumeration that represents those various levels. The DisplayDetailLevel
enumeration is below:
public enum DisplayDetailLevel
{
Low = 1,
Medium = 2,
High = 3,
VeryHigh = 4
}
DataTemplates for Person Objects
At this point, if we were to run the program, the ItemsControl
would display each Person
object by simply displaying its fully qualified type name. In order to display each Person
object in a meaningful way, we need to create a DataTemplate
with which the ItemsControl
can render each Person
. However, since the rendering of a Person
changes based on the level of detail to show, we need one template for each detail level. I created four DataTemplates
and put them into the PersonDataTemplates.xaml file, which is a ResourceDictionary
that is loaded into the resource hierarchy at runtime. That ResourceDictionary
is below:
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:AdjustableDetailLevelDemo"
>
-->
<DataTemplate x:Key="{x:Static local:DisplayDetailLevel.Low}">
<TextBlock Text="{Binding Path=Name}" />
</DataTemplate>
-->
<DataTemplate x:Key="{x:Static local:DisplayDetailLevel.Medium}">
<TextBlock>
<TextBlock Text="{Binding Path=Name}" />
<Run>(</Run>
<TextBlock Text="{Binding Path=Age}" Margin="-4,0" />
<Run>)</Run>
</TextBlock>
</DataTemplate>
-->
<DataTemplate x:Key="{x:Static local:DisplayDetailLevel.High}">
<TextBlock>
<TextBlock Text="{Binding Path=Name}" />
<Run>(</Run>
<TextBlock Text="{Binding Path=Age}" Margin="-4,0" />
<Run>) -</Run>
<TextBlock Text="{Binding Path=Gender}" />
</TextBlock>
</DataTemplate>
-->
<DataTemplate x:Key="{x:Static local:DisplayDetailLevel.VeryHigh}">
<Border
x:Name="bd"
Background="LightBlue"
BorderBrush="Gray"
BorderThickness="1"
CornerRadius="6"
Margin="2,3"
Padding="4"
Width="300" Height="250"
>
<DockPanel>
-->
<ContentControl
DockPanel.Dock="Top"
Content="{Binding Path=.}"
ContentTemplate="{StaticResource {x:Static local:DisplayDetailLevel.High}}"
/>
<Image Width="250" Height="200" Source="{Binding Path=PhotoUri}" />
</DockPanel>
</Border>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding Path=Gender}" Value="F">
<Setter TargetName="bd" Property="Background" Value="Pink" />
</DataTrigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="BitmapEffect">
<Setter.Value>
<DropShadowBitmapEffect />
</Setter.Value>
</Setter>
</Trigger>
</DataTemplate.Triggers>
</DataTemplate>
</ResourceDictionary>
There are two important things to notice about the above XAML. Each DataTemplate
’s x:Key
is a value from the DisplayDetailLevel
enumeration we saw earlier. That fact will come into play later, when we see how the DataTemplate
s are located and applied. The other point of interest is how the ‘VeryHigh’ template makes use of the ‘High’ template to ensure that each has identical display text. This trick is accomplished by putting a ContentControl
into the DataTemplate
, assigning its ContentTemplate
to the ‘High’ template, and binding its Content
property to the containing template's inherited DataContext
(via the “{Binding Path=.}”
notation). This technique prevents us from having to duplicate the ‘High’ template declaration in the ‘VeryHigh’ template, which is great for maintenance and readability.
UI Controls and Resources
Next, we will look at the XAML for the controls seen in the Window
. For the time being, I have omitted the Resources
of the DockPanel
, just so that we can focus on the controls. Later we will see the resources, too.
<DockPanel>
<StackPanel
DockPanel.Dock="Bottom"
Background="LightGray"
Margin="4"
Orientation="Horizontal"
>
<TextBlock
Margin="2,0,4,0"
Text="Detail Level:"
VerticalAlignment="Center"
/>
<Slider
x:Name="_detailLevelSlider"
DockPanel.Dock="Bottom"
Minimum="1" Maximum="4"
SmallChange="1" LargeChange="1"
Value="0"
Width="120"
/>
</StackPanel>
<ScrollViewer>
<ItemsControl
x:Name="_personList"
ItemTemplate="{Binding
ElementName=_detailLevelSlider,
Path=Value,
Converter={StaticResource DetailLevelConv}}"
/>
</ScrollViewer>
</DockPanel>
The most important thing to observe is how the ItemsControl
has its ItemTemplate
property bound to the Value
property of the Slider
. That binding accurately expresses the relationship between those two controls, but, naturally, it makes no sense to directly bind a property of type Double
to a property of type DataTemplate
. That is why the binding’s Converter
property references a value converter, whose resource key is DetailLevelConv
. Now we will check out the resources used in this UI.
<DockPanel.Resources>
<ResourceDictionary>
-->
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="PersonDataTemplates.xaml" />
</ResourceDictionary.MergedDictionaries>
-->
<local:ResourceKeyToResourceConverter x:Key="ResourceConv" />
-->
<local:ValueConverterGroup x:Key="DetailLevelConv">
<local:DoubleToDisplayDetailLevelConverter />
<StaticResourceExtension ResourceKey="ResourceConv" />
</local:ValueConverterGroup>
</ResourceDictionary>
</DockPanel.Resources>
The merged dictionary is importing all four DataTemplate
s that render Person
objects. We already saw those previously. After that, there are three value converters. ValueConverterGroup
is a class I made and wrote an article about back in August 2006. It chains together any number of IValueConverter
objects, treating the output of one converter as input of the next converter. I use that tool here to combine two new value converters together.
The task of transforming a Slider
’s Value
into a DataTemplate
can be seen as two separate steps. First, we must translate a Double
into a value from the DisplayDetailLevel
enumeration. Then we must perform a resource lookup to find the correct DataTemplate
, by using the DisplayDetailLevel
value as the resource key. This is why I set each DataTemplate
’s x:Key
to a value of the DisplayDetailLevel
enumeration; so that we can easily locate the template via the current detail level setting.
Converting a Double to a DisplayDetailLevel
Here is the value converter responsible for translating a Double
to a DisplayDetailLevel
value:
[ValueConversion(typeof(Double), typeof(DisplayDetailLevel))]
public class DoubleToDisplayDetailLevelConverter : IValueConverter
{
public object Convert(
object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is double == false)
return Binding.DoNothing;
int num = System.Convert.ToInt32(value);
if (num < 1 || 4 < num)
return Binding.DoNothing;
return (DisplayDetailLevel)num;
}
public object ConvertBack(
object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotSupportedException("Cannot convert back");
}
}
Performing a Resource Lookup from a ValueConverter
The converter that performs the resource lookup was much trickier to write. It relies on some dirty hack magic to get the job done. If you have a policy that forbids you from relying on reflection to manipulate implementation details of the .NET framework, then you cannot use this class in your applications. This class relies on the Hillberg Freezable Trick and some homegrown reflection madness to coerce WPF into allowing a value converter to perform a resource lookup. Here is my ResourceKeytoResourceConverter
class:
[ValueConversion(typeof(object), typeof(object))]
public class ResourceKeyToResourceConverter
: Freezable,
IValueConverter
{
static readonly DependencyProperty DummyProperty =
DependencyProperty.Register(
"Dummy",
typeof(object),
typeof(ResourceKeyToResourceConverter));
public object Convert(
object value, Type targetType, object parameter, CultureInfo culture)
{
return this.FindResource(value);
}
public object ConvertBack(
object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotSupportedException("Cannot convert back");
}
object FindResource(object resourceKey)
{
var resourceReferenceExpression =
new DynamicResourceExtension(resourceKey).ProvideValue(null)
as Expression;
MethodInfo getValue = typeof(Expression).GetMethod(
"GetValue",
BindingFlags.Instance | BindingFlags.NonPublic);
object result = getValue.Invoke(
resourceReferenceExpression,
new object[] { this, DummyProperty });
if (result == DependencyProperty.UnsetValue)
return null;
var deferredResourceReference = result;
Setter setter = new Setter(DummyProperty, deferredResourceReference);
return setter.Value;
}
protected override Freezable CreateInstanceCore()
{
throw new NotImplementedException();
}
}
It is important to note that for ResourceKeyToResourceConverter
to work properly, you must add it directly to the Resources
of an element. You cannot add it to a separate ResourceDictionary
that is assigned to an element’s Resources
or merged into an existing ResourceDictionary
. The reason for this is that it relies on undocumented internal behavior of WPF to ensure the converter receives an inheritance context, which only happens properly when the converter is added directly to an element’s Resources
. This also explains why I had to add that converter to the ValueConverterGroup
via a StaticResourceExtension
, instead of adding it inline. Here is that XAML again:
<!---->
<local:ResourceKeyToResourceConverter x:Key="ResourceConv" />
<!---->
<local:ValueConverterGroup x:Key="DetailLevelConv">
<local:DoubleToDisplayDetailLevelConverter />
<StaticResourceExtension ResourceKey="ResourceConv" />
</local:ValueConverterGroup>
Cast and Crew
Revision History
- July 5, 2008 – Published my fiftieth article on The Code Project!