Asynchronious Splash Screen while CAB init

Topics: CAB & Smart Client Software Factory
May 16, 2006 at 10:11 PM
originally posted by: gherold

Hello,
Is there a way to have a splash screen while the CAB is initializing.
The splash screen should show up as soon as possible in Main()
The splash screen should be closed after the AfterShellCreated Event.
The splash screen can have some controls such as progressbar and sould display correctly.
Thanks a lot for your help.
May 17, 2006 at 1:26 AM
originally posted by: PJackson

We implemented this as follows:

1) We reference the splash form via a static field in the Shell Application -- we did this because we wanted to show the form immediately in Main() and before ShellApplication.Run() is called;

2) In ShellApplication.Start() we close the splash form before calling base.Start();

3) We create and initialize a service that can access the splash screen's methods (via an interface) -- the service allows us to do things like have each module update the splash screen with its loading status, etc.
May 17, 2006 at 1:34 AM
originally posted by: gherold

Thanks for your answer.
But it looks like the "Loop Message" is not running when the Splash Screen is displayed...
Does it display correctly ?
May 17, 2006 at 1:37 AM
originally posted by: PJackson

We found that we had to add an Application.DoEvents() call after showing it and each time it's updated.
May 17, 2006 at 1:53 AM
originally posted by: gherold

Ok, I will have a try.
Thanks for your help !
May 18, 2006 at 1:23 PM
originally posted by: CraigHunterFXA

I can elaborate on a design I've employed that works really well with the CAB.

I created a form class (say "SplashForm") with a bitmap for the product logo, a progress control in marquee mode to show progress, and a status text area.

In BeforeShellCreated, I create and start a new Thread and call ShowDialog in it's thread main. This gives the SplashForm it's own UI message pump, and is more reliable and IMHO better practice than calling Application.DoEvents.

I then needed a reliable way to set the status whilst the shell was initialising across this thread boundary. I started with Invoke but found that sometimes status text did not get displayed for sufficiently long to be visible, and it really seemed to depend on the timeslice given for SplashForm's UI thread.

I refactored SplashForm to expose the ability to "enqueue" status text items (safely across threads), and each can have an optional minimum duration that the status text item should be displayed for. SplashForm also has an instance of the new (at last!) Framework 2.0 Semaphore with initial count zero and max Int32.MaxValue. Every time a status text item is enqueued, the Semaphore is Released.

SplashForm also has a BackgroundWorker thread, which simply waits on the semaphore whilst there are items to display in the status text area. If there are no status text items currently enqueued then it just blocks. Whenever there is an item to display it uses ReportProgress to send the text to SplashForm's UI thread context (no need to use Invoke).

Participants in the Shell initialisation can now enqueue these status text items and they will be displayed when their turn comes, and for their specified minimum duration.

At the end of AfterShellCreated, I call a public method on SplashForm called "AllowToClose", which sets a "finish" flag and releases the semaphore. It then sits and waits on an event WaitHandle (say "SplashClosedWaitHandle") exposed by SplashForm.

No status text items are allowed to be enqueued after this.

From now on, when the BackgroundWorker thread has finally serviced all its remaining status text items to be displayed, it will terminate. The RunWorkerCompleted event for BackgroundWorker then sets the "SplashClosedWaitHandle" WaitHandle & calls Close on itself. The Splash now closes.

AfterShellCreated now wakes up on the WaitHandle it was parked on, and we're done.
May 19, 2006 at 2:38 AM
originally posted by: gherold

Thanks a lot Craig for this very interresting post.
Could you give us a sample code ?
May 19, 2006 at 6:43 AM
originally posted by: Shabbazz

Craig's work really sounds very interesting.
I'm very interested in the sample code, too.
May 19, 2006 at 11:04 AM
originally posted by: developerWilly

I am very interested in some sample code for this implementation also, Craig. I currently have a splashform hacked together using application.DoEvents but as you have described the status text display is quirky at best when using this method.

