Niklas
Niklas

Reputation: 27

Update UI from the constructor of a different window not working

I am currently trying to implement a splash screen. I have taken this tutorial as a starting point.

OnStartup in my App.xaml.cs looks like this:

protected override void OnStartup(StartupEventArgs e)
{
    //initialize the splash screen and set it as the application main window
    splashScreen = new MySplashScreen();
    this.MainWindow = splashScreen;
    splashScreen.Show();

    //in order to ensure the UI stays responsive, we need to
    //do the work on a different thread
    Task.Factory.StartNew(() =>
    {
        //we need to do the work in batches so that we can report progress
        for (int i = 1; i <= 100; i++)
        {
            //simulate a part of work being done
            System.Threading.Thread.Sleep(30);

            //because we're not on the UI thread, we need to use the Dispatcher
            //associated with the splash screen to update the progress bar
            splashScreen.Dispatcher.Invoke(() => splashScreen.Progress = i);
            splashScreen.Dispatcher.Invoke(() => splashScreen.MyText = i.ToString());
        }

        //once we're done we need to use the Dispatcher
        //to create and show the main window
        this.Dispatcher.Invoke(() =>
        {
            //initialize the main window, set it as the application main window
            //and close the splash screen
            var mainWindow = new MainWindow();
            this.MainWindow = mainWindow;
            mainWindow.Show();
            splashScreen.Close();
        });
    });
}

This works perfectly. The splash screen is called and progress (ProgressBar) is incremented up to 100.

Now I want to write progress to the splash screen not only from OnStartup, but also from the constructor of my MainWindow.

My MainWindow constructor:

 public MainWindow()
 {
    InitializeComponent();

    ((App)Application.Current).splashScreen.Dispatcher.Invoke(() => ((App)Application.Current).splashScreen.MyText = "From MainWindow");

    // do some stuff that takes a few seconds...

 }

This is not working as expected. The text "From MainWindow" is updated in the text box of the splash screen only after the constructor has been called completely. Not as expected before "do some stuff that takes a few seconds..." is executed.

What's my mistake? Is this even as possible as I thought?

Upvotes: 2

Views: 349

Answers (1)

BionicCode
BionicCode

Reputation: 28968

The Dispatcher is already busy creating the MainWindow as you invoked the constructor using Dispatcher.Invoke. Then in the constructor of MainWindow you invoked the Dispatcher again. Dispatcher.Invoke effectively enqueues the delegate into the dispatcher queue. Once the first delegate ran to completion the next one (in this case the one from inside the constructor of MainWindow) is dequeued and executed (always with respect to the given DispatcherPriority). That's why you have to wait until the constructor completes i.e. the first delegated has completed.

I highly recommend to use Progress<T> which is the recommended way of progress reporting starting from .NET 4.5 (Async in 4.5: Enabling Progress and Cancellation in Async APIs). Its constructor captures the current SynchronizationContext and executes the report callback on it. Since the instance of Progress<T> is created on the UI thread the callback will execute automatically on the proper thread so that no Dispatcher is required anymore. This will solve your problem. In addition when used in an asynchronous context the progress reporting can make use of cancellation too.

I also recommend to use async/ await to control the flow. The goal is to create the instance of MainWindow on the UI thread.
Also always avoid using Thread.Sleep as it will block the thread. In this case the UI thread which will get unresponsive and frozen as a result. Use the asynchronous (non-blocking) await Task.Delay instead. As a rule of thumb replace all references to Thread with Task, i.e. the Task Parallel Library is the preferred way to go (Task-based asynchronous programming).

I refactored your code accordingly:

App.xaml.cs

private SplashScreen { get; set; }

protected override async void OnStartup(StartupEventArgs e)
{
  // Initialize the splash screen.
  // The first Window shown becomes automatically the Application.Current.MainWindow
  this.SplashScreen = new MySplashScreen();
  this.SplashScreen.Show();

  // Create a Progress<T> instance which automatically 
  // captures the current SynchronizationContext (UI thread)
  // which makes the Dispatcher obsolete for reporting the progress to the UI. 
  // Pass a report (UI update) callback to the Progress<T> constructor,
  // which will execute automatically on the UI thread.
  // Because of the generic parameter which is in this case of type ValueTuple (C# 7),
  // 'System.ValueTuple' is required to be referenced (use NuGet Package Manager to install). 
  // Alternatively replace the tuple with an arg class.
  var progressReporter = new Progress<(int Value, string Message)>(ReportProgress);

  // Wait asynchronously for the background task to complete
  await DoWorkAsync(progressReporter);

  // Override the Application.Current.MainWindow instance.
  this.MainWindow = new MainWindow();

  // Asynchronously wait until MainWindow is initialized
  // Pass the Progress<T> instance to the method,
  // so that MainWindow can report progress too
  await this.MainWindow.InitializeAsync(progressReporter);

  this.SplashScreen.Close();    
  this.MainWindow.Show();    
}

private async Task DoWorkAsync(IProgress<(int Value, string Message)> progressReporter)
{
  // In order to ensure the UI stays responsive, we need to
  // do the work on a different thread
  await Task.Run(
    async () =>
    {
      // We need to do the work in batches so that we can report progress
      for (int i = 1; i <= 100; i++)
      {
        // Simulate a part of work being done
        await Task.Delay(30);

        progressReporter.Report((i, i.ToString()));            
      }
    });
}

// The progress report callback which is automatically invoked on the UI thread.   
// Requires 'System.ValueTuple' to be referenced (see NuGet)
private void ReportProgress((int Value, string Message) progress)
{
  this.SplashScreen.Progress = progress.Value;
  this.SplashScreen.MyText = progress.Message;
}

MainWindow.xaml.cs

public partial class MainWindow
{
  public MainWindow()
  {
    InitializeComponent();
  }


  public async Task InitializeAsync(IProgress<(int Value, string Message)> progressReporter)
  {
    await Task.Run(
      () =>
      {
        progressReporter.Report((100, "From MainWindow"));

        // Run the initialization routine that takes a few seconds   
      }
  }
}

Upvotes: 2

Related Questions