I've started thinkering around with runspaces in powershell and I was able to do the followings
The issue is one of the buttons (BTN_Exit)...
I would like to achieve that when clicked, it's not only closing the GUI but closes the RunSpace where it runs. I've got to the point where I managed to close the GUI and even to call the close sequence for the Runspace but (I assume) as the called function runs in the RunSpace itself, it just hangs up (RunSpace remains in "closing" state) -Apparently Powershell is unable to kill itself :) Luckily I am still able to dispose it from the main thread with $(Get-Runspace)[-1].Dispose()
(Manually)
I believe I need to connect back to the main thread where the GUI Runspace has been created and close it from there but I am unable to get back there within the Function. Manually if window is open I am able to execute all cmdlets within the close-runspace function and it achieves the desired goal.
I've tried adding $rs.connect($(Get-Runspace).Name -eq "Runspace1")
& $RS.disconnect()
If I try the same thing on the "Handle" or Instance it yields the same result. Neither was I able to call the original function on button click without passing the function to the RunSpace.
How would I get programmatically to the point that when the Button is being clicked it closes the GUI and disposes the Runspace?
Here is the code:
#===[___VARIABLES___]===
$GUI = [hashtable]::Synchronized(@{})
$GUI.Host = $host
#===[___FUNCTIONS___]===
function Close-RunSpace {
if(!($RSI01H.Iscompleted)){
$GUI.BS.Dispatcher.invoke([action]{
$GUI.BS.Close()
})
}
$RSI01.EndInvoke($RSI01H)
$RS.Close()
$RS.Dispose()
}
#===[__RunSpaceCfg__]===
$RS_ISS = [system.management.automation.runspaces.initialsessionstate]::CreateDefault()
$RS_ISS.Commands.Add((New-Object System.Management.Automation.Runspaces.SessionStateFunctionEntry -ArgumentList 'Close-RunSpace', (Get-Content Function:\Close-RunSpace -ErrorAction Stop)))
$RS = [runspacefactory]::CreateRunspace($RS_ISS)
$RS.ApartmentState = "STA"
$RS.ThreadOptions = "ReuseThread"
$RS.Open()
$RS.SessionStateProxy.SetVariable("GUI",$GUI)
$RS.SessionStateProxy.SetVariable("RS",$RS)
$RS.SessionStateProxy.SetVariable("RSI01",$RSI01)
$RS.SessionStateProxy.SetVariable("RSI01H",$RSI01H)
#===[___EXECUTION___]===
$RSI01 = [powershell]::Create().AddScript(
{
[void][System.Reflection.Assembly]::LoadWithPartialName("PresentationCore")
[void][System.Reflection.Assembly]::LoadWithPartialName("PresentationFramework")
[void][System.Reflection.Assembly]::LoadWithPartialName("WindowsBase")
[void][System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
$Def_XAML = DATA {'
<Window x:Name="BS" x:Class="PST.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:PST"
mc:Ignorable="d"
Title="BootStrapR"
Height="450"
Width="800"
ResizeMode="NoResize"
Topmost="True"
FontFamily="Arial"
WindowStartupLocation="CenterScreen"
WindowStyle="None">
<Grid>
<Button x:Name="BTN_Update"
Content="Update"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Margin="109,352,0,0"
Width="75"/>
<Button x:Name="BTN_Exit"
Content="exit"
HorizontalAlignment="Left"
Margin="431,352,0,0"
VerticalAlignment="Top"
Width="75"/>
<ProgressBar x:Name="pb"
HorizontalAlignment="Left"
Height="29"
Margin="10,150,0,0"
VerticalAlignment="Top"
Width="780"
Background="White"
Foreground="Black"
BorderBrush="White"
Value="0" />
<TextBlock x:Name="pstext"
HorizontalAlignment="Left"
Height="266"
Margin="26,22,0,0"
TextWrapping="Wrap"
Text="TextBlock"
VerticalAlignment="Top"
Width="223"/>
</Grid>
</Window>
'}
[xml]$XAML = $Def_XAML -replace 'x:Class=*.*','' `
-replace 'mc:Ignorable="d"','' `
-replace "x:Name",'Name'
$WPF = [Windows.Markup.XamlReader]::Load((New-Object System.Xml.XmlNodeReader $XAML))
$XAML.SelectNodes("//*[@Name]") | ForEach-Object{
$GUI."$($_.Name)" = $WPF.FindName($_.Name)
}
$GUI.Error = $Error
$GUI.BTN_Exit.Add_Click({
#$GUI.BS.Close()
Close-RunSpace
})
$GUI.BTN_Update.Add_Click({
$GUI.pb.Value+=1
})
$GUI.BS.ShowDialog() | Out-Null
}
)
$RSI01.Runspace = $RS
$RSI01.Runspace.Name = "GUI"
$RSI01H = $RSI01.BeginInvoke()
OH I forgot about this post! The solution/workaround I've found is in this article.
Basically the local RunSpace needs to be passed as well to the RunSpace where the GUI is running. (Which I did :) ) And then events can be raised which are calling function/scriptblocks etc. in the main thread. This event trigger then does the housekeeping stuff and cleans/removes the GUI Runspace NOTE: the window itself needs to be closed from within the GUI RunSpace / @ button click otherwise the Runspace won't get into IsComplete state and the called function will hang (wait till RS is completed)
#===[___VARIABLES___]===
$Global:GUI = [hashtable]::Synchronized(@{})
$Global:VAR = [hashtable]::Synchronized(@{})
$VAR.Host = $Host
#===[______XAML_____]===
$XAML_BS = @"
<Window x:Name="BootStrapper" x:Class="SIT_SDM.Window"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:SIT_SDM"
mc:Ignorable="d"
Height="450" Width="800" Background="#FF2F1333" WindowStyle="None">
<Grid>
<Button Name="BS_BTN_X" Content="Close" Margin="600,0,25,0" VerticalAlignment="Top" Width="75"/>
</Grid>
</Window>
"@
#===[___FUNCTIONS___]===
function BS_Close {
begin {
$Global:GUI.BootStrapper.Add_Closing({$_.Cancel = $true})
}
process {
if ($BS_Handle.IsCompleted) {
$BS.EndInvoke($BS_Handle)
$BS.RunSpace.Close()
$BS.RunSpace.Dispose()
} else {
Write-Error -Text "Runspace job not complete!"
}
Unregister-Event -SourceIdentifier "BS_Close"
}
end {
if ((Get-Runspace).count -ge 2) {
$(Get-Runspace)[-1].Dispose()
}
}
}
function Initialize-XAML {
[CmdletBinding()]
Param(
[Parameter()]
[string]$File,
[string]$Variable
)
[void][System.Reflection.Assembly]::LoadWithPartialName("PresentationCore")
[void][System.Reflection.Assembly]::LoadWithPartialName("PresentationFramework")
if (!([string]::IsNullOrEmpty($File))) {
$inputXAML = Get-Content -Path $File -ErrorAction Stop
} elseif (!([string]::IsNullOrEmpty($Variable))) {
$inputXAML = $Variable
} else {
#Write-Error "Neither File nor Variable has been declared"
break
}
#Clean XAML for PowerShell compatibilty
[xml]$XAML = $inputXAML -replace 'x:Class=*.*','' -replace 'mc:Ignorable="d"','' -replace "x:Name",'Name'
#Create the XAML reader using XML node reader
$ReadR = [Windows.Markup.XamlReader]::Load((New-Object System.Xml.XmlNodeReader $XAML))
#Grab named objects from tree and put in a flat structure using Xpath
$NamedNodes = $XAML.SelectNodes("//*[@*[contains(translate(name(.),'n','N'),'Name')]]")
$NamedNodes | ForEach-Object {
$Script:GUI.Add($_.Name, $ReadR.FindName($_.Name))
}
}
#===[___EXECUTION___]===
$InitialSessionState = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault()
$Functions = Get-Command -Module SITSD #Script is being used with import-module
foreach($Function in $Functions){
$functionDefinition = Get-Content function:\$Function
$functionEntry = New-Object System.Management.Automation.Runspaces.SessionStateFunctionEntry -ArgumentList $Function.Name, $functionDefinition
$InitialSessionState.Commands.Add($functionEntry)
}
$RS = [runspacefactory]::CreateRunspace($InitialSessionState)
$RS.ApartmentState = "STA"
$RS.ThreadOptions = "ReuseThread"
$RS.Open()
#Passing variables to Runspace
$RS.SessionStateProxy.SetVariable("GUI",$GUI)
$RS.SessionStateProxy.SetVariable("VAR",$VAR)
$RS.SessionStateProxy.SetVariable("XAML_BS",$XAML_BS)
Register-EngineEvent -SourceIdentifier "BS_Close" -Action {BS_Close}
$BS = [PowerShell]::Create().AddScript({
Initialize-XAML -Variable $XAML_BS
$GUI.BS_BTN_X.Add_Click({
$Global:VAR.host.UI.Write("Button pressed")
$GUI.BootStrapper.Close()
$Global:VAR.Host.Runspace.Events.GenerateEvent( "BS_Close", $GUI.BS_BTN_X, $null, "BS_Close")
})
$GUI.BootStrapper.Showdialog()|Out-Null
})
$BS.RunSpace = $RS
$BS_Handle = $BS.BeginInvoke()