如何在TreeView中混合数据绑定和静态级别?

13

我有一个数据库对象的集合,每个对象都包含模式对象和用户对象的集合。我想将它们绑定到TreeView上,并添加额外的静态层次结构,以使得结果的TreeView看起来更或多或少像这样:

<TreeView>
    <TreeViewItem Header="All the databases:">
        <TreeViewItem Header="Db1">
            <TreeViewItem Header="Here's all the schemas:">
                <TreeViewItem Header="Schema1"/>
                <TreeViewItem Header="Schema2"/>
            </TreeViewItem>
            <TreeViewItem Header="Here's all the users:">
                <TreeViewItem Header="User1"/>
                <TreeViewItem Header="User2"/>
            </TreeViewItem>
        </TreeViewItem>
        <TreeViewItem Header="Db2">
            <TreeViewItem Header="Here's all the schemas:">
                <TreeViewItem Header="Schema1"/>
                <TreeViewItem Header="Schema2"/>
            </TreeViewItem>
            <TreeViewItem Header="Here's all the users:">
                <TreeViewItem Header="User1"/>
                <TreeViewItem Header="User2"/>
            </TreeViewItem>
        </TreeViewItem>
    </TreeViewItem>
</TreeView>

我使用以下模板,已经接近我想要的效果:

<Window.Resources>
    <HierarchicalDataTemplate DataType="{x:Type smo:Database}">
        <TreeViewItem Header="{Binding Path=Name}">
            <TreeViewItem Header="Here's all the schemas:" ItemsSource="{Binding Path=Schemas}"/>
            <TreeViewItem Header="Here's all the users:" ItemsSource="{Binding Path=Users}"/>
        </TreeViewItem>
    </HierarchicalDataTemplate>
    <DataTemplate DataType="{x:Type smo:Schema}">
        <TextBlock Text="{Binding Path=Name}"/>
    </DataTemplate>
    <DataTemplate DataType="{x:Type smo:User}">
        <TextBlock Text="{Binding Path=Name}"/>
    </DataTemplate>
</Window.Resources>

然后在代码中我像这样设置了绑定:

TreeViewItem treeViewItem = new TreeViewItem();
treeViewItem.Header = "All the databases:";
treeViewItem.ItemsSource = server.Databases;
treeView.Items.Add(treeViewItem);

生成的TreeView看起来是我想要的,但是不能选择特定的模式或用户。显然,WPF将以数据库节点为根的整个子树视为单个项目,并且它仅选择整个子树。我需要能够选择特定的模式、用户或数据库。如何设置模板和绑定,使其按照我需要的方式工作?

4个回答

12

哦,这是一项非常令人沮丧的任务。我曾经尝试过很多次自己完成它。我有一个非常类似的需求,其中我有一个名为Customer的类,它包含了一个Locations集合和一个Orders集合。我希望Locations和Orders在树视图中成为“文件夹”。正如你已经发现的那样,所有展示如何绑定到自引用类型的TreeView示例几乎都没用。

首先,我试图手动构建FolderItemNode和ItemNode对象的树,我会在ViewModel中生成它们,但这违背了绑定的目的,因为它不会响应底层集合的更改。

然后,我想出了一种方法,它似乎运作得相当不错。

  • 在上述对象模型中,我创建了LocationCollection和OrderCollection类。它们都继承自ObservableCollection,并覆盖ToString()方法分别返回“Locations”和“Orders”。
  • 我创建了一个实现IMultiValueConverter接口的MultiCollectionConverter类
  • 我创建了一个FolderNode类,它具有Name和Items属性。这是代表树视图中“文件夹”的占位符对象。
  • 定义使用MultiBinding的hierarchicaldatatemplate,任何你想将多个子集合分组成文件夹的地方都可以使用。

生成的XAML类似于下面的代码,你可以获取一个具有所有类和XAML的zip文件,其中包含一个工作示例

