wpftreeviewtreeviewitem

WPF Keep only one TreeViewItem expanded if it is selected, collapse the others unless it is the ancestor of the Selected TreeViewItem


I have a TreeViewItem with custom Style, which I want to have a behavior like the following, so I want to set it as a side menu in my WPF application. The problem is that I want to keep only one TreeViewItem as expanded, while the others are collapsed, as long as they are not ancestors of the selected one. For example if I select, Cat, within Mammal, Mammal should obviously be expanded as well, but not Insects. If I select Africa, Africa and Continents must be expanded, while America, Europe and Asia must be collapsed. Also something strange happens, the SelectedItemChanged event is not firing, so the IsSelected property is not changing when selecting a new TreeViewItem, so I leave the style that I apply to the TreeViewItem.

Treeview with TreeViewItems how looks like

Custom Styles

        <Style x:Key="TreeViewExpanderHeaderStyle" TargetType="{x:Type ToggleButton}">
            <Setter Property="Background" Value="Transparent" />
            <Setter Property="HorizontalAlignment" Value="Stretch" />
            <Setter Property="VerticalAlignment" Value="Center" />
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type ToggleButton}">
                        <Border Background="{TemplateBinding Background}">
                            <Grid>
                                <Grid.ColumnDefinitions>
                                    <ColumnDefinition Width="auto"/>
                                    <ColumnDefinition Width="*"/>
                                    <ColumnDefinition Width="Auto"/>
                                </Grid.ColumnDefinitions>
                                <ContentPresenter Grid.Column="1" Content="{TemplateBinding Content}"
                                              ContentTemplate="{TemplateBinding ContentTemplate}"
                                              ContentStringFormat="{TemplateBinding ContentStringFormat}"
                                              ContentTemplateSelector="{TemplateBinding ContentTemplateSelector}"
                                              VerticalAlignment="Center"
                                              Margin="0,0,16,0" />
                                <ToggleButton Grid.Column="2"
                                          VerticalAlignment="Center"
                                          Foreground="{TemplateBinding Foreground}" x:Name="Expander" Visibility="{Binding HasItems, Converter={StaticResource BoolToVisibilityConverter}, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type TreeViewItem}}}"
                                          IsChecked="{Binding Path=IsChecked, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}">
                                    <ToggleButton.Style>
                                        <Style TargetType="{x:Type ToggleButton}">
                                            <Setter Property="Template">
                                                <Setter.Value>
                                                    <ControlTemplate TargetType="{x:Type ToggleButton}">
                                                        <Border Background="Transparent">
