From b5da0824cac3feaad454997ecd1d3d7c165cf081 Mon Sep 17 00:00:00 2001
From: Glenn Watson <5834289+glennawatson@users.noreply.github.com>
Date: Mon, 5 Jan 2026 10:59:14 +1100
Subject: [PATCH 01/25] breaking: Remove .NET Standard 2.0, modernize AOT
compatibility, and enhance test coverage
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
BREAKING CHANGES:
1. Removed .NET Standard 2.0 support - minimum target is now net6.0
2. Removed RxApp.cs - replaced with RxAppBuilder pattern
3. Removed PlatformRegistrationManager.cs - replaced with modern builder pattern
4. Solution file renamed: ReactiveUI.sln → reactiveui.slnx (SLNX format)
5. Removed polyfill attributes now built into modern .NET:
- CallerArgumentExpressionAttribute
- DoesNotReturnIfAttribute
- NotNullAttribute
- IsExternalInit
6. Removed platform-specific ComponentModelTypeConverter implementations
7. Removed legacy DefaultViewLocator.AOT.cs
## Migration Guide
### .NET Standard 2.0 Removal
Projects targeting .NET Standard 2.0 must upgrade to at least .NET 6.0. ReactiveUI
now requires modern .NET with built-in nullable reference types, init properties,
and AOT attributes.
**Before:**
- Supported: netstandard2.0, net462+, net6.0+
- Used polyfill attributes for modern C# features
**After:**
- Minimum: net6.0 for cross-platform, net462 for Windows-only legacy
- Uses built-in .NET attributes for nullable/AOT support
### RxApp Removal
The static RxApp class has been removed in favor of the builder pattern.
**Before:**
```csharp
RxApp.MainThreadScheduler.Schedule(() => { });
RxApp.TaskpoolScheduler.Schedule(() => { });
After:
// Use RxSchedulers for AOT-safe scheduler access
RxSchedulers.MainThreadScheduler.Schedule(() => { });
RxSchedulers.TaskpoolScheduler.Schedule(() => { });
// Or use builder pattern for initialization
var builder = RxAppBuilder.CreateReactiveUIBuilder(resolver)
.WithCoreServices()
.BuildApp();
Solution File Renamed
Before: src/ReactiveUI.sln (legacy text format)
After: src/reactiveui.slnx (XML-based format)
Impact:
- Update CI/CD scripts referencing ReactiveUI.sln → reactiveui.slnx
- Requires Visual Studio 2022 17.10+ or JetBrains Rider 2024.1+ for IDE support
- All dotnet CLI commands work identically (no syntax changes)
Major Enhancements
Test Coverage (80%+ achieved)
- Reorganized test projects for better coverage analysis
- Removed duplicate/obsolete test projects:
- ReactiveUI.AOTTests (consolidated into main tests)
- ReactiveUI.Builder.Tests (consolidated)
- ReactiveUI.Splat.Tests (functionality moved to core)
- ReactiveUI.Testing.Tests (consolidated)
- Fixed intermittent test failures with proper Locator scoping
- Added comprehensive MAUI activation tests
- Enhanced builder API tests with WithInstance coverage
AOT Compatibility Improvements
This branch addresses numerous AOT warnings and improves trimming compatibility:
- Removed reflection-heavy PlatformRegistrationManager
- Removed ComponentModelTypeConverter (used reflection)
- Streamlined view locator implementation (removed AOT-specific version)
- All polyfill attributes removed (used UnconditionalSuppressMessage)
- Improved DynamicallyAccessedMembers usage throughout codebase
Bug Fixes
- Nested property binding: Fixed redundant setter calls that caused performance
issues and unexpected behavior when binding to nested properties
- MAUI activation: Resolved activation lifecycle issues in MAUI controls
- Test flakiness: Fixed race conditions in Locator-dependent tests by
introducing LocatorScope pattern
API Enhancements
- Builder API: Added BuilderMixins with WithInstance pattern for better testability
- XML Documentation: Comprehensive docs added to:
- All public interfaces (IActivatableView, IViewFor, etc.)
- Suspension APIs (ISuspensionHost, ISuspensionDriver)
- Interaction APIs (IInteraction, IInteractionContext)
- View locator and activation APIs
- Usage Examples: Added code examples to public API documentation
Documentation Improvements
Added comprehensive educational documentation (CLAUDE.md, copilot-instructions.md):
SLNX Format Documentation
- What SLNX is (XML-based solution format, VS 2022 17.10+)
- Key differences from .sln (structured XML vs proprietary text)
- IDE compatibility requirements
- CLI usage (identical to .sln files)
Microsoft Testing Platform (MTP) Documentation
- What MTP is and why ReactiveUI uses it (modern test platform replacing VSTest)
- How MTP differs from VSTest (argument syntax, configuration)
- Configuration files explained (global.json, testconfig.json, Directory.Build.props)
- Best practices:
- Never use --no-build flag (causes stale binary issues)
- Correct argument placement: dotnet test flags BEFORE --, TUnit flags AFTER --
- How to see Console.WriteLine output (--output Detailed)
- Non-parallel test execution rationale
Command Reference Improvements
- Fixed coverage argument placement (--coverage goes BEFORE --)
- Reorganized command-line flags by tool (dotnet test vs TUnit)
- Removed contradictory examples (--no-build)
- All solution references updated to reactiveui.slnx
Dependency Updates
- TUnit 1.7.5 → latest (modern testing framework)
- Verify.TUnit 31.9.2 → 31.9.3 (snapshot testing)
- Microsoft.Extensions.DependencyModel → v10
- Roslynator.Analyzers → 4.15.0
- Syncfusion.MAUI.Toolkit → 1.0.8
Technical Details
Files Changed Statistics
- 819 files changed
- 59,545 insertions(+)
- 27,543 deletions(-)
Key Deletions
- src/ReactiveUI.sln → migrated to reactiveui.slnx
- src/ReactiveUI/RxApp.cs → replaced with RxAppBuilder pattern
- src/ReactiveUI/PlatformRegistrationManager.cs → replaced with builder pattern
- src/ReactiveUI/RegistrationNamespace.cs → obsolete with new registration approach
- src/ReactiveUI/Helpers/*.cs → polyfill attributes removed (4 files)
- src/ReactiveUI/IsExternalInit.cs → built into modern .NET
- src/ReactiveUI.*/GlobalUsings.cs → removed from Maui/WinUI (2 files)
- src/ReactiveUI/Platforms/*/ComponentModelTypeConverter.cs → reflection-based, removed (3 files)
- src/ReactiveUI/View/DefaultViewLocator.AOT.cs → consolidated with main implementation
- Multiple test projects consolidated (ReactiveUI.AOTTests, Builder.Tests, Splat.Tests, etc.)
Platform Support Matrix
Before:
- netstandard2.0 (cross-platform)
- net462, net472, net481 (Windows legacy)
- net6.0, net8.0, net9.0, net10.0 (modern)
After:
- net462, net472, net481 (Windows legacy - minimum for Windows-only projects)
- net6.0, net8.0, net9.0, net10.0 (cross-platform minimum)
- net8.0/9.0/10.0-windows10.0.19041.0 (Windows-specific features)
- iOS, tvOS, macOS, Android, MAUI targets unchanged
Co-authored-by: Christian Fischerauer christian.fischerauer@gmail.com
---
.claude/settings.local.json | 15 +
.editorconfig | 132 +
.github/copilot-instructions.md | 56 +-
CLAUDE.md | 90 +-
src/.claude/settings.local.json | 4 +-
src/Directory.Build.targets | 3 -
src/Directory.Packages.props | 2 +-
.../AndroidXReactiveUIBuilderExtensions.cs | 4 -
.../ControlFetcherMixin.cs | 6 +-
.../ReactiveAppCompatActivity.cs | 4 -
.../ReactiveAppCompatActivity{TViewModel}.cs | 4 -
.../ReactiveDialogFragment.cs | 8 -
.../ReactiveDialogFragment{TViewModel}.cs | 4 -
src/ReactiveUI.AndroidX/ReactiveFragment.cs | 4 -
.../ReactiveFragmentActivity.cs | 4 -
.../ReactiveFragmentActivity{TViewModel}.cs | 4 -
.../ReactiveFragment{TViewModel}.cs | 4 -
.../ReactivePreferenceFragment.cs | 8 -
.../ReactivePreferenceFragment{TViewModel}.cs | 8 -
.../ReactiveRecyclerViewViewHolder.cs | 6 +-
src/ReactiveUI.AndroidX/Registrations.cs | 10 +-
.../BlazorReactiveUIBuilderExtensions.cs | 4 -
.../Internal/ReactiveComponentHelpers.cs | 270 +
.../Internal/ReactiveComponentState.cs | 163 +
.../ReactiveComponentBase.cs | 118 +-
.../ReactiveInjectableComponentBase.cs | 123 +-
.../ReactiveLayoutComponentBase.cs | 122 +-
.../ReactiveOwningComponentBase.cs | 90 +-
src/ReactiveUI.Blazor/Registrations.cs | 46 +-
.../FollowObservableStateBehavior.cs | 16 -
src/ReactiveUI.Blend/ObservableTrigger.cs | 13 -
.../ReactiveUIBuilderDrawingExtensions.cs | 4 -
src/ReactiveUI.Drawing/Registrations.cs | 12 +-
.../ActivationForViewFetcher.cs | 19 +-
src/ReactiveUI.Maui/AutoSuspendHelper.cs | 20 +-
.../MauiReactiveUIBuilderExtensions.cs | 8 -
.../Common/AutoDataTemplateBindingHook.cs | 4 -
.../BooleanToVisibilityTypeConverter.cs | 66 +-
src/ReactiveUI.Maui/Common/ReactivePage.cs | 6 +-
src/ReactiveUI.Maui/Common/RoutedViewHost.cs | 62 +-
.../Common/RoutedViewHost{TViewModel}.cs | 191 +
.../Common/ViewModelViewHost.cs | 40 +-
.../Common/ViewModelViewHost{TViewModel}.cs | 197 +
.../VisibilityToBooleanTypeConverter.cs | 51 +
src/ReactiveUI.Maui/GlobalUsings.cs | 17 -
.../Internal/MauiReactiveHelpers.cs | 159 +
src/ReactiveUI.Maui/ReactiveCarouselView.cs | 6 +-
src/ReactiveUI.Maui/ReactiveContentPage.cs | 6 +-
src/ReactiveUI.Maui/ReactiveContentView.cs | 6 +-
src/ReactiveUI.Maui/ReactiveEntryCell.cs | 6 +-
src/ReactiveUI.Maui/ReactiveFlyoutPage.cs | 6 +-
src/ReactiveUI.Maui/ReactiveImageCell.cs | 6 +-
src/ReactiveUI.Maui/ReactiveImageItemView.cs | 42 +-
.../ReactiveMasterDetailPage.cs | 6 +-
src/ReactiveUI.Maui/ReactiveMultiPage.cs | 7 +-
src/ReactiveUI.Maui/ReactiveNavigationPage.cs | 6 +-
src/ReactiveUI.Maui/ReactiveShell.cs | 6 +-
src/ReactiveUI.Maui/ReactiveShellContent.cs | 6 +-
src/ReactiveUI.Maui/ReactiveSwitchCell.cs | 6 +-
src/ReactiveUI.Maui/ReactiveTabbedPage.cs | 6 +-
src/ReactiveUI.Maui/ReactiveTextCell.cs | 6 +-
src/ReactiveUI.Maui/ReactiveTextItemView.cs | 34 +-
src/ReactiveUI.Maui/ReactiveUI.Maui.csproj | 18 +
src/ReactiveUI.Maui/ReactiveViewCell.cs | 6 +-
src/ReactiveUI.Maui/Registrations.cs | 25 +-
src/ReactiveUI.Maui/RoutedViewHost.cs | 236 +-
.../RoutedViewHost{TViewModel}.cs | 332 +
src/ReactiveUI.Maui/ViewModelViewHost.cs | 49 +-
.../ViewModelViewHost{TViewModel}.cs | 195 +
.../DependencyObjectObservableForProperty.cs | 18 +-
.../WinUI/DispatcherQueueScheduler.cs | 6 +-
src/ReactiveUI.Testing/SchedulerExtensions.cs | 34 +-
.../WinUIReactiveUIBuilderExtensions.cs | 4 -
src/ReactiveUI.WinUI/GlobalUsings.cs | 15 -
src/ReactiveUI.WinUI/ReactiveUI.WinUI.csproj | 17 +
.../ActivationForViewFetcher.cs | 4 -
.../ContentControlBindingHook.cs | 4 -
.../CreatesWinformsCommandBinding.cs | 262 +-
.../PanelSetMethodBindingConverter.cs | 4 -
.../ReactiveUI.Winforms.csproj | 3 +
.../ReactiveUserControl.cs | 4 -
src/ReactiveUI.Winforms/Registrations.cs | 33 +-
src/ReactiveUI.Winforms/RoutedViewHost.cs | 10 +-
.../TableContentSetMethodBindingConverter.cs | 4 -
src/ReactiveUI.Winforms/ViewModelViewHost.cs | 20 +-
.../WinformsCreatesObservableForProperty.cs | 18 +-
.../ActivationForViewFetcher.cs | 4 -
src/ReactiveUI.Wpf/AutoSuspendHelper.cs | 24 +-
.../Binding/ValidationBindingMixins.cs | 4 -
.../Binding/ValidationBindingWpf.cs | 4 -
.../Common/AutoDataTemplateBindingHook.cs | 4 -
.../BooleanToVisibilityTypeConverter.cs | 66 +-
src/ReactiveUI.Wpf/Common/RoutedViewHost.cs | 10 +-
.../Common/ViewModelViewHost.cs | 4 -
.../VisibilityToBooleanTypeConverter.cs | 57 +
.../DependencyObjectObservableForProperty.cs | 14 +-
src/ReactiveUI.Wpf/Registrations.cs | 34 +-
.../Rx/Linq/DispatcherObservable.cs | 4 -
src/ReactiveUI.sln | 408 -
src/ReactiveUI.sln.DotSettings | 8 -
src/ReactiveUI.v3.ncrunchsolution | 6 -
.../Activation/CanActivateViewFetcher.cs | 4 -
.../Activation/IActivationForViewFetcher.cs | 4 -
src/ReactiveUI/Activation/ViewForMixins.cs | 34 +-
.../Bindings/BindingTypeConverter.cs | 71 +
.../Bindings/BindingTypeConverterDispatch.cs | 145 +
.../Bindings/Command/CommandBinder.cs | 128 +-
.../Command/CommandBinderImplementation.cs | 284 +-
.../CommandBinderImplementationMixins.cs | 58 +-
.../Bindings/Command/CreatesCommandBinding.cs | 79 +-
...reatesCommandBindingViaCommandParameter.cs | 284 +-
.../Command/CreatesCommandBindingViaEvent.cs | 308 +-
.../Command/ICommandBinderImplementation.cs | 28 +-
.../Converter/BooleanToStringTypeConverter.cs | 24 +
.../Converter/ByteToStringTypeConverter.cs | 53 +-
.../DateOnlyToStringTypeConverter.cs | 26 +
.../DateTimeOffsetToStringTypeConverter.cs | 24 +
.../DateTimeToStringTypeConverter.cs | 24 +
.../Converter/DecimalToStringTypeConverter.cs | 59 +-
.../Converter/DoubleToStringTypeConverter.cs | 59 +-
.../Converter/EqualityTypeConverter.cs | 127 +-
.../Converter/GuidToStringTypeConverter.cs | 24 +
.../Converter/IntegerToStringTypeConverter.cs | 53 +-
.../Converter/LongToStringTypeConverter.cs | 53 +-
.../NullableBooleanToStringTypeConverter.cs | 30 +
.../NullableByteToStringTypeConverter.cs | 65 +-
.../NullableDateOnlyToStringTypeConverter.cs | 32 +
...ableDateTimeOffsetToStringTypeConverter.cs | 30 +
.../NullableDateTimeToStringTypeConverter.cs | 30 +
.../NullableDecimalToStringTypeConverter.cs | 71 +-
.../NullableDoubleToStringTypeConverter.cs | 71 +-
.../NullableGuidToStringTypeConverter.cs | 30 +
.../NullableIntegerToStringTypeConverter.cs | 65 +-
.../NullableLongToStringTypeConverter.cs | 65 +-
.../NullableShortToStringTypeConverter.cs | 65 +-
.../NullableSingleToStringTypeConverter.cs | 71 +-
.../NullableTimeOnlyToStringTypeConverter.cs | 32 +
.../NullableTimeSpanToStringTypeConverter.cs | 30 +
.../Converter/ShortToStringTypeConverter.cs | 53 +-
.../Converter/SingleToStringTypeConverter.cs | 59 +-
.../Bindings/Converter/StringConverter.cs | 40 +-
.../Converter/StringToBooleanTypeConverter.cs | 29 +
.../Converter/StringToByteTypeConverter.cs | 29 +
.../StringToDateOnlyTypeConverter.cs | 31 +
.../StringToDateTimeOffsetTypeConverter.cs | 29 +
.../StringToDateTimeTypeConverter.cs | 29 +
.../Converter/StringToDecimalTypeConverter.cs | 29 +
.../Converter/StringToDoubleTypeConverter.cs | 29 +
.../Converter/StringToGuidTypeConverter.cs | 29 +
.../Converter/StringToIntegerTypeConverter.cs | 29 +
.../Converter/StringToLongTypeConverter.cs | 29 +
.../StringToNullableBooleanTypeConverter.cs | 36 +
.../StringToNullableByteTypeConverter.cs | 36 +
.../StringToNullableDateOnlyTypeConverter.cs | 38 +
...ngToNullableDateTimeOffsetTypeConverter.cs | 36 +
.../StringToNullableDateTimeTypeConverter.cs | 36 +
.../StringToNullableDecimalTypeConverter.cs | 36 +
.../StringToNullableDoubleTypeConverter.cs | 36 +
.../StringToNullableGuidTypeConverter.cs | 36 +
.../StringToNullableIntegerTypeConverter.cs | 36 +
.../StringToNullableLongTypeConverter.cs | 36 +
.../StringToNullableShortTypeConverter.cs | 36 +
.../StringToNullableSingleTypeConverter.cs | 36 +
.../StringToNullableTimeOnlyTypeConverter.cs | 38 +
.../StringToNullableTimeSpanTypeConverter.cs | 36 +
.../Converter/StringToShortTypeConverter.cs | 29 +
.../Converter/StringToSingleTypeConverter.cs | 29 +
.../StringToTimeOnlyTypeConverter.cs | 31 +
.../StringToTimeSpanTypeConverter.cs | 29 +
.../Converter/StringToUriTypeConverter.cs | 29 +
.../TimeOnlyToStringTypeConverter.cs | 26 +
.../TimeSpanToStringTypeConverter.cs | 24 +
.../Converter/UriToStringTypeConverter.cs | 30 +
.../Bindings/IBindingFallbackConverter.cs | 87 +
.../Bindings/IBindingTypeConverter.cs | 28 +-
.../IBindingTypeConverter{TFrom,TTo}.cs | 48 +
.../Bindings/ISetMethodBindingConverter.cs | 4 -
.../IInteractionBinderImplementation.cs | 10 +-
.../InteractionBinderImplementation.cs | 10 +-
.../Interaction/InteractionBindingMixins.cs | 12 +-
.../Property/IPropertyBinderImplementation.cs | 20 -
.../Property/PropertyBinderImplementation.cs | 881 +-
.../Property/PropertyBindingMixins.cs | 31 +-
src/ReactiveUI/Builder/IReactiveUIBuilder.cs | 48 +-
src/ReactiveUI/Builder/ReactiveUIBuilder.cs | 158 +-
src/ReactiveUI/Builder/RxAppBuilder.cs | 44 +
.../Expression/ExpressionRewriter.cs | 256 +-
src/ReactiveUI/Expression/Reflection.cs | 906 +-
.../Helpers/DoesNotReturnIfAttribute.cs | 37 -
.../Interfaces/ICreatesCommandBinding.cs | 107 +-
.../ICreatesObservableForProperty.cs | 90 +-
.../Interfaces/IHandleObservableErrors.cs | 2 +-
.../Interfaces/IPropertyBindingHook.cs | 4 -
.../IReactiveNotifyPropertyChanged.cs | 4 -
src/ReactiveUI/Interfaces/IRegistrar.cs | 44 +
.../Interfaces/ISuspensionDriver.cs | 100 +-
src/ReactiveUI/Interfaces/ISuspensionHost.cs | 4 +-
.../Interfaces/ISuspensionHost{TAppState}.cs | 58 +
src/ReactiveUI/Interfaces/IViewLocator.cs | 45 +-
src/ReactiveUI/Interfaces/IViewModule.cs | 39 +
.../Interfaces/IWantsToRegisterStuff.cs | 43 +-
src/ReactiveUI/IsExternalInit.cs | 13 -
src/ReactiveUI/Mixins/AutoPersistHelper.cs | 915 +-
src/ReactiveUI/Mixins/BuilderMixins.cs | 111 +-
.../Mixins/DependencyResolverMixins.cs | 138 +-
src/ReactiveUI/Mixins/ExpressionMixins.cs | 19 +-
.../MutableDependencyResolverAOTExtensions.cs | 35 +-
.../MutableDependencyResolverExtensions.cs | 35 +-
.../Mixins/ObservableLoggingMixin.cs | 7 +
src/ReactiveUI/Mixins/ObservableMixins.cs | 7 +
src/ReactiveUI/Mixins/ObservedChangedMixin.cs | 36 +-
.../ReactiveNotifyPropertyChangedMixin.cs | 109 +-
.../INPCObservableForProperty.cs | 10 +-
.../IROObservableForProperty.cs | 95 +-
.../OAPHCreationHelperMixin.cs | 60 -
.../ObservableAsPropertyHelper.cs | 18 +-
.../POCOObservableForProperty.cs | 98 +-
src/ReactiveUI/ObservableFuncMixins.cs | 7 +-
src/ReactiveUI/PlatformRegistrationManager.cs | 28 -
.../android/AndroidCommandBinders.cs | 36 +-
.../android/AndroidObservableForWidgets.cs | 411 +-
.../Platforms/android/AutoSuspendHelper.cs | 20 +-
.../android/BundleSuspensionDriver.cs | 128 +-
.../Platforms/android/ControlFetcherMixin.cs | 569 +-
.../android/FlexibleCommandBinder.cs | 309 +-
.../Platforms/android/LayoutViewHost.cs | 178 +-
.../android/PlatformRegistrations.cs | 20 +-
.../Platforms/android/ReactiveActivity.cs | 4 -
.../android/ReactiveActivity{TViewModel}.cs | 4 -
.../Platforms/android/ReactiveFragment.cs | 4 -
.../android/ReactiveFragment{TViewModel}.cs | 4 -
.../Platforms/android/ReactiveViewHost.cs | 207 +-
.../AppSupportJsonSuspensionDriver.cs | 193 +-
.../Converters/DateTimeNSDateConverter.cs | 42 -
.../DateTimeOffsetToNSDateConverter.cs | 27 +
.../Converters/DateTimeToNSDateConverter.cs | 27 +
.../Converters/NSDateToDateTimeConverter.cs | 33 +
.../NSDateToDateTimeOffsetConverter.cs | 33 +
.../NSDateToNullableDateTimeConverter.cs | 33 +
...NSDateToNullableDateTimeOffsetConverter.cs | 33 +
...NullableDateTimeOffsetToNSDateConverter.cs | 33 +
.../NullableDateTimeToNSDateConverter.cs | 33 +
.../apple-common/KVOObservableForProperty.cs | 264 +-
.../apple-common/ObservableForPropertyBase.cs | 361 +-
.../Platforms/apple-common/ReactiveControl.cs | 8 -
.../apple-common/ReactiveImageView.cs | 8 -
.../ReactiveSplitViewController.cs | 8 -
.../Platforms/apple-common/ReactiveView.cs | 8 -
.../apple-common/ReactiveViewController.cs | 8 -
.../apple-common/TargetActionCommandBinder.cs | 371 +-
.../apple-common/ViewModelViewHost.cs | 234 +-
.../Platforms/ios/UIKitCommandBinders.cs | 60 +-
.../ios/UIKitObservableForProperty.cs | 115 +-
.../Platforms/mac/AutoSuspendHelper.cs | 248 +-
.../Platforms/mac/PlatformRegistrations.cs | 36 +-
.../Platforms/mac/ReactiveWindowController.cs | 4 -
.../ComponentModelFallbackConverter.cs | 155 +
.../ComponentModelTypeConverter.cs | 79 -
.../net/ComponentModelFallbackConverter.cs | 151 +
.../net/ComponentModelTypeConverter.cs | 89 -
.../Platforms/net/PlatformRegistrations.cs | 12 +-
.../netstandard2.0/PlatformRegistrations.cs | 8 +-
.../Platforms/tizen/PlatformRegistrations.cs | 12 +-
.../Platforms/tvos/UIKitCommandBinders.cs | 73 +-
.../tvos/UIKitObservableForProperty.cs | 93 +-
.../uikit-common/AutoSuspendHelper.cs | 182 +-
.../uikit-common/CommonReactiveSource.cs | 744 +-
.../uikit-common/FlexibleCommandBinder.cs | 584 +-
.../uikit-common/IUICollViewAdapter.cs | 4 -
.../uikit-common/PlatformRegistrations.cs | 32 +-
.../ReactiveCollectionReusableView.cs | 8 -
.../uikit-common/ReactiveCollectionView.cs | 8 -
.../ReactiveCollectionViewCell.cs | 8 -
.../ReactiveCollectionViewController.cs | 8 -
.../ReactiveCollectionViewSource.cs | 16 -
.../ReactiveCollectionViewSourceExtensions.cs | 18 +-
.../ReactiveNavigationController.cs | 8 -
.../ReactivePageViewController.cs | 8 -
.../uikit-common/ReactiveTabBarController.cs | 8 -
.../uikit-common/ReactiveTableView.cs | 8 -
.../uikit-common/ReactiveTableViewCell.cs | 8 -
.../ReactiveTableViewController.cs | 8 -
.../uikit-common/ReactiveTableViewSource.cs | 16 -
.../ReactiveTableViewSourceExtensions.cs | 18 +-
.../Platforms/uikit-common/RoutedViewHost.cs | 15 +-
.../uikit-common/UICollectionViewAdapter.cs | 4 -
.../uikit-common/UITableViewAdapter.cs | 4 -
.../CallerArgumentExpressionAttribute.cs | 3 +
.../Polyfills/DoesNotReturnIfAttribute.cs | 44 +
.../DynamicallyAccessedMemberTypes.cs | 106 +
.../DynamicallyAccessedMembersAttribute.cs | 54 +
src/ReactiveUI/Polyfills/IsExternalInit.cs | 27 +
.../Polyfills/MaybeNullWhenAttribute.cs | 45 +
.../Polyfills/MemberNotNullAttribute.cs | 50 +
.../NotNullAttribute.cs | 10 +-
.../Polyfills/NotNullWhenAttribute.cs | 39 +
.../Polyfills/RequiresDynamicCodeAttribute.cs | 57 +
.../RequiresUnreferencedCodeAttribute.cs | 52 +
.../UnconditionalSuppressMessageAttribute.cs | 74 +
.../CombinedReactiveCommand.cs | 8 +-
.../ReactiveCommand/ReactiveCommand.cs | 106 +-
.../ReactiveCommand/ReactiveCommandMixins.cs | 10 +-
.../IReactiveObjectExtensions.cs | 2 +-
.../ReactiveObject/ReactiveObject.cs | 4 -
.../ReactiveObject/ReactiveRecord.cs | 16 -
.../ReactiveProperty/IReactiveProperty.cs | 4 -
.../ReactiveProperty/ReactiveProperty.cs | 187 +-
.../ReactivePropertyMixins.cs | 13 +-
.../DependencyResolverRegistrar.cs | 64 +
src/ReactiveUI/Registration/Registrations.cs | 154 +-
src/ReactiveUI/RegistrationNamespace.cs | 65 -
src/ReactiveUI/Routing/RoutingState.cs | 29 +-
src/ReactiveUI/RxApp.cs | 257 -
src/ReactiveUI/RxCacheSize.cs | 76 +
src/ReactiveUI/RxSchedulers.cs | 38 +-
src/ReactiveUI/RxState.cs | 74 +
src/ReactiveUI/RxSuspension.cs | 62 +
.../Suspension/DummySuspensionDriver.cs | 72 +-
src/ReactiveUI/Suspension/SuspensionHost.cs | 8 +-
.../Suspension/SuspensionHostExtensions.cs | 176 +-
.../Suspension/SuspensionHost{TAppState}.cs | 338 +
src/ReactiveUI/VariadicTemplates.cs | 10853 ++++++++--------
src/ReactiveUI/VariadicTemplates.tt | 88 +-
src/ReactiveUI/View/DefaultViewLocator.AOT.cs | 69 -
src/ReactiveUI/View/DefaultViewLocator.cs | 297 +-
src/ReactiveUI/View/ViewMappingBuilder.cs | 76 +
src/RxUI.DotSettings | 30 -
.../ReactiveUI.Builder.WpfApp/App.xaml.cs | 22 +-
.../Services/ChatNetworkService.cs | 4 +-
.../Services/FileJsonSuspensionDriver.cs | 46 +-
.../ViewModels/LobbyViewModel.cs | 4 +-
src/reactiveui.slnx | 47 +
.../AOTCompatibilityTests.cs | 12 -
.../ReactiveUI.AOTTests/AdvancedAOTTests.cs | 8 -
.../ReactiveUI.AOTTests/AssemblyHooks.cs | 6 +
.../ComprehensiveAOTMarkupTests.cs | 18 -
.../ComprehensiveAOTTests.cs | 10 -
.../FinalAOTValidationTests.cs | 10 -
.../ReactiveUI.AOTTests/TestReactiveObject.cs | 4 -
.../ViewLocatorAOTMappingTests.cs | 10 +-
.../BlazorReactiveUIBuilderExtensionsTests.cs | 8 +
.../ActivationForViewFetcherTests.cs | 4 +-
.../BuilderInstanceMixinsHappyPathTests.cs | 12 +
.../Mixins/BuilderMixinsTests.cs | 14 +-
.../ReactiveUIBuilderBlockingTests.cs | 4 +-
.../ReactiveUIBuilderRxAppMigrationTests.cs | 254 +
.../AutoSuspendHelperTest.cs | 18 +-
.../BooleanToVisibilityTypeConverterTest.cs | 37 +-
.../Builder/MauiDispatcherSchedulerTest.cs | 14 +-
.../ViewModelViewHostTest.cs | 5 +-
.../AwaiterTest.cs | 6 +-
.../CommandBinding/CommandBindingTests.cs | 25 +-
.../Commands/ReactiveCommandTest.cs | 7 +-
.../Locator/DefaultViewLocatorTests.cs | 100 +-
.../Locator/Mocks/RoutableFooCustomView.cs | 6 +-
.../Locator/ViewLocatorTest.cs | 20 +-
.../MessageBusTest.cs | 8 +-
...utableDependencyResolverExtensionsTests.cs | 4 +-
.../PlatformRegistrationManagerTest.cs | 57 -
...pprovalTests.Blend.DotNet10_0.verified.txt | 6 +-
.../Platforms/windows-xaml/MockWindow.xaml | 2 +-
.../windows-xaml/RoutedViewHostTests.cs | 14 +-
.../RxAppDependencyObjectTests.cs | 9 +-
.../Utilities/DispatcherSchedulerScope.cs | 22 +-
.../windows-xaml/XamlViewCommandTests.cs | 2 +-
.../XamlViewDependencyResolverTests.cs | 4 +-
...rovalTests.Winforms.DotNet8_0.verified.txt | 60 +-
.../Platforms/winforms/CommandBindingTests.cs | 12 +-
.../winforms/DefaultPropertyBindingTests.cs | 12 +-
.../winforms/Mocks/FakeViewLocator.cs | 24 +-
.../winforms/Mocks/TestForm.Designer.cs | 1 -
.../Mocks/TestFormNotCanActivate.Designer.cs | 1 -
.../WinFormsViewDependencyResolverTests.cs | 4 +-
...piApprovalTests.Wpf.DotNet8_0.verified.txt | 36 +-
.../BooleanToVisibilityTypeConverterTest.cs | 56 +-
.../Platforms/wpf/DefaultViewLocatorTests.cs | 27 +-
.../CanExecuteExecutingView.xaml | 2 +-
.../wpf/Mocks/CommandBindingViewModel.cs | 2 +-
.../Mocks/TransitionMock/TCMockWindow.xaml | 2 +-
.../Platforms/wpf/WpfActiveContentTests.cs | 4 +-
.../WpfCommandBindingImplementationTests.cs | 55 +-
.../wpf/WpfViewDependencyResolverTests.cs | 4 +-
.../PropertyBinderImplementationTests.cs | 16 +-
.../RandomTests.cs | 103 +-
.../ReactiveUI.NonParallel.Tests.csproj | 2 +
.../PocoObservableForPropertyTests.cs | 27 +-
.../ReactiveUI.NonParallel.Tests/RxAppTest.cs | 6 +-
.../SuspensionHostExtensionsTests.cs | 37 +-
.../SplatAdapterTests.cs | 26 +-
.../SchedulerExtensionTests.cs | 52 +-
...alTests.ReactiveUI.DotNet10_0.verified.txt | 1721 +--
...valTests.ReactiveUI.DotNet8_0.verified.txt | 1721 +--
...valTests.ReactiveUI.DotNet9_0.verified.txt | 1721 +--
...rovalTests.Testing.DotNet10_0.verified.txt | 12 +-
...provalTests.Testing.DotNet9_0.verified.txt | 12 +-
.../ReactiveUI.Tests/API/ApiApprovalTests.cs | 2 +-
.../Activation/ActivatingViewTests.cs | 42 +-
.../ComponentModelTypeConverterTest.cs | 178 -
.../BindingTypeConvertersTest.cs | 73 -
.../BindingTypeConvertersUnitTests.cs | 131 +-
...sCommandBindingViaCommandParameterTests.cs | 12 +-
.../CreatesCommandBindingViaEventTests.cs | 12 +-
.../PropertyBindingMixinsTests.cs | 9 +-
.../ByteToStringTypeConverterTests.cs | 80 +-
.../DecimalToStringTypeConverterTests.cs | 75 +-
.../DoubleToStringTypeConverterTests.cs | 84 +-
.../EqualityTypeConverterTests.cs | 148 +-
.../IntegerToStringTypeConverterTests.cs | 75 +-
.../LongToStringTypeConverterTests.cs | 70 +-
.../NullableByteToStringTypeConverterTests.cs | 93 +-
...llableDecimalToStringTypeConverterTests.cs | 78 +-
...ullableDoubleToStringTypeConverterTests.cs | 87 +-
...llableIntegerToStringTypeConverterTests.cs | 73 +-
.../NullableLongToStringTypeConverterTests.cs | 73 +-
...NullableShortToStringTypeConverterTests.cs | 73 +-
...ullableSingleToStringTypeConverterTests.cs | 76 +-
.../ShortToStringTypeConverterTests.cs | 70 +-
.../SingleToStringTypeConverterTests.cs | 73 +-
.../TypeConverters/StringConverterTests.cs | 85 +-
.../StringToByteTypeConverterTests.cs | 71 +
.../StringToDecimalTypeConverterTests.cs | 51 +
.../StringToDoubleTypeConverterTests.cs | 62 +
.../StringToIntegerTypeConverterTests.cs | 61 +
.../StringToLongTypeConverterTests.cs | 61 +
.../StringToNullableByteTypeConverterTests.cs | 71 +
...ringToNullableDecimalTypeConverterTests.cs | 51 +
...tringToNullableDoubleTypeConverterTests.cs | 62 +
...ringToNullableIntegerTypeConverterTests.cs | 61 +
.../StringToNullableLongTypeConverterTests.cs | 61 +
...StringToNullableShortTypeConverterTests.cs | 61 +
...tringToNullableSingleTypeConverterTests.cs | 52 +
.../StringToShortTypeConverterTests.cs | 61 +
.../StringToSingleTypeConverterTests.cs | 51 +
.../Commands/CreatesCommandBindingTests.cs | 6 +-
.../StaticState/LocatorScope.cs | 10 +-
...cope.cs => RxSchedulersSchedulersScope.cs} | 14 +-
src/tests/ReactiveUI.Tests/Mocks/Foo.cs | 2 +-
.../PlatformRegistrationsTest.cs | 60 -
.../Resolvers/DependencyResolverTests.cs | 170 -
.../ReactiveUI.Tests/RxAppBuilderTest.cs | 91 +
440 files changed, 24457 insertions(+), 18380 deletions(-)
create mode 100644 .claude/settings.local.json
create mode 100644 src/ReactiveUI.Blazor/Internal/ReactiveComponentHelpers.cs
create mode 100644 src/ReactiveUI.Blazor/Internal/ReactiveComponentState.cs
create mode 100644 src/ReactiveUI.Maui/Common/RoutedViewHost{TViewModel}.cs
create mode 100644 src/ReactiveUI.Maui/Common/ViewModelViewHost{TViewModel}.cs
create mode 100644 src/ReactiveUI.Maui/Common/VisibilityToBooleanTypeConverter.cs
delete mode 100644 src/ReactiveUI.Maui/GlobalUsings.cs
create mode 100644 src/ReactiveUI.Maui/Internal/MauiReactiveHelpers.cs
create mode 100644 src/ReactiveUI.Maui/RoutedViewHost{TViewModel}.cs
create mode 100644 src/ReactiveUI.Maui/ViewModelViewHost{TViewModel}.cs
delete mode 100644 src/ReactiveUI.WinUI/GlobalUsings.cs
create mode 100644 src/ReactiveUI.Wpf/Common/VisibilityToBooleanTypeConverter.cs
delete mode 100644 src/ReactiveUI.sln
delete mode 100644 src/ReactiveUI.sln.DotSettings
delete mode 100644 src/ReactiveUI.v3.ncrunchsolution
create mode 100644 src/ReactiveUI/Bindings/BindingTypeConverter.cs
create mode 100644 src/ReactiveUI/Bindings/BindingTypeConverterDispatch.cs
create mode 100644 src/ReactiveUI/Bindings/Converter/BooleanToStringTypeConverter.cs
create mode 100644 src/ReactiveUI/Bindings/Converter/DateOnlyToStringTypeConverter.cs
create mode 100644 src/ReactiveUI/Bindings/Converter/DateTimeOffsetToStringTypeConverter.cs
create mode 100644 src/ReactiveUI/Bindings/Converter/DateTimeToStringTypeConverter.cs
create mode 100644 src/ReactiveUI/Bindings/Converter/GuidToStringTypeConverter.cs
create mode 100644 src/ReactiveUI/Bindings/Converter/NullableBooleanToStringTypeConverter.cs
create mode 100644 src/ReactiveUI/Bindings/Converter/NullableDateOnlyToStringTypeConverter.cs
create mode 100644 src/ReactiveUI/Bindings/Converter/NullableDateTimeOffsetToStringTypeConverter.cs
create mode 100644 src/ReactiveUI/Bindings/Converter/NullableDateTimeToStringTypeConverter.cs
create mode 100644 src/ReactiveUI/Bindings/Converter/NullableGuidToStringTypeConverter.cs
create mode 100644 src/ReactiveUI/Bindings/Converter/NullableTimeOnlyToStringTypeConverter.cs
create mode 100644 src/ReactiveUI/Bindings/Converter/NullableTimeSpanToStringTypeConverter.cs
create mode 100644 src/ReactiveUI/Bindings/Converter/StringToBooleanTypeConverter.cs
create mode 100644 src/ReactiveUI/Bindings/Converter/StringToByteTypeConverter.cs
create mode 100644 src/ReactiveUI/Bindings/Converter/StringToDateOnlyTypeConverter.cs
create mode 100644 src/ReactiveUI/Bindings/Converter/StringToDateTimeOffsetTypeConverter.cs
create mode 100644 src/ReactiveUI/Bindings/Converter/StringToDateTimeTypeConverter.cs
create mode 100644 src/ReactiveUI/Bindings/Converter/StringToDecimalTypeConverter.cs
create mode 100644 src/ReactiveUI/Bindings/Converter/StringToDoubleTypeConverter.cs
create mode 100644 src/ReactiveUI/Bindings/Converter/StringToGuidTypeConverter.cs
create mode 100644 src/ReactiveUI/Bindings/Converter/StringToIntegerTypeConverter.cs
create mode 100644 src/ReactiveUI/Bindings/Converter/StringToLongTypeConverter.cs
create mode 100644 src/ReactiveUI/Bindings/Converter/StringToNullableBooleanTypeConverter.cs
create mode 100644 src/ReactiveUI/Bindings/Converter/StringToNullableByteTypeConverter.cs
create mode 100644 src/ReactiveUI/Bindings/Converter/StringToNullableDateOnlyTypeConverter.cs
create mode 100644 src/ReactiveUI/Bindings/Converter/StringToNullableDateTimeOffsetTypeConverter.cs
create mode 100644 src/ReactiveUI/Bindings/Converter/StringToNullableDateTimeTypeConverter.cs
create mode 100644 src/ReactiveUI/Bindings/Converter/StringToNullableDecimalTypeConverter.cs
create mode 100644 src/ReactiveUI/Bindings/Converter/StringToNullableDoubleTypeConverter.cs
create mode 100644 src/ReactiveUI/Bindings/Converter/StringToNullableGuidTypeConverter.cs
create mode 100644 src/ReactiveUI/Bindings/Converter/StringToNullableIntegerTypeConverter.cs
create mode 100644 src/ReactiveUI/Bindings/Converter/StringToNullableLongTypeConverter.cs
create mode 100644 src/ReactiveUI/Bindings/Converter/StringToNullableShortTypeConverter.cs
create mode 100644 src/ReactiveUI/Bindings/Converter/StringToNullableSingleTypeConverter.cs
create mode 100644 src/ReactiveUI/Bindings/Converter/StringToNullableTimeOnlyTypeConverter.cs
create mode 100644 src/ReactiveUI/Bindings/Converter/StringToNullableTimeSpanTypeConverter.cs
create mode 100644 src/ReactiveUI/Bindings/Converter/StringToShortTypeConverter.cs
create mode 100644 src/ReactiveUI/Bindings/Converter/StringToSingleTypeConverter.cs
create mode 100644 src/ReactiveUI/Bindings/Converter/StringToTimeOnlyTypeConverter.cs
create mode 100644 src/ReactiveUI/Bindings/Converter/StringToTimeSpanTypeConverter.cs
create mode 100644 src/ReactiveUI/Bindings/Converter/StringToUriTypeConverter.cs
create mode 100644 src/ReactiveUI/Bindings/Converter/TimeOnlyToStringTypeConverter.cs
create mode 100644 src/ReactiveUI/Bindings/Converter/TimeSpanToStringTypeConverter.cs
create mode 100644 src/ReactiveUI/Bindings/Converter/UriToStringTypeConverter.cs
create mode 100644 src/ReactiveUI/Bindings/IBindingFallbackConverter.cs
create mode 100644 src/ReactiveUI/Bindings/IBindingTypeConverter{TFrom,TTo}.cs
delete mode 100644 src/ReactiveUI/Helpers/DoesNotReturnIfAttribute.cs
create mode 100644 src/ReactiveUI/Interfaces/IRegistrar.cs
create mode 100644 src/ReactiveUI/Interfaces/ISuspensionHost{TAppState}.cs
create mode 100644 src/ReactiveUI/Interfaces/IViewModule.cs
delete mode 100644 src/ReactiveUI/IsExternalInit.cs
delete mode 100644 src/ReactiveUI/PlatformRegistrationManager.cs
delete mode 100644 src/ReactiveUI/Platforms/apple-common/Converters/DateTimeNSDateConverter.cs
create mode 100644 src/ReactiveUI/Platforms/apple-common/Converters/DateTimeOffsetToNSDateConverter.cs
create mode 100644 src/ReactiveUI/Platforms/apple-common/Converters/DateTimeToNSDateConverter.cs
create mode 100644 src/ReactiveUI/Platforms/apple-common/Converters/NSDateToDateTimeConverter.cs
create mode 100644 src/ReactiveUI/Platforms/apple-common/Converters/NSDateToDateTimeOffsetConverter.cs
create mode 100644 src/ReactiveUI/Platforms/apple-common/Converters/NSDateToNullableDateTimeConverter.cs
create mode 100644 src/ReactiveUI/Platforms/apple-common/Converters/NSDateToNullableDateTimeOffsetConverter.cs
create mode 100644 src/ReactiveUI/Platforms/apple-common/Converters/NullableDateTimeOffsetToNSDateConverter.cs
create mode 100644 src/ReactiveUI/Platforms/apple-common/Converters/NullableDateTimeToNSDateConverter.cs
create mode 100644 src/ReactiveUI/Platforms/mobile-common/ComponentModelFallbackConverter.cs
delete mode 100644 src/ReactiveUI/Platforms/mobile-common/ComponentModelTypeConverter.cs
create mode 100644 src/ReactiveUI/Platforms/net/ComponentModelFallbackConverter.cs
delete mode 100644 src/ReactiveUI/Platforms/net/ComponentModelTypeConverter.cs
rename src/ReactiveUI/{Helpers => Polyfills}/CallerArgumentExpressionAttribute.cs (89%)
create mode 100644 src/ReactiveUI/Polyfills/DoesNotReturnIfAttribute.cs
create mode 100644 src/ReactiveUI/Polyfills/DynamicallyAccessedMemberTypes.cs
create mode 100644 src/ReactiveUI/Polyfills/DynamicallyAccessedMembersAttribute.cs
create mode 100644 src/ReactiveUI/Polyfills/IsExternalInit.cs
create mode 100644 src/ReactiveUI/Polyfills/MaybeNullWhenAttribute.cs
create mode 100644 src/ReactiveUI/Polyfills/MemberNotNullAttribute.cs
rename src/ReactiveUI/{Helpers => Polyfills}/NotNullAttribute.cs (83%)
create mode 100644 src/ReactiveUI/Polyfills/NotNullWhenAttribute.cs
create mode 100644 src/ReactiveUI/Polyfills/RequiresDynamicCodeAttribute.cs
create mode 100644 src/ReactiveUI/Polyfills/RequiresUnreferencedCodeAttribute.cs
create mode 100644 src/ReactiveUI/Polyfills/UnconditionalSuppressMessageAttribute.cs
create mode 100644 src/ReactiveUI/Registration/DependencyResolverRegistrar.cs
delete mode 100644 src/ReactiveUI/RegistrationNamespace.cs
delete mode 100644 src/ReactiveUI/RxApp.cs
create mode 100644 src/ReactiveUI/RxCacheSize.cs
create mode 100644 src/ReactiveUI/RxState.cs
create mode 100644 src/ReactiveUI/RxSuspension.cs
create mode 100644 src/ReactiveUI/Suspension/SuspensionHost{TAppState}.cs
delete mode 100644 src/ReactiveUI/View/DefaultViewLocator.AOT.cs
create mode 100644 src/ReactiveUI/View/ViewMappingBuilder.cs
delete mode 100644 src/RxUI.DotSettings
create mode 100644 src/reactiveui.slnx
create mode 100644 src/tests/ReactiveUI.Builder.Tests/ReactiveUIBuilderRxAppMigrationTests.cs
delete mode 100644 src/tests/ReactiveUI.NonParallel.Tests/PlatformRegistrationManagerTest.cs
delete mode 100644 src/tests/ReactiveUI.Tests/Binding/ComponentModelTypeConverterTest.cs
delete mode 100644 src/tests/ReactiveUI.Tests/BindingTypeConvertersTest.cs
create mode 100644 src/tests/ReactiveUI.Tests/Bindings/TypeConverters/StringToByteTypeConverterTests.cs
create mode 100644 src/tests/ReactiveUI.Tests/Bindings/TypeConverters/StringToDecimalTypeConverterTests.cs
create mode 100644 src/tests/ReactiveUI.Tests/Bindings/TypeConverters/StringToDoubleTypeConverterTests.cs
create mode 100644 src/tests/ReactiveUI.Tests/Bindings/TypeConverters/StringToIntegerTypeConverterTests.cs
create mode 100644 src/tests/ReactiveUI.Tests/Bindings/TypeConverters/StringToLongTypeConverterTests.cs
create mode 100644 src/tests/ReactiveUI.Tests/Bindings/TypeConverters/StringToNullableByteTypeConverterTests.cs
create mode 100644 src/tests/ReactiveUI.Tests/Bindings/TypeConverters/StringToNullableDecimalTypeConverterTests.cs
create mode 100644 src/tests/ReactiveUI.Tests/Bindings/TypeConverters/StringToNullableDoubleTypeConverterTests.cs
create mode 100644 src/tests/ReactiveUI.Tests/Bindings/TypeConverters/StringToNullableIntegerTypeConverterTests.cs
create mode 100644 src/tests/ReactiveUI.Tests/Bindings/TypeConverters/StringToNullableLongTypeConverterTests.cs
create mode 100644 src/tests/ReactiveUI.Tests/Bindings/TypeConverters/StringToNullableShortTypeConverterTests.cs
create mode 100644 src/tests/ReactiveUI.Tests/Bindings/TypeConverters/StringToNullableSingleTypeConverterTests.cs
create mode 100644 src/tests/ReactiveUI.Tests/Bindings/TypeConverters/StringToShortTypeConverterTests.cs
create mode 100644 src/tests/ReactiveUI.Tests/Bindings/TypeConverters/StringToSingleTypeConverterTests.cs
rename src/tests/ReactiveUI.Tests/Infrastructure/StaticState/{RxAppSchedulersScope.cs => RxSchedulersSchedulersScope.cs} (81%)
delete mode 100644 src/tests/ReactiveUI.Tests/PlatformRegistrationsTest.cs
delete mode 100644 src/tests/ReactiveUI.Tests/Resolvers/DependencyResolverTests.cs
diff --git a/.claude/settings.local.json b/.claude/settings.local.json
new file mode 100644
index 0000000000..818a131d99
--- /dev/null
+++ b/.claude/settings.local.json
@@ -0,0 +1,15 @@
+{
+ "permissions": {
+ "allow": [
+ "Bash(ls:*)",
+ "Bash(dotnet build:*)",
+ "Bash(find:*)",
+ "Bash(dotnet test:*)",
+ "Bash(git log:*)",
+ "Bash(grep:*)",
+ "Bash(Select-String -Pattern \"\\(error|warning|Build succeeded|Build FAILED\\)\")",
+ "Bash(Select-Object -First 50)",
+ "Bash(Select-String -Pattern \"\\\\\\(error|warning|Build succeeded|Build FAILED\\\\\\)\")"
+ ]
+ }
+}
diff --git a/.editorconfig b/.editorconfig
index 4abe783c5b..6d767eabfe 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -800,6 +800,138 @@ dotnet_diagnostic.NUnit3004.severity = error # Field should be disposed in TearD
dotnet_diagnostic.NUnit4001.severity = error # Simplify the Values attribute
dotnet_diagnostic.NUnit4002.severity = error # Use Specific constraint
+###################
+# Trimming Analyzer Warnings (IL2001 - IL2123)
+# See: https://learn.microsoft.com/en-us/dotnet/core/deploying/trimming/trim-warnings/
+###################
+dotnet_diagnostic.IL2001.severity = error # Type in UnreferencedCode attribute doesn't have matching RequiresUnreferencedCode
+dotnet_diagnostic.IL2002.severity = error # Method with RequiresUnreferencedCode called from code without that attribute
+dotnet_diagnostic.IL2003.severity = error # RequiresUnreferencedCode attribute is only supported on methods
+dotnet_diagnostic.IL2004.severity = error # Incorrect RequiresUnreferencedCode signature
+dotnet_diagnostic.IL2005.severity = error # Could not resolve dependency assembly
+dotnet_diagnostic.IL2007.severity = error # Could not process embedded resource
+dotnet_diagnostic.IL2008.severity = error # Could not find type in assembly
+dotnet_diagnostic.IL2009.severity = error # Could not find method in type
+dotnet_diagnostic.IL2010.severity = error # Invalid value for PreserveDependencyAttribute
+dotnet_diagnostic.IL2011.severity = error # Unknown body modification
+dotnet_diagnostic.IL2012.severity = error # Could not find field in type
+dotnet_diagnostic.IL2013.severity = error # Substitution file contains invalid XML
+dotnet_diagnostic.IL2014.severity = error # Missing substitution file
+dotnet_diagnostic.IL2015.severity = error # Invalid XML encountered in substitution file
+dotnet_diagnostic.IL2016.severity = error # Could not find type from substitution XML
+dotnet_diagnostic.IL2017.severity = error # Could not find method in type specified in substitution XML
+dotnet_diagnostic.IL2018.severity = error # Could not find field in type specified in substitution XML
+dotnet_diagnostic.IL2019.severity = error # Could not find interface implementation in type
+dotnet_diagnostic.IL2022.severity = error # Type in DynamicallyAccessedMembers attribute doesn't have matching DynamicallyAccessedMembers annotation
+dotnet_diagnostic.IL2023.severity = error # Method returning DynamicallyAccessedMembers annotated type requires the same annotation
+dotnet_diagnostic.IL2024.severity = error # Multiple DynamicallyAccessedMembers annotations on a member are not supported
+dotnet_diagnostic.IL2025.severity = error # Duplicate preserve attribute
+dotnet_diagnostic.IL2026.severity = error # Using member annotated with RequiresUnreferencedCode
+dotnet_diagnostic.IL2027.severity = error # RequiresUnreferencedCodeAttribute is only supported on methods and constructors
+dotnet_diagnostic.IL2028.severity = error # Invalid RequiresUnreferencedCode attribute usage
+dotnet_diagnostic.IL2029.severity = error # RequiresUnreferencedCode attribute on type is not supported
+dotnet_diagnostic.IL2030.severity = error # Dynamic invocation of method requiring unreferenced code is not safe
+dotnet_diagnostic.IL2031.severity = error # Could not resolve dependency assembly from embedded resource
+dotnet_diagnostic.IL2032.severity = error # Error reading debug symbols
+dotnet_diagnostic.IL2033.severity = error # Trying to modify a sealed type
+dotnet_diagnostic.IL2034.severity = error # Value passed to the implicit 'this' parameter does not satisfy 'DynamicallyAccessedMembersAttribute' requirements
+dotnet_diagnostic.IL2035.severity = error # Unrecognized value passed to the parameter of method with 'DynamicallyAccessedMembersAttribute' requirements
+dotnet_diagnostic.IL2036.severity = error # Interface implementation has different DynamicallyAccessedMembers annotations than interface
+dotnet_diagnostic.IL2037.severity = error # BaseType annotation doesn't match
+dotnet_diagnostic.IL2038.severity = error # Derived type doesn't have matching DynamicallyAccessedMembers annotation
+dotnet_diagnostic.IL2039.severity = error # Implementation method doesn't have matching DynamicallyAccessedMembers annotation
+dotnet_diagnostic.IL2040.severity = error # Interface member doesn't have matching DynamicallyAccessedMembers annotation
+dotnet_diagnostic.IL2041.severity = error # GetType call on DynamicallyAccessedMembers annotated generic parameter
+dotnet_diagnostic.IL2042.severity = error # The DynamicallyAccessedMembersAttribute value used in a custom attribute is not compatible
+dotnet_diagnostic.IL2043.severity = error # DynamicallyAccessedMembersAttribute on property conflicts with base property
+dotnet_diagnostic.IL2044.severity = error # DynamicallyAccessedMembersAttribute on event conflicts with base event
+dotnet_diagnostic.IL2045.severity = error # Field type doesn't satisfy 'DynamicallyAccessedMembersAttribute' requirements
+dotnet_diagnostic.IL2046.severity = error # Trimmer couldn't find PreserveBaseOverridesAttribute on a method
+dotnet_diagnostic.IL2048.severity = error # Internal attribute couldn't be removed
+dotnet_diagnostic.IL2049.severity = error # Could not process data format message
+dotnet_diagnostic.IL2050.severity = error # Correctness of COM interop cannot be guaranteed after trimming
+dotnet_diagnostic.IL2051.severity = error # COM related type is trimmed
+dotnet_diagnostic.IL2052.severity = error # Resolving member reference for P/Invoke into type that is trimmed
+dotnet_diagnostic.IL2053.severity = error # Target method is trimmed
+dotnet_diagnostic.IL2054.severity = error # Generic constraint type is annotated with DynamicallyAccessedMembersAttribute which requires unreferenced code
+dotnet_diagnostic.IL2055.severity = error # Type implements COM visible type but has no GUID
+dotnet_diagnostic.IL2056.severity = error # Generic parameter with DynamicallyAccessedMembers annotation is not publicly visible
+dotnet_diagnostic.IL2057.severity = error # Unrecognized value passed to the parameter of method with DynamicallyAccessedMembersAttribute requirements
+dotnet_diagnostic.IL2058.severity = error # Parameter types of method doesn't have matching DynamicallyAccessedMembers annotation
+dotnet_diagnostic.IL2059.severity = error # Unrecognized reflection pattern
+dotnet_diagnostic.IL2060.severity = error # Unrecognized value passed to parameter with DynamicallyAccessedMembersAttribute
+dotnet_diagnostic.IL2061.severity = error # Value passed to implicit this parameter doesn't satisfy DynamicallyAccessedMembersAttribute requirements
+dotnet_diagnostic.IL2062.severity = error # Value passed to parameter doesn't satisfy DynamicallyAccessedMembersAttribute requirements
+dotnet_diagnostic.IL2063.severity = error # Value returned from method doesn't satisfy DynamicallyAccessedMembersAttribute requirements
+dotnet_diagnostic.IL2064.severity = error # Value assigned to field doesn't satisfy DynamicallyAccessedMembersAttribute requirements
+dotnet_diagnostic.IL2065.severity = error # Value passed to implicit this parameter doesn't satisfy DynamicallyAccessedMembersAttribute requirements
+dotnet_diagnostic.IL2066.severity = error # Value stored in field doesn't satisfy DynamicallyAccessedMembersAttribute requirements
+dotnet_diagnostic.IL2067.severity = error # Value passed to implicit this parameter doesn't satisfy DynamicallyAccessedMembersAttribute requirements
+dotnet_diagnostic.IL2068.severity = error # Value passed to parameter doesn't satisfy DynamicallyAccessedMembersAttribute requirements
+dotnet_diagnostic.IL2069.severity = error # Value returned from method doesn't satisfy DynamicallyAccessedMembersAttribute requirements
+dotnet_diagnostic.IL2070.severity = error # Value stored in field doesn't satisfy DynamicallyAccessedMembersAttribute requirements
+dotnet_diagnostic.IL2071.severity = error # Value returned from method doesn't satisfy DynamicallyAccessedMembersAttribute requirements
+dotnet_diagnostic.IL2072.severity = error # Value passed to parameter doesn't satisfy DynamicallyAccessedMembersAttribute requirements
+dotnet_diagnostic.IL2073.severity = error # Value returned from method doesn't satisfy DynamicallyAccessedMembersAttribute requirements
+dotnet_diagnostic.IL2074.severity = error # Value stored in field doesn't satisfy DynamicallyAccessedMembersAttribute requirements
+dotnet_diagnostic.IL2075.severity = error # Value passed to implicit this parameter doesn't satisfy DynamicallyAccessedMembersAttribute requirements
+dotnet_diagnostic.IL2076.severity = error # Value returned from method doesn't satisfy DynamicallyAccessedMembersAttribute requirements
+dotnet_diagnostic.IL2077.severity = error # Value passed to parameter doesn't satisfy DynamicallyAccessedMembersAttribute requirements
+dotnet_diagnostic.IL2078.severity = error # Value returned from method doesn't satisfy DynamicallyAccessedMembersAttribute requirements
+dotnet_diagnostic.IL2079.severity = error # Value stored in field doesn't satisfy DynamicallyAccessedMembersAttribute requirements
+dotnet_diagnostic.IL2080.severity = error # Value passed to implicit this parameter doesn't satisfy DynamicallyAccessedMembersAttribute requirements
+dotnet_diagnostic.IL2081.severity = error # Value returned from method doesn't satisfy DynamicallyAccessedMembersAttribute requirements
+dotnet_diagnostic.IL2082.severity = error # Value passed to parameter doesn't satisfy DynamicallyAccessedMembersAttribute requirements
+dotnet_diagnostic.IL2083.severity = error # Value stored in field doesn't satisfy DynamicallyAccessedMembersAttribute requirements
+dotnet_diagnostic.IL2084.severity = error # Value returned from method doesn't satisfy DynamicallyAccessedMembersAttribute requirements
+dotnet_diagnostic.IL2085.severity = error # Value passed to implicit this parameter doesn't satisfy DynamicallyAccessedMembersAttribute requirements
+dotnet_diagnostic.IL2087.severity = error # Value passed to parameter doesn't satisfy DynamicallyAccessedMembersAttribute requirements
+dotnet_diagnostic.IL2088.severity = error # Value returned from method doesn't satisfy DynamicallyAccessedMembersAttribute requirements
+dotnet_diagnostic.IL2089.severity = error # Value stored in field doesn't satisfy DynamicallyAccessedMembersAttribute requirements
+dotnet_diagnostic.IL2090.severity = error # Value passed to implicit this parameter doesn't satisfy DynamicallyAccessedMembersAttribute requirements
+dotnet_diagnostic.IL2091.severity = error # Target generic argument doesn't satisfy 'DynamicallyAccessedMembersAttribute' requirements
+dotnet_diagnostic.IL2092.severity = error # Value passed to generic parameter doesn't satisfy DynamicallyAccessedMembersAttribute requirements
+dotnet_diagnostic.IL2093.severity = error # Value stored in field doesn't satisfy DynamicallyAccessedMembersAttribute requirements
+dotnet_diagnostic.IL2094.severity = error # DynamicallyAccessedMembers on 'this' parameter doesn't match overridden member
+dotnet_diagnostic.IL2095.severity = error # Value returned from method doesn't satisfy DynamicallyAccessedMembersAttribute requirements
+dotnet_diagnostic.IL2096.severity = error # Calling method on statically typed generic instance requires unreferenced code
+dotnet_diagnostic.IL2097.severity = error # Value passed to parameter doesn't satisfy DynamicallyAccessedMembersAttribute requirements
+dotnet_diagnostic.IL2098.severity = error # Value stored in field doesn't satisfy DynamicallyAccessedMembersAttribute requirements
+dotnet_diagnostic.IL2099.severity = error # Value returned from method doesn't satisfy DynamicallyAccessedMembersAttribute requirements
+dotnet_diagnostic.IL2100.severity = error # XML stream doesn't conform to the schema
+dotnet_diagnostic.IL2101.severity = error # Embedded XML in assembly couldn't be loaded
+dotnet_diagnostic.IL2102.severity = error # Invalid warning number passed to UnconditionalSuppressMessage
+dotnet_diagnostic.IL2103.severity = error # Value passed to the 'propertyAccessExpression' parameter doesn't satisfy DynamicallyAccessedMembersAttribute requirements
+dotnet_diagnostic.IL2104.severity = error # Assembly that was specified through a custom step
+dotnet_diagnostic.IL2105.severity = error # Type from a custom step that couldn't be loaded
+dotnet_diagnostic.IL2106.severity = error # Method from a custom step that couldn't be loaded
+dotnet_diagnostic.IL2107.severity = error # Methods in types that derive from RemotingClientProxy cannot be statically determined
+dotnet_diagnostic.IL2108.severity = error # Invalid scope for UnconditionalSuppressMessage
+dotnet_diagnostic.IL2109.severity = error # Method doesn't have matching DynamicallyAccessedMembers annotation
+dotnet_diagnostic.IL2110.severity = error # Invalid member name in UnconditionalSuppressMessage
+dotnet_diagnostic.IL2111.severity = error # Method with parameters or return value with DynamicallyAccessedMembersAttribute is not supported
+dotnet_diagnostic.IL2112.severity = error # Reflection call to method with DynamicallyAccessedMembersAttribute requirements cannot be statically analyzed
+dotnet_diagnostic.IL2113.severity = error # DynamicallyAccessedMembers on type references Type.MakeGenericType with different requirements
+dotnet_diagnostic.IL2114.severity = error # DynamicallyAccessedMembers mismatch on signature types
+dotnet_diagnostic.IL2115.severity = error # DynamicallyAccessedMembers on type or base types references member which requires unreferenced code
+dotnet_diagnostic.IL2116.severity = error # DynamicallyAccessedMembers on parameter types doesn't match overridden parameter
+dotnet_diagnostic.IL2117.severity = error # Methods with DynamicallyAccessedMembersAttribute annotations cannot be replaced
+dotnet_diagnostic.IL2122.severity = error # Reflection call to method with UnreferencedCode attribute cannot be statically analyzed
+dotnet_diagnostic.IL2123.severity = error # DynamicallyAccessedMembers on method or parameter doesn't match overridden member
+
+###################
+# AOT Analyzer Warnings (IL3xxx)
+# See: https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/warnings/
+###################
+dotnet_diagnostic.IL3050.severity = error # Using member annotated with RequiresDynamicCode
+dotnet_diagnostic.IL3051.severity = error # RequiresDynamicCode attribute is only supported on methods and constructors
+dotnet_diagnostic.IL3052.severity = error # RequiresDynamicCode attribute on type is not supported
+dotnet_diagnostic.IL3053.severity = error # Assembly has RequiresDynamicCode attribute
+dotnet_diagnostic.IL3054.severity = error # Generic expansion in type requires dynamic code
+dotnet_diagnostic.IL3055.severity = error # MakeGenericType on non-supported type requires dynamic code
+dotnet_diagnostic.IL3056.severity = error # MakeGenericMethod on non-supported method requires dynamic code
+dotnet_diagnostic.IL3057.severity = error # Reflection access to generic parameter requires dynamic code
+
#############################################
# C++ Files
#############################################
diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
index 14f8a63357..99c15dd672 100644
--- a/.github/copilot-instructions.md
+++ b/.github/copilot-instructions.md
@@ -41,9 +41,21 @@ Invoke-WebRequest -Uri https://dot.net/v1/dotnet-install.ps1 -OutFile dotnet-ins
### Solution Files
-* Main solution: `src/ReactiveUI.sln` (repository `root/src` directory)
+* Main solution: `src/reactiveui.slnx` (repository `root/src` directory)
* Integration tests: `integrationtests/` directory contains platform-specific solutions. These are not required for most tasks to compile.
+**About SLNX Format:**
+
+This repository uses **SLNX** (XML-based solution format) introduced in Visual Studio 2022 17.10+, instead of the legacy `.sln` format.
+
+**Key characteristics:**
+- Structured XML format vs. proprietary text format
+- Better support for complex multi-platform projects like ReactiveUI
+- Full compatibility with `dotnet` CLI (works identically to .sln)
+- Requires Visual Studio 2022 17.10+ or JetBrains Rider 2024.1+ for IDE support
+
+All commands in this document reference `src/reactiveui.slnx`.
+
---
## 🛠️ Build & Test Commands
@@ -67,24 +79,42 @@ dotnet workload restore
cd ..
# Restore NuGet packages
-dotnet restore src/ReactiveUI.sln
+dotnet restore src/reactiveui.slnx
# Build the solution (requires Windows for platform-specific targets)
-dotnet build src/ReactiveUI.sln -c Release -warnaserror
+dotnet build src/reactiveui.slnx -c Release -warnaserror
```
### Test Commands (Microsoft Testing Platform)
**CRITICAL:** This repository uses MTP configured in `src/global.json`. All TUnit-specific arguments must be passed after `--`.
+**Microsoft Testing Platform (MTP) Overview:**
+
+MTP is the modern test execution platform replacing VSTest, providing:
+- Native integration with `dotnet test` command
+- Better performance and modern architecture for .NET 6.0+
+- Enhanced test filtering and parallel execution control
+- Required for TUnit framework (ReactiveUI's chosen test framework)
+
+**Key Differences from VSTest:**
+- Arguments passed AFTER `--`: `dotnet test -- --treenode-filter "..."`
+- Configured via `global.json` (specifies "Microsoft.Testing.Platform")
+- Test settings in `testconfig.json` (parallel execution, coverage format)
+
+**Best Practices:**
+- **Never use `--no-build`** - always build before testing to avoid stale binaries
+- Use `--output Detailed` BEFORE `--` to see Console.WriteLine output
+- TUnit runs non-parallel (`"parallel": false`) to prevent test interference
+
**Note:** Commands below assume repository root as working directory. Use `src/` prefix for paths.
```powershell
# Run all tests
-dotnet test src/ReactiveUI.sln -c Release --no-build
+dotnet test src/reactiveui.slnx -c Release
# Run tests with code coverage (Microsoft Code Coverage)
-dotnet test src/ReactiveUI.sln -- --coverage --coverage-output-format cobertura
+dotnet test src/reactiveui.slnx --coverage --coverage-output-format cobertura
# Run specific test project
dotnet test --project src/tests/ReactiveUI.Tests/ReactiveUI.Tests.csproj
@@ -96,16 +126,16 @@ dotnet test --project src/tests/ReactiveUI.Tests/ReactiveUI.Tests.csproj -- --tr
dotnet test --project src/tests/ReactiveUI.Tests/ReactiveUI.Tests.csproj -- --treenode-filter "/*/*/MyClassName/*"
# Filter by test property (e.g., Category)
-dotnet test src/ReactiveUI.sln -- --treenode-filter "/*/*/*/*[Category=Integration]"
+dotnet test src/reactiveui.slnx -- --treenode-filter "/*/*/*/*[Category=Integration]"
# List all available tests
dotnet test --project src/tests/ReactiveUI.Tests/ReactiveUI.Tests.csproj -- --list-tests
# Fail fast (stop on first failure)
-dotnet test src/ReactiveUI.sln -- --fail-fast
+dotnet test src/reactiveui.slnx -- --fail-fast
# Generate TRX report with coverage
-dotnet test src/ReactiveUI.sln -- --coverage --coverage-output-format cobertura --report-trx --output Detailed
+dotnet test src/reactiveui.slnx --coverage --coverage-output-format cobertura -- --report-trx --output Detailed
```
**TUnit Treenode-Filter Syntax:**
@@ -118,10 +148,14 @@ Examples:
- All tests in namespace: `--treenode-filter "/*/MyNamespace/*/*"`
- Filter by property: `--treenode-filter "/*/*/*/*[Category=Integration]"`
-**Key TUnit Command-Line Flags:**
-- `--treenode-filter` - Filter tests by path or properties
+**Key Command-Line Flags:**
+
+**dotnet test flags (BEFORE `--`):**
- `--coverage` - Enable Microsoft Code Coverage
- `--coverage-output-format` - Set format (cobertura, xml, coverage)
+
+**TUnit flags (AFTER `--`):**
+- `--treenode-filter` - Filter tests by path or properties
- `--report-trx` - Generate TRX reports
- `--output` - Verbosity (Normal or Detailed)
- `--list-tests` - Display tests without running
@@ -528,7 +562,7 @@ _total = this.WhenAnyValue(
```powershell
# Build with warnings as errors (includes StyleCop violations)
-dotnet build src/ReactiveUI.sln -c Release -warnaserror
+dotnet build src/reactiveui.slnx -c Release -warnaserror
```
**Important:** Style violations will cause build failures. Use an IDE with EditorConfig support (Visual Studio, VS Code, Rider) to automatically format code according to project standards.
diff --git a/CLAUDE.md b/CLAUDE.md
index 3a77aab352..f07d1ad859 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -8,6 +8,27 @@ This project uses **Microsoft Testing Platform (MTP)** with the **TUnit** testin
See: https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-test?tabs=dotnet-test-with-mtp
+### Solution Format: SLNX
+
+**Note**: This repository uses **SLNX** (XML-based solution format) instead of the legacy SLN format.
+
+**What is SLNX?**
+- Modern XML-based solution file format introduced in Visual Studio 2022 17.10+
+- Replaces the legacy text-based `.sln` format
+- More structured and easier to parse programmatically
+- Better support for complex multi-platform projects
+- Full compatibility with `dotnet` CLI commands
+
+**Key Differences from .sln:**
+- **Format:** Structured XML vs. legacy text format
+- **Readability:** Human-readable XML schema vs. proprietary text format
+- **Tooling:** Requires VS 2022 17.10+ or Rider 2024.1+ for IDE support
+- **CLI:** Works identically with all `dotnet build/test` commands
+
+**File Location:** `src/reactiveui.slnx`
+
+All build and test commands in this document reference `reactiveui.slnx`. The file works identically to traditional `.sln` files with dotnet CLI tools.
+
### Prerequisites
```powershell
@@ -21,7 +42,7 @@ dotnet workload restore
cd ..
# Restore NuGet packages
-dotnet restore ReactiveUI.sln
+dotnet restore reactiveui.slnx
```
### Build Commands
@@ -30,27 +51,55 @@ dotnet restore ReactiveUI.sln
```powershell
# Build the solution (requires Windows for platform-specific targets)
-dotnet build ReactiveUI.sln -c Release
+dotnet build reactiveui.slnx -c Release
# Build with warnings as errors (includes StyleCop violations)
-dotnet build ReactiveUI.sln -c Release -warnaserror
+dotnet build reactiveui.slnx -c Release -warnaserror
# Clean the solution
-dotnet clean ReactiveUI.sln
+dotnet clean reactiveui.slnx
```
### Test Commands (Microsoft Testing Platform)
**CRITICAL:** This repository uses MTP configured in `global.json`. All TUnit-specific arguments must be passed after `--`:
+**What is Microsoft Testing Platform (MTP)?**
+
+MTP is the modern test execution platform for .NET, replacing the legacy VSTest platform. It provides:
+- **Native integration** with `dotnet test` command
+- **Better performance** through optimized test discovery and execution
+- **Modern architecture** designed for current .NET versions (6.0+)
+- **Enhanced control** over test execution with detailed filtering and reporting
+
+**Why ReactiveUI uses MTP:**
+- Required for TUnit testing framework (modern alternative to xUnit/NUnit)
+- Better integration with build systems and CI/CD pipelines
+- Improved test isolation and parallel execution control
+- Native support for modern .NET features
+
+**Key Difference from VSTest:**
+- MTP arguments are passed AFTER `--` separator: `dotnet test -- --mtp-args`
+- VSTest used different command syntax: `dotnet test --vstest-args`
+- MTP is configured via `global.json` (see "Key Configuration Files" section)
+
+**Configuration:**
+- `global.json`: Specifies MTP as the test runner (`"Microsoft.Testing.Platform"`)
+- `testconfig.json`: Test execution settings (parallel: false, coverage format)
+- `Directory.Build.props`: Enables `TestingPlatformDotnetTestSupport` for test projects
+
+**IMPORTANT Testing Best Practices:**
+- **Do NOT use `--no-build` flag** when running tests. Always build before testing to ensure all code changes (including test changes) are compiled. Using `--no-build` can cause tests to run against stale binaries and produce misleading results.
+- Use `--output Detailed` to see Console.WriteLine output from tests. This must be placed BEFORE the `--` separator
+- TUnit runs tests non-parallel by default in this repository (`"parallel": false` in testconfig.json) to avoid test interference
+
+**Working Directory:** All test commands must be run from the `./src` folder.
+
The working folder must be `./src` folder. These commands won't function properly without the correct working folder.
```powershell
# Run all tests in the solution
-dotnet test --solution ReactiveUI.sln -c Release
-
-# Run all tests without building first (faster when code hasn't changed)
-dotnet test --solution ReactiveUI.sln -c Release --no-build
+dotnet test --solution reactiveui.slnx -c Release
# Run all tests in a specific project
dotnet test --project tests/ReactiveUI.Tests/ReactiveUI.Tests.csproj
@@ -66,31 +115,31 @@ dotnet test --project tests/ReactiveUI.Tests/ReactiveUI.Tests.csproj -- --treeno
dotnet test --project tests/ReactiveUI.Tests/ReactiveUI.Tests.csproj -- --treenode-filter "/*/MyNamespace/*/*"
# Filter by test property (e.g., Category)
-dotnet test --solution ReactiveUI.sln -- --treenode-filter "/*/*/*/*[Category=Integration]"
+dotnet test --solution reactiveui.slnx -- --treenode-filter "/*/*/*/*[Category=Integration]"
# Run tests with code coverage (Microsoft Code Coverage)
-dotnet test --solution ReactiveUI.sln -- --coverage --coverage-output-format cobertura
+dotnet test --solution reactiveui.slnx --coverage --coverage-output-format cobertura
# Run tests with detailed output
-dotnet test --solution ReactiveUI.sln -- --output Detailed
+dotnet test --solution reactiveui.slnx -- --output Detailed
# List all available tests without running them
dotnet test --project tests/ReactiveUI.Tests/ReactiveUI.Tests.csproj -- --list-tests
# Fail fast (stop on first failure)
-dotnet test --solution ReactiveUI.sln -- --fail-fast
+dotnet test --solution reactiveui.slnx -- --fail-fast
# Control parallel test execution
-dotnet test --solution ReactiveUI.sln -- --maximum-parallel-tests 4
+dotnet test --solution reactiveui.slnx -- --maximum-parallel-tests 4
# Generate TRX report
-dotnet test --solution ReactiveUI.sln -- --report-trx
+dotnet test --solution reactiveui.slnx -- --report-trx
# Disable logo for cleaner output
dotnet test --project tests/ReactiveUI.Tests/ReactiveUI.Tests.csproj -- --disable-logo
# Combine options: coverage + TRX report + detailed output
-dotnet test --solution ReactiveUI.sln -- --coverage --coverage-output-format cobertura --report-trx --output Detailed
+dotnet test --solution reactiveui.slnx --coverage --coverage-output-format cobertura -- --report-trx --output Detailed
```
**Alternative: Using `dotnet run` for single project**
@@ -115,14 +164,17 @@ The `--treenode-filter` follows the pattern: `/{AssemblyName}/{Namespace}/{Class
**Note:** Use single asterisks (`*`) to match segments. Double asterisks (`/**`) are not supported in treenode-filter.
-### Key TUnit Command-Line Flags
+### Key Command-Line Flags
+**dotnet test flags (BEFORE `--`):**
+- `--coverage` - Enable Microsoft Code Coverage
+- `--coverage-output-format` - Set coverage format (cobertura, xml, coverage)
+
+**TUnit flags (AFTER `--`):**
- `--treenode-filter` - Filter tests by path pattern or properties (syntax: `/{Assembly}/{Namespace}/{Class}/{Method}`)
- `--list-tests` - Display available tests without running
- `--fail-fast` - Stop after first failure
- `--maximum-parallel-tests` - Limit concurrent execution (default: processor count)
-- `--coverage` - Enable Microsoft Code Coverage
-- `--coverage-output-format` - Set coverage format (cobertura, xml, coverage)
- `--report-trx` - Generate TRX format reports
- `--output` - Control verbosity (Normal or Detailed)
- `--no-progress` - Suppress progress reporting
@@ -383,7 +435,7 @@ _total = this.WhenAnyValue(
## Important Notes
- **Repository Location:** Working directory is `C:\source\reactiveui\src`
-- **Main Solution:** `ReactiveUI.sln`
+- **Main Solution:** `reactiveui.slnx`
- **Benchmarks:** Separate solution at `Benchmarks/ReactiveUI.Benchmarks.sln`
- **Integration Tests:** Platform-specific solutions in `integrationtests/` (not required for most development)
- **No shallow clones:** Repository requires full recursive clone for git version information used by build system
diff --git a/src/.claude/settings.local.json b/src/.claude/settings.local.json
index 9b9d047ef3..46438355c8 100644
--- a/src/.claude/settings.local.json
+++ b/src/.claude/settings.local.json
@@ -7,7 +7,9 @@
"Bash(dotnet test:*)",
"Bash(ls:*)",
"Bash(grep:*)",
- "Bash(dotnet clean:*)"
+ "Bash(dotnet clean:*)",
+ "WebFetch(domain:tunit.dev)",
+ "Bash(find:*)"
]
}
}
diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets
index fd3a50d51d..d6fad1a2de 100644
--- a/src/Directory.Build.targets
+++ b/src/Directory.Build.targets
@@ -7,9 +7,6 @@
false
-
- $(DefineConstants);NETSTANDARD;PORTABLE
-
$(DefineConstants);NET_461;XAML
diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props
index b818a5d239..88ecd94e7a 100644
--- a/src/Directory.Packages.props
+++ b/src/Directory.Packages.props
@@ -4,7 +4,7 @@
true
- 18.1.1
+ 19.1.1
1.17.0
2.9.2.1
10.0.0
diff --git a/src/ReactiveUI.AndroidX/Builder/AndroidXReactiveUIBuilderExtensions.cs b/src/ReactiveUI.AndroidX/Builder/AndroidXReactiveUIBuilderExtensions.cs
index 70a3da00da..888d25e65c 100644
--- a/src/ReactiveUI.AndroidX/Builder/AndroidXReactiveUIBuilderExtensions.cs
+++ b/src/ReactiveUI.AndroidX/Builder/AndroidXReactiveUIBuilderExtensions.cs
@@ -25,10 +25,6 @@ public static class AndroidXReactiveUIBuilderExtensions
///
/// The builder instance.
/// The builder instance for chaining.
-#if NET6_0_OR_GREATER
- [RequiresUnreferencedCode("Uses reflection to create instances of types.")]
- [RequiresDynamicCode("Uses reflection to create instances of types.")]
-#endif
public static IReactiveUIBuilder WithAndroidX(this IReactiveUIBuilder builder)
{
if (builder is null)
diff --git a/src/ReactiveUI.AndroidX/ControlFetcherMixin.cs b/src/ReactiveUI.AndroidX/ControlFetcherMixin.cs
index 22d0c34709..d8fc49dd3b 100644
--- a/src/ReactiveUI.AndroidX/ControlFetcherMixin.cs
+++ b/src/ReactiveUI.AndroidX/ControlFetcherMixin.cs
@@ -25,10 +25,8 @@ public static class ControlFetcherMixin
/// The fragment.
/// The inflated view.
/// The resolve members.
-#if NET6_0_OR_GREATER
- [RequiresDynamicCode("WireUpControls uses methods that require dynamic code generation")]
- [RequiresUnreferencedCode("WireUpControls uses methods that may require unreferenced code")]
-#endif
+ [RequiresUnreferencedCode("Android resource discovery uses reflection over generated resource types that may be trimmed.")]
+ [RequiresDynamicCode("Android resource discovery uses reflection that may require dynamic code generation.")]
public static void WireUpControls(this Fragment fragment, View inflatedView, ResolveStrategy resolveMembers = ResolveStrategy.Implicit)
{
ArgumentExceptionHelper.ThrowIfNull(fragment);
diff --git a/src/ReactiveUI.AndroidX/ReactiveAppCompatActivity.cs b/src/ReactiveUI.AndroidX/ReactiveAppCompatActivity.cs
index ea33d25aa8..6e1e835e1c 100644
--- a/src/ReactiveUI.AndroidX/ReactiveAppCompatActivity.cs
+++ b/src/ReactiveUI.AndroidX/ReactiveAppCompatActivity.cs
@@ -15,10 +15,6 @@ namespace ReactiveUI.AndroidX;
/// This is an Activity that is both an Activity and has ReactiveObject powers
/// (i.e. you can call RaiseAndSetIfChanged).
///
-#if NET6_0_OR_GREATER
-[RequiresDynamicCode("ReactiveAppCompatActivity inherits from ReactiveObject which uses extension methods that require dynamic code generation")]
-[RequiresUnreferencedCode("ReactiveAppCompatActivity inherits from ReactiveObject which uses extension methods that may require unreferenced code")]
-#endif
public class ReactiveAppCompatActivity : AppCompatActivity, IReactiveObject, IReactiveNotifyPropertyChanged, IHandleObservableErrors
{
private readonly Subject _activated = new();
diff --git a/src/ReactiveUI.AndroidX/ReactiveAppCompatActivity{TViewModel}.cs b/src/ReactiveUI.AndroidX/ReactiveAppCompatActivity{TViewModel}.cs
index 2d60c87ff5..79e0ff1cb6 100644
--- a/src/ReactiveUI.AndroidX/ReactiveAppCompatActivity{TViewModel}.cs
+++ b/src/ReactiveUI.AndroidX/ReactiveAppCompatActivity{TViewModel}.cs
@@ -10,10 +10,6 @@ namespace ReactiveUI.AndroidX;
/// (i.e. you can call RaiseAndSetIfChanged).
///
/// The view model type.
-#if NET6_0_OR_GREATER
-[RequiresDynamicCode("ReactiveAppCompatActivity uses methods that require dynamic code generation")]
-[RequiresUnreferencedCode("ReactiveAppCompatActivity uses methods that may require unreferenced code")]
-#endif
public class ReactiveAppCompatActivity : ReactiveAppCompatActivity, IViewFor, ICanActivate
where TViewModel : class
{
diff --git a/src/ReactiveUI.AndroidX/ReactiveDialogFragment.cs b/src/ReactiveUI.AndroidX/ReactiveDialogFragment.cs
index 68c5db6d1e..7369e4a3c1 100644
--- a/src/ReactiveUI.AndroidX/ReactiveDialogFragment.cs
+++ b/src/ReactiveUI.AndroidX/ReactiveDialogFragment.cs
@@ -9,10 +9,6 @@ namespace ReactiveUI.AndroidX;
/// This is a Fragment that is both an Activity and has ReactiveObject powers
/// (i.e. you can call RaiseAndSetIfChanged).
///
-#if NET6_0_OR_GREATER
-[RequiresDynamicCode("ReactiveDialogFragment uses methods that require dynamic code generation")]
-[RequiresUnreferencedCode("ReactiveDialogFragment uses methods that may require unreferenced code")]
-#endif
public class ReactiveDialogFragment : global::AndroidX.Fragment.App.DialogFragment, IReactiveNotifyPropertyChanged, IReactiveObject, IHandleObservableErrors
{
private readonly Subject _activated = new();
@@ -57,10 +53,6 @@ protected ReactiveDialogFragment()
void IReactiveObject.RaisePropertyChanged(PropertyChangedEventArgs args) => PropertyChanged?.Invoke(this, args);
///
-#if NET6_0_OR_GREATER
- [RequiresDynamicCode("SuppressChangeNotifications uses methods that require dynamic code generation")]
- [RequiresUnreferencedCode("SuppressChangeNotifications uses methods that may require unreferenced code")]
-#endif
public IDisposable SuppressChangeNotifications() => IReactiveObjectExtensions.SuppressChangeNotifications(this);
///
diff --git a/src/ReactiveUI.AndroidX/ReactiveDialogFragment{TViewModel}.cs b/src/ReactiveUI.AndroidX/ReactiveDialogFragment{TViewModel}.cs
index 459ee69deb..cec0a91d52 100644
--- a/src/ReactiveUI.AndroidX/ReactiveDialogFragment{TViewModel}.cs
+++ b/src/ReactiveUI.AndroidX/ReactiveDialogFragment{TViewModel}.cs
@@ -10,10 +10,6 @@ namespace ReactiveUI.AndroidX;
/// (i.e. you can call RaiseAndSetIfChanged).
///
/// The view model type.
-#if NET6_0_OR_GREATER
-[RequiresDynamicCode("ReactiveDialogFragment uses methods that require dynamic code generation")]
-[RequiresUnreferencedCode("ReactiveDialogFragment uses methods that may require unreferenced code")]
-#endif
public class ReactiveDialogFragment : ReactiveDialogFragment, IViewFor, ICanActivate
where TViewModel : class
{
diff --git a/src/ReactiveUI.AndroidX/ReactiveFragment.cs b/src/ReactiveUI.AndroidX/ReactiveFragment.cs
index 37384402c0..d0bd94455b 100644
--- a/src/ReactiveUI.AndroidX/ReactiveFragment.cs
+++ b/src/ReactiveUI.AndroidX/ReactiveFragment.cs
@@ -9,10 +9,6 @@ namespace ReactiveUI.AndroidX;
/// This is a Fragment that is both an Activity and has ReactiveObject powers
/// (i.e. you can call RaiseAndSetIfChanged).
///
-#if NET6_0_OR_GREATER
-[RequiresDynamicCode("ReactiveFragment inherits from ReactiveObject which uses extension methods that require dynamic code generation")]
-[RequiresUnreferencedCode("ReactiveFragment inherits from ReactiveObject which uses extension methods that may require unreferenced code")]
-#endif
[ExcludeFromCodeCoverage]
public class ReactiveFragment : global::AndroidX.Fragment.App.Fragment, IReactiveNotifyPropertyChanged, IReactiveObject, IHandleObservableErrors
{
diff --git a/src/ReactiveUI.AndroidX/ReactiveFragmentActivity.cs b/src/ReactiveUI.AndroidX/ReactiveFragmentActivity.cs
index fbdec77011..909a5a939a 100644
--- a/src/ReactiveUI.AndroidX/ReactiveFragmentActivity.cs
+++ b/src/ReactiveUI.AndroidX/ReactiveFragmentActivity.cs
@@ -14,10 +14,6 @@ namespace ReactiveUI.AndroidX;
/// This is an Activity that is both an Activity and has ReactiveObject powers
/// (i.e. you can call RaiseAndSetIfChanged).
///
-#if NET6_0_OR_GREATER
-[RequiresDynamicCode("ReactiveFragmentActivity inherits from ReactiveObject which uses extension methods that require dynamic code generation")]
-[RequiresUnreferencedCode("ReactiveFragmentActivity inherits from ReactiveObject which uses extension methods that may require unreferenced code")]
-#endif
public class ReactiveFragmentActivity : FragmentActivity, IReactiveObject, IReactiveNotifyPropertyChanged, IHandleObservableErrors
{
private readonly Subject _activated = new();
diff --git a/src/ReactiveUI.AndroidX/ReactiveFragmentActivity{TViewModel}.cs b/src/ReactiveUI.AndroidX/ReactiveFragmentActivity{TViewModel}.cs
index baf4a02c8b..85c8609875 100644
--- a/src/ReactiveUI.AndroidX/ReactiveFragmentActivity{TViewModel}.cs
+++ b/src/ReactiveUI.AndroidX/ReactiveFragmentActivity{TViewModel}.cs
@@ -10,10 +10,6 @@ namespace ReactiveUI.AndroidX;
/// (i.e. you can call RaiseAndSetIfChanged).
///
/// The view model type.
-#if NET6_0_OR_GREATER
-[RequiresDynamicCode("ReactiveFragmentActivity inherits from ReactiveObject which uses extension methods that require dynamic code generation")]
-[RequiresUnreferencedCode("ReactiveFragmentActivity inherits from ReactiveObject which uses extension methods that may require unreferenced code")]
-#endif
public class ReactiveFragmentActivity : ReactiveFragmentActivity, IViewFor, ICanActivate
where TViewModel : class
{
diff --git a/src/ReactiveUI.AndroidX/ReactiveFragment{TViewModel}.cs b/src/ReactiveUI.AndroidX/ReactiveFragment{TViewModel}.cs
index dd83c18d45..9400a30eeb 100644
--- a/src/ReactiveUI.AndroidX/ReactiveFragment{TViewModel}.cs
+++ b/src/ReactiveUI.AndroidX/ReactiveFragment{TViewModel}.cs
@@ -10,10 +10,6 @@ namespace ReactiveUI.AndroidX;
/// (i.e. you can call RaiseAndSetIfChanged).
///
/// The view model type.
-#if NET6_0_OR_GREATER
-[RequiresDynamicCode("ReactiveFragment uses methods that require dynamic code generation")]
-[RequiresUnreferencedCode("ReactiveFragment uses methods that may require unreferenced code")]
-#endif
public class ReactiveFragment : ReactiveFragment, IViewFor, ICanActivate
where TViewModel : class
{
diff --git a/src/ReactiveUI.AndroidX/ReactivePreferenceFragment.cs b/src/ReactiveUI.AndroidX/ReactivePreferenceFragment.cs
index ad02b3a4df..db101a1dc2 100644
--- a/src/ReactiveUI.AndroidX/ReactivePreferenceFragment.cs
+++ b/src/ReactiveUI.AndroidX/ReactivePreferenceFragment.cs
@@ -13,10 +13,6 @@ namespace ReactiveUI.AndroidX;
/// This is a PreferenceFragment that is both an Activity and has ReactiveObject powers
/// (i.e. you can call RaiseAndSetIfChanged).
///
-#if NET6_0_OR_GREATER
-[RequiresDynamicCode("SuppressChangeNotifications uses methods that require dynamic code generation")]
-[RequiresUnreferencedCode("SuppressChangeNotifications uses methods that may require unreferenced code")]
-#endif
public abstract class ReactivePreferenceFragment : PreferenceFragmentCompat, IReactiveNotifyPropertyChanged, IReactiveObject, IHandleObservableErrors
{
private readonly Subject _activated = new();
@@ -65,10 +61,6 @@ protected ReactivePreferenceFragment(in IntPtr handle, JniHandleOwnership owners
public IObservable Deactivated => _deactivated.AsObservable();
///
-#if NET6_0_OR_GREATER
- [RequiresDynamicCode("SuppressChangeNotifications uses methods that require dynamic code generation")]
- [RequiresUnreferencedCode("SuppressChangeNotifications uses methods that may require unreferenced code")]
-#endif
public IDisposable SuppressChangeNotifications() => IReactiveObjectExtensions.SuppressChangeNotifications(this);
///
diff --git a/src/ReactiveUI.AndroidX/ReactivePreferenceFragment{TViewModel}.cs b/src/ReactiveUI.AndroidX/ReactivePreferenceFragment{TViewModel}.cs
index 96a0daa623..02bfda82e8 100644
--- a/src/ReactiveUI.AndroidX/ReactivePreferenceFragment{TViewModel}.cs
+++ b/src/ReactiveUI.AndroidX/ReactivePreferenceFragment{TViewModel}.cs
@@ -12,10 +12,6 @@ namespace ReactiveUI.AndroidX;
/// (i.e. you can call RaiseAndSetIfChanged).
///
/// The view model type.
-#if NET6_0_OR_GREATER
-[RequiresDynamicCode("ReactivePreferenceFragment uses methods that require dynamic code generation")]
-[RequiresUnreferencedCode("ReactivePreferenceFragment uses methods that may require unreferenced code")]
-#endif
public abstract class ReactivePreferenceFragment : ReactivePreferenceFragment, IViewFor, ICanActivate
where TViewModel : class
{
@@ -33,10 +29,6 @@ protected ReactivePreferenceFragment()
///
/// The handle.
/// The ownership.
-#if NET6_0_OR_GREATER
- [RequiresDynamicCode("ReactivePreferenceFragment uses methods that require dynamic code generation")]
- [RequiresUnreferencedCode("ReactivePreferenceFragment uses methods that may require unreferenced code")]
-#endif
protected ReactivePreferenceFragment(in IntPtr handle, JniHandleOwnership ownership)
: base(handle, ownership)
{
diff --git a/src/ReactiveUI.AndroidX/ReactiveRecyclerViewViewHolder.cs b/src/ReactiveUI.AndroidX/ReactiveRecyclerViewViewHolder.cs
index e95a790787..e48d700d07 100644
--- a/src/ReactiveUI.AndroidX/ReactiveRecyclerViewViewHolder.cs
+++ b/src/ReactiveUI.AndroidX/ReactiveRecyclerViewViewHolder.cs
@@ -17,10 +17,8 @@ namespace ReactiveUI.AndroidX;
/// A implementation that binds to a reactive view model.
///
/// The type of the view model.
-#if NET6_0_OR_GREATER
-[RequiresDynamicCode("ReactiveRecyclerViewViewHolder inherits from ReactiveObject which uses extension methods that require dynamic code generation")]
-[RequiresUnreferencedCode("ReactiveRecyclerViewViewHolder inherits from ReactiveObject which uses extension methods that may require unreferenced code")]
-#endif
+[RequiresUnreferencedCode("Android property discovery uses reflection over generated resource types that may be trimmed.")]
+[RequiresDynamicCode("Android property discovery discovery uses reflection that may require dynamic code generation.")]
public class ReactiveRecyclerViewViewHolder : RecyclerView.ViewHolder, ILayoutViewHost, IViewFor, IReactiveNotifyPropertyChanged>, IReactiveObject, ICanActivate
where TViewModel : class, IReactiveObject
{
diff --git a/src/ReactiveUI.AndroidX/Registrations.cs b/src/ReactiveUI.AndroidX/Registrations.cs
index 6597048483..72e08dd09f 100644
--- a/src/ReactiveUI.AndroidX/Registrations.cs
+++ b/src/ReactiveUI.AndroidX/Registrations.cs
@@ -14,17 +14,13 @@ namespace ReactiveUI.AndroidX;
public class Registrations : IWantsToRegisterStuff
{
///
-#if NET6_0_OR_GREATER
- [RequiresUnreferencedCode("Uses reflection to create instances of types.")]
- [RequiresDynamicCode("Uses reflection to create instances of types.")]
-#endif
- public void Register(Action, Type> registerFunction)
+ public void Register(IRegistrar registrar)
{
- ArgumentExceptionHelper.ThrowIfNull(registerFunction);
+ ArgumentExceptionHelper.ThrowIfNull(registrar);
// Leverage core Android platform registrations already present in ReactiveUI.Platforms android.
// This ensures IPlatformOperations, binding converters, and schedulers are configured.
- new PlatformRegistrations().Register(registerFunction);
+ new PlatformRegistrations().Register(registrar);
// AndroidX specific registrations could be added here if needed in the future.
diff --git a/src/ReactiveUI.Blazor/Builder/BlazorReactiveUIBuilderExtensions.cs b/src/ReactiveUI.Blazor/Builder/BlazorReactiveUIBuilderExtensions.cs
index b855569a25..976d08c7a6 100644
--- a/src/ReactiveUI.Blazor/Builder/BlazorReactiveUIBuilderExtensions.cs
+++ b/src/ReactiveUI.Blazor/Builder/BlazorReactiveUIBuilderExtensions.cs
@@ -31,10 +31,6 @@ public static class BlazorReactiveUIBuilderExtensions
///
/// The builder instance.
/// The builder instance for chaining.
-#if NET6_0_OR_GREATER
- [RequiresDynamicCode("WithBlazor uses methods that require dynamic code generation")]
- [RequiresUnreferencedCode("WithBlazor uses methods that may require unreferenced code")]
-#endif
public static IReactiveUIBuilder WithBlazor(this IReactiveUIBuilder builder)
{
if (builder is null)
diff --git a/src/ReactiveUI.Blazor/Internal/ReactiveComponentHelpers.cs b/src/ReactiveUI.Blazor/Internal/ReactiveComponentHelpers.cs
new file mode 100644
index 0000000000..bd4f245526
--- /dev/null
+++ b/src/ReactiveUI.Blazor/Internal/ReactiveComponentHelpers.cs
@@ -0,0 +1,270 @@
+// Copyright (c) 2025 .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.AspNetCore.Components;
+
+namespace ReactiveUI.Blazor.Internal;
+
+///
+/// Internal helper methods for reactive Blazor components.
+/// Provides shared functionality for activation wiring, property change observation, and state management.
+///
+///
+///
+/// This class centralizes common reactive patterns used across all Blazor component base classes,
+/// eliminating code duplication and providing a single source of truth for reactive behavior.
+///
+///
+/// Performance: All methods are optimized to minimize allocations and use static delegates where possible
+/// to avoid closure allocations. Observable creation patterns are designed for efficient subscription management.
+///
+///
+internal static class ReactiveComponentHelpers
+{
+ ///
+ /// Creates an observable that produces a value for each
+ /// notification raised by .
+ ///
+ /// The source object that implements .
+ ///
+ /// An observable sequence of pulses, one for each property change notification.
+ ///
+ ///
+ ///
+ /// This method uses Observable.Create to create a highly efficient event-based observable
+ /// with direct event handler management, avoiding the overhead of Observable.FromEvent.
+ ///
+ ///
+ /// Performance: Observable.Create is more efficient than Observable.FromEvent as it avoids
+ /// delegate conversions and provides direct control over subscription lifecycle. The event
+ /// handler is a local function that captures minimal state, optimizing allocation overhead.
+ ///
+ ///
+ /// Thrown when is .
+ public static IObservable CreatePropertyChangedPulse(INotifyPropertyChanged source)
+ {
+ ArgumentNullException.ThrowIfNull(source);
+
+ return Observable.Create(observer =>
+ {
+ void Handler(object? sender, PropertyChangedEventArgs e) => observer.OnNext(Unit.Default);
+
+ source.PropertyChanged += Handler;
+ return Disposable.Create(() => source.PropertyChanged -= Handler);
+ });
+ }
+
+ ///
+ /// Wires ReactiveUI activation semantics to the specified view model if it implements .
+ ///
+ /// The view model type that implements .
+ /// The view model to wire activation for.
+ /// The reactive component state that provides activation/deactivation observables.
+ ///
+ ///
+ /// This method sets up a two-way binding between the component's activation lifecycle and the view model's
+ /// . When the component is activated, the view model's activator is triggered.
+ /// When the component is deactivated, the view model's activator is deactivated.
+ ///
+ ///
+ /// The activation subscription is added to to ensure
+ /// it is disposed when the component is disposed. The deactivation subscription does not require explicit disposal
+ /// as it is a fire-and-forget operation that completes when the component is disposed.
+ ///
+ ///
+ /// Performance: This is a low-frequency setup operation that occurs once during component initialization.
+ /// The guard check ensures no work is done if the view model doesn't support activation.
+ ///
+ ///
+ ///
+ /// Thrown when or is .
+ ///
+ public static void WireActivationIfSupported(T? viewModel, ReactiveComponentState state)
+ where T : class, INotifyPropertyChanged
+ {
+ ArgumentNullException.ThrowIfNull(state);
+
+ if (viewModel is not IActivatableViewModel avm)
+ {
+ return;
+ }
+
+ // Subscribe to component activation and trigger view model activation
+ state.Activated
+ .Subscribe(_ => avm.Activator.Activate())
+ .DisposeWith(state.LifetimeDisposables);
+
+ // Deactivation subscription does not need disposal tracking beyond component lifetime
+ state.Deactivated.Subscribe(_ => avm.Activator.Deactivate());
+ }
+
+ ///
+ /// Creates an observable that emits the current view model (if non-null) and then emits each
+ /// subsequent non-null view model assignment.
+ ///
+ /// The view model type that implements .
+ /// A function that returns the current view model value.
+ ///
+ /// An action that adds a handler to the event.
+ ///
+ ///
+ /// An action that removes a handler from the event.
+ ///
+ ///
+ /// The name of the view model property to observe. Typically "ViewModel".
+ ///
+ ///
+ /// An observable sequence of non-null view models. Emits the current view model once (if non-null),
+ /// then emits each subsequent non-null view model assignment.
+ ///
+ ///
+ ///
+ /// This method creates a cold observable using Observable.Create. Each subscription
+ /// gets its own event handler that is properly cleaned up when the subscription is disposed.
+ ///
+ ///
+ /// The observable filters property changes to only emit when the view model property changes (using
+ /// ordinal string comparison for performance). Null view models are filtered out to ensure downstream
+ /// operators always receive non-null values.
+ ///
+ ///
+ /// Performance: Uses for property name comparison, which is
+ /// the fastest string comparison method and matches the typical behavior of expressions.
+ ///
+ ///
+ ///
+ /// Thrown when , ,
+ /// , or is .
+ ///
+ public static IObservable CreateViewModelChangedStream(
+ Func getCurrentViewModel,
+ Action addPropertyChangedHandler,
+ Action removePropertyChangedHandler,
+ string viewModelPropertyName)
+ where T : class, INotifyPropertyChanged
+ {
+ ArgumentNullException.ThrowIfNull(getCurrentViewModel);
+ ArgumentNullException.ThrowIfNull(addPropertyChangedHandler);
+ ArgumentNullException.ThrowIfNull(removePropertyChangedHandler);
+ ArgumentNullException.ThrowIfNull(viewModelPropertyName);
+
+ return Observable.Create(
+ observer =>
+ {
+ // Emit current value once to preserve the original "Skip(1)" behavior in consumers
+ var current = getCurrentViewModel();
+ if (current is not null)
+ {
+ observer.OnNext(current);
+ }
+
+ // Handler for subsequent changes
+ void Handler(object? sender, PropertyChangedEventArgs e)
+ {
+ // Use ordinal comparison for best performance; nameof() produces ordinal strings
+ if (!string.Equals(e.PropertyName, viewModelPropertyName, StringComparison.Ordinal))
+ {
+ return;
+ }
+
+ var vm = getCurrentViewModel();
+ if (vm is not null)
+ {
+ observer.OnNext(vm);
+ }
+ }
+
+ addPropertyChangedHandler(Handler);
+ return Disposable.Create(() => removePropertyChangedHandler(Handler));
+ });
+ }
+
+ ///
+ /// Wires reactivity that triggers UI re-rendering when the view model changes or when the current
+ /// view model raises property changed events.
+ ///
+ /// The view model type that implements .
+ /// A function that returns the current view model value.
+ ///
+ /// An action that adds a handler to the event.
+ ///
+ ///
+ /// An action that removes a handler from the event.
+ ///
+ ///
+ /// The name of the view model property to observe. Typically "ViewModel".
+ ///
+ ///
+ /// A callback to invoke when the UI should be re-rendered. Typically
+ /// wrapped in .
+ ///
+ ///
+ /// A disposable that tears down all subscriptions when disposed. Should be assigned to
+ /// .
+ ///
+ ///
+ ///
+ /// This method creates two subscriptions that work together to provide comprehensive UI reactivity:
+ /// 1. A subscription that triggers re-render when the view model instance changes (skipping the initial value).
+ /// 2. A subscription that triggers re-render when any property on the current view model changes.
+ ///
+ ///
+ /// The view model stream is created with Publish and RefCount operators
+ /// to ensure the underlying observable is shared between both subscriptions, preventing duplicate event handler registrations.
+ ///
+ ///
+ /// The Switch operator ensures that when the view model changes, the old view model's
+ /// property changes are automatically unsubscribed and the new view model's property changes are subscribed.
+ ///
+ ///
+ /// Performance: The Publish().RefCount() pattern minimizes allocations by sharing the underlying observable.
+ /// The Switch operator efficiently manages subscription lifecycle to prevent memory leaks from old view models.
+ ///
+ ///
+ ///
+ /// Thrown when any parameter is .
+ ///
+ public static IDisposable WireViewModelChangeReactivity(
+ Func getCurrentViewModel,
+ Action addPropertyChangedHandler,
+ Action removePropertyChangedHandler,
+ string viewModelPropertyName,
+ Action stateHasChangedCallback)
+ where T : class, INotifyPropertyChanged
+ {
+ ArgumentNullException.ThrowIfNull(getCurrentViewModel);
+ ArgumentNullException.ThrowIfNull(addPropertyChangedHandler);
+ ArgumentNullException.ThrowIfNull(removePropertyChangedHandler);
+ ArgumentNullException.ThrowIfNull(viewModelPropertyName);
+ ArgumentNullException.ThrowIfNull(stateHasChangedCallback);
+
+ // Create a shared stream of non-null view models:
+ // - Emits the current ViewModel once (if non-null)
+ // - Emits subsequent non-null ViewModel assignments
+ // The Publish().RefCount(2) pattern shares the subscription between two consumers
+ var viewModelChanged = CreateViewModelChangedStream(
+ getCurrentViewModel,
+ addPropertyChangedHandler,
+ removePropertyChangedHandler,
+ viewModelPropertyName)
+ .Publish()
+ .RefCount(2);
+
+ return new CompositeDisposable
+ {
+ // Skip the initial value to avoid an immediate extra render on first render
+ viewModelChanged
+ .Skip(1)
+ .Subscribe(_ => stateHasChangedCallback()),
+
+ // Re-render on any ViewModel property change
+ // Switch unsubscribes from the previous ViewModel automatically when it changes
+ viewModelChanged
+ .Select(static vm => CreatePropertyChangedPulse(vm))
+ .Switch()
+ .Subscribe(_ => stateHasChangedCallback())
+ };
+ }
+}
diff --git a/src/ReactiveUI.Blazor/Internal/ReactiveComponentState.cs b/src/ReactiveUI.Blazor/Internal/ReactiveComponentState.cs
new file mode 100644
index 0000000000..29d94f45a8
--- /dev/null
+++ b/src/ReactiveUI.Blazor/Internal/ReactiveComponentState.cs
@@ -0,0 +1,163 @@
+// Copyright (c) 2025 .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.Diagnostics.CodeAnalysis;
+
+namespace ReactiveUI.Blazor.Internal;
+
+///
+/// Internal state container for reactive Blazor components.
+/// Manages activation lifecycle, subscriptions, and disposal semantics.
+///
+/// The view model type that implements .
+///
+///
+/// This class encapsulates the common reactive infrastructure shared across all reactive Blazor component base classes,
+/// eliminating code duplication and centralizing allocation patterns for better performance and maintainability.
+///
+///
+/// Performance: All fields are initialized inline to minimize allocation overhead. The state instance should be
+/// created once per component and reused throughout the component's lifetime.
+///
+///
+internal sealed class ReactiveComponentState : IDisposable
+ where T : class, INotifyPropertyChanged
+{
+ ///
+ /// Signals component activation. Emits when is called.
+ ///
+ private readonly Subject _initSubject = new();
+
+ ///
+ /// Signals component deactivation. Emits when is called.
+ ///
+ ///
+ /// Suppressed CA2213 because this subject is used for signaling only and is disposed explicitly in .
+ ///
+ [SuppressMessage("Design", "CA2213:Disposable fields should be disposed", Justification = "Disposed explicitly in Dispose method.")]
+ private readonly Subject _deactivateSubject = new();
+
+ ///
+ /// Holds subscriptions tied to the component lifetime. Disposed when the component is disposed.
+ ///
+ private readonly CompositeDisposable _lifetimeDisposables = [];
+
+ ///
+ /// Holds subscriptions created on first render.
+ ///
+ ///
+ /// This SerialDisposable avoids framework conflicts that occur when certain subscriptions are created
+ /// during OnInitialized rather than OnAfterRender. The subscription is replaced each time
+ /// is assigned.
+ ///
+ private readonly SerialDisposable _firstRenderSubscriptions = new();
+
+ ///
+ /// Indicates whether the state has been disposed. Prevents double disposal.
+ ///
+ private bool _disposed;
+
+ ///
+ /// Gets an observable that emits when the component is activated.
+ ///
+ ///
+ /// This observable emits once during component initialization and can be used to trigger
+ /// reactive activation patterns for view models implementing .
+ ///
+ public IObservable Activated => _initSubject.AsObservable();
+
+ ///
+ /// Gets an observable that emits when the component is deactivated.
+ ///
+ ///
+ /// This observable emits during component disposal, allowing cleanup operations to execute
+ /// while subscriptions are still active.
+ ///
+ public IObservable Deactivated => _deactivateSubject.AsObservable();
+
+ ///
+ /// Gets the composite disposable for lifetime subscriptions.
+ ///
+ ///
+ /// Use this to register subscriptions that should live for the entire component lifetime.
+ /// All subscriptions added here will be disposed when the component is disposed.
+ ///
+ [SuppressMessage("Style", "RCS1085:Use auto-implemented property", Justification = "Explicit field backing provides clarity and follows established pattern in this class.")]
+ public CompositeDisposable LifetimeDisposables => _lifetimeDisposables;
+
+ ///
+ /// Gets or sets the disposable for first-render-only subscriptions.
+ ///
+ ///
+ ///
+ /// This property wraps a to ensure that setting a new subscription
+ /// automatically disposes the previous one. Typically set once during OnAfterRender when firstRender is true.
+ ///
+ ///
+ /// Performance: The property is intentionally implemented with explicit getters and setters rather than
+ /// as an auto-property to provide controlled access to the underlying SerialDisposable's Disposable property,
+ /// ensuring proper disposal semantics.
+ ///
+ ///
+ [SuppressMessage("Style", "RCS1085:Use auto-implemented property", Justification = "Intentional wrapper for SerialDisposable.Disposable property to ensure proper disposal semantics.")]
+ public IDisposable? FirstRenderSubscriptions
+ {
+ get => _firstRenderSubscriptions.Disposable;
+ set => _firstRenderSubscriptions.Disposable = value;
+ }
+
+ ///
+ /// Notifies observers that the component has been activated.
+ ///
+ ///
+ /// Call this method during component initialization (typically in OnInitialized) to signal
+ /// that the component is now active and ready for reactive operations.
+ ///
+ public void NotifyActivated() => _initSubject.OnNext(Unit.Default);
+
+ ///
+ /// Notifies observers that the component is being deactivated.
+ ///
+ ///
+ ///
+ /// Call this method during component disposal to signal that the component is shutting down.
+ /// This notification occurs before subscriptions are disposed, allowing observers to perform
+ /// cleanup while their subscriptions are still active.
+ ///
+ ///
+ /// Performance: This method is typically called once during disposal and incurs minimal overhead.
+ ///
+ ///
+ public void NotifyDeactivated() => _deactivateSubject.OnNext(Unit.Default);
+
+ ///
+ /// Disposes all managed resources held by this state container.
+ ///
+ ///
+ ///
+ /// Disposal order is critical for correct cleanup behavior:
+ /// 1. First-render subscriptions are disposed first (may depend on lifetime subscriptions).
+ /// 2. Lifetime subscriptions are disposed next (general cleanup).
+ /// 3. Subjects are disposed last (signal completion to any remaining observers).
+ ///
+ ///
+ /// This method is idempotent; calling it multiple times has no effect after the first call.
+ ///
+ ///
+ public void Dispose()
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ _firstRenderSubscriptions.Dispose();
+ _lifetimeDisposables.Dispose();
+ _initSubject.Dispose();
+ _deactivateSubject.Dispose();
+
+ _disposed = true;
+ }
+}
diff --git a/src/ReactiveUI.Blazor/ReactiveComponentBase.cs b/src/ReactiveUI.Blazor/ReactiveComponentBase.cs
index 56f6db959e..a13dc3a36d 100644
--- a/src/ReactiveUI.Blazor/ReactiveComponentBase.cs
+++ b/src/ReactiveUI.Blazor/ReactiveComponentBase.cs
@@ -7,23 +7,41 @@
using Microsoft.AspNetCore.Components;
+using ReactiveUI.Blazor.Internal;
+
namespace ReactiveUI.Blazor;
///
-/// A base component for handling property changes and updating the blazer view appropriately.
+/// A base component for handling property changes and updating the Blazor view appropriately.
///
-/// The type of view model. Must support INotifyPropertyChanged.
+/// The type of view model. Must support .
+///
+///
+/// This component triggers when either the view model instance changes or
+/// the current view model raises .
+///
+///
+/// Trimming/AOT: this type avoids expression-tree-based ReactiveUI helpers (e.g. WhenAnyValue) and uses event-based
+/// observables instead.
+///
+///
public class ReactiveComponentBase : ComponentBase, IViewFor, INotifyPropertyChanged, ICanActivate, IDisposable
where T : class, INotifyPropertyChanged
{
- private readonly Subject _initSubject = new();
- [SuppressMessage("Design", "CA2213: Dispose object", Justification = "Used for deactivation.")]
- private readonly Subject _deactivateSubject = new();
- private readonly CompositeDisposable _compositeDisposable = [];
+ ///
+ /// Encapsulates reactive state and lifecycle management for this component.
+ ///
+ private readonly ReactiveComponentState _state = new();
+ ///
+ /// Backing field for .
+ ///
private T? _viewModel;
- private bool _disposedValue; // To detect redundant calls
+ ///
+ /// Indicates whether the instance has been disposed.
+ ///
+ private bool _disposed;
///
public event PropertyChangedEventHandler? PropertyChanged;
@@ -53,15 +71,16 @@ public T? ViewModel
}
///
- public IObservable Activated => _initSubject.AsObservable();
+ public IObservable Activated => _state.Activated;
///
- public IObservable Deactivated => _deactivateSubject.AsObservable();
+ public IObservable Deactivated => _state.Deactivated;
- ///
+ ///
+ /// Disposes the component and releases managed resources.
+ ///
public void Dispose()
{
- // Do not change this code. Put cleanup code in Dispose(bool disposing) below.
Dispose(true);
GC.SuppressFinalize(this);
}
@@ -69,53 +88,23 @@ public void Dispose()
///
protected override void OnInitialized()
{
- if (ViewModel is IActivatableViewModel avm)
- {
- Activated.Subscribe(_ => avm.Activator.Activate()).DisposeWith(_compositeDisposable);
- Deactivated.Subscribe(_ => avm.Activator.Deactivate());
- }
-
- _initSubject.OnNext(Unit.Default);
+ ReactiveComponentHelpers.WireActivationIfSupported(ViewModel, _state);
+ _state.NotifyActivated();
base.OnInitialized();
}
///
-#if NET6_0_OR_GREATER
- [RequiresDynamicCode("OnAfterRender uses methods that require dynamic code generation")]
- [RequiresUnreferencedCode("OnAfterRender uses methods that may require unreferenced code")]
- [SuppressMessage("AOT", "IL3051:'RequiresDynamicCodeAttribute' annotations must match across all interface implementations or overrides.", Justification = "ComponentBase is an external reference")]
- [SuppressMessage("Trimming", "IL2046:'RequiresUnreferencedCodeAttribute' annotations must match across all interface implementations or overrides.", Justification = "ComponentBase is an external reference")]
-#endif
protected override void OnAfterRender(bool firstRender)
{
if (firstRender)
{
- // The following subscriptions are here because if they are done in OnInitialized, they conflict with certain JavaScript frameworks.
- var viewModelChanged =
- this.WhenAnyValue, T?>(nameof(ViewModel))
- .WhereNotNull()
- .Publish()
- .RefCount(2);
-
- viewModelChanged
- .Skip(1) // Skip the initial value to avoid unnecessary re-render when ViewModel changes
- .Subscribe(_ => InvokeAsync(StateHasChanged))
- .DisposeWith(_compositeDisposable);
-
- viewModelChanged
- .Select(x =>
- Observable
- .FromEvent(
- eventHandler =>
- {
- void Handler(object? sender, PropertyChangedEventArgs e) => eventHandler(Unit.Default);
- return Handler;
- },
- eh => x.PropertyChanged += eh,
- eh => x.PropertyChanged -= eh))
- .Switch()
- .Subscribe(_ => InvokeAsync(StateHasChanged))
- .DisposeWith(_compositeDisposable);
+ // These subscriptions are intentionally created here (not OnInitialized) due to framework interop constraints.
+ _state.FirstRenderSubscriptions = ReactiveComponentHelpers.WireViewModelChangeReactivity(
+ () => ViewModel,
+ h => PropertyChanged += h,
+ h => PropertyChanged -= h,
+ nameof(ViewModel),
+ () => InvokeAsync(StateHasChanged));
}
base.OnAfterRender(firstRender);
@@ -124,25 +113,30 @@ protected override void OnAfterRender(bool firstRender)
///
/// Invokes the property changed event.
///
- /// The name of the property.
- protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ /// The name of the changed property.
+ protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) =>
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
///
- /// Cleans up the managed resources of the object.
+ /// Releases managed resources used by the component.
///
- /// If it is getting called by the Dispose() method rather than a finalizer.
+ ///
+ /// to release managed resources; to release unmanaged resources only.
+ ///
protected virtual void Dispose(bool disposing)
{
- if (!_disposedValue)
+ if (_disposed)
{
- if (disposing)
- {
- _initSubject.Dispose();
- _compositeDisposable.Dispose();
- _deactivateSubject.OnNext(Unit.Default);
- }
+ return;
+ }
- _disposedValue = true;
+ if (disposing)
+ {
+ // Notify deactivation first so observers can perform cleanup while subscriptions are still active.
+ _state.NotifyDeactivated();
+ _state.Dispose();
}
+
+ _disposed = true;
}
}
diff --git a/src/ReactiveUI.Blazor/ReactiveInjectableComponentBase.cs b/src/ReactiveUI.Blazor/ReactiveInjectableComponentBase.cs
index 31db8e4315..b630bd3dbc 100644
--- a/src/ReactiveUI.Blazor/ReactiveInjectableComponentBase.cs
+++ b/src/ReactiveUI.Blazor/ReactiveInjectableComponentBase.cs
@@ -7,23 +7,44 @@
using Microsoft.AspNetCore.Components;
+using ReactiveUI.Blazor.Internal;
+
namespace ReactiveUI.Blazor;
///
-/// A base component for handling property changes and updating the blazer view appropriately.
+/// A base component for handling property changes and updating the Blazor view appropriately.
///
-/// The type of view model. Must support INotifyPropertyChanged.
+/// The type of view model. Must support .
+///
+///
+/// This component triggers when either the view model instance changes or
+/// the current view model raises .
+///
+///
+/// Trimming/AOT: this type avoids expression-tree-based ReactiveUI helpers (e.g. WhenAnyValue) and uses event-based
+/// observables instead.
+///
+///
+/// The is provided via DI using .
+///
+///
public class ReactiveInjectableComponentBase : ComponentBase, IViewFor, INotifyPropertyChanged, ICanActivate, IDisposable
where T : class, INotifyPropertyChanged
{
- private readonly Subject _initSubject = new();
- [SuppressMessage("Design", "CA2213: Dispose object", Justification = "Used for deactivation.")]
- private readonly Subject _deactivateSubject = new();
- private readonly CompositeDisposable _compositeDisposable = [];
+ ///
+ /// Encapsulates reactive state and lifecycle management for this component.
+ ///
+ private readonly ReactiveComponentState _state = new();
+ ///
+ /// Backing field for .
+ ///
private T? _viewModel;
- private bool _disposedValue; // To detect redundant calls
+ ///
+ /// Indicates whether the instance has been disposed.
+ ///
+ private bool _disposed;
///
public event PropertyChangedEventHandler? PropertyChanged;
@@ -53,15 +74,16 @@ public T? ViewModel
}
///
- public IObservable Activated => _initSubject.AsObservable();
+ public IObservable Activated => _state.Activated;
///
- public IObservable Deactivated => _deactivateSubject.AsObservable();
+ public IObservable Deactivated => _state.Deactivated;
- ///
+ ///
+ /// Disposes the component and releases managed resources.
+ ///
public void Dispose()
{
- // Do not change this code. Put cleanup code in Dispose(bool disposing) below.
Dispose(true);
GC.SuppressFinalize(this);
}
@@ -69,55 +91,23 @@ public void Dispose()
///
protected override void OnInitialized()
{
- if (ViewModel is IActivatableViewModel avm)
- {
- Activated.Subscribe(_ => avm.Activator.Activate()).DisposeWith(_compositeDisposable);
- Deactivated.Subscribe(_ => avm.Activator.Deactivate());
- }
-
- _initSubject.OnNext(Unit.Default);
-
+ ReactiveComponentHelpers.WireActivationIfSupported(ViewModel, _state);
+ _state.NotifyActivated();
base.OnInitialized();
}
///
-
-#if NET6_0_OR_GREATER
- [RequiresDynamicCode("OnAfterRender uses methods that require dynamic code generation")]
- [RequiresUnreferencedCode("OnAfterRender uses methods that may require unreferenced code")]
- [SuppressMessage("AOT", "IL3051:'RequiresDynamicCodeAttribute' annotations must match across all interface implementations or overrides.", Justification = "ComponentBase is an external reference")]
- [SuppressMessage("Trimming", "IL2046:'RequiresUnreferencedCodeAttribute' annotations must match across all interface implementations or overrides.", Justification = "ComponentBase is an external reference")]
-#endif
protected override void OnAfterRender(bool firstRender)
{
if (firstRender)
{
- // The following subscriptions are here because if they are done in OnInitialized, they conflict with certain JavaScript frameworks.
- var viewModelChanged =
- this.WhenAnyValue, T?>(nameof(ViewModel))
- .WhereNotNull()
- .Publish()
- .RefCount(2);
-
- viewModelChanged
- .Skip(1) // Skip the initial value to avoid unnecessary re-render when ViewModel changes
- .Subscribe(_ => InvokeAsync(StateHasChanged))
- .DisposeWith(_compositeDisposable);
-
- viewModelChanged
- .Select(x =>
- Observable
- .FromEvent(
- eventHandler =>
- {
- void Handler(object? sender, PropertyChangedEventArgs e) => eventHandler(Unit.Default);
- return Handler;
- },
- eh => x.PropertyChanged += eh,
- eh => x.PropertyChanged -= eh))
- .Switch()
- .Subscribe(_ => InvokeAsync(StateHasChanged))
- .DisposeWith(_compositeDisposable);
+ // These subscriptions are intentionally created here (not OnInitialized) due to framework interop constraints.
+ _state.FirstRenderSubscriptions = ReactiveComponentHelpers.WireViewModelChangeReactivity(
+ () => ViewModel,
+ h => PropertyChanged += h,
+ h => PropertyChanged -= h,
+ nameof(ViewModel),
+ () => InvokeAsync(StateHasChanged));
}
base.OnAfterRender(firstRender);
@@ -126,25 +116,30 @@ protected override void OnAfterRender(bool firstRender)
///
/// Invokes the property changed event.
///
- /// The name of the property.
- protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ /// The name of the changed property.
+ protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) =>
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
///
- /// Cleans up the managed resources of the object.
+ /// Releases managed resources used by the component.
///
- /// If it is getting called by the Dispose() method rather than a finalizer.
+ ///
+ /// to release managed resources; to release unmanaged resources only.
+ ///
protected virtual void Dispose(bool disposing)
{
- if (!_disposedValue)
+ if (_disposed)
{
- if (disposing)
- {
- _initSubject.Dispose();
- _compositeDisposable.Dispose();
- _deactivateSubject.OnNext(Unit.Default);
- }
+ return;
+ }
- _disposedValue = true;
+ if (disposing)
+ {
+ // Notify deactivation first so observers can perform cleanup while subscriptions are still active.
+ _state.NotifyDeactivated();
+ _state.Dispose();
}
+
+ _disposed = true;
}
}
diff --git a/src/ReactiveUI.Blazor/ReactiveLayoutComponentBase.cs b/src/ReactiveUI.Blazor/ReactiveLayoutComponentBase.cs
index 5206d4c561..af06a3696f 100644
--- a/src/ReactiveUI.Blazor/ReactiveLayoutComponentBase.cs
+++ b/src/ReactiveUI.Blazor/ReactiveLayoutComponentBase.cs
@@ -7,23 +7,41 @@
using Microsoft.AspNetCore.Components;
+using ReactiveUI.Blazor.Internal;
+
namespace ReactiveUI.Blazor;
///
-/// A base component for handling property changes and updating the blazer view appropriately.
+/// A base component for handling property changes and updating the Blazor view appropriately.
///
-/// The type of view model. Must support INotifyPropertyChanged.
+/// The type of view model. Must support .
+///
+///
+/// This component supports ReactiveUI activation semantics and triggers
+/// when either the view model instance changes or the current view model raises .
+///
+///
+/// Trimming/AOT: this type avoids expression-tree-based ReactiveUI helpers (e.g. WhenAnyValue) and uses event-based
+/// observables instead.
+///
+///
public class ReactiveLayoutComponentBase : LayoutComponentBase, IViewFor, INotifyPropertyChanged, ICanActivate, IDisposable
where T : class, INotifyPropertyChanged
{
- private readonly Subject _initSubject = new();
- [SuppressMessage("Design", "CA2213: Dispose object", Justification = "Used for deactivation.")]
- private readonly Subject _deactivateSubject = new();
- private readonly CompositeDisposable _compositeDisposable = [];
+ ///
+ /// Encapsulates reactive state and lifecycle management for this component.
+ ///
+ private readonly ReactiveComponentState _state = new();
+ ///
+ /// Backing field for .
+ ///
private T? _viewModel;
- private bool _disposedValue; // To detect redundant calls
+ ///
+ /// Indicates whether the instance has been disposed.
+ ///
+ private bool _disposed;
///
public event PropertyChangedEventHandler? PropertyChanged;
@@ -53,15 +71,16 @@ public T? ViewModel
}
///
- public IObservable Activated => _initSubject.AsObservable();
+ public IObservable Activated => _state.Activated;
///
- public IObservable Deactivated => _deactivateSubject.AsObservable();
+ public IObservable Deactivated => _state.Deactivated;
- ///
+ ///
+ /// Disposes the component and releases managed resources.
+ ///
public void Dispose()
{
- // Do not change this code. Put cleanup code in Dispose(bool disposing) below.
Dispose(true);
GC.SuppressFinalize(this);
}
@@ -69,81 +88,54 @@ public void Dispose()
///
protected override void OnInitialized()
{
- if (ViewModel is IActivatableViewModel avm)
- {
- Activated.Subscribe(_ => avm.Activator.Activate()).DisposeWith(_compositeDisposable);
- Deactivated.Subscribe(_ => avm.Activator.Deactivate());
- }
-
- _initSubject.OnNext(Unit.Default);
+ ReactiveComponentHelpers.WireActivationIfSupported(ViewModel, _state);
+ _state.NotifyActivated();
base.OnInitialized();
}
///
-#pragma warning disable RCS1168 // Parameter name differs from base name.
-#if NET6_0_OR_GREATER
- [RequiresDynamicCode("OnAfterRender uses methods that require dynamic code generation")]
- [RequiresUnreferencedCode("OnAfterRender uses methods that may require unreferenced code")]
- [SuppressMessage("AOT", "IL3051:'RequiresDynamicCodeAttribute' annotations must match across all interface implementations or overrides.", Justification = "LayoutComponentBase is an external reference")]
- [SuppressMessage("Trimming", "IL2046:'RequiresUnreferencedCodeAttribute' annotations must match across all interface implementations or overrides.", Justification = "LayoutComponentBase is an external reference")]
-#endif
- protected override void OnAfterRender(bool isFirstRender)
-#pragma warning restore RCS1168 // Parameter name differs from base name.
+ protected override void OnAfterRender(bool firstRender)
{
- if (isFirstRender)
+ if (firstRender)
{
- var viewModelChanged =
- this.WhenAnyValue, T?>(nameof(ViewModel))
- .WhereNotNull()
- .Publish()
- .RefCount(2);
-
- viewModelChanged
- .Skip(1) // Skip the initial value to avoid unnecessary re-render when ViewModel changes
- .Subscribe(_ => InvokeAsync(StateHasChanged))
- .DisposeWith(_compositeDisposable);
-
- viewModelChanged
- .Select(x =>
- Observable
- .FromEvent(
- eventHandler =>
- {
- void Handler(object? sender, PropertyChangedEventArgs e) => eventHandler(Unit.Default);
- return Handler;
- },
- eh => x.PropertyChanged += eh,
- eh => x.PropertyChanged -= eh))
- .Switch()
- .Subscribe(_ => InvokeAsync(StateHasChanged))
- .DisposeWith(_compositeDisposable);
+ _state.FirstRenderSubscriptions = ReactiveComponentHelpers.WireViewModelChangeReactivity(
+ () => ViewModel,
+ h => PropertyChanged += h,
+ h => PropertyChanged -= h,
+ nameof(ViewModel),
+ () => InvokeAsync(StateHasChanged));
}
- base.OnAfterRender(isFirstRender);
+ base.OnAfterRender(firstRender);
}
///
/// Invokes the property changed event.
///
/// The name of the property.
- protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) =>
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
///
- /// Cleans up the managed resources of the object.
+ /// Releases managed resources used by the component.
///
- /// If it is getting called by the Dispose() method rather than a finalizer.
+ ///
+ /// to release managed resources; to release unmanaged resources only.
+ ///
protected virtual void Dispose(bool disposing)
{
- if (!_disposedValue)
+ if (_disposed)
{
- if (disposing)
- {
- _initSubject.Dispose();
- _compositeDisposable.Dispose();
- _deactivateSubject.OnNext(Unit.Default);
- }
+ return;
+ }
- _disposedValue = true;
+ if (disposing)
+ {
+ // Notify deactivation first so observers can perform cleanup while subscriptions are still active.
+ _state.NotifyDeactivated();
+ _state.Dispose();
}
+
+ _disposed = true;
}
}
diff --git a/src/ReactiveUI.Blazor/ReactiveOwningComponentBase.cs b/src/ReactiveUI.Blazor/ReactiveOwningComponentBase.cs
index 3570cdc9e9..362f4e2c2b 100644
--- a/src/ReactiveUI.Blazor/ReactiveOwningComponentBase.cs
+++ b/src/ReactiveUI.Blazor/ReactiveOwningComponentBase.cs
@@ -2,24 +2,45 @@
// 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.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using Microsoft.AspNetCore.Components;
+using ReactiveUI.Blazor.Internal;
+
namespace ReactiveUI.Blazor;
///
-/// A base component for handling property changes and updating the blazer view appropriately.
+/// A base component for handling property changes and updating the Blazor view appropriately.
///
-/// The type of view model. Must support INotifyPropertyChanged.
+/// The type of view model. Must support .
+///
+///
+/// This component triggers when either the view model instance changes or
+/// the current view model raises .
+///
+///
+/// Trimming/AOT: this type avoids expression-tree-based ReactiveUI helpers (e.g. WhenAnyValue) and uses event-based
+/// observables instead.
+///
+///
+/// This type derives from so the DI scope and owned service lifetime are
+/// managed by the base class.
+///
+///
public class ReactiveOwningComponentBase : OwningComponentBase, IViewFor, INotifyPropertyChanged, ICanActivate
where T : class, INotifyPropertyChanged
{
- private readonly Subject _initSubject = new();
- [SuppressMessage("Design", "CA2213: Dispose object", Justification = "Used for deactivation.")]
- private readonly Subject _deactivateSubject = new();
- private readonly CompositeDisposable _compositeDisposable = [];
+ ///
+ /// Encapsulates reactive state and lifecycle management for this component.
+ ///
+ private readonly ReactiveComponentState _state = new();
+ ///
+ /// Backing field for .
+ ///
private T? _viewModel;
///
@@ -52,28 +73,22 @@ public T? ViewModel
}
///
- public IObservable Activated => _initSubject.AsObservable();
+ public IObservable Activated => _state.Activated;
///
- public IObservable Deactivated => _deactivateSubject.AsObservable();
+ public IObservable Deactivated => _state.Deactivated;
///
protected override void OnInitialized()
{
- if (ViewModel is IActivatableViewModel avm)
- {
- Activated.Subscribe(_ => avm.Activator.Activate()).DisposeWith(_compositeDisposable);
- Deactivated.Subscribe(_ => avm.Activator.Deactivate());
- }
-
- _initSubject.OnNext(Unit.Default);
+ ReactiveComponentHelpers.WireActivationIfSupported(ViewModel, _state);
+ _state.NotifyActivated();
base.OnInitialized();
}
///
#if NET6_0_OR_GREATER
- [RequiresDynamicCode("OnAfterRender uses methods that require dynamic code generation")]
- [RequiresUnreferencedCode("OnAfterRender uses methods that may require unreferenced code")]
+ [RequiresUnreferencedCode("OnAfterRender wires reactive subscriptions that may not be trimming-safe in all environments.")]
[SuppressMessage("AOT", "IL3051:'RequiresDynamicCodeAttribute' annotations must match across all interface implementations or overrides.", Justification = "ComponentBase is an external reference")]
[SuppressMessage("Trimming", "IL2046:'RequiresUnreferencedCodeAttribute' annotations must match across all interface implementations or overrides.", Justification = "ComponentBase is an external reference")]
#endif
@@ -81,32 +96,13 @@ protected override void OnAfterRender(bool firstRender)
{
if (firstRender)
{
- // The following subscriptions are here because if they are done in OnInitialized, they conflict with certain JavaScript frameworks.
- var viewModelChanged =
- this.WhenAnyValue, T?>(nameof(ViewModel))
- .WhereNotNull()
- .Publish()
- .RefCount(2);
-
- viewModelChanged
- .Skip(1) // Skip the initial value to avoid unnecessary re-render when ViewModel changes
- .Subscribe(_ => InvokeAsync(StateHasChanged))
- .DisposeWith(_compositeDisposable);
-
- viewModelChanged
- .Select(x =>
- Observable
- .FromEvent(
- eventHandler =>
- {
- void Handler(object? sender, PropertyChangedEventArgs e) => eventHandler(Unit.Default);
- return Handler;
- },
- eh => x.PropertyChanged += eh,
- eh => x.PropertyChanged -= eh))
- .Switch()
- .Subscribe(_ => InvokeAsync(StateHasChanged))
- .DisposeWith(_compositeDisposable);
+ // These subscriptions are intentionally created here (not OnInitialized) due to framework interop constraints.
+ _state.FirstRenderSubscriptions = ReactiveComponentHelpers.WireViewModelChangeReactivity(
+ () => ViewModel,
+ h => PropertyChanged += h,
+ h => PropertyChanged -= h,
+ nameof(ViewModel),
+ () => InvokeAsync(StateHasChanged));
}
base.OnAfterRender(firstRender);
@@ -124,9 +120,11 @@ protected override void Dispose(bool disposing)
{
if (disposing)
{
- _initSubject.Dispose();
- _compositeDisposable.Dispose();
- _deactivateSubject.OnNext(Unit.Default);
+ // Notify deactivation first so observers can perform cleanup while subscriptions are still active.
+ _state.NotifyDeactivated();
+ _state.Dispose();
}
+
+ base.Dispose(disposing);
}
}
diff --git a/src/ReactiveUI.Blazor/Registrations.cs b/src/ReactiveUI.Blazor/Registrations.cs
index 6b95a38a81..7161dbdb8b 100644
--- a/src/ReactiveUI.Blazor/Registrations.cs
+++ b/src/ReactiveUI.Blazor/Registrations.cs
@@ -14,39 +14,33 @@ namespace ReactiveUI.Blazor;
public class Registrations : IWantsToRegisterStuff
{
///
-#if NET6_0_OR_GREATER
- [RequiresDynamicCode("Register uses methods that require dynamic code generation")]
- [RequiresUnreferencedCode("Register uses methods that may require unreferenced code")]
- [SuppressMessage("Trimming", "IL2046:'RequiresUnreferencedCodeAttribute' annotations must match across all interface implementations or overrides.", Justification = "Not all paths use reflection")]
- [SuppressMessage("AOT", "IL3051:'RequiresDynamicCodeAttribute' annotations must match across all interface implementations or overrides.", Justification = "Not all paths use reflection")]
-#endif
- public void Register(Action, Type> registerFunction)
+ public void Register(IRegistrar registrar)
{
#if NET6_0_OR_GREATER
- ArgumentNullException.ThrowIfNull(registerFunction);
+ ArgumentNullException.ThrowIfNull(registrar);
#else
- if (registerFunction is null)
+ if (registrar is null)
{
- throw new ArgumentNullException(nameof(registerFunction));
+ throw new ArgumentNullException(nameof(registrar));
}
#endif
- registerFunction(static () => new StringConverter(), typeof(IBindingTypeConverter));
- registerFunction(static () => new ByteToStringTypeConverter(), typeof(IBindingTypeConverter));
- registerFunction(static () => new NullableByteToStringTypeConverter(), typeof(IBindingTypeConverter));
- registerFunction(static () => new ShortToStringTypeConverter(), typeof(IBindingTypeConverter));
- registerFunction(static () => new NullableShortToStringTypeConverter(), typeof(IBindingTypeConverter));
- registerFunction(static () => new IntegerToStringTypeConverter(), typeof(IBindingTypeConverter));
- registerFunction(static () => new NullableIntegerToStringTypeConverter(), typeof(IBindingTypeConverter));
- registerFunction(static () => new LongToStringTypeConverter(), typeof(IBindingTypeConverter));
- registerFunction(static () => new NullableLongToStringTypeConverter(), typeof(IBindingTypeConverter));
- registerFunction(static () => new SingleToStringTypeConverter(), typeof(IBindingTypeConverter));
- registerFunction(static () => new NullableSingleToStringTypeConverter(), typeof(IBindingTypeConverter));
- registerFunction(static () => new DoubleToStringTypeConverter(), typeof(IBindingTypeConverter));
- registerFunction(static () => new NullableDoubleToStringTypeConverter(), typeof(IBindingTypeConverter));
- registerFunction(static () => new DecimalToStringTypeConverter(), typeof(IBindingTypeConverter));
- registerFunction(static () => new NullableDecimalToStringTypeConverter(), typeof(IBindingTypeConverter));
- registerFunction(static () => new PlatformOperations(), typeof(IPlatformOperations));
+ registrar.RegisterConstant(static () => new StringConverter());
+ registrar.RegisterConstant(static () => new ByteToStringTypeConverter());
+ registrar.RegisterConstant(static () => new NullableByteToStringTypeConverter());
+ registrar.RegisterConstant(static () => new ShortToStringTypeConverter());
+ registrar.RegisterConstant(static () => new NullableShortToStringTypeConverter());
+ registrar.RegisterConstant(static () => new IntegerToStringTypeConverter());
+ registrar.RegisterConstant(static () => new NullableIntegerToStringTypeConverter());
+ registrar.RegisterConstant(static () => new LongToStringTypeConverter());
+ registrar.RegisterConstant(static () => new NullableLongToStringTypeConverter());
+ registrar.RegisterConstant(static () => new SingleToStringTypeConverter());
+ registrar.RegisterConstant(static () => new NullableSingleToStringTypeConverter());
+ registrar.RegisterConstant(static () => new DoubleToStringTypeConverter());
+ registrar.RegisterConstant(static () => new NullableDoubleToStringTypeConverter());
+ registrar.RegisterConstant(static () => new DecimalToStringTypeConverter());
+ registrar.RegisterConstant(static () => new NullableDecimalToStringTypeConverter());
+ registrar.RegisterConstant(static () => new PlatformOperations());
if (Type.GetType("Mono.Runtime") is not null)
{
diff --git a/src/ReactiveUI.Blend/FollowObservableStateBehavior.cs b/src/ReactiveUI.Blend/FollowObservableStateBehavior.cs
index b34af233f3..04c8ff0cb4 100644
--- a/src/ReactiveUI.Blend/FollowObservableStateBehavior.cs
+++ b/src/ReactiveUI.Blend/FollowObservableStateBehavior.cs
@@ -3,10 +3,6 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for full license information.
-#if NET6_0_OR_GREATER
-using System.Diagnostics.CodeAnalysis;
-#endif
-
using System.Reactive.Concurrency;
using System.Windows;
using System.Windows.Controls;
@@ -17,10 +13,6 @@ namespace ReactiveUI.Blend;
///
/// Behavior that tracks the state of an observable.
///
-#if NET6_0_OR_GREATER
-[RequiresDynamicCode("OnStateObservableChanged uses methods that require dynamic code generation")]
-[RequiresUnreferencedCode("OnStateObservableChanged uses methods that may require unreferenced code")]
-#endif
public class FollowObservableStateBehavior : Behavior
{
///
@@ -71,10 +63,6 @@ public FrameworkElement TargetObject
///
/// The sender.
/// The event args.
-#if NET6_0_OR_GREATER
- [RequiresDynamicCode("InternalOnStateObservableChangedForTesting uses methods that require dynamic code generation")]
- [RequiresUnreferencedCode("InternalOnStateObservableChangedForTesting uses methods that may require unreferenced code")]
-#endif
internal static void InternalOnStateObservableChangedForTesting(DependencyObject? sender, DependencyPropertyChangedEventArgs e) =>
OnStateObservableChanged(sender, e);
@@ -83,10 +71,6 @@ internal static void InternalOnStateObservableChangedForTesting(DependencyObject
///
/// The sender.
/// The instance containing the event data.
-#if NET6_0_OR_GREATER
- [RequiresDynamicCode("OnStateObservableChanged uses methods that require dynamic code generation")]
- [RequiresUnreferencedCode("OnStateObservableChanged uses methods that may require unreferenced code")]
-#endif
protected static void OnStateObservableChanged(DependencyObject? sender, DependencyPropertyChangedEventArgs e)
{
ArgumentExceptionHelper.ThrowIfNotOfType(sender);
diff --git a/src/ReactiveUI.Blend/ObservableTrigger.cs b/src/ReactiveUI.Blend/ObservableTrigger.cs
index 3a0eb76adf..dcf44beb0b 100644
--- a/src/ReactiveUI.Blend/ObservableTrigger.cs
+++ b/src/ReactiveUI.Blend/ObservableTrigger.cs
@@ -3,7 +3,6 @@
// 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.Diagnostics.CodeAnalysis;
using System.Reactive.Concurrency;
using System.Windows;
@@ -14,10 +13,6 @@ namespace ReactiveUI.Blend;
///
/// A blend based trigger which will be activated when a IObservable triggers.
///
-#if NET6_0_OR_GREATER
-[RequiresDynamicCode("ObservableTrigger uses methods that require dynamic code generation")]
-[RequiresUnreferencedCode("ObservableTrigger uses methods that may require unreferenced code")]
-#endif
public class ObservableTrigger : TriggerBase
{
///
@@ -53,10 +48,6 @@ public IObservable
/// The sender.
/// The event args.
-#if NET6_0_OR_GREATER
- [RequiresDynamicCode("InternalOnObservableChangedForTesting uses methods that require dynamic code generation")]
- [RequiresUnreferencedCode("InternalOnObservableChangedForTesting uses methods that may require unreferenced code")]
-#endif
internal static void InternalOnObservableChangedForTesting(DependencyObject sender, DependencyPropertyChangedEventArgs e) =>
OnObservableChanged(sender, e);
@@ -65,10 +56,6 @@ internal static void InternalOnObservableChangedForTesting(DependencyObject send
///
/// The sender.
/// The instance containing the event data.
-#if NET6_0_OR_GREATER
- [RequiresDynamicCode("OnObservableChanged uses methods that require dynamic code generation")]
- [RequiresUnreferencedCode("OnObservableChanged uses methods that may require unreferenced code")]
-#endif
protected static void OnObservableChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
ArgumentExceptionHelper.ThrowIfNotOfType(sender, nameof(sender));
diff --git a/src/ReactiveUI.Drawing/Builder/ReactiveUIBuilderDrawingExtensions.cs b/src/ReactiveUI.Drawing/Builder/ReactiveUIBuilderDrawingExtensions.cs
index e3c5e9c0ee..c5204620f8 100644
--- a/src/ReactiveUI.Drawing/Builder/ReactiveUIBuilderDrawingExtensions.cs
+++ b/src/ReactiveUI.Drawing/Builder/ReactiveUIBuilderDrawingExtensions.cs
@@ -17,10 +17,6 @@ public static class ReactiveUIBuilderDrawingExtensions
///
/// The builder instance.
/// The builder instance for method chaining.
-#if NET6_0_OR_GREATER
- [RequiresDynamicCode("The method uses reflection and will not work in AOT environments.")]
- [RequiresUnreferencedCode("The method uses reflection and will not work in AOT environments.")]
-#endif
public static IReactiveUIBuilder WithDrawing(this IReactiveUIBuilder builder)
{
ArgumentExceptionHelper.ThrowIfNull(builder);
diff --git a/src/ReactiveUI.Drawing/Registrations.cs b/src/ReactiveUI.Drawing/Registrations.cs
index 0b87612a19..3c2157c46b 100644
--- a/src/ReactiveUI.Drawing/Registrations.cs
+++ b/src/ReactiveUI.Drawing/Registrations.cs
@@ -14,18 +14,12 @@ namespace ReactiveUI.Drawing;
public class Registrations : IWantsToRegisterStuff
{
///
-#if NET6_0_OR_GREATER
- [RequiresDynamicCode("Register uses methods that require dynamic code generation")]
- [RequiresUnreferencedCode("Register uses methods that may require unreferenced code")]
- [SuppressMessage("Trimming", "IL2046:'RequiresUnreferencedCodeAttribute' annotations must match across all interface implementations or overrides.", Justification = "Not all paths use reflection")]
- [SuppressMessage("AOT", "IL3051:'RequiresDynamicCodeAttribute' annotations must match across all interface implementations or overrides.", Justification = "Not all paths use reflection")]
-#endif
- public void Register(Action, Type> registerFunction)
+ public void Register(IRegistrar registrar)
{
- ArgumentExceptionHelper.ThrowIfNull(registerFunction);
+ ArgumentExceptionHelper.ThrowIfNull(registrar);
#if NETFRAMEWORK || (NET5_0_OR_GREATER && WINDOWS)
- registerFunction(static () => new PlatformBitmapLoader(), typeof(IBitmapLoader));
+ registrar.RegisterConstant(static () => new PlatformBitmapLoader());
#endif
}
}
diff --git a/src/ReactiveUI.Maui/ActivationForViewFetcher.cs b/src/ReactiveUI.Maui/ActivationForViewFetcher.cs
index 0ad14d4f9a..fcd3d7638a 100644
--- a/src/ReactiveUI.Maui/ActivationForViewFetcher.cs
+++ b/src/ReactiveUI.Maui/ActivationForViewFetcher.cs
@@ -8,6 +8,8 @@
#if WINUI_TARGET
using Microsoft.UI.Xaml;
+using ReactiveUI.Maui.Internal;
+
using Windows.Foundation;
#endif
@@ -39,10 +41,6 @@ public int GetAffinityForView(Type view) =>
? 10 : 0;
///
-#if NET6_0_OR_GREATER
- [RequiresDynamicCode("GetActivationForView uses methods that require dynamic code generation")]
- [RequiresUnreferencedCode("GetActivationForView uses methods that may require unreferenced code")]
-#endif
public IObservable GetActivationForView(IActivatableView view)
{
var activation =
@@ -153,10 +151,6 @@ public IObservable GetActivationForView(IActivatableView view)
return appearing.Merge(disappearing);
}
#else
-#if NET6_0_OR_GREATER
- [RequiresDynamicCode("GetActivationFor uses methods that require dynamic code generation")]
- [RequiresUnreferencedCode("GetActivationFor uses methods that may require unreferenced code")]
-#endif
private static IObservable? GetActivationFor(FrameworkElement? view)
{
if (view is null)
@@ -186,9 +180,16 @@ public IObservable GetActivationForView(IActivatableView view)
x => view.Unloaded += x,
x => view.Unloaded -= x);
+ // Observe IsHitTestVisible property changes using DependencyProperty (AOT-safe)
+ var isHitTestVisible = MauiReactiveHelpers.CreatePropertyValueObservable(
+ view,
+ nameof(view.IsHitTestVisible),
+ FrameworkElement.IsHitTestVisibleProperty,
+ () => view.IsHitTestVisible);
+
return viewLoaded
.Merge(viewUnloaded)
- .Select(b => b ? view.WhenAnyValue(nameof(view.IsHitTestVisible)).SkipWhile(x => !x) : Observables.False)
+ .Select(b => b ? isHitTestVisible.SkipWhile(x => !x) : Observables.False)
.Switch()
.DistinctUntilChanged();
}
diff --git a/src/ReactiveUI.Maui/AutoSuspendHelper.cs b/src/ReactiveUI.Maui/AutoSuspendHelper.cs
index b422cdf358..f0f0ef2f08 100644
--- a/src/ReactiveUI.Maui/AutoSuspendHelper.cs
+++ b/src/ReactiveUI.Maui/AutoSuspendHelper.cs
@@ -10,7 +10,7 @@ namespace ReactiveUI.Maui;
///
///
///
-/// Instantiate this class to wire to MAUI's
+/// Instantiate this class to wire to MAUI's
/// callbacks. The helper propagates OnStart, OnResume, and OnSleep to the suspension host so state
/// drivers created via SetupDefaultSuspendResume can serialize view models consistently across Android, iOS, and
/// desktop targets.
@@ -26,8 +26,8 @@ namespace ReactiveUI.Maui;
/// public App()
/// {
/// _autoSuspendHelper = new AutoSuspendHelper();
-/// RxApp.SuspensionHost.CreateNewAppState = () => new MainState();
-/// RxApp.SuspensionHost.SetupDefaultSuspendResume(new FileSuspensionDriver(FileSystem.AppDataDirectory));
+/// RxSuspension.SuspensionHost.CreateNewAppState = () => new MainState();
+/// RxSuspension.SuspensionHost.SetupDefaultSuspendResume(new FileSuspensionDriver(FileSystem.AppDataDirectory));
/// _autoSuspendHelper.OnCreate();
///
/// InitializeComponent();
@@ -44,10 +44,6 @@ namespace ReactiveUI.Maui;
///
///
///
-#if NET6_0_OR_GREATER
-[RequiresDynamicCode("AutoSuspendHelper uses RxApp.SuspensionHost which requires dynamic code generation")]
-[RequiresUnreferencedCode("AutoSuspendHelper uses RxApp.SuspensionHost which may require unreferenced code")]
-#endif
public partial class AutoSuspendHelper : IEnableLogger, IDisposable
{
private readonly Subject _onSleep = new();
@@ -66,11 +62,11 @@ public partial class AutoSuspendHelper : IEnableLogger, IDisposable
///
public AutoSuspendHelper()
{
- RxApp.SuspensionHost.IsLaunchingNew = _onLaunchingNew;
- RxApp.SuspensionHost.IsResuming = _onResume;
- RxApp.SuspensionHost.IsUnpausing = _onStart;
- RxApp.SuspensionHost.ShouldPersistState = _onSleep;
- RxApp.SuspensionHost.ShouldInvalidateState = UntimelyDemise;
+ RxSuspension.SuspensionHost.IsLaunchingNew = _onLaunchingNew;
+ RxSuspension.SuspensionHost.IsResuming = _onResume;
+ RxSuspension.SuspensionHost.IsUnpausing = _onStart;
+ RxSuspension.SuspensionHost.ShouldPersistState = _onSleep;
+ RxSuspension.SuspensionHost.ShouldInvalidateState = UntimelyDemise;
}
///
diff --git a/src/ReactiveUI.Maui/Builder/MauiReactiveUIBuilderExtensions.cs b/src/ReactiveUI.Maui/Builder/MauiReactiveUIBuilderExtensions.cs
index e155bc7be8..7945f71db1 100644
--- a/src/ReactiveUI.Maui/Builder/MauiReactiveUIBuilderExtensions.cs
+++ b/src/ReactiveUI.Maui/Builder/MauiReactiveUIBuilderExtensions.cs
@@ -61,10 +61,6 @@ public static partial class MauiReactiveUIBuilderExtensions
/// The builder instance.
/// The MAUI dispatcher to use for the main thread scheduler.
/// The builder instance for chaining.
-#if NET6_0_OR_GREATER
- [RequiresDynamicCode("The method uses reflection and will not work in AOT environments.")]
- [RequiresUnreferencedCode("The method uses reflection and will not work in AOT environments.")]
-#endif
public static IReactiveUIBuilder WithMaui(this IReactiveUIBuilder builder, IDispatcher? dispatcher = null)
{
if (builder is null)
@@ -105,10 +101,6 @@ public static MauiAppBuilder UseReactiveUI(this MauiAppBuilder builder, ActionThe dispatcher.
/// A The builder instance for chaining.
/// builder.
-#if NET6_0_OR_GREATER
- [RequiresDynamicCode("The method uses reflection and will not work in AOT environments.")]
- [RequiresUnreferencedCode("The method uses reflection and will not work in AOT environments.")]
-#endif
public static MauiAppBuilder UseReactiveUI(this MauiAppBuilder builder, IDispatcher dispatcher)
{
if (builder is null)
diff --git a/src/ReactiveUI.Maui/Common/AutoDataTemplateBindingHook.cs b/src/ReactiveUI.Maui/Common/AutoDataTemplateBindingHook.cs
index 7e719ec593..6027ac8905 100644
--- a/src/ReactiveUI.Maui/Common/AutoDataTemplateBindingHook.cs
+++ b/src/ReactiveUI.Maui/Common/AutoDataTemplateBindingHook.cs
@@ -34,10 +34,6 @@ public class AutoDataTemplateBindingHook : IPropertyBindingHook
});
///
-#if NET6_0_OR_GREATER
- [RequiresDynamicCode("ExecuteHook uses methods that require dynamic code generation")]
- [RequiresUnreferencedCode("ExecuteHook uses methods that may require unreferenced code")]
-#endif
public bool ExecuteHook(object? source, object target, Func[]> getCurrentViewModelProperties, Func[]> getCurrentViewProperties, BindingDirection direction)
{
ArgumentNullException.ThrowIfNull(getCurrentViewProperties);
diff --git a/src/ReactiveUI.Maui/Common/BooleanToVisibilityTypeConverter.cs b/src/ReactiveUI.Maui/Common/BooleanToVisibilityTypeConverter.cs
index 067c6cf865..cca82c39f9 100644
--- a/src/ReactiveUI.Maui/Common/BooleanToVisibilityTypeConverter.cs
+++ b/src/ReactiveUI.Maui/Common/BooleanToVisibilityTypeConverter.cs
@@ -3,6 +3,8 @@
// 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.Diagnostics.CodeAnalysis;
+
#if WINUI_TARGET
using Microsoft.UI.Xaml;
#else
@@ -16,56 +18,44 @@ namespace ReactiveUI;
#endif
///
-/// This type convert converts between Boolean and XAML Visibility - the
-/// conversionHint is a BooleanToVisibilityHint.
+/// Converts to .
///
-public class BooleanToVisibilityTypeConverter : IBindingTypeConverter
+///
+///
+/// The conversion supports a as the conversion hint parameter:
+///
+///
+/// - - True maps to Visible, False maps to Collapsed.
+/// - - Inverts the boolean before conversion (True → Collapsed, False → Visible).
+/// - - Use Hidden instead of Collapsed for false values (MAUI only, ignored on WinUI).
+///
+///
+/// Hints can be combined using bitwise OR (e.g., BooleanToVisibilityHint.Inverse | BooleanToVisibilityHint.UseHidden).
+///
+///
+public sealed class BooleanToVisibilityTypeConverter : BindingTypeConverter
{
///
- public int GetAffinityForObjects(Type fromType, Type toType)
- {
- if (fromType == typeof(bool) && toType == typeof(Visibility))
- {
- return 10;
- }
-
- if (fromType == typeof(Visibility) && toType == typeof(bool))
- {
- return 10;
- }
-
- return 0;
- }
+ public override int GetAffinityForObjects() => 10;
///
- public bool TryConvert(object? from, Type toType, object? conversionHint, out object result)
+ public override bool TryConvert(bool from, object? conversionHint, [NotNullWhen(true)] out Visibility result)
{
- var hint = conversionHint is BooleanToVisibilityHint visibilityHint ?
- visibilityHint :
- BooleanToVisibilityHint.None;
+ var hint = conversionHint is BooleanToVisibilityHint visibilityHint
+ ? visibilityHint
+ : BooleanToVisibilityHint.None;
- if (toType == typeof(Visibility) && from is bool fromBool)
- {
- var fromAsBool = (hint & BooleanToVisibilityHint.Inverse) != 0 ? !fromBool : fromBool;
+ var value = (hint & BooleanToVisibilityHint.Inverse) != 0 ? !from : from;
#if !WINUI_TARGET
- var notVisible = (hint & BooleanToVisibilityHint.UseHidden) != 0 ? Visibility.Hidden : Visibility.Collapsed;
+ var notVisible = (hint & BooleanToVisibilityHint.UseHidden) != 0
+ ? Visibility.Hidden
+ : Visibility.Collapsed;
#else
- const Visibility notVisible = Visibility.Collapsed;
+ const Visibility notVisible = Visibility.Collapsed;
#endif
- result = fromAsBool ? Visibility.Visible : notVisible;
- return true;
- }
-
- if (from is Visibility fromAsVis)
- {
- result = fromAsVis == Visibility.Visible ^ (hint & BooleanToVisibilityHint.Inverse) == 0;
- }
- else
- {
- result = Visibility.Visible;
- }
+ result = value ? Visibility.Visible : notVisible;
return true;
}
}
diff --git a/src/ReactiveUI.Maui/Common/ReactivePage.cs b/src/ReactiveUI.Maui/Common/ReactivePage.cs
index 2dfda8d0d5..eb492ddc6d 100644
--- a/src/ReactiveUI.Maui/Common/ReactivePage.cs
+++ b/src/ReactiveUI.Maui/Common/ReactivePage.cs
@@ -79,12 +79,8 @@ namespace ReactiveUI;
///
/// The type of the view model backing the view.
///
-#if NET6_0_OR_GREATER
-[RequiresDynamicCode("ReactivePage uses methods that require dynamic code generation")]
-[RequiresUnreferencedCode("ReactivePage uses methods that may require unreferenced code")]
-#endif
[SuppressMessage("WinRT", "CsWinRT1029:Types used in signatures should be WinRT types", Justification = "This is a netstandard2.0 library")]
-public partial class ReactivePage :
+public partial class ReactivePage<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TViewModel> :
Page, IViewFor
where TViewModel : class
{
diff --git a/src/ReactiveUI.Maui/Common/RoutedViewHost.cs b/src/ReactiveUI.Maui/Common/RoutedViewHost.cs
index 31f42c552f..2d6588ba4c 100644
--- a/src/ReactiveUI.Maui/Common/RoutedViewHost.cs
+++ b/src/ReactiveUI.Maui/Common/RoutedViewHost.cs
@@ -7,6 +7,7 @@
using Microsoft.UI.Xaml;
using ReactiveUI;
+using ReactiveUI.Maui.Internal;
namespace ReactiveUI;
@@ -15,6 +16,8 @@ namespace ReactiveUI;
/// the View and wire up the ViewModel whenever a new ViewModel is
/// navigated to. Put this control as the only control in your Window.
///
+[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
{
///
@@ -35,15 +38,12 @@ public partial class RoutedViewHost : TransitioningContentControl, IActivatableV
public static readonly DependencyProperty ViewContractObservableProperty =
DependencyProperty.Register("ViewContractObservable", typeof(IObservable), typeof(RoutedViewHost), new PropertyMetadata(Observable.Default));
+ private readonly CompositeDisposable _subscriptions = [];
private string? _viewContract;
///
/// Initializes a new instance of the class.
///
-#if NET6_0_OR_GREATER
- [RequiresDynamicCode("RoutedViewHost uses methods that require dynamic code generation")]
- [RequiresUnreferencedCode("RoutedViewHost uses methods that may require unreferenced code")]
-#endif
public RoutedViewHost()
{
HorizontalContentAlignment = HorizontalAlignment.Stretch;
@@ -76,20 +76,46 @@ public RoutedViewHost()
.StartWith(platformGetter())
.DistinctUntilChanged();
- IRoutableViewModel? currentViewModel = null;
- var vmAndContract = this.WhenAnyObservable(x => x.Router.CurrentViewModel).Do(x => currentViewModel = x).StartWith(currentViewModel).CombineLatest(
- this.WhenAnyObservable(x => x.ViewContractObservable).Do(x => _viewContract = x).StartWith(ViewContract),
+ // Observe Router property changes using DependencyProperty (AOT-friendly)
+ var routerChanged = MauiReactiveHelpers.CreatePropertyValueObservable(
+ this,
+ nameof(Router),
+ RouterProperty,
+ () => Router);
+
+ // Observe ViewContractObservable property changes using DependencyProperty (AOT-friendly)
+ var viewContractObservableChanged = MauiReactiveHelpers.CreatePropertyValueObservable(
+ this,
+ nameof(ViewContractObservable),
+ ViewContractObservableProperty,
+ () => ViewContractObservable);
+
+ // Observe current view model from router
+ var currentViewModel = routerChanged
+ .Where(router => router is not null)
+ .SelectMany(router => router!.CurrentViewModel)
+ .StartWith((IRoutableViewModel?)null);
+
+ // Flatten the ViewContractObservable observable-of-observable
+ var viewContract = viewContractObservableChanged
+ .SelectMany(x => x ?? Observable.Return(null))
+ .Do(x => _viewContract = x)
+ .StartWith(ViewContract);
+
+ var vmAndContract = currentViewModel.CombineLatest(
+ viewContract,
(viewModel, contract) => (viewModel, contract));
- this.WhenActivated(d =>
- {
- // NB: The DistinctUntilChanged is useful because most views in
- // WinRT will end up getting here twice - once for configuring
- // the RoutedViewHost's ViewModel, and once on load via SizeChanged
- d(vmAndContract.DistinctUntilChanged().Subscribe(
+ // Subscribe directly without WhenActivated
+ // NB: The DistinctUntilChanged is useful because most views in
+ // WinRT will end up getting here twice - once for configuring
+ // the RoutedViewHost's ViewModel, and once on load via SizeChanged
+ vmAndContract
+ .DistinctUntilChanged()
+ .Subscribe(
ResolveViewForViewModel,
- ex => RxApp.DefaultExceptionHandler.OnNext(ex)));
- });
+ ex => RxState.DefaultExceptionHandler.OnNext(ex))
+ .DisposeWith(_subscriptions);
}
///
@@ -140,10 +166,8 @@ public string? ViewContract
///
public IViewLocator? ViewLocator { get; set; }
-#if NET6_0_OR_GREATER
- [RequiresDynamicCode("ResolveViewForViewModel uses methods that require dynamic code generation")]
- [RequiresUnreferencedCode("ResolveViewForViewModel uses methods that may require unreferenced code")]
-#endif
+ [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)
{
if (x.viewModel is null)
diff --git a/src/ReactiveUI.Maui/Common/RoutedViewHost{TViewModel}.cs b/src/ReactiveUI.Maui/Common/RoutedViewHost{TViewModel}.cs
new file mode 100644
index 0000000000..fdc50e4f07
--- /dev/null
+++ b/src/ReactiveUI.Maui/Common/RoutedViewHost{TViewModel}.cs
@@ -0,0 +1,191 @@
+// Copyright (c) 2025 .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.
+
+#if WINUI_TARGET
+using Microsoft.UI.Xaml;
+
+using ReactiveUI;
+using ReactiveUI.Maui.Internal;
+
+namespace ReactiveUI;
+
+///
+/// This control hosts the View associated with a Router, and will display
+/// the View and wire up the ViewModel whenever a new ViewModel is
+/// navigated to. Put this control as the only control in your Window.
+/// This generic version provides AOT-compatibility by using compile-time type information.
+///
+/// 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
+ where TViewModel : class, IRoutableViewModel
+{
+ ///
+ /// The router dependency property.
+ ///
+ public static readonly DependencyProperty RouterProperty =
+ DependencyProperty.Register("Router", typeof(RoutingState), typeof(RoutedViewHost), new PropertyMetadata(null));
+
+ ///
+ /// The default content property.
+ ///
+ public static readonly DependencyProperty DefaultContentProperty =
+ DependencyProperty.Register("DefaultContent", typeof(object), typeof(RoutedViewHost), new PropertyMetadata(null));
+
+ ///
+ /// The view contract observable property.
+ ///
+ public static readonly DependencyProperty ViewContractObservableProperty =
+ DependencyProperty.Register("ViewContractObservable", typeof(IObservable), typeof(RoutedViewHost), new PropertyMetadata(Observable.Default));
+
+ private readonly CompositeDisposable _subscriptions = [];
+ private string? _viewContract;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public RoutedViewHost()
+ {
+ HorizontalContentAlignment = HorizontalAlignment.Stretch;
+ VerticalContentAlignment = VerticalAlignment.Stretch;
+
+ var platform = AppLocator.Current.GetService();
+ Func platformGetter = () => default;
+
+ if (platform is null)
+ {
+ // NB: This used to be an error but WPF design mode can't read
+ // good or do other stuff good.
+ this.Log().Error("Couldn't find an IPlatformOperations implementation. Please make sure you have installed the latest version of the ReactiveUI packages for your platform. See https://reactiveui.net/docs/getting-started/installation for guidance.");
+ }
+ else
+ {
+ platformGetter = () => platform.GetOrientation();
+ }
+
+ ViewContractObservable = ModeDetector.InUnitTestRunner()
+ ? Observable.Never
+ : Observable.FromEvent(
+ eventHandler =>
+ {
+ void Handler(object sender, SizeChangedEventArgs e) => eventHandler(platformGetter());
+ return Handler;
+ },
+ x => SizeChanged += x,
+ x => SizeChanged -= x)
+ .StartWith(platformGetter())
+ .DistinctUntilChanged();
+
+ // Observe Router property changes using DependencyProperty (AOT-friendly)
+ var routerChanged = MauiReactiveHelpers.CreatePropertyValueObservable(
+ this,
+ nameof(Router),
+ RouterProperty,
+ () => Router);
+
+ // Observe ViewContractObservable property changes using DependencyProperty (AOT-friendly)
+ var viewContractObservableChanged = MauiReactiveHelpers.CreatePropertyValueObservable(
+ this,
+ nameof(ViewContractObservable),
+ ViewContractObservableProperty,
+ () => ViewContractObservable);
+
+ // Observe current view model from router
+ var currentViewModel = routerChanged
+ .Where(router => router is not null)
+ .SelectMany(router => router!.CurrentViewModel)
+ .StartWith((IRoutableViewModel?)null);
+
+ // Flatten the ViewContractObservable observable-of-observable
+ var viewContract = viewContractObservableChanged
+ .SelectMany(x => x ?? Observable.Return(null))
+ .Do(x => _viewContract = x)
+ .StartWith(ViewContract);
+
+ var vmAndContract = currentViewModel.CombineLatest(
+ viewContract,
+ (viewModel, contract) => (viewModel, contract));
+
+ // Subscribe directly without WhenActivated
+ // NB: The DistinctUntilChanged is useful because most views in
+ // WinRT will end up getting here twice - once for configuring
+ // the RoutedViewHost's ViewModel, and once on load via SizeChanged
+ vmAndContract
+ .DistinctUntilChanged()
+ .Subscribe(
+ ResolveViewForViewModel,
+ ex => RxState.DefaultExceptionHandler.OnNext(ex))
+ .DisposeWith(_subscriptions);
+ }
+
+ ///
+ /// Gets or sets the of the view model stack.
+ ///
+ public RoutingState Router
+ {
+ get => (RoutingState)GetValue(RouterProperty);
+ set => SetValue(RouterProperty, value);
+ }
+
+ ///
+ /// Gets or sets the content displayed whenever there is no page currently
+ /// routed.
+ ///
+ public object DefaultContent
+ {
+ get => GetValue(DefaultContentProperty);
+ set => SetValue(DefaultContentProperty, value);
+ }
+
+ ///
+ /// Gets or sets the view contract observable.
+ ///
+ ///
+ /// The view contract observable.
+ ///
+ public IObservable ViewContractObservable
+ {
+ get => (IObservable)GetValue(ViewContractObservableProperty);
+ set => SetValue(ViewContractObservableProperty, value);
+ }
+
+ ///
+ /// Gets or sets the view contract.
+ ///
+ public string? ViewContract
+ {
+ get => _viewContract;
+ set => ViewContractObservable = Observable.Return(value);
+ }
+
+ ///
+ /// Gets or sets the view locator.
+ ///
+ ///
+ /// The view locator.
+ ///
+ public IViewLocator? ViewLocator { get; set; }
+
+ ///
+ /// 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)
+ {
+ if (x.viewModel is null)
+ {
+ Content = DefaultContent;
+ return;
+ }
+
+ var viewLocator = ViewLocator ?? ReactiveUI.ViewLocator.Current;
+
+ // Use the generic ResolveView method - this is AOT-safe!
+ var view = viewLocator.ResolveView(x.contract) ?? viewLocator.ResolveView() ?? throw new Exception($"Couldn't find view for '{typeof(TViewModel).Name}'.");
+ view.ViewModel = x.viewModel as TViewModel;
+ Content = view;
+ }
+}
+#endif
diff --git a/src/ReactiveUI.Maui/Common/ViewModelViewHost.cs b/src/ReactiveUI.Maui/Common/ViewModelViewHost.cs
index 81edeacee0..0286a7ae0e 100644
--- a/src/ReactiveUI.Maui/Common/ViewModelViewHost.cs
+++ b/src/ReactiveUI.Maui/Common/ViewModelViewHost.cs
@@ -12,6 +12,8 @@ namespace ReactiveUI;
/// the ViewModel property and display it. This control is very useful
/// inside a DataTemplate to display the View associated with a ViewModel.
///
+[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 ViewModelViewHost : TransitioningContentControl, IViewFor, IEnableLogger
{
///
@@ -38,15 +40,12 @@ public partial class ViewModelViewHost : TransitioningContentControl, IViewFor,
public static readonly DependencyProperty ContractFallbackByPassProperty =
DependencyProperty.Register("ContractFallbackByPass", typeof(bool), typeof(ViewModelViewHost), new PropertyMetadata(false));
+ private readonly CompositeDisposable _subscriptions = [];
private string? _viewContract;
///
/// Initializes a new instance of the class.
///
-#if NET6_0_OR_GREATER
- [RequiresDynamicCode("ViewModelViewHost uses methods that require dynamic code generation")]
- [RequiresUnreferencedCode("ViewModelViewHost uses methods that may require unreferenced code")]
-#endif
public ViewModelViewHost()
{
var platform = AppLocator.Current.GetService();
@@ -78,19 +77,28 @@ public ViewModelViewHost()
.StartWith(platformGetter())
.DistinctUntilChanged();
- var contractChanged = this.WhenAnyObservable(x => x.ViewContractObservable).Do(x => _viewContract = x).StartWith(ViewContract);
- var viewModelChanged = this.WhenAnyValue(nameof(ViewModel)).StartWith(ViewModel);
- var vmAndContract = contractChanged
+ // Observe ViewModel property changes without expression trees (AOT-friendly)
+ var viewModelChanged = MauiReactiveHelpers.CreatePropertyValueObservable(
+ this,
+ nameof(ViewModel),
+ ViewModelProperty,
+ () => ViewModel);
+
+ // Combine contract observable with ViewModel changes
+ var vmAndContract = ViewContractObservable
+ .Do(x => _viewContract = x)
.CombineLatest(viewModelChanged, (contract, vm) => (ViewModel: vm, Contract: contract));
- this.WhenActivated(d =>
- {
- d(contractChanged
+ // Subscribe directly without WhenActivated
+ ViewContractObservable
.ObserveOn(RxSchedulers.MainThreadScheduler)
- .Subscribe(x => _viewContract = x ?? string.Empty));
+ .Subscribe(x => _viewContract = x ?? string.Empty)
+ .DisposeWith(_subscriptions);
- d(vmAndContract.DistinctUntilChanged().Subscribe(x => ResolveViewForViewModel(x.ViewModel, x.Contract)));
- });
+ vmAndContract
+ .DistinctUntilChanged()
+ .Subscribe(x => ResolveViewForViewModel(x.ViewModel, x.Contract))
+ .DisposeWith(_subscriptions);
}
///
@@ -148,10 +156,8 @@ public bool ContractFallbackByPass
///
/// ViewModel.
/// contract used by ViewLocator.
-#if NET6_0_OR_GREATER
- [RequiresDynamicCode("ViewModelViewHost uses methods that require dynamic code generation")]
- [RequiresUnreferencedCode("ViewModelViewHost uses methods that may require unreferenced code")]
-#endif
+ [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.")]
protected virtual void ResolveViewForViewModel(object? viewModel, string? contract)
{
if (viewModel is null)
diff --git a/src/ReactiveUI.Maui/Common/ViewModelViewHost{TViewModel}.cs b/src/ReactiveUI.Maui/Common/ViewModelViewHost{TViewModel}.cs
new file mode 100644
index 0000000000..56e1aa282c
--- /dev/null
+++ b/src/ReactiveUI.Maui/Common/ViewModelViewHost{TViewModel}.cs
@@ -0,0 +1,197 @@
+// Copyright (c) 2025 .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;
+
+namespace ReactiveUI;
+
+///
+/// This content control will automatically load the View associated with
+/// the ViewModel property and display it. This control is very useful
+/// inside a DataTemplate to display the View associated with a ViewModel.
+/// This generic version provides AOT-compatibility by using compile-time type information.
+///
+/// The type of the view model. Must have a public parameterless constructor.
+public partial class ViewModelViewHost<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TViewModel> : TransitioningContentControl, IViewFor, IEnableLogger
+ where TViewModel : class
+{
+ ///
+ /// The default content dependency property.
+ ///
+ public static readonly DependencyProperty DefaultContentProperty =
+ DependencyProperty.Register(nameof(DefaultContent), typeof(object), typeof(ViewModelViewHost), new PropertyMetadata(null));
+
+ ///
+ /// The view model dependency property.
+ ///
+ public static readonly DependencyProperty ViewModelProperty =
+ DependencyProperty.Register(nameof(ViewModel), typeof(TViewModel), typeof(ViewModelViewHost), new PropertyMetadata(null));
+
+ ///
+ /// The view contract observable dependency property.
+ ///
+ public static readonly DependencyProperty ViewContractObservableProperty =
+ DependencyProperty.Register(nameof(ViewContractObservable), typeof(IObservable), typeof(ViewModelViewHost), new PropertyMetadata(Observable.Default));
+
+ ///
+ /// The ContractFallbackByPass dependency property.
+ ///
+ public static readonly DependencyProperty ContractFallbackByPassProperty =
+ DependencyProperty.Register("ContractFallbackByPass", typeof(bool), typeof(ViewModelViewHost), new PropertyMetadata(false));
+
+ private readonly CompositeDisposable _subscriptions = [];
+ private string? _viewContract;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ViewModelViewHost()
+ {
+ var platform = AppLocator.Current.GetService();
+ Func platformGetter = () => default;
+
+ if (platform is null)
+ {
+ // NB: This used to be an error but WPF design mode can't read
+ // good or do other stuff good.
+ this.Log().Error("Couldn't find an IPlatformOperations implementation. Please make sure you have installed the latest version of the ReactiveUI packages for your platform. See https://reactiveui.net/docs/getting-started/installation for guidance.");
+ }
+ else
+ {
+ platformGetter = () => platform.GetOrientation();
+ }
+
+ ViewContractObservable = ModeDetector.InUnitTestRunner()
+ ? Observable.Never
+ : Observable.FromEvent(
+ eventHandler =>
+ {
+#pragma warning disable SA1313 // Parameter names should begin with lower-case letter
+ void Handler(object? _, SizeChangedEventArgs __) => eventHandler(platformGetter());
+#pragma warning restore SA1313 // Parameter names should begin with lower-case letter
+ return Handler;
+ },
+ x => SizeChanged += x,
+ x => SizeChanged -= x)
+ .StartWith(platformGetter())
+ .DistinctUntilChanged();
+
+ // Observe ViewModel property changes without expression trees (AOT-friendly)
+ var viewModelChanged = MauiReactiveHelpers.CreatePropertyValueObservable(
+ this,
+ nameof(ViewModel),
+ ViewModelProperty,
+ () => ViewModel);
+
+ // Combine contract observable with ViewModel changes
+ var vmAndContract = ViewContractObservable
+ .Do(x => _viewContract = x)
+ .CombineLatest(viewModelChanged, (contract, vm) => (ViewModel: vm, Contract: contract));
+
+ // Subscribe directly without WhenActivated
+ ViewContractObservable
+ .ObserveOn(RxSchedulers.MainThreadScheduler)
+ .Subscribe(x => _viewContract = x ?? string.Empty)
+ .DisposeWith(_subscriptions);
+
+ vmAndContract
+ .DistinctUntilChanged()
+ .Subscribe(x => ResolveViewForViewModel(x.ViewModel, x.Contract))
+ .DisposeWith(_subscriptions);
+ }
+
+ ///
+ /// Gets or sets the view contract observable.
+ ///
+ public IObservable ViewContractObservable
+ {
+ get => (IObservable)GetValue(ViewContractObservableProperty);
+ set => SetValue(ViewContractObservableProperty, value);
+ }
+
+ ///
+ /// Gets or sets the content displayed by default when no content is set.
+ ///
+ public object DefaultContent
+ {
+ get => GetValue(DefaultContentProperty);
+ set => SetValue(DefaultContentProperty, value);
+ }
+
+ ///
+ /// Gets or sets the ViewModel to display.
+ ///
+ public TViewModel? ViewModel
+ {
+ get => (TViewModel?)GetValue(ViewModelProperty);
+ set => SetValue(ViewModelProperty, value);
+ }
+
+ ///
+ /// Gets or sets the ViewModel to display (non-generic interface implementation).
+ ///
+ object? IViewFor.ViewModel
+ {
+ get => ViewModel;
+ set => ViewModel = value as TViewModel;
+ }
+
+ ///
+ /// Gets or sets the view contract.
+ ///
+ public string? ViewContract
+ {
+ get => _viewContract;
+ set => ViewContractObservable = Observable.Return(value);
+ }
+
+ ///
+ /// Gets or sets a value indicating whether should bypass the default contract fallback behavior.
+ ///
+ public bool ContractFallbackByPass
+ {
+ get => (bool)GetValue(ContractFallbackByPassProperty);
+ set => SetValue(ContractFallbackByPassProperty, value);
+ }
+
+ ///
+ /// Gets or sets the view locator.
+ ///
+ public IViewLocator? ViewLocator { get; set; }
+
+ ///
+ /// resolve view for view model with respect to contract.
+ ///
+ /// ViewModel.
+ /// contract used by ViewLocator.
+ protected virtual void ResolveViewForViewModel(TViewModel? viewModel, string? contract)
+ {
+ if (viewModel is null)
+ {
+ Content = DefaultContent;
+ return;
+ }
+
+ var viewLocator = ViewLocator ?? ReactiveUI.ViewLocator.Current;
+
+ // Use the generic ResolveView method - this is AOT-safe!
+ var viewInstance = viewLocator.ResolveView(contract);
+ if (viewInstance is null && !ContractFallbackByPass)
+ {
+ viewInstance = viewLocator.ResolveView();
+ }
+
+ if (viewInstance is null)
+ {
+ Content = DefaultContent;
+ this.Log().Warn($"The {nameof(ViewModelViewHost)} could not find a valid view for the view model of type {typeof(TViewModel)} and value {viewModel}.");
+ return;
+ }
+
+ viewInstance.ViewModel = viewModel;
+
+ Content = viewInstance;
+ }
+}
diff --git a/src/ReactiveUI.Maui/Common/VisibilityToBooleanTypeConverter.cs b/src/ReactiveUI.Maui/Common/VisibilityToBooleanTypeConverter.cs
new file mode 100644
index 0000000000..0099206d13
--- /dev/null
+++ b/src/ReactiveUI.Maui/Common/VisibilityToBooleanTypeConverter.cs
@@ -0,0 +1,51 @@
+// Copyright (c) 2025 .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.Diagnostics.CodeAnalysis;
+
+#if WINUI_TARGET
+using Microsoft.UI.Xaml;
+#else
+using Microsoft.Maui;
+#endif
+
+#if IS_MAUI
+namespace ReactiveUI.Maui;
+#else
+namespace ReactiveUI;
+#endif
+
+///
+/// Converts to .
+///
+///
+///
+/// The conversion supports a as the conversion hint parameter:
+///
+///
+/// - - Visible maps to True, other values map to False.
+/// - - Inverts the result (Visible → False, other → True).
+///
+///
+/// This converter enables two-way binding between boolean properties and visibility.
+///
+///
+public sealed class VisibilityToBooleanTypeConverter : BindingTypeConverter
+{
+ ///
+ public override int GetAffinityForObjects() => 10;
+
+ ///
+ public override bool TryConvert(Visibility from, object? conversionHint, [NotNullWhen(true)] out bool result)
+ {
+ var hint = conversionHint is BooleanToVisibilityHint visibilityHint
+ ? visibilityHint
+ : BooleanToVisibilityHint.None;
+
+ var isVisible = from == Visibility.Visible;
+ result = (hint & BooleanToVisibilityHint.Inverse) != 0 ? !isVisible : isVisible;
+ return true;
+ }
+}
diff --git a/src/ReactiveUI.Maui/GlobalUsings.cs b/src/ReactiveUI.Maui/GlobalUsings.cs
deleted file mode 100644
index 5a788f7c2d..0000000000
--- a/src/ReactiveUI.Maui/GlobalUsings.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-// Copyright (c) 2025 .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.
-
-global using System;
-global using System.Diagnostics.CodeAnalysis;
-global using System.Linq;
-global using System.Reactive;
-global using System.Reactive.Concurrency;
-global using System.Reactive.Disposables;
-global using System.Reactive.Disposables.Fluent;
-global using System.Reactive.Linq;
-global using System.Reactive.Subjects;
-global using System.Threading.Tasks;
-
-global using Splat;
diff --git a/src/ReactiveUI.Maui/Internal/MauiReactiveHelpers.cs b/src/ReactiveUI.Maui/Internal/MauiReactiveHelpers.cs
new file mode 100644
index 0000000000..b56b85886d
--- /dev/null
+++ b/src/ReactiveUI.Maui/Internal/MauiReactiveHelpers.cs
@@ -0,0 +1,159 @@
+// Copyright (c) 2025 .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.ComponentModel;
+using System.Reactive;
+
+#if IS_WINUI
+using Microsoft.UI.Xaml;
+#endif
+
+namespace ReactiveUI.Maui.Internal;
+
+///
+/// Internal helper methods for reactive operations in MAUI controls.
+/// These methods provide AOT-friendly alternatives to WhenAny* patterns.
+///
+internal static class MauiReactiveHelpers
+{
+ ///
+ /// Creates an observable that emits when the specified property changes on the source object.
+ /// Uses PropertyChanged event directly without expression trees, making it AOT-compatible.
+ ///
+ /// The object to observe.
+ /// The name of the property to observe (use nameof()).
+ /// An observable that emits Unit when the property changes.
+ ///
+ /// This method uses Observable.Create for better performance compared to Observable.FromEvent.
+ /// It filters PropertyChanged events to only emit when the specified property changes.
+ ///
+ public static IObservable CreatePropertyChangedPulse(INotifyPropertyChanged source, string propertyName)
+ {
+ ArgumentNullException.ThrowIfNull(source);
+ ArgumentNullException.ThrowIfNull(propertyName);
+
+ return Observable.Create(observer =>
+ {
+ void Handler(object? sender, PropertyChangedEventArgs e)
+ {
+ if (string.IsNullOrEmpty(e.PropertyName) ||
+ string.Equals(e.PropertyName, propertyName, StringComparison.Ordinal))
+ {
+ observer.OnNext(Unit.Default);
+ }
+ }
+
+ source.PropertyChanged += Handler;
+ return Disposable.Create(() => source.PropertyChanged -= Handler);
+ });
+ }
+
+ ///
+ /// Creates an observable that emits the current value of a property whenever it changes.
+ /// Uses PropertyChanged event directly without expression trees, making it AOT-compatible.
+ ///
+ /// The type of the property value.
+ /// The object to observe (must implement INotifyPropertyChanged).
+ /// The name of the property to observe (use nameof()).
+ /// A function to retrieve the current property value.
+ /// An observable that emits the property value when it changes.
+ ///
+ /// This provides an AOT-friendly alternative to WhenAnyValue by avoiding expression trees.
+ /// The observable immediately emits the current value upon subscription, then emits whenever the property changes.
+ /// This overload works with any INotifyPropertyChanged implementation and is available for MAUI.
+ ///
+ public static IObservable CreatePropertyValueObservable(
+ INotifyPropertyChanged source,
+ string propertyName,
+ Func getPropertyValue)
+ {
+ ArgumentNullException.ThrowIfNull(source);
+ ArgumentNullException.ThrowIfNull(propertyName);
+ ArgumentNullException.ThrowIfNull(getPropertyValue);
+
+ return Observable.Create(observer =>
+ {
+ // Emit initial value
+ observer.OnNext(getPropertyValue());
+
+ void Handler(object? sender, PropertyChangedEventArgs e)
+ {
+ if (string.IsNullOrEmpty(e.PropertyName) ||
+ string.Equals(e.PropertyName, propertyName, StringComparison.Ordinal))
+ {
+ observer.OnNext(getPropertyValue());
+ }
+ }
+
+ source.PropertyChanged += Handler;
+ return Disposable.Create(() => source.PropertyChanged -= Handler);
+ });
+ }
+
+#if IS_WINUI
+ ///
+ /// Creates an observable that emits the current value of a DependencyProperty whenever it changes.
+ /// This is a WinUI-specific overload that avoids reflection by accepting the DependencyProperty directly.
+ ///
+ /// The type of the property value.
+ /// The DependencyObject to observe.
+ /// The name of the property to observe (use nameof()).
+ /// The DependencyProperty to observe.
+ /// A function to retrieve the current property value.
+ /// An observable that emits the property value when it changes.
+ ///
+ /// This provides an AOT-friendly alternative to WhenAnyValue by avoiding expression trees and reflection.
+ /// The observable immediately emits the current value upon subscription, then emits whenever the property changes.
+ ///
+ public static IObservable CreatePropertyValueObservable(
+ DependencyObject source,
+ string propertyName,
+ DependencyProperty property,
+ Func getPropertyValue)
+ {
+ ArgumentNullException.ThrowIfNull(source);
+ ArgumentNullException.ThrowIfNull(propertyName);
+ ArgumentNullException.ThrowIfNull(property);
+ ArgumentNullException.ThrowIfNull(getPropertyValue);
+
+ return Observable.Create(observer =>
+ {
+ // Emit initial value
+ observer.OnNext(getPropertyValue());
+
+ // Register for property changes using the provided DependencyProperty
+ var token = source.RegisterPropertyChangedCallback(property, (sender, dp) =>
+ {
+ observer.OnNext(getPropertyValue());
+ });
+
+ return Disposable.Create(() => source.UnregisterPropertyChangedCallback(property, token));
+ });
+ }
+#endif
+
+ ///
+ /// Wires up activation for a view model that supports activation.
+ ///
+ /// The view model to activate.
+ /// Observable that signals when the view is activated.
+ /// Observable that signals when the view is deactivated.
+ /// A disposable that manages the activation subscriptions.
+ public static IDisposable WireActivationIfSupported(
+ object? viewModel,
+ IObservable activatedSignal,
+ IObservable deactivatedSignal)
+ {
+ if (viewModel is not IActivatableViewModel activatable)
+ {
+ return Disposable.Empty;
+ }
+
+ var activatedSub = activatedSignal.Subscribe(_ => activatable.Activator.Activate());
+ var deactivatedSub = deactivatedSignal.Subscribe(_ => activatable.Activator.Deactivate());
+
+ return new CompositeDisposable(activatedSub, deactivatedSub);
+ }
+}
diff --git a/src/ReactiveUI.Maui/ReactiveCarouselView.cs b/src/ReactiveUI.Maui/ReactiveCarouselView.cs
index 283da108be..eca2058766 100644
--- a/src/ReactiveUI.Maui/ReactiveCarouselView.cs
+++ b/src/ReactiveUI.Maui/ReactiveCarouselView.cs
@@ -13,11 +13,7 @@ namespace ReactiveUI.Maui;
/// The type of the view model.
///
///
-#if NET6_0_OR_GREATER
-[RequiresDynamicCode("ReactiveCarouselView uses methods that require dynamic code generation")]
-[RequiresUnreferencedCode("ReactiveCarouselView uses methods that may require unreferenced code")]
-#endif
-public partial class ReactiveCarouselView : CarouselView, IViewFor
+public partial class ReactiveCarouselView<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TViewModel> : CarouselView, IViewFor
where TViewModel : class
{
///
diff --git a/src/ReactiveUI.Maui/ReactiveContentPage.cs b/src/ReactiveUI.Maui/ReactiveContentPage.cs
index ca201557f4..9091f9ee60 100644
--- a/src/ReactiveUI.Maui/ReactiveContentPage.cs
+++ b/src/ReactiveUI.Maui/ReactiveContentPage.cs
@@ -13,11 +13,7 @@ namespace ReactiveUI.Maui;
/// The type of the view model.
///
///
-#if NET6_0_OR_GREATER
-[RequiresDynamicCode("ReactiveContentPage uses methods that require dynamic code generation")]
-[RequiresUnreferencedCode("ReactiveContentPage uses methods that may require unreferenced code")]
-#endif
-public partial class ReactiveContentPage : ContentPage, IViewFor
+public partial class ReactiveContentPage<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TViewModel> : ContentPage, IViewFor
where TViewModel : class
{
///
diff --git a/src/ReactiveUI.Maui/ReactiveContentView.cs b/src/ReactiveUI.Maui/ReactiveContentView.cs
index b61ef86b86..dabf645a0d 100644
--- a/src/ReactiveUI.Maui/ReactiveContentView.cs
+++ b/src/ReactiveUI.Maui/ReactiveContentView.cs
@@ -13,11 +13,7 @@ namespace ReactiveUI.Maui;
/// The type of the view model.
///
///
-#if NET6_0_OR_GREATER
-[RequiresDynamicCode("ReactiveContentView uses methods that require dynamic code generation")]
-[RequiresUnreferencedCode("ReactiveContentView uses methods that may require unreferenced code")]
-#endif
-public partial class ReactiveContentView : ContentView, IViewFor
+public partial class ReactiveContentView<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TViewModel> : ContentView, IViewFor
where TViewModel : class
{
///
diff --git a/src/ReactiveUI.Maui/ReactiveEntryCell.cs b/src/ReactiveUI.Maui/ReactiveEntryCell.cs
index 85000a137a..d1566b6e17 100644
--- a/src/ReactiveUI.Maui/ReactiveEntryCell.cs
+++ b/src/ReactiveUI.Maui/ReactiveEntryCell.cs
@@ -14,13 +14,9 @@ namespace ReactiveUI.Maui;
/// The type of the view model.
///
///
-#if NET6_0_OR_GREATER
-[RequiresDynamicCode("ReactiveEntryCell uses methods that require dynamic code generation")]
-[RequiresUnreferencedCode("ReactiveEntryCell uses methods that may require unreferenced code")]
-#endif
[Obsolete("ListView and its cells are obsolete in .NET MAUI, please use CollectionView with a DataTemplate and a ReactiveContentView-based view instead. This will be removed in a future release.")]
[ExcludeFromCodeCoverage]
-public partial class ReactiveEntryCell : EntryCell, IViewFor
+public partial class ReactiveEntryCell<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TViewModel> : EntryCell, IViewFor
where TViewModel : class
{
///
diff --git a/src/ReactiveUI.Maui/ReactiveFlyoutPage.cs b/src/ReactiveUI.Maui/ReactiveFlyoutPage.cs
index 5f370fa6f9..191ea310dd 100644
--- a/src/ReactiveUI.Maui/ReactiveFlyoutPage.cs
+++ b/src/ReactiveUI.Maui/ReactiveFlyoutPage.cs
@@ -13,11 +13,7 @@ namespace ReactiveUI.Maui;
/// The type of the view model.
///
///
-#if NET6_0_OR_GREATER
-[RequiresDynamicCode("ReactiveFlyoutPage uses methods that require dynamic code generation")]
-[RequiresUnreferencedCode("ReactiveFlyoutPage uses methods that may require unreferenced code")]
-#endif
-public partial class ReactiveFlyoutPage : FlyoutPage, IViewFor
+public partial class ReactiveFlyoutPage<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TViewModel> : FlyoutPage, IViewFor
where TViewModel : class
{
///
diff --git a/src/ReactiveUI.Maui/ReactiveImageCell.cs b/src/ReactiveUI.Maui/ReactiveImageCell.cs
index 096d3481ab..d89b8452b4 100644
--- a/src/ReactiveUI.Maui/ReactiveImageCell.cs
+++ b/src/ReactiveUI.Maui/ReactiveImageCell.cs
@@ -14,13 +14,9 @@ namespace ReactiveUI.Maui;
/// The type of the view model.
///
///
-#if NET6_0_OR_GREATER
-[RequiresDynamicCode("ReactiveImageCell uses methods that require dynamic code generation")]
-[RequiresUnreferencedCode("ReactiveImageCell uses methods that may require unreferenced code")]
-#endif
[Obsolete("ListView and its cells are obsolete in .NET MAUI, please use CollectionView with a DataTemplate and a ReactiveContentView-based view instead. This will be removed in a future release.")]
[ExcludeFromCodeCoverage]
-public partial class ReactiveImageCell : ImageCell, IViewFor
+public partial class ReactiveImageCell<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TViewModel> : ImageCell, IViewFor
where TViewModel : class
{
///
diff --git a/src/ReactiveUI.Maui/ReactiveImageItemView.cs b/src/ReactiveUI.Maui/ReactiveImageItemView.cs
index 320758b37b..832868b266 100644
--- a/src/ReactiveUI.Maui/ReactiveImageItemView.cs
+++ b/src/ReactiveUI.Maui/ReactiveImageItemView.cs
@@ -15,11 +15,7 @@ namespace ReactiveUI.Maui;
///
/// The type of the view model.
///
-#if NET6_0_OR_GREATER
-[RequiresDynamicCode("ReactiveImageItemView uses methods that require dynamic code generation")]
-[RequiresUnreferencedCode("ReactiveImageItemView uses methods that may require unreferenced code")]
-#endif
-public partial class ReactiveImageItemView : ReactiveContentView
+public partial class ReactiveImageItemView<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TViewModel> : ReactiveContentView
where TViewModel : class
{
///
@@ -67,6 +63,7 @@ public partial class ReactiveImageItemView : ReactiveContentView),
default(Color));
+ private readonly CompositeDisposable _propertyBindings = [];
private readonly Image _image;
private readonly Label _textLabel;
private readonly Label _detailLabel;
@@ -81,20 +78,23 @@ public ReactiveImageItemView()
WidthRequest = 40,
HeightRequest = 40,
VerticalOptions = LayoutOptions.Center,
- HorizontalOptions = LayoutOptions.Start
+ HorizontalOptions = LayoutOptions.Start,
+ Source = ImageSource // Set initial value
};
_textLabel = new Label
{
FontSize = 16,
- VerticalOptions = LayoutOptions.Center
+ VerticalOptions = LayoutOptions.Center,
+ Text = Text // Set initial value
};
_detailLabel = new Label
{
FontSize = 12,
VerticalOptions = LayoutOptions.Center,
- Opacity = 0.7
+ Opacity = 0.7,
+ Text = Detail // Set initial value
};
var textStackLayout = new StackLayout
@@ -116,12 +116,26 @@ public ReactiveImageItemView()
Content = mainStackLayout;
- // Bind the control properties to the bindable properties
- _image.SetBinding(Image.SourceProperty, new Binding(nameof(ImageSource), source: this));
- _textLabel.SetBinding(Label.TextProperty, new Binding(nameof(Text), source: this));
- _textLabel.SetBinding(Label.TextColorProperty, new Binding(nameof(TextColor), source: this));
- _detailLabel.SetBinding(Label.TextProperty, new Binding(nameof(Detail), source: this));
- _detailLabel.SetBinding(Label.TextColorProperty, new Binding(nameof(DetailColor), source: this));
+ // Use expression-based property observation instead of string-based bindings (AOT-safe)
+ MauiReactiveHelpers.CreatePropertyValueObservable(this, nameof(ImageSource), () => ImageSource)
+ .Subscribe(value => _image.Source = value)
+ .DisposeWith(_propertyBindings);
+
+ MauiReactiveHelpers.CreatePropertyValueObservable(this, nameof(Text), () => Text)
+ .Subscribe(value => _textLabel.Text = value)
+ .DisposeWith(_propertyBindings);
+
+ MauiReactiveHelpers.CreatePropertyValueObservable(this, nameof(TextColor), () => TextColor)
+ .Subscribe(value => _textLabel.TextColor = value)
+ .DisposeWith(_propertyBindings);
+
+ MauiReactiveHelpers.CreatePropertyValueObservable(this, nameof(Detail), () => Detail)
+ .Subscribe(value => _detailLabel.Text = value)
+ .DisposeWith(_propertyBindings);
+
+ MauiReactiveHelpers.CreatePropertyValueObservable(this, nameof(DetailColor), () => DetailColor)
+ .Subscribe(value => _detailLabel.TextColor = value)
+ .DisposeWith(_propertyBindings);
}
///
diff --git a/src/ReactiveUI.Maui/ReactiveMasterDetailPage.cs b/src/ReactiveUI.Maui/ReactiveMasterDetailPage.cs
index fd17b2e9a6..a7c9b00cf2 100644
--- a/src/ReactiveUI.Maui/ReactiveMasterDetailPage.cs
+++ b/src/ReactiveUI.Maui/ReactiveMasterDetailPage.cs
@@ -13,11 +13,7 @@ namespace ReactiveUI.Maui;
/// The type of the view model.
///
///
-#if NET6_0_OR_GREATER
-[RequiresDynamicCode("ReactiveMasterDetailPage uses methods that require dynamic code generation")]
-[RequiresUnreferencedCode("ReactiveMasterDetailPage uses methods that may require unreferenced code")]
-#endif
-public partial class ReactiveMasterDetailPage : FlyoutPage, IViewFor
+public partial class ReactiveMasterDetailPage<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TViewModel> : FlyoutPage, IViewFor
where TViewModel : class
{
///
diff --git a/src/ReactiveUI.Maui/ReactiveMultiPage.cs b/src/ReactiveUI.Maui/ReactiveMultiPage.cs
index d7f0b3b0f9..41e3a97670 100644
--- a/src/ReactiveUI.Maui/ReactiveMultiPage.cs
+++ b/src/ReactiveUI.Maui/ReactiveMultiPage.cs
@@ -14,12 +14,7 @@ namespace ReactiveUI.Maui;
/// The type of the view model.
///
///
-#if NET6_0_OR_GREATER
-[RequiresDynamicCode("ReactiveMultiPage uses methods that require dynamic code generation")]
-[RequiresUnreferencedCode("ReactiveMultiPage uses methods that may require unreferenced code")]
-[SuppressMessage("Trimming", "IL2091:Target generic argument does not satisfy 'DynamicallyAccessedMembersAttribute' in target method or type. The generic parameter of the source method or type does not have matching annotations.", Justification = "MultiPage is a third party component")]
-#endif
-public abstract class ReactiveMultiPage : MultiPage, IViewFor
+public abstract class ReactiveMultiPage<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.PublicProperties)] TPage, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TViewModel> : MultiPage, IViewFor
where TPage : Page
where TViewModel : class
{
diff --git a/src/ReactiveUI.Maui/ReactiveNavigationPage.cs b/src/ReactiveUI.Maui/ReactiveNavigationPage.cs
index 4937d5893c..e1701654bc 100644
--- a/src/ReactiveUI.Maui/ReactiveNavigationPage.cs
+++ b/src/ReactiveUI.Maui/ReactiveNavigationPage.cs
@@ -12,11 +12,7 @@ namespace ReactiveUI.Maui;
///
/// The type of the view model.
///
-#if NET6_0_OR_GREATER
-[RequiresDynamicCode("ReactiveNavigationPage uses methods that require dynamic code generation")]
-[RequiresUnreferencedCode("ReactiveNavigationPage uses methods that may require unreferenced code")]
-#endif
-public partial class ReactiveNavigationPage : NavigationPage, IViewFor
+public partial class ReactiveNavigationPage<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TViewModel> : NavigationPage, IViewFor
where TViewModel : class
{
///
diff --git a/src/ReactiveUI.Maui/ReactiveShell.cs b/src/ReactiveUI.Maui/ReactiveShell.cs
index 6879627c46..d28a9d52f7 100644
--- a/src/ReactiveUI.Maui/ReactiveShell.cs
+++ b/src/ReactiveUI.Maui/ReactiveShell.cs
@@ -13,11 +13,7 @@ namespace ReactiveUI.Maui;
/// The type of the view model.
///
///
-#if NET6_0_OR_GREATER
-[RequiresDynamicCode("ReactiveShell uses methods that require dynamic code generation")]
-[RequiresUnreferencedCode("ReactiveShell uses methods that may require unreferenced code")]
-#endif
-public partial class ReactiveShell : Shell, IViewFor
+public partial class ReactiveShell<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TViewModel> : Shell, IViewFor
where TViewModel : class
{
///
diff --git a/src/ReactiveUI.Maui/ReactiveShellContent.cs b/src/ReactiveUI.Maui/ReactiveShellContent.cs
index 294100864b..c3213d8ec9 100644
--- a/src/ReactiveUI.Maui/ReactiveShellContent.cs
+++ b/src/ReactiveUI.Maui/ReactiveShellContent.cs
@@ -13,11 +13,7 @@ namespace ReactiveUI.Maui;
/// The type of the view model.
///
///
-#if NET6_0_OR_GREATER
-[RequiresDynamicCode("ReactiveShellContent uses methods that require dynamic code generation")]
-[RequiresUnreferencedCode("ReactiveShellContent uses methods that may require unreferenced code")]
-#endif
-public partial class ReactiveShellContent : ShellContent, IActivatableView
+public partial class ReactiveShellContent<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TViewModel> : ShellContent, IActivatableView
where TViewModel : class
{
///
diff --git a/src/ReactiveUI.Maui/ReactiveSwitchCell.cs b/src/ReactiveUI.Maui/ReactiveSwitchCell.cs
index 775aff938b..1a6dfc8979 100644
--- a/src/ReactiveUI.Maui/ReactiveSwitchCell.cs
+++ b/src/ReactiveUI.Maui/ReactiveSwitchCell.cs
@@ -14,13 +14,9 @@ namespace ReactiveUI.Maui;
/// The type of the view model.
///
///
-#if NET6_0_OR_GREATER
-[RequiresDynamicCode("ReactiveSwitchCell uses methods that require dynamic code generation")]
-[RequiresUnreferencedCode("ReactiveSwitchCell uses methods that may require unreferenced code")]
-#endif
[Obsolete("ListView and its cells are obsolete in .NET MAUI, please use CollectionView with a DataTemplate and a ReactiveContentView-based view instead. This will be removed in a future release.")]
[ExcludeFromCodeCoverage]
-public partial class ReactiveSwitchCell : SwitchCell, IViewFor
+public partial class ReactiveSwitchCell<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TViewModel> : SwitchCell, IViewFor
where TViewModel : class
{
///
diff --git a/src/ReactiveUI.Maui/ReactiveTabbedPage.cs b/src/ReactiveUI.Maui/ReactiveTabbedPage.cs
index 6ef1b244ce..9c2e555845 100644
--- a/src/ReactiveUI.Maui/ReactiveTabbedPage.cs
+++ b/src/ReactiveUI.Maui/ReactiveTabbedPage.cs
@@ -13,11 +13,7 @@ namespace ReactiveUI.Maui;
/// The type of the view model.
///
///
-#if NET6_0_OR_GREATER
-[RequiresDynamicCode("ReactiveTabbedPage uses methods that require dynamic code generation")]
-[RequiresUnreferencedCode("ReactiveTabbedPage uses methods that may require unreferenced code")]
-#endif
-public partial class ReactiveTabbedPage : TabbedPage, IViewFor
+public partial class ReactiveTabbedPage<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TViewModel> : TabbedPage, IViewFor
where TViewModel : class
{
///
diff --git a/src/ReactiveUI.Maui/ReactiveTextCell.cs b/src/ReactiveUI.Maui/ReactiveTextCell.cs
index e29ae8cc40..ad6f945d37 100644
--- a/src/ReactiveUI.Maui/ReactiveTextCell.cs
+++ b/src/ReactiveUI.Maui/ReactiveTextCell.cs
@@ -14,13 +14,9 @@ namespace ReactiveUI.Maui;
/// The type of the view model.
///
///
-#if NET6_0_OR_GREATER
-[RequiresDynamicCode("ReactiveTextCell uses methods that require dynamic code generation")]
-[RequiresUnreferencedCode("ReactiveTextCell uses methods that may require unreferenced code")]
-#endif
[Obsolete("ListView and its cells are obsolete in .NET MAUI, please use CollectionView with a DataTemplate and a ReactiveContentView-based view instead. This will be removed in a future release.")]
[ExcludeFromCodeCoverage]
-public partial class ReactiveTextCell : TextCell, IViewFor
+public partial class ReactiveTextCell<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TViewModel> : TextCell, IViewFor
where TViewModel : class
{
///
diff --git a/src/ReactiveUI.Maui/ReactiveTextItemView.cs b/src/ReactiveUI.Maui/ReactiveTextItemView.cs
index c1ca2479a4..c23ee8dd32 100644
--- a/src/ReactiveUI.Maui/ReactiveTextItemView.cs
+++ b/src/ReactiveUI.Maui/ReactiveTextItemView.cs
@@ -15,11 +15,7 @@ namespace ReactiveUI.Maui;
///
/// The type of the view model.
///
-#if NET6_0_OR_GREATER
-[RequiresDynamicCode("ReactiveTextItemView uses methods that require dynamic code generation")]
-[RequiresUnreferencedCode("ReactiveTextItemView uses methods that may require unreferenced code")]
-#endif
-public partial class ReactiveTextItemView : ReactiveContentView
+public partial class ReactiveTextItemView<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TViewModel> : ReactiveContentView
where TViewModel : class
{
///
@@ -58,6 +54,7 @@ public partial class ReactiveTextItemView : ReactiveContentView),
default(Color));
+ private readonly CompositeDisposable _propertyBindings = [];
private readonly Label _textLabel;
private readonly Label _detailLabel;
@@ -69,14 +66,16 @@ public ReactiveTextItemView()
_textLabel = new Label
{
FontSize = 16,
- VerticalOptions = LayoutOptions.Center
+ VerticalOptions = LayoutOptions.Center,
+ Text = Text // Set initial value
};
_detailLabel = new Label
{
FontSize = 12,
VerticalOptions = LayoutOptions.Center,
- Opacity = 0.7
+ Opacity = 0.7,
+ Text = Detail // Set initial value
};
var stackLayout = new StackLayout
@@ -89,11 +88,22 @@ public ReactiveTextItemView()
Content = stackLayout;
- // Bind the label properties to the bindable properties
- _textLabel.SetBinding(Label.TextProperty, new Binding(nameof(Text), source: this));
- _textLabel.SetBinding(Label.TextColorProperty, new Binding(nameof(TextColor), source: this));
- _detailLabel.SetBinding(Label.TextProperty, new Binding(nameof(Detail), source: this));
- _detailLabel.SetBinding(Label.TextColorProperty, new Binding(nameof(DetailColor), source: this));
+ // Use expression-based property observation instead of string-based bindings (AOT-safe)
+ MauiReactiveHelpers.CreatePropertyValueObservable(this, nameof(Text), () => Text)
+ .Subscribe(value => _textLabel.Text = value)
+ .DisposeWith(_propertyBindings);
+
+ MauiReactiveHelpers.CreatePropertyValueObservable(this, nameof(TextColor), () => TextColor)
+ .Subscribe(value => _textLabel.TextColor = value)
+ .DisposeWith(_propertyBindings);
+
+ MauiReactiveHelpers.CreatePropertyValueObservable(this, nameof(Detail), () => Detail)
+ .Subscribe(value => _detailLabel.Text = value)
+ .DisposeWith(_propertyBindings);
+
+ MauiReactiveHelpers.CreatePropertyValueObservable(this, nameof(DetailColor), () => DetailColor)
+ .Subscribe(value => _detailLabel.TextColor = value)
+ .DisposeWith(_propertyBindings);
}
///
diff --git a/src/ReactiveUI.Maui/ReactiveUI.Maui.csproj b/src/ReactiveUI.Maui/ReactiveUI.Maui.csproj
index e805586e08..3886fc821a 100644
--- a/src/ReactiveUI.Maui/ReactiveUI.Maui.csproj
+++ b/src/ReactiveUI.Maui/ReactiveUI.Maui.csproj
@@ -22,6 +22,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -39,7 +55,9 @@
+
+
diff --git a/src/ReactiveUI.Maui/ReactiveViewCell.cs b/src/ReactiveUI.Maui/ReactiveViewCell.cs
index d92cc3ed63..c21df79c40 100644
--- a/src/ReactiveUI.Maui/ReactiveViewCell.cs
+++ b/src/ReactiveUI.Maui/ReactiveViewCell.cs
@@ -14,13 +14,9 @@ namespace ReactiveUI.Maui;
/// The type of the view model.
///
///
-#if NET6_0_OR_GREATER
-[RequiresDynamicCode("ReactiveViewCell uses methods that require dynamic code generation")]
-[RequiresUnreferencedCode("ReactiveViewCell uses methods that may require unreferenced code")]
-#endif
[Obsolete("ListView and its cells are obsolete in .NET MAUI, please use CollectionView with a DataTemplate and a ReactiveContentView-based view instead. This will be removed in a future release.")]
[ExcludeFromCodeCoverage]
-public partial class ReactiveViewCell : ViewCell, IViewFor
+public partial class ReactiveViewCell<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TViewModel> : ViewCell, IViewFor
where TViewModel : class
{
///
diff --git a/src/ReactiveUI.Maui/Registrations.cs b/src/ReactiveUI.Maui/Registrations.cs
index 085b68596c..f35817b8b1 100644
--- a/src/ReactiveUI.Maui/Registrations.cs
+++ b/src/ReactiveUI.Maui/Registrations.cs
@@ -22,24 +22,19 @@ namespace ReactiveUI.Maui;
public class Registrations : IWantsToRegisterStuff
{
///
-#if NET6_0_OR_GREATER
- [RequiresDynamicCode("Register uses methods that require dynamic code generation")]
- [RequiresUnreferencedCode("Register uses methods that may require unreferenced code")]
- [SuppressMessage("Trimming", "IL2046:'RequiresUnreferencedCodeAttribute' annotations must match across all interface implementations or overrides.", Justification = "Not all paths use reflection")]
- [SuppressMessage("AOT", "IL3051:'RequiresDynamicCodeAttribute' annotations must match across all interface implementations or overrides.", Justification = "Not all paths use reflection")]
-#endif
- public void Register(Action, Type> registerFunction)
+ public void Register(IRegistrar registrar)
{
- ArgumentNullException.ThrowIfNull(registerFunction);
+ ArgumentNullException.ThrowIfNull(registrar);
- registerFunction(static () => new ActivationForViewFetcher(), typeof(IActivationForViewFetcher));
- registerFunction(static () => new BooleanToVisibilityTypeConverter(), typeof(IBindingTypeConverter));
+ registrar.RegisterConstant(static () => new ActivationForViewFetcher());
+ registrar.RegisterConstant(static () => new BooleanToVisibilityTypeConverter());
+ registrar.RegisterConstant(static () => new VisibilityToBooleanTypeConverter());
#if WINUI_TARGET
- registerFunction(static () => new PlatformOperations(), typeof(IPlatformOperations));
- registerFunction(static () => new DependencyObjectObservableForProperty(), typeof(ICreatesObservableForProperty));
- registerFunction(static () => new AutoDataTemplateBindingHook(), typeof(IPropertyBindingHook));
- registerFunction(static () => new ComponentModelTypeConverter(), typeof(IBindingTypeConverter));
+ registrar.RegisterConstant(static () => new PlatformOperations());
+ registrar.RegisterConstant(static () => new DependencyObjectObservableForProperty());
+ registrar.RegisterConstant(static () => new AutoDataTemplateBindingHook());
+ registrar.RegisterConstant(static () => new ComponentModelFallbackConverter());
if (!ModeDetector.InUnitTestRunner())
{
@@ -47,7 +42,7 @@ public void Register(Action, Type> registerFunction)
RxSchedulers.TaskpoolScheduler = TaskPoolScheduler.Default;
}
- RxApp.SuppressViewCommandBindingMessage = true;
+ RxSchedulers.SuppressViewCommandBindingMessage = true;
#endif
}
}
diff --git a/src/ReactiveUI.Maui/RoutedViewHost.cs b/src/ReactiveUI.Maui/RoutedViewHost.cs
index 5dd74cf5d3..2a099b595e 100644
--- a/src/ReactiveUI.Maui/RoutedViewHost.cs
+++ b/src/ReactiveUI.Maui/RoutedViewHost.cs
@@ -35,130 +35,138 @@ public partial class RoutedViewHost : NavigationPage, IActivatableView, IEnableL
typeof(RoutedViewHost),
false);
+ private readonly CompositeDisposable _subscriptions = [];
private string? _action;
+ private bool _currentlyNavigating;
///
/// Initializes a new instance of the class.
///
/// You *must* register an IScreen class representing your App's main Screen.
-#if NET6_0_OR_GREATER
- [RequiresDynamicCode("RoutedViewHost uses methods that require dynamic code generation")]
- [RequiresUnreferencedCode("RoutedViewHost uses methods that may require unreferenced code")]
-#endif
+ [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 RoutedViewHost()
{
- this.WhenActivated(async disposable =>
- {
- var currentlyNavigating = false;
-
- Observable.FromEventPattern(
- x => Router!.NavigationStack.CollectionChanged += x,
- x => Router!.NavigationStack.CollectionChanged -= x)
- .Where(_ => !currentlyNavigating && Router?.NavigationStack.Count == 0)
- .Subscribe(async _ => await SyncNavigationStacksAsync())
- .DisposeWith(disposable);
-
- Router?
- .NavigateBack
- .Subscribe(async _ =>
+ // Subscribe directly without WhenActivated
+ Observable.FromEventPattern(
+ x => Router!.NavigationStack.CollectionChanged += x,
+ x => Router!.NavigationStack.CollectionChanged -= x)
+ .Where(_ => !_currentlyNavigating && Router?.NavigationStack.Count == 0)
+ .Subscribe(async _ => await SyncNavigationStacksAsync())
+ .DisposeWith(_subscriptions);
+
+ Router?
+ .NavigateBack
+ .Subscribe(async _ =>
+ {
+ try
{
- try
- {
- currentlyNavigating = true;
- await PopAsync();
- }
- finally
- {
- currentlyNavigating = false;
- }
+ _currentlyNavigating = true;
+ await PopAsync();
+ }
+ finally
+ {
+ _currentlyNavigating = false;
+ }
- _action = "NavigatedBack";
- InvalidateCurrentViewModel();
- await SyncNavigationStacksAsync();
- })
- .DisposeWith(disposable);
-
- Router?
- .Navigate
- .Where(_ => StacksAreDifferent())
- .ObserveOn(RxSchedulers.MainThreadScheduler)
- .SelectMany(_ => PagesForViewModel(Router.GetCurrentViewModel()))
- .SelectMany(async page =>
+ _action = "NavigatedBack";
+ InvalidateCurrentViewModel();
+ await SyncNavigationStacksAsync();
+ })
+ .DisposeWith(_subscriptions);
+
+ Router?
+ .Navigate
+ .Where(_ => StacksAreDifferent())
+ .ObserveOn(RxSchedulers.MainThreadScheduler)
+ .SelectMany(_ => PagesForViewModel(Router.GetCurrentViewModel()))
+ .SelectMany(async page =>
+ {
+ var animated = true;
+ var attribute = page.GetType().GetCustomAttribute();
+ if (attribute is not null)
{
- var animated = true;
- var attribute = page.GetType().GetCustomAttribute();
- if (attribute is not null)
- {
- animated = false;
- }
+ animated = false;
+ }
- try
- {
- currentlyNavigating = true;
- await PushAsync(page, animated);
- }
- finally
- {
- currentlyNavigating = false;
- }
+ try
+ {
+ _currentlyNavigating = true;
+ await PushAsync(page, animated);
+ }
+ finally
+ {
+ _currentlyNavigating = false;
+ }
- await SyncNavigationStacksAsync();
-
- return page;
- })
- .Subscribe()
- .DisposeWith(disposable);
-
- var poppingEvent = Observable.FromEvent, Unit>(
- eventHandler =>
- {
- void Handler(object? sender, NavigationEventArgs e) => eventHandler(Unit.Default);
- return Handler;
- },
- x => Popped += x,
- x => Popped -= x);
-
- // NB: User pressed the Application back as opposed to requesting Back via Router.NavigateBack.
- poppingEvent
- .Where(_ => !currentlyNavigating && Router is not null)
- .Subscribe(_ =>
+ await SyncNavigationStacksAsync();
+
+ return page;
+ })
+ .Subscribe()
+ .DisposeWith(_subscriptions);
+
+ var poppingEvent = Observable.FromEvent, Unit>(
+ eventHandler =>
+ {
+ void Handler(object? sender, NavigationEventArgs e) => eventHandler(Unit.Default);
+ return Handler;
+ },
+ x => Popped += x,
+ x => Popped -= x);
+
+ // NB: User pressed the Application back as opposed to requesting Back via Router.NavigateBack.
+ poppingEvent
+ .Where(_ => !_currentlyNavigating && Router is not null)
+ .Subscribe(_ =>
+ {
+ if (Router?.NavigationStack.Count > 0)
{
- if (Router?.NavigationStack.Count > 0)
- {
- Router.NavigationStack.RemoveAt(Router.NavigationStack.Count - 1);
- }
+ Router.NavigationStack.RemoveAt(Router.NavigationStack.Count - 1);
+ }
- _action = "Popped";
- InvalidateCurrentViewModel();
- })
- .DisposeWith(disposable);
-
- var poppingToRootEvent = Observable.FromEvent, Unit>(
- eventHandler =>
- {
- void Handler(object? sender, NavigationEventArgs e) => eventHandler(Unit.Default);
- return Handler;
- },
- x => PoppedToRoot += x,
- x => PoppedToRoot -= x);
-
- poppingToRootEvent
- .Where(_ => !currentlyNavigating && Router is not null)
- .Subscribe(_ =>
+ _action = "Popped";
+ InvalidateCurrentViewModel();
+ })
+ .DisposeWith(_subscriptions);
+
+ var poppingToRootEvent = Observable.FromEvent, Unit>(
+ eventHandler =>
+ {
+ void Handler(object? sender, NavigationEventArgs e) => eventHandler(Unit.Default);
+ return Handler;
+ },
+ x => PoppedToRoot += x,
+ x => PoppedToRoot -= x);
+
+ poppingToRootEvent
+ .Where(_ => !_currentlyNavigating && Router is not null)
+ .Subscribe(_ =>
+ {
+ for (var i = Router?.NavigationStack.Count - 1; i > 0; i--)
{
- for (var i = Router?.NavigationStack.Count - 1; i > 0; i--)
+ if (i.HasValue)
{
- if (i.HasValue)
- {
- Router?.NavigationStack.RemoveAt(i.Value);
- }
+ Router?.NavigationStack.RemoveAt(i.Value);
}
+ }
- _action = "PoppedToRoot";
- InvalidateCurrentViewModel();
- })
- .DisposeWith(disposable);
- await SyncNavigationStacksAsync();
+ _action = "PoppedToRoot";
+ InvalidateCurrentViewModel();
+ })
+ .DisposeWith(_subscriptions);
+
+ // Perform initial sync asynchronously
+ _ = Task.Run(async () =>
+ {
+ try
+ {
+ await SyncNavigationStacksAsync();
+ }
+ catch (Exception ex)
+ {
+ this.Log().Error(ex, "Failed to perform initial navigation stack sync");
+ }
});
var screen = AppLocator.Current.GetService() ?? throw new Exception("You *must* register an IScreen class representing your App's main Screen");
@@ -188,10 +196,8 @@ public bool SetTitleOnNavigate
///
/// The vm.
/// An observable of the page associated to a .
-#if NET6_0_OR_GREATER
- [RequiresDynamicCode("PagesForViewModel uses methods that require dynamic code generation")]
- [RequiresUnreferencedCode("PagesForViewModel uses methods that may require unreferenced code")]
-#endif
+ [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.")]
protected virtual IObservable PagesForViewModel(IRoutableViewModel? vm)
{
if (vm is null)
@@ -223,10 +229,8 @@ protected virtual IObservable PagesForViewModel(IRoutableViewModel? vm)
///
/// The vm.
/// An observable of the page associated to a .
-#if NET6_0_OR_GREATER
- [RequiresDynamicCode("PagesForViewModel uses methods that require dynamic code generation")]
- [RequiresUnreferencedCode("PagesForViewModel uses methods that may require unreferenced code")]
-#endif
+ [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.")]
protected virtual Page PageForViewModel(IRoutableViewModel vm)
{
ArgumentNullException.ThrowIfNull(vm);
@@ -276,10 +280,8 @@ protected void InvalidateCurrentViewModel()
/// to affect manipulations like Add or Clear.
///
/// A representing the asynchronous operation.
-#if NET6_0_OR_GREATER
- [RequiresDynamicCode("SyncNavigationStacksAsync uses methods that require dynamic code generation")]
- [RequiresUnreferencedCode("SyncNavigationStacksAsync uses methods that may require unreferenced code")]
-#endif
+ [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.")]
protected async Task SyncNavigationStacksAsync()
{
if (Navigation.NavigationStack.Count != Router.NavigationStack.Count
diff --git a/src/ReactiveUI.Maui/RoutedViewHost{TViewModel}.cs b/src/ReactiveUI.Maui/RoutedViewHost{TViewModel}.cs
new file mode 100644
index 0000000000..c2c3592d24
--- /dev/null
+++ b/src/ReactiveUI.Maui/RoutedViewHost{TViewModel}.cs
@@ -0,0 +1,332 @@
+// Copyright (c) 2025 .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.Collections.Specialized;
+using System.Reflection;
+
+using Microsoft.Maui.Controls;
+
+namespace ReactiveUI.Maui;
+
+///
+/// This is a generic that serves as a router with compile-time type safety.
+/// This version is fully AOT-compatible and does not use reflection-based view resolution.
+///
+/// The type of the view model. Must have a public parameterless constructor.
+///
+///
+public partial class RoutedViewHost<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TViewModel> : NavigationPage, IActivatableView, IEnableLogger
+ where TViewModel : class, IRoutableViewModel
+{
+ ///
+ /// The router bindable property.
+ ///
+ public static readonly BindableProperty RouterProperty = BindableProperty.Create(
+ nameof(Router),
+ typeof(RoutingState),
+ typeof(RoutedViewHost),
+ default(RoutingState));
+
+ ///
+ /// The Set Title on Navigate property.
+ ///
+ public static readonly BindableProperty SetTitleOnNavigateProperty = BindableProperty.Create(
+ nameof(SetTitleOnNavigate),
+ typeof(bool),
+ typeof(RoutedViewHost),
+ false);
+
+ private readonly CompositeDisposable _subscriptions = [];
+ private string? _action;
+ private bool _currentlyNavigating;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// You *must* register an IScreen class representing your App's main Screen.
+ public RoutedViewHost()
+ {
+ // Subscribe directly without WhenActivated
+ Observable.FromEventPattern(
+ x => Router!.NavigationStack.CollectionChanged += x,
+ x => Router!.NavigationStack.CollectionChanged -= x)
+ .Where(_ => !_currentlyNavigating && Router?.NavigationStack.Count == 0)
+ .Subscribe(async _ => await SyncNavigationStacksAsync())
+ .DisposeWith(_subscriptions);
+
+ Router?
+ .NavigateBack
+ .Subscribe(async _ =>
+ {
+ try
+ {
+ _currentlyNavigating = true;
+ await PopAsync();
+ }
+ finally
+ {
+ _currentlyNavigating = false;
+ }
+
+ _action = "NavigatedBack";
+ InvalidateCurrentViewModel();
+ await SyncNavigationStacksAsync();
+ })
+ .DisposeWith(_subscriptions);
+
+ Router?
+ .Navigate
+ .Where(_ => StacksAreDifferent())
+ .ObserveOn(RxSchedulers.MainThreadScheduler)
+ .SelectMany(_ => PagesForViewModel(Router.GetCurrentViewModel()))
+ .SelectMany(async page =>
+ {
+ var animated = true;
+ var attribute = page.GetType().GetCustomAttribute();
+ if (attribute is not null)
+ {
+ animated = false;
+ }
+
+ try
+ {
+ _currentlyNavigating = true;
+ await PushAsync(page, animated);
+ }
+ finally
+ {
+ _currentlyNavigating = false;
+ }
+
+ await SyncNavigationStacksAsync();
+
+ return page;
+ })
+ .Subscribe()
+ .DisposeWith(_subscriptions);
+
+ var poppingEvent = Observable.FromEvent, Unit>(
+ eventHandler =>
+ {
+ void Handler(object? sender, NavigationEventArgs e) => eventHandler(Unit.Default);
+ return Handler;
+ },
+ x => Popped += x,
+ x => Popped -= x);
+
+ // NB: User pressed the Application back as opposed to requesting Back via Router.NavigateBack.
+ poppingEvent
+ .Where(_ => !_currentlyNavigating && Router is not null)
+ .Subscribe(_ =>
+ {
+ if (Router?.NavigationStack.Count > 0)
+ {
+ Router.NavigationStack.RemoveAt(Router.NavigationStack.Count - 1);
+ }
+
+ _action = "Popped";
+ InvalidateCurrentViewModel();
+ })
+ .DisposeWith(_subscriptions);
+
+ var poppingToRootEvent = Observable.FromEvent, Unit>(
+ eventHandler =>
+ {
+ void Handler(object? sender, NavigationEventArgs e) => eventHandler(Unit.Default);
+ return Handler;
+ },
+ x => PoppedToRoot += x,
+ x => PoppedToRoot -= x);
+
+ poppingToRootEvent
+ .Where(_ => !_currentlyNavigating && Router is not null)
+ .Subscribe(_ =>
+ {
+ for (var i = Router?.NavigationStack.Count - 1; i > 0; i--)
+ {
+ if (i.HasValue)
+ {
+ Router?.NavigationStack.RemoveAt(i.Value);
+ }
+ }
+
+ _action = "PoppedToRoot";
+ InvalidateCurrentViewModel();
+ })
+ .DisposeWith(_subscriptions);
+
+ // Perform initial sync asynchronously
+ _ = Task.Run(async () =>
+ {
+ try
+ {
+ await SyncNavigationStacksAsync();
+ }
+ catch (Exception ex)
+ {
+ this.Log().Error(ex, "Failed to perform initial navigation stack sync");
+ }
+ });
+
+ var screen = AppLocator.Current.GetService() ?? throw new Exception("You *must* register an IScreen class representing your App's main Screen");
+ Router = screen.Router;
+ }
+
+ ///
+ /// Gets or sets the of the view model stack.
+ ///
+ public RoutingState Router
+ {
+ get => (RoutingState)GetValue(RouterProperty);
+ set => SetValue(RouterProperty, value);
+ }
+
+ ///
+ /// Gets or sets a value indicating whether gets or sets the Set Title of the view model stack.
+ ///
+ public bool SetTitleOnNavigate
+ {
+ get => (bool)GetValue(SetTitleOnNavigateProperty);
+ set => SetValue(SetTitleOnNavigateProperty, value);
+ }
+
+ ///
+ /// Pages for view model.
+ ///
+ /// The vm.
+ /// An observable of the page associated to a .
+ protected virtual IObservable PagesForViewModel(IRoutableViewModel? vm)
+ {
+ if (vm is null)
+ {
+ return Observable.Empty();
+ }
+
+ // Use the generic ResolveView method - this is AOT-safe!
+ var ret = ViewLocator.Current.ResolveView();
+ if (ret is null)
+ {
+ var msg = $"Couldn't find a View for ViewModel. You probably need to register an IViewFor<{typeof(TViewModel).Name}>";
+
+ return Observable.Throw(new Exception(msg));
+ }
+
+ ret.ViewModel = vm as TViewModel;
+
+ var pg = (Page)ret;
+ if (SetTitleOnNavigate)
+ {
+ pg.Title = vm.UrlPathSegment;
+ }
+
+ return Observable.Return(pg);
+ }
+
+ ///
+ /// Page for view model.
+ ///
+ /// The vm.
+ /// A page associated to a .
+ protected virtual Page PageForViewModel(IRoutableViewModel vm)
+ {
+ ArgumentNullException.ThrowIfNull(vm);
+
+ // Use the generic ResolveView method - this is AOT-safe!
+ var ret = ViewLocator.Current.ResolveView();
+ if (ret is null)
+ {
+ var msg = $"Couldn't find a View for ViewModel. You probably need to register an IViewFor<{typeof(TViewModel).Name}>";
+
+ throw new Exception(msg);
+ }
+
+ ret.ViewModel = vm as TViewModel;
+
+ var pg = (Page)ret;
+
+ if (SetTitleOnNavigate)
+ {
+ RxSchedulers.MainThreadScheduler.Schedule(() => pg.Title = vm.UrlPathSegment);
+ }
+
+ return pg;
+ }
+
+ ///
+ /// Invalidates current page view model.
+ ///
+ protected void InvalidateCurrentViewModel()
+ {
+ var vm = Router?.GetCurrentViewModel();
+ if (CurrentPage is IViewFor page && vm is not null)
+ {
+ if (page.ViewModel?.GetType() == vm.GetType())
+ {
+ // don't replace view model if vm is null or an incompatible type.
+ page.ViewModel = vm;
+ }
+ else
+ {
+ this.Log().Info($"The view type '{page.GetType().FullName}' is not compatible with '{vm.GetType().FullName}' this was called by {_action}, the viewmodel was not invalidated");
+ }
+ }
+ }
+
+ ///
+ /// Syncs page's navigation stack with
+ /// to affect manipulations like Add or Clear.
+ ///
+ /// A representing the asynchronous operation.
+ protected async Task SyncNavigationStacksAsync()
+ {
+ if (Navigation.NavigationStack.Count != Router.NavigationStack.Count
+ || StacksAreDifferent())
+ {
+ if (Navigation.NavigationStack.Count > 2)
+ {
+ for (var i = Navigation.NavigationStack.Count - 2; i >= 0; i--)
+ {
+ Navigation.RemovePage(Navigation.NavigationStack[i]);
+ }
+ }
+
+ Page? rootPage;
+ if (Navigation.NavigationStack.Count >= 1)
+ {
+ rootPage = Navigation.NavigationStack[0];
+ }
+ else
+ {
+ rootPage = PageForViewModel(Router.NavigationStack[0]);
+ await Navigation.PushAsync(rootPage, false);
+ }
+
+ if (Router.NavigationStack.Count >= 1)
+ {
+ for (var i = 0; i < Router.NavigationStack.Count - 1; i++)
+ {
+ var page = PageForViewModel(Router.NavigationStack[i]);
+ Navigation.InsertPageBefore(page, rootPage);
+ }
+ }
+ }
+ }
+
+ private bool StacksAreDifferent()
+ {
+ for (var i = 0; i < Router.NavigationStack.Count; i++)
+ {
+ var vm = Router.NavigationStack[i];
+ var page = Navigation.NavigationStack[i];
+
+ if (page is not IViewFor view || !ReferenceEquals(view.ViewModel, vm))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/src/ReactiveUI.Maui/ViewModelViewHost.cs b/src/ReactiveUI.Maui/ViewModelViewHost.cs
index bc1e7b8d37..27ac208721 100644
--- a/src/ReactiveUI.Maui/ViewModelViewHost.cs
+++ b/src/ReactiveUI.Maui/ViewModelViewHost.cs
@@ -12,6 +12,8 @@ namespace ReactiveUI.Maui;
/// to be displayed should be assigned to the property. Optionally, the chosen view can be
/// customized by specifying a contract via or .
///
+[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.")]
public partial class ViewModelViewHost : ContentView, IViewFor
{
///
@@ -49,15 +51,12 @@ public partial class ViewModelViewHost : ContentView, IViewFor
typeof(ViewModelViewHost),
false);
+ private readonly CompositeDisposable _subscriptions = [];
private string? _viewContract;
///
/// Initializes a new instance of the class.
///
-#if NET6_0_OR_GREATER
- [RequiresDynamicCode("ViewModelViewHost uses methods that require dynamic code generation")]
- [RequiresUnreferencedCode("ViewModelViewHost uses methods that may require unreferenced code")]
-#endif
public ViewModelViewHost()
{
// NB: InUnitTestRunner also returns true in Design Mode
@@ -69,19 +68,25 @@ public ViewModelViewHost()
ViewContractObservable = Observable.Default;
- var vmAndContract = this.WhenAnyValue(nameof(ViewModel)).CombineLatest(
- this.WhenAnyObservable(x => x.ViewContractObservable),
- (vm, contract) => new { ViewModel = vm, Contract = contract, });
-
- this.WhenActivated(() =>
- (IDisposable[])[
- vmAndContract.Subscribe(x =>
- {
- _viewContract = x.Contract;
-
- ResolveViewForViewModel(x.ViewModel, x.Contract);
- })
- ]);
+ // Observe ViewModel property changes without expression trees (AOT-friendly)
+ var viewModelChanged = MauiReactiveHelpers.CreatePropertyValueObservable(
+ this,
+ nameof(ViewModel),
+ () => ViewModel);
+
+ // Combine ViewModel and ViewContractObservable streams
+ var vmAndContract = viewModelChanged.CombineLatest(
+ ViewContractObservable,
+ (vm, contract) => new { ViewModel = vm, Contract = contract });
+
+ // Subscribe directly without WhenActivated
+ vmAndContract
+ .Subscribe(x =>
+ {
+ _viewContract = x.Contract;
+ ResolveViewForViewModel(x.ViewModel, x.Contract);
+ })
+ .DisposeWith(_subscriptions);
}
///
@@ -142,10 +147,8 @@ public bool ContractFallbackByPass
///
/// ViewModel.
/// contract used by ViewLocator.
-#if NET6_0_OR_GREATER
- [RequiresDynamicCode("ResolveViewForViewModel uses methods that require dynamic code generation")]
- [RequiresUnreferencedCode("ResolveViewForViewModel uses methods that may require unreferenced code")]
-#endif
+ [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.")]
protected virtual void ResolveViewForViewModel(object? viewModel, string? contract)
{
if (viewModel is null)
@@ -164,12 +167,12 @@ protected virtual void ResolveViewForViewModel(object? viewModel, string? contra
if (viewInstance is null)
{
- throw new Exception($"Couldn't find view for '{viewModel}'.");
+ throw new InvalidOperationException($"Couldn't find view for '{viewModel}'.");
}
if (viewInstance is not View castView)
{
- throw new Exception($"View '{viewInstance.GetType().FullName}' is not a subclass of '{typeof(View).FullName}'.");
+ throw new InvalidOperationException($"View '{viewInstance.GetType().FullName}' is not a subclass of '{typeof(View).FullName}'.");
}
viewInstance.ViewModel = viewModel;
diff --git a/src/ReactiveUI.Maui/ViewModelViewHost{TViewModel}.cs b/src/ReactiveUI.Maui/ViewModelViewHost{TViewModel}.cs
new file mode 100644
index 0000000000..9d20336ae7
--- /dev/null
+++ b/src/ReactiveUI.Maui/ViewModelViewHost{TViewModel}.cs
@@ -0,0 +1,195 @@
+// Copyright (c) 2025 .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.Maui.Controls;
+
+namespace ReactiveUI.Maui;
+
+///
+/// This content view will automatically load and host the view for the given view model. The view model whose view is
+/// to be displayed should be assigned to the property. Optionally, the chosen view can be
+/// customized by specifying a contract via or .
+///
+/// The type of the view model. Must have a public parameterless constructor for AOT compatibility.
+///
+/// This is the AOT-compatible generic version of ViewModelViewHost. It uses compile-time type information
+/// to resolve views without reflection, making it safe for Native AOT and trimming scenarios.
+///
+public partial class ViewModelViewHost<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TViewModel> : ContentView, IViewFor
+ where TViewModel : class
+{
+ ///
+ /// Identifies the property.
+ ///
+ public static readonly BindableProperty ViewModelProperty = BindableProperty.Create(
+ nameof(ViewModel),
+ typeof(TViewModel),
+ typeof(ViewModelViewHost));
+
+ ///
+ /// Identifies the property.
+ ///
+ public static readonly BindableProperty DefaultContentProperty = BindableProperty.Create(
+ nameof(DefaultContent),
+ typeof(View),
+ typeof(ViewModelViewHost),
+ default(View));
+
+ ///
+ /// Identifies the property.
+ ///
+ public static readonly BindableProperty ViewContractObservableProperty = BindableProperty.Create(
+ nameof(ViewContractObservable),
+ typeof(IObservable),
+ typeof(ViewModelViewHost),
+ Observable.Never);
+
+ ///
+ /// The ContractFallbackByPass dependency property.
+ ///
+ public static readonly BindableProperty ContractFallbackByPassProperty = BindableProperty.Create(
+ nameof(ContractFallbackByPass),
+ typeof(bool),
+ typeof(ViewModelViewHost),
+ false);
+
+ private readonly CompositeDisposable _subscriptions = [];
+ private string? _viewContract;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ViewModelViewHost()
+ {
+ // NB: InUnitTestRunner also returns true in Design Mode
+ if (ModeDetector.InUnitTestRunner())
+ {
+ ViewContractObservable = Observable.Never;
+ return;
+ }
+
+ ViewContractObservable = Observable.Default;
+
+ // Observe ViewModel property changes without expression trees (AOT-friendly)
+ var viewModelChanged = MauiReactiveHelpers.CreatePropertyValueObservable(
+ this,
+ nameof(ViewModel),
+ () => ViewModel);
+
+ // Combine ViewModel and ViewContractObservable streams
+ var vmAndContract = viewModelChanged.CombineLatest(
+ ViewContractObservable,
+ (vm, contract) => new { ViewModel = vm, Contract = contract });
+
+ // Subscribe directly without WhenActivated
+ vmAndContract
+ .Subscribe(x =>
+ {
+ _viewContract = x.Contract;
+ ResolveViewForViewModel(x.ViewModel, x.Contract);
+ })
+ .DisposeWith(_subscriptions);
+ }
+
+ ///
+ /// Gets or sets the view model whose associated view is to be displayed.
+ ///
+ public TViewModel? ViewModel
+ {
+ get => (TViewModel?)GetValue(ViewModelProperty);
+ set => SetValue(ViewModelProperty, value);
+ }
+
+ ///
+ /// Gets or sets the view model whose associated view is to be displayed.
+ ///
+ object? IViewFor.ViewModel
+ {
+ get => ViewModel;
+ set => ViewModel = (TViewModel?)value;
+ }
+
+ ///
+ /// Gets or sets the content to display when is .
+ ///
+ public View DefaultContent
+ {
+ get => (View)GetValue(DefaultContentProperty);
+ set => SetValue(DefaultContentProperty, value);
+ }
+
+ ///
+ /// Gets or sets the observable which signals when the contract to use when resolving the view for the given view model has changed.
+ ///
+ public IObservable ViewContractObservable
+ {
+ get => (IObservable)GetValue(ViewContractObservableProperty);
+ set => SetValue(ViewContractObservableProperty, value);
+ }
+
+ ///
+ /// Gets or sets the fixed contract to use when resolving the view for the given view model.
+ ///
+ ///
+ /// This property is a mere convenience so that a fixed contract can be assigned directly in XAML.
+ ///
+ public string? ViewContract
+ {
+ get => _viewContract;
+ set => ViewContractObservable = Observable.Return(value);
+ }
+
+ ///
+ /// Gets or sets a value indicating whether should bypass the default contract fallback behavior.
+ ///
+ public bool ContractFallbackByPass
+ {
+ get => (bool)GetValue(ContractFallbackByPassProperty);
+ set => SetValue(ContractFallbackByPassProperty, value);
+ }
+
+ ///
+ /// Gets or sets the override for the view locator to use when resolving the view. If unspecified, will be used.
+ ///
+ public IViewLocator? ViewLocator { get; set; }
+
+ ///
+ /// Resolves and displays the view for the given view model with respect to the contract.
+ /// This method uses the generic ResolveView method which is AOT-compatible.
+ ///
+ /// The view model to resolve a view for.
+ /// The contract to use when resolving the view.
+ protected virtual void ResolveViewForViewModel(TViewModel? viewModel, string? contract)
+ {
+ if (viewModel is null)
+ {
+ Content = DefaultContent;
+ return;
+ }
+
+ var viewLocator = ViewLocator ?? ReactiveUI.ViewLocator.Current;
+
+ // Use the generic ResolveView method - this is AOT-safe!
+ var viewInstance = viewLocator.ResolveView(contract);
+ if (viewInstance is null && !ContractFallbackByPass)
+ {
+ viewInstance = viewLocator.ResolveView();
+ }
+
+ if (viewInstance is null)
+ {
+ throw new InvalidOperationException($"Couldn't find view for '{viewModel}'.");
+ }
+
+ if (viewInstance is not View castView)
+ {
+ throw new InvalidOperationException($"View '{viewInstance.GetType().FullName}' is not a subclass of '{typeof(View).FullName}'.");
+ }
+
+ viewInstance.ViewModel = viewModel;
+
+ Content = castView;
+ }
+}
diff --git a/src/ReactiveUI.Maui/WinUI/DependencyObjectObservableForProperty.cs b/src/ReactiveUI.Maui/WinUI/DependencyObjectObservableForProperty.cs
index c2d480819a..09c3f88670 100644
--- a/src/ReactiveUI.Maui/WinUI/DependencyObjectObservableForProperty.cs
+++ b/src/ReactiveUI.Maui/WinUI/DependencyObjectObservableForProperty.cs
@@ -6,9 +6,10 @@
#if WINUI_TARGET
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
+
#if IS_MAUI
-using System.Linq.Expressions;
#endif
+using System.Linq.Expressions;
using System.Reflection;
using Microsoft.UI.Xaml;
@@ -21,10 +22,7 @@ namespace ReactiveUI;
public class DependencyObjectObservableForProperty : ICreatesObservableForProperty
{
///
-#if NET6_0_OR_GREATER
- [RequiresDynamicCode("GetAffinityForObject uses methods that require dynamic code generation")]
[RequiresUnreferencedCode("GetAffinityForObject uses methods that may require unreferenced code")]
-#endif
public int GetAffinityForObject(Type type, string propertyName, bool beforeChanged = false)
{
if (!typeof(DependencyObject).GetTypeInfo().IsAssignableFrom(type.GetTypeInfo()))
@@ -41,10 +39,7 @@ public int GetAffinityForObject(Type type, string propertyName, bool beforeChang
}
///
-#if NET6_0_OR_GREATER
- [RequiresDynamicCode("GetNotificationForProperty uses methods that require dynamic code generation")]
[RequiresUnreferencedCode("GetNotificationForProperty uses methods that may require unreferenced code")]
-#endif
public IObservable> GetNotificationForProperty(object sender, Expression expression, string propertyName, bool beforeChanged = false, bool suppressWarnings = false)
{
ArgumentNullException.ThrowIfNull(sender);
@@ -92,10 +87,7 @@ public int GetAffinityForObject(Type type, string propertyName, bool beforeChang
});
}
-#if NET6_0_OR_GREATER
- [RequiresDynamicCode("ActuallyGetProperty uses methods that require dynamic code generation")]
[RequiresUnreferencedCode("ActuallyGetProperty uses methods that may require unreferenced code")]
-#endif
private static PropertyInfo? ActuallyGetProperty(TypeInfo typeInfo, string propertyName)
{
var current = typeInfo;
@@ -113,10 +105,7 @@ public int GetAffinityForObject(Type type, string propertyName, bool beforeChang
return null;
}
-#if NET6_0_OR_GREATER
- [RequiresDynamicCode("ActuallyGetField uses methods that require dynamic code generation")]
[RequiresUnreferencedCode("ActuallyGetField uses methods that may require unreferenced code")]
-#endif
private static FieldInfo? ActuallyGetField(TypeInfo typeInfo, string propertyName)
{
var current = typeInfo;
@@ -134,10 +123,7 @@ public int GetAffinityForObject(Type type, string propertyName, bool beforeChang
return null;
}
-#if NET6_0_OR_GREATER
- [RequiresDynamicCode("GetDependencyPropertyFetcher uses methods that require dynamic code generation")]
[RequiresUnreferencedCode("GetDependencyPropertyFetcher uses methods that may require unreferenced code")]
-#endif
private static Func? GetDependencyPropertyFetcher(Type type, string propertyName)
{
var typeInfo = type.GetTypeInfo();
diff --git a/src/ReactiveUI.Maui/WinUI/DispatcherQueueScheduler.cs b/src/ReactiveUI.Maui/WinUI/DispatcherQueueScheduler.cs
index be1cebfe4d..efcea406f0 100644
--- a/src/ReactiveUI.Maui/WinUI/DispatcherQueueScheduler.cs
+++ b/src/ReactiveUI.Maui/WinUI/DispatcherQueueScheduler.cs
@@ -129,7 +129,7 @@ private IDisposable ScheduleSlow(TState state, TimeSpan dueTime, Func
{
- var t = Interlocked.Exchange(ref timer, null);
+ var t = System.Threading.Interlocked.Exchange(ref timer, null);
if (t != null)
{
try
@@ -149,7 +149,7 @@ private IDisposable ScheduleSlow(TState state, TimeSpan dueTime, Func
{
- var t = Interlocked.Exchange(ref timer, null);
+ var t = System.Threading.Interlocked.Exchange(ref timer, null);
if (t != null)
{
t.Stop();
@@ -196,7 +196,7 @@ public IDisposable SchedulePeriodic(TState state, TimeSpan period, Func<
return Disposable.Create(() =>
{
- var t = Interlocked.Exchange(ref timer, null);
+ var t = System.Threading.Interlocked.Exchange(ref timer, null);
if (t != null)
{
t.Stop();
diff --git a/src/ReactiveUI.Testing/SchedulerExtensions.cs b/src/ReactiveUI.Testing/SchedulerExtensions.cs
index 7d24eec139..374e2dfa1a 100644
--- a/src/ReactiveUI.Testing/SchedulerExtensions.cs
+++ b/src/ReactiveUI.Testing/SchedulerExtensions.cs
@@ -22,22 +22,18 @@ public static class SchedulerExtensions
/// The scheduler to use.
/// An object that when disposed, restores the previous default
/// schedulers.
-#if NET6_0_OR_GREATER
- [RequiresDynamicCode("WithScheduler uses methods that require dynamic code generation")]
- [RequiresUnreferencedCode("WithScheduler uses methods that may require unreferenced code")]
-#endif
public static IDisposable WithScheduler(IScheduler scheduler)
{
- var prevDef = RxApp.MainThreadScheduler;
- var prevTask = RxApp.TaskpoolScheduler;
+ var prevDef = RxSchedulers.MainThreadScheduler;
+ var prevTask = RxSchedulers.TaskpoolScheduler;
- RxApp.MainThreadScheduler = scheduler;
- RxApp.TaskpoolScheduler = scheduler;
+ RxSchedulers.MainThreadScheduler = scheduler;
+ RxSchedulers.TaskpoolScheduler = scheduler;
return Disposable.Create(() =>
{
- RxApp.MainThreadScheduler = prevDef;
- RxApp.TaskpoolScheduler = prevTask;
+ RxSchedulers.MainThreadScheduler = prevDef;
+ RxSchedulers.TaskpoolScheduler = prevTask;
});
}
@@ -52,10 +48,6 @@ public static IDisposable WithScheduler(IScheduler scheduler)
/// The scheduler to use.
/// The function to execute.
/// The return value of the function.
-#if NET6_0_OR_GREATER
- [RequiresDynamicCode("With uses methods that require dynamic code generation")]
- [RequiresUnreferencedCode("With uses methods that may require unreferenced code")]
-#endif
public static TRet With(this T scheduler, Func block)
where T : IScheduler
{
@@ -81,10 +73,6 @@ public static TRet With(this T scheduler, Func block)
/// The scheduler to use.
/// The function to execute.
/// The return value of the function.
-#if NET6_0_OR_GREATER
- [RequiresDynamicCode("WithAsync uses methods that require dynamic code generation")]
- [RequiresUnreferencedCode("WithAsync uses methods that may require unreferenced code")]
-#endif
public static async Task WithAsync(this T scheduler, Func> block)
where T : IScheduler
{
@@ -106,10 +94,6 @@ public static async Task WithAsync(this T scheduler, FuncThe type.
/// The scheduler to use.
/// The action to execute.
-#if NET6_0_OR_GREATER
- [RequiresDynamicCode("With uses methods that require dynamic code generation")]
- [RequiresUnreferencedCode("With uses methods that may require unreferenced code")]
-#endif
public static void With(this T scheduler, Action block)
where T : IScheduler =>
scheduler.With(x =>
@@ -126,10 +110,6 @@ public static void With(this T scheduler, Action block)
/// The scheduler to use.
/// The action to execute.
/// A representing the asynchronous operation.
-#if NET6_0_OR_GREATER
- [RequiresDynamicCode("WithAsync uses methods that require dynamic code generation")]
- [RequiresUnreferencedCode("WithAsync uses methods that may require unreferenced code")]
-#endif
public static Task WithAsync(this T scheduler, Func block)
where T : IScheduler =>
scheduler.WithAsync(async x =>
@@ -223,5 +203,3 @@ public static Recorded> OnCompletedAt(this TestScheduler sche
/// Timespan for virtual scheduler to use.
public static long FromTimeSpan(this TestScheduler scheduler, TimeSpan span) => span.Ticks;
}
-
-// vim: tw=120 ts=4 sw=4 et :
diff --git a/src/ReactiveUI.WinUI/Builder/WinUIReactiveUIBuilderExtensions.cs b/src/ReactiveUI.WinUI/Builder/WinUIReactiveUIBuilderExtensions.cs
index 00457bee91..8b4a34c89e 100644
--- a/src/ReactiveUI.WinUI/Builder/WinUIReactiveUIBuilderExtensions.cs
+++ b/src/ReactiveUI.WinUI/Builder/WinUIReactiveUIBuilderExtensions.cs
@@ -23,10 +23,6 @@ public static class WinUIReactiveUIBuilderExtensions
///
/// The builder instance.
/// The builder instance for chaining.
-#if NET6_0_OR_GREATER
- [RequiresDynamicCode("WinUIReactiveUIBuilderExtensions uses methods that require dynamic code generation")]
- [RequiresUnreferencedCode("WinUIReactiveUIBuilderExtensions uses methods that may require unreferenced code")]
-#endif
public static IReactiveUIBuilder WithWinUI(this IReactiveUIBuilder builder)
{
ArgumentExceptionHelper.ThrowIfNull(builder);
diff --git a/src/ReactiveUI.WinUI/GlobalUsings.cs b/src/ReactiveUI.WinUI/GlobalUsings.cs
deleted file mode 100644
index 59ac6b836d..0000000000
--- a/src/ReactiveUI.WinUI/GlobalUsings.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-// Copyright (c) 2025 .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.
-
-global using System;
-global using System.Diagnostics.CodeAnalysis;
-global using System.Linq;
-global using System.Linq.Expressions;
-global using System.Reactive.Concurrency;
-global using System.Reactive.Disposables;
-global using System.Reactive.Linq;
-global using System.Threading;
-
-global using Splat;
diff --git a/src/ReactiveUI.WinUI/ReactiveUI.WinUI.csproj b/src/ReactiveUI.WinUI/ReactiveUI.WinUI.csproj
index a37f527bb3..fff7797a01 100644
--- a/src/ReactiveUI.WinUI/ReactiveUI.WinUI.csproj
+++ b/src/ReactiveUI.WinUI/ReactiveUI.WinUI.csproj
@@ -27,12 +27,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/ReactiveUI.Winforms/ActivationForViewFetcher.cs b/src/ReactiveUI.Winforms/ActivationForViewFetcher.cs
index a2d548179d..b46f047081 100644
--- a/src/ReactiveUI.Winforms/ActivationForViewFetcher.cs
+++ b/src/ReactiveUI.Winforms/ActivationForViewFetcher.cs
@@ -21,10 +21,6 @@ public class ActivationForViewFetcher : IActivationForViewFetcher, IEnableLogger
public int GetAffinityForView(Type view) => typeof(Control).GetTypeInfo().IsAssignableFrom(view.GetTypeInfo()) ? 10 : 0;
///
-#if NET6_0_OR_GREATER
- [RequiresDynamicCode("GetActivationForView uses methods that require dynamic code generation")]
- [RequiresUnreferencedCode("GetActivationForView uses methods that may require unreferenced code")]
-#endif
public IObservable GetActivationForView(IActivatableView view)
{
// Startup: Control.HandleCreated > Control.BindingContextChanged > Form.Load > Control.VisibleChanged > Form.Activated > Form.Shown
diff --git a/src/ReactiveUI.Winforms/ContentControlBindingHook.cs b/src/ReactiveUI.Winforms/ContentControlBindingHook.cs
index 575c25de03..0af41829c8 100644
--- a/src/ReactiveUI.Winforms/ContentControlBindingHook.cs
+++ b/src/ReactiveUI.Winforms/ContentControlBindingHook.cs
@@ -13,10 +13,6 @@ namespace ReactiveUI.Winforms;
public class ContentControlBindingHook : IPropertyBindingHook
{
///
-#if NET6_0_OR_GREATER
- [RequiresDynamicCode("ExecuteHook uses methods that require dynamic code generation")]
- [RequiresUnreferencedCode("ExecuteHook uses methods that may require unreferenced code")]
-#endif
public bool ExecuteHook(object? source, object target, Func[]> getCurrentViewModelProperties, Func[]> getCurrentViewProperties, BindingDirection direction)
{
ArgumentExceptionHelper.ThrowIfNull(getCurrentViewProperties);
diff --git a/src/ReactiveUI.Winforms/CreatesWinformsCommandBinding.cs b/src/ReactiveUI.Winforms/CreatesWinformsCommandBinding.cs
index 645841dc37..b3dad7252b 100644
--- a/src/ReactiveUI.Winforms/CreatesWinformsCommandBinding.cs
+++ b/src/ReactiveUI.Winforms/CreatesWinformsCommandBinding.cs
@@ -4,30 +4,36 @@
// See the LICENSE file in the project root for full license information.
using System.Reflection;
+using System.Runtime.CompilerServices;
using System.Windows.Input;
namespace ReactiveUI.Winforms;
///
-/// This binder is the default binder for connecting to arbitrary events.
+/// Default command binder for Windows Forms controls that connects an to an event on a target object.
///
-public class CreatesWinformsCommandBinding : ICreatesCommandBinding
+///
+///
+/// This binder supports a small set of conventional "default" events (for example, Click, MouseUp),
+/// and can also bind to an explicitly named event.
+///
+///
+/// Reflection-based event lookup and string-based event subscription are not trimming/AOT-safe in general.
+/// Use the generic overloads with explicit add/remove handler delegates to avoid the reflection cost.
+///
+///
+public sealed class CreatesWinformsCommandBinding : ICreatesCommandBinding
{
// NB: These are in priority order
private static readonly List<(string name, Type type)> _defaultEventsToBind =
[
("Click", typeof(EventArgs)),
- ("MouseUp", typeof(System.Windows.Forms.MouseEventArgs)),
- ];
+ ("MouseUp", typeof(System.Windows.Forms.MouseEventArgs))];
///
-#if NET6_0_OR_GREATER
- [RequiresDynamicCode("GetAffinityForObject uses methods that require dynamic code generation")]
- [RequiresUnreferencedCode("GetAffinityForObject uses methods that may require unreferenced code")]
-#endif
- public int GetAffinityForObject(Type type, bool hasEventTarget)
+ public int GetAffinityForObject<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicEvents | DynamicallyAccessedMemberTypes.PublicProperties)] T>(bool hasEventTarget)
{
- var isWinformControl = typeof(Control).IsAssignableFrom(type);
+ var isWinformControl = typeof(Control).IsAssignableFrom(typeof(T));
if (isWinformControl)
{
@@ -39,60 +45,51 @@ public int GetAffinityForObject(Type type, bool hasEventTarget)
return 6;
}
- return _defaultEventsToBind.Any(x =>
+ return _defaultEventsToBind.Any(static x =>
{
- var ei = type.GetEvent(x.name, BindingFlags.Public | BindingFlags.FlattenHierarchy | BindingFlags.Instance);
+ var ei = typeof(T).GetEvent(x.name, BindingFlags.Public | BindingFlags.FlattenHierarchy | BindingFlags.Instance);
return ei is not null;
}) ? 4 : 0;
}
- ///
-#if NET6_0_OR_GREATER
- public int GetAffinityForObject<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicEvents | DynamicallyAccessedMemberTypes.PublicProperties)] T>(
- bool hasEventTarget)
-#else
- public int GetAffinityForObject(
- bool hasEventTarget)
-#endif
+ ///
+ /// Binds a command to the default event on a Windows Forms control.
+ /// This method uses direct type checking and the AOT-safe add/remove handler overload instead of reflection.
+ ///
+ /// The type of the target object.
+ /// The command to bind. If , no binding is created.
+ /// The target object.
+ /// An observable that supplies command parameter values.
+ /// A disposable that unbinds the command, or null if no default event was found.
+ /// Thrown when is .
+ [RequiresUnreferencedCode("String/reflection-based event binding may require members removed by trimming.")]
+ public IDisposable? BindCommandToObject<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicEvents | DynamicallyAccessedMemberTypes.NonPublicEvents)] T>(ICommand? command, T? target, IObservable