Your method appears to be a much more elegant solution but I am new to the idea of semaphores and am not completely understanding how your status text queue works.

Any further help will be greatly appreciated.
-Will
May 19, 2006 at 1:09 PM
originally posted by: CraigHunterFXA

Hi Guys, thanks for your feedback.

Certainly.
I will prepare a standalone sample and post it up (hopefully) during next week.

Cheers...
Craig.
May 25, 2006 at 11:40 AM
originally posted by: CraigHunterFXA

Done ... it's here in User Samples - "Asynchronous Splash Screen For CAB":

http://www.gotdotnet.com/Community/UserSamples/Details.aspx?SampleGuid=27DF8F87-37A7-4751-8198-B9A3526BBCDD

From the readme:

This is a .NET 2.0 Windows Forms project demonstrating a technique for displaying a splash screen asynchronously during application initialisation. It was designed to be consumed within Composite UI Application Block (CAB) projects, but the "SplashForm" class could be extracted from this project to operate standalone without the CAB.

It overcomes the difficulties associated with safely background threading a UI form and still providing the ability to set status text from the main application thread. Status text requests can have a minimum time specified for each to be displayed. The main application thread can then wait on a WaitHandle for all queued status text requests to be completed whereupon the splash automatically closes and the main application shell is then displayed.

The SplashForm class was integrated into a CAB "Lightweight Smart Client" project from the CAB Smart Client Software Factory guidance extensions package. My extensions in ShellApplication.cs to use the SplashForm class have been annotated with "CH".

Let me know how you all go.

Craig.
May 26, 2006 at 4:39 AM
originally posted by: developerWilly

Brilliant work Craig, I set it up in my CAB application and it works perfectly! I can see that you put time and effort into perfecting this technique and I appreciate very much your sharing it with us and taking the time to create such a nice sample application.

Thanks again,
Will
May 26, 2006 at 6:31 AM
originally posted by: caldarola

Great work, but I have a mis-behaviour because I'm using a Login Module at startup so the splash form doesn't close never.
May 29, 2006 at 6:45 PM
originally posted by: taytay

Indeed - this is great! Well done Craig, and thank you very much for taking the time to put this together for us!

My application looks better already!
May 29, 2006 at 7:03 PM
originally posted by: taytay

I just discovered a race condition in the code. Quite easy to fix though...

The splashThread is started, and then the event handlers for Items and services being added are immediately hooked up. If the first item gets added before the splashThread gets its first timeslice, the event handler will try to reference the splashForm, but it hasn't been created yet. To fix this, I just create the form before the splashThread is started. The thread's job is still to simply call ShowDialog. But this way, the event handlers are guaranteed to have a splashForm to reference. Does anyone see any errors with this fix?

So now in BeforeShellCreated I simply call StartSplashForm();

And here is StartSplashForm:
private void StartSplashForm()
{
splashForm = new SplashForm();
splashForm.EnqueueStatusText("Application Initialising...", 500);
splashForm.SplashImage = Properties.Resources.YNABLogo;
splashForm.StartPosition = FormStartPosition.CenterScreen;

splashThread = new Thread(new ThreadStart(this.SplashThreadMain));
splashThread.Priority = ThreadPriority.AboveNormal;
splashThread.Start();
}

And SplashThreadMain becomes...
private void SplashThreadMain()
{
splashForm.ShowDialog();
}
May 29, 2006 at 8:03 PM
originally posted by: taytay

I made a couple more changes that I considered minor improvements, and I thought I'd share them:

I realized that there was a pause between the splash screen saying it was starting and the app actually starting, so I've moved the splashForm.AllowToClose (and surrounding code) to the Start() method of the ShellApplication. Now it closes the shellForm right before it calls base.Start(). That way it occurs after the modules are loaded rather than before, giving me the opportunity to display information about the loading modules, and reducing the delay between the splash screen dissapearing and the main shell showing.

