Search code examples
wpfcontextmenuhierarchicaldatatemplate

Struggling with HierarchicalDataTemplate in ContextMenu


I'd like to be able to bind the ItemsSource of a ContextMenu to a Collection in my view model, show Separators in the ContextMenu, and the ItemsSource has to be hierarchical (every level of the hierarchy will look the same).

In one of my other questions I managed to be able to show menu items and separators in a data bound ContextMenu, but now I struggle with making the ItemsSource hierarchical.

Right now I don't know what's going on, maybe you can enlighten me?

Here's my code again (simplified to be short, but working):

MenuItemViewModel.vb

Public Class MenuItemViewModel
    Implements ICommand

    Public Event CanExecuteChanged As EventHandler Implements ICommand.CanExecuteChanged

    Public Property IsSeparator As Boolean
    Public Property Caption As String
    Private ReadOnly _subItems As List(Of MenuItemViewModel)

    Public Sub New(createItems As Boolean, level As Byte)
        _subItems = New List(Of MenuItemViewModel)

        If createItems Then
            _subItems.Add(New MenuItemViewModel(level < 4, level + 1) With {.Caption = "SubItem 1"})
            _subItems.Add(New MenuItemViewModel(False, level + 1) With {.IsSeparator = True, .Caption = "SubSep 1"})
            _subItems.Add(New MenuItemViewModel(level < 4, level + 1) With {.Caption = "SubItem 2"})
        End If
    End Sub

    Public ReadOnly Property SubItems As List(Of MenuItemViewModel)
        Get
            Return _subItems
        End Get
    End Property

    Public ReadOnly Property Command As ICommand
        Get
            Return Me
        End Get
    End Property

    Public Sub Execute(ByVal parameter As Object) Implements ICommand.Execute
        MessageBox.Show(Me.Caption)
    End Sub

    Public Function CanExecute(ByVal parameter As Object) As Boolean Implements ICommand.CanExecute
        Return True
    End Function

End Class

The view model for each menu item on each level has a Caption to show in the context menu, an IsSeparator flag to indicate whether it's a separator or a functional menu item, a Command to be bound to when being a functional menu item and of course a SubItems collection containing functional menu items and separators down to a certain hierarchy level.

MainViewModel.vb

Public Class MainViewModel
    Private ReadOnly _items As List(Of MenuItemViewModel)

    Public Sub New()
        _items = New List(Of MenuItemViewModel)
        _items.Add(New MenuItemViewModel(True, 0) With {.Caption = "Item 1"})
        _items.Add(New MenuItemViewModel(False, 0) With {.IsSeparator = True, .Caption = "Sep 1"})
        _items.Add(New MenuItemViewModel(True, 0) With {.Caption = "Item 2"})
        _items.Add(New MenuItemViewModel(True, 0) With {.Caption = "Item 3"})
        _items.Add(New MenuItemViewModel(False, 0) With {.IsSeparator = True, .Caption = "Sep 2"})
        _items.Add(New MenuItemViewModel(True, 0) With {.Caption = "Item 4"})
    End Sub

    Public ReadOnly Property Items As List(Of MenuItemViewModel)
        Get
            Return _items
        End Get
    End Property

End Class

The main view model does only have an Items collection containing functional menu items as well as separators.

MainWindow.xaml

<Window x:Class="MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp3"
        mc:Ignorable="d"
        d:DataContext="{d:DesignInstance local:MainViewModel, IsDesignTimeCreatable=True}"
        Title="MainWindow" Height="450" Width="800">

    <Window.DataContext>
        <local:MainViewModel />
    </Window.DataContext>

    <Window.Resources>
        <ControlTemplate x:Key="mist" TargetType="{x:Type MenuItem}">
            <Separator />
        </ControlTemplate>

        <ControlTemplate x:Key="mict" TargetType="{x:Type MenuItem}">
            <MenuItem Header="{Binding Caption}" Command="{Binding Command}" ItemsSource="{Binding SubItems}" />
        </ControlTemplate>

        <Style x:Key="cmics" TargetType="{x:Type MenuItem}">
            <Setter Property="Template" Value="{StaticResource mict}" />
            <Style.Triggers>
                <DataTrigger Binding="{Binding IsSeparator}" Value="True">
                    <Setter Property="Template" Value="{StaticResource mist}" />
                </DataTrigger>
            </Style.Triggers>
        </Style>
    </Window.Resources>

    <Grid>
        <TextBox HorizontalAlignment="Center" VerticalAlignment="Center" Text="Right click me">
            <TextBox.ContextMenu>
                <ContextMenu ItemsSource="{Binding Items}" ItemContainerStyle="{StaticResource cmics}">
                    <ContextMenu.ItemTemplate>
                        <HierarchicalDataTemplate DataType="{x:Type local:MenuItemViewModel}" ItemsSource="{Binding SubItems}" />
                    </ContextMenu.ItemTemplate>
                </ContextMenu>
            </TextBox.ContextMenu>
        </TextBox>
    </Grid>
</Window>

The window resources contain two ControlTemplates "mist" and "mict" and a Style "cmics" that switches between the two ControlTemplates depending on the value of the IsSeparator flag. This works fine as long as the ItemsSource is not hierarchical (see my other question).

If my Style "cmics" is attached to the ItemContainerStyle of the ContextMenu only (as in my example code) then it looks like this:

enter image description here

The first level works but the others don't. This doesn't change when attaching my Style "cmics" to the ItemContainerStyle of the HierarchicalDataTemplate as well.

If I only attach my Style "cmics" to the HierarchicalDataTemplate then it looks like this:

enter image description here

The first level doesn't show captions and separators, the second level works and the other levels don't work.

So, how can I persuade the ContextMenu to use my Style "cmics" as the ItemContainerStyle for every hierarchy level?


Solution

  • I found an answer here.

    I had to create an empty view model just for the separators and a class that derives from ItemContainerTemplateSelector to return the DataTemplate that belongs to the type of the menu item ("MenuItemViewModel" or "SeparatorViewModel").

    The linked article should be self explanatory.