Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Custom TreeView Layout in WPF

0.00/5 (No votes)
12 Sep 2014 1  
This is an alternative for Custom TreeView Layout in WPF

Introduction

I needed to draw lines between the nodes and collapsing/expanding of the nodes. Turned out that collapsing/expanding was very easy to implement.

Drawing lines was a bit more difficult.

The solution works perfect but I’m sure there is a simpler approach to this problem. Anyway, if someone needs a solution, this one works.

The treeview:

Using the Code

Below is the ControlTemplate.

There are 3 rows in the first Grid.

  • The first one is for the horizontal line(s).
  • the second one for the UserControl.
  • The last one holds the ItemsPresenter.
<ControlTemplate TargetType="TreeViewItem">
                            <Grid>
                                <Grid.RowDefinitions>
                                    <RowDefinition Height="Auto" />
                                    <RowDefinition Height="Auto" />
                                    <RowDefinition Height="*" />
                                </Grid.RowDefinitions>
                                <Grid Grid.Row="0">
                                    <Grid.ColumnDefinitions>
                                        <ColumnDefinition Width="1*"></ColumnDefinition>
                                        <ColumnDefinition Width="1*"></ColumnDefinition>
                                    </Grid.ColumnDefinitions>
                                    <Line Grid.Column="0" SnapsToDevicePixels="True" 
                                    Visibility="{Binding  ., UpdateSourceTrigger=PropertyChanged, 
                                    Converter={c:IsFlowElementFrom_ToLeftLineVisiblility_Converter}}" 
                                    Grid.Row="0" HorizontalAlignment="Stretch" 
                                    VerticalAlignment="Bottom" Stroke="Black" 
                                    X2="{Binding ActualWidth, RelativeSource={RelativeSource Self}}" 
                                    StrokeThickness="2" />
                                    <Line Grid.Column="1" SnapsToDevicePixels="True" 
                                    Visibility="{Binding  ., UpdateSourceTrigger=PropertyChanged, 
                                    Converter={c:IsFlowElementFrom_ToRigthLineVisiblility_Converter}}" 
                                    Grid.Row="0" HorizontalAlignment="Stretch" 
                                    VerticalAlignment="Bottom" Stroke="Black" 
                                    X2="{Binding ActualWidth, RelativeSource={RelativeSource Self}}" 
                                    StrokeThickness="2" />
                                </Grid>
                                <StackPanel Grid.Row="1" Orientation="Vertical" 
                                HorizontalAlignment="Center">
                                    <f:FlowElementControl FlowElementControl_IsReported_Event=
                                    "FlowElementControl_FlowElementControl_IsReported_Event" 
                                                          FlowElementControl_IsSelected_Event=
                                                          "FlowElementControl_IsSelected_Event"  
                                                          ButtonExpand_ClickEvent=
                                                          "FlowElementControl_ButtonExpand_ClickEvent"  
                                                          ButtonExtraInfo_ClickEvent=
                                                          "FlowElementControl_ButtonExtraInfo_ClickEvent" 
                                                          FlowElementControl_IsUserResponse_Event=
                                                          "FlowElementControl_FlowElementControl_IsUserResponse_Event"
                                                          Visibility="{Binding  .FlowElementFrom.TreeviewItem_IsExpanded, 
                                                          UpdateSourceTrigger=PropertyChanged, 
                                                          Converter={c:IsParentExpandedToVisibility_Converter}}" 
                                                          Tag="{Binding .}"  
                                                          HorizontalAlignment="Center">
                                    </f:FlowElementControl>
                                </StackPanel>
                                <ItemsPresenter Grid.Row="2">
                                </ItemsPresenter>
                            </Grid>
                        </ControlTemplate>

The horizontal line between the nodes are actually two lines. They are both the half length of the first Grid. To do this, we place a new Grid in the first row and give this Grid 2 equally spaced columns (1*) (the width of the first Grid changes according to the nodes below them, so cannot use a fixed width of the lines).

