From 8515e027ef2074c1378061efde89b939be03bc68 Mon Sep 17 00:00:00 2001 From: artyom Date: Thu, 20 Jun 2019 11:43:04 +0300 Subject: [PATCH 1/6] Add AutoSuspendHelper implementation --- src/Avalonia.ReactiveUI/AutoSuspendHelper.cs | 44 ++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 src/Avalonia.ReactiveUI/AutoSuspendHelper.cs diff --git a/src/Avalonia.ReactiveUI/AutoSuspendHelper.cs b/src/Avalonia.ReactiveUI/AutoSuspendHelper.cs new file mode 100644 index 0000000000..e39ecb4a0f --- /dev/null +++ b/src/Avalonia.ReactiveUI/AutoSuspendHelper.cs @@ -0,0 +1,44 @@ +// 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; + +namespace Avalonia.ReactiveUI +{ + /// + /// 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. + /// + public sealed class AutoSuspendHelper + { + public AutoSuspendHelper(Application app) + { + RxApp.SuspensionHost.IsResuming = Observable.Never(); + RxApp.SuspensionHost.IsLaunchingNew = Observable.Return(Unit.Default); + + var exiting = new Subject(); + app.Exit += (o, e) => + { + // This is required to prevent the app from shutting down too early. + var manual = new ManualResetEvent(false); + exiting.OnNext(Disposable.Create(() => manual.Set())); + manual.WaitOne(); + }; + RxApp.SuspensionHost.ShouldPersistState = exiting; + + var errored = new Subject(); + AppDomain.CurrentDomain.UnhandledException += (o, e) => errored.OnNext(Unit.Default); + RxApp.SuspensionHost.ShouldInvalidateState = errored; + } + } +} From 5230e465a663fee27dc9d05d562230073bcca669 Mon Sep 17 00:00:00 2001 From: artyom Date: Thu, 20 Jun 2019 12:03:33 +0300 Subject: [PATCH 2/6] Correct wrong variable name --- .../AvaloniaActivationForViewFetcherTest.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs index 2c81e8fea3..1d85312b1a 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs @@ -171,7 +171,7 @@ namespace Avalonia.ReactiveUI.UnitTests [Fact] public void Activation_For_View_Fetcher_Should_Support_Windows() { - using (var application = UnitTestApplication.Start(TestServices.MockWindowingPlatform)) + using (UnitTestApplication.Start(TestServices.MockWindowingPlatform)) { var window = new TestWindowWithWhenActivated(); Assert.False(window.Active); @@ -187,7 +187,7 @@ namespace Avalonia.ReactiveUI.UnitTests [Fact] public void Activatable_Window_View_Model_Is_Activated_And_Deactivated() { - using (var application = UnitTestApplication.Start(TestServices.MockWindowingPlatform)) + using (UnitTestApplication.Start(TestServices.MockWindowingPlatform)) { var viewModel = new ActivatableViewModel(); var window = new ActivatableWindow { ViewModel = viewModel }; From 6c16ac48032730a7b8ef1fd5bb701f251878028f Mon Sep 17 00:00:00 2001 From: artyom Date: Thu, 20 Jun 2019 12:38:59 +0300 Subject: [PATCH 3/6] Unit test AutoSuspendHelper --- .../AutoSuspendHelperTest.cs | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 tests/Avalonia.ReactiveUI.UnitTests/AutoSuspendHelperTest.cs diff --git a/tests/Avalonia.ReactiveUI.UnitTests/AutoSuspendHelperTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/AutoSuspendHelperTest.cs new file mode 100644 index 0000000000..a3464878e6 --- /dev/null +++ b/tests/Avalonia.ReactiveUI.UnitTests/AutoSuspendHelperTest.cs @@ -0,0 +1,60 @@ +// 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 Avalonia.Controls; +using Avalonia.Rendering; +using Avalonia.Platform; +using Avalonia.UnitTests; +using Avalonia; +using ReactiveUI; +using DynamicData; +using Xunit; +using Splat; +using Avalonia.Markup.Xaml; +using System.ComponentModel; +using System.Threading.Tasks; +using System.Reactive; +using Avalonia.ReactiveUI; +using System.Reactive.Subjects; +using System.Reactive.Linq; +using System.Collections.Generic; + +namespace Avalonia.ReactiveUI.UnitTests +{ + public class AutoSuspendHelperTest + { + [Fact] + public void AutoSuspendHelper_Should_Immediately_Fire_IsLaunchingNew() + { + using (UnitTestApplication.Start(TestServices.MockWindowingPlatform)) + { + var isLaunchingReceived = false; + var application = AvaloniaLocator.Current.GetService(); + var suspension = new AutoSuspendHelper(application); + + RxApp.SuspensionHost.IsLaunchingNew.Subscribe(_ => isLaunchingReceived = true); + Assert.True(isLaunchingReceived); + } + } + + [Fact] + public void ShouldPersistState_Should_Fire_On_App_Exit_When_SuspensionDriver_Is_Initialized() + { + using (UnitTestApplication.Start(TestServices.MockWindowingPlatform)) + { + var shouldPersistReceived = false; + var application = AvaloniaLocator.Current.GetService(); + var suspension = new AutoSuspendHelper(application); + + RxApp.SuspensionHost.ShouldPersistState.Subscribe(_ => shouldPersistReceived = true); + RxApp.SuspensionHost.SetupDefaultSuspendResume(new DummySuspensionDriver()); + + application.Shutdown(); + Assert.True(shouldPersistReceived); + } + } + } +} \ No newline at end of file From 009f39ae743c72da1c058d2a31251b6b12c47379 Mon Sep 17 00:00:00 2001 From: artyom Date: Sun, 30 Jun 2019 19:43:19 +0300 Subject: [PATCH 4/6] Update AutoSuspendHelper to use IControlledApplicationLifetime --- src/Avalonia.ReactiveUI/AutoSuspendHelper.cs | 65 +++++++++++++++---- .../AutoSuspendHelperTest.cs | 14 ++-- 2 files changed, 64 insertions(+), 15 deletions(-) diff --git a/src/Avalonia.ReactiveUI/AutoSuspendHelper.cs b/src/Avalonia.ReactiveUI/AutoSuspendHelper.cs index e39ecb4a0f..a4f14a4138 100644 --- a/src/Avalonia.ReactiveUI/AutoSuspendHelper.cs +++ b/src/Avalonia.ReactiveUI/AutoSuspendHelper.cs @@ -11,6 +11,8 @@ using System.Reactive.Linq; using System.Reactive; using ReactiveUI; using System; +using Avalonia.Controls.ApplicationLifetimes; +using Splat; namespace Avalonia.ReactiveUI { @@ -19,26 +21,67 @@ namespace Avalonia.ReactiveUI /// Avalonia applications. Call its constructor in your app's composition root, /// before calling the RxApp.SuspensionHost.SetupDefaultSuspendResume method. /// - public sealed class AutoSuspendHelper + public sealed class AutoSuspendHelper : IEnableLogger, IDisposable { - public AutoSuspendHelper(Application app) + private readonly Subject _shouldPersistState = new Subject(); + private readonly Subject _isLaunchingNew = new Subject(); + + /// + /// Initializes a new instance of the class. + /// + /// Pass in the Application.ApplicationLifetime property. + public AutoSuspendHelper(IApplicationLifetime lifetime) { RxApp.SuspensionHost.IsResuming = Observable.Never(); - RxApp.SuspensionHost.IsLaunchingNew = Observable.Return(Unit.Default); + RxApp.SuspensionHost.IsLaunchingNew = _isLaunchingNew; - var exiting = new Subject(); - app.Exit += (o, e) => + if (lifetime is IControlledApplicationLifetime controlled) { - // This is required to prevent the app from shutting down too early. - var manual = new ManualResetEvent(false); - exiting.OnNext(Disposable.Create(() => manual.Set())); - manual.WaitOne(); - }; - RxApp.SuspensionHost.ShouldPersistState = exiting; + 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(); AppDomain.CurrentDomain.UnhandledException += (o, e) => errored.OnNext(Unit.Default); RxApp.SuspensionHost.ShouldInvalidateState = errored; } + + /// + /// Call this method in your App.OnFrameworkInitializationCompleted method. + /// + public void OnFrameworkInitializationCompleted() => _isLaunchingNew.OnNext(Unit.Default); + + /// + /// Disposes internally stored observers. + /// + 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."); + } } } diff --git a/tests/Avalonia.ReactiveUI.UnitTests/AutoSuspendHelperTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/AutoSuspendHelperTest.cs index a3464878e6..d370454656 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/AutoSuspendHelperTest.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/AutoSuspendHelperTest.cs @@ -21,6 +21,7 @@ using Avalonia.ReactiveUI; using System.Reactive.Subjects; using System.Reactive.Linq; using System.Collections.Generic; +using System.Threading; namespace Avalonia.ReactiveUI.UnitTests { @@ -33,9 +34,10 @@ namespace Avalonia.ReactiveUI.UnitTests { var isLaunchingReceived = false; var application = AvaloniaLocator.Current.GetService(); - var suspension = new AutoSuspendHelper(application); + var suspension = new AutoSuspendHelper(application.ApplicationLifetime); RxApp.SuspensionHost.IsLaunchingNew.Subscribe(_ => isLaunchingReceived = true); + suspension.OnFrameworkInitializationCompleted(); Assert.True(isLaunchingReceived); } } @@ -47,12 +49,16 @@ namespace Avalonia.ReactiveUI.UnitTests { var shouldPersistReceived = false; var application = AvaloniaLocator.Current.GetService(); - var suspension = new AutoSuspendHelper(application); + var suspension = new AutoSuspendHelper(application.ApplicationLifetime); - RxApp.SuspensionHost.ShouldPersistState.Subscribe(_ => shouldPersistReceived = true); RxApp.SuspensionHost.SetupDefaultSuspendResume(new DummySuspensionDriver()); + RxApp.SuspensionHost.ShouldPersistState.Subscribe(_ => shouldPersistReceived = true); + suspension.OnFrameworkInitializationCompleted(); - application.Shutdown(); + var source = new CancellationTokenSource(); + source.CancelAfter(TimeSpan.FromMilliseconds(100)); + application.Run(source.Token); + Assert.True(shouldPersistReceived); } } From da467dd647432679b894be948b613f2f931fc3c7 Mon Sep 17 00:00:00 2001 From: artyom Date: Sun, 30 Jun 2019 21:00:18 +0300 Subject: [PATCH 5/6] Fix failing unit tests --- .../AutoSuspendHelperTest.cs | 49 ++++++++++++------- 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/tests/Avalonia.ReactiveUI.UnitTests/AutoSuspendHelperTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/AutoSuspendHelperTest.cs index d370454656..142eba41d5 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/AutoSuspendHelperTest.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/AutoSuspendHelperTest.cs @@ -4,40 +4,53 @@ 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; -using Avalonia.Markup.Xaml; -using System.ComponentModel; -using System.Threading.Tasks; -using System.Reactive; -using Avalonia.ReactiveUI; -using System.Reactive.Subjects; -using System.Reactive.Linq; -using System.Collections.Generic; -using System.Threading; namespace Avalonia.ReactiveUI.UnitTests { public class AutoSuspendHelperTest { + [DataContract] + public class AppState + { + [DataMember] + public string Example { get; set; } + } + [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(); - var suspension = new AutoSuspendHelper(application.ApplicationLifetime); + 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); } } @@ -45,21 +58,23 @@ namespace Avalonia.ReactiveUI.UnitTests [Fact] public void ShouldPersistState_Should_Fire_On_App_Exit_When_SuspensionDriver_Is_Initialized() { - using (UnitTestApplication.Start(TestServices.MockWindowingPlatform)) + using (UnitTestApplication.Start(TestServices.MockWindowingPlatform)) + using (var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current)) { var shouldPersistReceived = false; var application = AvaloniaLocator.Current.GetService(); - var suspension = new AutoSuspendHelper(application.ApplicationLifetime); + application.ApplicationLifetime = lifetime; - RxApp.SuspensionHost.SetupDefaultSuspendResume(new DummySuspensionDriver()); + // 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(); - var source = new CancellationTokenSource(); - source.CancelAfter(TimeSpan.FromMilliseconds(100)); - application.Run(source.Token); - + lifetime.Shutdown(); Assert.True(shouldPersistReceived); + Assert.Equal("Foo", RxApp.SuspensionHost.GetAppState().Example); } } } From 6eed5187468b46f416837afc718bf1ff2d5bc450 Mon Sep 17 00:00:00 2001 From: artyom Date: Sun, 30 Jun 2019 21:08:12 +0300 Subject: [PATCH 6/6] Indicate exotic lifetimes are not supported --- .../AutoSuspendHelperTest.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/Avalonia.ReactiveUI.UnitTests/AutoSuspendHelperTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/AutoSuspendHelperTest.cs index 142eba41d5..876f37cc9e 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/AutoSuspendHelperTest.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/AutoSuspendHelperTest.cs @@ -36,6 +36,11 @@ namespace Avalonia.ReactiveUI.UnitTests public string Example { get; set; } } + public class ExoticApplicationLifetimeWithoutLifecycleEvents : IDisposable, IApplicationLifetime + { + public void Dispose() { } + } + [Fact] public void AutoSuspendHelper_Should_Immediately_Fire_IsLaunchingNew() { @@ -77,5 +82,17 @@ namespace Avalonia.ReactiveUI.UnitTests Assert.Equal("Foo", RxApp.SuspensionHost.GetAppState().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.ApplicationLifetime = lifetime; + Assert.Throws(() => new AutoSuspendHelper(application.ApplicationLifetime)); + } + } } } \ No newline at end of file