Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<PropertyGroup Label="Shared Version Variables">
<SplatVersion>20.2.0</SplatVersion>
<PrimitivesVersion>7.0.0</PrimitivesVersion>
<PrimitivesVersion>7.1.0</PrimitivesVersion>
<TUnitVersion>1.61.38</TUnitVersion>
<!-- StyleSharp.Analyzers, PerformanceSharp.Analyzers and SecuritySharp.Analyzers ship from the
same release pipeline and always share a version. -->
Expand Down
28 changes: 11 additions & 17 deletions src/ReactiveUI.Maui/Common/RoutedViewHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@
#else
using ReactiveUI.Maui.Internal;
#endif
using Splat;

#if REACTIVE_SHIM
namespace ReactiveUI.Reactive;
#else
Expand All @@ -26,7 +24,7 @@ namespace ReactiveUI;
/// </summary>
[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
{
/// <summary>The router dependency property.</summary>
public static readonly DependencyProperty RouterProperty =
Expand Down Expand Up @@ -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);
}

/// <summary>Gets or sets the <see cref="RoutingState"/> of the view model stack.</summary>
Expand Down Expand Up @@ -107,23 +98,26 @@ public string? ViewContract
/// </value>
public IViewLocator? ViewLocator { get; set; }

/// <inheritdoc/>
void IMauiRoutedViewHost.SetObservedViewContract(string? contract) => _viewContract = contract;

/// <summary>Resolves and hosts the view for the supplied view model/contract pair.</summary>
/// <param name="x">The view model and contract to resolve a view for.</param>
/// <param name="route">The view model and contract to resolve a view for.</param>
[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;
}
}
Expand Down
26 changes: 10 additions & 16 deletions src/ReactiveUI.Maui/Common/RoutedViewHost{TViewModel}.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@
#else
using ReactiveUI.Maui.Internal;
#endif
using Splat;

#if REACTIVE_SHIM
namespace ReactiveUI.Reactive;
#else
Expand All @@ -27,7 +25,7 @@ namespace ReactiveUI;
/// </summary>
/// <typeparam name="TViewModel">The type of the view model. Must have a public parameterless constructor and implement IRoutableViewModel.</typeparam>
public partial class RoutedViewHost<
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TViewModel> : TransitioningContentControl, IActivatableView, IEnableLogger
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TViewModel> : TransitioningContentControl, IActivatableView, IMauiRoutedViewHost
where TViewModel : class, IRoutableViewModel
{
/// <summary>The router dependency property.</summary>
Expand Down Expand Up @@ -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);
}

/// <summary>Gets or sets the view locator.</summary>
Expand Down Expand Up @@ -109,14 +100,17 @@ public string? ViewContract
}
}

/// <inheritdoc/>
void IMauiRoutedViewHost.SetObservedViewContract(string? contract) => _viewContract = contract;

