Click here to Skip to main content
Click here to Skip to main content

Advanced Custom TreeView Layout in WPF

By , 29 Jan 2007
 

Introduction

In a previous article here on the CodeProject I demonstrated one way of redefining how a WPF TreeView can be displayed. In that article we examined how to make a TreeView look like an Org Chart by using some XAML magic. In this article we will explore a more complex and interactive customization, which makes the TreeView present its items as a set of "nested buckets."

Show me the money

Let's take look at what the customized TreeView looks like. Later we will see how this custom layout was implemented. The screenshot seen below is of a demo application, which is available for download via a link at the top of this page.

The TreeView with a layout customization in effect.

The top portion of the user interface is the customized TreeView. The innermost items provide links to Wikipedia pages containing information about the cities named in those items. The bottom portion of the UI is a Frame element, which loads and displays the Wikipedia pages chosen in the TreeView.

If we did not apply a layout customization to the TreeView seen above, it would look like this:

Plain Jane

What is a layout customization?

Take another quick glance again at the non-customized TreeView screenshot above. Notice that the leaf items are rendered as hyperlinks, even though a layout customization is not in effect. The way a TreeViewItem's content renders is affected by data templates. What I refer to as a "layout customization" does not deal with rendering an item's content, rather, it explains how to render that which contains an item's content and how those containers are positioned relative to one another. The item content is irrelevant for our purposes.

TreeView and TreeViewItem both derive from ItemsControl. An ItemsControl contains item containers, which can be thought of as 'boxes' that hold arbitrary content. The content of those boxes is the data which the user consumes (i.e. the stuff that the user cares about and pays the most attention to). In a TreeView those boxes are represented by TreeViewItem objects. A layout customization organizes the TreeViewItems – explaining how they should be positioned, how they should be rendered, if they should be shown or hidden, etc.

How it works

Data Format

Before delving into the code which implements our custom layout, let's take a moment to review the data being displayed. In the demo application a TreeView is bound to XML data, which is in this simple format:

<?xml version="1.0" encoding="utf-8" ?>
<Countries>
  <Country CountryName="USA">
    <Region RegionName="California">
      <City 
        CityName="Los Angeles" 
        Uri="http://en.wikipedia.org/wiki/Los_Angeles" />
      
      <!-- More City elements... -->
      
    </Region>

    <!-- More Region elements... -->

  </Country>

  <!-- More Country elements... -->

</Countries>

The TreeView displays the Country, Region, and City elements as TreeViewItems. It renders Country and Region items as collapsible groups, whose caption is the CountryName or RegionName attribute value, and the inner list of items is taken from the element's set of nested child elements (a country contains regions, and a region contains cities).

TreeViewItem Style

Below is an abridged version of the Style which contains most of the custom layout implementation:

