3

I have a method for exporting data. I do it via a new thread so that the GUI remains responsive. At the end it opens a SaveFileDialog which is not working without an invoke. With the below modification it's working but again, the GUI is unresponsive. Any clue?

private void button1_Click(object sender, EventArgs e)
{
   Thread thread = new Thread(method);
   thread.Start();
}
public void medhod()
{
   if (this.InvokeRequired)
   {
       Invoke(new MethodInvoker(delegate() { method(); }));
   }
   else
   {
       //Code
       //SaveFileDialog
   }
}

*Edit: Another approach would be to leave the export code in the new thread, and put the SaveFileDialog back to the original thread. All I need is 1st thread to "pause" and then continue once the 2nd thread is over. Ideas are welcome.

fishmong3r
  • 1,414
  • 4
  • 24
  • 51

4 Answers4

3

The problem is running any sort of UI component in a non-UI thread is generally a bad idea - especially a modal dialog.

Instead, put the actual background processing code into another thread and once finished call back into the UI thread and launch the save dialog. The TPL makes this sort of thing very trivial e.g.

Task.Factory.StartNew(() => {
    // do background processing
}).ContinueWith((task) => {
    // show save dialog
}, TaskScheduler.FromCurrentSynchronizationContext());
James
  • 80,725
  • 18
  • 167
  • 237
  • Error: The current SynchronizationContext may not be used as a TaskScheduler. – fishmong3r Jan 23 '14 at 11:58
  • 1
    Looks like there is a [known issue](http://stackoverflow.com/questions/4659257/how-can-synchronizationcontext-current-of-the-main-thread-become-null-in-a-windo?rq=1) with the task scheduler in .NET 4.0 which causes the synchronization context to be set to `null`. Follow the links for various workarounds or upgrade to 4.5. – James Jan 23 '14 at 12:14
  • Thank you, will check this out. Meanwhile I will just use Luaan's solution. – fishmong3r Jan 23 '14 at 13:25
3

Your issue is probably what Luaan comment points. You have long operation which you want to put into thread, but then you invoke the whole operation into UI thread and it will block the UI thread for a duration.

Do it like this:

private void button1_Click(object sender, EventArgs e)
{
    (new Thread(method)).Start();
}
private void method()
{
    //Code
    Invoke(() =>
    {
        //SaveFileDialog
    });
}

You don't need to check for InvokeRequired, because it will be required anyway. The way you use it is a pattern of defining method, which can be called from either thread. But in this case it typically contains very short operation to interact with UI controls.

James
  • 80,725
  • 18
  • 167
  • 237
Sinatr
  • 20,892
  • 15
  • 90
  • 319
  • "*you don't need to check for `InvokeRequired`, because it will be required anyway*" - not if `method` is also called from the UI directly. – James Jan 23 '14 at 13:13
  • 1
    @James, I agree, but it is obvious, what OP is hiding long operation which he doesn't want to run in UI thread. – Sinatr Jan 23 '14 at 13:14
  • One small issue. If I do the above it's OK, but it runs the `Code` part twice before actually opening the `SaveFileDialog`. So when reaching the end of `Code` it starts it again. Any idea on this? – fishmong3r Jan 23 '14 at 13:53
  • Yes I did. I'm as blind as a mole. Thanks again. – fishmong3r Jan 23 '14 at 14:01
  • You're welcomed. One more important thing is **not** to make method `public`. – Sinatr Jan 23 '14 at 14:11
  • Why is that important here? – fishmong3r Jan 23 '14 at 14:32
  • Specifically, this method is intended to be used only by `button1_Click`. If you want to run it from somewhere, then perhaps you want *also create a thread* for that. So then make a public method (which will create a thread). Generally, do not make anything public unless it is *intended* to be so. – Sinatr Jan 23 '14 at 14:35
  • You have an unnecessary cast there, just call `Invoke(() => { })` – James Jan 23 '14 at 15:23
  • @James, I was using `MethodInvoker` and only recently learned a bit about lambdas =D Thanks. – Sinatr Jan 23 '14 at 15:28
0

Try this:

void btnClick(object sender, EventArgs e)
{
    var t = new Thread(doStuff);
    t.SetApartmentState(ApartmentState.STA);
    t.Start();
}

void doStuff()
{
    SaveFileDialog sfd = new SaveFileDialog();

    sfd.ShowDialog();   
}

The SaveFileDialog is run in a separate thread (it has to have a single threaded apartment), and it doesn't block the UI thread of your application. However, be very careful about everything you do, it can be very unstable.

The basic issue is that your application is running a Windows Messaging loop, which is basically a loop that iterates over Windows Messages coming from the OS (and other applications). If the loop is stuck for some reason, the application becomes unresponsive (when you click the mouse, the OS sends a WM_MOUSEDOWN and many more methods to your message queue, which has to be depopulated by the message loop to do anything). Being in the ShowDialog method is exactly one of the ways the loop gets stuck - your form can no longer process any messages, because it never gets a chance to.

Now, what Invoke does is, that it adds the method you want to call to another queue on the target form. The next chance the form gets during the windows messaging loop, it executes all the items in the invoke queue - again, the messaging loop gets stuck.

Now, how does the dialog receive windows messages? Easy, in practice, it creates its own WM loop. This is very little but another while loop, that only terminates as the modal dialog closes - blocking the messaging loop of the parent form (or rather, the application).

The problem with the code above is that it could steal the messaging loop from our parent window. The solution is to explicitly create a new window with a new messaging loop, by explicitly passing the owner to ShowDialog:

void doStuff()
{
    NativeWindow nw = null;

    try
    {
        nw = new NativeWindow();

        nw.CreateHandle(new CreateParams());
        SaveFileDialog sfd = new SaveFileDialog();

        sfd.ShowDialog(nw);
    }
    finally
    {
        if (nw != null)
            nw.DestroyHandle();
    }
}
Luaan
  • 62,244
  • 7
  • 97
  • 116
  • You shouldn't run UI components in a non-UI thread. "*be very careful about everything you do, it can be very unstable*" - why even suggest it then? – James Jan 23 '14 at 11:41
  • @James Because you only have to handle the interaction between the two UI threads carefuly - which is where the `Invoke` method finally comes into action. – Luaan Jan 23 '14 at 11:48
  • think you missed the point of my comment, it's **never** a good idea to attempt to modify *any* sort of UI component on any other thread than the one it was created on because all UI components have thread affinity. – James Jan 23 '14 at 11:52
  • @James: Yes, but I'm creating the SaveFileDialog on its own thread. That's the point - as long as its never used in the original forms UI thread, and the original forms UI components aren't used in the SFD thread, all is well. After all, you run a hundred applications on your computer, each with its own UI thread; the process doesn't matter, only the windows do. – Luaan Jan 23 '14 at 11:54
  • @James: All that said, yes, I do like your solution better. If your scenario is just that you're doing background processing and need to show a dialog at the end, it's great. – Luaan Jan 23 '14 at 11:59
  • sorry but your code is *not* the equivalent of 2 UI threads. There is no message pump in the second so your dialog will be unresponsive (if appear at all). Then how do you expect to block the *other* UI thread by calling show modal on the other? Very tricky & dangerous path you are venturing down... – James Jan 23 '14 at 12:13
  • 1
    @James: No, each of the threads does have its own messaging queue. That's because the `ShowDialog` method does create a new application thread context if the current thread doesn't have one (the context is ThreadStatic). That's actually why its a huge hazard to use the same form on two threads - you're receiving messages on two threads using the same window handle; creating an explicit new `NativeWindow` solves this easily. – Luaan Jan 23 '14 at 12:18
  • @James: In any case, I do agree that this is something you shouldn't do unless you absolutely have to. I had to use this approach while hosting applications inside a different application (a silly legacy module-based system which relied on having the modules create their own UI forms, while also requiring them to reside in the same process space; I came in to solve the fact that it "acted funky" - the original dev had no idea how WM works. Making everything run on a single UI thread wouldn't quite be possible). – Luaan Jan 23 '14 at 12:22
  • `NativeWindow` is really just a WM listener it should be used to intercept messages, not run as a separate UI thread. I'd argue that your implementation of is not the correct approach, and as you have already stated, very unstable. – James Jan 23 '14 at 12:49
-1

try to use BeginInvoke() that is asyncronous instead of Invoke that is syncronous see What's the difference between Invoke() and BeginInvoke() for a better trattation of the argument.

Community
  • 1
  • 1
abrfra
  • 634
  • 2
  • 6
  • 24
  • If I simply change the `Invoke(new MethodInvoker(delegate() { ws_uptime_query(); }));` to `BeginInvoke(new MethodInvoker(delegate() { ws_uptime_query(); }));` it's still not responsive. Can you please share some example code? – fishmong3r Jan 23 '14 at 11:33
  • 1
    `BeginInvoke` won't change anything here. – James Jan 23 '14 at 11:41