Хабрахабр

Роутинг в кроссплатформенном .NET приложении с сохранением состояния на диск на примере .NET Core, ReactiveUI и Avalonia

При этом, для улучшения UX приложение должно сохранять состояние элементов интерфейса на диск при приостановке или выключении, восстанавливать состояние с диска при повторном запуске программы. Пользовательские интерфейсы современных прикладных приложений, как правило, сложны — зачастую необходимо реализовывать поддержку постраничной навигации, обрабатывать разного рода поля ввода, на основе выбранных пользователем параметров отображать или скрывать информацию.

Так, для WPF используется событие Exit, для Xamarin. MVVM фреймворк ReactiveUI предлагает сохранять состояние приложения путём сериализации графа моделей представления в момент приостановки программы, при этом механизмы определения момента приостановки различаются для фреймворков и платформ. Android — ActivityPaused, для Xamarin.iOS — DidEnterBackground, для UWP — перегрузка OnLaunched.

Материал предполагает наличие базовых представлений о шаблоне проектирования MVVM и о реактивном программировании в контексте языка C# и платформы . В данном материале рассмотрим использование ReactiveUI для сохранения и восстановления состояния ПО с GUI, включая состояние роутера, на примере кроссплатформенного GUI фреймворка Avalonia. Последовательность действий, описанная в статье, применима к ОС Windows 10 и Ubuntu 18. NET у читателя.

Создание проекта

NET Core из шаблона Avalonia, установим пакет Avalonia. Чтобы попробовать роутинг в действии, создадим новый проект . Убедитесь, что перед началом работы у вас установлены . ReactiveUI — тонкий слой интеграции Avalonia и ReactiveUI. NET Core SDK и git.

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

Создадим папки Views/ и ViewModels/ в корне проекта, изменим имя класса MainWindow на MainView для удобства, переместим его в каталог Views/, изменив пространства имён соответствующим образом — на ReactiveUI. Применим вызов UseReactiveUI к билдеру приложения Avalonia, расположенному в Program.cs. Suspension. Samples. Views.

class Program

}

Убедимся, что приложение запускается и показывает окошко с надписью Welcome to Avalonia!

dotnet run --framework netcoreapp2.1

Подключение предварительных сборок Avalonia из MyGet

Чтобы IDE и . Для подключения и использования самых новых сборок Avalonia, автоматически публикуемых в MyGet при изменениях ветки master репозитория Avalonia на GitHub, используем файл конфигурации источников пакетов nuget.config. Воспользуемся средствами . NET Core CLI увидели nuget.config, необходимо сгенерировать sln файл для созданного выше проекта. NET Core CLI:

dotnet new sln
dotnet sln ReactiveUI.Samples.Suspension.sln add ReactiveUI.Samples.Suspension.csproj

Создадим файл nuget.config в папке с .sln-файлом следующего содержания:

<?xml version="1.0" encoding="utf-8"?>
<configuration> <packageSources> <add key="Avalonia" value="https://www.myget.org/F/avalonia-ci/api/v2" /> </packageSources>
</configuration>

Обновим пакеты Avalonia до нужной версии (как минимум 0. Может потребоваться перезапуск IDE, либо выгрузка и загрузка решения целиком. 1-cibuild0002371-beta) с помощью интерфейса пакетного менеджера NuGet вашей IDE, либо с помощью инструментов командной строки Windows или терминала Linux: 8.

dotnet add package Avalonia.ReactiveUI --version 0.8.1-cibuild0002371-beta
dotnet add package Avalonia.Desktop --version 0.8.1-cibuild0002371-beta
dotnet add package Avalonia --version 0.8.1-cibuild0002371-beta

ReactiveUI в Program.cs. Потребуется добавить using Avalonia. Убедимся, что после обновления пакетов проект запускается и показывает окно приветствия по умолчанию.

dotnet run --framework netcoreapp2.1

Кроссплатформенный роутинг ReactiveUI

