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

WPF 的项级表示模型

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.65/5 (17投票s)

2008 年 8 月 24 日

GPL3

9分钟阅读

viewsIcon

84974

downloadIcon

6

通过在您的领域模型集合内容和模板生成的 WPF 对象之间插入一个表示模型层(也称为 ViewModel),让您的工作更轻松。

引言

正如 Josh Smith 在文章通过使用 ViewModel 模式简化 WPF TreeView中演示的那样,在您的领域模型集合内容和模板生成的 WPF 类之间插入一个表示模型层,可以使使用 WPF 变得更容易。Josh 展示了如何避免对模板生成的项(例如 TreeViewItem 的实例)编写过程代码。尝试这样做会导致头痛,让 WPF 看起来更像是一种负担而不是福利。当您在 XAML 中设置模板生成的项的属性时,WPF 运行良好,但是如果您尝试直接在代码隐藏文件中这样做,那么这种易用性就会丧失。所以,通过采用间接方法来避免这种痛苦。

为了从代码隐藏文件中访问和修改模板生成的项的属性,您首先需要将它们绑定到控件的 ItemsSource 集合中的任何内容。您想要绑定的许多属性(例如 IsSelectedIsExpanded)将不会出现在您的领域对象上,您也不希望将它们添加到那里,因为它们是 GUI 相关的而不是领域相关的。解决方案,正如 Josh 在他的文章中提出的,是创建一个额外的表示模型层,该层由位于您的领域模型集合内容和 WPF 类的模板生成实例之间的对象组成。

在您的 WPF 探索过程中,您可能会发现其他适合表示模型层的属性。这些属性显然更偏向 GUI 相关而不是领域相关,但作为数据绑定源(无需依赖值转换器)将非常方便。本文和随附的代码库扩展了这一思想。该库使得以最少的代码轻松创建前面描述的表示模型成为可能。ItemPresentationModel 类提供了领域模型类型的表示定制表示。而 HierarchicalPresentationModel 类则通过将修改从领域层集合或树结构传播到分层表示模型来帮助您。

术语

已经描述了两种非常相似的设计模式,每种模式都使用略有不同的术语。Martin Fowler 的原始表示模型模式使用“表示”一词来指代 UI,而 John Gossman 后来的 WPF 特有Model-View-ViewModel则使用“视图”。我更喜欢原始的,并将在本文中始终使用它。

至于特定领域的表示模型类,没有普遍接受的命名约定。Fowler 著作中的示例使用“Pmod”+ 领域类名组合,如“PmodAlbum”。Josh Smith 的文章最初使用领域类名 + “Presenter”,但有读者指出“这有点名不副实”。因此,他改用领域类名 + “ViewModel”,这遵循 John Gossman 的术语。有一段时间,我在表示模型类后缀“View”,但这个词在指代 UI 层方面的突出使用使我后来改为后缀“PM”。

WPF 中的表示模型

本文并非关于表示模型模式本身。Fowler 写道,“表示模型不是一个对特定领域对象友好的 GUI 外观”。然而,我使用的表示模型形式为每个领域对象提供了一个数据绑定友好的外观,用于其对应的模板生成项(例如,TreeViewItem)。这种模式的变体就是我所说的“项表示模型”,本文附带的库中有一个同名的相应基类。

这个 ItemPresentationModel 类用一个表示模型包装了一个领域对象(“Item”)。该领域对象可以通过类的“DomainItem”属性公开访问。这意味着您不必费心编写重复的桥接属性,形式如下

public string RegionName {
    get { return _region.RegionName; }
}

因为您可以直接访问包装的领域项目,所以不会添加任何值。

变化流

应用程序通常需要显示所有屏幕上的当前数据。忽略这一点,正如我在网上其他与 WPF 相关的文章中看到的那样,可能会随着代码库的增长而导致问题。例如,如果领域模型对象 dm1 有两个相应的表示模型对象 pm1pm2,并且如果 pm1 修改了 dm1,那么 pm2 将包含过时数据。如果领域模型中的某些逻辑修改了 dm1,那么对于 pm1pm2 都可能发生同样的问题。解决方案是使领域模型可观察,以便更改自动流向表示模型。

演示 1

此示例演示了创建 ItemPresentationModel 子类有多么容易。Demo1ItemPM 表示模型类的代码如下

public class Demo1ItemPM: ItemPresentationModel<SampleItem> {
    public Demo1ItemPM ( SampleItem domainItem )
      : base( domainItem ) {
        UnsavedName = domainItem.Name;
    }

    private string unsavedName;

    public string UnsavedName {
        get { return unsavedName; }
        set { unsavedName = value; }
    }
}

ItemPresentationModel 派生使您获得上面提到的 DomainItem 属性。您还从 Josh 的文章中获得了 IsSelected 属性,它在 XAML 文件中进行数据绑定,如下所示

