2

I've got the following program flow in my Windows Forms application (WPF is not a viable option unfortunately):

  1. The GUI Thread creates a splash screen and a pretty empty main window, both inheriting Form.
  2. The splash screen is shown and given to Application.Run().
  3. The splash screen will send an event which triggers an async Event Handler which performs initialization, using the IProgress interface to report progress back to the GUI. (This works flawlessly.)
  4. At some point during the initialization, I need to dynamically create GUI components based on information provided by certain plugins and add them to the Main Window.

At this point I'm stuck: I know I need to ask the GUI thread to create those components for me, but there is no Control I could call InvokeRequired on. Doing MainWindow.InvokeRequired works neither.

The only idea I could come up with was to fire an event which is connected to a factory in the GUI Thread, and then wait for that factory to fire another event which provides the created controls. However I am pretty sure there is a more robust solution. Does anyone know how to achieve this?

Tim Meyer
  • 12,210
  • 8
  • 64
  • 97
  • 1
    Well, you'll need to do all that work on the GUI thread - so why not just prepare all the information to actually build the GUI on your separate thread, and then pass all that back to the splashscreen as a success value? The GUI will be blocked for that time anyway. – Luaan Jun 18 '15 at 13:02
  • 1
    Exactly just create all the controls in a panel for example and once you are done pass along that single GUI element and add it to the form. – Franck Jun 18 '15 at 13:02
  • 2
    But, you in fact have a GUI. Using the splashscreen's Begin/Invoke() method is just as valid. You'll gain some elegance points by copying SynchronizationContext.Current and using its Post method in the thread. Or using async as intended, the continuation should run on the GUI thread. – Hans Passant Jun 18 '15 at 13:14
  • @HansPassant Thanks for the last part of your comment, it leaded to (what I consider) a rather elegant solution which completely decouples the splash screen from any initialization tasks. Check my answer if you're interested in more details. – Tim Meyer Jun 19 '15 at 13:02

2 Answers2

0

Using the comments on my question, especially the note about the continuation method which made me find this very useful question, I achieved the following:

  • The first part of initialization is performed asynchronously (no change).
  • The second part of the initialization (which creates the UI elements) is performed afterwards as a Continuation Task, in the context of the UI thread.
  • Apart from the rather short GUI initialization part, the Splash Screen is responsive (i.e. the mouse cursor does not change to "Waiting" once it hovers the Splash Screen).
  • Neither of the initialization routines knows the splash screen at all (i.e. I could easily exchange it).
  • The core controller only knows the SplashScreen interface and does not even know it is a Control.
  • There currently is no exception handling. This is my next task but doesn't affect this question.

TL;DR: The code looks somewhat like this:

public void Start(ISplashScreen splashScreen, ...)
{
    InitializationResult initializationResult = null;
    var progress = new Progress<int>((steps) => splashScreen.IncrementProgress(steps));
    splashScreen.Started += async (sender, args) => await Task.Factory.StartNew(

             // Perform non-GUI initialization - The GUI thread will be responsive in the meantime.
             () => Initialize(..., progress, out initializationResult)

        ).ContinueWith(

            // Perform GUI initialization afterwards in the UI context
            (task) =>
                {
                    InitializeGUI(initializationResult, progress);
                    splashScreen.CloseSplash();
                },
            TaskScheduler.FromCurrentSynchronizationContext()

        );

    splashScreen.Finished += (sender, args) => RunApplication(initializationResult);

    splashScreen.SetProgressRange(0, initializationSteps);        
    splashScreen.ShowSplash();

    Application.Run();
}
Community
  • 1
  • 1
Tim Meyer
  • 12,210
  • 8
  • 64
  • 97
0

It is much easier to manage multiple forms and display one while the other is working or being constructed.

I suggest you try the following:

  • When application is started you create splash screen form so your Program.cs is like this

    static void Main()
    {
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);
        Application.Run(new SplashForm());
    }
    
  • Inside the splash form constructor, create a new thread (I will use BackgroundWorker but there are other options like tasks) to build your main form.

    public SplashForm()
    {
        InitializeComponent();
        backgroundWorker1.WorkerSupportsCancellation = true;
        backgroundWorker1.WorkerReportsProgress = true;
        backgroundWorker1.DoWork += new DoWorkEventHandler(backgroundWorker1_DoWork);
        backgroundWorker1.ProgressChanged += new ProgressChangedEventHandler(backgroundWorker1_ProgressChanged);
        backgroundWorker1.RunWorkerAsync();
    }
    
  • Now we need to write the SplashForm member functions to tell background worker what to do

    private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
    {
        BackgroundWorker worker = sender as BackgroundWorker;
    
        // Perform non-GUI initialization - The GUI thread will be responsive in the meantime
    
        // My time consuming operation is just this loop.
        //make sure you use worker.ReportProgress() here
        for (int i = 1; (i <= 10); i++)
        {
            if ((worker.CancellationPending == true))
            {
                e.Cancel = true;
                break;
            }
            else
            {
                System.Threading.Thread.Sleep(500);
                worker.ReportProgress((i * 10));
            }
        }
    
        SetVisible(false);
        MainForm mainForm = new MainForm();
        mainForm.ShowDialog();
    
        //instead of
        //this.Visible = false;
    }
    
    private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e)
    {
        this.progressBar1.Value = e.ProgressPercentage;
    }
    
  • You might have noticed by now, I am using another member function to hide the splash screen. It is because you are now in another thread and you can't just use this.visible = false;. Here is a link on the matter.

    delegate void SetTextCallback(bool visible);
    private void SetVisible(bool visible)
    {
        // InvokeRequired required compares the thread ID of the
        // calling thread to the thread ID of the creating thread.
        // If these threads are different, it returns true.
        if (this.InvokeRequired)
        {
            SetTextCallback d = new SetTextCallback(SetVisible);
            this.Invoke(d, new object[] { visible });
        }
        else
        {
            this.Visible = visible;
        }
    }
    

When I run this sample project it shows the progress bar and then loads the MainForm windows form after hiding the SplashForm.

This way you can put any controls that you might need inside the MainForm constructor. The part you shortened as // Perform GUI initialization afterwards in the UI context should go into MainForm constructor.

Hope this helps.

  • Thanks for your proposal, however in your solution, the splash screen class implements both thread management and application initialization. The splash screen should not be [responsible](https://en.wikipedia.org/wiki/Single_responsibility_principle) for that, as that tightly couples the splash screen to a specific project. In my solution, I can reuse the splash screen (interface) in several projects and I could maybe even exchange it with a WPF splash screen by implementing ISplashScreen, without having to implement thread management and initialization again. – Tim Meyer Jun 22 '15 at 05:57
  • Your ISplashScreen is reusable and you can convert mine into something reusable too. You could create a form building background worker class and pass it to the splash screen constructor as a parameter. It wouldn't have to know what the "dowork" function actually does. ProgressChanged would need to remain the same as the progress bar is a member of SplashForm and a generic code may not be able to update it. – Bahadir Acar Jun 22 '15 at 07:13