3

I have a form which take a few seconds to load. So therefore, I want to show a little form with the text 'Loading, please wait'. When the form is finished loading, the loading form must be closed.

So, I made a simple class which Shows a loading form in a thread:

public class ShowLoadingForm
{
    Thread _thread;

    public void Show()
    {
        try
        {
            _thread = new Thread(new ThreadStart(ShowForm));
            _thread.SetApartmentState(ApartmentState.STA);
            _thread.IsBackground = true;
            _thread.Start();
        }
        catch (Exception ex)
        {
            ErrorMessages.UnknownError(true, ex);
        }
    }

    private void ShowForm()
    {
        LoadingForm f = new LoadingForm();
        f.TopMost = true;
        f.ShowInTaskbar = true;
        f.SetText(" Loading... ");
        f.Show();
    }

    public void Close()
    {
        _thread.Abort();
    }
}

In the main form I have:

_loadingForm = new ShowLoadingForm();
_loadingForm.Show();

BUT. After this piece of code, I do something on the main form: this.Opacity = 0;. At this point, I can see in the debugger that the thread is stopped working and a ThreadStateException is thrown and the loading form is disappeared.

Why is this?

Martijn
  • 24,441
  • 60
  • 174
  • 261
  • eeeeek!............If a form has long calculations, spawn a background thread to perform them (inside the form instantiation) – Mitch Wheat Feb 09 '11 at 12:32
  • @Mitch Wheat fully agree – rene Feb 09 '11 at 12:33
  • While the form is loading, the form is also doing some calculations for the position of some controls. This gives some flashing/flickering and I don;t want the user to see this. – Martijn Feb 09 '11 at 12:34
  • @Martijn, can you post the stacktrace and the source of the exception? – Devendra D. Chavan Feb 09 '11 at 12:35
  • This is a classic case of posting a problem that is related to a perceived solution, instead of simply stating the real problem. – Mitch Wheat Feb 09 '11 at 12:36
  • @Devendra D. Chavan: I don't see the stacktrace. I see in the debugger that this exception is thrown by some properties – Martijn Feb 09 '11 at 12:39
  • If it takes long to display the main form even after making sure you are doing it properly, you can setup a splashscreen form as a main form, load the real main form in the background and then show it. – Jaroslav Jandek Feb 09 '11 at 12:39
  • @Jaroslav Jandek: How would I load the main form in the background? – Martijn Feb 09 '11 at 12:43

6 Answers6

1

This is how you would show a simple Splash Screen (form). How you design it is up to you (I prefer a picture box and the form without the panel - looks nice and it is VERY simple)).

public partial class MainForm : Form
{
    public MainForm()
    {
        SplashForm sf = new SplashForm();
        sf.Show();

        InitializeComponent();

        this.SuspendLayout();
        // Do the positioning...
        this.ResumeLayout();

        sf.Close();
    }
}
Jaroslav Jandek
  • 9,463
  • 1
  • 28
  • 30
  • I don't want to show the mainform while it is 'build', because it doesn't look very nice. How should I implement that your way? – Martijn Feb 09 '11 at 13:16
  • erm... Exactly like this... You "build the form" in the place where I put the comment "Do the positioning...". The form will show immediately after the splash is closed. – Jaroslav Jandek Feb 09 '11 at 13:31
  • Sorry, I didn't read well enough to see you are in the constructor. I've read somewhere that it isn't a good idea to load heavy data in the constructor, but is this case an exception? – Martijn Feb 09 '11 at 13:50
  • You do not have to do it in a constructor... Just do it anywhere you do the "heavy loading". You may have to call `BringToFront` so the main form is visible after the splash is closed. Also, you have to call `Application.DoEvents()` during heavy loading or starting the splash in a new thread (which I prefer). I can give you sample of both. – Jaroslav Jandek Feb 09 '11 at 14:06
1

Your program bombs because you abort the thread but do not take care of the window. It is liable to try to run code because of a Windows notification, that will nosedive on ThreadStateException because the thread is aborted. You simply cannot end the thread by aborting it.

Here's a general class to solve this problem, it takes care of shutting down the waiting form and the thread cleanly.

using System;
using System.Drawing;
using System.Windows.Forms;
using System.Threading;

class Loader : IDisposable {
    private AutoResetEvent initialized = new AutoResetEvent(false);
    private Form loadForm;
    private Rectangle ownerRect;
    private bool closeOkay;

    public Loader(Form owner, Form pleaseWait) {
        if (pleaseWait.IsDisposed) throw new InvalidOperationException("Create a *new* form instance");
        loadForm = pleaseWait;
        loadForm.TopMost = true;
        loadForm.ShowInTaskbar = false;
        loadForm.StartPosition = FormStartPosition.Manual;
        ownerRect = new Rectangle(owner.Location, owner.Size);
        loadForm.Load += delegate {
            loadForm.Location = new Point(
                ownerRect.Left + (ownerRect.Width - loadForm.Width) / 2,
                ownerRect.Top + (ownerRect.Height - loadForm.Height) / 2);
            initialized.Set();
        };
        loadForm.FormClosing += new FormClosingEventHandler((s, ea) => {
            ea.Cancel = !closeOkay;
        });
        var t = new Thread(() => {
            Application.Run(loadForm);
        });
        t.SetApartmentState(ApartmentState.STA);
        t.IsBackground = true;
        t.Start();
        initialized.WaitOne();
    }