/// <summary>
/// Resolves and displays the view for the given view model and contract.
/// This method uses the generic ViewLocator.ResolveView{TViewModel} which is AOT-safe.
/// </summary>
/// <param name="x">Tuple containing the view model and contract.</param>
private void ResolveViewForViewModel((IRoutableViewModel? viewModel, string? contract) x)
/// <param name="route">Tuple containing the view model and contract.</param>
private void ResolveViewForViewModel((IRoutableViewModel? viewModel, string? contract) route)
{
if (x.viewModel is null)
if (route.viewModel is null)
{
Content = DefaultContent;
return;
Expand All @@ -125,9 +119,9 @@ private void ResolveViewForViewModel((IRoutableViewModel? viewModel, string? con
var viewLocator = ViewLocator ?? ReactiveUI.ViewLocator.Current;

// Use the generic ResolveView<TViewModel> method - this is AOT-safe!
var view = viewLocator.ResolveView<TViewModel>(x.contract) ?? viewLocator.ResolveView<TViewModel>()
var view = viewLocator.ResolveView<TViewModel>(route.contract) ?? viewLocator.ResolveView<TViewModel>()
?? throw new InvalidOperationException($"Couldn't find view for '{nameof(TViewModel)}'.");
view.ViewModel = x.viewModel as TViewModel;
view.ViewModel = route.viewModel as TViewModel;
Content = view;
}
}
Expand Down
29 changes: 29 additions & 0 deletions src/ReactiveUI.Maui/Internal/IMauiRoutedViewHost.cs
Original file line number Diff line number Diff line change
@@ -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

/// <summary>Supplies the host-specific operations used to initialize a WinUI routed host.</summary>
internal interface IMauiRoutedViewHost : IEnableLogger
{
/// <summary>Gets the router.</summary>
RoutingState Router { get; }

/// <summary>Gets or sets the view-contract observable.</summary>
IObservable<string?> ViewContractObservable { get; set; }

/// <summary>Gets the current view contract.</summary>
string? ViewContract { get; }

/// <summary>Stores the latest observed view contract without replacing its source observable.</summary>
/// <param name="contract">The observed contract.</param>
void SetObservedViewContract(string? contract);
}
38 changes: 18 additions & 20 deletions src/ReactiveUI.Maui/Internal/MauiReactiveHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -141,29 +141,27 @@ internal static IObservable<T> CreatePropertyValueObservable<T>(
}

/// <summary>Initializes a WinUI routed host and its view-contract subscriptions.</summary>
/// <param name="host">The host, logger, and view-contract setter.</param>
/// <param name="router">The router property metadata and accessor.</param>
/// <param name="viewContractObservable">The view-contract observable property metadata and accessor.</param>
/// <param name="getViewContract">Gets the current view contract.</param>
/// <param name="setViewContract">Stores the latest view contract.</param>
/// <param name="resolveView">Resolves a routed view model and contract.</param>
/// <typeparam name="THost">The routed-host type.</typeparam>
/// <param name="host">The host to initialize.</param>
/// <param name="routerProperty">The host's router dependency property.</param>
/// <param name="viewContractObservableProperty">The host's view-contract observable dependency property.</param>
/// <param name="subscriptions">Collects the host subscription.</param>
internal static void InitializeRoutedViewHost(
(FrameworkElement Source, IFullLogger Logger, Action<IObservable<string?>> SetViewContractObservable) host,
(string Name, DependencyProperty Property, Func<RoutingState> GetValue) router,
(string Name, DependencyProperty Property, Func<IObservable<string?>> GetValue) viewContractObservable,
Func<string?> getViewContract,
Action<string?> setViewContract,
Action<(IRoutableViewModel? viewModel, string? contract)> resolveView,
MultipleDisposable subscriptions)
/// <param name="resolveView">Resolves a routed view model and contract.</param>
internal static void InitializeRoutedViewHost<THost>(
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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<TargetFrameworks>$(ReactiveUIModernTargets)</TargetFrameworks>
<OutputType>Exe</OutputType>
<IsTestProject>true</IsTestProject>
<IsPackable>false</IsPackable>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<!--
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<TargetFrameworks>$(ReactiveUIMauiTestTargets)</TargetFrameworks>
<OutputType>Exe</OutputType>
<UseMaui>true</UseMaui>
<IsTestProject>true</IsTestProject>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<OutputType>Exe</OutputType>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsTestProject>true</IsTestProject>
<IsPackable>false</IsPackable>
</PropertyGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<TargetFrameworks>$(ReactiveUIMauiTestTargets)</TargetFrameworks>
<OutputType>Exe</OutputType>
<UseMaui>true</UseMaui>
<IsTestProject>true</IsTestProject>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
<!-- Target mobile platforms - Android and iOS (net9 mobile EOL 12 May 2026) -->
<TargetFrameworks>net10.0-android;net10.0-ios</TargetFrameworks>
<OutputType>Exe</OutputType>
<IsTestProject>true</IsTestProject>
<IsPackable>false</IsPackable>
<NoWarn>$(NoWarn);CS1591</NoWarn>
</PropertyGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
<TargetFrameworks>$(ReactiveUITestingTargets)</TargetFrameworks>
<OutputType>Exe</OutputType>
<RootNamespace>ReactiveUI.Tests</RootNamespace>
<IsTestProject>true</IsTestProject>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DynamicData"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
<PropertyGroup>
<TargetFrameworks>$(ReactiveUITestingTargets)</TargetFrameworks>
<OutputType>Exe</OutputType>
<IsTestProject>true</IsTestProject>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
<PropertyGroup>
<TargetFrameworks>$(ReactiveUITestingTargets)</TargetFrameworks>
<OutputType>Exe</OutputType>
<IsTestProject>true</IsTestProject>
<IsPackable>false</IsPackable>
<FodyTargetFramework>netstandard2.0</FodyTargetFramework>
<FodyTargetFramework Condition=" $(TargetFramework.StartsWith('net4')) ">$(TargetFramework)</FodyTargetFramework>
</PropertyGroup>
Expand Down
2 changes: 2 additions & 0 deletions src/tests/ReactiveUI.Tests/ReactiveUI.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
<PropertyGroup>
<TargetFrameworks>$(ReactiveUITestingTargets)</TargetFrameworks>
<OutputType>Exe</OutputType>
<IsTestProject>true</IsTestProject>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\ReactiveUI.Test.Utilities\ReactiveUI.Test.Utilities.csproj"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
<PropertyGroup>
<TargetFrameworks>$(ReactiveUITestingUITargets)</TargetFrameworks>
<OutputType>Exe</OutputType>
<IsTestProject>true</IsTestProject>
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
Expand Down
2 changes: 2 additions & 0 deletions src/tests/ReactiveUI.Wpf.Tests/ReactiveUI.Wpf.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
<TargetFrameworks>$(ReactiveUITestingUITargets)</TargetFrameworks>
<OutputType>Exe</OutputType>
<RootNamespace>ReactiveUI.Tests</RootNamespace>
<IsTestProject>true</IsTestProject>
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
Expand Down
Loading