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 @@ - + diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index 083182a370..524362fcf9 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; @@ -347,7 +347,7 @@ namespace Avalonia.Controls internal 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/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(); } 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. /// 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); + } + } +} 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);