I also changed the Items.Added event handler to only display ModuleInit objects. In my particular app, this level of granularity is just right. Also, I can display the ToString of the module, which provides a nice way to enumerate the major parts of the app to the user as they load.

Thanks again for the code,

Taylor
May 29, 2006 at 11:26 PM
originally posted by: gherold

Thanks a lot Craig for with very well done sample and for sharing it with us!
May 30, 2006 at 11:46 AM
originally posted by: CraigHunterFXA

Thanks all for your kind feedback, appreciated.

Taylor - nice findings!

I will update the sample to integrate your recommendations.

Craig.
Jun 7, 2006 at 12:10 PM
originally posted by: CraigHunterFXA

Have updated the same user sample download with taytay's findings and some other usage enhancements (SplashForm itself is unchanged from the previous release).

Change List
=========

ShellApplication.cs:

  • SplashForm instance and assignment of SplashImage now occurs in BeforeShellCreated before the splash's UI thread is started. I have not found any problems with this approach during testing.

  • SplashForm.AllowToClose call (and related) moved to new override of Start method.

  • In BeforeShellCreated, now just subscribes to the ModuleLoaded event of the IModuleLoaderService implementation to advise when modules are loaded. Other inline event handlers initialisation used in the previous release are now retained commented-out for illustration purposes.

  • In the ModuleLoaded event handler, check to see if an assembly-level CAB ModuleAttribute is defined for the module just loaded (since this event handler gives you the Assembly reference in its event args). If so, use the value of its "Name" property to announce the loaded module. Otherwise rever to using the raw assembly name.

# Note I have chosen the CAB's ModuleAttribute for this purpose but you could equally reflect on any assembly-level attributes if desired, e.g. AssemblyDescription, or your own custom attribute class.

Module.cs:

  • Included single sample CAB module in the solution which declares a ModuleAttribute in its Module.cs file (look for CH prefixed comment) to illustrate the above.


Cheers, happy splashing.
Craig.
Jun 7, 2006 at 4:38 PM
originally posted by: taytay

Thanks for the update Craig. I too began subscribing to the ModuleLoaded event, but I noticed that in the DependentModuleLoaderService, (at least in the last SCBAT Library implementation I had), fires the OnModuleLoaded event for each module after ALL of the modules have been loaded. This means that you don't get loaded events between each module load, but rather get a whole heap of events all at the end. Because of that, I have made the following change to the last two for loops of the InnerLoad method of the DependentModuleLoaderService:
Old Code:
foreach (ModuleMetadata module in loadOrder)
module.InitializeModuleClasses(workItem, OnModuleInitializing);

foreach (ModuleMetadata module in loadOrder)
module.NotifyOfLoadedModule(OnModuleLoaded);

New Code:

foreach (ModuleMetadata module in loadOrder)
{
module.InitializeModuleClasses(workItem, OnModuleInitializing);
module.NotifyOfLoadedModule(OnModuleLoaded);
}

Now I get updates as it finishes initializing each module. Granted, I wish that the IModuleLoaderService had a Loading event as well so that I could update the UI before it was done loading, but oh well - it's just a brief splash screen. :)

Thanks again for the new drop.
Jun 8, 2006 at 3:52 AM
originally posted by: jbranson

That is a great work.

On my side, I have a small problem with the ShellForm that does not come to Front when I launch the Shell.exe from the Explorer or from VS2005 (without debugging).

Do you reprodruce that problem?

Thanks again for your help.
Jun 8, 2006 at 4:17 AM
originally posted by: taytay

I had that problem too, but forgot to post about it. I solved it with this code:

In the Start() method, I add an event handler for the shell load:

Here is my new Start() method:

