WPF Windows Initializing is locking the separated thread in .Net 8

55 Views Asked by At

I have the following code that I have used for many years in .Net Framework 4.8 to show a splash screen on my startup:

      Thread newWindowThread = new Thread(new ThreadStart(() =>
      {
        WpfSplashScreen splash = new WpfSplashScreen();//Get stuck here!
        splash.Show();
        Thread.Sleep(3000);
        splash.Close();
      }));

      newWindowThread.SetApartmentState(ApartmentState.STA);
      newWindowThread.IsBackground = true;
      newWindowThread.Start();

      //Loading some things...
      Thread.Sleep(4000);

But after migrating my project to .Net 8 it stopped working like before. Now the new thread created to show the SplashScreen gets stuck at the dialog initializing and only continues after the main thread has finished, this causes my SplashScreen to only open after all loading is complete.

While debugging, if I step into 'WpfSplashScreen()' I can see that the thread stops at the 'InitializeComponent()' method.

I don't want to change my SlpashScreen to another component, that is not the idea here, I have used the same approach to open other dialogs on their own threads in many parts of my program for different reasons (like for example a progressbar window) and all cases have the same problem after migration. The SplashScreen is just the simplest example I have about this problem. So I really need to know how to make the Wpf dialog initialization not hang the separate thread in .Net 8.

Any idea?

2

There are 2 best solutions below

0
EldHasp On

Although your method worked, I still wouldn’t recommend doing it that way. In general, I would leave the explicit creation of threads for cases of extreme necessity, when it is impossible to do otherwise. The same applies to all UI elements. You should create and work with them only in the main application thread.

Therefore, I would suggest that you replace your code with the code I proposed below. And it’s better to replace the code not only in Core/Net applications, but also in the Framework.

    {
        WpfSplashScreen splash = new WpfSplashScreen();
        splash.Loaded += async delegate
        {
            await Task.Delay(3000);
            splash.Close();
        };
        splash.Show();
    }

Since you often use a display like this, you can create an extension method and use it:

    public static class WindowHelper
    {
        public static void Show(this Window window, int delayClose)
        {
            window.Loaded += async delegate
            {
                await Task.Delay(delayClose);
                window.Close();
            };
            window.Show();
        }
    }
    {
        new WpfSplashScreen().Show(3000);
    }
0
BionicCode On

You must start a Dispatcher to make it a WPF UI thread. Otherwise, it's a simple STA thread. The heart of an application is the message queue.

When you manually start a Dispatcher you are also responsible for shutting it down. Otherwise, it will continue to run although the associated thread has been terminated for a long time already.

Note, if you want to show a simple image, WPF offers a built-in splash screen support that is really simple to setup: add an image file to your startup project and set its build action to "SplashScreen".

Also, unless you do CPU intensive work on the UI thread that requires the dispatcher, you should avoid creating a new UI thread.
Simply run your long running initializations on a background thread, or even better, asynchronously.

A good and clean solution is to initialize your application from an async e.g., Application.Startup event callback. Here you can call async initialization routines and once you're done show the main window.
For example:

App.xaml.cs

private SplashScreen splashScreen;

private async void Application_Startup(object sender, StartupEventArgs e)
{
  // Create and show the window on the main dispatcher thread
  this.splashScreen = new Window() { Content = "Please wait!" };
  this.splashScreen.Show();

  // Depending on your design you can now call the required initialization routines.
  // When they are async they won't block the main dispatcher thread.
  // For example:
  var mainWindow = new MainWindow();
  mainWindow.Loaded += OnMainWindowLoaded;
  await mainWindow.InitializeAsync();
  mainWindow.Show();
}

private void OnMainWindowLoaded(object sender, RoutedEventArgs e)
{
  var mainWindow = (Window)sender;
  mainWindow.Loaded -= OnMainWindowLoaded;
  this.splashScreen.Close();
  this.splashScreen = null;
}

When you compare that async code to your original version you can tell that it is much more resource friendly and elegant. And simpler to understand, what means less potential for bugs.
For example, your current version doesn't work because you missed important details about WPF main threads. And it creates a memory leak because you missed important details about how the dispatcher is managed. In contrast, the async version is harder to do wrong.

The following example shows how to properly create and manage a WPF dispatcher thread:

App.xaml

<Application Startup="Application_Startup">

</Application>

Application.cs

public partial class App : Application
{
  private SplashScreen? splashScreen;

  private void Application_Startup(object sender, StartupEventArgs e)
  {      
    ShowSplashScreen();

    var mainWindow = new MainWindow();
    mainWindow.Loaded += OnMainWindowLoaded;

    // Simulate the dispatcher intensive initialization (potentially avoidable)
    Thread.Sleep(10000);

    mainWindow.Show();
  }

  private void OnMainWindowLoaded(object sender, RoutedEventArgs e)
  {
    var mainWindow = (Window)sender;
    mainWindow.Loaded -= OnMainWindowLoaded;
    CloseSplashScreen();
  }

  private void ShowSplashScreen()
  {
    Thread newDispatcherThread = new Thread(new ThreadStart(() =>
    {
      this.splashScreen = new SplashScreen();
      this.splashScreen.Closed += OnSplashScreenClosed;
      this.splashScreen.Show();

      // Start the message queue
      Dispatcher.Run();
    }));

    newDispatcherThread.SetApartmentState(ApartmentState.STA);
    newDispatcherThread.IsBackground = true;
    newDispatcherThread.Start();
  }

  private void CloseSplashScreen()
  { 
    // Don't allow the delegate to capture the field value 
    SplashScreen splashScreen = this.splashScreen;

    splashScreen.Closed += OnSplashScreenClosed;
    splashScreen.Dispatcher.Invoke(
      splashScreen.Close, 
      DispatcherPriority.Send);
  }

  private void OnSplashScreenClosed(object? sender, EventArgs e)
  {
    /* Cleanup resources */

    this.splashScreen.Dispatcher.InvokeShutdown();
    this.splashScreen.Closed -= OnSplashScreenClosed;

    // Allow the instance to be garbage collected
    this.splashScreen = null;
  }
}