Mark
Mark

Reputation: 79

Process.Start() an executable and return a DialogResult to the caller

When I want to display message I'm calling Message Display sub program(EXE) from my main Program (calling program). I cannot get called exe dialog result to caller.

        Dim psiProcessInfo As New ProcessStartInfo
        With psiProcessInfo
            .FileName = "DisplayMessage"
            .Arguments = ("FormName$C$lblMessageLine01$lblMessageLine02$lblMessageLine03")
        End With
        Process.Start(psiProcessInfo)

above I display calling section.

Private Sub dlgDisplayMessage_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        ' Input Parameter Accepted
        strInputMessage = Command()
        
        ' Composite Parameter Seperator
        Dim strParaSeperator As String = "$"
        Dim strCompersitePara As String = Microsoft.VisualBasic.Interaction.Command
        ' Parameter Split & Assign To Array
        Dim arParameter() As String = strCompersitePara.Split(strParaSeperator.ToCharArray)
        
        With pbPictureBox
            Select Case lblMessageType.Text
                Case Is = "C" ' Critical
                    .Image = My.Resources.Critical
                Case Is = "E" ' Exclamation
                    .Image = My.Resources.Exclamation
                Case Is = "Q" ' Question
                    .Image = My.Resources.Question
            End Select
            .Visible = True
        End With

        With txtMessageBody
            .Multiline = True
            .Size = New Size(386, 215)
            .Location = New Point(24, 53)
            .ScrollBars = ScrollBars.Vertical
            .TextAlign = HorizontalAlignment.Center
            .Text = vbCrLf & _
            lblMessageLine01.Text.Trim & _
            vbCrLf & vbCrLf & _
            lblMessageLine02.Text.Trim & _
            vbCrLf & vbCrLf & _
            lblMessageLine03.Text.Trim
            .Visible = True
        End With
        With cmdCancel
            .Focus()
        End With
End Sub



Private Sub cmdYes_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles cmdYes.Click
    
        Me.DialogResult = System.Windows.Forms.DialogResult.Yes
    
End Sub

Private Sub cmdCancel_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles cmdCancel.Click
    Try
        Me.DialogResult = Windows.Forms.DialogResult.No
    
End Sub

Display message dialog coding I display above. I want to know how I get DialogResult.OK or DialogResult.No to calling exe.

Edited

According to the Jimi I change my caller program code. But still it didnt return any value.

Dim p As New Process()
    p.StartInfo.UseShellExecute = False
    p.StartInfo.ErrorDialog = True
    p.StartInfo.RedirectStandardOutput = True
    p.StartInfo.UseShellExecute = False
    p.StartInfo.Arguments = ("FormName$C$lblMessageLine01$lblMessageLine02$lblMessageLine03")
    p.StartInfo.FileName = "DisplayMessage"
    p.Start()

    Dim output As String = p.StandardOutput.ReadToEnd()
    p.WaitForExit()

    MessageBox.Show(output)

Upvotes: 1

Views: 437

Answers (1)

Jimi
Jimi

Reputation: 32248

A few suggestions about the way the dlgDisplayMessage Form handles the CommandLine arguments:

  • You should use Environment.GetCommandLineArgs() to get an array of values passed to the command line. These values are meant to be separated by a space.
    The first item always represents the executable path. Recommended, it's a .Net method, much easier to translate to another .Net language.
    You could also use My.Application.CommandLineArgs, the difference is that the first item is the first parameter of the command line, not the executable path. Avoid Interaction.Command()

  • I think that those lblMessageLine01 etc. parts are actually meant to be the content of some Labels. In this case, you should of course use the lblMessageLine01.Text property. You can use an interpolated string to add these values to the command line. Since these Labels may contain multiple words separated by a space, you need to enclose these values in double quotes. For example:

    Dim commandLine = $"FormName C ""{lblMessageLine01.Text}${lblMessageLine02.Text}"""  
    
  • To return a value from the Dialog, using the Process class, you have a few options:

Of course you could also use some form of Interprocess Communications, but this a different scenario than what described in the OP.


Imports System.IO

