Output PowerShell Runspace Function to WPF textbox - wpf

I am having trouble getting my "sync-folder" function to output to the textbox as it runs. I have tried looking over a lot of guides around the net including searching here and I'm not able to figure it out. I am hoping someone who understands this better than me can show me how to do it in my test example below:
$Global:syncHash = [hashtable]::Synchronized(#{})
$newRunspace =[runspacefactory]::CreateRunspace()
$newRunspace.ApartmentState = "STA"
$newRunspace.ThreadOptions = "ReuseThread"
$newRunspace.Open()
$newRunspace.SessionStateProxy.SetVariable("syncHash",$syncHash)
# Load WPF assembly if necessary
[void][System.Reflection.Assembly]::LoadWithPartialName('presentationframework')
$psCmd = [PowerShell]::Create().AddScript({
[xml]$xaml = #"
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="TestApp" Height="450" Width="800">
<Grid>
<Button x:Name="runButton" Content="Run" HorizontalAlignment="Left" Margin="293,37,0,0" VerticalAlignment="Top" Width="189" Height="40"/>
<TextBox x:Name="textbox" TextWrapping="NoWrap" FontFamily="Consolas" ScrollViewer.VerticalScrollBarVisibility="Auto" IsReadOnly="True" Margin="10,137,10,10"/>
</Grid>
</Window>
"#
# Remove XML attributes that break a couple things.
# Without this, you must manually remove the attributes
# after pasting from Visual Studio. If more attributes
# need to be removed automatically, add them below.
$AttributesToRemove = #(
'x:Class',
'mc:Ignorable'
)
foreach ($Attrib in $AttributesToRemove) {
if ( $xaml.Window.GetAttribute($Attrib) ) {
$xaml.Window.RemoveAttribute($Attrib)
}
}
$reader=(New-Object System.Xml.XmlNodeReader $xaml)
$syncHash.Window=[Windows.Markup.XamlReader]::Load( $reader )
[xml]$XAML = $xaml
$xaml.SelectNodes("//*[#*[contains(translate(name(.),'n','N'),'Name')]]") | %{
#Find all of the form types and add them as members to the synchash
$syncHash.Add($_.Name,$syncHash.Window.FindName($_.Name) )
}
$Script:JobCleanup = [hashtable]::Synchronized(#{})
$Script:Jobs = [system.collections.arraylist]::Synchronized((New-Object System.Collections.ArrayList))
#region Background runspace to clean up jobs
$jobCleanup.Flag = $True
$newRunspace =[runspacefactory]::CreateRunspace()
$newRunspace.ApartmentState = "STA"
$newRunspace.ThreadOptions = "ReuseThread"
$newRunspace.Open()
$newRunspace.SessionStateProxy.SetVariable("jobCleanup",$jobCleanup)
$newRunspace.SessionStateProxy.SetVariable("jobs",$jobs)
$jobCleanup.PowerShell = [PowerShell]::Create().AddScript({
#Routine to handle completed runspaces
Do {
Foreach($runspace in $jobs) {
If ($runspace.Runspace.isCompleted) {
[void]$runspace.powershell.EndInvoke($runspace.Runspace)
$runspace.powershell.dispose()
$runspace.Runspace = $null
$runspace.powershell = $null
}
}
#Clean out unused runspace jobs
$temphash = $jobs.clone()
$temphash | Where {
$_.runspace -eq $Null
} | ForEach {
$jobs.remove($_)
}
Start-Sleep -Seconds 1
} while ($jobCleanup.Flag)
})
$jobCleanup.PowerShell.Runspace = $newRunspace
$jobCleanup.Thread = $jobCleanup.PowerShell.BeginInvoke()
#endregion Background runspace to clean up jobs
#=========================================
#=========================================
#=========================================
#================= runButton =============
$syncHash.runButton.Add_Click({
$newRunspace =[runspacefactory]::CreateRunspace()
$newRunspace.ApartmentState = "STA"
$newRunspace.ThreadOptions = "ReuseThread"
$newRunspace.Open()
$newRunspace.SessionStateProxy.SetVariable("SyncHash",$SyncHash)
$PowerShell = [PowerShell]::Create().AddScript({
Function Update-Window {
Param (
$Control,
$Property,
$Value,
[switch]$AppendContent
)
# This is kind of a hack, there may be a better way to do this
If ($Property -eq "Close") {
$syncHash.Window.Dispatcher.invoke([action]{$syncHash.Window.Close()},"Normal")
Return
}
# This updates the control based on the parameters passed to the function
$syncHash.$Control.Dispatcher.Invoke([action]{
# This bit is only really meaningful for the TextBox control, which might be useful for logging progress steps
If ($PSBoundParameters['AppendContent']) {
$syncHash.$Control.AppendText($Value)
} Else {
$syncHash.$Control.$Property = $Value
}
}, "Normal")
}
#============== START stuff to do =================
function sync-folder {
function timestamp {
$timestamp = "$(Get-Date -f 'yyyy-MM-dd HH:mm:ss:fff')"
Write-Output "$timestamp "
}
$MainLog = "C:\ProgramData\test\Logs\Main_Log.txt"
$SyncLog = "C:\ProgramData\test\Logs\Sync_Log.txt"
$RemoteSync = "\\remote\sync\path\test"
$RobocopySource = "\\remote\location\"
$RobocopyDestination = "C:\ProgramData\test"
# Check if Log exists...
# If not exist, then create it:
If (!(Test-Path $MainLog)) {
# Create MainLog.txt
New-Item -ItemType File -Force $MainLog
# Write to log:
Write-Output "$(timestamp) Main Log File has been created." >> $MainLog
}
If (!(Test-Path $SyncLog)) {
# Create MainLog.txt
New-Item -ItemType File -Force $SyncLog
# Write to log:
Write-Output "$(timestamp) Sync Log File has been created." >> $SyncLog
}
if (Test-Path $RemoteSync) {
Write-Output "$(timestamp) Sync location is reachable. Starting sync..." >> $MainLog
Write-Output "$(timestamp) Sync location is reachable. Starting sync..." >> $SyncLog
robocopy $RobocopySource $RobocopyDestination /MIR /FFT /R:3 /W:10 /NP /NDL /UNILOG+:$SyncLog
Write-Output "$(timestamp) Sync complete. Check $SyncLog for details." >> $MainLog
Write-Output "$(timestamp) Sync complete." >> $SyncLog
exit
} else {
Write-Output "$(timestamp) Sync location is NOT reachable. Synchronization aborted..." >> $MainLog
Write-Output "$(timestamp) Exiting SYNC Task..." >> $MainLog
Write-Output "$(timestamp) Sync location NOT reachable. Synchronization aborted..." >> $SyncLog
Write-Output "$(timestamp) Exiting SYNC Task..." >> $SyncLog
exit
}
}
sync-folder
#============== END stuff to do ===================
})
$PowerShell.Runspace = $newRunspace
[void]$Jobs.Add((
[pscustomobject]#{
PowerShell = $PowerShell
Runspace = $PowerShell.BeginInvoke()
}
))
})
$syncHash.Window.Add_Closed({
Write-Verbose 'Halt runspace cleanup job processing'
$jobCleanup.Flag = $False
#Stop all runspaces
$jobCleanup.PowerShell.Dispose()
})
$syncHash.Window.ShowDialog() | Out-Null
$syncHash.Error = $Error
})
#=====================================
# Shows the form
#=====================================
$psCmd.Runspace = $newRunspace
$data = $psCmd.BeginInvoke()