<Window x:Class="WpfApplication2.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:Local="clr-namespace:WpfApplication2"
        Title="MainWindow" Height="350" Width="525" Loaded="Window_Loaded">

    <Window.Resources>

        <!-- THIS IS YOUR FOLDER NODE -->
        <HierarchicalDataTemplate DataType="{x:Type Local:FolderNode}" ItemsSource="{Binding Items}">
            <Label FontWeight="Bold" Content="{Binding Name}" />
        </HierarchicalDataTemplate>

        <!-- THIS CUSTOMER HAS TWO FOLDERS, LOCATIONS AND ORDERS -->
        <HierarchicalDataTemplate DataType="{x:Type Local:Customer}">
            <HierarchicalDataTemplate.ItemsSource>
                <MultiBinding>
                    <MultiBinding.Converter>
                        <Local:MultiCollectionConverter />
                    </MultiBinding.Converter>
                    <Binding Path="Locations" />
                    <Binding Path="Orders" />
                </MultiBinding>
            </HierarchicalDataTemplate.ItemsSource>
            <Label Content="{Binding Name}" />
        </HierarchicalDataTemplate>

        <!-- OPTIONAL, YOU DON'T NEED SPECIFIC DATA TEMPLATES FOR THESE CLASSES -->
        <DataTemplate DataType="{x:Type Local:Location}">
            <Label Content="{Binding Title}" />
        </DataTemplate>
        <DataTemplate DataType="{x:Type Local:Order}">
            <Label Content="{Binding Title}" />
        </DataTemplate>

    </Window.Resources>

    <DockPanel>
        <TreeView Name="tree" Width="200" DockPanel.Dock="Left" />
        <Grid />
    </DockPanel>

</Window>

Folders in TreeView


6
你提供的工作示例还存在吗?我刚试着访问了一下,但链接似乎失效了。 - psubsee2003

2
问题在于TreeView不太适合您想要实现的功能:它期望所有子节点都是相同类型的。由于您的数据库节点具有类型为Collection<Schemas>和Collection<Users>的节点,因此无法使用HierarchicalDataTemplate。更好的方法是使用包含ListBoxes的嵌套展开器。
下面的代码应该可以实现您想要的功能,并尽可能接近您的原始意图:
<Window x:Class="TreeViewSelection.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:smo="clr-namespace:TreeViewSelection"
    Title="Window1" Height="300" Width="300">
    <Window.Resources>
        <Style TargetType="ListBox">
            <Setter Property="BorderThickness" Value="0"/>
        </Style>
        <DataTemplate DataType="{x:Type smo:Database}">
                <TreeViewItem Header="{Binding Name}">
                    <TreeViewItem Header="Schemas">
                        <ListBox ItemsSource="{Binding Schemas}"/>
                    </TreeViewItem>
                    <TreeViewItem Header="Users">
                    <ListBox ItemsSource="{Binding Users}"/>
                </TreeViewItem>
                </TreeViewItem> 
        </DataTemplate>
        <DataTemplate DataType="{x:Type smo:User}" >
            <TextBlock Text="{Binding Name}"/>
        </DataTemplate>
        <DataTemplate DataType="{x:Type smo:Schema}">
            <TextBlock Text="{Binding Name}"/>
        </DataTemplate>
    </Window.Resources>
    <StackPanel>
        <TreeViewItem ItemsSource="{Binding DataBases}" Header="All DataBases">
        </TreeViewItem>
    </StackPanel>
</Window>

using System.Collections.ObjectModel;
using System.Windows;

namespace TreeViewSelection
{
    public partial class Window1 : Window
    {
        public ObservableCollection<Database> DataBases { get; set; }
        public Window1()
        {
            InitializeComponent();
            DataBases = new ObservableCollection<Database>
                            {
                                new Database("Db1"),
                                new Database("Db2")
                            };
            DataContext = this;
        }
    }

    public class Database:DependencyObject
    {
        public string Name { get; set; }
        public ObservableCollection<Schema> Schemas { get; set; }
        public ObservableCollection<User> Users { get; set; }

        public Database(string name)
        {
            Name = name;
            Schemas=new ObservableCollection<Schema>
                        {
                            new Schema("Schema1"),
                            new Schema("Schema2")
                        };
            Users=new ObservableCollection<User>
                      {
                          new User("User1"),
                          new User("User2")
                      };
        }
    }