<Style TargetType="TreeViewItem">
  <Style.Resources>
    <!-- Resources omitted for clarity... -->
  </Style.Resources>

  <Setter Property="Template">
    <Setter.Value>
      <ControlTemplate TargetType="TreeViewItem">
        <Grid Margin="8,4">
          <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
          </Grid.ColumnDefinitions>
          <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
          </Grid.RowDefinitions>

          <!-- This Border contains elements which display 
               the content and child items of the TreeViewItem. -->
          <Border Name="Bd" 
            Background="{StaticResource ItemAreaBrush}"
            BorderBrush="{StaticResource ItemBorderBrush}" 
            BorderThickness="0.6" 
            CornerRadius="8"              
            Padding="6"     
            SnapsToDevicePixels="True"
            >
            <Grid>
              <!-- Items with children are shown in an Expander. -->
              <Expander Name="Exp" 
                IsExpanded="{TemplateBinding TreeViewItem.IsExpanded}">
                <Expander.Header>
                  <!-- Displays the item's header in the Expander. -->
                  <ContentPresenter ContentSource="Header" />
                </Expander.Header>
                <!-- Displays the item's children. -->
                <ItemsPresenter />
              </Expander>

              <!--Items without children are shown in a ContentPresenter.-->
              <ContentPresenter Name="CntPres"
                ContentSource="Header"
                HorizontalAlignment="Center"
                VerticalAlignment="Center"
                Visibility="Collapsed" 
                />
            </Grid>
          </Border>
        </Grid>

        <ControlTemplate.Triggers>
          <!-- If the TreeViewItem has child items,
               show it in an Expander.  Otherwise
               hide the Expander and show the hidden
               ContentPresenter. -->
          <Trigger Property="TreeViewItem.HasItems" Value="false">
            <Setter 
              TargetName="Exp" 
              Property="Visibility" 
              Value="Collapsed" />
            <Setter 
              TargetName="CntPres" 
              Property="Visibility" 
              Value="Visible" />
          </Trigger>

          <!--When the item is selected in the TreeView, use the 
              "selected" colors and give it a drop shadow. -->
          <Trigger Property="IsSelected" Value="true">
            <!-- Setters omitted for clarity... -->
          </Trigger>
        </ControlTemplate.Triggers>
      </ControlTemplate>
    </Setter.Value>
  </Setter>

  <!-- Make each TreeViewItem show it's children 
       in a StackPanel. If it is a root item then
       the Orientation will be 'Horizontal', else
       'Vertical'. -->
  <Setter Property="ItemsPanel">
    <Setter.Value>
      <ItemsPanelTemplate>
        <ItemsPanelTemplate.Resources>
          <local:ItemsPanelOrientationConverter x:Key="conv" />
        </ItemsPanelTemplate.Resources>
        <StackPanel 
          IsItemsHost="True" 
          Orientation="{Binding 
            RelativeSource={x:Static RelativeSource.TemplatedParent}, 
            Converter={StaticResource conv}}" 
          />
      </ItemsPanelTemplate>
    </Setter.Value>
  </Setter>
</Style>

Choosing the right visuals

There are a couple aspects of the XAML seen above worth pointing out. The Style sets the Template property of TreeViewItem to a ControlTemplate. That template has the job of explaining how a TreeViewItem instance should be rendered. Items that represent a Country or Region XML element must render as collapsible groups, but City elements should not. Let's take a closer look at how that is achieved. I stripped away some unimportant settings, so that we can focus on the essential information:

<Border>
  <Grid>
    <!-- Items with children are shown in an Expander. -->
    <Expander Name="Exp">
      <Expander.Header>
        <!-- Displays the item's header in the Expander. -->
        <ContentPresenter ContentSource="Header" />
      </Expander.Header>
      <!-- Displays the item's children. -->
      <ItemsPresenter />
    </Expander>

    <!-- Items without children are shown in a ContentPresenter. -->
    <ContentPresenter Name="CntPres"
      ContentSource="Header"      
      Visibility="Collapsed" />
  </Grid>
</Border>

The XAML above creates a Border element which contains a Grid panel. That Grid has one row and one column (i.e. one "cell"). That cell contains an Expander and a ContentPresenter, but only one of those two elements will ever be visible at any given moment. The Expander is there in case the TreeViewItem has child items. The ContentPresenter will be shown if the item does not have any children, in this case, if it represents a City element in the XML data.

The control template has a Trigger to determine which element should be used to render the TreeViewItem. That Trigger is seen below:

<Trigger Property="TreeViewItem.HasItems" Value="false">
  <Setter 
    TargetName="Exp" 
    Property="Visibility" 
    Value="Collapsed" />
  <Setter 
    TargetName="CntPres" 
    Property="Visibility" 
    Value="Visible" />
</Trigger>

Item layout direction

Another tricky aspect to the layout seen in the screenshot at the top of this article has to do with the direction in which TreeViewItems are arranged. The items representing Country and Region elements are arranged in a horizontal row, but the City items are in a vertical list.

Arranging the root items (the Country items) in a horizontal row requires the TreeView's ItemsPanel property to be set to a StackPanel with a horizontal orientation. Here is some XAML from the demo app's main Window which configures the TreeView:

<TreeView Name="tree" 
  DataContext="{StaticResource countriesXml}" 
  ItemsSource="{Binding}"
  >
  <!-- Import the resource file with the 
       new TreeViewItem style. -->
  <TreeView.Resources>
    <ResourceDictionary 
      Source="GroupedTreeViewItemStyle.xaml" />
  </TreeView.Resources>

  <!-- Arrange the root items horizontally. -->
  <TreeView.ItemsPanel>
    <ItemsPanelTemplate>
      <StackPanel 
        IsItemsHost="True" 
        Orientation="Horizontal" />
    </ItemsPanelTemplate>
  </TreeView.ItemsPanel>
