How to make UI responsive while calling external exe in C#?

Viewed 201

I am writing a WPF program which use a exe file to capture data from a hardware. It takes around 2 seconds to finish a single call. And I have use this exe multiple times (>500) back to back with different arguments. I've to wait for each process to finish before the next call.Because, I can't run multiple exe same time. The hardware doesn't support it. In the mean time I've show an update to the UI and also keep the UI responsive for canceling the task anytime.

I'm confused about what to and how to use async-await, Dispatcher.BeginInvoke or Task.run to solve my problem. Any help or idea will be highly appreciated.

    ObservableCollection < String > fileNames = new ObservableCollection < string > ();
    //fileNames is used to show the file names a ListBox in UI. It have to be
    // updated in real time.

    private void BtnStartCapture_Click(object sender, RoutedEventArgs e) {
        for (int i = 1; i <= CaptureSettings.NoOfFrames; i++) {
            String currentFile;
            if (CaptureSettings.CkBoard1 == true) {

                currentFile = CaptureSettings.CaptureFrame(1, i);
                fileNames.Add(currentFile);
            }

            if (CaptureSettings.CkBoard2 == true) {
                currentFile = CaptureSettings.CaptureFrame(2, i);
                fileNames.Add(currentFile);
            }

        }

    }

    internal String CaptureFrame(int boardId, int frameNo) {

        string arg = createCommandLIneArgument(boardId, frameNo);
        try {
            ProcessStartInfo pInfo1 = new ProcessStartInfo {
                FileName = "GrabberTest1.exe",
                Arguments = arg,
                WindowStyle = ProcessWindowStyle.Hidden
            };

            var process1 = Process.Start(pInfo1);
            process1.WaitForExit();

            return Path.GetFileNameWithoutExtension(arg);
        } catch(Exception) {
            return "Failed " + Path.GetFileNameWithoutExtension(arg);
        }
    }

private void BtnCancelCapture_Click(object sender, RoutedEventArgs e) {
//to do
}
2 Answers

You've got 3 questions here:

  1. How do I wait for a process to exit without blocking the UI thread?
  2. How do I prevent a button from being clicked again until it finishes?
  3. How do I cancel?

That's a lot for a single Stack Overflow question; in the future, please only ask one question at a time.

How do I wait for a process to exit without blocking the UI thread?

You could use TaskCompletionSource<T> to hook into Exited as such:

public static Task<int> WaitForExitedAsync(this Process process)
{
  var tcs = new TaskCompletionSource<int>();
  EventHandler handler = null;
  handler = (_, __) =>
  {
    process.Exited -= handler;
    tcs.TrySetResult(process.ExitCode);
  };
  process.Exited += handler;
  return tcs.Task;
}

However, this code has a few caveats:

  1. You must set Process.EnableRaisingEvents to true before the process starts.
  2. You must call WaitForExitedAsync before the process starts.
  3. When Exited is raised, that does not mean that the stdout/stderr streams are finished. The only way to flush those streams is to call WaitForExit (after the process has exited). Not exactly intuitive.

For simplicity, you may just want to call WaitForExit on a background thread instead. That would use an extra unnecessary thread, but for a GUI app, that isn't critical.

In your code, you can push CaptureFrame off to a background thread:

private async void BtnStartCapture_Click(object sender, RoutedEventArgs e)
{
  for (int i = 1; i <= CaptureSettings.NoOfFrames; i++)
  {
    String currentFile;
    if (CaptureSettings.CkBoard1 == true)
    {
      currentFile = await Task.Run(() => CaptureSettings.CaptureFrame(1, i));
      fileNames.Add(currentFile);
    }

    if (CaptureSettings.CkBoard2 == true)
    {
      currentFile = await Task.Run(() => CaptureSettings.CaptureFrame(2, i));
      fileNames.Add(currentFile);
    }
  }
}

Note that async void is used here only because this is an event handler. Normally, you should avoid async void.

How do I prevent a button from being clicked again until it finishes?

One common pattern is to disable the button while it is running, as such:

private async void BtnStartCapture_Click(object sender, RoutedEventArgs e)
{
  BtnStartCapture.Enabled = false;
  try
  {
    for (int i = 1; i <= CaptureSettings.NoOfFrames; i++)
    {
      String currentFile;
      if (CaptureSettings.CkBoard1 == true)
      {
        currentFile = await Task.Run(() => CaptureSettings.CaptureFrame(1, i));
        fileNames.Add(currentFile);
      }

      if (CaptureSettings.CkBoard2 == true)
      {
        currentFile = await Task.Run(() => CaptureSettings.CaptureFrame(2, i));
        fileNames.Add(currentFile);
      }
    }
  }
  finally
  {
    BtnStartCapture.Enabled = true;
  }
}

How do I cancel?

Cancellation in .NET follows a standard pattern. The code being canceled observes a CancellationToken, which can be set from a CancellationTokenSource. Each CancellationTokenSource is a way to cancel the operation, but it can only be used once. So in this case, you want a new CancellationTokenSource each time the operation begins.

You could interpret a cancellation request as a kill request for your external process, but in your case I think it would be better to interpret the cancellation request as "let the current external process finish; just don't capture the next frame". I'm thinking this is better because the external process communicates with a hardware device (which we don't want to get into an unexpected state), and because it's fairly fast.

private CancellationTokenSource _cts;

private async void BtnStartCapture_Click(object sender, RoutedEventArgs e)
{
  _cts = new CancellationTokenSource();
  var token = _cts.Token;
  BtnStartCapture.Enabled = false;
  BtnCancelCapture.Enabled = true;
  try
  {
    for (int i = 1; i <= CaptureSettings.NoOfFrames; i++)
    {
      token.ThrowIfCancellationRequested();
      String currentFile;
      if (CaptureSettings.CkBoard1 == true)
      {
        currentFile = await Task.Run(() => CaptureSettings.CaptureFrame(1, i));
        fileNames.Add(currentFile);
      }

      if (CaptureSettings.CkBoard2 == true)
      {
        currentFile = await Task.Run(() => CaptureSettings.CaptureFrame(2, i));
        fileNames.Add(currentFile);
      }
    }
  }
  catch (OperationCanceledException)
  {
    // TODO: decide what to do here - clear fileNames? Display a message? Nothing?
  }
  finally
  {
    BtnStartCapture.Enabled = true;
    BtnCancelCapture.Enabled = false;
  }
}

private void BtnCancelCapture_Click(object sender, RoutedEventArgs e)
{
  _cts.Cancel();
}

The following uses the Invoke and WaitForExit methods to achieve your desired result.

Edit I edited the below code so it would support "back to back" calls to the thread that starts the process.

    static readonly object _object = new object();
    public int iProcCount = 0;
    public void StartExe()
    {
        System.Diagnostics.Process proc;
        lock (_object) // Because WaitForExit is inside the lock all other 
                       // instances of this thread must wait before the 
                       // previous thread has finished
        {
            proc = System.Diagnostics.Process.Start(strExePath);
            proc.WaitForExit(); // This is not on the UI thread so it will not block the UI thread
        }
        this.Invoke(new Action(() => UpdateGUI(this, "Finished Proc " + iProcCount)));
        iProcCount++;
    }

    public void UpdateGUI(Form theGui, string msg)
    { // This will wil update the GUI when the GUI thread is not busy
        lblGUIUpdate.Text = msg;
    }

    private void Button_Click(object sender, EventArgs e)
    {
        Thread th = new Thread(StartExe);
        th.Start(); // After this, the GUI thread will not block
    }  
Related