protected override void Start()
{
// CH: Shell is now ready to be shown. Enqueue this last status text item and inform the
// splash that it can close when finished, then wait on the wait handle until it actually closes.
splashForm.EnqueueStatusText("Starting Shell...", 200);
splashForm.AllowToClose();
splashForm.SplashClosedWaitHandle.WaitOne();

//TB: My new code
base.Shell.Load += new EventHandler(Shell_Load);

base.Start();
}

And here is what the Shell_Load looks like:

void Shell_Load(object sender, EventArgs e)
{
//TB: Must call activate before BringToFront will work.
base.Shell.Activate();
base.Shell.BringToFront();
}


Cheers,

Taylor
Jun 17, 2006 at 10:51 AM
originally posted by: sagi44

Have u posted the sample yet, i woul appreciate it very much if u could.
Jul 4, 2006 at 10:27 AM
originally posted by: taytay

Weird - for some reason this code doesn't appear to be working for me. Has anyone solved this consistently, or does anyone else even have this problem?
Jul 4, 2006 at 4:51 PM
originally posted by: sagi44

Worked great for me !
Jul 5, 2006 at 6:50 AM
originally posted by: taytay

Interesting - so this is even when launching from Explorer that it works and comes to the front?

It no longer comes to the front for me unless I click on the splash screen while it loads. It's as if not even the splash screen is considered to be the active application when it's loading.

I am seeing some strange behavior with relation to the program's z-order. When I display a child workitem that has a view in a WindowWorkspace and then hide the WindowWorkspace's form, the main shell view is actually moved behind other windows, almost as if "SendToBack" was being called on it. When I call show on the WindowWorkspace's form, the main shell comes back. It's very strange.

If anyone's ever seen their main form go to the back of the z-order on their desktop, I'd love to hear from you.
Sep 20, 2006 at 11:04 AM
originally posted by: bil_simser

Was this code ever posted? Craig?

EDIT: never mind, didn't see the additional pages in the thread. Thanks!
Sep 20, 2006 at 1:20 PM
originally posted by: CraigHunterFXA

Thanks everyone for your feedback regarding the sinister 'bring to front' issue.

Re: Comment from Julius/David on the sample download page - http://www.gotdotnet.com/Community/UserSamples/Details.aspx?SampleGuid=27DF8F87-37A7-4751-8198-B9A3526BBCDD

... the suggestion is a good one and has given me an idea to try.

The event firing technique you describe is effectively similar to the WaitHandle thread synchronisation handle which the main application thread waits for in the Start override method via "splashForm.SplashClosedWaitHandle.WaitOne()" and is "Set" by the Splash in it's OnClosed override. The difference - which may turn out to be an important one - is in the context from where the Set is performed.

Therefore instead of creating a C# delegate/event to synchronise when to successfully bring the Shell to front, I have refactored the existing thread synchronisation handle usage as described below:

1. Remove "splashClosedEvent.Set()" from OnClosed override.

2. Add new public method:

public void SetSplashClosedEventHandle()
{
this.splashClosedEvent.Set();
}

