Powershell -notin array won't trigger if statement - arrays

I've recently begun to create a powershell script including a GUI.
To prevent the GUI from freezing I've created a background job in which my function "xyz" runs...
I want to capture a specific window title. If this window closes the if should be fired.
Now my problem with this script is the following:
If I run the script without putting it in the background job it does notice the if statement and returns the value I want to have.
If I run the script and put the task into the background job this job won't stop and won't recognize the if statement.
Does someone have a solution for this problem?
Function xyz {
$global:TitleArray = #("Termius - Hosts","Some Window Title","...")
$global:WindowClosed = $false
$global:TitleArray | Out-Host
$global:Job = start-job -Name FindGame {
Do {
(Get-Process | Where-Object {$_.MainWindowTitle -ne ""} | Select-Object MainWindowTitle) | % {
if($_.MainWindowTitle -in $global:TitleArray){
$global:WindowFound = $true
$global:FoundWindowName = $_.MainWindowTitle
do {
$WindowArray = #()
(Get-Process | Where-Object {$_.MainWindowTitle -ne ""} | Select-Object MainWindowTitle) | % {
$WindowArray += $_.MainWindowTitle
}
if($global:FoundWindowName -notin $WindowArray){
$global:WindowClosed = $true
}
Sleep 1
} while ($global:WindowClosed -ne $true)
}
if($global:WindowClosed){
$Global:FoundWindowName | Out-Host
exit
}
Sleep 1
}
} while ($true)
}
}
xyz

PowerShell jobs (of type PSJob) run in a separate process, so they have no access whatsoever to your calling program's environment, not even variables in the Global scope.
To get information into the job, you should define a param() block in the job's script block, and then use the -ArgumentList parameter of Start-Job to send in values. Do note that this will be a one time passing in of values.
To return data from the job, you just send it out through the pipeline as usual, and to access the data you'll need to use Get-Job to determine whether the job "has additional data", and if it does, you use Receive-Job to retrieve that data.
Unfortunately all this means that you still need your main thread to be managing the background job, defeating the purpose.
You might look into Register-ObjectEvent instead, and pair that with some kind of .Net object that can raise events. It appears that these do run in-process, but more than that, they can fire based on events you might actually be interested in without managing a loop.
So at its simplest, and maybe just to get acquainted you can look at a timer example where a timer object fires the event on an interval:
$timer = new-object timers.timer
$action = {write-host "Timer Elapse Event: $(get-date -Format ‘HH:mm:ss’)"}
$timer.Interval = 3000 #3 seconds
Register-ObjectEvent -InputObject $timer -EventName elapsed –SourceIdentifier thetimer -Action $action
$timer.start()
#to stop run
$timer.stop()
#cleanup
Unregister-Event thetimer
But the examples on the Microsoft page even include monitoring a Process Creation Event from WMI, so that and/or and process exit event(?) might be worth looking into.

Related

PowerShell WPF UI detect key combination