Public Class dlgDisplayMessage
    ' For testing. Replace the Bitmaps with anything else
    Private images As New Dictionary(Of String, Bitmap) From {
        {"C", SystemIcons.Warning.ToBitmap()},
        {"E", SystemIcons.Exclamation.ToBitmap()},
        {"Q", SystemIcons.Question.ToBitmap()}
    }

    Protected Overrides Sub OnLoad(e As EventArgs)
        ' The first item is always the executable path
        Dim arParameter = Environment.GetCommandLineArgs()

        If arParameter.Count > 1 AndAlso images.ContainsKey(arParameter(1)) Then
            pbPictureBox.Image = images(arParameter(1))
        End If

        If arParameter.Count > 2 Then
            txtMessageBody.Visible = True
            txtMessageBody.AppendText(String.Join(Environment.NewLine, arParameter(3).Split("$"c)))
        End If
        MyBase.OnLoad(e)
    End Sub

    Private Sub btnOK_Click(sender As Object, e As EventArgs) Handles btnOK.Click
        Console.WriteLine("Some data to return")
        Console.Out.WriteLine("More data")
        Environment.Exit(DialogResult.OK)
    End Sub

    Private Sub btnCancel_Click(sender As Object, e As EventArgs) Handles btnCancel.Click
        Using sw = New StreamWriter(Console.OpenStandardOutput())
            sw.AutoFlush = True
            sw.WriteLine("Other data to return")
        End Using
        Environment.Exit(DialogResult.Cancel)
    End Sub

    Private Sub btnAbort_Click(sender As Object, e As EventArgs) Handles btnAbort.Click
        Console.Error.WriteLine("Some errors to report")
        Environment.Exit(DialogResult.Abort)
    End Sub
End Class

The application that starts this executable can get the outcome of the Dialog in different ways: reading the StandardOutput, StandardError or ExitCode of the Process it launched. Or all of them, of course.

I assume the application that creates this Dialog is under your control (you made it).

