From 64a44cb2b1c207861226873199143f08531556f9 Mon Sep 17 00:00:00 2001
From: Glenn Watson <5834289+glennawatson@users.noreply.github.com>
Date: Sun, 26 Jul 2026 13:04:39 +1000
Subject: [PATCH 1/3] refactor(winui): share routed host setup
- Extract the common routed-host initialization behind a testable contract.
- Add real WinUI coverage across every supported Windows target.
- Mark executable test projects explicitly as tests and non-packable.
---
src/ReactiveUI.Maui/Common/RoutedViewHost.cs | 28 ++---
.../Common/RoutedViewHost{TViewModel}.cs | 26 ++--
.../Internal/IMauiRoutedViewHost.cs | 29 +++++
.../Internal/MauiReactiveHelpers.cs | 38 +++---
src/ReactiveUI.WinUI/ReactiveUI.WinUI.csproj | 3 +
src/reactiveui.slnx | 1 +
.../ReactiveUI.Blazor.Tests.csproj | 1 +
.../ReactiveUI.Builder.Maui.Tests.csproj | 1 +
.../ReactiveUI.Builder.Tests.csproj | 1 +
.../ReactiveUI.Maui.Tests.csproj | 1 +
...ReactiveUI.NonParallel.Mobile.Tests.csproj | 2 +
.../ReactiveUI.Routing.Tests.csproj | 2 +
.../ReactiveUI.Splat.Tests.csproj | 1 +
.../ReactiveUI.Testing.Tests.csproj | 2 +
.../ReactiveUI.Tests/ReactiveUI.Tests.csproj | 2 +
.../ReactiveUI.WinForms.Tests.csproj | 2 +
.../ReactiveUI.WinUI.Tests.csproj | 34 +++++
.../WinUI/AssemblyHooks.cs | 17 +++
.../WinUI/MauiReactiveHelpersTest.cs | 116 ++++++++++++++++++
.../WinUI/WinUITestExecutor.cs | 100 +++++++++++++++
.../WinUIFallbackTest.cs | 16 +++
.../ReactiveUI.Wpf.Tests.csproj | 2 +
22 files changed, 372 insertions(+), 53 deletions(-)
create mode 100644 src/ReactiveUI.Maui/Internal/IMauiRoutedViewHost.cs
create mode 100644 src/tests/ReactiveUI.WinUI.Tests/ReactiveUI.WinUI.Tests.csproj
create mode 100644 src/tests/ReactiveUI.WinUI.Tests/WinUI/AssemblyHooks.cs
create mode 100644 src/tests/ReactiveUI.WinUI.Tests/WinUI/MauiReactiveHelpersTest.cs
create mode 100644 src/tests/ReactiveUI.WinUI.Tests/WinUI/WinUITestExecutor.cs
create mode 100644 src/tests/ReactiveUI.WinUI.Tests/WinUIFallbackTest.cs
diff --git a/src/ReactiveUI.Maui/Common/RoutedViewHost.cs b/src/ReactiveUI.Maui/Common/RoutedViewHost.cs
index 572894e9c8..a18a5047ba 100644
--- a/src/ReactiveUI.Maui/Common/RoutedViewHost.cs
+++ b/src/ReactiveUI.Maui/Common/RoutedViewHost.cs
@@ -11,8 +11,6 @@
#else
using ReactiveUI.Maui.Internal;
#endif
-using Splat;
-
#if REACTIVE_SHIM
namespace ReactiveUI.Reactive;
#else
@@ -26,7 +24,7 @@ namespace ReactiveUI;
///
[RequiresUnreferencedCode("This class uses reflection to determine view model types at runtime through ViewLocator, which may be incompatible with trimming.")]
[RequiresDynamicCode("ViewLocator.ResolveView uses reflection which is incompatible with AOT compilation.")]
-public partial class RoutedViewHost : TransitioningContentControl, IActivatableView, IEnableLogger
+public partial class RoutedViewHost : TransitioningContentControl, IActivatableView, IMauiRoutedViewHost
{
/// The router dependency property.
public static readonly DependencyProperty RouterProperty =
@@ -56,14 +54,7 @@ public RoutedViewHost()
HorizontalContentAlignment = HorizontalAlignment.Stretch;
VerticalContentAlignment = VerticalAlignment.Stretch;
- MauiReactiveHelpers.InitializeRoutedViewHost(
- (this, this.Log(), observable => ViewContractObservable = observable),
- (nameof(Router), RouterProperty, () => Router),
- (nameof(ViewContractObservable), ViewContractObservableProperty, () => ViewContractObservable),
- () => ViewContract,
- contract => _viewContract = contract,
- ResolveViewForViewModel,
- _subscriptions);
+ MauiReactiveHelpers.InitializeRoutedViewHost(this, RouterProperty, ViewContractObservableProperty, _subscriptions, ResolveViewForViewModel);
}
/// Gets or sets the of the view model stack.
@@ -107,23 +98,26 @@ public string? ViewContract
///
public IViewLocator? ViewLocator { get; set; }
+ ///
+ void IMauiRoutedViewHost.SetObservedViewContract(string? contract) => _viewContract = contract;
+
/// Resolves and hosts the view for the supplied view model/contract pair.
- /// The view model and contract to resolve a view for.
+ /// The view model and contract to resolve a view for.
[RequiresUnreferencedCode("This method uses reflection to determine the view model type at runtime, which may be incompatible with trimming.")]
[RequiresDynamicCode("If some of the generic arguments are annotated (either with DynamicallyAccessedMembersAttribute, "
+ "or generic constraints), trimming can't validate that the requirements of those annotations are met.")]
- private void ResolveViewForViewModel((IRoutableViewModel? viewModel, string? contract) x)
+ private void ResolveViewForViewModel((IRoutableViewModel? viewModel, string? contract) route)
{
- if (x.viewModel is null)
+ if (route.viewModel is null)
{
Content = DefaultContent;
return;
}
var viewLocator = ViewLocator ?? ReactiveUI.ViewLocator.Current;
- var view = (viewLocator.ResolveView(x.viewModel, x.contract) ?? viewLocator.ResolveView(x.viewModel))
- ?? throw new InvalidOperationException($"Couldn't find view for '{x.viewModel}'.");
- view.ViewModel = x.viewModel;
+ var view = (viewLocator.ResolveView(route.viewModel, route.contract) ?? viewLocator.ResolveView(route.viewModel))
+ ?? throw new InvalidOperationException($"Couldn't find view for '{route.viewModel}'.");
+ view.ViewModel = route.viewModel;
Content = view;
}
}
diff --git a/src/ReactiveUI.Maui/Common/RoutedViewHost{TViewModel}.cs b/src/ReactiveUI.Maui/Common/RoutedViewHost{TViewModel}.cs
index ddd0d83a68..bf158a711b 100644
--- a/src/ReactiveUI.Maui/Common/RoutedViewHost{TViewModel}.cs
+++ b/src/ReactiveUI.Maui/Common/RoutedViewHost{TViewModel}.cs
@@ -11,8 +11,6 @@
#else
using ReactiveUI.Maui.Internal;
#endif
-using Splat;
-
#if REACTIVE_SHIM
namespace ReactiveUI.Reactive;
#else
@@ -27,7 +25,7 @@ namespace ReactiveUI;
///
/// The type of the view model. Must have a public parameterless constructor and implement IRoutableViewModel.
public partial class RoutedViewHost<
- [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TViewModel> : TransitioningContentControl, IActivatableView, IEnableLogger
+ [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TViewModel> : TransitioningContentControl, IActivatableView, IMauiRoutedViewHost
where TViewModel : class, IRoutableViewModel
{
/// The router dependency property.
@@ -58,14 +56,7 @@ public RoutedViewHost()
HorizontalContentAlignment = HorizontalAlignment.Stretch;
VerticalContentAlignment = VerticalAlignment.Stretch;
- MauiReactiveHelpers.InitializeRoutedViewHost(
- (this, this.Log(), observable => ViewContractObservable = observable),
- (nameof(Router), RouterProperty, () => Router),
- (nameof(ViewContractObservable), ViewContractObservableProperty, () => ViewContractObservable),
- () => ViewContract,
- contract => _viewContract = contract,
- ResolveViewForViewModel,
- _subscriptions);
+ MauiReactiveHelpers.InitializeRoutedViewHost(this, RouterProperty, ViewContractObservableProperty, _subscriptions, ResolveViewForViewModel);
}
/// Gets or sets the view locator.
@@ -109,14 +100,17 @@ public string? ViewContract
}
}
+ ///
+ void IMauiRoutedViewHost.SetObservedViewContract(string? contract) => _viewContract = contract;
+
///
/// Resolves and displays the view for the given view model and contract.
/// This method uses the generic ViewLocator.ResolveView{TViewModel} which is AOT-safe.
///
- /// Tuple containing the view model and contract.
- private void ResolveViewForViewModel((IRoutableViewModel? viewModel, string? contract) x)
+ /// Tuple containing the view model and contract.
+ private void ResolveViewForViewModel((IRoutableViewModel? viewModel, string? contract) route)
{
- if (x.viewModel is null)
+ if (route.viewModel is null)
{
Content = DefaultContent;
return;
@@ -125,9 +119,9 @@ private void ResolveViewForViewModel((IRoutableViewModel? viewModel, string? con
var viewLocator = ViewLocator ?? ReactiveUI.ViewLocator.Current;
// Use the generic ResolveView method - this is AOT-safe!
- var view = viewLocator.ResolveView(x.contract) ?? viewLocator.ResolveView()
+ var view = viewLocator.ResolveView(route.contract) ?? viewLocator.ResolveView()
?? throw new InvalidOperationException($"Couldn't find view for '{nameof(TViewModel)}'.");
- view.ViewModel = x.viewModel as TViewModel;
+ view.ViewModel = route.viewModel as TViewModel;
Content = view;
}
}
diff --git a/src/ReactiveUI.Maui/Internal/IMauiRoutedViewHost.cs b/src/ReactiveUI.Maui/Internal/IMauiRoutedViewHost.cs
new file mode 100644
index 0000000000..bf84c1df7b
--- /dev/null
+++ b/src/ReactiveUI.Maui/Internal/IMauiRoutedViewHost.cs
@@ -0,0 +1,29 @@
+// Copyright (c) 2009-2026 .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 Splat;
+
+#if REACTIVE_SHIM
+namespace ReactiveUI.Reactive.Maui.Internal;
+#else
+namespace ReactiveUI.Maui.Internal;
+#endif
+
+/// Supplies the host-specific operations used to initialize a WinUI routed host.
+internal interface IMauiRoutedViewHost : IEnableLogger
+{
+ /// Gets the router.
+ RoutingState Router { get; }
+
+ /// Gets or sets the view-contract observable.
+ IObservable ViewContractObservable { get; set; }
+
+ /// Gets the current view contract.
+ string? ViewContract { get; }
+
+ /// Stores the latest observed view contract without replacing its source observable.
+ /// The observed contract.
+ void SetObservedViewContract(string? contract);
+}
diff --git a/src/ReactiveUI.Maui/Internal/MauiReactiveHelpers.cs b/src/ReactiveUI.Maui/Internal/MauiReactiveHelpers.cs
index 96045b4e63..6a1f687c4f 100644
--- a/src/ReactiveUI.Maui/Internal/MauiReactiveHelpers.cs
+++ b/src/ReactiveUI.Maui/Internal/MauiReactiveHelpers.cs
@@ -141,29 +141,27 @@ internal static IObservable CreatePropertyValueObservable(
}
/// Initializes a WinUI routed host and its view-contract subscriptions.
- /// The host, logger, and view-contract setter.
- /// The router property metadata and accessor.
- /// The view-contract observable property metadata and accessor.
- /// Gets the current view contract.
- /// Stores the latest view contract.
- /// Resolves a routed view model and contract.
+ /// The routed-host type.
+ /// The host to initialize.
+ /// The host's router dependency property.
+ /// The host's view-contract observable dependency property.
/// Collects the host subscription.
- internal static void InitializeRoutedViewHost(
- (FrameworkElement Source, IFullLogger Logger, Action> SetViewContractObservable) host,
- (string Name, DependencyProperty Property, Func GetValue) router,
- (string Name, DependencyProperty Property, Func> GetValue) viewContractObservable,
- Func getViewContract,
- Action setViewContract,
- Action<(IRoutableViewModel? viewModel, string? contract)> resolveView,
- MultipleDisposable subscriptions)
+ /// Resolves a routed view model and contract.
+ internal static void InitializeRoutedViewHost(
+ THost host,
+ DependencyProperty routerProperty,
+ DependencyProperty viewContractObservableProperty,
+ MultipleDisposable subscriptions,
+ Action<(IRoutableViewModel? viewModel, string? contract)> resolveView)
+ where THost : FrameworkElement, IMauiRoutedViewHost
{
- host.SetViewContractObservable(CreateViewContractObservable(host.Source, host.Logger));
+ host.ViewContractObservable = CreateViewContractObservable(host, host.Log());
SubscribeRoutedViewHost(
- host.Source,
- router,
- viewContractObservable,
- getViewContract,
- setViewContract,
+ host,
+ (nameof(host.Router), routerProperty, () => host.Router),
+ (nameof(host.ViewContractObservable), viewContractObservableProperty, () => host.ViewContractObservable),
+ () => host.ViewContract,
+ host.SetObservedViewContract,
resolveView,
subscriptions);
}
diff --git a/src/ReactiveUI.WinUI/ReactiveUI.WinUI.csproj b/src/ReactiveUI.WinUI/ReactiveUI.WinUI.csproj
index c12dd0dc45..68b1caa192 100644
--- a/src/ReactiveUI.WinUI/ReactiveUI.WinUI.csproj
+++ b/src/ReactiveUI.WinUI/ReactiveUI.WinUI.csproj
@@ -29,6 +29,9 @@
+
+
+
diff --git a/src/reactiveui.slnx b/src/reactiveui.slnx
index 66ed88f6e3..0b4b06faca 100644
--- a/src/reactiveui.slnx
+++ b/src/reactiveui.slnx
@@ -48,6 +48,7 @@
+
diff --git a/src/tests/ReactiveUI.Blazor.Tests/ReactiveUI.Blazor.Tests.csproj b/src/tests/ReactiveUI.Blazor.Tests/ReactiveUI.Blazor.Tests.csproj
index 3df0c92aed..3ea128d3ef 100644
--- a/src/tests/ReactiveUI.Blazor.Tests/ReactiveUI.Blazor.Tests.csproj
+++ b/src/tests/ReactiveUI.Blazor.Tests/ReactiveUI.Blazor.Tests.csproj
@@ -9,6 +9,7 @@
$(ReactiveUIModernTargets)
Exe
true
+ false
enable
enable
net10.0-android;net10.0-ios
Exe
+ true
+ false
$(NoWarn);CS1591
diff --git a/src/tests/ReactiveUI.Routing.Tests/ReactiveUI.Routing.Tests.csproj b/src/tests/ReactiveUI.Routing.Tests/ReactiveUI.Routing.Tests.csproj
index 8f581e5280..6e67acca76 100644
--- a/src/tests/ReactiveUI.Routing.Tests/ReactiveUI.Routing.Tests.csproj
+++ b/src/tests/ReactiveUI.Routing.Tests/ReactiveUI.Routing.Tests.csproj
@@ -9,6 +9,8 @@
$(ReactiveUITestingTargets)
Exe
ReactiveUI.Tests
+ true
+ false
diff --git a/src/tests/ReactiveUI.Splat.Tests/ReactiveUI.Splat.Tests.csproj b/src/tests/ReactiveUI.Splat.Tests/ReactiveUI.Splat.Tests.csproj
index 72ec181213..e839f5d667 100644
--- a/src/tests/ReactiveUI.Splat.Tests/ReactiveUI.Splat.Tests.csproj
+++ b/src/tests/ReactiveUI.Splat.Tests/ReactiveUI.Splat.Tests.csproj
@@ -2,6 +2,7 @@
$(ReactiveUITestingTargets)
Exe
+ true
false
diff --git a/src/tests/ReactiveUI.Testing.Tests/ReactiveUI.Testing.Tests.csproj b/src/tests/ReactiveUI.Testing.Tests/ReactiveUI.Testing.Tests.csproj
index 410e7b9135..e5921809c7 100644
--- a/src/tests/ReactiveUI.Testing.Tests/ReactiveUI.Testing.Tests.csproj
+++ b/src/tests/ReactiveUI.Testing.Tests/ReactiveUI.Testing.Tests.csproj
@@ -2,6 +2,8 @@
$(ReactiveUITestingTargets)
Exe
+ true
+ false
netstandard2.0
$(TargetFramework)
diff --git a/src/tests/ReactiveUI.Tests/ReactiveUI.Tests.csproj b/src/tests/ReactiveUI.Tests/ReactiveUI.Tests.csproj
index 0cb93a8579..607e00b719 100644
--- a/src/tests/ReactiveUI.Tests/ReactiveUI.Tests.csproj
+++ b/src/tests/ReactiveUI.Tests/ReactiveUI.Tests.csproj
@@ -8,6 +8,8 @@
$(ReactiveUITestingTargets)
Exe
+ true
+ false
diff --git a/src/tests/ReactiveUI.WinForms.Tests/ReactiveUI.WinForms.Tests.csproj b/src/tests/ReactiveUI.WinForms.Tests/ReactiveUI.WinForms.Tests.csproj
index 9eae3ee806..45c7aa557c 100644
--- a/src/tests/ReactiveUI.WinForms.Tests/ReactiveUI.WinForms.Tests.csproj
+++ b/src/tests/ReactiveUI.WinForms.Tests/ReactiveUI.WinForms.Tests.csproj
@@ -8,6 +8,8 @@
$(ReactiveUITestingUITargets)
Exe
+ true
+ false
diff --git a/src/tests/ReactiveUI.WinUI.Tests/ReactiveUI.WinUI.Tests.csproj b/src/tests/ReactiveUI.WinUI.Tests/ReactiveUI.WinUI.Tests.csproj
new file mode 100644
index 0000000000..8cb7995cbb
--- /dev/null
+++ b/src/tests/ReactiveUI.WinUI.Tests/ReactiveUI.WinUI.Tests.csproj
@@ -0,0 +1,34 @@
+
+
+ $(ReactiveUITestingUITargets)
+ Exe
+ true
+ false
+
+
+
+
+
+
+
+
+
+
+
+ true
+ None
+ false
+ false
+ false
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/tests/ReactiveUI.WinUI.Tests/WinUI/AssemblyHooks.cs b/src/tests/ReactiveUI.WinUI.Tests/WinUI/AssemblyHooks.cs
new file mode 100644
index 0000000000..9e824c4ea9
--- /dev/null
+++ b/src/tests/ReactiveUI.WinUI.Tests/WinUI/AssemblyHooks.cs
@@ -0,0 +1,17 @@
+// Copyright (c) 2009-2026 .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.
+
+[assembly: NotInParallel]
+
+namespace ReactiveUI.WinUI.Tests;
+
+/// Controls the lifetime of the WinUI application used by this test assembly.
+public static class AssemblyHooks
+{
+ /// Stops the WinUI application after all tests complete.
+ /// A representing the asynchronous operation.
+ [After(Assembly)]
+ public static Task AssemblyCleanup() => WinUITestExecutor.StopAsync();
+}
diff --git a/src/tests/ReactiveUI.WinUI.Tests/WinUI/MauiReactiveHelpersTest.cs b/src/tests/ReactiveUI.WinUI.Tests/WinUI/MauiReactiveHelpersTest.cs
new file mode 100644
index 0000000000..89f08b1eec
--- /dev/null
+++ b/src/tests/ReactiveUI.WinUI.Tests/WinUI/MauiReactiveHelpersTest.cs
@@ -0,0 +1,116 @@
+// Copyright (c) 2009-2026 .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 Microsoft.UI.Xaml;
+using ReactiveUI.Maui.Internal;
+using TUnit.Core.Executors;
+
+namespace ReactiveUI.WinUI.Tests;
+
+/// Tests the shared WinUI routed-host initialization.
+public sealed class MauiReactiveHelpersTest
+{
+ /// Verifies that routed-host initialization assigns its contract stream and starts route resolution.
+ /// A representing the asynchronous operation.
+ [Test]
+ [TestExecutor]
+ public async Task InitializeRoutedViewHost_InitializesContractAndRouteSubscriptions()
+ {
+ var host = new TestWinUiRoutedHost();
+ var resolutions = new List<(IRoutableViewModel? viewModel, string? contract)>();
+ MultipleDisposable subscriptions = [];
+
+ MauiReactiveHelpers.InitializeRoutedViewHost(
+ host,
+ TestWinUiRoutedHost.RouterProperty,
+ TestWinUiRoutedHost.ViewContractObservableProperty,
+ subscriptions,
+ resolutions.Add);
+
+ await Assert.That(host.ViewContractObservable).IsNotNull();
+ await Assert.That(resolutions.Count).IsEqualTo(1);
+ await Assert.That(resolutions[0].viewModel).IsNull();
+ await Assert.That(resolutions[0].contract).IsNull();
+
+ subscriptions.Dispose();
+ }
+
+ /// Verifies that the non-generic routed-host overload uses the shared initialization.
+ /// A representing the asynchronous operation.
+ [Test]
+ [TestExecutor]
+ public async Task RoutedViewHost_InitializesViewContractObservable()
+ {
+ var host = new RoutedViewHost();
+
+ await Assert.That(host.ViewContractObservable).IsNotNull();
+ await Assert.That(host.ViewContract).IsNull();
+ }
+
+ /// Verifies that the generic routed-host overload uses the shared initialization.
+ /// A representing the asynchronous operation.
+ [Test]
+ [TestExecutor]
+ public async Task GenericRoutedViewHost_InitializesViewContractObservable()
+ {
+ var host = new RoutedViewHost();
+
+ await Assert.That(host.ViewContractObservable).IsNotNull();
+ await Assert.That(host.ViewContract).IsNull();
+ }
+
+ /// Minimal routed host used to exercise the shared initialization helper directly.
+ private sealed class TestWinUiRoutedHost : FrameworkElement, IMauiRoutedViewHost
+ {
+ /// The router dependency property.
+ public static readonly DependencyProperty RouterProperty =
+ DependencyProperty.Register(nameof(Router), typeof(RoutingState), typeof(TestWinUiRoutedHost), new(null));
+
+ /// The view-contract observable dependency property.
+ public static readonly DependencyProperty ViewContractObservableProperty =
+ DependencyProperty.Register(
+ nameof(ViewContractObservable),
+ typeof(IObservable),
+ typeof(TestWinUiRoutedHost),
+ new(Signal.Emit(null)));
+
+ ///
+ public RoutingState Router
+ {
+ get => (RoutingState)GetValue(RouterProperty);
+ set => SetValue(RouterProperty, value);
+ }
+
+ ///
+ public IObservable ViewContractObservable
+ {
+ get => (IObservable)GetValue(ViewContractObservableProperty);
+ set => SetValue(ViewContractObservableProperty, value);
+ }
+
+ ///
+ public string? ViewContract { get; private set; }
+
+ ///
+ void IMauiRoutedViewHost.SetObservedViewContract(string? contract) => ViewContract = contract;
+ }
+
+ /// Minimal view model used to construct the generic routed host.
+ private sealed class TestWinUiRoutableViewModel : ReactiveObject, IRoutableViewModel
+ {
+ ///
+ public string? UrlPathSegment => null;
+
+ ///
+ public IScreen HostScreen { get; } = new TestWinUiScreen();
+ }
+
+ /// Minimal screen used by the generic routed-host view model.
+ private sealed class TestWinUiScreen : IScreen
+ {
+ ///
+ public RoutingState Router { get; } = new();
+ }
+}
diff --git a/src/tests/ReactiveUI.WinUI.Tests/WinUI/WinUITestExecutor.cs b/src/tests/ReactiveUI.WinUI.Tests/WinUI/WinUITestExecutor.cs
new file mode 100644
index 0000000000..a84701c037
--- /dev/null
+++ b/src/tests/ReactiveUI.WinUI.Tests/WinUI/WinUITestExecutor.cs
@@ -0,0 +1,100 @@
+// Copyright (c) 2009-2026 .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 Microsoft.UI.Dispatching;
+using Microsoft.UI.Xaml;
+using ReactiveUI.Tests.Utilities.AppBuilder;
+using TUnit.Core.Interfaces;
+
+namespace ReactiveUI.WinUI.Tests;
+
+/// Runs tests on a shared STA thread initialized with a WinUI XAML application.
+public sealed class WinUITestExecutor : ITestExecutor
+{
+ /// Receives the shared dispatcher after the WinUI application initializes.
+ private static readonly TaskCompletionSource DispatcherReady =
+ new(TaskCreationOptions.RunContinuationsAsynchronously);
+
+ /// Initializes static members of the class.
+ static WinUITestExecutor() => StartDispatcher();
+
+ ///
+ public async ValueTask ExecuteTest(TestContext context, Func action)
+ {
+ ArgumentNullException.ThrowIfNull(action);
+
+ var dispatcher = await DispatcherReady.Task.ConfigureAwait(false);
+ var completion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ if (!dispatcher.TryEnqueue(() => _ = ExecuteAsync(action, completion)))
+ {
+ throw new InvalidOperationException("The WinUI test dispatcher is unavailable.");
+ }
+
+ await completion.Task.ConfigureAwait(false);
+ }
+
+ /// Stops the shared WinUI application after the test assembly completes.
+ /// A representing the asynchronous operation.
+ internal static async Task StopAsync()
+ {
+ var dispatcher = await DispatcherReady.Task.ConfigureAwait(false);
+ var completion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ if (!dispatcher.TryEnqueue(() =>
+ {
+ Application.Current.Exit();
+ completion.SetResult();
+ }))
+ {
+ return;
+ }
+
+ await completion.Task.ConfigureAwait(false);
+ }
+
+ /// Starts the shared WinUI application thread.
+ private static void StartDispatcher()
+ {
+ var thread = new Thread(static () =>
+ Application.Start(static ignored =>
+ {
+ _ = new TestApplication();
+ DispatcherReady.SetResult(DispatcherQueue.GetForCurrentThread());
+ }));
+ thread.IsBackground = true;
+ thread.Name = "WinUI test dispatcher";
+ thread.SetApartmentState(ApartmentState.STA);
+ thread.Start();
+ }
+
+ /// Executes a test and reports its completion to the calling test thread.
+ /// The test action.
+ /// Receives the test result.
+ /// A representing the asynchronous operation.
+ private static async Task ExecuteAsync(Func action, TaskCompletionSource completion)
+ {
+ var helper = new AppBuilderTestHelper();
+ try
+ {
+ helper.Initialize(static builder => _ = builder.WithCoreServices());
+ try
+ {
+ await action().ConfigureAwait(true);
+ }
+ finally
+ {
+ helper.CleanUp();
+ }
+
+ completion.SetResult();
+ }
+ catch (Exception exception)
+ {
+ completion.SetException(exception);
+ }
+ }
+
+ /// Minimal WinUI application used to initialize XAML for the test process.
+ private sealed class TestApplication : Application;
+}
diff --git a/src/tests/ReactiveUI.WinUI.Tests/WinUIFallbackTest.cs b/src/tests/ReactiveUI.WinUI.Tests/WinUIFallbackTest.cs
new file mode 100644
index 0000000000..f4fdd1d1f5
--- /dev/null
+++ b/src/tests/ReactiveUI.WinUI.Tests/WinUIFallbackTest.cs
@@ -0,0 +1,16 @@
+// Copyright (c) 2009-2026 .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.
+
+namespace ReactiveUI.WinUI.Tests;
+
+/// Verifies the non-Windows fallback used by the cross-platform solution build.
+public sealed class WinUIFallbackTest
+{
+ /// Verifies that the fallback target does not load WinUI.
+ /// A representing the asynchronous operation.
+ [Test]
+ public async Task WinUIAssembly_IsNotLoaded() =>
+ await Assert.That(Type.GetType("Microsoft.UI.Xaml.Application, Microsoft.WinUI")).IsNull();
+}
diff --git a/src/tests/ReactiveUI.Wpf.Tests/ReactiveUI.Wpf.Tests.csproj b/src/tests/ReactiveUI.Wpf.Tests/ReactiveUI.Wpf.Tests.csproj
index 431e00a13b..d7dcfd7f1f 100644
--- a/src/tests/ReactiveUI.Wpf.Tests/ReactiveUI.Wpf.Tests.csproj
+++ b/src/tests/ReactiveUI.Wpf.Tests/ReactiveUI.Wpf.Tests.csproj
@@ -9,6 +9,8 @@
$(ReactiveUITestingUITargets)
Exe
ReactiveUI.Tests
+ true
+ false
From 169a80ccb068b4d524f4dc5d7cecb663fe6659b1 Mon Sep 17 00:00:00 2001
From: Glenn Watson <5834289+glennawatson@users.noreply.github.com>
Date: Sun, 26 Jul 2026 13:04:39 +1000
Subject: [PATCH 2/3] build(deps): update primitives to 7.1.0
- Use the latest compatible ReactiveUI.Primitives release across the target framework matrix.
---
src/Directory.Packages.props | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props
index e3585ebcda..5fd5935ac9 100644
--- a/src/Directory.Packages.props
+++ b/src/Directory.Packages.props
@@ -6,7 +6,7 @@
20.2.0
- 7.0.0
+ 7.1.0
1.61.38
From 09c7c7e05a14c6b08921e50dd2d4edfa63d50a0d Mon Sep 17 00:00:00 2001
From: Glenn Watson <5834289+glennawatson@users.noreply.github.com>
Date: Sun, 26 Jul 2026 14:37:26 +1000
Subject: [PATCH 3/3] remove redundant tests
---
src/ReactiveUI.WinUI/ReactiveUI.WinUI.csproj | 3 -
src/reactiveui.slnx | 1 -
.../ReactiveUI.WinUI.Tests.csproj | 34 -----
.../WinUI/AssemblyHooks.cs | 17 ---
.../WinUI/MauiReactiveHelpersTest.cs | 116 ------------------
.../WinUI/WinUITestExecutor.cs | 100 ---------------
.../WinUIFallbackTest.cs | 16 ---
7 files changed, 287 deletions(-)
delete mode 100644 src/tests/ReactiveUI.WinUI.Tests/ReactiveUI.WinUI.Tests.csproj
delete mode 100644 src/tests/ReactiveUI.WinUI.Tests/WinUI/AssemblyHooks.cs
delete mode 100644 src/tests/ReactiveUI.WinUI.Tests/WinUI/MauiReactiveHelpersTest.cs
delete mode 100644 src/tests/ReactiveUI.WinUI.Tests/WinUI/WinUITestExecutor.cs
delete mode 100644 src/tests/ReactiveUI.WinUI.Tests/WinUIFallbackTest.cs
diff --git a/src/ReactiveUI.WinUI/ReactiveUI.WinUI.csproj b/src/ReactiveUI.WinUI/ReactiveUI.WinUI.csproj
index 68b1caa192..c12dd0dc45 100644
--- a/src/ReactiveUI.WinUI/ReactiveUI.WinUI.csproj
+++ b/src/ReactiveUI.WinUI/ReactiveUI.WinUI.csproj
@@ -29,9 +29,6 @@
-
-
-
diff --git a/src/reactiveui.slnx b/src/reactiveui.slnx
index 0b4b06faca..66ed88f6e3 100644
--- a/src/reactiveui.slnx
+++ b/src/reactiveui.slnx
@@ -48,7 +48,6 @@
-
diff --git a/src/tests/ReactiveUI.WinUI.Tests/ReactiveUI.WinUI.Tests.csproj b/src/tests/ReactiveUI.WinUI.Tests/ReactiveUI.WinUI.Tests.csproj
deleted file mode 100644
index 8cb7995cbb..0000000000
--- a/src/tests/ReactiveUI.WinUI.Tests/ReactiveUI.WinUI.Tests.csproj
+++ /dev/null
@@ -1,34 +0,0 @@
-
-
- $(ReactiveUITestingUITargets)
- Exe
- true
- false
-
-
-
-
-
-
-
-
-
-
-
- true
- None
- false
- false
- false
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/tests/ReactiveUI.WinUI.Tests/WinUI/AssemblyHooks.cs b/src/tests/ReactiveUI.WinUI.Tests/WinUI/AssemblyHooks.cs
deleted file mode 100644
index 9e824c4ea9..0000000000
--- a/src/tests/ReactiveUI.WinUI.Tests/WinUI/AssemblyHooks.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-// Copyright (c) 2009-2026 .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.
-
-[assembly: NotInParallel]
-
-namespace ReactiveUI.WinUI.Tests;
-
-/// Controls the lifetime of the WinUI application used by this test assembly.
-public static class AssemblyHooks
-{
- /// Stops the WinUI application after all tests complete.
- /// A representing the asynchronous operation.
- [After(Assembly)]
- public static Task AssemblyCleanup() => WinUITestExecutor.StopAsync();
-}
diff --git a/src/tests/ReactiveUI.WinUI.Tests/WinUI/MauiReactiveHelpersTest.cs b/src/tests/ReactiveUI.WinUI.Tests/WinUI/MauiReactiveHelpersTest.cs
deleted file mode 100644
index 89f08b1eec..0000000000
--- a/src/tests/ReactiveUI.WinUI.Tests/WinUI/MauiReactiveHelpersTest.cs
+++ /dev/null
@@ -1,116 +0,0 @@
-// Copyright (c) 2009-2026 .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 Microsoft.UI.Xaml;
-using ReactiveUI.Maui.Internal;
-using TUnit.Core.Executors;
-
-namespace ReactiveUI.WinUI.Tests;
-
-/// Tests the shared WinUI routed-host initialization.
-public sealed class MauiReactiveHelpersTest
-{
- /// Verifies that routed-host initialization assigns its contract stream and starts route resolution.
- /// A representing the asynchronous operation.
- [Test]
- [TestExecutor]
- public async Task InitializeRoutedViewHost_InitializesContractAndRouteSubscriptions()
- {
- var host = new TestWinUiRoutedHost();
- var resolutions = new List<(IRoutableViewModel? viewModel, string? contract)>();
- MultipleDisposable subscriptions = [];
-
- MauiReactiveHelpers.InitializeRoutedViewHost(
- host,
- TestWinUiRoutedHost.RouterProperty,
- TestWinUiRoutedHost.ViewContractObservableProperty,
- subscriptions,
- resolutions.Add);
-
- await Assert.That(host.ViewContractObservable).IsNotNull();
- await Assert.That(resolutions.Count).IsEqualTo(1);
- await Assert.That(resolutions[0].viewModel).IsNull();
- await Assert.That(resolutions[0].contract).IsNull();
-
- subscriptions.Dispose();
- }
-
- /// Verifies that the non-generic routed-host overload uses the shared initialization.
- /// A representing the asynchronous operation.
- [Test]
- [TestExecutor]
- public async Task RoutedViewHost_InitializesViewContractObservable()
- {
- var host = new RoutedViewHost();
-
- await Assert.That(host.ViewContractObservable).IsNotNull();
- await Assert.That(host.ViewContract).IsNull();
- }
-
- /// Verifies that the generic routed-host overload uses the shared initialization.
- /// A representing the asynchronous operation.
- [Test]
- [TestExecutor]
- public async Task GenericRoutedViewHost_InitializesViewContractObservable()
- {
- var host = new RoutedViewHost();
-
- await Assert.That(host.ViewContractObservable).IsNotNull();
- await Assert.That(host.ViewContract).IsNull();
- }
-
- /// Minimal routed host used to exercise the shared initialization helper directly.
- private sealed class TestWinUiRoutedHost : FrameworkElement, IMauiRoutedViewHost
- {
- /// The router dependency property.
- public static readonly DependencyProperty RouterProperty =
- DependencyProperty.Register(nameof(Router), typeof(RoutingState), typeof(TestWinUiRoutedHost), new(null));
-
- /// The view-contract observable dependency property.
- public static readonly DependencyProperty ViewContractObservableProperty =
- DependencyProperty.Register(
- nameof(ViewContractObservable),
- typeof(IObservable),
- typeof(TestWinUiRoutedHost),
- new(Signal.Emit(null)));
-
- ///
- public RoutingState Router
- {
- get => (RoutingState)GetValue(RouterProperty);
- set => SetValue(RouterProperty, value);
- }
-
- ///
- public IObservable ViewContractObservable
- {
- get => (IObservable)GetValue(ViewContractObservableProperty);
- set => SetValue(ViewContractObservableProperty, value);
- }
-
- ///
- public string? ViewContract { get; private set; }
-
- ///
- void IMauiRoutedViewHost.SetObservedViewContract(string? contract) => ViewContract = contract;
- }
-
- /// Minimal view model used to construct the generic routed host.
- private sealed class TestWinUiRoutableViewModel : ReactiveObject, IRoutableViewModel
- {
- ///
- public string? UrlPathSegment => null;
-
- ///
- public IScreen HostScreen { get; } = new TestWinUiScreen();
- }
-
- /// Minimal screen used by the generic routed-host view model.
- private sealed class TestWinUiScreen : IScreen
- {
- ///
- public RoutingState Router { get; } = new();
- }
-}
diff --git a/src/tests/ReactiveUI.WinUI.Tests/WinUI/WinUITestExecutor.cs b/src/tests/ReactiveUI.WinUI.Tests/WinUI/WinUITestExecutor.cs
deleted file mode 100644
index a84701c037..0000000000
--- a/src/tests/ReactiveUI.WinUI.Tests/WinUI/WinUITestExecutor.cs
+++ /dev/null
@@ -1,100 +0,0 @@
-// Copyright (c) 2009-2026 .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 Microsoft.UI.Dispatching;
-using Microsoft.UI.Xaml;
-using ReactiveUI.Tests.Utilities.AppBuilder;
-using TUnit.Core.Interfaces;
-
-namespace ReactiveUI.WinUI.Tests;
-
-/// Runs tests on a shared STA thread initialized with a WinUI XAML application.
-public sealed class WinUITestExecutor : ITestExecutor
-{
- /// Receives the shared dispatcher after the WinUI application initializes.
- private static readonly TaskCompletionSource DispatcherReady =
- new(TaskCreationOptions.RunContinuationsAsynchronously);
-
- /// Initializes static members of the class.
- static WinUITestExecutor() => StartDispatcher();
-
- ///
- public async ValueTask ExecuteTest(TestContext context, Func action)
- {
- ArgumentNullException.ThrowIfNull(action);
-
- var dispatcher = await DispatcherReady.Task.ConfigureAwait(false);
- var completion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
- if (!dispatcher.TryEnqueue(() => _ = ExecuteAsync(action, completion)))
- {
- throw new InvalidOperationException("The WinUI test dispatcher is unavailable.");
- }
-
- await completion.Task.ConfigureAwait(false);
- }
-
- /// Stops the shared WinUI application after the test assembly completes.
- /// A representing the asynchronous operation.
- internal static async Task StopAsync()
- {
- var dispatcher = await DispatcherReady.Task.ConfigureAwait(false);
- var completion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
- if (!dispatcher.TryEnqueue(() =>
- {
- Application.Current.Exit();
- completion.SetResult();
- }))
- {
- return;
- }
-
- await completion.Task.ConfigureAwait(false);
- }
-
- /// Starts the shared WinUI application thread.
- private static void StartDispatcher()
- {
- var thread = new Thread(static () =>
- Application.Start(static ignored =>
- {
- _ = new TestApplication();
- DispatcherReady.SetResult(DispatcherQueue.GetForCurrentThread());
- }));
- thread.IsBackground = true;
- thread.Name = "WinUI test dispatcher";
- thread.SetApartmentState(ApartmentState.STA);
- thread.Start();
- }
-
- /// Executes a test and reports its completion to the calling test thread.
- /// The test action.
- /// Receives the test result.
- /// A representing the asynchronous operation.
- private static async Task ExecuteAsync(Func action, TaskCompletionSource completion)
- {
- var helper = new AppBuilderTestHelper();
- try
- {
- helper.Initialize(static builder => _ = builder.WithCoreServices());
- try
- {
- await action().ConfigureAwait(true);
- }
- finally
- {
- helper.CleanUp();
- }
-
- completion.SetResult();
- }
- catch (Exception exception)
- {
- completion.SetException(exception);
- }
- }
-
- /// Minimal WinUI application used to initialize XAML for the test process.
- private sealed class TestApplication : Application;
-}
diff --git a/src/tests/ReactiveUI.WinUI.Tests/WinUIFallbackTest.cs b/src/tests/ReactiveUI.WinUI.Tests/WinUIFallbackTest.cs
deleted file mode 100644
index f4fdd1d1f5..0000000000
--- a/src/tests/ReactiveUI.WinUI.Tests/WinUIFallbackTest.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-// Copyright (c) 2009-2026 .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.
-
-namespace ReactiveUI.WinUI.Tests;
-
-/// Verifies the non-Windows fallback used by the cross-platform solution build.
-public sealed class WinUIFallbackTest
-{
- /// Verifies that the fallback target does not load WinUI.
- /// A representing the asynchronous operation.
- [Test]
- public async Task WinUIAssembly_IsNotLoaded() =>
- await Assert.That(Type.GetType("Microsoft.UI.Xaml.Application, Microsoft.WinUI")).IsNull();
-}