2

I want to:

  1. Show a form with a textbox.
  2. Run an external program (notepad.exe for ease of example).
  3. Continue to allow the user to enter data into the form textbox whilst notepad is running.
  4. Run some more (continue) native form code when notepad closes. This will update the form, amongst other things.

I'm having problems making this happen. I'm aware of a multitude of posts about this similar issue, but haven't found a solution that works for me.

I have tried:

  • Doing a waitforexit, but this of course blocks the UI and users cannot enter data.
  • Attempting an asynchronous process call, where another method is called when this process is completed. This causes a problem where the new method is called from another thread and can't update the form.
  • Doing a wait/sleep loop in the UI, but again this will naturally block the UI.

What would be the neatest, and simplest solution for a simple Windows Form program? There are no extra classes used, and all code is in the Form1 class.

MrBeatnik
  • 97
  • 3
  • 9
  • can you show the code that you are currently using.. I think that this is something that you could use http://stackoverflow.com/questions/1728099/visual-c-sharp-gui-stops-responding-when-process-waitforexit-is-used – MethodMan Sep 10 '14 at 15:14

4 Answers4

4

The Process class fires an Exited event when the process exits. You can add a handler to that event to execute code when the process exits without blocking the UI thread:

process.EnableRaisingEvents = true;
process.Exited += (s, args) => DoStuff();

Alternatively you could create a Task that represents the completion of the process to leverage the TPL for asynchrony:

public static Task WhenExited(this Process process)
{
    var tcs = new TaskCompletionSource<bool>();
    process.EnableRaisingEvents = true;
    process.Exited += (s, args) => tcs.TrySetResult(true);
    return tcs.Task;
}

This would allow you to write:

await process.WhenExited();
UpdateUI();
Servy
  • 202,030
  • 26
  • 332
  • 449
  • Finally, instead of just nagging on other people ideas ..., a decent answer. – Darek Sep 10 '14 at 15:32
  • The first piece of code looks the easiest, but I am having trouble implementing this. Particularlly calling the DoStuff method (which I have created) is giving me "No overload for method 'DoStuff' takes 0 arguments'. Method is invoked with `private void DoStuff(Object sender, System.EventArgs e)` – MrBeatnik Sep 10 '14 at 15:56
  • The Process Exited event will be running on a worker thread as well (a generated anonymous method), and if you try to interact directly with the UI thread will throw an exception. – lordjeb Sep 10 '14 at 16:01
  • @MrBeatnik Then either pass in those parameters, or, more sensibly, just make a parameter-less overload since I don't imagine you'd need those parameters for anything. – Servy Sep 10 '14 at 16:01
  • @lordjeb Correct, you would need to marshal to the UI thread in the handler. The alternative being to let the TPL handle it for you, as shown in the answer. – Servy Sep 10 '14 at 16:02
  • If I attempt to substitute `process.Exited += (s, args) => DoStuff();` with `process.Exited += (s, args) => DoStuff(null,null);` or `process.Exited += (s, args) => DoStuff(s, args);` I get the issue with cross threading. – MrBeatnik Sep 10 '14 at 16:02
  • @lordjeb, that is what I was originally getting in my OP (second point in what I have tried. – MrBeatnik Sep 10 '14 at 16:06
  • @MrBeatnik Yes, you'll need to marshal the code to the UI thread. There are half a dozen different ways of doing this. I showed you one of them in my answer, although you're free to use another such as invoking on a control or grabbing the synchronization context. If you really need help, Google is full of examples of how to marshal code to the UI thread in all sorts of different ways. – Servy Sep 10 '14 at 16:10
  • Or you could just use a BackgroundWorker which does it for you. ;) – lordjeb Sep 10 '14 at 16:12
  • @lordjeb At the cost of creating an entirely new thread, entirely needlessly. Creating a thread is a particularly expensive operation. Far, far, more expensive that is needed to just marshal some code to the UI thread. Now if you actually had some long running CPU bound work that you wanted to perform in another thread then sure, BGW is a fine tool. That's not what we have here though. It's using a sledgehammer to drive a nail. The TPL allows this operation to be done far more easily and effectively. – Servy Sep 10 '14 at 16:14
  • @Servy Based on what I see in the debugger, the Exited event fires on an entirely new thread as well. – lordjeb Sep 10 '14 at 16:19
  • @lordjeb It'll fire in a thread pool thread, that's a noticeable difference. – Servy Sep 10 '14 at 16:20
  • Thanks for the detail here. Got it working by marshalling the return of the DoStuff() function. – MrBeatnik Sep 16 '14 at 08:56
