Reputation: 7148
I recently added a login form to my application. This form shows prior to a splash screen that is shown while the main application form is loaded and various IO objects are instantiated.
Prior to the login form this is how my Program.cs would start the application
if (mutex.WaitOne(TimeSpan.Zero, true))
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
SplashScreen.ShowSplashScreen();
Application.Run(MainForm.Instance);
mutex.ReleaseMutex();
}
With the new login for the application is now started like so
if (mutex.WaitOne(TimeSpan.Zero, true))
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
UserSessionSelection ussDialog = new UserSessionSelection();
if (ussDialog.ShowDialog() == DialogResult.OK)
{
SplashScreen.ShowSplashScreen();
Application.Run(MainForm.Instance);
}
mutex.ReleaseMutex();
}
Here is the SplashScreen
class
public partial class SplashScreen : Form
{
public static SplashScreen Instance { get { return lazyInstance.Value; } }
private static readonly Lazy<SplashScreen> lazyInstance =
new Lazy<SplashScreen>(() => new SplashScreen());
private SplashScreen()
{
InitializeComponent();
CenterToScreen();
TopMost = true;
}
static public void NewLoadingUpdate(String message, int percent)
{
NewUpdateDelegate nud = new NewUpdateDelegate(NewLoadingUpdateInternal);
SplashScreen.Instance.Invoke(nud, new object[] { message, percent });
}
static private void NewLoadingUpdateInternal(String message, int percent)
{
SplashScreen.Instance.lblLoadingText.Text = message;
SplashScreen.Instance.pgProgress.Value = percent;
}
private delegate void NewUpdateDelegate(String message, int percent);
private delegate void CloseDelegate();
static public void ShowSplashScreen()
{
Thread thread = new Thread(new ThreadStart(SplashScreen.ShowForm));
thread.IsBackground = true;
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
}
static private void ShowForm()
{
Application.Run(SplashScreen.Instance);
}
static public void CloseForm()
{
SplashScreen.Instance.Invoke(new CloseDelegate(SplashScreen.CloseFormInternal));
}
static private void CloseFormInternal()
{
SplashScreen.Instance.Close();
}
}
The error specifically happens with ShowForm
the specific text is
An unhandled exception of type 'System.InvalidOperationException'
occurred in System.Windows.Forms.dll
Additional information: Cross-thread operation not valid: Control
'SplashScreen' accessed from a thread other than the thread it was created on.
The error only happens about 1/20 times when the application starts. I never encountered it prior to the login form.
Any ideas as to what causes this?
EDIT: For those late to the party, I think this SO question will help. Wait for a thread to actually start in c#
Upvotes: 0
Views: 552
Reputation: 63772
You need to create the SplashScreen
on the same thread where you're using it.
But wait, that's what I'm doing, isn't it? Well, no - you're seeing a quite typical race condition.
The core of your problem, I suspect, is using Lazy
to initialize the splash screen, combined with not waiting for the form to be created in your ShowSplashScreen
method. In your main form, you refer to SplashScreen.Instance
. Now, if the first thread that tried to read the instance is your splashscreen message loop, you're fine - that's the 19 in 20.
However, it's perfectly possible that the main UI thread gets there first - you don't block in ShowSplashScreen
. In that case, the splash screen is created on the main UI thread, and you're in trouble - and good thing you're not using InvokeRequired
, because that would have hidden the error even further.
Why does this have anything to do with the new login form? Well, I suspect that it's a timing thing, really - your code is broken with or without the login form. However, ShowDialog
starts a new message loop, similar to Application.Run
. This also means that a synchronization context has to be created - something that would otherwise only happen on your Application.Run(MainForm.Instance)
line. The key point is that you've managed to make your race condition much wider - there is no longer as much time between the ShowSplashScreen
call and the first time the splash screen is accessed in MainForm
- and the result is BOOM.
Do not allow the ShowSplashScreen
method to return until the instance is properly created, and you'll be fine. Multi-threading is hard - don't try to guess your way around. A good starting point would be http://www.albahari.com/threading/ - make sure you pay plenty of attention to proper synchronization and signalling.
Upvotes: 2