3. Change "SplashThreadMain" to call this new public method on the Splash after the ShowDialog call. Scoped with with try/finally so will always be set in case an exception were to propagate from the Splash. Dispose invoked on modal dialog as per best practice (see MSDN - Form.ShowDialog, http://blogs.msdn.com/jfoscoding/articles/450835.aspx and http://www.thecodeproject.com/dotnet/WorkingWithWinForms.asp) :

try
{
splashForm.ShowDialog();
}
finally
{
splashForm.SetSplashClosedEventHandle();
splashForm.Dispose();
}

The existing call to base.Shell.BringToFront() will now only proceed after ShowDialog completes in the splash thread, as opposed to previously which was during the OnClosed override of the Splash - and it turns out this may have been too early for BringToFront to work effectively in a multihreaded situation such as this.

The sample project works fine with this refactor, but it was usually when exercised from a heavy fully-fledged CAB app that it previously manifested itself due to timing influences.

Please try this refactor out and post to this forum if it does/does not fix this issue once and for all. If all good, then I will post a new drop of the sample project.

Thanks all.
Craig.
Sep 21, 2006 at 3:57 AM
originally posted by: bil_simser

Craig,

I made the three changes to the current sample you provided. It works fine when running from the IDE but the Shell form doesn't pop up if I run it from Explorer. I'm going to change it to see if the suggestion by Julius/David works better.
Sep 21, 2006 at 4:54 AM
originally posted by: bil_simser

Anyone know how to keep the splash screen around until after a module initializes? I have a module that fetches some data from a backend so the shell doesn't come up immediately. I want to keep the splash screen up while the module is loaded so I guess the splash would have to be over the shell while the shell and module load. Not sure how to do this? I'm thinking now to publish an event from the module and have the shell subscribe to it but looking for ideas. Thanks.
Sep 22, 2006 at 3:06 PM
originally posted by: taytay

I'm quite curious to hear if you got anywhere with this one Bill (Bil?). I was about to implement Craig's idea, but if it doesn't work... :)
Sep 22, 2006 at 3:18 PM
originally posted by: CraigHunterFXA

Taylor ... are you still having this issue, or did you end up finding another workaround (other than the ones we've previously discussed and tried out)?
Sep 22, 2006 at 5:21 PM
originally posted by: taytay

Craig,

I'm afraid I still have the trouble, but I haven't tried the workarounds yet. I think I was waiting for someone to shout Eureka first. I'll be giving it some more love this weekend most likely. My project is due at the end of this month, so I can't let it sit too long. ;)

Cheers,

Taylor
Sep 23, 2006 at 4:16 AM
originally posted by: bil_simser

Craig,

Can you post an updated drop of your sample with the changes? I applied the changes you suggested but I still have the problem with focus. I'm not sure if I missed anything or not.

Also, is there any ideas on delaying the closing of the spash screen so you can shut it down when all modules are loaded? I haven't tried doing a pub/sub approach with this but think it might work, just not sure if I can subscribe to an event from inside the Shell.
Sep 26, 2006 at 1:08 PM
originally posted by: CraigHunterFXA

OK, since the managed methods are not honouring our z-order requests reliably, I've gone native.
This is now working for me 100% of the time...

1. Add the following namespace import if haven't already:

using System.Runtime.InteropServices;

2. Add the following P/Invoke declaration:

DllImport("User32.dll")
public static extern Int32 SetForegroundWindow(IntPtr hWnd);

3. After the call to "splashForm.SplashClosedWaitHandle.WaitOne()" in the Start method override, do this before the call to base.Start:

ShellApplication.SetForegroundWindow(base.Shell.Handle);

Please try this solution and advise if it works reliably. If so, I will post a new drop.
Fingers crossed.
Craig.
Sep 26, 2006 at 1:10 PM
originally posted by: CraigHunterFXA

Bil,

To delay closing until all modules are loaded, ensure you're calling the "AllowToClose" method in your Start() specialisation in ShellApplication.cs, and not in, say, AfterShellCreated. See discussions and suggestions by taytay earlier in this thread.

If possible I'd like to get feedback on my P/Invoke suggestion before uploading a new drop.
Sep 26, 2006 at 1:16 PM
originally posted by: bil_simser

Hi Craig,

Thanks for the info. I'll try the P/Invoke method tonight (should be a couple of hours before I get to it) and post my results.

As for the delay on the splash, what I really want to do is:

1. Launch the app
2. Show the splash screen
3. Show the shell
4. Load the modules
5. On loading the last module, then shut down the splash

Note: the order of 3 or 4 isn't important, I just want the splash to end when all modules have loaded completely
Sep 26, 2006 at 1:47 PM
originally posted by: taytay

I'll try this new code out shortly as well. Thanks Craig. As for making it close down after the modules are loaded, I believe I made some changes to the way the CAB fires module loading events in order to get some of the behavior I wanted. I'll investigate and report back on some of my other enhancements...
Sep 27, 2006 at 3:29 AM
originally posted by: bil_simser

Try as I might, I cannot get things to focus. The only way it works when I launch it from Explorer or a run command, is to click on the splash screen. If I don't the shell tray button will flash when the splash screen goes away. Maybe it's just me. Anyways, our app is a ClickOnce app and seems to work in that way anyways.

I'm still not sure about the loading of modules. I do have the AllowToClose method in my Start() specialization, however the splash screen goes away THEN the module initializes and loads the data it needs (it's just a grid with some values pulled from a backend).
Sep 27, 2006 at 5:45 AM
originally posted by: MarcoPaul

