How to mix databound and static levels in a TreeView?
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>
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; } }}
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.