11. Okt 2016
Lesedauer 14 Min.
UWP kurz und komplett
Wesentliches zu UWP-Apps
Nicht jede UWP-App braucht gleich ein komplexes App-Framework.

Häufig reichen für eine App der Universal Windows Platform (UWP) bereits die Standardmöglichkeiten von UWP mit wenigen Zeilen eigenem Code. Und mit dem neuen Single Process Model braucht man auch keine Background-Tasks mehr. Die meisten UWP-Apps benötigen lediglich einige grundlegende Funktionalitäten, wie etwa:
- Unterstützung des UWP-Lebenszyklus,
- Wiederherstellen des Sitzungsstatus nach Suspendierung der App,
- Navigation,
- Hintergrundverarbeitung,
- Lizenzierung,
- Fehlerverfolgung,
- Ablaufverfolgung,
- Unterstützung für ein helles und ein dunkles Farbschema,
- regionsspezifische Formatierungen,
- Einstellungen (Settings), Info über die App (About).
Grundlagen
Das Beispiel implementiert die Grundfunktionalitäten von UWP-Apps inklusive einer einfachen Navigation über ein Pivot-Element sowie eine Schaltfläche. Es nutzt nur einige wenige Basisklassen, kein App-Framework und kein MVVM-Muster. Der damit vorgestellte Rahmen ist für viele Apps völlig ausreichend. Um die Verständlichkeit zu erhöhen, ist die Struktur des Codes sehr flach gehalten. Die vorgestellten Lösungsansätze sollen das Verständnis komplexer App-Frameworks erleichtern und beim Erweitern solcher Frameworks um fehlende Grundfunktionen helfen. Der Autor hat die vorgestellten Ansätze nicht genauso im produktivem Einsatz. Er möchte seine Erfahrungen mit veröffentlichten Apps und die Ergebnisse aktueller Experimente teilen. Anregungen und Kritik sind ausgesprochen willkommen.Details zu komplexerer Navigation mit Hamburger-Menüs sowie zur Zurücknavigation finden Sie ebenso in diesem Heft [2], Möglichkeiten zur Fehlerverfolgung in einem weiteren Schwerpunktartikel in dieser Ausgabe [3]. Hintergrundinformationen zur Ablaufverfolgung in UWP-Apps mit der Trace-Klasse des Autors finden Sie im Blogpost unter [4].Um wesentliche Punkte herauszuarbeiten, beschränkt sich dieser Artikel beim Aktualisieren von Daten auf Befehle des Nutzers und automatische Pull-Benachrichtigungen. Für viele Apps sind aber automatische Push-Benachrichtigungen die bessere Lösung, siehe [5]. Das Beispiel nutzt folgende Basisklassen und Hilfsfunktionen:- BindableBase mit Dispatcher.Invoke,
- BindablePage,
- AsyncEx,
- ControlHelper,
- AppData.
Das Beispiel nutzt ApplicationData zum Speichern von Nutzerdaten, App-Daten und dem Sitzungsstatus. Es nutzt SetVersionAsync zum Reagieren auf einen inkompatiblen Sitzungsstatus. Dabei übernimmt die Hilfsklasse AppData mittels Json.NET die Serialisierung von Datentypen, die ApplicationData standardmäßig nicht unterstützt, und erleichtert damit den Umgang mit fehlenden Werten. Für Konsistenz und Performance speichert es zusammengehörige Werte als CompositeValues.
Hintergrundverarbeitung und Lebenszyklus
Die meisten UWP-Apps brauchen Background-Code, der läuft, wenn sich die App nicht im Vordergrund befindet. Mit dem Single Process Model aus dem Windows 10 Anniversary Update SDK lässt sich die Hintergrundverarbeitung nun auch ohne BackgroundTasks realisieren [10]. Das vereinfacht den gesamten Code. Insbesondere wird das Synchronisieren gleichzeitiger Ressourcenzugriffe aus Vordergrund-App und Hintergrundverarbeitungen erheblich einfacher. Das Single Process Model führt die Hintergrundverarbeitung im Prozess der App aus und erweitert den Lebenszyklus um die Ereignisse EnteredBackground und LeavingBackgroud, siehe Bild 1 und Listing 1. Bild 2 zeigt die typische Reihenfolge wesentlicher Ereignisse. Beachten Sie, dass LeavingBackground beim Starten ausgelöst wird, auch wenn vorher keine Hintergrundverarbeitung stattfand.
Auch mit dem Single Process Model erfolgt die Aktivierung der Hintergrundverarbeitung über Background-Trigger. Eine Capability ins appxmanifest einzutragen ist aber nicht mehr nötig. Beim Verwenden des neuen Modells ist der letzte sinnvolle Zeitpunkt zum Speichern des Sitzungsstatus nicht mehr beim Auftreten von OnSuspending, sondern bei EnteredBackground. Analog dazu ist das Ereignis LeavingBackgroud der beste Zeitpunkt zum Aktualisieren von UI-Daten.
Listing 1: App-Klasse mit Lebenszyklus-Ereignissen (Teil 1)
<span class="hljs-keyword">NotInheritable</span> <span class="hljs-keyword">Class</span> App <br/> <span class="hljs-keyword">Inherits</span> Application <br/> <br/> <span class="hljs-keyword">Public</span> <span class="hljs-keyword">Shared</span> SessionState <span class="hljs-keyword">As</span> <span class="hljs-keyword">New</span> Settings( <br/> ApplicationData.Current.LocalSettings
Die größte Erleichterung bringt das Single Process Model bei der Synchronisation gleichzeitiger Ressourcenzugriffe aus Vordergrund- und Hintergrundverarbeitung. BackgroundTasks laufen in einem von der App separaten Prozess. Damit ist die prozessübergreifende Synchronisation in UWP-Apps ausgesprochen knifflig, weil die nötigen Named Mutexe Thread-affin sind und sich deshalb mit dem typischerweise asynchronen UWP-Code schlecht vertragen (siehe [11]). Mit dem Single Process Model ist die Synchronisation nun relativ einfach, weil alles in demselben Prozess läuft und sich deshalb für die asynchrone Verarbeitung geeignete In-Process-Synchronisationsmechanismen nutzen lassen.
Listing 2: Hintergrundverarbeitung synchronisieren
<span class="hljs-keyword">Public</span> <span class="hljs-keyword">Shared</span> Async <span class="hljs-keyword">Sub</span> DoBackgroundWork(
Das Beispiel synchronisiert das gleichzeitige Holen und Speichern neuer Wechselkurse durch periodische Hintergrundverarbeitung und die Aktionen des Nutzers über ein AsyncLock, siehe Listing 2. Durch das Synchronisieren wird die Konsistenz des Updates auf den Settings-Wert RatesAsOfDate und die Datei conversions.json sichergestellt und mittels RefreshIsNecessary ein überflüssiger Webzugriff für den Fall gleichzeitiger Aufrufe vermieden. Zusätzlich wird im UI visualisiert, dass gerade eine Verarbeitung läuft – der Einfachheit halber durch Deaktivieren der kompletten CommandBar über den Schalter WebExchangeRates.IsBusy.Das Single Process Model entbindet den Entwickler jedoch nicht von der Notwendigkeit, periodische Aktivitäten – zusätzlich zum Hintergrund – auch mittels Timer-Ereignissen im Vordergrund zu realisieren. Denn der Nutzer kann ja weiterhin die Hintergrundverarbeitung über die Windows-Einstellungen unterbinden.Das Beispiel realisiert dies über die Klasse Threading.Timer. Es verwendet keinen DispatcherTimer, um den UI-Thread nicht unnötig zu belasten. Gegebenenfalls erforderliche Zugriffe im UI-Thread erledigen dabei BindableBase mit Dispatcher.Invoke für PropertyChanged-Ereignisse und DispatcherHelper.RunOnUIThreadIfNecessary für CollectionChanged-Ereignisse.
Nutzerdaten, App-Daten und Sitzungsstatus
Bei den Daten von Apps ist zu unterscheiden zwischen den permanent zu speichernden Nutzerdaten, den App-Daten und dem Sitzungsstatus (session state, suspension state). Nutzerdaten sind die Daten, welche die Nutzer mit der App verwalten, also zum Beispiel die Dokumente einer Office-App oder die Einkaufsliste einer Shopping-App. App-Daten sind an die Existenz der App gebundene Daten, zum Beispiel die Einstellungen der App (app settings). Nutzerdaten lassen sich auf unterschiedlichste Arten speichern, zum Beispiel über ApplicationData in lokalen oder wandernden (roaming) Settings-Containern oder auch in Dateien, über das Entity Framework in einer lokalen SQLite-Datenbank oder über Dienste im Web. App-Daten speichert man üblicherweise in ApplicationData.Das Erhalten des Sitzungsstatus ist nötig, um den UWP-Lebenszyklus zu unterstützen [12]. UWP-Apps können ja bei Nichtbenutzung automatisch von Windows beendet werden. Dies geschieht, sobald Systemressourcen freigemacht werden müssen, und beim Herunterfahren von Windows. Danach befindet die App sich im Status ApplicationExecutionState.Terminated. Wenn der Nutzer nun zu einer so beendeten App zurückwechselt, sollte es für ihn nicht erkennbar sein, dass die App zwischenzeitlich beendet war. Dazu muss der letzte Sitzungsstatus genau so wiederhergestellt werden, als ob die App die ganze Zeit durchgelaufen wäre.Mit dem NavigationCacheMode lässt sich der Sitzungsstatus einer App leicht automatisch durch UWP erhalten, aber nur, solange diese nicht beendet wird. Mit der Einstellung Required erhält UWP den Status aller Page-Instanzen, mit Enabled maximal die in CacheSize angegebene Anzahl von Instanzen. Zur vollen Kontrolle über den Status lässt sich der NavigationCacheMode ausschalten und wie für beendete Apps manuell behandeln. Für den Fall, dass die App beendet wurde, muss man mit eigenem Code für den Erhalt des Sitzungsstatus sorgen.Häufig werden als Sitzungsstatus nur die Werte unvollständiger Eingaben und der Navigations-Status (also aktive Page und Navigationshistorie) erwähnt. Der Sitzungsstatus wirklicher Apps ist aber meist deutlich komplexer. Er umfasst unter anderem auch, welches Element zuletzt den Fokus hatte, welche Listeneinträge gewählt waren, sowie die Scroll-Positionen in Listen, siehe Bild 3.
Zum Beenden von Apps ist zu beachten, dass UWP-Apps beim Beenden nicht informiert werden. Sie sind zu diesem Zeitpunkt ja bereits suspendiert und laufen also gar nicht mehr. Deshalb müssen sie ihren Sitzungsstatus spätestens beim Ereignis Suspending speichern, beziehungsweise mit dem Single Process Model beim Ereignis EnteredBackground.
Sitzungsstatus erhalten
Bei den Modern Apps von Windows 8.x gehörten zu manchen Projektvorlagen die Helferklassen SuspensionManager und NavigationHelper. Sie halfen unter anderem beim Erhalten des Sitzungsstatus. Mit UWP werden diese Klassen nicht mehr bereitgestellt. Sie lassen sich jedoch, wenn gewünscht, leicht übernehmen. Das Beispiel nutzt stattdessen die Hilfsklasse AppData zum Speichern von Nutzerdaten, App-Daten und dem Sitzungsstatus. Es verwendet dabei folgende Containerstruktur in ApplicationData.Current.LocalSettings:
<span class="hljs-comment">SessionState</span>
<span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-comment">StateTimeStamp</span>
<span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-comment">navState</span>
<span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-comment">MainPage</span>
<span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-comment">MainPage</span>
<span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-comment">focusedElementHash</span>
<span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-comment">numbersListViewVerticalOffset</span>
<span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-comment">SelectableListView_SelectedIndex</span>
<span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-string">.</span><span class="hljs-string">.</span><span class="hljs-string">.</span>
<span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-comment">ResultPage</span>
<span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-string">.</span><span class="hljs-string">.</span><span class="hljs-string">.</span>
<span class="hljs-comment">AppSettings</span>
<span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-comment">ColorTheme</span>
<span class="hljs-comment">Content</span>
<span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-comment">RatesAsOfDate</span>
<span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-comment">\Content</span> <span class="hljs-comment">(Folder!)</span>
<span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-literal">-</span><span class="hljs-comment">conversions</span><span class="hljs-string">.</span><span class="hljs-comment">json</span>
Das Erhalten des Status von Eingabefeldern und des Navigationsstatus ist relativ unkompliziert. Die Werte von Feldern lassen sich einfach speichern, und zum Wiederherstellen des Status lassen sie sich dem Content der Felder oder gebundenen Properties zuweisen. Analoges gilt für ausgewählte Listenelemente. So bindet das Beispiel den SelectedIndex von
_SelectableListView an einen Wert des Sitzungsstatus. Der Navigationsstatus lässt sich zum Speichern mittels GetNavigationState ermitteln und mit SetNavigationState leicht wiederherstellen.Das Erhalten des Fokus auf dem zuletzt aktiven Element und der Scroll-Positionen in Listen ist aber je nach App deutlich komplexer. Das Wiederherstellen ist hier erst möglich, wenn diese Elemente bereits existieren, also noch nicht in OnNavigatedTo, sondern erst bei späteren Ereignissen wie *Loaded. Das Beispiel speichert zur Identifikation des aktiven Elements dessen HashCode und identifiziert das Element beim Wiederherstellen mittels eines Filters zu FindChild, siehe Listing 3. Zum Speichern der Scroll-Position in Listen verwendet es die Eigenschaft VerticalOffset des beinhalteten ScrollViewers und zum Wiederherstellen dessen Methode ChangeView.
_SelectableListView an einen Wert des Sitzungsstatus. Der Navigationsstatus lässt sich zum Speichern mittels GetNavigationState ermitteln und mit SetNavigationState leicht wiederherstellen.Das Erhalten des Fokus auf dem zuletzt aktiven Element und der Scroll-Positionen in Listen ist aber je nach App deutlich komplexer. Das Wiederherstellen ist hier erst möglich, wenn diese Elemente bereits existieren, also noch nicht in OnNavigatedTo, sondern erst bei späteren Ereignissen wie *Loaded. Das Beispiel speichert zur Identifikation des aktiven Elements dessen HashCode und identifiziert das Element beim Wiederherstellen mittels eines Filters zu FindChild, siehe Listing 3. Zum Speichern der Scroll-Position in Listen verwendet es die Eigenschaft VerticalOffset des beinhalteten ScrollViewers und zum Wiederherstellen dessen Methode ChangeView.
Listing 3: Erhalten von Page-Session-State und Nutzerdaten (Teil 1)
<span class="hljs-keyword">Public</span> <span class="hljs-keyword">NotInheritable</span> <span class="hljs-keyword">Class</span> MainPage : <br/> <span class="hljs-keyword">Inherits</span> BindablePage <br/><br/> <span class="hljs-keyword">Private</span> _pageState <span class="hljs-keyword">As</span> <span class="hljs-keyword">New</span> AppData( <br/> App.SessionState.Container.CreateContainer( <br/> <span class="hljs-keyword">Me</span>.<span class="hljs-built_in">GetType</span>.Name, <br/> ApplicationDataCreateDisposition.Always)) <br/> ... <br/> <span class="hljs-keyword">Protected</span> <span class="hljs-keyword">Overrides</span> Async <span class="hljs-keyword">Sub</span> OnNavigatedTo( <br/> e <span class="hljs-keyword">As</span> NavigationEventArgs) <br/><br/> <span class="hljs-keyword">MyBase</span>.OnNavigatedTo(e) <br/> Await RestoreContentAsync() <br/> <span class="hljs-keyword">If</span> App.SessionState.Exists <span class="hljs-keyword">Then</span> RestorePageState() <br/> <span class="hljs-keyword">End</span> <span class="hljs-keyword">Sub</span> <br/><br/> <span class="hljs-keyword">Private</span> Async <span class="hljs-keyword">Function</span> RestoreContentAsync() <span class="hljs-keyword">As</span> Task <br/> Await ExchangeRates.GetRatesFromFileAsync() <br/> <span class="hljs-keyword">End</span> <span class="hljs-keyword">Function</span> <br/><br/> <span class="hljs-keyword">Protected</span> <span class="hljs-keyword">Overrides</span> Async <span class="hljs-keyword">Sub</span> OnNavigatedFrom( <br/> e <span class="hljs-keyword">As</span> NavigationEventArgs) <br/><br/> <span class="hljs-keyword">MyBase</span>.OnNavigatedFrom(e) <br/> Await SaveContentAsync() <br/> SavePageState() <br/> <span class="hljs-keyword">End</span> <span class="hljs-keyword">Sub</span> <br/><br/> <span class="hljs-keyword">Private</span> Async <span class="hljs-keyword">Function</span> SaveContentAsync() <span class="hljs-keyword">As</span> Task <br/> Await ExchangeRates.SaveRatesToFileAsync <br/> <span class="hljs-keyword">End</span> <span class="hljs-keyword">Function</span> <br/><br/> <span class="hljs-keyword">Private</span> <span class="hljs-keyword">Sub</span> _MainPivot_PivotItemLoading(<br/> sender <span class="hljs-keyword">As</span> Pivot, <br/> args <span class="hljs-keyword">As</span> PivotItemEventArgs) <br/><br/> <span class="hljs-keyword">If</span> sender.SelectedIndex = <span class="hljs-number">0</span> <span class="hljs-keyword">Then</span> <br/> <span class="hljs-keyword">Dim</span> focusedElementHash <span class="hljs-keyword">As</span> <span class="hljs-built_in">Integer</span>? = <br/> _pageState.GetValue(<span class="hljs-keyword">Of</span> <span class="hljs-built_in">Integer</span>?)(NameOf( <br/> focusedElementHash), <span class="hljs-literal">Nothing</span>) <br/> <span class="hljs-keyword">If</span> focusedElementHash <span class="hljs-keyword">IsNot</span> <span class="hljs-literal">Nothing</span> <span class="hljs-keyword">Then</span> <br/> <span class="hljs-keyword">Dim</span> focusedControl = <br/> ControlHelper.FindChild(<span class="hljs-keyword">Of</span> Control)(<br/> _RootGrid, <span class="hljs-keyword">Function</span>(x) x.GetHashCode() = <br/> focusedElementHash) <br/> focusedControl?.Focus(<br/> FocusState.Programmatic) <br/> <span class="hljs-keyword">End</span> <span class="hljs-keyword">If</span> <br/> <span class="hljs-keyword">Dim</span> numbersListViewVerticalOffset <span class="hljs-keyword">As</span> <span class="hljs-built_in">Double</span>? = <br/> _pageState.GetValue(<span class="hljs-keyword">Of</span> <span class="hljs-built_in">Double</span>?)(NameOf( <br/> numbersListViewVerticalOffset)) <br/> <span class="hljs-keyword">If</span> numbersListViewVerticalOffset <br/> <span class="hljs-keyword">IsNot</span> <span class="hljs-literal">Nothing</span> <span class="hljs-keyword">Then</span> <br/> GetScrollViewer(<br/> _NumbersListView).ChangeView(<span class="hljs-literal">Nothing</span>, <br/> numbersListViewVerticalOffset, <span class="hljs-literal">Nothing</span>) <br/> <span class="hljs-keyword">End</span> <span class="hljs-keyword">If</span> <br/> <span class="hljs-keyword">End</span> <span class="hljs-keyword">If</span> <br/> <span class="hljs-keyword">End</span> <span class="hljs-keyword">Sub</span> <br/><br/> <span class="hljs-keyword">Private</span> <span class="hljs-keyword">Sub</span> MainPage_Loaded(sender <span class="hljs-keyword">As</span> <span class="hljs-built_in">Object</span>, <br/> e <span class="hljs-keyword">As</span> RoutedEventArgs) <span class="hljs-keyword">Handles</span> <span class="hljs-keyword">Me</span>.Loaded <br/> _pageState.Clear() <br/> <span class="hljs-keyword">End</span> <span class="hljs-keyword">Sub</span> <br/><br/> <span class="hljs-keyword">Private</span> <span class="hljs-keyword">Sub</span> SavePageState() <br/> <span class="hljs-keyword">Dim</span> state <span class="hljs-keyword">As</span> <span class="hljs-keyword">New</span> Dictionary(<span class="hljs-keyword">Of</span> <span class="hljs-built_in">String</span>, <span class="hljs-built_in">Object</span>) <br/> state.Add(NameOf(MainPivot_SelectedIndex), <br/> MainPivot_SelectedIndex) <br/> state.Add(NameOf(<br/> SelectableListView_SelectedIndex), <br/> SelectableListView_SelectedIndex) <br/> _pageState.StoreValues(<span class="hljs-keyword">Me</span>.<span class="hljs-built_in">GetType</span>.Name, state) <br/><br/> <span class="hljs-keyword">Dim</span> focusedElementHash <span class="hljs-keyword">As</span> <span class="hljs-built_in">Integer</span>? = <br/> FocusManager.GetFocusedElement?.GetHashCode <br/> _pageState.StoreValue(NameOf(focusedElementHash), <br/> focusedElementHash) <br/> <span class="hljs-keyword">Dim</span> numbersListViewVerticalOffset = <br/> GetScrollViewer(_NumbersListView)?.VerticalOffset <br/> _pageState.StoreValue(NameOf(<br/> numbersListViewVerticalOffset), <br/> numbersListViewVerticalOffset) <br/> <span class="hljs-keyword">End</span> <span class="hljs-keyword">Sub</span> <br/><br/> <span class="hljs-keyword">Private</span> <span class="hljs-keyword">Sub</span> RestorePageState() <br/> Trace.Debug(<span class="hljs-string">""</span>) <br/> <span class="hljs-keyword">Dim</span> state <span class="hljs-keyword">As</span> Dictionary(<span class="hljs-keyword">Of</span> <span class="hljs-built_in">String</span>, <span class="hljs-built_in">Object</span>) = <br/> _pageState.GetValues(<span class="hljs-keyword">Me</span>.<span class="hljs-built_in">GetType</span>.Name) <br/> <span class="hljs-keyword">If</span> state <span class="hljs-keyword">Is</span> <span class="hljs-literal">Nothing</span> <span class="hljs-keyword">Then</span> <span class="hljs-keyword">Return</span> <br/> MainPivot_SelectedIndex = <br/> state(NameOf(MainPivot_SelectedIndex)) <br/> SelectableListView_SelectedIndex = <br/> state(NameOf(SelectableListView_SelectedIndex)) <br/> <span class="hljs-keyword">End</span> <span class="hljs-keyword">Sub</span> <br/> ... <br/><br/>
Microsoft empfiehlt, das Speichern von Daten zum Sitzungsstatus nicht auf die letztmöglichen Ereignisse OnSuspending beziehungsweise EnteredBackground zu konzentrieren, sondern es möglichst inkrementell auszuführen, zum Beispiel direkt nach Änderungen, damit sich die Speicherzeit unauffällig über die Arbeit des Nutzers verteilt und sich nicht im letztmöglichen Ereignis anhäuft.Es gibt einige Situationen, in denen es nicht sinnvoll ist, den gespeicherten Sitzungsstatus wiederherzustellen. Für den Fall, dass der Nutzer selbst die App beendet hat (ApplicationExecutionState.ClosedByUser) verwirft man den Sitzungsstatus im Allgemeinen. Weitere Gründe zum Verwerfen des Sitzungsstatus sind unerwartete Fehler, ein Update auf eine inkompatible App-Version oder wenn der Status einfach zu alt ist. Das Beispiel löscht den Sitzungsstatus bei nicht behebbaren Ausnahmen in App.UnhandledException und bei inkompatiblen Versionen in ApplicationData_SetVersion. Zum Reagieren auf einen veralteten Status merkt es sich beim Speichern des Status einen Zeitstempel und verwirft den Status, wenn dieser älter als eine Stunde ist (isSessionStateStale). Dieser Zeitstempel lässt sich auch nutzen, um in OnResuming ein mittels NavigationCacheMode konfiguriertes automatisches Erhalten des Sitzungsstatus durch UWP zu überschreiben. Das ist sinnvoll, weil nicht benutzte Apps bei ausreichenden Systemressourcen über lange Zeit – unter Umständen tagelang – suspendiert bleiben können, ohne dass Windows sie beendet. Mit Windows (Phone) 8.x empfahl Microsoft, den letzten Zustand nur wiederherzustellen, wenn der Nutzer die App fortsetzt, zum Beispiel indem er diese wieder in den Vordergrund holt. Wenn der Nutzer eine automatisch beendete App über das Startmenü aktiviert, lautete die Empfehlung, den Sitzungsstatus zu verwerfen. Diese Empfehlung scheint es für UWP-Apps nicht mehr zu geben.
(Trial) an oder erlauben den Kauf von In-App-Produkten im Windows Store. Das UWP-SDK bietet hierzu das Store-API Windows.ApplicationModel.Store. Infos zum Lizenzierungsstatus der App und von Produkten lassen sich mit der synchronen Funktion Store.LicenseInformation ermitteln, beschreibende Informationen wie Preise und Altersbeschränkungen mit der asynchronen Funktion Store.LoadListingInformationAsync. Zusätzlich gibt es das Ereignis LicenseChanged, mit dem sich die App über Änderungen der Lizenzierung informieren lassen kann. Um Lizenzierungs- und Listing-Infos vom Store zu holen, ist eine Internetverbindung nötig. Dies könnte bei unterbrochener Internetverbindung zu Problemen führen. Deshalb cacht LicenseInformation seine Informationen automatisch lokal und führt deshalb bei fehlender Verbindung nicht zu langen Wartezeiten. Damit kann man LicenseInformation einfach in OnLaunched aufrufen. Wie genau das Cachen funktioniert, ist leider nicht dokumentiert. Zum Testen lassen sich die Infos des Stores mittels Store.CurrentAppSimulator lokal simulieren.Mit dem Anniversary Update SDK gibt es zusätzlich ein alternatives Store-API namens Windows.Services.Store. Mit diesem hat der Autor noch keine Erfahrungen. Hoffentlich behebt es die Fehler des alten API. Das neue API unterstützt nun endlich Abonnements von Produkten, zum Beispiel für ein sich jährliche verlängerndes Abo einer Pro-Version.Das Beispiel fasst die Store-Infos mit Funktionen zum Prüfen der Lizenzierung und zum Kaufen von Produkten in der Klasse Licensing zusammen. Es nutzt die Store-Infos an mehreren Stellen. Im About-Flyout wird angezeigt, ob und für wie lange noch die App sich im Trial-Modus befindet oder ob der Nutzer eine Vollversion besitzt. Der Nutzer kann von hier aus die Vollversion kaufen.Beim Aktualisieren der Wechselkurse aus dem UI heraus wird geprüft, ob der Trial-Modus abgelaufen ist, und dem Nutzer wird die Vollversion aktiv zum Kauf angeboten. Beim automatischen Aktualisieren im Hintergrund wird bei abgelaufenem und bei bald ablaufendem Trial-Modus eine interaktive Benachrichtigung im Action Center angezeigt, siehe Bild 2.Wenn der Nutzer in dieser Nachricht auf die Schaltfläche Get full version … klickt wird die App im Vordergrund aktiviert und aus OnActivated heraus der Kauf der Vollversion angeboten. Um den Nutzer nicht mit vielen solcher Benachrichtigungen zu nerven wird die Nachricht nur einmal erzeugt und verfällt nach einem Tag.
Lizenzierung von App und Einkäufen
Viele Apps sind nicht völlig kostenlos. Sie bieten eine kostenpflichtige Vollversion mit einem kostenlosen Test-Modus(Trial) an oder erlauben den Kauf von In-App-Produkten im Windows Store. Das UWP-SDK bietet hierzu das Store-API Windows.ApplicationModel.Store. Infos zum Lizenzierungsstatus der App und von Produkten lassen sich mit der synchronen Funktion Store.LicenseInformation ermitteln, beschreibende Informationen wie Preise und Altersbeschränkungen mit der asynchronen Funktion Store.LoadListingInformationAsync. Zusätzlich gibt es das Ereignis LicenseChanged, mit dem sich die App über Änderungen der Lizenzierung informieren lassen kann. Um Lizenzierungs- und Listing-Infos vom Store zu holen, ist eine Internetverbindung nötig. Dies könnte bei unterbrochener Internetverbindung zu Problemen führen. Deshalb cacht LicenseInformation seine Informationen automatisch lokal und führt deshalb bei fehlender Verbindung nicht zu langen Wartezeiten. Damit kann man LicenseInformation einfach in OnLaunched aufrufen. Wie genau das Cachen funktioniert, ist leider nicht dokumentiert. Zum Testen lassen sich die Infos des Stores mittels Store.CurrentAppSimulator lokal simulieren.Mit dem Anniversary Update SDK gibt es zusätzlich ein alternatives Store-API namens Windows.Services.Store. Mit diesem hat der Autor noch keine Erfahrungen. Hoffentlich behebt es die Fehler des alten API. Das neue API unterstützt nun endlich Abonnements von Produkten, zum Beispiel für ein sich jährliche verlängerndes Abo einer Pro-Version.Das Beispiel fasst die Store-Infos mit Funktionen zum Prüfen der Lizenzierung und zum Kaufen von Produkten in der Klasse Licensing zusammen. Es nutzt die Store-Infos an mehreren Stellen. Im About-Flyout wird angezeigt, ob und für wie lange noch die App sich im Trial-Modus befindet oder ob der Nutzer eine Vollversion besitzt. Der Nutzer kann von hier aus die Vollversion kaufen.Beim Aktualisieren der Wechselkurse aus dem UI heraus wird geprüft, ob der Trial-Modus abgelaufen ist, und dem Nutzer wird die Vollversion aktiv zum Kauf angeboten. Beim automatischen Aktualisieren im Hintergrund wird bei abgelaufenem und bei bald ablaufendem Trial-Modus eine interaktive Benachrichtigung im Action Center angezeigt, siehe Bild 2.Wenn der Nutzer in dieser Nachricht auf die Schaltfläche Get full version … klickt wird die App im Vordergrund aktiviert und aus OnActivated heraus der Kauf der Vollversion angeboten. Um den Nutzer nicht mit vielen solcher Benachrichtigungen zu nerven wird die Nachricht nur einmal erzeugt und verfällt nach einem Tag.
Info und Einstellungen
Die meisten Apps bieten die Funktionen Info über und Einstellungen (About, Settings). Das Beispiel realisiert diese mittels UserControls in Flyouts zu AppBarButtons, siehe Bild 3. UserControls sind nötig, weil dieselbe Oberfläche in der CommandBar für große Geräte (oben) und für einhändig bedienbare Geräte (unten) genutzt wird. Die komplette CommandBar lässt sich nicht über ein UserControl wiederverwenden, weil BottomAppBar kein UserControl als Content erlaubt. Um Ihnen die lästige Sucherei nach der UWP-spezifischen Syntax von Store-Aufrufen zur Bewertung und zum Anzeigen von Apps desselben Herausgebers zu ersparen, sind diese in AboutUserControl generisch auscodiert.Farbschemata
Mit dem Anniversary Update erlaubt es Windows, zwischen einem hellen und einem dunklen Farbschema (theme) zu wechseln, siehe Settings | Color | App Mode.Der Autor bevorzugt die dunkle Darstellung. Sie ist entspannend für die Augen und reduziert den Batterieverbrauch. Leider ist die dunkle Darstellung immer noch nicht in allen Standard-Apps umgesetzt, der Datei-Explorer ist das prominenteste Beispiel dafür. Je nach dargestelltem Inhalt einer App kann es sinnvoll sein, den Nutzer das Farbschema und die Aktzentfarbe spezifisch für die jeweilige App wählen zu lassen.Das Beispiel ermöglicht die Wahl des Farbschemas über Settings. Die Realisierung ist leider etwas knifflig, weil die Universal Windows Platform es nicht ermöglicht, die Standardeinstellung von Windows zu ermitteln, und es nicht erlaubt, das RequestedTheme einer App während ihrer Laufzeit zu ändern. Es umgeht diese Limitierung unter anderem durch Verwenden von rootFrame.RequestedTheme.Für das dunkle Farbschema ist die Standardfarbe der TitleBar Weiß. Der Beispiel-Code verbessert dies mit der Prozedur AdjustTitleBarColorToTheme auf eine dunklere Farbgebung. Die StatusBar mobiler Geräte (ganz oben angezeigt) ist beim hellen Farbschema zunächst nicht sichtbar, weil Vordergrund und Hintergrund Weiß sind. Das Beispiel korrigiert dies mit der Prozedur FixMobileStatusBarBackground.Sprachen und regionales Format
In Windows 10 kann der Nutzer mehrere Sprachen, eine Region, ein regionales Format sowie mehrere Bildschirmtastaturen (on-screen keyboards) konfigurieren. Eine detaillierte Beschreibung der Möglichkeiten und Probleme damit finden Sie in [13]. UWP-Apps ermöglichen es zwar, Mehrsprachigkeit zu realisieren und dem Nutzer zu erlauben, in der App eine von Windows abweichende Sprache zu wählen. Sie sollen aber unverständlicherweise das vom Nutzer konfigurierte regionale Format ignorieren. Dies wird in der Entwicklergemeinde heftig kritisiert, und auch Microsofts Standard-Apps wie Mail, und Kalender missachten die hauseigene Vorgabe und formatieren beispielsweise Datumsangaben entsprechend dem regionalen Format.Das UWP-SDK bietet leider keinen sauberen Weg zum Ermitteln des regionalen Formats. Das Beispiel behilft sich, indem es die aktuelle CultureInfo mittels eines DateTimeFormatters zu irgendeiner installierten Sprache ermittelt (siehe GetRegionalFormatCultureInfo) und beim jedem Formatieren nutzt:
String.<span class="hljs-keyword">Format</span>(RegionalFormatCultureInfo,
formatterString, <span class="hljs-keyword">value</span>)
Fazit und Ausblick
Einige Grundfunktionalitäten sind für ziemlich jede UWP-App nötig. Die in diesem Artikel und mit dem Beispielprojekt vorgestellten einfachen Lösungen dazu sind als Rahmen für viele Apps bereits ausreichend. Das Wiederherstellen des Sitzungsstatus ist in den meisten Apps deutlich komplexer, als viele Beispiele im Web glauben machen wollen. Je nach Entwicklungsvorhaben gilt es abzuwägen, ob der Einsatz eines App-Frameworks wie Template10 sinnvoll sein könnte. Dabei ist jedoch zu bedenken, dass sich mit Abhängigkeiten zu solchen Frameworks die Nutzung neuer SDK-Features erschweren oder verzögern kann.Der Kauf von Xamarin durch Microsoft eröffnet viele interessante Möglichkeiten für plattformübergreifende Apps. Wie sich das und die Verfügbarkeit der Windows Bridge for iOS sowie der Desktop Bridge für Win32-Apps zukünftig auf die App-Landschaft auswirken wird, bleibt abzuwarten. Unter [14] finden Sie konkrete Infos zur Desktop Bridge (Projekt Centennial).Fussnoten
- Jerry Nixon, Template10, http://www.dotnetpro.de/SL1612AppRahmen1
- Peter Meinl, Hamburger oder Pivot?, Grundlegende Gestaltung von Oberflächen für UWP-Apps, dotnetpro 12/2016, Seite 20 ff., http://www.dotnetpro.de/A1612UI
- Peter Meinl, Was war da los?, dotnetpro 12/2016, Seite 38 ff., http://www.dotnetpro.de/A1612ErrorHandling
- Peter Meinl, Tracing for UWP-Apps, http://www.dotnetpro.de/SL1612AppRahmen2
- Peter Meinl, Nachrichtenschieber, Azure-Push-Benach-richtigungen für UWP-Apps, dotnetpro 12/2016, Seite 32 ff., http://www.dotnetpro.de/A1612Push
- Scott Hanselman, Comparing two techniques in .NET Asynchronous Coordination Primitives, http://www.dotnetpro.de/SL1612AppRahmen3
- Stephen Toub, Building Async Coordination Primitive, http://www.dotnetpro.de/SL1612AppRahmen4
- Stephen Cleary, AsyncEx, http://www.dotnetpro.de/SL1612AppRahmen5
- Maarten Merken, Finding Parent, Child, Children and Sibling controls in XAML for WinRT, http://www.dotnetpro.de/SL1612AppRahmen6
- Microsoft, Background activity with the Single Process Model, http://www.dotnetpro.de/SL1612AppRahmen7
- Peter Meinl, How to synchronize resource access between UWP app and its background tasks?, http://www.dotnetpro.de/SL1612AppRahmen8
- MSDN, App lifecycle, http://www.dotnetpro.de/SL1612AppRahmen9
- Peter Meinl, Quirks with date and number formats in UWP Apps and Windows 10, http://www.dotnetpro.de/SL1612AppRahmen10
- Microsoft, The path from a desktop app to a UWP app, http://www.dotnetpro.de/SL1612AppRahmen11