I'm late to contribute to this thread and I admit I have not read the entire thing. What I do is show my splash screen (non-modal / topmost) in the BeforeShellCreated() method and dispose of it in the AfterShellCreated() method. This seems to work fine. I'm sure I must be missing the main issue here ;)
Oct 12, 2006 at 5:59 AM
originally posted by: taytay

Hi Craig,

Thanks for the suggestion. This method works for me if I call it on the shell's Shown event handler. If I just try calling it in the Start method as you suggest, it doesn't appear to work. I am using a heavily modified version of your code on this particular project, but to my knowledge, the form isn't actually shown before base.Start, so I don't think the SetForegroundWindow can actually do anything. Calling it from the shown event handler seems to work like a champ though. Has anyone else tried Craig's suggestion?

Also, has anyone else seen thier debugger act funny if they happen to have the splash form in a Watch or Locals window? It was actually crashing my app (because it tried to evaluate some properties, timed out...and then got confused). To get around it, I had to decorate my SplashForm with the following attribute:
System.Diagnostics.DebuggerDisplay("SplashForm - WARNING: If you're on the main thread, DO NOT expand this object in the watch window (see comments)...")

That prevents the debugger from calling ToString on the form and warns you NOT to hit that plus sign from the wrong thread...

Cheers,

Taylor
Oct 18, 2006 at 12:34 PM
originally posted by: CraigHunterFXA

Thanks for your feedback Taylor.

I agree moving the call to SetForegroundWindow into OnShown seems most reliable of all.

There has been some other suggested approaches posted as Comments on the sample page (http://www.gotdotnet.com/Community/UserSamples/Details.aspx?SampleGuid=27DF8F87-37A7-4751-8198-B9A3526BBCDD).

I think most have now found an appropriate workaround that works best for their app, but if the above approach remains the most stable and reliable then I will upload a new drop with this approach.

Cheers.
Craig.
Oct 21, 2006 at 9:25 AM
originally posted by: chudley

I have implemented the work around for the focus/activation problem as described above. However, the only thing that happens for me is the application item on the task bar flashes, the application does not gain focus. This only happens when executing outside of visual studio (eg from windows explorer).

Anybody have any ideas.

Thanks,
Chad
Oct 21, 2006 at 9:31 AM
originally posted by: taytay

I forgot to mention that I also set my form to TopMost briefly. This is what my "Shown" event handler on my shell looks like now:

this.TopMost = true;
SetForegroundWindow(this.Handle);
this.TopMost = false;

Cheers,

Taylor
Oct 21, 2006 at 9:45 AM
originally posted by: chudley

Thanks Taylor. The app comes to the front now.

Later,
Chad
Jan 30, 2011 at 11:36 PM
Edited Feb 2, 2011 at 10:29 PM

Hi.

The link with source code is broken, but I found this:

http://smartclient.codeplex.com/Thread/View.aspx?ThreadId=24177 (last post)

 

This standalone example works great, but when I copy SplashForm to my solution and modify my ShellApplication.cs file it dosn't work :( (no compilation error)

Splash Screen show in background and newer hide... it waiting for something...... then ShellForm never Show. I debug solution many times but I can't find the problem. I work on SCSF 2008. Any idea? Have you tried this example code in your own solutions?

 

Please help.

Thanks.