65.9K
CodeProject 正在变化。 阅读更多。
Home

WPF PropertyGrid - MVVM 技术

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.86/5 (10投票s)

2010年3月9日

CPOL

5分钟阅读

viewsIcon

65676

downloadIcon

1784

如何构建一个多列 ListView,使其能够根据行数据类型选择单元格模板;以及如何为每个单元格模板动态创建 ViewModel。

引言

WPF 中有两个常见问题:基于数据类型为 ListView 列选择数据模板,以及实现将数据格式化为 UI 可以绑定的内容的胶水逻辑。

本文介绍了解决这两个问题的方法;然而,重要的是解决思路,而非解决方案本身。WPF 的范式截然不同,希望这个例子能鼓励 UI 设计中更具创新性的思考。

假定您已了解 Model-View-ViewModel (MVVM) 模式;如果您之前没有接触过该模式,我强烈建议您阅读 维基百科上关于 MVVM 的文章John Gossman 关于 Model-View-ViewModel 的原创博文

背景:多列 ListViews

多列 ListViewView 属性设置为 GridView 的实例,如下面的代码所示:

<ListView.View>
    <GridView>
        <GridViewColumn Header="Name" DisplayMemberBinding="{Binding Name}"/>
        <GridViewColumn Header="Value" Width="200" />
    </GridView>
</ListView.View>

由于我们要构建一个属性网格,所以希望第二列根据属性的类型显示不同种类的控件。

GridViewColumn 有两个相关的属性:CellTemplateCellTemplateSelector。然而,两者似乎都存在障碍:

  • CellTemplate 只允许您指定一个模板,该模板将用于所有行;
  • CellTemplateSelector 要求您编写一个模板选择器类。

编写模板选择器似乎特别不必要,因为 WPF 已经有一个很好的机制可以根据数据类型选择数据模板。

根据数据选择单元格模板

解决此问题的方法是认识到 CellTemplate 不必呈现单元格本身的内容。诀窍在于使用 CellTemplate 来承载 ContentPresenter,而 ContentPresenter 实际上会选择合适的数据模板!

<GridViewColumn Header="Value" Width="200">
    <GridViewColumn.CellTemplate>
        <DataTemplate>
            <!-- The cell template is a ContentPresenter, hence the actual
                 template will be selected by type. -->
            <ContentPresenter Content="{Binding}"/>
        </DataTemplate>
    </GridViewColumn.CellTemplate>
</GridViewColumn>

请注意,ContentPresenterContent 属性绑定到 CellTemplate 的数据上下文。因此,CellTemplate 在其 DataContext 设置为正在呈现的行的情况下被实例化,然后它只是将行传递给 ContentPresenter,后者选择合适的数据模板。

只要为每种行类型定义了数据模板,Value 列现在就会生成特定于该行的内容。因此,让我们定义两个数据模板,一个用于 TextItem,一个用于 ColorItem

<DataTemplate DataType="{x:Type clr:TextItem}">
    <TextBox Text="{Binding Text}"/>
</DataTemplate>
            
<DataTemplate DataType="{x:Type clr:ColorItem}">
    <ComboBox />
</DataTemplate>

我希望 ComboBox 显示颜色值的下拉列表,但不幸的是,我的 ColorItem 类是这样的:

[Serializable]
public class ColorItem : ItemBase
{
    // Inherited from the base class:
    // public string Name { get; set; }
    public float Red { get; set; }
    public float Green { get; set; }
    public float Blue { get; set; }
}

我可以使用 IValueConverter 的实现将 ColorItem 转换为 System.Windows.Media.Color 结构。但是,当选择更改时,如何将其转换回 ColorItem?特别是,我无法将 Name 属性存储在 Color 结构中,因此没有足够的信息来执行反向转换。

ViewModel 应用

最理想的情况是将每个 ColorItem 包装在一个 ViewModel 中,该 ViewModel 可以执行诸如从 RGB 值转换为 Color 并返回等智能操作。存在两个问题:

  • 列表中的并非所有项都是 ColorItem
  • 我们希望避免修改原始列表。

需要的是类似这样的逻辑:“如果该项是 ColorItem,则创建 ColorItemViewModel 并将 DataTemplate 绑定到它。”那么,我们如何为每个 ColorItem 数据模板生成 ColorItemViewModel 呢?

这可能并不明显,但在 ResourceDictionary 中创建任意类的实例是可能的。

<DataTemplate.Resources>
    <clr:ColorItemViewModel x:Key="ViewModel" />
</DataTemplate.Resources>

这就解决了一半问题。每次实例化 DataTemplate 时,都会在其资源字典中创建一个 ColorItemViewModel 实例。现在,问题是如何将其与实际数据连接起来。大胆猜测一下:

<DataTemplate.Resources>
    <clr:ColorItemViewModel x:Key="ViewModel" Item="{Binding}" />
</DataTemplate.Resources>

难道就这么简单吗?起初我以为是这样——但事实证明我错了。以我使用的方式进行绑定需要 DataContext。即使我的 ViewModel 有 DataContext,ResourceDictionary 也没有,因此没有继承链可以让 ViewModel 获取正确的上下文。但并非所有希望都已破灭!ViewModel 没有理由不能是一个(不可见的)FrameworkElement。这是代码:

<DataTemplate DataType="{x:Type clr:ColorItem}">
    <Grid>
        <clr:ColorItemViewModel x:Name="Persona" Item="{Binding}"/>
        <ComboBox DataContext="{Binding ElementName=Persona}"
                  ItemsSource="{Binding AvailableColors}"
                  SelectedItem="{Binding Color}" />
    </Grid>
</DataTemplate>

该 Grid 用作占位符,以容纳不可见的 ViewModel 和 ComboBox。只要 ViewModel 继承自 FrameworkElement,这就能很好地工作。

请注意,我们已经替换了 ComboBoxDataContext,将其设置为 ViewModel 而非原始 ColorItem。现在,ItemsSource 绑定将从 ColorItemViewModel 实例获取可用颜色。

关注点

这里有两个主要概念。第一,将数据模板嵌套在其他数据模板中,使用 ContentPresenter 的想法。这允许进行各种有趣的技巧,例如在不更改项的原始数据模板的情况下,为列表项的 RenderTransform 添加动画。

第二个(也是更有趣的)技巧是使用 DataTemplate 中的不可见 FrameworkElement 为每个实例拉入逻辑。由于这些是在每次实例化数据模板时创建的,因此每个数据项将拥有您逻辑类的一个唯一实例。然后,您可以使用绑定语法将逻辑连接到视觉元素。

结论

本文并非旨在提供实现 PropertyGrid 的权威示例。在 WPF 中,有许多方法可以解决同一个问题。但这是一个优雅的解决方案,我希望它能为其他优雅的解决方案提供灵感。

历史

在本篇文章的第一个版本中,我犯了一个大错误,将 ViewModel 放在了 Resources 部分。这个想法很巧妙,但我忘了考虑 ViewModel 如何获得 DataContext。这说明并非所有好的想法都能如你所愿地运作。

© . All rights reserved.