حفظ حالة التوجيه على القرص في تطبيق واجهة المستخدم الرسومية .NET Framework عبر النظام الأساسي مع ReactiveUI و Avalonia

صورة


واجهات المستخدم لتطبيقات المؤسسة الحديثة معقدة للغاية. غالبًا ما تحتاج ، كمطور ، إلى تنفيذ التنقل داخل التطبيق أو التحقق من صحة إدخال المستخدم أو إظهار أو إخفاء الشاشات بناءً على تفضيلات المستخدم. للحصول على أفضل UX ، يجب أن يكون تطبيقك قادرًا على حفظ الحالة على القرص عند تعليق التطبيق واستعادة الحالة عند استئناف التطبيق.


يوفر ReactiveUI تسهيلات تسمح لك بالاستمرار في حالة التطبيق من خلال إجراء تسلسل لشجرة طراز العرض عند إيقاف تشغيل التطبيق أو تعليقه. تختلف أحداث التعليق لكل منصة. يستخدم ReactiveUI حدث Exit لـ WPF و ActivityPaused لـ Xamarin.Android و DidEnterBackground for Xamarin.iOS و OnLaunched لـ OnLaunched .


في هذا البرنامج التعليمي ، سنقوم ببناء نموذج تطبيقي يوضح استخدام ميزة ReactiveUI Suspension مع Avalonia - إطار عمل واجهة المستخدم الرسومية المستندة إلى .NET Core XAML. من المتوقع أن تكون على دراية بنمط MVVM ومع الامتدادات التفاعلية قبل قراءة هذه الملاحظة. يجب أن تعمل الخطوات الموضحة في البرنامج التعليمي إذا كنت تستخدم نظام التشغيل Windows 10 أو Ubuntu 18 وتثبيت .NET SDK. لنبدأ!


تمهيد المشروع


لرؤية توجيه ReactiveUI قيد التنفيذ ، نقوم بإنشاء مشروع .NET Core جديد يستند إلى قوالب تطبيق Avalonia. ثم نقوم بتثبيت حزمة Avalonia.ReactiveUI . توفر الحزمة خطافات دورة حياة Avalonia خاصة بالمنصة ، وبنية توجيه وتفعيل . تذكر تثبيت .NET Core و 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 dotnet add package Avalonia.Desktop dotnet add package Avalonia 

لنقم بتشغيل التطبيق والتأكد من أنه يعرض نافذة تعرض "مرحبًا بك في Avalonia!"


 # Use .NET Core version which you have installed. # It can be netcoreapp2.0, netcoreapp2.1 and so on. dotnet run --framework netcoreapp3.0 

صورة


تثبيت أفالونيا معاينة يبني من MyGet


يتم نشر أحدث حزم Avalonia إلى MyGet في كل مرة يتم فيها دفع التزام جديد إلى الفرع master لمستودع Avalonia على GitHub. لاستخدام أحدث الحزم من MyGet في تطبيقنا ، سنقوم بإنشاء ملف nuget.config . ولكن قبل القيام بذلك ، نقوم بإنشاء ملف sln للمشروع الذي تم إنشاؤه مسبقًا ، باستخدام .NET Core CLI :


 dotnet new sln # Ctrl+C dotnet sln ReactiveUI.Samples.Suspension.sln add ReactiveUI.Samples.Suspension.csproj 

الآن نقوم بإنشاء ملف nuget.config بالمحتوى التالي:


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

عادةً ما يتطلب الأمر إعادة تشغيل لإجبار IDE الخاص بنا على اكتشاف الحزم من خلاصة MyGet المضافة حديثًا ، لكن إعادة التحميل يجب أن تساعد أيضًا. بعد ذلك ، نقوم بترقية حزم Avalonia إلى الإصدار الأحدث (أو على الأقل إلى 0.9.1 ) باستخدام واجهة المستخدم الرسومية مدير حزمة NuGet ، أو .NET Core CLI:


 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 

يجب أن يبدو ملف ReactiveUI.Samples.Suspension.csproj إلى حد ما كما يلي الآن:


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

