powershell - 如果我在 powershell 脚本中使用 System.Windows.Forms,则使用 System.Net.WebClient 异步上传文件的 Register-ObjectEvent 将停止工作

标签 powershell winforms asynchronous events runspace

我想在Form上从Internet下载文件,并在ProgressBar中显示下载进度。为此,我订阅加载事件并执行异步加载。一切正常。这是简化的代码(删除了所有不必要的):

Add-Type -assembly System.Windows.Forms

$isDownloaded = $False
$webMain = New-Object System.Net.WebClient

Register-ObjectEvent -InputObject $webMain -EventName 'DownloadFileCompleted' -SourceIdentifier WebMainDownloadFileCompleted -Action {    
    $Global:isDownloaded = $True
}

Register-ObjectEvent -InputObject $webMain -EventName 'DownloadProgressChanged' -SourceIdentifier WebMainDownloadProgressChanged -Action {
    $Global:Data = $event
}

function DownloadFile($Link, $Path, $Name) {

    write-host "begin"

    $Global:webMain.DownloadFileAsync($Link, $Path)
    
    While (!$isDownloaded) {
        $percent = $Global:Data.SourceArgs.ProgressPercentage
        If ($percent -ne $null) {
            write-host $percent
        }
        Wait-Event -Timeout 1
    }

    write-host "end"
}

DownloadFile 'https://www.7-zip.org/a/7z2301-x64.exe' 'D:\7Zip.exe' '7Zip'

一切正常。现在我将其添加到代码中的任何位置

$Form1 = New-Object System.Windows.Forms.Form

$Button1 = 新对象 System.Windows.Forms.Button

并且脚本不起作用。 DownloadProgressChanged 和 DownloadFileCompleted 事件根本不会发生。

问题:为什么仅创建表单或按钮就会干扰脚本?

如果没有 System.Windows.Forms.Form,代码也可以工作,但我最终需要创建一个表单并在其上渲染加载。

DownloadFileAsync 可以工作 - 文件已下载,但在使用任何 New-Object System.Windows.Forms.* 时,Register-ObjectEvent 本身中的事件不会发生(但没有它们也能正常工作)。

最佳答案

正如评论中所述,PowerShell 对于异步编程来说并不是一种很好的语言,问题在于 .ShowDialog() 会阻塞线程并且不允许您的事件正常执行。解决方案是在单独的运行空间中注册事件,下面是如何实现这一点的最小示例(尽可能最小)。我添加了一些指针注释来帮助您理解逻辑,尽管代码显然并不容易,如前所述,PowerShell 不是为此设计的语言,C# 会让您更轻松。

演示:

demo

代码:

Add-Type -Assembly System.Windows.Forms

[System.Windows.Forms.Application]::EnableVisualStyles()

$form = [System.Windows.Forms.Form]@{
    Size            = '500, 150'
    FormBorderStyle = 'Fixed3d'
}
$btn = [System.Windows.Forms.Button]@{
    Name     = 'MyButton'
    Text     = 'Click Me!'
    Size     = '90, 30'
    Location = '370, 70'
    Anchor   = 'Bottom, Right'
}
$btn.Add_Click({
    # disable the button here to allow a single download at a time
    $this.Enabled = $false
    # hardcoded link here for demo
    $downloader.DownloadFileAsync('https://www.7-zip.org/a/7z2301-x64.exe', "$pwd\7Zip.exe")
})
$progress = [System.Windows.Forms.ProgressBar]@{
    Name     = 'MyProgressBar'
    Size     = '460, 40'
    Location = '10, 10'
}
$form.Controls.AddRange(($btn, $progress))

# create a WebClient instance
$downloader = [System.Net.WebClient]::new()
# create a new runspace where the Download Events will execute
# this new runspace will have the PSHost hooked so that things like
# `Write-Host`, `Out-Host`, `Write-Warning`, etc. goes straight to the console
# its easier for troubleshooting but not mandatory
$rs = [runspacefactory]::CreateRunspace($Host)
$rs.Open()
# add the `$form` instance to the runspace scope
$rs.SessionStateProxy.PSVariable.Set([psvariable]::new('form', $form))

# the code that will initiate the events in the new runspace
$ps = [powershell]::Create().AddScript({
    $registerObjectEventSplat = @{
        InputObject      = $args[0] # $args[0] = $downloader
        EventName        = 'DownloadProgressChanged'
        SourceIdentifier = 'WebMainDownloadProgressChanged'
        Action           = {
            # lock the form before doing anything, otherwise there could be a race condition with
            # `DownloadFileCompleted` event
            [System.Threading.Monitor]::Enter($form)
            $progress = $form.Controls.Find('MyProgressBar', $false)[0]
            # for demo, in addition to increment the progress bar,
            # show the percentage to the console
            $eventArgs.ProgressPercentage | Out-Host
            # increment the progress bar
            $progress.Value = $eventArgs.ProgressPercentage
            # release the form
            [System.Threading.Monitor]::Exit($form)
        }
    }
    Register-ObjectEvent @registerObjectEventSplat
    $registerObjectEventSplat['EventName'] = 'DownloadFileCompleted'
    $registerObjectEventSplat['SourceIdentifier'] = 'WebMainDownloadFileCompleted'
    $registerObjectEventSplat['Action'] = {
        [System.Threading.Monitor]::Enter($form)
        # when the download is completed, enable the button
        $form.Controls.Find('MyButton', $false)[0].Enabled = $true
        # and show this to the console
        Write-Host 'Download Completed!'
        [System.Threading.Monitor]::Exit($form)
    }
    Register-ObjectEvent @registerObjectEventSplat
}).AddArgument($downloader)
$ps.Runspace = $rs
$task = $ps.BeginInvoke()
$form.ShowDialog()

关于powershell - 如果我在 powershell 脚本中使用 System.Windows.Forms,则使用 System.Net.WebClient 异步上传文件的 Register-ObjectEvent 将停止工作,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/77418645/

相关文章:

json - 如何将此 JSON 往返传输到 PSObject 并返回到 Powershell

c# - 运行简单的 PowerShell 命令时发生异常 : CLR/System. Management.Automation.Runspaces.PSSnapInException

windows - 访问 Windows 服务器上的特定用户环境变量

c# - 是否可以在 windows 窗体中使用 'sandbox' IE?

winforms - 绘制渐变矩形的有效方法

swift - 在 swift 单例中调用异步函数的正确方法

Powershell - 重新启动并继续脚本

c# - clickonce 下载文件失败

C++ 优先级队列 - 根据更新的优先级重新排序

java - XMemcached中的异步设置