Click here to Skip to main content
13,592,561 members
Click here to Skip to main content
Add your own
alternative version

Tagged as

Stats

17.3K views
513 downloads
12 bookmarked
Posted 29 Apr 2016
Licenced CPOL

TabControl With TreeView Navigation

, 29 Aug 2016
Rate this:
Please Sign up or sign in to vote.
A TreeView that displays content when a TreeViewItem is clicked

*Discontinued here and recontinued on CodePlex.

Introduction

The standard WPF TabControl is great for many things except it doesn't provide a means to navigate items via TreeViewItems as opposed to stacked buttons.

I wanted a solution that was fast, efficient, easy to customize, and just as simple to use as a TabControl is.

Enter, TabbedTree.

Preview

Table Of Contents

Using the Code

TabbedTree is an inherited TreeView with some modifications made to the default template. To specify a TreeViewItem's content, the Tag property should be set. In the future, I'd like to define an attached property for specifying content so the Tag property is freed up.

Note, if you download the article version of this control, you will notice MAJOR differences to the CodePlex one. Previously, TreeViewItems weren't supported and TabbedTree contained a TreeView, which made data bindings confusing and logic too complicated. Now, TabbedTree is a TreeView and it accepts TreeViewItems like normal.

Usage

For generic items.

<local:TabbedTree MenuWidth="30*" ContentWidth="70*" 

Margin="15" ContentBorderThickness="1">
    <TreeViewItem Header="Item 1">
        <TreeViewItem.Tag>
            <Label Content="Item 1 Content"/>
        </TreeViewItem.Tag>
        <TreeViewItem Header="Item 4">
            <TreeViewItem.Tag>
                <ComboBox HorizontalAlignment="Left" VerticalAlignment="Top">
                    <ComboBoxItem Content="Apple"/>
                    <ComboBoxItem Content="Orange"/>
                    <ComboBoxItem Content="Banana"/>
                </ComboBox>
            </TreeViewItem.Tag>
        </TreeViewItem>
        <TreeViewItem Header="Item 5">
            <TreeViewItem.Tag>
                <Label Content="Item 5 Content"/>
            </TreeViewItem.Tag>
        </TreeViewItem>
        <TreeViewItem Header="Item 6">
            <TreeViewItem.Tag>
                <TextBox Text="Sample Text" 

                HorizontalAlignment="Left" VerticalAlignment="Top"/>
            </TreeViewItem.Content>
        </TreeViewItem>
        <TreeViewItem Header="Item 7">
            <TreeViewItem.Tag>
                <Label Content="Item 7 Content"/>
            </TreeViewItem.Tag>
        </TreeViewItem>
    </TreeViewItem>
    <TreeViewItem Header="Item 2">
        <TreeViewItem.Tag>
            <Label Content="Item 2 Content"/>
        </TreeViewItem.Tag>
        <TreeViewItem Header="Item 8">
            <TreeViewItem.Tag>
                <Label Content="Item 8 Content"/>
            </TreeViewItem.Tag>
        </TreeViewItem>
        <TreeViewItem Header="Item 9">
            <TreeViewItem.Tag>
                <Label Content="Item 9 Content"/>
            </TreeViewItem.Tag>
        </TreeViewItem>
    </TreeViewItem>
    <TreeViewItem Header="Item 3">
        <TreeViewItem.Tag>
            <Label Content="Item 3 Content"/>
        </TreeViewItem.Tag>
        <TreeViewItem Header="Item 10">
            <TreeViewItem.Tag>
                <Label Content="Item 10 Content"/>
            </TreeViewItem.Tag>
        </TreeViewItem>
    </TreeViewItem>
</local:TabbedTree>

And if you want to bind to a collection.

<Controls.Extended:TabbedTree 

    MaxHeight="300" 

    ScrollViewer.VerticalScrollBarVisibility="Auto" 

    ItemsSource="{Binding TreeViewSource, UpdateSourceTrigger=PropertyChanged}" 

    MenuWidth="30*" 

    ContentWidth="70*" 

    ContentBorderThickness="1" 

    ContentBorderBrush="DarkGray">
    <Controls.Extended:TabbedTree.ItemContainerStyle>
        <Style TargetType="TreeViewItem" BasedOn="{StaticResource {x:Type TreeViewItem}}">
            <Setter Property="IsExpanded" Value="True"/>
        </Style>
    </Controls.Extended:TabbedTree.ItemContainerStyle>
    <Controls.Extended:TabbedTree.ItemTemplate>
        <HierarchicalDataTemplate ItemsSource="{Binding Items}">
            <TextBlock Text="{Binding Name}"/>
        </HierarchicalDataTemplate>
    </Controls.Extended:TabbedTree.ItemTemplate>
    <Controls.Extended:TabbedTree.ContentTemplate>
        <DataTemplate>
            <TextBox Text="{Binding Message}"/>
        </DataTemplate>
    </Controls.Extended:TabbedTree.ContentTemplate>
</Controls.Extended:TabbedTree>

Template

All I did was implement the default template of the TreeView with the addition of a ContentControl. The ContentControl displays the content that corresponds to the TreeViewItem clicked by binding to the selected item of the TreeView.

<Style x:Key="{x:Type local:TabbedTree}" TargetType="{x:Type local:TabbedTree}">
    <Setter Property="ExpandOnClick" Value="False"/>
    <Setter Property="CollapseSiblings" Value="False"/>
    <Setter Property="HorizontalAlignment" Value="Stretch"/>
    <Setter Property="VerticalAlignment" Value="Stretch"/>
    <Setter Property="Padding" Value="10,0,0,0"/>
    <Setter Property="BorderThickness" Value="0"/>
    <Setter Property="Background" Value="Transparent"/>
    <Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Visible"/>
    <Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Visible"/>
    <Setter Property="SnapsToDevicePixels" Value="True" />
    <Setter Property="VirtualizingPanel.IsVirtualizing" Value="True"/>
    <Setter Property="VirtualizingPanel.VirtualizationMode" Value="Recycling"/>
    <Setter Property="ItemsPanel">
        <Setter.Value>
            <ItemsPanelTemplate>
                <VirtualizingStackPanel IsItemsHost="True"/>
            </ItemsPanelTemplate>
        </Setter.Value>
    </Setter>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type local:TabbedTree}">
                <Border

                    Background="{TemplateBinding Background}"

                    BorderThickness="{TemplateBinding BorderThickness}"

                    BorderBrush="{TemplateBinding BorderBrush}"

                    Margin="{TemplateBinding Margin}"

                    Padding="{TemplateBinding Padding}">
                    <Grid>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="{TemplateBinding MenuWidth}"/>
                            <ColumnDefinition Width="{TemplateBinding ContentWidth}"/>
                        </Grid.ColumnDefinitions>
                        <ScrollViewer 

                            VerticalAlignment="Stretch"

                            BorderThickness="{TemplateBinding MenuBorderThickness}" 

                            Background="{TemplateBinding MenuBackground}" 

                            BorderBrush="{TemplateBinding MenuBorderBrush}"

                            Focusable="False" 

                            CanContentScroll="True">
                            <ItemsPresenter Margin="{TemplateBinding Padding}" 

                             HorizontalAlignment="Stretch"/>
                        </ScrollViewer>
                        <Border 

                            Grid.Column="1"

                            Padding="{TemplateBinding ContentPadding}" 

                            BorderThickness="{TemplateBinding ContentBorderThickness}" 

                            Background="{TemplateBinding ContentBackground}" 

                            BorderBrush="{TemplateBinding ContentBorderBrush}">
                            <ContentControl

                                Content="{TemplateBinding SelectedItem}"

                                IsHitTestVisible="False">
                                <ContentControl.Resources>
                                    <DataTemplate x:Key="DefaultContentTemplate">
                                        <ContentPresenter Content="{Binding Tag}"/>
                                    </DataTemplate>
                                </ContentControl.Resources>
                                <ContentControl.Style>
                                    <Style TargetType="ContentControl">
                                        <Style.Triggers>
                                            <DataTrigger Binding="{Binding ContentTemplate, 
                                             RelativeSource={RelativeSource AncestorType=
                                             {x:Type local:TabbedTree}}, 
                                             Converter={StaticResource NullObjectToBooleanConverter}}" 

                                             Value="True">
                                                <Setter Property="ContentTemplate" 

                                                 Value="{Binding ContentTemplate, 
                                                 RelativeSource={RelativeSource AncestorType=
                                                                {x:Type local:TabbedTree}}}"/>
                                            </DataTrigger>
                                            <DataTrigger Binding="{Binding ContentTemplate, 
                                             RelativeSource={RelativeSource AncestorType=
                                             {x:Type local:TabbedTree}}, 
                                             Converter={StaticResource NullObjectToBooleanConverter}}" 

                                             Value="False">
                                                <Setter Property="ContentTemplate" 

                                                 Value="{StaticResource DefaultContentTemplate}"/>
                                            </DataTrigger>
                                        </Style.Triggers>
                                    </Style>
                                </ContentControl.Style>
                            </ContentControl>
                        </Border>
                    </Grid>
                </Border>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