نقوم بإنشاء مجلدين جديدين داخل دليل جذر المشروع ، يدعى Views/ و ViewModels/ على التوالي. بعد ذلك ، قمنا بإعادة تسمية فئة MainView إلى MainView إلى المجلد Views/ المجلد. تذكر أن تعيد تسمية المراجع إلى الفصل المحرر في ملف XAML المقابل ، وإلا فلن يتم تجميع المشروع. تذكر أيضًا تغيير مساحة الاسم لـ MainView إلى ReactiveUI.Samples.Suspension.Views للحصول على التناسق. بعد ذلك ، نقوم بتحرير ملفين آخرين ، Program.cs و App.xaml.cs نضيف مكالمة إلى UseReactiveUI إلى منشئ تطبيق Avalonia ، OnFrameworkInitializationCompleted رمز تهيئة التطبيق إلى طريقة OnFrameworkInitializationCompleted لتتوافق مع إرشادات إدارة تطبيق Avalonia


Program.cs


 class Program { // The entry point. Things aren't ready yet, so at this point // you shouldn't use any Avalonia types or anything that // expects a SynchronizationContext to be ready. public static void Main(string[] args) => BuildAvaloniaApp() .StartWithClassicDesktopLifetime(args); // This method is required for IDE previewer infrastructure. // Don't remove, otherwise, the visual designer will break. public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure<App>() .UseReactiveUI() // required! .UsePlatformDetect() .LogToDebug(); } 

App.xaml.cs


 public class App : Application { public override void Initialize() => AvaloniaXamlLoader.Load(this); // The entrypoint for your application. Here you initialize your // MVVM framework, DI container and other components. You can use // the ApplicationLifetime property here to detect if the app // is running on a desktop platform or on a mobile platform (WIP). public override void OnFrameworkInitializationCompleted() { new Views.MainView().Show(); base.OnFrameworkInitializationCompleted(); } } 

قبل محاولة إنشاء المشروع ، نضمن إضافة توجيه using Avalonia.ReactiveUI الجزء العلوي من ملف Program.cs . على الأرجح ، قام IDE الخاص بنا باستيراد مساحة الاسم بالفعل ، ولكن إذا لم يحدث ذلك ، فسنحصل على خطأ وقت الترجمة. أخيرًا ، حان الوقت للتأكد من أن التطبيق يجمع ويدير ويظهر نافذة جديدة:


 dotnet run --framework netcoreapp2.1 

صورة


عبر منصة ReactiveUI التوجيه


هناك طريقتان عامتان لتنظيم التنقل داخل التطبيق في تطبيق .NET عبر الأنظمة الأساسية - العرض أولاً وعرض الطراز أولاً. يفترض النهج السابق أن طبقة العرض تدير كومة التنقل - على سبيل المثال ، باستخدام فئات الإطار والصفحة الخاصة بالنظام الأساسي. مع النهج الأخير ، تهتم طبقة نموذج العرض بالتنقل عبر تجريد النظام الأساسي. تم تصميم الأدوات ReactiveUI مع الأخذ في الاعتبار طريقة عرض نموذج العرض الأول. يتكون توجيه ReactiveUI من تطبيق IScreen ، والذي يحتوي على حالة التوجيه الحالية ، والعديد من IRoutableViewModel وعناصر تحكم XAML خاصة RoutedViewHost تسمى RoutedViewHost .




يقوم كائن RoutingState بتغليف إدارة مكدس التنقل. IScreen هو جذر التنقل ، ولكن على الرغم من الاسم ، فإنه ليس من الضروري أن يشغل الشاشة بأكملها. يتفاعل RoutedViewHost مع التغييرات في RoutingState المنضم ويضمن العرض المناسب لـ IRoutableViewModel المحدد حاليًا. سيتم توضيح الوظيفة الموضحة بأمثلة أكثر شمولًا لاحقًا.


استمرار حالة نموذج العرض


النظر في نموذج عرض شاشة البحث كمثال.




سنقرر ، ما هي خصائص نموذج العرض التي يجب حفظها عند إيقاف تشغيل التطبيق وأي الخصائص التي يجب إعادة إنشائها. ليست هناك حاجة لحفظ حالة الأمر التفاعلي الذي ينفذ واجهة ICommand . عادةً ما تتم تهيئة فئة ReactiveCommand<TIn, TOut> في المُنشئ ، CanExecute مؤشر CanExecute عادةً بشكل كامل على خصائص طراز العرض ويتم إعادة حسابه في كل مرة تتغير فيها أي من هذه الخصائص. إنه أمر قابل للنقاش إذا كنت تريد الاحتفاظ بنتائج البحث ، ولكن حفظ استعلام البحث يعد فكرة جيدة.


ViewModels / SearchViewModel.cs


 [DataContract] public class SearchViewModel : ReactiveObject, IRoutableViewModel { private readonly ReactiveCommand<Unit, Unit> _search; private string _searchQuery; // We inject the IScreen implementation via the constructor. // If we receive null, we use Splat.Locator to resolve the // default implementation. The parameterless constructor is // required for the deserialization feature to work. public SearchViewModel(IScreen screen = null) { HostScreen = screen ?? Locator.Current.GetService<IScreen>(); // Each time the search query changes, we check if the search // query is empty. If it is, we disable the command. var canSearch = this .WhenAnyValue(x => x.SearchQuery) .Select(query => !string.IsNullOrWhiteSpace(query)); // Buttons bound to the command will stay disabled // as long as the command stays disabled. _search = ReactiveCommand.CreateFromTask( () => Task.Delay(1000), // emulate a long-running operation 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); } } 

نحتفل بفئة نموذج العرض بالكامل باستخدام السمة [DataContract] ، وعلّق على الخصائص التي سنقوم بتسلسلها باستخدام السمة [DataMember] . هذا يكفي إذا كنا سنستخدم وضع تسلسل الاشتراك. النظر في أوضاع التسلسل ، يعني إلغاء الاشتراك أنه سيتم إجراء تسلسل لجميع الحقول والخصائص العامة ، ما لم تتجاهلها صراحة من خلال التعليق التوضيحي [IgnoreDataMember] ، يعني التقيد عكس ذلك. بالإضافة إلى ذلك ، نقوم بتطبيق واجهة IRoutableViewModel في فئة طراز العرض الخاصة بنا. هذا مطلوب أثناء استخدام طراز العرض كجزء من مكدس التنقل.


تفاصيل التنفيذ لنموذج عرض تسجيل الدخول

ViewModels / LoginViewModel.cs


 [DataContract] public class LoginViewModel : ReactiveObject, IRoutableViewModel { private readonly ReactiveCommand<Unit, Unit> _login; private string _password; private string _username; // We inject the IScreen implementation via the constructor. // If we receive null, we use Splat.Locator to resolve the // default implementation. The parameterless constructor is // required for the deserialization feature to work. public LoginViewModel(IScreen screen = null) { HostScreen = screen ?? Locator.Current.GetService<IScreen>(); // When any of the specified properties change, // we check if user input is valid. var canLogin = this .WhenAnyValue( x => x.Username, x => x.Password, (user, pass) => !string.IsNullOrWhiteSpace(user) && !string.IsNullOrWhiteSpace(pass)); // Buttons bound to the command will stay disabled // as long as the command stays disabled. _login = ReactiveCommand.CreateFromTask( () => Task.Delay(1000), // emulate a long-running operation 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); } // Note: Saving passwords to disk isn't a good idea. public string Password { get => _password; set => this.RaiseAndSetIfChanged(ref _password, value); } } 

يقوم طرازا العرض بتطبيق واجهة IRoutableViewModel في شاشة التنقل. حان الوقت الآن لتطبيق واجهة IScreen . مرة أخرى ، نستخدم سمات [DataContract] للإشارة إلى الأجزاء التي يجب إجراء تسلسل وتلك التي يجب تجاهلها. في المثال أدناه ، يتم إعلان RoutingState خاصية RoutingState بشكل متعمد على أنه عام - وهذا يسمح RoutingState بتعديل الخاصية عندما يتم إلغاء تسلسلها.


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() { // If the authorization page is currently shown, then // we disable the "Open authorization view" button. var canLogin = this .WhenAnyObservable(x => x.Router.CurrentViewModel) .Select(current => !(current is LoginViewModel)); _login = ReactiveCommand.Create( () => { Router.Navigate.Execute(new LoginViewModel()); }, canLogin); // If the search screen is currently shown, then we // disable the "Open search view" button. 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; } 

في نموذج العرض الرئيسي الخاص بنا ، نقوم بحفظ حقل واحد فقط على القرص - وهو RoutingState نوع RoutingState . لا يتعين علينا حفظ حالة الأوامر التفاعلية ، حيث أن توفرها يعتمد تمامًا على الحالة الحالية لجهاز التوجيه ويتغير بشكل تفاعلي. لتتمكن من استعادة جهاز التوجيه إلى الحالة التي كان فيها بالضبط ، نقوم بتضمين معلومات النوع الموسعة الخاصة IRoutableViewModel عند إجراء تسلسل IRoutableViewModel التوجيه. سوف نستخدم TypenameHandling.All إعداد Newtonsoft.Json لتحقيق ذلك لاحقًا. نضع MainViewModel في ViewModels/ المجلد ، وضبط مساحة الاسم ليكون ReactiveUI.Samples.Suspension.ViewModels .




التوجيه في تطبيق أفالونيا


في الوقت الحالي ، قمنا بتطبيق نموذج العرض التقديمي الخاص بطلبنا. في وقت لاحق ، يمكن استخراج فئات طراز العرض في مجموعة منفصلة تستهدف .NET Standard ، لذلك يمكن إعادة استخدام الجزء الأساسي من تطبيقنا عبر عدة أطر .NET GUI. الآن حان الوقت لتنفيذ جزء واجهة المستخدم الرسومية الخاص بأفالونيا من تطبيقنا. نقوم بإنشاء ملفين في المجلد Views/ ، المسمى SearchView.xaml و SearchView.xaml.cs على التوالي. هذان هما الجزءان من طريقة عرض بحث واحدة - الأولى هي واجهة المستخدم الموصوفة بالتعريف في XAML ، والأخرى تحتوي على رمز C # خلف. هذا هو أساسًا طريقة عرض نموذج البحث التي تم إنشاؤها مسبقًا.


يجب أن تشعر باللهجة XAML المستخدمة في Avalonia على الفور للمطورين القادمين من WPF أو UWP أو XF. في المثال أعلاه ، نقوم بإنشاء تخطيط بسيط يحتوي على مربع نص وزر يقوم بتشغيل البحث. نحن نربط الخصائص والأوامر من SearchViewModel إلى عناصر التحكم المعلنة في SearchView .


المشاهدات / 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> 

المشاهدات / SearchView.xaml.cs


 public sealed class SearchView : ReactiveUserControl<SearchViewModel> { public SearchView() { // The call to WhenActivated is used to execute a block of code // when the corresponding view model is activated and deactivated. this.WhenActivated((CompositeDisposable disposable) => { }); AvaloniaXamlLoader.Load(this); } } 

قد يجد مطورو WPF و SearchView.xaml لملف SearchView.xaml مألوفًا أيضًا. تتم إضافة دعوة إلى WhenActivated لتنفيذ منطق تنشيط العرض. يتم التخلص من الوسيطة التي يتم التخلص منها باعتبارها الوسيطة الأولى لـ WhenActivated عند إلغاء تنشيط العرض. إذا كان التطبيق الخاص بك يستخدم الملاحظات الساخنة (مثل خدمات تحديد الموقع ، وأجهزة ضبط الوقت ، ومجمعات الأحداث) ، فسيكون قرارًا حكيمًا إرفاق الاشتراكات WhenActivated القابل للتصرف عن طريق إضافة مكالمة DisposeWith ، وبالتالي فإن العرض سيتم إلغاء الاشتراك من تلك الملاحظات الساخنة و لن تحدث تسرب الذاكرة.


تفاصيل التنفيذ لعرض تسجيل الدخول

المشاهدات / 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> 

المشاهدات / LoginView.xaml.cs


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

نقوم بتحرير ملفات Views/MainView.xaml و Views/MainView.xaml.cs . نضيف عنصر التحكم RoutedViewHost من Avalonia.ReactiveUI مساحة الاسم إلى تخطيط الإطار الرئيسي XAML وربط خاصية Router RoutedViewHost.Router بخاصية RoutedViewHost.Router . نضيف زرين ، أحدهما يفتح صفحة البحث والآخر يفتح صفحة التفويض.


المشاهدات / 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> <!-- The RoutedViewHost XAML control observes the bound RoutingState. It subscribes to changes in the navigation stack and embedds the appropriate view for the currently selected view model. --> <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> 

المشاهدات / MainView.xaml.cs


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

تطبيق تجريبي توجيه Avalonia و ReactiveUI بسيط جاهز الآن. عندما يضغط المستخدم على أزرار البحث أو تسجيل الدخول ، يتم استدعاء الأمر الذي يؤدي إلى التنقل ويتم تحديث RoutingState . يلاحظ عنصر التحكم RoutedViewHost XAML حالة التوجيه ، ويحاول حل تطبيق IViewFor<TViewModel> Locator.Current من Locator.Current . إذا تم IViewFor<TViewModel> تطبيق IViewFor<TViewModel> ، فسيتم إنشاء مثيل جديد IViewFor<TViewModel> في نافذة Avalonia.




نقوم بتسجيل IViewFor و IViewFor الخاصة IScreen في طريقة App.OnFrameworkInitializationCompleted ، باستخدام Locator.CurrentMutable . تسجيل IViewFor مطلوب للتحكم RoutedViewHost للعمل. يتيح تسجيل IScreen إمكانية البحث عن خاصية تهيئة SearchViewModel و LoginViewModel أثناء إلغاء التسلسل ، وذلك باستخدام مُنشئ بلا معلمات.


App.xaml.cs


 public override void OnFrameworkInitializationCompleted() { // Here we register our view models. Locator.CurrentMutable.RegisterConstant<IScreen>(new MainViewModel()); Locator.CurrentMutable.Register<IViewFor<SearchViewModel>>(() => new SearchView()); Locator.CurrentMutable.Register<IViewFor<LoginViewModel>>(() => new LoginView()); // Here we resolve the root view model and initialize main view data context. new MainView { DataContext = Locator.Current.GetService<IScreen>() }.Show(); base.OnFrameworkInitializationCompleted(); } 

دعنا نطلق تطبيقنا ونتأكد من أداء التوجيه كما ينبغي. إذا حدث خطأ ما في علامة XAML UI ، فسيقوم مترجم Avalonia XamlIl بإبلاغنا بأي أخطاء في وقت الترجمة. علاوة على ذلك ، XamlIl يدعم تصحيح الأخطاء XAML !


 dotnet run --framework netcoreapp3.0 



حفظ واستعادة حالة التطبيق


لقد حان الوقت الآن لتنفيذ برنامج التشغيل المعلق المسؤول عن حفظ واستعادة حالة التطبيق عندما يكون التطبيق معلقًا ويستأنف. AutoSuspendHelper فئة AutoSuspendHelper الخاصة AutoSuspendHelper الأساسي بتهيئة الأشياء ، فأنت كمطور تحتاج فقط لإنشاء مثيل لها في جذر تكوين التطبيق. أيضًا ، تحتاج إلى تهيئة مصنع RxApp.SuspensionHost.CreateNewAppState . إذا كان التطبيق لا يحتوي على بيانات محفوظة ، أو إذا كانت البيانات المحفوظة تالفة ، فإن ReactiveUI تستدعي طريقة المصنع هذه لإنشاء مثيل افتراضي لكائن حالة التطبيق.


ثم ، نقوم باستدعاء طريقة RxApp.SuspensionHost.SetupDefaultSuspendResume ، وتمرير نسخة جديدة من ISuspensionDriver إليها. دعنا ISuspensionDriver واجهة ISuspensionDriver باستخدام Newtonsoft.Json وفصول من مساحة اسم System.IO .


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

تحتوي الطريقة الموصوفة على عيب - لن تعمل بعض فئات System.IO مع إطار عمل UWP ، وتعمل تطبيقات UWP في صندوق رمل وتقوم بالأشياء بشكل مختلف. من السهل حل ذلك - كل ما عليك القيام به هو استخدام فصول StorageFolder و StorageFolder بدلاً من File and Directory عند استهداف UWP. لقراءة مكدس التنقل من القرص ، يجب أن يدعم برنامج التعليق تعليق إلغاء تسلسل كائنات JSON إلى IRoutableViewModel ملموسة ، ولهذا السبب نستخدم إعداد TypeNameHandling.All Newtonsoft.Json serializer. نسجل برنامج التعليق في جذر تكوين التطبيق ، في طريقة App.OnFrameworkInitializationCompleted :


 public override void OnFrameworkInitializationCompleted() { // Initialize suspension hooks. var suspension = new AutoSuspendHelper(ApplicationLifetime); RxApp.SuspensionHost.CreateNewAppState = () => new MainViewModel(); RxApp.SuspensionHost.SetupDefaultSuspendResume(new NewtonsoftJsonSuspensionDriver("appstate.json")); suspension.OnFrameworkInitializationCompleted(); // Read main view model state from disk. var state = RxApp.SuspensionHost.GetAppState<MainViewModel>(); Locator.CurrentMutable.RegisterConstant<IScreen>(state); // Register views. Locator.CurrentMutable.Register<IViewFor<SearchViewModel>>(() => new SearchView()); Locator.CurrentMutable.Register<IViewFor<LoginViewModel>>(() => new LoginView()); // Show the main window. new MainView { DataContext = Locator.Current.GetService<IScreen>() }.Show(); base.OnFrameworkInitializationCompleted(); } 

تقوم فئة AutoSuspendHelper من حزمة Avalonia.ReactiveUI AutoSuspendHelper دورة حياة للتطبيق الخاص بك ، لذلك سيكون إطار ReactiveUI على دراية ISuspensionDriver كتابة حالة التطبيق على القرص ، وذلك باستخدام تطبيق ISuspensionDriver المتوفر. بعد أن أطلقنا تطبيقنا ، سيقوم برنامج التشغيل المعلق بإنشاء ملف JSON جديد باسم appstate.json . بعد إجراء تغييرات في واجهة المستخدم (على سبيل المثال ، اكتب إلى حد ما في حقول النص ، أو انقر فوق أي زر) ثم أغلق التطبيق ، appstate.json ملف appstate.json مشابه لما يلي:


appstate.json

لاحظ أن كل كائن 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 أو Xamarin Native.


صورة


المكافأة: يمكن تنفيذ واجهة ISuspensionDriver باستخدام Akavache - وهو متجر غير متزامن ومستمر لقيمة المفتاح. إذا قمت بتخزين بياناتك في قسم UserAccount أو قسم Secure ، فسيتم نسخ بياناتك تلقائيًا على السحابة على iOS و UWP وستتاح عبر جميع الأجهزة التي تم تثبيت التطبيق عليها. أيضًا ، يوجد BundleSuspensionDriver في حزمة ReactiveUI.AndroidSupport . Xamarin.Essentials يمكن استخدام واجهات برمجة تطبيقات SecureStorage لتخزين البيانات أيضًا. يمكنك أيضًا تخزين حالة التطبيق على خادم بعيد أو في خدمة سحابية مستقلة عن النظام الأساسي.



Source: https://habr.com/ru/post/ar462307/


All Articles