How to mix databound and static levels in a TreeView?

余生长醉 提交于 2019-12-02 20:56:58
Josh

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.

psubsee2003

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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!