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?
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.
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>
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;
}
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With