65.9K
CodeProject is changing. Read more.
Home

Custom TreeView Layout in WPF

starIconstarIconstarIconstarIconstarIcon

5.00/5 (4 votes)

Sep 12, 2014

CPOL

2 min read

viewsIcon

27913

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