none
WPF UI running in seperate runspace - able to set/get controls via synchronized hash table, but referencing the control via the hash table from within an event handler causes both runspaces to hang. RRS feed

  • Question

  • I am trying to build a proof of concept where a WPF form is hosted in a seperate runspace and updates are handled from the primary shell/runspace. I have had some success thanks to a great article by Boe Prox, but I am having an issue I wanted to open up to see if anyone had a suggestion.

    My goals are as follows:

    1.) Set control properties from the primary runspace (Completed)
    2.) Get control properties from the primary runspace (Completed)
    3.) Respond to WPF form events in the UI runspace from the primary runspace (Kind of broken).

    I 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 hanging both runspaces. As a result, I am unable to update the form based on an event being fired by a control.

    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 and output using write-host.
    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 from multiple threads trying to access the synchronized nature of the hash table, but I am unsure as to why / how to work around it. If you comment out the line "$SyncHash.TXT_Output.Dispatcher.Invoke("Send",[action]{$SyncHash.TXT_Output.Text = $Value})" within update-textbox the event handler will execute/complete.

    $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 = @"
    <Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
        <Window.Resources>
            <DataTemplate x:Key="DTMPL_Name">
                <TextBlock Text="{Binding Path=Name}" />
            </DataTemplate>
        </Window.Resources>
        <DockPanel LastChildFill="True">
            <StackPanel Orientation="Horizontal" DockPanel.Dock="Top">
                <Label Name="LBL_Services" Content="Services:" />
                <ComboBox Name="CMB_Services" ItemTemplate="{StaticResource DTMPL_Name}"/>
            </StackPanel>
            <TextBox Name="TXT_Output"/>
        </DockPanel>
    </Window>
    "@
    $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)")}

    Friday, April 25, 2014 9:29 PM

Answers

  • The main runspace is probably not accessible to the child runspace. 

    The $form is defined in the main runspace and is not visible from the child.

    I do not think you can do this without creating your own threads.

    Remember that PowerShell is designed to be a safe tool for testing, administration and instrumentation of systems. It is not a development language and has been to some degree disabled for security and simplicity. Some things should only be done with full applications.

    You can create a GUI app and have it host PowerShell.  That would seem to be more normal and easier.


    ¯\_(ツ)_/¯

    • Marked as answer by Scriptabit Monday, May 12, 2014 7:45 PM
    Monday, April 28, 2014 4:35 PM

All replies

  • Yup.  You cannot call across thread or you are in danger of a deadlock.  You must define a delegate to handle the event for you.  Research using delegates in WPF between threads.   I saw a pretty good example about a week ago.


    ¯\_(ツ)_/¯

    Friday, April 25, 2014 10:06 PM
  • Thanks. I will see what I can find regarding delegates in WPF between threads. I do have another question regarding this that hopefully you may have more insight on...

    I am under the impression that the synchronized hash table is thread safe and can be accessed by multiple threads... I have the main runspace and the UI runspace and both can access the hash table. Why does accessing the hash table from the event delegate cause a deadlock issue vs. calling it from the other runspaces? Does the event delegate run in the context of the main or UI runspace?

    I also tested a scenario where the event handler for the form object just fired a new-event call to an event registered in the main runspace, but it would lock as well when the main runspace register-event delegate attempted to access the hash table.

    I guess I'm looking for clarification on what constiutes a runspace/thread and where I am running into the issue.

    Thanks again.

    Monday, April 28, 2014 3:07 PM
  • Runspaces are isolated.  I do not think you can communicate across runspaces.  I saw a supposed example of how to do something similar but do not have the link.

    Thread safe structures have nothing to do with cross thread communication.  I am sure that only means they do no allow simultaneous updates that lose state.  It has nothing to do with deadlock.


    ¯\_(ツ)_/¯


    • Edited by jrv Monday, April 28, 2014 3:31 PM
    Monday, April 28, 2014 3:30 PM
  • Your questions on delegates are good.  I believe that the delegates run in the target runspace but are defined in the current runspace and passed into the target.  The delegate can update variables in the calling thread but runs in the target thread.  It is a way for the compiler/system to do a safe cross thread update.  I believe the only transparent way of true cross thread communication is via async methods.  Sync methods require sync objects.

    You mention "respond to control events in main runspace" .  Depending on what "respond" means I don't see how this would be possible.


    ¯\_(ツ)_/¯

    Monday, April 28, 2014 3:38 PM
  • Thanks again for the responses. This may not be possible, but I thought I would throw it out there. I appreciate your help in looking into this.

    To clarify the "Respond to control events in the main runspace"... I'm would like to have an event generated by a form object in the UI runspace (ex: combo box selectionchanged event) trigger a delegate within the main runspace and have that delegate in the main runspace update the form in the UI runspace.

    ex:

    1.) User changes selection on combo box generating form event

    2.) Event calls delegate (which I have gotten to work)

    3.) Delegate does some basic processing (works)

    4.) Delegate attempts to update form in UI runspace (hangs)

    As to the delegates / which runspace they are running in. I see the $synchash variable if I run get-var within a delegate, but I do not see the $Form variable so I am assuming that they are in the main runspace. Do you agree with that assumption?


    Monday, April 28, 2014 4:00 PM
  • The main runspace is probably not accessible to the child runspace. 

    The $form is defined in the main runspace and is not visible from the child.

    I do not think you can do this without creating your own threads.

    Remember that PowerShell is designed to be a safe tool for testing, administration and instrumentation of systems. It is not a development language and has been to some degree disabled for security and simplicity. Some things should only be done with full applications.

    You can create a GUI app and have it host PowerShell.  That would seem to be more normal and easier.


    ¯\_(ツ)_/¯

    • Marked as answer by Scriptabit Monday, May 12, 2014 7:45 PM
    Monday, April 28, 2014 4:35 PM