<Path Data="M7.41,8.58L12,13.17L16.59,8.58L18,10L12,16L6,10L7.41,8.58Z" Opacity=".38" x:Name="ExpandPath" RenderTransformOrigin="0.5,0.5" Height="24" Width="24" Fill="{TemplateBinding Foreground}"/>

                                                        </Border>
                                                    </ControlTemplate>
                                                </Setter.Value>
                                            </Setter>
                                        </Style>
                                    </ToggleButton.Style>
                                </ToggleButton>
                            </Grid>
                        </Border>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>

        <Style x:Key="MyTreeViewItemFocusVisual">
            <Setter Property="Control.Template">
                <Setter.Value>
                    <ControlTemplate>
                        <Rectangle/>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>

        <Style TargetType="{x:Type TreeViewItem}">
            <Setter Property="BorderThickness" Value="1" />
            <Setter Property="BorderBrush" Value="Gray"/>
            <Setter Property="Background" Value="Transparent"/>
            <Setter Property="HorizontalContentAlignment" Value="{Binding HorizontalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"/>
            <Setter Property="VerticalContentAlignment" Value="{Binding VerticalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"/>
            <Setter Property="Padding" Value="8" />
            <Setter Property="FocusVisualStyle" Value="{StaticResource MyTreeViewItemFocusVisual}"/>
            <Setter Property="Foreground" Value="Black"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type TreeViewItem}">
                        <Border BorderThickness="{TemplateBinding BorderThickness}" BorderBrush="{TemplateBinding BorderBrush}">
                            <VisualStateManager.VisualStateGroups>
                                <VisualStateGroup x:Name="SelectionStates">
                                    <VisualStateGroup.Transitions>
                                        <VisualTransition GeneratedDuration="0:0:0.6"/>
                                    </VisualStateGroup.Transitions>
                                    <VisualState Name="Selected">
                                        <Storyboard>
                                            <DoubleAnimation Storyboard.TargetName="HeaderSite"
                                                         Storyboard.TargetProperty="Opacity"
                                                         To="0.18" Duration="0"/>
                                        </Storyboard>
                                    </VisualState>
                                    <VisualState Name="Unselected"/>
                                </VisualStateGroup>
                                <VisualStateGroup x:Name="ExpansionStates">
                                    <VisualStateGroup.Transitions>
                                        <VisualTransition GeneratedDuration="0" To="Expanded">
                                            <VisualTransition.GeneratedEasingFunction>
                                                <CubicEase EasingMode="EaseOut"/>
                                            </VisualTransition.GeneratedEasingFunction>
                                            <Storyboard>
                                                <ObjectAnimationUsingKeyFrames Storyboard.TargetName="ItemsHost" Storyboard.TargetProperty="Visibility">
                                                    <DiscreteObjectKeyFrame KeyTime="0" Value="{x:Static Visibility.Visible}" />
                                                </ObjectAnimationUsingKeyFrames>
                                                <DoubleAnimation Storyboard.TargetProperty="Opacity" Storyboard.TargetName="ItemsHost" To="1" Duration="0:0:0.3"/>
                                            </Storyboard>
                                        </VisualTransition>
                                        <VisualTransition GeneratedDuration="0" To="Collapsed">
                                            <VisualTransition.GeneratedEasingFunction>
                                                <CubicEase EasingMode="EaseOut"/>
                                            </VisualTransition.GeneratedEasingFunction>
                                            <Storyboard>
                                                <ObjectAnimationUsingKeyFrames Storyboard.TargetName="ItemsHost" Storyboard.TargetProperty="Visibility">
                                                    <DiscreteObjectKeyFrame KeyTime="0:0:0.3" Value="{x:Static Visibility.Collapsed}" />
                                                </ObjectAnimationUsingKeyFrames>
                                                <DoubleAnimation Storyboard.TargetProperty="Opacity" Storyboard.TargetName="ItemsHost" To="0" Duration="0:0:0.3"/>
                                            </Storyboard>
                                        </VisualTransition>
                                    </VisualStateGroup.Transitions>
                                    <VisualState x:Name="Expanded">
                                        <Storyboard>
                                            <DoubleAnimation Storyboard.TargetProperty="Opacity" Storyboard.TargetName="ItemsHost" To="1" Duration="0"/>
                                            <ObjectAnimationUsingKeyFrames Storyboard.TargetName="ItemsHost" Storyboard.TargetProperty="Visibility">
                                                <DiscreteObjectKeyFrame KeyTime="0" Value="{x:Static Visibility.Visible}" />
                                            </ObjectAnimationUsingKeyFrames>
                                        </Storyboard>
                                    </VisualState>
                                    <VisualState x:Name="Collapsed">
                                        <Storyboard>
                                            <DoubleAnimation Storyboard.TargetProperty="Opacity" Storyboard.TargetName="ItemsHost" To="0" Duration="0"/>
                                            <ObjectAnimationUsingKeyFrames Storyboard.TargetName="ItemsHost" Storyboard.TargetProperty="Visibility">
                                                <DiscreteObjectKeyFrame KeyTime="0" Value="{x:Static Visibility.Hidden}" />
                                            </ObjectAnimationUsingKeyFrames>
                                        </Storyboard>
                                    </VisualState>
                                </VisualStateGroup>
                            </VisualStateManager.VisualStateGroups>
                            <DockPanel Background="{TemplateBinding Background}">
                                <ToggleButton Name="HeaderSite"
                                              DockPanel.Dock="Top"
                                              BorderThickness="0" Cursor="Hand"
                                              IsChecked="{Binding Path=IsExpanded,RelativeSource={RelativeSource TemplatedParent}}"
                                              Style="{StaticResource TreeViewExpanderHeaderStyle}"
                                              Opacity=".87"
                                              Foreground="{TemplateBinding Foreground}"
                                              Content="{TemplateBinding Header}"
                                              ContentTemplate="{TemplateBinding HeaderTemplate}"
                                              ContentTemplateSelector="{TemplateBinding HeaderTemplateSelector}"
                                              ContentStringFormat="{TemplateBinding HeaderStringFormat}"/>
                                <Border Name="ContentSite" DockPanel.Dock="Bottom">
                                    <StackPanel x:Name="ItemsPanel" Margin="10 0 0 0">
                                        <StackPanel.Height>
                                            <MultiBinding Converter="{StaticResource MathMlpMultipleConverter}">
                                                <Binding ElementName="ItemsHost" Path="ActualHeight"/>
                                                <Binding ElementName="ItemsHost" Path="Opacity"/>
                                            </MultiBinding>
                                        </StackPanel.Height>
                                        <ItemsPresenter x:Name="ItemsHost" VerticalAlignment="Top" Opacity="0" Visibility="Collapsed"/>
                                    </StackPanel>
                                </Border>
                            </DockPanel>
                        </Border>
                        <ControlTemplate.Triggers>
                            <Trigger Property="IsSelected" Value="True">
                                <Setter Property="TextElement.Foreground" Value="Red"/>
                            </Trigger>
                            <Trigger Property="IsEnabled" Value="false">
                                <Setter Property="Opacity" Value=".56"/>
                            </Trigger>
                            
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
            <Style.Triggers>

            </Style.Triggers>
        </Style>

