committed by
GitHub
6 changed files with 192 additions and 5 deletions
@ -0,0 +1,87 @@ |
|||||
|
// Copyright (c) The Avalonia Project. All rights reserved.
|
||||
|
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
||||
|
|
||||
|
using Avalonia; |
||||
|
using Avalonia.VisualTree; |
||||
|
using Avalonia.Controls; |
||||
|
using System.Threading; |
||||
|
using System.Reactive.Disposables; |
||||
|
using System.Reactive.Subjects; |
||||
|
using System.Reactive.Linq; |
||||
|
using System.Reactive; |
||||
|
using ReactiveUI; |
||||
|
using System; |
||||
|
using Avalonia.Controls.ApplicationLifetimes; |
||||
|
using Splat; |
||||
|
|
||||
|
namespace Avalonia.ReactiveUI |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// A ReactiveUI AutoSuspendHelper which initializes suspension hooks for
|
||||
|
/// Avalonia applications. Call its constructor in your app's composition root,
|
||||
|
/// before calling the RxApp.SuspensionHost.SetupDefaultSuspendResume method.
|
||||
|
/// </summary>
|
||||
|
public sealed class AutoSuspendHelper : IEnableLogger, IDisposable |
||||
|
{ |
||||
|
private readonly Subject<IDisposable> _shouldPersistState = new Subject<IDisposable>(); |
||||
|
private readonly Subject<Unit> _isLaunchingNew = new Subject<Unit>(); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="AutoSuspendHelper"/> class.
|
||||
|
/// </summary>
|
||||
|
/// <param name="lifetime">Pass in the Application.ApplicationLifetime property.</param>
|
||||
|
public AutoSuspendHelper(IApplicationLifetime lifetime) |
||||
|
{ |
||||
|
RxApp.SuspensionHost.IsResuming = Observable.Never<Unit>(); |
||||
|
RxApp.SuspensionHost.IsLaunchingNew = _isLaunchingNew; |
||||
|
|
||||
|
if (lifetime is IControlledApplicationLifetime controlled) |
||||
|
{ |
||||
|
this.Log().Debug("Using IControlledApplicationLifetime events to handle app exit."); |
||||
|
controlled.Exit += (sender, args) => OnControlledApplicationLifetimeExit(); |
||||
|
RxApp.SuspensionHost.ShouldPersistState = _shouldPersistState; |
||||
|
} |
||||
|
else if (lifetime != null) |
||||
|
{ |
||||
|
var type = lifetime.GetType().FullName; |
||||
|
var message = $"Don't know how to detect app exit event for {type}."; |
||||
|
throw new NotSupportedException(message); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
var message = "ApplicationLifetime is null. " |
||||
|
+ "Ensure you are initializing AutoSuspendHelper " |
||||
|
+ "when Avalonia application initialization is completed."; |
||||
|
throw new ArgumentNullException(message); |
||||
|
} |
||||
|
|
||||
|
var errored = new Subject<Unit>(); |
||||
|
AppDomain.CurrentDomain.UnhandledException += (o, e) => errored.OnNext(Unit.Default); |
||||
|
RxApp.SuspensionHost.ShouldInvalidateState = errored; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Call this method in your App.OnFrameworkInitializationCompleted method.
|
||||
|
/// </summary>
|
||||
|
public void OnFrameworkInitializationCompleted() => _isLaunchingNew.OnNext(Unit.Default); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Disposes internally stored observers.
|
||||
|
/// </summary>
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
_shouldPersistState.Dispose(); |
||||
|
_isLaunchingNew.Dispose(); |
||||
|
} |
||||
|
|
||||
|
private void OnControlledApplicationLifetimeExit() |
||||
|
{ |
||||
|
this.Log().Debug("Received IControlledApplicationLifetime exit event."); |
||||
|
var manual = new ManualResetEvent(false); |
||||
|
_shouldPersistState.OnNext(Disposable.Create(() => manual.Set())); |
||||
|
|
||||
|
manual.WaitOne(); |
||||
|
this.Log().Debug("Completed actions on IControlledApplicationLifetime exit event."); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,98 @@ |
|||||
|
// Copyright (c) The Avalonia Project. All rights reserved.
|
||||
|
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
||||
|
|
||||
|
using System; |
||||
|
using System.Reactive.Concurrency; |
||||
|
using System.Reactive.Disposables; |
||||
|
using System.ComponentModel; |
||||
|
using System.Threading.Tasks; |
||||
|
using System.Reactive; |
||||
|
using System.Reactive.Subjects; |
||||
|
using System.Reactive.Linq; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Runtime.Serialization; |
||||
|
using System.Threading; |
||||
|
using Avalonia.Controls.ApplicationLifetimes; |
||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Rendering; |
||||
|
using Avalonia.Platform; |
||||
|
using Avalonia.UnitTests; |
||||
|
using Avalonia.Markup.Xaml; |
||||
|
using Avalonia.ReactiveUI; |
||||
|
using Avalonia; |
||||
|
using ReactiveUI; |
||||
|
using DynamicData; |
||||
|
using Xunit; |
||||
|
using Splat; |
||||
|
|
||||
|
namespace Avalonia.ReactiveUI.UnitTests |
||||
|
{ |
||||
|
public class AutoSuspendHelperTest |
||||
|
{ |
||||
|
[DataContract] |
||||
|
public class AppState |
||||
|
{ |
||||
|
[DataMember] |
||||
|
public string Example { get; set; } |
||||
|
} |
||||
|
|
||||
|
public class ExoticApplicationLifetimeWithoutLifecycleEvents : IDisposable, IApplicationLifetime |
||||
|
{ |
||||
|
public void Dispose() { } |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void AutoSuspendHelper_Should_Immediately_Fire_IsLaunchingNew() |
||||
|
{ |
||||
|
using (UnitTestApplication.Start(TestServices.MockWindowingPlatform)) |
||||
|
using (var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current)) |
||||
|
{ |
||||
|
var isLaunchingReceived = false; |
||||
|
var application = AvaloniaLocator.Current.GetService<Application>(); |
||||
|
application.ApplicationLifetime = lifetime; |
||||
|
|
||||
|
// Initialize ReactiveUI Suspension as in real-world scenario.
|
||||
|
var suspension = new AutoSuspendHelper(application.ApplicationLifetime); |
||||
|
RxApp.SuspensionHost.IsLaunchingNew.Subscribe(_ => isLaunchingReceived = true); |
||||
|
suspension.OnFrameworkInitializationCompleted(); |
||||
|
|
||||
|
Assert.True(isLaunchingReceived); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void ShouldPersistState_Should_Fire_On_App_Exit_When_SuspensionDriver_Is_Initialized() |
||||
|
{ |
||||
|
using (UnitTestApplication.Start(TestServices.MockWindowingPlatform)) |
||||
|
using (var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current)) |
||||
|
{ |
||||
|
var shouldPersistReceived = false; |
||||
|
var application = AvaloniaLocator.Current.GetService<Application>(); |
||||
|
application.ApplicationLifetime = lifetime; |
||||
|
|
||||
|
// Initialize ReactiveUI Suspension as in real-world scenario.
|
||||
|
var suspension = new AutoSuspendHelper(application.ApplicationLifetime); |
||||
|
RxApp.SuspensionHost.CreateNewAppState = () => new AppState { Example = "Foo" }; |
||||
|
RxApp.SuspensionHost.ShouldPersistState.Subscribe(_ => shouldPersistReceived = true); |
||||
|
RxApp.SuspensionHost.SetupDefaultSuspendResume(new DummySuspensionDriver()); |
||||
|
suspension.OnFrameworkInitializationCompleted(); |
||||
|
|
||||
|
lifetime.Shutdown(); |
||||
|
Assert.True(shouldPersistReceived); |
||||
|
Assert.Equal("Foo", RxApp.SuspensionHost.GetAppState<AppState>().Example); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void AutoSuspendHelper_Should_Throw_For_Not_Supported_Lifetimes() |
||||
|
{ |
||||
|
using (UnitTestApplication.Start(TestServices.MockWindowingPlatform)) |
||||
|
using (var lifetime = new ExoticApplicationLifetimeWithoutLifecycleEvents()) |
||||
|
{ |
||||
|
var application = AvaloniaLocator.Current.GetService<Application>(); |
||||
|
application.ApplicationLifetime = lifetime; |
||||
|
Assert.Throws<NotSupportedException>(() => new AutoSuspendHelper(application.ApplicationLifetime)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue