views:

294

answers:

3

Hi there,

I'm trying to change the layout of a databound treeview from this:

alt text

To this:

alt text

And of course selection must works properly:

alt text

Do you have any ideas about how to do that. I've been trying to change the template but I can't find out a way to have this behavior. Maybe a component already exists...

Thanks for your help !

+3  A: 

This is difficult. It seems to need a HierarchicalDataTemplate, but because the behavior you want requires multiple ItemsControls, it is not going to work as expected. I don't think there is a way to create a TreeView template in XAML that will do this. Your best bet is to create a custom items control of some sort. You will probably need to do the items binding in code, rather than in XAML, because without the HierarchicalDataTemplate the XAML has no way of understanding nested relationships.

That being said, if you are guaranteed to only have 2 levels of nesting (as in your example), you could do this easily with the following mark-up:

<Window.Resources>
    <DataTemplate x:Key="ItemTemplate">
        <TextBlock Text="{Binding Name}"/>
    </DataTemplate>
</Window.Resources>

<StackPanel Orientation="Horizontal">
    <ListBox Name="Level1" Width="150" Height="150"
             ItemsSource="{Binding Collection}" 
             ItemTemplate="{StaticResource ItemTemplate}"/>
    <ListBox Name="Level2" Width="150" Height="150"
             ItemsSource="{Binding ElementName=Level1, Path=SelectedValue.Children}"
             ItemTemplate="{StaticResource ItemTemplate}"/>
    <ListBox Name="Level3" Width="150" Height="150"
             ItemsSource="{Binding ElementName=Level2, Path=SelectedValue.Children}"
             ItemTemplate="{StaticResource ItemTemplate}"/>
</StackPanel>

Where Collection is your root items collection and there is a property on each item called Children containing the child collection.

But I think what you are asking for is an items control that can support any number of nested levels, not just 2. So in that case, I would do this in code-behind. The binding will be the same- that is, at each level, the ListBox should be bound to the parent level's items. But you will obviously need to iterate and create one ListBox for each nested level.

Charlie
A: 

I finally find a way out, but like you say Charlie, it involves creating ListBox:

  • I create a new CustomControl which inherits Control (I couldn’t use neither Selector or TreeView because I wouldn’t have been able to manage the SelectedItem property from the derived class)
  • In the template of this CustomControl is an ItemsControl. This ItemsControl has its ItemTemplate property set to a DataTemplate containing a ListBox.
  • The CustomControl has a Depth property of type int. This property indicates the number of ListBox that should be generated.
  • The CustomControl automatically databound ListBoxes together: each ListBox’s ItemsSource property is databound to the SelectedItem’s children property of the previous ListBox in the visual tree.
  • The CustomControl has a SelectedItem property and a SelectionChanged event (like Selector-derived class).
  • I added an IsReallySelected attached property to the ListBoxItem which are generated. This enables to databing an IsSelected property of the ViewModel class behind the control with the IsSelected of the ListBoxItem. I had to create an attached property because its value is true when the ListBoxItem is selected AND the parent ListBox has IsSelectionActive set to true.

I blogged about this solution (with source code) on my blog.

Jalfp
A: 

Its too bad I didn't notice this question before you went to all that work. It is easy to restyle a TreeView to appear this way: The only code required is a single very simple attached property, "VisibleWhenCurrentOf".

The way to do it is to:

  1. Style TreeViewItem to include a ListBox in its ControlTemplate outside the ItemsPresenter.

  2. Control the visibility of the TreeViewItem template using "VisibleWhenCurrentOf", so that a given item is only visible inside the ItemsPresenter if it is the current item within the ListBox.

Restyling details

Here is the XAML for the relevant templates:

<ControlTemplate TargetType="TreeView">
  <DockPanel>

    <ListBox
      ItemsSource="{TemplateBinding ItemsSource}"
      IsSyncrhonizedWithCurrentItem="true"
      Style="{DynamicResource BoxesTreeViewBoxStyle}"
      ItemTemplate="{Binding HeaderTemplate}"
      ItemTemplateSelector="{Binding HeaderTemplateSelector}" />

    <ItemsPresenter />
  </DockPanel>
</ControlTemplate>

