views:

71

answers:

2

I have encountered an odd issue with the way I am showing a splash form, that causes an InvalidAsynchronousStateException to be thrown.

First of all, here is the code for Main{} where I start the splash form:

[STAThread]
static void Main(string[] args)
{
    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);

    Thread splash = new Thread(new ThreadStart(ShowSplash));
    splash.Start();

     Application.Run(new MainForm());
}

static void ShowSplash()
{
    using (SplashForm splash = new SplashForm())
    {
        Application.Run(splash);
    }
}

I am using .NET2.0, with Win XP.

During some testing where the app was left running for may hours, I noticed that the number of exceptions would occasionally increase by one or two. (Numbers obtained by PerfMon, viewing '# of Exceps Thrown' counter.) These exceptions seem to be caught and swallowed by the runtime, because they do not ripple up and cause anything to go wrong in the app itself. At least nothing that I can determine anyway.

I have discovered that the exception is thrown when the UserPreferenceChanged event is fired by the system. Since finding this out, I can generate the exception at will by changing the background picture or screen saver, etc.

I am not explicitly subscribing to this event myself anywhere in code, but I understand (via the power of Google) that all top level controls and forms subscribe to this event automatically.

I still have not determined why this event is being fired in the first place, as it appears to happen while the app is running over night, but I guess that is another mystery to be solved.

Now, if I stop the splash form thread from running, the exception disappears. Run the thread, it comes back. So, it appears that something is not unsubscribing from the event, and this is causing the subsequent exception perhaps?

Interestingly, if I substitute my splash form with a default, out of the box Form, the problem still remains:

static void ShowSplash()
{
    using (Form splash = new Form())
    {
        Application.Run(splash);
    }
}

While this form is being displayed, any UserPreferenceChanged events do not cause any exceptions. As soon as the form is closed, and the thread exits, exceptions will be thrown.

Further research has lead me to this Microsoft article, that contains the following comment:

Common causes are a splash screens created on a secondary UI thread or any controls created on worker threads.

Hmm, guilty as charged by the looks of it. Note that my app is not freezing or doing anything untoward though.

At the moment, this is more of a curiosity than anything else, but I am conecerned that there may be some hidden nasties here waiting to bite in the future.

To me, it looks like the form or the message pump started by Application.Run is not cleaning up properly when it terminates.

Any thoughts?

+1  A: 

I was able to simulate your problem and I can offer a work around, but there might be a better option out there since this was the first time I had come across this.

One option to avoid the exception is to not actuall close the splash screen, but rather just hide it. Something like this

public partial class SplashForm : Form
{
  public SplashForm()
  {
    InitializeComponent();
  }

  // Not shown here, this is wired to the FormClosing event!!!
  private void SplashForm_FormClosing(object sender, FormClosingEventArgs e)
  {      
    e.Cancel = true;
    this.Hide();
  }
}

Then it will be important that you make the thread that you run the splash screen on a background thread to ensure that the application is not kept alive by the splash screen thread. So your code would look something like this

[STAThread]  
static void Main(string[] args)  
{  
    Application.EnableVisualStyles();  
    Application.SetCompatibleTextRenderingDefault(false);  

    Thread splash = new Thread(new ThreadStart(ShowSplash));          
    splash.IsBackground = true;
    splash.Start();  

     Application.Run(new MainForm());  
}  

static void ShowSplash()  
{  
    using (SplashForm splash = new SplashForm())  
    {  
        Application.Run(splash);  
    }  
}
Chris Taylor
@Chris : Thanks, that will indeed work and I may use this option. I'm glad that you could reproduce this issue. (Is it even an issue?) Any thoughts on why it happens? I'm getting bogged down in Reflector looking for clues. Trouble is, I have no real idea what I'm looking at. :)
Andy
+1  A: 

Yes, you are running afoul with the SystemEvents class. That class creates a hidden window that listens to system events. Particularly the UserPreferenceChanged event, a lot of controls use that event to know when they need to repaint themselves because the system colors were changed.

Problem is, the initialization code that creates the window is very sensitive to the apartment state of the thread that calls it. Which in your case is wrong, you didn't call Thread.SetApartmentState() to switch to STA. That's very important for threads that display a UI.

Beware that your workaround isn't actually a fix, the system events will be raised on the wrong thread. Your splash thread instead of the UI thread of your program. You'll still get random and extremely hard to diagnose failure when an actual system event gets fired. Most infamously when the user locks the workstation, the program deadlocks when it is unlocked again.

I think calling Thread.SetApartmentState() should fix your problem. Not 100% sure, these UI thread interactions are very difficult to analyze and I haven't gotten this wrong yet. Note that .NET already has very solid support for splash screens. It definitely gets details like this right.

Hans Passant
@Hans : Thank you for your answer. Unfortunately, the suggestion of SetApartmentState(ApartmentState.STA) didn't have any effect as the exception is still thrown. Perhaps this issue is actually a bit more insidious than it first appeared. The more I think about the workaround, the less I am happy with it. The link you provided is interesting however, and worth of a closer look.
Andy
I just now clicked on that link and realized I just told you things you already knew. Well, WindowsFormsApplicationBase will definitely solve your problem.
Hans Passant