We need two lines in order to hide or show them accordingly to the position of the nodes.

  • If the node is the most left one, we hide the left line. (TreeviewItem_IsFirst = true)
  • If the node is the most right one, we hide right line. (TreeviewItem_IsLast = true)
  • If the node is neither the most left or right, we show both lines. (TreeviewItem_IsFirst = false, TreeviewItem_IsLast = false)

The first and last nodes are set in the database.

SetFirstAndLast()

We have to do this everytime we add, delete, copy or move items.

You can add a sort order if needed.

        private void RecursiveSetFirstLast(Database.FlowElements flowelement)
        {
            List<Database.FlowElements> toList = flowelement.FlowElementsTo.ToList();
            for (int i = 0; i < toList.Count; i++)
            {
                if (i == 0)
                    toList[i].TreeviewItem_IsFirst = true;
                else
                    toList[i].TreeviewItem_IsFirst = false;

                if (i == toList.Count - 1)
                    toList[i].TreeviewItem_IsLast = true;
                else
                    toList[i].TreeviewItem_IsLast = false;

                if (i != 0 & i != toList.Count - 1)
                {
                    toList[i].TreeviewItem_IsFirst = false;
                    toList[i].TreeviewItem_IsLast = false;
                }
                RecursiveSetFirstLast(toList[i]);
            }
        }
        private void SetFirstAndLast()
        {
            //set first and last, so we can draw the treelines properly
            foreach (Database.FlowElements f in flowElementsNotLinked) RecursiveSetFirstLast(f);
        }
flowElementsNotLinked = Currenttemplate.FlowElements.Where(f => f.FlowElementFrom == null).ToList();

Next, we need to set the visibility of the lines. This can be done with a converter: (You need two of them, left and right)

class IsFlowElementFrom_ToLeftLineVisiblility_Converter : System.Windows.Markup.MarkupExtension, IValueConverter
    {
        public IsFlowElementFrom_ToLeftLineVisiblility_Converter()
        {

        }
        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            Database.FlowElements flowelement = (Database.FlowElements)value;
            //do not show the line if there are no elements above
            if (flowelement == null || flowelement.FlowElementFrom == null) return Visibility.Hidden;
            //don't show the left line if it is the first element
            if (flowelement.TreeviewItem_IsFirst) return Visibility.Hidden;
            return Visibility.Visible;
        }
        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            return true;
        }
        private static IsFlowElementFrom_ToLeftLineVisiblility_Converter instance;
        public override object ProvideValue(IServiceProvider serviceProvider)
        {
            if (instance == null)
                instance = new IsFlowElementFrom_ToLeftLineVisiblility_Converter();
            return instance;
        }
    }

Last thing to do is draw two short vertical lines in your usercontrol (top and bottom). And show/hide them based on the elements above or below them. You can use the same principle as above.

Best to replace the bottom line of the control with a menuItem or button with a plus or minus in it. For collapsing and expanding.

<Menu Background="Transparent" HorizontalAlignment="Center" VerticalAlignment="Center">
                <MenuItem x:Name="ButtonExpand" Tag="{Binding .}" Visibility="{Binding ., UpdateSourceTrigger=PropertyChanged, Converter={c:FlowelementsTo_ToButtonVisiblility_Converter}}" IsEnabled="True" Click="ButtonExpand_Click">
                    <MenuItem.Header>
                        <TextBlock FontSize="12" Text="{Binding  ., UpdateSourceTrigger=PropertyChanged, Converter={c:IsExpandedToTextBlockText_Converter}}" xml:space="preserve"></TextBlock>
                    </MenuItem.Header>
                    <MenuItem.Effect>
                        <DropShadowEffect Opacity="0.5" ShadowDepth="4" BlurRadius="8"/>
                    </MenuItem.Effect>
                    <MenuItem.ToolTip>
                        <TextBlock>
                              Collapse/Expand this flowelement.
                        </TextBlock>
                    </MenuItem.ToolTip>
                </MenuItem>
            </Menu>

Points of Interest

I tried some very different approaches, this was the first one that worked. Next try would have been just drawing on the canvas.

During testing, I gave all containers a different color and margin. This shows you what is actually happing and can be very helpful.

History

  • 12th September, 2014: Initial version

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here