<Style TargetType="{x:Type ListBoxItem}">
    <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}"/>
</Style>

UnsavedName正是您会添加到表示模型中的属性,因为它不属于领域模型,但其与数据绑定一起使用可以使事情变得更简单。这是用于编辑项目名称的文本框

<TextBox Text="{Binding Path=UnsavedName, UpdateSourceTrigger=PropertyChanged}"
  Margin="-3,0,0,0"
  FocusManager.FocusedElement="{Binding RelativeSource={RelativeSource Self}}"/>

多亏了绑定,editableContentControl_ButtonClick() 方法可以这样响应 saveButtoncancelButton 的点击

else if ( "saveButton".Equals( clickedButton.Name ) )
    targetItem.DomainItem.Name = targetItem.UnsavedName;
else if ( "cancelButton".Equals( clickedButton.Name ) )
    targetItem.UnsavedName = targetItem.DomainItem.Name;

如果点击 saveButton,则更新领域对象的名称(之后您将更改持久化到数据库)。如果点击 cancelButton,则重置 UnsavedName 值。

运行演示,并查看 Demo1.xamlDemo1.xaml.cs 的源代码,了解更多详细信息。

演示 2

现在我们转向表示分层领域模型结构,使用与模板生成的 TreeViewItem 接口的表示模型。对于此示例,我们有一个 SampleTreeNode 领域类及其相应的 SampleTreeNodePM 表示模型。SampleTreeNodePM 的超类是我的库的 HierarchicalPresentationModel 基类。它包装了领域树结构的单个项,并具有一个递归的 SubItems 属性,该属性用于 HierarchicalDataTemplate

<HierarchicalDataTemplate DataType="{x:Type uimodel:SampleTreeNodePM}"
  ItemsSource="{Binding Path=SubItems}">
    <TextBlock Text="{Binding Path=DomainItem.Name}"/>
</HierarchicalDataTemplate>

当您实例化此包装器类型的根对象(请参见下面的代码片段)时,它会从领域树结构中填充其树,首先包装每个项。然后将此根对象的 SubItems 属性绑定到 TreeView.ItemSource(片段中的下一行)。但是如果领域模型此后发生变化,表示模型如何更新以反映这些变化?例如,如果用户删除 SampleTreeNode 对象的子项,如何将该变化传播到其对应的 SampleTreeNodePM?幸运的是,我的 HierarchicalPresentationModel 类为您处理了这种同步。创建后,每个 HierarchicalPresentationModel 对象都会监听其相应领域集合的更改事件,向自身添加/移除子项以保持同步。

public partial class Demo2 : UserControl {
    private SampleTreeNodePM rootWrapper;
    
    public Demo2() {
        InitializeComponent();
        rootWrapper = new SampleTreeNodePM();
        sampleTreeView.ItemsSource = rootWrapper.SubItems;
    }

因此,HierarchicalPresentationModel 对象充当一种自定义集合视图(类似于 WPF 的 CollectionView),覆盖原始领域模型子项集合。从领域集合到其相应集合视图的绑定是单向的;也就是说,领域集合的更改会影响集合视图,但反之则不然。从集合视图到其相应 TreeView(如果是根)或 TreeViewItem(对于所有其他节点)的 ItemsSource 属性的绑定也是单向的。为了以编程方式添加/删除项,客户端代码修改领域集合;这些更改然后以所需的方式一路传播到 TreeViewTreeViewItem

这是 SampleTreeNodePM 演示模型类的代码

public sealed class SampleTreeNodePM
  : HierarchicalPresentationModel<SampleTreeNode, SampleTreeNodePM> {

    /// <summary>Creates a root-level SampleTreeNodePM.</summary>
    public SampleTreeNodePM() : this( SampleTreeNode.Root ) { }

    /// <summary>
    /// Creates a SampleTreeNodePM that does not (yet) have any sub-items.
    /// </summary>
    /// <param name="domainItem">
    /// The SampleTreeNode to be represented by this node</param>
    private SampleTreeNodePM( SampleTreeNode domainItem )
      : base( domainItem, domainItem.SubNodes, true, true, true ) { }

    /// <inheritdoc/>
    protected override SampleTreeNodePM CreateInstance( SampleTreeNode domainItem ) {
        return new SampleTreeNodePM( domainItem );
    }

    /// <inheritdoc/>
    protected override HierarchicalPlacementArgs<SampleTreeNodePM>
      DesiredPosition( SampleTreeNode itemToAdd ) {
        return new HierarchicalPlacementArgs<SampleTreeNodePM>(
          OutliningCommands.NewChild, this, false );
    }
}

此子类由四个部分组成

