2

I am working on a PowerShell script with a small WPF GUI. My code is organzized in a class from which a singleton is created. I have read that $this inside an event handler script block points to the event sender and not to my containing class instance. How can I access my class instance from the event handler?

Ex.

class MyClass {

    $form  #Reference to the WPF form


    [void] StartAction([object] $sender, [System.Windows.RoutedEventArgs] $e) {
        
        ...
    }


    [void] SetupEventHandlers() {

        $this.form.FindName("BtnStartAction").add_Click({ 
            param($sender, $e) 
            # !!!! Does not work, $this is not my class instance but the event sender !!!!
            $this.StartAction($sender, $e) 
        })

    }


    [void] Run() {

        $this.InitWpf() #Initializes the form from a XAML file.

        $this.SetupEventHandlers()

        ...
    }
}


$instance = [MyClass]::new()
$instance.Run()
2
  • 3
    Can you try creating an instance inside SetupEventHandlers like $instance=$this and use $instance inside the handler(add_Click)? Commented Nov 22, 2021 at 17:20
  • the $sender is your class in add_click. StartAction is not an event handler, it is just a method of your class. Modify StartAction signature and call the method ([MyClass]$sender).StartAction() in the event handler by casting $sender Commented Nov 22, 2021 at 19:32

2 Answers 2

4
  • Indeed, the automatic $this variable in a script block acting as a .NET event handler refers to the event sender.

  • If an event-handler script block is set up from inside a method of a PowerShell custom class, the event-sender definition of $this shadows the usual definition in a class method (referring to the class instance at hand).

There are two workarounds, both relying on PowerShell's dynamic scoping, which allows descendant scopes to see variables from ancestral scopes.

  • Preferably, use Get-Variable -Scope 1 to reference the parent scope's $this value (event-handler script blocks run in a child scope of the caller).
    [void] SetupEventHandlers() {

        $this.form.FindName("BtnStartAction").add_Click({ 
            param($sender, $e) 
            # Get the value of $this from the parent scope.
            (Get-Variable -ValueOnly -Scope 1 this).StartAction($sender, $e) 
        })

    }
  • Alternatively, taking more direct advantage of dynamic scoping, you can go with Abdul Niyas P M's suggestion, namely to define a helper variable in the caller's scope that references the custom-class instance under a different name, which you can reference in - potentially multiple - event-handler script blocks set up from the same method:

    • Note that a call to .GetNewClosure() is required on the script block, so as to make the helper variable available inside the script block.Tip of the hat to Sven.

    • Note that you'll need to apply to this technique to each and every method from which event handlers are set up, so if there are multiple ones (not in your case), the (Get-Variable -ValueOnly -Scope 1 this) option is the simpler solution.

    [void] SetupEventHandlers() {

        # Set up a helper variable that points to $this
        # under a different name.
        $thisClassInstance = $this

        # Note the .GetNewClosure() call.
        $this.form.FindName("BtnStartAction").add_Click({ 
            param($sender, $e) 
            # Reference the helper variable.             
            $thisClassInstance.StartAction($sender, $e) 
        }.GetNewClosure())

    }

Also note that, as of PowerShell 7.2, you cannot directly use custom-class methods as event handlers - this answer shows workarounds, which also require solving the $this shadowing problem.


Self-contained sample code:

  • Important: Before running the code below, ensure that you have run the following in your session:

    # Load WPF assemblies.
    Add-Type -AssemblyName PresentationCore, PresentationFramework
    
  • Unfortunately, placing this call inside a script that contains the code below does not work, because class definitions are processed at parse time, i.e., before the Add-Type command runs, whereas all .NET types referenced by a class must already be loaded - see this answer.

    • While using assembly statements in lieu of Add-Type calls may some day be a solution (they too are processed at parse time), their types aren't currently discovered until runtime, leading to the same problem - see GitHub issue #3641; as a secondary problem, well-known assemblies cannot currently be referenced by file name only; e.g., using assembly PresentationCore.dll does not work, unlike Add-Type -AssemblyName PresentationCore - see GitHub issue #11856
