Reputation: 27
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
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