    public void Dispose() {
        if (loadForm == null) throw new InvalidOperationException();
        loadForm.Invoke((MethodInvoker)delegate {
            closeOkay = true;
            loadForm.Close(); 
        });
        loadForm = null;
    }

}

Sample usage:

    private void button1_Click(object sender, EventArgs e) {
        using (new Loader(this, new LoadingForm())) {
            System.Threading.Thread.Sleep(3000);
        }
    }
Hans Passant
  • 922,412
  • 146
  • 1,693
  • 2,536
  • 'Your program bombs because you abort the thread but do not take care of the window'. The `_thread.Abort` is never called. The thread stops working at the moment `this.Opacity = 0` is called. – Martijn Feb 09 '11 at 13:52
  • Maybe there is more than one way your code can bomb, not sure. Changing the Opacity property causes the window to be recreated, that can have side-effects. I posted this answer as a general solution, not a specific fix for your version. – Hans Passant Feb 09 '11 at 14:01
  • Your class works great! I have a few questions about it though. 1) How can I pass status text to the pleaseWait form? 2) What does `new Thread(() => { Application.Run(loadForm); });` do? 3) Why use `Application.Run()`? – Martijn Feb 09 '11 at 14:04
  • 1) All it needs is a form object. You can setup the form any way you like it. Give it a constructor with a string argument for example. 2) It creates the thread that keeps the load form alive. 3) That starts the message loop for the form so that it can respond to paint requests etc. Required because your UI thread is dead for a while. – Hans Passant Feb 09 '11 at 14:08
  • 1)Can I also use Properties? Because the status can change 2) `new Thread(() => { Application.Run(loadForm); });` is the notation of an anonymous delegate, right? Between `{..}` you specify the body of the 'method'. Am I right? 3) Can you explain that a little bit more? Why not just use `loadForm.Show()`? – Martijn Feb 09 '11 at 14:14
  • It is a lambda. The Load event handler is an anonymous method. The FormClosing event handler is a lambda again. I mixed it up well here. – Hans Passant Feb 09 '11 at 14:17
  • You have to use Invoke(), just like the Dispose() method does, to update properties of the form. The Show() method call is automatic because of the Application.Run() call. – Hans Passant Feb 09 '11 at 14:19
  • Thnx. One more question. Why use `AutoResetEvent`? – Martijn Feb 09 '11 at 14:28
  • It solves a race condition, closing the form won't work correctly if the loading form wasn't fully created yet by the time Dispose() is called. Which is fairly likely to happen on a slow machine. – Hans Passant Feb 09 '11 at 14:32
0

I believe you should not create and show a form from a Background thread, UI should be manipulated from the main UI thread, then you can have worker threads doings some background tasks but not creating and showing forms.

Davide Piras
  • 43,984
  • 10
  • 98
  • 147
0

Suggest you check out Control.SuspendLayout Method and Control.ResumeLayout Method

Mitch Wheat
  • 295,962
  • 43
  • 465
  • 541
  • I hava three controls which heights are always calculated when the window resizes. So, I have to call Suspend/resume layout for each control and I won't have any flickering left? – Martijn Feb 09 '11 at 12:42
  • Also see: http://stackoverflow.com/questions/835100/winforms-suspendlayout-resumelayout-is-not-enough – Mitch Wheat Feb 09 '11 at 12:44
  • I still see a lot of flickering. Because that and the amount of data to load I want to show a form with the please wait text. – Martijn Feb 09 '11 at 12:46
0

if your problem is with flickering of forms then use this code in form load which uses double-buffering to hide flickering

this.SetStyle(ControlStyles.DoubleBuffer | 
      ControlStyles.UserPaint | 
      ControlStyles.AllPaintingInWmPaint,
      true);
   this.UpdateStyles();
Nighil
  • 4,099
  • 7
  • 30
  • 56
0

I am doing exactly what you want, and I can show you mine thread function - so you can maybe derive what is wrong at your end - mine works ok in development and production on few dozens deployed machines.

    static void ThreadFunc()
    {
        _splash = new Splash();
        _splash.Show();
        while (!_shouldClose)
        {
            Application.DoEvents();
            Thread.Sleep(100);
            if (new Random().Next(1000) < 10)
            {
                _splash.Invoke(new MethodInvoker(_splash.RandomizeText));
            }
        }
        for (int n = 0; n < 18; n++)
        {
            Application.DoEvents();
            Thread.Sleep(60);
        }
        if (_splash != null)
        {
            _splash.Close();
            _splash = null;
        }
    }

Splash here is my form - splash screen. I have this:

    static public void ShowSplash()
    {
        _shouldClose = false;
        Thread t = new Thread(ThreadFunc);
        t.Priority = ThreadPriority.Lowest;
        t.Start();
    }

that I call from my Program.Main() BEFORE main form of the application is shown. And app loads normally, updates the main screen, and after all processing is done, I use:

    internal static void RemoveSplash()
    {
        _shouldClose = true;
    }
Daniel Mošmondor
  • 19,718
  • 12
  • 58
  • 99
  • My form must be loaded after a selection is made in a grid. But I will try this. – Martijn Feb 09 '11 at 12:52
  • Just out of curiosity : what's up with the for loop? and the if statement in the while loop? – Martijn Feb 09 '11 at 13:21
  • On the form I have control that needs DoEvents() to work properly, some form of MarqueueProgressBar. If statement in the while loop updates the 'funny loading messages' on the form :) - RandomizeText() – Daniel Mošmondor Feb 09 '11 at 13:33