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