Search code examples
powershellscopedropbox

Powershell - lambda function - modified variable scope


I have a windows form with a drop box I need to save the selected index, but out of the lambda scope the variable is still set to zero

$List = New-Object system.Windows.Forms.ComboBox
$List.text = “”
$List.Size = New-Object System.Drawing.Size(280,20)
# Add the items in the dropdown list
@("a","b") | ForEach-Object {[void] $List.Items.Add($_)}
# Select the default value
$List.SelectedIndex = 0
$List.location = New-Object System.Drawing.Point(10,$Y); $Y+=30
$List.Font = ‘Microsoft Sans Serif,10’
$selected=0
$List.add_SelectedIndexChanged({
   ([ref]$selected) = $List.SelectedIndex
})
$form.Controls.Add($List)

then when I show the dialog and check the value

if ($form.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) { return $selected }

variable $selected is =0 even if I select the second element in the list

am I missing something ?

thanks for your help


Solution

  • ([ref]$selected) returns a [ref] instance that wraps either a given value or - when casting a variable to it - is a dynamic reference to that variable. Either way, the wrapped value must be accessed via the .Value property.

    Therefore, replace ([ref]$selected) = $List.SelectedIndex with:

    # Note the need for .Value
    ([ref] $selected).Value = $List.SelectedIndex
    

    Note:

    • The primary purpose of [ref] is to pass ref or out parameter values to .NET APIs.

    • Here, you're repurposing it to refer to $selected variable defined in an ancestral (parent) scope in a manner that allows updating it.

      • If you did just $selected = $List.SelectedIndex, a local $selected variable would implicitly be created,[1] confined to the event-handler script block, given that such script blocks run in a child scope of the caller.

    Conceptually clearer alternatives:

    • If you know the $selected variable to have been created in the script scope (as is true in your case), you can use the $script: scope specifier to refer to it, which also allows updating it:

      # Update the $selected variable in the *script* scope.
      $script:selected = $List.SelectedIndex
      
    • If you want to update the variable in the parent scope - which may or may not the script scope - use the Set-Variable cmdlet with -Scope 1:

      # Update the $selected variable in the *parent* scope (-Scope 1)
      Set-Variable -Scope 1 -Name selected -Value $List.SelectedIndex
      
    • If you want to update the variable in the closest ancestral scope in which it was defined (whatever scope that may be), use Get-Variable and assign to the returned variable object's .Value property:

      (Get-Variable -Name selected).Value $List.SelectedIndex
      
      • This is the equivalent of the ([ref] $selected).Value = ... technique; note that both techniques require that such an ancestral variable already exist - by contrast, the $script:selected = ... and Set-Variable -Scope 1 selected ... techniques create the variable on demand.

    As for what you tried:

    Trying the non-effective form ([ref]$selected) = $List.SelectedIndex is understandable, and the fact that such an assignment is seemingly quietly ignored makes it harder to detect the problem:

    In short: Your attempt created a local $selected variable containing a [ref] instance that (statically) wraps the value of $List.SelectedIndex:

    • ([ref] $selected) = $List.SelectedIndex is the same as [ref] $selected = $List.SelectedIndex, which is a regular type-constrained variable assignment.

      • That enclosing the assignment target in (...) is effectively ignored may be surprising, but that's how it has always worked.
    • That is, variable $selected is assigned to, which implicitly creates a local variable, and - by virtue of the "cast" placed to the left of the target variable - the values it can hold are constrained to instance of [ref], meaning that you can only assign values that either already are [ref] instances or are convertible to [ref].

    • Because any value can be converted to [ref] (e.g. [ref] 1), the newly created local $selected variable ended up containing a [ref] instance that statically wraps the assigned value, i.e. the then-current value of $List.SelectedIndex.

      • If we take the type-constraining aspect out of the picture, your attempt was equivalent to the following, which makes it clearer why it didn't work:

        # Creates *local* variable $selected, holding a [ref] instance.
        $selected = [ref] $List.SelectedIndex
        
    • Because a local variable was accidentally created, the script-level definition of $selected remained unchanged.


    [1] This perhaps surprising behavior is explained in this answer.