当所有项目都在包装器中时,如何为 TreeView 编写数据模板?

How do I write the DataTemplates for a TreeView when all the items are in a wrapper?

我正在处理一个 WPF 项目。我最近做了一个重构,将我的一些模型 object 放入包装器中。我删减了 classes 以便他们阅读得很好:

public class Wrapper
{
    public AbstractModel Model; 
}

public abstract class AbstractModel 
{
    public string DisplayName;
}

public class Parent : AbstractModel
{
    public ObservableCollection<Wrapper> MyChildren;
}

public class Child : AbstractModel
{
    //Nothing relevant to the issue (I think).
}

//a .xaml.cs file
public partial class MyPropertiesControl
{
    //This is actually a dependency property that I bind to in other views, but I was too lazy to write the whole thing out.
    public ObservableCollection<Wrapper> ItemsToDisplay { get; }
}

我想显示一个 TreeView,其中显示了 Wrappers 的集合及其(潜在的)children。以前,没有 Wrapper class,只有 AbstractModel、Parent 和 Child。在这种情况下创建树视图非常简单:

<TreeView ItemsSource="{Binding ElementName=Root, Path=ItemsToDisplay}">
    <TreeView.Resources>
        <HierarchicalDataTemplate DataType="{x:Type Parent}" ItemsSource="{Binding MyChildren}">
            <Label Content="{Binding DisplayName}"/>
        </HierarchicalDataTemplate>

        <DataTemplate DataType="{x:Type Child}">
            <Label Content="{Binding DisplayName}" />
        </DataTemplate>
    </TreeView.Resources>
</TreeView>

既然 object 都在包装器中,我不知道如何编写模板。我尝试了很多不同的东西。这是我尝试过的两个想法:

<TreeView ItemsSource="{Binding ElementName=Root, Path=ItemsToDisplay}">
    <TreeView.Resources>
        <DataTemplate DataType="{x:Type Wrapper}">
            <ContentControl Content="{Binding Model}">
                <ContentControl.Resources>
                    <HierarchicalDataTemplate DataType="{x:Type Parent}" ItemsSource="{Binding MyChildren}">
                        <Label Content="{Binding DisplayName}"/>
                    </HierarchicalDataTemplate>

                    <DataTemplate DataType="{x:Type Child}">
                        <Label Content="{Binding DisplayName}"/>
                    </DataTemplate>

                </ContentControl.Resources>
            </ContentControl>
        </DataTemplate>
    </TreeView.Resources>
</TreeView>
<TreeView ItemsSource="{Binding ElementName=Root, Path=ItemsToDisplay}">
    <TreeView.Resources>
        <DataTemplate DataType="{x:Type Wrapper}">
            <ContentControl Content="{Binding Model}">
                <ContentControl.Resources>
                    <HierarchicalDataTemplate DataType="{x:Type Parent}" ItemsSource="{Binding MyChildren}">
                        <HierarchicalDataTemplate.Resources>
                            <DataTemplate DataType="{x:Type Wrapper}">
                                <ContentControl Content="{Binding Model}">
                                    <ContentControl.Resources>
                                        <DataTemplate DataType="{x:Type Child}">
                                            <Label Content="{Binding DisplayName}"/>
                                        </DataTemplate>
                                    </ContentControl.Resources>
                                </ContentControl>
                            </DataTemplate>
                        </HierarchicalDataTemplate.Resources>

                        <Label Content="{Binding DisplayName}"/>
                        
                    </HierarchicalDataTemplate>

                </ContentControl.Resources>
            </ContentControl>
        </DataTemplate>
    </TreeView.Resources>
</TreeView>

在这两种情况下,我只看到 parent 的第一层,没有看到任何 children。甚至没有 WPF 在没有为该类型定义的模板时使用的默认 class 名称,但 parent 节点实际上根本不显示 children。

我为鼠标在树视图上拖动添加了一个事件处理程序,并在事件处理程序上放置了一个断点,这样我就可以确保所有集合都已填充(它们是)。

这里还有其他人将 wrapped objects 绑定到树视图吗?您是如何让所有数据正确显示的?

我想另一个潜在的问题是通过 ContentControls 进入包装的 object 可能不是最好的主意。我没有从中得到 code-smell 的感觉,但我无法以更简洁的方式访问包装的 object 有点烦人。如果有人有任何补救的想法,它可能有助于解决 TreeView 问题。

我不确定是否有任何 XAML 唯一的方法可以做到这一点。但是你应该可以使用 DataTemplateSelector.

Provides a way to choose a DataTemplate based on the data object and the data-bound element.

他们在该文档中给出的示例涉及根据所显示项目的 属性 在两个 DataTemplate 之间进行选择。这正是您想要的:根据 Model.

的类型选择两个 HierarchicalDataTemplate 之一

包装器的一个 DataTemplate 就可以了。在它里面绑定到模型属性:

<TreeView ItemsSource="{Binding ElementName=Root, Path=ItemsToDisplay}">
    <TreeView.Resources>
        <HierarchicalDataTemplate DataType="{x:Type local:Wrapper}" 
                                  ItemsSource="{Binding Path=Model.MyChildren}">
            <Label Content="{Binding Model.DisplayName}"/>
        </HierarchicalDataTemplate>
    </TreeView.Resources>
</TreeView>

Child 类型没有 MyChildren 属性,所以它会产生一个警告:

System.Windows.Data Error: 40 : BindingExpression path error: 'MyChildren' property not found on 'object' ''Child' (HashCode=46092238)'. BindingExpression:Path=Model.MyChildren; DataItem='Wrapper' (HashCode=16310625); target element is 'TreeViewItem' (Name=''); target property is 'ItemsSource' (type 'IEnumerable')

可以安全地忽略它。或者您可以将 MyChildren 属性 移动到 AbstractModel 并在 Child 对象中将其留空。


我创建了一个测试 TreeView (tv):

public class Wrapper
{
    public AbstractModel Model { get; set;}

    public static implicit operator Wrapper(Parent p)
    {
        return new Wrapper { Model = p };
    }

    public static implicit operator Wrapper(Child c)
    {
        return new Wrapper { Model = c };
    }
}

使用此数据(隐式类型转换和集合初始化器用于缩短集合初始化)

var L = new List<Wrapper>()
{
    new Parent 
    { 
        DisplayName = "A", 
        MyChildren = 
        { 
            new Child { DisplayName = "A1"}, 
            new Parent 
            { 
                DisplayName = "X", 
                MyChildren = 
                { 
                    new Parent { DisplayName = "X1" } 
                }
            }
        } 
    },
    new Child  { DisplayName = "B"},
    new Parent { DisplayName = "C", MyChildren = { new Child { DisplayName = "C1"} } },
};
tv.ItemsSource = L;