Search code examples
wpfxamltemplatescontextmenutreeviewitem

Show ContextMenu on whole TreeViewItem line in WPF


I have a TreeView with heterogeneuos node types. Each node type is configuered in XAML using a HierarchicalDataTemplate.

Some of the nodes have a ContextMenu, which is dependent on the type of the node. The ContextMenus are defined as static resources in XAML and attached to a DockPanel in the HierarchicalDataTemplate.

In addition, I am using the ControlTemplate for a TreeViewItem described by bendewey in the following StackOverflow question https://stackoverflow.com/a/672123/1626109. This ControlTemplate is defined so that the complete TreeViewItem is highlighted when it is selected. A left click on any part of the line, selects the item.

In contrast, the context menus, which are defined in the HierarchicalDataTemplate, only work on the right hand part of the line.

I am looking for a way to configure the ContextMenus so that they are also available on the complete line.

This seems to require attaching the context menu to an element in the ControlTemplate, with a TemplateBinding to something defined in the HierarchicalDataTemplate, but I can't figure out how to do it.

By the way, the solution explorer in Visual Studio has exactly this behavior. The context menu is dependent on the node type, and it is available on the complete item, including to the left of the expand/collapse button.


Solution

  • (Edited) You need to get rid of the two columns of the Grid in TreeViewItem Style:

    <Window.Resources>
        <local:LeftMarginMultiplierConverter Length="19" x:Key="lengthConverter" />
        <SolidColorBrush x:Key="GlyphBrush" Color="#444" />
        <Style x:Key="ExpandCollapseToggleStyle" TargetType="ToggleButton">
            <Setter Property="Focusable" Value="False"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="ToggleButton">
                        <Grid Width="15" Height="13"
          Background="Transparent">
                            <Path x:Name="ExpandPath"
            HorizontalAlignment="Left" 
            VerticalAlignment="Center" 
            Margin="1,1,1,1"
            Fill="{StaticResource GlyphBrush}"
            Data="M 4 0 L 8 4 L 4 8 Z"/>
                        </Grid>
                        <ControlTemplate.Triggers>
                            <Trigger Property="IsChecked"
               Value="True">
                                <Setter Property="Data"
                TargetName="ExpandPath"
                Value="M 0 4 L 8 4 L 4 8 Z"/>
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
        <Style x:Key="TreeViewItemFocusVisual">
            <Setter Property="Control.Template">
                <Setter.Value>
                    <ControlTemplate>
                        <Border>
                            <Rectangle Margin="0,0,0,0"
                 StrokeThickness="5"
                 Stroke="Black"
                 StrokeDashArray="1 2"
                 Opacity="0"/>
                        </Border>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
        <Style x:Key="{x:Type TreeViewItem}"
     TargetType="{x:Type TreeViewItem}">
            <Setter Property="Background"
      Value="Transparent"/>
            <Setter Property="HorizontalContentAlignment"
      Value="{Binding Path=HorizontalContentAlignment,
              RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"/>
            <Setter Property="VerticalContentAlignment"
      Value="{Binding Path=VerticalContentAlignment,
              RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"/>
            <Setter Property="Padding"
      Value="1,0,0,0"/>
            <Setter Property="Foreground"
      Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
            <Setter Property="FocusVisualStyle"
      Value="{StaticResource TreeViewItemFocusVisual}"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type TreeViewItem}">
                        <ControlTemplate.Resources>
                            <local:LeftMarginMultiplierConverter Length="19" x:Key="lengthConverter" /> 
                        </ControlTemplate.Resources>
                        <StackPanel>
                            <Border Name="Bd"
              Background="{TemplateBinding Background}"
              BorderBrush="{TemplateBinding BorderBrush}"
              BorderThickness="{TemplateBinding BorderThickness}"
              Padding="{TemplateBinding Padding}">
                                <Grid>
                                    <ToggleButton Panel.ZIndex="2" x:Name="Expander"
                                                  HorizontalAlignment="Left"
                  Style="{StaticResource ExpandCollapseToggleStyle}"
                  IsChecked="{Binding Path=IsExpanded,
                              RelativeSource={RelativeSource TemplatedParent}}"
                  ClickMode="Press" 
                                                  Margin="{Binding  Converter={StaticResource lengthConverter}, ConverterParameter=0,
                              RelativeSource={RelativeSource TemplatedParent}}"/>
    
                                    <ContentPresenter x:Name="PART_Header" ContentSource="Header"
                      HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}">
                                    </ContentPresenter>
                                </Grid>
                            </Border>
                            <ItemsPresenter x:Name="ItemsHost" />
                        </StackPanel>
                        <ControlTemplate.Triggers>
                            <Trigger Property="IsExpanded"
               Value="false">
                                <Setter TargetName="ItemsHost"
                Property="Visibility"
                Value="Collapsed"/>
                            </Trigger>
                            <Trigger Property="HasItems"
               Value="false">
                                <Setter TargetName="Expander"
                Property="Visibility"
                Value="Hidden"/>
                            </Trigger>
                            <MultiTrigger>
                                <MultiTrigger.Conditions>
                                    <Condition Property="HasHeader"
                     Value="false"/>
                                    <Condition Property="Width"
                     Value="Auto"/>
                                </MultiTrigger.Conditions>
                                <Setter TargetName="PART_Header"
                Property="MinWidth"
                Value="75"/>
                            </MultiTrigger>
                            <MultiTrigger>
                                <MultiTrigger.Conditions>
                                    <Condition Property="HasHeader"
                     Value="false"/>
                                    <Condition Property="Height"
                     Value="Auto"/>
                                </MultiTrigger.Conditions>
                                <Setter TargetName="PART_Header"
                Property="MinHeight"
                Value="19"/>
                            </MultiTrigger>
                            <Trigger Property="IsSelected"
               Value="true">
                                <Setter TargetName="Bd"
                Property="Background"
                Value="{DynamicResource {x:Static SystemColors.HighlightBrushKey}}"/>
                                <Setter Property="Foreground"
                Value="{DynamicResource {x:Static SystemColors.HighlightTextBrushKey}}"/>
                            </Trigger>
                            <MultiTrigger>
                                <MultiTrigger.Conditions>
                                    <Condition Property="IsSelected"
                     Value="true"/>
                                    <Condition Property="IsSelectionActive"
                     Value="false"/>
                                </MultiTrigger.Conditions>
                                <Setter TargetName="Bd"
                Property="Background"
                Value="{DynamicResource {x:Static SystemColors.ControlBrushKey}}"/>
                                <Setter Property="Foreground"
                Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
                            </MultiTrigger>
                            <Trigger Property="IsEnabled"
               Value="false">
                                <Setter Property="Foreground"
                Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </Window.Resources>
    

    However, this will distort the Header-Items arrangements. Therefore, you need to adjust the Margin in the HierarchicalDataTemplate part:

    <TreeView Margin="50" HorizontalContentAlignment="Stretch" DataContext="{Binding}" ItemsSource="{Binding Models}">
        <TreeView.ItemTemplate>
            <HierarchicalDataTemplate DataType="{x:Type local:Model}" ItemsSource="{Binding Models}">
                <Border Background="Transparent">
                    <TextBlock Margin="{Binding  Converter={StaticResource lengthConverter}, ConverterParameter=1,
                                                                     RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=TreeViewItem}}" 
                               Text="{Binding Name}" />
                    <Border.ContextMenu>
                        <ContextMenu>
                            <MenuItem Header="dddd"/>
                        </ContextMenu>
                    </Border.ContextMenu>
                </Border>
            </HierarchicalDataTemplate>
        </TreeView.ItemTemplate>
    </TreeView>
    

    Note that you should adjusted the converter to take the required extra margin into account:

    public class LeftMarginMultiplierConverter : IValueConverter
    {
        public double Length { get; set; } 
    
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            var item = value as TreeViewItem;
            if (item == null)
                return new Thickness(0);
            int extra = int.Parse(parameter.ToString());
            var t = item.GetDepth();
            return new Thickness(Length * (item.GetDepth() + extra), 0, 0, 0);
        }
    
        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new System.NotImplementedException();
        }
    }
    

    If converter parameter is 1, it will add one point to the depth of the item. I use it for the Header part.

    Another approach might be to add a DataTemplate to the ContentPresenter of the TreeViewItem Style. While I don't know how you can bind the ContextMenu, I prefer this one.