wpfcontextmenumenuitem

MenuItem SubMenu appearing on left hand side of ContextMenu instead of default


This question relates to the post here - the original question was resolved, but then another cropped up.

As you can see the MenuItem is appearing the wrong side

ContextMenu (in app)

As the prior issue was related to another control library I tested it with a minimal example...

ContextMenu - Minimal

Opened a new MVC project targeting .NET 8. Gave the grid on the MainWindow a name and then added a ToolBar with a button and a DataGrid. Applied the ContextMenu to each in turn and got the same behavior.

Private Sub MainWindow_Initialized(sender As Object, e As EventArgs) Handles Me.Initialized

For i As Integer = 0 To 2
    Dim vRow As New RowDefinition
    If i = 1 Then
        vRow.Height = New GridLength(35, GridUnitType.Star)


    Else
        vRow.Height = New GridLength(35)
    End If
    TestGrid.RowDefinitions.Add(vRow)
Next

Dim vToolBar As New ToolBar
With vToolBar

End With
Grid.SetRow(vToolBar, 0)
TestGrid.Children.Add(vToolBar)

Dim vButton As New Button
With vButton
    .Content = "Test"
    .ContextMenu = ReturnContexMenu()
End With
vToolBar.Items.Add(vButton)

Dim vDG As New DataGrid
With vDG
    '  .ContextMenu = ReturnContexMenu()
End With
Grid.SetRow(vDG, 1)
TestGrid.Children.Add(vDG)

End Sub 

...and this is the ContextMenu

Private Function ReturnContexMenu() As ContextMenu
Try
    Dim FlagMI As New MenuItem
    With FlagMI
        .Header = "Quick Flag"
        .Name = "FlagMI"
    End With

    Dim MarkReadMI As New MenuItem
    With MarkReadMI
        .Header = "Mark as read"
        .Name = "MarkReadMI"
    End With

    Dim MarkUnreadMI As New MenuItem
    With MarkUnreadMI
        .Header = "Mark as unread"
        .Name = "MarkUnreadMI"
    End With

    Dim MarkAsMI As New MenuItem
    With MarkAsMI
        .Header = "Mark as"
        .Items.Add(MarkReadMI)
        .Items.Add(MarkUnreadMI)
    End With

    Dim vMainContext As New ContextMenu
    With vMainContext
        .Name = "MainContextMenu"
        .Items.Add(FlagMI)
        .Items.Add(MarkAsMI)
    End With

    Return vMainContext
Catch ex As Exception

    Return Nothing
End Try
End Function

I have tried adding a class for MenuItem and adding the DependencyProperty of Primatives.PlacementMode and setting the default value to PlacementMode.Right - in fact I tried every setting, but it has no effect.

Public Class MIx
Inherits System.Windows.Controls.MenuItem

Public Shared ReadOnly HoldingItem As DependencyProperty = DependencyProperty.Register(
    name:="IsHoldingItem", propertyType:=GetType(Primitives.PlacementMode), ownerType:=GetType(MIx),
    typeMetadata:=New FrameworkPropertyMetadata(defaultValue:=PlacementMode.Right))

Protected Overrides Sub OnInitialized(e As EventArgs)
    MyBase.OnInitialized(e)
    Style = Nothing



End Sub

Public Property IsHoldingItem As Primitives.PlacementMode
    Get
        Return GetValue(HoldingItem)
    End Get
    Set(value As Primitives.PlacementMode)
        SetValue(HoldingItem, value)
    End Set
End Property
End Class

==========EDIT========

With a big thanks to @BionicCode for all his hard work, it now works

ContextMenu working

Just in case someone else runs into the same problem onApplyTemplate is...

 Public Overrides Sub onApplyTemplate()
 MyBase.OnApplyTemplate()
 Me.PART_Popup = TryCast(GetTemplateChild("PART_Popup"), Popup)
 If Not (Me.PART_Popup) Is Nothing Then
     Me.PART_Popup.CustomPopupPlacementCallback = New CustomPopupPlacementCallback(AddressOf OnChildItemHostPlacementChanged)
     Me.PART_Popup.Placement = PlacementMode.Custom
 End If
 End Sub