</TreeView>

The next piece of the puzzle requires a little more trickery than just setting a property. Since the TreeViewItems that represent Region elements must be arranged horizontally but City items must be listed vertically, we need to use a value converter to determine at runtime what orientation a TreeViewItem's ItemsPanel should use. Here's the XAML from the Style seen previously which sets the ItemsPanel for a TreeViewItem:

<Setter Property="ItemsPanel">
  <Setter.Value>
    <ItemsPanelTemplate>
      <ItemsPanelTemplate.Resources>
        <local:ItemsPanelOrientationConverter x:Key="conv" />
      </ItemsPanelTemplate.Resources>
      <StackPanel 
        IsItemsHost="True" 
        Orientation="{Binding 
            RelativeSource={x:Static RelativeSource.TemplatedParent}, 
            Converter={StaticResource conv}}" 
          />
    </ItemsPanelTemplate>
  </Setter.Value>
</Setter>

The StackPanel used as the ItemsPanel template has its Orientation property bound. The binding uses a value converter to determine whether the StackPanel should have a horizontal or vertical orientation. Here's the code for the value converter:

[ValueConversion( typeof( ItemsPresenter ), typeof( Orientation ) )]
public class ItemsPanelOrientationConverter : IValueConverter
{  
 // Returns 'Horizontal' for root TreeViewItems 
 // and 'Vertical' for all other items.
 public object Convert( 
  object value, Type targetType, object parameter, CultureInfo culture )
 {
  // The 'value' argument should reference 
  // an ItemsPresenter.
  ItemsPresenter itemsPresenter = value as ItemsPresenter;
  if( itemsPresenter == null )
   return Binding.DoNothing;

  // The ItemsPresenter's templated parent 
  // should be a TreeViewItem.
  TreeViewItem item = itemsPresenter.TemplatedParent as TreeViewItem; 
  if( item == null )
   return Binding.DoNothing;

  // If the item is contained in a TreeView then it is
  // a root item.  Otherwise it is contained in another
  // TreeViewItem, in which case it is not a root.
  bool isRoot = 
   ItemsControl.ItemsControlFromItemContainer( item ) is TreeView;

  // The children of root items are layed out
  // in a horizontal row.  The grandchild items 
  // (i.e. cities) are layed out vertically.
  return
   isRoot ?
   Orientation.Horizontal :
   Orientation.Vertical;
 }

 public object ConvertBack( 
  object value, Type targetType, object parameter, CultureInfo culture )
 {
  throw new NotSupportedException( "Cannot convert back." );
 }
}

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Josh Smith
Software Developer (Senior) Cynergy Systems
United States United States
Member
Josh creates software, for iOS and Windows.
 
He works at Cynergy Systems as a Senior Experience Developer.
 
Read his iOS Programming for .NET Developers[^] book to learn how to write iPhone and iPad apps by leveraging your existing .NET skills.
 
Use his Master WPF[^] app on your iPhone to sharpen your WPF skills on the go.
 
Check out his Advanced MVVM[^] book.
 
Visit his WPF blog[^] or stop by his iOS blog[^].

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
QuestionBeautifulmembervinchenzo.93928 Mar '13 - 3:21 
Just simple and functionnal
Very elegant approch
QuestionRemove Header FocusmemberDemonPT29 Aug '11 - 7:43 
Hi, nice article you have there. But you don't explain how to remove the doted square over the selected header of the expander. Does anyone know how to remove it?
I tried < Expander ... FocusVisualStyle = "{x:Null}"/> Nothing
Also tried < Expander.Header>
< Setter Property="FocusVisualStyle" Value="{x:Null}" /> ....
 
Nothing.
 
Can anyone help me here?
AnswerRe: Remove Header Focusmembervinchenzo.93928 Mar '13 - 3:26 
IsTabStop="False"
GeneralMy vote of 5memberVano Maisuradze18 Aug '11 - 23:18 
Excellent! Thanks for sharing.
QuestionQuick questionmemberJPR197821 Jun '11 - 5:57 
I want to do something similar to this but have the items horizontal and stretch them so they take up the width of the treeview. The horizontal bit is fairly straight forward but how can you stretch the TreeViewItems?
QuestionNode expansionmemberPaull7823 Apr '10 - 5:15 
First of all many thanks for this beautiful tutorial!
 
