0

I like to create my contextmenu's programmatically. I generally do not add the items to the contextmenustrip until it is opening as the items that get displayed are dependent on other aspects of the design that are variable.

I have found that the contextmenustrips seem to require two right clicks to display. I've tried adding the menu items in different events (opening, opened, etc) and also manually setting the contextmenustrip's visibility to true to no avail.

I can't for the life of me figure out why two right clicks are necessary. If you create a blank winforms project and then replace all the code with this, it'll demonstrate the issue.


Public Class Form1
    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles Me.Load
        Dim currContextMenuStrip As New ContextMenuStrip
        Me.ContextMenuStrip = currContextMenuStrip
        AddHandler currContextMenuStrip.Opening, AddressOf ContextMenuStrip1_Opening
    End Sub

    Private Sub ContextMenuStrip1_Opening(sender As Object, e As CancelEventArgs)
        Dim currContextMenuStrip As ContextMenuStrip = sender
        Dim menuTxt As String = "&Find"
        'only add the menu if it doesn't already exist
        If (From f In currContextMenuStrip.Items Where f.text = menuTxt).Count = 0 Then
            Dim newMenuItem As New ToolStripMenuItem
            newMenuItem.Text = menuTxt
            currContextMenuStrip.Items.Add(newMenuItem)
        End If
    End Sub
End Class

EDIT: Just figured out it seems to be connected to the fact that the contextmenustrip doesn't have any items on the first right click. If I add a dummy item, then hide it once other items are added, it works on the first right click. So confused!

This works:

    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles Me.Load
        Dim currContextMenuStrip As New ContextMenuStrip
        Me.ContextMenuStrip = currContextMenuStrip
        AddHandler currContextMenuStrip.Opening, AddressOf ContextMenuStrip1_Opening

        'add a dummy item
        Dim newMenuItem As New ToolStripMenuItem
        newMenuItem.Text = "dummy"
        currContextMenuStrip.Items.Add(newMenuItem)
    End Sub

    Private Sub ContextMenuStrip1_Opening(sender As Object, e As CancelEventArgs)
        Dim currContextMenuStrip As ContextMenuStrip = sender
        Dim menuTxt As String = "&Find"
        'only add the menu if it doesn't already exist
        If (From f In currContextMenuStrip.Items Where f.text = menuTxt).Count = 0 Then
            Dim newMenuItem As New ToolStripMenuItem
            newMenuItem.Text = menuTxt
            currContextMenuStrip.Items.Add(newMenuItem)
        End If

        'hide the dummy item
        Dim items As List(Of ToolStripMenuItem) = (From f As ToolStripMenuItem In currContextMenuStrip.Items Where f.Text = "dummy").ToList
        items.First.visible = False
    End Sub