I am writing a UI to run in WinPE. I have everything working so far and am trying to make it so the user cannot close the window. I have made it full screen and disabled the close (X) button, but the user can still press Alt+F4 to close the UI. I have been able to make it so that if the user hits F8 the UI is in front so the user cannot Alt+Tab to it. I have read so many ways to do this but nothing covers it for PowerShell I am not sure how to implement it in the script. It is not running in a Runspace.
Here is what I have tried:
$altF4Pressed = {
[System.Windows.Input.KeyEventArgs]$Alt = $args[1]
if ( ($Alt.Key -eq 'System') -or ($Alt.Key -eq 'F4') ) {
if ($_.CloseReason -eq 'UserClosing') {
$UI.Cancel = $true
}
}
}
$null = $UI.add_KeyDown($altF4Pressed)
I have also read to do this (Disabling Alt+F4 in a Powershell form), but this does not work.
#disable closing window using Alt+F4
$UI_KeyDown = [System.Windows.Forms.KeyEventHandler]{
#Event Argument: $_ = [System.Windows.Forms.KeyEventArgs]
if ($_.Alt -eq $true -and $_.KeyCode -eq 'F4') {
$script:altF4Pressed = $true;
}
}
$UI_Closing = [System.Windows.Forms.FormClosingEventHandler]{
#Event Argument: $_ = [System.Windows.Forms.FormClosingEventArgs]
if ($script:altF4Pressed)
{
if ($_.CloseReason -eq 'UserClosing') {
$_.Cancel = $true
$script:altF4Pressed = $false;
}
}
Else{
[System.Windows.Forms.Application]::Exit();
# Stop-Process $pid
}
}
$UI.Add_Closing({$UI_Closing})
$UI.add_KeyDown({$UI_KeyDown})
I have also tried to do this:
$UI.Add_KeyDown({
$key = $_.Key
If ([System.Windows.Input.Keyboard]::IsKeyDown("RightAlt") -OR [System.Windows.Input.Keyboard]::IsKeyDown("LeftAlt")) {
Switch ($Key) {
"F4" {
$script:altF4Pressed = $true;
write-host "Alt+f4 was pressed"
}
}
}
})
It detects the first keyboard press, but not the next one while the other is pressed. I think I need to use a Keybinding event instead, just not sure how to implement that in Powershell at the App level (not input level). I read you can add a keybinding to XAML code itself, but how do that with Powershell to detect the key combination (Alt+F4) when the UI presents itself?
You don't have a replicate-able example and I don't want to write one just for this, but I think this may be of assistance.
$altF4Pressed = {
[System.Windows.Input.KeyEventArgs]$Alt = $args[1]
if ( ($Alt.Key -eq 'System') -or ($Alt.Key -eq 'F4') ) {
if ($_.CloseReason -eq 'UserClosing') {
$UI.Cancel = $true
$Alt.Handled = $true
}
}
}
$null = $UI.add_KeyDown([System.Windows.Input.KeyEventHandler]::new($altF4Pressed))
Also IIRC for WPF, you'll want to use System.Windows.Input not System.Windows.Forms, so the second snippet you posted may actually work if you change the namespace.
I found the detection of keys with forms or WPF to be well above my skill level. However, I was able to accomplish the same task with a different approach. I needed to do this with an install script in Windows 10.
What I did was disable both left/right alt keys and F4 during the time the script was running and revert the changes, delete the key, after completed.
With WinPE's registry getting "reset" on each reboot it's your call if you want to diable the change after you make it.
# Disable Left & Right ALT keys and F4
Set-ItemProperty -path "HKLM:\SYSTEM\CurrentControlSet\Control\Keyboard Layout" -name "Scancode Map" -Value ([byte[]](0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x04,0x00,0x00,0x00,0x00,0x00,0x3e,0x00,0x00,0x00,0x38,0x00,0x00,0x00,0x38,0xe0,0x00,0x00,0x00,0x00))
To re-enable Alt + F4 simply delete the "Scancode Map" registry value and reboot again.
I found the answer here: http://www.edugeek.net/forums/windows-server-2008-r2/121900-disable-alt-f4.html

Test-Connection $False will not convert to ArrayList

