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
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;
}
Attribution Note: Content copied from OP's final solution, posted as an edit to the question, rather than as an answer
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.
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>