<ControlTemplate TargetType="TreeViewItem">
  <DockPanel
    local:VisibilityHelper.VisibleWhenCurrentOf="{Binding ItemsSource, RelativeSource={RelativeSource FindAncestor,HeaderedItemsControl,2}">

    <ListBox
      ItemsSource="{TemplateBinding ItemsSource}"
      IsSyncrhonizedWithCurrentItem="true"
      Style="{DynamicResource BoxesTreeViewBoxStyle}"
      ItemTemplate="{Binding HeaderTemplate}"
      ItemTemplateSelector="{Binding HeaderTemplateSelector}" />

    <ItemsPresenter />
  </DockPanel>
</ControlTemplate>

These two templates are identical except for the conditional visibilty. The way this works is that the "+" in front of the tree item becomes a ListBox, and all items except the one selected in the ListBox are hidden.

Your BoxesTreeViewBoxStyle should set a margin around the ListBox so they will space correctly. You can actually simplify this further by putting the ListBox property values in the style, but I find it more convenient to set them in the ControlTemplate so I can restyle the ListBox without having to remember these settings.

Attached property

Here is the code for the VisibleWhenCurrentOf attached property:

public class VisibilityHelper : DependencyObject
{

  // VisibleWhenCurrentOf
  public static object GetVisibleWhenCurrentOf(DependencyObject obj) { return (object)obj.GetValue(VisibleWhenCurrentOfProperty); }
  public static void SetVisibleWhenCurrentOf(DependencyObject obj, object value) { obj.SetValue(VisibleWhenCurrentOfProperty, value); }
  public static readonly DependencyProperty VisibleWhenCurrentOfProperty = DependencyProperty.RegisterAttached("VisibleWhenCurrentOf", typeof(object), typeof(VisibilityHelper), new UIPropertyMetadata
  {
    PropertyChangedCallback = (sender, e) =>
    {
      var element = sender as FrameworkElement;
      if(e.OldValue!=null)
      {
        var oldView = e.OldValue as ICollectionView ?? CollectionViewSource.GetDefaultView(e.OldValue);
        oldView.CurrentChanged -= UpdateVisibilityBasedOnCurrentOf;
        if(e.NewValue==null) element.DataContextChanged -= UpdateVisibilityBasedOnCurrentOf;
      }
      if(e.NewValue!=null)
      {
        var newView = e.NewValue as ICollectionView ?? CollectionViewSource.GetDefaultView(e.OldValue);
        newView.CurrentChanged += UpdateVisibilityBasedOnCurrentOf;
        if(e.OldValue==null) element.DataContextChanged += UpdateVisibilityBasedOnCurrentOf;
      }
      UpdateVisibilityBasedOnCurrentOf(sender);
    }
  });

  static void UpdateVisibilityBasedOnCurrentOf(object sender, DependencyPropertyChangedEventArgs e) { UpdateVisibilityBasedOnCurrentOf(sender); }
  static void UpdateVisibilityBasedOnCurrentOf(object sender, EventArgs e) { UpdateVisibilityBasedOnCurrentOf(sender); }
  static void UpdateVisibilityBasedOnCurrentOf(object sender)
  {
    var element = sender as FrameworkElement;

    var source = GetVisibleWhenCurrentOf(element);
    var view = source==null ? null : source as ICollectionView ?? CollectionViewSource.GetDefaultView(source);

    var visible = view==null || view.CurrentItem == element.DataContext;

    element.Visibility = visible ? Visibility.Visible : Visibility.Collapsed;
  }

}

There is nothing complex here: Any time DataContext or the view's Current changes, visibilty is recomputed. The PropertyChangedCallback simply sets event handlers to detect these conditions and the UpdateVisibiltyBasedOnCurrentOf handler recomputes visibility.

Advantages of this solution

Since this solution is a real TreeView:

  1. You get all the selection handling functionality for free.
  2. It works with any number of tree levels.
  3. You can use all the features of HierarchicalDataTemplate, including HeaderTemplate and HeaderTemplateSelector
  4. You can use different ItemsSource bindings at each level rather than every collection requiring a "Children" proerty
  5. It is a lot less code than a custom control
Ray Burns
Hi Ray,Thank you for taking time to share your solution. It looks much better than my custom control ! However I can't find a way to make it work... I think there was a spelling error in the code of the attached property but still, it doesn't work. Could you please share a downloadable archive ?
Jalfp