|
|||||||||||||||||||||||
|
|||||||||||||||||||||||
|
Announcements
Services
Chapters
Feature Zones
|
IntroductionI wanted to launch a script from a Windows Form application and display the standard output in a text box as the process was running. Surely you're not surprised to learn that multithreading is involved. It turns out you'll have at least four threads running to do this simple task. To keep things simple and sane, I've reused code from other another source, so I must first give credit to the MSDN article "Give Your .NET-based Application a Fast and Responsive UI with Multiple Threads" by I.D. Griffiths. I highly suggest reading this article for more background on multithreading in Windows Forms applications. Thanks also to Chad Christensen for his suggestions in using a RichTextBox. Creating a class to call a processA script or executable can be run using Process process = new Process();
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;
process.StartInfo.CreateNoWindow = true;
process.StartInfo.FileName = FileName;
process.StartInfo.Arguments = Arguments;
process.StartInfo.WorkingDirectory = WorkingDirectory;
process.Start();
Notice that the standard output and standard error have both been redirected. There are two string output = process.StandardOutput.ReadToEnd();
Reading to the end will not work for this application, since we want to read the data as the process is running. Multiple threadsThe solution is to use multiple threads. One thread is dedicated to running the process and two more threads are dedicated to reading the standard error and standard output. This is mentioned in MSDN documentation. Each of these two threads will run a simple function that sits in a loop reading from the stream until the stream is closed. void ReadStdOut()
{
string str;
while ((str = process.StandardOutput.ReadLine()) != null)
{
// do something with str
}
}
After each line is read into str, we would like to notify a windows form to display the text. Raising an event is probably the best way to accomplish this. For every new line of text received (on either StandardOutput or StandardError) an event will be raised. A windows form class can subscribe to these events and update a text box. Simple, but it won't quite work without some additional work. Important rule of windows formsThere is an important rule of windows forms and multithreading. Controls are (almost entirely) not thread safe. This means that an event raised from any thread other than the UI Thread cannot use methods or properties of a control. There are a few methods guaranteed to be safe including Thankfully, we can inherit from the class AsyncOperation (written by I.D. Griffiths from the above mentioned MSDN article) to solve several problems. First, this class allows us to raise an event on a UI thread of a hosting or target control. The above function becomes: public delegate void DataReceivedHandler(object sender,
DataReceivedEventArgs e);
public event DataReceivedHandler StdOutReceived;
void ReadStdOut()
{
string str;
while ((str = process.StandardOutput.ReadLine()) != null)
{
FireAsync(StdOutReceived, this, new DataReceivedEventArgs(str));
}
}
The second thing AsyncOperation provides is a method of canceling a process. Let's take a look at that class in more detail. Inheriting from AsyncOperationAsyncOperation is an abstract base class that assists in creating cancelable worker threads that can fire events back on a UI control (or form). It provides two main methods that are called by a form class: AsyncOperation requires that one method be overridden: The implementation of void protected override void DoWork()()
{
// Start a new process for the cmd
Process process = new Process();
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;
process.StartInfo.CreateNoWindow = true;
process.StartInfo.FileName = FileName;
process.StartInfo.Arguments = Arguments;
process.StartInfo.WorkingDirectory = WorkingDirectory;
process.Start();
// Invoke stdOut and stdErr readers - each
// has its own thread to guarantee that they aren't
// blocked by, or cause a block to, the actual
// process running (or the gui).
new MethodInvoker(ReadStdOut).BeginInvoke(null, null);
new MethodInvoker(ReadStdErr).BeginInvoke(null, null);
// Wait for the process to end, or cancel it
while (! process.HasExited)
{
Thread.Sleep(SleepTime); // sleep
if (CancelRequested)
{
// Not a very nice way to end a process,
// but effective.
process.Kill();
AcknowledgeCancel();
}
}
}
The methods The formAs shown in the picture above, the form is quite simple. It consists of a rich text box to show the standard output and standard input, a button to run a process (Ok), and a button to cancel the process (Cancel). The Ok button calls the private void writeStreamInfo(object sender, DataReceivedEventArgs e)
{
this.richTextBox1.AppendText(e.Text + Environment.NewLine);
}
ImprovementsAdding a progress bar to the form is one nice improvement for the user interface. Of course, you have to know the progress of the program being run. One option is to have the script tell you explicitly in the standard output with lines such as: "Percent completion = 30". Your "writeStreamInfo" function would filter those lines and update a progress bar. Standard Error can be displayed in red or some other color (or in a separate rich text box) to highlight any errors found. At the end of the process, a dialog could be displayed with a list of all errors. Providing support for standard input shouldn't be too difficult, but integrating it with the windows form may be tough. Perhaps a separate single-line text box whose contents are sent to the standard input stream through a method on ProcessCaller. These are just a few ideas of improvements you can make. ConclusionUsing the class Revision History
|
||||||||||||||||||||||