NET — view-first и view model-first. Как правило, выделяют два основных подхода к реализации навигации между страницами приложения . Инструменты ReactiveUI, организующие роутинг в приложении, ориентированы на использование подхода view model-first. View-first подход подразумевает управление стеком навигации и переходами между страницами на уровне Представления в терминологии MVVM — например, при помощи классов Frame и Page в случае UWP или WPF, а при использовании view model-first подхода навигацию реализуют на уровне моделей представления. Роутинг ReactiveUI состоит из реализации IScreen, содержащей состояние роутера, нескольких реализаций IRoutableViewModel и платформозависимого элемента управления XAML — RoutedViewHost.

IScreen является корнем стека навигации, при этом корней навигации в приложении может быть несколько. Состояние роутера представлено объектом RoutingState, который управляет стеком навигации. Описанная функциональность будет проиллюстрирована примерами ниже. RoutedViewHost наблюдает за состоянием соответствующего ему роутера RoutingState, реагируя на изменения в стеке навигации путём встраивания соответствующего IRoutableViewModel элемента управления XAML.

Сохранение состояния моделей представления на диск

Рассмотрим типичную модель представления экрана поиска информации в качестве примера.

Сохранять состояние команд ReactiveUI, реализующих интерфейс ICommand и привязываемых к кнопкам, необходимости нет — ReactiveCommand<TIn, TOut> создаются и инициализируются в конструкторе, при этом состояние индикатора CanExecute зависит от свойств модели представления и пересчитывается при их изменении. Мы должны решить, какие элементы модели представления экрана сохранять на диск во время приостановки или выключения приложения, а какие — пересоздавать каждый раз при запуске. Необходимость сохранения результатов поиска — спорный вопрос — зависит от специфики приложения, а вот состояние поля ввода SearchQuery было бы разумно сохранять и восстанавливать!

ViewModels/SearchViewModel.cs

[DataContract]
public class SearchViewModel : ReactiveObject, IRoutableViewModel
{ private readonly ReactiveCommand<Unit, Unit> _search; private string _searchQuery; // Получаем реализацию IScreen через конструктор, при получении NULL // достаём IScreen из Splat.Locator. Конструктор без параметров // необходим для коррекной десериализации модели представления. public SearchViewModel(IScreen screen = null) { HostScreen = screen ?? Locator.Current.GetService<IScreen>(); // При каждом изменении свойства SearchQuery проверяем, // соблюдены ли условия начала поиска. var canSearch = this .WhenAnyValue(x => x.SearchQuery) .Select(query => !string.IsNullOrWhiteSpace(query)); // Привязанные к команде кнопки будут выключены, пока // условия начала поиска не соблюдены. _search = ReactiveCommand.CreateFromTask( () => Task.Delay(1000), // эмулируем длительную операцию canSearch); } public IScreen HostScreen { get; } public string UrlPathSegment => "/search"; public ICommand Search => _search; [DataMember] public string SearchQuery { get => _searchQuery; set => this.RaiseAndSetIfChanged(ref _searchQuery, value); }
}

Этого достаточно в том случае, если используемый сериализатор использует opt-in подход — сохраняет на диск только явно помеченные атрибутами свойства, в случае opt-out подхода необходимо промаркировать атрибутами [IgnoreDataMember] те свойства, сохранять которые на диск не нужно. Класс модели представления пометим атрибутом [DataContract], а свойства, которые необходимо сериализовать — атрибутами [DataMember]. Дополнительно, реализуем интерфейс IRoutableViewModel в нашей модели представления, чтобы впоследствии она смогла стать частью стека навигации роутера приложения.

Аналогично реализуем модель представления страницы авторизации

ViewModels/LoginViewModel.cs

