Click here to Skip to main content
15,868,420 members
Articles / Programming Languages / C#

Immediate Display of WinForms using the Shown() Event

Rate me:
Please Sign up or sign in to vote.
4.76/5 (63 votes)
12 Feb 2010CPOL6 min read 169K   3.3K   137   39
How to cause a Winform (and all its child controls) to fully render before you do additional processing.

Introduction

One of my pet peeves is forms that don't instantly appear. You see it all the time: the user clicks a button, then waits a few seconds until the expected UI appears. This is typically due to the newly-created form performing some time-consuming (blocking) task in its Load() event handler (or, in the non-.NET world, the WM_INITDIALOG message handler). Aside from being poor UI design (never leave the user wondering what's happening!), it can have some really undesirable consequences: a button-click doesn't have an immediate effect, users will tend to click the button again, which can result in the action being invoked twice.

Since my earliest Windows programming, I've always implemented "instant feedback" in my Forms (f/k/a "dialog boxes"). My technique has evolved as Windows API has evolved; I'll discuss my older techniques (which can still be used in .NET), and end up with my current implementation using .NET WinForms APIs.

The Goal

Create a simple, reliable mechanism for doing post-form-load processing which ensures that the form is "fully rendered" before the post-load processing commences. This means that all the controls in the form itself as well as all child controls (in the dialog template) have been drawn as the user would expect to see them.

Background

This is a pretty basic technique; novice WinForms coders should be able to implement it.

An understanding of the startup sequence of a Form (a.k.a. window, a.k.a. dialog) is useful: http://msdn.microsoft.com/en-us/library/86faxx0d%28VS.80%29.aspx.

Most of these events have direct Windows message (WM_*) analogs. However, .NET adds the System.Windows.Forms.Form.Shown event - which does not have a corresponding Windows message - and that event is the basis for a relatively clean way to do post-Load() processing. (MSDN docs on the Shown() event: read here).

Complicating the issue is the asynchronous, not-totally-predictable nature of messages. When a window is created (and thus its children are created), the exact sequence of the various windows' messages varies from instance to instance. Certainly, each window's messages occur in the same sequence every time, but the sequence of the parent/child messages is unpredictable, creating a race condition. The most unfortunate result of this being that sometimes a dialog will render properly, sometimes it won't. Removing this uncertainty - ensuring predictability - is a big part of this solution.

Finally, it's very important to perform your blocking processing after the form and all children are fully rendered. Blocking the UI thread (and message queue) mid-render, results in some pretty partial ugly UI that looks like your app crashed!

WinformsUsingShownEvent/PartiallyRenderedForm.png

