PowerShell classes and .NET events - powershell

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()

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:

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()

Related

Powershell add type in class in PSM1

In a PS1, this works
class pxInitFailureMessage {
static [void] Send ([int32]$processID, [String]$title, [string]$message) {
Add-Type -AssemblyName:System.Windows.Forms
Add-Type -AssemblyName:System.Drawing
$balloonTip = New-Object System.Windows.Forms.NotifyIcon
$balloonTip.icon = [system.drawing.icon]::ExtractAssociatedIcon($(Get-Process -id:$processID | Select-Object -expandProperty:Path))
$balloonTip.balloonTipIcon = 'Error'
$balloonTip.balloonTipTitle = $title
$balloonTip.balloonTipText = $message
$balloonTip.visible = $true
$balloonTip.ShowBalloonTip(0)
$balloonTip.Dispose
}
}
[pxInitFailureMessage]::Send($pid, 'Title', 'Message is just some extra text')
But, move that class to a library.PSM1 file, and this in the PS1
Using module '\\Mac\iCloud Drive\Px Tools 4.#\Dev 4.0\#Spikes\Windows7\library.psm1'
[pxInitFailureMessage]::Send($pid, 'Title', 'Message is just some extra text')
And it works in the ISE, but not in the console when run from a shortcut. I get Unable to find type [system.drawing.icon].
Obviously the first Add-Type works, I get no error at New-Object. So why is the second type load failing? I also tried moving the two Add-Type lines out of the class, and into the root of the module file, with the same results. What DOES work is adding those lines to the PS1, between using module (which has to be the first non remarked line) and the call. That works, but then you don't have a self contained class, which seems to suck a bit. What am I misunderstanding here?
How does one make a self contained class that uses .NET types and work with it from a module file?
For anyone interested, the solution I came up with is as follows. In library.PSM1 I have...
class pxInitFailureMessage {
static [void] Send ([int32]$processID, [String]$title, [string]$message, [System.Drawing.Icon]$icon) {
Add-Type -AssemblyName:System.Windows.Forms
$balloonTip = New-Object System.Windows.Forms.NotifyIcon
$balloonTip.icon = $icon
$balloonTip.balloonTipIcon = 'Error'
$balloonTip.balloonTipTitle = $title
$balloonTip.balloonTipText = $message
$balloonTip.visible = $true
$balloonTip.ShowBalloonTip(0)
$balloonTip.Dispose
}
}
And in my scrip I have...
using assembly System.Drawing
Using module '.\library.psm1'
$processIcon = [system.drawing.icon]::ExtractAssociatedIcon($(Get-Process -id:$pID | Select-Object -expandProperty:Path))
[pxInitFailureMessage]::Send($pid, 'Title', 'Message is just some extra text', $processIcon)
The key is that the class IS now self contained, and follows good practice of providing the class with everything it needs, so it doesn't have to go looking outside itself. I could just as easily use my own ICO file for the icon, like so...
$processIcon = [system.drawing.icon]::New('\\Mac\iCloud Drive\Px Tools 4.#\Dev 4.0\Px_Resources\PxIcon.ico')
Again with a literal path in this example.
So, I learned a thing today. Woot.

Running Access Macro in Powershell