End Class
Dan
  • 89
  • 6
  • 1
    You should set `Option Strict On`, fix the problem that arise, then describe what this code is meant to do: you have a newly declared (*empty*) ContextMenuStrip, you then attach a new ToolStripMenuItem on-the-fly (while the *empty* menu is being shown, btw without any event handler for the selection). Of course the MenuItem will be presented the next time you show that ContextMenuStrip. – Jimi Jan 22 '21 at 02:20
  • Yes, I do have event handlers for the items in my real code. They seemed to not matter for this behavior so I removed them to keep the example brief. I don't have Option Strict to On in the Compile properties. Would that matter? it created a few errors I didn't know how to fix immediately. – Dan Jan 22 '21 at 02:23
  • 1
    `Option Strict` is clearly `Off`, since you have late binding all over the place. Of course you cannot do this: `Dim currContextMenuStrip As ContextMenuStrip = sender` (and you shouldn't) with the option `On`. This is a very odd way to handle a ContextMenuStrip. Do you really need to check whether you have added those MenuItems each time the menu is opened? Why don't you setup a procedure that adds and removes MenuItems based on conditions you set (if conditions are set, not clear from this code why a MenuItem is added at this time) – Jimi Jan 22 '21 at 02:24
  • 2
    *"So confused"*. It seems fairly clear to me what's happening. It's an optimisation. Let's say that you have a box and you know that the box is empty. If someone asks you to tip all the items in the box onto a table, are you going to go through the charade of opening the box and turning it upside down, only for nothing to fall out? Of course not. You'd just tell them that the box is empty and do nothing. That's what's happening here. The system knows that the menu is empty and that there's nothing to show so it just doesn't try. – jmcilhinney Jan 22 '21 at 02:34
  • Thanks for the feedback so far. I rebuilt the code with Option Strict On and it did not change the behavior. I can post if you think its pertinent. I check whether the items are present as otherwise, they would be added again and again every time the user right clicks. Given dynamics in my real app, I don't want to add them until the first right click. – Dan Jan 22 '21 at 02:34
  • @jmcilhinney, but if I ask you to tip all the items onto the table AND THEN drop something in, you'd know to do something. I tend to agree the optimization is doing something like you describe, but its a little over zealous, no? – Dan Jan 22 '21 at 02:36
  • 1
    *"its a little over zealous"*. Maybe so. Maybe this is an unintended consequence of that optimisation but that doesn't change the intention or the implementation. You've pretty much found your workaround already, so go with it. If that's not convenient because it means that you can't simply test the count, there are plenty of other options. You could just use a `Boolean` field to indicate whether the menu has been initialised, or you might derive your own custom class, e.g. add a dummy item automatically and set a read-only flag that gets reset when an item is added from outside. – jmcilhinney Jan 22 '21 at 02:41
  • Maybe a little off topic here, but can I ask what the reason for constructing context menus like this is? Seems to me like your solving a problem of your own making. – Hursey Jan 22 '21 at 02:47
  • 3
    **Suggestion:** add all the items that you need in design time or in `Load` event, then in the `Opening` event toggle the visibility according to the current condition. Or, add the default items at design time and go back in your code where the items of the `ContextMenuStrip` should be replaced and add/remove from there. Should be some where to do that, like `SelectedIndexChanged` event, or a selected tab changed ...etc. No clear context to tell you where. Elsewhere but the `Opening` event. – dr.null Jan 22 '21 at 03:02
  • 1
    I have had to build menus in code at times too, e.g. a recent file list can't be determined at design time and it may change between uses of the menu, so I had to build it on each use. I seem to recall having to add a dummy item too, now that you mention it. I just cleared and rebuilt the menu each time so it wasn't a problem. You say that you want to avoid rebuilding the menu each time, so presumably it won't be changing. In that case, why can't you build the menu earlier, e.g. when the form loads or when the user enters whatever input is required to determine the menu contents? – jmcilhinney Jan 22 '21 at 03:05
  • 1
    Looks like I kinda repeated some of what @dr.null said. – jmcilhinney Jan 22 '21 at 03:11

3 Answers3

2

If you really need to do things this way, one option is to create your own custom ContextMenuStrip that accounts for the behaviour when there are no items and the requirement for a dummy item. I used this code:

Imports System.ComponentModel

Public Class Form1

    Private WithEvents menu As New ContextMenuStrip

    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        ContextMenuStrip = menu
    End Sub

    Private Sub menu_Opening(sender As Object, e As CancelEventArgs) Handles menu.Opening
        If menu.Items.Count = 0 Then
            menu.Items.AddRange({New ToolStripMenuItem("First"),
                                 New ToolStripMenuItem("Second"),
                                 New ToolStripMenuItem("Third")})
        End If
    End Sub

    Private Sub menu_ItemClicked(sender As Object, e As ToolStripItemClickedEventArgs) Handles menu.ItemClicked
        MessageBox.Show(e.ClickedItem.Text)
    End Sub

End Class

and saw the same behaviour you describe. I then defined this class:

Public Class ContextMenuStripEx
    Inherits ContextMenuStrip

    Private dummyItem As ToolStripItem

    Public ReadOnly Property IsInitialised As Boolean
        Get
            Return dummyItem Is Nothing
        End Get
    End Property

    Public Sub New()
        dummyItem = Items.Add(CStr(Nothing))
    End Sub

    ''' <inheritdoc />
    Protected Overrides Sub OnItemAdded(e As ToolStripItemEventArgs)
        If Not IsInitialised Then
            Items.Remove(dummyItem)
            dummyItem = Nothing
        End If

        MyBase.OnItemAdded(e)
    End Sub

End Class

and changed my form code to this:

Imports System.ComponentModel

Public Class Form1

    Private WithEvents menu As New ContextMenuStripEx

    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        ContextMenuStrip = menu
    End Sub

    Private Sub menu_Opening(sender As Object, e As CancelEventArgs) Handles menu.Opening
        If Not menu.IsInitialised Then
            menu.Items.AddRange({New ToolStripMenuItem("First"),
                                 New ToolStripMenuItem("Second"),
                                 New ToolStripMenuItem("Third")})
        End If
    End Sub

    Private Sub menu_ItemClicked(sender As Object, e As ToolStripItemClickedEventArgs) Handles menu.ItemClicked
        MessageBox.Show(e.ClickedItem.Text)
    End Sub

End Class

and it worked as desired. Note that the last code snippet uses the custom type and its custom property.

jmcilhinney
  • 50,448
  • 5
  • 26
  • 46
1

Thanks for all the help and suggestions! I ultimately decided to build the superset of menus in the Designer and then just show/hide at run time. That's probably faster on each right click then rebuilding the menu each time.

Dan
  • 89
  • 6
1

Microsoft has old style and new style context menus. The Popup event was used for the old style context menus and it received a plain EventArgs object. The new context menus use the Opening event which receives a CancelEventArgs object. If currContextMenuStrip.Items doesn't contain any items, e.Cancel will be set to True when the event handler is called (which caused the problem you encountered). The fix is to add the menu items and then set e.Cancel to False. It should display fine after that. To make sure items were actually added, the assignment of e.Cancel can be guarded with an if statement as follows:

If currContextMenuStrip.Items.Count <> 0 Then
   e.Cancel = False
End If
Daniel LB
  • 25
  • 6