PowerShell: Work in progress event action

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 "Event-Action" line is displayed.

If I use a form and run the above code by clicking a button,

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 does the first Event action take place.

With the third click, the second Event action is executed, etc.

If I make several clicks in quick succession, the result is unpredictable.

Also, when I close the form with the button in the upper right corner,

the last "open" event is executed.

Note. For testing PowerShell ISE is preferred because the PS console displays

string only under certain circumstances.

Can someone please let me understand what's going on here?

Thanks in advance!


nimizen.

Thanks for your explanation, but I really don't understand why the StateChanged event is not fired or displayed in the main script until there is some form action. I would appreciate another attempt to explain this to me.

What I want to accomplish is multithreading with PowerShell and Forms.

My plan is this:

The script shows the form to the user.

The user enters and presses a button. Based on user input, a set of jobs is started with Start-Job and a StateChanged event is logged for each job.

While the tasks are in progress, the user can perform any action on the form (including stopping the tasks with a button) and the form will be rescheduled as needed.

The script responds to any events that are triggered by the form or its child controls.

Also the script reacts to every StateChanged event.

When the StateChanged event occurs, the status of each job is checked, and if all jobs have a Completed status, the job results are retrieved using the Get-Job and displayed to the user.

This all works fine, except the StateChanged event is not displayed in 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 will most likely resort to a workaround that at least gives the user multithreading. This 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()

      

+1


source to share


2 answers


The state property of Powershell properties is read-only; this means that you cannot customize the job status any way you like before starting work. When you track the statechanged event, it doesn't fire until the click event reappears and the "seen" state changes from "running" to "complete", after which your script block will execute. This is also the reason why the script block is executed when the form is closed.

The following script removes the need to track the event and monitors the state instead. I assume you want to run the "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()

      

Also, notice the lines:



$this.Enabled = $false
[System.Windows.Forms.Application]::DoEvents()
$this.Enabled = $true

      

These lines ensure that the button will not attend queue events. Obviously you can remove the "write-host" lines, I left them out so you can see how the state changes when the script is executed.

Hope it helps.

0


source


There are many (StackOverflow) questions and answers about this persistent mysticism of combining form (or WPF) events with .NET events (e.g. EngineEvents

, ObjectEvents

and WmiEvents

) in PowerShell:

They all have two points: even there are multiple threads, there are two different "listeners" in one thread. When your script is ready to receive form events (using ShowDialog

or DoEvents

), it cannot listen for .NET events at the same time. Conversely, if the script is exposed to .NET events during command processing (for example, Start-Sleep

or specifically listen for .NET events using commands like Wait-Event

orWait-Job

), your form will not be able to listen to event forms. This means that either .NET events or form events are queued simply because your form is on the same thread as your (.NET) listeners you are trying to create. As in the nimizen example, with the right look at the first heads, your form will be irresponsible to all other form events (button clicks) the moment you check the state of background workers and you have to press the button again and again to find out if it is still β€˜*Doing Stuff’

. To get around this, you might consider concatenating the method DoEvents

in a loop where you are constantly checking the state of background workers, but that doesn't look like a good way either: see Using Application.DoEvents () So the only way out (I can see) is for one thread to invoke the form on another thread, which I think can only be done with [runspacefactory]::CreateRunspace()

, since it is able to synchronize the form control between hits and directly trigger an event with that forms (for example TextChanged

).

(if there is another way, I want to know how and how to work with 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 Writing PowerShell Output (How It Happens) in WPF UI Control

0


source







All Articles