AddHandler to ToolStripMenuItem through ContextMenuStrip.Opening event not working - vb.net

Hi to you all and Merry Christmas.
I have recently inherited a VB projects that I must add functionality. So I have the code below:
Private Sub AddItems()
Dim itemMenu = DirectCast(ContextMenuStrip.Items.Find("name", False)(0), ToolStripMenuItem)
For Each dbObject In dbObjects
Dim item As New ToolStripMenuItem(dbObject.Name)
item.Tag = dbObject
AddHandler item.Click, AddressOf Item_Click
itemMenu.DropDownItems.Add(item)
Next
End Sub
Private Sub RemoveItems()
Dim itemMenu = DirectCast(ContextMenuStrip.Items.Find("name", False)(0), ToolStripMenuItem)
For Each item As ToolStripItem In itemMenu.DropDownItems
RemoveHandler item.Click, AddressOf Item_Click
Next
itemMenu.DropDownItems.Clear()
End Sub
Private Sub ContextMenuStrip_Opening(sender As Object, e As System.ComponentModel.CancelEventArgs) Handles ContextMenuStrip.Opening
AddItems()
End Sub
Private Sub ContextMenuStrip_Closing(sender As Object, e As ToolStripDropDownClosingEventArgs) Handles ContextMenuStrip.Closing
RemoveItems()
End Sub
Private Sub Item_Click(sender As Object, e As EventArgs)
' Do the work
End Sub
The logic is to fill a sub-menu in a context menu each time with valid database objects.
The problem is that this code does not work. It adds the items to context menu perfectly but the AddHandler item.Click, AddressOf Item_Click does nothing.
The strange thing is that if I call the AddItems() in Form_Load then it works perfectly.
Any help would be appreciated.
2016.12.22 Solution after WozzeC answer
Private Sub AddItems()
RemoveItems()
Dim itemMenu = DirectCast(ContextMenuStrip.Items.Find("name", False)(0), ToolStripMenuItem)
For Each dbObject In dbObjects
Dim item As New ToolStripMenuItem(dbObject.Name)
item.Tag = dbObject
AddHandler item.Click, AddressOf Item_Click
itemMenu.DropDownItems.Add(item)
Next
End Sub
Private Sub RemoveItems()
Dim itemMenu = DirectCast(ContextMenuStrip.Items.Find("name", False)(0), ToolStripMenuItem)
For Each item As ToolStripItem In itemMenu.DropDownItems
RemoveHandler item.Click, AddressOf Item_Click
Next
itemMenu.DropDownItems.Clear()
End Sub
Private Sub ContextMenuStrip_Opening(sender As Object, e As System.ComponentModel.CancelEventArgs) Handles ContextMenuStrip.Opening
AddItems()
End Sub
Private Sub Item_Click(sender As Object, e As EventArgs)
' Do the work
End Sub

I have been playing around with this for a little while now and I have managed to reproduce your issue. The reason for your headache is that RemoveItems fires before your Click event. So when you perform RemoveHandler, the click event disappear.
What I suggest that you do instead is to not add the click handler on the ContextMenuItems. Instead you add an EventHandler for DropDownItemClicked on the Parent node. The result will be the same as if Item.Click worked, but without the headache of handling Handlers dynamically.
I also tried switching from Closing to Closed event on the ContextMenu for RemoveItems. But to no avail I am afraid.
Another way to solve this issue is by moving RemoveItems into the first row of AddItems. Then you remove the closing event and its call to RemoveItems. This means that whenever you want to create a new ContextMenu the previous one is disposed properly. This will also solve your future bug where the ContextMenu items are added twice or more. Which currently happens for you if you right click multiple times really fast.

Related

ContextMenuStrip Requires Two Right Clicks to Display

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
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.
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.
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

Removing and adding event handlers for different ComboBoxes where the ComboBox is passed as a parameter

