Browse Source

Merge branch 'master' into bindingoperations-donothing

Steven Kirk 6 years ago
parent
commit
9eb80474ee

+ 1 - 1
src/Avalonia.Controls/ApplicationLifetimes/ControlledApplicationLifetimeExitEventArgs.cs

@@ -6,7 +6,7 @@ using System;
 namespace Avalonia.Controls.ApplicationLifetimes
 {
     /// <summary>
-    /// Contains the arguments for the <see cref="IClassicDesktopStyleApplicationLifetime.Exit"/> event.
+    /// Contains the arguments for the <see cref="IControlledApplicationLifetime.Exit"/> event.
     /// </summary>
     public class ControlledApplicationLifetimeExitEventArgs : EventArgs
     {

+ 1 - 1
src/Avalonia.Controls/ApplicationLifetimes/StartupEventArgs.cs

@@ -8,7 +8,7 @@ using System.Linq;
 namespace Avalonia.Controls.ApplicationLifetimes
 {
     /// <summary>
-    /// Contains the arguments for the <see cref="IClassicDesktopStyleApplicationLifetime.Startup"/> event.
+    /// Contains the arguments for the <see cref="IControlledApplicationLifetime.Startup"/> event.
     /// </summary>
     public class ControlledApplicationLifetimeStartupEventArgs : EventArgs
     {

+ 3 - 1
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
 {
     /// <summary>
-    /// Describes the possible values for <see cref="Application.ShutdownMode"/>.
+    /// Describes the possible values for <see cref="IClassicDesktopStyleApplicationLifetime.ShutdownMode"/>.
     /// </summary>
     public enum ShutdownMode
     {

+ 87 - 0
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
+{
+    /// <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.");
+        }
+    }
+}

+ 98 - 0
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>();
+                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));
+            }
+        }
+    }
+}

+ 2 - 2
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 };