views:

309

answers:

3

Hey fellas.

I have an application that is designed to be run across a network. This means that the initial run of this application can take a while. So I've been putting together a splash screen to pretty the process up some.

It uses threading to show the form via a static method. I'm still something of a threading novice, so when I got errors I was a bit confused as to what and why.

Turns out my code is perfectly fine when I run it outside the Visual Studio debugger. But when I run it from inside the debugger, I get the exception:

"Cross-thread operation not valid: Control '' accessed from a thread other than the thread it was created on."

Here's my class:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Threading;

namespace MyApp
{
    public partial class SplashScreen : Form
    {
        private static double OPACITY_INCREMENT = .05;

        private static SplashScreen _splashForm;
        private static SplashScreen SplashForm
        {
            get { return SplashScreen._splashForm; }
            set { SplashScreen._splashForm = value; }
        }

        private static void ShowForm()
        {
            SplashScreen.SplashForm = new SplashScreen();            
            Application.Run(SplashScreen.SplashForm);
        }

        internal static void CloseForm()
        {
            if (SplashScreen.SplashForm != null &&
                SplashScreen.SplashForm.IsDisposed == false)
            {
                // Make it start going away.
                OPACITY_INCREMENT = -OPACITY_INCREMENT;
            }

            SplashScreen.SplashThread = null;
            SplashScreen.SplashForm = null;
        }

        private static Thread _splashThread;
        private static Thread SplashThread
        {
            get { return SplashScreen._splashThread; }
            set { SplashScreen._splashThread = value; }
        }

        internal static void ShowSplashScreen()
        {
            if (SplashScreen.SplashForm != null)
            {
                return;
            }

            SplashScreen.SplashThread = new Thread(new ThreadStart(SplashScreen.ShowForm));
            SplashScreen.SplashThread.IsBackground = true;
            SplashScreen.SplashThread.SetApartmentState(ApartmentState.STA);
            SplashScreen.SplashThread.Start();
        }

        public SplashScreen()
        {
            InitializeComponent();
            this.timerFade.Start();
            this.ClientSize = this.BackgroundImage.Size;
        }

        private void SplashScreen_Load(object sender, EventArgs e)
        {
        }

        private void timerFade_Tick(object sender, EventArgs e)
        {
            if (OPACITY_INCREMENT > 0)
            {
                if (this.Opacity < 1)
                    this.Opacity += OPACITY_INCREMENT;
            }
            else
            {
                if (this.Opacity > 0)
                    this.Opacity += OPACITY_INCREMENT;
                else
                {
                    this.Invoke(new MethodInvoker(this.TryClose));
                }
            }
        }

        private void TryClose()
        {
            if (this.InvokeRequired)
            {
                this.BeginInvoke(new MethodInvoker(this.TryClose));
            }

            this.Close();
        }
    }
}

I'm calling the splash screen from inside the Program.cs Main method.

namespace CIMA
{
    static class Program
    {
// <summary>
        ///     The main entry point for the application.
        /// </summary>
        [STAThread]
        static void Main(string[] args)
        {            
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);

            SplashScreen.ShowSplashScreen();

            // Code omitted for brevity.

            SplashScreen.CloseForm();

            // Code omitted for brevity.
        }
    }
}

I would like to be able to call SplashScreen.CloseForm() from inside one of my other forms, but I haven't got quite so far as trying that out yet. I'm confused as to what the Debugger is up to.

Am I missing something? Or do I have to disable the splash screen if it is being run in the debugger?

If so, what's a nice way of doing this? I want to avoid resorting to Compilation Symbols if possible, because I hate keeping track of them.

+4  A: 

In .NET you are not allowed to access a control from a thread that didn't create the control. In your code it looks like a separate thread is creating the splash form and the main thread tries to close the splash form.

I suggest you do the other way round: create the splash form in the main thread and have the separate thread just update it.

Also, in your TryClose method there's a return missing after the call to BeginInvoke I guess.

You may, however, look at other implementations for splash screens. There are some ready-to-use ones, for example http://www.codeproject.com/KB/cs/prettygoodsplashscreen.aspx

Thorsten Dittmar
The article you linked to is the source I worked from when writing my own screen - I wanted to actually write up a screen from scratch to try and get a feel for what was actually going on.Also: The thing to remember is that the error I'm receiving is only coming up in the <em>debugger</em>. The code itself is fine. It's the visual studio debugger that's the issue.I'll try a straight copy of the source and see if that works out.
Ubiquitous Che
Okay, I downloaded that piece of source code. I'm getting the same error in the debugger, even though it works fine once it's compiled.
Ubiquitous Che
A: 

Okay - I found a workaround.

System.Diagnostics.Debugger.IsAttached

I added the following using statement:

using System.Diagnostics;

... and changed the ShowSplashScreen() method as follows:

    internal static void ShowSplashScreen()
    {
        if (!Debugger.IsAttached)
        {
            if (SplashScreen.SplashForm != null)
            {
                return;
            }

            SplashScreen.SplashThread = new Thread(new ThreadStart(SplashScreen.ShowForm));
            SplashScreen.SplashThread.IsBackground = true;
            SplashScreen.SplashThread.SetApartmentState(ApartmentState.STA);
            SplashScreen.SplashThread.Start();
        }
    }

So my program omits the splash screen if I run from Visual Studio, or it runs with a Splash screen if not. No more altering my code every time I test a rebuild.