Currently working on making a new report that will be generated with PowerShell. Using PowerShell to build a HTML email. I have one other report working fine but ran into an unexpected issue on this one.
The below code is just s sample from the script I am still building. Still adding pieces to the script but testing it as I move forward. I added a Test-Connection to see if a computer was responding or not and lost the ability to build an array.
My final goal with this report is to import a list of names from a file and then loop over all of the computers to see if they are pinging and gather some information from them using Get-WMIObject, etc.
The below code will replicate the issue I am having but I am not sure how to solve it. I've narrowed down the issue to when Test-Connection returns 'False'. On line 26 I am filtering for just results that returned a 'False' on Test-Connection to save them into its own array so that I can use that array in a different part of my code to build the HTML table/HTML to send out the email.
Only the flipside, if I tell it to look for only 'True', it will save into the array without issue.
This is the error that PowerShell is giving when doing filtering by 'False'.
Cannot convert value "#{Computer_Name=Computer1; Ping_Status=False}" to type "System.Collections.ArrayList". Error: "Cannot convert the "#{Computer_Name=Computer1 Ping_Status=False}" value of type "Selected.System.Management.Automation.PSCustomObject" to type "System.Collections.ArrayList"."
Please let me know if there is any other information that I can provide. I've been stuck on this one for a while. Co-workers are even say this is a weird one.
Is there something unique about the way Test-Connection return a 'False'?
CLS
[string]$ErrorActionPreference = "Continue"
[System.Collections.ArrayList]$Names = #(
"Computer1"
"Computer2"
)
[System.Collections.ArrayList]$WMI_Array = #()
[System.Collections.ArrayList]$Ping_Status_False = #()
foreach ($Name in $Names) {
[bool]$Ping_Status = Test-Connection $Name -Count 1 -Quiet
$WMI_Array_Object = [PSCustomObject]#{
'Computer_Name' = $Name
'Ping_Status' = $Ping_Status
}
$WMI_Array.Add($WMI_Array_Object) | Out-Null
}
$WMI_Array | Format-Table
[System.Collections.ArrayList]$Ping_Status_False = $WMI_Array | Where-Object {$_.Ping_Status -eq $false} | Select-Object Computer_Name, Ping_Status
$Ping_Status_False
The problem is not Test-Connection but that this statement
$WMI_Array | Where-Object {$_.Ping_Status -eq $false} | Select-Object Computer_Name, Ping_Status
produces just a single result. Which is not an array, and can thus not be converted to an ArrayList. The behavior is identical when you filter for $_.PingStatus -eq $true with just a single matching object, so I suspect that you had either more than one successfully pinged host or none at all when you tested that condition and it didn't throw the same error.
You could mitigate the problem by wrapping the statement in the array subexpression operator:
[Collections.ArrayList]$Ping_Status_False = #($WMI_Array |
Where-Object {$_.Ping_Status -eq $false} |
Select-Object Computer_Name, Ping_Status)
Or, you could simply drop all the pointless type-casting from your code:
$ErrorActionPreference = "Continue"
$Names = 'Computer1', 'Computer2'
$WMI_Array = foreach ($Name in $Names) {
[PSCustomObject]#{
'Computer_Name' = $Name
'Ping_Status' = [bool](Test-Connection $Name -Count 1 -Quiet)
}
}
$WMI_Array | Where-Object { -not $_.Ping_Status }

Piping ListView items in separate runspace

I have a form written in PowerShell (as it uses PS to run commands over hundreds of servers) but this bit had me stumped for awhile:
$listview.Invoke([System.Action[String]]{
Param
(
[String]$data
)
write-warning "1"
$LVitem = ($LVresults.Items | Where { $_.Name -eq "Test Account" })
write-warning "2"
$LVitem.Tag.Output += $data + "`n"
}, "Testing")
It's in a separate runspace that runs an Invoke-Command to a specific server and pipes the output to this code block.
Now if I run the Where statement in the main runspace (where the form is created) it works perfectly fine. In the separate runspace however it locks up the form. Warning 1 is displayed, Warning 2 is not.
Piping to a Foreach statement has the same problem, but I can use:
Foreach ($item in $listview.Items) {
if ($item.Name -eq "Test Account") { $LVitem = $item }
}
Can anyone explain this? I'm not doing anything fancy with the ListView or its items, it just seems the ListView doesn't like its items being piped in another runspace.
This is the problem with PowerShell event system. It deadlocks when event handler generate event with wait to completion option.
Register-EngineEvent -SourceIdentifier MyEvent1 -Action {Write-Host 'Does not get called.'}
Register-EngineEvent -SourceIdentifier MyEvent2 -Action {$ExecutionContext.Events.GenerateEvent('MyEvent1',$null,$null,$null,$true,$true)}
New-Event -SourceIdentifier MyEvent2
So, what your setup have to do with events?
Script blocks literals remembers current session state (including Runspace) at creation time. And if at script block invocation time current Runspace for current thread ([Runspace]::DefaultRunspace) is different or busy processing something in different thread, then script block invoked by generating event to original Runspace with wait to completion option.
Another interesting thing is that: if target Runspace does not start processing events in 250 milliseconds, then PowerShell start event processing in the thread waiting for event completion.
In your code you have two nested script block invokations:
Script block passed as delegate to $listview.Invoke method.
Script block passed to Where-Object cmdlet.
As I see, your code have three possible outcomes:
Script block's original Runspace is free, so script block executed in different thread (not UI thread).
$PowerShell=[PowerShell]::Create()
$PowerShell.Runspace=[RunspaceFactory]::CreateRunspace($Host)
$PowerShell.Runspace.Open()
$Stopwatch=[Diagnostics.Stopwatch]::new()
$PowerShell.Runspace.SessionStateProxy.PSVariable.Set('Stopwatch',$Stopwatch)
$ScriptBlock=$PowerShell.AddScript{{
Write-Host "$($Stopwatch.ElapsedMilliseconds): $([Threading.Thread]::CurrentThread.ManagedThreadId)"
{Write-Host OK}.Invoke()
}}.Invoke()
$PowerShell.Commands.Clear()
& {
$Stopwatch.Start()
Write-Host "$($Stopwatch.ElapsedMilliseconds): $([Threading.Thread]::CurrentThread.ManagedThreadId)"
$ScriptBlock.Invoke()
}
Script block's original Runspace is busy, so script block executed in current thread and deadlocks on nested script block invocation.
$PowerShell=[PowerShell]::Create()
$PowerShell.Runspace=[RunspaceFactory]::CreateRunspace($Host)
$PowerShell.Runspace.Open()
$Stopwatch=[Diagnostics.Stopwatch]::new()
$PowerShell.Runspace.SessionStateProxy.PSVariable.Set('Stopwatch',$Stopwatch)
$ScriptBlock=$PowerShell.AddScript{{
Write-Host "$($Stopwatch.ElapsedMilliseconds): $([Threading.Thread]::CurrentThread.ManagedThreadId)"
{Write-Host Deadlock}.Invoke()
}}.Invoke()
$PowerShell.Commands.Clear()
& {
$AsyncResult=$PowerShell.AddScript{Start-Sleep 10}.BeginInvoke()
$Stopwatch.Start()
Write-Host "$($Stopwatch.ElapsedMilliseconds): $([Threading.Thread]::CurrentThread.ManagedThreadId)"
$ScriptBlock.Invoke()
}
Script block's original Runspace initially busy, but get free before nested script block invocation, so script block executed in current thread and does not deadlocks.
$PowerShell=[PowerShell]::Create()
$PowerShell.Runspace=[RunspaceFactory]::CreateRunspace($Host)
$PowerShell.Runspace.Open()
$Stopwatch=[Diagnostics.Stopwatch]::new()
$PowerShell.Runspace.SessionStateProxy.PSVariable.Set('Stopwatch',$Stopwatch)
$ScriptBlock=$PowerShell.AddScript{{
Write-Host "$($Stopwatch.ElapsedMilliseconds): $([Threading.Thread]::CurrentThread.ManagedThreadId)"
Start-Sleep 10
{Write-Host OK}.Invoke()
}}.Invoke()
$PowerShell.Commands.Clear()
& {
$AsyncResult=$PowerShell.AddScript{Start-Sleep 5}.BeginInvoke()
$Stopwatch.Start()
Write-Host "$($Stopwatch.ElapsedMilliseconds): $([Threading.Thread]::CurrentThread.ManagedThreadId)"
$ScriptBlock.Invoke()
}
To workaround this issue you need to detach script block from its original Runspace. You can achieve that by creating new script block from string using [ScriptBlock]::Create method.
$PowerShell=[PowerShell]::Create()
$PowerShell.Runspace=[RunspaceFactory]::CreateRunspace($Host)
$PowerShell.Runspace.Open()
$Stopwatch=[Diagnostics.Stopwatch]::new()
$PowerShell.Runspace.SessionStateProxy.PSVariable.Set('Stopwatch',$Stopwatch)
$ScriptBlock=$PowerShell.AddScript{[ScriptBlock]::Create{
Write-Host "$($Stopwatch.ElapsedMilliseconds): $([Threading.Thread]::CurrentThread.ManagedThreadId)"
{Write-Host OK}.Invoke()
}}.Invoke()
$PowerShell.Commands.Clear()
& {
$AsyncResult=$PowerShell.AddScript{Start-Sleep 10}.BeginInvoke()
$Stopwatch.Start()
Write-Host "$($Stopwatch.ElapsedMilliseconds): $([Threading.Thread]::CurrentThread.ManagedThreadId)"
$ScriptBlock.Invoke()
}

