diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ControlledApplicationLifetimeExitEventArgs.cs b/src/Avalonia.Controls/ApplicationLifetimes/ControlledApplicationLifetimeExitEventArgs.cs
index d4c3b27f7a..2963c019cc 100644
--- a/src/Avalonia.Controls/ApplicationLifetimes/ControlledApplicationLifetimeExitEventArgs.cs
+++ b/src/Avalonia.Controls/ApplicationLifetimes/ControlledApplicationLifetimeExitEventArgs.cs
@@ -6,7 +6,7 @@ using System;
namespace Avalonia.Controls.ApplicationLifetimes
{
///
- /// Contains the arguments for the event.
+ /// Contains the arguments for the event.
///
public class ControlledApplicationLifetimeExitEventArgs : EventArgs
{
diff --git a/src/Avalonia.Controls/ApplicationLifetimes/StartupEventArgs.cs b/src/Avalonia.Controls/ApplicationLifetimes/StartupEventArgs.cs
index 4c08712707..423832793e 100644
--- a/src/Avalonia.Controls/ApplicationLifetimes/StartupEventArgs.cs
+++ b/src/Avalonia.Controls/ApplicationLifetimes/StartupEventArgs.cs
@@ -8,7 +8,7 @@ using System.Linq;
namespace Avalonia.Controls.ApplicationLifetimes
{
///
- /// Contains the arguments for the event.
+ /// Contains the arguments for the event.
///
public class ControlledApplicationLifetimeStartupEventArgs : EventArgs
{
diff --git a/src/Avalonia.Controls/ShutdownMode.cs b/src/Avalonia.Controls/ShutdownMode.cs
index 46e27ff4e1..ee593c28a8 100644
--- a/src/Avalonia.Controls/ShutdownMode.cs
+++ b/src/Avalonia.Controls/ShutdownMode.cs
@@ -1,10 +1,12 @@
// 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.Controls.ApplicationLifetimes;
+
namespace Avalonia.Controls
{
///
- /// Describes the possible values for .
+ /// Describes the possible values for .
///
public enum ShutdownMode
{
diff --git a/src/Avalonia.ReactiveUI/AutoSuspendHelper.cs b/src/Avalonia.ReactiveUI/AutoSuspendHelper.cs
new file mode 100644
index 0000000000..a4f14a4138
--- /dev/null
+++ b/src/Avalonia.ReactiveUI/AutoSuspendHelper.cs
@@ -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
+{
+ ///
+ /// 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 : IEnableLogger, IDisposable
+ {
+ 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 = _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();
+ 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
new file mode 100644
index 0000000000..876f37cc9e
--- /dev/null
+++ b/tests/Avalonia.ReactiveUI.UnitTests/AutoSuspendHelperTest.cs
@@ -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.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.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().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
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 };