    public class Schema:DependencyObject
    {
        public string Name { get; set; }
        public Schema(string name)
        {
            Name = name;   
        }
    }

    public class User:DependencyObject
    {
        public string Name { get; set; }
        public User(string name)
        {
            Name = name;
        }
    }
}

1
TreeView 组件肯定不要求所有的子节点都是相同类型的。例如,可以参考 http://www.codeplex.com/ComplexDataTemplates。 - Robert Rossney
好的,我可能对此有所错误。也许唯一的问题是TreeViewItem的默认内容不支持选定项的概念(ListBox支持)。您提到的文章似乎提供了一种解决方法,而不是证明同一级别的TreeViewItems需要具有相同类型的内容。 - Dabblernl
感谢您的输入!我确认Robert的评论,即TreeView不希望所有子节点都是相同类型。 不幸的是,您的方法并不完全有效。虽然现在它确实允许选择叶级节点(有些进展),但如果我单击数据库名称,则仍会选择整个子树,只留下一个白色(未选中)矩形,其中ListBox位于其中。 - Pawel Marciniak
奇怪,这里没有发生过。你确定你使用了完全相同的XAML吗? - Dabblernl
我的错误。我没有注意到您直接将根TreeViewItem嵌入StackPanel而不是嵌入TreeView中。这绝对更接近我所寻找的内容,但我仍然无法选择任何数据库节点(例如“Db1”),而这对于我的情况来说是必需的,很遗憾。 - Pawel Marciniak
抱歉,我放弃了。希望我给你一些新的想法。请告诉我们你是如何解决它的! - Dabblernl

0

这是对Josh的解决方案进行修改以适用于SMO(我的原始问题陈述):

<Window.Resources>
    <HierarchicalDataTemplate DataType="{x:Type local:FolderNode}" ItemsSource="{Binding Items}">
        <TextBlock Text="{Binding Name}"/>
    </HierarchicalDataTemplate>
    <HierarchicalDataTemplate DataType="{x:Type smo:Database}">
        <HierarchicalDataTemplate.ItemsSource>
            <MultiBinding>
                <MultiBinding.Converter>
                    <local:MultiCollectionConverter />
                </MultiBinding.Converter>
                <Binding Path="Schemas" />
                <Binding Path="Users" />
            </MultiBinding>
        </HierarchicalDataTemplate.ItemsSource>
        <TextBlock Text="{Binding Name}"/>
    </HierarchicalDataTemplate>
    <DataTemplate DataType="{x:Type smo:User}" >
        <TextBlock Text="{Binding Name}"/>
    </DataTemplate>
    <DataTemplate DataType="{x:Type smo:Schema}">
        <TextBlock Text="{Binding Name}"/>
    </DataTemplate>
</Window.Resources>

以及修改后的转换器:

public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
    FolderNode[] result = new FolderNode[values.Length];
    for (int i = 0; i < values.Length; ++i)
    {
        result[i].Items = (IEnumerable)values[i];
        result[i].Name = values[i] is UserCollection ? "Users" : "Schemas";
    }
    return result;
}
注明:此内容是从 OP的最终解决方案中复制而来,作为问题的编辑发布,而不是作为答案发布。

0

你需要从数据库中填充你在绑定中使用的属性。目前,你正在使用一个新的TreeViewItem作为数据源,因此你所说的它将所有内容视为单个节点是有道理的,因为你把它放在了一个单独的节点中。

你需要加载数据库数据并将其附加到你在WPF模板中用作绑定项的属性上。


Tony,我不确定你的意思。虽然我正在使用新的TreeViewItem,但我是通过TreeViewItem的ItemsSource属性绑定数据库集合的。数据库集合是由SMO自动填充的(我没有包含设置代码以避免在我的帖子中添加无关的细节)。您能否提供一些代码示例来说明您的建议? - Pawel Marciniak
我猜我误解了你最初的问题。你的.Databases属性将所有数据加载到类和依赖属性中,这些属性在你的绑定中使用,对吧?我以为你的绑定没有从数据源正确加载。但似乎不是这个问题。 - Tony The Lion

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接