在 WPF 中选择运行时要查看的详细级别






4.78/5 (22投票s)
解释如何允许用户选择要查看的信息量。
引言
本文介绍了一个小型WPF程序,该程序允许用户选择要查看的每个数据项的详细信息量。此功能的背后概念与Vista中的Windows资源管理器中的“视图”功能非常相似,如下所示。

背景
通常,用户界面为特定用户的需求提供了过少或过多的信息。开发人员和设计师不可能为应用程序的所有用户创建完美的UI,特别是当应用程序有很多用户时。次优选择是允许用户决定他/她想查看哪些信息。应用程序通常通过允许用户选择要在数据网格中查看的字段,或在“选项”对话框中包含“高级”屏幕等方式来实现这一点。本文通过允许用户调整Slider
控件来决定要查看的每个项目的信息量,展示了如何实现此功能。
演示应用程序
在此页面的顶部,您可以下载随文章一起提供的示例程序。运行该应用程序并减小其高度时,它看起来像这样:

UI基本上由一个ItemsControl
和一个Slider
控件组成。ItemsControl
显示Person
对象的集合,而Slider
决定了有关每个人的详细信息的显示级别。如果您将Slider
稍微向右移动,UI将如下所示:

现在,每个人的年龄都显示在姓名的括号中。将Slider
进一步向右移动将产生如下UI:

此时,我们可以看到每个人的姓名、年龄和性别。Slider
仍有更多移动空间,所以让我们看看如果我们将其滑动到最右侧并稍微调整Window
的大小会发生什么。

