3
using System.Windows.Forms;

public class App
{
    [STAThread]
    public static void Main()
    {
        string fname;
        using (var d = new OpenFileDialog())
        {
            if (d.ShowDialog() != DialogResult.OK)
            {
                return;
            }
            fname = d.FileName;
        }
        //Application.ExitThread();
        for (; ;)
            ;
    }
}

The above code shows me a file dialog. Once I select a file and press open, the for loop is executed, but the (frozen) dialog remains.

Once I uncomment Application.ExitThread() the dialog disappears as expected.

Does that work as intended? Why doesn't using make the window disappear? Where can I find more info about this?

Alexander Schmidt
  • 5,631
  • 4
  • 39
  • 79
  • 1
    I've tested your code, the (file open) dialog closes correctly as expected. – Jeroen van Langen Jun 06 '16 at 11:02
  • 1
    Does the dialog disappear if you call [`Application.DoEvents()`](https://msdn.microsoft.com/en-us/library/system.windows.forms.application.doevents%28v=vs.110%29.aspx) after disposing of it? – yaakov Jun 06 '16 at 11:19
  • 8
    Nobody is going to repro your problem from that snippet. In a GUI app it is crucial to never hang the main thread with any halt-and-catch-fire code like for(;;); Lots of things stop working, painting in particular can no longer occur. So any pixels in a window cannot be updated and you are likely to see the ghost of whatever was displayed on top of the window. Invest time in a decent tutorial or book, how to correctly write GUI code is rarely discovered with trial-and-error. – Hans Passant Jun 06 '16 at 11:24
  • 2
    Consider that a diagnostic. Your code should not need to call DoEvents(). – H H Jun 07 '16 at 06:21
  • The application I'm actually writing (my question's code was just a minimal example) stops making use of winforms once I select a file I'd like to open. So it's not a long-running thing interrupting the GUI's responsibility – it's "the begin of another journey" and a good moment for winforms to "flush all the things". – Alexander Aleksandrovič Klimov Jun 07 '16 at 09:09
  • 2
    You should have mentioned that in the question. Anyway, it doesn't change the problem. A [single-threaded apartment (STA) thread](https://msdn.microsoft.com/en-us/library/system.stathreadattribute.aspx) is required to have a message loop. The dialog runs one internally, via the `ShowDialog` method, as long as it is on the screen. But once the user dismisses it, the message loop goes away. No one is pumping messages anymore, violating the conditions of an STA thread. In fact, your application has gone stupid because it's locked in a tight, infinite loop. – Cody Gray - on strike Jun 07 '16 at 12:22
  • 1
    If it starts "the begin of another journey", then it should do that imho on a OS level: Why not spawn a console application (with hidden window) with the given file as a parameter? Then your gui app could gracefully terminate. – Daniel Gilbert Jun 08 '16 at 13:50
  • Because I already have a process so I can keep using it. Why to spawn a new one and terminating the first one? I'm not writing a *nix daemon. – Alexander Aleksandrovič Klimov Jun 08 '16 at 14:10

3 Answers3

12

You have discovered the primary problem with single-threaded applications... long running operations freeze the user interface.

Your DoEvents() call essentially "pauses" your code and gives other operations, like the UI, a chance to run, then resumes. The problem is that your UI is now frozen again until you call DoEvents() again. Actually, DoEvents() is a very problematic approach (some call it evil). You really should not use it.

You have better options.

Putting your long running operation in another thread helps to ensure that the UI remains responsive and that your work is done as efficiently as possible. The processor is able to switch back and forth between the two threads to give the illusion of simultaneous execution without the difficulty of full-blown multi-processes.

One of the easier ways to accomplish this is to use a BackgroundWorker, though they have generally fallen out of favor (for reasons I'm not going to get into in this post: further reading). They are still part of .NET however and have a lower learning curve then other approaches, so I'd still suggest that new developers play around with them in hobby projects.

The best approach currently is .NET's Tasks library. If your long running operation is already in a thread (for example, it's a database query and you are just waiting for it to complete), and if the library supports it, then you could take advantage of Tasks using the async keyword and not have to think twice about it. Even if it's not already in a thread or in a supported library, you could still spin up a new Task and have it executed in a separate Thread via Task.Run(). .NET Tasks have the advantage of baked in language support and a lot more, like coordinating multiple Tasks and chaining Tasks together.

Community
  • 1
  • 1
JDB
  • 25,172
  • 5
  • 72
  • 123
  • 1
    I would say the problems with DoEvents are *far* more numerous and severe than your answer makes them out to be. Furthermore, the "single-threaded applications" thing is rather a red herring. All GUI work is going to be done on a single thread. It is a hard requirement that apps have a single UI thread. Sure, you can work around this using something like a BackgroundWorker, but all that's going to do is invoke the UI thread from the background thread. It works in many cases, but doesn't work in every case. Tasks has the same problems as long as you're trying to update the UI. – Cody Gray - on strike Jun 07 '16 at 03:46
  • 1
    @CodyGray - I removed the bit about DoEvents and linked to more information. Honestly, I haven't used used DoEvents in years in favor of async and forgot just how problematic it actually was. – JDB Jun 07 '16 at 04:03
  • @CodyGray - Though I still disagree with your confusion of "single-threaded applications" and "All GUI work is going to be done on a single thread." In a single-threaded application, *all* work, GUI and everything else, is done on a single thread, which is the problem and the point - the OP needs some background threads for his/her long running operations (which would make the application *multi-threaded*) – JDB Jun 07 '16 at 04:16
  • The application I'm actually writing (my question's code was just a minimal example) stops making use of winforms once I select a file I'd like to open. So it's not a long-running thing interrupting the GUI's responsibility – it's "the begin of another journey" and a good moment for winforms to "flush all the things". – Alexander Aleksandrovič Klimov Jun 07 '16 at 09:02
  • 2
    @alex please read the linked material (especially the "very problematic approach") so you know what you are getting into... DoEvents can have very severe side effects. – JDB Jun 07 '16 at 11:18
5

JDB already explained in his answer why (generally speaking) your code doesn't work as expected. Let me add a small bit to suggest a workaround (for your specific case and for when you just need to use a system dialog and then go on like it was a console application).

You're trying to use Application.DoEvents(), OK it seems to work and in your case you do not have re-entrant code. However are you sure that all relevant messages are correctly processed? How many times you should call Application.DoEvents()? Are you sure you correctly initialize everything (I'm talking about the ApplicationContext)? Second problem is more pragmatic, OpenFileDialog needs COM, COM (here) needs STAThread, STAThread needs a message pump. I can't tell you in which way it will fail but for sure it may fail.

First of all note that usually applications start main message loop using Application.Run(). You don't expect to see new MyWindow().ShowDialog(), right? Your example is not different, let Application.Run(Form) overload creates the ApplicationContext for you (and handle HandleDestroyed event when form closes which will finally call - surprise - Application.ExitThread()). Unfortunately OpenFileDialog does not inherit from Form then you have to host it inside a dummy form to use Application.Run().

You do not need to explicitly call dlg.Dispose() (let WinForms manage objects lifetime) if you add the dialog inside the form with the designer.

using System;
using System.Windows.Forms;

public class App
{
    [STAThread]
    public static void Main()
    {
        string fname = AskForFile();
        if (fname == null)
            return;

        LongRunningProcess(fname);
    }

    private static string AskForFile()
    {
        string fileName = null;

        var form = new Form() { Visible = false };
        form.Load += (o, e) => { 
            using (var dlg = new OpenFileDialog())
            {
                if (dlg.ShowDialog() == DialogResult.OK)
                    fileName = dlg.FileName;
            }

            ((Form)o).Close();
        };

        Application.Run(form);

        return fileName;
    }
}
Community
  • 1
  • 1
Adriano Repetti
  • 65,416
  • 20
  • 137
  • 208
  • OpenFileDialog seems not to be castable to ApplicationContext. – Alexander Aleksandrovič Klimov Jun 07 '16 at 17:07
  • Why do you have to? If you want to mimic (for learning purposes, no real reason to do it) what Application.Run() does then pick a disassembler or directly public source code. You will see there is much more code that you may expect – Adriano Repetti Jun 07 '16 at 18:03
  • I mean: It seems to be required to cast OpenFileDialog to ApplicationContext to run your code – and that doesn't work. (I copy&pasted your code into my IDE and can't run it for that reason.) – Alexander Aleksandrovič Klimov Jun 07 '16 at 18:28
  • *"You do not need to explicitly call dlg.Dispose()"* I'm not sure in what context you mean this, but I have to nitpick anyway. You absolutely *do* need to call the Dispose method if you're showing the dialog modally. What you've done here, wrapping its creation in a using statement is morally equivalent but preferable. – Cody Gray - on strike Jun 08 '16 at 05:34
  • @CodyGray of course you need to call `.Dispose()` of any `IDisposable` member (and `using` is equivalent), what I mean (sorry to don't be clear enough) is that you do not need to explicitly call it when using WinForms designer to insert the dialog in the form. – Adriano Repetti Jun 08 '16 at 06:58
0

No, you don't have to call Application.ExitThread().

Application.ExitThread() terminates the calling thread's message loop and forces the destruction of the frozen dialog. Although "that works", it's better to unfreeze the dialog if the cause of the freeze is known.

In this case pressing open seems to fire a close-event which doesn't have any chance to finish. Application.DoEvents() gives it that chance and makes the dialog disappear.

NathanOliver
  • 171,901
  • 28
  • 288
  • 402
  • 5
    `DoEvents` is bad suggestion most of the time. There are plenty of existing discussion on why Sleep (or busy wait as show in question) freezes UI (i.e. http://stackoverflow.com/questions/27356165/why-does-thread-sleep-freeze-the-form) and how to properly handle such cases with timers, backgroud workers or `async` code. – Alexei Levenkov Jun 07 '16 at 00:36
  • 11
    Careful, you are jumping from the frying pan into the fire here. You have replaced `ExitThread` (a bad idea) with a call to `DoEvents` (arguably a worse idea). Both are really terrible hacks, evidence that you don't understand what the problem is or the correct way of designing an application so it doesn't have that problem. I can't really give you any more help than this, since your question is utterly lacking in a description of what you are actually trying to accomplish. Clearly no real-world program uses an infinite `for (;;)` loop. It will *never* work in a GUI application. – Cody Gray - on strike Jun 07 '16 at 03:49
  • The application I'm actually writing (my question's code was _just a minimal example_) stops making use of winforms once I select a file I'd like to open. So it's not a long-running thing interrupting the GUI's responsibility – it's "the begin of another journey" and a good moment for winforms to "flush all the things". – Alexander Aleksandrovič Klimov Jun 07 '16 at 09:05