  1. 用于创建树根的 public 构造函数。
  2. 一个将用于创建所有其他节点的 private 构造函数。
  3. 重写了 CreateInstance() 方法,该方法通过后一个构造函数返回一个实例。
  4. 重写 DesiredPosition() 方法,在此情况下,它使用最简单的实现,即向子项集合添加新项。返回值的构造函数的第三个参数是 false,表示生成的集合未排序。

至于 #2 中的基构造函数调用,您可能想知道传递的三个布尔值是什么

  1. 指定表示模型子项的索引是否与其对应的源集合中的索引相同。我们完全镜像了领域层次结构,因此在这种情况下值为 true
  2. 指定一旦其父项被删除,子项是否也随之删除,在这种情况下也为 true
  3. 当此值为 true 时,会将对 DesiredPosition() 实现有用的调试信息写入控制台。

HierarchicalPresentationModel 派生自 ItemPresentationModel,因此从它派生会为您提供与演示 1 中提到的相同属性,以及 Josh 的 IsExpanded 属性,该属性以这种方式进行数据绑定

<Style TargetType="{x:Type TreeViewItem}">
    <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}"/>
    <Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}"/>
</Style>

在您自己的项目中使用 SampleTreeNodePM 作为模板。并查看 Demo2 代码隐藏文件的其余部分,了解如何从树结构中添加、移动和删除节点。

演示 3

HierarchicalPresentationModel 类还有另一个用途,即提供扁平集合的层次表示。也就是说,给定来自领域模型的扁平集合输入,该类可以帮助您生成一个分层表示模型,准备好绑定到 TreeView。在 WPF 中有几种不同的方法可以做到这一点,但这是我所知道的唯一一种将扁平集合显示为其他项的子项的方法。

事实上,HierarchicalPresentationModel 代码的最初目的是将扁平集合显示为层次结构。这是我第一次使用表示模型模式,甚至在我知道表示模型模式是什么之前!数据绑定 IsSelectedIsExpanded 属性的添加是我后来才意识到的一个优点,在我遇到 Josh Smith 的简化 WPF TreeView...文章之后。

此处的演示使用与演示 1 相同的领域数据。比较演示 1 和 3 可能很有趣,因为它们显示相同的数据,只是使用了不同的表示模型。

Demo3ItemPM 类的源代码如下

public sealed class Demo3ItemPM
  : HierarchicalPresentationModel<SampleItem,Demo3ItemPM> {

    /// <summary>Creates a root-level Demo3ItemPM.</summary>
    public Demo3ItemPM() : base( new Representative("Root"),
      App.SampleListContainer.SampleList, false, false, true ) { }

    /// <summary>
    /// Creates a Demo3ItemPM that does not (yet) have any sub-items.
    /// </summary>
    /// <param name="domainItem">The sample item to be represented by this node</param>
    private Demo3ItemPM( SampleItem domainItem )
      : base( domainItem, null, false, false, true ) { }

    /// <inheritdoc/>
    protected override Demo3ItemPM CreateInstance( SampleItem domainItem ) {
        return new Demo3ItemPM( domainItem );
    }

    /// <inheritdoc/>
    protected override HierarchicalPlacementArgs<Demo3ItemPM> DesiredPosition(
      SampleItem itemToAdd ) { ... }

    public Representative RepresentedBy {
        get {
            Represented represented = DomainItem as Represented;
            if ( represented != null ) return represented.RepresentedBy;
            else return null;
        }
        set {
            Represented represented = DomainItem as Represented;
            if ( represented != null ) represented.RepresentedBy = value;
        }
    }
}

此子类由与演示 2 中相同的四个部分组成。但是,您会注意到 private 构造函数中的一个区别。在这种情况下,为基构造函数的 subItemsSource 参数传递了 null 值。这表示基构造函数不会递归创建子项,这也是我们在此情况下想要的,因为 DesiredPosition() 方法将决定子树的排列。之后,您会注意到前两个布尔值也与上一个演示不同:子项索引在此情况下不对应,并且删除不会级联。

DesiredPosition() 方法的主体被省略了,因为它有点长。如果您有类似的场景,那么您应该查看它。除了提到的特定类型(RepresentedRepresentative),该逻辑可能适用于您的情况。之后是 RepresentedBy 桥接属性,我添加它是因为它使数据绑定到 representedByComboBox 变得容易得多

<ComboBox Name="representedByComboBox" SelectedItem="{Binding RepresentedBy}"
  DisplayMemberPath="Name" Padding="5,2" Margin="0,0,10,10"/>

问题在于涉及了两个不同的子类——一个具有 RepresentedBy 属性,另一个没有。此桥接属性通过在 RepresentedBy 属性不存在时仅返回 null 值来防止绑定错误。

© . All rights reserved.