I have one question, is it possible to expand the nodes only when clicking on the expander arrow icon? Now the expansion happens even if I click in any point of the header row.
 
I coudn't manage to understand how to do it in a simple way...
 
Thanks in advance for any help!
Paolo
Generalgoodgroupzhujinlong1984091316 Apr '09 - 20:39 
good
GeneralJust came accross itmemberDmitri Nesteruk14 Dec '08 - 10:29 
Wow, this is precisely what I needed! Thanks! Off to experiment...
GeneralNeed your help.memberDharmender18 Nov '08 - 19:49 
Hey josh,
 
The article is seriously productive. I am new to WPF and loaded with a task which is some what you have done Confused | :confused: .
 
Could you help me on the following points.
 
1. Adding drag and drop functionality to move a node from one node to another in your existing Advanced TreeView control.
 
2. How to access other controls inside the node.
 

Looking forward for your positive reply.
 
Thanks
DG
QuestionCan this be applied to a TreeView with HierarchicalDataTemplate?memberjaschwa@clearwire.net16 Oct '08 - 8:50 
Great article, like always. You WPF Disciple guys write some of the most helpful stuff on the 'Net.
 
I tried to apply this article to a TreeView which uses HierarchicalDataTemplates but couldn't get it to work. Can it be done and how?
 
Thanks.
 
Jeff S.
GeneralEventsmemberPascal Ganaye19 Aug '08 - 22:54 
First I must admit I had a look at this code yesterday and I found it incredible.
Then I slept on it, and I am sill in awe.
 
I am trying to use this code in my work project and it showed my data datastructue exactly how I intended.
 
I have an issue however with the TreeViewItem.Expanded events.
 
<TreeView ItemsSource="{Binding}"
TreeView.SelectedItemChanged="TreeView_SelectedItemChanged"
TreeViewItem.Expanded="TreeView_Expanded">
 
The SelectedItemChanged event fires but the Expanded event fires only sometime when I manually select and item in the tree.
 
I am really an absolute beginner in WPF, can you give some insight on what's required to make it work?
GeneralRe: EventsmvpJosh Smith25 Aug '08 - 2:22 
Pascal Ganaye wrote:
The SelectedItemChanged event fires but the Expanded event fires only sometime when I manually select and item in the tree.

 
The Expanded event will not be raised when you select an item. It should only be raised when an item changes state from being collapsed to expanded. I have never seen a bug regarding that functionality in TreeView.
 
:josh:
My WPF Blog[^]
Sleep is overrated.

GeneralRe: EventsmemberMember 178098912 Feb '09 - 2:40 
Are you sure there is no bug?
 
If I try to add event TreeViewItem.Expanded="tree_Expanded"
To your treeview It is sometimes fired
then i expand the expander sometime don't.. Why is what?
I really need this and can't understand this strange behavior :/

QuestionExpansion of a tree also meaning selection?membermikekreuzer24 Mar '08 - 12:54 
Is there a way to get the expansion of a tree node to also mean that node's selection? (You don't have any nodes that are both items & parents in your example, but thta's a pretty common scenario).
 
Using forms I suppose I'd get Exp to not mark click messages as handled, but in WPF ... some sort of trigger maybe? But where? The template has me stumped.
 
Thanks if you have the time and the inclination - Mike
GeneralRe: Expansion of a tree also meaning selection?mvpJosh Smith25 Mar '08 - 3:37 
mikekreuzer wrote:
Is there a way to get the expansion of a tree node to also mean that node's selection?

 
You might want to try binding the TreeViewItem's IsSelected property to its IsExpanded property.
 

mikekreuzer wrote:
You don't have any nodes that are both items & parents in your example

 
I'm not sure what you mean by that. Each "bucket" in the example is a TreeViewItem.
 
:josh:
My WPF Blog[^]
All of life is just a big rambling blog post.

GeneralRe: Expansion of a tree also meaning selection?membermikekreuzer25 Mar '08 - 21:07 
Josh Smith wrote:
mikekreuzer wrote:
Is there a way to get the expansion of a tree node to also mean that node's selection?
 