Related

Powershell ISE form: How to stop Robocopy with function?

I have a Windows Form created in PowerShell ISE, used to perform robocopy with a button.
It works fine, but the interface hangs when the robocopy is in progress, which I believe to be normal behaviour; something about the tasks running in the same thread.
I want to be able to stop this robocopy process while it is running, I need some help on how to do this.
I created a button and function to do this, I just need help on what to do in the function (and elsewhere if needed).
I've seen someone do this, but it doesn't seem to work for me; it outputs "Robocopy has been terminated" like it's supposed to but continues running through the files:
function start_robocopy {
robocopy "$($InputSource.Text)" "$($InputDestination.Text)" /ndl /e /np /tee /L | ForEach-Object {
[void] [System.Windows.Forms.Application]::DoEvents() }
}
function stop_robocopy {
if (Get-Process -Name robocopy -ErrorAction SilentlyContinue) {Stop-Process -Name robocopy -Force
$outputBox.AppendText("Robocopy has been terminated.")}
My full code here; see function template created at the bottom:
Add-Type -AssemblyName System.Windows.Forms
[System.Windows.Forms.Application]::EnableVisualStyles()
$form = New-Object System.Windows.Forms.Form
$form.Size = New-Object System.Drawing.Size(850,582)
$form.add_Load($FormEvent_Load)
$outputBox = New-Object System.Windows.Forms.TextBox
$outputBox.Location = New-Object System.Drawing.Size(200,110)
$outputBox.Size = New-Object System.Drawing.Size(620,420)
$outputBox.MultiLine = $True
$outputBox.ScrollBars = "Both"
$outputBox.BackColor = "White"
$outputBox.ReadOnly = $True
$Form.Controls.Add($outputBox)
#-----FORM END-----#
#-----BUTTONS START-----#
$StartBtn = New-Object System.Windows.Forms.Button
$StartBtn.Location = New-Object System.Drawing.Size(10,80)
$StartBtn.Size = New-Object System.Drawing.Size(180,35)
$StartBtn.Text = "Start Robocopy"
$StartBtn.Add_Click({start_robocopy})
$form.Controls.Add($StartBtn)
$StopBtn = New-Object System.Windows.Forms.Button
$StopBtn.Location = New-Object System.Drawing.Size(10,150)
$StopBtn.Size = New-Object System.Drawing.Size(180,35)
$StopBtn.Text = "Stop Robocopy"
$StopBtn.Add_Click({stop_robocopy})
$form.Controls.Add($StopBtn)
#-----BUTTONS END-----#
#-----INPUTBOXES START-----#
$InputSource = New-Object System.Windows.Forms.TextBox
$InputSource.Text="C:\test\src"
$InputSource.Location = New-Object System.Drawing.Size(10,15)
$InputSource.Size = New-Object System.Drawing.Size(140,20)
$form.Controls.Add($InputSource)
$InputDestination = New-Object System.Windows.Forms.TextBox
$InputDestination.Text="C:\test\dst"
$InputDestination.Location = New-Object System.Drawing.Size(10,40)
$InputDestination.Size = New-Object System.Drawing.Size(140,20)
$form.Controls.Add($InputDestination)
#-----INPUTBOXES END-----#
#-----FUNCTIONS START-----#
function start_robocopy {
robocopy "$($InputSource.Text)" "$($InputDestination.Text)" /ndl /e /np /tee /L | ForEach-Object { $outputBox.AppendText($_ + "`r`n") }
}
function stop_robocopy {
#WHAT TO DO HERE?
}
#-----FUNCTIONS END-----#
$form.ShowDialog()
You can use PowerShell background job so that you have control over it and it will not hang the UI.
Use Start-Job and capture the jobId and then use the same jobId to stop the job in stop_robocopy.
function start_robocopy {
$source = Write-Host $InputSource.Text;
$destination = Write-Host $InputDestination.Text;
$jobId = Invoke-Command -ScriptBlock {Start-Job -ScriptBlock { robocopy "$source" "$destination" /ndl /e /np /tee /L | ForEach-Object { $outputBox.AppendText($_ + "`r`n") } }}
$global:newJobId = $jobId.Id
}
function stop_robocopy {
$jobId = Get-Job -Id $newJobId;
Invoke-Command -ScriptBlock { Stop-job -Id $jobId.Id }
Write-Host "Stopped: $($jobId.Id)"
}

change tray icon based on event

I have code that will start process in tray (task bar). After right click on tray icon it will show menu. After clicking on first menu item winform window starts. This winform shows status about the notepad process. My goal is to change the tray icon based on the status of notepad (if notepad is running then show online.ico otherwise show offline.ico). If I understand correct then my code is starting/stopping System.Windows.Forms.Timer every time winform window is opened/closed and I'm not sure if this is the best possible approach. My guess is that I need to somehow start timer "outside" of OnMenuItem1ClickEventFn so it can somehow reload *.ico files. Following script is heavily inspired by this site:
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
function Test-Notepad {
[bool](Get-Process -Name 'notepad' -ErrorAction SilentlyContinue)
}
function OnMenuItem1ClickEventFn () {
# Build Label object
$Label = New-Object System.Windows.Forms.Label
$Label.Name = "labelName"
$Label.AutoSize = $True
# Set and start timer
$timer = New-Object System.Windows.Forms.Timer
$timer.Interval = 1000
$timer.Add_Tick({
if ($Label){
$Label.Text = if (Test-Notepad) { "Notepad is running" } else { "Notepad is NOT running" }
}
})
$timer.Start()
# Build Form object
$Form = New-Object System.Windows.Forms.Form
$Form.Text = "My Form"
$Form.Size = New-Object System.Drawing.Size(200,200)
$Form.StartPosition = "CenterScreen"
$Form.Topmost = $True
$Form.Add_Closing({ $timer.Dispose() }) # Dispose() also stops the timer.
$Form.Controls.Add($Label) # Add label to form
$form.ShowDialog()| Out-Null # Show the Form
}
function OnMenuItem4ClickEventFn () {
$Main_Tool_Icon.Visible = $false
$window.Close()
Stop-Process $pid
}
function create_taskbar_menu{
# Create menu items
$Main_Tool_Icon = New-Object System.Windows.Forms.NotifyIcon
$Main_Tool_Icon.Text = "Icon Text"
$Main_Tool_Icon.Icon = $icon
$Main_Tool_Icon.Visible = $true
$MenuItem1 = New-Object System.Windows.Forms.MenuItem
$MenuItem1.Text = "Menu Item 1"
$MenuItem2 = New-Object System.Windows.Forms.MenuItem
$MenuItem2.Text = "Menu Item 2"
$MenuItem3 = New-Object System.Windows.Forms.MenuItem
$MenuItem3.Text = "Menu Item 3"
$MenuItem4 = New-Object System.Windows.Forms.MenuItem
$MenuItem4.Text = "Exit"
# Add menu items to context menu
$contextmenu = New-Object System.Windows.Forms.ContextMenu
$Main_Tool_Icon.ContextMenu = $contextmenu
$Main_Tool_Icon.contextMenu.MenuItems.AddRange($MenuItem1)
$Main_Tool_Icon.contextMenu.MenuItems.AddRange($MenuItem2)
$Main_Tool_Icon.contextMenu.MenuItems.AddRange($MenuItem3)
$Main_Tool_Icon.contextMenu.MenuItems.AddRange($MenuItem4)
$MenuItem4.add_Click({OnMenuItem4ClickEventFn})
$MenuItem1.add_Click({OnMenuItem1ClickEventFn})
}
$Current_Folder = split-path $MyInvocation.MyCommand.Path
# Add assemblies for WPF and Mahapps
[System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms') | out-null
[System.Reflection.Assembly]::LoadWithPartialName('presentationframework') | out-null
[System.Reflection.Assembly]::LoadWithPartialName('System.Drawing') | out-null
[System.Reflection.Assembly]::LoadWithPartialName('WindowsFormsIntegration') | out-null
# [System.Reflection.Assembly]::LoadFrom("Current_Folder\assembly\MahApps.Metro.dll") | out-null
# Choose an icon to display in the systray
$icon = [System.Drawing.Icon]::ExtractAssociatedIcon("$Current_Folder/icons/online.ico")
# use this icon when notepad is not running
# $icon = [System.Drawing.Icon]::ExtractAssociatedIcon("$Current_Folder/icons/offline.ico")
create_taskbar_menu
# Make PowerShell Disappear - Thanks Chrissy
$windowcode = '[DllImport("user32.dll")] public static extern bool ShowWindowAsync(IntPtr hWnd, int nCmdShow);'
$asyncwindow = Add-Type -MemberDefinition $windowcode -name Win32ShowWindowAsync -namespace Win32Functions -PassThru
$null = $asyncwindow::ShowWindowAsync((Get-Process -PID $pid).MainWindowHandle, 0)
# Use a Garbage colection to reduce Memory RAM
# https://dmitrysotnikov.wordpress.com/2012/02/24/freeing-up-memory-in-powershell-using-garbage-collector/
# https://learn.microsoft.com/fr-fr/dotnet/api/system.gc.collect?view=netframework-4.7.2
[System.GC]::Collect()
# Create an application context for it to all run within - Thanks Chrissy
# This helps with responsiveness, especially when clicking Exit - Thanks Chrissy
$appContext = New-Object System.Windows.Forms.ApplicationContext
[void][System.Windows.Forms.Application]::Run($appContext)
EDITED: working solution based on #BACON answer
# Toggle following two lines
Set-StrictMode -Version Latest
# Set-StrictMode -Off
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
function Test-Notepad {
[bool](Get-Process -Name 'notepad' -ErrorAction SilentlyContinue)
}
function OnMenuItem1ClickEventFn () {
# Build Form object
$Form = New-Object System.Windows.Forms.Form
$Form.Text = "My Form"
$Form.Size = New-Object System.Drawing.Size(200,200)
$Form.StartPosition = "CenterScreen"
$Form.Topmost = $True
$Form.Controls.Add($Label) # Add label to form
$form.ShowDialog()| Out-Null # Show the Form
}
function OnMenuItem4ClickEventFn () {
$Main_Tool_Icon.Visible = $false
[System.Windows.Forms.Application]::Exit()
}
function create_taskbar_menu{
# Create menu items
$MenuItem1 = New-Object System.Windows.Forms.MenuItem
$MenuItem1.Text = "Menu Item 1"
$MenuItem2 = New-Object System.Windows.Forms.MenuItem
$MenuItem2.Text = "Menu Item 2"
$MenuItem3 = New-Object System.Windows.Forms.MenuItem
$MenuItem3.Text = "Menu Item 3"
$MenuItem4 = New-Object System.Windows.Forms.MenuItem
$MenuItem4.Text = "Exit"
# Add menu items to context menu
$contextmenu = New-Object System.Windows.Forms.ContextMenu
$Main_Tool_Icon.ContextMenu = $contextmenu
$Main_Tool_Icon.contextMenu.MenuItems.AddRange($MenuItem1)
$Main_Tool_Icon.contextMenu.MenuItems.AddRange($MenuItem2)
$Main_Tool_Icon.contextMenu.MenuItems.AddRange($MenuItem3)
$Main_Tool_Icon.contextMenu.MenuItems.AddRange($MenuItem4)
$MenuItem4.add_Click({OnMenuItem4ClickEventFn})
$MenuItem1.add_Click({OnMenuItem1ClickEventFn})
}
$Current_Folder = split-path $MyInvocation.MyCommand.Path
# Add assemblies for WPF and Mahapps
[System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms') | out-null
[System.Reflection.Assembly]::LoadWithPartialName('presentationframework') | out-null
[System.Reflection.Assembly]::LoadWithPartialName('System.Drawing') | out-null
[System.Reflection.Assembly]::LoadWithPartialName('WindowsFormsIntegration') | out-null
# [System.Reflection.Assembly]::LoadFrom("Current_Folder\assembly\MahApps.Metro.dll") | out-null
# Choose an icon to display in the systray
$onlineIcon = [System.Drawing.Icon]::ExtractAssociatedIcon("$Current_Folder/icons/online.ico")
# use this icon when notepad is not running
$offlineIcon = [System.Drawing.Icon]::ExtractAssociatedIcon("$Current_Folder/icons/offline.ico")
$Main_Tool_Icon = New-Object System.Windows.Forms.NotifyIcon
$Main_Tool_Icon.Text = "Icon Text"
$Main_Tool_Icon.Icon = if (Test-Notepad) { $onlineIcon } else { $offlineIcon }
$Main_Tool_Icon.Visible = $true
# Build Label object
$Label = New-Object System.Windows.Forms.Label
$Label.Name = "labelName"
$Label.AutoSize = $True
# Initialize the timer
$timer = New-Object System.Windows.Forms.Timer
$timer.Interval = 1000
$timer.Add_Tick({
if ($Label){
$Label.Text, $Main_Tool_Icon.Icon = if (Test-Notepad) {
"Notepad is running", $onlineIcon
} else {
"Notepad is NOT running", $offlineIcon
}
}
})
$timer.Start()
create_taskbar_menu
# Make PowerShell Disappear - Thanks Chrissy
$windowcode = '[DllImport("user32.dll")] public static extern bool ShowWindowAsync(IntPtr hWnd, int nCmdShow);'
$asyncwindow = Add-Type -MemberDefinition $windowcode -name Win32ShowWindowAsync -namespace Win32Functions -PassThru
$null = $asyncwindow::ShowWindowAsync((Get-Process -PID $pid).MainWindowHandle, 0)
# Use a Garbage colection to reduce Memory RAM
# https://dmitrysotnikov.wordpress.com/2012/02/24/freeing-up-memory-in-powershell-using-garbage-collector/
# https://learn.microsoft.com/fr-fr/dotnet/api/system.gc.collect?view=netframework-4.7.2
[System.GC]::Collect()
# Create an application context for it to all run within - Thanks Chrissy
# This helps with responsiveness, especially when clicking Exit - Thanks Chrissy
$appContext = New-Object System.Windows.Forms.ApplicationContext
try
{
[System.Windows.Forms.Application]::Run($appContext)
}
finally
{
foreach ($component in $timer, $Main_Tool_Icon, $offlineIcon, $onlineIcon, $appContext)
{
# The following test returns $false if $component is
# $null, which is really what we're concerned about
if ($component -is [System.IDisposable])
{
$component.Dispose()
}
}
Stop-Process -Id $PID
}
You already have code to set the icon used by the NotifyIcon...
$Main_Tool_Icon.Icon = $icon
...and to define the icons to use...
# Choose an icon to display in the systray
$icon = [System.Drawing.Icon]::ExtractAssociatedIcon("$Current_Folder/icons/online.ico")
# use this icon when notepad is not running
# $icon = [System.Drawing.Icon]::ExtractAssociatedIcon("$Current_Folder/icons/offline.ico")
...and to periodically test if Notepad is running and respond appropriately...
$timer.Add_Tick({
if ($Label){
$Label.Text = if (Test-Notepad) { "Notepad is running" } else { "Notepad is NOT running" }
}
})
You just need to combine them with a couple additional tweaks...
Store each icon in its own variable with a descriptive name so its easy to switch between them.
$Main_Tool_Icon needs to be defined outside the scope of create_taskbar_menu so it can be accessed inside of OnMenuItem1ClickEventFn. I moved the creation and initialization to just before the call to create_taskbar_menu, but you could also initialize it inside of create_taskbar_menu or some other function.
That ends up looking like this...
# Choose an icon to display in the systray
$onlineIcon = [System.Drawing.Icon]::ExtractAssociatedIcon("$Current_Folder/icons/online.ico")
# use this icon when notepad is not running
$offlineIcon = [System.Drawing.Icon]::ExtractAssociatedIcon("$Current_Folder/icons/offline.ico")
$Main_Tool_Icon = New-Object System.Windows.Forms.NotifyIcon
$Main_Tool_Icon.Text = "Icon Text"
$Main_Tool_Icon.Icon = if (Test-Notepad) { $onlineIcon } else { $offlineIcon }
$Main_Tool_Icon.Visible = $true
create_taskbar_menu
...and this...
$timer.Add_Tick({
if ($Label){
# Change the text and icon with one test
$Label.Text, $Main_Tool_Icon.Icon = if (Test-Notepad) {
"Notepad is running", $onlineIcon
} else {
"Notepad is NOT running", $offlineIcon
}
}
})
You'll see that an icon is selected based on the result of Test-Notepad both when $Main_Tool_Icon is initialized and when the Tick event is raised.
As for disposing $timer when $Form is closing...
$Form.Add_Closing({ $timer.Dispose() })
...that is almost an appropriate place to do that, however...
The Closing event is raised essentially when the form has been requested to close; it provides an opportunity for that request to be canceled. The Closed event would be more appropriate because it is raised when the form has actually been closed.
The documentation states that both the Closing and Closed events are obsolete. Use the FormClosed event instead.
Also, in OnMenuItem4ClickEventFn you are calling $window.Close() even though $window is not defined; I think you meant $Form.Close(). An alternative that I think is a bit cleaner would be to have clicking the Exit menu item merely exit the Windows Forms application loop...
function OnMenuItem4ClickEventFn () {
$Main_Tool_Icon.Visible = $false
[System.Windows.Forms.Application]::Exit()
}
...and then you can put your cleanup/teardown code in a finally block at the end of the script...
try
{
# This call returns when [System.Windows.Forms.Application]::Exit() is called
[System.Windows.Forms.Application]::Run($appContext)
}
finally
{
# $timer would also have to be defined at the script scope
# (outside of OnMenuItem1ClickEventFn) for this to work
$timer.Dispose()
# $Form, $Label, $Main_Tool_Icon, $onlineIcon, etc. would all be candidates for disposal...
# Exit the entire PowerShell process
Stop-Process $pid
}
The following is complete code that incorporates the changes mentioned above and works for me...
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
function Test-Notepad {
[bool](Get-Process -Name 'notepad' -ErrorAction SilentlyContinue)
}
function OnMenuItem1ClickEventFn () {
$timer.Start()
# Build Form object
$Form = New-Object System.Windows.Forms.Form
$Form.Text = "My Form"
$Form.Size = New-Object System.Drawing.Size(200,200)
$Form.StartPosition = "CenterScreen"
$Form.Topmost = $True
$Form.Add_Closing({ $timer.Dispose() }) # Dispose() also stops the timer.
$Form.Controls.Add($Label) # Add label to form
$form.ShowDialog()| Out-Null # Show the Form
}
function OnMenuItem4ClickEventFn () {
$Main_Tool_Icon.Visible = $false
[System.Windows.Forms.Application]::Exit()
}
function create_taskbar_menu{
# Create menu items
$MenuItem1 = New-Object System.Windows.Forms.MenuItem
$MenuItem1.Text = "Menu Item 1"
$MenuItem2 = New-Object System.Windows.Forms.MenuItem
$MenuItem2.Text = "Menu Item 2"
$MenuItem3 = New-Object System.Windows.Forms.MenuItem
$MenuItem3.Text = "Menu Item 3"
$MenuItem4 = New-Object System.Windows.Forms.MenuItem
$MenuItem4.Text = "Exit"
# Add menu items to context menu
$contextmenu = New-Object System.Windows.Forms.ContextMenu
$Main_Tool_Icon.ContextMenu = $contextmenu
$Main_Tool_Icon.contextMenu.MenuItems.AddRange($MenuItem1)
$Main_Tool_Icon.contextMenu.MenuItems.AddRange($MenuItem2)
$Main_Tool_Icon.contextMenu.MenuItems.AddRange($MenuItem3)
$Main_Tool_Icon.contextMenu.MenuItems.AddRange($MenuItem4)
$MenuItem4.add_Click({OnMenuItem4ClickEventFn})
$MenuItem1.add_Click({OnMenuItem1ClickEventFn})
}
$Current_Folder = split-path $MyInvocation.MyCommand.Path
# Add assemblies for WPF and Mahapps
[System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms') | out-null
[System.Reflection.Assembly]::LoadWithPartialName('presentationframework') | out-null
[System.Reflection.Assembly]::LoadWithPartialName('System.Drawing') | out-null
[System.Reflection.Assembly]::LoadWithPartialName('WindowsFormsIntegration') | out-null
# [System.Reflection.Assembly]::LoadFrom("Current_Folder\assembly\MahApps.Metro.dll") | out-null
# Choose an icon to display in the systray
$onlineIcon = [System.Drawing.Icon]::ExtractAssociatedIcon("$Current_Folder/icons/online.ico")
# use this icon when notepad is not running
$offlineIcon = [System.Drawing.Icon]::ExtractAssociatedIcon("$Current_Folder/icons/offline.ico")
$Main_Tool_Icon = New-Object System.Windows.Forms.NotifyIcon
$Main_Tool_Icon.Text = "Icon Text"
$Main_Tool_Icon.Icon = if (Test-Notepad) { $onlineIcon } else { $offlineIcon }
$Main_Tool_Icon.Visible = $true
# Build Label object
$Label = New-Object System.Windows.Forms.Label
$Label.Name = "labelName"
$Label.AutoSize = $True
# Initialize the timer
$timer = New-Object System.Windows.Forms.Timer
$timer.Interval = 1000
$timer.Add_Tick({
if ($Label){
$Label.Text, $Main_Tool_Icon.Icon = if (Test-Notepad) {
"Notepad is running", $onlineIcon
} else {
"Notepad is NOT running", $offlineIcon
}
}
})
create_taskbar_menu
# Make PowerShell Disappear - Thanks Chrissy
$windowcode = '[DllImport("user32.dll")] public static extern bool ShowWindowAsync(IntPtr hWnd, int nCmdShow);'
$asyncwindow = Add-Type -MemberDefinition $windowcode -name Win32ShowWindowAsync -namespace Win32Functions -PassThru
$null = $asyncwindow::ShowWindowAsync((Get-Process -PID $pid).MainWindowHandle, 0)
# Use a Garbage colection to reduce Memory RAM
# https://dmitrysotnikov.wordpress.com/2012/02/24/freeing-up-memory-in-powershell-using-garbage-collector/
# https://learn.microsoft.com/fr-fr/dotnet/api/system.gc.collect?view=netframework-4.7.2
[System.GC]::Collect()
# Create an application context for it to all run within - Thanks Chrissy
# This helps with responsiveness, especially when clicking Exit - Thanks Chrissy
$appContext = New-Object System.Windows.Forms.ApplicationContext
try
{
[System.Windows.Forms.Application]::Run($appContext)
}
finally
{
foreach ($component in $timer, $form, $Main_Tool_Icon, $offlineIcon, $onlineIcon, $appContext)
{
# The following test returns $false if $component is
# $null, which is really what we're concerned about
if ($component -is [System.IDisposable])
{
$component.Dispose()
}
}
Stop-Process -Id $PID
}

What's wrong with my powershell script? :(

I'm trying to make an automated script that automatically directs to a
secure website in IE, enters login credentials, and goes to a database tab
and a database link to download. This will require no user interaction and
be initiated through windows task scheduler.
However, it works about 75% of the time. I am new to
powershell and made novice mistakes, so any help or direction would
greatly be appreciated. Thank you!
PowerShell.exe -windowstyle hidden {
function Get-TimeStamp {
return "[{0:MM/dd/yy} {0:HH:mm:ss}]" -f (Get-Date)
}
Write-Output "$(Get-TimeStamp) Script Executed $dc" | Out-file C:\Users
\JohnSmith\Desktop\Script\ScriptLog.txt -append
$username = "admin"
$password = "admin123"
$ie = new-object -com InternetExplorer.Application
#Navigate to the login page
$ie.navigate("Login Page")
#Wait for the page to finish loading
do {sleep 1} until (-not ($ie.Busy))
$ie.visible = $true #comment this line after debugging
#Assigning DOM to $doc variable
$doc = $ie.document
try {
$usernameField = $doc.getElementById('userName')
#write-host $usernameField
$usernameField.value = $username
write-host $username
$passwordField = $doc.getElementById('password')
$passwordField.value = $password
write-host $pass
#Find and click the submit button
$submitButton = $doc.getElementById('login')
write-host $submitButton
$submitButton.click()
#Wait until login is complete
do {sleep 1} until (-not ($ie.Busy))
} catch {$null}
do {sleep 1} until (-not ($ie.Busy))
$wshell = New-Object -ComObject wscript.shell;
$wshell.AppActivate('title of the application window')
Sleep 1
Add-Type -AssemblyName System.Windows.Forms
[System.Windows.Forms.SendKeys]::SendWait('~');
$ie = new-object -com internetexplorer.application
$ie.visible=$true
$ie.navigate('Database Download' )
while($ie.busy) {sleep 1}
$link = $ie.Document.getElementsByTagName('A') | where-object
{$_.innerText -eq 'Download the Complete Database'}
$link.click()
$wshell = New-Object -ComObject wscript.shell;
$wshell.AppActivate('Internet Explorer')
Sleep 1
Add-Type -AssemblyName System.Windows.Forms
[System.Windows.Forms.SendKeys]::SendWait("{TAB}");
$wshell = New-Object -ComObject wscript.shell;
$wshell.AppActivate('Internet Explorer')
Sleep 1
Add-Type -AssemblyName System.Windows.Forms
[System.Windows.Forms.SendKeys]::SendWait('~');
(New-Object -COM 'Shell.Application').Windows() | Where-Object {
$_.Name -like '*Internet Explorer*'
} | ForEach-Object
{
$_.Quit()
[Runtime.Interopservices.Marshal]::ReleaseComObject($_)
}
[GC]::Collect()
[GC]::WaitForPendingFinalizers()
}
Here was my finished script. It's not as elegant as I would like it to be, but it does what I need it to do, for now! Thank you!
PowerShell.exe -windowstyle hidden {
function Get-TimeStamp {
return "[{0:MM/dd/yy} {0:HH:mm:ss}]" -f (Get-Date)
}
#Creates appending log file for everytime script runs
Write-Output "$(Get-TimeStamp) Script Executed $dc" | Out-file C:\Users
\JohnSmith\Desktop\DatabaseScript\DatabaseScriptLog.txt -append
$username = "admin"
$password = "admin123"
#write-host $pass
#Create the IE com object
$ie = new-object -com InternetExplorer.Application
#Navigate to the login page
Start-Process 'https://www.LoginPageToTheSecureSite'
<#$ie.navigate("https://www.LoginPageToTheSecureSite")
#Wait for the page to finish loading
do {sleep 1} until (-not ($ie.Busy))
$ie.visible = $true
#Assign the DOM to the $doc variable
$doc = $ie.document
try {
#Find the username field and set the value to that of variable
$usernameField = $doc.getElementById('User ID')
#write-host $usernameField
$usernameField.value = $username
write-host $username
#Find the password field and set the value to that of the result
#of a call to the get-password function with the parameter defined at
top
$passwordField = $doc.getElementById('Password')
$passwordField.value = $password
write-host $pass
#Submit button
$submitButton = $doc.getElementById('Login')
write-host $submitButton
$submitButton.click()
#Wait until login is complete
do {sleep 1} until (-not ($ie.Busy))
} catch {$null}
#Wait for page to finish loading
do {sleep 1} until (-not ($ie.Busy))
$wshell = New-Object -ComObject wscript.shell;
$wshell.AppActivate('title of the application window')
Sleep 1
Add-Type -AssemblyName System.Windows.Forms
[System.Windows.Forms.SendKeys]::SendWait('~');
$wshell.AppActivate('Internet Explorer')
Sleep 1
Add-Type -AssemblyName System.Windows.Forms
Start-Process 'https://www.DownloadTheDatabaseSelectionTab'
$wshell.AppActivate('Internet Explorer')
Sleep 1
Add-Type -AssemblyName System.Windows.Forms
Start-Process 'https://www.DownloadThisAsZipFile.zip'
$wshell = New-Object -ComObject wscript.shell;
$wshell.AppActivate('Opening Database Zip File')
Sleep 1
Add-Type -AssemblyName System.Windows.Forms
[System.Windows.Forms.SendKeys]::SendWait('~');
$wshell = New-Object -ComObject wscript.shell;
$wshell.AppActivate('Internet Explorer')
Sleep 1
Add-Type -AssemblyName System.Windows.Forms
[System.Windows.Forms.SendKeys]::SendWait("{TAB 2}");
#Closes every instance of IE
(New-Object -COM 'Shell.Application').Windows() | Where-Object {
$_.Name -like '*Internet Explorer*'
} | ForEach-Object {
$_.Quit()
}
#Releases COM Object, cleans up.
(New-Object -COM 'Shell.Application').Windows() | Where-Object {
$_.Name -like 'Internet Explorer'
} | ForEach-Object {
$_.Quit()
[Runtime.Interopservices.Marshal]::ReleaseComObject($_)
}
[GC]::Collect()
[GC]::WaitForPendingFinalizers()
}

Powershell $button.add_click executes the whole code inside the add_click block

I am new to WPF and event-handling using powershell, what I want to achieve is on click of a button, the progress bar should be shown from 0-100. But when I am running the following piece of code, it computes the whole code inside the add_click block and it only shows the last iteration in the loop.
I know maybe it is a silly solution, but I do need some help in this.
$syncHash = [hashtable]::Synchronized(#{})
$newRunspace =[runspacefactory]::CreateRunspace()
$newRunspace.ApartmentState = "STA"
$newRunspace.ThreadOptions = "ReuseThread"
$newRunspace.Open()
$newRunspace.SessionStateProxy.SetVariable("syncHash",$syncHash)
$psCmd = [PowerShell]::Create().AddScript({
$XamlPath="C:\Forms\MyFormsv1.xaml"
$inputXML = Get-Content -Path $XamlPath
[xml]$Global:xmlWPF = $inputXML -replace 'mc:Ignorable="d"','' -replace "x:N",'N' -replace '^<Win.*', '<Window'
#Add WPF and Windows Forms assemblies
try{
Add-Type -AssemblyName PresentationCore,PresentationFramework,WindowsBase,system.windows.forms
} catch {
Throw “Failed to load Windows Presentation Framework assemblies.”
}
$syncHash.Window=[Windows.Markup.XamlReader]::Load((new-object System.Xml.XmlNodeReader $xmlWPF))
[xml]$XAML = $xmlWPF
$xmlWPF.SelectNodes("//*[#*[contains(translate(name(.),'n','N'),'Name')]]") | %{
$Global:synchash.Add($_.Name,$synchash.Window.FindName($_.Name) )}
function tool1_progress{
$var1=50
for($index=1 ; $index -le $var1; $index++)
{
[int]$tmpProgNum= ($index/$var1) * 100
$syncHash.tool1_pb.Value= $tmpProgNum
$syncHash.consoleOutput.Text=$index
Start-Sleep -Milliseconds 250
}
}
$syncHash.myButton.add_click({tool1_progress})
$syncHash.Window.ShowDialog() | out-null
})
$psCmd.Runspace = $newRunspace
$data = $psCmd.BeginInvoke()
$tool1 is a label. $tool1_pb is a progress bar. $consoleOutput is a
text box.
I integrate WPF and PowerShell all the time. This website helped me immensely. You can't update a UI that's running on the same thread, so you need to invoke a new runspace using BeginInvoke()
$psCmd = [PowerShell]::Create().AddScript({
$Global:uiHash.Error = $Error
Add-Type -AssemblyName PresentationFramework,PresentationCore,WindowsBase
$xaml = #"YOURXAMLHERE"
$Global:uiHash.Window=[Windows.Markup.XamlReader]::Parse($xaml )
[xml]$XAML = $xaml
$xaml.SelectNodes("//*[#*[contains(translate(name(.),'n','N'),'Name')]]") | %{
$Global:uihash.Add($_.Name,$uihash.Window.FindName($_.Name) )}
$Global:uiHash.Window.ShowDialog() | out-null
})
$psCmd.Runspace = $newRunspace
$handle = $psCmd.BeginInvoke()
Then to update whatever it is you need, you would use Window.Dispatcher.Invoke
$Global:uiHash.Window.Dispatcher.Invoke([action]{$Global:uiHash.Window.Title = "MyWindowTitle"},"Normal")
EDIT
$Global:uiHash.Button1.Add_Click(tool1_progress)
EDIT
Throw in your XML and this will work. You have to create another runspace on top of the other in order to keep updating the textbox and progress bar. Note that synchash MUST be $Global:synchash. Functions courtesy of this post.
$Global:syncHash = [hashtable]::Synchronized(#{})
$newRunspace =[runspacefactory]::CreateRunspace()
$newRunspace.ApartmentState = "STA"
$newRunspace.ThreadOptions = "ReuseThread"
$newRunspace.Open()
$newRunspace.SessionStateProxy.SetVariable("syncHash",$syncHash)
$psCmd = [PowerShell]::Create().AddScript({
$XamlPath="C:\Forms\MyFormsv1.xaml"
$inputXML = #"YOURXMLHERE"#
[xml]$Global:xmlWPF = $inputXML
#Add WPF and Windows Forms assemblies
try{
Add-Type -AssemblyName PresentationCore,PresentationFramework,WindowsBase,system.windows.forms
} catch {
Throw “Failed to load Windows Presentation Framework assemblies.”
}
$Global:syncHash.Window=[Windows.Markup.XamlReader]::Load((new-object System.Xml.XmlNodeReader $xmlWPF))
[xml]$XAML = $xmlWPF
$xmlWPF.SelectNodes("//*[#*[contains(translate(name(.),'n','N'),'Name')]]") | %{
$Global:synchash.Add($_.Name,$Global:syncHash.Window.FindName($_.Name) )}
function Start-Runspace{
param($scriptblock)
$newRunspace =[runspacefactory]::CreateRunspace()
$newRunspace.ApartmentState = "STA"
$newRunspace.ThreadOptions = "ReuseThread"
$newRunspace.Open()
$newRunspace.SessionStateProxy.SetVariable("SyncHash",$global:synchash)
$psCmd = [PowerShell]::Create().AddScript($ScriptBlock)
$psCmd.Runspace = $newRunspace
$psCMD.BeginInvoke()
}
$SB = {
$var1=50
for($index=1 ; $index -le $var1; $index++)
{
[int]$tmpProgNum= ($index/$var1) * 100
$Global:syncHash.Window.Dispatcher.Invoke([action]{$Global:Synchash.tool1_pb.value = "$tmpprognum"},"Normal")
$Global:syncHash.Window.Dispatcher.Invoke([action]{$Global:Synchash.consoleoutput.text = "$index"},"Normal")
Start-Sleep -Milliseconds 250
}
}
$Global:syncHash.myButton.add_click({Start-Runspace $SB})
$Global:syncHash.Window.ShowDialog() | out-null
})
$psCmd.Runspace = $newRunspace
$data = $psCmd.BeginInvoke()
start-sleep -Milliseconds 300

Multiple functions in WPF GUI from runspace(s)

I have a little WPF Powershell GUI with a timer:
##############################################
[void] [System.Reflection.Assembly]::LoadWithPartialName("System.Drawing")
[void] [System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
$sync = [hashtable]::Synchronized(#{})
##############################################
##############################################Start form
$Form = New-Object System.Windows.Forms.Form
$Form.Size = New-Object System.Drawing.Size(600,400)
$Form.Text = "Testor"
$Form.MaximizeBox = $false
############################################## Start functions
Function Netchk {
$wlanchk1 = netsh wlan show interfaces | Select-String '\sSSID'
if ($wlanchk1 -ne $null){$wlanchk = $wlanchk1 -replace ('\s','')
$wlanchk -replace 'SSID:','Connected to: '}else{$wlanchk = "No wlan connected"}
$outputBox.Text = $wlanchk
}
############################################## end functions
############################################## Start group box
$groupBox = New-Object System.Windows.Forms.GroupBox
$groupBox.Location = New-Object System.Drawing.Size(10,10)
$groupBox.Autosize = $true
$groupBox.text = "Groupbox: "
$Form.Controls.Add($groupBox)
############################################## end group box
############################################## Start buttons
$Button1 = New-Object System.Windows.Forms.Button
$Button1.Location = new-object System.Drawing.Point(15,25)
$Button1.Size = New-Object System.Drawing.Size(200,30)
$Button1.Text = "Button1"
$groupBox.Controls.Add($Button1)
$Button1.Add_click({netchk})
$Button2 = New-Object System.Windows.Forms.Button
$Button2.Location = new-object System.Drawing.Point(15,55)
$Button2.Size = New-Object System.Drawing.Size(200,30)
$Button2.Text = "Button2"
$groupBox.Controls.Add($Button2)
$Button2.Add_click({})
$Button3 = New-Object System.Windows.Forms.Button
$Button3.Location = new-object System.Drawing.Point(15,85)
$Button3.Size = New-Object System.Drawing.Size(200,30)
$Button3.Text = "Button3"
$groupBox.Controls.Add($Button3)
$Button3.Add_click({})
$Button4 = New-Object System.Windows.Forms.Button
$Button4.Location = new-object System.Drawing.Point(15,115)
$Button4.Size = New-Object System.Drawing.Size(200,30)
$Button4.Text = "Button4"
$groupBox.Controls.Add($Button4)
$Button4.Add_click({})
############################################## end buttons
############################################## Start text field
$outputBox = New-Object System.Windows.Forms.TextBox
$outputBox.Location = New-Object System.Drawing.Size(10,200)
$outputBox.Size = New-Object System.Drawing.Size(565,150)
$outputBox.MultiLine = $True
$outputBox.ScrollBars = "Vertical"
$outputBox.Text = 0
$Form.Controls.Add($outputBox)
$Form.Controls.AddRange(#($sync.Textbox))
############################################## end text field
############################################## start label
$InitialFormWindowState = New-Object System.Windows.Forms.FormWindowState
$label1 = New-Object System.Windows.Forms.Label
$label1.Location = New-Object Drawing.Point (385, 30)
$label1.Width = 100
$label1.Height = 60
$label1.Text = 0
$label1.Font = New-Object System.Drawing.Font("Courier New",32,1,2,0)
############################################## end label
############################################## start timer
$timer1 = New-Object System.Windows.Forms.Timer
$timer1.Interval = 1000
$timer1.Enabled = $true
$time = 60
$script:StartTime = (Get-Date).AddSeconds($Time)
$timer1_OnTick = {
[TimeSpan]$span = $script:StartTime - (Get-Date)
$label1.Text = '{0:N0}' -f $span.TotalSeconds
if($span.TotalSeconds -le 0)
{
$timer1.Stop()
$timer1.enabled = $false
function1
$Form.Close()
stop-process -Id $PID
}
}
$timer1.add_tick($timer1_OnTick)
$Form.Controls.AddRange(#($sync.Timer))
##############################################
$sync.button1 = $button1
$sync.button2 = $button2
$sync.button3 = $button3
$sync.button4 = $button4
$sync.label1 = $label1
$sync.TextBox = $outputBox
$sync.Groupbox = $groupBox
$sync.Timer = $timer1
$Form.Controls.AddRange(#($sync.button1, $sync.button2, $sync.button3, $sync.button4, $sync.label1, $sync.TextBox, $sync.Groupbox ))
$Form.Add_Shown({$Form.Activate()})
[void] $Form.ShowDialog()
Because I'm new to powershell, I still cannot understand the runspaces launch method etc. How can I run the timer from its own runspace and how can I add functions to buttons? I need something {click} --> {open new runspace} --> {run function} while timer is still ticking in isolation. The netchk function is a simple task, what i want GUI to do.
I want to understand this =) Please, explain it to me.
Ok so your part of the way there, having created a synchronized hash table and set up a gui(although you should mark your hash table as a global variable like so
$GLOBAL:sync, all you really need to do is go ahead and create the runspace and then invoke it, which I'll show below.
First you'll need to wrap whatever code needs to be run in a separate runspace inside of a script block
$SB = {YOUR CODE HERE}
Next you need to create a new runspace
$newRunspace =[runspacefactory]::CreateRunspace()
Then You'll want to set some options, the apartment state is required for WPF applications(not sure about the WinForms you are using) and the Thread Options are recommended for performance.
$newRunspace.ApartmentState = "STA"
$newRunspace.ThreadOptions = "ReuseThread"
Now the runspace must be opened
$newRunspace.Open()
Now you can add your syncronized hash table to the runspace, this command will make it accessible via the variable $syncHash inside your code.
$newRunspace.SessionStateProxy.SetVariable("SyncHash",$sync)
After the runspace is created you'll also need to create a new powershell object and add your script. You can also add arguments here if you want but I find it easier to save whatever data is needed inside the synced hash table and using it from there.
$psCmd = [PowerShell]::Create().AddScript($SB)
Then you need to associate the runspace with the powershell object
$psCmd.Runspace = $newRunspace
and finally begin invoking the newly created object
$psCMD.BeginInvoke()
All together it looks like this
$newRunspace =[runspacefactory]::CreateRunspace()
$newRunspace.ApartmentState = "STA"
$newRunspace.ThreadOptions = "ReuseThread"
$newRunspace.Open()
$newRunspace.SessionStateProxy.SetVariable("SyncHash",$HashTable)
$psCmd = [PowerShell]::Create().AddScript($ScriptBlock)
$psCmd.Runspace = $newRunspace
$psCMD.BeginInvoke()
So in order to have the runspaces interact we need to use a global synced hash table, so set your existing $sync like this
$GLOBAL:sync = [hashtable]::Synchronized(#{})
Once you have that you'll need to add the code you currently have in your netchk function to a script block, which will look like so
$SB = {
$wlanchk1 = netsh wlan show interfaces | Select-String '\sSSID'
if ($wlanchk1 -ne $null){$wlanchk = $wlanchk1 -replace ('\s','')
$wlanchk -replace 'SSID:','Connected to: '}else{$wlanchk = "No wlan connected"}
$GLOBAL:sync.txtbox.Dispatcher.Invoke([action]{$GLOBAL:outputBox.Text = $wlanchk}, "Normal")
}
Don't worry about the dispatcher stuff right now, we'll get to that in a second. After you have your scriptblock created you'll need to start your runspace when the button is clicked, to do that I've created a Start-Runspace function for you that encapsulates the code above.
function Start-Runspace{
param($scriptblock)
$newRunspace =[runspacefactory]::CreateRunspace()
$newRunspace.ApartmentState = "STA"
$newRunspace.ThreadOptions = "ReuseThread"
$newRunspace.Open()
$newRunspace.SessionStateProxy.SetVariable("SyncHash",$global:sync)
$psCmd = [PowerShell]::Create().AddScript($ScriptBlock)
$psCmd.Runspace = $newRunspace
$psCMD.BeginInvoke()
}
You just need to pass it your scriptblock as a parameter and it will create and run your async runspace for you. You call it as an event like below
$Button1.Add_click({Start-Runspace $SB})
Ok now back to that weird dispatcher stuff. When you want to make changes to things that are attached to different threads you need to use the synced hash table so you can get access to the variables, however you also need to use the dispatcher since the thread you are working in does not own the dialog, since you've already added the control to your hash table you just need to call it's dispatcher and let it know what action to perform, which you do in the scriptblock like I showed above.
$GLOBAL:sync.txtbox.Dispatcher.Invoke([action]{$GLOBAL:outputBox.Text = $wlanchk}, "Normal")
From there is just a matter of repeating for each of your buttons and actions. Hope this helps you get going
Edit
Just want to add that if you are interested in doing things a little more simply using XAML instead of defining your GUI using the drawing api's you should check out a small blog post I did Here, there's also some additional runspace stuff on that site but it's geared more towards multi-tasking than GUI usage.
EDIT
$Global:uiHash = [hashtable]::Synchronized(#{})
$newRunspace =[runspacefactory]::CreateRunspace()
$newRunspace.ApartmentState = "STA"
$newRunspace.ThreadOptions = "ReuseThread"
$newRunspace.Open()
$newRunspace.SessionStateProxy.SetVariable("uiHash",$Global:uiHash)
$psCmd = [PowerShell]::Create().AddScript({
$Global:uiHash.Error = $Error
Add-Type -AssemblyName PresentationFramework,PresentationCore,WindowsBase
[xml]$xaml = #"
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
ResizeMode="NoResize"
Title="Zapuskator" Height="350" Width="500">
<Grid>
<Button Name="button" Content="Button" HorizontalAlignment="Left" Margin="21,21,0,0" VerticalAlignment="Top" Width="200"/>
<Button Name="button1" Content="Button1" HorizontalAlignment="Left" Margin="21,48,0,0" VerticalAlignment="Top" Width="200"/>
<Button Name="button2" Content="Button2" HorizontalAlignment="Left" Margin="21,75,0,0" VerticalAlignment="Top" Width="200"/>
<Button Name="button3" Content="Button3" HorizontalAlignment="Left" Margin="21,102,0,0" VerticalAlignment="Top" Width="200"/>
<TextBox Name="textBox" HorizontalAlignment="Right" Height="151" Margin="0,159,60,0" TextWrapping="Wrap" Text="" VerticalAlignment="Top" Width="422"/>
<GroupBox Name="groupBox" Header="Choose your session: " Margin="0,0,260,0" VerticalAlignment="Top" Height="138" HorizontalAlignment="Right" Width="222"/>
<Label Name="label" Content="60" HorizontalAlignment="Left" Margin="350,60,0,0" VerticalAlignment="Top" RenderTransformOrigin="-0.263,0.116" Height="60" Width="60"/>
</Grid>
</Window>
"#
$reader=(New-Object System.Xml.XmlNodeReader $xaml)
$Global:uiHash.Window=[Windows.Markup.XamlReader]::Load( $reader )
$Global:uiHash.Button = $Global:uiHash.window.FindName("Button")
$Global:uiHash.Button1 = $Global:uiHash.window.FindName("Button1")
$Global:uiHash.Button2 = $Global:uiHash.window.FindName("Button2")
$Global:uiHash.Button3 = $Global:uiHash.window.FindName("Button3")
$Global:uiHash.TextBox = $Global:uiHash.window.FindName("textBox")
$Global:uiHash.groupBox = $Global:uiHash.window.FindName("groupBox")
$Global:uiHash.label = $Global:uiHash.window.FindName("label")
$Global:uiHash.Button.Add_click({$Global:uiHash.Window.Dispatcher.Invoke([action]{$Global:uiHash.TextBox.AppendText("FFFFFFFF")}, "Normal")})
$Global:uiHash.Window.ShowDialog() | Out-Null
})
$psCmd.Runspace = $newRunspace
$handle = $psCmd.BeginInvoke()
Ok. Functions working as intended. I can change wallpapper or do whatever else. But I cannot pass text to textbox. Reworked UI with $GLOBAL:uiHash variable, it works, but... Like this I cannot change text on click. But when I try to change it like this:
Start-Sleep -s 5
$Global:uiHash.Window.Dispatcher.Invoke([action]{$Global:uiHash.TextBox.AppendText("FFFFF")},"Normal")
All fine, text appears.. What the..? What's wrong with it?
And by the way, Mike, text isnt changing, when I'm using your function. I think, I'm doing something wrong.

Resources