I'm trying to run an Access 2010 macro in PowerShell (v4.0 Windows 8.1) with the below code:
$Access = New-Object -com Access.Application
$Access.OpenCurrentDatabase("SomePath", $False, "Password")
$Access.Run("SomeProc")
$Access.CloseCurrentDatabase()
$Access.Quit()
[System.Runtime.InteropServices.Marshal]::ReleaseComObject($Access)
Remove-Variable Access
I get an error on the line $Access.Run("SomeProc") that there's not enough parameters specified:
Exception calling "Run" with "1" argument(s): "Invalid number of parameters. (Exception
from HRESULT: 0x8002000E (DISP_E_BADPARAMCOUNT))"
The procedure SomeProc does not require any parameters.
I've read the msdn article on the run method and only one parameter is required.
I've also tried this workaround which also failed to work for an unrelated reason.
Does anyone know what the cause of the error could be and how to get the method working?
This is a driver issue where the OLEDB libraries aren't loading correctly.
I was able to reproduce your error exactly, and I was able to work around it by opening Powershell from your SysWow directory instead of System32.
Try opening this version of Powershell (you'll have to run set-executionpolicy again), and see if it'll execute your script.
%SystemRoot%\syswow64\WindowsPowerShell\v1.0\powershell.exe
Helpful link: https://social.msdn.microsoft.com/Forums/en-US/4500877f-0031-426e-869d-bda33d9fe254/microsoftaceoledb120-provider-cannot-be-found-it-may-not-be-properly-installed?forum=adodotnetdataproviders
The C# signature is something like this:
public object Run(string Procedure, ref object Arg1, ... ref object Arg30) ...
It means that COM the Arg optional arguments are not optional in .NET because they are explicitly marked as [ref]. You need to provide all 32 args even if you don't use them.
Assuming you have the following VBA code:
Public Sub Greeting(ByVal strName As String)
MsgBox ("Hello, " & strName & "!"), vbInformation, "Greetings"
End Sub
You can either use call it like this:
$Access = New-Object -com Access.Application
$Access.OpenCurrentDatabase("Database1.accdb")
$runArgs = #([System.Reflection.Missing]::Value) * 31
$runArgs[0] = "Greeting" #Method Name
$runArgs[1] = "Jeno" #First Arg
$Access.GetType().GetMethod("Run").Invoke($Access, $runArgs)
In your case it will be:
$runArgs = #([System.Reflection.Missing]::Value) * 31
$runArgs[0] = "SomeProc"
$Access.GetType().GetMethod("Run").Invoke($Access, $runArgs)
I would probably try to add a helper to the access object:
Add-Member -InputObject $Access -MemberType ScriptMethod -Name "Run2" -Value {
$runArgs = #([System.Reflection.Missing]::Value) * 31
for($i = 0; $i -lt $args.Length; $i++){ $runArgs[$i] = $args[$i] }
$this.GetType().GetMethod("Run").Invoke($this, $runArgs)
}
Then you can use Run2 as you would expect:
$Access.Run2("Greeting", "Jeno")
$Access.Run2("SomeProc")

Powershell pass a function as a parameter

I am a newbie to the world of programming and I am trying to create a form using functions to create the buttons and labels etc. The form is created with the exception that the functions passed to on button click events are not being passed correctly. For example I have a function to create a button....
function new_btn ($name, $parent, $x, $y, $l, $h, $text, $onClick){
$object = New-Object System.Windows.Forms.Button
$object.Location = New-Object System.Drawing.Point($x, $y)
$Object.Size = New-Object System.Drawing.Size($l, $h)
$Object.Text = $text
$object.add_Click({$onClick})
New-Variable $name -Value $object -Scope global
(Get-Variable $parent).Value.Controls.Add((Get-Variable $name).value)
}
I then have the function that I want to run on the button click.....
function msg {
[System.Windows.Forms.MessageBox]::Show("We are proceeding with next step.")
}
I then call the function and feed it the parameters.......
new_btn getdbslist tab1 20 50 69 23 "Get DB's" msg
This produces the button as expected and adds it to tab1, but the on click event will not work, nothing happens at all. Any help would be very appreciated!
You are just passing a string. Instead pass a script block:
new_btn getdbslist tab1 20 50 69 23 'Get DBs' {
[System.Windows.Forms.MessageBox]::Show("We are proceeding with next step.")
}
And in your new_btn function you probably just need to use
$object.add_Click($onClick)
If you really want to pass a string, then you probably need to use the following:
$object.add_Click({ & $onClick })

Powershell Progress Bar in Windows Forms

I'm trying to add a progress bar to a form in powershell. I do not want to use PowerShell's Write-Progress cmdlet (because when I run the script from command line, it shows a text-based progress bar and I always want a form/graphic based bar).
I've tried this and it seems to work(found online):
[reflection.assembly]::loadwithpartialname("System.Windows.Forms") | Out-Null
[reflection.assembly]::loadwithpartialname("System.Drawing") | Out-Null
$form_main = New-Object System.Windows.Forms.Form
$progressBar1 = New-Object System.Windows.Forms.ProgressBar
$timer1 = New-Object System.Windows.Forms.Timer
$timer1_OnTick = {
$progressBar1.PerformStep()
}
$form_main.Text = 'ProgressBar demo'
$progressBar1.DataBindings.DefaultDataSourceUpdateMode = 0
$progressBar1.Step = 1
$progressBar1.Name = 'progressBar1'
$form_main.Controls.Add($progressBar1)
$timer1.Interval = 100
$timer1.add_tick($timer1_OnTick)
$timer1.Start()
$form_main.ShowDialog()| Out-Null
However, I do not want an event to update the progress bar (as does $timer1_OnTic in the example above) I want to update it myself by making calls throughout my script such as:
$progressBar1.PerformStep()
Or
$progressBar1.Value = 10
So it seems I need some sort of background worker that updates the progress bar whenever I make calls to PerformStep() or change the value of the progressBar
Calling ShowDialog stops all processing inside the script until the form is closed.
If I understand correctly, you should be able to change ShowDialog() to Show(), which will display the Dialog without blocking your script. You can then continue execution and update the progress bar.
You may be disappointed in the lack of interactivity of the form though.
A method I have had some success with is to use a child runspace for the GUI (in this case WPF) so it doesn't lock the script. Data can be accessed in both the parent and sub runspaces via the session state proxy.
e.g.
# define the shared variable
$sharedData = [HashTable]::Synchronized(#{});
$sharedData.Progress = 0;
$sharedData.state = 0;
$sharedData.EnableTimer = $true;
# Set up the runspace (STA is required for WPF)
$rs = [RunSpaceFactory]::CreateRunSpace();
$rs.ApartmentState = "STA";
$rs.ThreadOptions = "ReuseThread";
$rs.Open();
# configure the shared variable as accessible from both sides (parent and child runspace)
$rs.SessionStateProxy.setVariable("sharedData", $sharedData);
# define the code to run in the child runspace
$script = {
add-Type -assembly PresentationFramework;
add-Type -assembly PresentationCore;
add-Type -assembly WindowsBase;
[xml]$xaml = #"
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
MaxHeight="100" MinHeight="100" Height="100"
MaxWidth="320" MinWidth="320" Width="320"
WindowStyle="ToolWindow">
<Canvas Grid.Row="1">
<TextBlock Name="ProgressText" Canvas.Top="10" Canvas.Left="20">Hello world</TextBlock>
<ProgressBar Name="ProgressComplete" Canvas.Top="30" Canvas.Left="20" Width="260" Height="20" HorizontalAlignment="Center" Value="20" />
</Canvas>
</Window>
"#
# process the xaml above
$reader = New-Object System.Xml.XmlNodeReader $xaml;
$dialog = [Windows.Markup.XamlReader]::Load($reader);
# get an handle for the progress bar
$progBar = $dialog.FindName("ProgressComplete");
$progBar.Value = 0;
# define the code to run at each interval (update the bar)
# DON'T forget to include a way to stop the script
$scriptBlock = {
if ($sharedData.EnableTimer = $false) {
$timer.IsEnabled = $false;
$dialog.Close();
}
$progBar.value = $sharedData.Progress;
}
# at the timer to run the script on each 'tick'
$dialog.Add_SourceInitialized( {
$timer = new-Object System.Windows.Threading.DispatherTimer;
$timer.Interface = [TimeSpan]"0:0:0.50";
$timer.Add_Tick($scriptBlock);
$timer.Start();
if (!$timer.IsEnabled) {
$dialog.Close();
}
});
# Start the timer and show the dialog
&$scriptBlock;
$dialog.ShowDialog() | out-null;
}
$ps = [PowerShell]::Create();
$ps.Runspace = $rs;
$ps.AddScript($script).BeginInvoke();
# if you want data from your GUI, you can access it through the $sharedData variable
Write-Output $sharedData;
If you try this code, once the dialog is displayed you can change the progress bar by setting the value of $sharedData.Progress
This has allowed me to write plenty of dialogs for tools, I'm constrained by our infrastructure to use powershell from within a runspace and WPF seems to work much better than forms.
Have a look at Posh Progress Bar it has horizontal, vertical and circle progress bars.

Do helper functions in a NuGet Packages init.ps1 have to be global?

I'm wanting to write a couple commands for the NuGet package manager console to insert Gists from GitHub. I have 4 basic commands
List-Gists 'user'
Gist-Info 'gistId'
Gist-Contents 'gistId' 'fileName'
Gist-Insert 'gistId' 'fileName'
All of my commands depend on a couple utility functions, and I'm struggling with whether they need to be global or not.
# Json Parser
function parseJson([string]$json, [bool]$throwError = $true) {
try {
$result = $serializer.DeserializeObject( $json );
return $result;
} catch {
if($throwError) { throw "ERROR: Parsing Error"}
else { return $null }
}
}
function downloadString([string]$stringUrl) {
try {
return $webClient.DownloadString($stringUrl)
} catch {
throw "ERROR: Problem downloading from $stringUrl"
}
}
function parseUrl([string]$url) {
return parseJson(downloadString($url));
}
Can I just have these utility functions outside of my global functions, or will I need to include them in each of the global functions definition scope somehow?
No they don't. From your init.ps1 you can import a powershell module that you wrote (psm1) file and moving forward, this will be the way we recommend adding methods to the console environment.
Your init.ps1 would look something like this:
param($installPath, $toolsPath)
Import-Module (Join-Path $toolsPath MyModule.psm1)
In MyModule.psm1:
function MyPrivateFunction {
"Hello World"
}
function Get-Value {
MyPrivateFunction
}
# Export only the Get-Value method from this module so that's what gets added to the nuget console environment
Export-ModuleMember Get-Value
You can get more information on modules here http://msdn.microsoft.com/en-us/library/dd878340(v=VS.85).aspx