When working with GUIs you may have noticed that the Form can freeze when running long scripts. Previously I discussed how to make your loops more responsive in this article, but not every long script comes in the form of a loop. If you truly want to make your forms responsive, you will need to move these slow scripts into another thread and in the PowerShell world this means using jobs.

For those of you who aren’t familiar with PowerShell Jobs, they allow you to run scripts while freeing up the console to perform other tasks. In this case it will free up the GUI and allow it to respond to user input. This article will not cover the ins and outs of jobs and it expects the user has some basic knowledge of the jobs mechanism in PowerShell. For your convenience we will list the cmdlets that are directly related to jobs. Please refer to the MSDN PowerShell Jobs help page for more information.

There are two caveats you need to keep in mind when using jobs within a Form.

1. Never access or modify form controls directly from within a job. If you need to update a form control or show progress, use the Receive-Job cmdlet to gather any necessary information from the job first and then update the control from the main script. The form controls do not allow themselves to be accessed from a different thread (i.e., the job).

2. Don’t use Register-ObjectEvent cmdlet. To determine if a job is complete you will need to check the status of the job. If you try to register an event handler for the job’s StateChanged event using Register-ObjectEvent, you find that it will not seem to trigger while the form is displayed, unless you call the [System.Windows.Forms.Application]::DoEvents() method mentioned in the Creating Responsive Loops article.

Even with this work around you cannot access the form controls directly. Therefore, you will need to use a Timer control to check the job’s status periodically.

Creating a Form that utilizes Jobs

Now that we covered the caveats, we can now begin to modify our forms so that it can handle jobs.

Version 3.0.3 of PowerShell Studio has a Control Set called “Button – Start Job”. If you look at the control set it inserts a button and a timer. The timer checks the status of a job that is created when the button is pressed.

Button Click Event:

The button creates a job, starts the timer and uses the tag property of the timer to track it.

The Timer checks its Tag property, which contains the job object and checks the job’s State property to see if it is still running. If the job is complete, it stops the timer and enables the button, otherwise it continue to animate the button.

As you can see this works well with a single job and if you need multiple jobs to run at the same time then you have to create multiple timers.

Creating a new Job Tracker Framework

Let’s expand on this idea and create a system that can scale and that only requires a single timer.

First we need a list that the system can use to track the current jobs. It is defined as follows:

$JobTrackerList=New-Object System.Collections.ArrayList

Next I created functions to interface with the JobTracker Framework.

The first function is Add-JobTracker. This function creates and adds a new job to the Job Tracker. It allows you specify a script block that the job will run and optional script block that will be called when the job is completed and another when the timer performs an update.

