diff --git a/src/ReactiveUI.Tests/Platforms/wpf/Mocks/WpfTestWindow.cs b/src/ReactiveUI.Tests/Platforms/wpf/Mocks/WpfTestWindow.cs new file mode 100644 index 0000000000..0807b676c7 --- /dev/null +++ b/src/ReactiveUI.Tests/Platforms/wpf/Mocks/WpfTestWindow.cs @@ -0,0 +1,22 @@ +// Copyright (c) 2021 .NET Foundation and Contributors. All rights reserved. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System.Windows; +using System.Windows.Controls; + +namespace ReactiveUI.Tests.Wpf +{ + public class WpfTestWindow : Window, IActivatableView + { + public WpfTestWindow() + { + RootGrid = new Grid(); + + AddChild(RootGrid); + } + + public Grid RootGrid { get; } + } +} diff --git a/src/ReactiveUI.Tests/Platforms/wpf/WpfActivationForViewFetcherTest.cs b/src/ReactiveUI.Tests/Platforms/wpf/WpfActivationForViewFetcherTest.cs index 9faaadd414..d430aacdf2 100644 --- a/src/ReactiveUI.Tests/Platforms/wpf/WpfActivationForViewFetcherTest.cs +++ b/src/ReactiveUI.Tests/Platforms/wpf/WpfActivationForViewFetcherTest.cs @@ -1,130 +1,182 @@ -// Copyright (c) 2021 .NET Foundation and Contributors. All rights reserved. -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for full license information. - -using System; -using System.Reactive.Concurrency; -using System.Windows; - -using DynamicData; -using Xunit; - -using FactAttribute = Xunit.WpfFactAttribute; - -namespace ReactiveUI.Tests.Wpf -{ - public class WpfActivationForViewFetcherTest - { - [Fact] - public void FrameworkElementIsActivatedAndDeactivated() - { - var uc = new WpfTestUserControl(); - var activation = new ActivationForViewFetcher(); - - var obs = activation.GetActivationForView(uc); - obs.ToObservableChangeSet(scheduler: ImmediateScheduler.Instance).Bind(out var activated).Subscribe(); - - var loaded = new RoutedEventArgs(); - loaded.RoutedEvent = FrameworkElement.LoadedEvent; - - uc.RaiseEvent(loaded); - - new[] { true }.AssertAreEqual(activated); - - var unloaded = new RoutedEventArgs(); - unloaded.RoutedEvent = FrameworkElement.UnloadedEvent; - - uc.RaiseEvent(unloaded); - - new[] { true, false }.AssertAreEqual(activated); - } - - [Fact] - public void IsHitTestVisibleActivatesFrameworkElement() - { - var uc = new WpfTestUserControl(); - uc.IsHitTestVisible = false; - var activation = new ActivationForViewFetcher(); - - var obs = activation.GetActivationForView(uc); - obs.ToObservableChangeSet(scheduler: ImmediateScheduler.Instance).Bind(out var activated).Subscribe(); - - var loaded = new RoutedEventArgs(); - loaded.RoutedEvent = FrameworkElement.LoadedEvent; - - uc.RaiseEvent(loaded); - - // Loaded has happened. - new[] { true }.AssertAreEqual(activated); - - uc.IsHitTestVisible = true; - - // IsHitTestVisible true, we don't want the event to repeat unnecessarily. - new[] { true }.AssertAreEqual(activated); - - var unloaded = new RoutedEventArgs(); - unloaded.RoutedEvent = FrameworkElement.UnloadedEvent; - - uc.RaiseEvent(unloaded); - - // We had both a loaded/hit test visible change/unloaded happen. - new[] { true, false }.AssertAreEqual(activated); - } - - [Fact] - public void IsHitTestVisibleDeactivatesFrameworkElement() - { - var uc = new WpfTestUserControl(); - var activation = new ActivationForViewFetcher(); - - var obs = activation.GetActivationForView(uc); - obs.ToObservableChangeSet(scheduler: ImmediateScheduler.Instance).Bind(out var activated).Subscribe(); - - var loaded = new RoutedEventArgs(); - loaded.RoutedEvent = FrameworkElement.LoadedEvent; - - uc.RaiseEvent(loaded); - - new[] { true }.AssertAreEqual(activated); - - uc.IsHitTestVisible = false; - - new[] { true, false }.AssertAreEqual(activated); - } - - [Fact] - public void FrameworkElementIsActivatedAndDeactivatedWithHitTest() - { - var uc = new WpfTestUserControl(); - var activation = new ActivationForViewFetcher(); - - var obs = activation.GetActivationForView(uc); - obs.ToObservableChangeSet(scheduler: ImmediateScheduler.Instance).Bind(out var activated).Subscribe(); - - var loaded = new RoutedEventArgs(); - loaded.RoutedEvent = FrameworkElement.LoadedEvent; - - uc.RaiseEvent(loaded); - - new[] { true }.AssertAreEqual(activated); - - // this should deactivate it - uc.IsHitTestVisible = false; - - new[] { true, false }.AssertAreEqual(activated); - - // this should activate it - uc.IsHitTestVisible = true; - - new[] { true, false, true }.AssertAreEqual(activated); - - var unloaded = new RoutedEventArgs(); - unloaded.RoutedEvent = FrameworkElement.UnloadedEvent; - - uc.RaiseEvent(unloaded); - - new[] { true, false, true, false }.AssertAreEqual(activated); - } - } -} +// Copyright (c) 2021 .NET Foundation and Contributors. All rights reserved. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System; +using System.Reactive.Concurrency; +using System.Windows; + +using DynamicData; +using Xunit; + +using FactAttribute = Xunit.WpfFactAttribute; + +namespace ReactiveUI.Tests.Wpf +{ + public class WpfActivationForViewFetcherTest + { + [Fact] + public void FrameworkElementIsActivatedAndDeactivated() + { + var uc = new WpfTestUserControl(); + var activation = new ActivationForViewFetcher(); + + var obs = activation.GetActivationForView(uc); + obs.ToObservableChangeSet(scheduler: ImmediateScheduler.Instance).Bind(out var activated).Subscribe(); + + var loaded = new RoutedEventArgs(); + loaded.RoutedEvent = FrameworkElement.LoadedEvent; + + uc.RaiseEvent(loaded); + + new[] { true }.AssertAreEqual(activated); + + var unloaded = new RoutedEventArgs(); + unloaded.RoutedEvent = FrameworkElement.UnloadedEvent; + + uc.RaiseEvent(unloaded); + + new[] { true, false }.AssertAreEqual(activated); + } + + [Fact] + public void WindowIsActivatedAndDeactivated() + { + var window = new WpfTestWindow(); + var activation = new ActivationForViewFetcher(); + + var obs = activation.GetActivationForView(window); + obs.ToObservableChangeSet(scheduler: ImmediateScheduler.Instance).Bind(out var activated).Subscribe(); + + var loaded = new RoutedEventArgs(); + loaded.RoutedEvent = FrameworkElement.LoadedEvent; + + window.RaiseEvent(loaded); + + new[] { true }.AssertAreEqual(activated); + + window.Close(); + + new[] { true, false }.AssertAreEqual(activated); + } + + [StaFact] + public void WindowAndFrameworkElementAreActivatedAndDeactivated() + { + var window = new WpfTestWindow(); + var uc = new WpfTestUserControl(); + + window.RootGrid.Children.Add(uc); + + var activation = new ActivationForViewFetcher(); + + var windowObs = activation.GetActivationForView(window); + windowObs.ToObservableChangeSet(scheduler: ImmediateScheduler.Instance).Bind(out var windowActivated).Subscribe(); + + var ucObs = activation.GetActivationForView(uc); + ucObs.ToObservableChangeSet(scheduler: ImmediateScheduler.Instance).Bind(out var controlActivated).Subscribe(); + + var loaded = new RoutedEventArgs(); + loaded.RoutedEvent = FrameworkElement.LoadedEvent; + + window.RaiseEvent(loaded); + uc.RaiseEvent(loaded); + + new[] { true }.AssertAreEqual(windowActivated); + new[] { true }.AssertAreEqual(controlActivated); + + window.Dispatcher.InvokeShutdown(); + + new[] { true, false }.AssertAreEqual(windowActivated); + new[] { true, false }.AssertAreEqual(controlActivated); + } + + [Fact] + public void IsHitTestVisibleActivatesFrameworkElement() + { + var uc = new WpfTestUserControl(); + uc.IsHitTestVisible = false; + var activation = new ActivationForViewFetcher(); + + var obs = activation.GetActivationForView(uc); + obs.ToObservableChangeSet(scheduler: ImmediateScheduler.Instance).Bind(out var activated).Subscribe(); + + var loaded = new RoutedEventArgs(); + loaded.RoutedEvent = FrameworkElement.LoadedEvent; + + uc.RaiseEvent(loaded); + + // Loaded has happened. + new[] { true }.AssertAreEqual(activated); + + uc.IsHitTestVisible = true; + + // IsHitTestVisible true, we don't want the event to repeat unnecessarily. + new[] { true }.AssertAreEqual(activated); + + var unloaded = new RoutedEventArgs(); + unloaded.RoutedEvent = FrameworkElement.UnloadedEvent; + + uc.RaiseEvent(unloaded); + + // We had both a loaded/hit test visible change/unloaded happen. + new[] { true, false }.AssertAreEqual(activated); + } + + [Fact] + public void IsHitTestVisibleDeactivatesFrameworkElement() + { + var uc = new WpfTestUserControl(); + var activation = new ActivationForViewFetcher(); + + var obs = activation.GetActivationForView(uc); + obs.ToObservableChangeSet(scheduler: ImmediateScheduler.Instance).Bind(out var activated).Subscribe(); + + var loaded = new RoutedEventArgs(); + loaded.RoutedEvent = FrameworkElement.LoadedEvent; + + uc.RaiseEvent(loaded); + + new[] { true }.AssertAreEqual(activated); + + uc.IsHitTestVisible = false; + + new[] { true, false }.AssertAreEqual(activated); + } + + [Fact] + public void FrameworkElementIsActivatedAndDeactivatedWithHitTest() + { + var uc = new WpfTestUserControl(); + var activation = new ActivationForViewFetcher(); + + var obs = activation.GetActivationForView(uc); + obs.ToObservableChangeSet(scheduler: ImmediateScheduler.Instance).Bind(out var activated).Subscribe(); + + var loaded = new RoutedEventArgs(); + loaded.RoutedEvent = FrameworkElement.LoadedEvent; + + uc.RaiseEvent(loaded); + + new[] { true }.AssertAreEqual(activated); + + // this should deactivate it + uc.IsHitTestVisible = false; + + new[] { true, false }.AssertAreEqual(activated); + + // this should activate it + uc.IsHitTestVisible = true; + + new[] { true, false, true }.AssertAreEqual(activated); + + var unloaded = new RoutedEventArgs(); + unloaded.RoutedEvent = FrameworkElement.UnloadedEvent; + + uc.RaiseEvent(unloaded); + + new[] { true, false, true, false }.AssertAreEqual(activated); + } + } +} diff --git a/src/ReactiveUI.Wpf/ActivationForViewFetcher.cs b/src/ReactiveUI.Wpf/ActivationForViewFetcher.cs index c42f730ec8..b07cff5592 100644 --- a/src/ReactiveUI.Wpf/ActivationForViewFetcher.cs +++ b/src/ReactiveUI.Wpf/ActivationForViewFetcher.cs @@ -7,6 +7,7 @@ using System.Reactive.Linq; using System.Reflection; using System.Windows; +using System.Windows.Threading; namespace ReactiveUI { @@ -55,10 +56,49 @@ public IObservable GetActivationForView(IActivatableView view) x => fe.Unloaded += x, x => fe.Unloaded -= x); + var windowActivation = GetActivationForWindow(view); + + var dispatcherActivation = GetActivationForDispatcher(fe); + return viewLoaded .Merge(viewUnloaded) .Merge(hitTestVisible) + .Merge(windowActivation) + .Merge(dispatcherActivation) .DistinctUntilChanged(); } + + private static IObservable GetActivationForWindow(IActivatableView view) + { + if (view is not Window window) + { + return Observable.Empty; + } + + var viewClosed = Observable.FromEvent( + eventHandler => + { + void Handler(object? sender, EventArgs e) => eventHandler(false); + return Handler; + }, + x => window.Closed += x, + x => window.Closed -= x); + + return viewClosed; + } + + private static IObservable GetActivationForDispatcher(DispatcherObject dispatcherObject) + { + var dispatcherShutdownStarted = Observable.FromEvent( + eventHandler => + { + void Handler(object? sender, EventArgs e) => eventHandler(false); + return Handler; + }, + x => dispatcherObject.Dispatcher.ShutdownStarted += x, + x => dispatcherObject.Dispatcher.ShutdownStarted -= x); + + return dispatcherShutdownStarted; + } } }