I set a WPF ProgressBar's Is indeterminate to True and Visibility to Hidden. In a event handler I am trying to make the ProgressBar visible while a ObservableCollection is being updated (and a ListView whose ItemsSource is the ObservableCollection). I hope DoEvents() from System.Windows.Forms.Application can make it visible but it does not.
I notice SetPBarHelper(() => { ..} ) usually finish much earlier than the ListView shows visual changes.
How do I make the ProgressBar Visible inside event handler codes ?
How to tell if my ListView is still being updated even though the ObserableCollection has finished adding items ?
<ProgressBar x:Name="GeneralProgressBar" Width="300" Height="15" IsIndeterminate="True" Visibility="Hidden"/>
private void SetPBar(bool isVisible)
{
if (isVisible)
GeneralProgressBar.Visibility = System.Windows.Visibility.Visible;
else
GeneralProgressBar.Visibility = System.Windows.Visibility.Hidden;
}
private void SetPBarHelper(Action handler)
{
SetPBar(true); // try to make ProgressBar visible
System.Windows.Forms.Application.DoEvents();
handler(); // use the event handling, which run database query
SetPBar(false); // try to make ProgressBar disappear
}
private void CommandForumImagesBinding_Executed(object sender, ExecutedRoutedEventArgs e)
{
SetPBarHelper(() =>
{
if (e.Parameter == null)
return;
var vm = e.Parameter as ForumViewModel;
if (vm != null)
{
}
});
}
public sealed class ImageGroupCollection : ObservableCollection<ImageGroup>
{
public ImageGroupCollection() : base() { }
public void Update(DateTime start, DateTime end)
{
ClearItems();
var list = MyDatabase.GetRecords(start, end);
if (list != null)
{
foreach (var g in list)
{
Add(g);
}
}
}
}
Your problem is simply because you are blocking the UI thread. You can't do that.
By the time the UI thread is running the message loop again you have set ProgressBar.Visible = false. The ProgressBar is never drawn.
Assuming you are using .net 4.5 You need to rewrite the code as follows.
private async Task SetPBarHelper(Action handler)
{
SetPBar(true); // try to make ProgressBar visible
System.Windows.Forms.Application.DoEvents();
await Task.Run(handler); // use the event handling, which run database query
SetPBar(false); // try to make ProgressBar disappear
}
But overall you need to return control of the UI thread back to the application loop as soon as possible to allow it to redraw the windows, and run the update on a WorkerThread.
Related
We have built a huge winforms project, already in progress for multiple years.
Sometimes, our users get an exception which looks like this one.
The resolution of this problem seems to be:
don't acces UI components from a background thread
.
But since our project is a very big project with a lot of different threads, we don't succeed in finding all these.
Is there a way to check (with some tool or debugging option) which components are called from a background thread?
To clarify:
I created a sample winforms project with a single Form, containing two Button
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e)
{
button1.Text = "Clicked!";
}
private void button2_Click(object sender, EventArgs e)
{
Task.Run(() =>
{
button2.BackColor = Color.Red; //this does not throw an exception
//button2.Text = "Clicked"; //this throws an exception when uncommented
});
}
}
The background color of button2 is set to red when the button is clicked. This happens in a background thread (which is considered bad behavior). However, it doesn't (immediately) throw an exception. I would like a way to detect this as 'bad behavior'. Preferably by scanning my code, but if it's only possible by debugging, (so pausing as soon as a UI component is accessed from a background thread) it's also fine.
I've got 2 recommendations to use together, the first is a Visual Studio Plugin called DebugSingleThread.
You can freeze all the threads and work on one at a time (obviously the non-main-UI threads) and see each threads access to controls. Tedious I know but not so bad with the second method.
The second method is to get the steps in order to reproduce the problem. If you know the steps to reproduce it, it will be easier to see whats causing it. To do this I made this User Action Log project on Github.
It will record every action a user makes, you can read about it here on SO: User Activity Logging, Telemetry (and Variables in Global Exception Handlers).
I'd recommend you also log the Thread ID, then when you have been able to reproduce the problem, go to the end of the log and work out the exact steps. Its not as painful as it seems and its great for getting application telemetry.
You might be able to customise this project, eg trap a DataSource_Completed event or add a dummy DataSource property that sets the real Grids DataSource property and raises an INotifyPropertyChanged event - and if its a non-main thread ID then Debugger.Break();.
My gut feeling is you're changing a control's (eg a grid) data source in a background thread (for that non-freeze feel) and thats causing a problem with synchronisation. This is what happened to the other DevExpress customer who experienced this. Its discussed here in a different thread to the one you referenced.
Is your app set to ignore cross threading intentionally?
Cross-thread operations should be blowing up all the time in winforms. It checks for them like crazy in just about every method. for a starting point check out https://referencesource.microsoft.com/#System.Windows.Forms/winforms/Managed/System/WinForms/Control.cs.
Somewhere in your app, somebody might have put this line of code:
Control.CheckForIllegalCrossThreadCalls = False;
Comment that out and run the app, then follow the exceptions.
(Usually you can fix the problem by wrapping the update in an invoke, e.g., in a worker thread if you see textbox1.text=SomeString; change it to `textbox.invoke(()=>{textbox1.text=SomeString;});.
You may also have to add checking for InvokeRequired, use BeginInvoke to avoid deadlocks, and return values from invoke, those are all separate topics.
this is assuming even a moderate refactor is out of the question which for even a medium sized enterprise app is almost always the case.
Note: it's not possible to guarantee successful discovery of this case thru static analysis (that is, without running the app). unless you can solve the halting problem ... https://cs.stackexchange.com/questions/63403/is-the-halting-problem-decidable-for-pure-programs-on-an-ideal-computer etc...
I did this to search for that specific situation but of course, need to adjust it to your needs, but the purpose of this is to give you at least a possibility.
I called this method SearchForThreads but since it's just an example, you can call it whatever you want.
The main idea here is perhaps adding this Method call to a base class and call it on the constructor, makes it somewhat more flexible.
Then use reflection to invoke this method on all classes deriving from this base, and throw an exception or something if it finds this situation in any class.
There's one pre req, that is the usage of Framework 4.5.
This version of the framework added the CompilerServices attribute that gives us details about the Method's caller.
The documentation for this is here
With it we can open up the source file and dig into it.
What i did was just search for the situation you specified in your question, using rudimentary text search.
But it can give you an insight about how to do this on your solution, since i know very little about your solution, i can only work with the code you put on your post.
public static void SearchForThreads(
[System.Runtime.CompilerServices.CallerMemberName] string memberName = "",
[System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "",
[System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0)
{
var startKey = "this.Controls.Add(";
var endKey = ")";
List<string> components = new List<string>();
var designerPath = sourceFilePath.Replace(".cs", ".Designer.cs");
if (File.Exists(designerPath))
{
var designerText = File.ReadAllText(designerPath);
var initSearchPos = designerText.IndexOf(startKey) + startKey.Length;
do
{
var endSearchPos = designerText.IndexOf(endKey, initSearchPos);
var componentName = designerText.Substring(initSearchPos, (endSearchPos - initSearchPos));
componentName = componentName.Replace("this.", "");
if (!components.Contains(componentName))
components.Add(componentName);
} while ((initSearchPos = designerText.IndexOf(startKey, initSearchPos) + startKey.Length) > startKey.Length);
}
if (components.Any())
{
var classText = File.ReadAllText(sourceFilePath);
var ThreadPos = classText.IndexOf("Task.Run");
if (ThreadPos > -1)
{
do
{
var endThreadPos = classText.IndexOf("}", ThreadPos);
if (endThreadPos > -1)
{
foreach (var component in components)
{
var search = classText.IndexOf(component, ThreadPos);
if (search > -1 && search < endThreadPos)
{
Console.WriteLine($"Found a call to UI thread component at pos: {search}");
}
}
}
}
while ((ThreadPos = classText.IndexOf("Task.Run", ++ThreadPos)) < classText.Length && ThreadPos > 0);
}
}
}
I hope it helps you out.
You can get the Line number if you split the text so you can output it, but i didn't want to go through the trouble, since i don't know what would work for you.
string[] lines = classText.Replace("\r","").Split('\n');
Try that:
public static void Main(string[] args)
{
// Add the event handler for handling UI thread exceptions to the event.
Application.ThreadException += new ThreadExceptionEventHandler(exception handler);
// Set the unhandled exception mode to force all Windows Forms errors to go through the handler.
Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException);
// Add the event handler for handling non-UI thread exceptions to the event.
AppDomain.CurrentDomain.UnhandledException += // add the handler here
// Runs the application.
Application.Run(new ......);
}
Then you can log the message and the call stack and that should give you enough information to fix the issue.
I recommend you update your GUI to handle this situation automatically for your convenience. You instead use a set of inherited controls.
The general principle here is to override the property Set methods in a way to make them Thread Safe. So, in each overridden property, instead of a straight update of the base control, there's a check to see if an invoke is required (meaning we're on a separate thread the the GUI). Then, the Invoke call updates the property on the GUI thread, instead of the secondary thread.
So, if the inherited controls are used, the form code that is trying to update GUI elements from a secondary thread can be left as is.
Here is the textbox and button ones. You would add more of them as needed and add other properties as needed. Rather than putting code on individual forms.
You don't need to go into the designer, you can instead do a find/replace on the designer files only. For example, in ALL designer.cs files, you would replace System.Windows.Forms.TextBox with ThreadSafeControls.TextBoxBackgroundThread and System.Windows.Forms.Button with ThreadSafeControls.ButtonBackgroundThread.
Other controls can be created with the same principle, based on which control types & properties are being updated from the background thread.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Forms;
namespace ThreadSafeControls
{
class TextBoxBackgroundThread : System.Windows.Forms.TextBox
{
public override string Text
{
get
{
return base.Text;
}
set
{
if (this.InvokeRequired)
this.Invoke((MethodInvoker)delegate { base.Text = value; });
else
base.Text = value;
}
}
public override System.Drawing.Color ForeColor
{
get
{
return base.ForeColor;
}
set
{
if (this.InvokeRequired)
this.Invoke((MethodInvoker)delegate { base.ForeColor = value; });
else
base.ForeColor = value;
}
}
public override System.Drawing.Color BackColor
{
get
{
return base.BackColor;
}
set
{
if (this.InvokeRequired)
this.Invoke((MethodInvoker)delegate { base.BackColor = value; });
else
base.BackColor = value;
}
}
}
class ButtonBackgroundThread : System.Windows.Forms.Button
{
public override string Text
{
get
{
return base.Text;
}
set
{
if (this.InvokeRequired)
this.Invoke((MethodInvoker)delegate { base.Text = value; });
else
base.Text = value;
}
}
public override System.Drawing.Color ForeColor
{
get
{
return base.ForeColor;
}
set
{
if (this.InvokeRequired)
this.Invoke((MethodInvoker)delegate { base.ForeColor = value; });
else
base.ForeColor = value;
}
}
public override System.Drawing.Color BackColor
{
get
{
return base.BackColor;
}
set
{
if (this.InvokeRequired)
this.Invoke((MethodInvoker)delegate { base.BackColor = value; });
else
base.BackColor = value;
}
}
}
}
I am having an odd problem with protecting a section of code. My application is a tray app. I create a NotifyIcon inside my class (ApplicationContext). I have assigned a balloon click handler and a double click handler to the NotifyIcon object. there is also a context menu but I am not showing all code. Only important pieces.
public class SysTrayApplicationContext: ApplicationContext
{
private NotifyIcon notifyIcon;
private MainForm afDashBoardForm;
public SysTrayApplicationContext()
{
this.notifyIcon = new NotifyIcon();
this.notifyIcon.BalloonTipClicked += notifyIcon_BalloonTipClicked;
this.notifyIcon.MouseDoubleClick += notifyIcon_MouseDoubleClick;
// ... more code
}
Both handlers launch or create/show my form:
private void notifyIcon_MouseDoubleClick(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
openDashboard();
}
}
private void notifyIcon_BalloonTipClicked(object sender, EventArgs e)
{
openDashboard();
}
private void openDashboard()
{
if (dashBoardForm != null)
{
log.Debug("Dashboard form created already, so Activate it");
dashBoardForm.Activate();
}
else
{
log.Debug("Dashboard form does not exist, create it");
dashBoardForm = new MainForm();
dashBoardForm.Show();
}
}
There is a problem with the above code. Maybe more than 1. Issue: it is possible to display 2 dashboard forms which is not what I want. If user double clicks on tray icon while balloon message is displaying causes a race condition in openDashboard. I can reproduce this easily. So I added a lock around the code in openDashboard code and, to my surprise, that did NOT prevent 2 dashboard forms from displaying. I should not be able to create 2 MainForms. Where am I going wrong here?
here is the updated code with lock statement:
private void openDashboard()
{
lock (dashBoardFormlocker)
{
if (dashBoardForm != null)
{
log.Debug("Dashboard form created already, so Activate it");
dashBoardForm.Activate();
}
else
{
log.Debug("Dashboard form does not exist, create it");
dashBoardForm = new MainForm();
dashBoardForm.Show();
}
}
}
Note: lock object was added to the class and initialized in constructor.
private object dashBoardFormlocker;
UPDATE: Showing more code. this is how code gets started :
static void Main()
{
if (SingleInstance.Start())
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
XmlConfigurator.Configure();
// For a system tray application we don't want to create
// a form, we instead create a new ApplicationContext. The Run method takes
Application.Run(new SysTrayApplicationContext());
SingleInstance.Stop();
SingleInstance.Dispose();
}
}
}
UPDATE 2: Provide more code for clarity
public partial class MainForm : Form
{
public MainForm()
{
log.Trace("MainForm constructor...");
InitializeComponent();
// ... code not shown
this.label_OSVersion.Text = getOSFriendlyName();
// .. more code
}
private string getOSFriendlyName()
{
try
{
string result = string.Empty;
var mgmtObj = (from x in new ManagementObjectSearcher("SELECT Caption FROM Win32_OperatingSystem").Get().OfType<ManagementObject>()
select x.GetPropertyValue("Caption")).FirstOrDefault();
result = mgmtObj != null ? mgmtObj.ToString() : string.Empty;
OperatingSystem os = Environment.OSVersion;
String sp = os.ServicePack ?? string.Empty;
return !string.IsNullOrWhiteSpace(result) ? result + sp : "Unknown";
}
catch (System.Exception ex)
{
log.Error("Error trying to get the OS version", ex);
return "Unknown";
}
}
}
The main UI thread must always pump a message loop to support communication from COM components.
So when you do a blocking operation from the UI thread like locking or joining a thread, (EDIT: edited based on Peter Duniho's fix) the UI thread will enter an 'alertable' state, allowing COM to dispatch certain type of messages, which in turn can cause re-entrancy issues like in your scenario.
Look at the answer to this question (Why did entering a lock on a UI thread trigger an OnPaint event?) for a much more accurate explanation.
Looking at the source code of ManagementObjectSearcher.Get there is a lock (inside Initialize), and since you call it from the constructor of your form, it may lead to the second event triggering while the form's constructor has not finished. The assignment to the dashBoardFormlocker variable only happens after the constructor finishes, so that would explain why it was null on the second entry.
The moral of the story is never do blocking operations on the UI thread.
Without a good, minimal, complete code example that reliably reproduces the problem, it's impossible to know for sure what the problem is. But the guess by answerer tzachs seems reasonable. If so, you can fix your problem by changing your method to look like this:
private bool _dashboardOpen;
private void openDashboard()
{
if (_dashboardOpen)
{
if (dashBoardForm != null)
{
log.Debug("Dashboard form created already, so Activate it");
dashBoardForm.Activate();
}
}
else
{
log.Debug("Dashboard form does not exist, create it");
_dashboardOpen = true;
dashBoardForm = new MainForm();
dashBoardForm.Show();
}
}
In that way, any re-entrant attempt to open the window will be detected. Note that you still need the check for null before actually activating; you can't activate a window that hasn't actually finished being created yet. The subsequent call to Show() will take care of activation anyway, so ignoring the activation in the re-entrant case shouldn't matter.
I have two threads which uses the BeginInvoke method to change some Windows Form object's (Panel and Label) visibility attribute to false.The problem is that I'm not sure when the change happens. I can see that the panel is not there (so the BeginInvoke method works) but my if condition to check the visibility status always returns true the first time the form is activated.
bool notVisible = false;
private void LunchMainScreen_Activated(object sender, EventArgs e) {
String CurrentSite = "";
List<DateTime> availableDates = new List<DateTime>();
// Get available dates
Thread availableDatesThread = new Thread(delegate() {
availableDates = LunchUserPreferences.GetUserAvailableDates();
changeObjVisible(notVisible, selectAvailabilityPanel);
changeObjVisible(notVisible, whenLbl);
}
});
availableDatesThread.Start();
// Get user current site
Thread checkSiteThread = new Thread(delegate() {
CurrentSite = LunchUserPreferences.GetUserSite();
changeObjVisible(notVisible, selectSitePanel);
changeObjVisible(notVisible, whereLbl);
}
updateText(CurrentSite, CurrentSiteSetLbl);
});
checkSiteThread.Start();
while (selectSitePanel.Visible == false && selectAvailabilityPanel.Visible == false) {
// it NEVER gets here, even though the panels are NOT visible when the program loads
WhoLunchTable.Visible = false;
WhoLunchTable.SuspendLayout();
listOfAvailableGroups.Clear();
WhoLunchTable.Controls.Clear();
WhoLunchTable.RowStyles.Clear();
PopulateTable();
WhoLunchTable.Visible = true;
WhoLunchTable.ResumeLayout();
break;
}
}
private delegate void changeObjVisibleDelegate(bool visibility, object obj);
private void changeObjVisible(bool visibility, object obj) {
if (this.InvokeRequired) {
this.BeginInvoke(new changeObjVisibleDelegate(changeObjVisible), new object[] { visibility, obj });
return;
}
// downcast to the correct obj
if (obj is Panel) {
Panel panel = (Panel)obj;
panel.Visible = visibility;
}
if (obj is Label) {
Label lbl = (Label)obj;
lbl.Visible = visibility;
}
}
private delegate void updateTextDelegate(string text, Label lbl);
private void updateText(string text, Label lbl) {
if (this.InvokeRequired) {
this.BeginInvoke(new updateTextDelegate(updateText), new object[] { text, lbl });
return;
}
lbl.Text = text;
}
It does work fine when the Form is activated for the second time, for example:
The form loads for the first time and it doesn't go inside the while loop.
I minimize the form/program.
The LunchMainScreen_Activated runs again and it works as it should because it recognises that the panels are not visible.
UPDATE:
I had an idea after reading AlexF answer which solved the problem but it doesn't look like the ideal solution:
I've created a while condition that will only stop when both threads are not alive and an if condition inside it that will get this point in time and execute what I need:
while (availableDatesThread.IsAlive || checkSiteThread.IsAlive) {
// At least one thread is still alive, keeps checking it...
if (!availableDatesThread.IsAlive && !checkSiteThread.IsAlive) {
// Both threads should be dead now and the panels not visible
WhoLunchTable.Visible = false;
WhoLunchTable.SuspendLayout();
listOfAvailableGroups.Clear();
WhoLunchTable.Controls.Clear();
WhoLunchTable.RowStyles.Clear();
PopulateTable();
WhoLunchTable.Visible = true;
WhoLunchTable.ResumeLayout();
break;
}
}
Reading your code the first time the code doesn't enter in the while loop because selectSitePanel.Visible and selectAvailabilityPanel.Visible are true: this is because the availableDatesThread.Start(); and checkSiteThread.Start(); are started but not finished; those two calls are not blocking so the code continues and skips the while.
Meanwhile the two backgrund threads finishes so the second time the "Activated" event is raised the variables values are "correct" (at least for the last cycle).
Without waiting for the threads to finish you are rushing through the code before having a result for the needed value.
In other words, it's better to not use a background thread to update an interface for the use you need.
If you need you may continue to use the code the way you are using it but moving the "while" section in two separate functions: they may be called when the threads have finished their work and refresh the window in this moment and not in the "activate" event.
Okay, so I have an image element in my xaml inside a usercontrol
<Image HorizontalAlignment="Left" Margin="0,0,0,0" Name="imagePiece" Stretch="Fill" VerticalAlignment="Top" Source="{Binding Path=PieceGrid}" />
In the code behind I have in the constructor this:
AddHandler(FrameworkElement.MouseDownEvent, new MouseButtonEventHandler(imagePiece_MouseDown), true);
AddHandler(FrameworkElement.MouseUpEvent, new MouseButtonEventHandler(imagePiece_MouseUp), true);
I dynamically create several of these usercontrol's. The mousedown and mouseup work great and do exactly as I want. I can continually click the image and the event will fire every time.
However, I implemented a Timer object that fires every so often. That timer changes the image. In order to do that I have to do this:
private void releaseTheDaemon(object source, ElapsedEventArgs e)
{
int numberOfPieces = 0;
List<GridPieceView> allPlacedPieces = new List<GridPieceView>();
foreach (List<GridPieceView> list in piece)
{
foreach (GridPieceView p in list)
{
if (p.GridOwner != View.Owner.NotSelected)
{
allPlacedPieces.Add(p);
numberOfPieces = numberOfPieces + 1;
}
}
}
int randIndex = randomNumber.Next(0, numberOfPieces);
if (allPlacedPieces.Count > 0)
{
Dispatcher.Invoke(DispatcherPriority.Normal, new Action<GridPieceView>(daemonActions), allPlacedPieces[randIndex]);
}
}
private void daemonActions(GridPieceView gridPiece)
{
gridPiece.GridOwner = View.Owner.NotSelected;
gridPiece.setPieceWhite();
}
The setPieceWhite() method is what updates the view. After that update occurs, the mousedown and mouseup are lost. They won't fire.
For kicks and giggles, I tried doing this to the method:
private void daemonActions(GridPieceView gridPiece)
{
gridPiece.GridOwner = View.Owner.NotSelected;
gridPiece.setPieceWhite();
AddHandler(FrameworkElement.MouseDownEvent, new MouseButtonEventHandler(gridPiece.imagePiece_MouseDown), true);
AddHandler(FrameworkElement.MouseUpEvent, new MouseButtonEventHandler(gridPiece.imagePiece_MouseUp), true);
}
But that really messed up the events, and anytime I clicked on one of the images, all the rest of the images that previously went through this method, are fired as well.
Anyways, does anyone know how I am suppose to handle this issue?
I have a Winforms Application with a TabStrip Control. During runtime, UserControls are to be loaded into different tabs dynamically.
I want to present a "User Control xyz is loading" message to the user (setting an existing label to visible and changing its text) before the UserControl is loaded and until the loading is completely finished.
My approaches so far:
Trying to load the User Control in a BackgroundWorker thread. This fails, because I have to access Gui-Controls during the load of the UserControl
Trying to show the message in a BackgroundWorker thread. This obviously fails because the BackgroundWorker thread is not the UI thread ;-)
Show the Message, call DoEvents(), load the UserControl. This leads to different behaviour (flickering, ...) everytime I load a UserControl, and I can not control when and how to set it to invisible again.
To sum it up, I have two questions:
How to ensure the message is visible directly, before loading the User control
How to ensure the message is set to invisible again, just in the moment the UserControl is completely loaded (including all DataBindings, grid formattings, etc.)
what we use is similar to this:
create a new form that has whatever you want to show the user,
implement a static method where you can call this form to be created inside itself, to prevent memory leaks
create a new thread within this form so that form is running in a seperated thread and stays responsive; we use an ajax control that shows a progress bar filling up.
within the method you use to start the thread set its properties to topmost true to ensure it stays on top.
for instance do this in your main form:
loadingForm.ShowLoadingScreen("usercontrollname");
//do something
loadingform.CloseLoadingScreen();
in the loading form class;
public LoadingScreen()
{
InitializeComponent();
}
public static void ShowLoadingScreen(string usercontrollname)
{
// do something with the usercontroll name if desired
if (_LoadingScreenThread == null)
{
_LoadingScreenThread = new Thread(new ThreadStart(DoShowLoadingScreen));
_LoadingScreenThread.IsBackground = true;
_LoadingScreenThread.Start();
}
}
public static void CloseLoadingScreen()
{
if (_ls.InvokeRequired)
{
_ls.Invoke(new MethodInvoker(CloseLoadingScreen));
}
else
{
Application.ExitThread();
_ls.Dispose();
_LoadingScreenThread = null;
}
}
private static void DoShowLoadingScreen()
{
_ls = new LoadingScreen();
_ls.FormBorderStyle = FormBorderStyle.None;
_ls.MinimizeBox = false;
_ls.ControlBox = false;
_ls.MaximizeBox = false;
_ls.TopMost = true;
_ls.StartPosition = FormStartPosition.CenterScreen;
Application.Run(_ls);
}
Try again your second approach:
Trying to show the message in a BackgroundWorker thread. This obviously fails because the BackgroundWorker thread is not the UI thread ;-)
But this time, use the following code in your background thread in order to update your label:
label.Invoke((MethodInvoker) delegate {
label.Text = "User Control xyz is loading";
label.Visible = true;
});
// Load your user control
// ...
label.Invoke((MethodInvoker) delegate {
label.Visible = false;
});
Invoke allows you to update your UI in another thread.
Working from #wterbeek's example, I modified the class for my own purposes:
center it over the loading form
modification of its opacity
sizing it to the parent size
show it as a dialog and block all user interaction
I was required to show a throbber
I received a null error on line:
if (_ls.InvokeRequired)
so I added a _shown condition (if the action completes so fast that the _LoadingScreenThread thread is not even run) to check if the form exists or not.
Also, if the _LoadingScreenThread is not started, Application.Exit will close the main thread.
I thought to post it for it may help someone else. Comments in the code will explain more.
public partial class LoadingScreen : Form {
private static Thread _LoadingScreenThread;
private static LoadingScreen _ls;
//condition required to check if the form has been loaded
private static bool _shown = false;
private static Form _parent;
public LoadingScreen() {
InitializeComponent();
}
//added the parent to the initializer
//CHECKS FOR NULL HAVE NOT BEEN IMPLEMENTED
public static void ShowLoadingScreen(string usercontrollname, Form parent) {
// do something with the usercontroll name if desired
_parent = parent;
if (_LoadingScreenThread == null) {
_LoadingScreenThread = new Thread(new ThreadStart(DoShowLoadingScreen));
_LoadingScreenThread.SetApartmentState(ApartmentState.STA);
_LoadingScreenThread.IsBackground = true;
_LoadingScreenThread.Start();
}
}
public static void CloseLoadingScreen() {
//if the operation is too short, the _ls is not correctly initialized and it throws
//a null error
if (_ls!=null && _ls.InvokeRequired) {
_ls.Invoke(new MethodInvoker(CloseLoadingScreen));
} else {
if (_shown)
{
//if the operation is too short and the thread is not started
//this would close the main thread
_shown = false;
Application.ExitThread();
}
if (_LoadingScreenThread != null)
_LoadingScreenThread.Interrupt();
//this check prevents the appearance of the loader
//or its closing/disposing if shown
//have not found the answer
//if (_ls !=null)
//{
_ls.Close();
_ls.Dispose();
//}
_LoadingScreenThread = null;
}
}
private static void DoShowLoadingScreen() {
_ls = new LoadingScreen();
_ls.FormBorderStyle = FormBorderStyle.None;
_ls.MinimizeBox = false;
_ls.ControlBox = false;
_ls.MaximizeBox = false;
_ls.TopMost = true;
//get the parent size
_ls.Size = _parent.Size;
//get the location of the parent in order to show the form over the
//target form
_ls.Location = _parent.Location;
//in order to use the size and the location specified above
//we need to set the start position to "Manual"
_ls.StartPosition =FormStartPosition.Manual;
//set the opacity
_ls.Opacity = 0.5;
_shown = true;
//Replaced Application.Run with ShowDialog to show as dialog
//Application.Run(_ls);
_ls.ShowDialog();
}
}