2

Here you go:

    void Form1_Load(object sender, EventArgs e)
    {
        Task.Factory.StartNew(() =>
        {
            var p = Process.Start("notepad.exe");
            p.WaitForExit();
        }).ContinueWith(antecedant => { MessageBox.Show("Notepad closed"); });
    }
Darek
  • 4,687
  • 31
  • 47
  • This is creating a thread pool thread just to sit around doing nothing while the process runs. It's better to write the program in an actual asynchronous manor. – Servy Sep 10 '14 at 15:26
  • @Servy It is not doing nothing. It is waiting for the external process to finish. It is non-blocking in every way. – Darek Sep 10 '14 at 15:28
  • Parameters action Type: System.Action The action delegate to execute asynchronously. – Darek Sep 10 '14 at 15:29
  • 1
    The thread itself is doing nothing. You're spending all of the time to schedule it, and you're telling it that it's not allowed to actually *do* anything. If there were no *actually asynchronous* solution then it's the hacky work around that you simply have to live with, but *that's simply not the case*. The `Process` class provides an *actually* asynchronous means to be notified when it completes. The code isn't blocking *the UI thread* but it's blocking a thread pool thread, which is very much blocking in a way. – Servy Sep 10 '14 at 15:33
  • Excellent follow up ... +1 and +1 for the next. – Darek Sep 10 '14 at 15:33
  • 1
    The fact that you're not blocking *the UI thread* doesn't mean that you're not blocking the thread pool thread that you're allocating, entirely needlessly. – Servy Sep 10 '14 at 15:34
2

Here is my favorite way to do something like this with a BackgroundWorker. This has the advantage of the RunWorkerCompleted callback being on the main thread, so it can interact with the UI.

public partial class Form1 : Form
{
    ...
    private BackgroundWorker wrk;
    private void button1_Click(object sender, EventArgs e)
    {
        wrk = new BackgroundWorker();
        wrk.DoWork += (s, ea) => { /*Create your process and wait here*/ };
        wrk.RunWorkerCompleted += (s, ea) => { textBox1.Text = "Finished"; };
        wrk.RunWorkerAsync();
    }
}
lordjeb
  • 1,286
  • 9
  • 14
  • This is creating a new thread just so that it can sit around doing nothing while the process runs. – Servy Sep 10 '14 at 15:31
  • You might need synchronization context for this one ... UI updates from background threads usually result in cross-threading exceptions. – Darek Sep 10 '14 at 15:39
  • @Darek The BGW does that automatically. That's the whole point of its existence. – Servy Sep 10 '14 at 15:49
  • @Servy From what I can tell in a debugger, any of these methods: async await with a Task, using the Process.Exited event, BackgroundWorker, etc. all use an additional thread to handle the asynchronous situation. – lordjeb Sep 10 '14 at 16:14
  • @lordjeb It will use a thread pool thread for a few microseconds. That's radically different than creating an entirely new full thread, blocking it for a very long period of time, and then only ever using it to do those same few microseconds worth of actual work. The TPL approach would avoid the creation of a hard thread, through using the TPL, and it would only consume it's time for an almost immeasurable amount of time, instead of minutes. That's a pretty significant difference. – Servy Sep 10 '14 at 16:23
  • I initially used this and still had to marshal the return of the function that I called, but it did work. – MrBeatnik Sep 16 '14 at 08:58
1

You should start process in BackgroundWorker so you can catch complete event on same thread.

        BackgroundWorker worker = new BackgroundWorker();
        worker.DoWork += delegate {
            Process proc = Process.Start("YOUR-PROCESS-PATH");
            proc.Start();
            proc.WaitForExit();
        }
        worker.RunWorkerCompleted += worker_RunWorkerCompleted;
        worker.RunWorkerAsync();

then catch the worker ended event on called thread;

    void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
        //Do your thing o UI thread
    }
  • This is creating a new thread just so that it can sit around doing nothing while the process runs. – Servy Sep 10 '14 at 15:31