|
|||||||||||||||||||||
|
|||||||||||||||||||||
|
Announcements
Want a new Job?
Chapters
Services
Feature Zones
|
IntroductionThis 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:
BackgroundVery 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 The Demo AppAt 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
Now each person’s age appears in parentheses next to his/her name. Moving the
At this point, we can see each person’s name, age, and gender. The
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 EnvelopeA 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 ImplementationThere is a Implementing this entire feature should require just one binding. Sure, we could easily throw some code into the How It WorksI created a simple 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 /// <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 ObjectsAt this point, if we were to run the program, the <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 UI Controls and ResourcesNext, we will look at the XAML for the controls seen in the <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 <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 The task of transforming a Converting a Double to a DisplayDetailLevelHere is the value converter responsible for translating a [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 ValueConverterThe 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 /// <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 <!--
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
Revision History
| ||||||||||||||||||||