【问题标题】:Displaying SplashScreen Causing InvalidOperationException显示 SplashScreen 导致 InvalidOperationException
【发布时间】:2017-09-20 08:06:36
【问题描述】:

问题

我有一个 MVVM 应用程序,它使用 Caliburn.Micro 作为 MVVM 框架和 MEF 用于“依赖注入”(引用我知道它不是严格意义上的 DI 容器)。这个大型应用程序的合成过程开始花费越来越多的时间,这取决于 MEF 在应用程序启动期间正在进行的合成数量,因此我想使用动画启动屏幕。

下面我将概述我当前的代码,该代码在单独的线程上显示初始屏幕并尝试启动主应用程序

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

我的ISplashScreenManager 实现是

[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; }
    }
}

ISplashScreenViewModel.Show()ISplashScreenViewModel.Close() 方法分别显示和关闭相应的视图。

错误

这段代码似乎运行良好,因为它在后台线程上启动启动画面并且启动动画工作等。但是,当代码返回到引导程序时,该行

DisplayRootViewFor<IMainWindow>(); 

抛出 InvalidOperationException 并显示以下消息

调用线程无法访问此对象,因为另一个线程拥有它。

堆栈跟踪是

在 System.Windows.Threading.Dispatcher.VerifyAccess() 在 System.Windows.DependencyObject.GetValue(DependencyProperty dp) 在 MahApps.Metro.Controls.MetroWindow.get_Flyouts() 在 d:\projects\git\MahApps.Metro \src\MahApps.Metro\MahApps.Metro.Shared\Controls\MetroWindow.cs:D:\projects\git\MahApps.Metro\ 中 MahApps.Metro.Controls.MetroWindow.ThemeManagerOnIsThemeChanged(Object sender, OnThemeChangedEventArgs e) 的第 269 行src\MahApps.Metro\MahApps.Metro.Shared\Controls\MetroWindow.cs:System.EventHandler1.Invoke 的第 962 行(对象发送者,TEventArgs e) 在 d:\projects\git\MahApps.Metro\src\MahApps.Metro\MahApps.Metro.Shared\Controls\SafeRaise 中的 MahApps.Metro.Controls.SafeRaise.Raise[T](EventHandler1 eventToRaise, Object sender, T args) .cs: d:\projects\git\MahApps.Metro\src\MahApps.Metro\MahApps.Metro.Shared\ThemeManager\ThemeManager.cs 中 MahApps.Metro.ThemeManager.OnThemeChanged(Accent newAccent, AppTheme newTheme) 的第 26 行: d:\projects\git\MahApps.Metro\src\MahApps.Metro\MahApps.Metro.Shared\ThemeManager 中 MahApps.Metro.ThemeManager.ChangeAppStyle(ResourceDictionary resources, Tuple`2 oldThemeInfo, Accent newAccent, AppTheme newTheme) 的第 591 行\ThemeManager.cs:D:\projects\git\MahApps.Metro\src\MahApps.Metro\MahApps.Metro.Shared\ThemeManager 中 MahApps.Metro.ThemeManager.ChangeAppStyle(Application app, Accent newAccent, AppTheme newTheme) 的第 407 行\ThemeManager.cs:Augur.Core.Themes.ThemeManager.SetCurrentTheme(字符串名称)的第 345 行在 F:\Camus\Augur\Src\Augur\Core\Themes\ThemeManager.cs:Augur.Modules.Shell 的第 46 行。视图模块els.ShellViewModel.OnViewLoaded(Object view) in F:\Camus\Augur\Src\Augur\Modules\Shell\ViewModels\ShellViewModel.cs:Caliburn.Micro.XamlPlatformProvider 的第 73 行。c__DisplayClass11_0.b__0(Object s, RoutedEventArgs e) 在 Caliburn.Micro.View.c__DisplayClass8_0.b__0(Object s, RoutedEventArgs e) 在 System.Windows.EventRoute.InvokeHandlersImpl(Object source, RoutedEventArgs args, Boolean reRaised) 在 System.Windows.UIElement.RaiseEventImpl(DependencyObject sender , RoutedEventArgs args) 在 System.Windows.BroadcastEventHelper.BroadcastEvent(DependencyObject root, RoutedEvent routedEvent) 在 System.Windows.BroadcastEventHelper.BroadcastLoadedEvent(Object root) 在 MS.Internal.LoadedOrUnloadedOperation.DoWork() 在 System.Windows.Media.MediaContext。 FireLoadedPendingCallbacks() 在 System.Windows.Media.MediaContext.FireInvokeOnRenderCallbacks() 在 System.Windows.Media.MediaContext.RenderMessageHandlerCore(Object resizedCompositionTarget) 在 System.Windows.Media.MediaContext.RenderMessageHandler (Object resizedCompositionTarget) 在 System.Windows.Interop.HwndTarget.OnResize() 在 System.Windows.Interop.HwndTarget.HandleMessage(WindowMessage msg, IntPtr wparam, IntPtr lparam) 在 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) 在 System.Windows.Threading.ExceptionWrapper.TryCatchWhen(Object source, Delegate callback, Object args, Int32 numArgs, Delegate catchHandler) 在 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)

尝试的解决方案

我已尝试将执行DisplayRootViewFor&lt;IMainWindow&gt;(); 的代码更改为使用调度程序,如下所示

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

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

甚至

TaskScheduler guiScheduler = TaskScheduler.FromCurrentSynchronizationContext();

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

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

试图强制使用 Gui MainThread。以上所有都抛出相同的异常。

问题

  1. 如何调用DisplayRootViewFor&lt;IMainWindow&gt;() 方法并避免此异常?

  2. 这种显示动画飞溅的方法是否合法?

感谢您的宝贵时间。


编辑。我从很棒的 Hans Passant https://stackoverflow.com/a/4078528/626442 那里发现了这个答案。鉴于此,我尝试添加应用程序static App() { }ctor。

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

但是(可能不出所料)这对我没有帮助。相同位置的相同异常...

【问题讨论】:

  • 看起来 Splash show 正在访问 UI 线程。我们可以试试 Dispatcher.Invoke(()=>{splashScreen.Show();});在 DoShowSplashScreen() 方法中
  • 您的初始屏幕代码看起来很奇怪。它与 Application.Current 一起使用,它设置了 SynchronizationContext 一次就永远不会改变它?否则,你有一个小的复制项目吗?为什么不使用标准 WPF 的 SplashScreen ?来源甚至可以得到灵感:referencesource.microsoft.com/#WindowsBase/Base/System/Windows/…
  • 嗨@Simsons 我试过这个。这没有帮助,我得到了同样的例外。
  • @SimonMourier 感谢您抽出宝贵时间。我只为splashThread 设置SynchonizationContextSynchronizationContext 仅附加到当前线程 (codeproject.com/Articles/31971/…)。我错过了什么吗?正如我上面所说,标准 WPF 启动画面只允许显示静态图像,我想在动画启动画面上显示加载信息。如果您愿意提供帮助,我可以压缩我拥有的代码并通过 OneDrive 提供?
  • @MoonKnight - 只要我们可以轻松复制就可以了

标签: c# wpf mvvm mef caliburn.micro


【解决方案1】:

您正在一个新的后台线程上创建SplashScreenView 的实例,但随后在主 UI 线程的ThemeManager 类中调用MetroThemeManager.ChangeAppStyle

因为MetroWindow 类是您的SplashScreenView 的父类,您无法阻止它在内部订阅您通过在ThemeManager 类中调用MetroThemeManager.ChangeAppStyle 触发的ThemeManager.IsThemeChanged 事件。

由于MetroWindow 中的ThemeManager.IsThemeChanged 事件处理程序的代码无法在不同于创建的线程启动屏幕的主 UI 线程上执行,因此您应该 [1] 从标准 @ 派生您的 SplashScreenView 987654332@ 类以避免依赖于 MetroWindow 或 [2] 避免在您的 SplashScreenView 还活着时调用 MetroThemeManager.ChangeAppStyle

注释掉两行代码就是删除违规行为,请参见下面的行。但显然,实际的解决方案需要在另一个层面上完成,见上文。

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

【讨论】:

  • 伟大的工作。发现。应该看到那是公平的......这是最近才添加的代码。
  • 我很高兴它有帮助,间接依赖很难检测到,但是恕我直言,在内部处理 Loaded 事件也不是 Metro 方面的最佳设计决策,如果需要,很难覆盖。
猜你喜欢
  • 2012-02-09
  • 2014-03-18
  • 1970-01-01
  • 2022-01-11
  • 2021-04-10
  • 1970-01-01
  • 2012-05-08
  • 1970-01-01
相关资源
最近更新 更多