
Die Benutzeroberflächen moderner Anwendungen sind normalerweise komplex. Oft ist es erforderlich, die Unterstützung für die Seitennavigation zu implementieren, Eingabefelder verschiedener Art zu verarbeiten und Informationen basierend auf vom Benutzer ausgewählten Parametern anzuzeigen oder auszublenden. Um UX zu verbessern, muss die Anwendung gleichzeitig den Status der Schnittstellenelemente während des Suspendierens oder Herunterfahrens auf der Festplatte speichern und den Status von der Festplatte wiederherstellen, wenn das Programm neu gestartet wird.
Das ReactiveUI MVVM-Framework schlägt vor, den Status einer Anwendung beizubehalten, indem das Diagramm der Präsentationsmodelle zum Zeitpunkt der Programmunterbrechung serialisiert wird, während die Mechanismen zur Bestimmung des Zeitpunkts der Unterbrechung für Frameworks und Plattformen unterschiedlich sind. Daher wird für WPF das Exit
Ereignis für Xamarin.Android - ActivityPaused
, für Xamarin.iOS - DidEnterBackground
und für UWP - OnLaunched
Überladung verwendet.
In diesem Artikel wird die Verwendung von ReactiveUI zum Speichern und Wiederherstellen des Softwarestatus mit einer GUI, einschließlich des Status eines Routers, am Beispiel des plattformübergreifenden Avalonia -GUI-Frameworks betrachtet. Das Material setzt ein grundlegendes Verständnis des MVVM-Entwurfsmusters und der reaktiven Programmierung im Kontext der C # -Sprache und der .NET-Plattform für den Leser voraus. Die Schritte in diesem Artikel gelten für Windows 10 und Ubuntu 18.
Projekterstellung
Um das Routing in Aktion zu testen, erstellen Sie ein neues .NET Core-Projekt aus der Avalonia-Vorlage und installieren Sie das Avalonia.ReactiveUI
Paket - eine dünne Schicht aus Avalonia- und ReactiveUI-Integration. Stellen Sie sicher, dass Sie das .NET Core SDK und Git installiert haben, bevor Sie beginnen.
git clone https://github.com/AvaloniaUI/avalonia-dotnet-templates git --git-dir ./avalonia-dotnet-templates/.git checkout 9263c6b dotnet new --install ./avalonia-dotnet-templates dotnet new avalonia.app -o ReactiveUI.Samples.Suspension cd ./ReactiveUI.Samples.Suspension dotnet add package Avalonia.ReactiveUI dotnet add package Avalonia.Desktop dotnet add package Avalonia
Stellen Sie sicher, dass die Anwendung gestartet wird und ein Fenster mit der Aufschrift Willkommen bei Avalonia angezeigt wird!
# Use .NET Core version which you have installed. # It can be netcoreapp2.0, netcoreapp2.1 and so on. dotnet run --framework netcoreapp3.0

Connect Avalonia Pre-Builds von MyGet
Um die neuesten Avalonia-Builds zu verbinden und zu verwenden, die automatisch in MyGet veröffentlicht werden, wenn sich der Avalonia-Repository-Hauptzweig auf GitHub ändert, verwenden wir die nuget.config
Pakets nuget.config
. Damit die IDE- und .NET Core-CLI nuget.config
, müssen Sie eine sln
Datei für das oben erstellte Projekt generieren. Wir verwenden die Tools der .NET Core CLI:
dotnet new sln # Ctrl+C dotnet sln ReactiveUI.Samples.Suspension.sln add ReactiveUI.Samples.Suspension.csproj
Erstellen Sie eine Datei nuget.config
in einem Ordner mit einer .sln
Datei mit folgendem Inhalt:
<?xml version="1.0" encoding="utf-8"?> <configuration> <packageSources> <add key="AvaloniaCI" value="https://www.myget.org/F/avalonia-ci/api/v2" /> </packageSources> </configuration>
Möglicherweise müssen Sie die IDE neu starten oder die gesamte Lösung hochladen und herunterladen. Wir aktualisieren Avalonia-Pakete über die NuGet-Paketmanager-Oberfläche Ihrer IDE oder über die Windows-Befehlszeilentools oder das Linux-Terminal auf die erforderliche Version (mindestens 0.9.1
):
dotnet add package Avalonia.ReactiveUI --version 0.9.1 dotnet add package Avalonia.Desktop --version 0.9.1 dotnet add package Avalonia --version 0.9.1 cat ReactiveUI.Samples.Suspension.csproj
Die Projektdatei ReactiveUI.Samples.Suspension.csproj
sieht nun folgendermaßen aus:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>WinExe</OutputType> <TargetFramework>netcoreapp3.0</TargetFramework> </PropertyGroup> <ItemGroup> <Compile Update="**\*.xaml.cs"> <DependentUpon>%(Filename)</DependentUpon> </Compile> <AvaloniaResource Include="**\*.xaml"> <SubType>Designer</SubType> </AvaloniaResource> </ItemGroup> <ItemGroup> <PackageReference Include="Avalonia" Version="0.9.1" /> <PackageReference Include="Avalonia.Desktop" Version="0.9.1" /> <PackageReference Include="Avalonia.ReactiveUI" Version="0.9.1" /> </ItemGroup> </Project>
Erstellen Sie die Ordner Views/
und ViewModels/
im Projektstamm, ändern Sie den Namen der MainWindow
Klasse der MainWindow
in MainView
, verschieben Sie ihn in das Verzeichnis Views/
und ändern Sie die Namespaces entsprechend in ReactiveUI.Samples.Suspension.Views
. App.xaml.cs
Inhalt der Program.cs
App.xaml.cs
und " App.xaml.cs
den UseReactiveUI
Aufruf auf den Avalonia Application Builder an und verschieben Sie die Initialisierung der Hauptansicht in " OnFrameworkInitializationCompleted
, um den Empfehlungen zur Verwaltung des Anwendungslebenszyklus zu entsprechen:
Program.cs
class Program {
App.xaml.cs
public class App : Application { public override void Initialize() => AvaloniaXamlLoader.Load(this);
Sie müssen Program.cs using Avalonia.ReactiveUI
hinzufügen. Stellen Sie sicher, dass das Projekt nach dem Aktualisieren der Pakete gestartet wird und das Standard-Begrüßungsfenster anzeigt.
# Use .NET Core version which you have installed. # It can be netcoreapp2.0, netcoreapp2.1 and so on. dotnet run --framework netcoreapp3.0

In der Regel gibt es zwei Hauptansätze für die Implementierung der Navigation zwischen Seiten einer .NET-Anwendung: Ansicht zuerst und Ansicht Modell zuerst. Der View-First-Ansatz umfasst die Steuerung des Navigationsstapels und der Navigation zwischen Seiten auf View-Ebene in der MVVM-Terminologie - beispielsweise unter Verwendung der Frame- und Page- Klassen bei UWP oder WPF. Bei Verwendung des View-Model-First-Ansatzes wird die Navigation auf der Ebene von Präsentationsmodellen implementiert. ReactiveUI- Tools, die das Routing in der Anwendung organisieren, konzentrieren sich auf die Verwendung des View Model-First-Ansatzes. Das reaktive IScreen
Routing besteht aus einer IScreen
Implementierung, die den Status des Routers enthält, mehreren IRoutableViewModel
Implementierungen und dem plattformspezifischen XAML-Steuerelement RoutedViewHost
.

Der Status des Routers wird durch das RoutingState
Objekt dargestellt, das den Navigationsstapel steuert. IScreen
ist die Wurzel des Navigationsstapels, und die Anwendung enthält möglicherweise mehrere Navigationswurzeln. RoutedViewHost
überwacht den Status des entsprechenden RoutingState
Routers und reagiert auf Änderungen im Navigationsstapel, indem das entsprechende IRoutableViewModel
des XAML-Steuerelements eingebettet wird. Die beschriebene Funktionalität wird anhand der folgenden Beispiele veranschaulicht.
Speichern der Ansichtsmodelle auf der Festplatte
Betrachten Sie ein typisches Beispiel für einen Informationssuchbildschirm.

Wir müssen entscheiden, welche Elemente des Bildschirmdarstellungsmodells während der Pause oder des Herunterfahrens der Anwendung auf der Festplatte gespeichert werden sollen und welche bei jedem Start neu erstellt werden sollen. Der Status von ReactiveUI-Befehlen , die die ICommand
Schnittstelle implementieren und an Schaltflächen ReactiveCommand<TIn, TOut>
werden. ReactiveCommand<TIn, TOut>
wird im Konstruktor erstellt und initialisiert, während der Status des CanExecute
Indikators von den Eigenschaften des Ansichtsmodells abhängt und bei Änderungen neu berechnet wird. Die Notwendigkeit, Suchergebnisse zu speichern - ein strittiger Punkt - hängt von den Besonderheiten der Anwendung ab. Es ist jedoch SearchQuery
, den Status des SearchQuery
Eingabefelds zu speichern und wiederherzustellen!
ViewModels / SearchViewModel.cs
[DataContract] public class SearchViewModel : ReactiveObject, IRoutableViewModel { private readonly ReactiveCommand<Unit, Unit> _search; private string _searchQuery;
Die Klasse des Ansichtsmodells ist mit dem Attribut [DataMember]
und den Eigenschaften gekennzeichnet, die mit den [DataMember]
serialisiert [DataMember]
. Dies ist ausreichend, wenn der verwendete Serializer den Opt-In-Ansatz verwendet. Er speichert nur Eigenschaften, die explizit mit Attributen auf der Festplatte gekennzeichnet sind. Im Fall des Opt-Out-Ansatzes müssen die Eigenschaften, die nicht auf der Festplatte gespeichert werden müssen, mit den Attributen [IgnoreDataMember]
werden. Zusätzlich implementieren wir die IRoutableViewModel
Schnittstelle in unser Ansichtsmodell, damit sie später Teil des Navigationsrahmens des Anwendungsrouters werden kann.
Ebenso implementieren wir das Präsentationsmodell der AutorisierungsseiteViewModels / LoginViewModel.cs
[DataContract] public class LoginViewModel : ReactiveObject, IRoutableViewModel { private readonly ReactiveCommand<Unit, Unit> _login; private string _password; private string _username;
Präsentationsmodelle für zwei Seiten der Anwendung sind fertig, implementieren die IRoutableViewModel
Schnittstelle und können in den IScreen
Router integriert werden. Jetzt implementieren wir direkt IScreen
. Mit Hilfe der Attribute [DataContract]
markieren wir, welche Eigenschaften des Ansichtsmodells serialisiert und welche ignoriert werden sollen. [DataMember]
den öffentlichen Setter der Eigenschaft, die im folgenden Beispiel mit dem [DataMember]
gekennzeichnet ist. Die Eigenschaft ist absichtlich zum Schreiben geöffnet, damit der Serializer eine frisch erstellte Instanz des Objekts während der Modelldeserialisierung ändern kann.
ViewModels / MainViewModel.cs
[DataContract] public class MainViewModel : ReactiveObject, IScreen { private readonly ReactiveCommand<Unit, Unit> _search; private readonly ReactiveCommand<Unit, Unit> _login; private RoutingState _router = new RoutingState(); public MainViewModel() {
In unserer Anwendung muss nur RoutingState
auf der Festplatte gespeichert werden. Aus offensichtlichen Gründen müssen Befehle nicht auf der Festplatte gespeichert werden. Ihr Status hängt vollständig vom Router ab. Das serialisierte Objekt muss erweiterte Informationen zu den Typen enthalten, die das IRoutableViewModel
implementieren, damit der Navigationsstapel bei der Deserialisierung wiederhergestellt werden kann. Wir beschreiben die Logik des MainViewModel
Ansichtsmodells, platzieren die Klasse in ViewModels/MainViewModel.cs
und im entsprechenden ReactiveUI.Samples.Suspension.ViewModels
Namespace.

Routing in der Avalonia App
Die Benutzeroberflächenlogik auf der Ebenenebene des Modells und des Präsentationsmodells der Demoanwendung ist implementiert und kann in eine separate Assembly verschoben werden, die auf den .NET-Standard ausgerichtet ist, da sie nichts über das verwendete GUI-Framework weiß. Werfen wir einen Blick auf die Präsentationsebene. In der MVVM-Terminologie ist die Präsentationsschicht für das Rendern des Status des Präsentationsmodells auf dem Bildschirm verantwortlich. Um den aktuellen Status des RoutingState
Routers zu rendern, wird das im Avalonia.ReactiveUI
Paket enthaltene XAML RoutedViewHost
Steuerelement Avalonia.ReactiveUI
. Wir implementieren die GUI für SearchViewModel
- erstellen Sie dazu im Verzeichnis Views/
zwei Dateien: SearchView.xaml
und SearchView.xaml.cs
.
Eine Beschreibung der Benutzeroberfläche unter Verwendung des in Avalonia verwendeten XAML-Dialekts kommt Entwicklern auf der Windows Presentation Foundation, der Universal Windows Platform oder Xamarin.Forms wahrscheinlich bekannt vor. Im obigen Beispiel erstellen wir eine einfache Schnittstelle für das Suchformular. Wir zeichnen ein Textfeld zur Eingabe der Suchabfrage und eine Schaltfläche zum SearchViewModel
der Suche, während wir die Steuerelemente an die Eigenschaften des SearchViewModel
definierten SearchViewModel
Modells binden.
Ansichten / SearchView.xaml
<UserControl xmlns="https://github.com/avaloniaui" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" d:DataContext="{d:DesignInstance viewModels:SearchViewModel}" xmlns:viewModels="clr-namespace:ReactiveUI.Samples.Suspension.ViewModels" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="ReactiveUI.Samples.Suspension.Views.SearchView" xmlns:reactiveUi="http://reactiveui.net" mc:Ignorable="d"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="48" /> <RowDefinition Height="48" /> <RowDefinition Height="48" /> <RowDefinition Height="*" /> </Grid.RowDefinitions> <TextBlock Grid.Row="0" Text="Search view" Margin="5" /> <TextBox Grid.Row="1" Text="{Binding SearchQuery, Mode=TwoWay}" /> <Button Grid.Row="2" Content="Search" Command="{Binding Search}" /> </Grid> </UserControl>
Ansichten / SearchView.xaml.cs
public sealed class SearchView : ReactiveUserControl<SearchViewModel> { public SearchView() {
Der Code- SearchView.xaml
des SearchView.xaml
Steuerelements wird auch bekannten SearchView.xaml
und XF- SearchView.xaml
. Der Aufruf WhenActivated wird verwendet, um Code auszuführen, wenn die Ansicht oder das Ansichtsmodell aktiviert und deaktiviert wird. Wenn Ihre Anwendung Hot Observables (Timer, Geolocation, Verbindung zum Nachrichtenbus) verwendet, sollten Sie diese CompositeDisposable
Aufrufen von DisposeWith
an CompositeDisposable
DisposeWith
damit Hot Observables beim DisposeWith
XAML-Steuerelements und des entsprechenden Ansichtsmodells vom visuellen Baum keine neuen Werte mehr veröffentlichen und keine Lecks mehr auftreten Speicher.
Ebenso implementieren wir die Darstellung der Autorisierungsseite.Ansichten / LoginView.xaml
<UserControl xmlns="https://github.com/avaloniaui" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" d:DataContext="{d:DesignInstance viewModels:LoginViewModel, IsDesignTimeCreatable=True}" xmlns:viewModels="clr-namespace:ReactiveUI.Samples.Suspension.ViewModels" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="ReactiveUI.Samples.Suspension.Views.LoginView" xmlns:reactiveUi="http://reactiveui.net" mc:Ignorable="d"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="48" /> <RowDefinition Height="48" /> <RowDefinition Height="48" /> <RowDefinition Height="48" /> <RowDefinition Height="*" /> </Grid.RowDefinitions> <TextBlock Grid.Row="0" Text="Login view" Margin="5" /> <TextBox Grid.Row="1" Text="{Binding Username, Mode=TwoWay}" /> <TextBox Grid.Row="2" PasswordChar="*" Text="{Binding Password, Mode=TwoWay}" /> <Button Grid.Row="3" Content="Login" Command="{Binding Login}" /> </Grid> </UserControl>
Ansichten / LoginView.xaml.cs
public sealed class LoginView : ReactiveUserControl<LoginViewModel> { public LoginView() { this.WhenActivated(disposables => { }); AvaloniaXamlLoader.Load(this); } }
Bearbeiten Sie die Views/MainView.xaml.cs
Views/MainView.xaml
und Views/MainView.xaml.cs
. RoutedViewHost
XAML RoutedViewHost
im Namespace RoutedViewHost
auf dem Hauptbildschirm, und weisen RoutingState
der RoutingState
Eigenschaft den Status des RoutingState
Routers zu. Fügen Sie Schaltflächen hinzu, um zu den Such- und Autorisierungsseiten zu navigieren, und binden Sie sie an die ViewModels/MainViewModel
beschriebenen ViewModels/MainViewModel
Eigenschaften.
Ansichten / MainView.xaml
<Window xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="ReactiveUI.Samples.Suspension.Views.MainView" xmlns:reactiveUi="http://reactiveui.net" Title="ReactiveUI.Samples.Suspension"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="*" /> <RowDefinition Height="48" /> </Grid.RowDefinitions> <reactiveUi:RoutedViewHost Grid.Row="0" Router="{Binding Router}"> <reactiveUi:RoutedViewHost.DefaultContent> <TextBlock Text="Default Content" /> </reactiveUi:RoutedViewHost.DefaultContent> </reactiveUi:RoutedViewHost> <Grid Grid.Row="1"> <Grid.ColumnDefinitions> <ColumnDefinition Width="*" /> <ColumnDefinition Width="*" /> <ColumnDefinition Width="*" /> </Grid.ColumnDefinitions> <Button Grid.Column="0" Command="{Binding Search}" Content="Search" /> <Button Grid.Column="1" Command="{Binding Login}" Content="Login" /> <Button Grid.Column="2" Command="{Binding Router.NavigateBack}" Content="Back" /> </Grid> </Grid> </Window>
Ansichten / MainView.xaml.cs
public sealed class MainView : ReactiveWindow<MainViewModel> { public MainView() { this.WhenActivated(disposables => { }); AvaloniaXamlLoader.Load(this); } }
Eine einfache Anwendung, die die Routing- Funktionen von ReactiveUI und Avalonia demonstriert, ist bereit. Wenn Sie auf die Schaltflächen Search
und RoutingState
, werden die entsprechenden Befehle aufgerufen, eine neue Instanz des Ansichtsmodells erstellt und der RoutingState
aktualisiert. Das XAML- RoutedViewHost
, das die Änderungen am RoutingState
, versucht, den IViewFor<TViewModel>
, wobei TViewModel
der Typ des Ansichtsmodells ist, von Locator.Current
. Wenn eine registrierte Implementierung von IViewFor<TViewModel>
gefunden wird, wird eine neue Instanz erstellt, in RoutedViewHost
und im Avalonia-Anwendungsfenster angezeigt.

Wir registrieren die erforderlichen Komponenten IViewFor<TViewModel>
und IScreen
in der App.OnFrameworkInitializationCompleted
Methode unserer Anwendung mit Locator.CurrentMutable
. IViewFor<TViewModel>
erforderlich, damit RoutedViewHost
funktioniert, und IScreen
Registrierung von IScreen
erforderlich, damit beim Deserialisieren die LoginViewModel
SearchViewModel
und LoginViewModel
mithilfe des Konstruktors ohne Parameter und Locator.Current
korrekt initialisiert werden Locator.Current
.
App.xaml.cs
public override void OnFrameworkInitializationCompleted() {
Führen Sie die Anwendung aus und stellen Sie sicher, dass das Routing ordnungsgemäß funktioniert. Wenn das XAML-Markup fehlerhaft ist , teilt uns der in Avalonia verwendete XamlIl-Compiler in der Kompilierungsphase genau mit, wo. XamlIl unterstützt auch das XAML-Debugging direkt im IDE-Debugger !
dotnet run --framework netcoreapp3.0

Speichern und Wiederherstellen des gesamten Anwendungsstatus
Nachdem das Routing konfiguriert ist und funktioniert, beginnt der interessanteste Teil: Sie müssen das Speichern von Daten auf der Festplatte implementieren, wenn Sie die Anwendung schließen, und das Lesen von Daten von der Festplatte beim Start zusammen mit dem Status des Routers. Die Initialisierung von Hooks, die Ereignisse zum Starten und Schließen von Anwendungen abhören, wird von einer speziellen AutoSuspendHelper
Klasse durchgeführt, die für jede von ReactiveUI unterstützte Plattform AutoSuspendHelper
. Die Aufgabe des Entwicklers besteht darin, diese Klasse ganz am Anfang der Wurzel der Anwendungszusammensetzung zu initialisieren. Es ist auch erforderlich, die Eigenschaft RxApp.SuspensionHost.CreateNewAppState
Funktion zu initialisieren, die den Standardstatus der Anwendung RxApp.SuspensionHost.CreateNewAppState
wenn kein gespeicherter Status vorliegt oder ein unerwarteter Fehler aufgetreten ist oder wenn die gespeicherte Datei beschädigt ist.
Als Nächstes müssen Sie die Methode RxApp.SuspensionHost.SetupDefaultSuspendResume
und die Implementierung von ISuspensionDriver
- dem Treiber, der das ISuspensionDriver
und liest. Um ISuspensionDriver
zu implementieren, verwenden ISuspensionDriver
die Newtonsoft.Json
Bibliothek und den System.IO
Namespace, um mit dem Dateisystem zu arbeiten. Installieren Sie dazu das Newtonsoft.Json
Paket:
dotnet add package Newtonsoft.Json
Treiber / NewtonsoftJsonSuspensionDriver.cs
public class NewtonsoftJsonSuspensionDriver : ISuspensionDriver { private readonly string _file; private readonly JsonSerializerSettings _settings = new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.All }; public NewtonsoftJsonSuspensionDriver(string file) => _file = file; public IObservable<Unit> InvalidateState() { if (File.Exists(_file)) File.Delete(_file); return Observable.Return(Unit.Default); } public IObservable<object> LoadState() { var lines = File.ReadAllText(_file); var state = JsonConvert.DeserializeObject<object>(lines, _settings); return Observable.Return(state); } public IObservable<Unit> SaveState(object state) { var lines = JsonConvert.SerializeObject(state, _settings); File.WriteAllText(_file, lines); return Observable.Return(Unit.Default); } }
— System.IO
Universal Winows Platform, — File
Directory
StorageFile
StorageFolder
. , IRoutableViewModel
, Newtonsoft.Json
TypeNameHandling.All
. Avalonia — App.OnFrameworkInitializationCompleted
:
public override void OnFrameworkInitializationCompleted() {
AutoSuspendHelper
Avalonia.ReactiveUI
IApplicationLifetime
— , ISuspensionDriver
. ISuspensionDriver
appstate.json
:
appstate.json— $type
, , .
{ "$type": "ReactiveUI.Samples.Suspension.ViewModels.MainViewModel, ReactiveUI.Samples.Suspension", "Router": { "$type": "ReactiveUI.RoutingState, ReactiveUI", "_navigationStack": { "$type": "System.Collections.ObjectModel.ObservableCollection`1[[ReactiveUI.IRoutableViewModel, ReactiveUI]], System.ObjectModel", "$values": [ { "$type": "ReactiveUI.Samples.Suspension.ViewModels.SearchViewModel, ReactiveUI.Samples.Suspension", "SearchQuery": "funny cats" }, { "$type": "ReactiveUI.Samples.Suspension.ViewModels.LoginViewModel, ReactiveUI.Samples.Suspension", "Username": "worldbeater" } ] } } }
, , , , , , ! , , ReactiveUI — UWP WPF, Xamarin.Forms.

: ISuspensionDriver
Akavache — UserAccount
Secure
iOS UWP , , Android BundleSuspensionDriver ReactiveUI.AndroidSupport
. JSON Xamarin.Essentials SecureStorage
. , — !
Nützliche Links