Notice how the form's frame and unclipped client area have rendered but certain child controls' client areas still show the UI "below" the form. Also notice how the listbox has rendered but the other controls (GroupBoxes, ComboBoxes) have not (I haven't investigated why that is).

Here's how it should look fully rendered:

WinformsUsingShownEvent/FullyRenderedForm.png

Solution: Before .NET

(If you wrote Windows code before .NET - with or without MFC - this should look familiar.)

Here's a simplified sequence of the messages generated upon window creation:

WM_CREATE (window) or WM_INITDIALOG (dialogbox)
...
WM_SHOWWINDOW
...
WM_ACTIVATE (wParam has WA_* state)

In those days, I did something like this:

C++
#define WMUSER_FIRSTACTIVE  (WM_USER+1)
bool m_bSeenFirstActivation = false;
virtual void DoPostLoadProcessing();

LRESULT WndProc(msg, wparam, lparam)
{
    switch(msg)
    {
        case WM_ACTIVATE:
            if((wparam==WA_ACTIVE) && !m_bSeenFirstActivation)
            {
                m_bSeenFirstActivation = true;
                PostMessage(m_hWnd, WMUSER_FIRSTACTIVE);
            }
            break;
        case WMUSER_FIRSTACTIVE:
            DoPostLoadProcessing(); // Derived classes override this
            break;
    }
}

The theory was that, by posting a message to myself after receiving the initial activation message, I could be confident that:

  1. The dialog had been fully rendered, and
  2. The post-load processing is performed asynchronously and on the dialog's UI thread:
    • Using PostMessage() ensures it's asynchronous. The posted message is added to the end of the queue and gets processed after all pending (UI-related) messages are processed.
    • Doing stuff in the dialog's UI thread is important - many controls get very upset if you try to manipulate them from a thread other than the UI thread.

And, sometimes I did this:

C++
#define IDT_FIRSTACTIVE  0x100
bool m_bSeenFirstActivation = false;
virtual void DoPostLoadProcessing();

LRESULT WndProc(msg, wparam, lparam)
{
    switch(msg)
    {
        case WM_ACTIVATE:
            if((wparam==WA_ACTIVE) && !m_bSeenFirstActivation)
            {
                m_bSeenFirstActivation = true;
                SetTimer(m_hWnd, IDT_FIRSTACTIVE, 50, NULL);
                PostMessage(m_hWnd, WMUSER_FIRSTACTIVE);
            }
            break;
        case WM_TIMER:
            if(wParam == IDT_FIRSTACTIVE)
            {
                KillTimer(m_hWnd, IDT_FIRSTACTIVE);
                DoPostLoadProcessing(); // Derived classes override this
            }
            break;
    }
}

The theory here is essentially the same: Set a quick (50ms) timer in the initial activation. The WM_TIMER message occurs asynchronously and with some extra delay. The timer is immediately killed (we only wanted the first timer message), then the derived class' DoPostLoadProcessing() is called. (Yeah, I know that timers < 55ms are useless on a PC.)

Both techniques work pretty well, and could still be used in .NET, though they're messy.

Solution: Doing It in .NET

The Basics

.NET's addition of the Shown() event simplifies things greatly. Now, in theory, all you'd need to do is handle the Shown() event and do your processing there:

C#
void MyForm_Shown(object sender, EventArgs e)
{
    // Do blocking stuff here
}

Simple, right? Well, almost. It turns out that the Shown() event gets fired before all the form's child controls have fully rendered, so you still have the race condition. My solution to that is to call Application.DoEvents() to clear out the message queue before doing any additional post-processing:

C#
void MyForm_Shown(object sender, EventArgs e)
{
    Application.DoEvents();
    // Do blocking stuff here
}

A More Complete Solution

The problem with the basic solution above is that you must remember to call Application.DoEvents() in each form's Shown() event handler. While this is admittedly a minor nuisance, I've chosen to take the solution one step further by implementing a base dialog class that handles the Shown() event, calls DoEvents(), then invokes an event of its own:

C#
public class BaseForm : Form
{
    public delegate void LoadCompletedEventHandler();
    public event LoadCompletedEventHandler LoadCompleted;

    public BaseForm()
    {
        this.Shown += new EventHandler(BaseForm_Shown);
    }

    void BaseForm_Shown(object sender, EventArgs e)
    {
        Application.DoEvents();
        if (LoadCompleted != null)
            LoadCompleted();
    }
}

And, derived forms simply implement a LoadCompleted() handler:

C#
this.LoadCompleted += new 
      FormLoadCompletedDemo.BaseForm.LoadCompletedEventHandler(
      this.MyFormUsingBase_LoadCompleted);
    
    ...

private void MyFormUsingBase_LoadCompleted()
{
    // Do blocking stuff here
}

and... bonus!: LoadCompleted() appears in Visual Studio's Properties/Events pane:

WinformsUsingShownEvent/LoadCompletedInPropertiesEventsPane.png

(I named the event LoadCompleted so that it would appear immediately after the Load event in the events pane, making it easier to find.)

With that, you can do all your long-duration stuff in LoadCompleted() and be confident that the basic UI will be fully rendered while the user waits. Of course, you should still follow good UI practices such as showing a WaitCursor, maybe disabling the controls until they're usable (a useful visual cue to the user), and perhaps even showing a progressbar for really long waits (e.g.: show a marquee progressbar during a SQL call that takes 5+ seconds).

Demo Project

The attached VS2008/C# project demonstrates the behavior and the solution. It implements a form with four buttons:

WinformsUsingShownEvent/DemoMainForm.png

All four buttons do the same thing, which is pop up a child dialog containing:

  • A listbox that gets populated with 50K items. This population blocks the UI thread for a couple of seconds, which allows the app to demonstrate the solutions to the problem.
  • A few other controls, principally ComboBoxes. For whatever reason, ComboBoxes are particularly prone to incomplete rendering due to blocking code.

Each child form sets the WaitCursor in its Load() event as a visual feedback that things are happening.

However, each button causes a different form-loading behavior:

  • Do processing in Load event - the listbox is loaded in the form's Load() event handler, resulting in the form not appearing until the listbox is fully loaded (bad).
  • Open Form Without DoEvents - the listbox is loaded in the form's Shown() event handler, but Application.DoEvents() is not called, resulting in the form appearing partially rendered until listbox-load completes (very bad).
  • Open Form *with* DoEvents - the listbox is loaded in the form's Shown() event handler and Application.DoEvents() is called, resulting in the form appearing fully rendered until listbox-load completes (good!!!).
  • Open Form derived from BaseForm - Same as Open Form *with* DoEvents, but implemented using a base class and custom event.

Conclusion

Like so many things in Windows (and in life!), the solution to this problem is pretty simple, but figuring it out takes a bit of trial and error. I hope that my solution outlined above saves you some time and helps you create more responsive UIs.

History

  • 12th February, 2010: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
United States United States
I can type "while" and "for" very quickly

Comments and Discussions

 
QuestionLoad time more than doubles Pin
MarkB12310-Feb-21 22:21
MarkB12310-Feb-21 22:21 
AnswerRe: Load time more than doubles Pin
MarkB12310-Feb-21 22:25
MarkB12310-Feb-21 22:25 
SuggestionWithout LoadCompleted Pin
kassiop12-Feb-13 2:09
kassiop12-Feb-13 2:09 
QuestionHow to 'inactivate' the Baseform? Pin
dherrmann30-Oct-12 7:00
dherrmann30-Oct-12 7:00 
AnswerRe: How to 'inactivate' the Baseform? Pin
DLChambers30-Oct-12 7:03
DLChambers30-Oct-12 7:03 
GeneralRe: How to 'inactivate' the Baseform? Pin
dherrmann30-Oct-12 7:12
dherrmann30-Oct-12 7:12 
GeneralMy vote of 5 Pin
Wühlmaus10-Sep-12 23:24
Wühlmaus10-Sep-12 23:24 
GeneralMy vote of 5 Pin
John Sathish Tamilarasu28-May-12 18:58
John Sathish Tamilarasu28-May-12 18:58 
GeneralOnShown trouble Pin
Gonzalo Cao15-Jun-10 22:44
Gonzalo Cao15-Jun-10 22:44 
GeneralGood article! Pin
devendrad_debu3-May-10 7:09
devendrad_debu3-May-10 7:09 
GeneralAn odd behavior (not linked to the article though) Pin
qyte6414-Mar-10 2:12
qyte6414-Mar-10 2:12 
AnswerRe: An odd behavior (not linked to the article though) Pin
DLChambers14-Mar-10 7:12
DLChambers14-Mar-10 7:12 
GeneralWell written and clear Pin
gillindsay8-Mar-10 6:09
professionalgillindsay8-Mar-10 6:09 
GeneralThis works for me. Pin
tonyt20-Feb-10 22:00
tonyt20-Feb-10 22:00 
GeneralRe: This works for me. Pin
DLChambers21-Feb-10 0:13
DLChambers21-Feb-10 0:13 
GeneralBackgroundWorker Pin
BillB416-Feb-10 12:18
BillB416-Feb-10 12:18 
AnswerRe: BackgroundWorker Pin
DLChambers17-Feb-10 2:26
DLChambers17-Feb-10 2:26 
GeneralRe: BackgroundWorker [modified] Pin
BillB417-Feb-10 4:43
BillB417-Feb-10 4:43 
QuestionHow about for UserControls? Pin
BarryGilbert16-Feb-10 10:15
BarryGilbert16-Feb-10 10:15 
AnswerRe: How about for UserControls? Pin
DLChambers16-Feb-10 10:36
DLChambers16-Feb-10 10:36 
GeneralRe: How about for UserControls? Pin
BarryGilbert16-Feb-10 11:30
BarryGilbert16-Feb-10 11:30 
GeneralRe: How about for UserControls? Pin
switchez5-Jun-18 3:32
switchez5-Jun-18 3:32 
QuestionDatabase filled combos and listbox Pin
abacrotto16-Feb-10 2:35
abacrotto16-Feb-10 2:35 
AnswerRe: Database filled combos and listbox Pin
DLChambers16-Feb-10 2:53
DLChambers16-Feb-10 2:53 
GeneralThanks for writing this Pin
Middle Manager16-Feb-10 1:58
Middle Manager16-Feb-10 1:58 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.