[DataContract]
public class LoginViewModel : ReactiveObject, IRoutableViewModel
{ private readonly ReactiveCommand<Unit, Unit> _login; private string _password; private string _username; // Получаем реализацию IScreen через конструктор, при получении NULL // достаём IScreen из Splat.Locator. Конструктор без параметров // необходим для коррекной десериализации модели представления. public LoginViewModel(IScreen screen = null) { HostScreen = Locator.Current.GetService<IScreen>(); // При каждом изменении свойств Username и Password // проверяем, можно ли начать процедуру авторизации. var canLogin = this .WhenAnyValue( x => x.Username, x => x.Password, (user, pass) => !string.IsNullOrWhiteSpace(user) && !string.IsNullOrWhiteSpace(pass)); // Привязанные к команде кнопки будут выключены, пока // пользовательский ввод не завершён. _login = ReactiveCommand.CreateFromTask( () => Task.Delay(1000), // эмулируем длительную операцию canLogin); } public IScreen HostScreen { get; } public string UrlPathSegment => "/login"; public ICommand Login => _login; [DataMember] public string Username { get => _username; set => this.RaiseAndSetIfChanged(ref _username, value); } // Пароль на диск не сохраняем из соображений безопасности! public string Password { get => _password; set => this.RaiseAndSetIfChanged(ref _password, value); }
}

Теперь реализуем непосредственно IScreen. Модели представления двух страниц приложения готовы, реализуют интерфейс IRoutableViewModel и могут быть встроены в роутер IScreen. Обратите внимание на публичный сеттер свойства, помеченного атрибутом [DataMember], на примере ниже — свойство намеренно открыто для записи для того, чтобы сериализатор мог изменить свежесозданный экземпляр объекта при десериализации модели. Промаркируем с помощью атрибутов [DataContract], какие свойства модели представления сериализовывать, а какие — игнорировать.

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() { // Если в данный момент отображается экран авторизации, // выключим кнопку, открывающую страницу авторизации. var canLogin = this .WhenAnyObservable(x => x.Router.CurrentViewModel) .Select(current => !(current is LoginViewModel)); _login = ReactiveCommand.Create( () => { Router.Navigate.Execute(new LoginViewModel()); }, canLogin); // Если в данный момент отображается экран поиска, // выключим кнопку, открывающую страницу поиска. var canSearch = this .WhenAnyObservable(x => x.Router.CurrentViewModel) .Select(current => !(current is SearchViewModel)); _search = ReactiveCommand.Create( () => { Router.Navigate.Execute(new SearchViewModel()); }, canSearch); } [DataMember] public RoutingState Router { get => _router; set => this.RaiseAndSetIfChanged(ref _router, value); } public ICommand Search => _search; public ICommand Login => _login;
}

В сериализованный объект необходимо включать расиширенную информацию о типах, реализующих IRoutableViewModel, чтобы при десериализации стек навигации мог быть восстановлен. В нашем приложении сохранять на диск необходимо только RoutingState, команды по очевидным причинам сохранять на диск не нужно — их состояние целиком зависит от роутера. Samples. Опишем логику модели представления MainViewModel, поместим класс в ViewModels/MainViewModel.cs и в соответствующее пространство имён ReactiveUI. ViewModels. Suspension.

Роутинг в приложении Avalonia

NET Standard, поскольку ничего не знает об используемом GUI-фреймворке. Логика пользовательского интерфейса на уровне слоёв модели и модели представления демо-приложения реализована и может быть вынесена в отдельную сборку, нацеленную на . Слой представления в терминологии MVVM отвечает за отрисовку состояния модели представления на экран, для отрисовки текущего состояния роутера RoutingState используется элемент управления XAML RoutedViewHost, содержащийся в пакете Avalonia. Займёмся слоем представления. Реализуем GUI для SearchViewModel — для этого в директории Views/ создадим два файла: SearchView.xaml и SearchView.xaml.cs. ReactiveUI.

Forms. Описание пользовательского интерфейса с помощью диалекта XAML, используемого в Avalonia, скорее всего покажется знакомым разработчикам на Windows Presentation Foundation, Universal Windows Platform или Xamarin. В примере выше мы создаём тривиальный интерфейс формы поиска — рисуем текстовое поле для ввода поискового запроса и кнопку, запускающую поиск, при этом привязываем элементы управления к свойствам модели представления SearchViewModel, определённой выше.

Views/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>

Views/SearchView.xaml.cs

public sealed class SearchView : ReactiveUserControl<SearchViewModel>
{ public SearchView() { // Вызов WhenActivated используется для выполнения некоторого // кода в момент активации и деактивации модели представления. this.WhenActivated((CompositeDisposable disposable) => { }); AvaloniaXamlLoader.Load(this); }
}

Вызов WhenActivated используется для выполнения некоторого кода в момент активации и деактивации представления или модели представления. Знакомым разработчикам на WPF, UWP и XF покажется и code-behind элемента управления SearchView.xaml. Если ваше приложение использует hot observables (таймеры, геолокацию, соединение с шиной сообщений), будет разумно присоединить их к CompositeDisposable вызовом DisposeWith, чтобы при откреплении элемента управления XAML и соответствующей ему модели представления от визуального дерева hot observables перестали публиковать новые значения и не произошло утечек памяти.

Аналогично реализуем представление страницы авторизации

Views/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>

Views/LoginView.xaml.cs

public sealed class LoginView : ReactiveUserControl<LoginViewModel>
{ public LoginView() { this.WhenActivated(disposables => { }); AvaloniaXamlLoader.Load(this); }
}

Расположим элемент управления XAML RoutedViewHost из пространства имён Avalonia. Отредактируем файлы Views/MainView.xaml и Views/MainView.xaml.cs. Router. ReactiveUI на главном экране, привяжем состояние роутера RoutingState к свойству RoutedViewHost. Добавим кнопки для навигации на страницы поиска и авторизации, привяжем их к свойствам ViewModels/MainViewModel, описанной выше.

Views/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> <!-- Элемент управления, наблюдающий за RoutingState, встраивающий подходящее View текущей ViewModel --> <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>

Views/MainView.xaml.cs

public sealed class MainView : ReactiveWindow<MainViewModel>
{ public MainView() { this.WhenActivated(disposables => { }); AvaloniaXamlLoader.Load(this); }
}

При нажати на кнопки Search и Login вызываются соответствующие команды, создаётся новый экземпляр модели представления и обновляется RoutingState. Простое приложение, демонстрирующее возможности роутинга ReactiveUI и Avalonia, готово. Current. Элемент управления XAML RoutedViewHost, подписавшийся на изменения RoutingState, пробует достать тип IViewFor<TViewModel>, где TViewModel — тип модели представления, из Locator. Если зарегистрированная реализация IViewFor<TViewModel> найдена, будет создан её новый экземпляр, встроен в RoutedViewHost и отображён в окне приложения Avalonia.

AppMain нашего приложения, используя Locator. Зарегистрируем необходимые компоненты IViewFor<TViewModel> и IScreen в методе Program. Регистрация IViewFor<TViewModel> необходима для корректной работы RoutedViewHost, а регистрация IScreen нужна, чтобы при десериализации объекты SearchViewModel и LoginViewModel могли корректно проинициализироваться, используя конструктор без параметров и Locator. CurrentMutable. Current.

Program.cs

private static void AppMain(Application app, string[] args)
{ // Регистрируем модели представления. Locator.CurrentMutable.RegisterConstant<IScreen>(new MainViewModel()); Locator.CurrentMutable.Register<IViewFor<SearchViewModel>>(() => new SearchView()); Locator.CurrentMutable.Register<IViewFor<LoginViewModel>>(() => new LoginView()); // Получаем корневую модель представления и инициализируем контекст данных. app.Run(new MainView { DataContext = Locator.Current.GetService<IScreen>() });
}

Если возникнут какие-либо ошибки в разметке XAML, используемый в Avalonia компилятор XamlIl сообщит нам, где именно, на этапе компиляции. Запустим приложение и убедимся, что роутинг работает корректно. А ещё XamlIl поддерживает отладку XAML прямо в дебаггере IDE!

dotnet run --framework netcoreapp2.1

Сохранение и восстановление состояния приложения целиком

Инициализацией хуков, слушающих события запуска и закрытия приложения, занимается специальный класс AutoSuspendHelper, свой для каждой платформы, которую поддерживает ReactiveUI. Теперь, когда роутинг настроен и работает, начинается самое интересное — необходимо реализовать сохранение данных на диск при закрытии приложения и чтение данных с диска при его старте, вместе с состоянием роутера. Также необходимо проинициализировать свойство RxApp. Задача разработчика — проинициализировать этот класс в самом начале корня композиции приложения. CreateNewAppState функцией, которая вернёт состояние приложения по умолчанию, если сохранённое состояние отсутствует или произошла непредвиденная ошибка, или если сохранённый файл повреждён. SuspensionHost.

SuspensionHost. Далее необходимо вызвать метод RxApp. Для реализации ISuspensionDriver используем библиотеку Newtonsoft. SetupDefaultSuspendResume, передав ему реализацию ISuspensionDriver — драйвера, занимающегося записью и чтением объекта состояния. IO для работы с файловой системой. Json и пространство имён System. Json: Для этого установим пакет Newtonsoft.

dotnet add package Newtonsoft.Json

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

IO не работает с Universal Winows Platform, но это легко исправить — достаточно вместо File и Directory использовать StorageFile и StorageFolder. У данного подхода есть минусы — System. Json этого можно достичь с включённой настройкой TypeNameHandling. Чтобы прочитать стек навигации с диска, драйвер должен поддерживать десериализацию конкретного типа в интерфейс IRoutableViewModel, с Newtonsoft. Зарегистрируем драйвер в корне композиции приложения Avalonia — в AppMain: All у сериализатора.

private static void AppMain(Application app, string[] args)
{ // Инициализируем хуки приостановки. var suspension = new AutoSuspendHelper(app); RxApp.SuspensionHost.CreateNewAppState = () => new MainViewModel(); RxApp.SuspensionHost.SetupDefaultSuspendResume(new NewtonsoftJsonSuspensionDriver("appstate.json")); // Регистрируем сервисы, читаем с диска корневую модель представления. var state = RxApp.SuspensionHost.GetAppState<MainViewModel>(); Locator.CurrentMutable.RegisterConstant<IScreen>(state); Locator.CurrentMutable.Register<IViewFor<SearchViewModel>>(() => new SearchView()); Locator.CurrentMutable.Register<IViewFor<LoginViewModel>>(() => new LoginView()); // Запускаем программу. app.Run(new MainView { DataContext = Locator.Current.GetService<IScreen>() });
}

Приведённая выше реализация 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 не включена реализация AutoSuspendHelper (#2672), поэтому сконструируем вспомогательный класс самостоятельно — это довольно просто, однако стоит учитывать, что в дальнейшем API Avalonia изменится с целью поддержки событий приостановки мобильных приложений. Заметим, что на момент написания статьи в пакет Avalonia.

public sealed class AutoSuspendHelper : IEnableLogger
{ public AutoSuspendHelper(Application app) { RxApp.SuspensionHost.IsResuming = Observable.Never<Unit>(); RxApp.SuspensionHost.IsLaunchingNew = Observable.Return(Unit.Default); var exiting = new Subject<IDisposable>(); app.OnExit += (o, e) => { // Блокировка необходима, чтобы предотвратить преждевременное // завершение работы нашего приложения. var manual = new ManualResetEvent(false); exiting.OnNext(Disposable.Create(() => manual.Set())); manual.WaitOne(); }; RxApp.SuspensionHost.ShouldPersistState = exiting; var errored = new Subject<Unit>(); RxApp.SuspensionHost.ShouldInvalidateState = errored; AppDomain.CurrentDomain.UnhandledException += (o, e) => errored.OnNext(Unit.Default); }
}

Стоит отметить, что данная функциональность будет работать с любой платформой, поддерживаемой ReactiveUI — как с UWP и WPF, так и с Xamarin. Если вы откроете, например, страницу поиска, впечатаете текст в поля ввода, закроете и запустите приложение снова, вы увидите в точности тот экран, который видели при выключении программы! Forms.

AndroidSupport. Бонус: ISuspensionDriver может быть реализован с помощью Akavache — при хранении состояния в секции UserAccount и Secure в случае iOS и UWP данные будут загружены в облако и синхронизированы со всеми устройствами, на которых установлено приложение, а для Android существует реализация BundleSuspensionDriver в пакете ReactiveUI. Essentials SecureStorage. Также при желании можно записывать JSON файлы в Xamarin. Отметим, что хранить состояние приложения можно и на собственном удалённом сервере — всё в руках разработчика!

Полезные ссылки

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть