Dynamically Created Checkbox Handlers

Hello clever folk,

I had a simple bit of code to help me select columns to keep or extract from a simple CSV file. It originally used a list of column names but I thought it would be a bit of fun to instead make a GUI to allow me to select them. Well, I’m not having fun now and pulling my hair out with what appears to be a variable scope issue.

This is my simple code:

###
# Process required paramter(s)
###
param(
	[Parameter (Mandatory=$True)][Alias("CSV")][ValidateScript({Test-Path $_ -PathType leaf})][string]$f_CSV
)

###
# Global/Script variables
###
$h_selected_columns = @{}

###
# Grab the contents of the CSV and use the first line to allow selection of
# the columns to retain.
###
$o_CSV = Import-Csv -Path $f_CSV

### Pull in the libraries for the GUI
[void] [System.Reflection.Assembly]::LoadWithPartialName("System.Drawing") 
[void] [System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")  

### Create the form we shall display in.
$o_form = New-Object System.Windows.Forms.Form    
$o_form.Size = New-Object System.Drawing.Size(600,700)
$o_form.AutoScroll = $true
$o_form.text ="Select required CSV columns" 

### Add the Okay and Concal buttons to the form
# Create OK button
$o_OKButton = New-Object System.Windows.Forms.Button
$o_OKButton.Text = "Okay"
$o_OKButton.DialogResult = "OK"
$o_OKButton.Add_Click({
    # Handle OK button click here
	$o_form.Close()
})

# Create Cancel button
$o_CancelButton = New-Object System.Windows.Forms.Button
$o_CancelButton.Text = "Cancel"
$o_CancelButton.DialogResult = "Cancel"
$o_CancelButton.Add_Click({
    # Handle Cancel button click here
    $o_form.Close()
})

# Add buttons to the form
$o_form.Controls.Add($o_OKButton)
$o_form.Controls.Add($o_CancelButton)

### Create a group for the checkboxes
$o_groupBox = New-Object System.Windows.Forms.GroupBox
$o_groupBox.Location = New-Object System.Drawing.Size(5,5)  
$o_groupBox.text = "Availabe CSV columns:" 
$o_form.Controls.Add($o_groupBox)

### Initilise checkboxes (create first empty checkbox?)
$o_checkboxes += New-Object System.Windows.Forms.CheckBox
$o_checkboxes.Location = New-Object System.Drawing.Size(10,30) 
$o_checkboxes = @() # cast as an array?

###
# Set the spacing to use for the checkboxes, initialise a hash table for the checkbox objects and
# then loop through the column headers.
###
$i_Offset = 20
$o_checkbox = @{}
foreach ($s_column_name in $o_CSV[0].psobject.Properties.Name)
{
	# Setup the new checkbox
    $o_checkbox[$s_column_name] = New-Object System.Windows.Forms.CheckBox
    $o_checkbox[$s_column_name].Text = "Column: $s_column_name"
    $o_checkbox[$s_column_name].Location = New-Object System.Drawing.Size(10,$i_Offset) 
	$o_checkbox[$s_column_name].AutoSize = 1
	
	# Setup the handler for this checkbox
	########## THIS IS THE BIT THAT DOESN'T APPEAR TO WORK!!!!! ############
	########## THIS IS THE BIT THAT DOESN'T APPEAR TO WORK!!!!! ############
	########## THIS IS THE BIT THAT DOESN'T APPEAR TO WORK!!!!! ############
	########## THIS IS THE BIT THAT DOESN'T APPEAR TO WORK!!!!! ############
	$o_checkbox[$s_column_name].Add_CheckStateChanged({
		if ($o_checkbox[$s_column_name].Checked) {
			$h_selected_columns[$s_column_name]=$True
		}
		else {
			$h_selected_columns[$s_column_name]=$False
		}
	})

	# Add this checkbox to the group of checkboxes
    $i_Offset += 30
    $o_groupBox.Controls.Add($o_checkbox[$s_column_name]) 
    $o_checkboxes += $o_checkbox[$s_column_name]
}

### Resize the groupbox to the size of the list of checkboxes
$i_groupBox_size = 31.5*$o_checkboxes.Count
$o_groupBox.size = New-Object System.Drawing.Size(585,$i_groupBox_size)
$o_OKButton.Location = New-Object System.Drawing.Size(10, (20+$i_groupBox_size))
$o_CancelButton.Location = New-Object System.Drawing.Size(100, (20+$i_groupBox_size))

### Pop the form and check return value.
if ($o_form.ShowDialog() -eq "Cancel")
{
	### User decided not to proceed with processing the CSV file
	Exit 0
}

###
# User has made their selection and now we process the CSV outputting only the
# selected columns.... OR I WOULD IF I GOT MORE THAN ONE ENTRY!!!!
###
$h_selected_columns

###
# Clean exit
###
Exit 0

The script executes without error, but doesn’t work and appears to be overwriting all the checkbox handlers with the same (final) value of $s_column_name for all the handlers. No matter what I check I always get the final column coming back (and also not true but I’ll worry about that later!). Also worth noting that I created the array/hash of checkboxes in an attempt to resolve this issue, it was simply reusing the same variable in the loop originally.

I found another post which I think tackles this but I didn’t understand WHY this fails to work. I think understanding why something doesn’t work is as valuable us understanding the solution, and certainly better than blindly just copying someone else’s code?!

Can anyone please try to explain why my array/hash of checkboxes is not building correctly and instead using only the last column variable/value regardless which checkboxes I select?

Would really appreciate some guidance. Cheers,
CP.

I didn’t get the same result as you. I would only get back the ones that were chosen. It appears you want a listing of all checkboxes with a status of true/false depending on if they are selected or not. Also, I learned the hard way (like you are) about closures. Scriptblocks are magical, one little addition you’ll see is .GetNewClosure(). I used to know better the details but I’m afraid that memory location in my brain is currently unavailable. Try these changes.

###
# Process required paramter(s)
###
param(
	[Parameter (Mandatory=$True)][Alias("CSV")][ValidateScript({Test-Path $_ -PathType leaf})][string]$f_CSV
)

###
# Global/Script variables
###
$h_selected_columns = @{}

###
# Grab the contents of the CSV and use the first line to allow selection of
# the columns to retain.
###
$o_CSV = Import-Csv -Path $f_CSV

### Pull in the libraries for the GUI
[void] [System.Reflection.Assembly]::LoadWithPartialName("System.Drawing") 
[void] [System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")  

### Create the form we shall display in.
$o_form = New-Object System.Windows.Forms.Form    
$o_form.Size = New-Object System.Drawing.Size(600,700)
$o_form.AutoScroll = $true
$o_form.text ="Select required CSV columns" 

### Add the Okay and Concal buttons to the form
# Create OK button
$o_OKButton = New-Object System.Windows.Forms.Button
$o_OKButton.Text = "Okay"
$o_OKButton.DialogResult = "OK"
$o_OKButton.Add_Click({
    # Handle OK button click here
	$o_form.Close()
})

# Create Cancel button
$o_CancelButton = New-Object System.Windows.Forms.Button
$o_CancelButton.Text = "Cancel"
$o_CancelButton.DialogResult = "Cancel"
$o_CancelButton.Add_Click({
    # Handle Cancel button click here
    $o_form.Close()
})

# Add buttons to the form
$o_form.Controls.Add($o_OKButton)
$o_form.Controls.Add($o_CancelButton)

### Create a group for the checkboxes
$o_groupBox = New-Object System.Windows.Forms.GroupBox
$o_groupBox.Location = New-Object System.Drawing.Size(5,5)  
$o_groupBox.text = "Availabe CSV columns:" 
$o_form.Controls.Add($o_groupBox)

### Initilise checkboxes (create first empty checkbox?)
$o_checkboxes += New-Object System.Windows.Forms.CheckBox
$o_checkboxes.Location = New-Object System.Drawing.Size(10,30) 
$o_checkboxes = @() # cast as an array?

###
# Set the spacing to use for the checkboxes, initialise a hash table for the checkbox objects and
# then loop through the column headers.
###
$i_Offset = 20
$o_checkbox = @{}
foreach ($s_column_name in $o_CSV[0].psobject.Properties.Name)
{
	# Setup the new checkbox
    $o_checkbox[$s_column_name] = New-Object System.Windows.Forms.CheckBox
    $o_checkbox[$s_column_name].Text = "Column: $s_column_name"
    $o_checkbox[$s_column_name].Location = New-Object System.Drawing.Size(10,$i_Offset) 
	$o_checkbox[$s_column_name].AutoSize = 1
	
	# Since you want to see all the columns, by default set it to false. If they are chosen they will be updated.
    $h_selected_columns[$s_column_name]=$False

	# Setup the handler for this checkbox
        # The magic GetNewClosure method will make the scriptblock remember it's state at that exact time
	$o_checkbox[$s_column_name].Add_CheckStateChanged({
		if ($o_checkbox[$s_column_name].Checked) {
			$h_selected_columns[$s_column_name]=$True
		}
	}.GetNewClosure())

	# Add this checkbox to the group of checkboxes
    $i_Offset += 30
    $o_groupBox.Controls.Add($o_checkbox[$s_column_name]) 
    $o_checkboxes += $o_checkbox[$s_column_name]
}

### Resize the groupbox to the size of the list of checkboxes
$i_groupBox_size = 31.5*$o_checkboxes.Count
$o_groupBox.size = New-Object System.Drawing.Size(585,$i_groupBox_size)
$o_OKButton.Location = New-Object System.Drawing.Size(10, (20+$i_groupBox_size))
$o_CancelButton.Location = New-Object System.Drawing.Size(100, (20+$i_groupBox_size))

### Pop the form and check return value.
if ($o_form.ShowDialog() -eq "Cancel")
{
	### User decided not to proceed with processing the CSV file
	Exit 0
}

###
# User has made their selection and now we process the CSV outputting only the
# selected columns.... OR I WOULD IF I GOT MORE THAN ONE ENTRY!!!!
###
$h_selected_columns

###
# Clean exit
###
Exit 0

Thank you @krzydoug , adding the `.GetNewClosure()’ did the trick. I would have spent a VERY long time working that out so I can’t thank you enough!

There was one little glitch in your fine offering which was the removal of the else clause in the handler, this is actually needed in the event someone changes their mind and unchecks a checkbox; it needs to be reset to false.

With the script working I was able to remove my earlier failed attempt, the use of a hash table for the checkboxes. I must go and surf the heck out of .GetNewClosure(). to understand what is going on but once added each iteration setting up handlers worked using a local/working variable.

In the end it the bit that matters looked like this:

###
# Set the spacing to use for the checkboxes and then loop through the column headers.
###
$i_Offset = 20
foreach ($s_column_name in $o_CSV[0].psobject.Properties.Name)
{
	# Setup the new checkbox
    $o_checkbox = New-Object System.Windows.Forms.CheckBox
    $o_checkbox.Text = "Column: $s_column_name"
    $o_checkbox.Location = New-Object System.Drawing.Size(10,$i_Offset) 
	$o_checkbox.AutoSize = 1
	
	# Initilise the results hash table,  set default values to false.
	$h_selected_columns[$s_column_name]=$False
	
	# Setup the handler for this checkbox
	$o_checkbox.Add_CheckStateChanged({
		if ($o_checkbox.Checked) {
			$h_selected_columns[$s_column_name]=$True
		}
		else {
			$h_selected_columns[$s_column_name]=$False
		}
	}.GetNewClosure())

	# Add this checkbox to the group of checkboxes
    $i_Offset += 30
    $o_groupBox.Controls.Add($o_checkbox) 
    $o_checkboxes += $o_checkbox
}

Thanks again, really appreciate it. Cheers,
CP.