Reputation: 23833
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 ManualResetEvent
s, 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.EventHandler
1.Invoke(Object sender, TEventArgs e) at MahApps.Metro.Controls.SafeRaise.Raise[T](EventHandler
1 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
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