PowerShell and WPF: Writing Data to a UI From a Different Runspace

One of the biggest pain points when working with WPF and PowerShell is that everything is done on a single thread with the UI. This includes performing various activities related to events such as button clicking and selecting data. While an operation is being performed (especially a long running operation), you will most likely see the window appear to be hung and unable to be moved or really any sort of action similar to below:

image

So it might not look like much is happening here, and you would be right. The window is hung as it is loading up in the background. Sometimes it may show a Not Responding message, and other times (like now) it will not.  As I mentioned, this is due to only a single thread being used to handle both the UI and the operation, meaning that only one thing can be handled at a time. The longer the operation, the longer the the wait to even move the window some place else. Perhaps this isn’t too bad for some people, but if most people see this, they start to complain or believe something is wrong.

Usually, my solution for this has been to use many PSJobs along with an event watcher (Register-ObjectEvent) to check on each background PowerShell job and handle the data from each job when it completes. When the last job is finished and the event handler performs the last data update, it would then perform a cleanup by removing the last job and itself, thus freeing up resources. I have used this method or something similar to this in a few of my scripts and also my project: PoshPAIG. While this method does in fact do its job, it is prone to issues with the background jobs and as you can see from a previous post, it can be a pretty heavy operation. Also mentioned in that post was the use of background runspaces that you create to handle other operations.

Similar to PSJobs, background runspaces cannot access other variables between other runspaces, which is by design to prevent issues. The way around this is to use a thread safe collection, such as a synchronized hash table, which is what I plan on using in the following example as it will provide easier access to the WPF controls.

Display the UI

$syncHash = [hashtable]::Synchronized(@{})
$newRunspace =[runspacefactory]::CreateRunspace()
$newRunspace.ApartmentState = "STA"
$newRunspace.ThreadOptions = "ReuseThread"          
$newRunspace.Open()
$newRunspace.SessionStateProxy.SetVariable("syncHash",$syncHash)          
$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"
        x:Name="Window" Title="Initial Window" WindowStartupLocation = "CenterScreen"
        Width = "600" Height = "800" ShowInTaskbar = "True">
        <TextBox x:Name = "textbox" Height = "400" Width = "600"/>
    </Window>
"@
 
    $reader=(New-Object System.Xml.XmlNodeReader $xaml)
    $syncHash.Window=[Windows.Markup.XamlReader]::Load( $reader )
    $syncHash.TextBox = $syncHash.window.FindName("textbox")
    $syncHash.Window.ShowDialog() | Out-Null
    $syncHash.Error = $Error
})
$psCmd.Runspace = $newRunspace
$data = $psCmd.BeginInvoke()

The first line is where I set up my synchronized hash table that will then be fed into the new runspace by using the SessionState.SetVariable() method on the new runspace object. This will set up the runspace to already know what that variable is and what might already be assigned to it. But, by using this sync’d hash table, it is now thread safe so we can manipulate the contents of the hash table in a different runspace.  You will notice that I decided to store the relevant controls to this hash table so they will be made available outside of the UI’s runspace.

I am also placing everything for the UI in its own runspace so I can have my console free to do whatever I wish. Keep in mind that even though the console is open to use while the UI is still running, if I close the PowerShell console, the UI will also close as well.

 

image

With the UI open, I still have access to my console or ISE to do whatever I need to do. The next step is to actually make the connection to the other runspace and push some data to my UI. Looking at my synchronized hash table, you can see each control that I decided to add to the hash table in the case that I need to manipulate it.

$syncHash

 

 

PS C:\Users\Administrator> $syncHash
Name                           Value                                          
—-                           —–                                          
TextBox                        System.Windows.Controls.TextBox                
Window                         System.Windows.Window
Error                          {} 

Now if you think that you can read the control for all of its information, you will be partially right.

$syncHash.Window

image

Not exactly the information you would want to be seeing while looking at the Window control. The reason for this is that we do not have ownership of this control in our current runspace. The owner of this control is in another thread.

For further proof, lets try to push some simple text to the textbox and see what happens.

$syncHash.TextBox.Text = 'test'
PS C:\Users\Administrator> $syncHash.TextBox.Text = ‘test’
Exception setting “Text”: “The calling thread cannot access this object
because a different thread owns it.”
At line:1 char:1
+ $syncHash.TextBox.Text = ‘test’
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
   + CategoryInfo          : NotSpecified: (:) [], SetValueInvocationExceptio
  n
   + FullyQualifiedErrorId : ExceptionWhenSetting

So how does on get around this issue? The answer lies within the Dispatcher object for each control. This object allows you to access the object while it is in a different thread to manipulate it (in this case, push data to the UI). Using the Invoke() method of the dispatcher object and supplying 2 parameters (Callback, DispatcherPriority). The callback is a delegate that you can assign a script block that contains the code you want to run against the UI through the dispatcher. The Dispatcher Priority tell what the priority of the operation should be when the Dispatcher is used. In most cases, using ‘Normal’ should be enough. The table below lists the possible priorities that can be used.

Description

Member name

Invalid

The enumeration value is -1. This is an invalid priority.

Inactive

The enumeration value is 0. Operations are not processed.

SystemIdle

The enumeration value is 1. Operations are processed when the system is idle.

ApplicationIdle

The enumeration value is 2. Operations are processed when the application is idle.

ContextIdle

The enumeration value is 3. Operations are processed after background operations have completed.

Background

The enumeration value is 4. Operations are processed after all other non-idle operations are completed.

Input

The enumeration value is 5. Operations are processed at the same priority as input.

Loaded

The enumeration value is 6. Operations are processed when layout and render has finished but just before items at input priority are serviced. Specifically this is used when raising the Loaded event.

Render

The enumeration value is 7. Operations processed at the same priority as rendering.

DataBind

The enumeration value is 8. Operations are processed at the same priority as data binding.

Normal

The enumeration value is 9. Operations are processed at normal priority. This is the typical application priority.

Send

The enumeration value is 10. Operations are processed before other asynchronous operations. This is the highest priority.

Ok, enough talk about this and lets finally see this in action! For my example, I am going to simply change the background color of the Window to Black while the UI is open and from a different runspace.

$syncHash.Window.Dispatcher.invoke(
    [action]{$syncHash.Window.Background='Black'},
    "Normal"
)

