PowerShell多运行空间事件传递

5
我一直在寻找一种方法,在不同的runspace之间传递事件,但是没有找到任何方法。以下代码段创建了一个后台runspace,其中显示了一个仅有一个按钮的小窗口。点击该按钮后,它将发布一个事件,主要的runspace应该接收到该事件:
$Global:x = [Hashtable]::Synchronized(@{})
$x.Host = $Host
$Global:rs = [RunspaceFactory]::CreateRunspace()
$rs.ApartmentState,$rs.ThreadOptions = "STA","ReUseThread"
$rs.Open()
$rs.SessionStateProxy.SetVariable("x",$x)
$Global:cmd = [PowerShell]::Create().AddScript(@'
Add-Type -AssemblyName PresentationFramework,PresentationCore,WindowsBase
$x.w = [Windows.Markup.XamlReader]::Parse(@"
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
MaxWidth="800" WindowStartupLocation="CenterScreen" WindowStyle="None" SizeToContent="WidthAndHeight">
<Button Name="test" Content="Starte Installation"/>
</Window>
"@)
$x.test = $x.w.Content.FindName('test')
$x.test.Add_Click( {New-Event -SourceIdentifier "TestClicked" -MessageData "test event"} )
$x.w.ShowDialog()
'@)
$cmd.Runspace = $rs
$null = $cmd.BeginInvoke()
while(!($x.ContainsKey("test"))) {Sleep -Milliseconds 500}
Register-EngineEvent -SourceIdentifier "TestClicked" -Action {$event}

但是那样行不通。我将最后几行改成了这样:
$x.test.Add_Click( {$x.Host.Runspace.Events.GenerateEvent( "TestClicked", $x.test, $null, "test event") } )
$x.w.ShowDialog()
'@)
$cmd.Runspace = $rs
$null = $cmd.BeginInvoke()
Wait-Event -SourceIdentifier "TestClicked"

...这个方法也没有起作用。我猜是因为我不能在子RS中调用父RS中的函数。很奇怪,我遇到过一些情况,Get-Event返回了一些“TestClicked”事件,但是我无法回忆起来或重现...

编辑:显然上述方法有些作用——我又遇到了问题,与一些函数有关。大多数人都知道由Powershell-Blog上的Scripting Guy发布的Show-Control函数。由于我宁愿显示整个GUI而不是单个控件,我对其进行了修改:

Add-Type –assemblyName PresentationFramework,PresentationCore,WindowsBase,"System.Windows.Forms"

<#  Die folgende Funktion zeigt eine GUI an.  Die Informationen über die GUI
    müssen in XAML formuliert sein.  Sie können als String oder als Dateiname
    übergeben werden.
    Die Funktion erlaubt die Übergabe von WindowProperties als Hashtable
    (-> siehe [System.Windows.Window]), von gemeinsamen Objekten in einer syn-
    chronized HashTable und von Ereignissen, die mit den entsprechenden im xaml
    definierten Objekten verbunden werden.
    Der Switch "backgroundrunspace" macht, was sein Name sagt: er öffnet die GUI
    im Hintergrund, sodass das Hauptprogramm weiterlaufen kann.
#>
function Show-Control {
    param(
        [Parameter(Mandatory=$true,ParameterSetName="XamlString",ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
        [string] $xaml,

        [Parameter(Mandatory=$true,ParameterSetName="XamlFile",ValueFromPipeline=$false,ValueFromPipelineByPropertyName=$true)]
        [string] $xamlFile,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [Hashtable] $event,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [Hashtable] $windowProperties,

        # If this switch is set, Show-Control will run the control in the background runspace
        [switch] $backgroundRunspace,

        # To share Variables with the background runspace
        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [Hashtable] $sharedVariables
    )
    Begin
    {   # If it's in a background runspace, create a runspace and populate the runspace with Show-Control.
        if ($backgroundRunspace) {
            $newRunspace =[RunspaceFactory]::CreateRunspace()
            $newRunspace.ApartmentState,$newRunspace.ThreadOptions = "STA","ReuseThread"
            $newRunspace.Open()
            $newRunspace.SessionStateProxy.SetVariable("ParentHost",$Host)
            if ($sharedVariables) {
                $newRunspace.SessionStateProxy.SetVariable("sharedVariables",$sharedVariables)
            }
            $selfDefinition = "function Show-Control { $((Get-Command Show-Control).Definition) }"
            $psCmd = [PowerShell]::Create().AddScript($selfDefinition, $false)
            $psCmd.Runspace = $newRunspace
            $null = $psCmd.Invoke()
        } else {
            $window = New-Object Windows.Window
            $window.SizeToContent = "WidthAndHeight"
            # das Fenster in die sharedVariables aufnehmen
            if ($sharedVariables) {
                $sharedVariables.window=$window
            }
            if ($windowProperties) {
                foreach ($kv in $windowProperties.GetEnumerator()) {
                    $window."$($kv.Key)" = $kv.Value
                }
            }
            $visibleElements = @()
            $windowEvents = @()
        }
    }
    Process
    {   
        if ($backgroundRunspace) { # Invoke the command, using each parameter from commandlineparameters 
            $psCmd  = [Powershell]::Create().AddCommand("Show-Control",$false)
            $null = $psBoundParameters.Remove("BackgroundRunspace")
            $null = $psCmd.AddParameters($psBoundParameters)
<#            foreach ($namedArg in $psBoundParameters.GetEnumerator()) {
                $null = $psCmd.AddParameter($namedArg.Key, $namedArg.Value)                                                    
            }#>
            $psCmd.Runspace = $newRunspace
            $null = $psCmd.BeginInvoke()
        } else {
            # falls eine xaml-datei, dann diese in den xaml-string laden
            if($PSCmdlet.ParameterSetName -eq "xamlFile") {
                $xaml = [string](Get-Content -Encoding UTF8 -ReadCount 0 -Path $xamlFile)
            }
            # XAML parsen und so zu Objekten machen
            $window.Content=([system.windows.markup.xamlreader]::parse($xaml))
            # wir merken uns, ob wir ein Loaded-Event verknüpft haben
            $guiloaded_notadded = $true
            # event-hashtable parsen
            if($event) {
                foreach ($singleEvent in $event.GetEnumerator()) {
                    if ($singleEvent.Key.Contains(".")) {
                        # auseinander nehmen von Objektname und Eventname
                        $targetName = $singleEvent.Key.Split(".")[0].Trim()
                        $eventName = $singleEvent.Key.Split(".")[1].Trim()
                        if ($singleEvent.Key -like "Window.*") {
                            $target = $window
                        } else {
                            $target = $window.Content.FindName($targetName)                   
                        }                       
                    } else {    # kein Objektname -> das Fenster selbst ist das Objekt...
                        $target = $window
                        $eventName = $singleEvent.Key
                    }
                    # Prüfe, ob dieses Objekt auch dieses Event unterstützt, wenn ja: Skriptblock mit dem Event verheiraten
                    if( Get-Member -InputObject $target -MemberType Event -Name $eventName ) {
                        $eventMethod = $target."add_$eventName"
                        if( ($targetName -eq "Window") -and ($eventName -eq "Loaded") -and ($ParentHost)) {
                            $eventScript = [ScriptBlock]::Create( $singleEvent.Value.ToString() + "`n`$null = `$ParentHost.Runspace.Events.GenerateEvent('GUIloaded',$null,$null,$null)" )
                            $eventMethod.Invoke( $ExecutionContext.InvokeCommand.NewScriptBlock($eventScript) )
                            $guiloaded_notadded = $false
                        } else {
                            $eventMethod.Invoke( $ExecutionContext.InvokeCommand.NewScriptBlock($singleEvent.Value) )
                        }
                    }
                }
            }
            # wenn background (können wir hier nur durch Abfragen von "ParentHost" prüfen) und kein "Loaded" event,
            # dann das GUIloaded-event mit dem window.loaded event senden.
            if(($guiloaded_notadded) -and ($ParentHost)) {
                $window.add_Loaded( { 
                    $null = $ParentHost.Runspace.Events.GenerateEvent('GUIloaded',$null,$null,$null)
                } )
            }
            # benannte xaml-Objekte in die sharedVariables bringen...
            if($sharedVariables) {
                $match = [regex]::Matches($xaml,' [x]?[:]?Name="(\w+)"')
                foreach ($m in $match)
                {
                    $name = [string]($m.Groups[1].Value)
                    $sharedVariables.Add($name,$window.Content.FindName($name))
                }
            }
        }
    }
    End
    {
        if ($backgroundRunspace) {
            $newRunspace
        } else {
            $null = $window.ShowDialog()
            $window.Tag
            if($ParentHost) {
                $null = $ParentHost.Runspace.Events.GenerateEvent('WindowClosed',$null,$null,$window.Tag)
            }
        }
    }
}

非常抱歉之前用德语评论了。

现在使用这个函数(它还使用发送“GUIloaded”和“WindowClosed”事件的技术)并在函数调用中使用“GuI-events”,似乎无法从GUI事件中发送事件。就像这样:

Show-Control -xamlfile ($PSScriptRoot+"\WimMounter.xaml") -backgroundRunspace -sharedVariables $ui -event @{
    "Loaded" = {
        $Global:fdlg = New-Object System.Windows.Forms.OpenFileDialog
        $fdlg.CheckFileExists = $true
        $fdlg.Filter = "WIM-Image Files|*.wim"
        $fdlg.Title = "Bitte WIM-Datei auswählen"

        $Global:ddlg = New-Object System.Windows.Forms.FolderBrowserDialog
        $ddlg.Description = "Bitte Verzeichnis zum Mounten des Images auswählen"
        $ui.fn = ""
        $ui.in = ""
        $ui.md = ""
    }
    "selectFile.Click" = {
        if($Global:fdlg.ShowDialog() -eq "OK") {
            $sharedVariables.ImageFile.Text = $fdlg.FileName.Trim()
            $sharedVariables.pl.Content = ("Ausgewählt: `""+$fdlg.FileName.Trim()+"`" - wird untersucht...")
            $sharedVariables.pb.IsIndeterminate = $true
            $sharedVariables.ImageName.Items.Clear()
            $ParentHost.UI.WriteLine("gleich gibbs 'ImageSelected'")
            $ParentHost.Runspace.Events.GenerateEvent("ImageSelected",$null,$null,($fdlg.FileName.Trim()))
        }
    }
}

需要注意的是,$ui是一个全局的SyncHashTable。奇怪的是,那些"$ParentHost.UI.WriteLine()"调用能够在父控制台上工作并产生输出。而"GenerateEvent"调用似乎根本不起作用。既没有Get-Event显示任何事件,也没有通过Register-EngineEvent设置的操作被触发。
这方面有什么想法吗?
2个回答

13

我能够使用以下代码在父级运行空间中接收事件:

$Global:x = [Hashtable]::Synchronized(@{})
$x.Host = $Host
$rs = [RunspaceFactory]::CreateRunspace()
$rs.ApartmentState,$rs.ThreadOptions = "STA","ReUseThread"
$rs.Open()
$rs.SessionStateProxy.SetVariable("x",$x)
$cmd = [PowerShell]::Create().AddScript({
Add-Type -AssemblyName PresentationFramework,PresentationCore,WindowsBase
$x.w = [Windows.Markup.XamlReader]::Parse(@"
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
MaxWidth="800" WindowStartupLocation="CenterScreen" WindowStyle="None" SizeToContent="WidthAndHeight">
<Button Name="test" Content="Starte Installation"/>
</Window>
"@)
$x.test = $x.w.FindName('test')

$x.test.Add_Click({
    $x.Host.Runspace.Events.GenerateEvent( "TestClicked", $x.test, $null, "test event") 
} )  

$x.w.ShowDialog()
})
$cmd.Runspace = $rs
$handle = $cmd.BeginInvoke()
Register-EngineEvent -SourceIdentifier "TestClicked" -Action {$Global:x.host.UI.Write("Event Happened!")}

很奇怪,我已经尝试过这个方法,但结果难以复现。不过,将你的代码片段复制粘贴到ISE中并运行,效果很好。我会在我的当前项目中再试一次(它在WindowsPE内部)。 - exomium
1
顺便说一句,它在Windows PE中也非常好用 - 非常感谢,Boe! - exomium
1
哇,这太美了。谢谢。 - codepoke

2

2
谢谢,这并没有帮助。如果您已经阅读了我的帖子,您会看到我已经使用了SyncHashTable(代码的第一行)。这不是关于交换数据,而是关于在不必主动轮询的情况下交换事件。但还是谢谢您。 - exomium

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接