提问人:G_Hosa_Phat 提问时间:9/12/2023 最后编辑:G_Hosa_Phat 更新时间:9/12/2023 访问量:47
将事件处理程序作为参数传递给对象构造函数
Pass event handler to object constructor as an argument
问:
我有一种在内存中执行批处理脚本的方法,方法是传递命令列表并在新的 .我使用这种方法来运行诸如 和 命令之类的东西,它非常适合我的用例,这样我就不必使用用户凭据等在网络上保留随机文件。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
根据我尝试从特定批处理文件的命令行捕获的内容,我将修改 or 以查找 中的特定值。在这个特定示例中,我正在寻找来自 GnuPG () 的错误,这些错误表明文件的加密或解密失败。对于一个版本,我可能会更改要查找之类的东西。ProcErrorHandler
ProcOutputHandler
e.Data
gpg.exe
psql
ProcErrorHandler
FATAL
因此,我没有定义与其余代码的 and 内联,而是从该类开始,它目前如下所示: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
- 如何将 EventHandler 作为 StackOverflow 上的方法参数传递
- 如何将委托作为参数传递为事件处理程序?在 StackOverflow 上
- VB.NET 在 StackOverflow 上添加任何带有委托的 EventHandler 类型
- 在 StackOverflow 上将操作委托作为 C# 中的参数传递
- 在 Faithlife Code 博客上选拔代表
- 在 Linux 上使用 C# 将函数作为参数传递提示
我可能只是很密集,但我似乎无法弄清楚如何实际构建并将处理程序方法从类外部传递到构造函数中,因为我没有要分配给处理程序的 and 参数的值。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
当然,这仍然有一个“问题”超出了这个问题的范围,那就是在类外部的处理程序定义中包含 and 对象,但我相信一旦我得到处理程序方法正确地传递到我的构造函数中,我就可以解决这个问题。OutputWaitHandle
ErrorWaitHandle
答:
好吧,看来我只是很密集,我相信我只是想通了。从上面看,我似乎要么试图让它太简单,要么太复杂。在阅读了 Microsoft 的 How to: Pass Procedures to Another Procedure in Visual Basic 之后,我意识到我不需要在构造函数调用中将 指定为参数定义的一部分,但我确实需要使用语法来正确分配方法定义。看起来要起作用的声明如下:TestHandler
DataReceivedEventHandler
AddressOf
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
你会注意到这个“最终”迭代与我最初发布的内容有几个重要的区别:
- 该类的方法更“完整”一些,以匹配原始方法的功能
Execute()
BatchFile
ExecuteBatchInMemory()
- 构造函数现在使用类型而不是类型作为参数。我想我在某个时候做了这个改变,但忘了在其他任何地方注意到它,所以我想在这里指出它。
Action(Of Object, DataReceivedEventArgs)
DataReceivedEventHandler
- 我正在使用 Faithlife Code Blog 上的 Casting delegates 帖子中的方法将参数转换为特定的正确类型,以便该方法可以根据需要订阅/取消订阅它。
Cast()
Action(Of Object, DataReceivedEventArgs)
DataReceivedEventHandler
评论