I'm still at a loss for why I was getting the error for running inside a debugger and not for the general code, so I'm hoping some clever bugger on this site can come along and explain that one.

Otherwise, this work-around is good enough for me.

Ubiquitous Che
Thorsten's answer above is correct. The error that you're getting when using the debugger is an important error to heed. You're a self-described multi-threading novice and I would suggest following the "rules", whether or not you can avoid the error, of not accessing a control from a thread that didn't create the control. There's no need to start developing bad practices early in your multi-threading experience.
Kyle Gagnet
Cheers Kyle - Apologies if I took a wrong turn there. I'd presumed that the article Thorsten linked to was something he thought was correct. Should have been more careful. I'll correct it now - thanks for following that up, I would have carried on blissfully unaware otherwise.
Ubiquitous Che
A: 

Okay - here's another solution. I think this is right now. I'm only including it in case someone else comes along with the same problem. Short version: Thorsten was right.

The problem was the nature of my splash screen's closing function. I was triggering a fadeout on the splash form using a timer internal to the form. The timer would decrement the opacity of the form towards zero, then close the splash form when zero was reached.

To solve the problem, I did a few things:

  1. Renamed my original timerFade to timerFadeIn
  2. Created a new Timer on the Splash form: timerFadeOut
  3. Altered my SpalshScreen static methods so that the Form itself was being created on the same thread as the Program.Main method.
  4. Arranged the static methods so that timerFadeOut.Start() is being called from the Program.Main method.
  5. Used Invoke in the timerFadeOut_Tick event handler for both Opacity down-steps as well as the final Close method.

After these changes, the Splash screen works as intended. Well... Almost. The new splash screen doesn't start fading until after the main form is up - the main thread needs to get enough processing out of the road that the timer can actually start doing its job. But this is actually a good thing - I was meaning to change things around to make the Splash screen fades just after the main form loads anyway, so this turned out to be a plus.

Code below for the interested. Also, there's still a chance that I've implemented this cross-threading behavior incorrectly - I invite criticism. As Kyle suggested, I want to start out with good threading habits, not bad ones.

For anyone who doesn't want a FadeOut effect, I left in the code (commented out) for closing the form without use of the timer in the CloseForm() method.

public partial class SplashScreen : Form
{
    private static double OPACITY_INCREMENT = .05;

    private static SplashScreen _splashForm;
    private static SplashScreen SplashForm
    {
        get { return SplashScreen._splashForm; }
        set { SplashScreen._splashForm = value; }
    }

    private static void ShowForm()
    {        
        Application.Run(SplashScreen.SplashForm);
    }

    internal static void CloseForm()
    {
        if (SplashScreen.SplashForm != null &&
            SplashScreen.SplashForm.IsDisposed == false)
        {
            // Make it start going away.
            SplashScreen.SplashForm.FadeOut();
        }

        //if (SplashScreen.SplashForm.InvokeRequired)
        //{
        //    SplashScreen.SplashForm.Invoke(new MethodInvoker(SplashScreen.SplashForm.Close));
        //}
        //else
        //{
        //    SplashScreen.SplashForm.DoClose();
        //}

        SplashScreen.SplashThread = null;
        SplashScreen.SplashForm = null;
    }

    private void FadeOut()
    {
        this.timerFadeOut.Start();
    }

    private static Thread _splashThread;
    private static Thread SplashThread
    {
        get { return SplashScreen._splashThread; }
        set { SplashScreen._splashThread = value; }
    }

    internal static void ShowSplashScreen()
    {
        if (SplashScreen.SplashForm != null)
        {
            return;
        }

        SplashScreen.SplashForm = new SplashScreen();
        SplashScreen.SplashThread = new Thread(new ThreadStart(SplashScreen.ShowForm));
        SplashScreen.SplashThread.IsBackground = true;
        SplashScreen.SplashThread.SetApartmentState(ApartmentState.STA);
        SplashScreen.SplashThread.Start();
    }

    public SplashScreen()
    {
        InitializeComponent();
        this.ClientSize = this.pictureBox1.Size;
    }

    private void SplashScreen_Load(object sender, EventArgs e)
    {

    }

    private void timerFadeIn_Tick(object sender, EventArgs e)
    {
        if (OPACITY_INCREMENT > 0)
        {
            if (this.Opacity < 1)
            {
                this.Opacity += OPACITY_INCREMENT;
            }
            else
            {
                this.timerFadeIn.Stop();
            }
        }
    }

    private void SplashScreen_Shown(object sender, EventArgs e)
    {
        this.timerFadeIn.Start();
    }

    private void timerFadeOut_Tick(object sender, EventArgs e)
    {
        if (OPACITY_INCREMENT > 0)
        {
            if (this.Opacity > 0)
            {
                if (this.InvokeRequired)
                {
                    this.Invoke(new MethodInvoker(this.FadeOutStep));
                }
                else
                {
                    this.FadeOutStep();
                }                    
            }
            else
            {
                this.timerFadeOut.Stop();

                if (this.InvokeRequired)
                {
                    this.Invoke(new MethodInvoker(this.Close));
                }
                else
                {
                    this.Close();
                }
            }
        }
    }

    private void FadeOutStep()
    {
        this.Opacity -= OPACITY_INCREMENT;
    }
}
Ubiquitous Che