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

31 views Asked by At

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
1

There are 1 answers

7
BionicCode On BEST ANSWER

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