views:

256

answers:

3

I have a collection of Database objects, each containing collections of Schema objects and User objects. I want to bind them to a TreeView, but adding additional static levels in the hierarchy, so that the resulting TreeView looks more or less like this:

<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>

I was able to get pretty close to what I want by using the following templates:

<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>

Then in the code I set the binding like this:

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

The resulting TreeView looks like I want it to, but it's not possible to select a particular schema or user. Apparently WPF sees the whole subtree rooted at a database node as a single item, and it only selects the whole thing. I need to be able to select a particular schema, user or database. How do I set the templates and bindings so that it works the way I need?

Update: Here's a modification of Josh's solution to work with SMO (my original problem statement):

<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>

and the modified converter:

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;
}
A: 

You need to fill the properties you're using in your binding with data from your database. Currently you're using a new TreeViewItem, and using it as a datasource, so what you're saying about it seeing everything as a single node makes sense, as you've placed it in a single node.

You need to load your database data and attach it to the properties you've used in your WPF template as binding items.

Tony
Tony, I'm not sure what you mean. Although I'm using new TreeViewItem, I'm binding the collection of databases using the TreeViewItem's ItemsSource property. The collection of databases is automatically populated by SMO (I didn't include the setup code to avoid cluttering my post with irrelevant details). Could you please provide some code sample that illustrates what you're suggesting?
Pawel Marciniak
I'm guessing I misunderstood your original question. Your .Databases property loads all data into the class and dependency properties you're using in your bindings right? I thought your bindings weren't loaded properly from your datasource. But doesn't seem to be the issue.
Tony
A: 

The problem is that a TreeView is not very well suited to what you want to acomplish: It expects all the subnodes to be of the same type. As your database node has a node of type Collection<Schemas> and of type Collection<Users> you cannot use a HierarchicalDataTemplate. A Better approach is to use nested expanders that contain ListBoxes.

The code below does what you want I think,while being as close as possible to your original intent:

<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;
        }
    }
}
Dabblernl
A TreeView most certainly does not expect all of the subnodes to be of the same type. See, for instance, http://www.codeplex.com/ComplexDataTemplates.
Robert Rossney
OK, I may be wrong about that. Perhaps the only problem is that a TreeViewItem's default Content does not support the notion of a selected item (which a ListBox does). The article you mention seems to offer a workaround though, not prove of the fact that TreeViewItems on the same level need to have content of the same type.
Dabblernl
Thanks for your input! I confirm Robert's comment that TreeView doesn't expect all subnodes to be of the same type.Unfortunately, your approach doesn't work all that well. While now it indeed allows selecting leaf level nodes (some progress), it still selects the whole subtree if I click on database name, only leaving a white (non-selected) rectangle where the ListBox is.
Pawel Marciniak
Strange, that does not happen here. Are you sure you used my exact XAML?
Dabblernl
My mistake. I haven't noticed that you embed the root TreeViewItem directly inside a StackPanel rather than inside TreeView. It's definitely getting closer to what I was looking for, but I still can't select any database node (such as "Db1"), and that, unfortunately, is required for my scenario.
Pawel Marciniak
Sorry, I pass. Hopefully I gave you some new ideas. Let us know how you solved it!
Dabblernl
+3  A: 

Oh man this is an incredibly frustrating task. I've tried doing it myself many times. I had a very similar requirement where I've got something like a Customer class that has both a Locations collection and a Orders collection. I wanted Locations and Orders to be "folders" in the tree view. As you've discovered, all the TreeView examples that show you how to bind to self-referencing types are pretty much useless.

First I resorted to manually building a tree of FolderItemNode and ItemNode objects that I would generate in the ViewModel but this defeated the purpose of binding because it would not respond to underlying collection changes.

Then I came up with an approach which seems to work pretty well.

  • In the above described object model, I created classes LocationCollection and OrderCollection. They both inherit from ObservableCollection and override ToString() to return "Locations" and "Orders" respectively.
  • I create a MultiCollectionConverter class that implements IMultiValueConverter
  • I created a FolderNode class that has a Name and Items property. This is the placeholder object that will represent your "folders" in the tree view.
  • Define hierarchicaldatatemplate's that use MultiBinding anywhere that you want to group multiple child collections into folders.

The resulting XAML looks similar to the code below and you can grab a zip file which has all the classes and XAML in a working example.

<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

Josh Einstein
Yay! Works perfectly
Pawel Marciniak