Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Selecting the Detail Level to View at Runtime in WPF

0.00/5 (No votes)
5 Jul 2008 1  
Explains how to allow users to select the amount of information to view

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:

vista_view_selector.png

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:

screenshot_low.png

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:

screenshot_medium.png

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:

screenshot_high.png

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.

screenshot_veryhigh.png

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:

/// <summary>
/// Represents various settings for how much information
/// should be displayed to the end-user.
/// </summary>
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"
  >
  <!-- LOW -->
  <DataTemplate x:Key="{x:Static local:DisplayDetailLevel.Low}">
    <TextBlock Text="{Binding Path=Name}" />
  </DataTemplate>

  <!-- MEDIUM -->
  <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>

  <!-- HIGH -->
  <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>

  <!-- VERY HIGH -->
  <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>
        <!-- Inject the 'High' template here for consistent display text. -->
        <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 DataTemplates 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>
    <!-- 
    Merge in the dictionary of DataTemplates. 
    -->
    <ResourceDictionary.MergedDictionaries>
      <ResourceDictionary Source="PersonDataTemplates.xaml" />
    </ResourceDictionary.MergedDictionaries>

    <!-- 
    This converter must be in an element's Resources collection
    for it to be a valid source of a resource lookup.
    -->
    <local:ResourceKeyToResourceConverter x:Key="ResourceConv" />

    <!-- 
    This converter group transforms a Double into a DataTemplate.
    -->
    <local:ValueConverterGroup x:Key="DetailLevelConv">
      <local:DoubleToDisplayDetailLevelConverter />
      <StaticResourceExtension ResourceKey="ResourceConv" />
    </local:ValueConverterGroup>
  </ResourceDictionary>
</DockPanel.Resources>

The merged dictionary is importing all four DataTemplates 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:

/// <summary>
/// A value converter that performs a resource lookup on the conversion value.
/// </summary>
[ValueConversion(typeof(object), typeof(object))]
public class ResourceKeyToResourceConverter
    : Freezable, // Enable this converter to be the source of a resource lookup.
    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)
    {
        // NOTE: This code depends on internal implementation details of WPF and 
        // might break in a future release of the platform.  Use at your own risk!

        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 });

        // Either we do not have an inheritance context or the  
        // requested resource does not exist, so return null.
        if (result == DependencyProperty.UnsetValue)
            return null;

        // The requested resource was found, so we will receive a 
        // DeferredResourceReference object as a result of calling 
        // GetValue.  The only way to resolve that to the actual 
        // resource, without using reflection, is to have a Setter's 
        // Value property unwrap it for us.
        var deferredResourceReference = result;
        Setter setter = new Setter(DummyProperty, deferredResourceReference);
        return setter.Value;
    }

    protected override Freezable CreateInstanceCore()
    {
        // We are required to override this abstract method.
        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:

<!-- 
This converter must be in an element's Resources collection
for it to be a valid source of a resource lookup.
-->
<local:ResourceKeyToResourceConverter x:Key="ResourceConv" />

<!-- 
This converter group transforms a Double into a DataTemplate.
-->
<local:ValueConverterGroup x:Key="DetailLevelConv">
  <local:DoubleToDisplayDetailLevelConverter />
  <StaticResourceExtension ResourceKey="ResourceConv" />
</local:ValueConverterGroup>

Cast and Crew

cast_and_crew.png

Revision History

  • July 5, 2008 – Published my fiftieth article on The Code Project!

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here