Solution

  • Your issue is not reproducible. However, I believe it can be fixed for your special scenario when you manually control the placement of the internal Popup by setting Popup.Placement to PlacementMode.Custom:

    PositionableMenuItem.vb
    We extend MenuItem to gracefully handle the custom positioning of the subitems.
    The custom PositionableMenuItem retrieves its internal Popup and sets the Popup.Placement property to PlacemmentMode.Custom. This enables the Popup to invoke the Popup.CustomPopupPlacementCallback that we define in order to manually align the Popup (the sub-menu items).

    Use the new PositionableMenuItem.ChildPlacement property to allow the client to select the position (currently only left, top, right and bottom are supported) - see example below.

    Public Class PositionableMenuItem
        Inherits MenuItem
    
        Public Shared ReadOnly ChildPlacementProperty As DependencyProperty = DependencyProperty.Register(
            "ChildPlacement", 
            GetType(PlacementMode), 
            GetType(PositionableMenuItem), 
            New FrameworkPropertyMetadata(PlacementMode.Right))
    
        Public Property ChildPlacement As PlacementMode
            Get
                Return CType(GetValue(ChildPlacementProperty), PlacementMode)
            End Get
            Set(ByVal value As PlacementMode)
                Return SetValue(ChildPlacementProperty, value)
            End Set
        End Property
    
        Private Property PART_Popup As Popup
    
        Public Overrides Sub onApplyTemplate()
          MyBase.OnApplyTemplate()
          Me.PART_Popup = TryCast(GetTemplateChild("PART_Popup"), Popup)
          If Not (Me.PART_Popup) Is Nothing Then
            Me.PART_Popup.CustomPopupPlacementCallback = New CustomPopupPlacementCallback(AddressOf OnChildItemHostPlacementChanged)
            Me.PART_Popup.Placement = PlacementMode.Custom
          End If
        End Sub
    
        Private Function OnChildItemHostPlacementChanged(ByVal popupSize As Size, ByVal targetSize As Size, ByVal offset As Point) As CustomPopupPlacement()
            Dim parent = CType(Me.Parent, Control)
            Dim parentPadding As Thickness = parent.Padding
            Dim additionalTargetWidth As Double = parentPadding.Left + parentPadding.Right + parent.BorderThickness.Right
            Dim additionalTargetHeight As Double = parentPadding.Top + parentPadding.Bottom + parent.BorderThickness.Bottom
            Dim horizontalOffset As Double = 0
            Dim verticalOffset As Double = 0
    
            Select Case Me.ChildPlacement
                Case PlacementMode.Bottom
                    verticalOffset = targetSize.Height + additionalTargetHeight
                    horizontalOffset -= parentPadding.Left
                Case PlacementMode.Right
                    horizontalOffset = targetSize.Width + additionalTargetWidth
                Case PlacementMode.Left
                    horizontalOffset -= popupSize.Width + parentPadding.Left
                Case PlacementMode.Top
                    verticalOffset = -popupSize.Height
                    horizontalOffset = -parentPadding.Left
                Case Else
                    Throw New NotSupportedException()
            End Select
    
            Dim location = New Point(horizontalOffset, verticalOffset)
            Dim primaryAxis As PopupPrimaryAxis = If(horizontalOffset > 0, PopupPrimaryAxis.Horizontal, PopupPrimaryAxis.Vertical)
            Dim placement = New CustomPopupPlacement(location, primaryAxis)
            Return {placement}
        End Function
    
        Protected Overrides Sub OnInitialized(ByVal e As EventArgs)
            MyBase.OnInitialized(e)
            Me.Style = Nothing
        End Sub
    End Class
    

    Usage example

    You can use PositionableMenuItemto replace all MenuItem elements or only use it for those MenuItem elements that have child items.

    Private Function ReturnContexMenu() As ContextMenu
        Try
            Dim FlagMI As MenuItem = New PositionableMenuItem()
    
            If True Then
                Dim withBlock = FlagMI
                withBlock.Header = "Quick Flag"
                withBlock.Name = "FlagMI"
            End If
    
            Dim MarkReadMI As MenuItem = New PositionableMenuItem()
    
            If True Then
                Dim withBlock = MarkReadMI
                withBlock.Header = "Mark as read"
                withBlock.Name = "MarkReadMI"
            End If
    
            Dim MarkUnreadMI As MenuItem = New PositionableMenuItem()
    
            If True Then
                Dim withBlock = MarkUnreadMI
                withBlock.Header = "Mark as unread"
                withBlock.Name = "MarkUnreadMI"
            End If
    
            Dim MarkAsMI As MenuItem = New PositionableMenuItem() With {
                .ChildPlacement = PlacementMode.Right
            }
    
            If True Then
                Dim withBlock = MarkAsMI
                withBlock.Header = "Mark as"
                withBlock.Items.Add(MarkReadMI)
                withBlock.Items.Add(MarkUnreadMI)
            End If
    
            Dim vMainContext As ContextMenu = New ContextMenu()
    
            If True Then
                Dim withBlock = vMainContext
                withBlock.Name = "MainContextMenu"
                withBlock.Items.Add(MarkAsMI)
                withBlock.Items.Add(FlagMI)
            End If
    
            Return vMainContext
        Catch ex As Exception
            Return Nothing
        End Try
    End Function