image

And there you have it! Easily modify the UI from another runspace with no effort at all!

Function to Push Data to UI in a Different Runspace

Function Update-Window {
    Param (
        $Title,
        $Content,
        [switch]$AppendContent
    )
    $syncHash.textbox.Dispatcher.invoke([action]{
        $syncHash.Window.Title = $title
        If ($PSBoundParameters['AppendContent']) {
            $syncHash.TextBox.AppendText($Content)
        } Else {
            $syncHash.TextBox.Text = $Content
        }
    },
    "Normal")
}

This function is just a simple function that I can use to append or overwrite whatever happens to the in the text box. It uses the same method that I showed above with changing the Window’s background color, but instead of using the Window’s dispatcher, I am now using the Textbox’s dispatcher object to perform the operations.

Manipulate the UI From a Different Runspace

Update-Window -Title ("Services on {0}" -f $Env:Computername) `
              -Content (Get-Service | Sort Status -Desc| out-string)

image

Very cool! I was able to run Get-Service from my ISE and take the output from the cmdlet and write it to my textbox. Given, this is a very simple demo, but you can make this as complex as needed for UIs that have more moving parts. You could even use this as a debugging method to write out all Verbose and Debug statements from your scripts or functions out to another window if you wanted to.

Summary

So I have now shown you how you can share data between runspaces that can make building UIs much simpler, especially when it comes to long running jobs. Using a synchronized hash table and then supplying that to another runspace gets you setup for working between runspaces and then using the Dispatcher object on each control gets you ability to actually write to the controls. Personally, I think this is a huge leap forward for writing UIs as it now allows you to off load operations to another runspace while keeping your main window free to move or do whatever you want with it. I am very much looking forward to expanding on this and seeing what else I can do.

In my case, this is the next step in evolution for both of my projects: PoshPAIG and PoshChat that will rely less on a timer object to help handle data and message traffic to being able to directly write to a control from another runspace. I hope to see what you will do with this in your UI builds in the future!

This entry was posted in powershell, WPF and tagged , , , , , , . Bookmark the permalink.

54 Responses to PowerShell and WPF: Writing Data to a UI From a Different Runspace

  1. oddarneroll says:

    Can anybody help me, i want to add a simple progress bar to the gui and update the progressbar from another runspace.
    I dont understand how to create new gui stuff.
    Could anybody take the sample script and add a updatable progressbar under the textbox so i understand how to go about this?
    Thanks for sharing the script.

  2. Pingback: Please assist me in wrapping my head around Runspaces and Hashtables - How to Code .NET

  3. JTT says:

    Soo… Old post but does anyone know how hide/minimize a form running in another runspace triggered via a button.

    I need the close/maximize buttons disabled or removed. I can disable maximize but disabling close via standard methods means that I can’t close the form anymore. Update-Window Window Close | Out-Null fails.

    So I went ahead and created a button and added a click event to it.
    Some of these work well at minimizing the form, but then the form crashes and refuses to work. Once the form is minimized or hidden it stops working and any attempt at update-window or direct Invoke access causes ISE to hang.

  4. JL says:

    AMAZING! Great technique and information. Thank you very much.

  5. espo says:

    I have been struggling with passing the value from a text-box on my WPF GUI to be used in a function and return results back to the GUI. With help of this post and others I have finally got that to work. a new hurdle i am introducing is the ability to use “-Credential” in one of those functions. We have some tasks that require the use of an account with elevated privileges. I can get that working in a function in a script by itself, but not with the multiple runspaces. Any insight on how to have PS bring up that Credential prompt?

  6. Daniel Petcher says:

    I read your article over on the Hey, Scripting Guy blog as well as this one, and I notice that neither article mentions Windows Workflow Foundation as a way to run multiple tasks in the background. Have you compared the performance of this toolset to Runspaces?

  7. Pingback: XAML code in powershell doesn't run? | Mentalist Nuno

  8. Jim says:

    Hello, If this question was asked and answered I apologize.

    I have a Powershell WPF script – loading WPF objects. I followed your example above and see you use the following code to load the objects:

    $syncHash.TextBox = $syncHash.window.FindName(“textbox”)

    I’ve tried loading the objects – SyncHash.Button = (I have a good bit of buttons).

    For example:
    $syncHash.TextBox = $syncHash.window.FindName(“txtHostName”)
    $syncHash.Button = $syncHash.window.FindName(“btnGO”)
    $syncHash.Button = $syncHash.window.FindName(“btnTaskScheduler”)

    But it only detects .Textbox and the btnTaskScheduler button.

    I’ve confirmed that the script is running in a separate thread as I’ve gotten the same errors indicated above in your post and was able to change the background.

    I would prefer to load all WPF objects such as:

    $inputXML = $inputXML -replace ‘mc:Ignorable=”d”‘,” -replace “x:N”,’N’ -replace ‘^<Win.*’, ‘<Window’

    [xml]$XAML = $inputXML
    #===========================================================================

    Read XAML

    #===========================================================================
    $reader=(New-Object System.Xml.XmlNodeReader $xaml)
    try{$Form=[Windows.Markup.XamlReader]::Load( $reader )}
    catch{Write-Information -Verbose “Unable to load Windows.Markup.XamlReader. Double-check syntax and ensure .net is installed.”}

    #===========================================================================

    Store Form Objects In PowerShell, Functions: Connect to Controls

    #===========================================================================
    $xaml.SelectNodes(“//*[@Name]”) | ForEach-Object{Set-Variable -Name “WPF$($_.Name)” -Value $Form.FindName($_.Name)}

    Your help is greatly appreciated.

    Thanks!

    • Jim says:

      New information per the above… so I was able to figure out that I am able to add more objects by naming them uniquely. For example, instead of .Button, .Button .Button for each button — I name each button different – this loads up fine and I can use Dispatcher to change what I need.

      I would still like to figure out a way to to use the above code inside the run space.

      With the above code the named variables are for example:

      $WPFtxtHostName.Focus();
      $WPFbtnGO.Add_Click({})
      $WPFbtnRemoteAssistance.Add_Click({})
      $WPFbtnRemoteDesktop.Add_Click({})
      $WPFbtnServices.Add_Click({})
      $WPFbtnRegistry.Add_Click({})
      $WPFbtnEventViewer.Add_Click({})
      $WPFbtnCompMgmnt.Add_Click({})
      $WPFbtnTaskScheduler.Add_Click({})
      $WPFbtnUncPath.Add_Click({})

      Inside the brackets I have the functions that each of these buttons call to perform the associated processes.

      • Jim says:

        More regarding the above information…

        I was able to get the variable names listed above and then the values for them associated to the correct controls.

        $MultiTool_xaml.SelectNodes(“//*[@Name]”)| ForEach-Object{$MultiTool_SyncHash.”WPF$($_.Name)” = $MultiTool_SyncHash.Window.FindName($_.Name)}

  9. Pingback: WinForms, Runspaces, and Functions – oh my! – EphingAdmin

  10. T. Seidel says:

    Hello Boe,
    I would like to run in WindowsPE 5.0 a script with a gui. I’ve tested it for your reference. but unfortunately I can not use the Invoke () method of the object dispatcher.

    Unbenannt1.ps1 -> is your code from “Display the UI”
    Unbenannt2.ps1 -> is your code from “Function to Push Data to UI in a Different Runspace”

    PS N:> get-host

    Name : ConsoleHost
    Version : 4.0
    InstanceId : 99ba10db-cabc-453d-b8dc-8734b8c14b4a
    UI : System.Management.Automation.Internal.Host.InternalHostUserI
    nterface
    CurrentCulture : de-DE
    CurrentUICulture : de-DE
    PrivateData : Microsoft.PowerShell.ConsoleHost+ConsoleColorProxy
    IsRunspacePushed : False
    Runspace : System.Management.Automation.Runspaces.LocalRunspace

    PS N:> .\Unbenannt1.ps1
    PS N:> $syncHash.TextBox.Text = “test”
    The property ‘Text’ cannot be found on this object. Verify that the property
    exists and can be set.
    At line:1 char:1
    + $syncHash.TextBox.Text = “test”
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : InvalidOperation: (:) [], RuntimeException
    + FullyQualifiedErrorId : PropertyNotFound

    PS N:> .\Unbenannt2.ps1
    You cannot call a method on a null-valued expression.
    At N:\Unbenannt2.ps1:8 char:5
    + $syncHash.textbox.Dispatcher.invoke([action]{
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : InvalidOperation: (:) [], RuntimeException
    + FullyQualifiedErrorId : InvokeMethodOnNull

    PS N:>

    In Windows 8.1 ISE it works locally.
    Does anyone have any idea why it does not work in WindowsPE?

  11. Pingback: PowerShell Tip: Utilizing Runspaces for Responsive WPF GUI Applications | smsagent

  12. dezed99 says:

    Hi Boe

    I’m trying to use the sychronized hashtable in powershelll version 2 but im getting this error.

    $test = [hashtable]::Synchronized(@{})
    Object reference not set to an instance of an object.
    + CategoryInfo : NotSpecified: (:) [format-default], NullReferenceException
    + FullyQualifiedErrorId : System.NullReferenceException,Microsoft.PowerShell.Commands.FormatDefaultCommand

    Do you know if there is any workaround to this ?

    Thanks

    /Mikael

  13. Private says:

    Very nice! Lots of useful information here!

    This is working great from the PowerShell ISE but I can’t get it to work when launched from the console (“Run with PowerShell” in Windows). Any ideas?

  14. Pingback: Adding a Multithreaded DataGridView to a Powershell form | Sys Admin Jam

  15. mdedeboer says:

    Hi Boe,

    I noticed that you are starting with simply a script and creating your window inside of the synched hashtable. Now suppose you already have a form and you want to sync only a datagridview within that hash table. Is this do-able, or do you need to synchronize the entire form?

    For Example:

     
    #This code runs in the main runspace
    $hashDGProc = [hashtable]::Synchronized(@{ })
    $hashDGProc.DGRunningProcs = New-Object System.Windows.Forms.DataGridView
    $tabProcess.Controls.Add($hashDGProc.dgRunningProcs) #note that $tabProcess is not synched
    

    I then insert a bunch of rows into the grid view from another runspace…(this also freezes if I add enough rows so that it scrolls)

    #Then when I detect that the 2nd runspace is complete, I run this:
    $hashDGProc.dgRunningProcs.Dispatcher.Invoke([action]{ $Global:return = $hashDGProc.dgRunningProcs.Rows.Count }, "Normal")
    			$rowCount =  $Global:return
    

    In the end, the line $hashDGProc.dgRunningProcs.Dispatcher.Invoke([action] returns :
    ERROR: You cannot call a method on a null-valued expression.

    Am I doomed to synching the entire form?….my form is nearly 3000 lines of code…(don’t ask…its big)

    -Matthew

    • Boe Prox says:

      Hi Matthew.
      Just to make sure, but are you adding that synchronized collection into your runspace? That would seem like a reason why you would be getting that message back. I would have to test a little to see about sending a control from a whole form across a runspace and in a synced collection and then calling the Dispatcher.Invoke() method to update it.

      • mdedeboer says:

        Yes, the synchronized collection is passed as a parameter to the second runspace…I have pulled this code an plunked into a new form so I can give you a more complete code snippet:

        $frmMain_Load={
        	#TODO: Initialize Form Controls here
        	$hashDGProc = [hashtable]::Synchronized(@{ })
        	$hashDGProc.DGRunningProcs = New-Object System.Windows.Forms.DataGridView
        	$hashDGProc.dgRunningProcs.AutoSizeColumnsMode = "None"
        	$hashDGProc.dgRunningProcs.AutoSizeRowsMode = "None"
        	$hashDGProc.dgRunningProcs.Dock = "None"
        	$hashDGProc.dgRunningProcs.Location = New-Object System.Drawing.Point(18, 17)
        	$hashDGProc.dgRunningProcs.Margin = New-Object System.Windows.Forms.Padding(3, 3, 3, 3)
        	$hashDGProc.dgRunningProcs.ScrollBars = 'Both'
        	$hashDGProc.dgRunningProcs.ScrollBars = 'Both'
        	$hashDGProc.dgRunningProcs.Size = New-Object Drawing.Size(622, 309)
        	$hashDGProc.dgRunningProcs.ColumnCount = 4
        	$hashDGProc.dgRunningProcs.Columns[0].HeaderText = "Process ID"
        	$hashDGProc.dgRunningProcs.Columns[1].HeaderText = "Name"
        	$hashDGProc.dgRunningProcs.Columns[2].HeaderText = "Username"
        	$hashDGProc.dgRunningProcs.Columns[3].HeaderText = "CreationDate"
        	$hashDGProc.dgRunningProcs.Rows.Add("1", "ProcessName", "Username", (Get-Date))
        	$frmMain.Controls.Add($hashDGProc.dgRunningProcs)
        	
        	$strComputer = "1s8qps1"
        	
        	$sbProcScript = {
        		Param ($hashDGProc, $strComputer)
        		
        		#Get the Current Running Processes
        		$arrProc = Invoke-Command -ComputerName $strComputer -ScriptBlock {
        			Function WMIDateStringToDate($crdate)
        			{
        				If ($crdate -match ".\d*-\d*")
        				{
        					$crdate = $crdate -replace $matches[0], " "
        					$idate = [System.Int64]$crdate
        					$date = [DateTime]::ParseExact($idate, 'yyyyMMddHHmmss', $null)
        					return $date
        				}
        			}
        			gwmi -class win32_process | ForEach{
        				#Add the owner as a note property
        				Try { $objProcess = Add-Member -InputObject $_ -MemberType NoteProperty -Name UserName -Value ($_.GetOwner().User) -PassThru }
        				Catch [Exception] { $objProcess = Add-Member -InputObject $_ -MemberType NoteProperty -Name UserName -Value "" }
        				Finally { }
        				
        				#Add the reformated date as a note property
        				Try { Add-Member -InputObject $objProcess -MemberType NoteProperty -Name refCreationDate -Value (WMIDateStringToDate($_.CreationDate)) -PassThru }
        				Catch [Exception] { Add-Member -InputObject $objProcess -MemberType NoteProperty -Name refCreationDate -Value "" }
        				Finally { }
        				
        			} | Select-Object ProcessID, Name, Username, refCreationDate
        			
        		} -ErrorAction Stop
        		ForEach ($objProcess in $arrProc)
        		{
        			$hashDGProc.dgRunningProcs.Rows.Add($objProcess.ProcessID, $objProcess.Name, $objProcess.Username, $objProcess.refCreationDate)
        			#$hashDGProc.dgRunningProcs.Rows.Add($objProcess.ProcessID, $objProcess.Name, $arrProc.count, $objProcess.refCreationDate)
        			#Break
        		}
        		
        	}
        	
        	$sbProcComplete = {
        		If ($tabControl1.SelectedTab -eq $tabProcess)
        		{
        			#fnProcScriptBlocks
        			$MaxWaitCycles = 5
        			while (($SyncHash.dgRunningProcs.IsInitialized -eq $Null) -and ($MaxWaitCycles -gt 0))
        			{
        				Start-Sleep -Milliseconds 200
        				$MaxWaitCycles–-
        			}
        			$hashDGProc.dgRunningProcs.Dispatcher.Invoke([action]{ $Global:return = $hashDGProc.dgRunningProcs.Rows.Count }, "Normal")
        			Write-Host $Global:Return
        		}
        	}
        	
        	If (!($runspacepool))
        	{
        		$Script:runspaces = New-Object System.Collections.ArrayList
        		$sessionstate = [system.management.automation.runspaces.initialsessionstate]::CreateDefault()
        		$runspacepool = [runspacefactory]::CreateRunspacePool(1, 10, $sessionstate, $host)
        		$runspacepool.Open()
        	}
        	
        	$powershellRunSpace = [powershell]::Create()
        	$powershellRunSpace.AddScript($sbProcScript).AddArgument($hashDGProc).AddArgument($strComputer)
        	$powershellRunSpace.RunspacePool = $runspacepool
        	
        	$InstRunSpace = "" | Select-Object name, powershell, runspace, Computer, CompletedScript
        	$InstRunSpace.Name = (Get-Random)
        	
        	$InstRunSpace.Computer = $strComputer
        	$instRunSpace.Powershell = $powershellRunSpace
        	$InstRunSpace.CompletedScript = $sbProcComplete
        	$InstRunSpace.RunSpace = $powershellRunSpace.BeginInvoke()
        	$runspaces.Add($InstRunSpace) | Out-Null
        	
        	
        	If (!($timerCheckRunSpaceFinished.Enabled))
        	{
        		$timerCheckRunSpaceFinished.Enabled = $true
        		$timerCheckRunSpaceFinished.Start()
        	}
        }
        
        Function fnGet-RunspaceData
        {
        	$more = $false
        	Foreach ($runspace in $runspaces)
        	{
        		If ($runspace.Runspace.isCompleted)
        		{
        			$runspace.powershell.EndInvoke($runspace.Runspace)
        			$runspace.powershell.dispose()
        			$runspace.Runspace = $null
        			$runspace.powershell = $null
        			If ($runspace.CompletedScript)
        			{
        				& $runspace.completedScript
        			}
        		}
        		ElseIf ($runspace.Runspace -ne $null)
        		{
        			$more = $true
        		}
        	}
        	#Clean out unused runspace jobs
        	$temphash = $runspaces.clone()
        	$temphash | Where-Object -Property Runspace -eq $Null | ForEach-Object{ $Runspaces.remove($_) }
        }
        
        $timerCheckRunSpaceFinished_Tick={
        	#TODO: Place custom script here
        	fnGet-RunspaceData
        }
        

        Basically when my main form loads, it creates the hash and datagridview. This gets passed to the new runspace, and the gridview gets populated….I then have a timer that checks if the runspace is complete so I can take more actions later. The two problems I have with this code is:

        1. When I get more processes returned than fit in the datagridview it begins to scroll…this freezes the entire form.

        2. The line: $hashDGProc.dgRunningProcs.Dispatcher.Invoke([action]{ $Global:return = $hashDGProc.dgRunningProcs.Rows.Count }, “Normal”)
          returns “ERROR: You cannot call a method on a null-valued expression.”

        • mdedeboer says:

          Hi Boe,

          I got it figured out 🙂

          Because line 71 was in the same thread I created the hash table, I changed the code to:

          			$procCount = $hashDGProc.dgRunningProcs.Rows.Count
          

          Because I am using a form object instead of a Window, I changed lines 52-57 to:

          	$hashDGProc.DGRunningProcs.Invoke([action]{
          		ForEach ($objProcess in $arrProc)
          	       { 								                      $hashDGProc.dgRunningProcs.Rows.Add($objProcess.ProcessID, $objProcess.Name, $objProcess.Username, $objProcess.refCreationDate)
          		}
          	})
          

          Notice I am not using a dispatcher, but directly calling the invoke command. Not sure if this is the proper way to do this, but it does work. The result, is the form loads quite quickly, but is unusable for a split second while I am adding the rows if there is quite a few processes running (say upwards of 150+)

          Here is my updated code:

          
          $frmMain_Load={
          	#TODO: Initialize Form Controls here
          	$hashDGProc = [hashtable]::Synchronized(@{ })
          	$hashDGProc.DGRunningProcs = New-Object System.Windows.Forms.DataGridView
          	$hashDGProc.dgRunningProcs.AutoSizeColumnsMode = "None"
          	$hashDGProc.dgRunningProcs.AutoSizeRowsMode = "None"
          	$hashDGProc.dgRunningProcs.Dock = "None"
          	$hashDGProc.dgRunningProcs.Location = New-Object System.Drawing.Point(18, 17)
          	$hashDGProc.dgRunningProcs.Margin = New-Object System.Windows.Forms.Padding(3, 3, 3, 3)
          	$hashDGProc.dgRunningProcs.ScrollBars = 'Both'
          	$hashDGProc.dgRunningProcs.ScrollBars = 'Both'
          	$hashDGProc.dgRunningProcs.Size = New-Object Drawing.Size(622, 309)
          	$hashDGProc.dgRunningProcs.ColumnCount = 4
          	$hashDGProc.dgRunningProcs.Columns[0].HeaderText = "Process ID"
          	$hashDGProc.dgRunningProcs.Columns[1].HeaderText = "Name"
          	$hashDGProc.dgRunningProcs.Columns[2].HeaderText = "Username"
          	$hashDGProc.dgRunningProcs.Columns[3].HeaderText = "CreationDate"
          	$hashDGProc.dgRunningProcs.Rows.Add("1", "ProcessName", "Username", (Get-Date))
          	$frmMain.Controls.Add($hashDGProc.dgRunningProcs)
          	
          	$strComputer = "1s8qps1"
          	
          	$sbProcScript = {
          		Param ($hashDGProc, $strComputer)
          		
          		#Get the Current Running Processes
          		$arrProc = Invoke-Command -ComputerName $strComputer -ScriptBlock {
          			Function WMIDateStringToDate($crdate)
          			{
          				If ($crdate -match ".\d*-\d*")
          				{
          					$crdate = $crdate -replace $matches[0], " "
          					$idate = [System.Int64]$crdate
          					$date = [DateTime]::ParseExact($idate, 'yyyyMMddHHmmss', $null)
          					return $date
          				}
          			}
          			gwmi -class win32_process | <#Select-Object -First 5 |#> ForEach{
          				#Add the owner as a note property
          				Try { $objProcess = Add-Member -InputObject $_ -MemberType NoteProperty -Name UserName -Value ($_.GetOwner().User) -PassThru }
          				Catch [Exception] { $objProcess = Add-Member -InputObject $_ -MemberType NoteProperty -Name UserName -Value "" }
          				Finally { }
          				
          				#Add the reformated date as a note property
          				Try { Add-Member -InputObject $objProcess -MemberType NoteProperty -Name refCreationDate -Value (WMIDateStringToDate($_.CreationDate)) -PassThru }
          				Catch [Exception] { Add-Member -InputObject $objProcess -MemberType NoteProperty -Name refCreationDate -Value "" }
          				Finally { }
          				
          			} | Select-Object ProcessID, Name, Username, refCreationDate
          			
          		} -ErrorAction Stop
          		
          
          				$hashDGProc.DGRunningProcs.Invoke([action]{
          					ForEach ($objProcess in $arrProc)
          					{
          						$hashDGProc.dgRunningProcs.Rows.Add($objProcess.ProcessID, $objProcess.Name, $objProcess.Username, $objProcess.refCreationDate)
          					}
          				})
          			
          		
          	}
          	
          	$sbProcComplete = {
          		If ($tabControl1.SelectedTab -eq $tabProcess)
          		{
          			
          			$MaxWaitCycles = 5
          			while (($SyncHash.dgRunningProcs.IsInitialized -eq $Null) -and ($MaxWaitCycles -gt 0))
          			{
          				Start-Sleep -Milliseconds 200
          				$MaxWaitCycles–-
          			}
          			If ($hashDGProc.DGRunningProcs.InvokeRequired)
          			{
          				Write-Host "Invoke required"
          				$hashDGProc.dgRunningProcs.Dispatcher.Invoke([action]{ $Global:return = $hashDGProc.dgRunningProcs.Rows.Count }, "Normal")
          			}
          			else
          			{
          				Write-Host "Invoke not required"
          				Write-Host ($hashDGProc.dgRunningProcs.Rows.Count)
          			}
          		}
          	}
          	
          	
          	If (!($runspacepool))
          	{
          		$Script:runspaces = New-Object System.Collections.ArrayList
          		$sessionstate = [system.management.automation.runspaces.initialsessionstate]::CreateDefault()
          		$runspacepool = [runspacefactory]::CreateRunspacePool(1, 10, $sessionstate, $host)
          		$runspacepool.Open()
          	}
          	
          	$powershellRunSpace = [powershell]::Create()
          	$powershellRunSpace.AddScript($sbProcScript).AddArgument($hashDGProc).AddArgument($strComputer)
          	$powershellRunSpace.RunspacePool = $runspacepool
          	
          	$InstRunSpace = "" | Select-Object name, powershell, runspace, Computer, CompletedScript
          	$InstRunSpace.Name = (Get-Random)
          	
          	$InstRunSpace.Computer = $strComputer
          	$instRunSpace.Powershell = $powershellRunSpace
          	$InstRunSpace.CompletedScript = $sbProcComplete
          	$InstRunSpace.RunSpace = $powershellRunSpace.BeginInvoke()
          	$runspaces.Add($InstRunSpace) | Out-Null
          	
          	
          	If (!($timerCheckRunSpaceFinished.Enabled))
          	{
          		$timerCheckRunSpaceFinished.Enabled = $true
          		$timerCheckRunSpaceFinished.Start()
          	}
          }
          
          Function fnGet-RunspaceData
          {
          	$more = $false
          	Foreach ($runspace in $runspaces)
          	{
          		If ($runspace.Runspace.isCompleted)
          		{
          			$runspace.powershell.EndInvoke($runspace.Runspace)
          			$runspace.powershell.dispose()
          			$runspace.Runspace = $null
          			$runspace.powershell = $null
          			If ($runspace.CompletedScript)
          			{
          				& $runspace.completedScript
          			}
          		}
          		ElseIf ($runspace.Runspace -ne $null)
          		{
          			$more = $true
          		}
          	}
          	#Clean out unused runspace jobs
          	$temphash = $runspaces.clone()
          	$temphash | Where-Object -Property Runspace -eq $Null | ForEach-Object{ $Runspaces.remove($_) }
          }
          
          $timerCheckRunSpaceFinished_Tick={
          	#TODO: Place custom script here
          	fnGet-RunspaceData
          }
          
          • Boe Prox says:

            Nice work! 🙂

            The reason that you can use Invoke() on your control is because you are using Windows Forms (System.Windows.Forms.DataGridView) whereas my examples use Windows Presentation Foundation (WPF) which requires the use of the Dispatcher object to call Invoke().

            Glad that you got it working!

          • mdedeboer says:

            I just added a post on this to my blog: http://sysjam.wordpress.com/

            Thanks a bunch for your help! Its very challenging, but also very fun 🙂

  16. Pingback: WinForms, Runspaces, and Functions – oh my! - EphingAdmin

  17. Alen Williams says:

    I found this to be an invaluable resource in the development of a GUI app I wrote. One thing I’ve found out that other seems to be coming across is it matters which Powershell version you’re running when you called the Dispatcher.invoke command. Here’s some code I implemented for this:

    if($thisIsVersion2){
    $sharedMem.textbox.Dispatcher.invoke(“Normal”,[action]{
    $sharedMem.Window.Title = $title
    $sharedMem.TextBox.AppendText($Content)
    })
    } else {
    $sharedMem.textbox.Dispatcher.invoke([action]{
    $sharedMem.Window.Title = $title
    $sharedMem.TextBox.AppendText($Content)
    },
    “Normal”)
    }
    The action and priority are reversed in Powershell2 & 3, use the wrong one and it hangs… What I’m also seeing though (and I’d love any insight on), is that if a user runs my code after starting powershell.exe they get a different experience than if it’s running as a logon script. Interactive = good, logon script = freeze.

  18. C. L. Jones says:

    Hello Boe,

    Great post thanks for this! I’m experiencing a similar issue as some of the commentors regarding hanging in the UI when attempting to access controls in the syncHash within an event handler. No matter what I seem to do, the UI continues to freeze. For example:

    Add-Type -AssemblyName PresentationFramework,PresentationCore,WindowsBase
    $global:syncHash = [Hashtable]::Synchronized(@{})
    $syncHash.Host = $Host
    $runSpace = [RunspaceFactory]::CreateRunspace()
    $runSpace.ApartmentState,$runSpace.ThreadOptions = “STA”,”ReUseThread”
    $runSpace.Open()
    $runSpace.SessionStateProxy.SetVariable(“syncHash”,$syncHash)
    $cmd = [PowerShell]::Create().AddScript({
    Add-Type -AssemblyName PresentationFramework,PresentationCore,WindowsBase,System.Windows.Controls
    $syncHash.Window = [Windows.Markup.XamlReader]::Parse(@”

    “@)

    $syncHash.btnTest = $global:syncHash.Window.FindName(“btnTest”)
    $syncHash.txtText = $global:syncHash.Window.FindName(“txtTest”)

    $syncHash.btnTest.Add_Click({
    $syncHash.btnTest.IsEnabled = $false
    $syncHash.Host.Runspace.Events.GenerateEvent(“btnTestClicked”, $syncHash.btnTest, $null, $null)
    })

    $syncHash.Window.ShowDialog()
    })
    $cmd.Runspace = $runSpace
    $handle = $cmd.BeginInvoke()

    # THIS CODE FREEZES THE UI
    Register-EngineEvent -SourceIdentifier “btnTestClicked” -Action {
    $global:syncHash.btnTest.Dispatcher.invoke([System.Windows.Threading.DispatcherPriority]::Render, [action]{$global:syncHash.btnTest.IsEnabled = $true}, $null, $null)
    }

    Any thoughts would be greatly appreciated!

    Thanks.

  19. Scriptabit says:

    I am trying to build a proof of concept using this, and have the ability to read/write values to the form, but I am having difficulty with events. Specifically, I can fire and handle events, but the minute I try to reference the $SyncHash from within the event it appears to cause a blocking condition. As a result, I am unable to update the form based on an event being fired by a control. Any thoughts?

    In the example below, the form is loaded and the following steps occur:

    1.) Update-Combobox is called and it populates the combobox with a list of service names and selects the first item.
    2.) update-textbox is called and sets the Text property of the textbox.
    3.) The Text value of the textbox is read by the function read-textbox.
    4.) An event handle is registered for the SelectionChanged event for the combobox to call the update-textbox function used earlier.
    5.) If you change the selection on the combobox, the shell and UI hangs as soon as $SyncHash is referenced. I suspect this is causing some sort of blocking condition due to the synchronized nature of the hash table, but I am unsure as to why / how to work around it.

    $UI_JobScript =
    {
    try{
    Function New-Form ([XML]$XAML_Form){
    $XML_Node_Reader=(New-Object System.Xml.XmlNodeReader $XAML_Form)
    [Windows.Markup.XamlReader]::Load($XML_Node_Reader)
    }
    try{
    Add-Type –AssemblyName PresentationFramework
    Add-Type –AssemblyName PresentationCore
    Add-Type –AssemblyName WindowsBase
    }
    catch{
    Throw “Unable to load the requisite Windows Presentation Foundation assemblies. Please verify that the .NET Framework 3.5 Service Pack 1 or later is installed on this system.”
    }
    $Form = New-Form -XAML_Form $SyncHash.XAML_Form
    $SyncHash.Form = $Form

    $SyncHash.CMB_Services = $SyncHash.Form.FindName(“CMB_Services”)
    $SyncHash.TXT_Output = $SyncHash.Form.FindName(“TXT_Output”)

    $SyncHash.Form.ShowDialog() | Out-Null
    $SyncHash.Error = $Error
    }
    catch{
    write-host $_.Exception.Message
    }
    }
    #End UI_JobScript
    #Begin Main
    add-type -AssemblyName WindowsBase
    [XML]$XAML_Form = @”

    “@
    $SyncHash = [hashtable]::Synchronized(@{})
    $SyncHash.Add(“XAML_Form”,$XAML_Form)
    $SyncHash.Add(“InitialScript”, $InitialScript)
    $Normal = [System.Windows.Threading.DispatcherPriority]::Normal
    $UI_Runspace =[RunspaceFactory]::CreateRunspace()
    $UI_Runspace.ApartmentState = [System.Threading.ApartmentState]::STA
    $UI_Runspace.ThreadOptions = [System.Management.Automation.Runspaces.PSThreadOptions]::ReuseThread
    $UI_Runspace.Open()
    $UI_Runspace.SessionStateProxy.SetVariable(“SyncHash”,$SyncHash)
    $UI_Pipeline = [PowerShell]::Create()
    $UI_Pipeline.Runspace=$UI_Runspace
    $UI_Pipeline.AddScript($UI_JobScript) | out-Null
    $Job = $UI_Pipeline.BeginInvoke()
    $SyncHash.ServiceList = get-service | select name, status | Sort-Object -Property Name
    Function Update-Combobox{
    write-host “`nBegin Update-Combobox [$(get-date)]”
    $SyncHash.CMB_Services.Dispatcher.Invoke($Normal,[action]{$SyncHash.CMB_Services.ItemsSource = $SyncHash.ServiceList})
    $SyncHash.CMB_Services.Dispatcher.Invoke($Normal,[action]{$SyncHash.CMB_Services.SelectedIndex = 0})
    write-host “`End Update-Combobox [$(get-date)]”
    }
    Function Update-Textbox([string]$Value){
    write-host “`nBegin Update-Textbox [$(get-date)]”
    $SyncHash.TXT_Output.Dispatcher.Invoke(“Send”,[action]{$SyncHash.TXT_Output.Text = $Value})
    write-host “End Update-Textbox [$(get-date)]”
    }
    Function Read-Textbox(){
    write-host “`nBegin Read-Textbox [$(get-date)]”
    $SyncHash.TXT_Output.Dispatcher.Invoke($Normal,[action]{$Global:Return = $SyncHash.TXT_Output.Text})
    $Global:Return
    remove-variable -Name Return -scope Global
    write-host “End Read-Textbox [$(get-date)]”
    }
    #Give the form some time to load in the other runspace
    $MaxWaitCycles = 5
    while (($SyncHash.Form.IsInitialized -eq $Null)-and ($MaxWaitCycles -gt 0)){
    Start-Sleep -Milliseconds 200
    $MaxWaitCycles–
    }
    Update-ComboBox
    Update-Textbox -Value $(“Initial Load: $(get-date)”)
    Write-Host “Value Read From Textbox: $(Read-TextBox)”
    Register-ObjectEvent -InputObject $SyncHash.CMB_Services -EventName SelectionChanged -SourceIdentifier “CMB_Services.SelectionChanged” -action {Update-Textbox -Value $(“From Selection Changed Event: $(get-date)”)}

  20. Jamie McDade says:

    After the $psCmd.BeginInvoke() line, I tried adding for a test:

    # The next line fails (null-valued expression)
    $uiHash.OutputTextBox.Dispatcher.Invoke(“Normal”, [action]{
    for ($i = 0; $i -lt 10000; $++) {
    $uiHash.OutputTextBox.AppendText(“hi”)
    }
    })

    When I run $uiHash.Window, I see:

    Name Content
    ——- ———–

    This doesn’t match the example.

    Any ideas?

    • Boe Prox says:

      I saw you also asked this on SO and provided the answer there, but for anyone who comes here and sees your comment, I will also answer here.

      I would not use your For loop within a dispatcher as it will cause issues with the UI. Instead, move the For loop out and use the dispatcher within it instead:

      for ($i = 0; $i -lt 10000; $i++) {
      $uiHash.OutputTextBox.Dispatcher.Invoke(“Normal”, [action]{
      $uiHash.OutputTextBox.AppendText(“hi”)
      })
      }

  21. Pingback: Build a Tool that Uses Constrained PowerShell Endpoint - Hey, Scripting Guy! Blog - Site Home - TechNet Blogs

  22. Jerre says:

    Hi Boe.

    Great article even thou I only understand half of it 🙂

    Question, has this technique that you are describing been superseeded by Powershell Workflows or do they accomplish different things?

    • Boe Prox says:

      Thanks!

      Workflows and using runspaces are 2 different things and what I have written would definitely not be superseded by workflows.

      • Jerre says:

        Ok, thanks for the clarification, so if you were to build a GUI that for example installs the prereqs for configmanager and then configmanager itself to automate the setup, would you start the prereqinstalls and configmanagersetup in background runspaces and update the gui with progress and statusmessages through a synchronized hash table?

        Hoping for design advice here 🙂

        • Boe Prox says:

          Yes, I would do exactly that. I would keep the GUI in one runspace and then spawn additional runspaces for each thing that you need to install. A runspacepool might be a good idea as well because you can set the throttle limit of how many runspaces will be running at a time and then start throwing things into it and let it run.

  23. Pingback: WinForms, Runspaces, and Functions – oh my! - Ephing Admin

  24. Jay says:

    Hello Boe,

    I’m trying to add a ComboBox to a canva but no real data shows in the SyncHash for another thread to read/react to. In fact, so far I could add a RadioButton correctly but neither Listbox nor ComboBox worked.

    Any idea?

    Thanks!

    • Drako says:

      Same issue :

      Script :

      #Load the assembly…
      Add-Type -AssemblyName presentationframework

      # ARRAY OBJET = hash en mode synchro
      $syncHash = [hashtable]::Synchronized(@{})

      # Declaration du RunSpace Indépendant
      $newRunspace = [runspacefactory]::CreateRunspace()
      $newRunspace.ApartmentState = “STA”
      $newRunspace.ThreadOptions = “ReuseThread”
      $newRunspace.Open()
      $newRunspace.SessionStateProxy.SetVariable(“syncHash”,$syncHash)

      # XAML WPF
      $psCmd = [PowerShell]::Create().AddScript({
      [xml]$xaml = @”

      Item 0
      Item 1
      Item 2

      “@

      # Definitions des fields
      $reader=(New-Object System.Xml.XmlNodeReader $xaml)
      $syncHash.Window=[Windows.Markup.XamlReader]::Load( $reader )
      $syncHash.Canvas = $syncHash.Window.FindName(“Canvas”)
      $syncHash.TextBox = $syncHash.Window.FindName(“TextBox”)
      $syncHash.ComboBox = $syncHash.Window.FindName(“ComboBox”)

      $syncHash.Window.ShowDialog()
      $syncHash.Error = $Error
      })

      $psCmd.Runspace = $newRunspace
      $data = $psCmd.BeginInvoke()

      Result when i show content of syncHash :

      PS X:\> $syncHash

      Name Value
      —- —–
      TextBox System.Windows.Controls.TextBox: Initializing…
      ComboBox
      Canvas System.Windows.Controls.Canvas
      Window System.Windows.Window

      The ComboBox is empty.

      Help will be appreciate.
      Thanks

    • Jay says:

      If you have some time to look this up, here’s a small code example which reproduces the problem. pastebin . com / hBZy1AuK
      Posted on pastebin for the lack of formatting here is comments.. 😉

      • Boe Prox says:

        The combobox does in fact appear to be there ($synchash.combobox returns an object). I wonder if it has something to do with it being on another thread which is causing it to prevent you from reading anything else into it. I was able to access the combobox and view the content using the following approach:

        $syncHash.ComboBox.Dispatcher.Invoke(
        “Normal”,
        [action]{ $Global:return = $syncHash.ComboBox.Items | Select -Expand Content }
        ) | Out-Null
        $return

      • Jay says:

        Boe,

        Can’t do more indented reply here, sadly 🙂

        Thanks, this worked.

        Compared to what we had, more extensive code compared to the example, I think the “$Global:return” made the difference. A display of the synchash still shows empty but that’s not what we’re after anyway. As a combobox, selected index is what matters.

        Works now, thanks to your input!

  25. jlo says:

    This was a great tutorial for getting into wpf! Thank you.
    One question, Do you have an idea or process to output the wpf directly to a video instead of the desktop? Im trying to create animations and i cant figure out this last part. ive researched expression and a bunch of other stuff and i cant find anything leading me towards this answer. Thank you again.

  26. Sean says:

    Firstly, this is a great solution and write-up. I’ve run the sample code snippet successfully in PS and PS ISE, but PowerGUI freezes when updating the UI, even with the correct dispatcher.invoke() arguments order. Can anyone confirm this behaviour and whether there is a fix?

  27. Pingback: Episode 224 – Boe Prox talks about PoshWSUS and his other projects | PowerScripting Podcast

  28. jaykul says:

    Can’t resist pointing out that the Show-UI module takes care of all this for you…

  29. Pingback: Building an Xbox Live Widget using PowerShell | Learn Powershell | Achieve More

  30. Stefan says:

    Hi @all,

    important Infomation for all, who receive stuck windows and consoles.

    In my implementation the shared hashtable has different names in the different runspaces. Thus I tried to change a label-content like this (which led to frozen PS and Window):
    $Global:SplashShared.SplashLabel.Dispatcher.invoke(“Normal”,[action] { $Shared.SplashLabel.Content=”new Content” } ).

    Obviously in my UI-runspace the hashtable is called “$Shared” and in my primary shell it’s $SplashShared.

    This is what works:
    $Global:SplashShared.SplashLabel.Dispatcher.invoke(“Normal”,[action] { $Global:SplashShared.SplashLabel.Content=”new Content” } )

    Hope it helps other people as well …

  31. Jay says:

    Boe, great intro to UI with Xaml.
    I’ve ran into a puzzling problem while trying to follow your steps — the graphical runspace freezing whenever I tried to change either the background or pushing text into it.
    While drilling down the options of Dispatcher.Invoke, I noticed that it is expecting the options in another order… So to change the background I needed to put:
    “Normal”, [action]{$syncHash.Window.Background=’Black’}

    Do you have any idea why would this be? I am questioning portability across versions.
    Are you using PowerShell v3 or v2 ?
    Thanks!

    • Boe Prox says:

      Hi Jay,
      No issue with PowerShell versions, that was a case where I forgot to fix the code even after I realized my problem. 🙂 I did have it in the wrong order, which is why it locked up the UI when you attempted to run it. I actually made the same mistake in a different article, but happened to remember to fix it before posting the article. I will go back and update the code tonight so no one else has this issue. Thanks!

      • Jay says:

        Ah! This fixes my sanity check right there! LOL.
        Also, so no one else (like me) spend hours wondering how to *READ* data off the interface, when it is in an isolated Runspace:

        Function Read ([string]$Control) {
        # Read the content of a TextBox, or any control with a compatible
        # “Text” method.
        $syncHash.$Control.Dispatcher.Invoke(
        “Normal”,
        [action]{ $Var = $syncHash.$Control.Text }
        ) | Out-Null
        Return $Var
        }

        $input = Read(“Input1”) # Will get the content of the “Input1” TextBox.

        I don’t know if this is the most optimized/best way to do it, but this is how I could make it work. The $Var assignation was the part I had a hard time to figure, not sure code could be added into the [action] section..
        the “|out-null” is because it generates a $null output so Return send back an Array of {$null, “some test”} instead of the desired “some test” result.

  32. Pingback: PowerShell and WPF: TextBlock | Learn Powershell | Achieve More

  33. Will Steele says:

    Great write up. Covers a lot of important topics in one nice walk through. Excellent exploration of how to deal with GUI’s, multithreading and runspaces. Thanks for putting this out there. It’ll help with the development of better PowerShell forms for the community.

    • Boe Prox says:

      Thanks, Will! This was my goal for putting this article out for everyone. This is something that I had not seen written about from the PowerShell side of the house in regards to updating a UI from a different runspace. I am hoping the community can take this and really put some great stuff together!

Leave a reply to Jay Cancel reply