I'm trying to iterate over contextmenustrip items like this:
Public Sub TranslateContextMenuStrip(ByRef u As ContextMenuStrip)
For Each t As ToolStripMenuItem In u.Items 'here the error occurs
pProcessMenuItem(t) 'not here
Next
End Sub
But I have toolstrip separators in the contextmenustrip, and I'm getting the error
"System.InvalidCastException: The object of type System.Windows.Forms.ToolStripSeparator can't be converted to type System.Windows.Forms.ToolStripMenuItem"
as soon as it stumbles over a separator.
I wonder why this separator is included in items (I am requesting "For Each t As ToolStripMenuItem" so why does it return non-ToolStripMenuItems???) and how to catch this error or avoid it.
I found a solution, but not the problem:
For Each it As Object In u.Items
If TypeOf it Is ToolStripMenuItem Then
pProcessMenuItem(it)
End If
Next
I dont think For Each t As ToolStripMenuItem does what you might think it does.
It simply declares that the t iterator will be of Type ToolStripMenuItem. It does nothing to the item collection itself. When you come to seperator, you get the cast exception because a separator cannot be converted to a menu item.
Items is a collection of ToolStripItems. This is a base class used for all the Types a context menu can hold (menu item, combo, text box or separator). Since these all inherit from ToolStripItem the collection can hold any of them (specifically, a ToolStripSeparator item is also a ToolStripItem)
There are several ways to iterate or work with just the menu entries:
Filter the Items collection
For Each tsi As ToolStripMenuItem In u.Items.OfType(Of ToolStripMenuItem)()
' do something fun with tsi
Next
The OfType() extension filters the items collection to just menu items.
This is by far the simplest because your iterator tsi is the proper type. This is particularly true if your method is writen to expect menu items:
Sub pProcessMenuItem(item As ToolStripMenuItem)
The tsi iterator is same Type expected by the method, so no further steps are needed. Anything else will require casting either in the method or to call the method (or Option Strict Off).
Test the Type:
' iterate all the items
For Each tsi As ToolStripItem In u.Items
' test the type of each
If TypeOf tsi Is ToolStripMenuItem Then
' do something fun with tsi
End If
Next
Under Option Strict passing tsi to a method declared as shown above wont compile. You would have to cast before invoking your method:
pProcessMenuItem(CType(tsi, ToolStripMenuItem))
If the method is declared to accept a ToolStripItem or Object, the cast would have to take place in the method if you need to access any menu related properties.
The same is true using As Object to iterate:
For Each it As Object In u.Items
If TypeOf it Is ToolStripMenuItem Then
pProcessMenuItem(it)
End If
Next
The only way this will compile under Option Strict if the method argument is declared As Object. As above, Object may have to be cast to ToolStripMenuItem. The first method prevents any need for this.
Related
I have a form that alters the content of a class within a list box. The information is updated correctly, but my ToString override on my object doesn't refresh - meaning the old ToString doesn't change. How would I fix this?
Here's my object:
Public Class Destination
Public strDestinationName As String
Public strAddress As String
Public intQuality As Integer
Public intPrice As Integer
Public Overrides Function ToString() As String
Return strDestinationName
End Function
End Class
Here's the code where it should be updated
Dim selectedDestination As Destination
selectedDestination = CType(ListForm.lbNames.SelectedItem, Destination)
selectedDestination.strDestinationName = tbName.Text
selectedDestination.strAddress = tbAddress.Text
selectedDestination.intPrice = cbPrice.SelectedIndex
selectedDestination.intQuality = cbQuality.SelectedIndex
Me.Close()
Regardless of how you add items to a ListBox, it is the ListBox that actually displays the data. In your case, it appears that you are adding Destination objects to the ListBox somehow, given that the SelectedItem is a Destination object. Given that you have written that ToString method, you are presumably relying on that to produce the text that the ListBox displays for each item. You are now expecting to be able to change the value of the strDestinationName field of one of the items and have the ListBox reflect that change. How exactly do you think that is going to happen?
The ToString method has to be called in order to get the new value and who do you think is going to call it? It would be the ListBox that calls it because it is the ListBox that displays the result. When you change that field, you are expecting the ListBox to call your ToString method but why would it do that? What reason has the ListBox got to call that method? It has no knowledge of the change you made so why would it think that it has to get new data?
The solution to your problem is to change your code in some way to notify the ListBox that data has changed so that it knows that it needs to get that new data and display it. There are multiple ways that you could do that.
The simplest option would be to bind your data to the ListBox via a BindingSource and then, when you modify an item, call the ResetItem method or similar of the BindingSource. That will raise an event that is handled by the ListBox and the ListBox then knows that it needs to refresh the data for that item. That is what will prompt the ListBox to call your ToString method and get the new data to display. You would add the BindingSource to the form in the designer and then do the binding where you are currently adding the items, e.g.
Dim destinations As New List(Of Destination)
For i = 1 To 10
Dim d As New Destination
d.strDestinationName = "Destination " & i
destinations.Add(d)
Next
destinationBindingSource.DataSource = destinations
destinationListBox.DataSource = destinationBindingSource
The modification would look something like this:
Dim selectedDestination = DirectCast(destinationBindingSource.Current, Destination)
selectedDestination.strDestinationName = "New Destination"
destinationBindingSource.ResetCurrentItem()
The Current property returns the item currently selected in the bound UI and the ResetCurrentItem method notifies the bound UI to refresh the display of that item.
This is really not the best way to go about it though, given that you have control over the item type. What you ought to do is implement the type using properties rather than fields, get rid of the ToString method that only returns the value of one property and then add a change event to that property:
Public Class Destination
Private _destinationName As String
Public Property DestinationName As String
Get
Return _destinationName
End Get
Set(value As String)
If _destinationName <> value Then
_destinationName = value
OnDestinationNameChanged(EventArgs.Empty)
End If
End Set
End Property
Public Property Address As String
Public Property Quality As Integer
Public Property Price As Integer
Public Event DestinationNameChanged As EventHandler
Protected Overridable Sub OnDestinationNameChanged(e As EventArgs)
RaiseEvent DestinationNameChanged(Me, e)
End Sub
End Class
You can now bind a list of Destination objects directly and specify any of those properties as the DisplayMember to have that property value displayed:
Dim destinations As New List(Of Destination)
For i = 1 To 10
Dim d As New Destination
d.strDestinationName = "Destination " & i
destinations.Add(d)
Next
destinationListBox.DisplayMember = "DestinationName"
destinationListBox.DataSource = destinations
You don't need the ToString method because the DisplayMember specifies that the value of the property with that name should be displayed. When you modify the value of the DestinationName property of an item, it will raise the DestinationNameChanged event and that will notify the ListBox that it needs to refresh the display for that item, so you don't need any additional code to make the ListBox update.
That's fine if you only plan to modify existing items. There's still a problem if you want to add and/or remove items after binding though. The List(Of T) class that is used to bind the items to the control in this example does not have any events to notify the control of changes to the list like that. In that case, you can use a BindingSource again if you want. If you add and remove items via the BindingSource then it will raise that appropriate events and the ListBox will update. If you wanted to add and remove via the underlying list then you'd have to call an appropriate method of the BindingSource when you made a change.
An alternative would be to use a BindingList(Of Destination) instead of a List(Of Destination). As the name suggests, the BindingList(Of T) class is made for binding, so it will automatically raise the appropriate events when the list changes to enable the UI to update without extra code from you. Using the combination of property change events in your item class and a BindingList(Of T), you can add, edit and remove items in the bound list and the UI will reflect those changes automatically.
I have groupboxes that contain textboxes and I want to set all of them to ReadOnly=True. Here is what I tried (doesn't work):
EDIT (I forgot about Split container):
For Each SplitCon As Control In Me.Controls
If TypeOf SplitCon Is SplitContainer Then
For Each GBox As Control In SplitCon.Controls
If TypeOf GBox Is GroupBox Then
For Each ctrl As Control In GBox.Controls
If TypeOf (ctrl) Is TextBox Then
CType(ctrl, TextBox).ReadOnly = True
End If
Next
End If
Next
End If
Next
You can simplify things a great deal. Rather than looking thru all sorts of Control collections, create an array to act as a ToDo list. Then you can get rid of all the TypeOf and CType in the loop used.
' the ToDo list
Dim GrpBoxes = New GroupBox() {Groupbox1, Groupbox2,
Groupbox3, Groupbox4}
For Each grp In GrpBoxes
For Each tb As TextBox In grp.Controls.OfType(Of TextBox)()
tb.ReadOnly = True
Next
Next
Your code no longer depends on the form layout of the moment. The only thing you have to remember is to add any new GroupBox items to your list. You can also declare the array once ever for the whole form if you prefer (or even in the For Each statement)
Rather than working with Control objects, Controls.OfType(Of T) filters the collection and returns an object variable of that type, so there is no need to cast it or skip over controls you are not interested in. You can also tack on a Where method to further refine the list to include only do those with a certain name or Tag.
What it basically does is checks if the user input is already in ComboBox1. If it is, alerts the user. If not, it adds it to the combobox
The thing I don't get is the "For Each StringIterador In ComboBox1.Items loop". How could an Item object be placed in a String variable? I know Strings are objects but... You can't just place a random object into a String variable, can you? Also the String is later used as an Item object back "ComboBox1.GetItemText(StringIterador)"
Private Sub ComboBox1_KeyPress(sender As Object, e As KeyPressEventArgs) Handles ComboBox1.KeyPress
Dim StringIterador As String
If e.KeyChar = ControlChars.Cr Then
If ComboBox1.Text <> "" Then
For Each StringIterador In ComboBox1.Items
If ComboBox1.GetItemText(StringIterador).Equals(ComboBox1.Text) Then
MsgBox("ya está en la lista")
Exit Sub
Else
ComboBox1.Items.Add(ComboBox1.Text)
Exit Sub
End If
Next
End If
End If
End Sub
The documentation for the For Next statement (https://msdn.microsoft.com/en-us/library/5ebk1751.aspx) requires that "The data type of element must be such that the data type of the elements of group can be converted to it." so this code will work as long as each item can be converted to string. It's not storing the item object in a string, it's converting the item to a string and storing that.
I haven't tested this but I suspect that if you stored an object in Items that couldn't be converted to string a run-time exception would be raised. Of course since the code is adding ComboBox1.Text each time this code will only add text items and therefore won't set up a situation in which the string conversion would be invalid.
In the same way GetItemText() is documented as "If the DisplayMember property is not specified, the value returned by GetItemText is the value of the item's ToString method. Otherwise, the method returns the string value of the member specified in the DisplayMember property for the object specified in the item parameter." so, again, it's probably working because the objects added will return a string. If you added a complex object to the combobox you'd probably see the object's type displayed (which, from memory) is the fallback result of ToString().
I am trying to write a clever little function here that will add a specified delegate as an event handler to all the controls in a collection for any dynamic event. What I'm trying to do is write this as a completely generic function so that I could possibly use it in various different projects (perhaps including it in some sort of tools library).
Basically I want to specify a group of controls, the delegate to handle the event, and the type of event to handle. The problem that I'm running up against is that I can't figure out how to dynamically specify the event at run time.
Here's my 'work-in-progress' sub:
Private Sub AddHandlerToControls(controlList As ControlCollection, eventToHandle As EventHandler, eventHandlerDelegate As Func(Of Object, EventArgs), Optional filterList As List(Of Type) = {})
For Each controlInList As Control In controlList
If controlInList.HasChildren Then
AddHandlerToControls(controlInList.Controls, controlInList.MouseEnter, eventHandlerDelegate, filterList)
End If
If filterList.Count > 0 Then
If filterList.Contains(controlInList.GetType) = False Then
Continue For
End If
End If
AddHandler controlInList.MouseEnter, eventHandlerDelegate
Next
End Sub
Ideally I would like to use the eventToHandle parameter there at the end in the AddHandler statement instead of specifically using controlInList.MouseEnter. Like this:
AddHandler eventToHandle, eventHandlerDelegate
That way I could call this function dynamically in a form.load method, and call it sort of like how I did earlier in the sub where it's recursively calling itself for child controls. Somehow say "for this list of controls I would like to use this delegate as the 'MouseEnter' event handler". Like So:
AddHandlerToControls(Me.Controls, control.MouseEnter, MouseEnterHandlerDelegate, new List(Of Type) {TextBox, ComboBox})
This could just be wishfull thinking, I'm starting to think that this isn't quite possible at this level of 'genericness', but it's an interesting enough problem that I thought I should at least ask.
Edit for solution:
Jon Skeet's suggestion of using Reflection ended up working for me. Here's the final function:
Private Shared Sub AddHandlerToControls(controlList As Control.ControlCollection, eventToHandle As String, eventHandlerDelegate As MethodInfo, Optional filterList As List(Of Type) = Nothing)
For Each controlInList As Control In controlList
If controlInList.HasChildren Then
AddHandlerToControls(controlInList.Controls, eventToHandle, eventHandlerDelegate, filterList)
End If
If Not filterList Is Nothing Then
If filterList.Contains(controlInList.GetType) = False Then
Continue For
End If
End If
Dim dynamicEventInfo As EventInfo = controlInList.GetType.GetEvent(eventToHandle)
Dim handlerType As Type = dynamicEventInfo.EventHandlerType
Dim eventDelegate As [Delegate] = [Delegate].CreateDelegate(handlerType, eventHandlerDelegate)
dynamicEventInfo.AddEventHandler(controlInList, eventDelegate)
Next
End Sub
And how I call it and the delegate used:
AddHandlerToControls(Controls, "MouseClick", GetType(MainFrm).GetMethod("MouseClickEventDelegate"), New List(Of Type) From {GetType(TextBox), GetType(ComboBox)})
Shared Sub MouseClickEventDelegate(sender As Object, eventArgs As EventArgs)
sender.SelectAll()
End Sub
This allowed me to set all text boxes and combo boxes on my form (there's quite a few) to select all text when clicked into, in about 20 lines of code. The best part is that if I add any in the future, I won't have to worry about going back to add this handler, it'll be taken care of at run time. It may not be the cleanest solution, but it ended up working pretty well for me.
Two options:
Specify a "subscription delegate" via a lambda expression. I wouldn't like to guess at what this would look like in VB, but in C# it would be something like:
(control, handler) => control.MouseEnter += handler;
Then you just need to pass each control to the delegate.
Specify the event name as a string, and use reflection to fetch the event and subscribe (Type.GetEvent then EventInfo.AddEventHandler).
I recently upgraded a VB 6 project to .net. I'm having a problem with this block of code:
Dim CtrlName As System.Windows.Forms.MenuItem
For Each CtrlName In Form1.Controls
'Some code here
Next CtrlName
Now this code compiles but throws the following runtime error:
Unable to cast object of type 'System.Windows.Forms.Panel' to type 'System.Windows.Forms.MenuItem.
I have a panel control on the subject form. How do I resolve this?
Thanks.
You are iterating over all controls that are directly inside the form, not just the MenuItems. However, your variable is of type MenuItem. This is causing the problem.
For normal controls (e.g. Buttons), you’d want to use the following, easy fix; test inside the loop whether the control type is correct:
For Each control As Control In Form1.Controls
Dim btt As Button = TryCast(control, Button)
If btt IsNot Nothing Then
' Perform action
End If
Next
However, this does not work for MenuItems since these aren’t controls at all in WinForms, and they aren’t stored in the form’s Controls collection.
You need to iterate over the form’s Menu.MenuItems property instead.
The items in the Controls property of a form, which may or may not be MenuItem. Assuming that you just want to iterate over MenuItem objects you can change your code to:
For Each menuControl As MenuItem In Me.Controls.OfType(Of MenuItem)
' Some code
Next
Note that the menuControl variable is declared in the For so is only accessible within the block and is disposed automatically.
for each ctrl as control in me.controls
if typeof ctrl is menuitem then
' do stuff here
end if
next
typeof keyword allows you to test the type of control being examined in the control collection.
Found the answer after a bit of research, you need to search for the menu strip first and then loop through the items collection.
For Each ctrl As Control In Me.Controls
If TypeOf ctrl Is MenuStrip Then
Dim mnu As MenuStrip = DirectCast(ctrl, MenuStrip)
For Each x As ToolStripMenuItem In mnu.Items
Debug.Print(x.Name)
Next
End If
Next