I have a method that takes a ComboBox as a parameter and then adds data to it. When data is added, the SelectedIndexChangedEvent fires. Is there a way that, in the called method, I can remove the above event handler for whatever ComboBox is passed as a parameter and then add is at the end of the method? I know how to remove and add specific handlers, but can't figure out how to do it based on which ComboBox is passed.
Here's the method..
Private Sub PopulateComboBox(ByRef cboBox As ComboBox, ByVal itemSource As String)
'Remove handler for cboBox
'Do stuff that would otherwise cause the event handler to execute
'Add handler for cboBox
End Sub
I have 4 ComboBoxes - would it just be easier to remove all 4 event handlers and then add them again at the end of the code? I would however like to know if this is possible so that I can possibly apply to re-usable code in the future
The most basic way to go about this is to do this:
Private Sub PopulateComboBox(ByRef cboBox As ComboBox, ByVal itemSource As String)
RemoveHandler cboBox.SelectedIndexChanged, AddressOf ComboBox1_SelectedIndexChanged
'Do stuff that would otherwise cause the event handler to execute
AddHandler cboBox.SelectedIndexChanged, AddressOf ComboBox1_SelectedIndexChanged
End Sub
Private Sub ComboBox1_SelectedIndexChanged(sender As Object, e As EventArgs) Handles ComboBox1.SelectedIndexChanged
End Sub
Another option, which might be better in some circumstances, is to do this:
Private _ignoreComboBox As ComboBox = Nothing
Private Sub PopulateComboBox(ByRef cboBox As ComboBox, ByVal itemSource As String)
_ignoreComboBox = cboBox
'Do stuff that would otherwise cause the event handler to execute
_ignoreComboBox = Nothing
End Sub
Private Sub ComboBox1_SelectedIndexChanged(sender As Object, e As EventArgs) Handles ComboBox1.SelectedIndexChanged
If sender Is Not _ignoreComboBox Then
End If
End Sub
Or, to handle multiple combo boxes at the same time:
Private _ignoreComboBoxes As List(Of ComboBox) = New List(Of ComboBox)()
Private Sub PopulateComboBox(ByRef cboBox As ComboBox, ByVal itemSource As String)
_ignoreComboBoxes.Add(cboBox)
'Do stuff that would otherwise cause the event handler to execute
_ignoreComboBoxes.Remove(cboBox)
End Sub
Private Sub ComboBox1_SelectedIndexChanged(sender As Object, e As EventArgs) Handles ComboBox1.SelectedIndexChanged
If Not _ignoreComboBoxes.Contains(DirectCast(sender, ComboBox)) Then
End If
End Sub
Here is one way:
' these happen to map to the same event handler
Private cb1Event As EventHandler = AddressOf cbx_SelectedIndexChanged
Private cb2Event As EventHandler = AddressOf cbx_SelectedIndexChanged
Then when used:
PopulateComboBox(cb1, items, cb1Event)
PopulateComboBox(cb2, items, cb2Event)
' or
PopulateComboBox(cb3, items, AddressOf cbx_SelectedIndexChanged)
The method would be declared:
Private Sub PopulateComboBox(cboBox As ComboBox,
items As String, ev As EventHandler)
Personally, since you know the cbo involved anyway, I'd do that before the call:
RemoveHandler cb1.SelectedIndexChanged, AddressOf cbx_SelectedIndexChanged
PopulateComboBox(cb1, items)
AddHandler cb1.SelectedIndexChanged, AddressOf cbx_SelectedIndexChanged
There is not much to be gained by passing all the info to do something to something else so it can do what you know needs to be done.

dynamically create a new tab with textbox, button in the tab with click and keypress events (AddHandler is what i cant get to work)