functionAdd-JobTracker
{
<#
.SYNOPSIS
Add a new job to the JobTracker and starts the timer.
.DESCRIPTION
Add a new job to the JobTracker and starts the timer.
.PARAMETER Name
The name to assign to the Job
.PARAMETER JobScript
The script block that the Job will be performing.
Important: Do not access form controls from this script block.
.PARAMETER ArgumentList
The arguments to pass to the job
.PARAMETER CompleteScript
The script block that will be called when the job is complete.
The job is passed as an argument. The Job argument is null when the job fails.
.PARAMETER UpdateScript
The script block that will be called each time the timer ticks.
The job is passed as an argument. Use this to get the Job's progress.
.EXAMPLE
Job-Begin -Name "JobName" `
-JobScript {
Param($Argument1)#Pass any arguments using the ArgumentList parameter
#Important: Do not access form controls from this script block.
Get-WmiObject Win32_Process -Namespace "root\CIMV2"
}`
-CompletedScript {
Param($Job)
$results = Receive-Job -Job $Job
}`
-UpdateScript {
Param($Job)
#$results = Receive-Job -Job $Job -Keep
}
.LINK
#>Param(
[ValidateNotNull()]
[Parameter(Mandatory=$true)]
[string]$Name,
[ValidateNotNull()]
[Parameter(Mandatory=$true)]
[ScriptBlock]$JobScript,
$ArgumentList=$null,
[ScriptBlock]$CompletedScript,
[ScriptBlock]$UpdateScript)
#Start the Job$job=Start-Job-Name$Name-ScriptBlock$JobScript-ArgumentList$ArgumentListif($job-ne$null)
{
#Create a Custom Object to keep track of the Job & Script Blocks$psObject=New-Object System.Management.Automation.PSObject
Add-Member-InputObject$psObject-MemberType'NoteProperty'-Name Job -Value$jobAdd-Member-InputObject$psObject-MemberType'NoteProperty'-Name CompleteScript -Value$CompletedScriptAdd-Member-InputObject$psObject-MemberType'NoteProperty'-Name UpdateScript -Value$UpdateScript
[void]$JobTrackerList.Add($psObject)
#Start the Timerif(-not$timerJobTracker.Enabled)
{
$timerJobTracker.Start()
}
}
elseif($CompletedScript-ne$null)
{
#FailedInvoke-Command-ScriptBlock$CompletedScript-ArgumentList$null
}
}

A custom PSObject is used to keep track of the job and script blocks. Afterwards the PSObject is added to the Job Tracker list. Note: The corresponding job is passed as an argument to the CompletedScript and UpdateScript script blocks. This allows the user to use the Receive-Job cmdlet to access the full or partial results of a job.

Next we created a function called Update-JobTracker, which the timer uses to check the status of all the jobs in the Job Tracker List. If the job is complete, it will then call the job’s corresponding CompletedScript script block. Otherwise if the job is still running, it will then call the corresponding UpdateScript script block. If all the jobs are completed, then the function will stop the timer.

We update the timer tick event to call the update Function from our Timer Tick event:

$timerJobTracker_Tick={
Update-JobTracker
}

Note: You can modify the Timer’s Interval property to slow down or speed up the amount of time the timer waits before checking the progress of the pending jobs again.

The final function of our Job Tracker Framework is the Stop-JobTracker function. This function will stop all pending jobs and remove them from the Job Tracker list. The function will also stop the timer.

Note: You can call the Stop-JobTracker function in response to the user closing the form. This ensures there are no pending jobs running after the fact.

functionStop-JobTracker{<# .SYNOPSIS Stops and removes all Jobs from the list. #>#Stop the timer$timerJobTracker.Stop()

#Remove all the jobswhile($JobTrackerList.Count-gt0) {$job=$JobTrackerList[0].Job$JobTrackerList.RemoveAt(0)Stop-Job$jobRemove-Job$job }}

Now that we have a new framework, we can revise our initial Start Job button’s click event as follows:

All of the script is now contained within a single location. What’s more we can use the Add-JobTracker function to create multiple jobs using various sources as triggers without having to create multiple timers or add any other special considerations.

Displaying Progress with the Job Tracker:

If you want to display partial data or the Job’s progress, you should use the Receive-Job cmdlet during the update script block.

If you want to use a progress bar, you might consider having your job script output the percentage of progress.

If you return results as well as the progress status then you might want to use the Receive-Job’s Keep parameter to ensure the data is all there. You will also need to make sure you ignore the progress information when processing the final results. You can use any number of techniques to differentiate this information. For example, you can use strings that start with “P: ” to determine it is a progress indicator. Of course this also then means you will need to parse and filter this information when receiving job results. The sky is the limit as to how you wish to handle these situations.

Job Tracker Control Set!

The good news is you needn’t worry about copying and pasting the framework in to each of your forms. I created an easy to use Control Set that will insert the framework into your existing form. In addition, I updated the “Button – Start Job” control set to utilize the new Job Tracker framework. The beauty of it is that it will no longer insert duplicate timers, since it is now handled by the Job Tracker Control Set.

These control sets will be available in the next service release (v3.0.4).