and the TreeView

        <TreeView SelectedItemChanged="TreeView_SelectedItemChanged">
            <TreeViewItem Header="Animals">
                <TreeViewItem Header="Mammals">
                    <TreeViewItem Header="Cat"/>
                    <TreeViewItem Header="Dog"/>
                    <TreeViewItem Header="Horse"/>
                </TreeViewItem>
                <TreeViewItem Header="Insects">
                    <TreeViewItem Header="Fly"/>
                    <TreeViewItem Header="Wasp"/>
                    <TreeViewItem Header="Bee"/>
                </TreeViewItem>
            </TreeViewItem>
            <TreeViewItem Header="Continents">
                <TreeViewItem Header="Africa">
                    <TreeViewItem Header="Angola"/>
                    <TreeViewItem Header="Congo"/>
                    <TreeViewItem Header="Egypth"/>
                    <TreeViewItem Header="S. Africa"/>
                </TreeViewItem>
                <TreeViewItem Header="America">
                    <TreeViewItem Header="USA"/>
                    <TreeViewItem Header="Canada"/>
                    <TreeViewItem Header="Mexico"/>
                    <TreeViewItem Header="Brazil"/>
                </TreeViewItem>
                <TreeViewItem Header="Europe">
                    <TreeViewItem Header="UK"/>
                    <TreeViewItem Header="Spain"/>
                    <TreeViewItem Header="France"/>
                    <TreeViewItem Header="Italy"/>
                </TreeViewItem>
                <TreeViewItem Header="Asia">
                    <TreeViewItem Header="China"/>
                    <TreeViewItem Header="Korea"/>
                    <TreeViewItem Header="Japan"/>
                    <TreeViewItem Header="Viet Nam"/>
                </TreeViewItem>
            </TreeViewItem>
        </TreeView>


Solution

  • I recommend to use this simple snippet, a simple global style, which can be converted as a Behavior

    <Window.Resources>
        <Style TargetType="{x:Type TreeViewItem}">
            <EventSetter Event="Expanded" Handler="TreeViewItem_Expanded"/>
        </Style>
    </Window.Resources>
    

    And a handler for the EventSetter

    private void TreeViewItem_Expanded(object sender, RoutedEventArgs e)
    {
        var trvi = sender as TreeViewItem;
        //Using pattern matching
        //If parent is an ItemsControl and it's the current expanded item
        //That's a generic way to achieve such behavior
        if(trvi is {Parent: ItemsControl parent, IsExpanded: true })
            foreach (TreeViewItem item in parent.Items)
                item.IsExpanded = false;
    }
    

    UPDATE

    Since you asked to collapse the entire branch after select another branch, the you should walk & collapse recursively the nodes

    private void TreeViewItem_Expanded(object sender, RoutedEventArgs e)
    {
        var trvi = sender as TreeViewItem;
        if (trvi is { Parent: ItemsControl parent, IsExpanded: true })
            foreach (TreeViewItem item in parent.Items)
                if (item != trvi && item.IsExpanded)
                    CollapseBranch(item);
    
    
    }
    
    private static void CollapseBranch(TreeViewItem trvi)
    {
        trvi.IsExpanded = false;
    
        foreach (TreeViewItem item in trvi.Items)
            CollapseBranch(item);
    }