You might want to try binding the TreeViewItem's IsSelected property to its IsExpanded property.

 
Hm, already tried that, but evidently every node in the selected branch is checked for being expanded, found expanded, and selected in turn ... it's ok, I worked around the issue by making the header slightly more obvious. I might return to the problem later, but it looks like it needs code behind to work, I was hoping for a trick (!).
 
mikekreuzer wrote:
You don't have any nodes that are both items & parents in your example
 
I'm not sure what you mean by that. Each "bucket" in the example is a TreeViewItem.

 
What I meant was -- for example -- the USA and Colorado are parents but have no items to select in themselves, while Aspen, Boulder & Denver have items to select, but no children. Doesn't matter, thanks for the time (& nice article on templates & styles BTW).
 
- Mike
GeneralProblem loading Windows1.xamlmembergeorge.d11 Nov '07 - 4:48 
Trying to open Windows1.xaml in VS 2008:
 
>>The document contains errors that must be fixed before the designer can be loaded.
>>Error 1 Value 'GroupedTreeViewItemStyle.xaml' cannot be assigned to property 'Source'. >>The resource dictionary XAML file has errors and cannot be loaded.

GeneralRe: Problem loading Windows1.xamlmvpJosh Smith11 Nov '07 - 5:06 
I'm not surprised.  I developed that code/XAML way back before WPF was officially released.  I suggest just closing the designer and working directly in the XAML.

 
:josh:
My WPF Blog[^]
Without a strive for perfection I would be terribly bored.

Generallicencingmembervirtual.aussie10 Nov '07 - 12:04 
What is the situation with the licensing? Can I use it for non profitable program?
GeneralRe: licencingmvpJosh Smith10 Nov '07 - 12:21 
You can use it free of charge.  I don't care at all.

 
:josh:
My WPF Blog[^]
Without a strive for perfection I would be terribly bored.

QuestionImage in treeView ?memberbinoo5 Sep '07 - 3:30 
It's a very good work !
 
I'm new in WPF and I try to insert image to replace the text. It's possoble ?
 
In fact I want a diagramm control and maybe the treeView control could help me...
 
Thanks.
 
binoo...

QuestionFill the TreeView widthmemberpikul17 Aug '07 - 3:49 
Is it possible to have the root node stretch his horizontal size to the content size of the treeview and all childs have the same behaviour?

QuestionObject Hierarchy Instead of XMLmemberScott ---22 May '07 - 10:51 
OK. How can I use this with a dynamic object hierarchy?
 
I understand that I cannot use the same kind of binding. If I bind to a collection, I only have one level of objects. If I add my objects the the TreeView.Items, I also only have one level of objects. How can I dynamically build a tree with different types of objects and use a style? Confused | :confused:
 
Scott
AnswerRe: Object Hierarchy Instead of XMLmvpJosh Smith22 May '07 - 11:37 
It all depends on your data model and visual requirements. What exactly is your situation and what problem(s) do you have?
 
:josh:
My WPF Blog[^]
FYI - Bob is a scarecrow who keeps Chuck Norris away from CodeProject.

GeneralRe: Object Hierarchy Instead of XMLmemberScott ---22 May '07 - 12:13 
I am trying to display a collection of organizations. Each organization has a collection of groups, each group has a collection of individuals, individuals have a collection of contacts, a collection of achievements.
- Org1
- groups
- group1
+ person1
- person2
- contacts
- contact1
- contact2
- achievements
- achievement1
+ group2
...
+ Org2
...
 
Your sample uses the XmlDataProvider and the HierarchicalDataTemplate. From what I have read from Microsoft, an object hierarchy like this cannot be bound using the HierarchicalDataTemplate. So, I tried adding the objects directly to the TreeView with myTreeView.Items.Add(org1). But then how do I add group1 as a child of org1? So, first I wrap org1 in a TreeViewItem. Now, I get an empty block (no text displayed) for each root item. I have yet to attempt to add the child items to the root TreeViewItems.
 
I need to take more time to really understand what your code is doing and what is really happening between my c# code and XAML.
 
Sloggin' my way through...
Scott

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Permalink | Advertise | Privacy | Mobile
Web04 | 2.6.130516.1 | Last Updated 29 Jan 2007
Article Copyright 2007 by Josh Smith
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid