I wanted to delete all of the TextBoxes that I put on a form that start with "tbx". The below code only deleted some of them.

For Each ctl As Control In Panel1.Controls
    If ctl.Name.StartsWith("tbx") Then
        ctl.Dispose()
    End If
Next

So, I took it a step further in testing...

Now I'm really confused about the .controls collection. When I try to delete controls from Panel1 with the below code, it only deletes every other control instead of all controls.

For Each ctl As Control In Panel1.Controls
    ctl.Dispose()
Next

It looks like it deletes item(0) which moves everything back one step but then advances the pointer to the next step.

For example, if the following controls exist on the form...
tbx1
tbx2
tbx3
tbx4
on the first pass it disposes of tbx1 but the next iteration though the for each loop now points to tbx3, thus bypasssing tbx2.

Any explanation on why this is doing this and how to get around it?

Dani AI

Generated

Short answer: the loop is skipping because the Controls collection is being changed while you enumerate it. As noted, modifying a collection during a For Each invalidates the enumerator (many collections throw an error; Control.ControlCollection doesn’t always — which is why you see "every other" control disappear instead of an exception). (learn.microsoft.com)

Safer approaches (pick one that fits the goal):

  • Take a snapshot (copy) of the controls to remove, then iterate the snapshot and remove/dispose each item from the original collection. This avoids changing the enumerator you’re walking. Example pattern shown below.
  • Iterate by index from the end toward zero (this was suggested earlier by and ).
  • Repeatedly remove/Dispose the first child until Count is 0 (simple and robust for removing all children).

If you need to remove only textboxes whose name starts with "tbx", copying a filtered snapshot is readable and safe:

' take a snapshot (array) of matching controls, then remove & dispose them
Dim toRemove = Panel1.Controls.Cast(Of Control)().
                 Where(Function(c) c.Name.StartsWith("tbx")).
                 ToArray()

For Each ctl As Control In toRemove
    Panel1.Controls.Remove(ctl)   ' detach from parent
    ctl.Dispose()                 ' free native handles/resources
Next

Important notes and cautions:

  • Controls.Clear() removes controls from the collection but does not reliably Dispose them — that can leave native handles parked and lead to leaks. Call Dispose when you really want resources freed. (learn.microsoft.com)
  • In current .NET WinForms code Dispose() removes the control from its parent, but that behaviour comes from implementation, not a high-level contract — removing before Dispose is slightly safer in multithreaded scenarios. (referencesource.microsoft.com)
  • When removing many controls, suspend layout to avoid repeated layout/paint work, then resume when done (SuspendLayout/ResumeLayout). (learn.microsoft.com)

Tieback: the reverse-index solution that / suggested is perfectly valid; the snapshot pattern above is just another clear, maintainable alternative that avoids the "skip" behavior and lets you filter by type or name easily.

Recommended Answers

All 4 Replies

When you do a for each and remove controls from the collection you're iterating over, it creates problems like the one you're having.

Say if you have a list like

item1
item2
item3

You do a for each loop over the collection and remove the first record and then move to the second. With item1 gone, item2 is now the first record, and item3 is the second! The next candidate for removal is therefore not item2, but the new second record of item3.

Instead, consider doing a for loop and going in reverse order, and then remove items at specifix indexes.

Dim controlIndex As Integer = Panel1.Controls.Count - 1

        For index As Integer = controlIndex To 0 Step -1
            Panel1.Controls(index).Dispose() 
        Next

By removing items starting at the end, you're not affecting the order or position of the elements at the beginning and your removal process should work as you intend as you go backwards over the collection.

Thanks for the explanation and the workaround. That is what it looked like it was doing but didn't make sense. Someone should tell Microsoft that a "for each" should take a snapshot and process the snapshot.

When you do a for each and remove controls from the collection you're iterating over, it creates problems like the one you're having.

Say if you have a list like

item1
item2
item3

You do a for each loop over the collection and remove the first record and then move to the second. With item1 gone, item2 is now the first record, and item3 is the second! The next candidate for removal is therefore not item2, but the new second record of item3.

Instead, consider doing a for loop and going in reverse order, and then remove items at specifix indexes.

Dim controlIndex As Integer = Panel1.Controls.Count - 1

        For index As Integer = controlIndex To 0 Step -1
            Panel1.Controls(index).Dispose() 
        Next

By removing items starting at the end, you're not affecting the order or position of the elements at the beginning and your removal process should work as you intend as you go backwards over the collection.

Dim i As Integer 

for i=0 to panel1.controls.count-1
              Panel1.Controls(i).Dispose()
next

Remember to step backwards through the controls, otherwise you will get index out of range error.
This code also test if the current control is a TextBox for when you have different controls in that panel1 container.

For i As Integer = Panel1.Controls.Count - 1 To 0 Step -1
     If TypeOf (Panel1.Controls(i)) Is TextBox Then
           Panel1.Controls(i).Dispose()
     End If
Next
Be a part of the DaniWeb community

We're a friendly, industry-focused community of developers, IT pros, digital marketers, and technology enthusiasts meeting, networking, learning, and sharing knowledge.