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

Pass event handler to object constructor as an argument

提问人:G_Hosa_Phat 提问时间:9/12/2023 最后编辑:G_Hosa_Phat 更新时间:9/12/2023 访问量:47

问:

我有一种在内存中执行批处理脚本的方法,方法是传递命令列表并在新的 .我使用这种方法来运行诸如 和 命令之类的东西,它非常适合我的用例,这样我就不必使用用户凭据等在网络上保留随机文件。Processpsqlgpg.BAT

唯一的“问题”是,我目前必须为输出或错误处理程序(和)中所需的一些相对较小的变化维护该方法的多个副本。我想做的基本上是创建一个“”类,该类通过构造函数接受这些事件的自定义。.OutputDataReceived.ErrorDataReceivedBatchFileDataReceivedEventHandler

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

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

根据我尝试从特定批处理文件的命令行捕获的内容,我将修改 or 以查找 中的特定值。在这个特定示例中,我正在寻找来自 GnuPG () 的错误,这些错误表明文件的加密或解密失败。对于一个版本,我可能会更改要查找之类的东西。ProcErrorHandlerProcOutputHandlere.Datagpg.exepsqlProcErrorHandlerFATAL

因此,我没有定义与其余代码的 and 内联,而是从该类开始,它目前如下所示:ProcOutputHandlerProcErrorHandlerBatchFile

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

我遇到问题的地方是尝试实际创建事件处理程序方法以传递给构造函数以分配给 和 。我看了几个不同的例子,包括:STDOUTHandlerSTDERRHANDLER

我可能只是很密集,但我似乎无法弄清楚如何实际构建并将处理程序方法从类外部传递到构造函数中,因为我没有要分配给处理程序的 and 参数的值。BatchFilesenderDataReceivedEventArgs

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

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

当然,这仍然有一个“问题”超出了这个问题的范围,那就是在类外部的处理程序定义中包含 and 对象,但我相信一旦我得到处理程序方法正确地传递到我的构造函数中,我就可以解决这个问题。OutputWaitHandleErrorWaitHandle

vb.net 构造函数 委托 参数传递 事件处理程序

评论


答:

1赞 G_Hosa_Phat 9/12/2023 #1

好吧,看来我只是很密集,我相信我只是想通了。从上面看,我似乎要么试图让它太简单,要么太复杂。在阅读了 Microsoft 的 How to: Pass Procedures to Another Procedure in Visual Basic 之后,我意识到我不需要在构造函数调用中将 指定为参数定义的一部分,但我确实需要使用语法来正确分配方法定义。看起来要起作用的声明如下:TestHandlerDataReceivedEventHandlerAddressOfBatchFile

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()BatchFileExecuteBatchInMemory()
  2. 构造函数现在使用类型而不是类型作为参数。我想我在某个时候做了这个改变,但忘了在其他任何地方注意到它,所以我想在这里指出它。Action(Of Object, DataReceivedEventArgs)DataReceivedEventHandler
  3. 我正在使用 Faithlife Code Blog 上的 Casting delegates 帖子中的方法将参数转换为特定的正确类型,以便该方法可以根据需要订阅/取消订阅它。Cast()Action(Of Object, DataReceivedEventArgs)DataReceivedEventHandler