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.
- 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)
})
}
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.
[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)
}.GetNewClosure())
# -- 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:
