简体   繁体   English

在Powershell中将Dispatcher Invoke与运行空间一起使用

[英]Using Dispatcher Invoke with a Runspace in Powershell

I am trying to learn how to use a Runspace to pass values to a GUI. 我正在尝试学习如何使用运行空间将值传递给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 我一直在调整Boe Prox编写的脚本,试图了解Dispatcher.Invoke如何与运行空间一起工作,并遇到了一个非常奇怪的问题

 $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. 如果我使用的脚本的最后一行没有Update-Window -Title ("Services on {0}" -f $Env:Computername)我得到的是you cannot call a method on a null-valued expression. InvokeMethodOnNull error and the text is not appended. InvokeMethodOnNull错误,并且未附加文本。 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. 但是,如果我在Dispatcher.invoke行的正上方添加Update-Window -Title ("Services on {0}" -f $Env:Computername) ,我仍然会收到错误消息,但文本框包含附加的文本。

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. 我已经尝试了很多方法来使用Dispatcher.Invoke将内容添加到文本框,但总是以cannot call a method method on null错误cannot call a method method on null而没有成功,但是现在添加了一些引用UI并调用Dispatcher.Invoke的行似乎使它工作。

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? 首先,您是从Powershell_ISE还是从Powershell控制台运行代码? 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. 如果您将代码作为单个脚本运行,则问题是“ BeginInvoke”在单独的线程中在其自己的运行空间中运行脚本。 Before the window is properly created by this thread the main thread is already trying to set the value of title and the textbox. 在此线程正确创建窗口之前,主线程已经在尝试设置title和文本框的值。

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. 如果要将代码分为两部分,即在一个脚本中调用所有内容以进行begininvoke,然后在主脚本中进行调度程序调用,则在使哈希表变为全局时,代码也会遇到问题。

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. 结果显示线程ID以及调用之前和之后的时间(以滴答为单位),您可以清楚地看到开始调用之后的时间是在设置文本框时间之前的时间。

$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. 您可能还希望下载WPFRunspace ,这是一个Powershell模块,为WPF / MSForms和普通控制台Powershell脚本提供了背景工作。

Whether console or ISE makes no real difference to the actual "multithreading". 控制台还是ISE与实际的“多线程”没有任何实质性的区别。 The difference is in the scoping used by the ISE. 区别在于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. 通常,除非使用子脚本的范围修饰符或Dot采购,否则父级Powershell范围无法访问子变量,但子级范围将继承父级的任何变量。

Add a variable 添加一个变量

$myFirstValue = "The first value"

before the AddScript block and a second variable 在AddScript块和第二个变量之前

$mySecondValue = "The second value" 

within the AddScript block in the example script. 在示例脚本的AddScript块中。 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 在ISE控制台中,设置

$myThirdValue = "your value"

Do the same in an open powershell console session. 在打开的Powershell控制台会话中执行相同的操作。 Run the script in both the ISE and the powershell console session. 在ISE和powershell控制台会话中运行脚本。

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. 脚本运行后,在ISE中,您将能够访问$myFirstValue的值,但不能访问$mySecondValue ,脚本将在文本框中显示“您的值”。

In the console session neither $myFirstValue nor $mySecondValue is accessible but "your value" would have been displayed in the text box. 在控制台会话中, $myFirstValue$mySecondValue均不可访问,但“您的值”将显示在文本框中。

To explain what is happening the child script scope inherits the value of $myThirdValue so it is displayed in all cases. 为了解释发生了什么,子脚本作用域继承了$myThirdValue的值,因此在所有情况下都将其显示。 $mySecondValue is clearly in a separate runspace and so is not accessible in any case. $mySecondValue显然位于单独的运行空间中,因此无论如何都无法访问。 $myFirstValue is not accessible by the console session because of the general scoping rule above. 由于上面的常规作用域规则,控制台会话无法访问$myFirstValue

What's going on with the ISE, is it breaking the general rule? ISE发生了什么,是否违反了一般规则? Possibly for debugging reasons all panes in the ISE share the same scope. 可能出于调试原因,ISE中的所有窗格共享同一作用域。 If you open a "new powershell tab" from the file menu a separate runspace is created and the variable will no longer be available. 如果从文件菜单中打开“新的powershell选项卡”,则会创建一个单独的运行空间,并且该变量将不再可用。

You can get more help on this using get-help about_scope . 您可以使用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. 所有这一切的收获是,如果您要编写分布在多个文件中的脚本,则可能需要使用global / script修饰符,以确保从ISE迁移到控制台会话时脚本可以正常运行。 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. 我将使用ISE编写脚本,但是将在并发打开的控制台会话中运行它们,因为ISE始终可能保留控制台会话无法使用的值。

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. 我已经提到了这一点,因为在示例脚本中,您可以(按照我认为BoeProx的意图)从脚本中删除最后四行,然后再次运行该脚本,当窗口打开时,您可以在控制台中输入4行并更改打开窗口。 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). 这样操作意味着您不再需要开始睡眠,因为到键盘上输入第一行时,窗口已经打开(除非您真的非常快)。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM