将事件处理程序作为参数传递给对象构造函数

问题描述 投票:0回答:1

请参阅下面的最终工作代码


我有一种在内存中执行批处理脚本的方法,方法是传递命令列表并在新的

Process
中执行它们。我使用这种方法来运行
psql
gpg
命令之类的东西,它非常适合我的用例,这样我就不必在网络上保留带有用户凭据等的随机
.BAT
文件。

唯一的“问题”是,我目前必须维护该方法的多个副本,以应对输出或错误处理程序中我需要的一些相对较小的变化(

.OutputDataReceived
.ErrorDataReceived
)。我想做的基本上是创建一个“
BatchFile
”类,它通过构造函数接受这些事件的自定义
DataReceivedEventHandler

这是我当前每次需要运行批处理文件时复制/粘贴的原始代码:

Private Sub ExecuteBatchInMemory(ByVal Commands As List(Of String), ByVal CurrentUser As NetworkCredential)
    Dim BatchStartInfo As New ProcessStartInfo
    Dim BatchError As String = String.Empty

    With BatchStartInfo
        .FileName = "cmd.exe"
        .WorkingDirectory = Environment.SystemDirectory
        .Domain = CurrentUser.Domain
        .UserName = CurrentUser.UserName
        .Password = CurrentUser.SecurePassword
        .UseShellExecute = False
        .ErrorDialog = False

        .WindowStyle = ProcessWindowStyle.Normal
        .CreateNoWindow = False
        .RedirectStandardOutput = True
        .RedirectStandardError = True

        .RedirectStandardInput = True
    End With

    Using BatchProcess As New Process
        Dim BATExitCode As Integer = 0
        Dim CommandIndex As Integer = 0
        Dim ProcOutput As New Text.StringBuilder
        Dim ProcError As New Text.StringBuilder

        With BatchProcess
            .StartInfo = BatchStartInfo

            Using OutputWaitHandle As New Threading.AutoResetEvent(False)
                Using ErrorWaitHandle As New Threading.AutoResetEvent(False)
                    Dim ProcOutputHandler = Sub(sender As Object, e As DataReceivedEventArgs)
                                                If e.Data Is Nothing Then
                                                    OutputWaitHandle.Set()
                                                Else
                                                    ProcOutput.AppendLine(e.Data)
                                                End If
                                            End Sub

                    '>> This is effectively the DataReceivedEventHandler for
                    '   most of the "batch files" that execute psql.exe
                    Dim ProcErrorHandler = Sub(sender As Object, e As DataReceivedEventArgs)
                                               If e.Data Is Nothing Then
                                                   ErrorWaitHandle.Set()
                                               ElseIf e.Data.ToUpper.Contains("FAILED: ") Then
                                                   ProcError.AppendLine(e.Data)
                                               End If
                                           End Sub

                    AddHandler .OutputDataReceived, ProcOutputHandler
                    AddHandler .ErrorDataReceived, ProcErrorHandler

                    .Start()
                    .BeginOutputReadLine()
                    .BeginErrorReadLine()

                    While Not .HasExited
                        If .Threads.Count >= 1 AndAlso CommandIndex < Commands.Count Then
                            .StandardInput.WriteLine(Commands(Math.Min(System.Threading.Interlocked.Increment(CommandIndex), CommandIndex - 1)))
                        End If
                    End While

                    BATExitCode = .ExitCode
                    BatchError = ProcError.ToString.Trim

                    .WaitForExit()

                    RemoveHandler .OutputDataReceived, ProcOutputHandler
                    RemoveHandler .ErrorDataReceived, ProcErrorHandler
                End Using
            End Using
        End With

        If BATExitCode <> 0 OrElse (BatchError IsNot Nothing AndAlso Not String.IsNullOrEmpty(BatchError.Trim)) Then
            Throw New BatchFileException(BATExitCode, $"An error occurred: {BatchError}")
        End If
    End Using
