Working With The Process Class From A Windows Form

Saturday, March 6, 2004

There are times in .NET development when you may find it necessary to execute a command line program and grab the results. Using the Process class from the System.Diagnostics namespace we can develop the following Windows Form application to display the results of command entered in a TextBox control.

The first step is to create a class to encapsulate the work we do with the Process class. Many of the concepts used in this example are followed up with links in the Resources section of this article.

public class CmdProcessor
{
 
   /// 
   /// Execute takes a command string to pass as the arguments to cmd.exe.       
   /// 
   /// The command to execute. 
   /// Examples: 
   ///   "dir" 
   ///   "type ..\..\comdprocessor.cs"
   /// 
   public void Execute(string command)
   {
      _process = InitializeProcess(command);               
      _executing = true;
      _process.Start();

      AttachStreams();
      PrepareAsyncState();

      // read the streans asynchronously so 
      // control will return to the caller
      _standardOutput.BaseStream.BeginRead(
         _outputBuffer, 0, 
         _outputBuffer.Length,
         _outputReady,
         _outputState 
         );     

      _standardError.BaseStream.BeginRead(
         _errorBuffer, 0, 
         _errorBuffer.Length,
         _errorReady,
         _errorState
         );         
   }

   // event fires when text arrives on standard output or standard inpuy      
   public event CmdProcessorEventHandler TextReceived;

   // Initializes a ProcessStartInfo for the Process.
   protected virtual ProcessStartInfo GetStartInfo(string command)
   {
      ProcessStartInfo psi = new ProcessStartInfo();
      psi.FileName = "cmd.exe";     
      
      // /c tells cmd.exe to execute the following command and then terminate
      psi.Arguments = "/c " + command;
      
      // UseShellExecute = false required for stream redirection
      psi.UseShellExecute = false;
      
      // we will redirect standard streams to our own
      psi.RedirectStandardError = true;
      psi.RedirectStandardOutput = true;   
      
      // dont allow the DOS box to appear
      psi.CreateNoWindow = true;    
      
      return psi;
   }

   protected virtual Process InitializeProcess(string command)
   {
      if(_executing)
      {
         // don't allow client to start another process while one is 
         // currently executing
         throw new ApplicationException("A Process is currently executing");
      }

      _process = new Process();              
      _process.StartInfo = GetStartInfo(command);
      _process.EnableRaisingEvents = true;               
      _process.Exited += new EventHandler(_process_Exited); 
      return _process;
   }     

   private void AttachStreams()
   {
      _standardOutput = _process.StandardOutput;
      _standardError = _process.StandardError;   
   }

   private void _process_Exited(object sender, EventArgs e)
   {
      int exitCode = _process.ExitCode;
      if(TextReceived != null)
      {
         // return exit code as part of output
         TextReceived(
               this, 
               new CmdProcessorEventArgs("Exited with code: " + exitCode.ToString())
            );
      }
      
      _process.Dispose();         
      _process = null;
      _executing = false;
   }


   // output has arrived on either standard output or standard error.
   // finish the asynch call, and if text has arrived raise an event.
   // finally, we need to try to read more from the steam in case not 
   // all the outout has arrived.
   private void OutputCallback(IAsyncResult ar)
   {
      AsyncState state = (AsyncState)ar.AsyncState;
      
      int count = state.Stream.BaseStream.EndRead(ar);

      if(count > 0)
      {
         if(TextReceived != null)
         {
            string text = System.Text.Encoding.ASCII.GetString(state.Buffer, 0, count);
            TextReceived(this, new CmdProcessorEventArgs(text));
         }      

         state.Stream.BaseStream.BeginRead
            (
            state.Buffer, 0, 
            state.Buffer.Length,
            _outputReady,
            state
            );
      }
   }

   // this method prepares the callback delegates which will be invoked when the 
   // asychronous reads have results, and also prepares a "state" object to carry
   // the stream and buffer objects used in the asynch calls
   private void PrepareAsyncState()
   {
      _outputReady = new AsyncCallback(OutputCallback);
      _outputState = new AsyncState(_standardOutput, _outputBuffer);
      _errorReady = new AsyncCallback(OutputCallback);
      _errorState = new AsyncState(_standardError, _errorBuffer);
   }

   private bool _executing = false;
   private Process _process;      
   private StreamReader _standardOutput;
   private StreamReader _standardError;
   private byte[] _errorBuffer = new byte[512];
   private byte[] _outputBuffer = new byte[512];
   
   private AsyncCallback _outputReady;
   private AsyncState _outputState; 
   private AsyncCallback _errorReady;
   private AsyncState _errorState; 
}

public delegate void CmdProcessorEventHandler(object sender, CmdProcessorEventArgs e);

public class CmdProcessorEventArgs: EventArgs
{
   public CmdProcessorEventArgs(string text)
   {
      _text = text;
   }

   public string Output
   {
      get { return _text; }
      set { _text = value; }
   }
   protected string _text;
}

internal class AsyncState
{
   public AsyncState(StreamReader stream, byte[] buffer)
   {
      _stream = stream;
      _buffer = buffer;
   }

   public StreamReader Stream
   {
      get { return _stream; }
   }

   public byte[] Buffer
   {
      get { return _buffer; }
   }

   protected StreamReader _stream;
   protected byte[] _buffer;
}

Using the above class from a Windows Form requires the following code excerpted from a Form. The most important part of the code is to make sure a TextReceived event marshals back to the UI thread before updating the Text property of the txtOutput TextBox object.

private void button1_Click(object sender, System.EventArgs e)
{
   txtOutput.Text = String.Empty;

   CmdProcessor cmd = new CmdProcessor();
   cmd.TextReceived +=new CmdProcessorEventHandler(cmd_TextReceived);
   cmd.Execute(txtCommand.Text);         
}

private void cmd_TextReceived(object sender, CmdProcessorEventArgs e)
{
   if(InvokeRequired)
   {
      object[] args = { sender, e };
      Invoke(new CmdProcessorEventHandler(cmd_TextReceived), args);
   }
   else
   {
      txtOutput.Text += e.Output;
   }
}

Code

You can download the code as a VB.NET project.

Resources

Console Applications in .NET, or Teaching a New Dog Old Tricks

HOWTO: Write a Wrapper for a Command-Line Tool with Visual C# .NET

Safe, Simple Multithreading in Windows Forms, Part 1

Safe, Simple Multithreading in Windows Forms, Part 2

.NET Framework Developer's Guide Asynchronous File I/O

A Primer on Creating Type-Safe References to Methods in Visual Basic

.NET Asynchronous Programming Design Pattern

Give Your .NET-based Application a Fast and Responsive UI with Multiple Threads

by K. Scott Allen K.Scott Allen
My Pluralsight Courses
The Podcast!