Implementation

Most of the dependency properties defined are concerned with styling for convenience. E.g., you can set the background and border of the TabbedTree as a whole, the ScrollViewer that presents the items (menu), and the ContentControl (content) separately. You can also specify a width for the menu and content columns.

public class TabbedTree : AdvancedTreeView
{
    #region DependencyProperties

    public static DependencyProperty ContentTemplateProperty = 
             DependencyProperty.Register("ContentTemplate", typeof(DataTemplate), 
             typeof(TabbedTree), new FrameworkPropertyMetadata(null, 
             FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
    public DataTemplate ContentTemplate
    {
        get
        {
            return (DataTemplate)GetValue(ContentTemplateProperty);
        }
        set
        {
            SetValue(ContentTemplateProperty, value);
        }
    }

    public static DependencyProperty ContentBackgroundProperty = 
             DependencyProperty.Register("ContentBackground", typeof(Brush), 
             typeof(TabbedTree), new FrameworkPropertyMetadata(default(Brush), 
             FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
    public Brush ContentBackground
    {
        get
        {
            return (Brush)GetValue(ContentBackgroundProperty);
        }
        set
        {
            SetValue(ContentBackgroundProperty, value);
        }
    }

    public static DependencyProperty ContentBorderBrushProperty = 
               DependencyProperty.Register("ContentBorderBrush", typeof(Brush), 
               typeof(TabbedTree), new FrameworkPropertyMetadata(default(Brush), 
               FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
    public Brush ContentBorderBrush
    {
        get
        {
            return (Brush)GetValue(ContentBorderBrushProperty);
        }
        set
        {
            SetValue(ContentBorderBrushProperty, value);
        }
    }

    public static DependencyProperty ContentBorderThicknessProperty = 
              DependencyProperty.Register("ContentBorderThickness", 
              typeof(Thickness), typeof(TabbedTree), 
              new FrameworkPropertyMetadata(default(Thickness), 
              FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
    public Thickness ContentBorderThickness
    {
        get
        {
            return (Thickness)GetValue(ContentBorderThicknessProperty);
        }
        set
        {
            SetValue(ContentBorderThicknessProperty, value);
        }
    }

    public static DependencyProperty ContentPaddingProperty = 
               DependencyProperty.Register("ContentPadding", typeof(Thickness), 
               typeof(TabbedTree), new FrameworkPropertyMetadata(default(Thickness), 
               FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
    public Thickness ContentPadding
    {
        get
        {
            return (Thickness)GetValue(ContentPaddingProperty);
        }
        set
        {
            SetValue(ContentPaddingProperty, value);
        }
    }

    public static DependencyProperty MenuWidthProperty = 
            DependencyProperty.Register("MenuWidth", typeof(GridLength), 
            typeof(TabbedTree), new FrameworkPropertyMetadata(default(GridLength), 
            FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
    public GridLength MenuWidth
    {
        get
        {
            return (GridLength)GetValue(MenuWidthProperty);
        }
        set
        {
            SetValue(MenuWidthProperty, value);
        }
    }

    public static DependencyProperty ContentWidthProperty = 
           DependencyProperty.Register("ContentWidth", typeof(GridLength), 
           typeof(TabbedTree), new FrameworkPropertyMetadata(default(GridLength), 
           FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
    public GridLength ContentWidth
    {
        get
        {
            return (GridLength)GetValue(ContentWidthProperty);
        }
        set
        {
            SetValue(ContentWidthProperty, value);
        }
    }

    public static DependencyProperty MenuBorderThicknessProperty = 
            DependencyProperty.Register("MenuBorderThickness", typeof(Thickness), 
            typeof(TabbedTree), new FrameworkPropertyMetadata(default(Thickness), 
            FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
    public Thickness MenuBorderThickness
    {
        get
        {
            return (Thickness)GetValue(MenuBorderThicknessProperty);
        }
        set
        {
            SetValue(MenuBorderThicknessProperty, value);
        }
    }

    public static DependencyProperty MenuBackgroundProperty = 
           DependencyProperty.Register("MenuBackground", typeof(Brush), 
           typeof(TabbedTree), new FrameworkPropertyMetadata(default(Brush), 
           FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
    public Brush MenuBackground
    {
        get
        {
            return (Brush)GetValue(MenuBackgroundProperty);
        }
        set
        {
            SetValue(MenuBackgroundProperty, value);
        }
    }

    public static DependencyProperty MenuBorderBrushProperty = 
            DependencyProperty.Register("MenuBorderBrush", typeof(Brush), 
            typeof(TabbedTree), new FrameworkPropertyMetadata(default(Brush), 
            FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
    public Brush MenuBorderBrush
    {
        get
        {
            return (Brush)GetValue(MenuBorderBrushProperty);
        }
        set
        {
            SetValue(MenuBorderBrushProperty, value);
        }
    }

    public static DependencyProperty SelectedIndexProperty = 
              DependencyProperty.Register("SelectedIndex", typeof(string), 
              typeof(TabbedTree), new FrameworkPropertyMetadata(default(string), 
              FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnSelectedIndexChanged));
    public string SelectedIndex
    {
        get
        {
            return (string)GetValue(SelectedIndexProperty);
        }
        set
        {
            SetValue(SelectedIndexProperty, value);
        }
    }
    private static void OnSelectedIndexChanged(DependencyObject Object, 
            DependencyPropertyChangedEventArgs e)
    {
        TabbedTree TabbedTree = (TabbedTree)Object;
        TabbedTree.SelectIndex(TabbedTree.SelectedIndex);
    }

    #endregion

    #region TabbedTree

    public TabbedTree() : base()
    {
        this.DefaultStyleKey = typeof(TabbedTree);
    }

    public override void OnApplyTemplate()
    {
        base.ApplyTemplate();

        //If a selected index is not specified, select first by default.
        if (!string.IsNullOrEmpty(this.SelectedIndex))
            this.SelectIndex(this.SelectedIndex);
        else this.SetSelectedIndex(0);
    }

    #endregion

    #region Methods

    public void SetSelectedIndex(params int[] Values)
    {
        string Temp = string.Empty;
        foreach (int i in Values) Temp += i.ToString() + ",";
        Temp = Temp.TrimEnd(',');
        this.SelectedIndex = Temp;
    }

    List<int> GetIndices(string Index)
    {
        if (!string.IsNullOrEmpty(Index))
        {
            List<int> Values = new List<int>();
            string[] OldValues = Index.Split(',');
            try
            {
                for (int i = 0, Count = OldValues.Count(); i < Count; i++)
                    Values.Add(Convert.ToInt32(OldValues[i]));
                return Values;
            }
            catch
            {

            }
        }
        return default(List<int>);
    }

    /// <summary>
    /// An array that represents the index depth.
    /// </summary>
    /// <param name="Index">0-based index.</param>
    void SelectIndex(string Index)
    {
        List<int> Values = this.GetIndices(Index);
        if (Values == null)
            return;
        TreeViewItem Target = null;
        foreach (int i in Values)
        {
            if (Target == null)
            {
                if (this.Items.Count > i)
                    Target = this.Items[i] as TreeViewItem; //Can never be null; 
                                                 //guarentees Target != null after first pass or breaks.
                else break;
            }
            else
            {
                if (Target.Items.Count > i)
                    Target = Target.Items[i] as TreeViewItem;
                else break;
            }
        }
        if (Target != null)
            Target.IsSelected = true;
    }

    #endregion
}

Points of Interest

  • Content is loaded dynamically by binding the selected item of the TreeView to the DataContext of a ContentControl
  • Additional properties allow customizing the column widths, border, and background of the TreeView and content individually

History

  • 4/30/2016
    • Posted
  • 5/10/2016
    • Simplified control names
    • Added support for selecting an index. Index is input as a string of comma-separated integers, which represent the index of each node depth to travel. Will attempt to implement a TypeConverter for this in future
      • For example, SelectedIndex="0,3,2,5" would correspond to the first child's fourth child's third child's sixth child.
    • Enabled support for specifying x:Name attribute on controls contained in TreeItems. Before, this was not possible.
    • Still working on making bindings possible for controls contained in TreeItems. Currently, you'd have to implement a binding proxy and bind that to the DataContext of the TreeItem's immediate child.
    • Usage has changed slightly:
      • TreeItems are now direct descendents of their parents. The TreeItem's content is specified by setting nested Content property.

Future

The code in this article is now part of the open source project, Imagin.NET.

License

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

Share

About the Author

James J M
Software Developer Imagin
United States United States
No Biography provided

You may also be interested in...

Comments and Discussions

 
QuestionGood Efforts But... Pin
MayurDighe6-Sep-16 7:08
professionalMayurDighe6-Sep-16 7:08 
GeneralMy vote of 3 Pin
Member 1236439031-Aug-16 21:29
memberMember 1236439031-Aug-16 21:29 
GeneralRe: My vote of 3 Pin
James JM1-Sep-16 4:31
memberJames JM1-Sep-16 4:31 
GeneralMy vote of 3 Pin
Evgeny Bestfator30-Aug-16 3:48
professionalEvgeny Bestfator30-Aug-16 3:48 
GeneralRe: My vote of 3 Pin
James JM30-Aug-16 7:32
memberJames JM30-Aug-16 7:32 
GeneralRe: My vote of 3 Pin
Evgeny Bestfator30-Aug-16 21:57
professionalEvgeny Bestfator30-Aug-16 21:57 
GeneralRe: My vote of 3 Pin
James JM31-Aug-16 7:12
memberJames JM31-Aug-16 7:12 
Question-------------- Pin
DeathCaller30-Apr-16 2:48
memberDeathCaller30-Apr-16 2:48 
AnswerRe: how to use it Pin
Alex Y30-Apr-16 10:21
memberAlex Y30-Apr-16 10:21 

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

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Cookies | Terms of Use | Mobile
Web01-2016 | 2.8.180618.1 | Last Updated 30 Aug 2016
Article Copyright 2016 by James J M
Everything else Copyright © CodeProject, 1999-2018
Layout: fixed | fluid