MoonKnight
MoonKnight

Reputation: 23833

Splash Screen that Supports Loading Progress

I have the following bootstrapper class using Caliburn Micro for my MVVM framework

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

    public Bootstrapper()
    {
        PreInitialize();
        Initialize();
    }

    protected virtual void PreInitialize() { }

    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);

        // Now get all other assemblies (excluding the priority assemblies).
        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)
    {
        base.OnStartup(sender, suea);
        DisplayRootViewFor<IMainWindow>();
    }

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

    protected CompositionContainer Container { get; set; }

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

This is fine, works well, loads my exported modules etc. Now I want to implement a splash screen that shows progress (a progress bar and information on the loaded exports etc.), so I don't want the standard WPF SplashScreen which is merely a static image.

Now I have seen Custom caliburn.micro splashscreen with shell screen conductor but for me this is not the awswer, it is only after OnStartup that the MEF exports are loaded.

So, I have added the following SplashScreenManager class

[Export(typeof(ISplashScreenManager))]
[PartCreationPolicy(CreationPolicy.NonShared)]
public class SplashScreenManager : ISplashScreenManager
{
    private IWindowManager windowManager;
    private ISplashScreen splashScreen;

    private Thread splashThread;
    private Dispatcher splashDispacher;

    [ImportingConstructor]
    public SplashScreenManager(IWindowManager windowManager, ISplashScreen splashScreen)
    {
        if (windowManager == null)
            throw new ArgumentNullException("windowManager cannot be null");

        if (splashScreen == null)
            throw new ArgumentNullException("splashScreen cannot be null");

        this.windowManager = windowManager;
        this.splashScreen = splashScreen;
    }

    public void ShowSplashScreen()
    {
        splashDispacher = null;
        if (splashThread == null)
        {
            splashThread = new Thread(new ThreadStart(DoShowSplashScreen));
            splashThread.SetApartmentState(ApartmentState.STA);
            splashThread.IsBackground = true;
            splashThread.Start();
            Log.Trace("Splash screen thread started");
        }
    }

    private void DoShowSplashScreen()
    {
        splashDispacher = Dispatcher.CurrentDispatcher;
        SynchronizationContext.SetSynchronizationContext(
            new DispatcherSynchronizationContext(splashDispacher));

        splashScreen.Closed += (s, e) =>
            splashDispacher.BeginInvokeShutdown(DispatcherPriority.Background);
        Application.Current.Dispatcher.BeginInvoke(
            new System.Action(delegate { windowManager.ShowWindow(splashScreen); }));

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

    public void CloseSplashScreen()
    {
        if (splashDispacher != null)
        {
            splashDispacher.BeginInvoke(
                new System.Action(delegate { splashScreen.Close(); }));
            Log.Trace("Splash screen close requested");
        }
    }

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

which attempts shows the splash screen on a background thread using Caliburn's IWindowManager. Where ISplashScreenManager is

public interface ISplashScreenManager
{
    void ShowSplashScreen();

    void CloseSplashScreen();

    ISplashScreen SplashScreen { get; }
}

and then we have the ISplashScreen[ViewModel] implementation

[Export(typeof(ISplashScreen))]
[PartCreationPolicy(CreationPolicy.NonShared)]
public class SplashScreenViewModel : Screen, ISplashScreen, ISupportProgress
{
    // Code to provide updates to the view etc. 
}

with ISplashScreen as an empty marker interface at this point.

So my question is how do I correctly order the invocation of the SplashScreenViewModel so that the splash screen gets displayed whilst the modules get loaded in the bootstrapper GetInstance method?

I have tried doing something like

protected override void OnStartup(object sender, StartupEventArgs suea)
{
    var splashManager = Container.GetExportedValue<ISplashScreenManager>();
    var windowManager = IoC.Get<IWindowManager>();
    windowManager.ShowWindow(splashManager.SplashScreen);

    base.OnStartup(sender, suea);
    DisplayRootViewFor<IMainWindow>();

    splashManager.SplashScreen.TryClose();
}

But this instantly closes the splash screen and does not make use of my multi-threaded code to show the splash screen in the SplashScreenManager.

I am open to amending the code heavily to do what I want, but I can seem to get the correct combination at this point. I want to avoid going too deep into threading and using ManualResetEvents, before asking you fine folk for advice on how best to proceed.

Thanks for your time.


Partial Solution: I now have the following code in the OnStartup method in my bootstrapper class

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

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

    base.OnStartup(sender, suea);
    DisplayRootViewFor<IMainWindow>();

    // I have also tried this.
    Application.Current.Dispatcher.Invoke(() => DisplayRootViewFor<IMainWindow>());

    splashScreenManager.CloseSplashScreen();
}

The SplashScreenManager class is

[Export(typeof(ISplashScreenManager))]
[PartCreationPolicy(CreationPolicy.NonShared)]
public class SplashScreenManager : ISplashScreenManager
{
    private IWindowManager windowManager;
    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");
        }
    }

    private void DoShowSplashScreen()
    {
        // Get the splash vm on the splashThread.
        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)
        {
            splashScreen.Close();
            Log.Trace("Splash screen close requested");
        }
    }

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

This now displays the splash screen with a indeterminate progress bar (not yet wired up the messages) which looks like

Now, the problem is, when we hit the line

DisplayRootViewFor<IMainWindow>();

it throws an InvalidOperationException with the 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)

I have attempted to change the code to use the Application dispatcher and to store the task scheduler and use that with a Task to get back on the Gui thread. I am not sure why I am loosing the thread context, what am I doing wrong and how can I fix it?

The attempts to fix

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

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

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

or

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

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

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

and

TaskScheduler guiScheduler = TaskScheduler.FromCurrentSynchronizationContext();

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

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

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

Any ideas?

Upvotes: 0

Views: 1932

Answers (1)

mm8
mm8

Reputation: 169160

Why don't you just show the a splash screen window on a background thread the first thing you do in your OnStartup method and then close it once the initialization is finished?:

protected override async void OnStartup(object sender, StartupEventArgs suea)
{
    Application.Current.ShutdownMode = ShutdownMode.OnLastWindowClose;
    Window splashScreenWindow = null;
    Thread splashScreenWindowThread = new Thread(new ThreadStart(() =>
    {
        SynchronizationContext.SetSynchronizationContext(new DispatcherSynchronizationContext(Dispatcher.CurrentDispatcher));
        splashScreenWindow = new Window();
        splashScreenWindow.Content = new ProgressBar() { IsIndeterminate = true };
        splashScreenWindow.Closed += (ss, es) => Dispatcher.CurrentDispatcher.BeginInvokeShutdown(DispatcherPriority.Background);
        splashScreenWindow.Show();
        Dispatcher.Run();
    }));
    splashScreenWindowThread.SetApartmentState(ApartmentState.STA);
    splashScreenWindowThread.IsBackground = true;
    splashScreenWindowThread.Start();

    base.OnStartup(sender, suea);
    //...
    splashScreenWindow.Dispatcher.BeginInvoke(new Action(() => splashScreenWindow.Close()));
}

Upvotes: 2

Related Questions