I am trying to learn how to use a Runspace to pass values to a GUI. I have been tweaking the script written by Boe Prox, trying to understand how Dispatcher.Invoke works with a runspace and came across a very strange problem
$uiHash = [hashtable]::Synchronized(@{})
$newRunspace =[runspacefactory]::CreateRunspace()
$newRunspace.ApartmentState = "STA"
$newRunspace.ThreadOptions = "ReuseThread"
$newRunspace.Open()
$newRunspace.SessionStateProxy.SetVariable("uiHash",$uiHash)
$psCmd = [PowerShell]::Create().AddScript({
$uiHash.Error = $Error
[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 = "650" Height = "800" ShowInTaskbar = "True">
<TextBox x:Name = "textbox" Height = "400" Width = "600"/>
</Window>
"@
$reader=(New-Object System.Xml.XmlNodeReader $xaml)
$uiHash.Window=[Windows.Markup.XamlReader]::Load( $reader )
$uiHash.TextBox = $uiHash.window.FindName("textbox")
$uiHash.Window.ShowDialog() | Out-Null
})
$psCmd.Runspace = $newRunspace
$handle = $psCmd.BeginInvoke()
#-----------------------------------------
#Using the Dispatcher to send data from another thread to UI thread
Update-Window -Title ("Services on {0}" -f $Env:Computername)
$uiHash.Window.Dispatcher.invoke("Normal",[action]{$uiHash.TextBox.AppendText('test')})
If I were to use the last line of the script without the Update-Window -Title ("Services on {0}" -f $Env:Computername)
I get a you cannot call a method on a null-valued expression.
InvokeMethodOnNull error and the text is not appended. However, if I add Update-Window -Title ("Services on {0}" -f $Env:Computername)
right above the Dispatcher.invoke line, I still get the error, but the textbox contains the appended text.
What is the reason for this occurrence? I have tried so many ways to use the Dispatcher.Invoke to add content to textboxes but always end up with a cannot call a method method on null
error without any success, but now adding some lines that reference the UI and calling the Dispatcher.Invoke seems to make it work.
There are a couple of problems with your code that are probably causing the erratic error. Firstly, are you running the code from the Powershell_ISE or from the powershell console? Also, are you running the script in two parts with the dispatcher calls being made from the console after the window is open or as a single script including the dispatcher calls? If you are running the code as as single script then the problem is that the "BeginInvoke" runs the script within its own runspace in a separate thread. Before the window is properly created by this thread the main thread is already trying to set the value of title and the textbox.
If you were to split the code in two parts, ie call everything up to begininvoke in one script and then make the dispatcher calls in the main script the code will also have problems as you need to make the hashtable global.
I have modified your original code so that it will run in a single script. Notice the additon of the start-sleep to delay the dispatcher calls. The results show the thread id's and the times before and after invoke (in ticks) and you can clearly see that the time after the begin invoke is before the textbox time is set.
$Global:uiHash = [hashtable]::Synchronized(@{})
$newRunspace =[runspacefactory]::CreateRunspace()
$newRunspace.ApartmentState = "STA"
$newRunspace.ThreadOptions = "ReuseThread"
$newRunspace.Open()
$newRunspace.SessionStateProxy.SetVariable("uiHash",$Global:uiHash)
$psCmd = [PowerShell]::Create().AddScript({
$Global:uiHash.Error = $Error
Add-Type -AssemblyName PresentationFramework,PresentationCore,WindowsBase
$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 = "650" Height = "800" ShowInTaskbar = "True">
<Grid>
<TextBox x:Name = "textbox" Height = "400" Width = "600" TextWrapping="Wrap"/>
</Grid>
</Window>
"@
# $reader=(New-Object System.Xml.XmlNodeReader $xaml)
$Global:uiHash.Window=[Windows.Markup.XamlReader]::Parse($xaml )
$Global:uiHash.TextBox = $Global:uiHash.window.FindName("textbox")
$Global:uiHash.TextBox.Text = "Window Creation Thread Id: $([System.Threading.Thread]::CurrentThread.ManagedThreadId.ToString()) Time: $([System.Diagnostics.Stopwatch]::GetTimestamp()) `r`n"
$Global:uiHash.Window.ShowDialog() | out-null
})
$psCmd.Runspace = $newRunspace
$time1 = " Time before beginInvoke: $([System.Diagnostics.Stopwatch]::GetTimestamp()) `r`n"
$handle = $psCmd.BeginInvoke()
#-----------------------------------------
$time2 = " Time after beginInvoke: $([System.Diagnostics.Stopwatch]::GetTimestamp()) `r`n"
#Using the Dispatcher to send data from another thread to UI thread
Start-Sleep -Milliseconds 100
#Update-Window -Title ("Services on {0}" -f $Env:Computername)
$threadId = " Dispatcher Call Thread Id: $([System.Threading.Thread]::CurrentThread.ManagedThreadId.ToString()) Time: $([System.Diagnostics.Stopwatch]::GetTimestamp())`r`n "
$Global:uiHash.Window.Dispatcher.Invoke([action]{$Global:uiHash.TextBox.AppendText($time1)},"Normal")
$Global:uiHash.Window.Dispatcher.Invoke([action]{$Global:uiHash.TextBox.AppendText($time2)},"Normal")
$Global:uiHash.Window.Dispatcher.Invoke([action]{$Global:uiHash.TextBox.AppendText($threadId)},"Normal")
$Global:uiHash.Window.Dispatcher.Invoke([action]{$Global:uiHash.Window.Title = "$($env:ComputerName)"},"Normal")
You may also want to download WPFRunspace, a Powershell module that provides a backgroundworker for WPF/MSForms and ordinary console Powershell scripts.