End Sub

根据我尝试从特定批处理文件的命令行捕获的内容,我将修改

ProcErrorHandler
ProcOutputHandler
以查找
e.Data
中的特定值。在这个特定的示例中,我正在寻找来自 GnuPG (
gpg.exe
) 的错误,这些错误表明文件加密或解密失败。对于
psql
版本,我可能会更改
ProcErrorHandler
来查找
FATAL
或其他内容。

因此,我没有定义

ProcOutputHandler
ProcErrorHandler
与其余代码一致,而是开始使用
BatchFile
类,目前它看起来像这样:

Imports System.Net

Public Class BatchFile
    Implements IDisposable

    Private STDOUTWaitHandle As Threading.AutoResetEvent
    Private STDERRWaitHandle As Threading.AutoResetEvent
    Private Disposed As Boolean
    Private STDOUTHandler As DataReceivedEventHandler
    Private STDERRHandler As DataReceivedEventHandler

    Public Sub New()
        Initialize()
    End Sub

    Public Sub New(ByVal OutputHandler As DataReceivedEventHandler, ByVal ErrorHandler As DataReceivedEventHandler)
        Initialize()
        STDOUTHandler = OutputHandler
        STDERRHandler = ErrorHandler
    End Sub

    Public Sub Execute(ByVal Commands As List(Of String), Optional ByVal User As NetworkCredential = Nothing)
        Dim BatchStartInfo As New ProcessStartInfo
        Dim BatchError As String = String.Empty
        Dim CurrentUser As NetworkCredential = User

        If User Is Nothing Then
            CurrentUser = CredentialCache.DefaultNetworkCredentials
        End If

        With BatchStartInfo
            .FileName = "cmd.exe"
            .WorkingDirectory = Environment.SystemDirectory
            .Domain = CurrentUser.Domain
            .UserName = CurrentUser.UserName
            .Password = CurrentUser.SecurePassword
            .UseShellExecute = False
            .ErrorDialog = False

            .WindowStyle = ProcessWindowStyle.Normal
            .CreateNoWindow = False
            .RedirectStandardOutput = True
            .RedirectStandardError = True

            .RedirectStandardInput = True
        End With

        Using BatchProcess As New Process
            Dim BATExitCode As Integer = 0
            Dim CommandIndex As Integer = 0
            Dim ProcOutput As New Text.StringBuilder
            Dim ProcError As New Text.StringBuilder

            With BatchProcess
                .StartInfo = BatchStartInfo
                .EnableRaisingEvents = True

                AddHandler .OutputDataReceived, STDOUTHandler
                AddHandler .ErrorDataReceived, STDERRHandler
            End With
        End Using
    End Sub

    Private Sub Initialize()
        STDOUTWaitHandle = New Threading.AutoResetEvent(False)
        STDERRWaitHandle = New Threading.AutoResetEvent(False)
    End Sub

    Protected Overridable Sub Dispose(Disposing As Boolean)
        If Not Disposed Then
            If Disposing Then
                If STDOUTWaitHandle IsNot Nothing Then
                    STDOUTWaitHandle.Dispose()
                End If

                If STDERRWaitHandle IsNot Nothing Then
                    STDERRWaitHandle.Dispose()
                End If
            End If
            Disposed = True
        End If
    End Sub

    Public Sub Dispose() Implements IDisposable.Dispose
        Dispose(True)
        GC.SuppressFinalize(Me)
    End Sub
End Class

我遇到的问题是尝试实际创建事件处理程序方法以传递给构造函数以分配给

STDOUTHandler
STDERRHANDLER
。我看过几个不同的例子,包括:

我可能只是很密集,但我似乎无法弄清楚如何实际构建并将处理程序方法从

BatchFile

 类外部传递到构造函数中,因为我没有要分配给 
的值处理程序的 sender
DataReceivedEventArgs
 参数。

我建立了一个简单的方法:

Friend Sub TestHandler(ByVal sender as Object, ByVal e As DataReceivedEventArgs) Console.WriteLine(e.Data) End Sub
但是,当我尝试声明一个新的

BatchFile

时:

Dim testbatch As New BatchFile(TestHandler, TestHandler)
编译器显然会抛出一个错误,表明参数参数未指定。我也尝试过:

Dim testbatch As New BatchFile(DataReceivedEventHandler(AddressOf TestHandler), DataReceivedEventHandler(AddressOf TestHandler))
但这不起作用,因为 

DataReceivedEventHandler

 是一种类型,不能在表达式中使用。我尝试过的其他变体也得到了类似的结果,所以我不确定此时该怎么做。任何帮助或指示将不胜感激。

当然,仍然存在一个“问题”,超出了这个问题的范围,那就是在类外部的处理程序定义中包含

OutputWaitHandle

ErrorWaitHandle
 对象,但我相信我可以弄清楚一旦我将处理程序方法正确传递到我的构造函数中,就会出现这个问题。

vb.net constructor delegates parameter-passing eventhandler
1个回答
0
投票
嗯,看来我

只是太笨了,我相信我刚刚想通了。保留上面的 TestHandler

 方法,看来我要么试图让它变得太简单,要么太复杂。看起来可行的 
BatchFile
 声明如下:

Dim testbatch As New BatchFile(AddressOf TestHandler, AddressOf TestHandler)
我使用一些简单的 GnuPG 解密命令进行了快速测试,一切似乎都完全按照预期进行。我将 STDOUT 的结果打印到控制台,当我故意引入逻辑错误时,它会将 STDERR 的内容打印到控制台。

我意识到这是一个“简单的修复”,但因为我已经经历了尽可能多的迭代,所以我有点犹豫。为了防止其他人经历同样的挫折,我将在这里留下这个问题/答案以及完整的工作代码:


主控制台应用模块

Module BatchCommandTest Sub Main() Dim testbatch As New BatchFile(AddressOf TestHandler, AddressOf TestHandler) testbatch.Execute(New List(Of String) From {"CLS", "C:\GnuPG\gpg.exe --batch --verbose --passphrase <SECRET PASSWORD> --output ""C:\Temp\mytest.pdf"" --decrypt ""C:\Temp\test.pgp""", "EXIT"}, CredentialCache.DefaultNetworkCredentials) End Sub Friend Sub TestHandler(ByVal sender As Object, ByVal e As DataReceivedEventArgs) Console.WriteLine(e.Data) End Sub End Module


BATCHFILE
 班级

Imports System.Net Public Class BatchFile Implements IDisposable Private STDOUTWaitHandle As Threading.AutoResetEvent Private STDERRWaitHandle As Threading.AutoResetEvent Private Disposed As Boolean Private STDOUTHandler As DataReceivedEventHandler Private STDERRHandler As DataReceivedEventHandler Public Sub New() Initialize() End Sub Public Sub New(ByVal OutputHandler As Action(Of Object, DataReceivedEventArgs), ByVal ErrorHandler As Action(Of Object, DataReceivedEventArgs)) Initialize() STDOUTHandler = TryCast(Cast(OutputHandler, GetType(DataReceivedEventHandler)), DataReceivedEventHandler) STDERRHandler = TryCast(Cast(ErrorHandler, GetType(DataReceivedEventHandler)), DataReceivedEventHandler) End Sub Public Sub Execute(ByVal Commands As List(Of String), Optional ByVal User As NetworkCredential = Nothing) Dim BatchStartInfo As New ProcessStartInfo Dim BatchError As String = String.Empty Dim CurrentUser As NetworkCredential = User If User Is Nothing Then CurrentUser = CredentialCache.DefaultNetworkCredentials End If With BatchStartInfo .FileName = "cmd.exe" .WorkingDirectory = Environment.SystemDirectory .Domain = CurrentUser.Domain .UserName = CurrentUser.UserName .Password = CurrentUser.SecurePassword .UseShellExecute = False .ErrorDialog = False .WindowStyle = ProcessWindowStyle.Normal .CreateNoWindow = False .RedirectStandardOutput = True .RedirectStandardError = True .RedirectStandardInput = True End With Using BatchProcess As New Process Dim BATExitCode As Integer = 0 Dim CommandIndex As Integer = 0 Dim ProcOutput As New Text.StringBuilder Dim ProcError As New Text.StringBuilder With BatchProcess .StartInfo = BatchStartInfo AddHandler .OutputDataReceived, STDOUTHandler AddHandler .ErrorDataReceived, STDERRHandler .Start() .BeginOutputReadLine() .BeginErrorReadLine() While Not .HasExited If .Threads.Count >= 1 AndAlso CommandIndex < Commands.Count Then .StandardInput.WriteLine(Commands(Math.Min(System.Threading.Interlocked.Increment(CommandIndex), CommandIndex - 1))) End If End While .WaitForExit() BATExitCode = .ExitCode BatchError = ProcError.ToString.Trim RemoveHandler .OutputDataReceived, STDOUTHandler RemoveHandler .ErrorDataReceived, STDERRHandler If BATExitCode <> 0 OrElse Not (BatchError Is Nothing OrElse String.IsNullOrEmpty(BatchError.Trim)) Then Throw New BatchFileException(BATExitCode, $"An error occurred executing the in-memory batch script: {BatchError}") End If End With End Using End Sub Private Sub Initialize() STDOUTWaitHandle = New Threading.AutoResetEvent(False) STDERRWaitHandle = New Threading.AutoResetEvent(False) End Sub Protected Overridable Sub Dispose(Disposing As Boolean) If Not Disposed Then If Disposing Then If STDOUTWaitHandle IsNot Nothing Then STDOUTWaitHandle.Dispose() End If If STDERRWaitHandle IsNot Nothing Then STDERRWaitHandle.Dispose() End If End If Disposed = True End If End Sub Public Sub Dispose() Implements IDisposable.Dispose Dispose(True) GC.SuppressFinalize(Me) End Sub ' Cast() method from Faithlife Code Blog (https://faithlife.codes/blog/2008/07/casting_delegates/) Function Cast(ByVal source As [Delegate], ByVal type As Type) As [Delegate] If source Is Nothing Then Return Nothing End If Dim delegates As [Delegate]() = source.GetInvocationList() If delegates.Length = 1 Then Return [Delegate].CreateDelegate(type, delegates(0).Target, delegates(0).Method) End If Dim delegatesDest As [Delegate]() = New [Delegate](delegates.Length - 1) {} For nDelegate As Integer = 0 To delegates.Length - 1 delegatesDest(nDelegate) = [Delegate].CreateDelegate(type, delegates(nDelegate).Target, delegates(nDelegate).Method) Next Return [Delegate].Combine(delegatesDest) End Function End Class
您会注意到这个“最终”迭代与我最初发布的内容有几个重要的区别:

  1. Execute()
    类的
    BatchFile
    方法更加“完整”,以匹配原始
    ExecuteBatchInMemory()
    方法的功能
  2. 构造函数现在使用类型
  3. Action(Of Object, DataReceivedEventArgs)
     而不是 
    DataReceivedEventHandler
     类型作为参数。我想我在某个时候做了这个改变,但忘记在其他地方注意到它,所以我想在这里指出它。
  4. 我正在使用 Faithlife 代码博客上的
  5. Casting delegates
     帖子中的 
    Cast() 方法,将 Action(Of Object, DataReceivedEventArgs)
     参数转换为特定的、正确的 
    DataReceivedEventHandler
     类型,以便该方法可以根据需要订阅/取消订阅它.
© www.soinside.com 2019 - 2024. All rights reserved.