In any case, the Process StartInfo must set RedirectStandardOutput to True, optionally RedirectStandardError to True and UseShellExecute to False (starting with the System Shell doesn't allow redirections here)

You can then:

  • Start the Process

  • Read the StandardOutput and StandardError

  • Wait synchronously for the Process to exit, using Process.WaitForExit()

  • Read the Process ExitCode, e.g.:

    [Process].Start()
    Dim sOut = [Process].StandardOutput.ReadToEnd()
    Dim sErr = [Process].StandardError.ReadToEnd()
    
    [Process].WaitForExit()
    Dim exitCode = [Process]?.ExitCode
    [Process]?.Dispose()
    

This procedure is synchronous (blocking). There's a good chance you don't want this when the Process is started and its results waited in a GUI, since it will also block the User Interface.
Of course you could run a Task or start a Thread, then marshal back the results to the UI Thread.

You can also use the asynchronous (event-driven) version, subscribing to the OutputDataReceived, ErrorDataReceived and Exited events.
To enable the Exited event, you need to set Process.EnableRaisingEvents to True.
Also setting the Process.SynchronizingObject to the instance of a Control class (usually a Form, but any ISynchronizeInvoke object would do) that will handle the events. This because the Process' events are raised in ThreadPool Threads. Setting a UI element as the SynchronizingObject, causes the events to raise in the same Thread where object specified was created (the UI Thread, here).

This can be somewhat obnoxious in existing contexts, because you have to add the event handlers to a Form class, remember to remove them, dispose of the Process asynchronously etc.

So here's a helper class that transforms the event-driven procedure in an awaitable Task that can be executed from any async method. E.g., it can be called from the Click event handler of a Button, adding the Async keyword to the method.
It can be modified and used to test different scenarios and methods to start a Process and get its results in a separate environment.

It uses a TaskCompletionSource + Task.WhenAny().
The Exited event causes the TaskCompletionSource to set its result.

The helper class returns the contents of the Process' StandardOutput, StandardError and ExitCode value translated to a DialogResult value.
It can set a Timeout, to stop waiting for the Process to return a result, if specified.

Sample calling procedure:

Private Async Sub SomeButton_Click(sender As Object, e As EventArgs) Handles SomeButton.Click
    Dim exePath = "[The Executable Path]"
    Dim cmdLine = $"FormName C ""{lblMessageLine01.Text}${lblMessageLine02.Text}${lblMessageLine03.Text}"""

    Dim dlgResponse = New DialogResponseHelper(exePath, cmdLine)
    ' This call returns when the Dialog is closed or a Timeout occurs
    Dim exitCode = Await dlgResponse.Start()

    ' Read the StandardOutput results
    If dlgResponse.DialogData.Count > 0 Then
        For Each dataItem In dlgResponse.DialogData
            Console.WriteLine(dataItem)
        Next
    End If

    ' See whether errors are signaled
    Console.WriteLine(dlgResponse.DialogErrors.Count)

    If dlgResponse.ExitCode = DialogResult.OK Then
        ' Do something
    End If
End Sub

If the Interpolated Strings feature is not available, use String.Format() instead:

Dim dataOut = {lblMessageLine01.Text, lblMessageLine02.Text, lblMessageLine03.Text}
Dim cmdLine = String.Format("FormName C ""{0}${1}${2}""", dataOut)

DialogResponseHelper class:

Imports System.Collections.Concurrent
Imports System.Collections.Generic 
Imports System.Diagnostics
Imports System.IO
Imports System.Linq
Imports System.Threading
Imports System.Threading.Tasks
Imports System.Windows.Forms

Public Class DialogResponseHelper
    Private exitedTcs As TaskCompletionSource(Of Integer) = Nothing
    Private dlgData As New ConcurrentBag(Of String)()
    Private dlgErrors As New ConcurrentBag(Of String)()
    Private mExitCode As DialogResult = DialogResult.None
    Private proc As Process = Nothing

    Public Sub New(exePath As String, cmdLine As String, Optional timeout As Integer = Timeout.Infinite)
        ExecutablePath = exePath
        CommandLine = cmdLine
        WaitTimeout = timeout
    End Sub

    Public ReadOnly Property ExecutablePath As String
    Public ReadOnly Property CommandLine As String
    Public ReadOnly Property WaitTimeout As Integer
    Public ReadOnly Property ExitCode As DialogResult
        Get
            Return mExitCode
        End Get
    End Property

    Public ReadOnly Property DialogData As List(Of String)
        Get
            Return dlgData.ToList()
        End Get
    End Property
    Public ReadOnly Property DialogErrors As List(Of String)
        Get
            Return dlgErrors.ToList()
        End Get
    End Property

    Public Async Function Start() As Task(Of Integer)
        exitedTcs = New TaskCompletionSource(Of Integer)()

        proc = New Process()
        Dim psi = New ProcessStartInfo(ExecutablePath, CommandLine) With {
            .RedirectStandardError = True,
            .RedirectStandardOutput = True,
            .UseShellExecute = False,
            .WorkingDirectory = Path.GetDirectoryName(ExecutablePath)
        }

        proc.StartInfo = psi
        AddHandler proc.OutputDataReceived, AddressOf DataReceived
        AddHandler proc.ErrorDataReceived, AddressOf ErrorReceived
        AddHandler proc.Exited, AddressOf ProcessExited
        proc.EnableRaisingEvents = True

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

        Await Task.WhenAny(exitedTcs.Task, Task.Delay(WaitTimeout))

        If proc IsNot Nothing Then
            RemoveHandler proc.Exited, AddressOf ProcessExited
            RemoveHandler proc.ErrorDataReceived, AddressOf ErrorReceived
            RemoveHandler proc.OutputDataReceived, AddressOf DataReceived
            proc.Dispose()
            proc = Nothing
        End If
        Return exitedTcs.Task.Result
    End Function

    Private Sub DataReceived(sender As Object, e As DataReceivedEventArgs)
        If String.IsNullOrEmpty(e.Data) Then Return
        dlgData.Add(e.Data)
    End Sub

    Private Sub ErrorReceived(sender As Object, e As DataReceivedEventArgs)
        If String.IsNullOrEmpty(e.Data) Then Return
        dlgErrors.Add(e.Data)
    End Sub

    Private Sub ProcessExited(sender As Object, e As EventArgs)
        Dim exitId = (proc?.ExitCode).Value
        mExitCode = CType(exitId, DialogResult)
        exitedTcs.TrySetResult(exitId)
    End Sub
End Class

Upvotes: 1

Related Questions