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:

SetupEventHandlerslike$instance=$thisand use$instanceinside the handler(add_Click)?$senderis your class inadd_click.StartActionis not an event handler, it is just a method of your class. ModifyStartActionsignature and call the method([MyClass]$sender).StartAction()in the event handler by casting$sender