Displaying SplashScreen Causing InvalidOperationException
Asked Answered
C

1

1

Problem

I have a MVVM application that uses Caliburn.Micro as the MVVM framework and MEF for "dependency injection" (in quotes as I am aware it is not strictly an DI container). The composition process of this large application is starting to take increasingly large amounts of time based on the number of compositions MEF is undertaking during the launch of the application and as such I want to use an animated splash screen.

Below I will outline my current code that shows the splash screen on a separate thread and attempts to launch the main application

public class Bootstrapper : BootstrapperBase
{
    private List<Assembly> priorityAssemblies;
    private ISplashScreenManager splashScreenManager;

    public Bootstrapper()
    {
        Initialize();
    }

    protected override void Configure()
    {
        var directoryCatalog = new DirectoryCatalog(@"./");
        AssemblySource.Instance.AddRange(
             directoryCatalog.Parts
                  .Select(part => ReflectionModelServices.GetPartType(part).Value.Assembly)
                  .Where(assembly => !AssemblySource.Instance.Contains(assembly)));

        priorityAssemblies = SelectAssemblies().ToList();
        var priorityCatalog = new AggregateCatalog(priorityAssemblies.Select(x => new AssemblyCatalog(x)));
        var priorityProvider = new CatalogExportProvider(priorityCatalog);

        var mainCatalog = new AggregateCatalog(
            AssemblySource.Instance
                .Where(assembly => !priorityAssemblies.Contains(assembly))
                .Select(x => new AssemblyCatalog(x)));
        var mainProvider = new CatalogExportProvider(mainCatalog);

        Container = new CompositionContainer(priorityProvider, mainProvider);
        priorityProvider.SourceProvider = Container;
        mainProvider.SourceProvider = Container;

        var batch = new CompositionBatch();

        BindServices(batch);
        batch.AddExportedValue(mainCatalog);

        Container.Compose(batch);
    }

    protected virtual void BindServices(CompositionBatch batch)
    {
        batch.AddExportedValue<IWindowManager>(new WindowManager());
        batch.AddExportedValue<IEventAggregator>(new EventAggregator());
        batch.AddExportedValue(Container);
        batch.AddExportedValue(this);
    }


    protected override object GetInstance(Type serviceType, string key)
    {
        String contract = String.IsNullOrEmpty(key) ?
            AttributedModelServices.GetContractName(serviceType) :
            key;
        var exports = Container.GetExports<object>(contract);

        if (exports.Any())
            return exports.First().Value;

        throw new Exception(
            String.Format("Could not locate any instances of contract {0}.", contract));
    }

    protected override IEnumerable<object> GetAllInstances(Type serviceType)
    {
        return Container.GetExportedValues<object>(
            AttributedModelServices.GetContractName(serviceType));
    }

    protected override void BuildUp(object instance)
    {
        Container.SatisfyImportsOnce(instance);
    }

    protected override void OnStartup(object sender, StartupEventArgs suea)
    {
        splashScreenManager = Container.GetExportedValue<ISplashScreenManager>();
        splashScreenManager.ShowSplashScreen();

        base.OnStartup(sender, suea);
        DisplayRootViewFor<IMainWindow>(); // HERE is the Problem line.

        splashScreenManager.CloseSplashScreen();
    }

    protected override IEnumerable<Assembly> SelectAssemblies()
    {
        return new[] { Assembly.GetEntryAssembly() };
    }

    protected CompositionContainer Container { get; set; }

    internal IList<Assembly> PriorityAssemblies
    {
        get { return priorityAssemblies; }
    }
}

My ISplashScreenManager implementation is

[Export(typeof(ISplashScreenManager))]
[PartCreationPolicy(CreationPolicy.NonShared)]
public class SplashScreenManager : ISplashScreenManager
{
    private ISplashScreenViewModel splashScreen;
    private Thread splashThread;
    private Dispatcher splashDispacher;

    public void ShowSplashScreen()
    {
        splashDispacher = null;
        if (splashThread == null)
        {
            splashThread = new Thread(new ThreadStart(DoShowSplashScreen));
            splashThread.SetApartmentState(ApartmentState.STA);

            splashThread.IsBackground = true;
            splashThread.Name = "SplashThread"; 

            splashThread.Start();
            Log.Trace("Splash screen thread started");

            Application.Current.ShutdownMode = ShutdownMode.OnMainWindowClose;
            Application.Current.MainWindow = null;
        }
    }

    private void DoShowSplashScreen()
    {
        splashScreen = IoC.Get<ISplashScreenViewModel>();

        splashDispacher = Dispatcher.CurrentDispatcher;
        SynchronizationContext.SetSynchronizationContext(
            new DispatcherSynchronizationContext(splashDispacher));

        splashScreen.Closed += (s, e) =>
            splashDispacher.BeginInvokeShutdown(DispatcherPriority.Background);
        splashScreen.Show();

        Dispatcher.Run();
        Log.Trace("Splash screen shown and dispatcher started");
    }