# IMPORTANT: 
#  Be sure that you've run the following to load the WPF assemblies
#  BEFORE calling this script:
#    Add-Type -AssemblyName PresentationCore, PresentationFramework

class MyClass {

  $form  #Reference to the WPF form

  [void] InitWpf() {
    [xml] $xaml=@"
    <Window
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
            xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
            xmlns:local="clr-namespace:Test"
            Title="MainWindow" Height="500" Width="500">
        <Grid>
            <Button x:Name="BtnStartAction" Content="StartAction" HorizontalAlignment="Left" VerticalAlignment="Top" Width="100" IsDefault="True" Height="22" Margin="170,0,0,0" />        
            <TextBox x:Name="Log" Height="400" TextWrapping="Wrap" VerticalAlignment="Top" AcceptsReturn="True" AcceptsTab="True" Padding="4" VerticalScrollBarVisibility="Auto" Margin="0,40,0,0"/>
        </Grid>
    </Window>
"@    
    $this.form = [Windows.Markup.XamlReader]::Load((New-Object System.Xml.XmlNodeReader $xaml))
  }

  [void] StartAction([object] $sender, [System.Windows.RoutedEventArgs] $e) {
      
      $tb = $this.form.FindName("Log")
      $tb.Text += $e | Out-String
  }


  [void] SetupEventHandlers() {

      $btn = $this.form.FindName("BtnStartAction")

      # -- Solution with Get-Variable
      $btn.add_Click({ 
          param($sender, $e) 
          (Get-Variable -ValueOnly -Scope 1 this).StartAction($sender, $e) 
      })

      # -- Solution with helper variable.
      # Note the need for .GetNewClosure()
      # Helper variable that points to $this under a different name.
      $thisClassInstance = $this
      $btn.add_Click({ 
          param($sender, $e) 

          $thisClassInstance.StartAction($sender, $e)
      }.GetNewClosure())

  }

  [void] Run() {
      $this.InitWpf() #Initializes the form from a XAML file.
      $this.SetupEventHandlers()
      $this.form.ShowDialog()
  }
}


$instance = [MyClass]::new()
$instance.Run()

The above demonstrates both workarounds: when you press the StartAction button, both event handlers should add the event-arguments object they've each received to the text box, as shown in the following screenshot:

screenshot

Sign up to request clarification or add additional context in comments.

Comments

-1

Try this sample

class MyClass {

    $form  #Reference to the WPF form


    [void] StartAction([System.Windows.RoutedEventArgs] $e) {
        #$this ...
    }


    [void] SetupEventHandlers() {

        $this.form.FindName("BtnStartAction").add_Click({ 
            param($sender, $e) 
            ([MyClass]$sender).StartAction($e)
        })

    }


    [void] Run() {
        $this.InitWpf()
        $this.SetupEventHandlers()
    }
}


$instance = [MyClass]::new()
$instance.Run()

Edit 1: Could you try creating a delegate referring to a dedicated method of the class like this?

class MyClass {

    $form  #Reference to the WPF form
    
    [void] StartAction() {
        #$this ...
    }


    [void] SetupEventHandlers() {
        $handler = [System.EventHandler]::CreateDelegate([System.EventHandler], $this, "Handler")
    
        $this.form.FindName("BtnStartAction").add_Click($handler)

    }

    [void] Handler ([System.Object]$sender, [System.EventArgs]$e) {
        $this.StartAction()
    }

    [void] Run() {
        $this.InitWpf()
        $this.SetupEventHandlers()
    }
}


$instance = [MyClass]::new()
$instance.Run()

2 Comments

Note that the $sender in the event-handler script block is the event sender, i.e. the WPF button control, not the custom-class instance at hand. Unfortunately, $this _also refers to the event sender, and it is this shadowing of $this that makes it challenging to refer to the custom-class instance at hand.
I confirm that this does not work for me. Debugging shows: $sender.Name is BtnStartAction, so it's the button and not the custom-class instance and trying to cast it to MyClass fails.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.