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.
- 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.
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
来源:https://stackoverflow.com/questions/1815056/how-to-mix-databound-and-static-levels-in-a-treeview