    public void CloseSplashScreen()
    {
        if (splashDispacher != null)
        {
            splashDispacher.BeginInvokeShutdown(DispatcherPriority.Send);
            splashScreen.Close();
            Log.Trace("Splash screen close requested");
        }
    }

    public ISplashScreenViewModel SplashScreen
    {
        get { return splashScreen; }
    }
}

where the ISplashScreenViewModel.Show() and ISplashScreenViewModel.Close() methods show and close the corresponding view respectively.

Error

This code seems to work well insofar as it launches the splash screen on the background thread and the splash animation works etc. However, when the code returns to the bootstrapper the line

DisplayRootViewFor<IMainWindow>(); 

throws an InvalidOperationException with the following message

The calling thread cannot access this object because a different thread owns it.

The stack trace is

at System.Windows.Threading.Dispatcher.VerifyAccess() at System.Windows.DependencyObject.GetValue(DependencyProperty dp) at MahApps.Metro.Controls.MetroWindow.get_Flyouts() in d:\projects\git\MahApps.Metro\src\MahApps.Metro\MahApps.Metro.Shared\Controls\MetroWindow.cs:line 269 at MahApps.Metro.Controls.MetroWindow.ThemeManagerOnIsThemeChanged(Object sender, OnThemeChangedEventArgs e) in d:\projects\git\MahApps.Metro\src\MahApps.Metro\MahApps.Metro.Shared\Controls\MetroWindow.cs:line 962 at System.EventHandler1.Invoke(Object sender, TEventArgs e) at MahApps.Metro.Controls.SafeRaise.Raise[T](EventHandler1 eventToRaise, Object sender, T args) in d:\projects\git\MahApps.Metro\src\MahApps.Metro\MahApps.Metro.Shared\Controls\SafeRaise.cs:line 26 at MahApps.Metro.ThemeManager.OnThemeChanged(Accent newAccent, AppTheme newTheme) in d:\projects\git\MahApps.Metro\src\MahApps.Metro\MahApps.Metro.Shared\ThemeManager\ThemeManager.cs:line 591 at MahApps.Metro.ThemeManager.ChangeAppStyle(ResourceDictionary resources, Tuple`2 oldThemeInfo, Accent newAccent, AppTheme newTheme) in d:\projects\git\MahApps.Metro\src\MahApps.Metro\MahApps.Metro.Shared\ThemeManager\ThemeManager.cs:line 407 at MahApps.Metro.ThemeManager.ChangeAppStyle(Application app, Accent newAccent, AppTheme newTheme) in d:\projects\git\MahApps.Metro\src\MahApps.Metro\MahApps.Metro.Shared\ThemeManager\ThemeManager.cs:line 345 at Augur.Core.Themes.ThemeManager.SetCurrentTheme(String name) in F:\Camus\Augur\Src\Augur\Core\Themes\ThemeManager.cs:line 46 at Augur.Modules.Shell.ViewModels.ShellViewModel.OnViewLoaded(Object view) in F:\Camus\Augur\Src\Augur\Modules\Shell\ViewModels\ShellViewModel.cs:line 73 at Caliburn.Micro.XamlPlatformProvider.<>c__DisplayClass11_0.b__0(Object s, RoutedEventArgs e) at Caliburn.Micro.View.<>c__DisplayClass8_0.b__0(Object s, RoutedEventArgs e) at System.Windows.EventRoute.InvokeHandlersImpl(Object source, RoutedEventArgs args, Boolean reRaised) at System.Windows.UIElement.RaiseEventImpl(DependencyObject sender, RoutedEventArgs args) at System.Windows.BroadcastEventHelper.BroadcastEvent(DependencyObject root, RoutedEvent routedEvent) at System.Windows.BroadcastEventHelper.BroadcastLoadedEvent(Object root) at MS.Internal.LoadedOrUnloadedOperation.DoWork() at System.Windows.Media.MediaContext.FireLoadedPendingCallbacks() at System.Windows.Media.MediaContext.FireInvokeOnRenderCallbacks() at System.Windows.Media.MediaContext.RenderMessageHandlerCore(Object resizedCompositionTarget) at System.Windows.Media.MediaContext.RenderMessageHandler(Object resizedCompositionTarget) at System.Windows.Interop.HwndTarget.OnResize() at System.Windows.Interop.HwndTarget.HandleMessage(WindowMessage msg, IntPtr wparam, IntPtr lparam) at System.Windows.Interop.HwndSource.HwndTargetFilterMessage(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled) at MS.Win32.HwndWrapper.WndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled) at MS.Win32.HwndSubclass.DispatcherCallbackOperation(Object o) at System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Int32 numArgs) at System.Windows.Threading.ExceptionWrapper.TryCatchWhen(Object source, Delegate callback, Object args, Int32 numArgs, Delegate catchHandler) at System.Windows.Threading.Dispatcher.LegacyInvokeImpl(DispatcherPriority priority, TimeSpan timeout, Delegate method, Object args, Int32 numArgs) at MS.Win32.HwndSubclass.SubclassWndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam)

Attempted Solutions

I have attempted to change the code that executes DisplayRootViewFor<IMainWindow>(); to use a dispatcher as follows

base.OnStartup(sender, suea);
Application.Current.Dispatcher.Invoke(() => DisplayRootViewFor<IMainWindow>()); // Still throws the same exception.

and

base.OnStartup(sender, suea);
Application.Current.Dispatcher.BeginInvoke(
    new System.Action(delegate { DisplayRootViewFor<IMainWindow>(); })); // Still throws the same exception.

and even

TaskScheduler guiScheduler = TaskScheduler.FromCurrentSynchronizationContext();

splashScreenManager = Container.GetExportedValue<ISplashScreenManager>();
splashScreenManager.ShowSplashScreen();

Task.Factory.StartNew(() =>
{
    base.OnStartup(sender, suea);
    DisplayRootViewFor<IMainWindow>();
}, CancellationToken.None, 
   TaskCreationOptions.None, 
   guiScheduler);

Attempting to enforce the use of the Gui MainThread. All of the above throw the same exception.

Questions

  1. How can I call the DisplayRootViewFor<IMainWindow>() method and avoid this exception?

  2. Is this method of displaying an animated splash a legitimate one?

Thanks for your time.


Edit. I have discovered this answer from the awesome Hans Passant https://mcmap.net/q/777971/-net-4-0-and-the-dreaded-onuserpreferencechanged-hang. In light of this I have attempted to add the applications static App() { } ctor.

static App()
{
    // Other stuff.
    Microsoft.Win32.SystemEvents.UserPreferenceChanged += delegate { };
}

but (probably unsurprisingly) this has not helped me. Same exception at the same location...

Clubbable answered 23/4, 2017 at 17:56 Comment(13)
Looks like Splash show is accessing UI thread. Can we try Dispatcher.Invoke(()=>{splashScreen.Show();}); in DoShowSplashScreen() methodCheek
Your splash screen code looks strange. It plays with Application.Current, and it sets the SynchronizationContext once never changes it back? otherwise, do you have a small reproducing project? why don't you use the standard WPF's SplashScreen ? the source is even avalaible to get inspiration: referencesource.microsoft.com/#WindowsBase/Base/System/Windows/…Januaryjanuisz
Hi @Cheek I have tried this. This does not help, I get the same exception.Clubbable
@SimonMourier thank you for your time. I am setting the SynchonizationContext for the splashThread only. the SynchronizationContext is attached to the current thread only (codeproject.com/Articles/31971/…). Have I missed something? As I state above, the standard WPF splash only allows a static image to be shown, I would like to show loading information on an animated splash screen. I can zip the code I have and provide it via OneDrive if you'd be willing to help?Clubbable
@Clubbable - that would be fine as soon as we can reproduce easilyJanuaryjanuisz
Okay, thanks, let me trim down the solution and upload.Clubbable
@SimonMourier please find the solution here 1drv.ms/u/s!AuCd_PcRnNWpkR55Ob89v4G39el9. If you re-instate the NuGet packages and run the code, you will see the animated splash screen launch, but then the exception thrown. I would like a way to display the splash on its own thread with animation and then proceed with the main window. I set the Application.MainWindow = null to prevent WPF thinking the first displayed window is the main window...Clubbable
Please let me know when you have downloaded this so I can remove it.Clubbable
Hi @SimonMourier I will post the link again; could you let me know when you are ready and I will post it up. I really appreciate you having a look. Thanks again for your time...Clubbable
@Clubbable please post the solution again to have a look. Thanks..Erythropoiesis
Downloaded, thanks.Erythropoiesis
Any ideas on the problem?Clubbable
Sorry I though it was a permanent link. Missed iJanuaryjanuisz
E
5

You are creating an instance of SplashScreenView on a new background thread, but then calling MetroThemeManager.ChangeAppStyle inside ThemeManager class from the Main UI thread.

Because MetroWindow class is parent of your SplashScreenView you cannot prevent it from internally subscribing to the ThemeManager.IsThemeChanged event which you are triggering by calling MetroThemeManager.ChangeAppStyle inside ThemeManager class.

Since code of the ThemeManager.IsThemeChanged event handler inside MetroWindow cannot be executed on a Main UI thread which is different from the thread splash screen was created on you should either [1] derive your SplashScreenView from a standard Window class to avoid dependency on MetroWindow or [2] avoid calling MetroThemeManager.ChangeAppStyle while your SplashScreenView is still alive.

Commenting out two lines of code is removing the violation, see lines below. But obviously the actual solution needs to be done on another level, see above.

// this line is causing violation due to hidden dependency
MetroThemeManager.ChangeAppStyle(Application.Current, metroAccent, metroTheme);

// this line is causing an error when closing the splash screen
splashDispacher.BeginInvokeShutdown(DispatcherPriority.Send);
Erythropoiesis answered 27/4, 2017 at 21:36 Comment(2)
Great work. Spot on. Should have seen that one to be fair... It's code that was only recently added.Clubbable
I am glad it helped, indirect dependencies are hard to detect, but imho handling internally the Loaded event was not the best design decision on Metro side either, very hard to override if need be.Erythropoiesis

© 2022 - 2024 — McMap. All rights reserved.