windowspowershellformswinforms

Windows Form Foreach remove only removes half Form.Control elements


Why doesn't it remove both/all elements?

Code:

    $AAD = New-Object system.Windows.Forms.CheckBox
    $AAD.Name = 'AAD'
    $AAD.text = 'AAD'
    $AAD.Checked = $true 
    $AAD.AutoSize = $true
    $AAD.width = 47
    $AAD.height = 30
    $AAD.enabled = $true
    $AAD.location = New-Object System.Drawing.Point(12, 76)
    $AAD.Font = New-Object System.Drawing.Font('Microsoft Sans Serif', 10)

    $AD = New-Object system.Windows.Forms.CheckBox
    $AD.Name = 'AD'
    $AD.text = 'AD'
    $AD.Checked = $true 
    $AD.AutoSize = $true
    $AD.width = 45
    $AD.height = 20
    $AD.location = New-Object System.Drawing.Point(64, 76)
    $AD.Font = New-Object System.Drawing.Font('Microsoft Sans Serif', 10)

    $array = @('AAD', 'AD')
    $Form.Controls | where { $array -contains $_.Name } | foreach { $Form.Controls.RemoveByKey($_.Name) }

If i log the name of which elements it finds it logs this (So it does find both, but doesn't remove them both):

AAD
AD

Solution

  • I can't explain why it's failing to remove one of the controls but I can provide you a more robust way of doing it that will not have such issue using .Remove instead of .RemoveByKey:

    $form.Controls.AddRange(@($AD; $AAD))
    'AAD', 'AD' | ForEach-Object {
        foreach ($control in $Form.Controls.Find($_, $true)) {
            $form.Controls.Remove($control)
        }
    }
    $form.Controls.Count # 0
    

    Following up on Jimi's helpful comment:

    The reason why half of the Controls remain is that removing elements in a foreach loop modifies the collection you're iterating over. A backwards for loop is one of the common counter-measures -- In general, you should dispose of the Controls that are not need anymore instead of removing them. Controls.Remove() doesn't dispose anything (this behavior is somewhat different in .NET 5+, though).

    Modifying the collection while iterating it can explain OP's issue and the solution being a reverse for loop also works to solve the problem. I would still personally stick to the previous method shown in this answer using .Find and .Remove.

    $array = @('AAD', 'AD')
    for ($i = $form.Controls.Count - 1; $i -ge 0; $i--) {
        $control = $Form.Controls[$i]
        if ($control.Name -in $array) {
            $Form.Controls.RemoveByKey($control.Name) # -> Not needed
            # Disposing the control also removes it from the control collection
            $control.Dispose()
        }
    }
    $form.Controls.Count # 0