当程序显示最高级别的详细信息时,UI会发生很大变化。每个人都显示相同的显示文本,但现在我们看到他的/她的照片和背景颜色,蓝色或粉红色,取决于性别。
挑战WPF的极限
像这样的WPF编程问题有很多解决方案,每种解决方案都有其相对优势。事实证明,我认为最好的方法并不是WPF非常支持的。我认为我找到了一个WPF应该支持的边界情况场景,但它反而引入了一个需要变通方法的限制。我已经足够熟悉WPF,知道“WPF方式”的做事方法,在这种情况下,我认为WPF并没有充分支持解决此问题的WPF方式。也许我只是需要多出去走走…
理想的实现
有一个Slider
和一个ItemsControl
。ItemsControl
显示有关人员的信息,而Slider
的值决定了为每个人显示的信息量。每个可用的详细信息级别都需要某个DataTemplate
来渲染Person
对象。我们必须根据Slider
的值,将适当的DataTemplate
应用于ItemsControl
的ItemTemplate
属性。ItemsControl
的ItemTemplate
属性与Slider
的Value
属性之间存在直接关系。因此,这两个属性应该通过WPF数据绑定进行绑定。正如我们稍后将看到的,WPF不便于实现这种方法,因为没有支持的方式可以让值转换器执行资源查找。
实现整个功能应该只需要一次绑定。当然,我们可以轻松地在Window
的代码隐藏文件中添加一些代码,处理Slider
的ValueChanged
事件,通过查看Slider
的Value
来确定要使用的模板,然后将正确的DataTemplate
分配给ItemsControl
。对我来说,这是一个糟糕的选择。这会将Window
的代码隐藏文件当作不属于那里的代码的存储库。按照这个逻辑,我们还应该将应用程序的数据访问逻辑、业务逻辑以及奶奶的厨房水槽也放入其中!我们当然可以,也将做得更好。
工作原理
我创建了一个简单的Person
类,并在Window
的构造函数中,将其数组分配给ItemsControl
的ItemsSource
属性。下面的代码显示了该代码。
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"),
};
}
}
我们知道应用程序具有“详细信息级别”的概念,因此创建一个表示这些不同级别的枚举是有意义的。下面的DisplayDetailLevel
枚举。
/// <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
}
Person对象的DataTemplates
此时,如果我们运行程序,ItemsControl
将通过简单地显示每个Person
对象的完全限定类型名称来显示它。为了有意义地显示每个Person
对象,我们需要创建一个DataTemplate
,ItemsControl
可以使用该模板来渲染每个Person
。但是,由于Person
的渲染会根据要显示的详细信息级别而变化,因此我们需要为每个详细信息级别一个模板。我创建了四个DataTemplates
并将它们放入*PersonDataTemplates.xaml*文件中,这是一个在运行时加载到资源层次结构中的ResourceDictionary
。下面的ResourceDictionary
。
<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>
关于上面的XAML有两点需要注意。每个DataTemplate
的x:Key
是我们之前看到的DisplayDetailLevel
枚举中的一个值。当我们看到DataTemplate
s如何被定位和应用时,这个事实将发挥作用。另一处有趣的地方是“VeryHigh”模板如何利用“High”模板来确保它们具有相同的显示文本。这个技巧是通过将一个ContentControl
放入DataTemplate
中,将其ContentTemplate
设置为“High”模板,并将其Content
属性绑定到包含模板的继承的DataContext
(通过“{Binding Path=.}”表示法)来实现的。这种技术使我们不必在“VeryHigh”模板中重复“High”模板声明,这对于维护和可读性都很有好处。
UI控件和资源
接下来,我们将查看Window
中控件的XAML。暂时,我省略了DockPanel
的Resources
,以便我们可以专注于控件。稍后我们将看到资源。
<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>
最重要的是要注意ItemsControl
的ItemTemplate
属性如何绑定到Slider
的Value
属性。该绑定准确地表达了这两个控件之间的关系,但,当然,直接将Double
类型的属性绑定到DataTemplate
类型的属性是没有意义的。这就是为什么绑定中的Converter
属性引用了一个值转换器,其资源键为DetailLevelConv
。现在我们将查看此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>
合并的字典正在导入渲染Person
对象的全部四个DataTemplate
s。我们之前已经看到过它们。之后,有三个值转换器。ValueConverterGroup
是我编写的一个类,我在2006年8月写了一篇关于它的文章。它将任意数量的IValueConverter
对象链接在一起,将一个转换器的输出视为下一个转换器的输入。我在这里使用该工具将两个新的值转换器组合在一起。
将Slider
的Value
转换为DataTemplate
的任务可以看作是两个独立的步骤。首先,我们必须将一个Double
转换为DisplayDetailLevel
枚举中的一个值。然后,我们必须执行资源查找,通过使用DisplayDetailLevel
值作为资源键来找到正确的DataTemplate
。这就是我将每个DataTemplate
的x:Key
设置为DisplayDetailLevel
枚举值的原因;以便我们可以通过当前的详细信息级别设置轻松找到模板。
将Double转换为DisplayDetailLevel
这是负责将Double
转换为DisplayDetailLevel
值的 the value converter。
[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");
}
}
从ValueConverter执行资源查找
执行资源查找的转换器编写起来要棘手得多。它依赖一些肮脏的技巧来完成工作。如果您有禁止您依赖反射来操作.NET框架实现细节的策略,那么您不能在应用程序中使用此类。此类依赖于Hillberg Freezable Trick和一些自制的反射混乱,以强迫WPF允许值转换器执行资源查找。这是我的ResourceKeytoResourceConverter
类。
/// <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();
}
}
需要注意的是,为了使ResourceKeyToResourceConverter
正常工作,您必须将其直接添加到元素的Resources
中。您不能将其添加到分配给元素Resources
的单独ResourceDictionary
中,也不能将其合并到现有的ResourceDictionary
中。原因是它依赖于WPF的未记录的内部行为,以确保转换器接收到继承上下文,这只有当转换器直接添加到元素的Resources
中时才会正确发生。这也解释了我为什么必须通过StaticResourceExtension
将该转换器添加到ValueConverterGroup
,而不是内联添加它。这是XAML的再次显示。
<!--
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>
演员和幕后

修订历史
- 2008年7月5日 – 在Code Project上发表了我的第五十篇文章!