From 301268b5f75675ce9c69135fbffa65257c8e194b Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 12 Aug 2022 20:27:42 -0400 Subject: [PATCH 1/6] Update ReactiveUI --- build/ReactiveUI.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/ReactiveUI.props b/build/ReactiveUI.props index c3b136d41d..1911c02677 100644 --- a/build/ReactiveUI.props +++ b/build/ReactiveUI.props @@ -1,5 +1,5 @@ - + From 18128a59de1cc4e3a693b74d3aa747d4dbf58021 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 12 Aug 2022 20:27:59 -0400 Subject: [PATCH 2/6] Fix window Loaded and Unloaded events --- src/Avalonia.Controls/Control.cs | 8 +-- .../LoadedTests.cs | 71 +++++++++++++++++++ 2 files changed, 74 insertions(+), 5 deletions(-) create mode 100644 tests/Avalonia.Controls.UnitTests/LoadedTests.cs diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index 083182a370..c95c7d466b 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -8,6 +8,7 @@ using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Interactivity; +using Avalonia.LogicalTree; using Avalonia.Media; using Avalonia.Rendering; using Avalonia.Styling; @@ -104,7 +105,6 @@ namespace Avalonia.Controls private static readonly HashSet _loadedQueue = new HashSet(); private static readonly HashSet _loadedProcessingQueue = new HashSet(); - private bool _isAttachedToVisualTree = false; private bool _isLoaded = false; private DataTemplates? _dataTemplates; private IControl? _focusAdorner; @@ -344,10 +344,10 @@ namespace Avalonia.Controls /// Invoked as the first step of marking the control as loaded and raising the /// event. /// - internal void OnLoadedCore() + internal virtual void OnLoadedCore() { if (_isLoaded == false && - _isAttachedToVisualTree) + ((ILogical)this).IsAttachedToLogicalTree) { _isLoaded = true; OnLoaded(); @@ -395,7 +395,6 @@ namespace Avalonia.Controls protected sealed override void OnAttachedToVisualTreeCore(VisualTreeAttachmentEventArgs e) { base.OnAttachedToVisualTreeCore(e); - _isAttachedToVisualTree = true; InitializeIfNeeded(); @@ -406,7 +405,6 @@ namespace Avalonia.Controls protected sealed override void OnDetachedFromVisualTreeCore(VisualTreeAttachmentEventArgs e) { base.OnDetachedFromVisualTreeCore(e); - _isAttachedToVisualTree = false; OnUnloadedCore(); } diff --git a/tests/Avalonia.Controls.UnitTests/LoadedTests.cs b/tests/Avalonia.Controls.UnitTests/LoadedTests.cs new file mode 100644 index 0000000000..aaf0dce30e --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/LoadedTests.cs @@ -0,0 +1,71 @@ +using Avalonia.Platform; +using Avalonia.Threading; +using Avalonia.UnitTests; +using Moq; +using Xunit; + +namespace Avalonia.Controls.UnitTests; + +public class LoadedTests +{ + [Fact] + public void Window_Loads_And_Unloads() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + int loadedCount = 0, unloadedCount = 0; + var target = new Window(); + + target.Loaded += (_, _) => loadedCount++; + target.Unloaded += (_, _) => unloadedCount++; + + Assert.Equal(0, loadedCount); + Assert.Equal(0, unloadedCount); + + target.Show(); + Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); + Assert.True(target.IsLoaded); + + Assert.Equal(1, loadedCount); + Assert.Equal(0, unloadedCount); + + target.Close(); + + Assert.Equal(1, loadedCount); + Assert.Equal(1, unloadedCount); + Assert.False(target.IsLoaded); + } + } + + [Fact] + public void Control_Loads_And_Unloads() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + int loadedCount = 0, unloadedCount = 0; + var window = new Window(); + window.Show(); + + var target = new Button(); + + target.Loaded += (_, _) => loadedCount++; + target.Unloaded += (_, _) => unloadedCount++; + + Assert.Equal(0, loadedCount); + Assert.Equal(0, unloadedCount); + + window.Content = target; + Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); + Assert.True(target.IsLoaded); + + Assert.Equal(1, loadedCount); + Assert.Equal(0, unloadedCount); + + window.Content = null; + + Assert.Equal(1, loadedCount); + Assert.Equal(1, unloadedCount); + Assert.False(target.IsLoaded); + } + } +} From 543e98d753354b4bb1aa97026df8130bbe0b46d3 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 12 Aug 2022 20:28:50 -0400 Subject: [PATCH 3/6] Fix RoutedViewHost and ViewModelViewHost by implementing IStyleable --- src/Avalonia.ReactiveUI/RoutedViewHost.cs | 4 +++- src/Avalonia.ReactiveUI/ViewModelViewHost.cs | 6 ++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.ReactiveUI/RoutedViewHost.cs b/src/Avalonia.ReactiveUI/RoutedViewHost.cs index 775014d419..2d848d4cd7 100644 --- a/src/Avalonia.ReactiveUI/RoutedViewHost.cs +++ b/src/Avalonia.ReactiveUI/RoutedViewHost.cs @@ -50,7 +50,7 @@ namespace Avalonia.ReactiveUI /// ReactiveUI routing documentation website for more info. /// /// - public class RoutedViewHost : TransitioningContentControl, IActivatableView, IEnableLogger + public class RoutedViewHost : TransitioningContentControl, IActivatableView, IEnableLogger, IStyleable { /// /// for the property. @@ -126,6 +126,8 @@ namespace Avalonia.ReactiveUI /// public IViewLocator? ViewLocator { get; set; } + Type IStyleable.StyleKey => typeof(TransitioningContentControl); + /// /// Invoked when ReactiveUI router navigates to a view model. /// diff --git a/src/Avalonia.ReactiveUI/ViewModelViewHost.cs b/src/Avalonia.ReactiveUI/ViewModelViewHost.cs index 869238b377..0750fef067 100644 --- a/src/Avalonia.ReactiveUI/ViewModelViewHost.cs +++ b/src/Avalonia.ReactiveUI/ViewModelViewHost.cs @@ -2,7 +2,7 @@ using System; using System.Reactive.Disposables; using Avalonia.Controls; - +using Avalonia.Styling; using ReactiveUI; using Splat; @@ -13,7 +13,7 @@ namespace Avalonia.ReactiveUI /// the ViewModel property and display it. This control is very useful /// inside a DataTemplate to display the View associated with a ViewModel. /// - public class ViewModelViewHost : TransitioningContentControl, IViewFor, IEnableLogger + public class ViewModelViewHost : TransitioningContentControl, IViewFor, IEnableLogger, IStyleable { /// /// for the property. @@ -78,6 +78,8 @@ namespace Avalonia.ReactiveUI /// public IViewLocator? ViewLocator { get; set; } + Type IStyleable.StyleKey => typeof(TransitioningContentControl); + /// /// Invoked when ReactiveUI router navigates to a view model. /// From 65c559e6c9e46b9e9caa6d2f42a17ad5b65289ab Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 12 Aug 2022 20:29:00 -0400 Subject: [PATCH 4/6] Use Loaded event in ReactiveUI --- .../AvaloniaActivationForViewFetcher.cs | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/Avalonia.ReactiveUI/AvaloniaActivationForViewFetcher.cs b/src/Avalonia.ReactiveUI/AvaloniaActivationForViewFetcher.cs index 5c713804e9..9f69b4ee6e 100644 --- a/src/Avalonia.ReactiveUI/AvaloniaActivationForViewFetcher.cs +++ b/src/Avalonia.ReactiveUI/AvaloniaActivationForViewFetcher.cs @@ -2,6 +2,7 @@ using System; using System.Reactive.Linq; using Avalonia.VisualTree; using Avalonia.Controls; +using Avalonia.Interactivity; using ReactiveUI; namespace Avalonia.ReactiveUI @@ -25,27 +26,28 @@ namespace Avalonia.ReactiveUI public IObservable GetActivationForView(IActivatableView view) { if (!(view is IVisual visual)) return Observable.Return(false); - if (view is WindowBase window) return GetActivationForWindowBase(window); + if (view is Control control) return GetActivationForControl(control); return GetActivationForVisual(visual); } /// - /// Listens to Opened and Closed events for Avalonia windows. + /// Listens to Loaded and Unloaded + /// events for Avalonia Control. /// - private IObservable GetActivationForWindowBase(WindowBase window) + private IObservable GetActivationForControl(Control control) { - var windowLoaded = Observable - .FromEventPattern( - x => window.Opened += x, - x => window.Opened -= x) + var controlLoaded = Observable + .FromEventPattern( + x => control.Loaded += x, + x => control.Loaded -= x) .Select(args => true); - var windowUnloaded = Observable - .FromEventPattern( - x => window.Closed += x, - x => window.Closed -= x) + var controlUnloaded = Observable + .FromEventPattern( + x => control.Unloaded += x, + x => control.Unloaded -= x) .Select(args => false); - return windowLoaded - .Merge(windowUnloaded) + return controlLoaded + .Merge(controlUnloaded) .DistinctUntilChanged(); } From 2fdba46f0c4b7e6effbc7ea293752ffcc6f2ca2e Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 12 Aug 2022 22:21:06 -0400 Subject: [PATCH 5/6] Fix tests --- .../AutoSuspendHelperTest.cs | 14 +++++++++++++- .../AvaloniaActivationForViewFetcherTest.cs | 11 +++++++++++ .../ReactiveUserControlTest.cs | 5 +++++ .../ReactiveWindowTest.cs | 5 +++++ .../RoutedViewHostTest.cs | 4 ++++ .../ViewModelViewHostTest.cs | 3 +++ 6 files changed, 41 insertions(+), 1 deletion(-) diff --git a/tests/Avalonia.ReactiveUI.UnitTests/AutoSuspendHelperTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/AutoSuspendHelperTest.cs index c10f0ffcdb..196375fb40 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/AutoSuspendHelperTest.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/AutoSuspendHelperTest.cs @@ -7,6 +7,7 @@ using System.Reactive; using System.Reactive.Subjects; using System.Reactive.Linq; using System.Collections.Generic; +using System.IO; using System.Runtime.Serialization; using System.Threading; using Avalonia.Controls.ApplicationLifetimes; @@ -17,6 +18,7 @@ using Avalonia.UnitTests; using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; using Avalonia; +using Avalonia.Threading; using ReactiveUI; using DynamicData; using Xunit; @@ -93,13 +95,23 @@ namespace Avalonia.ReactiveUI.UnitTests var suspension = new AutoSuspendHelper(application.ApplicationLifetime); RxApp.SuspensionHost.CreateNewAppState = () => new AppState { Example = "Foo" }; RxApp.SuspensionHost.ShouldPersistState.Subscribe(_ => shouldPersistReceived = true); - RxApp.SuspensionHost.SetupDefaultSuspendResume(new DummySuspensionDriver()); + RxApp.SuspensionHost.SetupDefaultSuspendResume(new FakeSuspensionDriver()); suspension.OnFrameworkInitializationCompleted(); lifetime.Shutdown(); + Assert.True(shouldPersistReceived); Assert.Equal("Foo", RxApp.SuspensionHost.GetAppState().Example); } } + + private class FakeSuspensionDriver : ISuspensionDriver + { + public IObservable LoadState() => Observable.Empty(); + + public IObservable SaveState(object state) => Observable.Empty(); + + public IObservable InvalidateState() => Observable.Empty(); + } } } diff --git a/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs index c66a3c4ba1..a4e669cb34 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs @@ -12,6 +12,7 @@ using Xunit; using Splat; using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; +using Avalonia.Threading; namespace Avalonia.ReactiveUI.UnitTests { @@ -109,10 +110,12 @@ namespace Avalonia.ReactiveUI.UnitTests var fakeRenderedDecorator = new TestRoot(); fakeRenderedDecorator.Child = userControl; + Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); Assert.True(activated[0]); Assert.Equal(1, activated.Count); fakeRenderedDecorator.Child = null; + Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); Assert.True(activated[0]); Assert.False(activated[1]); Assert.Equal(2, activated.Count); @@ -139,9 +142,11 @@ namespace Avalonia.ReactiveUI.UnitTests var fakeRenderedDecorator = new TestRoot(); fakeRenderedDecorator.Child = userControl; + Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); Assert.True(userControl.Active); fakeRenderedDecorator.Child = null; + Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); Assert.False(userControl.Active); } @@ -154,9 +159,11 @@ namespace Avalonia.ReactiveUI.UnitTests Assert.False(window.Active); window.Show(); + Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); Assert.True(window.Active); window.Close(); + Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); Assert.False(window.Active); } } @@ -171,9 +178,11 @@ namespace Avalonia.ReactiveUI.UnitTests Assert.False(viewModel.IsActivated); window.Show(); + Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); Assert.True(viewModel.IsActivated); window.Close(); + Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); Assert.False(viewModel.IsActivated); } } @@ -187,9 +196,11 @@ namespace Avalonia.ReactiveUI.UnitTests Assert.False(viewModel.IsActivated); root.Child = control; + Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); Assert.True(viewModel.IsActivated); root.Child = null; + Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); Assert.False(viewModel.IsActivated); } } diff --git a/tests/Avalonia.ReactiveUI.UnitTests/ReactiveUserControlTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/ReactiveUserControlTest.cs index 4dd60e21c5..4bf999bed0 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/ReactiveUserControlTest.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/ReactiveUserControlTest.cs @@ -1,5 +1,6 @@ using System.Reactive.Disposables; using Avalonia.Controls; +using Avalonia.Threading; using Avalonia.UnitTests; using ReactiveUI; using Splat; @@ -69,12 +70,14 @@ namespace Avalonia.ReactiveUI.UnitTests Assert.False(view.ViewModel.IsActive); root.Child = view; + Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); Assert.NotNull(view.ViewModel); Assert.NotNull(view.DataContext); Assert.True(view.ViewModel.IsActive); root.Child = null; + Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); Assert.NotNull(view.ViewModel); Assert.NotNull(view.DataContext); @@ -90,12 +93,14 @@ namespace Avalonia.ReactiveUI.UnitTests Assert.False(view.ViewModel.IsActive); root.Child = view; + Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); Assert.NotNull(view.ViewModel); Assert.NotNull(view.DataContext); Assert.True(view.ViewModel.IsActive); root.Child = null; + Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); Assert.NotNull(view.ViewModel); Assert.NotNull(view.DataContext); diff --git a/tests/Avalonia.ReactiveUI.UnitTests/ReactiveWindowTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/ReactiveWindowTest.cs index 18a8a33f09..a71a500ed9 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/ReactiveWindowTest.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/ReactiveWindowTest.cs @@ -1,4 +1,5 @@ using System.Reactive.Disposables; +using Avalonia.Threading; using Avalonia.UnitTests; using ReactiveUI; using Splat; @@ -72,12 +73,14 @@ namespace Avalonia.ReactiveUI.UnitTests Assert.False(view.ViewModel.IsActive); view.Show(); + Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); Assert.NotNull(view.ViewModel); Assert.NotNull(view.DataContext); Assert.True(view.ViewModel.IsActive); view.Close(); + Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); Assert.NotNull(view.ViewModel); Assert.NotNull(view.DataContext); @@ -96,12 +99,14 @@ namespace Avalonia.ReactiveUI.UnitTests view.Show(); + Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); Assert.NotNull(view.ViewModel); Assert.NotNull(view.DataContext); Assert.True(view.ViewModel.IsActive); view.Close(); + Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); Assert.NotNull(view.ViewModel); Assert.NotNull(view.DataContext); Assert.False(view.ViewModel.IsActive); diff --git a/tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs index 244b5abc4e..56c7863861 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs @@ -15,6 +15,7 @@ using System.ComponentModel; using System.Threading.Tasks; using System.Reactive; using Avalonia.ReactiveUI; +using Avalonia.Threading; namespace Avalonia.ReactiveUI.UnitTests { @@ -75,6 +76,7 @@ namespace Avalonia.ReactiveUI.UnitTests Child = host }; + Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); Assert.NotNull(host.Content); Assert.IsType(host.Content); Assert.Equal(defaultContent, host.Content); @@ -126,6 +128,7 @@ namespace Avalonia.ReactiveUI.UnitTests Child = host }; + Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); Assert.NotNull(host.Content); Assert.IsType(host.Content); Assert.Equal(defaultContent, host.Content); @@ -191,6 +194,7 @@ namespace Avalonia.ReactiveUI.UnitTests Child = host }; + Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); Assert.NotNull(host.Content); Assert.Equal(defaultContent, host.Content); diff --git a/tests/Avalonia.ReactiveUI.UnitTests/ViewModelViewHostTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/ViewModelViewHostTest.cs index 858c476227..3d60e56c65 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/ViewModelViewHostTest.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/ViewModelViewHostTest.cs @@ -1,4 +1,5 @@ using Avalonia.Controls; +using Avalonia.Threading; using Avalonia.UnitTests; using ReactiveUI; using Splat; @@ -46,6 +47,7 @@ namespace Avalonia.ReactiveUI.UnitTests Child = host }; + Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); Assert.NotNull(host.Content); Assert.Equal(typeof(TextBlock), host.Content.GetType()); Assert.Equal(defaultContent, host.Content); @@ -91,6 +93,7 @@ namespace Avalonia.ReactiveUI.UnitTests Child = host }; + Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); Assert.NotNull(host.Content); Assert.Equal(typeof(TextBlock), host.Content.GetType()); Assert.Equal(defaultContent, host.Content); From 817afedf48282ad1d94e1ed3ab102265c6a52652 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 13 Aug 2022 21:40:07 -0400 Subject: [PATCH 6/6] Remove unused "virtual" --- src/Avalonia.Controls/Control.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index c95c7d466b..524362fcf9 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -344,7 +344,7 @@ namespace Avalonia.Controls /// Invoked as the first step of marking the control as loaded and raising the /// event. /// - internal virtual void OnLoadedCore() + internal void OnLoadedCore() { if (_isLoaded == false && ((ILogical)this).IsAttachedToLogicalTree)