I am creating some tabs and I need two things to work that I can't get to work. I need to AddHandler for a Textbox.Keypress event AND a Button.Click event. I can make these things work outside of the tabcontrol but not in.
In the example below my text box and buttons have same name from on tab to the another, I thought that might be my problem but even changing names between tabs does not work. I assume I need to be more specific in the AddHandler part to give the tab name as well as control. There is a logic in my real code to allow me to give unique names to each tab panel and controls, but i can't get the simple part to work.
I left some of the things I tried commented, but I tried LOTS and LOTS of other things.
Public Class Form1
Public Sub addTab(tabPageName As String)
Dim tabpage As New TabPage
tabpage.Text = tabPageName
tabpage.Name = "tabPage1" 'real code has logic to make sure names are unique
Dim label1 As New Label
Dim txtCreator As New TextBox
Dim combox1 As New ComboBox
Dim tabPageButton2 As New Button
tabPageButton2.Parent = tabpage
label1.Parent = tabpage
txtCreator.Parent = tabpage
combox1.Parent = tabpage
label1.Location = New Point(10, 10)
txtCreator.Location = New Point(150, 10)
combox1.Location = New Point(300, 10)
tabPageButton2.Location = New Point(20, 40)
label1.Text = "Creator"
txtCreator.Name = "txtCreator"
'fill the comboboxes...this will come from a database but testing now.
combox1.Items.Add("one")
combox1.Items.Add("two")
combox1.Items.Add("three") 'ok that works so should work from DB no problem.
tabRoleClass.TabPages.Add(tabpage)
End Sub
Private Sub Form1_Load(sender As Object, e As System.EventArgs) Handles Me.Load
addTab("First Tab")
AddHandler Controls("tabRoleClass.tabPage1.tabPageButton2").Click, AddressOf tabPageButton_click
'AddHandler CType(Controls("tabPageButton"), Button).Click, AddressOf tabPageButton_click
'AddHandler Controls("tabPageButton").Click, AddressOf tabPageButton_click
AddHandler CType(Controls("txtCreator"), TextBox).KeyPress, AddressOf txtcreator_keypress 'the Keypress to call lookup
End Sub
Private Sub tabPageButton_click(sender As System.Object, e As System.EventArgs) 'Handles tabPageButton.click
MessageBox.Show(tabRoleClass.SelectedTab.Name.ToString)
End Sub
Private Sub Button1_Click(sender As System.Object, e As System.EventArgs) Handles Button1.Click
addTab("Second Tab")
tabRoleClass.SelectedIndex = tabRoleClass.TabCount - 1
'AddHandler Controls("tabRoleClass.tabPage1.tabPageButton2").Click, AddressOf tabPageButton_click
'AddHandler CType(Controls("tabPageButton"), Button).Click, AddressOf tabPageButton_click
'AddHandler Controls("tabPageButton").Click, AddressOf tabPageButton_click
'AddHandler CType(Controls("txtCreator"), TextBox).KeyPress, AddressOf txtcreator_keypress 'the Keypress to call lookup
End Sub
Private Sub txtcreator_keypress(ByVal sender As Object, ByVal e As System.Windows.Forms.KeyPressEventArgs) 'Handles txtCreator.KeyPress
MessageBox.Show("keypress worked on " & tabRoleClass.SelectedTab.Name.ToString)
End Sub
End Class
This is a very confusing question and your code could really do with some cleaning, but you need to add the AddHandler code to the addTab subroutine as pointed out by #Plutonix:
Public Sub addTab(tabPageName As String)
Dim tabpage As New TabPage
Dim tabPageButton As New Button
Dim txtCreator As New TextBox
/.../
AddHandler tabPageButton.Click, AddressOf tabPageButton_click
AddHandler txtCreator.KeyDown, AddressOf txtcreator_keypress
tabRoleClass.TabPages.Add(tabpage)
End Sub
Private Sub tabPageButton_click()
MessageBox.Show(tabRoleClass.SelectedTab.Name.ToString)
End Sub
Private Sub txtcreator_keypress()
MessageBox.Show("keypress worked on " & tabRoleClass.SelectedTab.Name.ToString)
End Sub
Private Sub Form1_Load(sender As Object, e As System.EventArgs) Handles Me.Load
addTab("First Tab")
End Sub
Private Sub Button1_Click(sender As System.Object, e As System.EventArgs) Handles Button1.Click
addTab("Second Tab")
tabRoleClass.SelectedIndex = tabRoleClass.TabCount - 1
End Sub
AddHandler works by adding event handlers to your controls. This means that each time an event is raised during this runtime, the new event handler will handle the event; everytime you click your tabPageButton the associated event tabPageButton_click will handle it.
Therefore, you will only need to add the handler once, preferably upon the creation of the control. There is absolutely no need to create them upon every single keypress, for example. You should look up event handlers on MSDN.
Hope this helps!
Sorry if the code was confusing, I cut up my actual code to make a "sample" and I can see the confusion. Now of course I AM confused, I originally had the AddHandler INSIDE the addTab sub that creates the tab and it didn't work there, I incorrectly assumed the reason was that the control was not yet created so I moved it out. Moving it back in this sub this morning worked perfectly, I don't know what I did wrong but its working GREAT by moving it up to where it belongs, thanks A LOT, I worked on this for 2 days trying and googling things. Next time I will post real code instead of a sample to be less confusing and also remove my commented attemps (I thought those would help show what I was trying but I think it didn't)

Visual Studio - Affect all textboxes at once

I am trying to remove leading zeroes from my textboxes. I have the code working, but I have close to 50 textboxes. I don't really want to have to make 50 textbox.TextChanged events.
Is there anyway to affect all of the textboxes with the same code?
This is the code I am using:
Private Sub txtTier1_100_TextChanged(sender As Object, e As EventArgs) Handles txtTier1_100.TextChanged
txtTier1_100.Text = txtTier1_100.Text.TrimStart("0"c)
End Sub
First step is to define a general purpose handler
Private Sub HandleTextChanged(sender As Object, e As EventArgs)
Dim tb = CType(sender, TextBox)
tb.Text = tb.Text.TrimStart("0"c)
End Sub
Then attach every one of your TextBox instances to this single handler
AddHandler txtTier1_100.TextChanged, AddressOf HandleTextChanged
AddHandler txtTier1_101.TextChanged, AddressOf HandleTextChanged
AddHandler txtTier1_102.TextChanged, AddressOf HandleTextChanged
Note that if you had all of the TextBox instances in a collection this could be done with a For Each loop as well
ForEach tb in textBoxList
AddHandler tb.TextChanged, AddressOf HandleTextChanged
Next
Private Sub txtTier1_100_TextChanged(sender As Object, e As EventArgs) Handles txtTier1_100.TextChanged, txtTier1_101.TextChanged, txtTier1_102.TextChanged...
'sender is the right textbox
End Sub

BackgroundWorker freezes GUI

I have read other posts about this but I still can't seem to get it to work right.
Whenever my BackgroundWorker begins to do work, my function API.CheckForUpdate causes the GUI to hang. I can't click on anything. It only freezes for half a second, but is enough to notice.
How can I fix this? Should I dive deeper into API.CheckForUpdate and run individual threads on particular statements, or can I just have an all-inclusive thread that handles this? API.CheckForUpdate does not reference anything in Form1.
Also, I presume Form1_Load is not the best place to put the RunWorkerAsync call. Where is a better spot?
'Declarations
Dim ApplicationUpdate As BackgroundWorker = New BackgroundWorker
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
ApplicationUpdate.WorkerSupportsCancellation = True
ApplicationUpdate.WorkerReportsProgress = True
AddHandler ApplicationUpdate.DoWork, AddressOf ApplicationUpdate_DoWork
AddHandler ApplicationUpdate.ProgressChanged, AddressOf ApplicationUpdate_ProgressChanged
AddHandler ApplicationUpdate.RunWorkerCompleted, AddressOf ApplicationUpdate_RunWorkerCompleted
ApplicationUpdate.RunWorkerAsync()
End Sub
Private Sub ApplicationUpdate_DoWork(ByVal sender As Object, ByVal e As DoWorkEventArgs)
'Check for an update (get the latest version)
Dim LatestVersion = API.CheckForUpdate
End Sub
Private Sub ApplicationUpdate_ProgressChanged(ByVal sender As Object, ByVal e As ProgressChangedEventArgs)
'Nothing here
End Sub
Private Sub ApplicationUpdate_RunWorkerCompleted(ByVal sender As Object, ByVal e As RunWorkerCompletedEventArgs)
'Work completed
MsgBox("Done")
End Sub
Its not a background worker Fix but if you don't mind walking around and not finding the answer, you can code like so:
Keep in mind when you first Start a Thread and you are coding in a Model you MUST pass (me) into the initial thread because of VB having a concept of "Default Form Instances". For every Form in the application's namespace, there will be a default instance created in the My namespace under the Forms property.
and that is just adding an additional parameter like so
----------------------/ Starting Main Thread /-----------------------------------
Private Sub FindCustomerLocation()
Dim Findcontractor_Thread As New Thread(AddressOf **FindContractor_ThreadExecute**)
Findcontractor_Thread.Priority = ThreadPriority.AboveNormal
Findcontractor_Thread.Start(me)
End Sub
------------------/ Running Thread /---------------
Private Sub **FindContractor_ThreadExecute**(beginform as *NameOfFormComingFrom*)
Dim threadControls(1) As Object
threadControls(0) = Me.XamDataGrid1
threadControls(1) = Me.WebBrowserMap
**FindContractor_WorkingThread**(threadControls,beginform) ' ANY UI Calls back to the Main UI Thread MUST be delegated and Invoked
End Sub
------------------/ How to Set UI Calls from a Thread / ---------------------
Delegate Sub **FindContractor_WorkingThread**(s As Integer,beginform as *NameOfFormComingFrom*)
Sub **FindContractor_WorkingThreadInvoke**(ByVal s As Integer,beginform as *NameOfFormComingFrom*)
If beginform.mouse.InvokeRequired Then
Dim d As New FindContractor_WorkingThread(AddressOf FindContractor_WorkingThreadInvoke)
beginform.Invoke(d, New Object() {s,beginform})
Else
beginform.Mouse.OverrideCursor = Cursors.Wait
'Do something...
beginform.Mouse.OverrideCursor = Nothing
End If
End Sub
Sources From Pakks Answer Tested!
Try starting the process outside the Load event. Create a Timer and start it on the Load event, and then handle the event for the tick:
Private Sub Timer1_Tick(sender As Object, e As EventArgs) Handles Timer1.Tick
Timer1.Enabled = False
ApplicationUpdate.RunWorkerAsync()
End Sub