SplashScreen mit Feedback

    • WPF

      SplashScreen mit Feedback

      An einen SplashScreen stellt man folgende Anforderungen:
      • er soll im Nebenthread laufen, damit der vollständig mit Startup beschäftigte MainThread ihn nicht blockiert.
      • Es soll einen Kommunikations-Mechanismus geben, über den der MainThread Nachrichten über den Fortschritt des eigentlichen StartVorgangs der Anwendung senden und anzeigen kann.
      Ich stelle hier eine Lösung vor, die im Codebehind der Application.Xaml angesiedelt ist, in der Application.OnStartup-Überschreibung:

      VB.NET-Quellcode

      1. Imports System.Threading
      2. Class Application
      3. Private _Splash As Splash
      4. Private _SplashVM As SplashVM
      5. Protected Overrides Sub OnStartup(e As StartupEventArgs)
      6. MyBase.OnStartup(e)
      7. StartSplash()
      8. Thread.Sleep(1300) '"aufwändige Initialisierung"
      9. TrySplashMessage("erste Meldung vom LadeProzess")
      10. Thread.Sleep(1000)
      11. TrySplashMessage("oh, ich muß noch bisserl überlegen")
      12. Thread.Sleep(1000)
      13. Dim w = New MainWindow()
      14. AddHandler w.ContentRendered, Sub(s, cre) _Splash.Dispatcher.BeginInvoke(DirectCast(AddressOf CleanUpSplash, Action))
      15. 'AddHandler w.ContentRendered, Sub(s, cre) _Splash.DispatchInvoke(AddressOf CleanUpSplash)
      16. MainWindow = w
      17. w.Show()
      18. End Sub
      19. ''' <summary> splashMessage will be omitted, if the splash isn't ready yet </summary>
      20. Private Sub TrySplashMessage(splashMessage As String)
      21. If _Splash Is Nothing Then Return
      22. If _SplashVM Is Nothing Then
      23. _SplashVM = DirectCast(_Splash.Dispatcher.Invoke(DirectCast(Function() _Splash.DataContext, Func(Of Object))), SplashVM)
      24. '_SplashVM = DirectCast(_Splash.DispatchInvoke(Function() _Splash.DataContext), SplashVM)
      25. End If
      26. 'implement your own Progress-Reporting, e.g. with Progressbars, slide-shows or whatever
      27. _SplashVM.Text = splashMessage
      28. End Sub
      29. Private Sub StartSplash()
      30. Dim thr = New Thread(Sub()
      31. _Splash = New Splash()
      32. _Splash.ShowDialog()
      33. End Sub)
      34. thr.Priority = ThreadPriority.Highest
      35. thr.SetApartmentState(ApartmentState.STA)
      36. thr.Start()
      37. End Sub
      38. Private Sub CleanUpSplash()
      39. _Splash.Close()
      40. 'making objects inaccessible from code enables the GarbageCollector to clean them up
      41. _Splash = Nothing
      42. _SplashVM = Nothing
      43. End Sub
      44. End Class
      Um das zu verwenden muß man von der üblichen Architektur bisserl abweichen - vor allem kann man nun nicht mehr im Application.Xaml den StartupUri angeben. Dies deshalb, weil man ja codeseitige Kontrolle braucht über beide Windows: SplashWindow und MainWindow, damit man im MainWindow.ContentRendered-Event Zugriff auf den Splash hat, um ihn zu schließen.

      Erläuterung zum Code
      Im OnStartup() wird logischerweise als erstes der Splash gestartet, dann folgt die aufwändige Initialisierung mit gelegentlichen Meldungen an den Splash, dann wird das MainWindow erzeugt.
      In dessen ContentRendered-Event (Zeile#16) wird der SplashScreen dann geschlossen und aufgeräumt.
      Der Splash wird übrigens durch 2 Variablen repräsentiert: durch Splash (das in Xaml erstellte Window) und durch SplashVM, das codierte ViewModel des Splashes.
      Die Kommunikation läuft übers Viewmodel, und wie daran gebunden wird steht auf einem anneren Blatt (im Xaml, um genau zu sein ;)).
      Heikelster Punkt ist vlt. Zeile#25: da wird über den Dispatcher des Splashes mittels anonymer Function sein DataContext abgerufen, damit das Splash-Viewmodel hier verfügbar ist für die Kommunikation.
      Invoking über den Dispatcher ist von Microsoft bisserl misdesigned, sodass immer einen Cast erforderlich ist. (Ich vermute ja, die haben da intern eine "Behörde für umständliches Design" - ähnlich dem britischen Ministry of Silly Walks ;).)
      Jedenfalls habich gleich paar Extensions geproggt, mit denen man auch einfacher dispatchen kann (aber hier auskommentiert).

      Eine noch eigenartigere Eigenart ist, dass im Application.OnStartup() die Resourcen der Application-Klasse u.U. noch gar nicht initialisiert sind. Das ist problematisch, denn dann kann das MainWindow nicht angezeigt werden, falls es eine der Application-Resourcen benötigt.
      Lösung: Auslagern der Application-Resourcen in ResourceDictionaries:

      XML-Quellcode

      1. <Application x:Class="Application"
      2. xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      3. xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
      4. <!-- hier kein StartupUri! -->
      5. <Application.Resources>
      6. <ResourceDictionary>
      7. <ResourceDictionary.MergedDictionaries>
      8. <ResourceDictionary Source="ResourceDicts/Other.xaml"/>
      9. </ResourceDictionary.MergedDictionaries>
      10. </ResourceDictionary>
      11. </Application.Resources>
      12. </Application>
      Keine Ahnung, warum das hilft, aber es hilft ?(. Ist auch garnet schlecht, denn einen SplashScreen brauchen eh nur umfangreichen Anwendungen, bei denen es sowieso geraten ist, die vmtl. mannigfaltigen Anwendungs-Resourcen sinnvoll in ResourceDictionaries zu strukturieren.
      Dateien

      Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „ErfinderDesRades“ ()