Is there a way to send events to the parent job when using Start-WPFJob?

I would like to launch a non-blocking UI from a parent Powershell script and receive UI messages like button clicks from the child job. I have this kind of messaging working using WinForms, but I prefer to use ShowUI because of how much less code it takes to create a basic UI. Unfortunately, though, I haven't found a way to send messages back to the parent job using ShowUI.
[Works] Forwarding Events When Using Start-Job
Using Start-Job, forwarding events from a child to a parent job is rather straightforward. Here is an example:
$pj = Start-Job -Name "PlainJob" -ScriptBlock {
Register-EngineEvent -SourceIdentifier PlainJobEvent -Forward
New-Event -SourceIdentifier PlainJobEvent -MessageData 'My Message'
}
Wait-Event | select SourceIdentifier, MessageData | Format-List
As expected, it prints out:
SourceIdentifier : PlainJobEvent
MessageData : My Message
[Does Not Work] Forwarding Events When Using Start-WPFJob
Using Start-WPFJob, on the other hand, does not seem to forward events from the child to the parent. Consider this example:
Import-Module ShowUI
$wj = Start-WPFJob -ScriptBlock {
Register-EngineEvent -SourceIdentifier MySource -Forward
New-Button "Button" -On_Click {
New-Event -SourceIdentifier MySource -MessageData 'MyMessage'
}
}
Wait-Event | select SourceIdentifier, MessageData | Format-List
Running this example produces this window:
Clicking on the button, however, does not yield an event in the parent job.
Why doesn't the Start-WPFJob example yield events to the parent job?
Is there some other way to use ShowUI to produce a button in a non-blocking manner and receive events from it?
I can't get engineevents to forward properly so far (actually, I can't even get them to do anything, as far as I can tell), I think your best bet is to run the WPFJob, and instead of New-Event, update the $Window UIValue, and then from your main runspace, instead of Wait-Event, use Update-WPFJob in a loop.
I would stick this function into the module (actually, I will add it for the 1.5 release that's in source control but not released yet):
function Add-UIValue {
param(
[Parameter(ValueFromPipeline=$true)]
[Windows.FrameworkElement]
$Ui,
[PSObject]
$Value
)
process {
if ($psBoundParameters.ContainsKey('Value')) {
Set-UIValue $UI (#(Get-UIValue $UI -IgnoreChildControls) + #($Value))
} else {
Set-UIValue -Ui $ui
}
}
}
And then, something like this:
$job = Start-WPFJob {
Window {
Grid -Rows "1*", "Auto" {
New-ListBox -Row 0 -Name LB -Items (Get-ChildItem ~ -dir)
Button "Send" -Row 1 -On_Click { Add-UIValue $Window $LB.SelectedItem }
}
} -SizeToContent "Width" -MinHeight 800
}
Every time you click, would add the selected item to the UI output (if you run that window without the job and click the button a couple of times, then close the window, you'll get two outputs).
Then you can do something like this in the host instead of Wait-Event:
do {
Update-WPFJob -Job $job -Command { Get-UIValue $Window -IgnoreChildControls } -OutVariable Output
Start-Sleep -Mil 300
} while (!$Output)

PowerShell: Job Event Action with Form not executed

If I run the following code, the Event Action is executed:
$Job = Start-Job {'abc'}
Register-ObjectEvent -InputObject $Job -EventName StateChanged `
-Action {
Start-Sleep -Seconds 1
Write-Host '*Event-Action*'
}
The string 'Event-Action' is displayed.
If I use a Form and start the above code by clicking a button,
the Event Action is not executed:
[System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
$Form1 = New-Object Windows.Forms.Form
$Form1.Add_Shown({
$Form1.Activate()
})
$Button1 = New-Object System.Windows.Forms.Button
$Button1.Text = 'Test'
$Form1.Controls.Add($Button1)
$Button1.Add_Click({
Write-Host 'Test-Button was clicked'
$Job = Start-Job {'abc'}
Register-ObjectEvent -InputObject $Job -EventName StateChanged `
-Action {
Start-Sleep -Seconds 1
Write-Host '*Event-Action*'
}
})
$Form1.ShowDialog()
Only when I click the button again, the first Event Action is executed.
With the third click the second Event Action is executed and so on.
If I do multiple clicks in rapid succession, the result is unpredictable.
Furthermore when I close the form with the button in the upper right corner,
the last "open" Event Action is executed.
Note: For testing PowerShell ISE is to be preferred, because PS Console displays
the string only under certain circumstances.
Can someone please give me a clue what's going on here?
Thanks in advance!
nimizen.
Thanks for your explanation, but I don't really understand, why the StateChanged event is not fired or visible to the main script until there is some action with the Form. I'd appreciate another attempt to explain it to me.
What I want to accomplish is a kind of multithreading with PowerShell and Forms.
My plan is the following:
'
The script shows a Form to the user.
The user does some input and clicks a button.
Based on the user's input a set of Jobs are started with Start-Job and a StateChanged event is registered for each job.
While the Jobs are running, the user can perform any action on the Form (including stop the Jobs via a button) and the Form is repainted when necessary.
The script reacts to any events which are fired by the Form or its child controls.
Also the script reacts to each job's StateChanged event.
When a StateChanged event occurs, the state of each job is inspected, and if all jobs have the state 'Completed', the jobs' results are fetched with Receive-Job and displayed to the user.
'
All this works fine except that the StateChanged event is not visible to the main script.
The above is still my favorite solution and if you have any idea how to implement this, please let me know.
Otherwise I'll most likely resort to a workaround, which at least gives the user a multithreading feeling. It is illustrated in the following example:
[System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
$Form1 = New-Object Windows.Forms.Form
$Form1.Add_Shown({
$Form1.Activate()
})
$Button1 = New-Object System.Windows.Forms.Button
$Button1.Text = 'Test'
$Form1.Controls.Add($Button1)
$Button1.Add_Click({
$Form1.Focus()
Write-Host 'Test-Button was clicked'
$Job = Start-Job {Start-Sleep -Seconds 1; 'abc'}
Do {
Start-Sleep -Milliseconds 100
Write-Host 'JobState: ' $Job.State
[System.Windows.Forms.Application]::DoEvents()
}
Until ($Job.State -eq 'Completed')
Write-Host '*Action*'
})
$Form1.ShowDialog()
There are a lot of (StackOverflow) questions and answers about this ‘enduring mystique’ of combining form (or WPF) events with .NET events (like EngineEvents, ObjectEvents and WmiEvents) in PowerShell:
Do Jobs really work in background in powershell?
WPF events not working in Powershell - Carousel like feature in multi-threaded script
is it possible to control WMI events though runspace and the main form?
Is there a way to send events to the parent job when using Start-WPFJob?
Update WPF DataGrid ItemSource from Another Thread in PowerShell
They are all come down two one point: even there are multiple threads setup, there are two different 'listeners' in one thread. When your script is ready to receive form events (using ShowDialog or DoEvents) it can’t listen to .NET events at the same time. And visa versa: if script is open for .NET events while processing commands (like Start-Sleep or specifically listen for .NET events using commands like Wait-Event or Wait-Job), your form will not be able to listen to form events. Meaning that either the .NET events or the form events are being queued simply because your form is in the same thread as the .NET listener(s) your trying to create.
As with the nimizen example, with looks to be correct at the first glans, your form will be irresponsive to all other form events (button clicks) at the moment you’re checking the backgroundworker’s state and you have to click the button over and over again to find out whether it is still ‘*Doing Stuff’. To work around this, you might consider to combine the DoEvents method in a loop while you continuously checking the backgroundworker’s state but that doesn’t look to be a good way either, see: Use of Application.DoEvents()
So the only way out (I see) is to have one thread to trigger the form in the other thread which I think can only be done with using [runspacefactory]::CreateRunspace() as it is able to synchronize a form control between the treats and with that directly trigger a form event (as e.g. TextChanged).
(if there in another way, I eager to learn how and see a working example.)
Form example:
Function Start-Worker {
$SyncHash = [hashtable]::Synchronized(#{TextBox = $TextBox})
$Runspace = [runspacefactory]::CreateRunspace()
$Runspace.ThreadOptions = "UseNewThread" # Also Consider: ReuseThread
$Runspace.Open()
$Runspace.SessionStateProxy.SetVariable("SyncHash", $SyncHash)
$Worker = [PowerShell]::Create().AddScript({
$ThreadID = [appdomain]::GetCurrentThreadId()
$SyncHash.TextBox.Text = "Thread $ThreadID has started"
for($Progress = 0; $Progress -le 100; $Progress += 10) {
$SyncHash.TextBox.Text = "Thread $ThreadID at $Progress%"
Start-Sleep 1 # Some background work
}
$SyncHash.TextBox.Text = "Thread $ThreadID has finnished"
})
$Worker.Runspace = $Runspace
$Worker.BeginInvoke()
}
[Void][System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
$Form = New-Object Windows.Forms.Form
$TextBox = New-Object Windows.Forms.TextBox
$TextBox.Visible = $False
$TextBox.Add_TextChanged({Write-Host $TextBox.Text})
$Form.Controls.Add($TextBox)
$Button = New-Object System.Windows.Forms.Button
$Button.Text = "Start worker"
$Button.Add_Click({Start-Worker})
$Form.Controls.Add($Button)
$Form.ShowDialog()
For a WPF example, see: Write PowerShell Output (as it happens) to WPF UI Control
The state property of Powershell jobs is read-only; this means that you can't configure the job state to be anything before you actually start the job. When you're monitoring for the statechanged event, it doesn't fire until the click event comes around again and the state is 'seen' to change from 'running' to 'completed' at which point your script block executes. This is also the reason why the scriptblock executes when closing the form.
The following script removes the need to monitor the event and instead monitors the state. I assume you want to fire the on 'statechanged' code when the state is 'running'.
[System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
$Form1 = New-Object Windows.Forms.Form
$Form1.Add_Shown({
$Form1.Activate()
})
$Button1 = New-Object System.Windows.Forms.Button
$Button1.Text = 'Test'
$Form1.Controls.Add($Button1)
$Button1.Add_Click({
$this.Enabled = $false
Write-Host $Job.State " - (Before job started)"
$Job = Start-Job {'abc'}
Write-Host $Job.State " - (After job started)"
If ($Job.State -eq 'Running') {
Start-Sleep -Seconds 1
Write-Host '*Doing Stuff*'
}
Write-Host $Job.State " - (After IF scriptblock finished)"
[System.Windows.Forms.Application]::DoEvents()
$this.Enabled = $true
})
$Form1.ShowDialog()
In addition, note the lines:
$this.Enabled = $false
[System.Windows.Forms.Application]::DoEvents()
$this.Enabled = $true
These lines ensure the button doesn't queue click events. You can obviously remove the 'write-host' lines, I've left those in so you can see how the state changes as the script executes.
Hope this helps.

Resources