1

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.

2 Answers 2

2

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.

Sign up to request clarification or add additional context in comments.

1 Comment

Thank you for that clarification. Does running a script via the console vs Powershell ISE make any difference in terms of multi threading? Would your script be written differently if you wanted to deploy it through a console?
1

Whether console or ISE makes no real difference to the actual "multithreading". The difference is in the scoping used by the ISE. Generally, unless you use scope modifiers or Dot sourcing of a child script the parent powershell scope cannot access the child variables but a child scope will inherit any variables of the parent.

Add a variable

$myFirstValue = "The first value"

before the AddScript block and a second variable

$mySecondValue = "The second value" 

within the AddScript block in the example script. Then in the final line of the script add

$Global:uiHash.Window.Dispatcher.Invoke(
    [action]{$Global:uiHash.TextBox.AppendText($myThirdValue)},"Normal")

In the ISE console set the value of

$myThirdValue = "your value"

Do the same in an open powershell console session. Run the script in both the ISE and the powershell console session.

In the ISE after the script is run you will be able to access the value of $myFirstValue but not $mySecondValue and the script will display "your value" in the text box.

In the console session neither $myFirstValue nor $mySecondValue is accessible but "your value" would have been displayed in the text box.

To explain what is happening the child script scope inherits the value of $myThirdValue so it is displayed in all cases. $mySecondValue is clearly in a separate runspace and so is not accessible in any case. $myFirstValue is not accessible by the console session because of the general scoping rule above.

What's going on with the ISE, is it breaking the general rule? Possibly for debugging reasons all panes in the ISE share the same scope. If you open a "new powershell tab" from the file menu a separate runspace is created and the variable will no longer be available.

You can get more help on this using get-help about_scope.

The takeaway from all this is that if you are writing a script spread across multiple files you may need to make use of global/script modifiers to ensure it functions correctly when migrating from the ISE to a console session. I will use the ISE to write my scripts but will run them in a concurrently open console session as it is always possible that the ISE is persisting values that may otherwise not be available to a console session.

I have mentioned this because in the example script you can ( as I think BoeProx intended) remove the last four lines from the script then run the script again, while the window is open you can enter the 4 lines in the console and change the open window. Doing it this way means you no longer need to have the start-sleep because by the time you enter the first line at the keyboard the window is already open (unless you're really really fast).

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.