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 Observable /// /// 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 commandParameter) + where T : class { - var isWinformControl = typeof(Control).IsAssignableFrom(typeof(T)); + ArgumentExceptionHelper.ThrowIfNull(target); - if (isWinformControl) + // Preserve typical binding semantics: null command => no-op binding. + if (command is null) { - return 10; + return Disposable.Empty; } - if (hasEventTarget) + // Use direct type checking for known WinForms types first (AOT-friendly) + if (target is Control control) { - return 6; + // Most controls have a Click event (uses non-generic EventHandler) + return BindCommandToObject( + command, + control, + commandParameter, + h => control.Click += h, + h => control.Click -= h); } - return _defaultEventsToBind.Any(static x => - { - 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 - [RequiresDynamicCode("GetAffinityForObject uses methods that require dynamic code generation")] - [RequiresUnreferencedCode("GetAffinityForObject uses methods that may require unreferenced code")] -#endif - public IDisposable? BindCommandToObject(ICommand? command, object? target, IObservable commandParameter) - { -#if NET6_0_OR_GREATER - ArgumentExceptionHelper.ThrowIfNull(command); - ArgumentExceptionHelper.ThrowIfNull(target); -#else - ArgumentExceptionHelper.ThrowIfNull(command); - - ArgumentExceptionHelper.ThrowIfNull(target); -#endif - + // Fall back to reflection-based event discovery for other types const BindingFlags bf = BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy; - var type = target.GetType(); + var type = typeof(T); var eventInfo = _defaultEventsToBind .Select(x => new { EventInfo = type.GetEvent(x.name, bf), Args = x.type }) .FirstOrDefault(x => x.EventInfo is not null); @@ -102,27 +99,26 @@ public int GetAffinityForObject( return null; } - var mi = GetType().GetMethods().First(x => x.Name == "BindCommandToObject" && x.IsGenericMethod); - mi = mi.MakeGenericMethod(eventInfo.Args); + // Dynamically call the correct generic method based on event args type + if (eventInfo.Args == typeof(EventArgs)) + { + return BindCommandToObject(command, target, commandParameter, eventInfo.EventInfo?.Name!); + } + else if (eventInfo.Args == typeof(System.Windows.Forms.MouseEventArgs)) + { + return BindCommandToObject(command, target, commandParameter, eventInfo.EventInfo?.Name!); + } - return (IDisposable?)mi.Invoke(this, [command, target, commandParameter, eventInfo.EventInfo?.Name]); + return null; } /// -#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 IDisposable BindCommandToObject(ICommand? command, object? target, IObservable commandParameter, string eventName) + [RequiresUnreferencedCode("String/reflection-based event binding may require members removed by trimming.")] + public IDisposable? BindCommandToObject(ICommand? command, T? target, IObservable commandParameter, string eventName) + where T : class { -#if NET6_0_OR_GREATER ArgumentExceptionHelper.ThrowIfNull(command); ArgumentExceptionHelper.ThrowIfNull(target); -#else - ArgumentExceptionHelper.ThrowIfNull(command); - - ArgumentExceptionHelper.ThrowIfNull(target); -#endif var ret = new CompositeDisposable(); @@ -164,4 +160,144 @@ public IDisposable BindCommandToObject(ICommand? command, object? ta return ret; } + + /// + /// Binds a command to an event on a target object using explicit add/remove handler delegates. + /// This overload is AOT-safe and doesn't require reflection. + /// + /// The type of the target object. + /// The type of the event arguments. + /// The command to bind. If , no binding is created. + /// The target object. + /// An observable that supplies command parameter values. + /// Action that subscribes an event handler to the target event. + /// Action that unsubscribes an event handler from the target event. + /// A disposable that unbinds the command. + /// Thrown when , , or is . + public IDisposable? BindCommandToObject<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicEvents | DynamicallyAccessedMemberTypes.NonPublicEvents)] T, TEventArgs>( + ICommand? command, + T? target, + IObservable commandParameter, + Action> addHandler, + Action> removeHandler) + where T : class + where TEventArgs : EventArgs + { + ArgumentExceptionHelper.ThrowIfNull(target); + ArgumentExceptionHelper.ThrowIfNull(addHandler); + ArgumentExceptionHelper.ThrowIfNull(removeHandler); + + if (command is null) + { + return Disposable.Empty; + } + + object? latestParameter = null; + + void Handler(object? s, TEventArgs e) + { + var param = Volatile.Read(ref latestParameter); + if (command.CanExecute(param)) + { + command.Execute(param); + } + } + + var ret = new CompositeDisposable(); + ret.Add(commandParameter.Subscribe(x => Volatile.Write(ref latestParameter, x))); + + addHandler(Handler); + ret.Add(Disposable.Create(() => removeHandler(Handler))); + + // Handle Enabled property binding for Components + var targetType = typeof(T); + if (typeof(Component).IsAssignableFrom(targetType)) + { + var enabledProperty = targetType.GetRuntimeProperty("Enabled"); + + if (enabledProperty is not null) + { + object? latestParam = null; + ret.Add(commandParameter.Subscribe(x => Volatile.Write(ref latestParam, x))); + + ret.Add(Observable.FromEvent( + eventHandler => (_, _) => eventHandler(command.CanExecute(Volatile.Read(ref latestParam))), + x => command.CanExecuteChanged += x, + x => command.CanExecuteChanged -= x) + .StartWith(command.CanExecute(latestParam)) + .Subscribe(x => enabledProperty.SetValue(target, x, null))); + } + } + + return ret; + } + + /// + /// Binds a command to an event on a target object using explicit add/remove handler delegates for non-generic EventHandler. + /// This overload is AOT-safe and supports WinForms controls that use EventHandler instead of EventHandler<TEventArgs>. + /// + /// 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. + /// Action that subscribes an event handler to the target event. + /// Action that unsubscribes an event handler from the target event. + /// A disposable that unbinds the command. + /// Thrown when , , or is . + public IDisposable? BindCommandToObject<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicEvents | DynamicallyAccessedMemberTypes.NonPublicEvents)] T>( + ICommand? command, + T? target, + IObservable commandParameter, + Action addHandler, + Action removeHandler) + where T : class + { + ArgumentExceptionHelper.ThrowIfNull(target); + ArgumentExceptionHelper.ThrowIfNull(addHandler); + ArgumentExceptionHelper.ThrowIfNull(removeHandler); + + if (command is null) + { + return Disposable.Empty; + } + + object? latestParameter = null; + + void Handler(object? s, EventArgs e) + { + var param = Volatile.Read(ref latestParameter); + if (command.CanExecute(param)) + { + command.Execute(param); + } + } + + var ret = new CompositeDisposable(); + ret.Add(commandParameter.Subscribe(x => Volatile.Write(ref latestParameter, x))); + + addHandler(Handler); + ret.Add(Disposable.Create(() => removeHandler(Handler))); + + // Handle Enabled property binding for Components + var targetType = typeof(T); + if (typeof(Component).IsAssignableFrom(targetType)) + { + var enabledProperty = targetType.GetRuntimeProperty("Enabled"); + + if (enabledProperty is not null) + { + object? latestParam = null; + ret.Add(commandParameter.Subscribe(x => Volatile.Write(ref latestParam, x))); + + ret.Add(Observable.FromEvent( + eventHandler => (_, _) => eventHandler(command.CanExecute(Volatile.Read(ref latestParam))), + x => command.CanExecuteChanged += x, + x => command.CanExecuteChanged -= x) + .StartWith(command.CanExecute(latestParam)) + .Subscribe(x => enabledProperty.SetValue(target, x, null))); + } + } + + return ret; + } } diff --git a/src/ReactiveUI.Winforms/PanelSetMethodBindingConverter.cs b/src/ReactiveUI.Winforms/PanelSetMethodBindingConverter.cs index ae2f7c9073..c6551b8a6c 100644 --- a/src/ReactiveUI.Winforms/PanelSetMethodBindingConverter.cs +++ b/src/ReactiveUI.Winforms/PanelSetMethodBindingConverter.cs @@ -11,10 +11,6 @@ namespace ReactiveUI.Winforms; public class PanelSetMethodBindingConverter : ISetMethodBindingConverter { /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("GetAffinityForObjects uses methods that require dynamic code generation")] - [RequiresUnreferencedCode("GetAffinityForObjects uses methods that may require unreferenced code")] -#endif public int GetAffinityForObjects(Type? fromType, Type? toType) { if (toType != typeof(Control.ControlCollection)) diff --git a/src/ReactiveUI.Winforms/ReactiveUI.Winforms.csproj b/src/ReactiveUI.Winforms/ReactiveUI.Winforms.csproj index 0ad1bfa290..cd8b98c061 100644 --- a/src/ReactiveUI.Winforms/ReactiveUI.Winforms.csproj +++ b/src/ReactiveUI.Winforms/ReactiveUI.Winforms.csproj @@ -9,6 +9,9 @@ mvvm;reactiveui;rx;reactive extensions;observable;LINQ;events;frp;winforms;net;net472; $(NoWarn);IDE1006; True + false + false + false diff --git a/src/ReactiveUI.Winforms/ReactiveUserControl.cs b/src/ReactiveUI.Winforms/ReactiveUserControl.cs index 91750c1605..f3425772b2 100644 --- a/src/ReactiveUI.Winforms/ReactiveUserControl.cs +++ b/src/ReactiveUI.Winforms/ReactiveUserControl.cs @@ -12,10 +12,6 @@ namespace ReactiveUI.Winforms; /// The type of the view model. /// /// -#if NET6_0_OR_GREATER -[RequiresDynamicCode("ReactiveUserControl provides base functionality for ReactiveUI which may require dynamic code generation")] -[RequiresUnreferencedCode("ReactiveUserControl provides base functionality for ReactiveUI which may require unreferenced code")] -#endif public partial class ReactiveUserControl : UserControl, IViewFor where TViewModel : class { diff --git a/src/ReactiveUI.Winforms/Registrations.cs b/src/ReactiveUI.Winforms/Registrations.cs index 7911c4d74d..cdb3e49068 100644 --- a/src/ReactiveUI.Winforms/Registrations.cs +++ b/src/ReactiveUI.Winforms/Registrations.cs @@ -12,28 +12,21 @@ namespace ReactiveUI.Winforms; 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); - registerFunction(static () => new PlatformOperations(), typeof(IPlatformOperations)); - - registerFunction(static () => new CreatesWinformsCommandBinding(), typeof(ICreatesCommandBinding)); - registerFunction(static () => new WinformsCreatesObservableForProperty(), typeof(ICreatesObservableForProperty)); - registerFunction(static () => new ActivationForViewFetcher(), typeof(IActivationForViewFetcher)); - registerFunction(static () => new PanelSetMethodBindingConverter(), typeof(ISetMethodBindingConverter)); - registerFunction(static () => new TableContentSetMethodBindingConverter(), typeof(ISetMethodBindingConverter)); - registerFunction(static () => new StringConverter(), typeof(IBindingTypeConverter)); - registerFunction(static () => new SingleToStringTypeConverter(), typeof(IBindingTypeConverter)); - registerFunction(static () => new DoubleToStringTypeConverter(), typeof(IBindingTypeConverter)); - registerFunction(static () => new DecimalToStringTypeConverter(), typeof(IBindingTypeConverter)); - registerFunction(static () => new ComponentModelTypeConverter(), typeof(IBindingTypeConverter)); + registrar.RegisterConstant(static () => new PlatformOperations()); + registrar.RegisterConstant(static () => new CreatesWinformsCommandBinding()); + registrar.RegisterConstant(static () => new WinformsCreatesObservableForProperty()); + registrar.RegisterConstant(static () => new ActivationForViewFetcher()); + registrar.RegisterConstant(static () => new PanelSetMethodBindingConverter()); + registrar.RegisterConstant(static () => new TableContentSetMethodBindingConverter()); + registrar.RegisterConstant(static () => new StringConverter()); + registrar.RegisterConstant(static () => new SingleToStringTypeConverter()); + registrar.RegisterConstant(static () => new DoubleToStringTypeConverter()); + registrar.RegisterConstant(static () => new DecimalToStringTypeConverter()); + registrar.RegisterConstant(static () => new ComponentModelFallbackConverter()); if (!ModeDetector.InUnitTestRunner()) { diff --git a/src/ReactiveUI.Winforms/RoutedViewHost.cs b/src/ReactiveUI.Winforms/RoutedViewHost.cs index 4fa61d3158..717274c4df 100644 --- a/src/ReactiveUI.Winforms/RoutedViewHost.cs +++ b/src/ReactiveUI.Winforms/RoutedViewHost.cs @@ -9,10 +9,6 @@ namespace ReactiveUI.Winforms; /// A control host which will handling routing between different ViewModels and Views. /// [DefaultProperty("ViewModel")] -#if NET6_0_OR_GREATER -[RequiresDynamicCode("RoutedControlHost uses methods that require dynamic code generation")] -[RequiresUnreferencedCode("RoutedControlHost uses methods that may require unreferenced code")] -#endif public partial class RoutedControlHost : UserControl, IReactiveObject { private readonly CompositeDisposable _disposables = []; @@ -25,10 +21,6 @@ public partial class RoutedControlHost : UserControl, IReactiveObject /// /// Initializes a new instance of the class. /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("RoutedControlHost uses methods that require dynamic code generation")] - [RequiresUnreferencedCode("RoutedControlHost uses methods that may require unreferenced code")] -#endif public RoutedControlHost() { InitializeComponent(); @@ -89,7 +81,7 @@ public RoutedControlHost() ResumeLayout(); }, - RxApp.DefaultExceptionHandler!.OnNext)); + RxState.DefaultExceptionHandler!.OnNext)); } /// diff --git a/src/ReactiveUI.Winforms/TableContentSetMethodBindingConverter.cs b/src/ReactiveUI.Winforms/TableContentSetMethodBindingConverter.cs index 93d34c6753..d0ab12116d 100644 --- a/src/ReactiveUI.Winforms/TableContentSetMethodBindingConverter.cs +++ b/src/ReactiveUI.Winforms/TableContentSetMethodBindingConverter.cs @@ -11,10 +11,6 @@ namespace ReactiveUI.Winforms; public class TableContentSetMethodBindingConverter : ISetMethodBindingConverter { /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("GetAffinityForObjects uses methods that require dynamic code generation")] - [RequiresUnreferencedCode("GetAffinityForObjects uses methods that may require unreferenced code")] -#endif public int GetAffinityForObjects(Type? fromType, Type? toType) => toType != typeof(TableLayoutControlCollection) ? 0 diff --git a/src/ReactiveUI.Winforms/ViewModelViewHost.cs b/src/ReactiveUI.Winforms/ViewModelViewHost.cs index dcafb619c5..f2b225b41e 100644 --- a/src/ReactiveUI.Winforms/ViewModelViewHost.cs +++ b/src/ReactiveUI.Winforms/ViewModelViewHost.cs @@ -8,11 +8,13 @@ namespace ReactiveUI.Winforms; /// /// A view model control host which will find and host the View for a ViewModel. /// +/// +/// This class uses reflection to determine view model types at runtime through ViewLocator. +/// For AOT-compatible scenarios, use ViewModelControlHost<TViewModel> instead. +/// [DefaultProperty("ViewModel")] -#if NET6_0_OR_GREATER -[RequiresDynamicCode("ViewModelControlHost uses methods that require dynamic code generation")] -[RequiresUnreferencedCode("ViewModelControlHost 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 partial class ViewModelControlHost : UserControl, IReactiveObject, IViewFor { private readonly CompositeDisposable _disposables = []; @@ -27,10 +29,6 @@ public partial class ViewModelControlHost : UserControl, IReactiveObject, IViewF /// /// Initializes a new instance of the class. /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("ViewModelControlHost uses methods that require dynamic code generation")] - [RequiresUnreferencedCode("ViewModelControlHost uses methods that may require unreferenced code")] -#endif public ViewModelControlHost() { InitializeComponent(); @@ -146,10 +144,6 @@ protected override void Dispose(bool disposing) base.Dispose(disposing); } -#if NET6_0_OR_GREATER - [RequiresDynamicCode("SetupBindings uses methods that require dynamic code generation")] - [RequiresUnreferencedCode("SetupBindings uses methods that may require unreferenced code")] -#endif private IEnumerable SetupBindings() { var viewChanges = @@ -228,6 +222,6 @@ private IEnumerable SetupBindings() Content = view; } }, - RxApp.DefaultExceptionHandler!.OnNext); + RxState.DefaultExceptionHandler!.OnNext); } } diff --git a/src/ReactiveUI.Winforms/WinformsCreatesObservableForProperty.cs b/src/ReactiveUI.Winforms/WinformsCreatesObservableForProperty.cs index c3bc9b0cb7..cd36ed9012 100644 --- a/src/ReactiveUI.Winforms/WinformsCreatesObservableForProperty.cs +++ b/src/ReactiveUI.Winforms/WinformsCreatesObservableForProperty.cs @@ -12,21 +12,14 @@ namespace ReactiveUI.Winforms; /// particularly useful types. /// /// -#if NET6_0_OR_GREATER -[RequiresDynamicCode("WinformsCreatesObservableForProperty uses methods that require dynamic code generation")] -[RequiresUnreferencedCode("WinformsCreatesObservableForProperty uses methods that may require unreferenced code")] -#endif public class WinformsCreatesObservableForProperty : ICreatesObservableForProperty { private static readonly MemoizingMRUCache<(Type type, string name), EventInfo?> EventInfoCache = new( static (pair, _) => pair.type.GetEvent(pair.name + "Changed", BindingFlags.FlattenHierarchy | BindingFlags.Instance | BindingFlags.Public), - RxApp.SmallCacheLimit); + RxCacheSize.SmallCacheLimit); /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("GetAffinityForObject uses methods that require dynamic code generation")] - [RequiresUnreferencedCode("GetAffinityForObject uses methods that may require unreferenced code")] -#endif + [RequiresUnreferencedCode("Uses reflection over runtime types which is not trim- or AOT-safe.")] public int GetAffinityForObject(Type type, string propertyName, bool beforeChanged = false) { var supportsTypeBinding = typeof(Component).IsAssignableFrom(type); @@ -40,10 +33,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 + [RequiresUnreferencedCode("Uses reflection over runtime types which is not trim- or AOT-safe.")] public IObservable> GetNotificationForProperty(object sender, Expression expression, string propertyName, bool beforeChanged = false, bool suppressWarnings = false) { ArgumentExceptionHelper.ThrowIfNull(sender); @@ -70,7 +60,7 @@ public int GetAffinityForObject(Type type, string propertyName, bool beforeChang } }); - var scheduler = RxApp.MainThreadScheduler; + var scheduler = RxSchedulers.MainThreadScheduler; ei.AddEventHandler(sender, handler); return Disposable.Create(() => scheduler.Schedule(() => ei.RemoveEventHandler(sender, handler))); diff --git a/src/ReactiveUI.Wpf/ActivationForViewFetcher.cs b/src/ReactiveUI.Wpf/ActivationForViewFetcher.cs index 4e6728a230..ff81f99023 100644 --- a/src/ReactiveUI.Wpf/ActivationForViewFetcher.cs +++ b/src/ReactiveUI.Wpf/ActivationForViewFetcher.cs @@ -19,10 +19,6 @@ public class ActivationForViewFetcher : IActivationForViewFetcher public int GetAffinityForView(Type view) => typeof(FrameworkElement).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) { if (view is not FrameworkElement fe) diff --git a/src/ReactiveUI.Wpf/AutoSuspendHelper.cs b/src/ReactiveUI.Wpf/AutoSuspendHelper.cs index 0afd55d378..25dbcde52b 100644 --- a/src/ReactiveUI.Wpf/AutoSuspendHelper.cs +++ b/src/ReactiveUI.Wpf/AutoSuspendHelper.cs @@ -8,7 +8,7 @@ namespace ReactiveUI; /// -/// Wires WPF lifecycle events into so application state can be persisted automatically. +/// Wires WPF lifecycle events into so application state can be persisted automatically. /// /// /// @@ -33,18 +33,14 @@ namespace ReactiveUI; /// IdleTimeout = TimeSpan.FromSeconds(10) /// }; /// -/// RxApp.SuspensionHost.CreateNewAppState = () => new ShellState(); -/// RxApp.SuspensionHost.SetupDefaultSuspendResume(new FileSuspensionDriver(LocalAppDataProvider.Resolve())); +/// RxSuspension.SuspensionHost.CreateNewAppState = () => new ShellState(); +/// RxSuspension.SuspensionHost.SetupDefaultSuspendResume(new FileSuspensionDriver(LocalAppDataProvider.Resolve())); /// } /// } /// ]]> /// /// /// -#if NET6_0_OR_GREATER -[RequiresDynamicCode("AutoSuspendHelper uses RxApp.SuspensionHost and TaskpoolScheduler which require dynamic code generation")] -[RequiresUnreferencedCode("AutoSuspendHelper uses RxApp.SuspensionHost and TaskpoolScheduler which may require unreferenced code")] -#endif public class AutoSuspendHelper : IEnableLogger { /// @@ -55,7 +51,7 @@ public AutoSuspendHelper(Application app) { IdleTimeout = TimeSpan.FromSeconds(15.0); - RxApp.SuspensionHost.IsLaunchingNew = + RxSuspension.SuspensionHost.IsLaunchingNew = Observable.FromEvent( eventHandler => { @@ -65,13 +61,13 @@ public AutoSuspendHelper(Application app) x => app.Startup += x, x => app.Startup -= x); - RxApp.SuspensionHost.IsUnpausing = + RxSuspension.SuspensionHost.IsUnpausing = Observable.FromEvent( eventHandler => (_, _) => eventHandler(Unit.Default), x => app.Activated += x, x => app.Activated -= x); - RxApp.SuspensionHost.IsResuming = Observable.Never; + RxSuspension.SuspensionHost.IsResuming = Observable.Never; // NB: No way to tell OS that we need time to suspend, we have to // do it in-process @@ -89,16 +85,16 @@ public AutoSuspendHelper(Application app) x => app.Exit += x, x => app.Exit -= x); - RxApp.SuspensionHost.ShouldPersistState = exit.Merge( + RxSuspension.SuspensionHost.ShouldPersistState = exit.Merge( deactivated - .SelectMany(_ => Observable.Timer(IdleTimeout, RxApp.TaskpoolScheduler)) - .TakeUntil(RxApp.SuspensionHost.IsUnpausing) + .SelectMany(_ => Observable.Timer(IdleTimeout, RxSchedulers.TaskpoolScheduler)) + .TakeUntil(RxSuspension.SuspensionHost.IsUnpausing) .Repeat() .Select(_ => Disposable.Empty)); var untimelyDeath = new Subject(); AppDomain.CurrentDomain.UnhandledException += (_, _) => untimelyDeath.OnNext(Unit.Default); - RxApp.SuspensionHost.ShouldInvalidateState = untimelyDeath; + RxSuspension.SuspensionHost.ShouldInvalidateState = untimelyDeath; } /// diff --git a/src/ReactiveUI.Wpf/Binding/ValidationBindingMixins.cs b/src/ReactiveUI.Wpf/Binding/ValidationBindingMixins.cs index 16fe5ecd1a..9ebc5e3d3a 100644 --- a/src/ReactiveUI.Wpf/Binding/ValidationBindingMixins.cs +++ b/src/ReactiveUI.Wpf/Binding/ValidationBindingMixins.cs @@ -29,10 +29,6 @@ public static class ValidationBindingMixins /// An instance of that, when disposed, /// disconnects the binding. /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("BindWithValidation uses methods that require dynamic code generation")] - [RequiresUnreferencedCode("BindWithValidation uses methods that may require unreferenced code")] -#endif public static IReactiveBinding BindWithValidation(this TView view, TViewModel viewModel, Expression> viewModelPropertySelector, Expression> frameworkElementSelector) where TView : class, IViewFor where TViewModel : class diff --git a/src/ReactiveUI.Wpf/Binding/ValidationBindingWpf.cs b/src/ReactiveUI.Wpf/Binding/ValidationBindingWpf.cs index bb4e4bf9a0..9b893b4291 100644 --- a/src/ReactiveUI.Wpf/Binding/ValidationBindingWpf.cs +++ b/src/ReactiveUI.Wpf/Binding/ValidationBindingWpf.cs @@ -32,10 +32,6 @@ internal class ValidationBindingWpf : IReact private readonly string _vmPropertyName; private IDisposable? _inner; -#if NET6_0_OR_GREATER - [RequiresDynamicCode("ValidationBindingWpf uses methods that require dynamic code generation")] - [RequiresUnreferencedCode("ValidationBindingWpf uses methods that may require unreferenced code")] -#endif public ValidationBindingWpf( TView view, TViewModel viewModel, diff --git a/src/ReactiveUI.Wpf/Common/AutoDataTemplateBindingHook.cs b/src/ReactiveUI.Wpf/Common/AutoDataTemplateBindingHook.cs index beb285f8b0..adf5d53010 100644 --- a/src/ReactiveUI.Wpf/Common/AutoDataTemplateBindingHook.cs +++ b/src/ReactiveUI.Wpf/Common/AutoDataTemplateBindingHook.cs @@ -33,10 +33,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) { ArgumentExceptionHelper.ThrowIfNull(getCurrentViewProperties); diff --git a/src/ReactiveUI.Wpf/Common/BooleanToVisibilityTypeConverter.cs b/src/ReactiveUI.Wpf/Common/BooleanToVisibilityTypeConverter.cs index aee8c30278..520652417e 100644 --- a/src/ReactiveUI.Wpf/Common/BooleanToVisibilityTypeConverter.cs +++ b/src/ReactiveUI.Wpf/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 HAS_MAUI using Microsoft.Maui; @@ -22,56 +24,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 (WPF only, ignored on UNO/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 !HAS_UNO && !HAS_WINUI - var notVisible = (hint & BooleanToVisibilityHint.UseHidden) != 0 ? Visibility.Hidden : Visibility.Collapsed; + var notVisible = (hint & BooleanToVisibilityHint.UseHidden) != 0 + ? Visibility.Hidden + : Visibility.Collapsed; #else - var notVisible = Visibility.Collapsed; + var 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.Wpf/Common/RoutedViewHost.cs b/src/ReactiveUI.Wpf/Common/RoutedViewHost.cs index a4beffef27..b282ddcaae 100644 --- a/src/ReactiveUI.Wpf/Common/RoutedViewHost.cs +++ b/src/ReactiveUI.Wpf/Common/RoutedViewHost.cs @@ -59,10 +59,6 @@ class RoutedViewHost : TransitioningContentControl, IActivatableView, IEnableLog /// /// 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; @@ -106,7 +102,7 @@ public RoutedViewHost() this.WhenActivated(d => d(vmAndContract.DistinctUntilChanged().Subscribe( ResolveViewForViewModel, - ex => RxApp.DefaultExceptionHandler.OnNext(ex)))); + ex => RxState.DefaultExceptionHandler.OnNext(ex)))); } /// @@ -157,10 +153,6 @@ public string? ViewContract /// public IViewLocator? ViewLocator { get; set; } -#if NET6_0_OR_GREATER - [RequiresDynamicCode("RoutedViewHost uses methods that require dynamic code generation")] - [RequiresUnreferencedCode("RoutedViewHost uses methods that may require unreferenced code")] -#endif private void ResolveViewForViewModel((IRoutableViewModel? viewModel, string? contract) x) { if (x.viewModel is null) diff --git a/src/ReactiveUI.Wpf/Common/ViewModelViewHost.cs b/src/ReactiveUI.Wpf/Common/ViewModelViewHost.cs index 2c25ed620b..e7c866c02b 100644 --- a/src/ReactiveUI.Wpf/Common/ViewModelViewHost.cs +++ b/src/ReactiveUI.Wpf/Common/ViewModelViewHost.cs @@ -33,10 +33,6 @@ namespace ReactiveUI; /// the ViewModel property and display it. This control is very useful /// inside a DataTemplate to display the View associated with a ViewModel. /// -#if NET6_0_OR_GREATER -[RequiresDynamicCode("ViewModelViewHost uses ReactiveUI extension methods and RxApp which require dynamic code generation")] -[RequiresUnreferencedCode("ViewModelViewHost uses ReactiveUI extension methods and RxApp which may require unreferenced code")] -#endif public #if HAS_UNO partial diff --git a/src/ReactiveUI.Wpf/Common/VisibilityToBooleanTypeConverter.cs b/src/ReactiveUI.Wpf/Common/VisibilityToBooleanTypeConverter.cs new file mode 100644 index 0000000000..98b34dcfef --- /dev/null +++ b/src/ReactiveUI.Wpf/Common/VisibilityToBooleanTypeConverter.cs @@ -0,0 +1,57 @@ +// 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 HAS_MAUI +using Microsoft.Maui; + +#endif +#if HAS_WINUI +using Microsoft.UI.Xaml; +#elif HAS_UNO +using Windows.UI.Xaml; +#else +using System.Windows; +#endif + +#if HAS_UNO +namespace ReactiveUI.Uno +#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.Wpf/DependencyObjectObservableForProperty.cs b/src/ReactiveUI.Wpf/DependencyObjectObservableForProperty.cs index 24b445628a..a68cbb36c0 100644 --- a/src/ReactiveUI.Wpf/DependencyObjectObservableForProperty.cs +++ b/src/ReactiveUI.Wpf/DependencyObjectObservableForProperty.cs @@ -14,10 +14,6 @@ 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())) @@ -29,10 +25,6 @@ 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, System.Linq.Expressions.Expression expression, string propertyName, bool beforeChanged = false, bool suppressWarnings = false) { ArgumentExceptionHelper.ThrowIfNull(sender); @@ -57,17 +49,13 @@ public int GetAffinityForObject(Type type, string propertyName, bool beforeChang return Observable.Create>(subj => { var handler = new EventHandler((_, _) => subj.OnNext(new ObservedChange(sender, expression, default))); - var scheduler = RxApp.MainThreadScheduler; + var scheduler = RxSchedulers.MainThreadScheduler; dependencyPropertyDescriptor.AddValueChanged(sender, handler); return Disposable.Create(() => scheduler.Schedule(() => dependencyPropertyDescriptor.RemoveValueChanged(sender, handler))); }); } -#if NET6_0_OR_GREATER - [RequiresDynamicCode("GetDependencyProperty uses methods that require dynamic code generation")] - [RequiresUnreferencedCode("GetDependencyProperty uses methods that may require unreferenced code")] -#endif private static DependencyProperty? GetDependencyProperty(Type type, string propertyName) { var fi = Array.Find(type.GetTypeInfo().GetFields(BindingFlags.FlattenHierarchy | BindingFlags.Static | BindingFlags.Public), x => x.Name == propertyName + "Property" && x.IsStatic); diff --git a/src/ReactiveUI.Wpf/Registrations.cs b/src/ReactiveUI.Wpf/Registrations.cs index 3dafd8e599..c2265f1cb2 100644 --- a/src/ReactiveUI.Wpf/Registrations.cs +++ b/src/ReactiveUI.Wpf/Registrations.cs @@ -11,27 +11,21 @@ namespace ReactiveUI.Wpf; 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); - registerFunction(static () => new PlatformOperations(), typeof(IPlatformOperations)); - - registerFunction(static () => new ActivationForViewFetcher(), typeof(IActivationForViewFetcher)); - registerFunction(static () => new DependencyObjectObservableForProperty(), typeof(ICreatesObservableForProperty)); - registerFunction(static () => new StringConverter(), typeof(IBindingTypeConverter)); - registerFunction(static () => new SingleToStringTypeConverter(), typeof(IBindingTypeConverter)); - registerFunction(static () => new DoubleToStringTypeConverter(), typeof(IBindingTypeConverter)); - registerFunction(static () => new DecimalToStringTypeConverter(), typeof(IBindingTypeConverter)); - registerFunction(static () => new BooleanToVisibilityTypeConverter(), typeof(IBindingTypeConverter)); - registerFunction(static () => new AutoDataTemplateBindingHook(), typeof(IPropertyBindingHook)); - registerFunction(static () => new ComponentModelTypeConverter(), typeof(IBindingTypeConverter)); + registrar.RegisterConstant(static () => new PlatformOperations()); + registrar.RegisterConstant(static () => new ActivationForViewFetcher()); + registrar.RegisterConstant(static () => new DependencyObjectObservableForProperty()); + registrar.RegisterConstant(static () => new StringConverter()); + registrar.RegisterConstant(static () => new SingleToStringTypeConverter()); + registrar.RegisterConstant(static () => new DoubleToStringTypeConverter()); + registrar.RegisterConstant(static () => new DecimalToStringTypeConverter()); + registrar.RegisterConstant(static () => new BooleanToVisibilityTypeConverter()); + registrar.RegisterConstant(static () => new VisibilityToBooleanTypeConverter()); + registrar.RegisterConstant(static () => new AutoDataTemplateBindingHook()); + registrar.RegisterConstant(static () => new ComponentModelFallbackConverter()); if (!ModeDetector.InUnitTestRunner()) { @@ -40,6 +34,6 @@ public void Register(Action, Type> registerFunction) RxSchedulers.TaskpoolScheduler = TaskPoolScheduler.Default; } - RxApp.SuppressViewCommandBindingMessage = true; + RxSchedulers.SuppressViewCommandBindingMessage = true; } } diff --git a/src/ReactiveUI.Wpf/Rx/Linq/DispatcherObservable.cs b/src/ReactiveUI.Wpf/Rx/Linq/DispatcherObservable.cs index 5560683b77..19787135a7 100644 --- a/src/ReactiveUI.Wpf/Rx/Linq/DispatcherObservable.cs +++ b/src/ReactiveUI.Wpf/Rx/Linq/DispatcherObservable.cs @@ -67,7 +67,6 @@ public static IObservable ObserveOn(this IObservable return ObserveOn_(source, scheduler.Dispatcher, scheduler.Priority); } - /// /// Wraps the source sequence in order to run its observer callbacks on the dispatcher associated with the specified object. /// @@ -108,7 +107,6 @@ private static IObservable ObserveOn_(IObservable sou return Synchronization.ObserveOn(source, new DispatcherSynchronizationContext(dispatcher, priority)); } - private static IObservable ObserveOn_(IObservable source, Dispatcher dispatcher) { return Synchronization.ObserveOn(source, new DispatcherSynchronizationContext(dispatcher)); @@ -161,7 +159,6 @@ public static IObservable SubscribeOn(this IObservable /// Wraps the source sequence in order to run its subscription and unsubscription logic on the specified dispatcher scheduler. /// @@ -231,7 +228,6 @@ private static IObservable SubscribeOn_(IObservable s return Synchronization.SubscribeOn(source, new DispatcherSynchronizationContext(dispatcher, priority)); } - private static IObservable SubscribeOn_(IObservable source, Dispatcher dispatcher) { return Synchronization.SubscribeOn(source, new DispatcherSynchronizationContext(dispatcher)); diff --git a/src/ReactiveUI.sln b/src/ReactiveUI.sln deleted file mode 100644 index ff27163d7a..0000000000 --- a/src/ReactiveUI.sln +++ /dev/null @@ -1,408 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 18 -VisualStudioVersion = 18.0.11018.127 -MinimumVisualStudioVersion = 16.0.31613.86 -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{BD9762CF-E104-481C-96A6-26E624B86283}" - ProjectSection(SolutionItems) = preProject - ..\.editorconfig = ..\.editorconfig - Directory.Build.props = Directory.Build.props - Directory.Build.targets = Directory.Build.targets - Directory.Packages.props = Directory.Packages.props - stylecop.json = stylecop.json - ..\version.json = ..\version.json - EndProjectSection -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveUI", "ReactiveUI\ReactiveUI.csproj", "{464CB812-F99F-401B-BE4C-E8F0515CD19D}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveUI.Testing", "ReactiveUI.Testing\ReactiveUI.Testing.csproj", "{DDF89A7A-5CC9-4243-98E4-462860D5D963}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveUI.Blend", "ReactiveUI.Blend\ReactiveUI.Blend.csproj", "{C11F6165-6142-476F-83F1-CFEBC330C769}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveUI.Winforms", "ReactiveUI.Winforms\ReactiveUI.Winforms.csproj", "{B311B0EC-CEF3-45E6-BA7A-EC6AB58E7E7D}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveUI.Tests", "tests\ReactiveUI.Tests\ReactiveUI.Tests.csproj", "{2ADE0A50-5012-4341-8F4F-97597C2D6920}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveUI.Wpf", "ReactiveUI.Wpf\ReactiveUI.Wpf.csproj", "{15CF3AA6-9F7C-4F23-BAE7-4A93352E94B6}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveUI.Splat.Tests", "tests\ReactiveUI.Splat.Tests\ReactiveUI.Splat.Tests.csproj", "{7ED6D69F-138F-40BD-9F37-3E4050E4D19B}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveUI.Testing.Tests", "tests\ReactiveUI.Testing.Tests\ReactiveUI.Testing.Tests.csproj", "{CD8B19A9-316E-4FBC-8F0C-87ADC6AAD684}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveUI.Blazor", "ReactiveUI.Blazor\ReactiveUI.Blazor.csproj", "{0C7EDFF0-80BE-4FFC-A1B9-0C48043B71F3}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveUI.Drawing", "ReactiveUI.Drawing\ReactiveUI.Drawing.csproj", "{999D555D-C567-457C-95F7-8AD61310C56E}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{2AE709FA-BE58-4287-BFD3-E80BEB605125}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{3F9A8B1C-5D2E-4A7F-9B8C-1E6D4F5A7B9C}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveUI.Maui", "ReactiveUI.Maui\ReactiveUI.Maui.csproj", "{3C552CA0-C364-4E51-8825-35D082C1D5BA}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveUI.WinUI", "ReactiveUI.WinUI\ReactiveUI.WinUI.csproj", "{4FF9F04B-928E-47B6-836F-546B584F597C}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveUI.AndroidX", "ReactiveUI.AndroidX\ReactiveUI.AndroidX.csproj", "{3F642A3D-CBA6-4E17-903C-672FDBE9D9E8}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReactiveUI.AOT.Tests", "tests\ReactiveUI.AOTTests\ReactiveUI.AOT.Tests.csproj", "{D9CF5BB9-12FC-CF7E-B415-ED924A3E87AE}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReactiveUI.Builder.Tests", "tests\ReactiveUI.Builder.Tests\ReactiveUI.Builder.Tests.csproj", "{8034024A-2804-4920-921A-86ADCCBF0838}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReactiveUI.Builder.Maui.Tests", "tests\ReactiveUI.Builder.Maui.Tests\ReactiveUI.Builder.Maui.Tests.csproj", "{2A4B719C-4958-AD92-4A22-6472AAF98A37}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReactiveUI.Builder.WpfApp", "examples\ReactiveUI.Builder.WpfApp\ReactiveUI.Builder.WpfApp.csproj", "{B5B3101B-6638-418D-AE82-59984D9D6AC7}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReactiveUI.NonParallel.Tests", "tests\ReactiveUI.NonParallel.Tests\ReactiveUI.NonParallel.Tests.csproj", "{C4338ACD-DFDC-1724-6F35-36519B285D1A}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReactiveUI.Blazor.Tests", "tests\ReactiveUI.Blazor.Tests\ReactiveUI.Blazor.Tests.csproj", "{928C9161-B7A5-41EA-991C-C4FB44605F97}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReactiveUI.Maui.Tests", "tests\ReactiveUI.Maui.Tests\ReactiveUI.Maui.Tests.csproj", "{EEF549E7-18F7-4FAC-AB6D-8FB335DA5AE3}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Debug|arm64 = Debug|arm64 - Debug|x64 = Debug|x64 - Debug|x86 = Debug|x86 - Release|Any CPU = Release|Any CPU - Release|arm64 = Release|arm64 - Release|x64 = Release|x64 - Release|x86 = Release|x86 - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {464CB812-F99F-401B-BE4C-E8F0515CD19D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {464CB812-F99F-401B-BE4C-E8F0515CD19D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {464CB812-F99F-401B-BE4C-E8F0515CD19D}.Debug|arm64.ActiveCfg = Debug|Any CPU - {464CB812-F99F-401B-BE4C-E8F0515CD19D}.Debug|arm64.Build.0 = Debug|Any CPU - {464CB812-F99F-401B-BE4C-E8F0515CD19D}.Debug|x64.ActiveCfg = Debug|Any CPU - {464CB812-F99F-401B-BE4C-E8F0515CD19D}.Debug|x64.Build.0 = Debug|Any CPU - {464CB812-F99F-401B-BE4C-E8F0515CD19D}.Debug|x86.ActiveCfg = Debug|Any CPU - {464CB812-F99F-401B-BE4C-E8F0515CD19D}.Debug|x86.Build.0 = Debug|Any CPU - {464CB812-F99F-401B-BE4C-E8F0515CD19D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {464CB812-F99F-401B-BE4C-E8F0515CD19D}.Release|Any CPU.Build.0 = Release|Any CPU - {464CB812-F99F-401B-BE4C-E8F0515CD19D}.Release|arm64.ActiveCfg = Release|Any CPU - {464CB812-F99F-401B-BE4C-E8F0515CD19D}.Release|arm64.Build.0 = Release|Any CPU - {464CB812-F99F-401B-BE4C-E8F0515CD19D}.Release|x64.ActiveCfg = Release|Any CPU - {464CB812-F99F-401B-BE4C-E8F0515CD19D}.Release|x64.Build.0 = Release|Any CPU - {464CB812-F99F-401B-BE4C-E8F0515CD19D}.Release|x86.ActiveCfg = Release|Any CPU - {464CB812-F99F-401B-BE4C-E8F0515CD19D}.Release|x86.Build.0 = Release|Any CPU - {DDF89A7A-5CC9-4243-98E4-462860D5D963}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DDF89A7A-5CC9-4243-98E4-462860D5D963}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DDF89A7A-5CC9-4243-98E4-462860D5D963}.Debug|arm64.ActiveCfg = Debug|Any CPU - {DDF89A7A-5CC9-4243-98E4-462860D5D963}.Debug|arm64.Build.0 = Debug|Any CPU - {DDF89A7A-5CC9-4243-98E4-462860D5D963}.Debug|x64.ActiveCfg = Debug|Any CPU - {DDF89A7A-5CC9-4243-98E4-462860D5D963}.Debug|x64.Build.0 = Debug|Any CPU - {DDF89A7A-5CC9-4243-98E4-462860D5D963}.Debug|x86.ActiveCfg = Debug|Any CPU - {DDF89A7A-5CC9-4243-98E4-462860D5D963}.Debug|x86.Build.0 = Debug|Any CPU - {DDF89A7A-5CC9-4243-98E4-462860D5D963}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DDF89A7A-5CC9-4243-98E4-462860D5D963}.Release|Any CPU.Build.0 = Release|Any CPU - {DDF89A7A-5CC9-4243-98E4-462860D5D963}.Release|arm64.ActiveCfg = Release|Any CPU - {DDF89A7A-5CC9-4243-98E4-462860D5D963}.Release|arm64.Build.0 = Release|Any CPU - {DDF89A7A-5CC9-4243-98E4-462860D5D963}.Release|x64.ActiveCfg = Release|Any CPU - {DDF89A7A-5CC9-4243-98E4-462860D5D963}.Release|x64.Build.0 = Release|Any CPU - {DDF89A7A-5CC9-4243-98E4-462860D5D963}.Release|x86.ActiveCfg = Release|Any CPU - {DDF89A7A-5CC9-4243-98E4-462860D5D963}.Release|x86.Build.0 = Release|Any CPU - {C11F6165-6142-476F-83F1-CFEBC330C769}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C11F6165-6142-476F-83F1-CFEBC330C769}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C11F6165-6142-476F-83F1-CFEBC330C769}.Debug|arm64.ActiveCfg = Debug|Any CPU - {C11F6165-6142-476F-83F1-CFEBC330C769}.Debug|arm64.Build.0 = Debug|Any CPU - {C11F6165-6142-476F-83F1-CFEBC330C769}.Debug|x64.ActiveCfg = Debug|Any CPU - {C11F6165-6142-476F-83F1-CFEBC330C769}.Debug|x64.Build.0 = Debug|Any CPU - {C11F6165-6142-476F-83F1-CFEBC330C769}.Debug|x86.ActiveCfg = Debug|Any CPU - {C11F6165-6142-476F-83F1-CFEBC330C769}.Debug|x86.Build.0 = Debug|Any CPU - {C11F6165-6142-476F-83F1-CFEBC330C769}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C11F6165-6142-476F-83F1-CFEBC330C769}.Release|Any CPU.Build.0 = Release|Any CPU - {C11F6165-6142-476F-83F1-CFEBC330C769}.Release|arm64.ActiveCfg = Release|Any CPU - {C11F6165-6142-476F-83F1-CFEBC330C769}.Release|arm64.Build.0 = Release|Any CPU - {C11F6165-6142-476F-83F1-CFEBC330C769}.Release|x64.ActiveCfg = Release|Any CPU - {C11F6165-6142-476F-83F1-CFEBC330C769}.Release|x64.Build.0 = Release|Any CPU - {C11F6165-6142-476F-83F1-CFEBC330C769}.Release|x86.ActiveCfg = Release|Any CPU - {C11F6165-6142-476F-83F1-CFEBC330C769}.Release|x86.Build.0 = Release|Any CPU - {B311B0EC-CEF3-45E6-BA7A-EC6AB58E7E7D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B311B0EC-CEF3-45E6-BA7A-EC6AB58E7E7D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B311B0EC-CEF3-45E6-BA7A-EC6AB58E7E7D}.Debug|arm64.ActiveCfg = Debug|Any CPU - {B311B0EC-CEF3-45E6-BA7A-EC6AB58E7E7D}.Debug|arm64.Build.0 = Debug|Any CPU - {B311B0EC-CEF3-45E6-BA7A-EC6AB58E7E7D}.Debug|x64.ActiveCfg = Debug|Any CPU - {B311B0EC-CEF3-45E6-BA7A-EC6AB58E7E7D}.Debug|x64.Build.0 = Debug|Any CPU - {B311B0EC-CEF3-45E6-BA7A-EC6AB58E7E7D}.Debug|x86.ActiveCfg = Debug|Any CPU - {B311B0EC-CEF3-45E6-BA7A-EC6AB58E7E7D}.Debug|x86.Build.0 = Debug|Any CPU - {B311B0EC-CEF3-45E6-BA7A-EC6AB58E7E7D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B311B0EC-CEF3-45E6-BA7A-EC6AB58E7E7D}.Release|Any CPU.Build.0 = Release|Any CPU - {B311B0EC-CEF3-45E6-BA7A-EC6AB58E7E7D}.Release|arm64.ActiveCfg = Release|Any CPU - {B311B0EC-CEF3-45E6-BA7A-EC6AB58E7E7D}.Release|arm64.Build.0 = Release|Any CPU - {B311B0EC-CEF3-45E6-BA7A-EC6AB58E7E7D}.Release|x64.ActiveCfg = Release|Any CPU - {B311B0EC-CEF3-45E6-BA7A-EC6AB58E7E7D}.Release|x64.Build.0 = Release|Any CPU - {B311B0EC-CEF3-45E6-BA7A-EC6AB58E7E7D}.Release|x86.ActiveCfg = Release|Any CPU - {B311B0EC-CEF3-45E6-BA7A-EC6AB58E7E7D}.Release|x86.Build.0 = Release|Any CPU - {2ADE0A50-5012-4341-8F4F-97597C2D6920}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2ADE0A50-5012-4341-8F4F-97597C2D6920}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2ADE0A50-5012-4341-8F4F-97597C2D6920}.Debug|arm64.ActiveCfg = Debug|Any CPU - {2ADE0A50-5012-4341-8F4F-97597C2D6920}.Debug|arm64.Build.0 = Debug|Any CPU - {2ADE0A50-5012-4341-8F4F-97597C2D6920}.Debug|x64.ActiveCfg = Debug|Any CPU - {2ADE0A50-5012-4341-8F4F-97597C2D6920}.Debug|x64.Build.0 = Debug|Any CPU - {2ADE0A50-5012-4341-8F4F-97597C2D6920}.Debug|x86.ActiveCfg = Debug|Any CPU - {2ADE0A50-5012-4341-8F4F-97597C2D6920}.Debug|x86.Build.0 = Debug|Any CPU - {2ADE0A50-5012-4341-8F4F-97597C2D6920}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2ADE0A50-5012-4341-8F4F-97597C2D6920}.Release|Any CPU.Build.0 = Release|Any CPU - {2ADE0A50-5012-4341-8F4F-97597C2D6920}.Release|arm64.ActiveCfg = Release|Any CPU - {2ADE0A50-5012-4341-8F4F-97597C2D6920}.Release|arm64.Build.0 = Release|Any CPU - {2ADE0A50-5012-4341-8F4F-97597C2D6920}.Release|x64.ActiveCfg = Release|Any CPU - {2ADE0A50-5012-4341-8F4F-97597C2D6920}.Release|x64.Build.0 = Release|Any CPU - {2ADE0A50-5012-4341-8F4F-97597C2D6920}.Release|x86.ActiveCfg = Release|Any CPU - {2ADE0A50-5012-4341-8F4F-97597C2D6920}.Release|x86.Build.0 = Release|Any CPU - {15CF3AA6-9F7C-4F23-BAE7-4A93352E94B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {15CF3AA6-9F7C-4F23-BAE7-4A93352E94B6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {15CF3AA6-9F7C-4F23-BAE7-4A93352E94B6}.Debug|arm64.ActiveCfg = Debug|Any CPU - {15CF3AA6-9F7C-4F23-BAE7-4A93352E94B6}.Debug|arm64.Build.0 = Debug|Any CPU - {15CF3AA6-9F7C-4F23-BAE7-4A93352E94B6}.Debug|x64.ActiveCfg = Debug|Any CPU - {15CF3AA6-9F7C-4F23-BAE7-4A93352E94B6}.Debug|x64.Build.0 = Debug|Any CPU - {15CF3AA6-9F7C-4F23-BAE7-4A93352E94B6}.Debug|x86.ActiveCfg = Debug|Any CPU - {15CF3AA6-9F7C-4F23-BAE7-4A93352E94B6}.Debug|x86.Build.0 = Debug|Any CPU - {15CF3AA6-9F7C-4F23-BAE7-4A93352E94B6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {15CF3AA6-9F7C-4F23-BAE7-4A93352E94B6}.Release|Any CPU.Build.0 = Release|Any CPU - {15CF3AA6-9F7C-4F23-BAE7-4A93352E94B6}.Release|arm64.ActiveCfg = Release|Any CPU - {15CF3AA6-9F7C-4F23-BAE7-4A93352E94B6}.Release|arm64.Build.0 = Release|Any CPU - {15CF3AA6-9F7C-4F23-BAE7-4A93352E94B6}.Release|x64.ActiveCfg = Release|Any CPU - {15CF3AA6-9F7C-4F23-BAE7-4A93352E94B6}.Release|x64.Build.0 = Release|Any CPU - {15CF3AA6-9F7C-4F23-BAE7-4A93352E94B6}.Release|x86.ActiveCfg = Release|Any CPU - {15CF3AA6-9F7C-4F23-BAE7-4A93352E94B6}.Release|x86.Build.0 = Release|Any CPU - {7ED6D69F-138F-40BD-9F37-3E4050E4D19B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7ED6D69F-138F-40BD-9F37-3E4050E4D19B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7ED6D69F-138F-40BD-9F37-3E4050E4D19B}.Debug|arm64.ActiveCfg = Debug|Any CPU - {7ED6D69F-138F-40BD-9F37-3E4050E4D19B}.Debug|arm64.Build.0 = Debug|Any CPU - {7ED6D69F-138F-40BD-9F37-3E4050E4D19B}.Debug|x64.ActiveCfg = Debug|Any CPU - {7ED6D69F-138F-40BD-9F37-3E4050E4D19B}.Debug|x64.Build.0 = Debug|Any CPU - {7ED6D69F-138F-40BD-9F37-3E4050E4D19B}.Debug|x86.ActiveCfg = Debug|Any CPU - {7ED6D69F-138F-40BD-9F37-3E4050E4D19B}.Debug|x86.Build.0 = Debug|Any CPU - {7ED6D69F-138F-40BD-9F37-3E4050E4D19B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7ED6D69F-138F-40BD-9F37-3E4050E4D19B}.Release|Any CPU.Build.0 = Release|Any CPU - {7ED6D69F-138F-40BD-9F37-3E4050E4D19B}.Release|arm64.ActiveCfg = Release|Any CPU - {7ED6D69F-138F-40BD-9F37-3E4050E4D19B}.Release|arm64.Build.0 = Release|Any CPU - {7ED6D69F-138F-40BD-9F37-3E4050E4D19B}.Release|x64.ActiveCfg = Release|Any CPU - {7ED6D69F-138F-40BD-9F37-3E4050E4D19B}.Release|x64.Build.0 = Release|Any CPU - {7ED6D69F-138F-40BD-9F37-3E4050E4D19B}.Release|x86.ActiveCfg = Release|Any CPU - {7ED6D69F-138F-40BD-9F37-3E4050E4D19B}.Release|x86.Build.0 = Release|Any CPU - {CD8B19A9-316E-4FBC-8F0C-87ADC6AAD684}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CD8B19A9-316E-4FBC-8F0C-87ADC6AAD684}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CD8B19A9-316E-4FBC-8F0C-87ADC6AAD684}.Debug|arm64.ActiveCfg = Debug|Any CPU - {CD8B19A9-316E-4FBC-8F0C-87ADC6AAD684}.Debug|arm64.Build.0 = Debug|Any CPU - {CD8B19A9-316E-4FBC-8F0C-87ADC6AAD684}.Debug|x64.ActiveCfg = Debug|Any CPU - {CD8B19A9-316E-4FBC-8F0C-87ADC6AAD684}.Debug|x64.Build.0 = Debug|Any CPU - {CD8B19A9-316E-4FBC-8F0C-87ADC6AAD684}.Debug|x86.ActiveCfg = Debug|Any CPU - {CD8B19A9-316E-4FBC-8F0C-87ADC6AAD684}.Debug|x86.Build.0 = Debug|Any CPU - {CD8B19A9-316E-4FBC-8F0C-87ADC6AAD684}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CD8B19A9-316E-4FBC-8F0C-87ADC6AAD684}.Release|Any CPU.Build.0 = Release|Any CPU - {CD8B19A9-316E-4FBC-8F0C-87ADC6AAD684}.Release|arm64.ActiveCfg = Release|Any CPU - {CD8B19A9-316E-4FBC-8F0C-87ADC6AAD684}.Release|arm64.Build.0 = Release|Any CPU - {CD8B19A9-316E-4FBC-8F0C-87ADC6AAD684}.Release|x64.ActiveCfg = Release|Any CPU - {CD8B19A9-316E-4FBC-8F0C-87ADC6AAD684}.Release|x64.Build.0 = Release|Any CPU - {CD8B19A9-316E-4FBC-8F0C-87ADC6AAD684}.Release|x86.ActiveCfg = Release|Any CPU - {CD8B19A9-316E-4FBC-8F0C-87ADC6AAD684}.Release|x86.Build.0 = Release|Any CPU - {0C7EDFF0-80BE-4FFC-A1B9-0C48043B71F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0C7EDFF0-80BE-4FFC-A1B9-0C48043B71F3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0C7EDFF0-80BE-4FFC-A1B9-0C48043B71F3}.Debug|arm64.ActiveCfg = Debug|Any CPU - {0C7EDFF0-80BE-4FFC-A1B9-0C48043B71F3}.Debug|x64.ActiveCfg = Debug|Any CPU - {0C7EDFF0-80BE-4FFC-A1B9-0C48043B71F3}.Debug|x86.ActiveCfg = Debug|Any CPU - {0C7EDFF0-80BE-4FFC-A1B9-0C48043B71F3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0C7EDFF0-80BE-4FFC-A1B9-0C48043B71F3}.Release|Any CPU.Build.0 = Release|Any CPU - {0C7EDFF0-80BE-4FFC-A1B9-0C48043B71F3}.Release|arm64.ActiveCfg = Release|Any CPU - {0C7EDFF0-80BE-4FFC-A1B9-0C48043B71F3}.Release|x64.ActiveCfg = Release|Any CPU - {0C7EDFF0-80BE-4FFC-A1B9-0C48043B71F3}.Release|x86.ActiveCfg = Release|Any CPU - {999D555D-C567-457C-95F7-8AD61310C56E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {999D555D-C567-457C-95F7-8AD61310C56E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {999D555D-C567-457C-95F7-8AD61310C56E}.Debug|arm64.ActiveCfg = Debug|Any CPU - {999D555D-C567-457C-95F7-8AD61310C56E}.Debug|arm64.Build.0 = Debug|Any CPU - {999D555D-C567-457C-95F7-8AD61310C56E}.Debug|x64.ActiveCfg = Debug|Any CPU - {999D555D-C567-457C-95F7-8AD61310C56E}.Debug|x64.Build.0 = Debug|Any CPU - {999D555D-C567-457C-95F7-8AD61310C56E}.Debug|x86.ActiveCfg = Debug|Any CPU - {999D555D-C567-457C-95F7-8AD61310C56E}.Debug|x86.Build.0 = Debug|Any CPU - {999D555D-C567-457C-95F7-8AD61310C56E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {999D555D-C567-457C-95F7-8AD61310C56E}.Release|Any CPU.Build.0 = Release|Any CPU - {999D555D-C567-457C-95F7-8AD61310C56E}.Release|arm64.ActiveCfg = Release|Any CPU - {999D555D-C567-457C-95F7-8AD61310C56E}.Release|arm64.Build.0 = Release|Any CPU - {999D555D-C567-457C-95F7-8AD61310C56E}.Release|x64.ActiveCfg = Release|Any CPU - {999D555D-C567-457C-95F7-8AD61310C56E}.Release|x64.Build.0 = Release|Any CPU - {999D555D-C567-457C-95F7-8AD61310C56E}.Release|x86.ActiveCfg = Release|Any CPU - {999D555D-C567-457C-95F7-8AD61310C56E}.Release|x86.Build.0 = Release|Any CPU - {3C552CA0-C364-4E51-8825-35D082C1D5BA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3C552CA0-C364-4E51-8825-35D082C1D5BA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3C552CA0-C364-4E51-8825-35D082C1D5BA}.Debug|arm64.ActiveCfg = Debug|Any CPU - {3C552CA0-C364-4E51-8825-35D082C1D5BA}.Debug|arm64.Build.0 = Debug|Any CPU - {3C552CA0-C364-4E51-8825-35D082C1D5BA}.Debug|x64.ActiveCfg = Debug|Any CPU - {3C552CA0-C364-4E51-8825-35D082C1D5BA}.Debug|x64.Build.0 = Debug|Any CPU - {3C552CA0-C364-4E51-8825-35D082C1D5BA}.Debug|x86.ActiveCfg = Debug|Any CPU - {3C552CA0-C364-4E51-8825-35D082C1D5BA}.Debug|x86.Build.0 = Debug|Any CPU - {3C552CA0-C364-4E51-8825-35D082C1D5BA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3C552CA0-C364-4E51-8825-35D082C1D5BA}.Release|Any CPU.Build.0 = Release|Any CPU - {3C552CA0-C364-4E51-8825-35D082C1D5BA}.Release|arm64.ActiveCfg = Release|Any CPU - {3C552CA0-C364-4E51-8825-35D082C1D5BA}.Release|arm64.Build.0 = Release|Any CPU - {3C552CA0-C364-4E51-8825-35D082C1D5BA}.Release|x64.ActiveCfg = Release|Any CPU - {3C552CA0-C364-4E51-8825-35D082C1D5BA}.Release|x64.Build.0 = Release|Any CPU - {3C552CA0-C364-4E51-8825-35D082C1D5BA}.Release|x86.ActiveCfg = Release|Any CPU - {3C552CA0-C364-4E51-8825-35D082C1D5BA}.Release|x86.Build.0 = Release|Any CPU - {4FF9F04B-928E-47B6-836F-546B584F597C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4FF9F04B-928E-47B6-836F-546B584F597C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4FF9F04B-928E-47B6-836F-546B584F597C}.Debug|arm64.ActiveCfg = Debug|Any CPU - {4FF9F04B-928E-47B6-836F-546B584F597C}.Debug|arm64.Build.0 = Debug|Any CPU - {4FF9F04B-928E-47B6-836F-546B584F597C}.Debug|x64.ActiveCfg = Debug|Any CPU - {4FF9F04B-928E-47B6-836F-546B584F597C}.Debug|x64.Build.0 = Debug|Any CPU - {4FF9F04B-928E-47B6-836F-546B584F597C}.Debug|x86.ActiveCfg = Debug|Any CPU - {4FF9F04B-928E-47B6-836F-546B584F597C}.Debug|x86.Build.0 = Debug|Any CPU - {4FF9F04B-928E-47B6-836F-546B584F597C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4FF9F04B-928E-47B6-836F-546B584F597C}.Release|Any CPU.Build.0 = Release|Any CPU - {4FF9F04B-928E-47B6-836F-546B584F597C}.Release|arm64.ActiveCfg = Release|Any CPU - {4FF9F04B-928E-47B6-836F-546B584F597C}.Release|arm64.Build.0 = Release|Any CPU - {4FF9F04B-928E-47B6-836F-546B584F597C}.Release|x64.ActiveCfg = Release|Any CPU - {4FF9F04B-928E-47B6-836F-546B584F597C}.Release|x64.Build.0 = Release|Any CPU - {4FF9F04B-928E-47B6-836F-546B584F597C}.Release|x86.ActiveCfg = Release|Any CPU - {4FF9F04B-928E-47B6-836F-546B584F597C}.Release|x86.Build.0 = Release|Any CPU - {3F642A3D-CBA6-4E17-903C-672FDBE9D9E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3F642A3D-CBA6-4E17-903C-672FDBE9D9E8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3F642A3D-CBA6-4E17-903C-672FDBE9D9E8}.Debug|arm64.ActiveCfg = Debug|Any CPU - {3F642A3D-CBA6-4E17-903C-672FDBE9D9E8}.Debug|arm64.Build.0 = Debug|Any CPU - {3F642A3D-CBA6-4E17-903C-672FDBE9D9E8}.Debug|x64.ActiveCfg = Debug|Any CPU - {3F642A3D-CBA6-4E17-903C-672FDBE9D9E8}.Debug|x64.Build.0 = Debug|Any CPU - {3F642A3D-CBA6-4E17-903C-672FDBE9D9E8}.Debug|x86.ActiveCfg = Debug|Any CPU - {3F642A3D-CBA6-4E17-903C-672FDBE9D9E8}.Debug|x86.Build.0 = Debug|Any CPU - {3F642A3D-CBA6-4E17-903C-672FDBE9D9E8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3F642A3D-CBA6-4E17-903C-672FDBE9D9E8}.Release|Any CPU.Build.0 = Release|Any CPU - {3F642A3D-CBA6-4E17-903C-672FDBE9D9E8}.Release|arm64.ActiveCfg = Release|Any CPU - {3F642A3D-CBA6-4E17-903C-672FDBE9D9E8}.Release|arm64.Build.0 = Release|Any CPU - {3F642A3D-CBA6-4E17-903C-672FDBE9D9E8}.Release|x64.ActiveCfg = Release|Any CPU - {3F642A3D-CBA6-4E17-903C-672FDBE9D9E8}.Release|x64.Build.0 = Release|Any CPU - {3F642A3D-CBA6-4E17-903C-672FDBE9D9E8}.Release|x86.ActiveCfg = Release|Any CPU - {3F642A3D-CBA6-4E17-903C-672FDBE9D9E8}.Release|x86.Build.0 = Release|Any CPU - {D9CF5BB9-12FC-CF7E-B415-ED924A3E87AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D9CF5BB9-12FC-CF7E-B415-ED924A3E87AE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D9CF5BB9-12FC-CF7E-B415-ED924A3E87AE}.Debug|arm64.ActiveCfg = Debug|Any CPU - {D9CF5BB9-12FC-CF7E-B415-ED924A3E87AE}.Debug|arm64.Build.0 = Debug|Any CPU - {D9CF5BB9-12FC-CF7E-B415-ED924A3E87AE}.Debug|x64.ActiveCfg = Debug|Any CPU - {D9CF5BB9-12FC-CF7E-B415-ED924A3E87AE}.Debug|x64.Build.0 = Debug|Any CPU - {D9CF5BB9-12FC-CF7E-B415-ED924A3E87AE}.Debug|x86.ActiveCfg = Debug|Any CPU - {D9CF5BB9-12FC-CF7E-B415-ED924A3E87AE}.Debug|x86.Build.0 = Debug|Any CPU - {D9CF5BB9-12FC-CF7E-B415-ED924A3E87AE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D9CF5BB9-12FC-CF7E-B415-ED924A3E87AE}.Release|Any CPU.Build.0 = Release|Any CPU - {D9CF5BB9-12FC-CF7E-B415-ED924A3E87AE}.Release|arm64.ActiveCfg = Release|Any CPU - {D9CF5BB9-12FC-CF7E-B415-ED924A3E87AE}.Release|arm64.Build.0 = Release|Any CPU - {D9CF5BB9-12FC-CF7E-B415-ED924A3E87AE}.Release|x64.ActiveCfg = Release|Any CPU - {D9CF5BB9-12FC-CF7E-B415-ED924A3E87AE}.Release|x64.Build.0 = Release|Any CPU - {D9CF5BB9-12FC-CF7E-B415-ED924A3E87AE}.Release|x86.ActiveCfg = Release|Any CPU - {D9CF5BB9-12FC-CF7E-B415-ED924A3E87AE}.Release|x86.Build.0 = Release|Any CPU - {8034024A-2804-4920-921A-86ADCCBF0838}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8034024A-2804-4920-921A-86ADCCBF0838}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8034024A-2804-4920-921A-86ADCCBF0838}.Debug|arm64.ActiveCfg = Debug|Any CPU - {8034024A-2804-4920-921A-86ADCCBF0838}.Debug|arm64.Build.0 = Debug|Any CPU - {8034024A-2804-4920-921A-86ADCCBF0838}.Debug|x64.ActiveCfg = Debug|Any CPU - {8034024A-2804-4920-921A-86ADCCBF0838}.Debug|x64.Build.0 = Debug|Any CPU - {8034024A-2804-4920-921A-86ADCCBF0838}.Debug|x86.ActiveCfg = Debug|Any CPU - {8034024A-2804-4920-921A-86ADCCBF0838}.Debug|x86.Build.0 = Debug|Any CPU - {8034024A-2804-4920-921A-86ADCCBF0838}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8034024A-2804-4920-921A-86ADCCBF0838}.Release|Any CPU.Build.0 = Release|Any CPU - {8034024A-2804-4920-921A-86ADCCBF0838}.Release|arm64.ActiveCfg = Release|Any CPU - {8034024A-2804-4920-921A-86ADCCBF0838}.Release|arm64.Build.0 = Release|Any CPU - {8034024A-2804-4920-921A-86ADCCBF0838}.Release|x64.ActiveCfg = Release|Any CPU - {8034024A-2804-4920-921A-86ADCCBF0838}.Release|x64.Build.0 = Release|Any CPU - {8034024A-2804-4920-921A-86ADCCBF0838}.Release|x86.ActiveCfg = Release|Any CPU - {8034024A-2804-4920-921A-86ADCCBF0838}.Release|x86.Build.0 = Release|Any CPU - {2A4B719C-4958-AD92-4A22-6472AAF98A37}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2A4B719C-4958-AD92-4A22-6472AAF98A37}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2A4B719C-4958-AD92-4A22-6472AAF98A37}.Debug|arm64.ActiveCfg = Debug|Any CPU - {2A4B719C-4958-AD92-4A22-6472AAF98A37}.Debug|arm64.Build.0 = Debug|Any CPU - {2A4B719C-4958-AD92-4A22-6472AAF98A37}.Debug|x64.ActiveCfg = Debug|Any CPU - {2A4B719C-4958-AD92-4A22-6472AAF98A37}.Debug|x64.Build.0 = Debug|Any CPU - {2A4B719C-4958-AD92-4A22-6472AAF98A37}.Debug|x86.ActiveCfg = Debug|Any CPU - {2A4B719C-4958-AD92-4A22-6472AAF98A37}.Debug|x86.Build.0 = Debug|Any CPU - {2A4B719C-4958-AD92-4A22-6472AAF98A37}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2A4B719C-4958-AD92-4A22-6472AAF98A37}.Release|Any CPU.Build.0 = Release|Any CPU - {2A4B719C-4958-AD92-4A22-6472AAF98A37}.Release|arm64.ActiveCfg = Release|Any CPU - {2A4B719C-4958-AD92-4A22-6472AAF98A37}.Release|arm64.Build.0 = Release|Any CPU - {2A4B719C-4958-AD92-4A22-6472AAF98A37}.Release|x64.ActiveCfg = Release|Any CPU - {2A4B719C-4958-AD92-4A22-6472AAF98A37}.Release|x64.Build.0 = Release|Any CPU - {2A4B719C-4958-AD92-4A22-6472AAF98A37}.Release|x86.ActiveCfg = Release|Any CPU - {2A4B719C-4958-AD92-4A22-6472AAF98A37}.Release|x86.Build.0 = Release|Any CPU - {B5B3101B-6638-418D-AE82-59984D9D6AC7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B5B3101B-6638-418D-AE82-59984D9D6AC7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B5B3101B-6638-418D-AE82-59984D9D6AC7}.Debug|arm64.ActiveCfg = Debug|Any CPU - {B5B3101B-6638-418D-AE82-59984D9D6AC7}.Debug|arm64.Build.0 = Debug|Any CPU - {B5B3101B-6638-418D-AE82-59984D9D6AC7}.Debug|x64.ActiveCfg = Debug|Any CPU - {B5B3101B-6638-418D-AE82-59984D9D6AC7}.Debug|x64.Build.0 = Debug|Any CPU - {B5B3101B-6638-418D-AE82-59984D9D6AC7}.Debug|x86.ActiveCfg = Debug|Any CPU - {B5B3101B-6638-418D-AE82-59984D9D6AC7}.Debug|x86.Build.0 = Debug|Any CPU - {B5B3101B-6638-418D-AE82-59984D9D6AC7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B5B3101B-6638-418D-AE82-59984D9D6AC7}.Release|Any CPU.Build.0 = Release|Any CPU - {B5B3101B-6638-418D-AE82-59984D9D6AC7}.Release|arm64.ActiveCfg = Release|Any CPU - {B5B3101B-6638-418D-AE82-59984D9D6AC7}.Release|arm64.Build.0 = Release|Any CPU - {B5B3101B-6638-418D-AE82-59984D9D6AC7}.Release|x64.ActiveCfg = Release|Any CPU - {B5B3101B-6638-418D-AE82-59984D9D6AC7}.Release|x64.Build.0 = Release|Any CPU - {B5B3101B-6638-418D-AE82-59984D9D6AC7}.Release|x86.ActiveCfg = Release|Any CPU - {B5B3101B-6638-418D-AE82-59984D9D6AC7}.Release|x86.Build.0 = Release|Any CPU - {C4338ACD-DFDC-1724-6F35-36519B285D1A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C4338ACD-DFDC-1724-6F35-36519B285D1A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C4338ACD-DFDC-1724-6F35-36519B285D1A}.Debug|arm64.ActiveCfg = Debug|Any CPU - {C4338ACD-DFDC-1724-6F35-36519B285D1A}.Debug|arm64.Build.0 = Debug|Any CPU - {C4338ACD-DFDC-1724-6F35-36519B285D1A}.Debug|x64.ActiveCfg = Debug|Any CPU - {C4338ACD-DFDC-1724-6F35-36519B285D1A}.Debug|x64.Build.0 = Debug|Any CPU - {C4338ACD-DFDC-1724-6F35-36519B285D1A}.Debug|x86.ActiveCfg = Debug|Any CPU - {C4338ACD-DFDC-1724-6F35-36519B285D1A}.Debug|x86.Build.0 = Debug|Any CPU - {C4338ACD-DFDC-1724-6F35-36519B285D1A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C4338ACD-DFDC-1724-6F35-36519B285D1A}.Release|Any CPU.Build.0 = Release|Any CPU - {C4338ACD-DFDC-1724-6F35-36519B285D1A}.Release|arm64.ActiveCfg = Release|Any CPU - {C4338ACD-DFDC-1724-6F35-36519B285D1A}.Release|arm64.Build.0 = Release|Any CPU - {C4338ACD-DFDC-1724-6F35-36519B285D1A}.Release|x64.ActiveCfg = Release|Any CPU - {C4338ACD-DFDC-1724-6F35-36519B285D1A}.Release|x64.Build.0 = Release|Any CPU - {C4338ACD-DFDC-1724-6F35-36519B285D1A}.Release|x86.ActiveCfg = Release|Any CPU - {C4338ACD-DFDC-1724-6F35-36519B285D1A}.Release|x86.Build.0 = Release|Any CPU - {928C9161-B7A5-41EA-991C-C4FB44605F97}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {928C9161-B7A5-41EA-991C-C4FB44605F97}.Debug|Any CPU.Build.0 = Debug|Any CPU - {928C9161-B7A5-41EA-991C-C4FB44605F97}.Debug|arm64.ActiveCfg = Debug|Any CPU - {928C9161-B7A5-41EA-991C-C4FB44605F97}.Debug|arm64.Build.0 = Debug|Any CPU - {928C9161-B7A5-41EA-991C-C4FB44605F97}.Debug|x64.ActiveCfg = Debug|Any CPU - {928C9161-B7A5-41EA-991C-C4FB44605F97}.Debug|x64.Build.0 = Debug|Any CPU - {928C9161-B7A5-41EA-991C-C4FB44605F97}.Debug|x86.ActiveCfg = Debug|Any CPU - {928C9161-B7A5-41EA-991C-C4FB44605F97}.Debug|x86.Build.0 = Debug|Any CPU - {928C9161-B7A5-41EA-991C-C4FB44605F97}.Release|Any CPU.ActiveCfg = Release|Any CPU - {928C9161-B7A5-41EA-991C-C4FB44605F97}.Release|Any CPU.Build.0 = Release|Any CPU - {928C9161-B7A5-41EA-991C-C4FB44605F97}.Release|arm64.ActiveCfg = Release|Any CPU - {928C9161-B7A5-41EA-991C-C4FB44605F97}.Release|arm64.Build.0 = Release|Any CPU - {928C9161-B7A5-41EA-991C-C4FB44605F97}.Release|x64.ActiveCfg = Release|Any CPU - {928C9161-B7A5-41EA-991C-C4FB44605F97}.Release|x64.Build.0 = Release|Any CPU - {928C9161-B7A5-41EA-991C-C4FB44605F97}.Release|x86.ActiveCfg = Release|Any CPU - {928C9161-B7A5-41EA-991C-C4FB44605F97}.Release|x86.Build.0 = Release|Any CPU - {EEF549E7-18F7-4FAC-AB6D-8FB335DA5AE3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EEF549E7-18F7-4FAC-AB6D-8FB335DA5AE3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EEF549E7-18F7-4FAC-AB6D-8FB335DA5AE3}.Debug|arm64.ActiveCfg = Debug|Any CPU - {EEF549E7-18F7-4FAC-AB6D-8FB335DA5AE3}.Debug|arm64.Build.0 = Debug|Any CPU - {EEF549E7-18F7-4FAC-AB6D-8FB335DA5AE3}.Debug|x64.ActiveCfg = Debug|Any CPU - {EEF549E7-18F7-4FAC-AB6D-8FB335DA5AE3}.Debug|x64.Build.0 = Debug|Any CPU - {EEF549E7-18F7-4FAC-AB6D-8FB335DA5AE3}.Debug|x86.ActiveCfg = Debug|Any CPU - {EEF549E7-18F7-4FAC-AB6D-8FB335DA5AE3}.Debug|x86.Build.0 = Debug|Any CPU - {EEF549E7-18F7-4FAC-AB6D-8FB335DA5AE3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EEF549E7-18F7-4FAC-AB6D-8FB335DA5AE3}.Release|Any CPU.Build.0 = Release|Any CPU - {EEF549E7-18F7-4FAC-AB6D-8FB335DA5AE3}.Release|arm64.ActiveCfg = Release|Any CPU - {EEF549E7-18F7-4FAC-AB6D-8FB335DA5AE3}.Release|arm64.Build.0 = Release|Any CPU - {EEF549E7-18F7-4FAC-AB6D-8FB335DA5AE3}.Release|x64.ActiveCfg = Release|Any CPU - {EEF549E7-18F7-4FAC-AB6D-8FB335DA5AE3}.Release|x64.Build.0 = Release|Any CPU - {EEF549E7-18F7-4FAC-AB6D-8FB335DA5AE3}.Release|x86.ActiveCfg = Release|Any CPU - {EEF549E7-18F7-4FAC-AB6D-8FB335DA5AE3}.Release|x86.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {2ADE0A50-5012-4341-8F4F-97597C2D6920} = {2AE709FA-BE58-4287-BFD3-E80BEB605125} - {7ED6D69F-138F-40BD-9F37-3E4050E4D19B} = {2AE709FA-BE58-4287-BFD3-E80BEB605125} - {CD8B19A9-316E-4FBC-8F0C-87ADC6AAD684} = {2AE709FA-BE58-4287-BFD3-E80BEB605125} - {D9CF5BB9-12FC-CF7E-B415-ED924A3E87AE} = {2AE709FA-BE58-4287-BFD3-E80BEB605125} - {8034024A-2804-4920-921A-86ADCCBF0838} = {2AE709FA-BE58-4287-BFD3-E80BEB605125} - {2A4B719C-4958-AD92-4A22-6472AAF98A37} = {2AE709FA-BE58-4287-BFD3-E80BEB605125} - {B5B3101B-6638-418D-AE82-59984D9D6AC7} = {3F9A8B1C-5D2E-4A7F-9B8C-1E6D4F5A7B9C} - {C4338ACD-DFDC-1724-6F35-36519B285D1A} = {2AE709FA-BE58-4287-BFD3-E80BEB605125} - {928C9161-B7A5-41EA-991C-C4FB44605F97} = {2AE709FA-BE58-4287-BFD3-E80BEB605125} - {EEF549E7-18F7-4FAC-AB6D-8FB335DA5AE3} = {2AE709FA-BE58-4287-BFD3-E80BEB605125} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {9326B58C-0AD3-4527-B3F4-86B54673C62E} - EndGlobalSection - GlobalSection(TestCaseManagementSettings) = postSolution - CategoryFile = ReactiveUI.vsmdi - EndGlobalSection -EndGlobal diff --git a/src/ReactiveUI.sln.DotSettings b/src/ReactiveUI.sln.DotSettings deleted file mode 100644 index 8699d2d9df..0000000000 --- a/src/ReactiveUI.sln.DotSettings +++ /dev/null @@ -1,8 +0,0 @@ - - True - C:\Users\Oren\Source\Git\ReactiveUI\RxUI.DotSettings - ..\RxUI.DotSettings - True - 1 - True - True \ No newline at end of file diff --git a/src/ReactiveUI.v3.ncrunchsolution b/src/ReactiveUI.v3.ncrunchsolution deleted file mode 100644 index 10420ac91d..0000000000 --- a/src/ReactiveUI.v3.ncrunchsolution +++ /dev/null @@ -1,6 +0,0 @@ - - - True - True - - \ No newline at end of file diff --git a/src/ReactiveUI/Activation/CanActivateViewFetcher.cs b/src/ReactiveUI/Activation/CanActivateViewFetcher.cs index 5973934d7c..7368dc0232 100644 --- a/src/ReactiveUI/Activation/CanActivateViewFetcher.cs +++ b/src/ReactiveUI/Activation/CanActivateViewFetcher.cs @@ -29,10 +29,6 @@ public int GetAffinityForView(Type view) => /// /// The view to observe. /// An observable tracking whether the view is active. -#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) => view is not ICanActivate canActivate ? Observable.Return(false) diff --git a/src/ReactiveUI/Activation/IActivationForViewFetcher.cs b/src/ReactiveUI/Activation/IActivationForViewFetcher.cs index 0a5d181561..8e100274c8 100644 --- a/src/ReactiveUI/Activation/IActivationForViewFetcher.cs +++ b/src/ReactiveUI/Activation/IActivationForViewFetcher.cs @@ -55,9 +55,5 @@ public interface IActivationForViewFetcher /// /// The view to get the activation observable for. /// A Observable which will returns if Activation was successful. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("GetActivationForView uses methods that require dynamic code generation")] - [RequiresUnreferencedCode("GetActivationForView uses methods that may require unreferenced code")] -#endif IObservable GetActivationForView(IActivatableView view); } diff --git a/src/ReactiveUI/Activation/ViewForMixins.cs b/src/ReactiveUI/Activation/ViewForMixins.cs index a387c814b2..618df457c5 100644 --- a/src/ReactiveUI/Activation/ViewForMixins.cs +++ b/src/ReactiveUI/Activation/ViewForMixins.cs @@ -22,9 +22,7 @@ public static class ViewForMixins var score = x?.GetAffinityForView(t) ?? 0; return score > acc.count ? (score, x) : acc; }).viewFetcher, - RxApp.SmallCacheLimit); - - static ViewForMixins() => RxApp.EnsureInitialized(); + RxCacheSize.SmallCacheLimit); /// /// WhenActivated allows you to register a Func to be called when a @@ -99,10 +97,7 @@ public static void WhenActivated(this IActivatableViewModel item, Action /// A Disposable that deactivates this registration. -#if NET6_0_OR_GREATER - [RequiresUnreferencedCode("WhenActivated may reference types that could be trimmed")] - [RequiresDynamicCode("WhenActivated uses reflection which requires dynamic code generation")] -#endif + [RequiresUnreferencedCode("Evaluates expression-based member chains via reflection; members may be trimmed.")] public static IDisposable WhenActivated(this IActivatableView item, Func> block) // TODO: Create Test { ArgumentExceptionHelper.ThrowIfNull(item); @@ -126,10 +121,7 @@ public static IDisposable WhenActivated(this IActivatableView item, Func /// A Disposable that deactivates this registration. -#if NET6_0_OR_GREATER - [RequiresUnreferencedCode("WhenActivated may reference types that could be trimmed")] - [RequiresDynamicCode("WhenActivated uses reflection which requires dynamic code generation")] -#endif + [RequiresUnreferencedCode("Evaluates expression-based member chains via reflection; members may be trimmed.")] public static IDisposable WhenActivated(this IActivatableView item, Func> block, IViewFor? view) // TODO: Create Test { ArgumentExceptionHelper.ThrowIfNull(item); @@ -165,10 +157,7 @@ public static IDisposable WhenActivated(this IActivatableView item, Func /// A Disposable that deactivates this registration. -#if NET6_0_OR_GREATER - [RequiresUnreferencedCode("WhenActivated may reference types that could be trimmed")] - [RequiresDynamicCode("WhenActivated uses reflection which requires dynamic code generation")] -#endif + [RequiresUnreferencedCode("Evaluates expression-based member chains via reflection; members may be trimmed.")] public static IDisposable WhenActivated(this IActivatableView item, Action> block) => item.WhenActivated(block, null!); @@ -189,10 +178,7 @@ public static IDisposable WhenActivated(this IActivatableView item, Action /// A Disposable that deactivates this registration. -#if NET6_0_OR_GREATER - [RequiresUnreferencedCode("WhenActivated may reference types that could be trimmed")] - [RequiresDynamicCode("WhenActivated uses reflection which requires dynamic code generation")] -#endif + [RequiresUnreferencedCode("Evaluates expression-based member chains via reflection; members may be trimmed.")] public static IDisposable WhenActivated(this IActivatableView item, Action> block, IViewFor view) => // TODO: Create Test item.WhenActivated( () => @@ -219,10 +205,7 @@ public static IDisposable WhenActivated(this IActivatableView item, Action /// A Disposable that deactivates this registration. -#if NET6_0_OR_GREATER - [RequiresUnreferencedCode("WhenActivated may reference types that could be trimmed")] - [RequiresDynamicCode("WhenActivated uses reflection which requires dynamic code generation")] -#endif + [RequiresUnreferencedCode("Evaluates expression-based member chains via reflection; members may be trimmed.")] public static IDisposable WhenActivated(this IActivatableView item, Action block, IViewFor? view = null) => item.WhenActivated( () => @@ -251,10 +234,7 @@ private static CompositeDisposable HandleViewActivation(Func activation) { var vmDisposable = new SerialDisposable(); diff --git a/src/ReactiveUI/Bindings/BindingTypeConverter.cs b/src/ReactiveUI/Bindings/BindingTypeConverter.cs new file mode 100644 index 0000000000..12035db53d --- /dev/null +++ b/src/ReactiveUI/Bindings/BindingTypeConverter.cs @@ -0,0 +1,71 @@ +// 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. + +namespace ReactiveUI; + +/// +/// Base class for type-pair binding converters. +/// +/// The source type to convert from. +/// The target type to convert to. +/// +/// This base class supplies the "type-only" metadata (/) and the +/// object-based shim (), allowing the dispatch +/// layer to avoid reflection. +/// +public abstract class BindingTypeConverter : IBindingTypeConverter +{ + /// + public Type FromType => typeof(TFrom); + + /// + public Type ToType => typeof(TTo); + + /// + /// + /// The default implementation returns a constant affinity for an exact type-pair converter. + /// Override if you need different selection semantics within a pair-based registry. + /// + public virtual int GetAffinityForObjects() => 10; + + /// + public abstract bool TryConvert(TFrom? from, object? conversionHint, [NotNullWhen(true)] out TTo? result); + + /// + public bool TryConvertTyped(object? from, object? conversionHint, out object? result) + { + // Enforce the modern nullability contract: + // - Successful conversion must yield a non-null result. + // - Null input (common in UI clearing scenarios) is treated as "not converted" here, + // so the binding pipeline can decide whether to flow null directly. + if (from is null) + { + result = null; + return false; + } + + if (from is not TFrom castFrom) + { + result = null; + return false; + } + + if (!TryConvert(castFrom, conversionHint, out var typedResult)) + { + result = null; + return false; + } + + // Defensive: even though the typed method is annotated, ensure we never report success with null. + if (typedResult is null) + { + result = null; + return false; + } + + result = typedResult; + return true; + } +} diff --git a/src/ReactiveUI/Bindings/BindingTypeConverterDispatch.cs b/src/ReactiveUI/Bindings/BindingTypeConverterDispatch.cs new file mode 100644 index 0000000000..ecb05ee668 --- /dev/null +++ b/src/ReactiveUI/Bindings/BindingTypeConverterDispatch.cs @@ -0,0 +1,145 @@ +// 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. + +namespace ReactiveUI; + +/// +/// Dispatches conversions using a type-only fast-path, avoiding reflection. +/// +internal static class BindingTypeConverterDispatch +{ + /// + /// Attempts conversion via the converter's type-only metadata ( and + /// ) and object shim (). + /// + /// The converter. + /// The source value. + /// The target type requested by the caller. + /// Implementation-defined hint. + /// The converted result. + /// if conversion succeeded; otherwise . + internal static bool TryConvert( + IBindingTypeConverter converter, + object? from, + Type toType, + object? conversionHint, + out object? result) + { + ArgumentExceptionHelper.ThrowIfNull(converter); + ArgumentExceptionHelper.ThrowIfNull(toType); + + // With the modern contract (success => non-null), null input is not considered a conversion here. + // The binding pipeline should decide whether to flow null through directly. + if (from is null) + { + result = null; + return false; + } + + // Exact pair match keeps dispatch predictable and avoids assignability ambiguity. + if (converter.ToType != toType || converter.FromType != from.GetType()) + { + result = null; + return false; + } + + return converter.TryConvertTyped(from, conversionHint, out result); + } + + /// + /// Attempts conversion using a fallback converter. + /// + /// The fallback converter. + /// The source runtime type. + /// The source value (guaranteed non-null by caller). + /// The target type. + /// Implementation-defined hint. + /// The converted result. + /// if conversion succeeded; otherwise, . + internal static bool TryConvertFallback( + IBindingFallbackConverter converter, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type fromType, + object from, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type toType, + object? conversionHint, + out object? result) + { + ArgumentExceptionHelper.ThrowIfNull(converter); + ArgumentExceptionHelper.ThrowIfNull(from); + ArgumentExceptionHelper.ThrowIfNull(toType); + + // Delegate to fallback converter (from is guaranteed non-null) + if (!converter.TryConvert(fromType, from, toType, conversionHint, out result)) + { + result = null; + return false; + } + + // Enforce modern contract: success implies non-null result + if (result is null) + { + result = null; + return false; + } + + return true; + } + + /// + /// Unified dispatch method that handles both typed and fallback converters. + /// + /// The converter (either or ). + /// The source runtime type. + /// The source value. + /// The target type. + /// Implementation-defined hint. + /// The converted result. + /// if conversion succeeded; otherwise, . + /// + /// This method automatically dispatches to the appropriate converter type: + /// + /// - uses exact pair matching + /// - requires non-null input + /// + /// + internal static bool TryConvertAny( + object? converter, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type fromType, + object? from, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type toType, + object? conversionHint, + out object? result) + { + ArgumentExceptionHelper.ThrowIfNull(toType); + + if (converter is null) + { + result = null; + return false; + } + + // Dispatch to typed converter + if (converter is IBindingTypeConverter typedConverter) + { + return TryConvert(typedConverter, from, toType, conversionHint, out result); + } + + // Dispatch to fallback converter (requires non-null input) + if (converter is IBindingFallbackConverter fallbackConverter) + { + if (from is null) + { + result = null; + return false; + } + + return TryConvertFallback(fallbackConverter, fromType, from, toType, conversionHint, out result); + } + + // Unknown converter type + result = null; + return false; + } +} diff --git a/src/ReactiveUI/Bindings/Command/CommandBinder.cs b/src/ReactiveUI/Bindings/Command/CommandBinder.cs index 51a34c7abf..8a5c3b72c2 100644 --- a/src/ReactiveUI/Bindings/Command/CommandBinder.cs +++ b/src/ReactiveUI/Bindings/Command/CommandBinder.cs @@ -16,7 +16,6 @@ public static class CommandBinder static CommandBinder() { - RxApp.EnsureInitialized(); _binderImplementation = AppLocator.Current.GetService() ?? new CommandBinderImplementation(); @@ -31,23 +30,25 @@ static CommandBinder() /// The property type. /// The control type. /// The parameter type. - /// A class representing the binding. Dispose it to disconnect - /// the binding. + /// + /// A class representing the binding. Dispose it to disconnect the binding. + /// /// The View. /// The View model. /// The ViewModel command to bind. /// The name of the control on the view. - /// The ViewModel property to pass as the - /// param of the ICommand. - /// If specified, bind to the specific event - /// instead of the default. - /// NOTE: If this parameter is used inside WhenActivated, it's - /// important to dispose the binding when the view is deactivated. -#if NET6_0_OR_GREATER - [RequiresUnreferencedCode("This method uses reflection to access properties by name.")] - [RequiresDynamicCode("This method uses reflection to access properties by name.")] -#endif - public static IReactiveBinding BindCommand( + /// The ViewModel property to pass as the param of the ICommand. + /// + /// If specified, bind to the specific event instead of the default. + /// NOTE: If this parameter is used inside WhenActivated, it's important to dispose the binding when the view is deactivated. + /// + [RequiresUnreferencedCode("Dynamic observation uses reflection over members that may be trimmed.")] + public static IReactiveBinding BindCommand< + TView, + TViewModel, + TProp, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicEvents | DynamicallyAccessedMemberTypes.NonPublicEvents | DynamicallyAccessedMemberTypes.PublicProperties)] TControl, + TParam>( this TView view, TViewModel? viewModel, Expression> propertyName, @@ -56,8 +57,16 @@ public static IReactiveBinding BindCommand - _binderImplementation.BindCommand(viewModel, view, propertyName, controlName, withParameter, toEvent); + where TProp : ICommand + where TControl : class + { + ArgumentExceptionHelper.ThrowIfNull(view); + ArgumentExceptionHelper.ThrowIfNull(propertyName); + ArgumentExceptionHelper.ThrowIfNull(controlName); + ArgumentExceptionHelper.ThrowIfNull(withParameter); + + return _binderImplementation.BindCommand(viewModel, view, propertyName, controlName, withParameter, toEvent); + } /// /// Bind a command from the ViewModel to an explicitly specified control @@ -67,21 +76,23 @@ public static IReactiveBinding BindCommandThe view model type. /// The property type. /// The control type. - /// A class representing the binding. Dispose it to disconnect - /// the binding. + /// + /// A class representing the binding. Dispose it to disconnect the binding. + /// /// The View. /// The View model. /// The ViewModel command to bind. /// The name of the control on the view. - /// If specified, bind to the specific event - /// instead of the default. - /// NOTE: If this parameter is used inside WhenActivated, it's - /// important to dispose the binding when the view is deactivated. -#if NET6_0_OR_GREATER - [RequiresUnreferencedCode("This method uses reflection to access properties by name.")] - [RequiresDynamicCode("This method uses reflection to access properties by name.")] -#endif - public static IReactiveBinding BindCommand( + /// + /// If specified, bind to the specific event instead of the default. + /// NOTE: If this parameter is used inside WhenActivated, it's important to dispose the binding when the view is deactivated. + /// + [RequiresUnreferencedCode("Dynamic observation uses reflection over members that may be trimmed.")] + public static IReactiveBinding BindCommand< + TView, + TViewModel, + TProp, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicEvents | DynamicallyAccessedMemberTypes.NonPublicEvents | DynamicallyAccessedMemberTypes.PublicProperties)] TControl>( this TView view, TViewModel? viewModel, Expression> propertyName, @@ -89,8 +100,15 @@ public static IReactiveBinding BindCommand - _binderImplementation.BindCommand(viewModel, view, propertyName, controlName, toEvent); + where TProp : ICommand + where TControl : class + { + ArgumentExceptionHelper.ThrowIfNull(view); + ArgumentExceptionHelper.ThrowIfNull(propertyName); + ArgumentExceptionHelper.ThrowIfNull(controlName); + + return _binderImplementation.BindCommand(viewModel, view, propertyName, controlName, toEvent); + } /// /// Bind a command from the ViewModel to an explicitly specified control @@ -101,41 +119,41 @@ public static IReactiveBinding BindCommandThe property type. /// The control type. /// The parameter type. - /// A class representing the binding. Dispose it to disconnect - /// the binding. + /// + /// A class representing the binding. Dispose it to disconnect the binding. + /// /// The View. /// The View model. /// The ViewModel command to bind. /// The name of the control on the view. - /// The ViewModel property to pass as the - /// param of the ICommand. - /// If specified, bind to the specific event - /// instead of the default. - /// NOTE: If this parameter is used inside WhenActivated, it's - /// important to dispose the binding when the view is deactivated. -#if NET6_0_OR_GREATER - [RequiresUnreferencedCode("This method uses reflection to access properties by name.")] - [RequiresDynamicCode("This method uses reflection to access properties by name.")] -#endif - public static IReactiveBinding BindCommand( - this TView view, - TViewModel? viewModel, - Expression> propertyName, - Expression> controlName, - Expression> withParameter, - string? toEvent = null) + /// The ViewModel property to pass as the param of the ICommand. + /// + /// If specified, bind to the specific event instead of the default. + /// NOTE: If this parameter is used inside WhenActivated, it's important to dispose the binding when the view is deactivated. + /// + [RequiresUnreferencedCode("Dynamic observation uses reflection over members that may be trimmed.")] + public static IReactiveBinding BindCommand< + TView, + TViewModel, + TProp, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicEvents | DynamicallyAccessedMemberTypes.NonPublicEvents | DynamicallyAccessedMemberTypes.PublicProperties)] TControl, + TParam>( + this TView view, + TViewModel? viewModel, + Expression> propertyName, + Expression> controlName, + Expression> withParameter, + string? toEvent = null) where TView : class, IViewFor where TViewModel : class where TProp : ICommand + where TControl : class { ArgumentExceptionHelper.ThrowIfNull(view); + ArgumentExceptionHelper.ThrowIfNull(propertyName); + ArgumentExceptionHelper.ThrowIfNull(controlName); + ArgumentExceptionHelper.ThrowIfNull(withParameter); - return _binderImplementation.BindCommand( - viewModel, - view, - propertyName, - controlName, - withParameter, - toEvent); + return _binderImplementation.BindCommand(viewModel, view, propertyName, controlName, withParameter, toEvent); } } diff --git a/src/ReactiveUI/Bindings/Command/CommandBinderImplementation.cs b/src/ReactiveUI/Bindings/Command/CommandBinderImplementation.cs index 3e4aa107fe..fd16f04847 100644 --- a/src/ReactiveUI/Bindings/Command/CommandBinderImplementation.cs +++ b/src/ReactiveUI/Bindings/Command/CommandBinderImplementation.cs @@ -8,168 +8,228 @@ namespace ReactiveUI; /// -/// Used by the CommandBinder extension methods to handle binding View controls and ViewModel commands. +/// Implements command binding for extension methods by wiring ViewModel +/// instances to view controls and keeping the binding up to date as the command +/// and/or control instance changes. /// +/// +/// +/// This implementation uses expression rewriting and dynamic observation (via WhenAny* infrastructure) +/// to locate and track members described by expression trees. +/// +/// +/// For trimming/AOT: the public binding entry points are annotated because they may require reflection over +/// members that are not statically visible to the trimmer, and may require dynamic code paths depending on +/// platform/runtime. +/// +/// public class CommandBinderImplementation : ICommandBinderImplementation { /// - /// Bind a command from the ViewModel to an explicitly specified control - /// on the View. + /// Binds a command from the ViewModel to an explicitly specified control on the view. /// /// The view type. /// The view model type. - /// The property type. + /// The property type of the command. /// The control type. /// The parameter type. - /// The View model. - /// The View. - /// The ViewModel command to bind. - /// The name of the control on the view. - /// The ViewModel property to pass as the - /// param of the ICommand. - /// If specified, bind to the specific event - /// instead of the default. - /// NOTE: If this parameter is used inside WhenActivated, it's - /// important to dispose the binding when the view is deactivated. - /// - /// A class representing the binding. Dispose it to disconnect - /// the binding. - /// - /// nameof(vmProperty) - /// or - /// nameof(vmProperty). -#if NET6_0_OR_GREATER - [RequiresUnreferencedCode("This method uses reflection to access properties by name.")] - [RequiresDynamicCode("This method uses reflection to access properties by name.")] -#endif - public IReactiveBinding BindCommand( - TViewModel? viewModel, - TView view, - Expression> vmProperty, - Expression> controlProperty, - Expression> withParameter, - string? toEvent = null) + /// The view model instance. + /// The view instance. + /// An expression selecting the ViewModel command to bind. + /// An expression selecting the control on the view. + /// An expression selecting the ViewModel property to pass as the command parameter. + /// + /// If specified, binds to the given event instead of the default command event. + /// If used inside WhenActivated, ensure the returned binding is disposed when the view deactivates. + /// + /// An representing the binding; dispose it to disconnect. + /// + /// Thrown when or is . + /// + [RequiresUnreferencedCode("Dynamic observation uses reflection over members that may be trimmed.")] + public IReactiveBinding BindCommand< + TView, + TViewModel, + TProp, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicEvents | DynamicallyAccessedMemberTypes.NonPublicEvents | DynamicallyAccessedMemberTypes.PublicProperties)] TControl, + TParam>( + TViewModel? viewModel, + TView view, + Expression> vmProperty, + Expression> controlProperty, + Expression> withParameter, + string? toEvent = null) where TView : class, IViewFor where TViewModel : class where TProp : ICommand + where TControl : class { ArgumentExceptionHelper.ThrowIfNull(vmProperty); - ArgumentExceptionHelper.ThrowIfNull(controlProperty); var vmExpression = Reflection.Rewrite(vmProperty.Body); var controlExpression = Reflection.Rewrite(controlProperty.Body); + var source = Reflection.ViewModelWhenAnyValue(viewModel, view, vmExpression).Cast(); - var bindingDisposable = BindCommandInternal(source, view, controlExpression, withParameter.ToObservable(viewModel), toEvent); + var bindingDisposable = BindCommandInternal( + source, + view, + controlExpression, + withParameter.ToObservable(viewModel), + toEvent); return new ReactiveBinding( - view, - controlExpression, - vmExpression, - source, - BindingDirection.OneWay, - bindingDisposable); + view, + controlExpression, + vmExpression, + source, + BindingDirection.OneWay, + bindingDisposable); } /// - /// Bind a command from the ViewModel to an explicitly specified control - /// on the View. + /// Binds a command from the ViewModel to an explicitly specified control on the view. /// /// The view type. /// The view model type. - /// The property type. + /// The property type of the command. /// The control type. /// The parameter type. - /// A class representing the binding. Dispose it to disconnect - /// the binding. - /// The View model. - /// The View. - /// The ViewModel command to bind. - /// The name of the control on the view. - /// The ViewModel property to pass as the - /// param of the ICommand. - /// If specified, bind to the specific event - /// instead of the default. - /// NOTE: If this parameter is used inside WhenActivated, it's - /// important to dispose the binding when the view is deactivated. -#if NET6_0_OR_GREATER - [RequiresUnreferencedCode("This method uses reflection to access properties by name.")] - [RequiresDynamicCode("This method uses reflection to access properties by name.")] -#endif - public IReactiveBinding BindCommand( - TViewModel? viewModel, - TView view, - Expression> vmProperty, - Expression> controlProperty, - IObservable withParameter, - string? toEvent = null) + /// The view model instance. + /// The view instance. + /// An expression selecting the ViewModel command to bind. + /// An expression selecting the control on the view. + /// An observable providing values to pass as the command parameter. + /// + /// If specified, binds to the given event instead of the default command event. + /// If used inside WhenActivated, ensure the returned binding is disposed when the view deactivates. + /// + /// An representing the binding; dispose it to disconnect. + /// + /// Thrown when or is . + /// + [RequiresUnreferencedCode("Dynamic observation uses reflection over members that may be trimmed.")] + public IReactiveBinding BindCommand< + TView, + TViewModel, + TProp, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicEvents | DynamicallyAccessedMemberTypes.NonPublicEvents | DynamicallyAccessedMemberTypes.PublicProperties)] TControl, + TParam>( + TViewModel? viewModel, + TView view, + Expression> vmProperty, + Expression> controlProperty, + IObservable withParameter, + string? toEvent = null) where TView : class, IViewFor where TViewModel : class where TProp : ICommand + where TControl : class { ArgumentExceptionHelper.ThrowIfNull(vmProperty); - ArgumentExceptionHelper.ThrowIfNull(controlProperty); var vmExpression = Reflection.Rewrite(vmProperty.Body); var controlExpression = Reflection.Rewrite(controlProperty.Body); + var source = Reflection.ViewModelWhenAnyValue(viewModel, view, vmExpression).Cast(); - var bindingDisposable = BindCommandInternal(source, view, controlExpression, withParameter, toEvent); + var bindingDisposable = BindCommandInternal( + source, + view, + controlExpression, + withParameter, + toEvent); return new ReactiveBinding( - view, - controlExpression, - vmExpression, - source, - BindingDirection.OneWay, - bindingDisposable); + view, + controlExpression, + vmExpression, + source, + BindingDirection.OneWay, + bindingDisposable); } -#if NET6_0_OR_GREATER - [RequiresUnreferencedCode("This method uses reflection to access properties by name.")] - [RequiresDynamicCode("This method uses reflection to access properties by name.")] -#endif - private static IDisposable BindCommandInternal( - IObservable source, - TView view, - Expression controlExpression, - IObservable withParameter, - string? toEvent) + /// + /// Wires the current command/control pair to an binding, and updates that wiring + /// whenever the command instance or control instance changes. + /// + /// The view type. + /// The command type. + /// The parameter type. + /// The control type. + /// Observable producing command instances. + /// The view instance used to observe the control expression chain. + /// The rewritten expression identifying the control on the view. + /// Observable producing command parameter values. + /// Optional event name override for the binding. + /// + /// A disposable that tears down the observation subscription and the most-recently-created command binding. + /// + [RequiresUnreferencedCode("Evaluates expression-based member chains via reflection; members may be trimmed.")] + private static IDisposable BindCommandInternal< + TView, + TProp, + TParam, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicEvents | DynamicallyAccessedMemberTypes.NonPublicEvents | DynamicallyAccessedMemberTypes.PublicProperties)] TControl>( + IObservable source, + TView view, + Expression controlExpression, + IObservable withParameter, + string? toEvent) where TView : class, IViewFor where TProp : ICommand + where TControl : class { - var disposable = Disposable.Empty; - - var bindInfo = source.CombineLatest( - view.SubscribeToExpressionChain(controlExpression, false, false, RxApp.SuppressViewCommandBindingMessage).Select(x => x.GetValue()), - (val, host) => new { val, host }); + // SerialDisposable safely replaces and disposes the previous binding when a new one is assigned. + var currentBinding = new SerialDisposable(); + + // Cache boxing of parameter values once to avoid rebuilding the Select pipeline on every rebind. + var boxedParameter = withParameter.Select(static p => (object?)p); + + // Observe the control expression chain and extract the current control instance. + var controlValues = + view.SubscribeToExpressionChain( + controlExpression, + beforeChange: false, + skipInitial: false, + suppressWarnings: false) + .Select(static x => x.GetValue()); + + // CombineLatest ensures rebinding occurs when either the command or control changes. + // ValueTuple avoids per-notification heap allocations. + var bindInfo = source.CombineLatest(controlValues, static (command, host) => (command, host)); + + var subscription = bindInfo.Subscribe(tuple => + { + var (command, host) = tuple; - var propSub = bindInfo - .Subscribe(x => + // Preserve existing behavior: if the control is currently null, + // do not tear down or recreate the existing binding. + if (host is null) { - if (x.host is null) - { - return; - } - - disposable.Dispose(); - if (x is null) - { - disposable = Disposable.Empty; - return; - } - - disposable = !string.IsNullOrEmpty(toEvent) ? - CreatesCommandBinding.BindCommandToObject(x.val, x.host, withParameter.Select(y => (object)y!), toEvent) : - CreatesCommandBinding.BindCommandToObject(x.val, x.host, withParameter.Select(y => (object)y!)); - }); - - return Disposable.Create(() => - { - propSub.Dispose(); - disposable.Dispose(); + return; + } + + // Match original semantics: allow null if the cast fails. + var control = host as TControl; + + // Assigning to SerialDisposable disposes the previous binding deterministically. + currentBinding.Disposable = + !string.IsNullOrEmpty(toEvent) + ? CreatesCommandBinding.BindCommandToObject( + command, + control, + boxedParameter, + toEvent!) + : CreatesCommandBinding.BindCommandToObject( + command, + control, + boxedParameter); }); + + // Dispose ordering: stop producing new bindings first, then dispose the active binding. + return new CompositeDisposable(subscription, currentBinding); } } diff --git a/src/ReactiveUI/Bindings/Command/CommandBinderImplementationMixins.cs b/src/ReactiveUI/Bindings/Command/CommandBinderImplementationMixins.cs index ccfefbcf42..dbd3b12e98 100644 --- a/src/ReactiveUI/Bindings/Command/CommandBinderImplementationMixins.cs +++ b/src/ReactiveUI/Bindings/Command/CommandBinderImplementationMixins.cs @@ -12,26 +12,59 @@ namespace ReactiveUI; /// internal static class CommandBinderImplementationMixins { -#if NET6_0_OR_GREATER - [RequiresUnreferencedCode("BindCommand may reference types that could be trimmed")] - [RequiresDynamicCode("BindCommand uses reflection which requires dynamic code generation")] -#endif - public static IReactiveBinding BindCommand( + /// + /// Binds a command on the ViewModel to a control on the View (no explicit event name). + /// + /// The view type. + /// The view model type. + /// The command property type. + /// The control type. + /// The command binder implementation. + /// The view model instance. + /// The view instance. + /// Expression selecting the command property on the view model. + /// Expression selecting the control on the view. + /// Optional event name on the control that will trigger the command. + /// A reactive binding representing the command binding. + /// Thrown when , , , or is . + /// + /// Trimming note: requires only public properties on the control type. + /// + [RequiresUnreferencedCode("Dynamic observation uses reflection over members that may be trimmed.")] + public static IReactiveBinding BindCommand( this ICommandBinderImplementation @this, TViewModel? viewModel, TView view, Expression> propertyName, Expression> controlName, string? toEvent = null) - where TView : class, IViewFor - where TViewModel : class - where TProp : ICommand => + where TView : class, IViewFor + where TViewModel : class + where TProp : ICommand + where TControl : class => @this.BindCommand(viewModel, view, propertyName, controlName, Observable.Empty, toEvent); -#if NET6_0_OR_GREATER - [RequiresUnreferencedCode("BindCommand may reference types that could be trimmed")] - [RequiresDynamicCode("BindCommand uses reflection which requires dynamic code generation")] -#endif + /// + /// Binds a command on the ViewModel to a control on the View (optional explicit event name). + /// + /// The view type. + /// The view model type. + /// The command property type. + /// The control type. + /// The command parameter type. + /// The command binder implementation. + /// The view model instance. + /// The view instance. + /// Expression selecting the command property on the view model. + /// Expression selecting the control on the view. + /// Expression selecting a command parameter value from the view model. + /// Optional event name on the control that will trigger the command. + /// A reactive binding representing the command binding. + /// Thrown when , , , or is . + /// + /// Trimming note: if is specified, implementations may reflect over public events. + /// + [RequiresUnreferencedCode("Dynamic observation uses reflection over members that may be trimmed.")] public static IReactiveBinding BindCommand( this ICommandBinderImplementation @this, TViewModel? viewModel, @@ -43,6 +76,7 @@ public static IReactiveBinding BindCommand +/// AOT-compatible command binding helper that uses generic type parameters instead of reflection. +/// internal static class CreatesCommandBinding { - private static readonly MemoizingMRUCache _bindCommandCache = - new( - (t, _) => AppLocator.Current.GetServices() - .Aggregate((score: 0, binding: (ICreatesCommandBinding?)null), (acc, x) => - { - var score = x.GetAffinityForObject(t, false); - return (score > acc.score) ? (score, x) : acc; - }).binding, - RxApp.SmallCacheLimit); - - private static readonly MemoizingMRUCache _bindCommandEventCache = - new( - (t, _) => AppLocator.Current.GetServices() - .Aggregate((score: 0, binding: (ICreatesCommandBinding?)null), (acc, x) => - { - var score = x.GetAffinityForObject(t, true); - return (score > acc.score) ? (score, x) : acc; - }).binding, - RxApp.SmallCacheLimit); + /// + /// Binds a command to a control using default event discovery. Fully AOT-compatible. + /// + [RequiresUnreferencedCode("String/reflection-based event binding may require members removed by trimming.")] + public static IDisposable BindCommandToObject<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicEvents | DynamicallyAccessedMemberTypes.NonPublicEvents | DynamicallyAccessedMemberTypes.PublicProperties)] TControl>(ICommand? command, TControl? target, IObservable commandParameter) + where TControl : class + { + var binder = GetBinder(hasEventTarget: false); + var ret = binder.BindCommandToObject(command, target, commandParameter) + ?? throw new Exception($"Couldn't bind Command Binder for {typeof(TControl).FullName}"); + return ret; + } -#if NET6_0_OR_GREATER - [RequiresDynamicCode("BindCommandToObject uses reflection and generic method instantiation")] - [RequiresUnreferencedCode("BindCommandToObject may reference members that could be trimmed")] -#endif - public static IDisposable BindCommandToObject(ICommand? command, object? target, IObservable commandParameter) + /// + /// Binds a command to a control using a specific event. Fully AOT-compatible. + /// + [RequiresUnreferencedCode("String/reflection-based event binding may require members removed by trimming.")] + public static IDisposable BindCommandToObject<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicEvents | DynamicallyAccessedMemberTypes.NonPublicEvents | DynamicallyAccessedMemberTypes.PublicProperties)] TControl, TEventArgs>( + ICommand? command, + TControl? target, + IObservable commandParameter, + string eventName) + where TControl : class { - var type = target!.GetType(); - var binder = _bindCommandCache.Get(type) ?? throw new Exception($"Couldn't find a Command Binder for {type.FullName}"); - var ret = binder.BindCommandToObject(command, target, commandParameter) ?? throw new Exception($"Couldn't bind Command Binder for {type.FullName}"); + var binder = GetBinder(hasEventTarget: true); + var ret = binder.BindCommandToObject(command, target, commandParameter, eventName) + ?? throw new Exception($"Couldn't bind Command Binder for {typeof(TControl).FullName} and event {eventName}"); return ret; } -#if NET6_0_OR_GREATER - [RequiresDynamicCode("BindCommandToObject uses reflection and generic method instantiation")] - [RequiresUnreferencedCode("BindCommandToObject may reference members that could be trimmed")] -#endif - public static IDisposable BindCommandToObject(ICommand? command, object? target, IObservable commandParameter, string? eventName) + private static ICreatesCommandBinding GetBinder<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicEvents | DynamicallyAccessedMemberTypes.NonPublicEvents | DynamicallyAccessedMemberTypes.PublicProperties)] T>(bool hasEventTarget) { - var type = target!.GetType(); - var binder = _bindCommandEventCache.Get(type) ?? throw new Exception($"Couldn't find an Event Binder for {type.FullName} and event {eventName}"); - var eventArgsType = Reflection.GetEventArgsTypeForEvent(type, eventName); - var mi = binder.GetType().GetTypeInfo().DeclaredMethods.First(static x => x.Name == "BindCommandToObject" && x.IsGenericMethod); - mi = mi.MakeGenericMethod(eventArgsType); + var binder = AppLocator.Current.GetServices() + .Aggregate((score: 0, binding: (ICreatesCommandBinding?)null), (acc, x) => + { + var score = x.GetAffinityForObject(hasEventTarget); + return (score > acc.score) ? (score, x) : acc; + }).binding; - var ret = (IDisposable)mi.Invoke(binder, [command, target, commandParameter, eventName])! ?? throw new Exception($"Couldn't bind Command Binder for {type.FullName} and event {eventName}"); - return ret; + return binder ?? throw new Exception($"Couldn't find a Command Binder for {typeof(T).FullName}"); } } diff --git a/src/ReactiveUI/Bindings/Command/CreatesCommandBindingViaCommandParameter.cs b/src/ReactiveUI/Bindings/Command/CreatesCommandBindingViaCommandParameter.cs index 61cd392b74..4182e470aa 100644 --- a/src/ReactiveUI/Bindings/Command/CreatesCommandBindingViaCommandParameter.cs +++ b/src/ReactiveUI/Bindings/Command/CreatesCommandBindingViaCommandParameter.cs @@ -4,110 +4,226 @@ // 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; - -/// -/// Class that registers Command Binding and Command Parameter Binding. -/// -public class CreatesCommandBindingViaCommandParameter : ICreatesCommandBinding +namespace ReactiveUI { - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("Property access requires dynamic code generation")] - [RequiresUnreferencedCode("Property access may reference members that could be trimmed")] -#endif - public int GetAffinityForObject(Type type, bool hasEventTarget) + /// + /// Creates command bindings for objects that expose Command and CommandParameter + /// as public instance properties. + /// + /// + /// + /// This binder targets command-source style controls (for example, WPF-style controls) where command execution + /// is driven by setting properties rather than subscribing to an event. + /// + /// + /// Trimming/AOT note: This type uses name-based reflection to locate public properties. Consumers running under + /// trimming must ensure the relevant public properties are preserved on the target control types. This + /// requirement is expressed via on the public generic entry points. + /// + /// + /// Performance note: This implementation uses a per-closed-generic static cache (“holder”) rather than a global MRU. + /// Steady-state access is lock-free and reduces lookup overhead to static field reads. + /// + /// + public sealed class CreatesCommandBindingViaCommandParameter : ICreatesCommandBinding { - if (hasEventTarget) + /// + /// The expected name of the command property. + /// + private const string CommandPropertyName = "Command"; + + /// + /// The expected name of the command parameter property. + /// + private const string CommandParameterPropertyName = "CommandParameter"; + + /// + /// + /// If an explicit event target exists, this binder is not applicable and returns 0. + /// Otherwise, it returns 5 if the target type exposes the required public instance properties; otherwise it returns 0. + /// + public int GetAffinityForObject< + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicEvents | DynamicallyAccessedMemberTypes.PublicProperties)] T>(bool hasEventTarget) { - return 0; + if (hasEventTarget) + { + return 0; + } + + return Holder.HasRequiredProperties ? 5 : 0; } - var propsToFind = new[] + /// + /// + /// + /// This implementation is intentionally “best effort.” If required properties cannot be resolved for + /// , it returns to preserve legacy behavior where binder + /// selection is expected to be affinity-driven rather than exception-driven. + /// + /// + /// Disposal ordering minimizes observable races: the parameter subscription is disposed before restoring + /// the original parameter value. + /// + /// + /// The command property is set after establishing the parameter subscription, preserving historical ordering + /// semantics (“set Command last”). + /// + /// + [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 commandParameter) + where T : class { - new { Name = "Command", TargetType = typeof(ICommand) }, - new { Name = "CommandParameter", TargetType = typeof(object) }, - }; + ArgumentExceptionHelper.ThrowIfNull(target); + ArgumentExceptionHelper.ThrowIfNull(commandParameter); - return propsToFind.All(x => - { - var pi = type.GetRuntimeProperty(x.Name); - return pi is not null; - }) ? 5 : 0; - } + var commandProperty = Holder.CommandProperty; + var commandParameterProperty = Holder.CommandParameterProperty; - /// -#if NET6_0_OR_GREATER - public int GetAffinityForObject<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicEvents | DynamicallyAccessedMemberTypes.PublicProperties)] T>( - bool hasEventTarget) -#else - public int GetAffinityForObject( - bool hasEventTarget) -#endif - { - if (hasEventTarget) - { - return 0; + // Preserve historical behavior: silently no-op if properties are missing. + if (commandProperty is null || commandParameterProperty is null) + { + return Disposable.Empty; + } + + // Snapshot original values once. Restoration occurs on disposal. + var originalCommand = commandProperty.GetValue(target); + var originalParameter = commandParameterProperty.GetValue(target); + + // Subscribe first so dispose can stop updates before restoration. + // This delegate intentionally remains simple and allocation-minimal (no LINQ). + var subscription = commandParameter.Subscribe( + value => commandParameterProperty.SetValue(target, value)); + + // Set command last to preserve ordering semantics. + commandProperty.SetValue(target, command); + + // Use Rx disposable to unbind and restore original values. AnonymousDisposable is idempotent and thread-safe. + return Disposable.Create(() => + { + // Stop parameter updates first. + subscription.Dispose(); + + // Restore original values in a predictable order. + commandParameterProperty.SetValue(target, originalParameter); + commandProperty.SetValue(target, originalCommand); + }); } - var propsToFind = new[] - { - new { Name = "Command", TargetType = typeof(ICommand) }, - new { Name = "CommandParameter", TargetType = typeof(object) }, - }; + /// + /// + /// This binder is for command-property based binding. If an event name is specified, event-based binders + /// should be used. This method therefore returns . + /// + [RequiresUnreferencedCode("String/reflection-based event binding may require members removed by trimming.")] + public IDisposable? BindCommandToObject( + ICommand? command, + T? target, + IObservable commandParameter, + string eventName) + where T : class => Disposable.Empty; + + /// + /// + /// This binder is for command-property based binding. If an event name is specified, event-based binders + /// should be used. This method therefore returns . + /// + public IDisposable? BindCommandToObject<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicEvents | DynamicallyAccessedMemberTypes.NonPublicEvents)] T, TEventArgs>(ICommand? command, T? target, IObservable commandParameter, Action> addHandler, Action> removeHandler) + where T : class + where TEventArgs : EventArgs => Disposable.Empty; - return propsToFind.All(static x => + /// + /// Per-closed-generic cache of resolved command properties for a target type . + /// + /// + /// The target type. Public properties must be preserved in trimmed applications. + /// + /// + /// + /// This pattern avoids a global type-keyed cache that performs reflection in a cache factory delegate, + /// which is a frequent source of trimming warnings and hard-to-annotate member requirements. + /// + /// + /// Static initialization is thread-safe by the CLR. After initialization, access is lock-free. + /// + /// + private static class Holder< + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T> { - var pi = typeof(T).GetRuntimeProperty(x.Name); - return pi is not null; - }) ? 5 : 0; - } + /// + /// Gets a value indicating whether the target type exposes both required properties. + /// + internal static readonly bool HasRequiredProperties; - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("BindCommandToObject uses methods that require dynamic code generation")] - [RequiresUnreferencedCode("BindCommandToObject uses methods that may require unreferenced code")] -#endif - public IDisposable? BindCommandToObject(ICommand? command, object? target, IObservable commandParameter) - { - ArgumentExceptionHelper.ThrowIfNull(target); + /// + /// Gets the resolved public instance property named Command, or if missing. + /// + internal static readonly PropertyInfo? CommandProperty; - var type = target!.GetType(); - var cmdPi = type.GetRuntimeProperty("Command"); - var cmdParamPi = type.GetRuntimeProperty("CommandParameter"); - var ret = new CompositeDisposable(); + /// + /// Gets the resolved public instance property named CommandParameter, or if missing. + /// + internal static readonly PropertyInfo? CommandParameterProperty; - var originalCmd = cmdPi?.GetValue(target, null); - var originalCmdParam = cmdParamPi?.GetValue(target, null); + /// + /// Initializes static members of the class. + /// + static Holder() + { + ResolveProperties(typeof(T), out CommandProperty, out CommandParameterProperty); + HasRequiredProperties = CommandProperty is not null && CommandParameterProperty is not null; + } - ret.Add(Disposable.Create(() => - { - cmdPi?.SetValue(target, originalCmd, null); - cmdParamPi?.SetValue(target, originalCmdParam, null); - })); + /// + /// Resolves required properties via a single pass over public instance properties. + /// + /// The target type to inspect. + /// Receives the resolved Command property, if present. + /// Receives the resolved CommandParameter property, if present. + /// + /// The method avoids LINQ and repeated reflection calls to reduce overhead. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void ResolveProperties( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type type, + out PropertyInfo? command, + out PropertyInfo? commandParameter) + { + command = null; + commandParameter = null; - ret.Add(commandParameter.Subscribe(x => cmdParamPi?.SetValue(target, x, null))); - cmdPi?.SetValue(target, command, null); + var properties = type.GetProperties(BindingFlags.Instance | BindingFlags.Public); - return ret; - } + for (var i = 0; i < properties.Length; i++) + { + var p = properties[i]; + var name = p.Name; - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("BindCommandToObject uses methods that require dynamic code generation")] - [RequiresUnreferencedCode("BindCommandToObject uses methods that may require unreferenced code")] -#endif - public IDisposable BindCommandToObject(ICommand? command, object? target, IObservable commandParameter, string eventName) -#if MONO - where TEventArgs : EventArgs -#endif - { - // NB: We should fall back to the generic Event-based handler if - // an event target is specified -#pragma warning disable IDE0022 // Use expression body for methods - return Disposable.Empty; -#pragma warning restore IDE0022 // Use expression body for methods + if (command is null && + string.Equals(name, CommandPropertyName, StringComparison.Ordinal)) + { + command = p; + continue; + } + + if (commandParameter is null && + string.Equals(name, CommandParameterPropertyName, StringComparison.Ordinal)) + { + commandParameter = p; + } + + if (command is not null && commandParameter is not null) + { + return; + } + } + } + } } } diff --git a/src/ReactiveUI/Bindings/Command/CreatesCommandBindingViaEvent.cs b/src/ReactiveUI/Bindings/Command/CreatesCommandBindingViaEvent.cs index c03bef4a7d..d587e45f58 100644 --- a/src/ReactiveUI/Bindings/Command/CreatesCommandBindingViaEvent.cs +++ b/src/ReactiveUI/Bindings/Command/CreatesCommandBindingViaEvent.cs @@ -4,17 +4,33 @@ // 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; /// -/// This binder is the default binder for connecting to arbitrary events. +/// Default command binder that connects an to an event on a target object. /// -public class CreatesCommandBindingViaEvent : ICreatesCommandBinding +/// +/// +/// This binder supports a small set of conventional "default" events (for example, Click), +/// 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 with the add/remove handler delegates to avoid the reflection cost. +/// +/// +public sealed class CreatesCommandBindingViaEvent : ICreatesCommandBinding { - // NB: These are in priority order - private static readonly List<(string name, Type type)> _defaultEventsToBind = + /// + /// Default events to attempt, in priority order. + /// + /// + /// The first event found on the target type is used. + /// + private static readonly (string Name, Type ArgsType)[] DefaultEventsToBind = [ ("Click", typeof(EventArgs)), ("TouchUpInside", typeof(EventArgs)), @@ -22,90 +38,268 @@ public class CreatesCommandBindingViaEvent : ICreatesCommandBinding ]; /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("Event binding requires dynamic code generation")] - [RequiresUnreferencedCode("Event binding may reference members that could be trimmed")] -#endif - public int GetAffinityForObject(Type type, bool hasEventTarget) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int GetAffinityForObject< + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicEvents | DynamicallyAccessedMemberTypes.PublicProperties)]T>(bool hasEventTarget) { if (hasEventTarget) { return 5; } - return _defaultEventsToBind.Any(x => - { - var ei = type.GetRuntimeEvent(x.name); - return ei is not null; - }) ? 3 : 0; + // Fast, allocation-free per-closed-generic cache. + return DefaultEventCache.HasDefaultEvent ? 3 : 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 target object using a generic type parameter. + /// + /// 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. + /// Thrown when is . + /// + /// Thrown when no default event exists on and the caller did not specify an event explicitly. + /// + [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 commandParameter) + where T : class { - if (hasEventTarget) + ArgumentExceptionHelper.ThrowIfNull(target); + + // Typical binding semantics: null command => no-op binding. + if (command is null) { - return 5; + return Disposable.Empty; } - return _defaultEventsToBind.Any(static x => + var eventName = DefaultEventCache.DefaultEventName; + if (eventName is null) { - var ei = typeof(T).GetRuntimeEvent(x.name); - return ei is not null; - }) ? 3 : 0; + throw new InvalidOperationException( + $"Couldn't find a default event to bind to on {typeof(T).FullName}, specify an event explicitly"); + } + + // Default events in this binder are EventArgs-shaped. + return BindCommandToObject(command, target, commandParameter, eventName); } - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("Event binding requires dynamic code generation and reflection")] - [RequiresUnreferencedCode("Event binding may reference members that could be trimmed")] -#endif - public IDisposable? BindCommandToObject(ICommand? command, object? target, IObservable commandParameter) + /// + /// Binds a command to a specific event on a target object using generic type parameters. + /// + /// The type of the target object. + /// The event arguments type. + /// The command to bind. If , no binding is created. + /// The target object. + /// An observable that supplies command parameter values. + /// The name of the event to bind to. + /// A disposable that unbinds the command. + /// + /// Thrown when or is . + /// + /// + /// Thrown when is empty. + /// + [RequiresUnreferencedCode("String/reflection-based event binding may require members removed by trimming.")] + public IDisposable? BindCommandToObject( + ICommand? command, + T? target, + IObservable commandParameter, + string eventName) + where T : class { ArgumentExceptionHelper.ThrowIfNull(target); + ArgumentExceptionHelper.ThrowIfNull(eventName); - var type = target!.GetType(); - var eventInfo = _defaultEventsToBind - .Select(x => new { EventInfo = type.GetRuntimeEvent(x.name), Args = x.type }) - .FirstOrDefault(x => x.EventInfo is not null) ?? throw new Exception( - $"Couldn't find a default event to bind to on {target.GetType().FullName}, specify an event explicitly"); - var mi = GetType().GetRuntimeMethods().First(x => x.Name == "BindCommandToObject" && x.IsGenericMethod); - mi = mi.MakeGenericMethod(eventInfo.Args); + if (eventName.Length == 0) + { + throw new ArgumentException("Event name must not be empty.", nameof(eventName)); + } + + if (command is null) + { + return Disposable.Empty; + } + + // Parameter value may be updated on a different thread than the event callback; + // ensure a consistent publication/read. + object? latestParameter = null; + + var ret = new CompositeDisposable(); - return (IDisposable?)mi.Invoke(this, [command, target, commandParameter, eventInfo.EventInfo?.Name]); + ret.Add(commandParameter.Subscribe(static x => + { + // Store under volatile semantics. + Volatile.Write(ref Unsafe.As(ref x), x); // no-op; keeps delegate static-friendly + })); + + // The above static trick is not useful because we still need to update latestParameter; keep a single closure, + // but use Volatile for correctness. + ret.Clear(); + + ret.Add(commandParameter.Subscribe(x => Volatile.Write(ref latestParameter, x))); + + var evt = Observable.FromEventPattern(target, eventName); + + ret.Add(evt.Subscribe(_ => + { + var param = Volatile.Read(ref latestParameter); + if (command.CanExecute(param)) + { + command.Execute(param); + } + })); + + return ret; } - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("Event binding requires dynamic code generation")] - [RequiresUnreferencedCode("Event binding may reference members that could be trimmed")] -#endif - public IDisposable BindCommandToObject(ICommand? command, object? target, IObservable commandParameter, string eventName) -#if MONO + /// + /// Binds a command to a specific event on a target object using explicit add/remove handler delegates. + /// + /// The type of the target object. + /// The event arguments type. + /// The command to bind. If , no binding is created. + /// The target object. + /// An observable that supplies command parameter values. + /// Adds the handler to the target event. + /// Removes the handler from the target event. + /// A disposable that unbinds the command. + /// + /// Thrown when , , or is . + /// + public IDisposable? BindCommandToObject<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicEvents | DynamicallyAccessedMemberTypes.NonPublicEvents)] T, TEventArgs>( + ICommand? command, + T? target, + IObservable commandParameter, + Action> addHandler, + Action> removeHandler) + where T : class where TEventArgs : EventArgs -#endif { - var ret = new CompositeDisposable(); + ArgumentExceptionHelper.ThrowIfNull(target); + ArgumentExceptionHelper.ThrowIfNull(addHandler); + ArgumentExceptionHelper.ThrowIfNull(removeHandler); + // Preserve typical binding semantics: null command => no-op binding. + if (command is null) + { + return Disposable.Empty; + } + + // latestParameter may be produced on a different thread than the UI event. object? latestParameter = null; - var evt = Observable.FromEventPattern(target!, eventName); - ret.Add(commandParameter.Subscribe(x => latestParameter = x)); + // Stable delegate for deterministic unsubscription. + void Handler(object? s, TEventArgs e) + { + var param = Volatile.Read(ref latestParameter); + if (command.CanExecute(param)) + { + command.Execute(param); + } + } - ret.Add(evt.Subscribe(_ => + // Subscribe to parameter changes first so the first event sees the latest parameter. + var parameterSub = commandParameter.Subscribe(x => Volatile.Write(ref latestParameter, x)); + + // Hook the event after parameter subscription; unhook deterministically on dispose. + addHandler(Handler); + + return new CompositeDisposable( + parameterSub, + Disposable.Create(() => removeHandler(Handler))); + } + + /// + /// Binds a command to an event using explicit add/remove handler actions (non-reflection). + /// + /// The target type. + /// The command to bind. If , no binding is created. + /// The target object. + /// An observable that supplies command parameter values. + /// Adds the handler to the target event. + /// Removes the handler from the target event. + /// A disposable that unbinds the command. + /// + /// Thrown when , , or is . + /// + public IDisposable BindCommandToObject( + ICommand? command, + T? target, + IObservable commandParameter, + Action addHandler, + Action removeHandler) + where T : class + { + ArgumentExceptionHelper.ThrowIfNull(target); + ArgumentExceptionHelper.ThrowIfNull(addHandler); + ArgumentExceptionHelper.ThrowIfNull(removeHandler); + + if (command is null) + { + return Disposable.Empty; + } + + object? latestParameter = null; + + // Stable delegate for deterministic unsubscription. + void Handler(object? s, EventArgs e) { - if (command!.CanExecute(latestParameter)) + var param = Volatile.Read(ref latestParameter); + if (command.CanExecute(param)) { - command.Execute(latestParameter); + command.Execute(param); } - })); + } + var ret = new CompositeDisposable + { + commandParameter.Subscribe(x => Volatile.Write(ref latestParameter, x)), + Disposable.Create(() => removeHandler(Handler)) + }; + + addHandler(Handler); return ret; } + + /// + /// Per-closed-generic cache for default event resolution. + /// + /// The target type. + private static class DefaultEventCache< + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicEvents)]T> + { + /// + /// Gets the selected default event name for , or if none exists. + /// + public static readonly string? DefaultEventName = FindDefaultEventName(); + + /// + /// Gets a value indicating whether has any default event supported by this binder. + /// + public static readonly bool HasDefaultEvent = DefaultEventName is not null; + + private static string? FindDefaultEventName() + { + var type = typeof(T); + + // Avoid LINQ allocations; scan in priority order. + for (var i = 0; i < DefaultEventsToBind.Length; i++) + { + var name = DefaultEventsToBind[i].Name; + if (type.GetRuntimeEvent(name) is not null) + { + return name; + } + } + + return null; + } + } } diff --git a/src/ReactiveUI/Bindings/Command/ICommandBinderImplementation.cs b/src/ReactiveUI/Bindings/Command/ICommandBinderImplementation.cs index 232735e000..813da4a4a7 100644 --- a/src/ReactiveUI/Bindings/Command/ICommandBinderImplementation.cs +++ b/src/ReactiveUI/Bindings/Command/ICommandBinderImplementation.cs @@ -27,20 +27,18 @@ internal interface ICommandBinderImplementation : IEnableLogger /// The type of control on the view. /// The type of the parameter to pass to the ICommand. /// A reactive binding. Often only used for disposing the binding. -#if NET6_0_OR_GREATER - [RequiresUnreferencedCode("This method uses reflection to access properties by name.")] - [RequiresDynamicCode("This method uses reflection to access properties by name.")] -#endif - IReactiveBinding BindCommand( + [RequiresUnreferencedCode("Dynamic observation uses reflection over members that may be trimmed.")] + IReactiveBinding BindCommand( TViewModel? viewModel, TView view, Expression> vmProperty, Expression> controlProperty, Expression> withParameter, string? toEvent = null) - where TView : class, IViewFor - where TViewModel : class - where TProp : ICommand; + where TView : class, IViewFor + where TViewModel : class + where TProp : ICommand + where TControl : class; /// /// Binds the command on a ViewModel to a control on the View. @@ -57,18 +55,16 @@ IReactiveBinding BindCommandThe type of control on the view. /// The type of the parameter to pass to the ICommand. /// A reactive binding. Often only used for disposing the binding. -#if NET6_0_OR_GREATER - [RequiresUnreferencedCode("This method uses reflection to access properties by name.")] - [RequiresDynamicCode("This method uses reflection to access properties by name.")] -#endif - IReactiveBinding BindCommand( + [RequiresUnreferencedCode("Dynamic observation uses reflection over members that may be trimmed.")] + IReactiveBinding BindCommand( TViewModel? viewModel, TView view, Expression> vmProperty, Expression> controlProperty, IObservable withParameter, string? toEvent = null) - where TView : class, IViewFor - where TViewModel : class - where TProp : ICommand; + where TView : class, IViewFor + where TViewModel : class + where TProp : ICommand + where TControl : class; } diff --git a/src/ReactiveUI/Bindings/Converter/BooleanToStringTypeConverter.cs b/src/ReactiveUI/Bindings/Converter/BooleanToStringTypeConverter.cs new file mode 100644 index 0000000000..67516aebb0 --- /dev/null +++ b/src/ReactiveUI/Bindings/Converter/BooleanToStringTypeConverter.cs @@ -0,0 +1,24 @@ +// 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; + +/// +/// Converts to . +/// +public sealed class BooleanToStringTypeConverter : BindingTypeConverter +{ + /// + public override int GetAffinityForObjects() => 10; + + /// + public override bool TryConvert(bool from, object? conversionHint, [NotNullWhen(true)] out string? result) + { + result = from.ToString(); + return true; + } +} diff --git a/src/ReactiveUI/Bindings/Converter/ByteToStringTypeConverter.cs b/src/ReactiveUI/Bindings/Converter/ByteToStringTypeConverter.cs index e6b4495f3e..fcf1d14ae2 100644 --- a/src/ReactiveUI/Bindings/Converter/ByteToStringTypeConverter.cs +++ b/src/ReactiveUI/Bindings/Converter/ByteToStringTypeConverter.cs @@ -1,59 +1,24 @@ -// Copyright (c) 2025 .NET Foundation and Contributors. All rights reserved. +// 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; /// -/// Short To String Type Converter. +/// Converts values to . /// -/// -public class ByteToStringTypeConverter : IBindingTypeConverter +public sealed class ByteToStringTypeConverter : BindingTypeConverter { /// - public int GetAffinityForObjects(Type fromType, Type toType) - { - if (fromType == typeof(byte) && toType == typeof(string)) - { - return 10; - } - - if (fromType == typeof(string) && toType == typeof(byte)) - { - 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(byte from, object? conversionHint, [NotNullWhen(true)] out string? result) { - if (toType == typeof(string) && from is byte fromByte) - { - if (conversionHint is int byteHint) - { - result = fromByte.ToString($"D{byteHint}"); - return true; - } - - result = fromByte.ToString(); - return true; - } - - if (from is string fromString) - { - var success = byte.TryParse(fromString, out var outByte); - if (success) - { - result = outByte; - - return true; - } - } - - result = null!; - return false; + result = from.ToString(); + return true; } } diff --git a/src/ReactiveUI/Bindings/Converter/DateOnlyToStringTypeConverter.cs b/src/ReactiveUI/Bindings/Converter/DateOnlyToStringTypeConverter.cs new file mode 100644 index 0000000000..705cf4e0a3 --- /dev/null +++ b/src/ReactiveUI/Bindings/Converter/DateOnlyToStringTypeConverter.cs @@ -0,0 +1,26 @@ +// 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 NET6_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; + +namespace ReactiveUI; + +/// +/// Converts to . +/// +public sealed class DateOnlyToStringTypeConverter : BindingTypeConverter +{ + /// + public override int GetAffinityForObjects() => 10; + + /// + public override bool TryConvert(DateOnly from, object? conversionHint, [NotNullWhen(true)] out string? result) + { + result = from.ToString(); + return true; + } +} +#endif diff --git a/src/ReactiveUI/Bindings/Converter/DateTimeOffsetToStringTypeConverter.cs b/src/ReactiveUI/Bindings/Converter/DateTimeOffsetToStringTypeConverter.cs new file mode 100644 index 0000000000..372fb9cd1f --- /dev/null +++ b/src/ReactiveUI/Bindings/Converter/DateTimeOffsetToStringTypeConverter.cs @@ -0,0 +1,24 @@ +// 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; + +/// +/// Converts to . +/// +public sealed class DateTimeOffsetToStringTypeConverter : BindingTypeConverter +{ + /// + public override int GetAffinityForObjects() => 10; + + /// + public override bool TryConvert(DateTimeOffset from, object? conversionHint, [NotNullWhen(true)] out string? result) + { + result = from.ToString(); + return true; + } +} diff --git a/src/ReactiveUI/Bindings/Converter/DateTimeToStringTypeConverter.cs b/src/ReactiveUI/Bindings/Converter/DateTimeToStringTypeConverter.cs new file mode 100644 index 0000000000..90df99affc --- /dev/null +++ b/src/ReactiveUI/Bindings/Converter/DateTimeToStringTypeConverter.cs @@ -0,0 +1,24 @@ +// 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; + +/// +/// Converts to . +/// +public sealed class DateTimeToStringTypeConverter : BindingTypeConverter +{ + /// + public override int GetAffinityForObjects() => 10; + + /// + public override bool TryConvert(DateTime from, object? conversionHint, [NotNullWhen(true)] out string? result) + { + result = from.ToString(); + return true; + } +} diff --git a/src/ReactiveUI/Bindings/Converter/DecimalToStringTypeConverter.cs b/src/ReactiveUI/Bindings/Converter/DecimalToStringTypeConverter.cs index 06d050fa01..90e704132e 100644 --- a/src/ReactiveUI/Bindings/Converter/DecimalToStringTypeConverter.cs +++ b/src/ReactiveUI/Bindings/Converter/DecimalToStringTypeConverter.cs @@ -1,65 +1,24 @@ -// Copyright (c) 2025 .NET Foundation and Contributors. All rights reserved. +// 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; /// -/// Decimal To String Type Converter. +/// Converts values to . /// -/// -public class DecimalToStringTypeConverter : IBindingTypeConverter +public sealed class DecimalToStringTypeConverter : BindingTypeConverter { /// - public int GetAffinityForObjects(Type fromType, Type toType) - { - if (fromType == typeof(decimal) && toType == typeof(string)) - { - return 10; - } - - if (fromType == typeof(string) && toType == typeof(decimal)) - { - 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(decimal from, object? conversionHint, [NotNullWhen(true)] out string? result) { - if (toType == typeof(string) && from is decimal fromDecimal) - { - if (conversionHint is int decimalHint) - { - result = fromDecimal.ToString($"F{decimalHint}"); - return true; - } - - result = fromDecimal.ToString(); - return true; - } - - if (from is string fromString) - { - var success = decimal.TryParse(fromString, out var outDecimal); - if (success) - { - if (conversionHint is int decimalHint) - { - result = Math.Round(outDecimal, decimalHint); - return true; - } - - result = outDecimal; - - return true; - } - } - - result = null!; - return false; + result = from.ToString(); + return true; } } diff --git a/src/ReactiveUI/Bindings/Converter/DoubleToStringTypeConverter.cs b/src/ReactiveUI/Bindings/Converter/DoubleToStringTypeConverter.cs index 47e6fe8ba3..345d79c9b0 100644 --- a/src/ReactiveUI/Bindings/Converter/DoubleToStringTypeConverter.cs +++ b/src/ReactiveUI/Bindings/Converter/DoubleToStringTypeConverter.cs @@ -1,65 +1,24 @@ -// Copyright (c) 2025 .NET Foundation and Contributors. All rights reserved. +// 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; /// -/// Double To String Type Converter. +/// Converts values to . /// -/// -public class DoubleToStringTypeConverter : IBindingTypeConverter +public sealed class DoubleToStringTypeConverter : BindingTypeConverter { /// - public int GetAffinityForObjects(Type fromType, Type toType) - { - if (fromType == typeof(double) && toType == typeof(string)) - { - return 10; - } - - if (fromType == typeof(string) && toType == typeof(double)) - { - 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(double from, object? conversionHint, [NotNullWhen(true)] out string? result) { - if (toType == typeof(string) && from is double fromDouble) - { - if (conversionHint is int doubleHint) - { - result = fromDouble.ToString($"F{doubleHint}"); - return true; - } - - result = fromDouble.ToString(); - return true; - } - - if (from is string fromString) - { - var success = double.TryParse(fromString, out var outDouble); - if (success) - { - if (conversionHint is int doubleHint) - { - result = Math.Round(outDouble, doubleHint); - return true; - } - - result = outDouble; - - return true; - } - } - - result = null!; - return false; + result = from.ToString(); + return true; } } diff --git a/src/ReactiveUI/Bindings/Converter/EqualityTypeConverter.cs b/src/ReactiveUI/Bindings/Converter/EqualityTypeConverter.cs index 6278fc8e07..8613db5ea9 100644 --- a/src/ReactiveUI/Bindings/Converter/EqualityTypeConverter.cs +++ b/src/ReactiveUI/Bindings/Converter/EqualityTypeConverter.cs @@ -1,124 +1,41 @@ -// Copyright (c) 2025 .NET Foundation and Contributors. All rights reserved. +// 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.Reflection; +using System.Diagnostics.CodeAnalysis; namespace ReactiveUI; /// -/// The default converter, simply converts between types that are equal or -/// can be converted (i.e. Button => UIControl). +/// Converts any value to by comparing it with a hint value using . /// -public class EqualityTypeConverter : IBindingTypeConverter +/// +/// +/// This converter is useful for binding scenarios where you need to determine if a value +/// equals a specific comparison value. The comparison value should be provided via the +/// conversionHint parameter. +/// +/// +/// Example: Convert an enum value to bool by comparing with a specific enum member. +/// +/// +public sealed class EqualityTypeConverter : IBindingTypeConverter { - private static readonly MemoizingMRUCache _referenceCastCache = new( - static (_, _) => _methodInfo ??= typeof(EqualityTypeConverter).GetRuntimeMethods().First(static x => x.Name == nameof(DoReferenceCast)), RxApp.SmallCacheLimit); - - private static MethodInfo? _methodInfo; - - /// - /// Handles casting for a reference. Understands about nullable types - /// and can cast appropriately. - /// - /// The object we are casting from. - /// The target we want to cast to. - /// The new value after it has been casted. - /// If we cannot cast the object. - public static object? DoReferenceCast(object? from, Type targetType) - { - ArgumentExceptionHelper.ThrowIfNull(targetType); - var backingNullableType = Nullable.GetUnderlyingType(targetType); - - if (backingNullableType is null) - { - if (from is null) - { - if (targetType.GetTypeInfo().IsValueType) - { - throw new InvalidCastException("Can't convert from nullable-type which is null to non-nullable type"); - } - - return null; - } - - if (IsInstanceOfType(from, targetType)) - { - return from; - } - - throw new InvalidCastException(); - } - - if (from is null) - { - return null; - } - - var converted = Convert.ChangeType(from, backingNullableType, null); - if (!IsInstanceOfType(converted, targetType)) - { - throw new InvalidCastException(); - } - - return converted; - } - /// - public int GetAffinityForObjects(Type fromType, Type toType) - { - if (toType.GetTypeInfo().IsAssignableFrom(fromType.GetTypeInfo())) - { - return 100; - } - - // NB: WPF is terrible. - if (fromType == typeof(object)) - { - return 100; - } - - var realType = Nullable.GetUnderlyingType(fromType); - if (realType is not null) - { - return GetAffinityForObjects(realType, toType); - } + public Type FromType => typeof(object); - realType = Nullable.GetUnderlyingType(toType); - if (realType is not null) - { - return GetAffinityForObjects(fromType, realType); - } + /// + public Type ToType => typeof(bool); - return 0; - } + /// + public int GetAffinityForObjects() => 5; /// - public bool TryConvert(object? from, Type toType, object? conversionHint, out object? result) + public bool TryConvertTyped(object? from, object? conversionHint, [NotNullWhen(true)] out object? result) { - ArgumentExceptionHelper.ThrowIfNull(toType); - - var mi = _referenceCastCache.Get(toType); - - try - { - result = mi.Invoke(null, [from, toType]); - } - catch (Exception ex) - { - this.Log().Warn(ex, "Couldn't convert object to type: " + toType); - result = null; - return false; - } - + // Always return a bool result + result = Equals(from, conversionHint); return true; } - - private static bool IsInstanceOfType(object from, Type targetType) - { - ArgumentExceptionHelper.ThrowIfNull(from); - ArgumentExceptionHelper.ThrowIfNull(targetType); - return targetType.IsInstanceOfType(from); - } } diff --git a/src/ReactiveUI/Bindings/Converter/GuidToStringTypeConverter.cs b/src/ReactiveUI/Bindings/Converter/GuidToStringTypeConverter.cs new file mode 100644 index 0000000000..a1b9c51b0f --- /dev/null +++ b/src/ReactiveUI/Bindings/Converter/GuidToStringTypeConverter.cs @@ -0,0 +1,24 @@ +// 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; + +/// +/// Converts to using the "D" format (standard hyphenated format). +/// +public sealed class GuidToStringTypeConverter : BindingTypeConverter +{ + /// + public override int GetAffinityForObjects() => 10; + + /// + public override bool TryConvert(Guid from, object? conversionHint, [NotNullWhen(true)] out string? result) + { + result = from.ToString("D"); + return true; + } +} diff --git a/src/ReactiveUI/Bindings/Converter/IntegerToStringTypeConverter.cs b/src/ReactiveUI/Bindings/Converter/IntegerToStringTypeConverter.cs index b2267e6b74..95c7b795c7 100644 --- a/src/ReactiveUI/Bindings/Converter/IntegerToStringTypeConverter.cs +++ b/src/ReactiveUI/Bindings/Converter/IntegerToStringTypeConverter.cs @@ -1,59 +1,24 @@ -// Copyright (c) 2025 .NET Foundation and Contributors. All rights reserved. +// 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; /// -/// Integer To String Type Converter. +/// Converts values to . /// -/// -public class IntegerToStringTypeConverter : IBindingTypeConverter +public sealed class IntegerToStringTypeConverter : BindingTypeConverter { /// - public int GetAffinityForObjects(Type fromType, Type toType) - { - if (fromType == typeof(int) && toType == typeof(string)) - { - return 10; - } - - if (fromType == typeof(string) && toType == typeof(int)) - { - 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(int from, object? conversionHint, [NotNullWhen(true)] out string? result) { - if (toType == typeof(string) && from is int fromInt) - { - if (conversionHint is int intHint) - { - result = fromInt.ToString($"D{intHint}"); - return true; - } - - result = fromInt.ToString(); - return true; - } - - if (from is string fromString) - { - var success = int.TryParse(fromString, out var outInt); - if (success) - { - result = outInt; - - return true; - } - } - - result = null!; - return false; + result = from.ToString(); + return true; } } diff --git a/src/ReactiveUI/Bindings/Converter/LongToStringTypeConverter.cs b/src/ReactiveUI/Bindings/Converter/LongToStringTypeConverter.cs index 2509908a76..a7c791e93a 100644 --- a/src/ReactiveUI/Bindings/Converter/LongToStringTypeConverter.cs +++ b/src/ReactiveUI/Bindings/Converter/LongToStringTypeConverter.cs @@ -1,59 +1,24 @@ -// Copyright (c) 2025 .NET Foundation and Contributors. All rights reserved. +// 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; /// -/// Integer To String Type Converter. +/// Converts values to . /// -/// -public class LongToStringTypeConverter : IBindingTypeConverter +public sealed class LongToStringTypeConverter : BindingTypeConverter { /// - public int GetAffinityForObjects(Type fromType, Type toType) - { - if (fromType == typeof(long) && toType == typeof(string)) - { - return 10; - } - - if (fromType == typeof(string) && toType == typeof(long)) - { - 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(long from, object? conversionHint, [NotNullWhen(true)] out string? result) { - if (toType == typeof(string) && from is long fromLong) - { - if (conversionHint is int longHint) - { - result = fromLong.ToString($"D{longHint}"); - return true; - } - - result = fromLong.ToString(); - return true; - } - - if (from is string fromString) - { - var success = long.TryParse(fromString, out var outLong); - if (success) - { - result = outLong; - - return true; - } - } - - result = null!; - return false; + result = from.ToString(); + return true; } } diff --git a/src/ReactiveUI/Bindings/Converter/NullableBooleanToStringTypeConverter.cs b/src/ReactiveUI/Bindings/Converter/NullableBooleanToStringTypeConverter.cs new file mode 100644 index 0000000000..7ad561b792 --- /dev/null +++ b/src/ReactiveUI/Bindings/Converter/NullableBooleanToStringTypeConverter.cs @@ -0,0 +1,30 @@ +// 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; + +/// +/// Converts nullable to . +/// +public sealed class NullableBooleanToStringTypeConverter : BindingTypeConverter +{ + /// + public override int GetAffinityForObjects() => 10; + + /// + public override bool TryConvert(bool? from, object? conversionHint, [NotNullWhen(true)] out string? result) + { + if (!from.HasValue) + { + result = null; + return false; + } + + result = from.Value.ToString(); + return true; + } +} diff --git a/src/ReactiveUI/Bindings/Converter/NullableByteToStringTypeConverter.cs b/src/ReactiveUI/Bindings/Converter/NullableByteToStringTypeConverter.cs index 6354066309..ba9165e2cc 100644 --- a/src/ReactiveUI/Bindings/Converter/NullableByteToStringTypeConverter.cs +++ b/src/ReactiveUI/Bindings/Converter/NullableByteToStringTypeConverter.cs @@ -1,71 +1,30 @@ -// Copyright (c) 2025 .NET Foundation and Contributors. All rights reserved. +// 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; /// -/// Short To String Type Converter. +/// Converts nullable values to . /// -/// -public class NullableByteToStringTypeConverter : IBindingTypeConverter +public sealed class NullableByteToStringTypeConverter : BindingTypeConverter { /// - public int GetAffinityForObjects(Type fromType, Type toType) - { - if (fromType == typeof(byte?) && toType == typeof(string)) - { - return 10; - } - - if (fromType == typeof(string) && toType == typeof(byte?)) - { - 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(byte? from, object? conversionHint, [NotNullWhen(true)] out string? result) { - if (toType == typeof(string) && from is byte fromByte) + if (!from.HasValue) { - if (conversionHint is int byteHint) - { - result = fromByte.ToString($"D{byteHint}"); - return true; - } - - result = fromByte.ToString(); - return true; - } - - if (from is null) - { - result = null!; - return true; - } - - if (from is string fromString) - { - if (string.IsNullOrEmpty(fromString)) - { - result = null!; - return true; - } - - var success = byte.TryParse(fromString, out var outByte); - if (success) - { - result = outByte; - - return true; - } + result = null; + return false; } - result = null!; - return false; + result = from.Value.ToString(); + return true; } } diff --git a/src/ReactiveUI/Bindings/Converter/NullableDateOnlyToStringTypeConverter.cs b/src/ReactiveUI/Bindings/Converter/NullableDateOnlyToStringTypeConverter.cs new file mode 100644 index 0000000000..720a1099ba --- /dev/null +++ b/src/ReactiveUI/Bindings/Converter/NullableDateOnlyToStringTypeConverter.cs @@ -0,0 +1,32 @@ +// 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 NET6_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; + +namespace ReactiveUI; + +/// +/// Converts nullable to . +/// +public sealed class NullableDateOnlyToStringTypeConverter : BindingTypeConverter +{ + /// + public override int GetAffinityForObjects() => 10; + + /// + public override bool TryConvert(DateOnly? from, object? conversionHint, [NotNullWhen(true)] out string? result) + { + if (!from.HasValue) + { + result = null; + return false; + } + + result = from.Value.ToString(); + return true; + } +} +#endif diff --git a/src/ReactiveUI/Bindings/Converter/NullableDateTimeOffsetToStringTypeConverter.cs b/src/ReactiveUI/Bindings/Converter/NullableDateTimeOffsetToStringTypeConverter.cs new file mode 100644 index 0000000000..c59444214a --- /dev/null +++ b/src/ReactiveUI/Bindings/Converter/NullableDateTimeOffsetToStringTypeConverter.cs @@ -0,0 +1,30 @@ +// 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; + +/// +/// Converts nullable to . +/// +public sealed class NullableDateTimeOffsetToStringTypeConverter : BindingTypeConverter +{ + /// + public override int GetAffinityForObjects() => 10; + + /// + public override bool TryConvert(DateTimeOffset? from, object? conversionHint, [NotNullWhen(true)] out string? result) + { + if (!from.HasValue) + { + result = null; + return false; + } + + result = from.Value.ToString(); + return true; + } +} diff --git a/src/ReactiveUI/Bindings/Converter/NullableDateTimeToStringTypeConverter.cs b/src/ReactiveUI/Bindings/Converter/NullableDateTimeToStringTypeConverter.cs new file mode 100644 index 0000000000..2488164dfa --- /dev/null +++ b/src/ReactiveUI/Bindings/Converter/NullableDateTimeToStringTypeConverter.cs @@ -0,0 +1,30 @@ +// 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; + +/// +/// Converts nullable to . +/// +public sealed class NullableDateTimeToStringTypeConverter : BindingTypeConverter +{ + /// + public override int GetAffinityForObjects() => 10; + + /// + public override bool TryConvert(DateTime? from, object? conversionHint, [NotNullWhen(true)] out string? result) + { + if (!from.HasValue) + { + result = null; + return false; + } + + result = from.Value.ToString(); + return true; + } +} diff --git a/src/ReactiveUI/Bindings/Converter/NullableDecimalToStringTypeConverter.cs b/src/ReactiveUI/Bindings/Converter/NullableDecimalToStringTypeConverter.cs index 8e948034e1..367718988b 100644 --- a/src/ReactiveUI/Bindings/Converter/NullableDecimalToStringTypeConverter.cs +++ b/src/ReactiveUI/Bindings/Converter/NullableDecimalToStringTypeConverter.cs @@ -1,77 +1,30 @@ -// Copyright (c) 2025 .NET Foundation and Contributors. All rights reserved. +// 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; /// -/// Decimal To String Type Converter. +/// Converts nullable values to . /// -/// -public class NullableDecimalToStringTypeConverter : IBindingTypeConverter +public sealed class NullableDecimalToStringTypeConverter : BindingTypeConverter { /// - public int GetAffinityForObjects(Type fromType, Type toType) - { - if (fromType == typeof(decimal?) && toType == typeof(string)) - { - return 10; - } - - if (fromType == typeof(string) && toType == typeof(decimal?)) - { - 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(decimal? from, object? conversionHint, [NotNullWhen(true)] out string? result) { - if (toType == typeof(string) && from is decimal fromDecimal) + if (!from.HasValue) { - if (conversionHint is int decimalHint) - { - result = fromDecimal.ToString($"F{decimalHint}"); - return true; - } - - result = fromDecimal.ToString(); - return true; - } - - if (from is null) - { - result = null!; - return true; - } - - if (from is string fromString) - { - if (string.IsNullOrEmpty(fromString)) - { - result = null!; - return true; - } - - var success = decimal.TryParse(fromString, out var outDecimal); - if (success) - { - if (conversionHint is int decimalHint) - { - result = Math.Round(outDecimal, decimalHint); - return true; - } - - result = outDecimal; - - return true; - } + result = null; + return false; } - result = null!; - return false; + result = from.Value.ToString(); + return true; } } diff --git a/src/ReactiveUI/Bindings/Converter/NullableDoubleToStringTypeConverter.cs b/src/ReactiveUI/Bindings/Converter/NullableDoubleToStringTypeConverter.cs index 6d2fac6e50..9b8e23624c 100644 --- a/src/ReactiveUI/Bindings/Converter/NullableDoubleToStringTypeConverter.cs +++ b/src/ReactiveUI/Bindings/Converter/NullableDoubleToStringTypeConverter.cs @@ -1,77 +1,30 @@ -// Copyright (c) 2025 .NET Foundation and Contributors. All rights reserved. +// 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; /// -/// Double To String Type Converter. +/// Converts nullable values to . /// -/// -public class NullableDoubleToStringTypeConverter : IBindingTypeConverter +public sealed class NullableDoubleToStringTypeConverter : BindingTypeConverter { /// - public int GetAffinityForObjects(Type fromType, Type toType) - { - if (fromType == typeof(double?) && toType == typeof(string)) - { - return 10; - } - - if (fromType == typeof(string) && toType == typeof(double?)) - { - 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(double? from, object? conversionHint, [NotNullWhen(true)] out string? result) { - if (toType == typeof(string) && from is double fromDouble) + if (!from.HasValue) { - if (conversionHint is int doubleHint) - { - result = fromDouble.ToString($"F{doubleHint}"); - return true; - } - - result = fromDouble.ToString(); - return true; - } - - if (from is null) - { - result = null!; - return true; - } - - if (from is string fromString) - { - if (string.IsNullOrEmpty(fromString)) - { - result = null!; - return true; - } - - var success = double.TryParse(fromString, out var outDouble); - if (success) - { - if (conversionHint is int doubleHint) - { - result = Math.Round(outDouble, doubleHint); - return true; - } - - result = outDouble; - - return true; - } + result = null; + return false; } - result = null!; - return false; + result = from.Value.ToString(); + return true; } } diff --git a/src/ReactiveUI/Bindings/Converter/NullableGuidToStringTypeConverter.cs b/src/ReactiveUI/Bindings/Converter/NullableGuidToStringTypeConverter.cs new file mode 100644 index 0000000000..3c6da307c7 --- /dev/null +++ b/src/ReactiveUI/Bindings/Converter/NullableGuidToStringTypeConverter.cs @@ -0,0 +1,30 @@ +// 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; + +/// +/// Converts nullable to using the "D" format (standard hyphenated format). +/// +public sealed class NullableGuidToStringTypeConverter : BindingTypeConverter +{ + /// + public override int GetAffinityForObjects() => 10; + + /// + public override bool TryConvert(Guid? from, object? conversionHint, [NotNullWhen(true)] out string? result) + { + if (!from.HasValue) + { + result = null; + return false; + } + + result = from.Value.ToString("D"); + return true; + } +} diff --git a/src/ReactiveUI/Bindings/Converter/NullableIntegerToStringTypeConverter.cs b/src/ReactiveUI/Bindings/Converter/NullableIntegerToStringTypeConverter.cs index 8e74ae5482..01c210e488 100644 --- a/src/ReactiveUI/Bindings/Converter/NullableIntegerToStringTypeConverter.cs +++ b/src/ReactiveUI/Bindings/Converter/NullableIntegerToStringTypeConverter.cs @@ -1,71 +1,30 @@ -// Copyright (c) 2025 .NET Foundation and Contributors. All rights reserved. +// 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; /// -/// Integer To String Type Converter. +/// Converts nullable values to . /// -/// -public class NullableIntegerToStringTypeConverter : IBindingTypeConverter +public sealed class NullableIntegerToStringTypeConverter : BindingTypeConverter { /// - public int GetAffinityForObjects(Type fromType, Type toType) - { - if (fromType == typeof(int?) && toType == typeof(string)) - { - return 10; - } - - if (fromType == typeof(string) && toType == typeof(int?)) - { - 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(int? from, object? conversionHint, [NotNullWhen(true)] out string? result) { - if (toType == typeof(string) && from is int fromInt) + if (!from.HasValue) { - if (conversionHint is int intHint) - { - result = fromInt.ToString($"D{intHint}"); - return true; - } - - result = fromInt.ToString(); - return true; - } - - if (from is null) - { - result = null!; - return true; - } - - if (from is string fromString) - { - if (string.IsNullOrEmpty(fromString)) - { - result = null!; - return true; - } - - var success = int.TryParse(fromString, out var outInt); - if (success) - { - result = outInt; - - return true; - } + result = null; + return false; } - result = null!; - return false; + result = from.Value.ToString(); + return true; } } diff --git a/src/ReactiveUI/Bindings/Converter/NullableLongToStringTypeConverter.cs b/src/ReactiveUI/Bindings/Converter/NullableLongToStringTypeConverter.cs index 167dc266ce..892bbbd494 100644 --- a/src/ReactiveUI/Bindings/Converter/NullableLongToStringTypeConverter.cs +++ b/src/ReactiveUI/Bindings/Converter/NullableLongToStringTypeConverter.cs @@ -1,71 +1,30 @@ -// Copyright (c) 2025 .NET Foundation and Contributors. All rights reserved. +// 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; /// -/// Integer To String Type Converter. +/// Converts nullable values to . /// -/// -public class NullableLongToStringTypeConverter : IBindingTypeConverter +public sealed class NullableLongToStringTypeConverter : BindingTypeConverter { /// - public int GetAffinityForObjects(Type fromType, Type toType) - { - if (fromType == typeof(long?) && toType == typeof(string)) - { - return 10; - } - - if (fromType == typeof(string) && toType == typeof(long?)) - { - 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(long? from, object? conversionHint, [NotNullWhen(true)] out string? result) { - if (toType == typeof(string) && from is long fromLong) + if (!from.HasValue) { - if (conversionHint is int longHint) - { - result = fromLong.ToString($"D{longHint}"); - return true; - } - - result = fromLong.ToString(); - return true; - } - - if (from is null) - { - result = null!; - return true; - } - - if (from is string fromString) - { - if (string.IsNullOrEmpty(fromString)) - { - result = null!; - return true; - } - - var success = long.TryParse(fromString, out var outLong); - if (success) - { - result = outLong; - - return true; - } + result = null; + return false; } - result = null!; - return false; + result = from.Value.ToString(); + return true; } } diff --git a/src/ReactiveUI/Bindings/Converter/NullableShortToStringTypeConverter.cs b/src/ReactiveUI/Bindings/Converter/NullableShortToStringTypeConverter.cs index b13cade79f..b4126b6b11 100644 --- a/src/ReactiveUI/Bindings/Converter/NullableShortToStringTypeConverter.cs +++ b/src/ReactiveUI/Bindings/Converter/NullableShortToStringTypeConverter.cs @@ -1,71 +1,30 @@ -// Copyright (c) 2025 .NET Foundation and Contributors. All rights reserved. +// 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; /// -/// Short To String Type Converter. +/// Converts nullable values to . /// -/// -public class NullableShortToStringTypeConverter : IBindingTypeConverter +public sealed class NullableShortToStringTypeConverter : BindingTypeConverter { /// - public int GetAffinityForObjects(Type fromType, Type toType) - { - if (fromType == typeof(short?) && toType == typeof(string)) - { - return 10; - } - - if (fromType == typeof(string) && toType == typeof(short?)) - { - 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(short? from, object? conversionHint, [NotNullWhen(true)] out string? result) { - if (toType == typeof(string) && from is short fromShort) + if (!from.HasValue) { - if (conversionHint is int shortHint) - { - result = fromShort.ToString($"D{shortHint}"); - return true; - } - - result = fromShort.ToString(); - return true; - } - - if (from is null) - { - result = null!; - return true; - } - - if (from is string fromString) - { - if (string.IsNullOrEmpty(fromString)) - { - result = null!; - return true; - } - - var success = short.TryParse(fromString, out var outShort); - if (success) - { - result = outShort; - - return true; - } + result = null; + return false; } - result = null!; - return false; + result = from.Value.ToString(); + return true; } } diff --git a/src/ReactiveUI/Bindings/Converter/NullableSingleToStringTypeConverter.cs b/src/ReactiveUI/Bindings/Converter/NullableSingleToStringTypeConverter.cs index 5e18b145d0..1cd6981857 100644 --- a/src/ReactiveUI/Bindings/Converter/NullableSingleToStringTypeConverter.cs +++ b/src/ReactiveUI/Bindings/Converter/NullableSingleToStringTypeConverter.cs @@ -1,77 +1,30 @@ -// Copyright (c) 2025 .NET Foundation and Contributors. All rights reserved. +// 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; /// -/// Single To String Type Converter. +/// Converts nullable values to . /// -/// -public class NullableSingleToStringTypeConverter : IBindingTypeConverter +public sealed class NullableSingleToStringTypeConverter : BindingTypeConverter { /// - public int GetAffinityForObjects(Type fromType, Type toType) - { - if (fromType == typeof(float?) && toType == typeof(string)) - { - return 10; - } - - if (fromType == typeof(string) && toType == typeof(float?)) - { - 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(float? from, object? conversionHint, [NotNullWhen(true)] out string? result) { - if (toType == typeof(string) && from is float fromSingle) + if (!from.HasValue) { - if (conversionHint is int singleHint) - { - result = fromSingle.ToString($"F{singleHint}"); - return true; - } - - result = fromSingle.ToString(); - return true; - } - - if (from is null) - { - result = null!; - return true; - } - - if (from is string fromString) - { - if (string.IsNullOrEmpty(fromString)) - { - result = null!; - return true; - } - - var success = float.TryParse(fromString, out var outSingle); - if (success) - { - if (conversionHint is int singleHint) - { - result = Convert.ToSingle(Math.Round(outSingle, singleHint)); - return true; - } - - result = outSingle; - - return true; - } + result = null; + return false; } - result = null!; - return false; + result = from.Value.ToString(); + return true; } } diff --git a/src/ReactiveUI/Bindings/Converter/NullableTimeOnlyToStringTypeConverter.cs b/src/ReactiveUI/Bindings/Converter/NullableTimeOnlyToStringTypeConverter.cs new file mode 100644 index 0000000000..8cc4fde3a0 --- /dev/null +++ b/src/ReactiveUI/Bindings/Converter/NullableTimeOnlyToStringTypeConverter.cs @@ -0,0 +1,32 @@ +// 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 NET6_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; + +namespace ReactiveUI; + +/// +/// Converts nullable to . +/// +public sealed class NullableTimeOnlyToStringTypeConverter : BindingTypeConverter +{ + /// + public override int GetAffinityForObjects() => 10; + + /// + public override bool TryConvert(TimeOnly? from, object? conversionHint, [NotNullWhen(true)] out string? result) + { + if (!from.HasValue) + { + result = null; + return false; + } + + result = from.Value.ToString(); + return true; + } +} +#endif diff --git a/src/ReactiveUI/Bindings/Converter/NullableTimeSpanToStringTypeConverter.cs b/src/ReactiveUI/Bindings/Converter/NullableTimeSpanToStringTypeConverter.cs new file mode 100644 index 0000000000..79c32a0a71 --- /dev/null +++ b/src/ReactiveUI/Bindings/Converter/NullableTimeSpanToStringTypeConverter.cs @@ -0,0 +1,30 @@ +// 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; + +/// +/// Converts nullable to . +/// +public sealed class NullableTimeSpanToStringTypeConverter : BindingTypeConverter +{ + /// + public override int GetAffinityForObjects() => 10; + + /// + public override bool TryConvert(TimeSpan? from, object? conversionHint, [NotNullWhen(true)] out string? result) + { + if (!from.HasValue) + { + result = null; + return false; + } + + result = from.Value.ToString(); + return true; + } +} diff --git a/src/ReactiveUI/Bindings/Converter/ShortToStringTypeConverter.cs b/src/ReactiveUI/Bindings/Converter/ShortToStringTypeConverter.cs index a6b20bc6d7..dcf8195c62 100644 --- a/src/ReactiveUI/Bindings/Converter/ShortToStringTypeConverter.cs +++ b/src/ReactiveUI/Bindings/Converter/ShortToStringTypeConverter.cs @@ -1,59 +1,24 @@ -// Copyright (c) 2025 .NET Foundation and Contributors. All rights reserved. +// 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; /// -/// Short To String Type Converter. +/// Converts values to . /// -/// -public class ShortToStringTypeConverter : IBindingTypeConverter +public sealed class ShortToStringTypeConverter : BindingTypeConverter { /// - public int GetAffinityForObjects(Type fromType, Type toType) - { - if (fromType == typeof(short) && toType == typeof(string)) - { - return 10; - } - - if (fromType == typeof(string) && toType == typeof(short)) - { - 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(short from, object? conversionHint, [NotNullWhen(true)] out string? result) { - if (toType == typeof(string) && from is short fromShort) - { - if (conversionHint is int shortHint) - { - result = fromShort.ToString($"D{shortHint}"); - return true; - } - - result = fromShort.ToString(); - return true; - } - - if (from is string fromString) - { - var success = short.TryParse(fromString, out var outShort); - if (success) - { - result = outShort; - - return true; - } - } - - result = null!; - return false; + result = from.ToString(); + return true; } } diff --git a/src/ReactiveUI/Bindings/Converter/SingleToStringTypeConverter.cs b/src/ReactiveUI/Bindings/Converter/SingleToStringTypeConverter.cs index 29ed40e089..dec35c1c47 100644 --- a/src/ReactiveUI/Bindings/Converter/SingleToStringTypeConverter.cs +++ b/src/ReactiveUI/Bindings/Converter/SingleToStringTypeConverter.cs @@ -1,65 +1,24 @@ -// Copyright (c) 2025 .NET Foundation and Contributors. All rights reserved. +// 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; /// -/// Single To String Type Converter. +/// Converts values to . /// -/// -public class SingleToStringTypeConverter : IBindingTypeConverter +public sealed class SingleToStringTypeConverter : BindingTypeConverter { /// - public int GetAffinityForObjects(Type fromType, Type toType) - { - if (fromType == typeof(float) && toType == typeof(string)) - { - return 10; - } - - if (fromType == typeof(string) && toType == typeof(float)) - { - 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(float from, object? conversionHint, [NotNullWhen(true)] out string? result) { - if (toType == typeof(string) && from is float fromSingle) - { - if (conversionHint is int singleHint) - { - result = fromSingle.ToString($"F{singleHint}"); - return true; - } - - result = fromSingle.ToString(); - return true; - } - - if (from is string fromString) - { - var success = float.TryParse(fromString, out var outSingle); - if (success) - { - if (conversionHint is int singleHint) - { - result = Convert.ToSingle(Math.Round(outSingle, singleHint)); - return true; - } - - result = outSingle; - - return true; - } - } - - result = null!; - return false; + result = from.ToString(); + return true; } } diff --git a/src/ReactiveUI/Bindings/Converter/StringConverter.cs b/src/ReactiveUI/Bindings/Converter/StringConverter.cs index 6d253d31fd..56b1e13520 100644 --- a/src/ReactiveUI/Bindings/Converter/StringConverter.cs +++ b/src/ReactiveUI/Bindings/Converter/StringConverter.cs @@ -1,24 +1,46 @@ -// Copyright (c) 2025 .NET Foundation and Contributors. All rights reserved. +// 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; /// -/// Calls ToString on types. In WPF, ComponentTypeConverter should win -/// instead of this, since It's Better™. +/// Converts to (identity converter). /// -public class StringConverter : IBindingTypeConverter +/// +/// This converter provides a fast path for string-to-string bindings without +/// requiring reflection or TypeDescriptor. +/// +public sealed class StringConverter : IBindingTypeConverter { /// - public int GetAffinityForObjects(Type fromType, Type toType) => toType == typeof(string) ? 2 : 0; + public Type FromType => typeof(string); + + /// + public Type ToType => typeof(string); /// - public bool TryConvert(object? from, Type toType, object? conversionHint, out object? result) + public int GetAffinityForObjects() => 10; + + /// + public bool TryConvertTyped(object? from, object? conversionHint, [NotNullWhen(true)] out object? result) { - // XXX: All Of The Localization - result = from?.ToString(); - return true; + if (from is null) + { + result = null; + return false; + } + + if (from is string s) + { + result = s; + return true; + } + + result = null; + return false; } } diff --git a/src/ReactiveUI/Bindings/Converter/StringToBooleanTypeConverter.cs b/src/ReactiveUI/Bindings/Converter/StringToBooleanTypeConverter.cs new file mode 100644 index 0000000000..791a4653c4 --- /dev/null +++ b/src/ReactiveUI/Bindings/Converter/StringToBooleanTypeConverter.cs @@ -0,0 +1,29 @@ +// 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; + +/// +/// Converts to using . +/// +public sealed class StringToBooleanTypeConverter : BindingTypeConverter +{ + /// + public override int GetAffinityForObjects() => 10; + + /// + public override bool TryConvert(string? from, object? conversionHint, [NotNullWhen(true)] out bool result) + { + if (from is null) + { + result = default; + return false; + } + + return bool.TryParse(from, out result); + } +} diff --git a/src/ReactiveUI/Bindings/Converter/StringToByteTypeConverter.cs b/src/ReactiveUI/Bindings/Converter/StringToByteTypeConverter.cs new file mode 100644 index 0000000000..f46f4412ce --- /dev/null +++ b/src/ReactiveUI/Bindings/Converter/StringToByteTypeConverter.cs @@ -0,0 +1,29 @@ +// 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; + +/// +/// Converts to using . +/// +public sealed class StringToByteTypeConverter : BindingTypeConverter +{ + /// + public override int GetAffinityForObjects() => 10; + + /// + public override bool TryConvert(string? from, object? conversionHint, [NotNullWhen(true)] out byte result) + { + if (from is null) + { + result = default; + return false; + } + + return byte.TryParse(from, out result); + } +} diff --git a/src/ReactiveUI/Bindings/Converter/StringToDateOnlyTypeConverter.cs b/src/ReactiveUI/Bindings/Converter/StringToDateOnlyTypeConverter.cs new file mode 100644 index 0000000000..8c67eccaaa --- /dev/null +++ b/src/ReactiveUI/Bindings/Converter/StringToDateOnlyTypeConverter.cs @@ -0,0 +1,31 @@ +// 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 NET6_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; + +namespace ReactiveUI; + +/// +/// Converts to using . +/// +public sealed class StringToDateOnlyTypeConverter : BindingTypeConverter +{ + /// + public override int GetAffinityForObjects() => 10; + + /// + public override bool TryConvert(string? from, object? conversionHint, [NotNullWhen(true)] out DateOnly result) + { + if (from is null) + { + result = default; + return false; + } + + return DateOnly.TryParse(from, out result); + } +} +#endif diff --git a/src/ReactiveUI/Bindings/Converter/StringToDateTimeOffsetTypeConverter.cs b/src/ReactiveUI/Bindings/Converter/StringToDateTimeOffsetTypeConverter.cs new file mode 100644 index 0000000000..031a2272f5 --- /dev/null +++ b/src/ReactiveUI/Bindings/Converter/StringToDateTimeOffsetTypeConverter.cs @@ -0,0 +1,29 @@ +// 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; + +/// +/// Converts to using . +/// +public sealed class StringToDateTimeOffsetTypeConverter : BindingTypeConverter +{ + /// + public override int GetAffinityForObjects() => 10; + + /// + public override bool TryConvert(string? from, object? conversionHint, [NotNullWhen(true)] out DateTimeOffset result) + { + if (from is null) + { + result = default; + return false; + } + + return DateTimeOffset.TryParse(from, out result); + } +} diff --git a/src/ReactiveUI/Bindings/Converter/StringToDateTimeTypeConverter.cs b/src/ReactiveUI/Bindings/Converter/StringToDateTimeTypeConverter.cs new file mode 100644 index 0000000000..1c042a188a --- /dev/null +++ b/src/ReactiveUI/Bindings/Converter/StringToDateTimeTypeConverter.cs @@ -0,0 +1,29 @@ +// 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; + +/// +/// Converts to using . +/// +public sealed class StringToDateTimeTypeConverter : BindingTypeConverter +{ + /// + public override int GetAffinityForObjects() => 10; + + /// + public override bool TryConvert(string? from, object? conversionHint, [NotNullWhen(true)] out DateTime result) + { + if (from is null) + { + result = default; + return false; + } + + return DateTime.TryParse(from, out result); + } +} diff --git a/src/ReactiveUI/Bindings/Converter/StringToDecimalTypeConverter.cs b/src/ReactiveUI/Bindings/Converter/StringToDecimalTypeConverter.cs new file mode 100644 index 0000000000..74115db495 --- /dev/null +++ b/src/ReactiveUI/Bindings/Converter/StringToDecimalTypeConverter.cs @@ -0,0 +1,29 @@ +// 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; + +/// +/// Converts to using . +/// +public sealed class StringToDecimalTypeConverter : BindingTypeConverter +{ + /// + public override int GetAffinityForObjects() => 10; + + /// + public override bool TryConvert(string? from, object? conversionHint, [NotNullWhen(true)] out decimal result) + { + if (from is null) + { + result = default; + return false; + } + + return decimal.TryParse(from, out result); + } +} diff --git a/src/ReactiveUI/Bindings/Converter/StringToDoubleTypeConverter.cs b/src/ReactiveUI/Bindings/Converter/StringToDoubleTypeConverter.cs new file mode 100644 index 0000000000..f0cd5673e7 --- /dev/null +++ b/src/ReactiveUI/Bindings/Converter/StringToDoubleTypeConverter.cs @@ -0,0 +1,29 @@ +// 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; + +/// +/// Converts to using . +/// +public sealed class StringToDoubleTypeConverter : BindingTypeConverter +{ + /// + public override int GetAffinityForObjects() => 10; + + /// + public override bool TryConvert(string? from, object? conversionHint, [NotNullWhen(true)] out double result) + { + if (from is null) + { + result = default; + return false; + } + + return double.TryParse(from, out result); + } +} diff --git a/src/ReactiveUI/Bindings/Converter/StringToGuidTypeConverter.cs b/src/ReactiveUI/Bindings/Converter/StringToGuidTypeConverter.cs new file mode 100644 index 0000000000..65cba9a1be --- /dev/null +++ b/src/ReactiveUI/Bindings/Converter/StringToGuidTypeConverter.cs @@ -0,0 +1,29 @@ +// 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; + +/// +/// Converts to using . +/// +public sealed class StringToGuidTypeConverter : BindingTypeConverter +{ + /// + public override int GetAffinityForObjects() => 10; + + /// + public override bool TryConvert(string? from, object? conversionHint, [NotNullWhen(true)] out Guid result) + { + if (from is null) + { + result = default; + return false; + } + + return Guid.TryParse(from, out result); + } +} diff --git a/src/ReactiveUI/Bindings/Converter/StringToIntegerTypeConverter.cs b/src/ReactiveUI/Bindings/Converter/StringToIntegerTypeConverter.cs new file mode 100644 index 0000000000..16fa11256e --- /dev/null +++ b/src/ReactiveUI/Bindings/Converter/StringToIntegerTypeConverter.cs @@ -0,0 +1,29 @@ +// 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; + +/// +/// Converts to using . +/// +public sealed class StringToIntegerTypeConverter : BindingTypeConverter +{ + /// + public override int GetAffinityForObjects() => 10; + + /// + public override bool TryConvert(string? from, object? conversionHint, [NotNullWhen(true)] out int result) + { + if (from is null) + { + result = default; + return false; + } + + return int.TryParse(from, out result); + } +} diff --git a/src/ReactiveUI/Bindings/Converter/StringToLongTypeConverter.cs b/src/ReactiveUI/Bindings/Converter/StringToLongTypeConverter.cs new file mode 100644 index 0000000000..0e2786952d --- /dev/null +++ b/src/ReactiveUI/Bindings/Converter/StringToLongTypeConverter.cs @@ -0,0 +1,29 @@ +// 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; + +/// +/// Converts to using . +/// +public sealed class StringToLongTypeConverter : BindingTypeConverter +{ + /// + public override int GetAffinityForObjects() => 10; + + /// + public override bool TryConvert(string? from, object? conversionHint, [NotNullWhen(true)] out long result) + { + if (from is null) + { + result = default; + return false; + } + + return long.TryParse(from, out result); + } +} diff --git a/src/ReactiveUI/Bindings/Converter/StringToNullableBooleanTypeConverter.cs b/src/ReactiveUI/Bindings/Converter/StringToNullableBooleanTypeConverter.cs new file mode 100644 index 0000000000..02d149ae30 --- /dev/null +++ b/src/ReactiveUI/Bindings/Converter/StringToNullableBooleanTypeConverter.cs @@ -0,0 +1,36 @@ +// 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; + +/// +/// Converts to nullable using . +/// +public sealed class StringToNullableBooleanTypeConverter : BindingTypeConverter +{ + /// + public override int GetAffinityForObjects() => 10; + + /// + public override bool TryConvert(string? from, object? conversionHint, [NotNullWhen(true)] out bool? result) + { + if (from is null) + { + result = null; + return false; + } + + if (bool.TryParse(from, out var value)) + { + result = value; + return true; + } + + result = null; + return false; + } +} diff --git a/src/ReactiveUI/Bindings/Converter/StringToNullableByteTypeConverter.cs b/src/ReactiveUI/Bindings/Converter/StringToNullableByteTypeConverter.cs new file mode 100644 index 0000000000..d121130881 --- /dev/null +++ b/src/ReactiveUI/Bindings/Converter/StringToNullableByteTypeConverter.cs @@ -0,0 +1,36 @@ +// 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; + +/// +/// Converts to nullable using . +/// +public sealed class StringToNullableByteTypeConverter : BindingTypeConverter +{ + /// + public override int GetAffinityForObjects() => 10; + + /// + public override bool TryConvert(string? from, object? conversionHint, [NotNullWhen(true)] out byte? result) + { + if (from is null) + { + result = null; + return false; + } + + if (byte.TryParse(from, out var value)) + { + result = value; + return true; + } + + result = null; + return false; + } +} diff --git a/src/ReactiveUI/Bindings/Converter/StringToNullableDateOnlyTypeConverter.cs b/src/ReactiveUI/Bindings/Converter/StringToNullableDateOnlyTypeConverter.cs new file mode 100644 index 0000000000..404e5e6b48 --- /dev/null +++ b/src/ReactiveUI/Bindings/Converter/StringToNullableDateOnlyTypeConverter.cs @@ -0,0 +1,38 @@ +// 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 NET6_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; + +namespace ReactiveUI; + +/// +/// Converts to nullable using . +/// +public sealed class StringToNullableDateOnlyTypeConverter : BindingTypeConverter +{ + /// + public override int GetAffinityForObjects() => 10; + + /// + public override bool TryConvert(string? from, object? conversionHint, [NotNullWhen(true)] out DateOnly? result) + { + if (from is null) + { + result = null; + return false; + } + + if (DateOnly.TryParse(from, out var value)) + { + result = value; + return true; + } + + result = null; + return false; + } +} +#endif diff --git a/src/ReactiveUI/Bindings/Converter/StringToNullableDateTimeOffsetTypeConverter.cs b/src/ReactiveUI/Bindings/Converter/StringToNullableDateTimeOffsetTypeConverter.cs new file mode 100644 index 0000000000..5185abcf17 --- /dev/null +++ b/src/ReactiveUI/Bindings/Converter/StringToNullableDateTimeOffsetTypeConverter.cs @@ -0,0 +1,36 @@ +// 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; + +/// +/// Converts to nullable using . +/// +public sealed class StringToNullableDateTimeOffsetTypeConverter : BindingTypeConverter +{ + /// + public override int GetAffinityForObjects() => 10; + + /// + public override bool TryConvert(string? from, object? conversionHint, [NotNullWhen(true)] out DateTimeOffset? result) + { + if (from is null) + { + result = null; + return false; + } + + if (DateTimeOffset.TryParse(from, out var value)) + { + result = value; + return true; + } + + result = null; + return false; + } +} diff --git a/src/ReactiveUI/Bindings/Converter/StringToNullableDateTimeTypeConverter.cs b/src/ReactiveUI/Bindings/Converter/StringToNullableDateTimeTypeConverter.cs new file mode 100644 index 0000000000..1c9d031a2d --- /dev/null +++ b/src/ReactiveUI/Bindings/Converter/StringToNullableDateTimeTypeConverter.cs @@ -0,0 +1,36 @@ +// 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; + +/// +/// Converts to nullable using . +/// +public sealed class StringToNullableDateTimeTypeConverter : BindingTypeConverter +{ + /// + public override int GetAffinityForObjects() => 10; + + /// + public override bool TryConvert(string? from, object? conversionHint, [NotNullWhen(true)] out DateTime? result) + { + if (from is null) + { + result = null; + return false; + } + + if (DateTime.TryParse(from, out var value)) + { + result = value; + return true; + } + + result = null; + return false; + } +} diff --git a/src/ReactiveUI/Bindings/Converter/StringToNullableDecimalTypeConverter.cs b/src/ReactiveUI/Bindings/Converter/StringToNullableDecimalTypeConverter.cs new file mode 100644 index 0000000000..021e6517e7 --- /dev/null +++ b/src/ReactiveUI/Bindings/Converter/StringToNullableDecimalTypeConverter.cs @@ -0,0 +1,36 @@ +// 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; + +/// +/// Converts to nullable using . +/// +public sealed class StringToNullableDecimalTypeConverter : BindingTypeConverter +{ + /// + public override int GetAffinityForObjects() => 10; + + /// + public override bool TryConvert(string? from, object? conversionHint, [NotNullWhen(true)] out decimal? result) + { + if (from is null) + { + result = null; + return false; + } + + if (decimal.TryParse(from, out var value)) + { + result = value; + return true; + } + + result = null; + return false; + } +} diff --git a/src/ReactiveUI/Bindings/Converter/StringToNullableDoubleTypeConverter.cs b/src/ReactiveUI/Bindings/Converter/StringToNullableDoubleTypeConverter.cs new file mode 100644 index 0000000000..9b82801a1a --- /dev/null +++ b/src/ReactiveUI/Bindings/Converter/StringToNullableDoubleTypeConverter.cs @@ -0,0 +1,36 @@ +// 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; + +/// +/// Converts to nullable using . +/// +public sealed class StringToNullableDoubleTypeConverter : BindingTypeConverter +{ + /// + public override int GetAffinityForObjects() => 10; + + /// + public override bool TryConvert(string? from, object? conversionHint, [NotNullWhen(true)] out double? result) + { + if (from is null) + { + result = null; + return false; + } + + if (double.TryParse(from, out var value)) + { + result = value; + return true; + } + + result = null; + return false; + } +} diff --git a/src/ReactiveUI/Bindings/Converter/StringToNullableGuidTypeConverter.cs b/src/ReactiveUI/Bindings/Converter/StringToNullableGuidTypeConverter.cs new file mode 100644 index 0000000000..671c25f341 --- /dev/null +++ b/src/ReactiveUI/Bindings/Converter/StringToNullableGuidTypeConverter.cs @@ -0,0 +1,36 @@ +// 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; + +/// +/// Converts to nullable using . +/// +public sealed class StringToNullableGuidTypeConverter : BindingTypeConverter +{ + /// + public override int GetAffinityForObjects() => 10; + + /// + public override bool TryConvert(string? from, object? conversionHint, [NotNullWhen(true)] out Guid? result) + { + if (from is null) + { + result = null; + return false; + } + + if (Guid.TryParse(from, out var value)) + { + result = value; + return true; + } + + result = null; + return false; + } +} diff --git a/src/ReactiveUI/Bindings/Converter/StringToNullableIntegerTypeConverter.cs b/src/ReactiveUI/Bindings/Converter/StringToNullableIntegerTypeConverter.cs new file mode 100644 index 0000000000..632a78bd0b --- /dev/null +++ b/src/ReactiveUI/Bindings/Converter/StringToNullableIntegerTypeConverter.cs @@ -0,0 +1,36 @@ +// 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; + +/// +/// Converts to nullable using . +/// +public sealed class StringToNullableIntegerTypeConverter : BindingTypeConverter +{ + /// + public override int GetAffinityForObjects() => 10; + + /// + public override bool TryConvert(string? from, object? conversionHint, [NotNullWhen(true)] out int? result) + { + if (from is null) + { + result = null; + return false; + } + + if (int.TryParse(from, out var value)) + { + result = value; + return true; + } + + result = null; + return false; + } +} diff --git a/src/ReactiveUI/Bindings/Converter/StringToNullableLongTypeConverter.cs b/src/ReactiveUI/Bindings/Converter/StringToNullableLongTypeConverter.cs new file mode 100644 index 0000000000..818819bb6d --- /dev/null +++ b/src/ReactiveUI/Bindings/Converter/StringToNullableLongTypeConverter.cs @@ -0,0 +1,36 @@ +// 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; + +/// +/// Converts to nullable using . +/// +public sealed class StringToNullableLongTypeConverter : BindingTypeConverter +{ + /// + public override int GetAffinityForObjects() => 10; + + /// + public override bool TryConvert(string? from, object? conversionHint, [NotNullWhen(true)] out long? result) + { + if (from is null) + { + result = null; + return false; + } + + if (long.TryParse(from, out var value)) + { + result = value; + return true; + } + + result = null; + return false; + } +} diff --git a/src/ReactiveUI/Bindings/Converter/StringToNullableShortTypeConverter.cs b/src/ReactiveUI/Bindings/Converter/StringToNullableShortTypeConverter.cs new file mode 100644 index 0000000000..d187e2421c --- /dev/null +++ b/src/ReactiveUI/Bindings/Converter/StringToNullableShortTypeConverter.cs @@ -0,0 +1,36 @@ +// 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; + +/// +/// Converts to nullable using . +/// +public sealed class StringToNullableShortTypeConverter : BindingTypeConverter +{ + /// + public override int GetAffinityForObjects() => 10; + + /// + public override bool TryConvert(string? from, object? conversionHint, [NotNullWhen(true)] out short? result) + { + if (from is null) + { + result = null; + return false; + } + + if (short.TryParse(from, out var value)) + { + result = value; + return true; + } + + result = null; + return false; + } +} diff --git a/src/ReactiveUI/Bindings/Converter/StringToNullableSingleTypeConverter.cs b/src/ReactiveUI/Bindings/Converter/StringToNullableSingleTypeConverter.cs new file mode 100644 index 0000000000..8e57a46a1d --- /dev/null +++ b/src/ReactiveUI/Bindings/Converter/StringToNullableSingleTypeConverter.cs @@ -0,0 +1,36 @@ +// 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; + +/// +/// Converts to nullable using . +/// +public sealed class StringToNullableSingleTypeConverter : BindingTypeConverter +{ + /// + public override int GetAffinityForObjects() => 10; + + /// + public override bool TryConvert(string? from, object? conversionHint, [NotNullWhen(true)] out float? result) + { + if (from is null) + { + result = null; + return false; + } + + if (float.TryParse(from, out var value)) + { + result = value; + return true; + } + + result = null; + return false; + } +} diff --git a/src/ReactiveUI/Bindings/Converter/StringToNullableTimeOnlyTypeConverter.cs b/src/ReactiveUI/Bindings/Converter/StringToNullableTimeOnlyTypeConverter.cs new file mode 100644 index 0000000000..22a75a9905 --- /dev/null +++ b/src/ReactiveUI/Bindings/Converter/StringToNullableTimeOnlyTypeConverter.cs @@ -0,0 +1,38 @@ +// 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 NET6_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; + +namespace ReactiveUI; + +/// +/// Converts to nullable using . +/// +public sealed class StringToNullableTimeOnlyTypeConverter : BindingTypeConverter +{ + /// + public override int GetAffinityForObjects() => 10; + + /// + public override bool TryConvert(string? from, object? conversionHint, [NotNullWhen(true)] out TimeOnly? result) + { + if (from is null) + { + result = null; + return false; + } + + if (TimeOnly.TryParse(from, out var value)) + { + result = value; + return true; + } + + result = null; + return false; + } +} +#endif diff --git a/src/ReactiveUI/Bindings/Converter/StringToNullableTimeSpanTypeConverter.cs b/src/ReactiveUI/Bindings/Converter/StringToNullableTimeSpanTypeConverter.cs new file mode 100644 index 0000000000..676ca7c430 --- /dev/null +++ b/src/ReactiveUI/Bindings/Converter/StringToNullableTimeSpanTypeConverter.cs @@ -0,0 +1,36 @@ +// 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; + +/// +/// Converts to nullable using . +/// +public sealed class StringToNullableTimeSpanTypeConverter : BindingTypeConverter +{ + /// + public override int GetAffinityForObjects() => 10; + + /// + public override bool TryConvert(string? from, object? conversionHint, [NotNullWhen(true)] out TimeSpan? result) + { + if (from is null) + { + result = null; + return false; + } + + if (TimeSpan.TryParse(from, out var value)) + { + result = value; + return true; + } + + result = null; + return false; + } +} diff --git a/src/ReactiveUI/Bindings/Converter/StringToShortTypeConverter.cs b/src/ReactiveUI/Bindings/Converter/StringToShortTypeConverter.cs new file mode 100644 index 0000000000..b1212023d2 --- /dev/null +++ b/src/ReactiveUI/Bindings/Converter/StringToShortTypeConverter.cs @@ -0,0 +1,29 @@ +// 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; + +/// +/// Converts to using . +/// +public sealed class StringToShortTypeConverter : BindingTypeConverter +{ + /// + public override int GetAffinityForObjects() => 10; + + /// + public override bool TryConvert(string? from, object? conversionHint, [NotNullWhen(true)] out short result) + { + if (from is null) + { + result = default; + return false; + } + + return short.TryParse(from, out result); + } +} diff --git a/src/ReactiveUI/Bindings/Converter/StringToSingleTypeConverter.cs b/src/ReactiveUI/Bindings/Converter/StringToSingleTypeConverter.cs new file mode 100644 index 0000000000..263d033b22 --- /dev/null +++ b/src/ReactiveUI/Bindings/Converter/StringToSingleTypeConverter.cs @@ -0,0 +1,29 @@ +// 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; + +/// +/// Converts to using . +/// +public sealed class StringToSingleTypeConverter : BindingTypeConverter +{ + /// + public override int GetAffinityForObjects() => 10; + + /// + public override bool TryConvert(string? from, object? conversionHint, [NotNullWhen(true)] out float result) + { + if (from is null) + { + result = default; + return false; + } + + return float.TryParse(from, out result); + } +} diff --git a/src/ReactiveUI/Bindings/Converter/StringToTimeOnlyTypeConverter.cs b/src/ReactiveUI/Bindings/Converter/StringToTimeOnlyTypeConverter.cs new file mode 100644 index 0000000000..8af6a14517 --- /dev/null +++ b/src/ReactiveUI/Bindings/Converter/StringToTimeOnlyTypeConverter.cs @@ -0,0 +1,31 @@ +// 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 NET6_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; + +namespace ReactiveUI; + +/// +/// Converts to using . +/// +public sealed class StringToTimeOnlyTypeConverter : BindingTypeConverter +{ + /// + public override int GetAffinityForObjects() => 10; + + /// + public override bool TryConvert(string? from, object? conversionHint, [NotNullWhen(true)] out TimeOnly result) + { + if (from is null) + { + result = default; + return false; + } + + return TimeOnly.TryParse(from, out result); + } +} +#endif diff --git a/src/ReactiveUI/Bindings/Converter/StringToTimeSpanTypeConverter.cs b/src/ReactiveUI/Bindings/Converter/StringToTimeSpanTypeConverter.cs new file mode 100644 index 0000000000..aa4d5f40df --- /dev/null +++ b/src/ReactiveUI/Bindings/Converter/StringToTimeSpanTypeConverter.cs @@ -0,0 +1,29 @@ +// 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; + +/// +/// Converts to using . +/// +public sealed class StringToTimeSpanTypeConverter : BindingTypeConverter +{ + /// + public override int GetAffinityForObjects() => 10; + + /// + public override bool TryConvert(string? from, object? conversionHint, [NotNullWhen(true)] out TimeSpan result) + { + if (from is null) + { + result = default; + return false; + } + + return TimeSpan.TryParse(from, out result); + } +} diff --git a/src/ReactiveUI/Bindings/Converter/StringToUriTypeConverter.cs b/src/ReactiveUI/Bindings/Converter/StringToUriTypeConverter.cs new file mode 100644 index 0000000000..83ea5f8a6a --- /dev/null +++ b/src/ReactiveUI/Bindings/Converter/StringToUriTypeConverter.cs @@ -0,0 +1,29 @@ +// 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; + +/// +/// Converts to using . +/// +public sealed class StringToUriTypeConverter : BindingTypeConverter +{ + /// + public override int GetAffinityForObjects() => 10; + + /// + public override bool TryConvert(string? from, object? conversionHint, [NotNullWhen(true)] out Uri? result) + { + if (from is null) + { + result = null; + return false; + } + + return Uri.TryCreate(from, UriKind.RelativeOrAbsolute, out result); + } +} diff --git a/src/ReactiveUI/Bindings/Converter/TimeOnlyToStringTypeConverter.cs b/src/ReactiveUI/Bindings/Converter/TimeOnlyToStringTypeConverter.cs new file mode 100644 index 0000000000..ed164c08e0 --- /dev/null +++ b/src/ReactiveUI/Bindings/Converter/TimeOnlyToStringTypeConverter.cs @@ -0,0 +1,26 @@ +// 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 NET6_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; + +namespace ReactiveUI; + +/// +/// Converts to . +/// +public sealed class TimeOnlyToStringTypeConverter : BindingTypeConverter +{ + /// + public override int GetAffinityForObjects() => 10; + + /// + public override bool TryConvert(TimeOnly from, object? conversionHint, [NotNullWhen(true)] out string? result) + { + result = from.ToString(); + return true; + } +} +#endif diff --git a/src/ReactiveUI/Bindings/Converter/TimeSpanToStringTypeConverter.cs b/src/ReactiveUI/Bindings/Converter/TimeSpanToStringTypeConverter.cs new file mode 100644 index 0000000000..81374856b5 --- /dev/null +++ b/src/ReactiveUI/Bindings/Converter/TimeSpanToStringTypeConverter.cs @@ -0,0 +1,24 @@ +// 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; + +/// +/// Converts to . +/// +public sealed class TimeSpanToStringTypeConverter : BindingTypeConverter +{ + /// + public override int GetAffinityForObjects() => 10; + + /// + public override bool TryConvert(TimeSpan from, object? conversionHint, [NotNullWhen(true)] out string? result) + { + result = from.ToString(); + return true; + } +} diff --git a/src/ReactiveUI/Bindings/Converter/UriToStringTypeConverter.cs b/src/ReactiveUI/Bindings/Converter/UriToStringTypeConverter.cs new file mode 100644 index 0000000000..c85459870b --- /dev/null +++ b/src/ReactiveUI/Bindings/Converter/UriToStringTypeConverter.cs @@ -0,0 +1,30 @@ +// 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; + +/// +/// Converts to . +/// +public sealed class UriToStringTypeConverter : BindingTypeConverter +{ + /// + public override int GetAffinityForObjects() => 10; + + /// + public override bool TryConvert(Uri? from, object? conversionHint, [NotNullWhen(true)] out string? result) + { + if (from is null) + { + result = null; + return false; + } + + result = from.ToString(); + return true; + } +} diff --git a/src/ReactiveUI/Bindings/IBindingFallbackConverter.cs b/src/ReactiveUI/Bindings/IBindingFallbackConverter.cs new file mode 100644 index 0000000000..d78687df5f --- /dev/null +++ b/src/ReactiveUI/Bindings/IBindingFallbackConverter.cs @@ -0,0 +1,87 @@ +// 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; + +/// +/// Represents a converter that can handle runtime type pairs not covered by typed converters. +/// Fallback converters are consulted only after all typed converters fail to match. +/// +/// +/// +/// Fallback converters exist for scenarios where conversion logic depends on runtime type +/// characteristics that cannot be expressed as compile-time generic pairs. +/// +/// +/// Common use cases: +/// +/// Component model type descriptors (reflection-based) +/// Platform-specific type conversions +/// IConvertible-based conversions +/// +/// +/// +/// Affinity Guidelines: +/// +/// 0 = Not supported +/// 1 = Last resort (ComponentModel/TypeDescriptor) +/// 3 = Broad runtime conversion (IConvertible/numeric widening) +/// 5 = Strong structural match (enum parsing, nullable unwrapping) +/// +/// Do not return affinity ≥10 to avoid competing with typed converters. +/// +/// +public interface IBindingFallbackConverter : IEnableLogger +{ + /// + /// Calculates affinity for the specified runtime type pair. + /// + /// The runtime source type. + /// The target type. + /// + /// Affinity score (0-5 range). Higher values indicate stronger match. + /// Return 0 or negative if this converter cannot handle the pair. + /// + /// + /// + /// This method MUST be: + /// + /// Pure (no side effects) + /// Fast (cache any expensive metadata) + /// Safe (no exceptions, no user code execution) + /// + /// + /// + /// This method is invoked during converter selection and may be called frequently. + /// Results should be cached internally where appropriate. + /// + /// + int GetAffinityForObjects([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type fromType, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type toType); + + /// + /// Attempts to convert the value to the target type. + /// + /// The runtime source type (guaranteed non-null). + /// The value to convert (guaranteed non-null). + /// The target type (guaranteed non-null). + /// Implementation-defined conversion hint (e.g., format string, culture). + /// + /// The converted value. Guaranteed non-null when this method returns . + /// + /// if conversion succeeded; otherwise, . + /// + /// + /// When this method returns , the parameter + /// is guaranteed to be non-null (modern nullability contract). + /// + /// + /// Null input handling is performed by the dispatch layer. This method will never receive + /// null as the parameter. + /// + /// + bool TryConvert([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type fromType, object from, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type toType, object? conversionHint, [NotNullWhen(true)] out object? result); +} diff --git a/src/ReactiveUI/Bindings/IBindingTypeConverter.cs b/src/ReactiveUI/Bindings/IBindingTypeConverter.cs index e5c19d764b..e26296508f 100644 --- a/src/ReactiveUI/Bindings/IBindingTypeConverter.cs +++ b/src/ReactiveUI/Bindings/IBindingTypeConverter.cs @@ -12,6 +12,16 @@ namespace ReactiveUI; /// public interface IBindingTypeConverter : IEnableLogger { + /// + /// Gets the source type supported by this converter. + /// + Type FromType { get; } + + /// + /// Gets the target type supported by this converter. + /// + Type ToType { get; } + /// /// Returns a positive integer when this class supports /// TryConvert for this particular Type. If the method isn't supported at @@ -19,20 +29,16 @@ public interface IBindingTypeConverter : IEnableLogger /// return a positive value, the host will use the one which returns /// the highest value. When in doubt, return '2' or '0'. /// - /// The source type to convert from. - /// The target type to convert to. /// A positive integer if TryConvert is supported, /// zero or a negative value otherwise. - int GetAffinityForObjects(Type fromType, Type toType); + int GetAffinityForObjects(); /// - /// Convert a given object to the specified type. + /// Attempts to convert using the typed implementation, exposed via an object-based shim. /// - /// The object to convert. - /// The type to coerce the object to. - /// An implementation-defined value, - /// usually to specify things like locale awareness. - /// An object that is of the type . - /// True if conversion was successful. - bool TryConvert(object? from, Type toType, object? conversionHint, out object? result); + /// The source value. + /// Implementation-defined hint. + /// The converted value. + /// if conversion succeeded; otherwise . + bool TryConvertTyped(object? from, object? conversionHint, out object? result); } diff --git a/src/ReactiveUI/Bindings/IBindingTypeConverter{TFrom,TTo}.cs b/src/ReactiveUI/Bindings/IBindingTypeConverter{TFrom,TTo}.cs new file mode 100644 index 0000000000..8ae7f48239 --- /dev/null +++ b/src/ReactiveUI/Bindings/IBindingTypeConverter{TFrom,TTo}.cs @@ -0,0 +1,48 @@ +// 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; + +/// +/// Generic type-safe interface for converting between specific types. +/// Implement this alongside for AOT-safe conversions. +/// +/// The source type to convert from. +/// The target type to convert to. +/// +/// +/// This interface provides compile-time type safety for type conversions, +/// enabling AOT-compatible code paths that avoid boxing and reflection. +/// +/// +/// The generic method +/// is preferred over the base interface's object-based method when types +/// are known at compile time. +/// +/// +public interface IBindingTypeConverter : IBindingTypeConverter +{ + /// + /// Convert a value to the target type in a type-safe manner. + /// + /// The value to convert. + /// Implementation-defined hint for conversion (e.g., format string, locale). + /// The converted value. Guaranteed non-null when this method returns . + /// if conversion succeeded; otherwise, . + /// + /// + /// This method is AOT-safe as all types are known at compile time. + /// No reflection or boxing is required for value types. + /// + /// + /// When this method returns , the parameter + /// is guaranteed to be non-null. Use to enable + /// nullable reference type checks. + /// + /// + bool TryConvert(TFrom? from, object? conversionHint, out TTo? result); +} diff --git a/src/ReactiveUI/Bindings/ISetMethodBindingConverter.cs b/src/ReactiveUI/Bindings/ISetMethodBindingConverter.cs index c9ae1c2e79..06702cbdfc 100644 --- a/src/ReactiveUI/Bindings/ISetMethodBindingConverter.cs +++ b/src/ReactiveUI/Bindings/ISetMethodBindingConverter.cs @@ -21,10 +21,6 @@ public interface ISetMethodBindingConverter : IEnableLogger /// The target type to convert to. /// A positive integer if PerformSet is supported, /// zero or a negative value otherwise. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("GetAffinityForObjects uses methods that require dynamic code generation")] - [RequiresUnreferencedCode("GetAffinityForObjects uses methods that may require unreferenced code")] -#endif int GetAffinityForObjects(Type? fromType, Type? toType); /// diff --git a/src/ReactiveUI/Bindings/Interaction/IInteractionBinderImplementation.cs b/src/ReactiveUI/Bindings/Interaction/IInteractionBinderImplementation.cs index 388d6021eb..c9ee166c19 100644 --- a/src/ReactiveUI/Bindings/Interaction/IInteractionBinderImplementation.cs +++ b/src/ReactiveUI/Bindings/Interaction/IInteractionBinderImplementation.cs @@ -22,10 +22,7 @@ public interface IInteractionBinderImplementation : IEnableLogger /// The interaction's input type. /// The interaction's output type. /// An object that when disposed, disconnects the binding. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("BindInteraction uses expression evaluation which requires dynamic code generation")] - [RequiresUnreferencedCode("BindInteraction uses expression evaluation which may require unreferenced code")] -#endif + [RequiresUnreferencedCode("Dynamic observation uses reflection over members that may be trimmed.")] IDisposable BindInteraction( TViewModel? viewModel, TView view, @@ -47,10 +44,7 @@ IDisposable BindInteraction( /// The interaction's output type. /// The interaction's signal type. /// An object that when disposed, disconnects the binding. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("BindInteraction uses expression evaluation which requires dynamic code generation")] - [RequiresUnreferencedCode("BindInteraction uses expression evaluation which may require unreferenced code")] -#endif + [RequiresUnreferencedCode("Dynamic observation uses reflection over members that may be trimmed.")] IDisposable BindInteraction( TViewModel? viewModel, TView view, diff --git a/src/ReactiveUI/Bindings/Interaction/InteractionBinderImplementation.cs b/src/ReactiveUI/Bindings/Interaction/InteractionBinderImplementation.cs index cdad94e97e..5d67d4609d 100644 --- a/src/ReactiveUI/Bindings/Interaction/InteractionBinderImplementation.cs +++ b/src/ReactiveUI/Bindings/Interaction/InteractionBinderImplementation.cs @@ -11,10 +11,7 @@ namespace ReactiveUI; public class InteractionBinderImplementation : IInteractionBinderImplementation { /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("The handler may use serialization which requires dynamic code generation")] - [RequiresUnreferencedCode("The handler may use serialization which may require unreferenced code")] -#endif + [RequiresUnreferencedCode("Dynamic observation uses reflection over members that may be trimmed.")] public IDisposable BindInteraction( TViewModel? viewModel, TView view, @@ -44,10 +41,7 @@ public IDisposable BindInteraction( } /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("The handler may use serialization which requires dynamic code generation")] - [RequiresUnreferencedCode("The handler may use serialization which may require unreferenced code")] -#endif + [RequiresUnreferencedCode("Dynamic observation uses reflection over members that may be trimmed.")] public IDisposable BindInteraction( TViewModel? viewModel, TView view, diff --git a/src/ReactiveUI/Bindings/Interaction/InteractionBindingMixins.cs b/src/ReactiveUI/Bindings/Interaction/InteractionBindingMixins.cs index 6443b78375..9690138ed7 100644 --- a/src/ReactiveUI/Bindings/Interaction/InteractionBindingMixins.cs +++ b/src/ReactiveUI/Bindings/Interaction/InteractionBindingMixins.cs @@ -30,8 +30,6 @@ public static class InteractionBindingMixins { private static readonly InteractionBinderImplementation _binderImplementation = new(); - static InteractionBindingMixins() => RxApp.EnsureInitialized(); - /// /// Binds the on a ViewModel to the specified handler. /// @@ -44,10 +42,7 @@ public static class InteractionBindingMixins /// The interaction's input type. /// The interaction's output type. /// An object that when disposed, disconnects the binding. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("BindInteraction uses expression binding which requires dynamic code generation")] - [RequiresUnreferencedCode("BindInteraction uses expression binding which may require unreferenced code")] -#endif + [RequiresUnreferencedCode("Dynamic observation uses reflection over members that may be trimmed.")] public static IDisposable BindInteraction( this TView view, TViewModel? viewModel, @@ -74,10 +69,7 @@ public static IDisposable BindInteraction( /// The interaction's output type. /// The interaction's signal type. /// An object that when disposed, disconnects the binding. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("BindInteraction uses expression binding which requires dynamic code generation")] - [RequiresUnreferencedCode("BindInteraction uses expression binding which may require unreferenced code")] -#endif + [RequiresUnreferencedCode("Dynamic observation uses reflection over members that may be trimmed.")] public static IDisposable BindInteraction( this TView view, TViewModel? viewModel, diff --git a/src/ReactiveUI/Bindings/Property/IPropertyBinderImplementation.cs b/src/ReactiveUI/Bindings/Property/IPropertyBinderImplementation.cs index bff02b55e9..436a93c377 100644 --- a/src/ReactiveUI/Bindings/Property/IPropertyBinderImplementation.cs +++ b/src/ReactiveUI/Bindings/Property/IPropertyBinderImplementation.cs @@ -47,10 +47,6 @@ public interface IPropertyBinderImplementation : IEnableLogger /// An instance of that, when disposed, /// disconnects the binding. /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("Bind uses expression trees which require dynamic code generation")] - [RequiresUnreferencedCode("Bind uses expression trees which may require unreferenced code")] -#endif IReactiveBinding Bind( TViewModel? viewModel, TView view, @@ -98,10 +94,6 @@ public interface IPropertyBinderImplementation : IEnableLogger /// An instance of that, when disposed, /// disconnects the binding. /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("Bind uses expression trees which require dynamic code generation")] - [RequiresUnreferencedCode("Bind uses expression trees which may require unreferenced code")] -#endif IReactiveBinding Bind( TViewModel? viewModel, TView view, @@ -153,10 +145,6 @@ public interface IPropertyBinderImplementation : IEnableLogger /// /// There is no registered converter from to . /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("OneWayBind uses expression trees which require dynamic code generation")] - [RequiresUnreferencedCode("OneWayBind uses expression trees which may require unreferenced code")] -#endif IReactiveBinding OneWayBind( TViewModel? viewModel, TView view, @@ -198,10 +186,6 @@ IReactiveBinding OneWayBind( /// An instance of that, when disposed, /// disconnects the binding. /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("OneWayBind uses expression trees which require dynamic code generation")] - [RequiresUnreferencedCode("OneWayBind uses expression trees which may require unreferenced code")] -#endif IReactiveBinding OneWayBind( TViewModel? viewModel, TView view, @@ -235,10 +219,6 @@ IReactiveBinding OneWayBind( /// viewModel to view property. /// /// An object that when disposed, disconnects the binding. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("BindTo uses expression trees which require dynamic code generation")] - [RequiresUnreferencedCode("BindTo uses expression trees which may require unreferenced code")] -#endif IDisposable BindTo( IObservable observedChange, TTarget? target, diff --git a/src/ReactiveUI/Bindings/Property/PropertyBinderImplementation.cs b/src/ReactiveUI/Bindings/Property/PropertyBinderImplementation.cs index 3b0991ad27..214998760a 100644 --- a/src/ReactiveUI/Bindings/Property/PropertyBinderImplementation.cs +++ b/src/ReactiveUI/Bindings/Property/PropertyBinderImplementation.cs @@ -3,45 +3,68 @@ // 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.Globalization; using System.Reflection; +using System.Runtime.CompilerServices; namespace ReactiveUI; /// /// Provides methods to bind properties to observables. /// +/// +/// +/// This implementation uses dynamic expression parsing and reflection to support arbitrary property chains and dynamic +/// observation (WhenAnyDynamic). As such, it is not trimming- or AOT-friendly without additional preservation. +/// +/// +/// Trimming/AOT: this type is annotated because it performs reflection over runtime types and expression graphs that may +/// be trimmed, and may invoke members that are annotated for dynamic code. +/// +/// +[RequiresUnreferencedCode("Uses reflection over runtime types and expression graphs which may be trimmed.")] +[RequiresDynamicCode("Uses dynamic binding paths which may require runtime code generation or reflection-based invocation.")] public class PropertyBinderImplementation : IPropertyBinderImplementation { - private static readonly MemoizingMRUCache<(Type fromType, Type toType), IBindingTypeConverter?> _typeConverterCache = new( - (types, _) => AppLocator.Current.GetServices() - .Aggregate((currentAffinity: -1, currentBinding: default(IBindingTypeConverter)), (acc, x) => - { - var score = x?.GetAffinityForObjects(types.fromType, types.toType) ?? -1; - return score > acc.currentAffinity && score > 0 ? (score, x) : acc; - }).currentBinding, - RxApp.SmallCacheLimit); - - [SuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "Marked as Preserve")] - [SuppressMessage("Trimming", "IL2026:Calling members annotated with 'RequiresUnreferencedCodeAttribute' may break functionality when trimming application code.", Justification = "Marked as Preserve")] - private static readonly MemoizingMRUCache<(Type? fromType, Type? toType), ISetMethodBindingConverter?> _setMethodCache = new( - (type, _) => AppLocator.Current.GetServices() - .Aggregate((currentAffinity: -1, currentBinding: default(ISetMethodBindingConverter)), (acc, x) => - { - var score = x.GetAffinityForObjects(type.fromType, type.toType); - return score > acc.currentAffinity && score > 0 ? (score, x) : acc; - }).currentBinding, - RxApp.SmallCacheLimit); - - static PropertyBinderImplementation() => RxApp.EnsureInitialized(); - + /// + /// Caches the best converter for a given (fromType, toType) pair. + /// + /// + /// + /// The cached value is either an (preferred) or an . + /// + /// + /// A is used to avoid MRU eviction churn + /// and to keep lookup fast and allocation-free on the hot path. + /// + /// + private static readonly System.Collections.Concurrent.ConcurrentDictionary<(Type fromType, Type toType), object?> _typeConverterCache = new(); + + /// + /// Caches the best set-method converter for a given (fromType, toType) pair. + /// + /// + /// The cached value is the selected instance, or if none matches. + /// + private static readonly System.Collections.Concurrent.ConcurrentDictionary<(Type fromType, Type? toType), ISetMethodBindingConverter?> _setMethodCache = new(); + + /// + /// Initializes static members of the class. + /// Ensures ReactiveUI static initialization is performed before bindings are used. + /// + + /// + /// Represents a converter that attempts conversion and returns success via an parameter. + /// + /// The input value type. + /// The output value type. + /// The input value. + /// The converted output value. + /// if conversion succeeded; otherwise . private delegate bool OutFunc(T1 t1, out T2 t2); /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("The handler may use serialization which requires dynamic code generation")] - [RequiresUnreferencedCode("The handler may use serialization which may require unreferenced code")] -#endif public IReactiveBinding Bind( TViewModel? viewModel, TView view, @@ -57,18 +80,26 @@ public class PropertyBinderImplementation : IPropertyBinderImplementation { ArgumentExceptionHelper.ThrowIfNull(vmProperty); ArgumentExceptionHelper.ThrowIfNull(viewProperty); - var vmToViewConverter = vmToViewConverterOverride ?? GetConverterForTypes(typeof(TVMProp), typeof(TVProp)); - var viewToVMConverter = viewToVMConverterOverride ?? GetConverterForTypes(typeof(TVProp), typeof(TVMProp)); - if (vmToViewConverter is null || viewToVMConverter is null) + // Converter selection is a hot-path concern across many binds; keep it cached and loop-based. + var vmToViewConverterObj = vmToViewConverterOverride ?? GetConverterForTypes(typeof(TVMProp), typeof(TVProp)); + var viewToVMConverterObj = viewToVMConverterOverride ?? GetConverterForTypes(typeof(TVProp), typeof(TVMProp)); + + if (vmToViewConverterObj is null || viewToVMConverterObj is null) { throw new ArgumentException( - $"Can't two-way convert between {typeof(TVMProp)} and {typeof(TVProp)}. To fix this, register a IBindingTypeConverter or call the version with the converter Func."); + $"Can't two-way convert between {typeof(TVMProp)} and {typeof(TVProp)}. To fix this, register a IBindingTypeConverter or call the version with the converter Func."); } bool VmToViewFunc(TVMProp? vmValue, out TVProp vValue) { - var result = vmToViewConverter.TryConvert(vmValue, typeof(TVProp), conversionHint, out var tmp); + var result = BindingTypeConverterDispatch.TryConvertAny( + vmToViewConverterObj, + typeof(TVMProp), + vmValue, + typeof(TVProp), + conversionHint, + out var tmp); vValue = result && tmp is not null ? (TVProp)tmp : default!; return result; @@ -76,7 +107,13 @@ bool VmToViewFunc(TVMProp? vmValue, out TVProp vValue) bool ViewToVmFunc(TVProp vValue, out TVMProp? vmValue) { - var result = viewToVMConverter.TryConvert(vValue, typeof(TVMProp?), conversionHint, out var tmp); + var result = BindingTypeConverterDispatch.TryConvertAny( + viewToVMConverterObj, + typeof(TVProp), + vValue, + typeof(TVMProp?), + conversionHint, + out var tmp); vmValue = result && tmp is not null ? (TVMProp?)tmp : default; return result; @@ -86,10 +123,6 @@ bool ViewToVmFunc(TVProp vValue, out TVMProp? vmValue) } /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("The handler may use serialization which requires dynamic code generation")] - [RequiresUnreferencedCode("The handler may use serialization which may require unreferenced code")] -#endif public IReactiveBinding Bind( TViewModel? viewModel, TView view, @@ -123,10 +156,6 @@ bool ViewToVmFunc(TVProp vValue, out TVMProp? vmValue) } /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("The handler may use serialization which requires dynamic code generation")] - [RequiresUnreferencedCode("The handler may use serialization which may require unreferenced code")] -#endif public IReactiveBinding OneWayBind( TViewModel? viewModel, TView view, @@ -143,15 +172,29 @@ public IReactiveBinding OneWayBind(view, viewExpression, vmExpression, Observable.Empty(), BindingDirection.OneWay, Disposable.Empty); } - var source = Reflection.ViewModelWhenAnyValue(viewModel, view, vmExpression) - .SelectMany(x => !converter.TryConvert(x, viewType, conversionHint, out var tmp) ? Observable.Empty : Observable.Return(tmp)); + var source = + Reflection.ViewModelWhenAnyValue(viewModel, view, vmExpression) + .SelectMany(x => + !BindingTypeConverterDispatch.TryConvertAny( + converterObj, + x?.GetType() ?? typeof(object), + x, + viewType, + conversionHint, + out var tmp) + ? Observable.Empty + : Observable.Return(tmp)); var (disposable, obs) = BindToDirect(source, view, viewExpression); @@ -159,10 +202,6 @@ public IReactiveBinding OneWayBind -#if NET6_0_OR_GREATER - [RequiresDynamicCode("The handler may use serialization which requires dynamic code generation")] - [RequiresUnreferencedCode("The handler may use serialization which may require unreferenced code")] -#endif public IReactiveBinding OneWayBind( TViewModel? viewModel, TView view, @@ -177,6 +216,7 @@ public IReactiveBinding OneWayBind( var vmExpression = Reflection.Rewrite(vmProperty.Body); var viewExpression = Reflection.Rewrite(viewProperty.Body); + var ret = EvalBindingHooks(viewModel, view, vmExpression, viewExpression, BindingDirection.OneWay); if (!ret) { @@ -191,10 +231,6 @@ public IReactiveBinding OneWayBind( } /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("The handler may use serialization which requires dynamic code generation")] - [RequiresUnreferencedCode("The handler may use serialization which may require unreferenced code")] -#endif public IDisposable BindTo( IObservable observedChange, TTarget? target, @@ -214,21 +250,184 @@ public IDisposable BindTo( return Disposable.Empty; } - var converter = (vmToViewConverterOverride ?? GetConverterForTypes(typeof(TValue), typeof(TTValue?))) ?? throw new ArgumentException($"Can't convert {typeof(TValue)} to {typeof(TTValue)}. To fix this, register a IBindingTypeConverter"); - var source = observedChange.SelectMany(x => !converter.TryConvert(x, typeof(TTValue?), conversionHint, out var tmp) ? Observable.Empty : Observable.Return(tmp)); + var converterObj = + (vmToViewConverterOverride ?? GetConverterForTypes(typeof(TValue), typeof(TTValue?))) ?? + throw new ArgumentException($"Can't convert {typeof(TValue)} to {typeof(TTValue)}. To fix this, register a IBindingTypeConverter"); + + var source = + observedChange.SelectMany(x => + !BindingTypeConverterDispatch.TryConvertAny( + converterObj, + typeof(TValue), + x, + typeof(TTValue?), + conversionHint, + out var tmp) + ? Observable.Empty + : Observable.Return(tmp)); var (disposable, _) = BindToDirect(source, target!, viewExpression); return disposable; } - internal static IBindingTypeConverter? GetConverterForTypes(Type lhs, Type rhs) => - _typeConverterCache.Get((lhs, rhs)); + /// + /// Gets a converter for the specified type pair. + /// + /// The source type. + /// The target type. + /// + /// A converter instance (either or ) + /// if one is registered for the specified types; otherwise, . + /// + /// + /// This method caches results per type-pair. Typed converters (exact pair match) are preferred over fallback converters. + /// + internal static object? GetConverterForTypes(Type lhs, Type rhs) => + _typeConverterCache.GetOrAdd((lhs, rhs), static key => ResolveBestConverter(key.fromType, key.toType)); + + /// + /// Resolves the best converter for a given type pair. + /// + /// The source type. + /// The target type. + /// + /// The selected converter (typed preferred), or if none matches. + /// + private static object? ResolveBestConverter(Type fromType, Type toType) + { + // Phase 1: exact-pair typed converters by affinity. + var typed = AppLocator.Current.GetServices(); + var bestTypedScore = -1; + IBindingTypeConverter? bestTyped = null; + + foreach (var c in typed) + { + if (c is null || c.FromType != fromType || c.ToType != toType) + { + continue; + } -#if NET6_0_OR_GREATER - [RequiresDynamicCode("Type conversion requires dynamic code generation")] - [RequiresUnreferencedCode("Type conversion may reference types that could be trimmed")] -#endif + var score = c.GetAffinityForObjects(); + if (score > bestTypedScore && score > 0) + { + bestTypedScore = score; + bestTyped = c; + } + } + + if (bestTyped is not null) + { + return bestTyped; + } + + // Phase 2: fallback converters by affinity. + var fallbacks = AppLocator.Current.GetServices(); + var bestFallbackScore = -1; + IBindingFallbackConverter? bestFallback = null; + + foreach (var c in fallbacks) + { + if (c is null) + { + continue; + } + + var score = c.GetAffinityForObjects(fromType, toType); + if (score > bestFallbackScore && score > 0) + { + bestFallbackScore = score; + bestFallback = c; + } + } + + return bestFallback; + } + + /// + /// Converts an expression chain to a materialized array using collection expression syntax. + /// + /// The expression whose chain should be materialized. + /// + /// An array of expressions representing the chain, or when the chain cannot be obtained. + /// + private static Expression[]? GetExpressionChainArrayOrNull(Expression? expression) => + expression is null ? null : [.. expression.GetExpressionChain()]; + + /// + /// Creates the default value instance for used by the "replay on host changes" logic. + /// + /// The member type. + /// + /// A boxed default value for value types, or for reference types. + /// + private static object? CreateDefaultValueForType(Type type) + { + ArgumentExceptionHelper.ThrowIfNull(type); + + return type.GetTypeInfo().IsValueType ? Activator.CreateInstance(type) : null; + } + + /// + /// Determines whether values should be replayed when the host changes. + /// + /// The host expression chain. + /// + /// when replay-on-host-change behavior should be enabled; otherwise . + /// + /// + /// This preserves the original behavior: when the chain includes IViewFor.ViewModel, replay is disabled. + /// + private static bool ShouldReplayOnHostChanges(Expression[]? hostExpressionChain) + { + if (hostExpressionChain is null) + { + return true; + } + + for (var i = 0; i < hostExpressionChain.Length; i++) + { + if (hostExpressionChain[i] is MemberExpression member && + string.Equals(member.Member.Name, nameof(IViewFor.ViewModel), StringComparison.Ordinal)) + { + return false; + } + } + + return true; + } + + /// + /// Resolves the current host object for the binding by evaluating the host expression chain. + /// + /// The root binding target. + /// The expression chain used to compute the host. + /// + /// The resolved host object, or if the chain cannot be evaluated. + /// + private static object? ResolveHostFromChainOrNull(object target, Expression[] hostExpressionChain) + { + ArgumentExceptionHelper.ThrowIfNull(target); + ArgumentExceptionHelper.ThrowIfNull(hostExpressionChain); + + object? host = target; + + if (!Reflection.TryGetValueForPropertyChain(out host, host, hostExpressionChain)) + { + return null; + } + + return host; + } + + /// + /// Gets the set-method conversion shim for an assignment into a target member. + /// + /// The runtime type of the inbound value. + /// The target member type. + /// + /// A conversion function used by set-method converters, or if no converter is applicable. + /// private static Func? GetSetConverter(Type? fromType, Type? targetType) { if (fromType is null) @@ -236,181 +435,332 @@ public IDisposable BindTo( return null; } - var setter = _setMethodCache.Get((fromType, targetType)); - return setter is null ? null : setter.PerformSet; + var converter = _setMethodCache.GetOrAdd( + (fromType, targetType), + static key => ResolveBestSetMethodConverter(key.fromType, key.toType)); + + if (converter is null) + { + return null; + } + + // Adapt the converter's contract to the local call shape expected by SetThenGet. + // Note: do not store this delegate in the cache; the cache stores the converter instance. + return (currentValue, newValue, indexParameters) => converter.PerformSet(currentValue, newValue, indexParameters); } -#if NET6_0_OR_GREATER - [RequiresDynamicCode("Property binding requires dynamic code generation")] - [RequiresUnreferencedCode("Property binding may reference members that could be trimmed")] -#endif - private (IDisposable disposable, IObservable value) BindToDirect( - IObservable changeObservable, - TTarget target, - Expression viewExpression) - where TTarget : class + /// + /// Resolves the best for a given pair. + /// + /// The inbound runtime type. + /// The target type. + /// The selected converter, or if none matches. + private static ISetMethodBindingConverter? ResolveBestSetMethodConverter(Type fromType, Type? toType) { - var memberInfo = viewExpression.GetMemberInfo(); + var converters = AppLocator.Current.GetServices(); - var defaultSetter = Reflection.GetValueSetterOrThrow(memberInfo); - var defaultGetter = Reflection.GetValueFetcherOrThrow(memberInfo); - var synchronizedChanges = changeObservable.Synchronize(); + var bestScore = -1; + ISetMethodBindingConverter? best = null; - (bool shouldEmit, object? value) SetThenGet(object? paramTarget, object? paramValue, object?[]? paramParams) + foreach (var c in converters) { - var converter = GetSetConverter(paramValue?.GetType(), viewExpression.Type); + if (c is null) + { + continue; + } - if (defaultGetter is null) + var score = c.GetAffinityForObjects(fromType, toType); + if (score > bestScore && score > 0) { - throw new InvalidOperationException($"{nameof(defaultGetter)} was not found."); + bestScore = score; + best = c; } + } + + return best; + } + + /// + /// Determines whether is a direct member access on the root parameter. + /// + /// The view expression to inspect. + /// + /// if the member is directly on the root parameter; otherwise . + /// + private static bool IsDirectMemberOnRootParameter(Expression viewExpression) => + viewExpression.GetParent()?.NodeType == ExpressionType.Parameter; + + /// + /// Creates an observable that applies changes directly to when the member is directly on the root parameter. + /// + /// The target object type. + /// The value type emitted by the returned observable. + /// The change element type. + /// The synchronized change stream. + /// The target object. + /// The view expression describing the member to set. + /// The set-then-get delegate. + /// An observable sequence of values that were effectively set. + private static IObservable CreateDirectSetObservable( + IObservable synchronizedChanges, + TTarget target, + Expression viewExpression, + Func setThenGet) + where TTarget : class + { + ArgumentExceptionHelper.ThrowIfNull(synchronizedChanges); + ArgumentExceptionHelper.ThrowIfNull(target); + ArgumentExceptionHelper.ThrowIfNull(viewExpression); + ArgumentExceptionHelper.ThrowIfNull(setThenGet); + + var arguments = viewExpression.GetArgumentsArray(); + + return synchronizedChanges.Select(value => setThenGet(target, value, arguments)) + .Where(result => result.shouldEmit) + .Select(result => result.value is null ? default! : (TValue)result.value); + } + + /// + /// Creates the core "set then get" function that applies a value to a target member and returns whether a value should be emitted. + /// + /// The view expression describing the member to set. + /// The compiled getter for the member. + /// The compiled setter for the member. + /// + /// A delegate that sets and then gets the value, returning whether the value should be emitted and the resulting value. + /// + private static Func CreateSetThenGet( + Expression viewExpression, + Func getter, + Action setter) + { + ArgumentExceptionHelper.ThrowIfNull(viewExpression); + ArgumentExceptionHelper.ThrowIfNull(getter); + ArgumentExceptionHelper.ThrowIfNull(setter); + + return (paramTarget, paramValue, paramParams) => + { + var converter = GetSetConverter(paramValue?.GetType(), viewExpression.Type); if (converter is null) { - var currentValue = defaultGetter(paramTarget, paramParams); - var shouldUpdate = !EqualityComparer.Default.Equals(currentValue, paramValue); - - if (!shouldUpdate) + var currentValue = getter(paramTarget, paramParams); + if (EqualityComparer.Default.Equals(currentValue, paramValue)) { return (false, currentValue); } - defaultSetter?.Invoke(paramTarget, paramValue, paramParams); - return (true, defaultGetter(paramTarget, paramParams)); + setter(paramTarget, paramValue, paramParams); + return (true, getter(paramTarget, paramParams)); } - var value = defaultGetter(paramTarget, paramParams); - var convertedValue = converter(value, paramValue, paramParams); - var shouldEmit = !EqualityComparer.Default.Equals(value, convertedValue); + var existing = getter(paramTarget, paramParams); + var converted = converter(existing, paramValue, paramParams); + var shouldEmit = !EqualityComparer.Default.Equals(existing, converted); + return (shouldEmit, converted); + }; + } - return (shouldEmit, convertedValue); - } + /// + /// Creates an observable that applies changes to a member whose host is obtained via a property chain. + /// + /// The root target object type. + /// The value type emitted by the returned observable. + /// The change element type. + /// The synchronized change stream. + /// The root target object. + /// The view expression describing the member to set. + /// The set-then-get delegate. + /// The compiled getter for the member. + /// An observable sequence of values that were effectively set. + /// Thrown when the host expression cannot be obtained. + private static IObservable CreateChainedSetObservable( + IObservable synchronizedChanges, + TTarget target, + Expression viewExpression, + Func setThenGet, + Func getter) + where TTarget : class + { + ArgumentExceptionHelper.ThrowIfNull(synchronizedChanges); + ArgumentExceptionHelper.ThrowIfNull(target); + ArgumentExceptionHelper.ThrowIfNull(viewExpression); + ArgumentExceptionHelper.ThrowIfNull(setThenGet); + ArgumentExceptionHelper.ThrowIfNull(getter); - IObservable setObservable; + var hostExpression = viewExpression.GetParent() ?? throw new InvalidOperationException("Host expression was not found."); + var hostChain = GetExpressionChainArrayOrNull(hostExpression); + var hostChanges = target.WhenAnyDynamic(hostExpression, x => x.Value).Synchronize(); + var arguments = viewExpression.GetArgumentsArray(); + var propertyDefaultValue = CreateDefaultValueForType(viewExpression.Type); - if (viewExpression.GetParent()?.NodeType == ExpressionType.Parameter) - { - var arguments = viewExpression.GetArgumentsArray(); - setObservable = synchronizedChanges - .Select(value => SetThenGet(target, value, arguments)) - .Where(result => result.shouldEmit) - .Select(result => result.value is null ? default! : (TValue)result.value); - } - else + var shouldReplayOnHostChanges = ShouldReplayOnHostChanges(hostChain); + + return Observable.Create(observer => { - var hostExpression = viewExpression.GetParent(); - var hostExpressionChain = hostExpression?.GetExpressionChain()?.ToArray(); - var hostChanges = target.WhenAnyDynamic(hostExpression, x => x.Value).Synchronize(); - var arguments = viewExpression.GetArgumentsArray(); - var propertyDefaultValue = viewExpression.Type.GetTypeInfo().IsValueType ? Activator.CreateInstance(viewExpression.Type) : null; - var shouldReplayOnHostChanges = hostExpressionChain? - .OfType() - .Any(static expression => string.Equals(expression.Member.Name, nameof(IViewFor.ViewModel), StringComparison.Ordinal)) != true; - - setObservable = Observable.Create(observer => + ArgumentExceptionHelper.ThrowIfNull(observer); + + object? latestHost = null; + object? currentHost = null; + object? lastObservedValue = null; + var hasObservedValue = false; + + bool HostPropertyEqualsDefault(object? host) { - object? latestHost = null; - object? lastObservedValue = null; - object? currentHost = null; - var hasObservedValue = false; + if (host is null) + { + return false; + } - bool HostPropertyEqualsDefault(object? host) + var currentValue = getter(host, arguments); + return EqualityComparer.Default.Equals(currentValue, propertyDefaultValue); + } + + void ApplyValueToHost(object? host, object? value) + { + if (host is null || !hasObservedValue) { - if (host is null || defaultGetter is null) - { - return false; - } + return; + } - var currentValue = defaultGetter(host, arguments); - return EqualityComparer.Default.Equals(currentValue, propertyDefaultValue); + var (shouldEmit, result) = setThenGet(host, value, arguments); + if (!shouldEmit) + { + return; } - void ApplyValueToHost(object? host, object? value) + observer.OnNext(result is null ? default! : (TValue)result); + } + + var hostDisposable = hostChanges.Subscribe( + hostValue => { - if (host is null || !hasObservedValue) + latestHost = hostValue; + + if (ReferenceEquals(hostValue, currentHost)) { return; } - var (shouldEmit, result) = SetThenGet(host, value, arguments); - if (!shouldEmit) + currentHost = hostValue; + + if (!shouldReplayOnHostChanges || !hasObservedValue || !HostPropertyEqualsDefault(hostValue)) { return; } - observer.OnNext(result is null ? default! : (TValue)result); - } + ApplyValueToHost(hostValue, lastObservedValue); + }, + observer.OnError); - var hostDisposable = hostChanges - .Subscribe( - hostValue => - { - latestHost = hostValue; - - if (ReferenceEquals(hostValue, currentHost)) - { - return; - } - - currentHost = hostValue; - - if (!shouldReplayOnHostChanges || !hasObservedValue || !HostPropertyEqualsDefault(hostValue)) - { - return; - } - - ApplyValueToHost(hostValue, lastObservedValue); - }, - observer.OnError); - - var changeDisposable = synchronizedChanges - .Subscribe( - value => - { - hasObservedValue = true; - lastObservedValue = value; - - var host = latestHost; - if (hostExpressionChain is not null) - { - if (!Reflection.TryGetValueForPropertyChain(out host, target, hostExpressionChain)) - { - host = null; - } - - latestHost = host; - } - - if (host is null) - { - return; - } - - ApplyValueToHost(host, value); - }, - observer.OnError); - - return new CompositeDisposable(hostDisposable, changeDisposable); - }); - } + var changeDisposable = synchronizedChanges.Subscribe( + value => + { + hasObservedValue = true; + lastObservedValue = value; - return (setObservable.Subscribe(_ => { }, ex => - { - this.Log().Error(ex, $"{viewExpression} Binding received an Exception!"); - if (ex.InnerException is null) + var host = latestHost; + + if (hostChain is not null) + { + host = ResolveHostFromChainOrNull(target, hostChain); + latestHost = host; + } + + if (host is null) + { + return; + } + + ApplyValueToHost(host, value); + }, + observer.OnError); + + return new CompositeDisposable(hostDisposable, changeDisposable); + }); + } + + /// + /// Binds an observable to a target member directly using compiled accessors. + /// + /// The target object type. + /// The value type emitted by the returned observable. + /// The element type produced by . + /// The observable providing values to set. + /// The target object. + /// The rewritten member expression describing the target member. + /// + /// A tuple containing the subscription and an observable sequence of values that were effectively set. + /// + /// Thrown when a required getter cannot be resolved. + private (IDisposable disposable, IObservable value) BindToDirect( + IObservable changeObservable, + TTarget target, + Expression viewExpression) + where TTarget : class + { + ArgumentExceptionHelper.ThrowIfNull(changeObservable); + ArgumentExceptionHelper.ThrowIfNull(target); + ArgumentExceptionHelper.ThrowIfNull(viewExpression); + + var memberInfo = viewExpression.GetMemberInfo(); + + var setter = Reflection.GetValueSetterOrThrow(memberInfo); + var getter = Reflection.GetValueFetcherOrThrow(memberInfo) ?? throw new InvalidOperationException("getter was not found."); + + var synchronizedChanges = changeObservable.Synchronize(); + var setThenGet = CreateSetThenGet(viewExpression, getter, setter); + + IObservable setObservable = + IsDirectMemberOnRootParameter(viewExpression) + ? CreateDirectSetObservable(synchronizedChanges, target, viewExpression, setThenGet) + : CreateChainedSetObservable(synchronizedChanges, target, viewExpression, setThenGet, getter); + + var subscription = SubscribeWithBindingErrorHandling(setObservable, viewExpression); + + return (subscription, setObservable); + } + + /// + /// Subscribes to and applies binding error handling consistent with the binding engine. + /// + /// The element type of the observable. + /// The observable to subscribe to. + /// The view expression used for diagnostic messages. + /// The subscription disposable. + /// + /// Thrown when the binding receives an exception with an inner exception, matching legacy behavior. + /// + private IDisposable SubscribeWithBindingErrorHandling(IObservable setObservable, Expression viewExpression) + { + ArgumentExceptionHelper.ThrowIfNull(setObservable); + ArgumentExceptionHelper.ThrowIfNull(viewExpression); + + return setObservable.Subscribe( + _ => { }, + ex => { - return; - } + this.Log().Error(ex, $"{viewExpression} Binding received an Exception!"); + if (ex.InnerException is null) + { + return; + } - // If the exception is not null, we throw it wrapped in a TargetInvocationException. - throw new TargetInvocationException($"{viewExpression} Binding received an Exception!", ex.InnerException); - }), setObservable); + throw new TargetInvocationException($"{viewExpression} Binding received an Exception!", ex.InnerException); + }); } -#if NET6_0_OR_GREATER - [RequiresDynamicCode("Property binding requires dynamic code generation")] - [RequiresUnreferencedCode("Property binding may reference members that could be trimmed")] -#endif + /// + /// Evaluates registered instances to determine whether a binding should be created. + /// + /// The view model type. + /// The view type. + /// The view model instance (may be ). + /// The view instance. + /// The rewritten view model expression. + /// The rewritten view expression. + /// The binding direction. + /// if the binding should proceed; otherwise . private bool EvalBindingHooks(TViewModel? viewModel, TView view, Expression vmExpression, Expression viewExpression, BindingDirection direction) where TViewModel : class where TView : class, IViewFor @@ -418,25 +768,41 @@ private bool EvalBindingHooks(TViewModel? viewModel, TView vi var hooks = AppLocator.Current.GetServices(); ArgumentExceptionHelper.ThrowIfNull(view); + // Compile chains once for hook evaluation. + var vmChainGetter = vmExpression != null + ? new Reflection.CompiledPropertyChain([.. vmExpression.GetExpressionChain()]) + : null; + var viewChainGetter = new Reflection.CompiledPropertyChain([.. viewExpression.GetExpressionChain()]); + Func[]> vmFetcher = vmExpression is not null ? (() => - { - Reflection.TryGetAllValuesForPropertyChain(out var fetchedValues, viewModel, vmExpression.GetExpressionChain()); - return fetchedValues; - }) - : (() => - [ - new ObservedChange(null!, null, viewModel) - ]); - - var vFetcher = new Func[]>(() => + { + vmChainGetter!.TryGetAllValues(viewModel, out var fetchedValues); + return fetchedValues; + }) + : (() => [new ObservedChange(null!, null, viewModel)]); + + Func[]> vFetcher = () => { - Reflection.TryGetAllValuesForPropertyChain(out var fetchedValues, view, viewExpression.GetExpressionChain()); + viewChainGetter.TryGetAllValues(view, out var fetchedValues); return fetchedValues; - }); + }; - var shouldBind = hooks.Aggregate(true, (acc, x) => - acc && x.ExecuteHook(viewModel, view!, vmFetcher!, vFetcher!, direction)); + // Replace Aggregate with a loop to avoid enumerator overhead and closures. + var shouldBind = true; + foreach (var hook in hooks) + { + if (hook is null) + { + continue; + } + + if (!hook.ExecuteHook(viewModel, view!, vmFetcher!, vFetcher!, direction)) + { + shouldBind = false; + break; + } + } if (!shouldBind) { @@ -448,10 +814,6 @@ private bool EvalBindingHooks(TViewModel? viewModel, TView vi return shouldBind; } -#if NET6_0_OR_GREATER - [RequiresDynamicCode("Property binding requires dynamic code generation")] - [RequiresUnreferencedCode("Property binding may reference members that could be trimmed")] -#endif private ReactiveBinding BindImpl( TViewModel? viewModel, TView view, @@ -465,34 +827,48 @@ private bool EvalBindingHooks(TViewModel? viewModel, TView vi where TView : class, IViewFor { ArgumentExceptionHelper.ThrowIfNull(vmProperty); - ArgumentExceptionHelper.ThrowIfNull(viewProperty); var signalInitialUpdate = new Subject(); var vmExpression = Reflection.Rewrite(vmProperty.Body); var viewExpression = Reflection.Rewrite(viewProperty.Body); + // Pre-compile expression chains ONCE at binding setup time. + // This is the "reflection boundary". + Expression[] vmExpressionChainArray = [.. vmExpression.GetExpressionChain()]; + Expression[] viewExpressionChainArray = [.. viewExpression.GetExpressionChain()]; + + // VM chain expects root = view.ViewModel (object?). + var vmChainGetter = new Reflection.CompiledPropertyChain(vmExpressionChainArray); + + // View chain expects root = view (TView). + var viewChainGetter = new Reflection.CompiledPropertyChain(viewExpressionChainArray); + + // Setters for two-way binding. + var viewChainSetter = new Reflection.CompiledPropertyChainSetter(viewExpressionChainArray); + var vmChainSetter = new Reflection.CompiledPropertyChainSetter(vmExpressionChainArray); + IObservable<(bool isValid, object? view, bool isViewModel)>? changeWithValues = null; if (triggerUpdate == TriggerUpdate.ViewToViewModel) { var signalObservable = signalViewUpdate is not null - ? signalViewUpdate.Select(_ => false) - : view.WhenAnyDynamic(viewExpression, x => (TVProp?)x.Value).Select(_ => false); + ? signalViewUpdate.Select(_ => false) + : view.WhenAnyDynamic(viewExpression, x => (TVProp?)x.Value).Select(_ => false); var somethingChanged = Observable.Merge( - Reflection.ViewModelWhenAnyValue(viewModel, view, vmExpression).Select(_ => true), - signalInitialUpdate.Select(_ => true), - signalObservable); + Reflection.ViewModelWhenAnyValue(viewModel, view, vmExpression).Select(_ => true), + signalInitialUpdate.Select(_ => true), + signalObservable); changeWithValues = somethingChanged.Select(isVm => - !Reflection.TryGetValueForPropertyChain(out TVMProp vmValue, view.ViewModel, vmExpression.GetExpressionChain()) || - !Reflection.TryGetValueForPropertyChain(out TVProp vValue, view, viewExpression.GetExpressionChain()) + !vmChainGetter.TryGetValue(view.ViewModel, out TVMProp vmValue) || + !viewChainGetter.TryGetValue(view, out TVProp vValue) ? (false, null, false) : isVm ? !vmToViewConverter(vmValue, out var vmAsView) || EqualityComparer.Default.Equals(vValue, vmAsView) - ? (false, null, false) - : (true, vmAsView, isVm) + ? (false, null, false) + : (true, vmAsView, isVm) : !viewToVmConverter(vValue, out var vAsViewModel) || EqualityComparer.Default.Equals(vmValue, vAsViewModel) ? (false, null, false) : (true, vAsViewModel, isVm)); @@ -500,25 +876,24 @@ private bool EvalBindingHooks(TViewModel? viewModel, TView vi else { var somethingChanged = Observable.Merge( - signalViewUpdate is null ? - Reflection.ViewModelWhenAnyValue(viewModel, view, vmExpression).Select(_ => true) : - signalViewUpdate.Select(_ => true) - .Merge(Reflection.ViewModelWhenAnyValue(viewModel, view, vmExpression).Select(_ => true).Take(1)), - signalInitialUpdate.Select(_ => true), - view.WhenAnyDynamic(viewExpression, x => (TVProp?)x.Value).Select(_ => false)); - - changeWithValues = somethingChanged - .Select(isVm => - !Reflection.TryGetValueForPropertyChain(out TVMProp vmValue, view.ViewModel, vmExpression.GetExpressionChain()) || - !Reflection.TryGetValueForPropertyChain(out TVProp vValue, view, viewExpression.GetExpressionChain()) - ? (false, null, false) - : isVm - ? !vmToViewConverter(vmValue, out var vmAsView) || EqualityComparer.Default.Equals(vValue, vmAsView) - ? (false, null, false) - : (true, vmAsView, isVm) - : !viewToVmConverter(vValue, out var vAsViewModel) || EqualityComparer.Default.Equals(vmValue, vAsViewModel) - ? (false, null, false) - : (true, vAsViewModel, isVm)); + signalViewUpdate is null + ? Reflection.ViewModelWhenAnyValue(viewModel, view, vmExpression).Select(_ => true) + : signalViewUpdate.Select(_ => true) + .Merge(Reflection.ViewModelWhenAnyValue(viewModel, view, vmExpression).Select(_ => true).Take(1)), + signalInitialUpdate.Select(_ => true), + view.WhenAnyDynamic(viewExpression, x => (TVProp?)x.Value).Select(_ => false)); + + changeWithValues = somethingChanged.Select(isVm => + !vmChainGetter.TryGetValue(view.ViewModel, out TVMProp vmValue) || + !viewChainGetter.TryGetValue(view, out TVProp vValue) + ? (false, null, false) + : isVm + ? !vmToViewConverter(vmValue, out var vmAsView) || EqualityComparer.Default.Equals(vValue, vmAsView) + ? (false, null, false) + : (true, vmAsView, isVm) + : !viewToVmConverter(vValue, out var vAsViewModel) || EqualityComparer.Default.Equals(vmValue, vAsViewModel) + ? (false, null, false) + : (true, vAsViewModel, isVm)); } var ret = EvalBindingHooks(viewModel, view, vmExpression, viewExpression, BindingDirection.TwoWay); @@ -527,34 +902,34 @@ signalViewUpdate is null ? return null!; } - var changes = changeWithValues - .Where(value => value.isValid) - .Select(value => (value.view, value.isViewModel)) - .Publish() - .RefCount(); + var changes = + changeWithValues + .Where(value => value.isValid) + .Select(value => (value.view, value.isViewModel)) + .Publish() + .RefCount(); var disposable = changes.Subscribe(isVmWithLatestValue => { if (isVmWithLatestValue.isViewModel) { - Reflection.TrySetValueToPropertyChain(view, viewExpression.GetExpressionChain(), isVmWithLatestValue.view, false); + viewChainSetter.TrySetValue(view, isVmWithLatestValue.view, false); } else { - Reflection.TrySetValueToPropertyChain(view.ViewModel, vmExpression.GetExpressionChain(), isVmWithLatestValue.view, false); + vmChainSetter.TrySetValue(view.ViewModel, isVmWithLatestValue.view, false); } }); - // NB: Even though it's technically a two-way bind, most people - // want the ViewModel to win at first. + // NB: Even though it's technically a two-way bind, most people want the ViewModel to win at first. signalInitialUpdate.OnNext(true); return new ReactiveBinding( - view, - viewExpression, - vmExpression, - changes, - BindingDirection.TwoWay, - disposable); + view, + viewExpression, + vmExpression, + changes, + BindingDirection.TwoWay, + disposable); } } diff --git a/src/ReactiveUI/Bindings/Property/PropertyBindingMixins.cs b/src/ReactiveUI/Bindings/Property/PropertyBindingMixins.cs index b12207ec70..58f2d5d498 100644 --- a/src/ReactiveUI/Bindings/Property/PropertyBindingMixins.cs +++ b/src/ReactiveUI/Bindings/Property/PropertyBindingMixins.cs @@ -30,13 +30,14 @@ namespace ReactiveUI; /// ]]> /// /// +[RequiresUnreferencedCode("Evaluates expression-based member chains via reflection; members may be trimmed.")] +[RequiresDynamicCode("Uses dynamic binding paths which may require runtime code generation or reflection-based invocation.")] public static class PropertyBindingMixins { private static readonly PropertyBinderImplementation _binderImplementation; static PropertyBindingMixins() { - RxApp.EnsureInitialized(); _binderImplementation = new PropertyBinderImplementation(); } @@ -75,10 +76,6 @@ static PropertyBindingMixins() /// An instance of that, when disposed, /// disconnects the binding. /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("PropertyBindingMixins uses RxApp initialization and expression binding which require dynamic code generation")] - [RequiresUnreferencedCode("PropertyBindingMixins uses RxApp initialization and expression binding which may require unreferenced code")] -#endif public static IReactiveBinding Bind( this TView view, TViewModel? viewModel, @@ -131,10 +128,6 @@ static PropertyBindingMixins() /// An instance of that, when disposed, /// disconnects the binding. /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("PropertyBindingMixins uses RxApp initialization and expression binding which require dynamic code generation")] - [RequiresUnreferencedCode("PropertyBindingMixins uses RxApp initialization and expression binding which may require unreferenced code")] -#endif public static IReactiveBinding Bind( this TView view, TViewModel? viewModel, @@ -180,10 +173,6 @@ static PropertyBindingMixins() /// An instance of that, when disposed, /// disconnects the binding. /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("PropertyBindingMixins uses RxApp initialization and expression binding which require dynamic code generation")] - [RequiresUnreferencedCode("PropertyBindingMixins uses RxApp initialization and expression binding which may require unreferenced code")] -#endif public static IReactiveBinding Bind( this TView view, TViewModel? viewModel, @@ -223,10 +212,6 @@ static PropertyBindingMixins() /// An instance of that, when disposed, /// disconnects the binding. /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("PropertyBindingMixins uses RxApp initialization and expression binding which require dynamic code generation")] - [RequiresUnreferencedCode("PropertyBindingMixins uses RxApp initialization and expression binding which may require unreferenced code")] -#endif public static IReactiveBinding Bind( this TView view, TViewModel? viewModel, @@ -276,10 +261,6 @@ static PropertyBindingMixins() /// An instance of that, when disposed, /// disconnects the binding. /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("PropertyBindingMixins uses RxApp initialization and expression binding which require dynamic code generation")] - [RequiresUnreferencedCode("PropertyBindingMixins uses RxApp initialization and expression binding which may require unreferenced code")] -#endif public static IReactiveBinding OneWayBind( this TView view, TViewModel? viewModel, @@ -327,10 +308,6 @@ public static IReactiveBinding OneWayBind that, when disposed, /// disconnects the binding. /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("PropertyBindingMixins uses RxApp initialization and expression binding which require dynamic code generation")] - [RequiresUnreferencedCode("PropertyBindingMixins uses RxApp initialization and expression binding which may require unreferenced code")] -#endif public static IReactiveBinding OneWayBind( this TView view, TViewModel? viewModel, @@ -365,10 +342,6 @@ public static IReactiveBinding OneWayBind /// An object that when disposed, disconnects the binding. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("PropertyBindingMixins uses RxApp initialization and expression binding which require dynamic code generation")] - [RequiresUnreferencedCode("PropertyBindingMixins uses RxApp initialization and expression binding which may require unreferenced code")] -#endif public static IDisposable BindTo( this IObservable @this, TTarget? target, diff --git a/src/ReactiveUI/Builder/IReactiveUIBuilder.cs b/src/ReactiveUI/Builder/IReactiveUIBuilder.cs index 442755d485..0fe42fb075 100644 --- a/src/ReactiveUI/Builder/IReactiveUIBuilder.cs +++ b/src/ReactiveUI/Builder/IReactiveUIBuilder.cs @@ -29,7 +29,7 @@ namespace ReactiveUI.Builder; /// .RegisterSingletonViewModel() /// .WithRegistration(resolver => /// { -/// resolver.RegisterLazySingleton(() => new ApiClient(), typeof(IApiClient)); +/// resolver.RegisterLazySingleton(() => new ApiClient()); /// }) /// .BuildApp()); /// ]]> @@ -129,10 +129,6 @@ IReactiveUIBuilder RegisterViewModel() /// /// The type of the registration module that implements IWantsToRegisterStuff. /// 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 IReactiveUIBuilder WithPlatformModule() where T : IWantsToRegisterStuff, new(); @@ -140,10 +136,6 @@ IReactiveUIBuilder WithPlatformModule() /// Withes the platform services. /// /// The builder instance for chaining. -#if NET6_0_OR_GREATER - [RequiresUnreferencedCode("ProcessRegistrationForNamespace uses reflection to locate types which may be trimmed.")] - [RequiresDynamicCode("Calls ReactiveUI.IWantsToRegisterStuff.Register(Action, Type>)")] -#endif IReactiveUIBuilder WithPlatformServices(); /// @@ -170,16 +162,44 @@ IReactiveUIBuilder WithPlatformModule() /// IReactiveUIBuilder WithTaskPoolScheduler(IScheduler scheduler, bool setRxApp = true); + /// + /// Configures a custom exception handler for unhandled errors in ReactiveUI observables. + /// If not configured, ReactiveUI uses a default handler that breaks the debugger and throws UnhandledErrorException. + /// + /// The custom exception handler to use. + /// The builder instance for chaining. + IReactiveUIBuilder WithExceptionHandler(IObserver exceptionHandler); + + /// + /// Configures the non-generic suspension host for application lifecycle management. + /// Creates a default instance if not explicitly provided. + /// + /// The builder instance for chaining. + IReactiveUIBuilder WithSuspensionHost(); + + /// + /// Configures a typed suspension host for application lifecycle management. + /// Creates a instance configured for the specified app state type. + /// + /// The type of the application state to manage. + /// The builder instance for chaining. + IReactiveUIBuilder WithSuspensionHost(); + + /// + /// Configures custom cache size limits for ReactiveUI's internal memoizing caches. + /// If not configured, platform-specific defaults are used (32/64 for mobile, 64/256 for desktop). + /// + /// The small cache limit to use (must be greater than 0). + /// The big cache limit to use (must be greater than 0). + /// The builder instance for chaining. + IReactiveUIBuilder WithCacheSizes(int smallCacheLimit, int bigCacheLimit); + /// /// Withes the views from assembly. /// /// The assembly. /// 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 + [RequiresUnreferencedCode("Scans assembly for IViewFor implementations using reflection. For AOT compatibility, use the ReactiveUIBuilder pattern to RegisterView explicitly.")] IReactiveUIBuilder WithViewsFromAssembly(Assembly assembly); /// diff --git a/src/ReactiveUI/Builder/ReactiveUIBuilder.cs b/src/ReactiveUI/Builder/ReactiveUIBuilder.cs index 371a373015..3020a2303b 100644 --- a/src/ReactiveUI/Builder/ReactiveUIBuilder.cs +++ b/src/ReactiveUI/Builder/ReactiveUIBuilder.cs @@ -19,6 +19,10 @@ public sealed class ReactiveUIBuilder : AppBuilder, IReactiveUIBuilder, IReactiv private bool _coreRegistered; private bool _setRxAppMainScheduler; private bool _setRxAppTaskPoolScheduler; + private IObserver? _exceptionHandler; + private ISuspensionHost? _suspensionHost; + private int? _smallCacheLimit; + private int? _bigCacheLimit; /// /// Initializes a new instance of the class. @@ -101,16 +105,27 @@ public IReactiveUIBuilder WithRegistration(Action co return this; } + /// + /// Registers services using an IWantsToRegisterStuff implementation. + /// This method provides a migration path for users with existing IWantsToRegisterStuff implementations. + /// + /// The registration implementation. + /// The builder instance for chaining. + /// Thrown if registration is null. + public IReactiveUIBuilder WithRegistration(IWantsToRegisterStuff registration) + { + ArgumentExceptionHelper.ThrowIfNull(registration); + + var registrar = new DependencyResolverRegistrar(CurrentMutable); + registration.Register(registrar); + + return this; + } + /// /// Registers the platform-specific ReactiveUI services. /// /// The builder instance for method chaining. -#if NET6_0_OR_GREATER - [SuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Not using reflection")] - [SuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "Not using reflection")] - [RequiresUnreferencedCode("Calls ReactiveUI.IWantsToRegisterStuff.Register(Action, Type>)")] - [RequiresDynamicCode("Calls ReactiveUI.IWantsToRegisterStuff.Register(Action, Type>)")] -#endif public IReactiveUIBuilder WithPlatformServices() { if (_platformRegistered) @@ -129,12 +144,6 @@ public IReactiveUIBuilder WithPlatformServices() /// Registers the core ReactiveUI services in an AOT-compatible manner. /// /// The builder instance for chaining. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("Calls ReactiveUI.IWantsToRegisterStuff.Register(Action, Type>)")] - [RequiresUnreferencedCode("Calls ReactiveUI.IWantsToRegisterStuff.Register(Action, Type>)")] - [SuppressMessage("AOT", "IL3051:'RequiresDynamicCodeAttribute' annotations must match across all interface implementations or overrides.", Justification = "In Splat")] - [SuppressMessage("Trimming", "IL2046:'RequiresUnreferencedCodeAttribute' annotations must match across all interface implementations or overrides.", Justification = "In Splat")] -#endif public override IAppBuilder WithCoreServices() { if (!_coreRegistered) @@ -155,10 +164,7 @@ public override IAppBuilder WithCoreServices() /// /// The assembly to scan for views. /// 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 + [RequiresUnreferencedCode("Scans assembly for IViewFor implementations using reflection. For AOT compatibility, use the ReactiveUIBuilder pattern to RegisterView explicitly.")] public IReactiveUIBuilder WithViewsFromAssembly(Assembly assembly) { ArgumentExceptionHelper.ThrowIfNull(assembly); @@ -173,15 +179,12 @@ public IReactiveUIBuilder WithViewsFromAssembly(Assembly assembly) /// /// The type of the registration module that implements IWantsToRegisterStuff. /// 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 IReactiveUIBuilder WithPlatformModule() where T : IWantsToRegisterStuff, new() { var registration = new T(); - registration.Register((f, t) => CurrentMutable.RegisterConstant(f(), t)); + var registrar = new DependencyResolverRegistrar(CurrentMutable); + registration.Register(registrar); return this; } @@ -288,6 +291,63 @@ public IReactiveUIBuilder ConfigureSuspensionDriver(Action co } }); + /// + /// Configures a custom exception handler for unhandled errors in ReactiveUI observables. + /// + /// The custom exception handler to use. + /// The builder instance for chaining. + /// Thrown if exceptionHandler is null. + public IReactiveUIBuilder WithExceptionHandler(IObserver exceptionHandler) + { + _exceptionHandler = exceptionHandler ?? throw new ArgumentNullException(nameof(exceptionHandler)); + return this; + } + + /// + /// Configures the non-generic suspension host for application lifecycle management. + /// + /// The builder instance for chaining. + public IReactiveUIBuilder WithSuspensionHost() + { + _suspensionHost = new SuspensionHost(); + return this; + } + + /// + /// Configures a typed suspension host for application lifecycle management. + /// + /// The type of the application state to manage. + /// The builder instance for chaining. + public IReactiveUIBuilder WithSuspensionHost() + { + _suspensionHost = new SuspensionHost(); + return this; + } + + /// + /// Configures custom cache size limits for ReactiveUI's internal memoizing caches. + /// + /// The small cache limit to use (must be greater than 0). + /// The big cache limit to use (must be greater than 0). + /// The builder instance for chaining. + /// Thrown if either cache limit is less than or equal to 0. + public IReactiveUIBuilder WithCacheSizes(int smallCacheLimit, int bigCacheLimit) + { + if (smallCacheLimit <= 0) + { + throw new ArgumentOutOfRangeException(nameof(smallCacheLimit), "Small cache limit must be greater than 0."); + } + + if (bigCacheLimit <= 0) + { + throw new ArgumentOutOfRangeException(nameof(bigCacheLimit), "Big cache limit must be greater than 0."); + } + + _smallCacheLimit = smallCacheLimit; + _bigCacheLimit = bigCacheLimit; + return this; + } + /// /// Registers a custom view model with the dependency resolver. /// @@ -344,6 +404,12 @@ public IReactiveUIInstance BuildApp() throw new InvalidOperationException("Failed to create ReactiveUIInstance instance"); } + // Initialize static state (cache sizes, exception handler, suspension host) + InitializeStaticState(); + + // Mark ReactiveUI as initialized via builder pattern + RxAppBuilder.MarkAsInitialized(); + return appInstance; } @@ -935,6 +1001,56 @@ public IReactiveUIInstance WithInstance + /// Gets the platform-specific default small cache limit. + /// + /// The default small cache limit for the current platform. + private static int GetPlatformDefaultSmallCacheLimit() + { +#if ANDROID || IOS + return 32; +#else + return 64; +#endif + } + + /// + /// Gets the platform-specific default big cache limit. + /// + /// The default big cache limit for the current platform. + private static int GetPlatformDefaultBigCacheLimit() + { +#if ANDROID || IOS + return 64; +#else + return 256; +#endif + } + + /// + /// Initializes the static state for ReactiveUI based on builder configuration. + /// This includes cache sizes, exception handler, and suspension host. + /// + private void InitializeStaticState() + { + // Initialize cache sizes - use configured values or platform defaults + var smallCache = _smallCacheLimit ?? GetPlatformDefaultSmallCacheLimit(); + var bigCache = _bigCacheLimit ?? GetPlatformDefaultBigCacheLimit(); + RxCacheSize.Initialize(smallCache, bigCache); + + // Initialize exception handler if configured + if (_exceptionHandler is not null) + { + RxState.InitializeExceptionHandler(_exceptionHandler); + } + + // Initialize suspension host if configured + if (_suspensionHost is not null) + { + RxSuspension.InitializeSuspensionHost(_suspensionHost); + } + } + private void ConfigureSchedulers() => WithCustomRegistration(_ => { diff --git a/src/ReactiveUI/Builder/RxAppBuilder.cs b/src/ReactiveUI/Builder/RxAppBuilder.cs index b03ffb0418..08497e9ab5 100644 --- a/src/ReactiveUI/Builder/RxAppBuilder.cs +++ b/src/ReactiveUI/Builder/RxAppBuilder.cs @@ -10,6 +10,8 @@ namespace ReactiveUI.Builder; /// public static class RxAppBuilder { + private static int _hasBeenInitialized; // 0 = false, 1 = true + /// /// Creates a ReactiveUI builder with the Splat Locator instance. /// @@ -32,4 +34,46 @@ public static ReactiveUIBuilder CreateReactiveUIBuilder(this IMutableDependencyR var readonlyResolver = resolver as IReadonlyDependencyResolver ?? AppLocator.Current; return new(resolver, readonlyResolver); } + + /// + /// Ensures ReactiveUI has been initialized via the builder pattern. + /// Throws an exception if BuildApp() has not been called. + /// + /// Thrown if ReactiveUI has not been initialized via the builder pattern. + /// + /// + /// This method replaces the old RxApp.EnsureInitialized() pattern. + /// Call this method at the start of your application or in test setup to verify ReactiveUI is properly initialized. + /// + /// + /// To initialize ReactiveUI, call: + /// + /// RxAppBuilder.CreateReactiveUIBuilder() + /// .WithCoreServices() + /// .BuildApp(); + /// + /// + /// + public static void EnsureInitialized() + { + if (Volatile.Read(ref _hasBeenInitialized) == 0) + { + throw new InvalidOperationException( + "ReactiveUI has not been initialized. You must initialize ReactiveUI using the builder pattern. " + + "See https://www.reactiveui.net/docs/handbook/rxappbuilder.html for migration guidance.\n\n" + + "Example:\n" + + "RxAppBuilder.CreateReactiveUIBuilder()\n" + + " .WithCoreServices()\n" + + " .WithPlatformServices()\n" + + " .BuildApp();"); + } + } + + /// + /// Marks ReactiveUI as initialized. Called by ReactiveUIBuilder.BuildApp(). + /// + internal static void MarkAsInitialized() + { + Interlocked.Exchange(ref _hasBeenInitialized, 1); + } } diff --git a/src/ReactiveUI/Expression/ExpressionRewriter.cs b/src/ReactiveUI/Expression/ExpressionRewriter.cs index 82e35127f9..c74cf65d20 100644 --- a/src/ReactiveUI/Expression/ExpressionRewriter.cs +++ b/src/ReactiveUI/Expression/ExpressionRewriter.cs @@ -3,60 +3,69 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. +using System; +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; +using System.Linq.Expressions; using System.Reflection; +using System.Runtime.CompilerServices; using System.Text; namespace ReactiveUI; /// -/// Class for simplifying and validating expressions. +/// Rewrites and validates expression trees used by ReactiveUI binding infrastructure, normalizing +/// supported constructs into a consistent shape. /// -internal class ExpressionRewriter : ExpressionVisitor +/// +/// +/// This visitor intentionally supports a constrained set of expression node types. Unsupported shapes +/// are rejected with actionable exceptions to help callers correct their expressions. +/// +/// +/// Supported rewrites include: +/// +/// +/// is rewritten into an indexer access (get_Item / Item). +/// to a special-name indexer method (get_Item) is rewritten into an . +/// is rewritten into member access to Length. +/// is stripped. +/// +/// +/// Index expressions are only supported when all indices are constants. +/// +/// +internal sealed class ExpressionRewriter : ExpressionVisitor { + /// + /// Visits the specified expression node and rewrites supported shapes into their normalized form. + /// + /// The expression node to visit. + /// The rewritten expression. + /// Thrown when is . + /// Thrown when uses an unsupported node type or shape. public override Expression Visit(Expression? node) { ArgumentExceptionHelper.ThrowIfNull(node); - switch (node!.NodeType) + return node.NodeType switch { - case ExpressionType.ArrayIndex: - return VisitBinary((BinaryExpression)node); - case ExpressionType.ArrayLength: - return VisitUnary((UnaryExpression)node); - case ExpressionType.Call: - return VisitMethodCall((MethodCallExpression)node); - case ExpressionType.Index: - return VisitIndex((IndexExpression)node); - case ExpressionType.MemberAccess: - return VisitMember((MemberExpression)node); - case ExpressionType.Parameter: - return VisitParameter((ParameterExpression)node); - case ExpressionType.Constant: - return VisitConstant((ConstantExpression)node); - case ExpressionType.Convert: - return VisitUnary((UnaryExpression)node); - default: - var errorMessageBuilder = new StringBuilder($"Unsupported expression of type '{node.NodeType}' {node}."); - - if (node is BinaryExpression binaryExpression) - { - errorMessageBuilder.Append(" Did you meant to use expressions '") - .Append(binaryExpression.Left) - .Append("' and '") - .Append(binaryExpression.Right) - .Append("'?"); - } - - throw new NotSupportedException(errorMessageBuilder.ToString()); - } + ExpressionType.ArrayIndex => VisitBinary((BinaryExpression)node), + ExpressionType.ArrayLength => VisitUnary((UnaryExpression)node), + ExpressionType.Call => VisitMethodCall((MethodCallExpression)node), + ExpressionType.Index => VisitIndex((IndexExpression)node), + ExpressionType.MemberAccess => VisitMember((MemberExpression)node), + ExpressionType.Parameter => VisitParameter((ParameterExpression)node), + ExpressionType.Constant => VisitConstant((ConstantExpression)node), + ExpressionType.Convert => VisitUnary((UnaryExpression)node), + _ => throw CreateUnsupportedNodeException(node) + }; } -#if NET6_0_OR_GREATER - [RequiresDynamicCode("ExpressionRewriter uses reflection to access type properties which requires dynamic code generation")] - [RequiresUnreferencedCode("ExpressionRewriter uses reflection to access type properties which may require unreferenced code")] + [RequiresUnreferencedCode("Expression rewriting uses reflection over runtime types (e.g., Item/Length) which may be removed by trimming.")] + [RequiresDynamicCode("Expression rewriting uses reflection over runtime types and may not be compatible with AOT compilation.")] [SuppressMessage("AOT", "IL3051:'RequiresDynamicCodeAttribute' annotations must match across all interface implementations or overrides.", Justification = "Third Party Code")] [SuppressMessage("Trimming", "IL2046:'RequiresUnreferencedCodeAttribute' annotations must match across all interface implementations or overrides.", Justification = "Third Party Code")] -#endif protected override Expression VisitBinary(BinaryExpression node) { if (node.Right is not ConstantExpression) @@ -64,76 +73,74 @@ protected override Expression VisitBinary(BinaryExpression node) throw new NotSupportedException("Array index expressions are only supported with constants."); } - var left = Visit(node.Left); - var right = Visit(node.Right); + var instance = Visit(node.Left); + var index = (ConstantExpression)Visit(node.Right); - // Translate arrayindex into normal index expression - return Expression.MakeIndex(left, GetItemProperty(left.Type), [right]); + // Translate arrayindex into a normal index expression using the indexer property. + return Expression.MakeIndex(instance, GetItemProperty(instance.Type), [index]); } -#if NET6_0_OR_GREATER - [RequiresDynamicCode("ExpressionRewriter uses reflection to access type properties which requires dynamic code generation")] - [RequiresUnreferencedCode("ExpressionRewriter uses reflection to access type properties which may require unreferenced code")] + [RequiresUnreferencedCode("Expression rewriting uses reflection over runtime types (e.g., Item/Length) which may be removed by trimming.")] + [RequiresDynamicCode("Expression rewriting uses reflection over runtime types and may not be compatible with AOT compilation.")] [SuppressMessage("AOT", "IL3051:'RequiresDynamicCodeAttribute' annotations must match across all interface implementations or overrides.", Justification = "Third Party Code")] [SuppressMessage("Trimming", "IL2046:'RequiresUnreferencedCodeAttribute' annotations must match across all interface implementations or overrides.", Justification = "Third Party Code")] -#endif protected override Expression VisitUnary(UnaryExpression node) { - if (node.NodeType == ExpressionType.ArrayLength && node.Operand is not null) + if (node.Operand is null) { - var expression = Visit(node.Operand); - - var memberInfo = GetLengthProperty(expression.Type); - - return memberInfo switch - { - null => throw new InvalidOperationException("Could not find valid information for the array length operator."), - _ => Expression.MakeMemberAccess(expression, memberInfo) - }; + throw new ArgumentException("Could not find a valid operand for the node.", nameof(node)); } - else if (node.NodeType == ExpressionType.Convert && node.Operand is not null) + + if (node.NodeType == ExpressionType.Convert) { + // Strip conversion nodes to keep expression chains stable. return Visit(node.Operand); } - else if (node.Operand is not null) - { - return node.Update(Visit(node?.Operand)); - } - else + + if (node.NodeType == ExpressionType.ArrayLength) { - throw new ArgumentException("Could not find a valid operand for the node.", nameof(node)); + var operand = Visit(node.Operand); + var lengthProperty = GetLengthProperty(operand.Type); + + return Expression.MakeMemberAccess(operand, lengthProperty); } + + return node.Update(Visit(node.Operand)); } -#if NET6_0_OR_GREATER - [RequiresDynamicCode("ExpressionRewriter uses reflection to access type properties which requires dynamic code generation")] - [RequiresUnreferencedCode("ExpressionRewriter uses reflection to access type properties which may require unreferenced code")] + [RequiresUnreferencedCode("Expression rewriting uses reflection over runtime types (e.g., Item/Length) which may be removed by trimming.")] + [RequiresDynamicCode("Expression rewriting uses reflection over runtime types and may not be compatible with AOT compilation.")] [SuppressMessage("AOT", "IL3051:'RequiresDynamicCodeAttribute' annotations must match across all interface implementations or overrides.", Justification = "Third Party Code")] [SuppressMessage("Trimming", "IL2046:'RequiresUnreferencedCodeAttribute' annotations must match across all interface implementations or overrides.", Justification = "Third Party Code")] -#endif protected override Expression VisitMethodCall(MethodCallExpression node) { - // Rewrite a method call to an indexer as an index expression - if (node.Arguments.Any(static e => e is not ConstantExpression) || !node.Method.IsSpecialName) + if (!node.Method.IsSpecialName || !AllConstant(node.Arguments)) { throw new NotSupportedException("Index expressions are only supported with constants."); } if (node.Object is null) { - throw new ArgumentException("The Method call does not point towards an object.", nameof(node)); + throw new ArgumentException("The method call does not point towards an object.", nameof(node)); } var instance = Visit(node.Object); - IEnumerable arguments = Visit(node.Arguments); - // Translate call to get_Item into normal index expression - return Expression.MakeIndex(instance, GetItemProperty(instance.Type), arguments); + // Visit arguments explicitly to avoid LINQ allocations. + var args = VisitArgumentList(node.Arguments); + + return Expression.MakeIndex(instance, GetItemProperty(instance.Type), args); } + /// + /// Validates that index expressions only use constant arguments, then defers to the base visitor. + /// + /// The index expression. + /// The visited (and potentially rewritten) index expression. + /// Thrown when any index argument is not a constant. protected override Expression VisitIndex(IndexExpression node) { - if (node.Arguments.Any(static e => e is not ConstantExpression)) + if (!AllConstant(node.Arguments)) { throw new NotSupportedException("Index expressions are only supported with constants."); } @@ -141,21 +148,100 @@ protected override Expression VisitIndex(IndexExpression node) return base.VisitIndex(node); } -#if NET6_0_OR_GREATER - private static PropertyInfo? GetItemProperty([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type type) -#else - private static PropertyInfo? GetItemProperty(Type type) -#endif + /// + /// Creates a consistent exception for unsupported node types, including additional context for binary expressions. + /// + /// The unsupported node. + /// An exception to throw. + private static Exception CreateUnsupportedNodeException(Expression node) { - return type.GetRuntimeProperty("Item"); + // Preserve prior behavior: include helpful guidance for binary expressions. + var sb = new StringBuilder(96); + sb.Append("Unsupported expression of type '") + .Append(node.NodeType) + .Append("' ") + .Append(node) + .Append('.'); + + if (node is BinaryExpression be) + { + sb.Append(" Did you meant to use expressions '") + .Append(be.Left) + .Append("' and '") + .Append(be.Right) + .Append("'?"); + } + + return new NotSupportedException(sb.ToString()); } -#if NET6_0_OR_GREATER - private static PropertyInfo? GetLengthProperty([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type type) -#else - private static PropertyInfo? GetLengthProperty(Type type) -#endif + /// + /// Returns the indexer property (Item) for the specified type. + /// + /// The type to inspect. + /// The resolved indexer property. + /// Thrown when no indexer property can be found. + private static PropertyInfo GetItemProperty( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] + Type type) { - return type.GetRuntimeProperty("Length"); + // NOTE: Using the Type instance preserves trimming annotations; do not reconstruct Type from RuntimeTypeHandle. + var property = type.GetRuntimeProperty("Item"); + return property ?? throw new InvalidOperationException("Could not find a valid indexer property named 'Item'."); + } + + /// + /// Returns the Length property for the specified type. + /// + /// The type to inspect. + /// The resolved length property. + /// Thrown when no length property can be found. + private static PropertyInfo GetLengthProperty( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] + Type type) + { + // NOTE: Using the Type instance preserves trimming annotations; do not reconstruct Type from RuntimeTypeHandle. + var property = type.GetRuntimeProperty("Length"); + return property ?? throw new InvalidOperationException("Could not find valid information for the array length operator."); + } + + /// + /// Determines whether all expressions in the provided collection are constant expressions. + /// + /// The argument list. + /// if all arguments are constants; otherwise . + private static bool AllConstant(ReadOnlyCollection expressions) + { + for (var i = 0; i < expressions.Count; i++) + { + if (expressions[i] is not ConstantExpression) + { + return false; + } + } + + return true; + } + + /// + /// Visits a method argument list without LINQ allocations. + /// + /// The argument list to visit. + /// A visited argument array suitable for . + private Expression[] VisitArgumentList(ReadOnlyCollection arguments) + { + var count = arguments.Count; + if (count == 0) + { + return Array.Empty(); + } + + var visited = new Expression[count]; + for (var i = 0; i < count; i++) + { + visited[i] = Visit(arguments[i]); + } + + return visited; } } diff --git a/src/ReactiveUI/Expression/Reflection.cs b/src/ReactiveUI/Expression/Reflection.cs index 3eb094019d..b5a357605d 100644 --- a/src/ReactiveUI/Expression/Reflection.cs +++ b/src/ReactiveUI/Expression/Reflection.cs @@ -3,191 +3,249 @@ // 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.Reflection; +using System.Runtime.CompilerServices; using System.Text; namespace ReactiveUI; /// -/// Helper class for handling Reflection amd Expression tree related items. +/// Helper class for handling reflection and expression-tree related operations. /// +/// +/// +/// This type is part of ReactiveUI's infrastructure and is used by binding, observation, +/// and other reflection-heavy code paths. +/// +/// +/// Trimming/AOT note: Some APIs in this type are inherently trimming-unfriendly (e.g. string-based type resolution +/// and expression-driven member traversal). Such APIs are annotated accordingly. +/// +/// [Preserve(AllMembers = true)] public static class Reflection { - [SuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "Marked as Preserve")] - [SuppressMessage("Trimming", "IL2026:Calling members annotated with 'RequiresUnreferencedCodeAttribute' may break functionality when trimming application code.", Justification = "Marked as Preserve")] + /// + /// Cached expression rewriter used to simplify expression trees. + /// + /// + /// This instance is cached for performance. It is assumed that ExpressionRewriter is thread-safe for Visit. + /// If that assumption is invalid, this should be changed to a per-call instance or an object pool. + /// private static readonly ExpressionRewriter _expressionRewriter = new(); - [SuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "Marked as Preserve")] - [SuppressMessage("Trimming", "IL2026:Calling members annotated with 'RequiresUnreferencedCodeAttribute' may break functionality when trimming application code.", Justification = "Marked as Preserve")] - private static readonly MemoizingMRUCache _typeCache = new( - static (type, _) => GetTypeHelper(type), - 20); + /// + /// Cache for mapping type names to resolved instances. + /// + /// + /// Initialized lazily by to keep trimming-risky initialization inside the RUC boundary. + /// + private static MemoizingMRUCache? _typeCache; /// - /// Uses the expression re-writer to simplify the Expression down to it's simplest Expression. + /// Uses the expression re-writer to simplify the expression down to its simplest expression. /// /// The expression to rewrite. - /// The rewritten expression. + /// The rewritten expression, or if is . public static Expression Rewrite(Expression? expression) => _expressionRewriter.Visit(expression); /// - /// Will convert a Expression which points towards a property - /// to a string containing the property names. - /// The sub-properties will be separated by the '.' character. - /// Index based values will include [] after the name. + /// Converts an expression that points to a property chain into a dotted path string. + /// Sub-properties are separated by '.'. + /// Index-based values include [] after the name, with the index argument values. /// /// The expression to generate the property names from. - /// A string form for the property the expression is pointing to. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("Expression tree analysis requires dynamic code generation")] - [RequiresUnreferencedCode("Expression tree analysis may reference members that could be trimmed")] -#endif + /// A string representation for the property chain the expression points to. + /// Thrown when is . + /// + /// This method intentionally follows existing behavior, including the assumption that index arguments are + /// instances. + /// public static string ExpressionToPropertyNames(Expression? expression) // TODO: Create Test { ArgumentExceptionHelper.ThrowIfNull(expression); + var sb = new StringBuilder(); + var firstSegment = true; foreach (var exp in expression!.GetExpressionChain()) { - if (exp.NodeType != ExpressionType.Parameter) + if (exp.NodeType == ExpressionType.Parameter) { - // Indexer expression - if (exp.NodeType == ExpressionType.Index && exp is IndexExpression indexExpression && indexExpression.Indexer is not null) - { - sb.Append(indexExpression.Indexer.Name).Append('['); + continue; + } + + if (!firstSegment) + { + sb.Append('.'); + } + + if (exp.NodeType == ExpressionType.Index && + exp is IndexExpression indexExpression && + indexExpression.Indexer is not null) + { + sb.Append(indexExpression.Indexer.Name).Append('['); - foreach (var argument in indexExpression.Arguments) + var args = indexExpression.Arguments; + for (var i = 0; i < args.Count; i++) + { + if (i != 0) { - sb.Append(((ConstantExpression)argument).Value).Append(','); + sb.Append(','); } - sb.Replace(',', ']', sb.Length - 1, 1); - } - else if (exp.NodeType == ExpressionType.MemberAccess && exp is MemberExpression memberExpression) - { - sb.Append(memberExpression.Member.Name); + // Preserve original behavior: assumes ConstantExpression. + sb.Append(((ConstantExpression)args[i]).Value); } - } - sb.Append('.'); - } + sb.Append(']'); + } + else if (exp.NodeType == ExpressionType.MemberAccess && exp is MemberExpression memberExpression) + { + sb.Append(memberExpression.Member.Name); + } - if (sb.Length > 0) - { - sb.Remove(sb.Length - 1, 1); + firstSegment = false; } return sb.ToString(); } /// - /// Converts a MemberInfo into a Func which will fetch the value for the Member. - /// Handles either fields or properties. + /// Converts a into a delegate which fetches the value for the member. + /// Supports fields and properties. /// /// The member info to convert. - /// A Func that takes in the object/indexes and returns the value. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("Member access requires dynamic code generation")] - [RequiresUnreferencedCode("Member access may reference members that could be trimmed")] -#endif + /// + /// A delegate that takes (target, indexArguments) and returns the value; or + /// when the member is not a field or property. + /// + /// Thrown when is . + /// + /// + /// For fields, the existing behavior throws if the field value is . + /// + /// + /// Trimming note: this method does not discover members by name; it operates on an already-resolved . + /// + /// public static Func? GetValueFetcherForProperty(MemberInfo? member) // TODO: Create Test { ArgumentExceptionHelper.ThrowIfNull(member); - var field = member as FieldInfo; - if (field is not null) + if (member is FieldInfo field) + { + // Delegate captures 'field' (setup-time), avoiding repeated dynamic checks. + return (obj, _) => + { + var value = field.GetValue(obj); + return value ?? throw new InvalidOperationException(); + }; + } + + if (member is PropertyInfo property) { - return (obj, _) => field.GetValue(obj) ?? throw new InvalidOperationException(); + return property.GetValue; } - var property = member as PropertyInfo; - return property!.GetValue; + return null; } /// - /// Converts a MemberInfo into a Func which will fetch the value for the Member. - /// Handles either fields or properties. - /// If there is no field or property with the matching MemberInfo it'll throw - /// an ArgumentException. + /// Converts a into a delegate which fetches the value for the member. + /// Supports fields and properties and throws if the member is not supported. /// /// The member info to convert. - /// A Func that takes in the object/indexes and returns the value. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("Member access requires dynamic code generation")] - [RequiresUnreferencedCode("Member access may reference members that could be trimmed")] -#endif + /// A delegate that takes (target, indexArguments) and returns the value. + /// Thrown when is . + /// Thrown when is not a field or property. public static Func GetValueFetcherOrThrow(MemberInfo? member) // TODO: Create Test { ArgumentExceptionHelper.ThrowIfNull(member); var ret = GetValueFetcherForProperty(member); - return ret ?? throw new ArgumentException($"Type '{member!.DeclaringType}' must have a property '{member.Name}'"); } /// - /// Converts a MemberInfo into a Func which will set the value for the Member. - /// Handles either fields or properties. - /// If there is no field or property with the matching MemberInfo it'll throw - /// an ArgumentException. + /// Converts a into a delegate which sets the value for the member. + /// Supports fields and properties. /// /// The member info to convert. - /// A Func that takes in the object/indexes and sets the value. - public static Action GetValueSetterForProperty(MemberInfo? member) // TODO: Create Test + /// + /// A delegate that takes (target, value, indexArguments) and sets the value; or + /// when the member is not a field or property. + /// + /// Thrown when is . + /// + /// + /// This is the soft-fail setter API. It must not throw when the member is unsupported, + /// because callers (including compiled chain setters) rely on it for shouldThrow == false paths. + /// + /// + /// Trimming note: this method does not discover members by name; it operates on an already-resolved . + /// + /// + public static Action? GetValueSetterForProperty(MemberInfo? member) // TODO: Create Test { ArgumentExceptionHelper.ThrowIfNull(member); - var field = member as FieldInfo; - if (field is not null) + if (member is FieldInfo field) { return (obj, val, _) => field.SetValue(obj, val); } - var property = member as PropertyInfo; - return property!.SetValue; + if (member is PropertyInfo property) + { + return property.SetValue; + } + + return null; } /// - /// Converts a MemberInfo into a Func which will set the value for the Member. - /// Handles either fields or properties. - /// If there is no field or property with the matching MemberInfo it'll throw - /// an ArgumentException. + /// Converts a into a delegate which sets the value for the member. + /// Supports fields and properties and throws if the member is not supported. /// /// The member info to convert. - /// A Func that takes in the object/indexes and sets the value. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("Member access requires dynamic code generation")] - [RequiresUnreferencedCode("Member access may reference members that could be trimmed")] -#endif - public static Action? GetValueSetterOrThrow(MemberInfo? member) // TODO: Create Test + /// A delegate that takes (target, value, indexArguments) and sets the value. + /// Thrown when is . + /// Thrown when is not a field or property. + public static Action GetValueSetterOrThrow(MemberInfo? member) // TODO: Create Test { ArgumentExceptionHelper.ThrowIfNull(member); var ret = GetValueSetterForProperty(member); - return ret ?? throw new ArgumentException($"Type '{member!.DeclaringType}' must have a property '{member.Name}'"); } /// - /// Based on a list of Expressions get the value of the last property in the chain if possible. - /// The Expressions are typically property chains. Eg Property1.Property2.Property3 - /// The method will make sure that each Expression can get a value along the way - /// and get each property until each expression is evaluated. + /// Based on a list of expressions, attempts to get the value of the last property in the chain. /// - /// A output value where to store the value if the value can be fetched. + /// The expected type of the final value. + /// Receives the value if the chain can be evaluated. /// The object that starts the property chain. - /// A list of expressions which will point towards a property or field. - /// The type of the end value we are trying to get. - /// If the value was successfully retrieved or not. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("Expression evaluation requires dynamic code generation")] - [RequiresUnreferencedCode("Expression evaluation may reference members that could be trimmed")] -#endif + /// A sequence of expressions that point to properties/fields. + /// if the value was successfully retrieved; otherwise . + /// Thrown when is empty. + /// Thrown when is . + /// + /// Trimming note: this method may traverse arbitrary member chains represented by expressions; it is not possible + /// to express a complete trimming contract locally. + /// + [RequiresUnreferencedCode("Evaluates expression-based member chains via reflection; members may be trimmed.")] public static bool TryGetValueForPropertyChain(out TValue changeValue, object? current, IEnumerable expressionChain) // TODO: Create Test { - var expressions = expressionChain.ToList(); - foreach (var expression in expressions.SkipLast(1)) + var expressions = MaterializeExpressions(expressionChain); + var count = expressions.Length; + + if (count == 0) + { + throw new InvalidOperationException("Expression chain must contain at least one element."); + } + + for (var i = 0; i < count - 1; i++) { if (current is null) { @@ -195,6 +253,7 @@ public static bool TryGetValueForPropertyChain(out TValue changeValue, o return false; } + var expression = expressions[i]; current = GetValueFetcherOrThrow(expression.GetMemberInfo())(current, expression.GetArgumentsArray()); } @@ -203,39 +262,42 @@ public static bool TryGetValueForPropertyChain(out TValue changeValue, o changeValue = default!; return false; } -#if NET6_0_OR_GREATER - var lastExpression = expressions[^1]; -#else -#pragma warning disable RCS1246 // Use element access - var lastExpression = expressions.Last(); -#pragma warning restore RCS1246 // Use element access -#endif + + var lastExpression = expressions[count - 1]; changeValue = (TValue)GetValueFetcherOrThrow(lastExpression.GetMemberInfo())(current, lastExpression.GetArgumentsArray())!; return true; } /// - /// Based on a list of Expressions get a IObservedChanged for the value - /// of the last property in the chain if possible. - /// The Expressions are property chains. Eg Property1.Property2.Property3 - /// The method will make sure that each Expression can get a value along the way - /// and get each property until each expression is evaluated. + /// Based on a list of expressions, attempts to produce an array of + /// values representing each step in the property chain. /// - /// A IObservedChanged for the value. + /// Receives an array with one entry per expression in the chain. /// The object that starts the property chain. - /// A list of expressions which will point towards a property or field. - /// If the value was successfully retrieved or not. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("Expression evaluation requires dynamic code generation")] - [RequiresUnreferencedCode("Expression evaluation may reference members that could be trimmed")] -#endif + /// A sequence of expressions that point to properties/fields. + /// if all values were successfully retrieved; otherwise . + /// Thrown when is empty. + /// Thrown when is . + /// + /// This preserves the existing behavior: on early failure, the method writes a single + /// element at the failing index and returns . + /// + [RequiresUnreferencedCode("Evaluates expression-based member chains via reflection; members may be trimmed.")] public static bool TryGetAllValuesForPropertyChain(out IObservedChange[] changeValues, object? current, IEnumerable expressionChain) // TODO: Create Test { + var expressions = MaterializeExpressions(expressionChain); + var count = expressions.Length; + + changeValues = new IObservedChange[count]; + + if (count == 0) + { + throw new InvalidOperationException("Expression chain must contain at least one element."); + } + var currentIndex = 0; - var expressions = expressionChain.ToList(); - changeValues = new IObservedChange[expressions.Count]; - foreach (var expression in expressions.SkipLast(1)) + for (; currentIndex < count - 1; currentIndex++) { if (current is null) { @@ -243,10 +305,10 @@ public static bool TryGetAllValuesForPropertyChain(out IObservedChange(sender, expression, current); - currentIndex++; } if (current is null) @@ -255,42 +317,55 @@ public static bool TryGetAllValuesForPropertyChain(out IObservedChange(current, lastExpression, GetValueFetcherOrThrow(lastExpression.GetMemberInfo())(current, lastExpression.GetArgumentsArray())); + var lastExpression = expressions[count - 1]; + changeValues[currentIndex] = new ObservedChange( + current, + lastExpression, + GetValueFetcherOrThrow(lastExpression.GetMemberInfo())(current, lastExpression.GetArgumentsArray())); + return true; } /// - /// Based on a list of Expressions set a value - /// of the last property in the chain if possible. - /// The Expressions are property chains. Eg Property1.Property2.Property3 - /// The method will make sure that each Expression can use each value along the way - /// and set the last value. + /// Based on a list of expressions, attempts to set the value of the last property in the chain. /// + /// The type of the end value being set. /// The object that starts the property chain. - /// A list of expressions which will point towards a property or field. - /// The value to set on the last property in the Expression chain. - /// If we should throw if we are unable to set the value. - /// The type of the end value we are trying to set. - /// If the value was successfully retrieved or not. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("Expression evaluation requires dynamic code generation")] - [RequiresUnreferencedCode("Expression evaluation may reference members that could be trimmed")] -#endif + /// A sequence of expressions that point to properties/fields. + /// The value to set on the last property in the chain. + /// + /// If , throw when reflection members are missing; otherwise fail softly. + /// + /// if the value was successfully set; otherwise . + /// Thrown when is empty. + /// + /// Thrown when is and traversal is required. + /// + /// + /// Thrown when is and a required member is not settable. + /// + /// + /// Trimming note: this method may traverse arbitrary member chains represented by expressions; it is not possible + /// to express a complete trimming contract locally. + /// + [RequiresUnreferencedCode("Evaluates expression-based member chains via reflection; members may be trimmed.")] public static bool TrySetValueToPropertyChain(object? target, IEnumerable expressionChain, TValue value, bool shouldThrow = true) // TODO: Create Test { - var expressions = expressionChain.ToList(); - foreach (var expression in expressions.SkipLast(1)) + var expressions = MaterializeExpressions(expressionChain); + var count = expressions.Length; + + if (count == 0) { - var getter = shouldThrow ? - GetValueFetcherOrThrow(expression.GetMemberInfo()) : - GetValueFetcherForProperty(expression.GetMemberInfo()); + throw new InvalidOperationException("Expression chain must contain at least one element."); + } + + for (var i = 0; i < count - 1; i++) + { + var expression = expressions[i]; + + var getter = shouldThrow + ? GetValueFetcherOrThrow(expression.GetMemberInfo()) + : GetValueFetcherForProperty(expression.GetMemberInfo()); if (getter is not null) { @@ -303,16 +378,11 @@ public static bool TrySetValueToPropertyChain(object? target, IEnumerabl return false; } -#if NET6_0_OR_GREATER - var lastExpression = expressions[^1]; -#else -#pragma warning disable RCS1246 // Use element access - var lastExpression = expressions.Last(); -#pragma warning restore RCS1246 // Use element access -#endif - var setter = shouldThrow ? - GetValueSetterOrThrow(lastExpression.GetMemberInfo()) : - GetValueSetterForProperty(lastExpression.GetMemberInfo()); + var lastExpression = expressions[count - 1]; + + var setter = shouldThrow + ? GetValueSetterOrThrow(lastExpression.GetMemberInfo()) + : GetValueSetterForProperty(lastExpression.GetMemberInfo()); if (setter is null) { @@ -324,81 +394,157 @@ public static bool TrySetValueToPropertyChain(object? target, IEnumerabl } /// - /// Gets a Type from the specified type name. - /// Uses a cache to avoid having to use Reflection every time. + /// Gets a from the specified type name, using a cache to avoid repeated reflection. /// /// The name of the type. - /// If we should throw an exception if the type can't be found. - /// The type that was found or null. - /// If we were unable to find the type. + /// If , throw when the type cannot be found. + /// + /// The resolved , or if not found and is . + /// + /// + /// Thrown when the type cannot be found and is . + /// + /// + /// Trimming note: resolving types by string name is inherently trimming-unfriendly unless additional metadata is preserved externally. + /// + [RequiresUnreferencedCode("Resolves types by name and loads assemblies; types may be trimmed.")] public static Type? ReallyFindType(string? type, bool throwOnFailure) // TODO: Create Test { - var ret = _typeCache.Get(type ?? string.Empty); + var cache = Volatile.Read(ref _typeCache); + if (cache is null) + { + // Create inside the RUC boundary to avoid analyzer warnings from static initialization. + var created = new MemoizingMRUCache( + static (typeName, _) => GetTypeHelper(typeName), + 20); + + cache = Interlocked.CompareExchange(ref _typeCache, created, null) ?? created; + } + + var ret = cache.Get(type ?? string.Empty); return ret is not null || !throwOnFailure ? ret : throw new TypeLoadException(); } /// - /// Gets the appropriate EventArgs derived object for the specified event name for a Type. + /// Gets the appropriate -derived type for the specified event name on a . /// - /// The type of object to find the event on. + /// The type of object to find the event on. Must preserve public events under trimming. /// The name of the event. - /// The Type of the EventArgs to use. - /// If there is no event matching the name on the target type. -#if NET6_0_OR_GREATER - [RequiresUnreferencedCode("Event access may reference members that could be trimmed")] -#endif + /// The type of the event args used by the event handler. + /// Thrown when is . + /// Thrown if there is no event matching the name on the target type. + /// Thrown if the event handler type does not expose an Invoke method. + /// + /// Trimming note: the event handler type is obtained from , which does not carry + /// annotations. This prevents expressing a complete trimming contract here. + /// + [RequiresUnreferencedCode("Reflects over custom delegate Invoke signature; members may be trimmed.")] public static Type GetEventArgsTypeForEvent( -#if NET6_0_OR_GREATER - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] -#endif - Type type, - string? eventName) // TODO: Create Test + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type type, + string? eventName) // TODO: Create Test { ArgumentExceptionHelper.ThrowIfNull(type); - var ti = type; - var ei = ti.GetRuntimeEvent(eventName!); - if (ei is null || ei.EventHandlerType is null) + var eventInfo = type.GetRuntimeEvent(eventName!); + if (eventInfo is null || eventInfo.EventHandlerType is null) { throw new Exception($"Couldn't find {type.FullName}.{eventName}"); } - // Find the EventArgs type parameter of the event via digging around via reflection - return ei.EventHandlerType.GetRuntimeMethods().First(static x => x.Name == "Invoke").GetParameters()[1].ParameterType; + // Faster and allocation-free: do not enumerate runtime methods. + var invoke = eventInfo.EventHandlerType.GetMethod("Invoke") ?? throw new MissingMethodException(eventInfo.EventHandlerType.FullName, "Invoke"); + var parameters = invoke.GetParameters(); + return parameters[1].ParameterType; + } + + /// + /// Checks to make sure that the specified method names on the target type are overridden. + /// + /// The name of the calling type. + /// The type to check. Must preserve public and non-public methods under trimming. + /// The method names to check. + /// Thrown if any method is not overridden on the target type. + /// + /// Trimming note: this method inspects declared method names; the parameter is annotated accordingly. + /// + public static void ThrowIfMethodsNotOverloaded( + string callingTypeName, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods)] + Type targetType, + params string[] methodsToCheck) // TODO: Create Test + { + ArgumentExceptionHelper.ThrowIfNull(methodsToCheck); + + var methods = targetType.GetTypeInfo().DeclaredMethods; + + for (var i = 0; i < methodsToCheck.Length; i++) + { + var name = methodsToCheck[i]; + MethodInfo? found = null; + + foreach (var m in methods) + { + if (string.Equals(m.Name, name, StringComparison.Ordinal)) + { + found = m; + break; + } + } + + if (found is null) + { + throw new Exception($"Your class must implement {name} and call {callingTypeName}.{name}"); + } + } } /// - /// Checks to make sure that the specified method names on the target object - /// are overriden. + /// Checks to make sure that the specified method names on the target object are overridden. /// /// The name of the calling type. /// The object to check. - /// The name of the methods to check. - /// Thrown if the methods aren't overriden on the target object. -#if NET6_0_OR_GREATER - [RequiresUnreferencedCode("Method access may reference members that could be trimmed")] -#endif + /// The method names to check. + /// Thrown when is . + /// Thrown if any method is not overridden on the target object. + /// + /// Trimming note: the runtime type is discovered dynamically via , so this method + /// cannot express a complete trimming contract locally. + /// + [RequiresUnreferencedCode("Inspects declared methods on a runtime type; members may be trimmed.")] public static void ThrowIfMethodsNotOverloaded(string callingTypeName, object targetObject, params string[] methodsToCheck) // TODO: Create Test { - var (methodName, methodImplementation) = methodsToCheck - .Select(x => - { - var methods = targetObject.GetType().GetTypeInfo().DeclaredMethods; - return (methodName: x, methodImplementation: methods.FirstOrDefault(y => y.Name == x)); - }) - .FirstOrDefault(x => x.methodImplementation is null); - - if (methodName != default) + ArgumentExceptionHelper.ThrowIfNull(targetObject); + ArgumentExceptionHelper.ThrowIfNull(methodsToCheck); + + var methods = targetObject.GetType().GetTypeInfo().DeclaredMethods; + + for (var i = 0; i < methodsToCheck.Length; i++) { - throw new Exception($"Your class must implement {methodName} and call {callingTypeName}.{methodName}"); + var name = methodsToCheck[i]; + MethodInfo? found = null; + + foreach (var m in methods) + { + if (string.Equals(m.Name, name, StringComparison.Ordinal)) + { + found = m; + break; + } + } + + if (found is null) + { + throw new Exception($"Your class must implement {name} and call {callingTypeName}.{name}"); + } } } /// - /// Determines if the specified property is static or not. + /// Determines if the specified property is static. /// /// The property information to check. - /// If the property is static or not. + /// if the property is static; otherwise . + /// Thrown when is . public static bool IsStatic(this PropertyInfo item) // TODO: Create Test { ArgumentExceptionHelper.ThrowIfNull(item); @@ -407,10 +553,20 @@ public static bool IsStatic(this PropertyInfo item) // TODO: Create Test return method.IsStatic; } -#if NET6_0_OR_GREATER - [RequiresUnreferencedCode("ViewModelWhenAnyValue may reference types that could be trimmed")] - [RequiresDynamicCode("ViewModelWhenAnyValue uses reflection which requires dynamic code generation")] -#endif + /// + /// Creates an observable that switches to observing the provided expression on the current ViewModel. + /// + /// The view type. + /// The view model type. + /// The current view model (not used directly; preserved for signature compatibility). + /// The view instance. + /// The expression to observe dynamically. + /// An observable that emits values produced by the dynamic observation. + /// + /// Trimming note: dynamic observation via expression trees typically requires reflection over members + /// that may be trimmed; callers should preserve metadata for observed members. + /// + [RequiresUnreferencedCode("Dynamic observation uses reflection over members that may be trimmed.")] internal static IObservable ViewModelWhenAnyValue(TViewModel? viewModel, TView view, Expression? expression) where TView : class, IViewFor where TViewModel : class => @@ -419,28 +575,346 @@ internal static IObservable ViewModelWhenAnyValue(TVi .Select(x => ((TViewModel?)x).WhenAnyDynamic(expression, y => y.Value)) .Switch()!; -#if NET6_0_OR_GREATER - [RequiresUnreferencedCode("Method access may reference members that could be trimmed")] -#endif - private static Type? GetTypeHelper(string type) => Type.GetType( - type, - assemblyName => + /// + /// Attempts to resolve a type name using + /// with custom assembly resolution that first searches loaded assemblies and then tries to load by name. + /// + /// The type name. + /// The resolved type or . + /// + /// Trimming note: this is string-based type resolution and may fail under trimming without explicit preservation. + /// + [RequiresUnreferencedCode("Resolves types by name and loads assemblies; types may be trimmed.")] + private static Type? GetTypeHelper(string type) => + Type.GetType( + type, + assemblyName => + { + var assemblies = AppDomain.CurrentDomain.GetAssemblies(); + for (var i = 0; i < assemblies.Length; i++) + { + var a = assemblies[i]; + if (a.FullName == assemblyName.FullName) + { + return a; + } + } + + try + { + return Assembly.Load(assemblyName); + } + catch + { + return null; + } + }, + null, + false); + + /// + /// Materializes an expression chain into an array to enable index-based iteration without LINQ. + /// + /// The expression chain to materialize. + /// An array containing the expressions in enumeration order. + /// Thrown when is . + /// + /// This helper is used to reduce allocations and virtual dispatch in hot paths by enabling for-loop iteration. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Expression[] MaterializeExpressions(IEnumerable expressionChain) + { + ArgumentExceptionHelper.ThrowIfNull(expressionChain); + + if (expressionChain is Expression[] arr) + { + return arr; + } + + if (expressionChain is ICollection coll) + { + if (coll.Count == 0) + { + return Array.Empty(); + } + + var result = new Expression[coll.Count]; + coll.CopyTo(result, 0); + return result; + } + + return expressionChain.ToArray(); + } + + /// + /// Pre-compiled property chain that caches getter delegates and indexer arguments. + /// + /// The root type expected by the expression chain. + /// The final value type. + /// + /// + /// This type exists to move expression-chain enumeration and member resolution out of observable hot paths. + /// After construction, and execute using cached delegates. + /// + /// + /// Trimming note: constructing this type typically involves expression parsing and may be trimming-sensitive; + /// callers should treat construction as the “reflection boundary”. + /// + /// + internal sealed class CompiledPropertyChain + { + /// + /// Cached getter delegates for each expression step. + /// + private readonly Func[] _getters; + + /// + /// Cached argument arrays for each expression step (indexers); entries may be . + /// + private readonly object?[]?[] _arguments; + + /// + /// Cached expressions for each step, used for constructing observed changes. + /// + private readonly Expression[] _expressions; + + /// + /// Initializes a new instance of the class. + /// + /// The expression chain to compile. Must contain at least one expression. + /// Thrown when is . + /// Thrown when is empty. + public CompiledPropertyChain(Expression[] expressionChain) + { + ArgumentExceptionHelper.ThrowIfNull(expressionChain); + + if (expressionChain.Length == 0) + { + throw new InvalidOperationException("Expression chain must contain at least one element."); + } + + _expressions = expressionChain; + _getters = new Func[expressionChain.Length]; + _arguments = new object?[]?[expressionChain.Length]; + + for (var i = 0; i < expressionChain.Length; i++) + { + var expr = expressionChain[i]; + _getters[i] = GetValueFetcherOrThrow(expr.GetMemberInfo()); + _arguments[i] = expr.GetArgumentsArray(); + } + } + + /// + /// Attempts to get the final value from the property chain. + /// + /// The root object. + /// Receives the final value when successful. + /// if successful; otherwise . + public bool TryGetValue(TSource? source, out TValue value) { - var assembly = Array.Find(AppDomain.CurrentDomain.GetAssemblies(), z => z.FullName == assemblyName.FullName); - if (assembly is not null) + object? current = source; + var lastIndex = _getters.Length - 1; + + for (var i = 0; i < lastIndex; i++) + { + if (current is null) + { + value = default!; + return false; + } + + current = _getters[i](current, _arguments[i]); + } + + if (current is null) { - return assembly; + value = default!; + return false; } - try + value = (TValue)_getters[lastIndex](current, _arguments[lastIndex])!; + return true; + } + + /// + /// Attempts to get all intermediate values in the property chain as observed changes. + /// + /// The root object. + /// Receives an array with one entry per expression in the chain. + /// if successful; otherwise . + /// + /// Mirrors behavior: on early failure at index i, + /// writes changeValues[i] = null! and returns . + /// + public bool TryGetAllValues(TSource? source, out IObservedChange[] changeValues) + { + var count = _expressions.Length; + changeValues = new IObservedChange[count]; + + object? current = source; + var lastIndex = count - 1; + + for (var i = 0; i < lastIndex; i++) { - return Assembly.Load(assemblyName); + if (current is null) + { + changeValues[i] = null!; + return false; + } + + var sender = current; + current = _getters[i](current, _arguments[i]); + changeValues[i] = new ObservedChange(sender, _expressions[i], current); } - catch + + if (current is null) { - return null; + changeValues[lastIndex] = null!; + return false; } - }, - null, - false); + + changeValues[lastIndex] = new ObservedChange( + current, + _expressions[lastIndex], + _getters[lastIndex](current, _arguments[lastIndex])); + + return true; + } + } + + /// + /// Pre-compiled setter for a property chain. + /// + /// The root type expected by the expression chain. + /// The value type to set. + /// + /// + /// This type is designed for binding hot paths: traversal and setter invocation is performed using cached delegates. + /// It does not synthesize for intermediate nulls; it follows “Try*” semantics. + /// + /// + /// Trimming note: construction is typically the “reflection boundary”. + /// + /// + internal sealed class CompiledPropertyChainSetter + { + /// + /// Cached getter delegates used to walk from the root to the parent of the final member. + /// + private readonly Func[] _parentGetters; + + /// + /// Cached argument arrays for each parent step (indexers); entries may be . + /// + private readonly object?[]?[] _parentArguments; + + /// + /// Cached setter delegate for the final member; may be if the member is not settable. + /// + private readonly Action? _setter; + + /// + /// Cached argument array for the final setter (indexer); may be . + /// + private readonly object?[]? _setterArguments; + + /// + /// Cached error message for throwing when the final member is not settable. + /// + private readonly string _unsettableMemberMessage; + + /// + /// Initializes a new instance of the class. + /// + /// The expression chain to compile. Must contain at least one expression. + /// Thrown when is . + /// Thrown when is empty. + public CompiledPropertyChainSetter(Expression[] expressionChain) + { + ArgumentExceptionHelper.ThrowIfNull(expressionChain); + + if (expressionChain.Length == 0) + { + throw new InvalidOperationException("Expression chain must contain at least one element."); + } + + if (expressionChain.Length == 1) + { + _parentGetters = Array.Empty>(); + _parentArguments = Array.Empty(); + } + else + { + var parentCount = expressionChain.Length - 1; + _parentGetters = new Func[parentCount]; + _parentArguments = new object?[]?[parentCount]; + + for (var i = 0; i < parentCount; i++) + { + var expr = expressionChain[i]; + _parentGetters[i] = GetValueFetcherOrThrow(expr.GetMemberInfo()); + _parentArguments[i] = expr.GetArgumentsArray(); + } + } + + var lastExpr = expressionChain[expressionChain.Length - 1]; + _setter = GetValueSetterForProperty(lastExpr.GetMemberInfo()); + _setterArguments = lastExpr.GetArgumentsArray(); + + // Preserve legacy-style message format used by OrThrow helpers (type + member name). + var member = lastExpr.GetMemberInfo(); + _unsettableMemberMessage = $"Type '{member?.DeclaringType}' must have a property '{member?.Name}'"; + } + + /// + /// Attempts to set the value at the end of the property chain. + /// + /// The root object. + /// The value to set. + /// + /// If , throws for a null root and for an unsettable final member. + /// If , returns when the chain cannot be navigated or set. + /// + /// if the set succeeded; otherwise . + /// Thrown when is and is . + /// Thrown when the final member is not settable and is . + public bool TrySetValue(TSource? source, TValue value, bool shouldThrow = true) + { + object? current = source; + + if (current is null) + { + if (shouldThrow) + { + throw new ArgumentNullException(nameof(source)); + } + + return false; + } + + for (var i = 0; i < _parentGetters.Length; i++) + { + current = _parentGetters[i](current, _parentArguments[i]); + if (current is null) + { + // Preserve Try* semantics: intermediate nulls soft-fail; do not synthesize exceptions. + return false; + } + } + + if (_setter is null) + { + if (shouldThrow) + { + throw new ArgumentException(_unsettableMemberMessage); + } + + return false; + } + + _setter(current, value, _setterArguments); + return true; + } + } } diff --git a/src/ReactiveUI/Helpers/DoesNotReturnIfAttribute.cs b/src/ReactiveUI/Helpers/DoesNotReturnIfAttribute.cs deleted file mode 100644 index 0f9220da64..0000000000 --- a/src/ReactiveUI/Helpers/DoesNotReturnIfAttribute.cs +++ /dev/null @@ -1,37 +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. - -#if !NET5_0_OR_GREATER - -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; - -namespace System.Diagnostics.CodeAnalysis; - -/// -/// Indicates that a parameter captures the expression passed for another parameter as a string. -/// -[ExcludeFromCodeCoverage] -[DebuggerNonUserCode] -[AttributeUsage(AttributeTargets.Parameter)] -internal sealed class DoesNotReturnIfAttribute : Attribute -{ - /// Initializes a new instance of the class.. - /// - /// The condition parameter value. Code after the method will be considered unreachable by diagnostics if the argument to - /// the associated parameter matches this value. - /// - public DoesNotReturnIfAttribute(bool parameterValue) => ParameterValue = parameterValue; - - /// Gets a value indicating whether the condition parameter value. - public bool ParameterValue { get; } -} - -#else -using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; - -[assembly: TypeForwardedTo(typeof(DoesNotReturnIfAttribute))] -#endif diff --git a/src/ReactiveUI/Interfaces/ICreatesCommandBinding.cs b/src/ReactiveUI/Interfaces/ICreatesCommandBinding.cs index 882134951b..8c5d044b04 100644 --- a/src/ReactiveUI/Interfaces/ICreatesCommandBinding.cs +++ b/src/ReactiveUI/Interfaces/ICreatesCommandBinding.cs @@ -10,28 +10,10 @@ namespace ReactiveUI; /// /// Classes that implement this interface and registered inside Splat will be /// used to potentially provide binding to a ICommand in the ViewModel to a Control -/// in the View. +/// in the View. This interface is fully AOT-compatible using generic type parameters. /// public interface ICreatesCommandBinding { - /// - /// Returns a positive integer when this class supports - /// BindCommandToObject for this particular Type. If the method - /// isn't supported at all, return a non-positive integer. When multiple - /// implementations return a positive value, the host will use the one - /// which returns the highest value. When in doubt, return '2' or '0'. - /// - /// The type to query for. - /// If true, the host intends to use a custom - /// event target. - /// A positive integer if BCTO is supported, zero or a negative - /// value otherwise. -#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 - int GetAffinityForObject(Type type, bool hasEventTarget); - /// /// Returns a positive integer when this class supports binding a command /// to an object of the specified type. If the binding is not supported, @@ -42,62 +24,75 @@ public interface ICreatesCommandBinding /// Determines if the host intends to use a custom event target. /// The type of the object to query for compatibility with command binding. /// A positive integer if binding is supported, or zero/a negative value if not supported. -#if NET6_0_OR_GREATER - int GetAffinityForObject<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicEvents | DynamicallyAccessedMemberTypes.PublicProperties)] T>( - bool hasEventTarget); -#else - int GetAffinityForObject( - bool hasEventTarget); -#endif + int GetAffinityForObject<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicEvents | DynamicallyAccessedMemberTypes.PublicProperties)] T>(bool hasEventTarget); /// /// Bind an ICommand to a UI object, in the "default" way. The meaning - /// of this is dependent on the implementation. Implement this if you - /// have a new type of UI control that doesn't have + /// of this is dependent on the implementation. This method will discover + /// which event to bind to (e.g., Click, MouseUp) based on the control type. + /// Implement this if you have a new type of UI control that doesn't have /// Command/CommandParameter like WPF or has a non-standard event name /// for "Invoke". /// - /// The command to bind. - /// The target object, usually a UI control of - /// some kind. + /// The type of the target object to which the command is bound. Must be a reference type. + /// The command to bind. Can be null. + /// The target object, usually a UI control of some kind. Can be null. /// An IObservable source whose latest /// value will be passed as the command parameter to the command. Hosts /// will always pass a valid IObservable, but this may be /// Observable.Empty. - /// An IDisposable which will disconnect the binding when - /// disposed. -#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 - IDisposable? BindCommandToObject(ICommand? command, object? target, IObservable commandParameter); + /// An IDisposable which will disconnect the binding when disposed, or null if no binding was created. + [RequiresUnreferencedCode("String/reflection-based event binding may require members removed by trimming.")] + IDisposable? BindCommandToObject<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicEvents | DynamicallyAccessedMemberTypes.NonPublicEvents)] T>( + ICommand? command, + T? target, + IObservable commandParameter) + where T : class; /// /// Bind an ICommand to a UI object to a specific event. This event may /// be a standard .NET event, or it could be an event derived in another - /// manner (i.e. in MonoTouch). + /// manner (i.e. in MonoTouch). This method is fully AOT-compatible as it + /// uses generic type parameters instead of reflection. /// + /// The type of the target object to which the command is bound. Must be a reference type. /// The event argument type. - /// The command to bind. - /// The target object, usually a UI control of - /// some kind. + /// The command to bind. Can be null. + /// The target object, usually a UI control of some kind. Can be null. /// An IObservable source whose latest - /// value will be passed as the command parameter to the command. Hosts - /// will always pass a valid IObservable, but this may be - /// Observable.Empty. + /// value will be passed as the command parameter to the command. Hosts + /// will always pass a valid IObservable, but this may be + /// Observable.Empty. /// The event to bind to. - /// An IDisposable which will disconnect the binding when disposed. -#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 - IDisposable BindCommandToObject( + /// An IDisposable which will disconnect the binding when disposed, or null if no binding was created. + [RequiresUnreferencedCode("String/reflection-based event binding may require members removed by trimming.")] + IDisposable? BindCommandToObject( ICommand? command, - object? target, + T? target, IObservable commandParameter, string eventName) -#if MONO - where TEventArgs : EventArgs -#endif - ; + where T : class; + + /// + /// Binds a command to a specific event on a target object using explicit add/remove handler delegates. + /// + /// The type of the target object. + /// The event arguments type. + /// The command to bind. If , no binding is created. + /// The target object. + /// An observable that supplies command parameter values. + /// Adds the handler to the target event. + /// Removes the handler from the target event. + /// A disposable that unbinds the command. + /// + /// Thrown when , , or is . + /// + IDisposable? BindCommandToObject<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicEvents | DynamicallyAccessedMemberTypes.NonPublicEvents)] T, TEventArgs>( + ICommand? command, + T? target, + IObservable commandParameter, + Action> addHandler, + Action> removeHandler) + where T : class + where TEventArgs : EventArgs; } diff --git a/src/ReactiveUI/Interfaces/ICreatesObservableForProperty.cs b/src/ReactiveUI/Interfaces/ICreatesObservableForProperty.cs index 7e29574a1a..60fdbd2e85 100644 --- a/src/ReactiveUI/Interfaces/ICreatesObservableForProperty.cs +++ b/src/ReactiveUI/Interfaces/ICreatesObservableForProperty.cs @@ -6,51 +6,69 @@ namespace ReactiveUI; /// -/// ICreatesObservableForProperty represents an object that knows how to -/// create notifications for a given type of object. Implement this if you -/// are porting RxUI to a new UI toolkit, or generally want to enable WhenAny -/// for another type of object that can be observed in a unique way. +/// represents a component that can produce change notifications for a +/// given property on a given object. /// +/// +/// Implementations are typically platform-specific (e.g., a UI toolkit) but this interface must remain platform-agnostic. +/// public interface ICreatesObservableForProperty : IEnableLogger { /// - /// Returns a positive integer when this class supports - /// GetNotificationForProperty for this particular Type. If the method - /// isn't supported at all, return a non-positive integer. When multiple - /// implementations return a positive value, the host will use the one - /// which returns the highest value. When in doubt, return '2' or '0'. + /// Returns a positive integer when this instance supports for + /// the specified and . /// - /// The type to query for. - /// The property of the type to query for. - /// If true, returns whether GNFP is supported before a change occurs. - /// A positive integer if GNFP is supported, zero or a negative - /// value otherwise. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("GetAffinityForObject uses methods that require dynamic code generation")] - [RequiresUnreferencedCode("GetAffinityForObject uses methods that may require unreferenced code")] -#endif + /// + /// + /// If the method is not supported, return a non-positive integer. + /// When multiple implementations return a positive value, the host selects the highest value. + /// + /// + /// Implementations should avoid expensive work here; this is typically a hot-path query. + /// + /// + /// The runtime type to query. + /// The property name to query. + /// + /// If , indicates the caller requests notifications before the property value changes. + /// If , indicates after-change notifications. + /// + /// + /// A positive integer if supported; zero or a negative value otherwise. + /// + [RequiresUnreferencedCode("Uses reflection over runtime types which is not trim- or AOT-safe.")] int GetAffinityForObject(Type type, string propertyName, bool beforeChanged = false); /// - /// Subscribe to notifications on the specified property, given an - /// object and a property name. + /// Subscribes to change notifications for the specified on . /// /// The object to observe. - /// The expression on the object to observe. - /// This will be either a MemberExpression or an IndexExpression - /// depending on the property. + /// + /// The expression describing the observed member. + /// This is typically a MemberExpression or an IndexExpression. + /// + /// The property name to observe. + /// + /// If , signal before the property value changes; otherwise signal after the change. /// - /// The property of the type to query for. - /// If true, signal just before the - /// property value actually changes. If false, signal after the - /// property changes. - /// If true, no warnings should be logged. - /// An IObservable which is signaled whenever the specified - /// property on the object changes. If this cannot be done for a - /// specified value of beforeChanged, return Observable.Never. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("GetNotificationForProperty uses methods that require dynamic code generation")] - [RequiresUnreferencedCode("GetNotificationForProperty uses methods that may require unreferenced code")] -#endif - IObservable> GetNotificationForProperty(object sender, Expression expression, string propertyName, bool beforeChanged = false, bool suppressWarnings = false); + /// If , warnings should not be logged. + /// + /// An observable that produces an whenever the observed property changes. + /// If observing is not possible for the specified value, implementations should return + /// an observable that never produces values. + /// + /// + /// Thrown when is not compatible with the observing mechanism implemented by the instance. + /// + /// + /// The describes the observed member and is used to populate + /// instances emitted by the returned observable. + /// + [RequiresUnreferencedCode("Uses reflection over runtime types which is not trim- or AOT-safe.")] + IObservable> GetNotificationForProperty( + object sender, + Expression expression, + string propertyName, + bool beforeChanged = false, + bool suppressWarnings = false); } diff --git a/src/ReactiveUI/Interfaces/IHandleObservableErrors.cs b/src/ReactiveUI/Interfaces/IHandleObservableErrors.cs index 17da8775a3..867676b9ff 100644 --- a/src/ReactiveUI/Interfaces/IHandleObservableErrors.cs +++ b/src/ReactiveUI/Interfaces/IHandleObservableErrors.cs @@ -14,7 +14,7 @@ namespace ReactiveUI; /// /// /// Normally this IObservable is implemented with a ScheduledSubject whose -/// default Observer is RxApp.DefaultExceptionHandler - this means, that if +/// default Observer is RxSchedulers.DefaultExceptionHandler - this means, that if /// you aren't listening to ThrownExceptions and one appears, the exception /// will appear on the UI thread and crash the application. /// diff --git a/src/ReactiveUI/Interfaces/IPropertyBindingHook.cs b/src/ReactiveUI/Interfaces/IPropertyBindingHook.cs index 8517cbf590..3ad6089850 100644 --- a/src/ReactiveUI/Interfaces/IPropertyBindingHook.cs +++ b/src/ReactiveUI/Interfaces/IPropertyBindingHook.cs @@ -20,10 +20,6 @@ public interface IPropertyBindingHook /// Get current view model properties. /// Get current view properties. /// The Binding direction. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("ExecuteHook uses methods that require dynamic code generation")] - [RequiresUnreferencedCode("ExecuteHook uses methods that may require unreferenced code")] -#endif bool ExecuteHook( object? source, object target, diff --git a/src/ReactiveUI/Interfaces/IReactiveNotifyPropertyChanged.cs b/src/ReactiveUI/Interfaces/IReactiveNotifyPropertyChanged.cs index 7113263b87..b393c3d29a 100644 --- a/src/ReactiveUI/Interfaces/IReactiveNotifyPropertyChanged.cs +++ b/src/ReactiveUI/Interfaces/IReactiveNotifyPropertyChanged.cs @@ -33,9 +33,5 @@ public interface IReactiveNotifyPropertyChanged /// /// An object that, when disposed, reenables change /// notifications. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("SuppressChangeNotifications uses extension methods that require dynamic code generation")] - [RequiresUnreferencedCode("SuppressChangeNotifications uses extension methods that may require unreferenced code")] -#endif IDisposable SuppressChangeNotifications(); } diff --git a/src/ReactiveUI/Interfaces/IRegistrar.cs b/src/ReactiveUI/Interfaces/IRegistrar.cs new file mode 100644 index 0000000000..55860fb1fb --- /dev/null +++ b/src/ReactiveUI/Interfaces/IRegistrar.cs @@ -0,0 +1,44 @@ +// 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. + +namespace ReactiveUI; + +/// +/// Interface for registering services in a dependency injection container in an AOT-friendly manner. +/// This interface provides generic registration methods that preserve type information at compile time, +/// avoiding the need for runtime Type reflection and DynamicallyAccessedMembers attributes. +/// +public interface IRegistrar +{ + /// + /// Registers a constant value for a service type. The factory function is called once + /// and the result is registered as a singleton. + /// + /// The service type to register. + /// A factory function that creates the service instance. + /// An optional contract name for multiple registrations of the same type. + void RegisterConstant(Func factory, string? contract = null) + where TService : class; + + /// + /// Registers a lazy singleton for a service type. The factory function is called + /// the first time the service is resolved, and the same instance is returned for all subsequent resolutions. + /// + /// The service type to register. + /// A factory function that creates the service instance. + /// An optional contract name for multiple registrations of the same type. + void RegisterLazySingleton<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TService>(Func factory, string? contract = null) + where TService : class; + + /// + /// Registers a factory for a service type. The factory function is called each time + /// the service is resolved, creating a new instance. + /// + /// The service type to register. + /// A factory function that creates the service instance. + /// An optional contract name for multiple registrations of the same type. + void Register(Func factory, string? contract = null) + where TService : class; +} diff --git a/src/ReactiveUI/Interfaces/ISuspensionDriver.cs b/src/ReactiveUI/Interfaces/ISuspensionDriver.cs index 4bbf0d3ee4..889d5fe487 100644 --- a/src/ReactiveUI/Interfaces/ISuspensionDriver.cs +++ b/src/ReactiveUI/Interfaces/ISuspensionDriver.cs @@ -3,43 +3,99 @@ // 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.Text.Json.Serialization.Metadata; + namespace ReactiveUI; /// -/// ISuspensionDriver represents a class that can load/save state to persistent -/// storage. Most platforms have a basic implementation of this class, but you -/// probably want to write your own. +/// Represents a driver capable of loading and saving application state +/// to persistent storage. /// +/// +/// +/// This interface supports both legacy reflection-based serialization +/// and trimming/AOT-safe serialization using System.Text.Json source generation. +/// +/// +/// Implementations that support trimming or AOT should prefer the overloads +/// that accept . +/// +/// public interface ISuspensionDriver { /// /// Loads the application state from persistent storage. /// - /// An object observable. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("LoadState implementations may use serialization which requires dynamic code generation")] - [RequiresUnreferencedCode("LoadState implementations may use serialization which may require unreferenced code")] -#endif + /// + /// An observable that produces the deserialized application state + /// (or ). + /// + /// + /// This member typically relies on reflection-based serialization and is not + /// trimming or AOT friendly. + /// + [RequiresUnreferencedCode( + "Implementations commonly use reflection-based serialization. " + + "Prefer LoadState(JsonTypeInfo) for trimming or AOT scenarios.")] + [RequiresDynamicCode( + "Implementations commonly use reflection-based serialization. " + + "Prefer LoadState(JsonTypeInfo) for trimming or AOT scenarios.")] IObservable LoadState(); /// - /// Saves the application state to disk. + /// Saves the application state to persistent storage. + /// + /// The type of the application state. + /// The application state to persist. + /// + /// An observable that completes when the state has been saved. + /// + /// + /// This member typically relies on reflection-based serialization and is not + /// trimming or AOT friendly. + /// + [RequiresUnreferencedCode( + "Implementations commonly use reflection-based serialization. " + + "Prefer SaveState(T, JsonTypeInfo) for trimming or AOT scenarios.")] + [RequiresDynamicCode( + "Implementations commonly use reflection-based serialization. " + + "Prefer SaveState(T, JsonTypeInfo) for trimming or AOT scenarios.")] + IObservable SaveState(T state); + + /// + /// Loads application state from persistent storage using + /// source-generated System.Text.Json metadata. + /// + /// The expected state type. + /// + /// The source-generated metadata for . + /// + /// + /// An observable that produces the deserialized state + /// (or ). + /// + IObservable LoadState(JsonTypeInfo typeInfo); + + /// + /// Saves application state to persistent storage using + /// source-generated System.Text.Json metadata. /// - /// The application state. - /// A completed observable. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("SaveState implementations may use serialization which requires dynamic code generation")] - [RequiresUnreferencedCode("SaveState implementations may use serialization which may require unreferenced code")] -#endif - IObservable SaveState(object state); + /// The state type. + /// The state to persist. + /// + /// The source-generated metadata for . + /// + /// + /// An observable that completes when persistence succeeds. + /// + IObservable SaveState(T state, JsonTypeInfo typeInfo); /// - /// Invalidates the application state (i.e. deletes it from disk). + /// Invalidates the persisted application state + /// (for example, by deleting it from disk). /// - /// A completed observable. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("InvalidateState uses JsonSerializer which requires dynamic code generation")] - [RequiresUnreferencedCode("InvalidateState uses JsonSerializer which may require unreferenced code")] -#endif + /// + /// An observable that completes when the state has been invalidated. + /// IObservable InvalidateState(); } diff --git a/src/ReactiveUI/Interfaces/ISuspensionHost.cs b/src/ReactiveUI/Interfaces/ISuspensionHost.cs index 06343db148..50c24d437c 100644 --- a/src/ReactiveUI/Interfaces/ISuspensionHost.cs +++ b/src/ReactiveUI/Interfaces/ISuspensionHost.cs @@ -25,7 +25,7 @@ namespace ReactiveUI; /// /// These observables abstract platform terms such as "Launching", "Activated", and "Closing" into a /// consistent API so shared code can persist state without branching on specific UI stacks. Most -/// applications call RxApp.SuspensionHost.SetupDefaultSuspendResume() during startup to wire +/// applications call RxSuspension.SuspensionHost.SetupDefaultSuspendResume() during startup to wire /// default handlers, but the properties are public so advanced hosts can plug in their own monitoring. /// /// @@ -37,7 +37,7 @@ namespace ReactiveUI; /// /// /// new ShellState(); /// /// suspensionHost.IsLaunchingNew.Subscribe(_ => diff --git a/src/ReactiveUI/Interfaces/ISuspensionHost{TAppState}.cs b/src/ReactiveUI/Interfaces/ISuspensionHost{TAppState}.cs new file mode 100644 index 0000000000..a1dd922d8d --- /dev/null +++ b/src/ReactiveUI/Interfaces/ISuspensionHost{TAppState}.cs @@ -0,0 +1,58 @@ +// 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. + +namespace ReactiveUI.Interfaces; + +/// +/// Represents a standardized version of host operating system lifecycle signals with a strongly-typed application state. +/// +/// The application state type. +/// +/// +/// This interface is a strongly-typed companion to . It remains platform-agnostic and +/// retains the same lifecycle observables, while providing a typed surface. +/// +/// +/// Compatibility: this interface derives from . Implementations should typically +/// implement explicitly to project the typed state through the legacy object-based +/// contract. +/// +/// +public interface ISuspensionHost : ISuspensionHost +{ + /// + /// Gets or sets a function that can be used to create a new application state instance. + /// + /// + /// This is the typed counterpart to and is typically used when + /// the application is launching fresh or recovering from an invalidated state. + /// + Func? CreateNewAppStateTyped { get; set; } + + /// + /// Gets or sets the current application state. + /// + /// + /// This is the typed counterpart to . Implementations should ensure that + /// the legacy view remains consistent with this property. + /// + TAppState? AppStateValue { get; set; } + + /// + /// Gets an observable that signals when is assigned. + /// + /// + /// + /// This is a trimming/AOT-friendly change signal for app state updates. + /// Consumers can use this to observe state transitions without relying on ReactiveUI + /// property-change expression pipelines. + /// + /// + /// The observable does not guarantee replay; consumers that need the current value should combine this with + /// (or use an extension that emits the current value first). + /// + /// + IObservable AppStateValueChanged { get; } +} diff --git a/src/ReactiveUI/Interfaces/IViewLocator.cs b/src/ReactiveUI/Interfaces/IViewLocator.cs index 1aa8922990..a78b338abf 100644 --- a/src/ReactiveUI/Interfaces/IViewLocator.cs +++ b/src/ReactiveUI/Interfaces/IViewLocator.cs @@ -18,18 +18,23 @@ namespace ReactiveUI; /// /// /// (T? viewModel, string? contract = null) +/// private readonly Dictionary> _mappings = new(); +/// +/// public void Register(Func factory) +/// where TViewModel : class +/// where TView : class, IViewFor /// { -/// if (viewModel is null) -/// { -/// return null; -/// } +/// _mappings[typeof(TViewModel)] = () => factory(); +/// } /// -/// var viewTypeName = viewModel.GetType().FullName!.Replace("ViewModel", "View", StringComparison.Ordinal); -/// var viewType = Type.GetType(viewTypeName); -/// return viewType is null ? null : (IViewFor?)Activator.CreateInstance(viewType); +/// public IViewFor? ResolveView(string? contract = null) +/// where TViewModel : class +/// { +/// return _mappings.TryGetValue(typeof(TViewModel), out var factory) +/// ? (IViewFor)factory() +/// : null; /// } /// } /// ]]> @@ -38,15 +43,21 @@ namespace ReactiveUI; public interface IViewLocator : IEnableLogger { /// - /// Determines the view for an associated view model. + /// Resolves a view for a view model type known at compile time. Fully AOT-compatible. + /// + /// The view model type to resolve a view for. + /// Optional contract allowing multiple view registrations per view model. + /// The resolved view or when no registration is available. + IViewFor? ResolveView(string? contract = null) + where TViewModel : class; + + /// + /// Resolves a view for a view model type known at compile time. Fully AOT-compatible. /// - /// The view model type. - /// The view model for which a view is required. + /// The view model instance to resolve a view for. /// Optional contract allowing multiple view registrations per view model. /// The resolved view or when no registration is available. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("ResolveView uses reflection and type discovery which require dynamic code generation")] - [RequiresUnreferencedCode("ResolveView uses reflection and type discovery which may require unreferenced code")] -#endif - IViewFor? ResolveView(T? viewModel, string? contract = null); + [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.")] + IViewFor? ResolveView(object? instance, string? contract = null); } diff --git a/src/ReactiveUI/Interfaces/IViewModule.cs b/src/ReactiveUI/Interfaces/IViewModule.cs new file mode 100644 index 0000000000..756c609b9b --- /dev/null +++ b/src/ReactiveUI/Interfaces/IViewModule.cs @@ -0,0 +1,39 @@ +// 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. + +namespace ReactiveUI; + +/// +/// Represents a module that registers view-to-viewmodel mappings for AOT-compatible view resolution. +/// +/// +/// +/// View modules provide a way to organize view registrations by feature or module. +/// Implement this interface to create reusable, testable view registration logic. +/// +/// +/// +/// +/// (() => new LoginView()) +/// .Map(() => new RegisterView()) +/// .Map(() => new ForgotPasswordView()); +/// } +/// } +/// ]]> +/// +/// +public interface IViewModule +{ + /// + /// Registers view-to-viewmodel mappings with the provided view locator. + /// + /// The view locator to register mappings with. + void RegisterViews(DefaultViewLocator locator); +} diff --git a/src/ReactiveUI/Interfaces/IWantsToRegisterStuff.cs b/src/ReactiveUI/Interfaces/IWantsToRegisterStuff.cs index ae7691cd2d..745678ecc2 100644 --- a/src/ReactiveUI/Interfaces/IWantsToRegisterStuff.cs +++ b/src/ReactiveUI/Interfaces/IWantsToRegisterStuff.cs @@ -6,19 +6,42 @@ namespace ReactiveUI; /// -/// Used by ReactiveUI when first starting up, it will seek out classes -/// inside our own ReactiveUI projects. The implemented methods will -/// register with Splat their dependencies. +/// Represents a class that can register services with ReactiveUI's dependency resolver. /// +/// +/// +/// This interface is used with the ReactiveUI builder pattern to provide custom registrations. +/// The registration methods use generic types to avoid runtime reflection, making them compatible +/// with AOT compilation and trimming. +/// +/// +/// Usage with builder: +/// +/// public class MyCustomRegistrations : IWantsToRegisterStuff +/// { +/// public void Register(IRegistrar registrar) +/// { +/// registrar.RegisterConstant<IMyService>(() => new MyService()); +/// registrar.RegisterLazySingleton<IMyViewModel>(() => new MyViewModel()); +/// } +/// } +/// +/// // In your app initialization: +/// RxAppBuilder.CreateReactiveUIBuilder() +/// .WithCoreServices() +/// .WithPlatformServices() +/// .WithRegistration(new MyCustomRegistrations()) +/// .BuildApp(); +/// +/// +/// public interface IWantsToRegisterStuff { /// - /// Register platform dependencies inside Splat. + /// Register platform dependencies using the provided registrar. + /// This method uses generic registration to avoid runtime Type reflection, + /// making it compatible with AOT compilation and trimming. /// - /// A method the deriving class will class to register the type. -#if NET6_0_OR_GREATER - [RequiresUnreferencedCode("Uses reflection to create instances of types.")] - [RequiresDynamicCode("Uses reflection to create instances of types.")] -#endif - void Register(Action, Type> registerFunction); + /// The AOT-friendly registrar to use for registering services. + void Register(IRegistrar registrar); } diff --git a/src/ReactiveUI/IsExternalInit.cs b/src/ReactiveUI/IsExternalInit.cs deleted file mode 100644 index 0eceb00cfc..0000000000 --- a/src/ReactiveUI/IsExternalInit.cs +++ /dev/null @@ -1,13 +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. - -namespace System.Runtime.CompilerServices; - -/// -/// Reserved to be used by the compiler for tracking metadata. -/// This class should not be used by developers in source code. -/// -[EditorBrowsable(EditorBrowsableState.Never)] -internal static class IsExternalInit; diff --git a/src/ReactiveUI/Mixins/AutoPersistHelper.cs b/src/ReactiveUI/Mixins/AutoPersistHelper.cs index fdeb7a31a7..321bc1e744 100644 --- a/src/ReactiveUI/Mixins/AutoPersistHelper.cs +++ b/src/ReactiveUI/Mixins/AutoPersistHelper.cs @@ -5,30 +5,40 @@ using System.Collections.Specialized; using System.Reflection; +using System.Runtime.CompilerServices; using DynamicData; using DynamicData.Binding; +using ReactiveUI.Builder; + namespace ReactiveUI; /// /// Helper extension method class associated with the AutoPersist related functionality. /// -#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 class AutoPersistHelper { - private static readonly MemoizingMRUCache> _persistablePropertiesCache = new( - static (type, _) => type.GetTypeInfo().DeclaredProperties - .Where(static x => x.CustomAttributes.Any(static y => typeof(DataMemberAttribute).GetTypeInfo().IsAssignableFrom(y.AttributeType.GetTypeInfo()))) - .ToDictionary(static k => k.Name, static _ => true), - RxApp.SmallCacheLimit); + /// + /// Stores per-runtime-type persistence metadata computed via reflection. + /// + /// + /// + /// This cache is intentionally non-evicting for correctness and predictability. The number of distinct reactive + /// object runtime types in a typical application is small and stable; MRU eviction introduces churn and can + /// re-trigger expensive reflection. + /// + /// + /// This cache is used only when callers use the legacy reflection-based overloads and + /// the generic type does not match the runtime type of the instance. + /// + /// + private static readonly ConditionalWeakTable PersistMetadataByType = new(); - private static readonly MemoizingMRUCache _dataContractCheckCache = new( - static (t, _) => t.GetTypeInfo().GetCustomAttributes(typeof(DataContractAttribute), true).Length > 0, - RxApp.SmallCacheLimit); + /// + /// Initializes static members of the class. + /// + static AutoPersistHelper() => RxAppBuilder.EnsureInitialized(); /// /// AutoPersist allows you to automatically call a method when an object @@ -38,20 +48,36 @@ public static class AutoPersistHelper /// object to be saved. /// /// The reactive object type. - /// - /// The reactive object to watch for changes. - /// - /// - /// The asynchronous method to call to save the object to disk. - /// + /// The reactive object to watch for changes. + /// The asynchronous method to call to save the object to disk. /// /// The interval to save the object on. Note that if an object is constantly changing, /// it is possible that it will never be saved. /// - /// A Disposable to disable automatic persistence. - public static IDisposable AutoPersist(this T @this, Func> doPersist, TimeSpan? interval = null) - where T : IReactiveObject => - @this.AutoPersist(doPersist, Observable.Never, interval); + /// A disposable to disable automatic persistence. + /// + /// + /// This overload preserves historical behavior by reflecting over the runtime type when it differs from + /// . This behavior is trimming/AOT-unsafe unless the application explicitly preserves the + /// required members and attribute metadata. + /// + /// + /// For trimming/AOT-friendly behavior, prefer the overloads that accept . + /// + /// + [RequiresUnreferencedCode( + "AutoPersist may reflect over the runtime type when it differs from T. In trimmed/AOT builds, required property/attribute metadata " + + "may be removed unless explicitly preserved. Prefer the overloads that accept AutoPersistMetadata to avoid runtime reflection.")] + [RequiresDynamicCode( + "AutoPersist may reflect over the runtime type when it differs from T. In trimmed/AOT builds, required property/attribute metadata " + + "may be removed unless explicitly preserved. Prefer the overloads that accept AutoPersistMetadata to avoid runtime reflection.")] + public static IDisposable AutoPersist< + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] T>( + this T @this, + Func> doPersist, + TimeSpan? interval = null) + where T : IReactiveObject + => @this.AutoPersist(doPersist, Observable.Never, interval); /// /// AutoPersist allows you to automatically call a method when an object @@ -62,12 +88,8 @@ public static IDisposable AutoPersist(this T @this, Func /// /// The reactive object type. /// The save signal type. - /// - /// The reactive object to watch for changes. - /// - /// - /// The asynchronous method to call to save the object to disk. - /// + /// The reactive object to watch for changes. + /// The asynchronous method to call to save the object to disk. /// /// When invoked, the object will be saved regardless of whether it has changed. /// @@ -75,30 +97,69 @@ public static IDisposable AutoPersist(this T @this, Func /// The interval to save the object on. Note that if an object is constantly changing, /// it is possible that it will never be saved. /// - /// A Disposable to disable automatic persistence. - public static IDisposable AutoPersist(this T @this, Func> doPersist, IObservable manualSaveSignal, TimeSpan? interval = null) - where T : IReactiveObject + /// A disposable to disable automatic persistence. + /// Thrown when the object is not annotated with [DataContract]. + /// + /// + /// This overload preserves historical behavior by reflecting over the runtime type when it differs from + /// . This behavior is trimming/AOT-unsafe unless the application explicitly preserves the + /// required members and attribute metadata. + /// + /// + /// For trimming/AOT-friendly behavior, prefer the overloads that accept . + /// + /// + [RequiresUnreferencedCode( + "AutoPersist may reflect over the runtime type when it differs from T. In trimmed/AOT builds, required property/attribute metadata " + + "may be removed unless explicitly preserved. Prefer the overloads that accept AutoPersistMetadata to avoid runtime reflection.")] + [RequiresDynamicCode( + "AutoPersist may reflect over the runtime type when it differs from T. In trimmed/AOT builds, required property/attribute metadata " + + "may be removed unless explicitly preserved. Prefer the overloads that accept AutoPersistMetadata to avoid runtime reflection.")] + public static IDisposable AutoPersist< + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] T, + TDontCare>( + this T @this, + Func> doPersist, + IObservable manualSaveSignal, + TimeSpan? interval = null) + where T : IReactiveObject { + ArgumentExceptionHelper.ThrowIfNull(@this); + ArgumentExceptionHelper.ThrowIfNull(doPersist); + ArgumentExceptionHelper.ThrowIfNull(manualSaveSignal); + interval ??= TimeSpan.FromSeconds(3.0); - if (!_dataContractCheckCache.Get(@this.GetType())) + // Fast path: if T is the actual runtime type, use per-closed-generic cache (no CWT lookup). + // Slow path: preserve historical semantics by reflecting over the runtime type. + var runtimeType = @this.GetType(); + var metadata = runtimeType == typeof(T) + ? PersistMetadataHolder.Metadata + : GetMetadataForUnknownRuntimeType(runtimeType); + + if (!metadata.HasDataContract) { throw new ArgumentException("AutoPersist can only be applied to objects with [DataContract]"); } - var persistableProperties = _persistablePropertiesCache.Get(@this.GetType()); + var persistablePropertyNames = metadata.PersistablePropertyNames; - var saveHint = @this.GetChangedObservable().Where(x => x.PropertyName is not null && persistableProperties.ContainsKey(x.PropertyName)).Select(_ => Unit.Default).Merge(manualSaveSignal.Select(_ => Unit.Default)); + var saveHint = + @this.GetChangedObservable() + .Where(x => x.PropertyName is not null && persistablePropertyNames.Contains(x.PropertyName)) + .Select(static _ => Unit.Default) + .Merge(manualSaveSignal.Select(static _ => Unit.Default)); - var autoSaver = saveHint - .Throttle(interval.Value, RxApp.TaskpoolScheduler) - .SelectMany(_ => doPersist(@this)) - .Publish(); + var autoSaver = + saveHint + .Throttle(interval.Value, RxSchedulers.TaskpoolScheduler) + .SelectMany(_ => doPersist(@this)) + .Publish(); // NB: This rigamarole is to prevent the initialization of a class - // from triggering a save + // from triggering a save. var ret = new SingleAssignmentDisposable(); - RxApp.MainThreadScheduler.Schedule(() => + RxSchedulers.MainThreadScheduler.Schedule(() => { if (ret.IsDisposed) { @@ -112,37 +173,373 @@ public static IDisposable AutoPersist(this T @this, Func - /// Apply AutoPersistence to all objects in a collection. Items that are - /// no longer in the collection won't be persisted anymore. + /// Apply AutoPersistence to all objects in a collection using explicit persistence metadata. + /// This overload performs no runtime reflection and is suitable for trimming/AOT scenarios. /// /// The item type. - /// - /// The reactive collection to watch for changes. + /// The reactive collection to watch for changes. + /// The asynchronous method to call to save the object to disk. + /// The persistence metadata that determines which properties trigger persistence. + /// + /// The interval to save the object on. Note that if an object is constantly changing, + /// it is possible that it will never be saved. /// - /// - /// The asynchronous method to call to save the object to disk. + /// A disposable to disable automatic persistence. + /// + /// Thrown when indicates the object is not annotated with [DataContract]. + /// + public static IDisposable AutoPersistCollection( + this ObservableCollection @this, + Func> doPersist, + AutoPersistMetadata metadata, + TimeSpan? interval = null) + where TItem : IReactiveObject + => AutoPersistCollection(@this, doPersist, Observable.Never, metadata, interval); + + /// + /// Apply AutoPersistence to all objects in a collection using explicit persistence metadata. + /// This overload performs no runtime reflection and is suitable for trimming/AOT scenarios. + /// + /// The item type. + /// The manual save signal type. + /// The reactive collection to watch for changes. + /// The asynchronous method to call to save the object to disk. + /// When invoked, the object will be saved regardless of whether it has changed. + /// The persistence metadata that determines which properties trigger persistence. + /// The interval to save the object on. + /// A disposable to disable automatic persistence. + /// + /// Thrown when indicates the object is not annotated with [DataContract]. + /// + public static IDisposable AutoPersistCollection( + this ObservableCollection @this, + Func> doPersist, + IObservable manualSaveSignal, + AutoPersistMetadata metadata, + TimeSpan? interval = null) + where TItem : IReactiveObject + => AutoPersistCollection, TDontCare>(@this, doPersist, manualSaveSignal, metadata, interval); + + /// + /// Apply AutoPersistence to all objects in a collection using explicit persistence metadata. + /// This overload performs no runtime reflection and is suitable for trimming/AOT scenarios. + /// + /// The item type. + /// The collection type. + /// The manual save signal type. + /// The reactive collection to watch for changes. + /// The asynchronous method to call to save the object to disk. + /// When invoked, the object will be saved regardless of whether it has changed. + /// The persistence metadata that determines which properties trigger persistence. + /// The interval to save the object on. + /// A disposable to disable automatic persistence. + /// + /// Thrown when indicates the object is not annotated with [DataContract]. + /// + public static IDisposable AutoPersistCollection( + this TCollection @this, + Func> doPersist, + IObservable manualSaveSignal, + AutoPersistMetadata metadata, + TimeSpan? interval = null) + where TItem : IReactiveObject + where TCollection : INotifyCollectionChanged, IEnumerable + { + ArgumentExceptionHelper.ThrowIfNull(@this); + ArgumentExceptionHelper.ThrowIfNull(doPersist); + ArgumentExceptionHelper.ThrowIfNull(manualSaveSignal); + ArgumentExceptionHelper.ThrowIfNull(metadata); + + if (!metadata.HasDataContract) + { + throw new ArgumentException("AutoPersist can only be applied to objects with [DataContract]", nameof(metadata)); + } + + var disposerList = new Dictionary(); + + var subscription = @this.ActOnEveryObject( + onAdd: x => + { + if (disposerList.ContainsKey(x)) + { + return; + } + + disposerList[x] = x.AutoPersist(doPersist, manualSaveSignal, metadata, interval); + }, + onRemove: x => + { + if (!disposerList.TryGetValue(x, out var d)) + { + return; + } + + d.Dispose(); + disposerList.Remove(x); + }); + + return Disposable.Create(() => + { + subscription.Dispose(); + + foreach (var kvp in disposerList) + { + kvp.Value.Dispose(); + } + + disposerList.Clear(); + }); + } + + /// + /// Apply AutoPersistence to all objects in a read-only collection using explicit persistence metadata. + /// This overload performs no runtime reflection and is suitable for trimming/AOT scenarios. + /// + /// The item type. + /// The manual save signal type. + /// The reactive collection to watch for changes. + /// The asynchronous method to call to save the object to disk. + /// When invoked, the object will be saved regardless of whether it has changed. + /// The persistence metadata that determines which properties trigger persistence. + /// The interval to save the object on. + /// A disposable to disable automatic persistence. + /// + /// Thrown when indicates the object is not annotated with [DataContract]. + /// + public static IDisposable AutoPersistCollection( + this ReadOnlyObservableCollection @this, + Func> doPersist, + IObservable manualSaveSignal, + AutoPersistMetadata metadata, + TimeSpan? interval = null) + where TItem : IReactiveObject + => AutoPersistCollection, TDontCare>(@this, doPersist, manualSaveSignal, metadata, interval); + + /// + /// Apply AutoPersistence to all objects in a collection using a metadata provider. + /// This overload performs no runtime reflection and is suitable for trimming/AOT scenarios, + /// including polymorphic collections. + /// + /// The item type. + /// The collection type. + /// The manual save signal type. + /// The reactive collection to watch for changes. + /// The asynchronous method to call to save the object to disk. + /// When invoked, the object will be saved regardless of whether it has changed. + /// + /// A function that returns the persistence metadata to use for the specific item instance. + /// + /// The interval to save the object on. + /// A disposable to disable automatic persistence. + /// + /// Thrown when is . + /// + public static IDisposable AutoPersistCollection( + this TCollection @this, + Func> doPersist, + IObservable manualSaveSignal, + Func metadataProvider, + TimeSpan? interval = null) + where TItem : IReactiveObject + where TCollection : INotifyCollectionChanged, IEnumerable + { + ArgumentExceptionHelper.ThrowIfNull(@this); + ArgumentExceptionHelper.ThrowIfNull(doPersist); + ArgumentExceptionHelper.ThrowIfNull(manualSaveSignal); + ArgumentExceptionHelper.ThrowIfNull(metadataProvider); + + var disposerList = new Dictionary(); + + var subscription = @this.ActOnEveryObject( + onAdd: x => + { + if (disposerList.ContainsKey(x)) + { + return; + } + + // Non-RUC path: caller provides metadata explicitly. + var metadata = metadataProvider(x); + disposerList[x] = x.AutoPersist(doPersist, manualSaveSignal, metadata, interval); + }, + onRemove: x => + { + if (!disposerList.TryGetValue(x, out var d)) + { + return; + } + + d.Dispose(); + disposerList.Remove(x); + }); + + return Disposable.Create(() => + { + subscription.Dispose(); + + foreach (var kvp in disposerList) + { + kvp.Value.Dispose(); + } + + disposerList.Clear(); + }); + } + + /// + /// Creates a metadata provider for homogeneous collections where is the concrete runtime type. + /// This helper performs no runtime reflection and is suitable for trimming/AOT scenarios. + /// + /// The item type. + /// A function returning metadata for . + public static Func CreateMetadataProvider< + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] TItem>() + where TItem : IReactiveObject + { + var metadata = CreateMetadata(); + return _ => metadata; + } + + /// + /// AutoPersist overload that performs no runtime reflection and is suitable for trimming/AOT scenarios. + /// + /// The reactive object type. + /// The reactive object to watch for changes. + /// The asynchronous method to call to save the object to disk. + /// The persistence metadata to use for determining persistable properties. + /// The interval to save the object on. + /// A disposable to disable automatic persistence. + /// Thrown when indicates the object is not persistable. + public static IDisposable AutoPersist( + this T @this, + Func> doPersist, + AutoPersistMetadata metadata, + TimeSpan? interval = null) + where T : IReactiveObject + => @this.AutoPersist(doPersist, Observable.Never, metadata, interval); + + /// + /// AutoPersist overload that performs no runtime reflection and is suitable for trimming/AOT scenarios. + /// + /// The reactive object type. + /// The save signal type. + /// The reactive object to watch for changes. + /// The asynchronous method to call to save the object to disk. + /// + /// When invoked, the object will be saved regardless of whether it has changed. /// + /// The persistence metadata to use for determining persistable properties. /// /// The interval to save the object on. Note that if an object is constantly changing, /// it is possible that it will never be saved. /// - /// A Disposable to disable automatic persistence. - public static IDisposable AutoPersistCollection(this ObservableCollection @this, Func> doPersist, TimeSpan? interval = null) // TODO: Create Test - where TItem : IReactiveObject => - AutoPersistCollection(@this, doPersist, Observable.Never, interval); + /// A disposable to disable automatic persistence. + /// Thrown when indicates the object is not persistable. + public static IDisposable AutoPersist( + this T @this, + Func> doPersist, + IObservable manualSaveSignal, + AutoPersistMetadata metadata, + TimeSpan? interval = null) + where T : IReactiveObject + { + ArgumentExceptionHelper.ThrowIfNull(@this); + ArgumentExceptionHelper.ThrowIfNull(doPersist); + ArgumentExceptionHelper.ThrowIfNull(manualSaveSignal); + ArgumentExceptionHelper.ThrowIfNull(metadata); + + if (!metadata.HasDataContract) + { + throw new ArgumentException("AutoPersist can only be applied to objects with [DataContract]", nameof(metadata)); + } + + interval ??= TimeSpan.FromSeconds(3.0); + + var persistablePropertyNames = metadata.PersistablePropertyNames; + + var saveHint = + @this.GetChangedObservable() + .Where(x => x.PropertyName is not null && persistablePropertyNames.Contains(x.PropertyName)) + .Select(static _ => Unit.Default) + .Merge(manualSaveSignal.Select(static _ => Unit.Default)); + + var autoSaver = + saveHint + .Throttle(interval.Value, RxSchedulers.TaskpoolScheduler) + .SelectMany(_ => doPersist(@this)) + .Publish(); + + var ret = new SingleAssignmentDisposable(); + RxSchedulers.MainThreadScheduler.Schedule(() => + { + if (ret.IsDisposed) + { + return; + } + + ret.Disposable = autoSaver.Connect(); + }); + + return ret; + } + + /// + /// Creates trimming/AOT-friendly persistence metadata for . + /// + /// + /// The type to analyze for [DataContract] and [DataMember]. + /// + /// The computed persistence metadata. + /// + /// This method is analyzable by the trimmer due to the + /// on + /// and uses no runtime type discovery. + /// + public static AutoPersistMetadata CreateMetadata< + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] T>() + where T : IReactiveObject + => PersistMetadataHolder.Metadata.Public; /// /// Apply AutoPersistence to all objects in a collection. Items that are /// no longer in the collection won't be persisted anymore. /// /// The item type. - /// The return signal type. - /// - /// The reactive collection to watch for changes. - /// - /// - /// The asynchronous method to call to save the object to disk. + /// The reactive collection to watch for changes. + /// The asynchronous method to call to save the object to disk. + /// + /// The interval to save the object on. Note that if an object is constantly changing, + /// it is possible that it will never be saved. /// + /// A disposable to disable automatic persistence. + /// + /// This overload preserves historical behavior by delegating to the reflection-based AutoPersist pipeline for each item. + /// In trimming/AOT scenarios, required property/attribute metadata may be removed unless explicitly preserved. + /// Prefer the overloads that accept (or a metadata provider) to avoid runtime reflection. + /// + [RequiresUnreferencedCode( + "AutoPersistCollection may reflect over runtime item types via AutoPersist when generic type parameters do not match item runtime types. " + + "In trimmed/AOT builds, required property/attribute metadata may be removed unless explicitly preserved. " + + "Prefer the overloads that accept AutoPersistMetadata or a metadata provider to avoid runtime reflection.")] + [RequiresDynamicCode( + "AutoPersistCollection may reflect over runtime item types via AutoPersist when generic type parameters do not match item runtime types. " + + "In trimmed/AOT builds, required property/attribute metadata may be removed unless explicitly preserved. " + + "Prefer the overloads that accept AutoPersistMetadata or a metadata provider to avoid runtime reflection.")] + public static IDisposable AutoPersistCollection( + this ObservableCollection @this, + Func> doPersist, + TimeSpan? interval = null) // TODO: Create Test + where TItem : IReactiveObject + => AutoPersistCollection(@this, doPersist, Observable.Never, interval); + + /// + /// Apply AutoPersistence to all objects in a collection. Items that are + /// no longer in the collection won't be persisted anymore. + /// + /// The item type. + /// The return signal type. + /// The reactive collection to watch for changes. + /// The asynchronous method to call to save the object to disk. /// /// When invoked, the object will be saved regardless of whether it has changed. /// @@ -150,10 +547,27 @@ public static IDisposable AutoPersistCollection(this ObservableCollection /// The interval to save the object on. Note that if an object is constantly changing, /// it is possible that it will never be saved. /// - /// A Disposable to disable automatic persistence. - public static IDisposable AutoPersistCollection(this ObservableCollection @this, Func> doPersist, IObservable manualSaveSignal, TimeSpan? interval = null) - where TItem : IReactiveObject => - AutoPersistCollection, TDontCare>(@this, doPersist, manualSaveSignal, interval); + /// A disposable to disable automatic persistence. + /// + /// This overload preserves historical behavior by delegating to the reflection-based AutoPersist pipeline for each item. + /// In trimming/AOT scenarios, required property/attribute metadata may be removed unless explicitly preserved. + /// Prefer the overloads that accept (or a metadata provider) to avoid runtime reflection. + /// + [RequiresUnreferencedCode( + "AutoPersistCollection may reflect over runtime item types via AutoPersist when generic type parameters do not match item runtime types. " + + "In trimmed/AOT builds, required property/attribute metadata may be removed unless explicitly preserved. " + + "Prefer the overloads that accept AutoPersistMetadata or a metadata provider to avoid runtime reflection.")] + [RequiresDynamicCode( + "AutoPersistCollection may reflect over runtime item types via AutoPersist when generic type parameters do not match item runtime types. " + + "In trimmed/AOT builds, required property/attribute metadata may be removed unless explicitly preserved. " + + "Prefer the overloads that accept AutoPersistMetadata or a metadata provider to avoid runtime reflection.")] + public static IDisposable AutoPersistCollection( + this ObservableCollection @this, + Func> doPersist, + IObservable manualSaveSignal, + TimeSpan? interval = null) + where TItem : IReactiveObject + => AutoPersistCollection, TDontCare>(@this, doPersist, manualSaveSignal, interval); /// /// Apply AutoPersistence to all objects in a collection. Items that are @@ -161,12 +575,8 @@ public static IDisposable AutoPersistCollection(this Observabl /// /// The item type. /// The signal type. - /// - /// The reactive collection to watch for changes. - /// - /// - /// The asynchronous method to call to save the object to disk. - /// + /// The reactive collection to watch for changes. + /// The asynchronous method to call to save the object to disk. /// /// When invoked, the object will be saved regardless of whether it has changed. /// @@ -174,10 +584,27 @@ public static IDisposable AutoPersistCollection(this Observabl /// The interval to save the object on. Note that if an object is constantly changing, /// it is possible that it will never be saved. /// - /// A Disposable to disable automatic persistence. - public static IDisposable AutoPersistCollection(this ReadOnlyObservableCollection @this, Func> doPersist, IObservable manualSaveSignal, TimeSpan? interval = null) // TODO: Create Test - where TItem : IReactiveObject => - AutoPersistCollection, TDontCare>(@this, doPersist, manualSaveSignal, interval); + /// A disposable to disable automatic persistence. + /// + /// This overload preserves historical behavior by delegating to the reflection-based AutoPersist pipeline for each item. + /// In trimming/AOT scenarios, required property/attribute metadata may be removed unless explicitly preserved. + /// Prefer the overloads that accept (or a metadata provider) to avoid runtime reflection. + /// + [RequiresUnreferencedCode( + "AutoPersistCollection may reflect over runtime item types via AutoPersist when generic type parameters do not match item runtime types. " + + "In trimmed/AOT builds, required property/attribute metadata may be removed unless explicitly preserved. " + + "Prefer the overloads that accept AutoPersistMetadata or a metadata provider to avoid runtime reflection.")] + [RequiresDynamicCode( + "AutoPersistCollection may reflect over runtime item types via AutoPersist when generic type parameters do not match item runtime types. " + + "In trimmed/AOT builds, required property/attribute metadata may be removed unless explicitly preserved. " + + "Prefer the overloads that accept AutoPersistMetadata or a metadata provider to avoid runtime reflection.")] + public static IDisposable AutoPersistCollection( + this ReadOnlyObservableCollection @this, + Func> doPersist, + IObservable manualSaveSignal, + TimeSpan? interval = null) // TODO: Create Test + where TItem : IReactiveObject + => AutoPersistCollection, TDontCare>(@this, doPersist, manualSaveSignal, interval); /// /// Apply AutoPersistence to all objects in a collection. Items that are @@ -186,12 +613,8 @@ public static IDisposable AutoPersistCollection(this ReadOnlyO /// The item type. /// The collection type. /// The signal type. - /// - /// The reactive collection to watch for changes. - /// - /// - /// The asynchronous method to call to save the object to disk. - /// + /// The reactive collection to watch for changes. + /// The asynchronous method to call to save the object to disk. /// /// When invoked, the object will be saved regardless of whether it has changed. /// @@ -199,96 +622,115 @@ public static IDisposable AutoPersistCollection(this ReadOnlyO /// The interval to save the object on. Note that if an object is constantly changing, /// it is possible that it will never be saved. /// - /// A Disposable to disable automatic persistence. - public static IDisposable AutoPersistCollection(this TCollection @this, Func> doPersist, IObservable manualSaveSignal, TimeSpan? interval = null) // TODO: Create Test - where TItem : IReactiveObject - where TCollection : INotifyCollectionChanged, IEnumerable + /// A disposable to disable automatic persistence. + [RequiresUnreferencedCode( + "AutoPersistCollection may reflect over runtime item types via AutoPersist when generic type parameters do not match item runtime types. " + + "In trimmed/AOT builds, required property/attribute metadata may be removed unless explicitly preserved. " + + "Prefer the overloads that accept AutoPersistMetadata or a metadata provider to avoid runtime reflection.")] + [RequiresDynamicCode( + "AutoPersistCollection may reflect over runtime item types via AutoPersist when generic type parameters do not match item runtime types. " + + "In trimmed/AOT builds, required property/attribute metadata may be removed unless explicitly preserved. " + + "Prefer the overloads that accept AutoPersistMetadata or a metadata provider to avoid runtime reflection.")] + public static IDisposable AutoPersistCollection( + this TCollection @this, + Func> doPersist, + IObservable manualSaveSignal, + TimeSpan? interval = null) // TODO: Create Test + where TItem : IReactiveObject + where TCollection : INotifyCollectionChanged, IEnumerable { + ArgumentExceptionHelper.ThrowIfNull(@this); + ArgumentExceptionHelper.ThrowIfNull(doPersist); + ArgumentExceptionHelper.ThrowIfNull(manualSaveSignal); + + // Dictionary is used to preserve prior semantics: per-item disposable tracked by item instance. var disposerList = new Dictionary(); - var disposable = @this.ActOnEveryObject( - x => - { - if (disposerList.ContainsKey(x)) - { - return; - } - - disposerList[x] = x.AutoPersist(doPersist, manualSaveSignal, interval); - }, - x => - { - disposerList[x].Dispose(); - disposerList.Remove(x); - }); + var subscription = @this.ActOnEveryObject( + onAdd: x => + { + if (disposerList.TryGetValue(x, out _)) + { + return; + } + + disposerList[x] = x.AutoPersist(doPersist, manualSaveSignal, interval); + }, + onRemove: x => + { + if (!disposerList.TryGetValue(x, out var d)) + { + return; + } + + d.Dispose(); + disposerList.Remove(x); + }); return Disposable.Create(() => { - disposable.Dispose(); - disposerList.Values.ForEach(x => x.Dispose()); + subscription.Dispose(); + + foreach (var kvp in disposerList) + { + kvp.Value.Dispose(); + } + + disposerList.Clear(); }); } /// - /// Call methods 'onAdd' and 'onRemove' whenever an object is added or - /// removed from a collection. This class correctly handles both when + /// Call methods and whenever an object is added or + /// removed from a collection. This method correctly handles both when /// a collection is initialized, as well as when the collection is Reset. /// /// The item type. - /// - /// The reactive collection to watch for changes. - /// - /// - /// A method to be called when an object is added to the collection. - /// - /// - /// A method to be called when an object is removed from the collection. - /// - /// A Disposable that deactivates this behavior. - public static IDisposable ActOnEveryObject(this ObservableCollection @this, Action onAdd, Action onRemove) // TODO: Create Test - where TItem : IReactiveObject => - ActOnEveryObject>(@this, onAdd, onRemove); + /// The reactive collection to watch for changes. + /// A method to be called when an object is added to the collection. + /// A method to be called when an object is removed from the collection. + /// A disposable that deactivates this behavior. + public static IDisposable ActOnEveryObject( + this ObservableCollection @this, + Action onAdd, + Action onRemove) // TODO: Create Test + where TItem : IReactiveObject + => ActOnEveryObject>(@this, onAdd, onRemove); /// - /// Call methods 'onAdd' and 'onRemove' whenever an object is added or - /// removed from a collection. This class correctly handles both when + /// Call methods and whenever an object is added or + /// removed from a collection. This method correctly handles both when /// a collection is initialized, as well as when the collection is Reset. /// /// The item type. - /// - /// The reactive collection to watch for changes. - /// - /// - /// A method to be called when an object is added to the collection. - /// - /// - /// A method to be called when an object is removed from the collection. - /// - /// A Disposable that deactivates this behavior. - public static IDisposable ActOnEveryObject(this ReadOnlyObservableCollection @this, Action onAdd, Action onRemove) // TODO: Create Test - where TItem : IReactiveObject => - ActOnEveryObject>(@this, onAdd, onRemove); + /// The reactive collection to watch for changes. + /// A method to be called when an object is added to the collection. + /// A method to be called when an object is removed from the collection. + /// A disposable that deactivates this behavior. + public static IDisposable ActOnEveryObject( + this ReadOnlyObservableCollection @this, + Action onAdd, + Action onRemove) // TODO: Create Test + where TItem : IReactiveObject + => ActOnEveryObject>(@this, onAdd, onRemove); /// - /// Call methods 'onAdd' and 'onRemove' whenever an object is added or - /// removed from a collection. This class correctly handles both when + /// Call methods and whenever an object is added or + /// removed from a collection. This method correctly handles both when /// a collection is initialized, as well as when the collection is Reset. /// /// The item type. /// The collection type. - /// - /// The reactive collection to watch for changes. - /// - /// - /// A method to be called when an object is added to the collection. - /// - /// - /// A method to be called when an object is removed from the collection. - /// - /// A Disposable that deactivates this behavior. - public static IDisposable ActOnEveryObject(this TCollection collection, Action onAdd, Action onRemove) - where TItem : IReactiveObject - where TCollection : INotifyCollectionChanged, IEnumerable + /// The reactive collection to watch for changes. + /// A method to be called when an object is added to the collection. + /// A method to be called when an object is removed from the collection. + /// A disposable that deactivates this behavior. + public static IDisposable ActOnEveryObject( + this TCollection collection, + Action onAdd, + Action onRemove) + where TItem : IReactiveObject + where TCollection : INotifyCollectionChanged, IEnumerable { ArgumentExceptionHelper.ThrowIfNull(onAdd); ArgumentExceptionHelper.ThrowIfNull(onRemove); @@ -305,35 +747,41 @@ public static IDisposable ActOnEveryObject(this TCollection { changedDisposable.Dispose(); - collection.ForEach(onRemove); + foreach (var v in collection) + { + onRemove(v); + } }); } /// - /// Call methods 'onAdd' and 'onRemove' whenever an object is added or - /// removed from a collection. This class correctly handles both when + /// Call methods and whenever an object is added or + /// removed from a collection. This method correctly handles both when /// a collection is initialized, as well as when the collection is Reset. /// /// The item type. - /// - /// The reactive collection to watch for changes. - /// - /// - /// A method to be called when an object is added to the collection. - /// - /// - /// A method to be called when an object is removed from the collection. - /// - /// A Disposable that deactivates this behavior. - public static IDisposable ActOnEveryObject(this IObservable> @this, Action onAdd, Action onRemove) - where TItem : IReactiveObject => - @this.Subscribe(changeSet => + /// The observable change set to watch for changes. + /// A method to be called when an object is added to the collection. + /// A method to be called when an object is removed from the collection. + /// A disposable that deactivates this behavior. + public static IDisposable ActOnEveryObject( + this IObservable> @this, + Action onAdd, + Action onRemove) + where TItem : IReactiveObject + { + ArgumentExceptionHelper.ThrowIfNull(@this); + ArgumentExceptionHelper.ThrowIfNull(onAdd); + ArgumentExceptionHelper.ThrowIfNull(onRemove); + + return @this.Subscribe(changeSet => { foreach (var change in changeSet) { switch (change.Reason) { case ListChangeReason.Refresh: + // Preserve original ordering: remove all, then add all. foreach (var item in change.Range) { onRemove(item); @@ -385,4 +833,155 @@ public static IDisposable ActOnEveryObject(this IObservable + /// Gets metadata for a runtime type that is not statically known to the trimmer. + /// + /// The runtime type. + /// The computed persistence metadata. + /// + /// This path is trimming/AOT unsafe unless the application explicitly preserves the required members + /// (properties and related attribute metadata) for . + /// + [RequiresUnreferencedCode( + "AutoPersist reflects over the runtime type. In trimmed/AOT builds, required property/attribute metadata may be removed " + + "unless explicitly preserved. Prefer CreateMetadata() and the overloads that accept AutoPersistMetadata.")] + [RequiresDynamicCode( + "AutoPersist reflects over the runtime type. In trimmed/AOT builds, required property/attribute metadata may be removed " + + "unless explicitly preserved. Prefer CreateMetadata() and the overloads that accept AutoPersistMetadata.")] + private static PersistMetadata GetMetadataForUnknownRuntimeType(Type runtimeType) + => PersistMetadataByType.GetValue(runtimeType, static t => PersistMetadata.Create(t)); + + /// + /// Public-facing persistence metadata for AutoPersist. + /// + /// + /// This type exists so callers can provide persistence metadata explicitly in trimming/AOT scenarios, + /// avoiding runtime reflection over unknown types. + /// + public sealed class AutoPersistMetadata + { + /// + /// Initializes a new instance of the class. + /// + /// Whether the type is annotated with [DataContract]. + /// The set of property names annotated with [DataMember]. + /// + /// Thrown when is . + /// + public AutoPersistMetadata(bool hasDataContract, ISet persistablePropertyNames) + { + ArgumentExceptionHelper.ThrowIfNull(persistablePropertyNames); + + HasDataContract = hasDataContract; + PersistablePropertyNames = persistablePropertyNames; + } + + /// + /// Gets a value indicating whether the target type is annotated with [DataContract]. + /// + public bool HasDataContract { get; } + + /// + /// Gets the set of property names annotated with [DataMember]. + /// + public ISet PersistablePropertyNames { get; } + } + + /// + /// Holds precomputed metadata for a closed generic . + /// + /// + /// The type for which persistence metadata is computed. + /// + private static class PersistMetadataHolder< + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] T> + where T : IReactiveObject + { + /// + /// Gets the computed persistence metadata for . + /// + internal static readonly PersistMetadata Metadata = PersistMetadata.Create(typeof(T)); + } + + /// + /// Immutable persistence metadata for a given type. + /// + private sealed record PersistMetadata + { + /// + /// Initializes a new instance of the class. + /// + /// Whether the type is annotated with [DataContract]. + /// The set of property names annotated with [DataMember]. + private PersistMetadata(bool hasDataContract, HashSet persistablePropertyNames) + { + HasDataContract = hasDataContract; + PersistablePropertyNames = persistablePropertyNames; + Public = new AutoPersistMetadata(hasDataContract, persistablePropertyNames); + } + + /// + /// Gets a value indicating whether the target type is annotated with [DataContract]. + /// + internal bool HasDataContract { get; } + + /// + /// Gets the set of property names annotated with [DataMember]. + /// + internal HashSet PersistablePropertyNames { get; } + + /// + /// Gets a public metadata wrapper for callers. + /// + internal AutoPersistMetadata Public { get; } + + /// + /// Creates persistence metadata for a statically-known or explicitly-preserved type. + /// + /// The type to analyze. + /// The computed persistence metadata. + internal static PersistMetadata Create( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] + Type type) + { + // Preserve original semantics: [DataContract] is checked via GetCustomAttributes(..., inherit: true). + var hasDataContract = type.GetCustomAttributes(typeof(DataContractAttribute), inherit: true).Length > 0; + + // Preserve original semantics: consider DeclaredProperties only (not inherited properties). + // Use reflection flags directly to avoid GetTypeInfo() overhead. + var properties = type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly); + + HashSet? set = null; + + for (var i = 0; i < properties.Length; i++) + { + var p = properties[i]; + if (!HasDataMemberAttribute(p)) + { + continue; + } + + set ??= new HashSet(StringComparer.Ordinal); + set.Add(p.Name); + } + + set ??= new HashSet(StringComparer.Ordinal); + return new PersistMetadata(hasDataContract, set); + } + + /// + /// Determines whether a property is annotated with [DataMember]. + /// + /// The property to inspect. + /// if the property is annotated; otherwise . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool HasDataMemberAttribute(PropertyInfo property) + { + // Avoid LINQ allocations; use IsDefined which is efficient for the common case. + // DataMemberAttribute is not inherited by default, but preserve inherit=true for parity. + return property.IsDefined(typeof(DataMemberAttribute), inherit: true); + } + } } diff --git a/src/ReactiveUI/Mixins/BuilderMixins.cs b/src/ReactiveUI/Mixins/BuilderMixins.cs index 06607f0156..43a3b369ab 100644 --- a/src/ReactiveUI/Mixins/BuilderMixins.cs +++ b/src/ReactiveUI/Mixins/BuilderMixins.cs @@ -14,6 +14,88 @@ namespace ReactiveUI.Builder; /// public static class BuilderMixins { + /// + /// Registers view-to-viewmodel mappings inline using a fluent builder. + /// This method is fully AOT-compatible when all view types are known at compile time. + /// + /// The ReactiveUI builder instance. + /// Configuration action for registering views. + /// The builder for chaining. + /// Thrown when builder or configure is null. + /// Thrown when DefaultViewLocator is not registered in the service locator. + /// + /// + /// () + /// .RegisterViews(views => views + /// .Map() + /// .Map() + /// .Map()) + /// .Build(); + /// ]]> + /// + /// + public static IReactiveUIBuilder RegisterViews( + this IReactiveUIBuilder builder, + Action configure) + { + ArgumentExceptionHelper.ThrowIfNull(builder); + ArgumentExceptionHelper.ThrowIfNull(configure); + + var viewLocator = AppLocator.Current.GetService() as DefaultViewLocator + ?? throw new InvalidOperationException( + "DefaultViewLocator must be registered before calling RegisterViews. " + + "Ensure you've called WithPlatformModule() or manually registered DefaultViewLocator."); + + var mappingBuilder = new ViewMappingBuilder(viewLocator); + configure(mappingBuilder); + return builder; + } + + /// + /// Registers views using a reusable view module. + /// This method is fully AOT-compatible when all view types are known at compile time. + /// + /// The view module type to register. + /// The ReactiveUI builder instance. + /// The builder for chaining. + /// Thrown when builder is null. + /// Thrown when DefaultViewLocator is not registered in the service locator. + /// + /// + /// (() => new LoginView()) + /// .Map(() => new RegisterView()); + /// } + /// } + /// + /// new ReactiveUIBuilder() + /// .WithPlatformModule() + /// .WithViewModule() + /// .Build(); + /// ]]> + /// + /// + public static IReactiveUIBuilder WithViewModule(this IReactiveUIBuilder builder) + where TModule : IViewModule, new() + { + ArgumentExceptionHelper.ThrowIfNull(builder); + + var viewLocator = AppLocator.Current.GetService() as DefaultViewLocator + ?? throw new InvalidOperationException( + "DefaultViewLocator must be registered before calling WithViewModule. " + + "Ensure you've called WithPlatformModule() or manually registered DefaultViewLocator."); + + var module = new TModule(); + module.RegisterViews(viewLocator); + return builder; + } + /// /// Configures the task pool scheduler. /// @@ -32,6 +114,26 @@ public static IReactiveUIBuilder WithTaskPoolScheduler(this IReactiveUIBuilder b return builder; } + /// + /// Builds and configures the application using the ReactiveUI builder pattern. + /// + /// Use this extension method to finalize application setup when working with ReactiveUI. This + /// method should be called after all necessary configuration has been applied to the builder. + /// The application builder to configure. Must implement . + /// An instance representing the configured application. + /// Thrown if does not implement . + public static IReactiveUIBuilder BuildApp(this IAppBuilder appBuilder) + { + ArgumentExceptionHelper.ThrowIfNull(appBuilder); + if (appBuilder is IReactiveUIBuilder reactiveUIBuilder) + { + reactiveUIBuilder.BuildApp(); + return reactiveUIBuilder; + } + + throw new InvalidOperationException("The provided IAppBuilder is not an IReactiveUIBuilder. Ensure you are using the ReactiveUI builder pattern."); + } + /// /// Configures the main thread scheduler. /// @@ -93,10 +195,7 @@ public static IReactiveUIBuilder WithRegistration(this IReactiveUIBuilder builde /// 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 + [RequiresUnreferencedCode("Scans assembly for IViewFor implementations using reflection. For AOT compatibility, use the ReactiveUIBuilder pattern to RegisterView explicitly.")] public static IReactiveUIBuilder WithViewsFromAssembly(this IReactiveUIBuilder builder, Assembly assembly) { ArgumentExceptionHelper.ThrowIfNull(builder); @@ -114,10 +213,6 @@ public static IReactiveUIBuilder WithViewsFromAssembly(this IReactiveUIBuilder b /// The builder instance for method 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 IReactiveUIBuilder WithPlatformModule(this IReactiveUIBuilder builder) where T : IWantsToRegisterStuff, new() { diff --git a/src/ReactiveUI/Mixins/DependencyResolverMixins.cs b/src/ReactiveUI/Mixins/DependencyResolverMixins.cs index 63a1dd620c..22c96b38cd 100644 --- a/src/ReactiveUI/Mixins/DependencyResolverMixins.cs +++ b/src/ReactiveUI/Mixins/DependencyResolverMixins.cs @@ -5,7 +5,7 @@ using System.Reflection; -using Splat.Builder; +using ReactiveUI.Builder; namespace ReactiveUI; @@ -16,71 +16,9 @@ namespace ReactiveUI; public static class DependencyResolverMixins { /// - /// This method allows you to initialize resolvers with the default - /// ReactiveUI types. All resolvers used as the default - /// AppLocator.Current. - /// If no namespaces are passed in, all registrations will be checked. + /// Initializes static members of the class. /// - /// The resolver to initialize. - /// Which platforms to use. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("InitializeReactiveUI uses reflection to locate types which may be trimmed.")] - [RequiresUnreferencedCode("InitializeReactiveUI uses reflection to locate types which may be trimmed.")] -#endif - public static void InitializeReactiveUI(this IMutableDependencyResolver resolver, params RegistrationNamespace[] registrationNamespaces) - { - if (AppBuilder.UsingBuilder && !ModeDetector.InUnitTestRunner() && ReferenceEquals(resolver, AppLocator.CurrentMutable)) - { - // If the builder has been used for the default resolver in a non-test environment, - // do not re-register defaults via reflection for AppLocator.CurrentMutable. - return; - } - - ArgumentExceptionHelper.ThrowIfNull(resolver); - ArgumentExceptionHelper.ThrowIfNull(registrationNamespaces); - - var possibleNamespaces = new Dictionary - { - { RegistrationNamespace.Winforms, "ReactiveUI.Winforms" }, - { RegistrationNamespace.Wpf, "ReactiveUI.Wpf" }, - { RegistrationNamespace.Uno, "ReactiveUI.Uno" }, - { RegistrationNamespace.UnoWinUI, "ReactiveUI.Uno.WinUI" }, - { RegistrationNamespace.Blazor, "ReactiveUI.Blazor" }, - { RegistrationNamespace.Drawing, "ReactiveUI.Drawing" }, - { RegistrationNamespace.Avalonia, "ReactiveUI.Avalonia" }, - { RegistrationNamespace.Maui, "ReactiveUI.Maui" }, - { RegistrationNamespace.Uwp, "ReactiveUI.Uwp" }, - { RegistrationNamespace.WinUI, "ReactiveUI.WinUI" }, - }; - - if (registrationNamespaces.Length == 0) - { - registrationNamespaces = PlatformRegistrationManager.DefaultRegistrationNamespaces; - } - - var extraNs = - registrationNamespaces - .Where(ns => possibleNamespaces.ContainsKey(ns)) - .Select(ns => possibleNamespaces[ns]) - .ToArray(); - - // Set up the built-in registration - new Registrations().Register((f, t) => resolver.RegisterConstant(f(), t)); -#if NET6_0_OR_GREATER - new PlatformRegistrations().Register((f, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] t) => resolver.RegisterConstant(f(), t)); -#else - new PlatformRegistrations().Register((f, t) => resolver.RegisterConstant(f(), t)); -#endif - - var fdr = typeof(DependencyResolverMixins); - - var assemblyName = new AssemblyName(fdr.AssemblyQualifiedName!.Replace(fdr.FullName + ", ", string.Empty)); - - foreach (var ns in extraNs) - { - ProcessRegistrationForNamespace(ns, assemblyName, resolver); - } - } + static DependencyResolverMixins() => RxAppBuilder.EnsureInitialized(); /// /// Registers inside the Splat dependency container all the classes that derive off @@ -89,10 +27,7 @@ public static void InitializeReactiveUI(this IMutableDependencyResolver resolver /// /// The dependency injection resolver to register the Views with. /// The assembly to search using reflection for IViewFor classes. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("RegisterViewsForViewModels scans the provided assembly and creates instances via reflection; this is not compatible with AOT.")] - [RequiresUnreferencedCode("RegisterViewsForViewModels uses reflection over types and members which may be trimmed.")] -#endif + [RequiresUnreferencedCode("Scans assembly for IViewFor implementations using reflection. For AOT compatibility, use the ReactiveUIBuilder pattern to register views explicitly.")] public static void RegisterViewsForViewModels(this IMutableDependencyResolver resolver, Assembly assembly) { ArgumentExceptionHelper.ThrowIfNull(resolver); @@ -123,11 +58,13 @@ public static void RegisterViewsForViewModels(this IMutableDependencyResolver re } } -#if NET6_0_OR_GREATER - [RequiresDynamicCode("RegisterType creates instances via Activator.CreateInstance which requires dynamic code generation in AOT.")] - [RequiresUnreferencedCode("RegisterType uses reflection to locate parameterless constructors which may be trimmed.")] -#endif - private static void RegisterType(IMutableDependencyResolver resolver, TypeInfo ti, Type serviceType, string contract) + private static void RegisterType( + IMutableDependencyResolver resolver, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] + TypeInfo ti, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] + Type serviceType, + string contract) { var factory = TypeFactory(ti); if (ti.GetCustomAttribute() is not null) @@ -140,13 +77,8 @@ private static void RegisterType(IMutableDependencyResolver resolver, TypeInfo t } } -#if NET6_0_OR_GREATER - [RequiresUnreferencedCode("TypeFactory uses reflection to invoke parameterless constructors which may be trimmed.")] -#endif private static Func TypeFactory( -#if NET6_0_OR_GREATER - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] -#endif + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] TypeInfo typeInfo) { var parameterlessConstructor = typeInfo.DeclaredConstructors.FirstOrDefault(ci => ci.IsPublic && ci.GetParameters().Length == 0); @@ -155,50 +87,4 @@ private static Func TypeFactory( : () => Activator.CreateInstance(typeInfo.AsType()) ?? throw new Exception($"Failed to instantiate type {typeInfo.FullName} - ensure it has a public parameterless constructor."); } - -#if NET6_0_OR_GREATER - [RequiresUnreferencedCode("ProcessRegistrationForNamespace uses reflection to locate types which may be trimmed.")] - [RequiresDynamicCode("Calls ReactiveUI.IWantsToRegisterStuff.Register(Action, Type>)")] -#endif - private static void ProcessRegistrationForNamespace(string namespaceName, AssemblyName assemblyName, IMutableDependencyResolver resolver) - { - var targetTypeName = namespaceName + ".Registrations"; - - // Preferred path: find the target assembly by simple name among loaded assemblies - var asm = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => a.GetName().Name == namespaceName); - Type? registerTypeClass = null; - if (asm is null) - { - try - { - asm = Assembly.Load(new AssemblyName(namespaceName)); - } - catch - { - asm = null; - } - } - - if (asm is not null) - { - registerTypeClass = asm.GetType(targetTypeName, throwOnError: false, ignoreCase: false); - } - - // Fallback to legacy lookup using full name synthesis - if (registerTypeClass is null && assemblyName.Name is not null) - { - var fullName = targetTypeName + ", " + assemblyName.FullName.Replace(assemblyName.Name, namespaceName); - registerTypeClass = Reflection.ReallyFindType(fullName, false); - } - - if (registerTypeClass is not null) - { - var registerer = (IWantsToRegisterStuff)Activator.CreateInstance(registerTypeClass)!; -#if NET6_0_OR_GREATER - registerer.Register((f, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] t) => resolver.RegisterConstant(f(), t)); -#else - registerer.Register((f, t) => resolver.RegisterConstant(f(), t)); -#endif - } - } } diff --git a/src/ReactiveUI/Mixins/ExpressionMixins.cs b/src/ReactiveUI/Mixins/ExpressionMixins.cs index b45cc0459c..3ade3b193a 100644 --- a/src/ReactiveUI/Mixins/ExpressionMixins.cs +++ b/src/ReactiveUI/Mixins/ExpressionMixins.cs @@ -6,6 +6,8 @@ using System.Reflection; using System.Text; +using ReactiveUI.Builder; + namespace ReactiveUI; /// @@ -13,16 +15,17 @@ namespace ReactiveUI; /// public static class ExpressionMixins { + /// + /// Initializes static members of the class. + /// + static ExpressionMixins() => RxAppBuilder.EnsureInitialized(); + /// /// Gets all the chain of child expressions within a Expression. /// Handles property member accesses, objects and indexes. /// /// The expression. /// An enumerable of expressions. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("Expression chain analysis requires dynamic code generation")] - [RequiresUnreferencedCode("Expression chain analysis may reference members that could be trimmed")] -#endif public static IEnumerable GetExpressionChain(this Expression expression) { var expressions = new List(); @@ -90,10 +93,6 @@ public static IEnumerable GetExpressionChain(this Expression express /// /// The expression. /// The member info from the expression. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("Member info access requires dynamic code generation")] - [RequiresUnreferencedCode("Member info access may reference members that could be trimmed")] -#endif public static MemberInfo? GetMemberInfo(this Expression expression) { ArgumentExceptionHelper.ThrowIfNull(expression); @@ -121,10 +120,6 @@ public static IEnumerable GetExpressionChain(this Expression express /// /// The expression. /// The parent expression. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("Expression analysis requires dynamic code generation")] - [RequiresUnreferencedCode("Expression analysis may reference members that could be trimmed")] -#endif public static Expression? GetParent(this Expression expression) // TODO: Create Test { ArgumentExceptionHelper.ThrowIfNull(expression); diff --git a/src/ReactiveUI/Mixins/MutableDependencyResolverAOTExtensions.cs b/src/ReactiveUI/Mixins/MutableDependencyResolverAOTExtensions.cs index 57da048797..73287b9137 100644 --- a/src/ReactiveUI/Mixins/MutableDependencyResolverAOTExtensions.cs +++ b/src/ReactiveUI/Mixins/MutableDependencyResolverAOTExtensions.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 ReactiveUI.Builder; + namespace ReactiveUI; /// @@ -11,29 +13,42 @@ namespace ReactiveUI; /// internal static class MutableDependencyResolverAOTExtensions { -#if NET6_0_OR_GREATER - [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Generic registration does not use reflection")] - [UnconditionalSuppressMessage("AOT", "IL3050:Members annotated with 'RequiresDynamicCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Generic registration does not use dynamic code")] -#endif + /// + /// Initializes static members of the class. + /// + static MutableDependencyResolverAOTExtensions() => RxAppBuilder.EnsureInitialized(); + internal static IMutableDependencyResolver RegisterViewForViewModelAOT(this IMutableDependencyResolver resolver, string? contract = null) where TView : class, IViewFor, new() where TViewModel : class { ArgumentExceptionHelper.ThrowIfNull(resolver); - resolver.Register(static () => new TView(), typeof(IViewFor), contract ?? string.Empty); + if (contract is null) + { + resolver.Register>(static () => new TView()); + } + else + { + resolver.Register>(static () => new TView(), contract); + } + return resolver; } -#if NET6_0_OR_GREATER - [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Generic registration does not use reflection")] - [UnconditionalSuppressMessage("AOT", "IL3050:Members annotated with 'RequiresDynamicCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Generic registration does not use dynamic code")] -#endif internal static IMutableDependencyResolver RegisterSingletonViewForViewModelAOT(this IMutableDependencyResolver resolver, string? contract = null) where TView : class, IViewFor, new() where TViewModel : class { ArgumentExceptionHelper.ThrowIfNull(resolver); - resolver.RegisterLazySingleton(static () => new TView(), typeof(IViewFor), contract ?? string.Empty); + if (contract is null) + { + resolver.RegisterLazySingleton>(static () => new TView()); + } + else + { + resolver.RegisterLazySingleton>(static () => new TView(), contract); + } + return resolver; } } diff --git a/src/ReactiveUI/Mixins/MutableDependencyResolverExtensions.cs b/src/ReactiveUI/Mixins/MutableDependencyResolverExtensions.cs index d037e485cc..041a33402b 100644 --- a/src/ReactiveUI/Mixins/MutableDependencyResolverExtensions.cs +++ b/src/ReactiveUI/Mixins/MutableDependencyResolverExtensions.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 ReactiveUI.Builder; + namespace ReactiveUI; /// @@ -11,6 +13,11 @@ namespace ReactiveUI; /// public static class MutableDependencyResolverExtensions { + /// + /// Initializes static members of the class. + /// + static MutableDependencyResolverExtensions() => RxAppBuilder.EnsureInitialized(); + /// /// Registers a view for a view model via generics without reflection. /// @@ -19,16 +26,20 @@ public static class MutableDependencyResolverExtensions /// Resolver to register into. /// Optional contract. /// The resolver, for chaining. -#if NET6_0_OR_GREATER - [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Generic registration does not use reflection")] - [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "Generic registration does not use dynamic code")] -#endif public static IMutableDependencyResolver RegisterViewForViewModel(this IMutableDependencyResolver resolver, string? contract = null) where TView : class, IViewFor, new() where TViewModel : class { ArgumentExceptionHelper.ThrowIfNull(resolver); - resolver.Register(static () => new TView(), typeof(IViewFor), contract ?? string.Empty); + if (contract is null) + { + resolver.Register>(static () => new TView()); + } + else + { + resolver.Register>(static () => new TView(), contract); + } + return resolver; } @@ -40,16 +51,20 @@ public static IMutableDependencyResolver RegisterViewForViewModelResolver to register into. /// Optional contract. /// The resolver, for chaining. -#if NET6_0_OR_GREATER - [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Generic registration does not use reflection")] - [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "Generic registration does not use dynamic code")] -#endif public static IMutableDependencyResolver RegisterSingletonViewForViewModel(this IMutableDependencyResolver resolver, string? contract = null) where TView : class, IViewFor, new() where TViewModel : class { ArgumentExceptionHelper.ThrowIfNull(resolver); - resolver.RegisterLazySingleton(static () => new TView(), typeof(IViewFor), contract ?? string.Empty); + if (contract is null) + { + resolver.RegisterLazySingleton>(static () => new TView()); + } + else + { + resolver.RegisterLazySingleton>(static () => new TView(), contract); + } + return resolver; } } diff --git a/src/ReactiveUI/Mixins/ObservableLoggingMixin.cs b/src/ReactiveUI/Mixins/ObservableLoggingMixin.cs index 83e7457689..d37c014fb4 100644 --- a/src/ReactiveUI/Mixins/ObservableLoggingMixin.cs +++ b/src/ReactiveUI/Mixins/ObservableLoggingMixin.cs @@ -5,6 +5,8 @@ using System.Globalization; +using ReactiveUI.Builder; + namespace ReactiveUI; /// @@ -12,6 +14,11 @@ namespace ReactiveUI; /// public static class ObservableLoggingMixin { + /// + /// Initializes static members of the class. + /// + static ObservableLoggingMixin() => RxAppBuilder.EnsureInitialized(); + /// /// Logs an Observable to Splat's Logger. /// diff --git a/src/ReactiveUI/Mixins/ObservableMixins.cs b/src/ReactiveUI/Mixins/ObservableMixins.cs index 6896b8a4d7..ba413f982a 100644 --- a/src/ReactiveUI/Mixins/ObservableMixins.cs +++ b/src/ReactiveUI/Mixins/ObservableMixins.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 ReactiveUI.Builder; + namespace ReactiveUI; /// @@ -10,6 +12,11 @@ namespace ReactiveUI; /// public static class ObservableMixins { + /// + /// Initializes static members of the class. + /// + static ObservableMixins() => RxAppBuilder.EnsureInitialized(); + /// /// Returns only values that are not null. /// Converts the nullability. diff --git a/src/ReactiveUI/Mixins/ObservedChangedMixin.cs b/src/ReactiveUI/Mixins/ObservedChangedMixin.cs index 77d83852be..b9e84696c4 100644 --- a/src/ReactiveUI/Mixins/ObservedChangedMixin.cs +++ b/src/ReactiveUI/Mixins/ObservedChangedMixin.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 ReactiveUI.Builder; + namespace ReactiveUI; /// @@ -10,6 +12,11 @@ namespace ReactiveUI; /// public static class ObservedChangedMixin { + /// + /// Initializes static members of the class. + /// + static ObservedChangedMixin() => RxAppBuilder.EnsureInitialized(); + /// /// Returns the name of a property which has been changed. /// @@ -19,10 +26,6 @@ public static class ObservedChangedMixin /// /// The name of the property which has changed. /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("TryGetValue uses expression chain analysis and reflection which require dynamic code generation.")] - [RequiresUnreferencedCode("TryGetValue uses expression chain analysis and reflection which may reference members that could be trimmed.")] -#endif public static string GetPropertyName(this IObservedChange item) => item is null ? throw new ArgumentNullException(nameof(item)) @@ -40,10 +43,7 @@ item is null /// /// The current value of the property. /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("GetValue uses expression chain analysis and reflection which require dynamic code generation.")] - [RequiresUnreferencedCode("GetValue uses expression chain analysis and reflection which may reference members that could be trimmed.")] -#endif + [RequiresUnreferencedCode("Evaluates expression-based member chains via reflection; members may be trimmed.")] public static TValue GetValue(this IObservedChange item) => item is null ? throw new ArgumentNullException(nameof(item)) @@ -63,10 +63,7 @@ item is null /// /// The current value of the property. /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("GetValueOrDefault uses expression chain analysis and reflection which require dynamic code generation.")] - [RequiresUnreferencedCode("GetValueOrDefault uses expression chain analysis and reflection which may reference members that could be trimmed.")] -#endif + [RequiresUnreferencedCode("Evaluates expression-based member chains via reflection; members may be trimmed.")] public static TValue? GetValueOrDefault(this IObservedChange item) => // TODO: Create Test item is null ? throw new ArgumentNullException(nameof(item)) : !item.TryGetValue(out var returnValue) ? default : returnValue; @@ -83,10 +80,7 @@ item is null /// An Observable representing the stream of current values of /// the given change notification stream. /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("Value method uses GetValue which requires expression chain analysis and reflection.")] - [RequiresUnreferencedCode("Value method uses GetValue which may reference members that could be trimmed.")] -#endif + [RequiresUnreferencedCode("Evaluates expression-based member chains via reflection; members may be trimmed.")] public static IObservable Value(this IObservable> item) => // TODO: Create Test item.Select(GetValue); @@ -106,10 +100,7 @@ public static IObservable Value(this IObservable /// True if the entire expression was able to be followed, false otherwise. /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("TryGetValue uses expression chain analysis and reflection which require dynamic code generation.")] - [RequiresUnreferencedCode("TryGetValue uses expression chain analysis and reflection which may reference members that could be trimmed.")] -#endif + [RequiresUnreferencedCode("Evaluates expression-based member chains via reflection; members may be trimmed.")] internal static bool TryGetValue(this IObservedChange item, out TValue changeValue) { if (!Equals(item.Value, default(TValue))) @@ -140,10 +131,7 @@ internal static bool TryGetValue(this IObservedChange /// The target property to apply the change to. /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("TryGetValue uses expression chain analysis and reflection which require dynamic code generation.")] - [RequiresUnreferencedCode("TryGetValue uses expression chain analysis and reflection which may reference members that could be trimmed.")] -#endif + [RequiresUnreferencedCode("Evaluates expression-based member chains via reflection; members may be trimmed.")] internal static void SetValueToProperty( this IObservedChange item, TTarget target, diff --git a/src/ReactiveUI/Mixins/ReactiveNotifyPropertyChangedMixin.cs b/src/ReactiveUI/Mixins/ReactiveNotifyPropertyChangedMixin.cs index f8e5393c32..c76dbc465e 100644 --- a/src/ReactiveUI/Mixins/ReactiveNotifyPropertyChangedMixin.cs +++ b/src/ReactiveUI/Mixins/ReactiveNotifyPropertyChangedMixin.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 ReactiveUI.Builder; + namespace ReactiveUI; /// @@ -10,10 +12,9 @@ namespace ReactiveUI; /// Reactive Notify Property Changed based events. /// [Preserve(AllMembers = true)] +[RequiresUnreferencedCode("Creating Expressions requires unreferenced code because the members being referenced by the Expression may be trimmed.")] public static class ReactiveNotifyPropertyChangedMixin { - [SuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "Marked as Preserve")] - [SuppressMessage("Trimming", "IL2026:Calling members annotated with 'RequiresUnreferencedCodeAttribute' may break functionality when trimming application code.", Justification = "Marked as Preserve")] private static readonly MemoizingMRUCache<(Type senderType, string propertyName, bool beforeChange), ICreatesObservableForProperty?> _notifyFactoryCache = new( (t, _) => AppLocator.Current.GetServices() @@ -22,9 +23,12 @@ public static class ReactiveNotifyPropertyChangedMixin var score = x.GetAffinityForObject(t.senderType, t.propertyName, t.beforeChange); return score > acc.score ? (score, x) : acc; }).binding, - RxApp.BigCacheLimit); + RxCacheSize.BigCacheLimit); - static ReactiveNotifyPropertyChangedMixin() => RxApp.EnsureInitialized(); + /// + /// Initializes static members of the class. + /// + static ReactiveNotifyPropertyChangedMixin() => RxAppBuilder.EnsureInitialized(); /// /// ObservableForProperty returns an Observable representing the @@ -41,10 +45,7 @@ public static class ReactiveNotifyPropertyChangedMixin /// If true, the Observable will not notify with the initial value. /// If set to true, values are filtered with DistinctUntilChanged. /// An Observable representing the property change notifications for the given property name. -#if NET6_0_OR_GREATER - [RequiresUnreferencedCode("This method uses reflection to access properties by name.")] - [RequiresDynamicCode("This method uses reflection to access properties by name.")] -#endif + [RequiresUnreferencedCode("Creating Expressions requires unreferenced code because the members being referenced by the Expression may be trimmed.")] public static IObservable> ObservableForProperty( this TSender? item, string propertyName, @@ -147,10 +148,7 @@ static TValue GetCurrentValue(object sender, string name) /// The source object to observe properties of. /// The property name to observe. /// An observable sequence of observed changes for the given property name. -#if NET6_0_OR_GREATER - [RequiresUnreferencedCode("This method uses reflection to access properties by name.")] - [RequiresDynamicCode("This method uses reflection to access properties by name.")] -#endif + [RequiresUnreferencedCode("Creating Expressions requires unreferenced code because the members being referenced by the Expression may be trimmed.")] public static IObservable> ObservableForProperty( this TSender? item, string propertyName) @@ -165,10 +163,7 @@ public static IObservable> ObservableForPropert /// The property name to observe. /// If true, the observable will notify immediately before a property is going to change. /// An observable sequence of observed changes for the given property name. -#if NET6_0_OR_GREATER - [RequiresUnreferencedCode("This method uses reflection to access properties by name.")] - [RequiresDynamicCode("This method uses reflection to access properties by name.")] -#endif + [RequiresUnreferencedCode("Creating Expressions requires unreferenced code because the members being referenced by the Expression may be trimmed.")] public static IObservable> ObservableForProperty( this TSender? item, string propertyName, @@ -185,10 +180,7 @@ public static IObservable> ObservableForPropert /// If true, the observable will notify immediately before a property is going to change. /// If true, the observable will not notify with the initial value. /// An observable sequence of observed changes for the given property name. -#if NET6_0_OR_GREATER - [RequiresUnreferencedCode("This method uses reflection to access properties by name.")] - [RequiresDynamicCode("This method uses reflection to access properties by name.")] -#endif + [RequiresUnreferencedCode("Creating Expressions requires unreferenced code because the members being referenced by the Expression may be trimmed.")] public static IObservable> ObservableForProperty( this TSender? item, string propertyName, @@ -212,10 +204,7 @@ public static IObservable> ObservableForPropert /// An Observable representing the property change /// notifications for the given property. /// -#if NET6_0_OR_GREATER - [RequiresUnreferencedCode("This method uses reflection to access properties by name.")] - [RequiresDynamicCode("This method uses reflection to access properties by name.")] -#endif + [RequiresUnreferencedCode("Evaluates expression-based member chains via reflection; members may be trimmed.")] public static IObservable> ObservableForProperty( this TSender? item, Expression> property) => ObservableForProperty(item, property, false, true, true); @@ -238,10 +227,7 @@ public static IObservable> ObservableForPropert /// An Observable representing the property change /// notifications for the given property. /// -#if NET6_0_OR_GREATER - [RequiresUnreferencedCode("This method uses reflection to access properties by name.")] - [RequiresDynamicCode("This method uses reflection to access properties by name.")] -#endif + [RequiresUnreferencedCode("Evaluates expression-based member chains via reflection; members may be trimmed.")] public static IObservable> ObservableForProperty( this TSender? item, Expression> property, @@ -267,10 +253,7 @@ public static IObservable> ObservableForPropert /// An Observable representing the property change /// notifications for the given property. /// -#if NET6_0_OR_GREATER - [RequiresUnreferencedCode("This method uses reflection to access properties by name.")] - [RequiresDynamicCode("This method uses reflection to access properties by name.")] -#endif + [RequiresUnreferencedCode("Evaluates expression-based member chains via reflection; members may be trimmed.")] public static IObservable> ObservableForProperty( this TSender? item, Expression> property, @@ -298,10 +281,7 @@ public static IObservable> ObservableForPropert /// An Observable representing the property change /// notifications for the given property. /// -#if NET6_0_OR_GREATER - [RequiresUnreferencedCode("This method uses reflection to access properties by name.")] - [RequiresDynamicCode("This method uses reflection to access properties by name.")] -#endif + [RequiresUnreferencedCode("Evaluates expression-based member chains via reflection; members may be trimmed.")] public static IObservable> ObservableForProperty( this TSender? item, Expression> property, @@ -327,11 +307,11 @@ public static IObservable> ObservableForPropert */ return SubscribeToExpressionChain( - item, - property.Body, - beforeChange, - skipInitial, - isDistinct); + item, + property.Body, + beforeChange, + skipInitial, + isDistinct); } /// @@ -350,10 +330,7 @@ public static IObservable> ObservableForPropert /// item. /// An Observable representing the property change /// notifications for the given property. -#if NET6_0_OR_GREATER - [RequiresUnreferencedCode("This method uses reflection to access properties by name.")] - [RequiresDynamicCode("This method uses reflection to access properties by name.")] -#endif + [RequiresUnreferencedCode("Evaluates expression-based member chains via reflection; members may be trimmed.")] public static IObservable ObservableForProperty( this TSender? item, Expression> property, @@ -384,10 +361,7 @@ public static IObservable ObservableForProperty( /// immediately before a property is going to change. /// An Observable representing the property change /// notifications for the given property. -#if NET6_0_OR_GREATER - [RequiresUnreferencedCode("This method uses reflection to access properties by name.")] - [RequiresDynamicCode("This method uses reflection to access properties by name.")] -#endif + [RequiresUnreferencedCode("Evaluates expression-based member chains via reflection; members may be trimmed.")] public static IObservable ObservableForProperty( this TSender? item, Expression> property, @@ -414,10 +388,7 @@ public static IObservable ObservableForProperty( /// A observable which notifies about observed changes. /// /// If we cannot cast from the target value from the specified last property. -#if NET6_0_OR_GREATER - [RequiresUnreferencedCode("This method uses reflection to access properties by name.")] - [RequiresDynamicCode("This method uses reflection to access properties by name.")] -#endif + [RequiresUnreferencedCode("Evaluates expression-based member chains via reflection; members may be trimmed.")] public static IObservable> SubscribeToExpressionChain( this TSender? source, Expression? expression) // TODO: Create Test @@ -437,10 +408,7 @@ public static IObservable> SubscribeToExpressio /// A observable which notifies about observed changes. /// /// If we cannot cast from the target value from the specified last property. -#if NET6_0_OR_GREATER - [RequiresUnreferencedCode("This method uses reflection to access properties by name.")] - [RequiresDynamicCode("This method uses reflection to access properties by name.")] -#endif + [RequiresUnreferencedCode("Evaluates expression-based member chains via reflection; members may be trimmed.")] public static IObservable> SubscribeToExpressionChain( this TSender? source, Expression? expression, @@ -462,10 +430,7 @@ public static IObservable> SubscribeToExpressio /// A observable which notifies about observed changes. /// /// If we cannot cast from the target value from the specified last property. -#if NET6_0_OR_GREATER - [RequiresUnreferencedCode("This method uses reflection to access properties by name.")] - [RequiresDynamicCode("This method uses reflection to access properties by name.")] -#endif + [RequiresUnreferencedCode("Evaluates expression-based member chains via reflection; members may be trimmed.")] public static IObservable> SubscribeToExpressionChain( this TSender? source, Expression? expression, @@ -489,10 +454,7 @@ public static IObservable> SubscribeToExpressio /// A observable which notifies about observed changes. /// /// If we cannot cast from the target value from the specified last property. -#if NET6_0_OR_GREATER - [RequiresUnreferencedCode("This method uses reflection to access properties by name.")] - [RequiresDynamicCode("This method uses reflection to access properties by name.")] -#endif + [RequiresUnreferencedCode("Evaluates expression-based member chains via reflection; members may be trimmed.")] public static IObservable> SubscribeToExpressionChain( this TSender? source, Expression? expression, @@ -518,10 +480,7 @@ public static IObservable> SubscribeToExpressio /// A observable which notifies about observed changes. /// /// If we cannot cast from the target value from the specified last property. -#if NET6_0_OR_GREATER - [RequiresUnreferencedCode("This method uses reflection to access properties by name.")] - [RequiresDynamicCode("This method uses reflection to access properties by name.")] -#endif + [RequiresUnreferencedCode("Evaluates expression-based member chains via reflection; members may be trimmed.")] public static IObservable> SubscribeToExpressionChain( this TSender? source, Expression? expression, @@ -565,10 +524,7 @@ public static IObservable> SubscribeToExpressio return r; } -#if NET6_0_OR_GREATER - [RequiresUnreferencedCode("This method uses reflection to access properties by name.")] - [RequiresDynamicCode("This method uses reflection to access properties by name.")] -#endif + [RequiresUnreferencedCode("Evaluates expression-based member chains via reflection; members may be trimmed.")] private static IObservable> NestedObservedChanges(Expression expression, IObservedChange sourceChange, bool beforeChange, bool suppressWarnings) { // Make sure a change at a root node propagates events down @@ -586,10 +542,7 @@ public static IObservable> SubscribeToExpressio .Select(static x => new ObservedChange(x.Sender, x.Expression, x.GetValueOrDefault())); } -#if NET6_0_OR_GREATER - [RequiresUnreferencedCode("This method uses reflection to access properties by name.")] - [RequiresDynamicCode("This method uses reflection to access properties by name.")] -#endif + [RequiresUnreferencedCode("Evaluates expression-based member chains via reflection; members may be trimmed.")] private static IObservable> NotifyForProperty(object sender, Expression expression, bool beforeChange, bool suppressWarnings) { ArgumentExceptionHelper.ThrowIfNull(expression); @@ -600,7 +553,7 @@ public static IObservable> SubscribeToExpressio return result switch { - null => throw new Exception($"Could not find a ICreatesObservableForProperty for {sender.GetType()} property {propertyName}. This should never happen, your service locator is probably broken. 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."), + null => throw new InvalidOperationException($"Could not find a ICreatesObservableForProperty for {sender.GetType()} property {propertyName}. This should never happen, your service locator is probably broken. 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."), _ => result.GetNotificationForProperty(sender, expression, propertyName, beforeChange, suppressWarnings) }; } diff --git a/src/ReactiveUI/ObservableForProperty/INPCObservableForProperty.cs b/src/ReactiveUI/ObservableForProperty/INPCObservableForProperty.cs index 25a432d123..4906b7a7ef 100644 --- a/src/ReactiveUI/ObservableForProperty/INPCObservableForProperty.cs +++ b/src/ReactiveUI/ObservableForProperty/INPCObservableForProperty.cs @@ -13,10 +13,7 @@ namespace ReactiveUI; public class INPCObservableForProperty : 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 + [RequiresUnreferencedCode("Uses reflection over runtime types which is not trim- or AOT-safe.")] public int GetAffinityForObject(Type type, string propertyName, bool beforeChanged) { var target = beforeChanged ? typeof(INotifyPropertyChanging) : typeof(INotifyPropertyChanged); @@ -24,10 +21,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 + [RequiresUnreferencedCode("Uses reflection over runtime types which is not trim- or AOT-safe.")] public IObservable> GetNotificationForProperty(object sender, Expression expression, string propertyName, bool beforeChanged = false, bool suppressWarnings = false) { ArgumentExceptionHelper.ThrowIfNull(expression); diff --git a/src/ReactiveUI/ObservableForProperty/IROObservableForProperty.cs b/src/ReactiveUI/ObservableForProperty/IROObservableForProperty.cs index 46bae6a297..1253fca043 100644 --- a/src/ReactiveUI/ObservableForProperty/IROObservableForProperty.cs +++ b/src/ReactiveUI/ObservableForProperty/IROObservableForProperty.cs @@ -8,60 +8,79 @@ namespace ReactiveUI; /// -/// Generates Observables based on observing Reactive objects. +/// Generates observables for instances by subscribing to their change notifications. /// -public class IROObservableForProperty : ICreatesObservableForProperty +/// +/// +/// This implementation filters the change stream for a specific property name and projects each matching notification to +/// an . +/// +/// +/// Trimming/AOT: is annotated for trimming/AOT in this codebase. This type +/// repeats the required annotations on its public members to satisfy the interface contract. +/// +/// +public sealed class IROObservableForProperty : 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 + /// + /// + /// This implementation returns a higher affinity than the INPC-based implementation because every + /// also implements property change notification and should be preferred when available. + /// + /// The runtime type to query. + /// The property name to query. + /// + /// If , indicates the caller requests notifications before the property value changes. + /// If , indicates after-change notifications. + /// + /// + /// A positive integer if supported; zero otherwise. + /// + [RequiresUnreferencedCode("Uses reflection over runtime types which is not trim- or AOT-safe.")] public int GetAffinityForObject(Type type, string propertyName, bool beforeChanged = false) { - // NB: Since every IReactiveObject is also an INPC, we need to bind more - // tightly than INPCObservableForProperty, so we return 10 here - // instead of one -#pragma warning disable IDE0022 // Use expression body for methods + ArgumentExceptionHelper.ThrowIfNull(type); + ArgumentExceptionHelper.ThrowIfNull(propertyName); + + // NB: Since every IReactiveObject is also an INPC, we need to bind more tightly than INPCObservableForProperty. return typeof(IReactiveObject).GetTypeInfo().IsAssignableFrom(type.GetTypeInfo()) ? 10 : 0; -#pragma warning restore IDE0022 // Use expression body for methods } - /// -#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) + /// + /// Thrown when is . + /// Thrown when does not implement . + [RequiresUnreferencedCode("Uses reflection over runtime types which is not trim- or AOT-safe.")] + public IObservable> GetNotificationForProperty( + object sender, + Expression expression, + string propertyName, + bool beforeChanged = false, + bool suppressWarnings = false) { + ArgumentExceptionHelper.ThrowIfNull(sender); ArgumentExceptionHelper.ThrowIfNull(expression); + ArgumentExceptionHelper.ThrowIfNull(propertyName); if (sender is not IReactiveObject iro) { - throw new ArgumentException("Sender doesn't implement IReactiveObject"); + throw new ArgumentException("Sender doesn't implement IReactiveObject", nameof(sender)); } - var obs = beforeChanged ? iro.GetChangingObservable() : iro.GetChangedObservable(); - - if (beforeChanged) - { - if (expression.NodeType == ExpressionType.Index) - { - return obs.Where(x => x.PropertyName?.Equals(propertyName + "[]", StringComparison.InvariantCulture) == true) - .Select(_ => new ObservedChange(sender, expression, default)); - } + // For indexers, ReactiveObject reports "PropertyName[]". + var observedName = + expression.NodeType == ExpressionType.Index + ? string.Concat(propertyName, "[]") + : propertyName; - return obs.Where(x => x.PropertyName?.Equals(propertyName, StringComparison.InvariantCulture) == true) - .Select(_ => new ObservedChange(sender, expression, default)); - } + // Preserve the original comparison semantics. + const StringComparison comparison = StringComparison.InvariantCulture; - if (expression.NodeType == ExpressionType.Index) - { - return obs.Where(x => x.PropertyName?.Equals(propertyName + "[]", StringComparison.InvariantCulture) == true) - .Select(_ => new ObservedChange(sender, expression, default)); - } + var source = beforeChanged ? iro.GetChangingObservable() : iro.GetChangedObservable(); - return obs.Where(x => x.PropertyName?.Equals(propertyName, StringComparison.InvariantCulture) == true) - .Select(_ => new ObservedChange(sender, expression, default)); + // Keep the projection allocation-free; avoid repeating the same query shape. + return source + .Where(x => x.PropertyName is not null && x.PropertyName.Equals(observedName, comparison)) + .Select(static _ => default(object)) + .Select(_ => new ObservedChange(sender, expression, default)); } } diff --git a/src/ReactiveUI/ObservableForProperty/OAPHCreationHelperMixin.cs b/src/ReactiveUI/ObservableForProperty/OAPHCreationHelperMixin.cs index b8e25b1885..dfdfd30bfb 100644 --- a/src/ReactiveUI/ObservableForProperty/OAPHCreationHelperMixin.cs +++ b/src/ReactiveUI/ObservableForProperty/OAPHCreationHelperMixin.cs @@ -40,10 +40,6 @@ public static class OAPHCreationHelperMixin /// An initialized ObservableAsPropertyHelper; use this as the backing field /// for your property. /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("ObservableToProperty uses RaisingPropertyChanged which requires dynamic code generation")] - [RequiresUnreferencedCode("ObservableToProperty uses RaisingPropertyChanged which may require unreferenced code")] -#endif public static ObservableAsPropertyHelper ToProperty( this IObservable target, TObj source, @@ -92,10 +88,6 @@ public static ObservableAsPropertyHelper ToProperty( /// An initialized ObservableAsPropertyHelper; use this as the backing field /// for your property. /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("ObservableToProperty uses RaisingPropertyChanged which requires dynamic code generation")] - [RequiresUnreferencedCode("ObservableToProperty uses RaisingPropertyChanged which may require unreferenced code")] -#endif public static ObservableAsPropertyHelper ToProperty( this IObservable target, TObj source, @@ -139,10 +131,6 @@ public static ObservableAsPropertyHelper ToProperty( /// An initialized ObservableAsPropertyHelper; use this as the backing field /// for your property. /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("ObservableToProperty uses RaisingPropertyChanged which requires dynamic code generation")] - [RequiresUnreferencedCode("ObservableToProperty uses RaisingPropertyChanged which may require unreferenced code")] -#endif public static ObservableAsPropertyHelper ToProperty( this IObservable target, TObj source, @@ -189,10 +177,6 @@ public static ObservableAsPropertyHelper ToProperty( /// An initialized ObservableAsPropertyHelper; use this as the backing /// field for your property. /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("ObservableToProperty uses RaisingPropertyChanged which requires dynamic code generation")] - [RequiresUnreferencedCode("ObservableToProperty uses RaisingPropertyChanged which may require unreferenced code")] -#endif public static ObservableAsPropertyHelper ToProperty( this IObservable target, TObj source, @@ -249,10 +233,6 @@ public static ObservableAsPropertyHelper ToProperty( /// An initialized ObservableAsPropertyHelper; use this as the backing /// field for your property. /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("ObservableToProperty uses RaisingPropertyChanged which requires dynamic code generation")] - [RequiresUnreferencedCode("ObservableToProperty uses RaisingPropertyChanged which may require unreferenced code")] -#endif public static ObservableAsPropertyHelper ToProperty( this IObservable target, TObj source, @@ -300,10 +280,6 @@ public static ObservableAsPropertyHelper ToProperty( /// An initialized ObservableAsPropertyHelper; use this as the backing /// field for your property. /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("ObservableToProperty uses RaisingPropertyChanged which requires dynamic code generation")] - [RequiresUnreferencedCode("ObservableToProperty uses RaisingPropertyChanged which may require unreferenced code")] -#endif public static ObservableAsPropertyHelper ToProperty( this IObservable target, TObj source, @@ -355,10 +331,6 @@ public static ObservableAsPropertyHelper ToProperty( /// An initialized ObservableAsPropertyHelper; use this as the backing field /// for your property. /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("ObservableToProperty uses RaisingPropertyChanged which requires dynamic code generation")] - [RequiresUnreferencedCode("ObservableToProperty uses RaisingPropertyChanged which may require unreferenced code")] -#endif public static ObservableAsPropertyHelper ToProperty( this IObservable target, TObj source, @@ -400,10 +372,6 @@ public static ObservableAsPropertyHelper ToProperty( /// An initialized ObservableAsPropertyHelper; use this as the backing field /// for your property. /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("ObservableToProperty uses RaisingPropertyChanged which requires dynamic code generation")] - [RequiresUnreferencedCode("ObservableToProperty uses RaisingPropertyChanged which may require unreferenced code")] -#endif public static ObservableAsPropertyHelper ToProperty( this IObservable target, TObj source, @@ -454,10 +422,6 @@ public static ObservableAsPropertyHelper ToProperty( /// An initialized ObservableAsPropertyHelper; use this as the backing field /// for your property. /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("ObservableToProperty uses RaisingPropertyChanged which requires dynamic code generation")] - [RequiresUnreferencedCode("ObservableToProperty uses RaisingPropertyChanged which may require unreferenced code")] -#endif public static ObservableAsPropertyHelper ToProperty( this IObservable target, TObj source, @@ -508,10 +472,6 @@ public static ObservableAsPropertyHelper ToProperty( /// An initialized ObservableAsPropertyHelper; use this as the backing /// field for your property. /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("ObservableToProperty uses RaisingPropertyChanged which requires dynamic code generation")] - [RequiresUnreferencedCode("ObservableToProperty uses RaisingPropertyChanged which may require unreferenced code")] -#endif public static ObservableAsPropertyHelper ToProperty( this IObservable target, TObj source, @@ -567,10 +527,6 @@ public static ObservableAsPropertyHelper ToProperty( /// An initialized ObservableAsPropertyHelper; use this as the backing /// field for your property. /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("ObservableToProperty uses RaisingPropertyChanged which requires dynamic code generation")] - [RequiresUnreferencedCode("ObservableToProperty uses RaisingPropertyChanged which may require unreferenced code")] -#endif public static ObservableAsPropertyHelper ToProperty( this IObservable target, TObj source, @@ -591,10 +547,6 @@ public static ObservableAsPropertyHelper ToProperty( return result; } -#if NET6_0_OR_GREATER - [RequiresDynamicCode("ObservableToProperty uses RaisingPropertyChanged which requires dynamic code generation")] - [RequiresUnreferencedCode("ObservableToProperty uses RaisingPropertyChanged which may require unreferenced code")] -#endif internal static ObservableAsPropertyHelper ObservableToProperty( this TObj target, IObservable observable, @@ -632,10 +584,6 @@ internal static ObservableAsPropertyHelper ObservableToProperty ObservableToProperty( this TObj target, IObservable observable, @@ -672,10 +620,6 @@ internal static ObservableAsPropertyHelper ObservableToProperty ObservableToProperty( this TObj target, IObservable observable, @@ -698,10 +642,6 @@ internal static ObservableAsPropertyHelper ObservableToProperty ObservableToProperty( this TObj target, IObservable observable, diff --git a/src/ReactiveUI/ObservableForProperty/ObservableAsPropertyHelper.cs b/src/ReactiveUI/ObservableForProperty/ObservableAsPropertyHelper.cs index 6653c45ecf..6a5262f4e9 100644 --- a/src/ReactiveUI/ObservableForProperty/ObservableAsPropertyHelper.cs +++ b/src/ReactiveUI/ObservableForProperty/ObservableAsPropertyHelper.cs @@ -63,10 +63,6 @@ public sealed class ObservableAsPropertyHelper : IHandleObservableErrors, IDi /// The scheduler that the notifications will be provided on - /// this should normally be a Dispatcher-based scheduler. /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("ObservableAsPropertyHelper uses methods that require dynamic code generation")] - [RequiresUnreferencedCode("ObservableAsPropertyHelper uses methods that may require unreferenced code")] -#endif public ObservableAsPropertyHelper( IObservable observable, Action onChanged, @@ -104,10 +100,6 @@ public ObservableAsPropertyHelper( /// The scheduler that the notifications will provided on - this /// should normally be a Dispatcher-based scheduler. /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("ObservableAsPropertyHelper uses methods that require dynamic code generation")] - [RequiresUnreferencedCode("ObservableAsPropertyHelper uses methods that may require unreferenced code")] -#endif public ObservableAsPropertyHelper( IObservable observable, Action onChanged, @@ -146,10 +138,6 @@ public ObservableAsPropertyHelper( /// The scheduler that the notifications will provided on - this /// should normally be a Dispatcher-based scheduler. /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("ObservableAsPropertyHelper uses methods that require dynamic code generation")] - [RequiresUnreferencedCode("ObservableAsPropertyHelper uses methods that may require unreferenced code")] -#endif public ObservableAsPropertyHelper( IObservable observable, Action onChanged, @@ -164,7 +152,7 @@ public ObservableAsPropertyHelper( scheduler ??= CurrentThreadScheduler.Instance; onChanging ??= _ => { }; - _thrownExceptions = new Lazy>(() => new ScheduledSubject(CurrentThreadScheduler.Instance, RxApp.DefaultExceptionHandler)); + _thrownExceptions = new Lazy>(() => new ScheduledSubject(CurrentThreadScheduler.Instance, RxState.DefaultExceptionHandler)); _subject = new ScheduledSubject(scheduler); _subject.Subscribe( @@ -249,10 +237,6 @@ public T Value /// normally be a Dispatcher-based scheduler. /// /// A default property helper. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("ObservableAsPropertyHelper uses methods that require dynamic code generation")] - [RequiresUnreferencedCode("ObservableAsPropertyHelper uses methods that may require unreferenced code")] -#endif public static ObservableAsPropertyHelper Default(T? initialValue = default, IScheduler? scheduler = null) => // TODO: Create Test new(Observable.Never, static _ => { }, initialValue!, false, scheduler); diff --git a/src/ReactiveUI/ObservableForProperty/POCOObservableForProperty.cs b/src/ReactiveUI/ObservableForProperty/POCOObservableForProperty.cs index 8fe0a2ef39..c9bd43b44b 100644 --- a/src/ReactiveUI/ObservableForProperty/POCOObservableForProperty.cs +++ b/src/ReactiveUI/ObservableForProperty/POCOObservableForProperty.cs @@ -4,42 +4,96 @@ // See the LICENSE file in the project root for full license information. using System.Collections.Concurrent; +using System.Runtime.CompilerServices; namespace ReactiveUI; /// -/// This class is the final fallback for WhenAny, and will simply immediately -/// return the value of the type at the time it was created. It will also -/// warn the user that this is probably not what they want to do. +/// Final fallback implementation for WhenAny-style observation when no observable mechanism is available. /// -public class POCOObservableForProperty : ICreatesObservableForProperty +/// +/// +/// This implementation emits exactly one value (the current value at subscription time) and then never emits again. +/// +/// +/// If warnings are enabled, it logs a warning once per (runtime type, property name) pair to help callers detect +/// accidental POCO usage in observation chains. +/// +/// +/// Trimming/AOT: is annotated for trimming/AOT in this codebase; this type +/// repeats the required annotations on its public members to satisfy the interface contract. +/// +/// +public sealed class POCOObservableForProperty : ICreatesObservableForProperty { - private static readonly ConcurrentDictionary<(Type, string), bool> _hasWarned = new(); + /// + /// Tracks whether a warning has been logged for a given (runtime type, property name) pair. + /// + /// + /// This is a process-wide cache intended to avoid repeated warnings. It can grow with unique observed pairs. + /// + private static readonly ConcurrentDictionary<(Type Type, string PropertyName), byte> HasWarned = new(); - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("GetAffinityForObject uses reflection and type analysis")] - [RequiresUnreferencedCode("GetAffinityForObject may reference members that could be trimmed")] -#endif - public int GetAffinityForObject(Type type, string propertyName, bool beforeChanged = false) => 1; + /// + /// + /// This fallback returns a very low affinity to ensure it is only used when no more specific implementation applies. + /// + [RequiresUnreferencedCode("Uses reflection over runtime types which is not trim- or AOT-safe.")] + public int GetAffinityForObject(Type type, string propertyName, bool beforeChanged = false) + { + ArgumentExceptionHelper.ThrowIfNull(type); + ArgumentExceptionHelper.ThrowIfNull(propertyName); - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("GetNotificationForProperty uses reflection and type analysis")] - [RequiresUnreferencedCode("GetNotificationForProperty may reference members that could be trimmed")] -#endif - public IObservable> GetNotificationForProperty(object sender, Expression expression, string propertyName, bool beforeChanged = false, bool suppressWarnings = false) + return 1; + } + + /// + /// Thrown when is . + [RequiresUnreferencedCode("Uses reflection over runtime types which is not trim- or AOT-safe.")] + public IObservable> GetNotificationForProperty( + object sender, + Expression expression, + string propertyName, + bool beforeChanged = false, + bool suppressWarnings = false) { ArgumentExceptionHelper.ThrowIfNull(sender); + ArgumentExceptionHelper.ThrowIfNull(expression); + ArgumentExceptionHelper.ThrowIfNull(propertyName); + + if (!suppressWarnings) + { + WarnOnce(sender, propertyName); + } + + // Emit one value, then never complete to preserve legacy WhenAny semantics. + return Observable + .Return(new ObservedChange(sender, expression, default), RxSchedulers.MainThreadScheduler) + .Concat(Observable>.Never); + } + /// + /// Logs a POCO observation warning at most once per (runtime type, property name) pair. + /// + /// The observed object. + /// The observed property name. +#if NET8_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + private void WarnOnce(object sender, string propertyName) + { + // Hot path considerations: + // - Avoid ContainsKey + indexer (two lookups). + // - Use TryAdd as the single atomic gate. var type = sender.GetType(); - if (!_hasWarned.ContainsKey((type, propertyName)) && !suppressWarnings) + if (!HasWarned.TryAdd((type, propertyName), 0)) { - this.Log().Warn($"The class {type.FullName} property {propertyName} is a POCO type and won't send change notifications, WhenAny will only return a single value!"); - _hasWarned[(type, propertyName)] = true; + return; } - return Observable.Return(new ObservedChange(sender, expression, default), RxApp.MainThreadScheduler) - .Concat(Observable>.Never); + this.Log().Warn( + $"The class {type.FullName} property {propertyName} is a POCO type and won't send change notifications, WhenAny will only return a single value!"); } } diff --git a/src/ReactiveUI/ObservableFuncMixins.cs b/src/ReactiveUI/ObservableFuncMixins.cs index a6a1822408..f14b66ffa9 100644 --- a/src/ReactiveUI/ObservableFuncMixins.cs +++ b/src/ReactiveUI/ObservableFuncMixins.cs @@ -22,10 +22,7 @@ public static class ObservableFuncMixins /// /// An observable Result. /// -#if NET6_0_OR_GREATER - [RequiresUnreferencedCode("This method uses reflection to access properties by name.")] - [RequiresDynamicCode("This method uses reflection to access properties by name.")] -#endif + [RequiresUnreferencedCode("Dynamic observation uses reflection over members that may be trimmed.")] public static IObservable ToObservable( this Expression> expression, TSource? source, @@ -35,7 +32,7 @@ public static class ObservableFuncMixins ArgumentExceptionHelper.ThrowIfNull(expression); var sParam = Reflection.Rewrite(expression.Body); - return source.SubscribeToExpressionChain(sParam, beforeChange, skipInitial, RxApp.SuppressViewCommandBindingMessage) + return source.SubscribeToExpressionChain(sParam, beforeChange, skipInitial, RxSchedulers.SuppressViewCommandBindingMessage) .Select(static x => x.GetValue()) .Retry(); } diff --git a/src/ReactiveUI/PlatformRegistrationManager.cs b/src/ReactiveUI/PlatformRegistrationManager.cs deleted file mode 100644 index 04c9a7b5d6..0000000000 --- a/src/ReactiveUI/PlatformRegistrationManager.cs +++ /dev/null @@ -1,28 +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. - -namespace ReactiveUI; - -/// -/// Class that represents the platform registration for ReactiveUI. -/// -public static class PlatformRegistrationManager -{ - internal static RegistrationNamespace[] DefaultRegistrationNamespaces { get; } = -#if NET6_0_OR_GREATER - Enum.GetValues(); -#else - (RegistrationNamespace[])Enum.GetValues(typeof(RegistrationNamespace)); -#endif - - internal static RegistrationNamespace[] NamespacesToRegister { get; set; } = DefaultRegistrationNamespaces; - - /// - /// Set the platform namespaces to register. - /// This needs to be set before the first call to . - /// - /// The namespaces to register. - public static void SetRegistrationNamespaces(params RegistrationNamespace[] namespaces) => NamespacesToRegister = namespaces; -} diff --git a/src/ReactiveUI/Platforms/android/AndroidCommandBinders.cs b/src/ReactiveUI/Platforms/android/AndroidCommandBinders.cs index 147d524632..7f423afd9b 100644 --- a/src/ReactiveUI/Platforms/android/AndroidCommandBinders.cs +++ b/src/ReactiveUI/Platforms/android/AndroidCommandBinders.cs @@ -10,19 +10,43 @@ namespace ReactiveUI; /// -/// Android implementation that provides binding to an ICommand in the ViewModel to a Control -/// in the View. +/// Android implementation that provides binding to an ICommand in the ViewModel to a control in the View. /// [Preserve(AllMembers = true)] -public class AndroidCommandBinders : FlexibleCommandBinder +public sealed class AndroidCommandBinders : FlexibleCommandBinder { /// /// Initializes a new instance of the class. /// - [SuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Marked as Preserve")] + /// + /// Thrown when the Enabled property cannot be found on , which is required for binding. + /// public AndroidCommandBinders() { - var view = typeof(View); - Register(view, 9, (cmd, t, cp) => ForEvent(cmd, t, cp, "Click", view.GetRuntimeProperty("Enabled") ?? throw new InvalidOperationException("Could not find property 'Enabled' on type View, which is needed for binding"))); + var viewType = typeof(View); + + // Cache reflection metadata once at registration time. + var enabledProperty = + viewType.GetRuntimeProperty("Enabled") + ?? throw new InvalidOperationException( + "Could not find property 'Enabled' on type View, which is needed for binding"); + + // Precompute the setter once; ForEvent will no-op enabled sync if null (but for View it should exist). + var enabledSetter = Reflection.GetValueSetterForProperty(enabledProperty); + + Register(viewType, 9, (cmd, target, commandParameter) => + { + // Keep existing behavior: ForEvent throws if cmd is null. + // Also keep the "null commandParameter means use target" idiom (handled inside ForEvent overload). + var view = (View)target!; + + return ForEvent( + cmd, + view, + commandParameter, + addHandler: h => view.Click += h, + removeHandler: h => view.Click -= h, + enabledSetter: enabledSetter); + }); } } diff --git a/src/ReactiveUI/Platforms/android/AndroidObservableForWidgets.cs b/src/ReactiveUI/Platforms/android/AndroidObservableForWidgets.cs index 19a09d59ff..033ea67fe0 100644 --- a/src/ReactiveUI/Platforms/android/AndroidObservableForWidgets.cs +++ b/src/ReactiveUI/Platforms/android/AndroidObservableForWidgets.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.Collections.Frozen; +using System.Diagnostics.CodeAnalysis; using System.Runtime.Versioning; using Android.OS; @@ -15,173 +17,390 @@ namespace ReactiveUI; /// -/// Android view objects are not Generally Observable™, so hard-code some -/// particularly useful types. +/// Provides property change notifications for a curated set of Android widget types which are not generally observable +/// through standard property change mechanisms. /// +/// +/// +/// This implementation only supports after-change notifications; beforeChange is not supported. +/// +/// +/// A static dispatch table maps widget types and property names to observable factory functions. +/// +/// +/// Trimming/AOT: this type repeats the trimming/AOT annotations required by the +/// interface on its implementing members to satisfy the interface contract. +/// +/// [Preserve(AllMembers = true)] -public class AndroidObservableForWidgets : ICreatesObservableForProperty +public sealed class AndroidObservableForWidgets : ICreatesObservableForProperty { - private static readonly Dictionary<(Type viewType, string? propertyName), Func>>> _dispatchTable; + /// + /// Stores observable factory functions keyed by (widget type, property name). + /// + /// + /// This table is immutable after type initialization and is safe for concurrent reads. + /// + private static readonly FrozenDictionary<(Type ViewType, string PropertyName), Func>>> DispatchTable; + /// + /// Stores, per property name, the set of widget types that can produce notifications for that property. + /// + /// + /// This index supports efficient affinity checks and dispatch selection without scanning the entire dispatch table. + /// + private static readonly FrozenDictionary TypesByPropertyName; + + /// + /// Initializes static members of the class. + /// Initializes the static dispatch tables for the supported Android widgets. + /// + /// + /// This constructor runs once and constructs immutable lookup tables for fast concurrent reads. + /// [ObsoletedOSPlatform("android23.0")] [SupportedOSPlatform("android23.0")] - static AndroidObservableForWidgets() => - _dispatchTable = new[] + static AndroidObservableForWidgets() + { + var items = new[] { - CreateFromWidget(static v => v.Text, static (v, h) => v.TextChanged += h, static (v, h) => v.TextChanged -= h), - CreateFromWidget(static v => v.Value, static (v, h) => v.ValueChanged += h, static (v, h) => v.ValueChanged -= h), - CreateFromWidget(static v => v.Rating, static (v, h) => v.RatingBarChange += h, static (v, h) => v.RatingBarChange -= h), - CreateFromWidget(static v => v.Checked, static (v, h) => v.CheckedChange += h, static (v, h) => v.CheckedChange -= h), - CreateFromWidget(static v => v.Date, static (v, h) => v.DateChange += h, static (v, h) => v.DateChange -= h), - CreateFromWidget(static v => v.CurrentTab, static (v, h) => v.TabChanged += h, static (v, h) => v.TabChanged -= h), + CreateFromWidget( + static v => v.Text, + static (v, h) => v.TextChanged += h, + static (v, h) => v.TextChanged -= h), + + CreateFromWidget( + static v => v.Value, + static (v, h) => v.ValueChanged += h, + static (v, h) => v.ValueChanged -= h), + + CreateFromWidget( + static v => v.Rating, + static (v, h) => v.RatingBarChange += h, + static (v, h) => v.RatingBarChange -= h), + + CreateFromWidget( + static v => v.Checked, + static (v, h) => v.CheckedChange += h, + static (v, h) => v.CheckedChange -= h), + + CreateFromWidget( + static v => v.Date, + static (v, h) => v.DateChange += h, + static (v, h) => v.DateChange -= h), + + CreateFromWidget( + static v => v.CurrentTab, + static (v, h) => v.TabChanged += h, + static (v, h) => v.TabChanged -= h), + CreateTimePickerHourFromWidget(), CreateTimePickerMinuteFromWidget(), CreateFromAdapterView(), - }.ToDictionary(static k => (viewType: k.Type, propertyName: k.Property), static v => v.Func); + }; + + var dispatch = + new Dictionary<(Type ViewType, string PropertyName), Func>>>( + capacity: items.Length); + + var byProperty = + new Dictionary>(capacity: items.Length, comparer: StringComparer.Ordinal); + + for (var i = 0; i < items.Length; i++) + { + var item = items[i]; + + if (item.Property is null) + { + continue; + } - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("GetAffinityForObject uses reflection for property access and type checking which require dynamic code generation")] - [RequiresUnreferencedCode("GetAffinityForObject uses reflection for property access and type checking which may require unreferenced code")] -#endif + dispatch[(item.Type, item.Property)] = item.Func; + + if (!byProperty.TryGetValue(item.Property, out var list)) + { + list = new List(capacity: 2); + byProperty.Add(item.Property, list); + } + + list.Add(item.Type); + } + + DispatchTable = dispatch.ToFrozenDictionary(); + + var index = new Dictionary(byProperty.Count, StringComparer.Ordinal); + foreach (var pair in byProperty) + { + index[pair.Key] = pair.Value.ToArray(); + } + + TypesByPropertyName = index.ToFrozenDictionary(StringComparer.Ordinal); + } + + /// + /// + /// This implementation does not support before-change notifications. + /// + [RequiresUnreferencedCode("Uses reflection over runtime types which is not trim- or AOT-safe.")] public int GetAffinityForObject(Type type, string propertyName, bool beforeChanged = false) { + ArgumentNullException.ThrowIfNull(type); + ArgumentNullException.ThrowIfNull(propertyName); + if (beforeChanged) { return 0; } - return _dispatchTable.Keys.Any(x => x.viewType?.IsAssignableFrom(type) == true && x.propertyName?.Equals(propertyName) == true) ? 5 : 0; + if (!TypesByPropertyName.TryGetValue(propertyName, out var candidates)) + { + return 0; + } + + for (var i = 0; i < candidates.Length; i++) + { + if (candidates[i].IsAssignableFrom(type)) + { + return 5; + } + } + + return 0; } - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("GetNotificationForProperty uses reflection for property access and type checking which require dynamic code generation")] - [RequiresUnreferencedCode("GetNotificationForProperty uses reflection for property access and type checking which may require unreferenced code")] -#endif - public IObservable> GetNotificationForProperty(object sender, Expression expression, string propertyName, bool beforeChanged = false, bool suppressWarnings = false) + /// + /// + /// This implementation does not support before-change notifications. + /// + /// + /// Thrown when , , or is + /// . + /// + [RequiresUnreferencedCode("Uses reflection over runtime types which is not trim- or AOT-safe.")] + public IObservable> GetNotificationForProperty( + object sender, + Expression expression, + string propertyName, + bool beforeChanged = false, + bool suppressWarnings = false) { - var type = sender?.GetType(); - var tableItem = _dispatchTable.Keys.First(x => x.viewType?.IsAssignableFrom(type) == true && x.propertyName?.Equals(propertyName) == true); + ArgumentNullException.ThrowIfNull(sender); + ArgumentNullException.ThrowIfNull(expression); + ArgumentNullException.ThrowIfNull(propertyName); + + if (beforeChanged) + { + return Observable.Never>(); + } + + var senderType = sender.GetType(); + + if (!TypesByPropertyName.TryGetValue(propertyName, out var candidates)) + { + return Observable.Never>(); + } - return !_dispatchTable.TryGetValue(tableItem, out var dispatchItem) ? - Observable.Never>() : - dispatchItem.Invoke(sender!, expression); + for (var i = 0; i < candidates.Length; i++) + { + var candidateType = candidates[i]; + + if (!candidateType.IsAssignableFrom(senderType)) + { + continue; + } + + return DispatchTable.TryGetValue((candidateType, propertyName), out var factory) + ? factory(sender, expression) + : Observable.Never>(); + } + + return Observable.Never>(); } + /// + /// Creates a dispatch item for selection changes on instances. + /// + /// + /// Adapter selection is represented by two distinct events: and + /// . This dispatch item merges both into a single observable sequence. + /// + /// + /// A dispatch item mapping and the SelectedItem property to an observable factory. + /// private static DispatchItem CreateFromAdapterView() { - // AdapterView is more complicated because there are two events - one for select and one for deselect - // respond to both - const string PropName = "SelectedItem"; + const string propName = "SelectedItem"; return new DispatchItem( - typeof(AdapterView), - PropName, - (x, ex) => - { - var adapterView = (AdapterView)x; - - var itemSelected = - Observable - .FromEvent, ObservedChange - >( - eventHandler => - { - void Handler(object? sender, AdapterView.ItemSelectedEventArgs e) => - eventHandler(new ObservedChange(adapterView, ex, default)); - - return Handler; - }, - h => adapterView.ItemSelected += h, - h => adapterView.ItemSelected -= h); - - var nothingSelected = - Observable - .FromEvent, - ObservedChange>( - eventHandler => - { - void Handler(object? sender, AdapterView.NothingSelectedEventArgs e) => - eventHandler(new ObservedChange(adapterView, ex, default)); - - return Handler; - }, - h => adapterView.NothingSelected += h, - h => adapterView.NothingSelected -= h); - - return itemSelected.Merge(nothingSelected); - }); + typeof(AdapterView), + propName, + (x, ex) => + { + var adapterView = (AdapterView)x; + + var itemSelected = + Observable.FromEvent, ObservedChange>( + eventHandler => + { + void Handler(object? unusedSender, AdapterView.ItemSelectedEventArgs unusedEventArgs) => + eventHandler(new ObservedChange(adapterView, ex, default)); + + return Handler; + }, + h => adapterView.ItemSelected += h, + h => adapterView.ItemSelected -= h); + + var nothingSelected = + Observable.FromEvent, ObservedChange>( + eventHandler => + { + void Handler(object? unusedSender, AdapterView.NothingSelectedEventArgs unusedEventArgs) => + eventHandler(new ObservedChange(adapterView, ex, default)); + + return Handler; + }, + h => adapterView.NothingSelected += h, + h => adapterView.NothingSelected -= h); + + return itemSelected.Merge(nothingSelected); + }); } + /// + /// Creates a dispatch item for the hour property that is compatible with the current OS level. + /// + /// + /// Android introduced at API level 23. Earlier OS versions use + /// . + /// + /// A dispatch item for observing the hour value on a . [ObsoletedOSPlatform("android23.0")] [SupportedOSPlatform("android23.0")] private static DispatchItem CreateTimePickerHourFromWidget() { if ((int)Build.VERSION.SdkInt >= 23) { - return CreateFromWidget(static v => v.Hour, static (v, h) => v.TimeChanged += h, static (v, h) => v.TimeChanged -= h); + return CreateFromWidget( + static v => v.Hour, + static (v, h) => v.TimeChanged += h, + static (v, h) => v.TimeChanged -= h); } - return CreateFromWidget(static v => v.CurrentHour, static (v, h) => v.TimeChanged += h, static (v, h) => v.TimeChanged -= h); + return CreateFromWidget( + static v => v.CurrentHour, + static (v, h) => v.TimeChanged += h, + static (v, h) => v.TimeChanged -= h); } + /// + /// Creates a dispatch item for the minute property that is compatible with the current OS level. + /// + /// + /// Android introduced at API level 23. Earlier OS versions use + /// . + /// + /// A dispatch item for observing the minute value on a . [ObsoletedOSPlatform("android23.0")] [SupportedOSPlatform("android23.0")] private static DispatchItem CreateTimePickerMinuteFromWidget() { if ((int)Build.VERSION.SdkInt >= 23) { - return CreateFromWidget(static v => v.Minute, static (v, h) => v.TimeChanged += h, static (v, h) => v.TimeChanged -= h); + return CreateFromWidget( + static v => v.Minute, + static (v, h) => v.TimeChanged += h, + static (v, h) => v.TimeChanged -= h); } - return CreateFromWidget(static v => v.CurrentMinute, static (v, h) => v.TimeChanged += h, static (v, h) => v.TimeChanged -= h); + return CreateFromWidget( + static v => v.CurrentMinute, + static (v, h) => v.TimeChanged += h, + static (v, h) => v.TimeChanged -= h); } - [SuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Marked as Preserve")] - [SuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "Marked as Preserve")] - private static DispatchItem CreateFromWidget(Expression> property, Action> addHandler, Action> removeHandler) + /// + /// Creates a dispatch item for a widget type and property by subscribing to a widget event. + /// + /// + /// + /// The observable produced by the dispatch item emits a change record when the widget event fires. + /// + /// + /// Trimming/AOT: this helper uses expression inspection to derive the property name. It is preserved and suppressed + /// to avoid trimming/AOT analysis noise in supported platform builds. + /// + /// + /// The widget type. + /// The event args type for the widget event. + /// An expression selecting the widget property that is being observed. + /// Registers the event handler on the widget. + /// Unregisters the event handler from the widget. + /// A dispatch item for the widget and property. + /// Thrown when does not expose valid member info. + private static DispatchItem CreateFromWidget( + Expression> property, + Action> addHandler, + Action> removeHandler) where TView : View where TEventArgs : EventArgs { - var memberInfo = property.Body.GetMemberInfo() ?? throw new ArgumentException("Does not have a valid body member info.", nameof(property)); + var memberInfo = + property.Body.GetMemberInfo() ?? + throw new ArgumentException("Does not have a valid body member info.", nameof(property)); - // ExpressionToPropertyNames is used here as it handles boxing expressions that might - // occur due to our use of object + // ExpressionToPropertyNames is used here as it handles boxing expressions that might occur due to our use of object. var propName = memberInfo.Name; return new DispatchItem( - typeof(TView), - propName, - (x, ex) => - { - var view = (TView)x; - - return Observable.FromEvent, ObservedChange>( - eventHandler => - { - void Handler(object? sender, TEventArgs e) => - eventHandler(new ObservedChange(view, ex, default)); - - return Handler; - }, - h => addHandler(view, h), - h => removeHandler(view, h)); - }); + typeof(TView), + propName, + (x, ex) => + { + var view = (TView)x; + + return Observable.FromEvent, ObservedChange>( + eventHandler => + { + void Handler(object? unusedSender, TEventArgs unusedEventArgs) => + eventHandler(new ObservedChange(view, ex, default)); + + return Handler; + }, + h => addHandler(view, h), + h => removeHandler(view, h)); + }); } + /// + /// Represents a single dispatch table entry for a widget type and property. + /// private sealed record DispatchItem { + /// + /// Initializes a new instance of the class. + /// + /// The widget type for which observation is supported. + /// The property name that is observable for the widget type. + /// The observable factory function. public DispatchItem( Type type, string? property, Func>> func) => (Type, Property, Func) = (type, property, func); + /// + /// Gets the widget type for which observation is supported. + /// public Type Type { get; } + /// + /// Gets the property name that is observable for the widget type. + /// public string? Property { get; } + /// + /// Gets the observable factory function for the widget type and property. + /// public Func>> Func { get; } } } diff --git a/src/ReactiveUI/Platforms/android/AutoSuspendHelper.cs b/src/ReactiveUI/Platforms/android/AutoSuspendHelper.cs index 2b275d1dd4..1f4ebf41a2 100644 --- a/src/ReactiveUI/Platforms/android/AutoSuspendHelper.cs +++ b/src/ReactiveUI/Platforms/android/AutoSuspendHelper.cs @@ -14,7 +14,7 @@ namespace ReactiveUI; /// /// /// Register this helper inside your subclass to translate Activity lifecycle callbacks into -/// signals. The helper automatically distinguishes cold starts from restores by +/// signals. The helper automatically distinguishes cold starts from restores by /// inspecting and routes pause/save events to via /// . /// @@ -36,18 +36,14 @@ namespace ReactiveUI; /// { /// base.OnCreate(); /// _autoSuspendHelper = new AutoSuspendHelper(this); -/// RxApp.SuspensionHost.CreateNewAppState = () => new ShellState(); -/// RxApp.SuspensionHost.SetupDefaultSuspendResume(new FileSuspensionDriver(FilesDir!.AbsolutePath)); +/// RxSuspension.SuspensionHost.CreateNewAppState = () => new ShellState(); +/// RxSuspension.SuspensionHost.SetupDefaultSuspendResume(new FileSuspensionDriver(FilesDir!.AbsolutePath)); /// } /// } /// ]]> /// /// /// -#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 class AutoSuspendHelper : IEnableLogger, IDisposable { private readonly Subject _onCreate = new(); @@ -72,11 +68,11 @@ public AutoSuspendHelper(Application hostApplication) // TODO: Create Test _onCreate.Merge(_onSaveInstanceState).Subscribe(static x => LatestBundle = x); - RxApp.SuspensionHost.IsLaunchingNew = _onCreate.Where(static x => x is null).Select(static _ => Unit.Default); - RxApp.SuspensionHost.IsResuming = _onCreate.Where(static x => x is not null).Select(static _ => Unit.Default); - RxApp.SuspensionHost.IsUnpausing = _onRestart; - RxApp.SuspensionHost.ShouldPersistState = _onPause.Select(static _ => Disposable.Empty); - RxApp.SuspensionHost.ShouldInvalidateState = UntimelyDemise; + RxSuspension.SuspensionHost.IsLaunchingNew = _onCreate.Where(static x => x is null).Select(static _ => Unit.Default); + RxSuspension.SuspensionHost.IsResuming = _onCreate.Where(static x => x is not null).Select(static _ => Unit.Default); + RxSuspension.SuspensionHost.IsUnpausing = _onRestart; + RxSuspension.SuspensionHost.ShouldPersistState = _onPause.Select(static _ => Disposable.Empty); + RxSuspension.SuspensionHost.ShouldInvalidateState = UntimelyDemise; } /// diff --git a/src/ReactiveUI/Platforms/android/BundleSuspensionDriver.cs b/src/ReactiveUI/Platforms/android/BundleSuspensionDriver.cs index 7a9cb2db3d..62406bc5ce 100644 --- a/src/ReactiveUI/Platforms/android/BundleSuspensionDriver.cs +++ b/src/ReactiveUI/Platforms/android/BundleSuspensionDriver.cs @@ -5,58 +5,126 @@ using System.IO; using System.Text.Json; +using System.Text.Json.Serialization.Metadata; namespace ReactiveUI; /// -/// Loads and saves state to persistent storage. +/// Loads and saves application state using the platform bundle. /// -public class BundleSuspensionDriver : ISuspensionDriver +/// +/// +/// This driver supports both legacy reflection-based System.Text.Json serialization +/// and trimming/AOT-safe serialization via source-generated . +/// +/// +public sealed class BundleSuspensionDriver : ISuspensionDriver { - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("LoadState uses JsonSerializer.Deserialize which requires dynamic code generation")] - [RequiresUnreferencedCode("LoadState uses JsonSerializer.Deserialize which may require unreferenced code")] -#endif - public IObservable LoadState() // TODO: Create Test + private const string StateKey = "__state"; + + /// + [RequiresUnreferencedCode( + "Implementations commonly use reflection-based serialization. " + + "Prefer LoadState(JsonTypeInfo) for trimming or AOT scenarios.")] + [RequiresDynamicCode( + "Implementations commonly use reflection-based serialization. " + + "Prefer LoadState(JsonTypeInfo) for trimming or AOT scenarios.")] + public IObservable LoadState() { try { - // NB: Sometimes OnCreate gives us a null bundle + // NB: Sometimes OnCreate gives us a null bundle. if (AutoSuspendHelper.LatestBundle is null) { - return Observable.Throw(new Exception("New bundle, start from scratch")); + return Observable.Throw( + new InvalidOperationException("New bundle detected; no persisted state is available.")); } - var buffer = AutoSuspendHelper.LatestBundle.GetByteArray("__state"); - + var buffer = AutoSuspendHelper.LatestBundle.GetByteArray(StateKey); if (buffer is null) { - return Observable.Throw(new InvalidOperationException("The buffer __state could not be found.")); + return Observable.Throw( + new InvalidOperationException("The persisted state buffer could not be found.")); } - var st = new MemoryStream(buffer); + return Observable.FromAsync(async () => + { + await using var stream = new MemoryStream(buffer, writable: false); + return await JsonSerializer.DeserializeAsync(stream).ConfigureAwait(false); + }); + } + catch (Exception ex) + { + return Observable.Throw(ex); + } + } + + /// + [RequiresUnreferencedCode( + "Implementations commonly use reflection-based serialization. " + + "Prefer SaveState(T, JsonTypeInfo) for trimming or AOT scenarios.")] + [RequiresDynamicCode( + "Implementations commonly use reflection-based serialization. " + + "Prefer SaveState(T, JsonTypeInfo) for trimming or AOT scenarios.")] + public IObservable SaveState(T state) + { + try + { + using var stream = new MemoryStream(); + JsonSerializer.Serialize(stream, state); - return Observable.FromAsync(async () => await JsonSerializer.DeserializeAsync(st)); + AutoSuspendHelper.LatestBundle?.PutByteArray(StateKey, stream.ToArray()); + return Observables.Unit; } catch (Exception ex) { - return Observable.Throw(ex); + return Observable.Throw(ex); } } - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("SaveState uses JsonSerializer.Serialize which requires dynamic code generation")] - [RequiresUnreferencedCode("SaveState uses JsonSerializer.Serialize which may require unreferenced code")] -#endif - public IObservable SaveState(object state) // TODO: Create Test + /// + public IObservable LoadState(JsonTypeInfo typeInfo) { + ArgumentNullException.ThrowIfNull(typeInfo); + try { - var st = new MemoryStream(); - JsonSerializer.Serialize(st, state); - AutoSuspendHelper.LatestBundle?.PutByteArray("__state", st.ToArray()); + if (AutoSuspendHelper.LatestBundle is null) + { + return Observable.Throw( + new InvalidOperationException("New bundle detected; no persisted state is available.")); + } + + var buffer = AutoSuspendHelper.LatestBundle.GetByteArray(StateKey); + if (buffer is null) + { + return Observable.Throw( + new InvalidOperationException("The persisted state buffer could not be found.")); + } + + return Observable.FromAsync(async () => + { + await using var stream = new MemoryStream(buffer, writable: false); + return await JsonSerializer.DeserializeAsync(stream, typeInfo).ConfigureAwait(false); + }); + } + catch (Exception ex) + { + return Observable.Throw(ex); + } + } + + /// + public IObservable SaveState(T state, JsonTypeInfo typeInfo) + { + ArgumentNullException.ThrowIfNull(typeInfo); + + try + { + using var stream = new MemoryStream(); + JsonSerializer.Serialize(stream, state, typeInfo); + + AutoSuspendHelper.LatestBundle?.PutByteArray(StateKey, stream.ToArray()); return Observables.Unit; } catch (Exception ex) @@ -65,16 +133,12 @@ public IObservable SaveState(object state) // TODO: Create Test } } - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("InvalidateState uses JsonSerializer which requires dynamic code generation")] - [RequiresUnreferencedCode("InvalidateState uses JsonSerializer which may require unreferenced code")] -#endif - public IObservable InvalidateState() // TODO: Create Test + /// + public IObservable InvalidateState() { try { - AutoSuspendHelper.LatestBundle?.PutByteArray("__state", []); + AutoSuspendHelper.LatestBundle?.PutByteArray(StateKey, []); return Observables.Unit; } catch (Exception ex) diff --git a/src/ReactiveUI/Platforms/android/ControlFetcherMixin.cs b/src/ReactiveUI/Platforms/android/ControlFetcherMixin.cs index 43a6d79f7f..2f90bcb5c8 100644 --- a/src/ReactiveUI/Platforms/android/ControlFetcherMixin.cs +++ b/src/ReactiveUI/Platforms/android/ControlFetcherMixin.cs @@ -3,7 +3,10 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. +#nullable enable + using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Runtime.CompilerServices; @@ -15,247 +18,531 @@ namespace ReactiveUI; /// -/// Control Fetcher Mix-in helps you automatically wire-up Activities and -/// Fragments via property names, similar to Butter Knife, as well as allows -/// you to fetch controls manually. +/// Control fetcher helpers for Android that support wiring up properties to Android resource IDs by name. /// +/// +/// +/// This API is intended for classic Android view wiring patterns (e.g., Activities/Fragments/Views). +/// It performs name-to-resource-id resolution using reflection over the generated Android Resource classes, +/// and caches lookups per assembly and per root view. +/// +/// +/// Trimming/AOT: resource discovery uses reflection over generated resource types and may require preserving +/// those members. See and related remarks. +/// +/// public static partial class ControlFetcherMixin { - private static readonly ConcurrentDictionary> _controlIds = new(); + /// + /// Cache mapping an assembly to a case-insensitive resource-name→id map. + /// + /// + /// This cache is populated on demand. The per-assembly map is immutable after creation to allow lock-free reads. + /// + private static readonly ConcurrentDictionary> ControlIds = new(); - private static readonly ConditionalWeakTable> _viewCache = []; + /// + /// Cache mapping a root view object to a per-property cached instance. + /// + /// + /// + /// This cache avoids repeated FindViewById calls for the same root and property name. + /// + /// + /// Threading: Android UI access is typically single-threaded; however, this cache uses a concurrent dictionary + /// to avoid race conditions if called from multiple threads (e.g., during tests or unusual scheduling). + /// + /// + private static readonly ConditionalWeakTable> ViewCache = new(); /// - /// Gets the control from an activity. + /// Cache of wire-up property lists per runtime type and resolve strategy. /// - /// The activity. - /// The property name. - /// The return view. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("GetControl uses reflection to find and set properties")] - [RequiresUnreferencedCode("GetControl may reference properties that could be trimmed")] -#endif - public static View? GetControl(this Activity activity, [CallerMemberName] string? propertyName = null) // TODO: Create Test - => GetCachedControl(propertyName, activity, () => activity.FindViewById(GetControlIdByName(activity.GetType().Assembly, propertyName))); + /// + /// This avoids repeated reflection over properties when wiring up controls multiple times. + /// + private static readonly ConcurrentDictionary<(Type Type, ResolveStrategy Strategy), PropertyInfo[]> WireUpMembersCache = new(); /// - /// Gets the control from a view. + /// Gets a control from an using the calling member name as the default resource name. /// - /// The view. - /// The assembly containing the user-defined view. - /// The property. - /// The return view. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("GetControl uses reflection to find and set properties")] - [RequiresUnreferencedCode("GetControl may reference properties that could be trimmed")] -#endif - public static View? GetControl(this View view, Assembly assembly, [CallerMemberName] string? propertyName = null) // TODO: Create Test - => GetCachedControl(propertyName, view, () => view.FindViewById(GetControlIdByName(assembly, propertyName))); + /// The activity that hosts the view hierarchy. + /// + /// The property name to use as the resource identifier. Defaults to the calling member name. + /// + /// The resolved view if found; otherwise . + [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 View? GetControl(this Activity activity, [CallerMemberName] string? propertyName = null) => + GetCachedControl( + propertyName, + activity, + static (root, name) => + { + var act = (Activity)root; + var id = GetControlIdByName(act.GetType().Assembly, name); + return act.FindViewById(id); + }); /// - /// Wires a control to a property. + /// Gets a control from an Android using the calling member name as the default resource name. /// - /// The layout view host. - /// The resolve members. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WireUpControls uses reflection to find and set properties")] - [RequiresUnreferencedCode("WireUpControls may reference properties that could be trimmed")] -#endif - public static void WireUpControls(this ILayoutViewHost layoutHost, ResolveStrategy resolveMembers = ResolveStrategy.Implicit) // TODO: Create Test + /// The root view. + /// The assembly containing the user-defined view and its resources. + /// + /// The property name to use as the resource identifier. Defaults to the calling member name. + /// + /// The resolved view if found; otherwise . + [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 View? GetControl(this View view, Assembly assembly, [CallerMemberName] string? propertyName = null) => + GetCachedControl( + propertyName, + view, + static (root, name, state) => + { + var v = (View)root; + var id = GetControlIdByName(state, name); + return v.FindViewById(id); + }, + assembly); + + /// + /// Wires view controls to properties on an . + /// + /// The layout host that exposes a . + /// The resolve strategy for selecting properties to wire. + /// Thrown when is null. + /// + /// Thrown when a property cannot be wired to a view with a corresponding resource identifier. + /// + [RequiresUnreferencedCode("WireUpControls uses reflection to discover properties and attributes that may be trimmed.")] + [RequiresDynamicCode("WireUpControls uses reflection that may require dynamic code generation.")] + public static void WireUpControls(this ILayoutViewHost layoutHost, ResolveStrategy resolveMembers = ResolveStrategy.Implicit) { ArgumentExceptionHelper.ThrowIfNull(layoutHost); - var members = layoutHost.GetWireUpMembers(resolveMembers).ToList(); - foreach (var member in members) + var hostType = layoutHost.GetType(); + var members = GetWireUpMembersCached(hostType, resolveMembers); + + for (var i = 0; i < members.Length; i++) { + var member = members[i]; + try { - var view = layoutHost.View?.GetControl(layoutHost.GetType().Assembly, member.GetResourceName()); - member.SetValue(layoutHost, view); + var root = layoutHost.View; + var resourceName = member.GetResourceName(); + + var resolved = root is null + ? null + : root.GetControl(hostType.Assembly, resourceName); + + member.SetValue(layoutHost, resolved); } catch (Exception ex) { - throw new - MissingFieldException("Failed to wire up the Property " + member.Name + " to a View in your layout with a corresponding identifier", ex); + throw new MissingFieldException( + "Failed to wire up the Property " + member.Name + + " to a View in your layout with a corresponding identifier.", + ex); } } } /// - /// Wires a control to a property. + /// Wires view controls to properties on an Android . /// - /// The view. - /// The resolve members. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WireUpControls uses reflection to find and set properties")] - [RequiresUnreferencedCode("WireUpControls may reference properties that could be trimmed")] -#endif - public static void WireUpControls(this View view, ResolveStrategy resolveMembers = ResolveStrategy.Implicit) // TODO: Create Test + /// The view whose properties should be wired. + /// The resolve strategy for selecting properties to wire. + /// Thrown when is null. + /// + /// Thrown when a property cannot be wired to a view with a corresponding resource identifier. + /// + [RequiresUnreferencedCode("WireUpControls uses reflection to discover properties and attributes that may be trimmed.")] + [RequiresDynamicCode("WireUpControls uses reflection that may require dynamic code generation.")] + public static void WireUpControls(this View view, ResolveStrategy resolveMembers = ResolveStrategy.Implicit) { ArgumentExceptionHelper.ThrowIfNull(view); - var members = view.GetWireUpMembers(resolveMembers); + var viewType = view.GetType(); + var members = GetWireUpMembersCached(viewType, resolveMembers); + var asm = viewType.Assembly; - foreach (var member in members) + for (var i = 0; i < members.Length; i++) { + var member = members[i]; + try { - // Find the android control with the same name - var currentView = view.GetControl(view.GetType().Assembly, member.GetResourceName()); - - // Set the activity field's value to the view with that identifier + var resourceName = member.GetResourceName(); + var currentView = view.GetControl(asm, resourceName); member.SetValue(view, currentView); } catch (Exception ex) { - throw new - MissingFieldException("Failed to wire up the Property " + member.Name + " to a View in your layout with a corresponding identifier", ex); + throw new MissingFieldException( + "Failed to wire up the Property " + member.Name + + " to a View in your layout with a corresponding identifier.", + ex); } } } /// - /// Wires a control to a property. - /// This should be called in the Fragment's OnCreateView, with the newly inflated layout. + /// Wires view controls to properties on an Android . /// - /// The fragment. - /// The inflated view. - /// The resolve members. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WireUpControls uses reflection to find and set properties")] - [RequiresUnreferencedCode("WireUpControls may reference properties that could be trimmed")] -#endif - public static void WireUpControls(this Fragment fragment, View inflatedView, ResolveStrategy resolveMembers = ResolveStrategy.Implicit) // TODO: Create Test + /// The fragment whose properties should be wired. + /// The inflated view returned from OnCreateView. + /// The resolve strategy for selecting properties to wire. + /// + /// Thrown when or is null. + /// + /// + /// Thrown when a property cannot be wired to a view with a corresponding resource identifier. + /// + [RequiresUnreferencedCode("WireUpControls uses reflection to discover properties and attributes that may be trimmed.")] + [RequiresDynamicCode("WireUpControls uses reflection that may require dynamic code generation.")] + public static void WireUpControls(this Fragment fragment, View inflatedView, ResolveStrategy resolveMembers = ResolveStrategy.Implicit) { ArgumentExceptionHelper.ThrowIfNull(fragment); + ArgumentExceptionHelper.ThrowIfNull(inflatedView); - var members = fragment.GetWireUpMembers(resolveMembers); + var fragmentType = fragment.GetType(); + var members = GetWireUpMembersCached(fragmentType, resolveMembers); + var asm = fragmentType.Assembly; - foreach (var member in members) + for (var i = 0; i < members.Length; i++) { + var member = members[i]; + try { - // Find the android control with the same name from the view - var view = inflatedView.GetControl(fragment.GetType().Assembly, member.GetResourceName()); - - // Set the activity field's value to the view with that identifier - member.SetValue(fragment, view); + var resourceName = member.GetResourceName(); + var resolved = inflatedView.GetControl(asm, resourceName); + member.SetValue(fragment, resolved); } catch (Exception ex) { - throw new - MissingFieldException("Failed to wire up the Property " + member.Name + " to a View in your layout with a corresponding identifier", ex); + throw new MissingFieldException( + "Failed to wire up the Property " + member.Name + + " to a View in your layout with a corresponding identifier.", + ex); } } } /// - /// Wires a control to a property. + /// Wires view controls to properties on an . /// - /// The Activity. - /// The resolve members. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WireUpControls uses reflection to find and set properties")] - [RequiresUnreferencedCode("WireUpControls may reference properties that could be trimmed")] -#endif - public static void WireUpControls(this Activity activity, ResolveStrategy resolveMembers = ResolveStrategy.Implicit) // TODO: Create Test + /// The activity whose properties should be wired. + /// The resolve strategy for selecting properties to wire. + /// Thrown when is null. + /// + /// Thrown when a property cannot be wired to a view with a corresponding resource identifier. + /// + [RequiresUnreferencedCode("WireUpControls uses reflection to discover properties and attributes that may be trimmed.")] + [RequiresDynamicCode("WireUpControls uses reflection that may require dynamic code generation.")] + public static void WireUpControls(this Activity activity, ResolveStrategy resolveMembers = ResolveStrategy.Implicit) { ArgumentExceptionHelper.ThrowIfNull(activity); - var members = activity.GetWireUpMembers(resolveMembers); + var activityType = activity.GetType(); + var members = GetWireUpMembersCached(activityType, resolveMembers); - foreach (var member in members) + for (var i = 0; i < members.Length; i++) { + var member = members[i]; + try { - // Find the android control with the same name - var view = activity.GetControl(member.GetResourceName()); - - // Set the activity field's value to the view with that identifier - member.SetValue(activity, view); + var resourceName = member.GetResourceName(); + var resolved = activity.GetControl(resourceName); + member.SetValue(activity, resolved); } catch (Exception ex) { - throw new - MissingFieldException("Failed to wire up the Property " + member.Name + " to a View in your layout with a corresponding identifier", ex); + throw new MissingFieldException( + "Failed to wire up the Property " + member.Name + + " to a View in your layout with a corresponding identifier.", + ex); } } } -#if NET6_0_OR_GREATER - [RequiresDynamicCode("GetWireUpMembers uses reflection to analyze properties")] - [RequiresUnreferencedCode("GetWireUpMembers may reference properties that could be trimmed")] -#endif - internal static IEnumerable GetWireUpMembers(this object @this, ResolveStrategy resolveStrategy) + /// + /// Retrieves the set of properties on the specified object that are eligible for wire-up based on the provided + /// resolution strategy. + /// + /// This method uses reflection to discover properties, which may require dynamically generated + /// code and may be affected by code trimming. Use caution when calling this method in environments where reflection + /// or dynamic code generation is restricted. + /// The object whose properties are to be discovered for wire-up. + /// The strategy that determines which properties are considered for wire-up. + /// An enumerable collection of objects representing the properties eligible for wire-up. + /// The collection is empty if no matching properties are found. + [RequiresUnreferencedCode("Property discovery uses reflection and may require members removed by trimming.")] + [RequiresDynamicCode("Property discovery uses reflection that may require dynamic code generation.")] + internal static PropertyInfo[] GetWireUpMembers(this object @this, ResolveStrategy resolveStrategy) { - var members = @this.GetType().GetRuntimeProperties(); + var type = @this.GetType(); - return resolveStrategy switch - { - ResolveStrategy.ExplicitOptIn => - members.Where(static member => member.GetCustomAttribute(true) is not null), - ResolveStrategy.ExplicitOptOut => - members.Where(static member => typeof(View).IsAssignableFrom(member.PropertyType) && member.GetCustomAttribute(true) is null), - - // Implicit matches the Default. - _ => members.Where(static member => member.PropertyType.IsSubclassOf(typeof(View)) || member.GetCustomAttribute(true) is not null), - }; + return GetWireUpMembers(type, resolveStrategy); } + /// + /// Returns the set of properties eligible for wiring for a given runtime type and strategy. + /// + /// The runtime type. + /// The property selection strategy. + /// An array of properties eligible for wiring. + [RequiresUnreferencedCode("Property discovery uses reflection and may require members removed by trimming.")] + [RequiresDynamicCode("Property discovery uses reflection that may require dynamic code generation.")] + internal static PropertyInfo[] GetWireUpMembersCached(Type type, ResolveStrategy resolveStrategy) => + WireUpMembersCache.GetOrAdd((type, resolveStrategy), static key => + { + var members = key.Type.GetRuntimeProperties(); + + // Materialize once into a list then to array; no LINQ in per-wire loops. + var list = new List(); + + foreach (var member in members) + { + if (!member.CanWrite) + { + continue; + } + + switch (key.Strategy) + { + case ResolveStrategy.ExplicitOptIn: + if (member.GetCustomAttribute(inherit: true) is not null) + { + list.Add(member); + } + + break; + + case ResolveStrategy.ExplicitOptOut: + if (typeof(View).IsAssignableFrom(member.PropertyType) && + member.GetCustomAttribute(inherit: true) is null) + { + list.Add(member); + } + + break; + + default: + // Implicit: either a View-typed property or explicitly marked with WireUpResource. + if (member.PropertyType.IsSubclassOf(typeof(View)) || + member.GetCustomAttribute(inherit: true) is not null) + { + list.Add(member); + } + + break; + } + } + + return list.ToArray(); + }); + + /// + /// Gets the resource name for the specified property based on optional overrides. + /// + /// The property being wired. + /// The resource name to use. + [RequiresUnreferencedCode("Attribute lookup uses reflection and may require members removed by trimming.")] + [RequiresDynamicCode("Attribute lookup uses reflection that may require dynamic code generation.")] internal static string GetResourceName(this PropertyInfo member) { - var resourceNameOverride = member.GetCustomAttribute()?.ResourceNameOverride; - return resourceNameOverride ?? member.Name; + var attr = member.GetCustomAttribute(); + return attr?.ResourceNameOverride ?? member.Name; } - private static View? GetCachedControl(string? propertyName, object rootView, Func fetchControlFromView) + /// + /// Gets a cached control for a root view and property name, fetching it if absent. + /// + /// The cache key, typically the property name. + /// The root view object used as cache scope. + /// Factory used to fetch the view when not cached. + /// The cached view (possibly null). + private static View? GetCachedControl( + string? propertyName, + object rootView, + Func fetchControlFromView) { ArgumentExceptionHelper.ThrowIfNull(propertyName); - ArgumentExceptionHelper.ThrowIfNull(fetchControlFromView); - var ourViewCache = _viewCache.GetOrCreateValue(rootView); + var cache = ViewCache.GetOrCreateValue(rootView); - if (ourViewCache.TryGetValue(propertyName, out var ret)) + if (cache.TryGetValue(propertyName, out var existing)) { - return ret; + return existing; } - ret = fetchControlFromView(); + var created = fetchControlFromView(rootView, propertyName); - ourViewCache.Add(propertyName, ret); - return ret; + // ConcurrentDictionary indexer is safe; last write wins in a race. + cache[propertyName] = created; + return created; } -#if NET6_0_OR_GREATER - [RequiresDynamicCode("GetControlIdByName uses assembly reflection and type discovery")] - [RequiresUnreferencedCode("GetControlIdByName may reference types that could be trimmed")] -#endif - private static int GetControlIdByName(Assembly assembly, string? name) + /// + /// Gets a cached control for a root view and property name, fetching it if absent, with an extra state value. + /// + /// The type of state passed to the fetch function. + /// The cache key, typically the property name. + /// The root view object used as cache scope. + /// Factory used to fetch the view when not cached. + /// State passed to the fetch factory. + /// The cached view (possibly null). + private static View? GetCachedControl( + string? propertyName, + object rootView, + Func fetchControlFromView, + TState state) + { + ArgumentExceptionHelper.ThrowIfNull(propertyName); + ArgumentExceptionHelper.ThrowIfNull(fetchControlFromView); + + var cache = ViewCache.GetOrCreateValue(rootView); + + if (cache.TryGetValue(propertyName, out var existing)) + { + return existing; + } + + var created = fetchControlFromView(rootView, propertyName, state); + cache[propertyName] = created; + return created; + } + + /// + /// Resolves the Android resource ID for the given resource name within the specified assembly. + /// + /// The assembly whose generated resource types should be inspected. + /// The resource name. + /// The resolved integer resource ID. + /// Thrown when is null or empty. + /// Thrown when the name cannot be resolved to an ID. + [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.")] + private static int GetControlIdByName(Assembly assembly, string name) { - ArgumentExceptionHelper.ThrowIfNull(name); + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentException("Resource name must not be null or whitespace.", nameof(name)); + } + + var ids = ControlIds.GetOrAdd(assembly, static asm => BuildIdMap(asm)); + + if (ids.TryGetValue(name, out var id)) + { + return id; + } + + throw new MissingFieldException($"No Android resource id named '{name}' was found for assembly '{assembly.FullName}'."); + } - var ids = _controlIds.GetOrAdd( - assembly, - static currentAssembly => - { + /// + /// Builds an immutable mapping of resource name to integer ID for an assembly. + /// + /// The assembly to inspect. + /// A case-insensitive mapping of resource name to ID. + [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.")] + private static IReadOnlyDictionary BuildIdMap(Assembly assembly) + { #if NET8_0_OR_GREATER - var resources = Assembly.Load(currentAssembly - .GetReferencedAssemblies() - .First(static an => an.FullName.StartsWith("_Microsoft.Android.Resource.Designer")).ToString()) - .GetModules() - .SelectMany(static x => x.GetTypes()) - .First(static x => x.Name == "ResourceConstant"); + // Android .NET 8+ generates a resource designer in a referenced assembly. + var referenced = assembly.GetReferencedAssemblies(); + AssemblyName? designerName = null; + + for (var i = 0; i < referenced.Length; i++) + { + var an = referenced[i]; + if (an.FullName is not null && an.FullName.StartsWith("_Microsoft.Android.Resource.Designer", StringComparison.Ordinal)) + { + designerName = an; + break; + } + } + + if (designerName is null) + { + throw new InvalidOperationException("Could not locate the Android resource designer assembly."); + } + + var resourcesAssembly = Assembly.Load(designerName); + var modules = resourcesAssembly.GetModules(); + + Type? resources = null; + for (var i = 0; i < modules.Length && resources is null; i++) + { + var types = modules[i].GetTypes(); + for (var j = 0; j < types.Length; j++) + { + if (types[j].Name == "ResourceConstant") + { + resources = types[j]; + break; + } + } + } + + if (resources is null) + { + throw new InvalidOperationException("Could not locate generated resource type 'ResourceConstant'."); + } #else - var resources = currentAssembly.GetModules().SelectMany(x => x.GetTypes()).First(x => x.Name == "Resource"); + var modules = assembly.GetModules(); + Type? resources = null; + + for (var i = 0; i < modules.Length && resources is null; i++) + { + var types = modules[i].GetTypes(); + for (var j = 0; j < types.Length; j++) + { + if (types[j].Name == "Resource") + { + resources = types[j]; + break; + } + } + } + + if (resources is null) + { + throw new InvalidOperationException("Could not locate generated resource type 'Resource'."); + } #endif - var idType = resources.GetNestedType("Id") ?? throw new InvalidOperationException("Id is not a valid nested type in the resources."); - return idType.GetFields() - .Where(static x => x.FieldType == typeof(int)) - .ToDictionary(static k => k.Name, static v => ((int?)v.GetRawConstantValue()) ?? 0, StringComparer.InvariantCultureIgnoreCase); - }); + var idType = resources.GetNestedType("Id"); + if (idType is null) + { + throw new InvalidOperationException("Id is not a valid nested type in the generated resources."); + } + + var fields = idType.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static); + var dict = new Dictionary(fields.Length, StringComparer.InvariantCultureIgnoreCase); + + for (var i = 0; i < fields.Length; i++) + { + var f = fields[i]; + if (f.FieldType != typeof(int)) + { + continue; + } + + // Generated constants use raw constant values. + if (f.GetRawConstantValue() is int value) + { + dict[f.Name] = value; + } + } - return ids[name]; + return dict; } } diff --git a/src/ReactiveUI/Platforms/android/FlexibleCommandBinder.cs b/src/ReactiveUI/Platforms/android/FlexibleCommandBinder.cs index 95ab1dd1e9..5fb76292ca 100644 --- a/src/ReactiveUI/Platforms/android/FlexibleCommandBinder.cs +++ b/src/ReactiveUI/Platforms/android/FlexibleCommandBinder.cs @@ -6,8 +6,6 @@ using System.Reflection; using System.Windows.Input; -using ReactiveUI.Helpers; - namespace ReactiveUI; /// @@ -21,39 +19,7 @@ public abstract class FlexibleCommandBinder : ICreatesCommandBinding private readonly Dictionary _config = []; /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("GetAffinityForObject uses Reflection.GetValueSetterForProperty which requires dynamic code generation")] - [RequiresUnreferencedCode("GetAffinityForObject uses Reflection.GetValueSetterForProperty which may require unreferenced code")] -#endif - public int GetAffinityForObject(Type type, bool hasEventTarget) - { - if (hasEventTarget) - { - return 0; - } - - var match = _config.Keys - .Where(x => x.IsAssignableFrom(type)) - .OrderByDescending(x => _config[x].Affinity) - .FirstOrDefault(); - - if (match is null) - { - return 0; - } - - var typeProperties = _config[match]; - return typeProperties.Affinity; - } - - /// -#if NET6_0_OR_GREATER - public int GetAffinityForObject<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicEvents | DynamicallyAccessedMemberTypes.PublicProperties)] T>( - bool hasEventTarget) -#else - public int GetAffinityForObject( - bool hasEventTarget) -#endif + public int GetAffinityForObject<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicEvents | DynamicallyAccessedMemberTypes.PublicProperties)] T>(bool hasEventTarget) { if (hasEventTarget) { @@ -75,11 +41,9 @@ public int GetAffinityForObject( } /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("BindCommandToObject uses Reflection.GetValueSetterForProperty which requires dynamic code generation")] - [RequiresUnreferencedCode("BindCommandToObject uses Reflection.GetValueSetterForProperty which may require unreferenced code")] -#endif - public IDisposable BindCommandToObject(ICommand? command, object? target, IObservable commandParameter) + [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 commandParameter) + where T : class { ArgumentExceptionHelper.ThrowIfNull(target); @@ -95,13 +59,100 @@ public IDisposable BindCommandToObject(ICommand? command, object? target, IObser } /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("BindCommandToObject uses Reflection.GetValueSetterForProperty which requires dynamic code generation")] - [RequiresUnreferencedCode("BindCommandToObject uses Reflection.GetValueSetterForProperty which may require unreferenced code")] -#endif - public IDisposable BindCommandToObject(ICommand? command, object? target, IObservable commandParameter, string eventName) + [RequiresUnreferencedCode("String/reflection-based event binding may require members removed by trimming.")] + public IDisposable? BindCommandToObject(ICommand? command, T? target, IObservable commandParameter, string eventName) + where T : class + => Disposable.Empty; + + /// + public IDisposable? BindCommandToObject<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicEvents | DynamicallyAccessedMemberTypes.NonPublicEvents)] T, TEventArgs>( + ICommand? command, + T? target, + IObservable commandParameter, + Action> addHandler, + Action> removeHandler) + where T : class where TEventArgs : EventArgs - => throw new NotImplementedException(); + { + ArgumentExceptionHelper.ThrowIfNull(target); + ArgumentNullException.ThrowIfNull(addHandler); + ArgumentNullException.ThrowIfNull(removeHandler); + + // Match existing binder behavior: if there is no command, create a no-op binding. + if (command is null) + { + return Disposable.Empty; + } + + // Keep the existing "null means use target" idiom used by ForEvent. + commandParameter ??= Observable.Return((object?)target); + + // The latest parameter may be updated from a different thread than the event thread. + object? latestParam = null; + + // Stable handler for deterministic unsubscription. + void Handler(object? sender, TEventArgs e) + { + var param = Volatile.Read(ref latestParam); + if (command.CanExecute(param)) + { + command.Execute(param); + } + } + + // Subscribe to parameter updates first, then attach the event handler. + var parameterSub = commandParameter.Subscribe(x => Volatile.Write(ref latestParam, x)); + addHandler(Handler); + + // If we can locate a conventional enabled property on the runtime target, keep it in sync with CanExecute. + // This is intentionally best-effort and does not throw if the property is absent or cannot be set. + Action? enabledSetter = null; + try + { + // Common Android idiom: "Enabled" boolean property. + // Use runtime type so derived types are supported. + var enabledProp = typeof(T).GetRuntimeProperty("Enabled"); + if (enabledProp is not null) + { + enabledSetter = Reflection.GetValueSetterForProperty(enabledProp); + } + } + catch + { + // Best-effort only; ignore reflection failures. + enabledSetter = null; + } + + IDisposable? canExecuteSub = null; + if (enabledSetter is not null) + { + // Initial enabled state (default parameter is null until the first commandParameter emission). + enabledSetter(target, command.CanExecute(Volatile.Read(ref latestParam)), null); + + // Keep Enabled in sync with CanExecuteChanged. + canExecuteSub = Observable.FromEvent( + eventHandler => + { + void CanExecuteHandler(object? s, EventArgs e) => + eventHandler(command.CanExecute(Volatile.Read(ref latestParam))); + return CanExecuteHandler; + }, + h => command.CanExecuteChanged += h, + h => command.CanExecuteChanged -= h) + .Subscribe(x => enabledSetter(target, x, null)); + } + + // Dispose ordering: detach event handler and CanExecute subscription after stopping parameter updates. + // The handler instance is stable, so Remove is correct. + return canExecuteSub is null + ? new CompositeDisposable( + parameterSub, + Disposable.Create(() => removeHandler(Handler))) + : new CompositeDisposable( + parameterSub, + canExecuteSub, + Disposable.Create(() => removeHandler(Handler))); + } /// /// Creates a commands binding from event and a property. @@ -112,9 +163,7 @@ public IDisposable BindCommandToObject(ICommand? command, object? ta /// Command parameter. /// Event name. /// Enabled property name. -#if NET6_0_OR_GREATER - [RequiresUnreferencedCode("This member uses reflection to discover event members and associated delegate types.")] -#endif + [RequiresUnreferencedCode("String/reflection-based event binding may require members removed by trimming.")] protected static IDisposable ForEvent(ICommand? command, object? target, IObservable commandParameter, string eventName, PropertyInfo enabledProperty) { ArgumentExceptionHelper.ThrowIfNull(command); @@ -155,6 +204,168 @@ protected static IDisposable ForEvent(ICommand? command, object? target, IObserv .Subscribe(x => enabledSetter(target, x, null))); } + /// + /// Creates a command binding from an event using explicit add/remove handler delegates and optionally + /// synchronizes an enabled property with . + /// + /// The event arguments type. + /// The command to bind. + /// The event source object. + /// Observable producing command parameter values. If , is used. + /// Adds the handler to the event source. + /// Removes the handler from the event source. + /// + /// Optional setter for an enabled-like property. If , enabled synchronization is skipped. + /// The setter is expected to accept as the first argument and a value as the second. + /// + /// A disposable that unbinds the command and stops enabled synchronization. + /// + /// Thrown when is . + /// + protected static IDisposable ForEvent( + ICommand? command, + object? target, + IObservable? commandParameter, + Action> addHandler, + Action> removeHandler, + Action? enabledSetter) + where TEventArgs : EventArgs + { + ArgumentExceptionHelper.ThrowIfNull(command); + ArgumentExceptionHelper.ThrowIfNull(target); + ArgumentNullException.ThrowIfNull(addHandler); + ArgumentNullException.ThrowIfNull(removeHandler); + + // Preserve existing idiom: null commandParameter means use target. + commandParameter ??= Observable.Return(target); + + object? latestParam = null; + + // Stable handler for deterministic unsubscription. + void Handler(object? sender, TEventArgs e) + { + var param = Volatile.Read(ref latestParam); + if (command.CanExecute(param)) + { + command.Execute(param); + } + } + + // Subscribe to parameter updates first so the first event sees the latest parameter. + var parameterSub = commandParameter.Subscribe(x => Volatile.Write(ref latestParam, x)); + + // Hook the event without reflection. + addHandler(Handler); + + // If there is no enabled setter, we're done. + if (enabledSetter is null) + { + return new CompositeDisposable( + parameterSub, + Disposable.Create(() => removeHandler(Handler))); + } + + // Initial enabled state. + enabledSetter(target, command.CanExecute(Volatile.Read(ref latestParam)), null); + + // Keep enabled state in sync with CanExecuteChanged. + var canExecuteSub = Observable.FromEvent( + eventHandler => + { + void CanExecuteHandler(object? s, EventArgs e) => + eventHandler(command.CanExecute(Volatile.Read(ref latestParam))); + return CanExecuteHandler; + }, + h => command.CanExecuteChanged += h, + h => command.CanExecuteChanged -= h) + .Subscribe(x => enabledSetter(target, x, null)); + + return new CompositeDisposable( + parameterSub, + canExecuteSub, + Disposable.Create(() => removeHandler(Handler))); + } + + /// + /// Creates a command binding from an event using explicit add/remove handler delegates and optionally + /// synchronizes an enabled property with . + /// + /// The command to bind. + /// The event source object. + /// Observable producing command parameter values. If , is used. + /// Adds the handler to the event source. + /// Removes the handler from the event source. + /// + /// Optional setter for an enabled-like property. If , enabled synchronization is skipped. + /// The setter is expected to accept as the first argument and a value as the second. + /// + /// A disposable that unbinds the command and stops enabled synchronization. + /// + /// Thrown when is . + /// + protected static IDisposable ForEvent( + ICommand? command, + object? target, + IObservable? commandParameter, + Action addHandler, + Action removeHandler, + Action? enabledSetter) + { + ArgumentExceptionHelper.ThrowIfNull(command); + ArgumentExceptionHelper.ThrowIfNull(target); + ArgumentNullException.ThrowIfNull(addHandler); + ArgumentNullException.ThrowIfNull(removeHandler); + + // Preserve existing idiom: null commandParameter means use target. + commandParameter ??= Observable.Return(target); + + object? latestParam = null; + + // Stable handler for deterministic unsubscription. + void Handler(object? sender, EventArgs e) + { + var param = Volatile.Read(ref latestParam); + if (command.CanExecute(param)) + { + command.Execute(param); + } + } + + // Subscribe to parameter updates first so the first event sees the latest parameter. + var parameterSub = commandParameter.Subscribe(x => Volatile.Write(ref latestParam, x)); + + // Hook the event without reflection. + addHandler(Handler); + + // If there is no enabled setter, we're done. + if (enabledSetter is null) + { + return new CompositeDisposable( + parameterSub, + Disposable.Create(() => removeHandler(Handler))); + } + + // Initial enabled state. + enabledSetter(target, command.CanExecute(Volatile.Read(ref latestParam)), null); + + // Keep enabled state in sync with CanExecuteChanged. + var canExecuteSub = Observable.FromEvent( + eventHandler => + { + void CanExecuteHandler(object? s, EventArgs e) => + eventHandler(command.CanExecute(Volatile.Read(ref latestParam))); + return CanExecuteHandler; + }, + h => command.CanExecuteChanged += h, + h => command.CanExecuteChanged -= h) + .Subscribe(x => enabledSetter(target, x, null)); + + return new CompositeDisposable( + parameterSub, + canExecuteSub, + Disposable.Create(() => removeHandler(Handler))); + } + /// /// Registers an observable factory for the specified type and property. /// diff --git a/src/ReactiveUI/Platforms/android/LayoutViewHost.cs b/src/ReactiveUI/Platforms/android/LayoutViewHost.cs index 34743210cf..e69151f1a6 100644 --- a/src/ReactiveUI/Platforms/android/LayoutViewHost.cs +++ b/src/ReactiveUI/Platforms/android/LayoutViewHost.cs @@ -6,11 +6,20 @@ using Android.Content; using Android.Views; +using static ReactiveUI.ControlFetcherMixin; + namespace ReactiveUI; /// -/// A class that implements the Android ViewHolder pattern. Use it along -/// with GetViewHost. +/// Base class implementing the Android ViewHolder pattern. +/// +/// owns a single inflated instance and +/// optionally wires child controls to properties on the host. +/// +/// +/// This type provides both AOT-safe construction paths and a legacy reflection-based +/// auto-wireup path for compatibility. +/// /// public abstract class LayoutViewHost : ILayoutViewHost, IEnableLogger { @@ -19,59 +28,180 @@ public abstract class LayoutViewHost : ILayoutViewHost, IEnableLogger /// /// Initializes a new instance of the class. /// + /// + /// This constructor performs no inflation or wiring and exists to support + /// derived types that manage view creation manually. + /// protected LayoutViewHost() { } + /// + /// Initializes a new instance of the class by inflating + /// a layout resource. + /// + /// The Android context. + /// The layout resource identifier. + /// The parent view group. + /// Whether to attach the inflated view to the parent. + /// + /// + /// This constructor is fully AOT- and trimming-safe. + /// + /// + /// No automatic control wiring is performed. Consumers are expected to + /// wire controls explicitly. + /// + /// + protected LayoutViewHost( + Context context, + int layoutId, + ViewGroup parent, + bool attachToRoot) + { + ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(parent); + + View = Inflate(context, layoutId, parent, attachToRoot); + } + /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class by inflating + /// a layout resource and invoking an explicit, AOT-safe binder. /// - /// The CTX. - /// The layout identifier. - /// The parent. - /// if set to true [attach to root]. - /// if set to true [perform automatic wire-up]. -#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 - protected LayoutViewHost(Context ctx, int layoutId, ViewGroup parent, bool attachToRoot = false, bool performAutoWireup = true) + /// The Android context. + /// The layout resource identifier. + /// The parent view group. + /// Whether to attach the inflated view to the parent. + /// + /// A callback responsible for explicitly wiring child views to the host. + /// + /// + /// + /// This constructor is fully AOT-safe and avoids reflection entirely. + /// + /// + /// The callback is invoked only after has been assigned. + /// + /// + protected LayoutViewHost( + Context context, + int layoutId, + ViewGroup parent, + bool attachToRoot, + Action bind) + : this(context, layoutId, parent, attachToRoot) { - var inflater = LayoutInflater.FromContext(ctx); + ArgumentNullException.ThrowIfNull(bind); - if (inflater is null) + if (View is not null) { - return; + bind(this, View); } + } - View = inflater.Inflate(layoutId, parent, attachToRoot); + /// + /// Initializes a new instance of the class by inflating + /// a layout resource and optionally performing reflection-based auto-wireup. + /// + /// The Android context. + /// The layout resource identifier. + /// The parent view group. + /// Whether to attach the inflated view to the parent. + /// + /// If , performs automatic wiring using reflection. + /// + /// + /// The member resolution strategy used during auto-wireup. + /// + /// + /// + /// This constructor is not trimming- or AOT-safe when auto-wireup is enabled. + /// + /// + /// It exists for backward compatibility and should be avoided in new code. + /// + /// + [RequiresUnreferencedCode("Auto wire-up uses reflection and member discovery.")] + [RequiresDynamicCode("Auto wire-up relies on runtime type inspection.")] + protected LayoutViewHost( + Context context, + int layoutId, + ViewGroup parent, + bool attachToRoot, + bool performAutoWireup, + ResolveStrategy resolveStrategy) + { + ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(parent); + + View = Inflate(context, layoutId, parent, attachToRoot); if (performAutoWireup) { - this.WireUpControls(); + this.WireUpControls(resolveStrategy); } } - /// + /// public View? View { get => _view; - set { - if (_view == value) + if (ReferenceEquals(_view, value)) { return; } _view = value; + + // Associate the host with the view for retrieval via ViewMixins. _view?.SetTag(ViewMixins.ViewHostTag, this.ToJavaObject()); } } /// - /// Casts the LayoutViewHost to a View. + /// Implicitly converts a to its backing . + /// + /// The host instance. + public static implicit operator View?(LayoutViewHost host) + { + ArgumentExceptionHelper.ThrowIfNull(host); + return host._view; + } + + /// + /// Inflates an Android layout resource into a using the provided context. /// - /// The LayoutViewHost to cast. - public static implicit operator View?(LayoutViewHost layoutViewHost) => layoutViewHost?.View; + /// The Android context used to obtain a . + /// The layout resource identifier to inflate. + /// + /// The parent view group to associate with the inflated view during inflation. + /// This parameter may influence layout parameters even when is . + /// + /// + /// Whether the inflated view should be attached to during inflation. + /// + /// + /// The inflated instance. + /// + /// + /// Thrown when a cannot be obtained from the provided . + /// + /// + /// + /// This method centralizes layout inflation to avoid duplication across constructors and + /// to ensure consistent error handling. + /// + /// + /// The method performs no reflection and is fully compatible with AOT and trimming. + /// + /// + private static View Inflate(Context context, int layoutId, ViewGroup parent, bool attachToRoot) + { + var inflater = LayoutInflater.FromContext(context); + return inflater?.Inflate(layoutId, parent, attachToRoot) + ?? throw new InvalidOperationException("LayoutInflater could not be obtained from context."); + } } diff --git a/src/ReactiveUI/Platforms/android/PlatformRegistrations.cs b/src/ReactiveUI/Platforms/android/PlatformRegistrations.cs index ff7236a467..8059b624c0 100644 --- a/src/ReactiveUI/Platforms/android/PlatformRegistrations.cs +++ b/src/ReactiveUI/Platforms/android/PlatformRegistrations.cs @@ -3,8 +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 ReactiveUI.Helpers; - namespace ReactiveUI; /// @@ -14,18 +12,14 @@ namespace ReactiveUI; public class PlatformRegistrations : IWantsToRegisterStuff { /// -#if NET6_0_OR_GREATER - [SuppressMessage("Trimming", "IL2046:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Not using reflection")] - [SuppressMessage("AOT", "IL3051:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "Not using reflection")] -#endif - public void Register(Action, Type> registerFunction) // TODO: Create Test + public void Register(IRegistrar registrar) // TODO: Create Test { - ArgumentExceptionHelper.ThrowIfNull(registerFunction); + ArgumentExceptionHelper.ThrowIfNull(registrar); - registerFunction(static () => new PlatformOperations(), typeof(IPlatformOperations)); - registerFunction(static () => new ComponentModelTypeConverter(), typeof(IBindingTypeConverter)); - registerFunction(static () => new AndroidObservableForWidgets(), typeof(ICreatesObservableForProperty)); - registerFunction(static () => new AndroidCommandBinders(), typeof(ICreatesCommandBinding)); + registrar.RegisterConstant(static () => new PlatformOperations()); + registrar.RegisterConstant(static () => new ComponentModelFallbackConverter()); + registrar.RegisterConstant(static () => new AndroidObservableForWidgets()); + registrar.RegisterConstant(static () => new AndroidCommandBinders()); if (!ModeDetector.InUnitTestRunner()) { @@ -33,6 +27,6 @@ public void Register(Action, Type> registerFunction) // TODO: Creat RxSchedulers.MainThreadScheduler = HandlerScheduler.MainThreadScheduler; } - registerFunction(static () => new BundleSuspensionDriver(), typeof(ISuspensionDriver)); + registrar.RegisterConstant(static () => new BundleSuspensionDriver()); } } diff --git a/src/ReactiveUI/Platforms/android/ReactiveActivity.cs b/src/ReactiveUI/Platforms/android/ReactiveActivity.cs index 425ca0fbc4..8cead871e4 100644 --- a/src/ReactiveUI/Platforms/android/ReactiveActivity.cs +++ b/src/ReactiveUI/Platforms/android/ReactiveActivity.cs @@ -13,10 +13,6 @@ namespace ReactiveUI; /// 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("ReactiveActivity inherits from ReactiveObject which uses extension methods that require dynamic code generation")] -[RequiresUnreferencedCode("ReactiveActivity inherits from ReactiveObject which uses extension methods that may require unreferenced code")] -#endif public class ReactiveActivity : Activity, IReactiveObject, IReactiveNotifyPropertyChanged, IHandleObservableErrors { private readonly Subject _activated = new(); diff --git a/src/ReactiveUI/Platforms/android/ReactiveActivity{TViewModel}.cs b/src/ReactiveUI/Platforms/android/ReactiveActivity{TViewModel}.cs index 117009809b..736d51705b 100644 --- a/src/ReactiveUI/Platforms/android/ReactiveActivity{TViewModel}.cs +++ b/src/ReactiveUI/Platforms/android/ReactiveActivity{TViewModel}.cs @@ -12,10 +12,6 @@ namespace ReactiveUI; /// (i.e. you can call RaiseAndSetIfChanged). /// /// The view model type. -#if NET6_0_OR_GREATER -[RequiresDynamicCode("ReactiveActivity inherits from ReactiveObject which uses extension methods that require dynamic code generation")] -[RequiresUnreferencedCode("ReactiveActivity inherits from ReactiveObject which uses extension methods that may require unreferenced code")] -#endif public class ReactiveActivity : ReactiveActivity, IViewFor, ICanActivate where TViewModel : class { diff --git a/src/ReactiveUI/Platforms/android/ReactiveFragment.cs b/src/ReactiveUI/Platforms/android/ReactiveFragment.cs index 3d16ca3228..22762289c6 100644 --- a/src/ReactiveUI/Platforms/android/ReactiveFragment.cs +++ b/src/ReactiveUI/Platforms/android/ReactiveFragment.cs @@ -14,10 +14,6 @@ namespace ReactiveUI; /// 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 public class ReactiveFragment : Fragment, IReactiveNotifyPropertyChanged, IReactiveObject, IHandleObservableErrors { private readonly Subject _activated = new(); diff --git a/src/ReactiveUI/Platforms/android/ReactiveFragment{TViewModel}.cs b/src/ReactiveUI/Platforms/android/ReactiveFragment{TViewModel}.cs index a10004baac..a6bed33e27 100644 --- a/src/ReactiveUI/Platforms/android/ReactiveFragment{TViewModel}.cs +++ b/src/ReactiveUI/Platforms/android/ReactiveFragment{TViewModel}.cs @@ -14,10 +14,6 @@ namespace ReactiveUI; /// (i.e. you can call RaiseAndSetIfChanged). /// /// The view model type. -#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 public class ReactiveFragment : ReactiveFragment, IViewFor, ICanActivate where TViewModel : class { diff --git a/src/ReactiveUI/Platforms/android/ReactiveViewHost.cs b/src/ReactiveUI/Platforms/android/ReactiveViewHost.cs index a9d7d0e573..bf38052057 100644 --- a/src/ReactiveUI/Platforms/android/ReactiveViewHost.cs +++ b/src/ReactiveUI/Platforms/android/ReactiveViewHost.cs @@ -3,60 +3,147 @@ // 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.Reflection; +using System.Runtime.Serialization; using Android.Content; using Android.Views; +using static ReactiveUI.ControlFetcherMixin; + namespace ReactiveUI; /// -/// A class that implements the Android ViewHolder pattern with a -/// ViewModel. Use it along with GetViewHost. +/// A class that implements the Android ViewHolder pattern with a ViewModel. +/// Use it along with GetViewHost. /// /// The view model type. -#if NET6_0_OR_GREATER -[RequiresDynamicCode("ReactiveViewHost uses methods that require dynamic code generation")] -[RequiresUnreferencedCode("ReactiveViewHost uses methods that may require unreferenced code")] -#endif -public abstract class ReactiveViewHost : LayoutViewHost, IViewFor, IReactiveNotifyPropertyChanged>, IReactiveObject +/// +/// +/// Trimming/AOT: Prefer constructors that do not enable legacy auto-wireup. These paths avoid reflection and do not +/// allocate property metadata. +/// +/// +/// Compatibility: A legacy constructor is provided that enables reflection-based wiring and initializes +/// for older infrastructure. +/// +/// +public abstract class ReactiveViewHost : + LayoutViewHost, + IViewFor, + IReactiveNotifyPropertyChanged>, + IReactiveObject where TViewModel : class, IReactiveObject { /// /// All public properties. /// + /// + /// This field is used by legacy reflection-based wiring. It is not initialized by default in AOT-safe construction + /// paths to avoid reflection and allocations. If a derived type requires this, use the legacy constructor. + /// [SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401: Field should be private", Justification = "Legacy reasons")] [SuppressMessage("Design", "CA1051: Do not declare visible instance fields", Justification = "Legacy reasons")] [IgnoreDataMember] [JsonIgnore] protected Lazy? allPublicProperties; + /// + /// Backing field for . + /// private TViewModel? _viewModel; /// /// Initializes a new instance of the class. /// - /// The CTX. - /// The layout identifier. - /// The parent. - /// if set to true [attach to root]. - /// if set to true [perform automatic wire-up]. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("ReactiveViewHost uses methods that require dynamic code generation")] - [RequiresUnreferencedCode("ReactiveViewHost uses methods that may require unreferenced code")] -#endif - protected ReactiveViewHost(Context ctx, int layoutId, ViewGroup parent, bool attachToRoot = false, bool performAutoWireup = true) - : base(ctx, layoutId, parent, attachToRoot, performAutoWireup) => - SetupRxObj(); + /// + /// This constructor performs no inflation or wiring and is AOT-safe. + /// Derived types may assign manually. + /// + protected ReactiveViewHost() + { + SetupRxObjAot(); + } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class by inflating a layout resource. + /// + /// The Android context. + /// The layout resource identifier. + /// The parent view group. + /// Whether to attach the inflated view to the parent. + /// + /// This constructor is fully AOT- and trimming-safe and performs no reflection-based auto-wireup. + /// + protected ReactiveViewHost(Context ctx, int layoutId, ViewGroup parent, bool attachToRoot = false) + : base(ctx, layoutId, parent, attachToRoot) + { + SetupRxObjAot(); + } + + /// + /// Initializes a new instance of the class by inflating a layout resource + /// and invoking an explicit, AOT-safe binder callback. + /// + /// The Android context. + /// The layout resource identifier. + /// The parent view group. + /// Whether to attach the inflated view to the parent. + /// + /// A callback responsible for explicitly wiring child views to the host. + /// + /// + /// This constructor is fully AOT-safe and avoids reflection entirely. + /// + /// Thrown when is . + protected ReactiveViewHost(Context ctx, int layoutId, ViewGroup parent, bool attachToRoot, Action, View> bind) + : base( + ctx, + layoutId, + parent, + attachToRoot, + (host, view) => + { + // The base constructor guarantees 'host' is the derived instance. + bind((ReactiveViewHost)host, view); + }) + { + ArgumentNullException.ThrowIfNull(bind); + SetupRxObjAot(); + } + + /// + /// Initializes a new instance of the class by inflating a layout resource + /// and optionally performing reflection-based auto-wireup. /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("ReactiveViewHost uses methods that require dynamic code generation")] - [RequiresUnreferencedCode("ReactiveViewHost uses methods that may require unreferenced code")] -#endif - protected ReactiveViewHost() => SetupRxObj(); + /// The Android context. + /// The layout resource identifier. + /// The parent view group. + /// Whether to attach the inflated view to the parent. + /// + /// If , performs automatic wiring using reflection. + /// + /// + /// The member resolution strategy used during auto-wireup. + /// + /// + /// This constructor exists for backward compatibility and is not trimming/AOT safe when + /// is . + /// + [RequiresUnreferencedCode("Legacy auto-wireup uses reflection and member discovery.")] + [RequiresDynamicCode("Legacy auto-wireup relies on runtime type inspection.")] + protected ReactiveViewHost( + Context ctx, + int layoutId, + ViewGroup parent, + bool attachToRoot, + bool performAutoWireup, + ResolveStrategy resolveStrategy) + : base(ctx, layoutId, parent, attachToRoot, performAutoWireup, resolveStrategy) + { + SetupRxObjLegacyReflection(); + } /// public event PropertyChangedEventHandler? PropertyChanged; @@ -75,7 +162,7 @@ public TViewModel? ViewModel object? IViewFor.ViewModel { get => _viewModel; - set => _viewModel = (TViewModel?)value!; + set => ViewModel = (TViewModel?)value; } /// @@ -89,34 +176,24 @@ public TViewModel? ViewModel public IObservable>> Changed => this.GetChangedObservable(); /// - /// Gets the thrown exceptions. + /// Gets an observable of exceptions thrown during reactive operations on this instance. /// [IgnoreDataMember] [JsonIgnore] public IObservable ThrownExceptions => this.GetThrownExceptionsObservable(); /// - /// When this method is called, an object will not fire change - /// notifications (neither traditional nor Observable notifications) + /// When this method is called, an object will not fire change notifications (neither traditional nor observable) /// until the return value is disposed. /// - /// An object that, when disposed, reenables change - /// notifications. -#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); // TODO: Create Test + /// An that re-enables change notifications when disposed. + public IDisposable SuppressChangeNotifications() => IReactiveObjectExtensions.SuppressChangeNotifications(this); /// - /// Gets a value indicating if change notifications are enabled. + /// Gets a value indicating whether change notifications are enabled. /// - /// A value indicating if change notifications are on or off. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("AreChangeNotificationsEnabled uses methods that require dynamic code generation")] - [RequiresUnreferencedCode("AreChangeNotificationsEnabled uses methods that may require unreferenced code")] -#endif - public bool AreChangeNotificationsEnabled() => IReactiveObjectExtensions.AreChangeNotificationsEnabled(this); // TODO: Create Test + /// if change notifications are enabled; otherwise, . + public bool AreChangeNotificationsEnabled() => IReactiveObjectExtensions.AreChangeNotificationsEnabled(this); /// void IReactiveObject.RaisePropertyChanging(PropertyChangingEventArgs args) => PropertyChanging?.Invoke(this, args); @@ -124,18 +201,36 @@ public TViewModel? ViewModel /// void IReactiveObject.RaisePropertyChanged(PropertyChangedEventArgs args) => PropertyChanged?.Invoke(this, args); + /// + /// Reinitializes reactive infrastructure after deserialization. + /// + /// The streaming context. [OnDeserialized] -#if NET6_0_OR_GREATER - [RequiresDynamicCode("SetupRxObj uses methods that require dynamic code generation")] - [RequiresUnreferencedCode("SetupRxObj uses methods that may require unreferenced code")] -#endif - private void SetupRxObj(in StreamingContext sc) => SetupRxObj(); - -#if NET6_0_OR_GREATER - [RequiresDynamicCode("SetupRxObj uses methods that require dynamic code generation")] - [RequiresUnreferencedCode("SetupRxObj uses methods that may require unreferenced code")] -#endif - private void SetupRxObj() => - allPublicProperties = new Lazy(() => - [.. GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance)]); + private void SetupRxObj(in StreamingContext sc) => SetupRxObjAot(); + + /// + /// Initializes the instance for AOT-safe operation. + /// + /// + /// This method intentionally does not touch to avoid reflection and allocations. + /// + private void SetupRxObjAot() + { + // No reflection-based property caching in AOT-safe paths. + allPublicProperties = null; + } + + /// + /// Initializes legacy reflection metadata used by older auto-wireup infrastructure. + /// + /// + /// This allocates reflection metadata and is not trimming/AOT safe. + /// + [RequiresUnreferencedCode("This method uses reflection to enumerate public instance properties.")] + [RequiresDynamicCode("This method uses reflection to enumerate public instance properties.")] + private void SetupRxObjLegacyReflection() + { + allPublicProperties = new Lazy( + () => GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance)); + } } diff --git a/src/ReactiveUI/Platforms/apple-common/AppSupportJsonSuspensionDriver.cs b/src/ReactiveUI/Platforms/apple-common/AppSupportJsonSuspensionDriver.cs index 576662d6ad..d617b6107b 100644 --- a/src/ReactiveUI/Platforms/apple-common/AppSupportJsonSuspensionDriver.cs +++ b/src/ReactiveUI/Platforms/apple-common/AppSupportJsonSuspensionDriver.cs @@ -4,58 +4,140 @@ // See the LICENSE file in the project root for full license information. using System.IO; +using System.Runtime.CompilerServices; using System.Text.Json; +using System.Text.Json.Serialization.Metadata; using Foundation; namespace ReactiveUI; /// -/// Loads and saves state to persistent storage. +/// Loads and saves state to persistent storage under the platform Application Support directory. /// -public class AppSupportJsonSuspensionDriver : ISuspensionDriver +/// +/// +/// This driver supports two serialization modes: +/// +/// +/// +/// +/// Source-generated System.Text.Json via overloads accepting . This is trimming/AOT-friendly. +/// +/// +/// +/// +/// Reflection-based System.Text.Json via interface methods. These are marked with +/// and . +/// +/// +/// +/// +/// The persisted file name is state.dat. +/// +/// +public sealed class AppSupportJsonSuspensionDriver : ISuspensionDriver { - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("LoadState implementations may use serialization which requires dynamic code generation")] - [RequiresUnreferencedCode("LoadState implementations may use serialization which may require unreferenced code")] -#endif - public IObservable LoadState() + /// + /// The default subdirectory used beneath Application Support. + /// + private const string DefaultSubDirectory = "Data"; + + /// + /// The persisted state file name. + /// + private const string StateFileName = "state.dat"; + + /// + /// Lazily computed directory path used to reduce repeated Foundation calls and filesystem checks. + /// + private readonly Lazy _appDirectory; + + /// + /// Initializes a new instance of the class. + /// + /// + /// The application-specific subdirectory beneath Application Support to store state. Defaults to Data. + /// + /// Thrown when is . + public AppSupportJsonSuspensionDriver(string subDirectory = DefaultSubDirectory) + { + ArgumentNullException.ThrowIfNull(subDirectory); + + _appDirectory = new Lazy( + () => CreateAppDirectory(NSSearchPathDirectory.ApplicationSupportDirectory, subDirectory), + isThreadSafe: true); + } + + /// + public IObservable LoadState(JsonTypeInfo typeInfo) { + ArgumentNullException.ThrowIfNull(typeInfo); + try { - var target = Path.Combine(CreateAppDirectory(NSSearchPathDirectory.ApplicationSupportDirectory), "state.dat"); - - var result = default(object); - using (var st = File.OpenRead(target)) - { - result = JsonSerializer.Deserialize(st); - } + var path = GetStatePath(); + using var stream = File.OpenRead(path); + var result = JsonSerializer.Deserialize(stream, typeInfo); return Observable.Return(result); } catch (Exception ex) { - return Observable.Throw(ex); + return Observable.Throw(ex); + } + } + + /// + public IObservable SaveState(T state, JsonTypeInfo typeInfo) + { + ArgumentNullException.ThrowIfNull(typeInfo); + + try + { + var path = GetStatePath(); + using var stream = File.Open(path, FileMode.Create, FileAccess.Write, FileShare.None); + + JsonSerializer.Serialize(stream, state, typeInfo); + return Observables.Unit; + } + catch (Exception ex) + { + return Observable.Throw(ex); } } - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("SaveState implementations may use serialization which requires dynamic code generation")] - [RequiresUnreferencedCode("SaveState implementations may use serialization which may require unreferenced code")] -#endif - public IObservable SaveState(object state) + /// + [RequiresUnreferencedCode("Uses reflection-based System.Text.Json serialization for 'object'. Prefer LoadState(JsonTypeInfo) for trimming/AOT.")] + [RequiresDynamicCode("Uses reflection-based System.Text.Json serialization for 'object'. Prefer LoadState(JsonTypeInfo) for trimming/AOT.")] + public IObservable LoadState() { try { - var target = Path.Combine(CreateAppDirectory(NSSearchPathDirectory.ApplicationSupportDirectory), "state.dat"); + var path = GetStatePath(); + using var stream = File.OpenRead(path); + + // Reflection-based: object deserialization typically requires metadata at runtime. + var result = JsonSerializer.Deserialize(stream); + return Observable.Return(result); + } + catch (Exception ex) + { + return Observable.Throw(ex); + } + } - using (var st = File.Open(target, FileMode.Create)) - { - JsonSerializer.Serialize(st, state); - } + /// + [RequiresUnreferencedCode("Uses reflection-based System.Text.Json serialization for generic T. Prefer SaveState(T, JsonTypeInfo) for trimming/AOT.")] + [RequiresDynamicCode("Uses reflection-based System.Text.Json serialization for generic T. Prefer SaveState(T, JsonTypeInfo) for trimming/AOT.")] + public IObservable SaveState(T state) + { + try + { + var path = GetStatePath(); + using var stream = File.Open(path, FileMode.Create, FileAccess.Write, FileShare.None); + JsonSerializer.Serialize(stream, state); return Observables.Unit; } catch (Exception ex) @@ -64,17 +146,13 @@ public IObservable SaveState(object state) } } - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("InvalidateState uses JsonSerializer which requires dynamic code generation")] - [RequiresUnreferencedCode("InvalidateState uses JsonSerializer which may require unreferenced code")] -#endif + /// public IObservable InvalidateState() { try { - var target = Path.Combine(CreateAppDirectory(NSSearchPathDirectory.ApplicationSupportDirectory), "state.dat"); - File.Delete(target); + var path = GetStatePath(); + File.Delete(path); return Observables.Unit; } @@ -84,16 +162,53 @@ public IObservable InvalidateState() } } - private static string CreateAppDirectory(NSSearchPathDirectory targetDir, string subDir = "Data") + /// + /// Creates (if necessary) and returns the application storage directory beneath the specified special folder. + /// + /// The platform search path directory. + /// The application-specific subdirectory name. + /// The fully qualified directory path. + /// + /// Thrown when the platform path cannot be resolved or the bundle identifier is unavailable. + /// + private static string CreateAppDirectory(NSSearchPathDirectory targetDir, string subDir) { + // Allocate NSFileManager only once per driver instance via Lazy. Kept local and simple. var fm = new NSFileManager(); + var url = fm.GetUrl(targetDir, NSSearchPathDomain.All, null, true, out _); - var ret = Path.Combine(url.RelativePath!, NSBundle.MainBundle.BundleIdentifier, subDir); - if (!Directory.Exists(ret)) + if (url is null) { - Directory.CreateDirectory(ret); + throw new InvalidOperationException("Unable to resolve platform application support directory."); } - return ret; + var bundleId = NSBundle.MainBundle?.BundleIdentifier; + if (string.IsNullOrEmpty(bundleId)) + { + throw new InvalidOperationException("Unable to resolve application bundle identifier."); + } + + var basePath = url.RelativePath; + if (string.IsNullOrEmpty(basePath)) + { + throw new InvalidOperationException("Resolved application support path was empty."); + } + + var path = Path.Combine(basePath, bundleId, subDir); + + if (!Directory.Exists(path)) + { + Directory.CreateDirectory(path); + } + + return path; } + + /// + /// Computes the full path to the persisted state file. + /// + /// The absolute file path. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private string GetStatePath() + => Path.Combine(_appDirectory.Value, StateFileName); } diff --git a/src/ReactiveUI/Platforms/apple-common/Converters/DateTimeNSDateConverter.cs b/src/ReactiveUI/Platforms/apple-common/Converters/DateTimeNSDateConverter.cs deleted file mode 100644 index 06ea861b97..0000000000 --- a/src/ReactiveUI/Platforms/apple-common/Converters/DateTimeNSDateConverter.cs +++ /dev/null @@ -1,42 +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. - -using Foundation; - -namespace ReactiveUI; - -/// -/// Binding Type Converter for DateTime to NSDateTime. -/// -public class DateTimeNSDateConverter : IBindingTypeConverter -{ - internal static Lazy Instance { get; } = new(); - - /// - public int GetAffinityForObjects(Type fromType, Type toType) => - (fromType == typeof(DateTime) && toType == typeof(NSDate)) || - (toType == typeof(DateTime) && fromType == typeof(NSDate)) ? 4 : -1; - - /// - public bool TryConvert(object? from, Type toType, object? conversionHint, out object? result) - { - result = null; - - if (from?.GetType() == typeof(DateTime) && toType == typeof(NSDate)) - { - var dt = (DateTime)from; - result = (NSDate)dt; - return true; - } - else if (from?.GetType() == typeof(NSDate) && toType == typeof(DateTime)) - { - var dt = (NSDate)from; - result = (DateTime)dt; - return true; - } - - return false; - } -} diff --git a/src/ReactiveUI/Platforms/apple-common/Converters/DateTimeOffsetToNSDateConverter.cs b/src/ReactiveUI/Platforms/apple-common/Converters/DateTimeOffsetToNSDateConverter.cs new file mode 100644 index 0000000000..0097774b2e --- /dev/null +++ b/src/ReactiveUI/Platforms/apple-common/Converters/DateTimeOffsetToNSDateConverter.cs @@ -0,0 +1,27 @@ +// 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 UIKIT || MACOS +using System.Diagnostics.CodeAnalysis; +using Foundation; + +namespace ReactiveUI; + +/// +/// Converts to . +/// +public sealed class DateTimeOffsetToNSDateConverter : BindingTypeConverter +{ + /// + public override int GetAffinityForObjects() => 100; + + /// + public override bool TryConvert(DateTimeOffset from, object? conversionHint, [NotNullWhen(true)] out NSDate? result) + { + result = (NSDate)from.DateTime; + return true; + } +} +#endif diff --git a/src/ReactiveUI/Platforms/apple-common/Converters/DateTimeToNSDateConverter.cs b/src/ReactiveUI/Platforms/apple-common/Converters/DateTimeToNSDateConverter.cs new file mode 100644 index 0000000000..1a9c1c9840 --- /dev/null +++ b/src/ReactiveUI/Platforms/apple-common/Converters/DateTimeToNSDateConverter.cs @@ -0,0 +1,27 @@ +// 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 UIKIT || MACOS +using System.Diagnostics.CodeAnalysis; +using Foundation; + +namespace ReactiveUI; + +/// +/// Converts to . +/// +public sealed class DateTimeToNSDateConverter : BindingTypeConverter +{ + /// + public override int GetAffinityForObjects() => 100; + + /// + public override bool TryConvert(DateTime from, object? conversionHint, [NotNullWhen(true)] out NSDate? result) + { + result = (NSDate)from; + return true; + } +} +#endif diff --git a/src/ReactiveUI/Platforms/apple-common/Converters/NSDateToDateTimeConverter.cs b/src/ReactiveUI/Platforms/apple-common/Converters/NSDateToDateTimeConverter.cs new file mode 100644 index 0000000000..c11c0d2cd0 --- /dev/null +++ b/src/ReactiveUI/Platforms/apple-common/Converters/NSDateToDateTimeConverter.cs @@ -0,0 +1,33 @@ +// 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 UIKIT || MACOS +using System.Diagnostics.CodeAnalysis; +using Foundation; + +namespace ReactiveUI; + +/// +/// Converts to . +/// +public sealed class NSDateToDateTimeConverter : BindingTypeConverter +{ + /// + public override int GetAffinityForObjects() => 100; + + /// + public override bool TryConvert(NSDate? from, object? conversionHint, [NotNullWhen(true)] out DateTime result) + { + if (from is null) + { + result = default; + return false; + } + + result = (DateTime)from; + return true; + } +} +#endif diff --git a/src/ReactiveUI/Platforms/apple-common/Converters/NSDateToDateTimeOffsetConverter.cs b/src/ReactiveUI/Platforms/apple-common/Converters/NSDateToDateTimeOffsetConverter.cs new file mode 100644 index 0000000000..a4e6652961 --- /dev/null +++ b/src/ReactiveUI/Platforms/apple-common/Converters/NSDateToDateTimeOffsetConverter.cs @@ -0,0 +1,33 @@ +// 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 UIKIT || MACOS +using System.Diagnostics.CodeAnalysis; +using Foundation; + +namespace ReactiveUI; + +/// +/// Converts to . +/// +public sealed class NSDateToDateTimeOffsetConverter : BindingTypeConverter +{ + /// + public override int GetAffinityForObjects() => 100; + + /// + public override bool TryConvert(NSDate? from, object? conversionHint, [NotNullWhen(true)] out DateTimeOffset result) + { + if (from is null) + { + result = default; + return false; + } + + result = new DateTimeOffset((DateTime)from); + return true; + } +} +#endif diff --git a/src/ReactiveUI/Platforms/apple-common/Converters/NSDateToNullableDateTimeConverter.cs b/src/ReactiveUI/Platforms/apple-common/Converters/NSDateToNullableDateTimeConverter.cs new file mode 100644 index 0000000000..d83005f146 --- /dev/null +++ b/src/ReactiveUI/Platforms/apple-common/Converters/NSDateToNullableDateTimeConverter.cs @@ -0,0 +1,33 @@ +// 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 UIKIT || MACOS +using System.Diagnostics.CodeAnalysis; +using Foundation; + +namespace ReactiveUI; + +/// +/// Converts to nullable . +/// +public sealed class NSDateToNullableDateTimeConverter : BindingTypeConverter +{ + /// + public override int GetAffinityForObjects() => 100; + + /// + public override bool TryConvert(NSDate? from, object? conversionHint, [NotNullWhen(true)] out DateTime? result) + { + if (from is null) + { + result = null; + return false; + } + + result = (DateTime)from; + return true; + } +} +#endif diff --git a/src/ReactiveUI/Platforms/apple-common/Converters/NSDateToNullableDateTimeOffsetConverter.cs b/src/ReactiveUI/Platforms/apple-common/Converters/NSDateToNullableDateTimeOffsetConverter.cs new file mode 100644 index 0000000000..732da6d992 --- /dev/null +++ b/src/ReactiveUI/Platforms/apple-common/Converters/NSDateToNullableDateTimeOffsetConverter.cs @@ -0,0 +1,33 @@ +// 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 UIKIT || MACOS +using System.Diagnostics.CodeAnalysis; +using Foundation; + +namespace ReactiveUI; + +/// +/// Converts to nullable . +/// +public sealed class NSDateToNullableDateTimeOffsetConverter : BindingTypeConverter +{ + /// + public override int GetAffinityForObjects() => 100; + + /// + public override bool TryConvert(NSDate? from, object? conversionHint, [NotNullWhen(true)] out DateTimeOffset? result) + { + if (from is null) + { + result = null; + return false; + } + + result = new DateTimeOffset((DateTime)from); + return true; + } +} +#endif diff --git a/src/ReactiveUI/Platforms/apple-common/Converters/NullableDateTimeOffsetToNSDateConverter.cs b/src/ReactiveUI/Platforms/apple-common/Converters/NullableDateTimeOffsetToNSDateConverter.cs new file mode 100644 index 0000000000..51175c7335 --- /dev/null +++ b/src/ReactiveUI/Platforms/apple-common/Converters/NullableDateTimeOffsetToNSDateConverter.cs @@ -0,0 +1,33 @@ +// 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 UIKIT || MACOS +using System.Diagnostics.CodeAnalysis; +using Foundation; + +namespace ReactiveUI; + +/// +/// Converts nullable to . +/// +public sealed class NullableDateTimeOffsetToNSDateConverter : BindingTypeConverter +{ + /// + public override int GetAffinityForObjects() => 100; + + /// + public override bool TryConvert(DateTimeOffset? from, object? conversionHint, [NotNullWhen(true)] out NSDate? result) + { + if (!from.HasValue) + { + result = null; + return false; + } + + result = (NSDate)from.Value.DateTime; + return true; + } +} +#endif diff --git a/src/ReactiveUI/Platforms/apple-common/Converters/NullableDateTimeToNSDateConverter.cs b/src/ReactiveUI/Platforms/apple-common/Converters/NullableDateTimeToNSDateConverter.cs new file mode 100644 index 0000000000..9456c277b6 --- /dev/null +++ b/src/ReactiveUI/Platforms/apple-common/Converters/NullableDateTimeToNSDateConverter.cs @@ -0,0 +1,33 @@ +// 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 UIKIT || MACOS +using System.Diagnostics.CodeAnalysis; +using Foundation; + +namespace ReactiveUI; + +/// +/// Converts nullable to . +/// +public sealed class NullableDateTimeToNSDateConverter : BindingTypeConverter +{ + /// + public override int GetAffinityForObjects() => 100; + + /// + public override bool TryConvert(DateTime? from, object? conversionHint, [NotNullWhen(true)] out NSDate? result) + { + if (!from.HasValue) + { + result = null; + return false; + } + + result = (NSDate)from.Value; + return true; + } +} +#endif diff --git a/src/ReactiveUI/Platforms/apple-common/KVOObservableForProperty.cs b/src/ReactiveUI/Platforms/apple-common/KVOObservableForProperty.cs index 33a31b09b6..88695ff103 100644 --- a/src/ReactiveUI/Platforms/apple-common/KVOObservableForProperty.cs +++ b/src/ReactiveUI/Platforms/apple-common/KVOObservableForProperty.cs @@ -9,132 +9,224 @@ using Foundation; -namespace ReactiveUI; +using ReactiveUI; /// -/// This class provides notifications for Cocoa Framework objects based on -/// Key-Value Observing. Unfortunately, this class is a bit Tricky™, because -/// of the caveat mentioned below - there is no way up-front to be able to -/// tell whether a given property on an object is Key-Value Observable, we -/// only have to hope for the best :-/. +/// Provides change notifications for Cocoa instances using Key-Value Observing (KVO). /// -[Preserve(AllMembers = true)] -[RequiresUnreferencedCode("KVOObservableForProperty uses methods that may require unreferenced code")] -public class KVOObservableForProperty : ICreatesObservableForProperty +[ReactiveUI.Preserve(AllMembers = true)] +public sealed class KVOObservableForProperty : ICreatesObservableForProperty { - private static readonly MemoizingMRUCache<(Type type, string propertyName), bool> _declaredInNSObject; + /// + [RequiresUnreferencedCode("Uses reflection over runtime types which is not trim- or AOT-safe.")] + public int GetAffinityForObject(Type type, string propertyName, bool beforeChanged = false) + { + ArgumentNullException.ThrowIfNull(type); + ArgumentNullException.ThrowIfNull(propertyName); + + if (!typeof(NSObject).IsAssignableFrom(type)) + { + return 0; + } - static KVOObservableForProperty() + return IsDeclaredOnNSObject(type, propertyName) ? 15 : 0; + } + + /// + [RequiresUnreferencedCode("Uses reflection over runtime types which is not trim- or AOT-safe.")] + public IObservable> GetNotificationForProperty( + object sender, + Expression expression, + string propertyName, + bool beforeChanged = false, + bool suppressWarnings = false) { - var monotouchAssemblyName = typeof(NSObject).Assembly.FullName; + ArgumentNullException.ThrowIfNull(sender); + ArgumentNullException.ThrowIfNull(expression); + ArgumentNullException.ThrowIfNull(propertyName); - _declaredInNSObject = new MemoizingMRUCache<(Type type, string propertyName), bool>( - (pair, _) => - { - var thisType = pair.type; - - // Types that aren't NSObjects at all are uninteresting to us - if (!typeof(NSObject).IsAssignableFrom(thisType)) - { - return false; - } - - while (thisType is not null) - { - if (thisType.GetMembers(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly).Any(x => x.Name == pair.propertyName)) - { - // NB: This is a not-completely correct way to detect if - // an object is defined in an Obj-C class (it will fail if - // you're using a binding to a 3rd-party Obj-C library). - return thisType.Assembly.FullName == monotouchAssemblyName; - } - - thisType = thisType.BaseType; - } - - // The property doesn't exist at all - return false; - }, - RxApp.BigCacheLimit); + if (sender is not NSObject) + { + throw new ArgumentException("Sender must be an NSObject.", nameof(sender)); + } + + var keyPath = GetCocoaKeyPathUnsafe(sender.GetType(), propertyName); + + return GetNotificationForProperty( + sender, + expression, + propertyName, + keyPath, + beforeChanged, + suppressWarnings); } - /// -#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) => - _declaredInNSObject.Get((type, propertyName)) ? 15 : 0; - - /// -#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) + /// + /// Subscribes to KVO change notifications using a pre-resolved observation key (KVO key path). + /// + /// + /// + /// This helper wires NSObject AddObserver/RemoveObserver patterns + /// to an sequence and ensures deterministic unsubscription. + /// + /// + /// The returned disposable is idempotent and will remove the observer and release the pinned delegate instance. + /// + /// + /// The object to observe. Must be an . + /// + /// The expression describing the observed member. This value is surfaced in emitted + /// instances. + /// + /// The .NET property name being observed. + /// The Cocoa KVO key path to observe. + /// + /// If , request notifications using ; otherwise + /// request notifications using . + /// + /// If , warnings should not be logged. + /// + /// An observable that produces an whenever the KVO key path changes. + /// + /// + /// Thrown when , , , or + /// is . + /// + /// Thrown when is not an . + private static IObservable> GetNotificationForProperty( + object sender, + Expression expression, + string propertyName, + string observationKey, + bool beforeChanged = false, + bool suppressWarnings = false) { + ArgumentNullException.ThrowIfNull(sender); + ArgumentNullException.ThrowIfNull(expression); + ArgumentNullException.ThrowIfNull(propertyName); + ArgumentNullException.ThrowIfNull(observationKey); + if (sender is not NSObject obj) { - throw new ArgumentException("Sender isn't an NSObject"); + throw new ArgumentException("Sender must be an NSObject.", nameof(sender)); } - return Observable.Create>(subj => + return Observable.Create>(observer => { - var bobs = new BlockObserveValueDelegate((__, s, _) => - subj.OnNext(new ObservedChange(s, expression, default))); + ArgumentNullException.ThrowIfNull(observer); + + // Create a single stable delegate instance; KVO removal requires the same observer instance. + var callback = new BlockObserveValueDelegate((unusedKeyPath, observedObject, unusedChange) => + observer.OnNext(new ObservedChange(observedObject, expression, default))); - var pin = GCHandle.Alloc(bobs); + // Ensure the delegate is kept alive for the lifetime of the subscription. + var handle = GCHandle.Alloc(callback); - var keyPath = (NSString)FindCocoaNameFromNetName(sender.GetType(), propertyName); + var keyPath = (NSString)observationKey; - obj.AddObserver(bobs, keyPath, beforeChanged ? NSKeyValueObservingOptions.Old : NSKeyValueObservingOptions.New, IntPtr.Zero); + obj.AddObserver( + callback, + keyPath, + beforeChanged ? NSKeyValueObservingOptions.Old : NSKeyValueObservingOptions.New, + IntPtr.Zero); return Disposable.Create(() => { - obj.RemoveObserver(bobs, keyPath); - pin.Free(); + obj.RemoveObserver(callback, keyPath); + handle.Free(); }); }); } - private static string FindCocoaNameFromNetName([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type senderType, string propertyName) + /// + /// Determines whether the specified member name is declared on the type hierarchy rooted at . + /// + /// The runtime type to inspect. + /// The member name to test. + /// + /// if the member name is present on the inspected hierarchy; otherwise . + /// + [RequiresUnreferencedCode("Uses reflection over runtime types which is not trim- or AOT-safe.")] + private static bool IsDeclaredOnNSObject( + Type type, + string propertyName) { - var propIsBoolean = false; + var monotouchAssemblyName = typeof(NSObject).Assembly.FullName; - var pi = senderType.GetTypeInfo().DeclaredProperties.FirstOrDefault(static x => !x.IsStatic()); - if (pi is null) + var current = type; + while (current is not null) { - goto attemptGuess; - } + // Search only public instance members declared at this level. + var members = current.GetMembers(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly); - if (pi.DeclaringType == typeof(bool)) - { - propIsBoolean = true; + for (var i = 0; i < members.Length; i++) + { + if (string.Equals(members[i].Name, propertyName, StringComparison.Ordinal)) + { + // Historical heuristic: treat it as Obj-C-backed if it originates from the NSObject assembly. + return string.Equals(current.Assembly.FullName, monotouchAssemblyName, StringComparison.Ordinal); + } + } + + current = current.BaseType; } - var mi = pi.GetGetMethod(); - if (mi is null) - { - goto attemptGuess; - } + // The member doesn't exist on the hierarchy. + return false; + } - var attr = mi.GetCustomAttributes(true).OfType().FirstOrDefault(); - if (attr is null) - { - goto attemptGuess; - } + /// + /// Maps a .NET property name to an Objective-C selector / KVO key path using reflection over the runtime type. + /// + /// + /// + /// This method inspects the runtime type for an exported selector attribute on the getter, and falls back to a + /// naming convention when no export is found. + /// + /// + /// Trimming/AOT: this method reflects over runtime types and is not trimming-safe. + /// + /// + /// The runtime type of the sender. + /// The .NET property name. + /// The derived Cocoa key path to use for KVO. + [RequiresUnreferencedCode("Uses reflection over runtime types which is not trim- or AOT-safe.")] + private static string GetCocoaKeyPathUnsafe(Type senderType, string propertyName) + { + // Note: This logic preserves the original behavior pattern: best-effort attempt and fallback. + var property = senderType + .GetTypeInfo() + .DeclaredProperties + .FirstOrDefault(p => !p.IsStatic()); - if (attr.Selector is not null) + var propIsBoolean = false; + + if (property is not null) { - return attr.Selector; + propIsBoolean = property.PropertyType == typeof(bool); + + var getter = property.GetGetMethod(); + if (getter is not null) + { + var export = getter + .GetCustomAttributes(inherit: true) + .OfType() + .FirstOrDefault(); + + if (export?.Selector is not null) + { + return export.Selector; + } + } } - attemptGuess: if (propIsBoolean) { propertyName = "Is" + propertyName; } - return string.Concat(char.ToLowerInvariant(propertyName[0]).ToString(CultureInfo.InvariantCulture), propertyName.AsSpan(1)); + return string.Concat( + char.ToLowerInvariant(propertyName[0]).ToString(CultureInfo.InvariantCulture), + propertyName.AsSpan(1)); } } diff --git a/src/ReactiveUI/Platforms/apple-common/ObservableForPropertyBase.cs b/src/ReactiveUI/Platforms/apple-common/ObservableForPropertyBase.cs index b09031c4a0..8a6319ab7c 100644 --- a/src/ReactiveUI/Platforms/apple-common/ObservableForPropertyBase.cs +++ b/src/ReactiveUI/Platforms/apple-common/ObservableForPropertyBase.cs @@ -3,6 +3,13 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. +using System; +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; +using System.Linq.Expressions; +using System.Reactive.Disposables; +using System.Reactive.Linq; + using Foundation; #if UIKIT @@ -14,150 +21,360 @@ namespace ReactiveUI; /// -/// ObservableForPropertyBase represents an object that knows how to -/// create notifications for a given type of object. Implement this if you -/// are porting RxUI to a new UI toolkit, or generally want to enable WhenAny -/// for another type of object that can be observed in a unique way. +/// Represents an object that knows how to create notifications for a given type of object. +/// Implement this when porting ReactiveUI to a new UI toolkit, or to enable WhenAny* +/// support for another type that can be observed in a unique way. /// +/// +/// Implementations typically call +/// during construction to populate supported properties. +/// [Preserve] public abstract class ObservableForPropertyBase : ICreatesObservableForProperty { /// - /// Configuration map. + /// Message used for annotations on reflection-based event hookup. + /// + private const string RequiresUnreferencedCodeMessage = + "String-based event hookup uses reflection over members that may be trimmed."; + + /// + /// Message used for annotations on reflection-based event hookup. + /// + private const string RequiresDynamicCodeMessage = + "String-based event hookup may require runtime code generation and is not guaranteed to be AOT-compatible."; + + /// + /// Synchronization gate protecting and . + /// + private readonly object _gate = new(); + + /// + /// Configuration map keyed by registered type and then by property name. /// private readonly Dictionary> _config = []; + /// + /// Cache of the best matching registration for a runtime type and property name. + /// + /// + /// Entries are versioned so that updates via invalidate previous results without + /// requiring global cache clearing under lock. + /// + private readonly ConcurrentDictionary<(RuntimeTypeHandle Type, string Property), CacheEntry> _bestMatchCache = new(); + + /// + /// Monotonically increasing version for used to invalidate . + /// + private int _version; + /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("GetAffinityForObject uses methods that require dynamic code generation")] - [RequiresUnreferencedCode("GetAffinityForObject uses methods that may require unreferenced code")] -#endif + [RequiresUnreferencedCode("Uses reflection over runtime types which is not trim- or AOT-safe.")] public int GetAffinityForObject(Type type, string propertyName, bool beforeChanged = false) { - if (beforeChanged) - { - return 0; - } - - var match = _config.Keys - .Where(x => x.IsAssignableFrom(type) && _config[x].ContainsKey(propertyName)) - .Select(x => _config[x][propertyName]) - .OrderByDescending(x => x.Affinity) - .FirstOrDefault(); + ArgumentNullException.ThrowIfNull(type); + ArgumentNullException.ThrowIfNull(propertyName); - if (match is null) + if (beforeChanged) { return 0; } - return match.Affinity; + var match = ResolveBestMatch(type, propertyName); + return match is null ? 0 : match.Affinity; } /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("GetNotificationForProperty uses methods that require dynamic code generation")] - [RequiresUnreferencedCode("GetNotificationForProperty uses methods that may require unreferenced code")] -#endif + [RequiresUnreferencedCode("Uses reflection over runtime types which is not trim- or AOT-safe.")] public IObservable> GetNotificationForProperty( - object sender, Expression expression, string propertyName, bool beforeChanged = false, bool suppressWarnings = false) + object sender, + Expression expression, + string propertyName, + bool beforeChanged = false, + bool suppressWarnings = false) { ArgumentExceptionHelper.ThrowIfNull(sender); + ArgumentExceptionHelper.ThrowIfNull(expression); + ArgumentExceptionHelper.ThrowIfNull(propertyName); if (beforeChanged) { - return Observable>.Never; + return Observable>.Never; } var type = sender.GetType(); + var match = ResolveBestMatch(type, propertyName); - var match = _config.Keys - .Where(x => x.IsAssignableFrom(type) && _config[x].ContainsKey(propertyName)) - .Select(x => _config[x][propertyName]) - .OrderByDescending(x => x.Affinity) - .FirstOrDefault(); + if (match is null) + { + throw new NotSupportedException($"Notifications for {type.Name}.{propertyName} are not supported"); + } - return match is null - ? throw new NotSupportedException($"Notifications for {type.Name}.{propertyName} are not supported") - : match.CreateObservable.Invoke((NSObject)sender, expression); + // Do not invoke user-provided observable factories under lock. + return match.CreateObservable.Invoke((NSObject)sender, expression); } #if UIKIT /// - /// Creates an Observable for a UIControl Event. + /// Creates an observable sequence that produces a notification each time the given + /// is raised by the . /// - /// An observable. - /// The sender. - /// The expression. + /// The native sender. + /// The expression associated with the observed change. /// The control event to listen for. - protected static IObservable> ObservableFromUIControlEvent(NSObject sender, Expression expression, UIControlEvent evt) => - Observable.Create>(subj => + /// An observable sequence of observed changes. + protected static IObservable> ObservableFromUIControlEvent( + NSObject sender, + Expression expression, + UIControlEvent evt) => + Observable.Create>(observer => { var control = (UIControl)sender; - void Handler(object? s, EventArgs e) => subj.OnNext(new ObservedChange(sender, expression, default)); + // Stable delegate allows deterministic unsubscription. + void Handler(object? s, EventArgs e) => + observer.OnNext(new ObservedChange(sender, expression, default)); control.AddTarget(Handler, evt); - return Disposable.Create(() => control.RemoveTarget(Handler, evt)); }); #endif /// - /// Creates an Observable for a NSNotificationCenter notification. + /// Creates an observable sequence that produces a notification each time the specified + /// notification is posted for . /// - /// The from notification. - /// Sender. - /// Expression. - /// Notification. - protected static IObservable> ObservableFromNotification(NSObject sender, Expression expression, NSString notification) => - Observable.Create>(subj => + /// The native sender. + /// The expression associated with the observed change. + /// The notification name. + /// An observable sequence of observed changes. + protected static IObservable> ObservableFromNotification( + NSObject sender, + Expression expression, + NSString notification) => + Observable.Create>(observer => { var handle = NSNotificationCenter.DefaultCenter.AddObserver( notification, - _ => subj.OnNext(new ObservedChange(sender, expression, default)), + _ => observer.OnNext(new ObservedChange(sender, expression, default)), sender); return Disposable.Create(() => NSNotificationCenter.DefaultCenter.RemoveObserver(handle)); }); /// - /// Creates an Observable for a NSNotificationCenter notification. + /// Creates an observable sequence from an event using reflection-based string event lookup. /// - /// The from notification. - /// Sender. - /// The expression. + /// + /// Prefer the add/remove overloads (for example, + /// ) + /// for trimming/AOT compatibility. + /// + /// The native sender. + /// The expression associated with the observed change. /// The event name. - [RequiresUnreferencedCode("ObservableFromEvent uses methods that may require unreferenced code")] - protected static IObservable> ObservableFromEvent(NSObject sender, Expression expression, string eventName) => - Observable.Create>(subj => - Observable.FromEventPattern(sender, eventName).Subscribe(_ => - subj.OnNext(new ObservedChange(sender, expression, default)))); + /// An observable sequence of observed changes. + [RequiresUnreferencedCode(RequiresUnreferencedCodeMessage)] + [RequiresDynamicCode(RequiresDynamicCodeMessage)] + protected static IObservable> ObservableFromEvent( + NSObject sender, + Expression expression, + string eventName) => + Observable.Create>(observer => + Observable + .FromEventPattern(sender, eventName) + .Subscribe(_ => observer.OnNext(new ObservedChange(sender, expression, default)))); + + /// + /// Creates an observable sequence from an event using explicit add/remove handlers (non-reflection). + /// + /// The sender type. + /// The sender instance. + /// The expression associated with the observed change. + /// Adds the handler to the event source. + /// Removes the handler from the event source. + /// An observable sequence of observed changes. + protected static IObservable> ObservableFromEvent( + TSender sender, + Expression expression, + Action addHandler, + Action removeHandler) + where TSender : NSObject => + Observable.Create>(observer => + { + // Stable handler for deterministic unsubscription. + void Handler(object? s, EventArgs e) => + observer.OnNext(new ObservedChange(sender, expression, default)); + + addHandler(Handler); + return Disposable.Create(() => removeHandler(Handler)); + }); + + /// + /// Creates an observable sequence from a typed event using explicit add/remove handlers (non-reflection). + /// + /// The sender type. + /// The event args type. + /// The sender instance. + /// The expression associated with the observed change. + /// Adds the handler to the event source. + /// Removes the handler from the event source. + /// An observable sequence of observed changes. + protected static IObservable> ObservableFromEvent( + TSender sender, + Expression expression, + Action> addHandler, + Action> removeHandler) + where TSender : NSObject + where TEventArgs : EventArgs => + Observable.Create>(observer => + { + // Stable handler for deterministic unsubscription. + void Handler(object? s, TEventArgs e) => + observer.OnNext(new ObservedChange(sender, expression, default)); + + addHandler(Handler); + return Disposable.Create(() => removeHandler(Handler)); + }); + + /// + /// Registers an observable factory for the specified and . + /// + /// The type the property belongs to. + /// The property name. + /// The affinity score for this registration. + /// Factory that creates the observable for this property. + /// + /// Thrown when , , or is . + /// + protected void Register( + Type type, + string property, + int affinity, + Func>> createObservable) + { + ArgumentNullException.ThrowIfNull(type); + ArgumentNullException.ThrowIfNull(property); + ArgumentNullException.ThrowIfNull(createObservable); + + lock (_gate) + { + if (!_config.TryGetValue(type, out var typeProperties)) + { + typeProperties = []; + _config[type] = typeProperties; + } + + typeProperties[property] = new ObservablePropertyInfo(affinity, createObservable); + + // Invalidate caches by bumping version. + unchecked + { + _version++; + } + } + } /// - /// Registers an observable factory for the specified type and property. + /// Resolves the best registered match for a runtime type and property name, using a versioned cache. /// - /// Type. - /// Property. - /// Affinity. - /// Create observable. - protected void Register(Type type, string property, int affinity, Func>> createObservable) + /// The runtime type to resolve. + /// The property name to resolve. + /// The best matching registration, or if none exists. + private ObservablePropertyInfo? ResolveBestMatch(Type runtimeType, string propertyName) { - if (!_config.TryGetValue(type, out var typeProperties)) + // Fast path: check cache. + var key = (runtimeType.TypeHandle, propertyName); + if (_bestMatchCache.TryGetValue(key, out var cached)) { - typeProperties = []; - _config[type] = typeProperties; + // If config has not changed since the cached entry was computed, return it. + if (cached.Version == _version) + { + return cached.Info; + } } - typeProperties[property] = new ObservablePropertyInfo(affinity, createObservable); + // Slow path: compute under lock against a consistent snapshot of config. + ObservablePropertyInfo? best = null; + var versionSnapshot = 0; + + lock (_gate) + { + versionSnapshot = _version; + + foreach (var kvp in _config) + { + var registeredType = kvp.Key; + + if (!registeredType.IsAssignableFrom(runtimeType)) + { + continue; + } + + if (!kvp.Value.TryGetValue(propertyName, out var info)) + { + continue; + } + + if (best is null || info.Affinity > best.Affinity) + { + best = info; + } + } + } + + // Publish computed value to cache (including null, to avoid repeated scans for unsupported properties). + _bestMatchCache[key] = new CacheEntry(versionSnapshot, best); + return best; } - internal record ObservablePropertyInfo + /// + /// Represents a cached best-match result for a (runtime type, property) pair. + /// + private readonly record struct CacheEntry + { + /// + /// Initializes a new instance of the struct. + /// Initializes a new instance of the record. + /// + /// The configuration version the entry was computed from. + /// The resolved property information, or if unsupported. + public CacheEntry(int version, ObservablePropertyInfo? info) => (Version, Info) = (version, info); + + /// + /// Gets the configuration version the entry was computed from. + /// + public int Version { get; } + + /// + /// Gets the resolved property information, or if unsupported. + /// + public ObservablePropertyInfo? Info { get; } + } + + /// + /// Describes an observable factory registration for a property, including its affinity. + /// + internal sealed record ObservablePropertyInfo { - public ObservablePropertyInfo(int affinity, Func>> createObservable) => + /// + /// Initializes a new instance of the class. + /// + /// The affinity score for the registration. + /// The factory for creating the observable for this property. + public ObservablePropertyInfo( + int affinity, + Func>> createObservable) => (Affinity, CreateObservable) = (affinity, createObservable); + /// + /// Gets the affinity score for the registration. + /// public int Affinity { get; } + /// + /// Gets the observable factory for the registration. + /// public Func>> CreateObservable { get; } } } diff --git a/src/ReactiveUI/Platforms/apple-common/ReactiveControl.cs b/src/ReactiveUI/Platforms/apple-common/ReactiveControl.cs index 3f053c6c61..9a44b28620 100644 --- a/src/ReactiveUI/Platforms/apple-common/ReactiveControl.cs +++ b/src/ReactiveUI/Platforms/apple-common/ReactiveControl.cs @@ -21,10 +21,6 @@ namespace ReactiveUI; /// This is a UIControl that is both and UIControl and has a ReactiveObject powers /// (i.e. you can call RaiseAndSetIfChanged). /// -#if NET6_0_OR_GREATER -[RequiresDynamicCode("ReactiveControl inherits from ReactiveObject which uses extension methods that require dynamic code generation")] -[RequiresUnreferencedCode("ReactiveControl inherits from ReactiveObject which uses extension methods that may require unreferenced code")] -#endif public class ReactiveControl : UIControl, IReactiveNotifyPropertyChanged, IHandleObservableErrors, IReactiveObject, ICanActivate, ICanForceManualActivation { private readonly Subject _deactivated = new(); @@ -160,10 +156,6 @@ protected override void Dispose(bool disposing) /// /// The view model type. [SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:FileMayOnlyContainASingleType", Justification = "Classes with the same class names within.")] -#if NET6_0_OR_GREATER -[RequiresDynamicCode("ReactiveControl inherits from ReactiveObject which uses extension methods that require dynamic code generation")] -[RequiresUnreferencedCode("ReactiveControl inherits from ReactiveObject which uses extension methods that may require unreferenced code")] -#endif public abstract class ReactiveControl : ReactiveControl, IViewFor where TViewModel : class { diff --git a/src/ReactiveUI/Platforms/apple-common/ReactiveImageView.cs b/src/ReactiveUI/Platforms/apple-common/ReactiveImageView.cs index 5c8d7cc482..650dc121fd 100644 --- a/src/ReactiveUI/Platforms/apple-common/ReactiveImageView.cs +++ b/src/ReactiveUI/Platforms/apple-common/ReactiveImageView.cs @@ -21,10 +21,6 @@ namespace ReactiveUI; /// This is an ImageView that is both and ImageView and has a ReactiveObject powers /// (i.e. you can call RaiseAndSetIfChanged). /// -#if NET6_0_OR_GREATER -[RequiresDynamicCode("ReactiveImageView inherits from ReactiveObject which uses extension methods that require dynamic code generation")] -[RequiresUnreferencedCode("ReactiveImageView inherits from ReactiveObject which uses extension methods that may require unreferenced code")] -#endif public abstract class ReactiveImageView : NSImageView, IReactiveNotifyPropertyChanged, IHandleObservableErrors, IReactiveObject, ICanActivate, ICanForceManualActivation { private readonly Subject _activated = new(); @@ -169,10 +165,6 @@ protected override void Dispose(bool disposing) /// /// The view model type. [SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:FileMayOnlyContainASingleType", Justification = "Classes with the same class names within.")] -#if NET6_0_OR_GREATER -[RequiresDynamicCode("ReactiveImageView inherits from ReactiveObject which uses extension methods that require dynamic code generation")] -[RequiresUnreferencedCode("ReactiveImageView inherits from ReactiveObject which uses extension methods that may require unreferenced code")] -#endif public abstract class ReactiveImageView : ReactiveImageView, IViewFor where TViewModel : class { diff --git a/src/ReactiveUI/Platforms/apple-common/ReactiveSplitViewController.cs b/src/ReactiveUI/Platforms/apple-common/ReactiveSplitViewController.cs index f1ed531ce0..2e514b3a51 100644 --- a/src/ReactiveUI/Platforms/apple-common/ReactiveSplitViewController.cs +++ b/src/ReactiveUI/Platforms/apple-common/ReactiveSplitViewController.cs @@ -17,10 +17,6 @@ namespace ReactiveUI; /// This is a View that is both a NSSplitViewController and has ReactiveObject powers /// (i.e. you can call RaiseAndSetIfChanged). /// -#if NET6_0_OR_GREATER -[RequiresDynamicCode("ReactiveSplitViewController inherits from ReactiveObject which uses extension methods that require dynamic code generation")] -[RequiresUnreferencedCode("ReactiveSplitViewController inherits from ReactiveObject which uses extension methods that may require unreferenced code")] -#endif public abstract class ReactiveSplitViewController : NSSplitViewController, IReactiveNotifyPropertyChanged, IHandleObservableErrors, IReactiveObject, ICanActivate { private readonly Subject _activated = new(); @@ -162,10 +158,6 @@ protected override void Dispose(bool disposing) /// /// The view model type. [SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:FileMayOnlyContainASingleType", Justification = "Classes with the same class names within.")] -#if NET6_0_OR_GREATER -[RequiresDynamicCode("ReactiveSplitViewController inherits from ReactiveObject which uses extension methods that require dynamic code generation")] -[RequiresUnreferencedCode("ReactiveSplitViewController inherits from ReactiveObject which uses extension methods that may require unreferenced code")] -#endif public abstract class ReactiveSplitViewController : ReactiveSplitViewController, IViewFor where TViewModel : class { diff --git a/src/ReactiveUI/Platforms/apple-common/ReactiveView.cs b/src/ReactiveUI/Platforms/apple-common/ReactiveView.cs index b2201a7552..74f482d503 100644 --- a/src/ReactiveUI/Platforms/apple-common/ReactiveView.cs +++ b/src/ReactiveUI/Platforms/apple-common/ReactiveView.cs @@ -19,10 +19,6 @@ namespace ReactiveUI; /// This is a View that is both a NSView and has ReactiveObject powers /// (i.e. you can call RaiseAndSetIfChanged). /// -#if NET6_0_OR_GREATER -[RequiresDynamicCode("ReactiveView inherits from ReactiveObject which uses extension methods that require dynamic code generation")] -[RequiresUnreferencedCode("ReactiveView inherits from ReactiveObject which uses extension methods that may require unreferenced code")] -#endif public class ReactiveView : NSView, IReactiveNotifyPropertyChanged, IHandleObservableErrors, IReactiveObject, ICanActivate, ICanForceManualActivation { private readonly Subject _activated = new(); @@ -151,10 +147,6 @@ protected override void Dispose(bool disposing) /// /// The view model type. [SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:FileMayOnlyContainASingleType", Justification = "Classes with the same class names within.")] -#if NET6_0_OR_GREATER -[RequiresDynamicCode("ReactiveView inherits from ReactiveObject which uses extension methods that require dynamic code generation")] -[RequiresUnreferencedCode("ReactiveView inherits from ReactiveObject which uses extension methods that may require unreferenced code")] -#endif public abstract class ReactiveView : ReactiveView, IViewFor where TViewModel : class { diff --git a/src/ReactiveUI/Platforms/apple-common/ReactiveViewController.cs b/src/ReactiveUI/Platforms/apple-common/ReactiveViewController.cs index d37fb227fb..d4ef0cf6be 100644 --- a/src/ReactiveUI/Platforms/apple-common/ReactiveViewController.cs +++ b/src/ReactiveUI/Platforms/apple-common/ReactiveViewController.cs @@ -17,10 +17,6 @@ namespace ReactiveUI; /// This is a View that is both a NSViewController and has ReactiveObject powers /// (i.e. you can call RaiseAndSetIfChanged). /// -#if NET6_0_OR_GREATER -[RequiresDynamicCode("ReactiveViewController uses ReactiveUI extension methods which require dynamic code generation")] -[RequiresUnreferencedCode("ReactiveViewController uses ReactiveUI extension methods which may require unreferenced code")] -#endif public class ReactiveViewController : NSViewController, IReactiveNotifyPropertyChanged, IHandleObservableErrors, IReactiveObject, ICanActivate { private readonly Subject _activated = new(); @@ -159,10 +155,6 @@ protected override void Dispose(bool disposing) /// /// The view model type. [SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:FileMayOnlyContainASingleType", Justification = "Classes with the same class names within.")] -#if NET6_0_OR_GREATER -[RequiresDynamicCode("ReactiveViewController uses ReactiveUI extension methods which require dynamic code generation")] -[RequiresUnreferencedCode("ReactiveViewController uses ReactiveUI extension methods which may require unreferenced code")] -#endif public abstract class ReactiveViewController : ReactiveViewController, IViewFor where TViewModel : class { diff --git a/src/ReactiveUI/Platforms/apple-common/TargetActionCommandBinder.cs b/src/ReactiveUI/Platforms/apple-common/TargetActionCommandBinder.cs index 04497dcea3..7b6dee8935 100644 --- a/src/ReactiveUI/Platforms/apple-common/TargetActionCommandBinder.cs +++ b/src/ReactiveUI/Platforms/apple-common/TargetActionCommandBinder.cs @@ -3,7 +3,12 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. +#nullable enable + +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; using System.Reflection; +using System.Runtime.CompilerServices; using System.Windows.Input; using Foundation; @@ -19,140 +24,338 @@ namespace ReactiveUI; /// -/// TargetActionCommandBinder is an implementation of command binding that -/// understands Cocoa's Target / Action Framework. Many controls in Cocoa -/// that are effectively command sources (i.e. Buttons, Menus, etc), -/// participate in this framework. +/// An implementation that binds commands using Cocoa's +/// Target/Action mechanism. /// -#if NET6_0_OR_GREATER -[RequiresDynamicCode("TargetActionCommandBinder uses reflection for property access and Objective-C runtime features which require dynamic code generation")] -[RequiresUnreferencedCode("TargetActionCommandBinder uses reflection for property access and Objective-C runtime features which may require unreferenced code")] -#endif +/// +/// +/// Many Cocoa controls (buttons, menu items, toolbar items, etc.) participate in the Target/Action pattern. +/// This binder sets the control's Target and (when present) Action properties to route UI +/// invocations to an . +/// +/// +/// Trimming/AOT: the Target/Action path reflects over an unknown runtime type to locate properties named +/// Target, Action, and optionally Enabled. This is not trimming-safe and is annotated accordingly. +/// Prefer the add/remove handler overloads on where applicable. +/// +/// public class TargetActionCommandBinder : ICreatesCommandBinding { - private readonly Type[] _validTypes; - +#if UIKIT /// - /// Initializes a new instance of the class. + /// The set of Cocoa types that are valid Target/Action hosts in UIKit builds. /// - public TargetActionCommandBinder() => -#if UIKIT - _validTypes = - [ - typeof(UIControl), - ]; + private static readonly Type[] ValidTypes = [typeof(UIControl)]; #else - _validTypes = - [ - typeof(NSControl), - typeof(NSCell), - typeof(NSMenu), - typeof(NSMenuItem), - typeof(NSToolbarItem), - ]; + /// + /// The set of Cocoa types that are valid Target/Action hosts in AppKit builds. + /// + private static readonly Type[] ValidTypes = + [ + typeof(NSControl), + typeof(NSCell), + typeof(NSMenu), + typeof(NSMenuItem), + typeof(NSToolbarItem), + ]; #endif + /// + /// Cache of runtime property setters used to apply Target/Action/Enabled on Cocoa objects. + /// + /// + /// + /// This is stable type metadata and is therefore cached indefinitely. Eviction provides no value here and + /// would re-trigger reflection and setter generation. + /// + /// + /// A Action setter indicates the type does not expose an Action property. + /// An Enabled setter is optional. + /// + /// + private static readonly ConcurrentDictionary PropertySetterCache = new(); + /// - public int GetAffinityForObject(Type type, bool hasEventTarget) + public int GetAffinityForObject<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicEvents | DynamicallyAccessedMemberTypes.PublicProperties)] T>(bool hasEventTarget) { - if (!_validTypes.Any(x => x.IsAssignableFrom(type))) + if (hasEventTarget) { return 0; } - return !hasEventTarget ? 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 - { - if (!_validTypes.Any(static x => x.IsAssignableFrom(typeof(T)))) + var t = typeof(T); + for (var i = 0; i < ValidTypes.Length; i++) { - return 0; + if (ValidTypes[i].IsAssignableFrom(t)) + { + return 4; + } } - return !hasEventTarget ? 4 : 0; + return 0; } /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("BindCommandToObject uses Reflection.GetValueSetterOrThrow and GetValueSetterForProperty which require dynamic code generation")] - [RequiresUnreferencedCode("BindCommandToObject uses Reflection.GetValueSetterOrThrow and GetValueSetterForProperty which may require unreferenced code")] -#endif - public IDisposable? BindCommandToObject(ICommand? command, object? target, IObservable commandParameter) + /// + /// + /// This binds via Target/Action, not via a .NET event. It requires that the runtime type exposes a Target + /// property and that a selector named theAction: can be invoked on the target. + /// + /// + /// If the runtime type also exposes an Enabled property, it is synchronized with + /// and . + /// + /// + [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 commandParameter) + where T : class { - ArgumentExceptionHelper.ThrowIfNull(command); ArgumentExceptionHelper.ThrowIfNull(target); - commandParameter ??= Observable.Return(target); + // Match other binders: null command means "no binding". + if (command is null) + { + return Disposable.Empty; + } + + commandParameter ??= Observable.Return((object?)target); object? latestParam = null; - var ctlDelegate = new ControlDelegate( - _ => + + // Cocoa routes UI actions to a selector on the target; we provide a stable NSObject instance. + var ctlDelegate = new ControlDelegate(static _ => { }) + { + // IsEnabled is used on AppKit to validate menu items; keep it aligned with CanExecute. + IsEnabled = command.CanExecute(null), + }; + + // Avoid capturing in the Export method; store the block on the delegate. + ctlDelegate.SetBlock(_ => + { + var param = Volatile.Read(ref latestParam); + if (command.CanExecute(param)) { - if (command!.CanExecute(latestParam)) - { - command.Execute(latestParam); - } - }) - { IsEnabled = command!.CanExecute(latestParam) }; + command.Execute(param); + } + }); - var sel = new Selector("theAction:"); + // Selector name must match [Export] on ControlDelegate. + var selector = new Selector("theAction:"); - // TODO how does this work? Is there an Action property? - Reflection.GetValueSetterOrThrow(target!.GetType().GetRuntimeProperty("Action"))?.Invoke(target, sel, null); + var runtimeType = target.GetType(); + var setters = PropertySetterCache.GetOrAdd(runtimeType, static t => BuildSetters(t)); - var targetSetter = Reflection.GetValueSetterOrThrow(target.GetType().GetRuntimeProperty("Target")); - targetSetter?.Invoke(target, ctlDelegate, null); - var actionDisp = Disposable.Create(() => targetSetter?.Invoke(target, null, null)); + // Apply Action (if present) and Target (required). + setters.ActionSetter?.Invoke(target, selector, null); + setters.TargetSetter.Invoke(target, ctlDelegate, null); - var enabledSetter = Reflection.GetValueSetterForProperty(target.GetType().GetRuntimeProperty("Enabled")); - if (enabledSetter is null) + // Ensure we always detach target (and action if applicable) on dispose. + var detach = Disposable.Create(() => { - return actionDisp; + // Clear Target first to stop invocation, then clear Action (if available). + setters.TargetSetter.Invoke(target, null, null); + setters.ActionSetter?.Invoke(target, null, null); + }); + + // If Enabled isn't supported, binding is complete. + if (setters.EnabledSetter is null) + { + // Still track parameters so command execution uses the latest, but do not attempt Enabled sync. + return new CompositeDisposable( + detach, + commandParameter.Subscribe(x => Volatile.Write(ref latestParam, x))); } - // initial enabled state - enabledSetter(target, command.CanExecute(latestParam), null); + // Initial enabled state. + setters.EnabledSetter.Invoke(target, command.CanExecute(Volatile.Read(ref latestParam)), null); + ctlDelegate.IsEnabled = command.CanExecute(Volatile.Read(ref latestParam)); - return new CompositeDisposable( - actionDisp, - commandParameter.Subscribe(x => latestParam = x), - Observable.FromEvent( + // Keep Enabled (and AppKit validate) in sync with CanExecuteChanged. + var canExecuteChangedSub = Observable.FromEvent( eventHandler => { - void Handler(object? sender, EventArgs e) => eventHandler(command.CanExecute(latestParam)); + void Handler(object? s, EventArgs e) => + eventHandler(command.CanExecute(Volatile.Read(ref latestParam))); return Handler; }, - x => command.CanExecuteChanged += x, - x => command.CanExecuteChanged -= x) - .Subscribe(x => - { - enabledSetter(target, x, null); - ctlDelegate.IsEnabled = x; - })); + h => command.CanExecuteChanged += h, + h => command.CanExecuteChanged -= h) + .Subscribe(x => + { + setters.EnabledSetter.Invoke(target, x, null); + ctlDelegate.IsEnabled = x; + }); + + return new CompositeDisposable( + detach, + commandParameter.Subscribe(x => Volatile.Write(ref latestParam, x)), + canExecuteChangedSub); } /// - public IDisposable BindCommandToObject(ICommand? command, object? target, IObservable commandParameter, string eventName) - where TEventArgs : EventArgs => throw new NotImplementedException(); + /// + /// This overload binds to a named .NET event. It is reflection-based and therefore not trimming-safe. + /// Prefer the add/remove handler overload when you can supply delegates. + /// + [RequiresUnreferencedCode("String/reflection-based event binding may require members removed by trimming.")] + public IDisposable? BindCommandToObject( + ICommand? command, + T? target, + IObservable commandParameter, + string eventName) + where T : class + { + ArgumentExceptionHelper.ThrowIfNull(target); + + if (command is null) + { + return Disposable.Empty; + } + + ArgumentExceptionHelper.ThrowIfNull(eventName); - private class ControlDelegate(Action block) : NSObject + commandParameter ??= Observable.Return((object?)target); + + object? latestParam = null; + + // Stable handler for deterministic unsubscription is provided by Rx's FromEventPattern. + var evt = Observable.FromEventPattern(target, eventName); + + var paramSub = commandParameter.Subscribe(x => Volatile.Write(ref latestParam, x)); + var evtSub = evt.Subscribe(_ => + { + var param = Volatile.Read(ref latestParam); + if (command.CanExecute(param)) + { + command.Execute(param); + } + }); + + return new CompositeDisposable(paramSub, evtSub); + } + + /// + /// + /// This overload is fully AOT-compatible and should be preferred when an explicit event subscription API is available. + /// + public IDisposable? BindCommandToObject<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicEvents | DynamicallyAccessedMemberTypes.NonPublicEvents)] T, TEventArgs>( + ICommand? command, + T? target, + IObservable commandParameter, + Action> addHandler, + Action> removeHandler) + where T : class + where TEventArgs : EventArgs { - private readonly Action _block = block; + ArgumentExceptionHelper.ThrowIfNull(target); + ArgumentNullException.ThrowIfNull(addHandler); + ArgumentNullException.ThrowIfNull(removeHandler); + if (command is null) + { + return Disposable.Empty; + } + + commandParameter ??= Observable.Return((object?)target); + + object? latestParam = null; + + void Handler(object? sender, TEventArgs e) + { + var param = Volatile.Read(ref latestParam); + if (command.CanExecute(param)) + { + command.Execute(param); + } + } + + var paramSub = commandParameter.Subscribe(x => Volatile.Write(ref latestParam, x)); + addHandler(Handler); + + return new CompositeDisposable( + paramSub, + Disposable.Create(() => removeHandler(Handler))); + } + + /// + /// Creates and caches property setters required for Target/Action binding on the specified runtime type. + /// + /// The runtime type to inspect. + /// The cached setter bundle. + /// Thrown when the runtime type does not expose a required Target property. + [RequiresUnreferencedCode("Cocoa Target/Action binding reflects over runtime types to locate properties that may be removed by trimming.")] + private static Setters BuildSetters(Type type) + { + var actionProp = type.GetRuntimeProperty("Action"); + var targetProp = type.GetRuntimeProperty("Target"); + var enabledProp = type.GetRuntimeProperty("Enabled"); + + if (targetProp is null) + { + throw new InvalidOperationException( + $"Target property is required for {nameof(TargetActionCommandBinder)} on type {type.FullName}."); + } + + return new Setters( + ActionSetter: actionProp is not null ? Reflection.GetValueSetterOrThrow(actionProp) : null, + TargetSetter: Reflection.GetValueSetterOrThrow(targetProp), + EnabledSetter: enabledProp is not null ? Reflection.GetValueSetterForProperty(enabledProp) : null); + } + + /// + /// Represents the set of cached setters required to wire Target/Action and optionally Enabled. + /// + private readonly record struct Setters( + Action? ActionSetter, + Action TargetSetter, + Action? EnabledSetter); + + /// + /// Delegate object installed as the Cocoa Target for the theAction: selector. + /// + /// + /// This object must remain alive for the binding lifetime; it is referenced by the bound control's Target. + /// + private sealed class ControlDelegate : NSObject + { + private Action _block; + + /// + /// Initializes a new instance of the class. + /// + /// The action invoked when the control fires the bound selector. + public ControlDelegate(Action block) => _block = block; + + /// + /// Gets or sets a value indicating whether the command is currently executable. + /// + /// + /// On AppKit, this is used by validateMenuItem: to control menu item enabled state. + /// public bool IsEnabled { get; set; } + /// + /// Replaces the action invoked by . + /// + /// The new block to invoke. + public void SetBlock(Action block) => _block = block; + + /// + /// Selector invoked by Cocoa controls for Target/Action. + /// + /// The sender object. [Export("theAction:")] public void TheAction(NSObject sender) => _block(sender); #if !UIKIT + /// + /// AppKit menu item validation hook used to enable/disable menu items. + /// + /// The menu item being validated. + /// if the item should be enabled; otherwise . [Export("validateMenuItem:")] public bool ValidateMenuItem(NSMenuItem menuItem) => IsEnabled; #endif diff --git a/src/ReactiveUI/Platforms/apple-common/ViewModelViewHost.cs b/src/ReactiveUI/Platforms/apple-common/ViewModelViewHost.cs index ccb9c7943a..4432c70f4f 100644 --- a/src/ReactiveUI/Platforms/apple-common/ViewModelViewHost.cs +++ b/src/ReactiveUI/Platforms/apple-common/ViewModelViewHost.cs @@ -42,17 +42,51 @@ namespace ReactiveUI; /// ]]> /// /// -#if NET6_0_OR_GREATER -[RequiresDynamicCode("ViewModelViewHost uses ReactiveUI extension methods and RxApp properties which require dynamic code generation")] -[RequiresUnreferencedCode("ViewModelViewHost uses ReactiveUI extension methods and RxApp properties which may require unreferenced code")] -#endif +[RequiresUnreferencedCode("This class 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 class ViewModelViewHost : ReactiveViewController { + /// + /// Tracks the currently-adopted view controller and ensures it is disowned on replacement or disposal. + /// private readonly SerialDisposable _currentView; - private readonly ObservableAsPropertyHelper _viewContract; + + /// + /// Holds subscriptions created during initialization. + /// + private readonly CompositeDisposable _subscriptions; + + /// + /// Holds the subscription to (the inner observable) and swaps it when the + /// property changes. + /// + [SuppressMessage("Usage", "CA2213:Disposable fields should be disposed", Justification = "Disposed by _subscriptions")] + private readonly SerialDisposable _viewContractObservableSubscription; + + /// + /// Backing field for . This is updated by observing + /// and is raised as a property change for bindings. + /// + private string? _viewContract; + + /// + /// Backing field for . + /// private IViewLocator? _viewLocator; + + /// + /// Backing field for . + /// private NSViewController? _defaultContent; + + /// + /// Backing field for . + /// private object? _viewModel; + + /// + /// Backing field for . + /// private IObservable? _viewContractObservable; /// @@ -61,9 +95,17 @@ public class ViewModelViewHost : ReactiveViewController public ViewModelViewHost() { _currentView = new SerialDisposable(); - _viewContract = this - .WhenAnyObservable(static x => x.ViewContractObservable) - .ToProperty(this, static x => x.ViewContract, initialValue: null, scheduler: RxSchedulers.MainThreadScheduler); + _subscriptions = new CompositeDisposable(); + _viewContractObservableSubscription = new SerialDisposable(); + + // Drive ViewContract from ViewContractObservable without WhenAny*/expression trees (AOT-trimmer friendly). + // We always publish an initial null contract to preserve the original StartWith(null) behavior. + var contractStream = CreateViewContractStream() + .ObserveOn(RxSchedulers.MainThreadScheduler) + .Subscribe(SetViewContract); + + _subscriptions.Add(contractStream); + _subscriptions.Add(_viewContractObservableSubscription); Initialize(); } @@ -112,7 +154,7 @@ public IObservable? ViewContractObservable /// public string? ViewContract { - get => _viewContract.Value; + get => _viewContract; set => ViewContractObservable = Observable.Return(value); } @@ -123,11 +165,18 @@ protected override void Dispose(bool disposing) if (disposing) { + _subscriptions.Dispose(); _currentView.Dispose(); - _viewContract.Dispose(); } } + /// + /// Adds as a child controller of and ensures its view fills + /// the parent bounds. + /// + /// The parent controller. + /// The child controller to adopt. + /// Thrown when the parent's view is . private static void Adopt(NSViewController parent, NSViewController? child) { ArgumentExceptionHelper.ThrowIfNull(parent); @@ -174,6 +223,11 @@ private static void Adopt(NSViewController parent, NSViewController? child) #endif } + /// + /// Removes from its parent controller and removes its view from the view hierarchy. + /// + /// The child controller to disown. + /// Thrown when the child's view is . private static void Disown(NSViewController child) { if (child.View is null) @@ -188,57 +242,137 @@ private static void Disown(NSViewController child) child.RemoveFromParentViewController(); } + /// + /// Initializes reactive subscriptions that drive view resolution and controller swapping. + /// + [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 Initialize() { - var viewChange = this.WhenAnyValue(nameof(ViewModel)) - .CombineLatest( - this.WhenAnyObservable(x => x.ViewContractObservable).StartWith((string?)null), - (vm, contract) => new { ViewModel = vm, Contract = contract }) - .Where(x => x.ViewModel is not null); - - var defaultViewChange = this.WhenAnyValue(nameof(ViewModel)) - .CombineLatest( - this.WhenAnyValue(nameof(DefaultContent)), - (vm, defaultContent) => new { ViewModel = vm, DefaultContent = defaultContent }) - .Where(x => x.ViewModel is null && x.DefaultContent is not null) - .Select(x => x.DefaultContent); - - viewChange - .ObserveOn(RxSchedulers.MainThreadScheduler) - .Subscribe( - x => - { - var viewLocator = ViewLocator ?? ReactiveUI.ViewLocator.Current; - var view = viewLocator.ResolveView(x.ViewModel, x.Contract); - - if (view is null) + var viewModelChanges = ObserveProperty(static x => x.ViewModel, nameof(ViewModel)); + var defaultContentChanges = ObserveProperty(static x => x.DefaultContent, nameof(DefaultContent)); + var contractChanges = ObserveProperty(static x => x.ViewContract, nameof(ViewContract)); + + var viewChange = + viewModelChanges + .CombineLatest( + contractChanges, + static (vm, contract) => new { ViewModel = vm, Contract = contract }) + .Where(static x => x.ViewModel is not null); + + var defaultViewChange = + viewModelChanges + .CombineLatest( + defaultContentChanges, + static (vm, defaultContent) => new { ViewModel = vm, DefaultContent = defaultContent }) + .Where(static x => x.ViewModel is null && x.DefaultContent is not null) + .Select(static x => x.DefaultContent); + + _subscriptions.Add( + viewChange + .ObserveOn(RxSchedulers.MainThreadScheduler) + .Subscribe( + x => { - var message = $"Unable to resolve view for \"{x.ViewModel?.GetType()}\""; + var viewLocator = ViewLocator ?? ReactiveUI.ViewLocator.Current; + var view = viewLocator.ResolveView(x.ViewModel, x.Contract); - if (x.Contract is not null) + if (view is null) { - message += $" and contract \"{x.Contract.GetType()}\""; + var message = $"Unable to resolve view for \"{x.ViewModel?.GetType()}\""; + + if (x.Contract is not null) + { + message += $" and contract \"{x.Contract.GetType()}\""; + } + + message += "."; + throw new Exception(message); } - message += "."; - throw new Exception(message); - } + if (view is not NSViewController viewController) + { + //// TODO: As viewController may be NULL at this point this execution will never show the FullName, find fixed text to replace this with. + throw new Exception($"Resolved view type '{view?.GetType().FullName}' is not a '{typeof(NSViewController).FullName}'."); + } - if (view is not NSViewController viewController) - { - //// TODO: As viewController may be NULL at this point this execution will never show the FullName, find fixed text to replace this with. + view.ViewModel = x.ViewModel; + Adopt(this, viewController); + + _currentView.Disposable = + new CompositeDisposable( + viewController, + Disposable.Create(() => Disown(viewController))); + })); + + _subscriptions.Add( + defaultViewChange + .ObserveOn(RxSchedulers.MainThreadScheduler) + .Subscribe(x => Adopt(this, x))); + } - throw new Exception($"Resolved view type '{view?.GetType().FullName}' is not a '{typeof(NSViewController).FullName}'."); - } + /// + /// Creates a contract stream that (1) emits an initial value, (2) subscribes to the current + /// , and (3) swaps the inner subscription when the property changes. + /// + /// An observable of view contracts. + private IObservable CreateViewContractStream() + { + return Observable.Create( + observer => + { + // Preserve the previous StartWith((string?)null) semantics. + observer.OnNext(null); - view.ViewModel = x.ViewModel; - Adopt(this, viewController); + void SwapInner(IObservable? source) + { + _viewContractObservableSubscription.Disposable = + source is null + ? Disposable.Empty + : source.Subscribe(observer); + } + + // Subscribe to the initial observable (if any). + SwapInner(ViewContractObservable); + + // Listen for property changes and rewire the inner subscription. + var outerSubscription = + Changed + .Where(static e => e.PropertyName == nameof(ViewContractObservable)) + .Subscribe(_ => SwapInner(ViewContractObservable)); + + return new CompositeDisposable(outerSubscription); + }); + } - _currentView.Disposable = (CompositeDisposable?)[viewController, Disposable.Create(() => Disown(viewController))]; - }); + /// + /// Observes changes to a property without using WhenAny* APIs (avoids RUC/RDC from expression-based pipelines). + /// The observable emits the current value immediately and then emits on each subsequent property change. + /// + /// The property type. + /// A getter for the property value. + /// The name of the property to observe. + /// An observable that emits the property value. + private IObservable ObserveProperty(Func getter, string propertyName) + { + return Observable.Create( + observer => + { + observer.OnNext(getter(this)); + + return Changed + .Where(e => e.PropertyName == propertyName) + .Select(_ => getter(this)) + .Subscribe(observer); + }); + } - defaultViewChange - .ObserveOn(RxSchedulers.MainThreadScheduler) - .Subscribe(x => Adopt(this, x)); + /// + /// Updates the backing field and raises property changed notifications. + /// + /// The new contract value. + private void SetViewContract(string? contract) + { + this.RaiseAndSetIfChanged(ref _viewContract, contract, nameof(ViewContract)); } } diff --git a/src/ReactiveUI/Platforms/ios/UIKitCommandBinders.cs b/src/ReactiveUI/Platforms/ios/UIKitCommandBinders.cs index 3ee442e20e..a0a6447c9c 100644 --- a/src/ReactiveUI/Platforms/ios/UIKitCommandBinders.cs +++ b/src/ReactiveUI/Platforms/ios/UIKitCommandBinders.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. +#nullable enable + using System.Reflection; using UIKit; @@ -12,26 +14,66 @@ namespace ReactiveUI; /// /// UI Kit command binder platform registrations. /// -/// [Preserve(AllMembers = true)] -[RequiresUnreferencedCode("UIKitCommandBinders uses methods that may require unreferenced code")] -[RequiresDynamicCode("UIKitCommandBinders uses methods that may require unreferenced code")] -public class UIKitCommandBinders : FlexibleCommandBinder +public sealed class UIKitCommandBinders : FlexibleCommandBinder { - private const string Enabled = nameof(Enabled); + /// + /// The reflected property name used to control enabled state across UIKit types. + /// + private const string EnabledPropertyName = "Enabled"; + + /// + /// Cached for . + /// + private static readonly PropertyInfo UIControlEnabledProperty = + typeof(UIControl).GetRuntimeProperty(EnabledPropertyName) ?? + throw new InvalidOperationException("There is no Enabled property on UIControl which is needed for binding."); + + /// + /// Cached for . + /// + private static readonly PropertyInfo UIRefreshControlEnabledProperty = + typeof(UIRefreshControl).GetRuntimeProperty(EnabledPropertyName) ?? + throw new InvalidOperationException("There is no Enabled property on UIRefreshControl which is needed for binding."); + + /// + /// Cached for . + /// + private static readonly PropertyInfo UIBarButtonItemEnabledProperty = + typeof(UIBarButtonItem).GetRuntimeProperty(EnabledPropertyName) ?? + throw new InvalidOperationException("There is no Enabled property on UIBarButtonItem which is needed for binding."); /// /// Initializes a new instance of the class. /// public UIKitCommandBinders() { - Register(typeof(UIControl), 9, static (cmd, t, cp) => ForTargetAction(cmd, t, cp, typeof(UIControl).GetRuntimeProperty(Enabled) ?? throw new InvalidOperationException("There is no Enabled property on the UIControl which is needed for binding."))); - Register(typeof(UIRefreshControl), 10, static (cmd, t, cp) => ForEvent(cmd, t, cp, "ValueChanged", typeof(UIRefreshControl).GetRuntimeProperty(Enabled) ?? throw new InvalidOperationException("There is no Enabled property on the UIRefreshControl which is needed for binding."))); - Register(typeof(UIBarButtonItem), 10, static (cmd, t, cp) => ForEvent(cmd, t, cp, "Clicked", typeof(UIBarButtonItem).GetRuntimeProperty(Enabled) ?? throw new InvalidOperationException("There is no Enabled property on the UIBarButtonItem which is needed for binding."))); + // UIControl: prefer the AOT-safe target-action helper (no string event name). + Register(typeof(UIControl), 9, static (cmd, t, cp) => ForTargetAction(cmd, t, cp, UIControlEnabledProperty)); + + // UIRefreshControl: ValueChanged is a .NET event; use the AOT-safe ForEvent overload via add/remove delegates. + Register(typeof(UIRefreshControl), 10, (cmd, t, cp) => + ForEvent( + cmd, + (UIRefreshControl)t!, + cp, + addHandler: h => ((UIRefreshControl)t!).ValueChanged += h, // see note below + removeHandler: h => ((UIRefreshControl)t!).ValueChanged -= h, + UIRefreshControlEnabledProperty)); + + // UIBarButtonItem: Clicked is a .NET event; use the AOT-safe ForEvent overload via add/remove delegates. + Register(typeof(UIBarButtonItem), 10, (cmd, t, cp) => + ForEvent( + cmd, + (UIBarButtonItem)t!, + cp, + addHandler: h => ((UIBarButtonItem)t!).Clicked += h, + removeHandler: h => ((UIBarButtonItem)t!).Clicked -= h, + UIBarButtonItemEnabledProperty)); } /// - /// Gets the UIKitCommandBinders instance. + /// Gets a lazily-initialized singleton instance of . /// public static Lazy Instance { get; } = new(); } diff --git a/src/ReactiveUI/Platforms/ios/UIKitObservableForProperty.cs b/src/ReactiveUI/Platforms/ios/UIKitObservableForProperty.cs index b944c2bccd..d99152b9a9 100644 --- a/src/ReactiveUI/Platforms/ios/UIKitObservableForProperty.cs +++ b/src/ReactiveUI/Platforms/ios/UIKitObservableForProperty.cs @@ -8,43 +8,106 @@ namespace ReactiveUI; /// -/// UIKitObservableForProperty is an object that knows how to -/// create notifications for a given type of object. Implement this if you -/// are porting RxUI to a new UI toolkit, or generally want to enable WhenAny -/// for another type of object that can be observed in a unique way. +/// UIKitObservableForProperty provides toolkit-specific observable factories used by ReactiveUI +/// to generate change notifications for UIKit controls in WhenAny* and related operators. /// +/// +/// This implementation registers observable factories for common UIKit properties that change via +/// control events or notifications. +/// +/// For event-based notifications, this implementation uses explicit add/remove handler overloads +/// (non-reflection) provided by to improve performance and +/// trimming/AOT compatibility. +/// [Preserve] -public class UIKitObservableForProperty : ObservableForPropertyBase +public sealed class UIKitObservableForProperty : ObservableForPropertyBase { /// /// Initializes a new instance of the class. /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("UIKitObservableForProperty uses methods that require dynamic code generation")] - [RequiresUnreferencedCode("UIKitObservableForProperty uses methods that may require unreferenced code")] -#endif public UIKitObservableForProperty() { - Register(typeof(UIControl), "Value", 20, static (s, p) => ObservableFromUIControlEvent(s, p, UIControlEvent.ValueChanged)); - Register(typeof(UITextField), "Text", 30, static (s, p) => ObservableFromNotification(s, p, UITextField.TextFieldTextDidChangeNotification)); - Register(typeof(UITextView), "Text", 30, static (s, p) => ObservableFromNotification(s, p, UITextView.TextDidChangeNotification)); - Register(typeof(UIDatePicker), "Date", 30, static (s, p) => ObservableFromUIControlEvent(s, p, UIControlEvent.ValueChanged)); - Register(typeof(UISegmentedControl), "SelectedSegment", 30, static (s, p) => ObservableFromUIControlEvent(s, p, UIControlEvent.ValueChanged)); - Register(typeof(UISwitch), "On", 30, static (s, p) => ObservableFromUIControlEvent(s, p, UIControlEvent.ValueChanged)); - Register(typeof(UISegmentedControl), "SelectedSegment", 30, static (s, p) => ObservableFromUIControlEvent(s, p, UIControlEvent.ValueChanged)); - - // Warning: This will stomp the Control's delegate - Register(typeof(UITabBar), "SelectedItem", 30, static (s, p) => ObservableFromEvent(s, p, "ItemSelected")); - - // Warning: This will stomp the Control's delegate - Register(typeof(UISearchBar), "Text", 30, static (s, p) => ObservableFromEvent(s, p, "TextChanged")); + // UIControl "Value" changes via ValueChanged. + Register( + typeof(UIControl), + "Value", + affinity: 20, + static (sender, expr) => ObservableFromUIControlEvent(sender, expr, UIControlEvent.ValueChanged)); + + // UITextField "Text" changes via notification. + Register( + typeof(UITextField), + "Text", + affinity: 30, + static (sender, expr) => ObservableFromNotification(sender, expr, UITextField.TextFieldTextDidChangeNotification)); + + // UITextView "Text" changes via notification. + Register( + typeof(UITextView), + "Text", + affinity: 30, + static (sender, expr) => ObservableFromNotification(sender, expr, UITextView.TextDidChangeNotification)); + + // UIDatePicker "Date" changes via ValueChanged. + Register( + typeof(UIDatePicker), + "Date", + affinity: 30, + static (sender, expr) => ObservableFromUIControlEvent(sender, expr, UIControlEvent.ValueChanged)); + + // UISegmentedControl "SelectedSegment" changes via ValueChanged. + Register( + typeof(UISegmentedControl), + "SelectedSegment", + affinity: 30, + static (sender, expr) => ObservableFromUIControlEvent(sender, expr, UIControlEvent.ValueChanged)); + + // UISwitch "On" changes via ValueChanged. + Register( + typeof(UISwitch), + "On", + affinity: 30, + static (sender, expr) => ObservableFromUIControlEvent(sender, expr, UIControlEvent.ValueChanged)); + + // Warning: This event-based approach may change the control's behavior depending on external delegate usage. + // Use explicit add/remove to avoid reflection and trimming hazards. + Register( + typeof(UITabBar), + "SelectedItem", + affinity: 30, + (sender, expr) => + { + var tabBar = (UITabBar)sender; + return ObservableFromEvent( + tabBar, + expr, + addHandler: h => tabBar.ItemSelected += h, + removeHandler: h => tabBar.ItemSelected -= h); + }); + + // Warning: This event-based approach may change the control's behavior depending on external delegate usage. + // Use explicit add/remove to avoid reflection and trimming hazards. + Register( + typeof(UISearchBar), + "Text", + affinity: 30, + (sender, expr) => + { + var searchBar = (UISearchBar)sender; + return ObservableFromEvent( + searchBar, + expr, + addHandler: h => searchBar.TextChanged += h, + removeHandler: h => searchBar.TextChanged -= h); + }); } /// - /// Gets the UI Kit ObservableForProperty instance. + /// Gets the shared instance. /// -#if NET6_0_OR_GREATER - [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Deliberate>")] -#endif + /// + /// The instance is created lazily. Consumers typically register it with the service locator once + /// during application initialization. + /// public static Lazy Instance { get; } = new(); } diff --git a/src/ReactiveUI/Platforms/mac/AutoSuspendHelper.cs b/src/ReactiveUI/Platforms/mac/AutoSuspendHelper.cs index dd6ad2aa58..926cbc488b 100644 --- a/src/ReactiveUI/Platforms/mac/AutoSuspendHelper.cs +++ b/src/ReactiveUI/Platforms/mac/AutoSuspendHelper.cs @@ -3,6 +3,10 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + using AppKit; using Foundation; @@ -10,8 +14,9 @@ namespace ReactiveUI; /// -/// Bridges lifecycle notifications into on macOS. +/// Bridges lifecycle notifications into on macOS. /// +/// The concrete type. /// /// /// Instantiate this helper inside your to map DidFinishLaunching, @@ -30,8 +35,8 @@ namespace ReactiveUI; /// public override void DidFinishLaunching(NSNotification notification) /// { /// _suspensionHelper ??= new AutoSuspendHelper(this); -/// RxApp.SuspensionHost.CreateNewAppState = () => new ShellState(); -/// RxApp.SuspensionHost.SetupDefaultSuspendResume(new FileSuspensionDriver(AppStatePathProvider.Resolve())); +/// RxSuspension.SuspensionHost.CreateNewAppState = () => new ShellState(); +/// RxSuspension.SuspensionHost.SetupDefaultSuspendResume(new FileSuspensionDriver(AppStatePathProvider.Resolve())); /// base.DidFinishLaunching(notification); /// } /// } @@ -39,99 +44,143 @@ namespace ReactiveUI; /// /// /// -#if NET6_0_OR_GREATER -[RequiresDynamicCode("AutoSuspendHelper uses RxApp properties which require dynamic code generation")] -[RequiresUnreferencedCode("AutoSuspendHelper uses RxApp properties which may require unreferenced code")] -#endif -public class AutoSuspendHelper : IEnableLogger, IDisposable +public class AutoSuspendHelper<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods)] T> : IEnableLogger, IDisposable + where T : NSApplicationDelegate { + /// + /// Emits disposables used by subscribers to delimit persistence work. + /// private readonly Subject _shouldPersistState = new(); + + /// + /// Emits values to indicate the application is resuming from a prior persisted state. + /// private readonly Subject _isResuming = new(); + + /// + /// Emits values to indicate the application is becoming active again after being backgrounded/hidden. + /// private readonly Subject _isUnpausing = new(); + /// + /// Emits values to indicate an unexpected termination, prompting state invalidation. + /// + private readonly Subject _untimelyDemise = new(); + + /// + /// Cached handler so we can unsubscribe during . + /// + private readonly UnhandledExceptionEventHandler _unhandledExceptionHandler; + + /// + /// Tracks whether this instance has been disposed. + /// private bool _isDisposed; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The application delegate. - public AutoSuspendHelper(NSApplicationDelegate appDelegate) + /// + /// Thrown when is . + /// + /// + /// Thrown when required lifecycle methods are not declared on 's runtime type. + /// + public AutoSuspendHelper(T appDelegate) { - Reflection.ThrowIfMethodsNotOverloaded( - nameof(AutoSuspendHelper), - appDelegate, - nameof(ApplicationShouldTerminate), - nameof(DidFinishLaunching), - nameof(DidResignActive), - nameof(DidBecomeActive), - nameof(DidHide)); - - RxApp.SuspensionHost.IsLaunchingNew = Observable.Never; - RxApp.SuspensionHost.IsResuming = _isResuming; - RxApp.SuspensionHost.IsUnpausing = _isUnpausing; - RxApp.SuspensionHost.ShouldPersistState = _shouldPersistState; - - var untimelyDemise = new Subject(); - AppDomain.CurrentDomain.UnhandledException += (o, e) => - untimelyDemise.OnNext(Unit.Default); - - RxApp.SuspensionHost.ShouldInvalidateState = untimelyDemise; + ArgumentNullException.ThrowIfNull(appDelegate); + + // Developer-time guard. Cache the result per delegate runtime type to avoid repeated reflection. + EnsureMethodsNotOverloadedCached(); + + RxSuspension.SuspensionHost.IsLaunchingNew = Observable.Never; + RxSuspension.SuspensionHost.IsResuming = _isResuming; + RxSuspension.SuspensionHost.IsUnpausing = _isUnpausing; + RxSuspension.SuspensionHost.ShouldPersistState = _shouldPersistState; + + // Keep a stable delegate instance so we can unsubscribe on Dispose. + _unhandledExceptionHandler = (_, _) => _untimelyDemise.OnNext(Unit.Default); + AppDomain.CurrentDomain.UnhandledException += _unhandledExceptionHandler; + + RxSuspension.SuspensionHost.ShouldInvalidateState = _untimelyDemise; } /// - /// Applications the should terminate. + /// Handles the application termination request. /// - /// The sender. - /// The termination reply from the application. + /// The application instance requesting termination. + /// + /// to delay termination until persistence completes. + /// /// - /// Delays the OS shutdown until subscribers finish writing + /// Delays OS shutdown until subscribers finish writing /// , replying with /// once persistence completes. /// public NSApplicationTerminateReply ApplicationShouldTerminate(NSApplication sender) { + ThrowIfDisposed(); + + // Ensure the persist notification is emitted on the main thread, as callers typically interact with AppKit. RxSchedulers.MainThreadScheduler.Schedule(() => - _shouldPersistState.OnNext(Disposable.Create(() => - sender.ReplyToApplicationShouldTerminate(true)))); + _shouldPersistState.OnNext( + Disposable.Create(() => sender.ReplyToApplicationShouldTerminate(true)))); return NSApplicationTerminateReply.Later; } /// - /// Did finish launching. + /// Notifies the helper that the application finished launching. /// - /// The notification. + /// The launch notification. /// /// Signals so state drivers load the last persisted . /// - public void DidFinishLaunching(NSNotification notification) => _isResuming.OnNext(Unit.Default); + public void DidFinishLaunching(NSNotification notification) + { + ThrowIfDisposed(); + _isResuming.OnNext(Unit.Default); + } /// - /// Did resign active. + /// Notifies the helper that the application resigned active state. /// - /// The notification. + /// The resign-active notification. /// /// Requests an asynchronous save by emitting via . /// - public void DidResignActive(NSNotification notification) => _shouldPersistState.OnNext(Disposable.Empty); + public void DidResignActive(NSNotification notification) + { + ThrowIfDisposed(); + _shouldPersistState.OnNext(Disposable.Empty); + } /// - /// Did become active. + /// Notifies the helper that the application became active. /// - /// The notification. + /// The become-active notification. /// /// Signals so subscribers can refresh transient UI when the app regains focus. /// - public void DidBecomeActive(NSNotification notification) => _isUnpausing.OnNext(Unit.Default); + public void DidBecomeActive(NSNotification notification) + { + ThrowIfDisposed(); + _isUnpausing.OnNext(Unit.Default); + } /// - /// Did hide. + /// Notifies the helper that the application was hidden. /// - /// The notification. + /// The hide notification. /// /// Initiates a quick save when the app is hidden, mirroring the behavior of . /// - public void DidHide(NSNotification notification) => _shouldPersistState.OnNext(Disposable.Empty); + public void DidHide(NSNotification notification) + { + ThrowIfDisposed(); + _shouldPersistState.OnNext(Disposable.Empty); + } /// public void Dispose() @@ -141,9 +190,9 @@ public void Dispose() } /// - /// Disposes of resources inside the class. + /// Releases resources held by this helper. /// - /// If we are disposing managed resources. + /// If , disposes managed resources. protected virtual void Dispose(bool isDisposing) { if (_isDisposed) @@ -151,13 +200,104 @@ protected virtual void Dispose(bool isDisposing) return; } - if (isDisposing) + _isDisposed = true; + + if (!isDisposing) { - _isResuming?.Dispose(); - _isUnpausing?.Dispose(); - _shouldPersistState?.Dispose(); + return; } - _isDisposed = true; + // Unsubscribe first to avoid keeping this instance alive via the AppDomain event. + AppDomain.CurrentDomain.UnhandledException -= _unhandledExceptionHandler; + + _isResuming.Dispose(); + _isUnpausing.Dispose(); + _shouldPersistState.Dispose(); + _untimelyDemise.Dispose(); + } + + /// + /// Performs the "methods must be implemented" guard once per application delegate runtime type. + /// + /// + /// Thrown when required lifecycle methods are not declared on appDelegate's runtime type. + /// + /// + /// Delegates to on a cache miss, + /// but avoids repeating the reflection scan for each helper construction. + /// + private static void EnsureMethodsNotOverloadedCached() + { + var type = typeof(T); + + if (MethodForwardingValidationCache.IsValidated(type)) + { + return; + } + + Reflection.ThrowIfMethodsNotOverloaded( + nameof(AutoSuspendHelper), + type, + nameof(ApplicationShouldTerminate), + nameof(DidFinishLaunching), + nameof(DidResignActive), + nameof(DidBecomeActive), + nameof(DidHide)); + + MethodForwardingValidationCache.MarkValidated(type); + } + + /// + /// Throws if this instance has been disposed. + /// + /// Thrown when the instance is disposed. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ThrowIfDisposed() + { + if (_isDisposed) + { + throw new ObjectDisposedException(nameof(AutoSuspendHelper)); + } + } + + /// + /// Stores a process-wide cache of which delegate types have been validated for lifecycle forwarding. + /// + /// + /// Uses a single gate because this is cold-path initialization and the number of delegate types is tiny. + /// + private static class MethodForwardingValidationCache + { +#if NET9_0_OR_GREATER + private static readonly Lock Gate = new(); +#else + private static readonly object Gate = new(); +#endif + private static readonly Dictionary Validated = new(); + + /// + /// Returns whether the specified delegate type has been validated. + /// + /// The delegate runtime type. + /// if the type has been validated; otherwise . + public static bool IsValidated(Type type) + { + lock (Gate) + { + return Validated.ContainsKey(type); + } + } + + /// + /// Marks the specified delegate type as validated. + /// + /// The delegate runtime type. + public static void MarkValidated(Type type) + { + lock (Gate) + { + Validated[type] = 0; + } + } } } diff --git a/src/ReactiveUI/Platforms/mac/PlatformRegistrations.cs b/src/ReactiveUI/Platforms/mac/PlatformRegistrations.cs index a04b8590d0..30742a46de 100644 --- a/src/ReactiveUI/Platforms/mac/PlatformRegistrations.cs +++ b/src/ReactiveUI/Platforms/mac/PlatformRegistrations.cs @@ -12,22 +12,28 @@ namespace ReactiveUI; public class PlatformRegistrations : IWantsToRegisterStuff { /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("Platform registration uses ComponentModelTypeConverter and RxApp which require dynamic code generation")] - [RequiresUnreferencedCode("Platform registration uses ComponentModelTypeConverter and RxApp which 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); - registerFunction(static () => new PlatformOperations(), typeof(IPlatformOperations)); - registerFunction(static () => new ComponentModelTypeConverter(), typeof(IBindingTypeConverter)); - registerFunction(static () => new AppKitObservableForProperty(), typeof(ICreatesObservableForProperty)); - registerFunction(static () => new TargetActionCommandBinder(), typeof(ICreatesCommandBinding)); - registerFunction(static () => new DateTimeNSDateConverter(), typeof(IBindingTypeConverter)); - registerFunction(static () => new KVOObservableForProperty(), typeof(ICreatesObservableForProperty)); + registrar.RegisterConstant(static () => new PlatformOperations()); + registrar.RegisterConstant(static () => new ComponentModelFallbackConverter()); + registrar.RegisterConstant(static () => new AppKitObservableForProperty()); + registrar.RegisterConstant(static () => new TargetActionCommandBinder()); + + // DateTime ↔ NSDate converters + registrar.RegisterConstant(static () => new DateTimeToNSDateConverter()); + registrar.RegisterConstant(static () => new NullableDateTimeToNSDateConverter()); + registrar.RegisterConstant(static () => new NSDateToDateTimeConverter()); + registrar.RegisterConstant(static () => new NSDateToNullableDateTimeConverter()); + + // DateTimeOffset ↔ NSDate converters + registrar.RegisterConstant(static () => new DateTimeOffsetToNSDateConverter()); + registrar.RegisterConstant(static () => new NullableDateTimeOffsetToNSDateConverter()); + registrar.RegisterConstant(static () => new NSDateToDateTimeOffsetConverter()); + registrar.RegisterConstant(static () => new NSDateToNullableDateTimeOffsetConverter()); + + registrar.RegisterConstant(static () => new KVOObservableForProperty()); if (!ModeDetector.InUnitTestRunner()) { @@ -35,6 +41,6 @@ public void Register(Action, Type> registerFunction) RxSchedulers.MainThreadScheduler = new WaitForDispatcherScheduler(static () => new NSRunloopScheduler()); } - registerFunction(static () => new AppSupportJsonSuspensionDriver(), typeof(ISuspensionDriver)); + registrar.RegisterConstant(static () => new AppSupportJsonSuspensionDriver()); } } diff --git a/src/ReactiveUI/Platforms/mac/ReactiveWindowController.cs b/src/ReactiveUI/Platforms/mac/ReactiveWindowController.cs index bdf7f171e4..a91e543e96 100644 --- a/src/ReactiveUI/Platforms/mac/ReactiveWindowController.cs +++ b/src/ReactiveUI/Platforms/mac/ReactiveWindowController.cs @@ -13,10 +13,6 @@ namespace ReactiveUI; /// This is a NSWindowController that is both a NSWindowController and has ReactiveObject powers /// (i.e. you can call RaiseAndSetIfChanged). /// -#if NET6_0_OR_GREATER -[RequiresDynamicCode("ReactiveWindowController inherits from ReactiveObject which uses extension methods that require dynamic code generation")] -[RequiresUnreferencedCode("ReactiveWindowController inherits from ReactiveObject which uses extension methods that may require unreferenced code")] -#endif public class ReactiveWindowController : NSWindowController, IReactiveNotifyPropertyChanged, IHandleObservableErrors, IReactiveObject, ICanActivate { private readonly Subject _activated = new(); diff --git a/src/ReactiveUI/Platforms/mobile-common/ComponentModelFallbackConverter.cs b/src/ReactiveUI/Platforms/mobile-common/ComponentModelFallbackConverter.cs new file mode 100644 index 0000000000..6b650b18ec --- /dev/null +++ b/src/ReactiveUI/Platforms/mobile-common/ComponentModelFallbackConverter.cs @@ -0,0 +1,155 @@ +// 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.Concurrent; + +namespace ReactiveUI; + +/// +/// Fallback converter using System.ComponentModel.TypeDescriptor for reflection-based type conversion. +/// This converter is consulted only when no typed converter matches. +/// +/// +/// +/// This converter uses reflection and is not AOT-safe. It should be used as a last resort +/// when no typed converter can handle the conversion. +/// +/// +/// The converter caches component model capability lookups to avoid repeated expensive +/// reflection operations. +/// +/// +[Preserve(AllMembers = true)] +public sealed class ComponentModelFallbackConverter : IBindingFallbackConverter +{ + /// + /// Cache of resolved component model converters for specific (from,to) pairs. + /// + /// + /// This is a stable cache because type metadata tends to be reused and eviction causes repeated component model lookup. + /// + private static readonly ConcurrentDictionary<(Type From, Type To), TypeConverter?> _converterCache = new(); + + /// + /// Cache of component model capability lookups (whether conversion is supported). + /// + private static readonly ConcurrentDictionary<(Type From, Type To), bool> _capabilityCache = new(); + + /// + [UnconditionalSuppressMessage( + "ReflectionAnalysis", + "IL2026:RequiresUnreferencedCode", + Justification = "The callers of this method ensure getting the converter is trim compatible - i.e. the type is not Nullable.")] + public int GetAffinityForObjects([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type fromType, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type toType) + { + ArgumentExceptionHelper.ThrowIfNull(fromType); + ArgumentExceptionHelper.ThrowIfNull(toType); + + // Return cached capability or compute and cache + var canConvert = _capabilityCache.GetOrAdd((fromType, toType), static key => + { + try + { + // Check if component model can convert this pair + // String is a special case - use the target type's converter for string→T conversions + var (lookupFrom, lookupTo) = key.From == typeof(string) ? (key.To, key.From) : (key.From, key.To); + var converter = TypeDescriptor.GetConverter(lookupFrom); + return converter?.CanConvertTo(lookupTo) == true; + } + catch + { + // Component model threw - assume cannot convert + return false; + } + }); + + // Affinity 1 = last resort (only when nothing else works) + return canConvert ? 1 : 0; + } + + /// + [UnconditionalSuppressMessage( + "ReflectionAnalysis", + "IL2026:RequiresUnreferencedCode", + Justification = "The callers of this method ensure getting the converter is trim compatible - i.e. the type is not Nullable.")] + public bool TryConvert([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type fromType, object from, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type toType, object? conversionHint, [NotNullWhen(true)] out object? result) + { + ArgumentExceptionHelper.ThrowIfNull(from); + ArgumentExceptionHelper.ThrowIfNull(toType); + + try + { + // Get or create cached converter for this pair + var converter = GetConverter(fromType, toType); + if (converter is null) + { + this.Log().Debug("Component model cannot convert {FromType} to {ToType}", fromType, toType); + result = null; + return false; + } + + // Perform conversion + // String is a special case: use ConvertFrom for string→T conversions + var converted = (fromType == typeof(string)) + ? converter.ConvertFrom(from) + : converter.ConvertTo(from, toType); + + if (converted is not null) + { + result = converted; + return true; + } + + // Conversion returned null - treat as failure + result = null; + return false; + } + catch (FormatException ex) + { + // Format exception = conversion failed (expected for invalid input) + this.Log().Debug(ex, "Component model conversion failed (FormatException) for {FromType} -> {ToType}", fromType, toType); + result = null; + return false; + } + catch (Exception ex) when (ex.InnerException is FormatException or IndexOutOfRangeException) + { + // Component model often wraps format exceptions + this.Log().Debug(ex, "Component model conversion failed (wrapped exception) for {FromType} -> {ToType}", fromType, toType); + result = null; + return false; + } + catch (Exception ex) + { + // Unexpected exception - log as warning and treat as failure + this.Log().Warn(ex, "Component model conversion threw unexpected exception for {FromType} -> {ToType}", fromType, toType); + result = null; + return false; + } + } + + /// + /// Resolves a component model for the specified pair. + /// + /// The source type. + /// The target type. + /// + /// A converter instance if component model supports the conversion; otherwise . + /// + [UnconditionalSuppressMessage( + "ReflectionAnalysis", + "IL2026:RequiresUnreferencedCode", + Justification = "The callers of this method ensure getting the converter is trim compatible - i.e. the type is not Nullable.")] + private static TypeConverter? GetConverter([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type fromType, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type toType) + { + // Component model special-case: string conversion uses the target's converter for ConvertFrom(string). + var (lookupFrom, lookupTo) = fromType == typeof(string) ? (toType, fromType) : (fromType, toType); + + return _converterCache.GetOrAdd((fromType, toType), _ => + { + var converter = TypeDescriptor.GetConverter(lookupFrom); + return converter?.CanConvertTo(lookupTo) == true ? converter : null; + }); + } +} diff --git a/src/ReactiveUI/Platforms/mobile-common/ComponentModelTypeConverter.cs b/src/ReactiveUI/Platforms/mobile-common/ComponentModelTypeConverter.cs deleted file mode 100644 index 1f7aaef8c0..0000000000 --- a/src/ReactiveUI/Platforms/mobile-common/ComponentModelTypeConverter.cs +++ /dev/null @@ -1,79 +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. - -namespace ReactiveUI; - -/// -/// The component model binding type converter. -/// -[Preserve(AllMembers = true)] -public class ComponentModelTypeConverter : IBindingTypeConverter -{ - [SuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "TypeDescriptor is safe to use here as we are not using reflection ourselves")] - [SuppressMessage("AOT", "IL2077:Members annotated with 'RequiresDynamicCodeAttribute' require dynamic access otherwise can break functionality when ahead-of-time (AOT) compiling application code", Justification = "TypeDescriptor is safe to use here as we are not using dynamic code ourselves")] - private readonly MemoizingMRUCache<(Type fromType, Type toType), TypeConverter?> _typeConverterCache = new( - static (types, _) => - { - // NB: String is a Magical Type(tm) to TypeConverters. If we are - // converting from string => int, we need the Int converter, not - // the string converter :-/ - if (types.fromType == typeof(string)) - { - types = (types.toType, types.fromType); - } - - var converter = TypeDescriptor.GetConverter(types.fromType); - return converter.CanConvertTo(types.toType) ? converter : null; - }, - RxApp.SmallCacheLimit); - - /// - public int GetAffinityForObjects(Type fromType, Type toType) - { - var converter = _typeConverterCache.Get((fromType, toType)); - return converter is not null ? 10 : 0; - } - - /// - public bool TryConvert(object? from, Type toType, object? conversionHint, out object? result) - { - if (from is null) - { - result = null; - return true; - } - - var fromType = from.GetType(); - var converter = _typeConverterCache.Get((fromType, toType)) ?? throw new ArgumentException($"Can't convert {fromType} to {toType}. To fix this, register a IBindingTypeConverter"); - try - { - // TODO: This should use conversionHint to determine whether this is locale-aware or not - result = (fromType == typeof(string)) ? - converter.ConvertFrom(from) : converter.ConvertTo(from, toType); - - return true; - } - catch (FormatException) - { - result = null; - return false; - } - catch (Exception e) - { - // Errors from ConvertFrom end up here but wrapped in - // outer exception. Add more types here as required. - // IndexOutOfRangeException is given when trying to - // convert empty strings with some/all? converters - if (e.InnerException is IndexOutOfRangeException || - e.InnerException is FormatException) - { - result = null; - return false; - } - - throw new Exception($"Can't convert from {from.GetType()} to {toType}.", e); - } - } -} diff --git a/src/ReactiveUI/Platforms/net/ComponentModelFallbackConverter.cs b/src/ReactiveUI/Platforms/net/ComponentModelFallbackConverter.cs new file mode 100644 index 0000000000..83785e4f08 --- /dev/null +++ b/src/ReactiveUI/Platforms/net/ComponentModelFallbackConverter.cs @@ -0,0 +1,151 @@ +// 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.Concurrent; + +namespace ReactiveUI; + +/// +/// Fallback converter using System.ComponentModel.TypeDescriptor for reflection-based type conversion. +/// This converter is consulted only when no typed converter matches. +/// +/// +/// +/// This converter uses reflection and is not AOT-safe. It should be used as a last resort +/// when no typed converter can handle the conversion. +/// +/// +/// The converter caches component model capability lookups to avoid repeated expensive +/// reflection operations. +/// +/// +public sealed class ComponentModelFallbackConverter : IBindingFallbackConverter +{ + /// + /// Cache of resolved component model converters for specific (from,to) pairs. + /// + /// + /// This is a stable cache because type metadata tends to be reused and eviction causes repeated component model lookup. + /// + private static readonly ConcurrentDictionary<(Type From, Type To), TypeConverter?> _converterCache = new(); + + /// + /// Cache of component model capability lookups (whether conversion is supported). + /// + private static readonly ConcurrentDictionary<(Type From, Type To), bool> _capabilityCache = new(); + + /// + [UnconditionalSuppressMessage( + "ReflectionAnalysis", + "IL2026:RequiresUnreferencedCode", + Justification = "The callers of this method ensure getting the converter is trim compatible - i.e. the type is not Nullable.")] + public int GetAffinityForObjects([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type fromType, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type toType) + { + ArgumentExceptionHelper.ThrowIfNull(fromType); + ArgumentExceptionHelper.ThrowIfNull(toType); + + // Return cached capability or compute and cache + var canConvert = _capabilityCache.GetOrAdd((fromType, toType), static key => + { + try + { + // Check if component model can convert this pair + // String is a special case - use the target type's converter for string→T conversions + var (lookupFrom, lookupTo) = key.From == typeof(string) ? (key.To, key.From) : (key.From, key.To); + var converter = TypeDescriptor.GetConverter(lookupFrom); + return converter?.CanConvertTo(lookupTo) == true; + } + catch + { + // Component model threw - assume cannot convert + return false; + } + }); + + // Affinity 1 = last resort (only when nothing else works) + return canConvert ? 1 : 0; + } + + /// + public bool TryConvert([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type fromType, object from, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type toType, object? conversionHint, [NotNullWhen(true)] out object? result) + { + ArgumentExceptionHelper.ThrowIfNull(fromType); + ArgumentExceptionHelper.ThrowIfNull(from); + ArgumentExceptionHelper.ThrowIfNull(toType); + + try + { + // Get or create cached converter for this pair + var converter = GetConverter(fromType, toType); + if (converter is null) + { + this.Log().Debug("Component model cannot convert {FromType} to {ToType}", fromType, toType); + result = null; + return false; + } + + // Perform conversion + // String is a special case: use ConvertFrom for string→T conversions + var converted = (fromType == typeof(string)) + ? converter.ConvertFrom(from) + : converter.ConvertTo(from, toType); + + if (converted is not null) + { + result = converted; + return true; + } + + // Conversion returned null - treat as failure + result = null; + return false; + } + catch (FormatException ex) + { + // Format exception = conversion failed (expected for invalid input) + this.Log().Debug(ex, "Component model conversion failed (FormatException) for {FromType} -> {ToType}", fromType, toType); + result = null; + return false; + } + catch (Exception ex) when (ex.InnerException is FormatException or IndexOutOfRangeException) + { + // Component model often wraps format exceptions + this.Log().Debug(ex, "Component model conversion failed (wrapped exception) for {FromType} -> {ToType}", fromType, toType); + result = null; + return false; + } + catch (Exception ex) + { + // Unexpected exception - log as warning and treat as failure + this.Log().Warn(ex, "Component model conversion threw unexpected exception for {FromType} -> {ToType}", fromType, toType); + result = null; + return false; + } + } + + /// + /// Resolves a component model for the specified pair. + /// + /// The source type. + /// The target type. + /// + /// A converter instance if component model supports the conversion; otherwise . + /// + [UnconditionalSuppressMessage( + "ReflectionAnalysis", + "IL2026:RequiresUnreferencedCode", + Justification = "The callers of this method ensure getting the converter is trim compatible - i.e. the type is not Nullable.")] + private static TypeConverter? GetConverter([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type fromType, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type toType) + { + // Component model special-case: string conversion uses the target's converter for ConvertFrom(string). + var (lookupFrom, lookupTo) = fromType == typeof(string) ? (toType, fromType) : (fromType, toType); + + return _converterCache.GetOrAdd((fromType, toType), _ => + { + var converter = TypeDescriptor.GetConverter(lookupFrom); + return converter?.CanConvertTo(lookupTo) == true ? converter : null; + }); + } +} diff --git a/src/ReactiveUI/Platforms/net/ComponentModelTypeConverter.cs b/src/ReactiveUI/Platforms/net/ComponentModelTypeConverter.cs deleted file mode 100644 index 80eff99045..0000000000 --- a/src/ReactiveUI/Platforms/net/ComponentModelTypeConverter.cs +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright (c) 2022 .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. -// - -#nullable enable -using System.Diagnostics.CodeAnalysis; - -namespace ReactiveUI; - -/// -/// Binding Type Converter for component model. -/// -#if NET6_0_OR_GREATER -[RequiresDynamicCode("Component model type conversion uses reflection and dynamic code generation")] -[RequiresUnreferencedCode("Component model type conversion may reference types that could be trimmed")] -#endif -public class ComponentModelTypeConverter : IBindingTypeConverter -{ - private readonly MemoizingMRUCache<(Type fromType, Type toType), TypeConverter?> _typeConverterCache = new ( - (types, _) => - { - // NB: String is a Magical Type(tm) to TypeConverters. If we are - // converting from string => int, we need the Int converter, not - // the string converter :-/ - if (types.fromType == typeof(string)) - { - types = (types.toType, types.fromType); - } - - var converter = TypeDescriptor.GetConverter(types.fromType); - return converter.CanConvertTo(types.toType) ? converter : null; - }, RxApp.SmallCacheLimit); - - /// - public int GetAffinityForObjects(Type fromType, Type toType) - { - var converter = _typeConverterCache.Get((fromType, toType)); - return converter is not null ? 10 : 0; - } - - /// - public bool TryConvert(object? from, Type toType, object? conversionHint, out object? result) - { - if (from is null) - { - result = null; - return true; - } - - var fromType = from.GetType(); - var converter = _typeConverterCache.Get((fromType, toType)); - - if (converter is null) - { - throw new ArgumentException($"Can't convert {fromType} to {toType}. To fix this, register a IBindingTypeConverter"); - } - - try - { - // TODO: This should use conversionHint to determine whether this is locale-aware or not - result = (fromType == typeof(string)) ? - converter.ConvertFrom(from) : converter.ConvertTo(from, toType); - - return true; - } - catch (FormatException) - { - result = null; - return false; - } - catch (Exception e) - { - // Errors from ConvertFrom end up here but wrapped in - // outer exception. Add more types here as required. - // IndexOutOfRangeException is given when trying to - // convert empty strings with some/all? converters - if (e.InnerException is IndexOutOfRangeException || - e.InnerException is FormatException) - { - result = null; - return false; - } - - throw new Exception($"Can't convert from {@from.GetType()} to {toType}.", e); - } - } -} diff --git a/src/ReactiveUI/Platforms/net/PlatformRegistrations.cs b/src/ReactiveUI/Platforms/net/PlatformRegistrations.cs index 271c23de44..f9077f5978 100644 --- a/src/ReactiveUI/Platforms/net/PlatformRegistrations.cs +++ b/src/ReactiveUI/Platforms/net/PlatformRegistrations.cs @@ -13,17 +13,11 @@ namespace ReactiveUI; public class PlatformRegistrations : IWantsToRegisterStuff { /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("Platform registration uses ComponentModelTypeConverter and RxApp which require dynamic code generation")] - [RequiresUnreferencedCode("Platform registration uses ComponentModelTypeConverter and RxApp which 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); - registerFunction(() => new ComponentModelTypeConverter(), typeof(IBindingTypeConverter)); + registrar.RegisterConstant(static () => new ComponentModelFallbackConverter()); if (!ModeDetector.InUnitTestRunner()) { diff --git a/src/ReactiveUI/Platforms/netstandard2.0/PlatformRegistrations.cs b/src/ReactiveUI/Platforms/netstandard2.0/PlatformRegistrations.cs index 62809ef247..5a3cb06f0d 100644 --- a/src/ReactiveUI/Platforms/netstandard2.0/PlatformRegistrations.cs +++ b/src/ReactiveUI/Platforms/netstandard2.0/PlatformRegistrations.cs @@ -12,13 +12,9 @@ namespace ReactiveUI; public class PlatformRegistrations : IWantsToRegisterStuff { /// -#if NET6_0_OR_GREATER - [System.Diagnostics.CodeAnalysis.RequiresDynamicCode("Platform registration uses RxApp which requires dynamic code generation")] - [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("Platform registration uses RxApp which may require unreferenced code")] -#endif - public void Register(Action, Type> registerFunction) + public void Register(IRegistrar registrar) { - ArgumentExceptionHelper.ThrowIfNull(registerFunction); + ArgumentExceptionHelper.ThrowIfNull(registrar); if (!ModeDetector.InUnitTestRunner()) { diff --git a/src/ReactiveUI/Platforms/tizen/PlatformRegistrations.cs b/src/ReactiveUI/Platforms/tizen/PlatformRegistrations.cs index 979a7079b3..ecb19b4cae 100644 --- a/src/ReactiveUI/Platforms/tizen/PlatformRegistrations.cs +++ b/src/ReactiveUI/Platforms/tizen/PlatformRegistrations.cs @@ -12,16 +12,12 @@ namespace ReactiveUI; public class PlatformRegistrations : IWantsToRegisterStuff { /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("Platform registration uses ComponentModelTypeConverter and RxApp which require dynamic code generation")] - [RequiresUnreferencedCode("Platform registration uses ComponentModelTypeConverter and RxApp which may require unreferenced code")] -#endif - public void Register(Action, Type> registerFunction) + public void Register(IRegistrar registrar) { - ArgumentExceptionHelper.ThrowIfNull(registerFunction); + ArgumentExceptionHelper.ThrowIfNull(registrar); - registerFunction(() => new PlatformOperations(), typeof(IPlatformOperations)); - registerFunction(() => new ComponentModelTypeConverter(), typeof(IBindingTypeConverter)); + registrar.RegisterConstant(static () => new PlatformOperations()); + registrar.RegisterConstant(static () => new ComponentModelFallbackConverter()); if (!ModeDetector.InUnitTestRunner()) { diff --git a/src/ReactiveUI/Platforms/tvos/UIKitCommandBinders.cs b/src/ReactiveUI/Platforms/tvos/UIKitCommandBinders.cs index 54ca8b2172..300aae31a0 100644 --- a/src/ReactiveUI/Platforms/tvos/UIKitCommandBinders.cs +++ b/src/ReactiveUI/Platforms/tvos/UIKitCommandBinders.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2025 .NET Foundation and Contributors. All rights reserved. +// 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. @@ -10,32 +10,71 @@ namespace ReactiveUI; /// -/// UI Kit command binder platform registrations. +/// UIKit command binder platform registrations. /// -/// -#if NET6_0_OR_GREATER -[RequiresUnreferencedCode("FlexibleCommandBinder uses reflection for property access and type checking which may require unreferenced code.")] -#endif -public class UIKitCommandBinders : FlexibleCommandBinder +/// +/// +/// This binder registers UIKit-specific command bindings using AOT-friendly event subscription +/// where possible (explicit add/remove handler delegates) and avoids string/reflection-based +/// event hookup. +/// +/// +/// Enabled-state synchronization uses a cached for the platform +/// Enabled property and is performed via the shared infrastructure in +/// . +/// +/// +[Preserve(AllMembers = true)] +public sealed class UIKitCommandBinders : FlexibleCommandBinder { + /// + /// Cached Enabled property for (used by ). + /// + private static readonly PropertyInfo UIControlEnabledProperty = + typeof(UIControl).GetRuntimeProperty(nameof(UIControl.Enabled)) + ?? throw new InvalidOperationException("There is no Enabled property on UIControl which is required for binding."); + + /// + /// Cached Enabled property for . + /// + private static readonly PropertyInfo UIBarButtonItemEnabledProperty = + typeof(UIBarButtonItem).GetRuntimeProperty(nameof(UIBarButtonItem.Enabled)) + ?? throw new InvalidOperationException("There is no Enabled property on UIBarButtonItem which is required for binding."); + /// /// Initializes a new instance of the class. /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("UIKitCommandBinders uses methods that require dynamic code generation")] - [RequiresUnreferencedCode("UIKitCommandBinders uses methods that may require unreferenced code")] -#endif public UIKitCommandBinders() { - Register(typeof(UIControl), 9, static (cmd, t, cp) => ForTargetAction(cmd, t, cp, typeof(UIControl).GetRuntimeProperty("Enabled") ?? throw new InvalidOperationException("There is no Enabled property on the UIControl which is needed for binding."))); - Register(typeof(UIBarButtonItem), 10, static (cmd, t, cp) => ForEvent(cmd, t, cp, "Clicked", typeof(UIBarButtonItem).GetRuntimeProperty("Enabled") ?? throw new InvalidOperationException("There is no Enabled property on the UIBarButtonItem which is needed for binding."))); + // UIControl uses UIKit target-action rather than .NET events. + Register( + typeof(UIControl), + affinity: 9, + static (cmd, t, cp) => ForTargetAction(cmd, t, cp, UIControlEnabledProperty)); + + // UIBarButtonItem exposes a normal .NET event ("Clicked"). Use explicit add/remove to avoid reflection. + Register( + typeof(UIBarButtonItem), + affinity: 10, + static (cmd, t, cp) => + { + if (t is not UIBarButtonItem item) + { + return Disposable.Empty; + } + + return ForEvent( + command: cmd, + target: item, + commandParameter: cp, + addHandler: h => item.Clicked += h, + removeHandler: h => item.Clicked -= h, + enabledProperty: UIBarButtonItemEnabledProperty); + }); } /// - /// Gets the UIKitCommandBinders instance. + /// Gets the shared instance. /// -#if NET6_0_OR_GREATER - [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Deliberate")] -#endif public static Lazy Instance { get; } = new(); } diff --git a/src/ReactiveUI/Platforms/tvos/UIKitObservableForProperty.cs b/src/ReactiveUI/Platforms/tvos/UIKitObservableForProperty.cs index 5c62d5cdbb..475d9abb71 100644 --- a/src/ReactiveUI/Platforms/tvos/UIKitObservableForProperty.cs +++ b/src/ReactiveUI/Platforms/tvos/UIKitObservableForProperty.cs @@ -3,46 +3,95 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. +using System; + using UIKit; namespace ReactiveUI; /// -/// UIKitObservableForProperty is an object that knows how to -/// create notifications for a given type of object. Implement this if you -/// are porting RxUI to a new UI toolkit, or generally want to enable WhenAny -/// for another type of object that can be observed in a unique way. +/// Provides UIKit-specific observable factories used by ReactiveUI to generate change notifications for +/// UIKit controls in WhenAny* and related operators. /// +/// +/// This implementation registers observable factories for common UIKit properties that change via control +/// events or notifications. +/// [Preserve] -public class UIKitObservableForProperty : ObservableForPropertyBase +public sealed class UIKitObservableForProperty : ObservableForPropertyBase { /// /// Initializes a new instance of the class. /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("UIKitObservableForProperty uses methods that require dynamic code generation")] - [RequiresUnreferencedCode("UIKitObservableForProperty uses methods that may require unreferenced code")] -#endif public UIKitObservableForProperty() { - Register(typeof(UIControl), "Value", 20, static (s, p) => ObservableFromUIControlEvent(s, p, UIControlEvent.ValueChanged)); - Register(typeof(UITextField), "Text", 30, static (s, p) => ObservableFromNotification(s, p, UITextField.TextFieldTextDidChangeNotification)); - Register(typeof(UITextView), "Text", 30, static (s, p) => ObservableFromNotification(s, p, UITextView.TextDidChangeNotification)); - Register(typeof(UISegmentedControl), "SelectedSegment", 30, static (s, p) => ObservableFromUIControlEvent(s, p, UIControlEvent.ValueChanged)); - Register(typeof(UISegmentedControl), "SelectedSegment", 30, static (s, p) => ObservableFromUIControlEvent(s, p, UIControlEvent.ValueChanged)); + // UIControl "Value" changes via ValueChanged. + Register( + typeof(UIControl), + "Value", + affinity: 20, + static (sender, expression) => ObservableFromUIControlEvent(sender, expression, UIControlEvent.ValueChanged)); + + // UITextField "Text" changes via notification. + Register( + typeof(UITextField), + "Text", + affinity: 30, + static (sender, expression) => ObservableFromNotification(sender, expression, UITextField.TextFieldTextDidChangeNotification)); + + // UITextView "Text" changes via notification. + Register( + typeof(UITextView), + "Text", + affinity: 30, + static (sender, expression) => ObservableFromNotification(sender, expression, UITextView.TextDidChangeNotification)); + + // UISegmentedControl "SelectedSegment" changes via ValueChanged. + Register( + typeof(UISegmentedControl), + "SelectedSegment", + affinity: 30, + static (sender, expression) => ObservableFromUIControlEvent(sender, expression, UIControlEvent.ValueChanged)); - // Warning: This will stomp the Control's delegate - Register(typeof(UITabBar), "SelectedItem", 30, static (s, p) => ObservableFromEvent(s, p, "ItemSelected")); + // Warning: Event-based observation can impact control behavior depending on external delegate usage. + // Prefer explicit add/remove handler overloads (non-reflection) to improve performance and trimming/AOT compatibility. + Register( + typeof(UITabBar), + "SelectedItem", + affinity: 30, + static (sender, expression) => + { + var tabBar = (UITabBar)sender; + return ObservableFromEvent( + tabBar, + expression, + addHandler: h => tabBar.ItemSelected += h, + removeHandler: h => tabBar.ItemSelected -= h); + }); - // Warning: This will stomp the Control's delegate - Register(typeof(UISearchBar), "Text", 30, static (s, p) => ObservableFromEvent(s, p, "TextChanged")); + // Warning: Event-based observation can impact control behavior depending on external delegate usage. + // Prefer explicit add/remove handler overloads (non-reflection) to improve performance and trimming/AOT compatibility. + Register( + typeof(UISearchBar), + "Text", + affinity: 30, + static (sender, expression) => + { + var searchBar = (UISearchBar)sender; + return ObservableFromEvent( + searchBar, + expression, + addHandler: h => searchBar.TextChanged += h, + removeHandler: h => searchBar.TextChanged -= h); + }); } /// - /// Gets the UI Kit ObservableForProperty instance. + /// Gets the shared instance. /// -#if NET6_0_OR_GREATER - [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Deliberate")] -#endif + /// + /// The instance is created lazily. Consumers typically register it with the service locator once during + /// application initialization. + /// public static Lazy Instance { get; } = new(); } diff --git a/src/ReactiveUI/Platforms/uikit-common/AutoSuspendHelper.cs b/src/ReactiveUI/Platforms/uikit-common/AutoSuspendHelper.cs index 113567880b..408b607b18 100644 --- a/src/ReactiveUI/Platforms/uikit-common/AutoSuspendHelper.cs +++ b/src/ReactiveUI/Platforms/uikit-common/AutoSuspendHelper.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.Runtime.CompilerServices; + using Foundation; using UIKit; @@ -12,83 +14,81 @@ namespace ReactiveUI; /// -/// Bridges iOS lifecycle notifications into so applications can persist and +/// Bridges iOS lifecycle notifications into so applications can persist and /// restore state without manually wiring UIKit events. /// +/// The concrete type. /// /// -/// Instantiate inside your and forward the +/// Instantiate inside your and forward the /// FinishedLaunching, OnActivated, and DidEnterBackground events to the helper. The helper updates /// the shared observables and takes care of requesting background time when persisting /// application state. /// /// -/// -/// -/// -/// _autoSuspendHelper?.OnActivated(application); -/// -/// public override void DidEnterBackground(UIApplication application) => -/// _autoSuspendHelper?.DidEnterBackground(application); -/// } -/// ]]> -/// -/// -#if NET6_0_OR_GREATER -[RequiresDynamicCode("AutoSuspendHelper uses RxApp.SuspensionHost and reflection which require dynamic code generation")] -[RequiresUnreferencedCode("AutoSuspendHelper uses RxApp.SuspensionHost and reflection which may require unreferenced code")] -#endif -public class AutoSuspendHelper : IEnableLogger, IDisposable +public class AutoSuspendHelper< + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods)] T> : IEnableLogger, IDisposable + where T : UIApplicationDelegate { private readonly Subject _finishedLaunching = new(); private readonly Subject _activated = new(); private readonly Subject _backgrounded = new(); + private readonly Subject _untimelyDeath = new(); + + private readonly UnhandledExceptionEventHandler _unhandledExceptionHandler; private bool _isDisposed; /// - /// Initializes a new instance of the class. + /// Initializes static members of the class. /// - /// The uiappdelegate. - public AutoSuspendHelper(UIApplicationDelegate appDelegate) + /// + /// + /// This validation runs exactly once per closed generic type and avoids repeated reflection and cache/lock overhead. + /// + /// + /// The call uses the Type-based overload of ThrowIfMethodsNotOverloaded and expresses trimming requirements via + /// on , avoiding RequiresUnreferencedCode + /// propagation. + /// + /// + static AutoSuspendHelper() { Reflection.ThrowIfMethodsNotOverloaded( - nameof(AutoSuspendHelper), - appDelegate, - nameof(FinishedLaunching), - nameof(OnActivated), - nameof(DidEnterBackground)); + nameof(AutoSuspendHelper<>), + typeof(T), + nameof(FinishedLaunching), + nameof(OnActivated), + nameof(DidEnterBackground)); + } - RxApp.SuspensionHost.IsLaunchingNew = Observable.Never; - RxApp.SuspensionHost.IsResuming = _finishedLaunching.Select(_ => Unit.Default); - RxApp.SuspensionHost.IsUnpausing = _activated.Select(_ => Unit.Default); + /// + /// Initializes a new instance of the class. + /// + /// The application delegate instance that forwards lifecycle methods. + /// Thrown when is . + public AutoSuspendHelper(T appDelegate) + { + ArgumentExceptionHelper.ThrowIfNull(appDelegate); + + RxSuspension.SuspensionHost.IsLaunchingNew = Observable.Never; + RxSuspension.SuspensionHost.IsResuming = _finishedLaunching.Select(static _ => Unit.Default); + RxSuspension.SuspensionHost.IsUnpausing = _activated.Select(static _ => Unit.Default); - var untimelyDeath = new Subject(); - AppDomain.CurrentDomain.UnhandledException += (o, e) => untimelyDeath.OnNext(Unit.Default); + // Keep a stable delegate instance so we can unsubscribe on Dispose. + _unhandledExceptionHandler = (_, _) => _untimelyDeath.OnNext(Unit.Default); + AppDomain.CurrentDomain.UnhandledException += _unhandledExceptionHandler; - RxApp.SuspensionHost.ShouldInvalidateState = untimelyDeath; + RxSuspension.SuspensionHost.ShouldInvalidateState = _untimelyDeath; - RxApp.SuspensionHost.ShouldPersistState = _backgrounded.SelectMany(app => + RxSuspension.SuspensionHost.ShouldPersistState = _backgrounded.SelectMany(app => { - var taskId = app.BeginBackgroundTask(new NSAction(() => untimelyDeath.OnNext(Unit.Default))); + var taskId = app.BeginBackgroundTask(new NSAction(() => _untimelyDeath.OnNext(Unit.Default))); - // NB: We're being force-killed, signal invalidate instead + // NB: We're being force-killed, signal invalidate instead. if (taskId == UIApplication.BackgroundTaskInvalid) { - untimelyDeath.OnNext(Unit.Default); + _untimelyDeath.OnNext(Unit.Default); return Observable.Empty; } @@ -97,8 +97,8 @@ public AutoSuspendHelper(UIApplicationDelegate appDelegate) } /// - /// Gets the launch options captured from the most recent call to . Keys are converted - /// to strings and values are stringified for convenience when hydrating state. + /// Gets the launch options captured from the most recent call to . + /// Keys are converted to strings and values are stringified for convenience when hydrating state. /// public IDictionary? LaunchOptions { get; private set; } @@ -106,31 +106,63 @@ public AutoSuspendHelper(UIApplicationDelegate appDelegate) /// Notifies the helper that was /// invoked so it can propagate the observable. /// - /// The application. - /// The launch options. + /// The application instance. + /// The launch options dictionary. public void FinishedLaunching(UIApplication application, NSDictionary launchOptions) { - LaunchOptions = launchOptions is not null - ? launchOptions.Keys.ToDictionary(k => k.ToString(), v => launchOptions[v].ToString()) - : []; + ThrowIfDisposed(); + + // Preserve original behavior: if launchOptions is null, expose an empty dictionary. + if (launchOptions is null) + { + LaunchOptions = new Dictionary(0); + } + else + { + // Avoid LINQ allocations; keep behavior broadly equivalent to ToDictionary. + var keys = launchOptions.Keys; + var dict = new Dictionary(keys.Length); + + for (int i = 0; i < keys.Length; i++) + { + var k = keys[i]; + var keyString = k?.ToString() ?? string.Empty; + + var value = launchOptions[k]; + var valueString = value?.ToString() ?? string.Empty; + + // NSDictionary keys are unique by contract. + dict[keyString] = valueString; + } + + LaunchOptions = dict; + } // NB: This is run in-context (i.e. not scheduled), so by the time this - // statement returns, UIWindow should be created already + // statement returns, UIWindow should be created already. _finishedLaunching.OnNext(application); } /// /// Notifies the helper that occurred. /// - /// The application. - public void OnActivated(UIApplication application) => _activated.OnNext(application); + /// The application instance. + public void OnActivated(UIApplication application) + { + ThrowIfDisposed(); + _activated.OnNext(application); + } /// /// Notifies the helper that was raised so that /// persistence can begin. /// - /// The application. - public void DidEnterBackground(UIApplication application) => _backgrounded.OnNext(application); + /// The application instance. + public void DidEnterBackground(UIApplication application) + { + ThrowIfDisposed(); + _backgrounded.OnNext(application); + } /// public void Dispose() @@ -142,7 +174,7 @@ public void Dispose() /// /// Releases managed resources held by the helper. /// - /// If we are going to call Dispose methods on field items. + /// Whether to release managed resources. protected virtual void Dispose(bool isDisposing) { if (_isDisposed) @@ -150,13 +182,29 @@ protected virtual void Dispose(bool isDisposing) return; } - if (isDisposing) + _isDisposed = true; + + if (!isDisposing) { - _activated?.Dispose(); - _backgrounded?.Dispose(); - _finishedLaunching?.Dispose(); + return; } - _isDisposed = true; + // Unsubscribe first to avoid keeping the helper alive. + AppDomain.CurrentDomain.UnhandledException -= _unhandledExceptionHandler; + + // Dispose subjects to release downstream subscriptions deterministically. + _activated.Dispose(); + _backgrounded.Dispose(); + _finishedLaunching.Dispose(); + _untimelyDeath.Dispose(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ThrowIfDisposed() + { + if (_isDisposed) + { + throw new ObjectDisposedException(nameof(AutoSuspendHelper<>)); + } } } diff --git a/src/ReactiveUI/Platforms/uikit-common/CommonReactiveSource.cs b/src/ReactiveUI/Platforms/uikit-common/CommonReactiveSource.cs index a3f763242a..a6b417575b 100644 --- a/src/ReactiveUI/Platforms/uikit-common/CommonReactiveSource.cs +++ b/src/ReactiveUI/Platforms/uikit-common/CommonReactiveSource.cs @@ -6,6 +6,7 @@ using System.Collections; using System.Collections.Specialized; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using DynamicData; @@ -15,104 +16,180 @@ namespace ReactiveUI; -#if NET6_0_OR_GREATER -[RequiresDynamicCode("CommonReactiveSource uses methods that require dynamic code generation")] -[RequiresUnreferencedCode("CommonReactiveSource uses methods that may require unreferenced code")] -#endif +/// +/// Provides a common reactive data source implementation for collection-based UI views backed by sectioned data. +/// +/// The source item type. +/// The UI view type. +/// The UI cell type. +/// The section information type. +/// +/// +/// This type monitors the current and the collections inside each section, and translates +/// collection change notifications into batch updates for the underlying UI adapter. +/// +/// +/// Threading: all operations are expected to run on the creating thread (typically the platform main thread). If an +/// operation occurs off-thread, an is thrown. +/// +/// +/// Trimming/AOT: this implementation avoids expression-tree-based reactive helpers (e.g. WhenAnyValue/ObservableForProperty) +/// and instead filters ReactiveObject change streams by property name. +/// +/// internal sealed class CommonReactiveSource : ReactiveObject, IDisposable where TSectionInfo : ISectionInformation { + /// + /// Adapter used to manipulate the UI view (reload, begin/end updates, insert/delete items, etc.). + /// private readonly IUICollViewAdapter _adapter; + + /// + /// Managed thread id captured at construction time; used to validate calls occur on the expected thread. + /// private readonly int _mainThreadId; + + /// + /// Root disposable for subscriptions created by this instance. + /// private readonly CompositeDisposable _mainDisposables; + + /// + /// Holds subscriptions associated with the current value. Replaced when SectionInfo changes. + /// + [SuppressMessage("Usage", "CA2213:Disposable fields should be disposed", Justification = "Handled by the CompositeDisposable")] private readonly SerialDisposable _sectionInfoDisposable; - private readonly IList<(int section, PendingChange pendingChange)> _pendingChanges; + + /// + /// Pending collection changes captured while the UI is not reloading and before a scheduled batch update is applied. + /// + private readonly List<(int section, PendingChange pendingChange)> _pendingChanges; + + /// + /// Indicates whether pending changes are currently being collected for later application. + /// private bool _isCollectingChanges; + + /// + /// Backing store for . + /// private IReadOnlyList _sectionInfo; /// /// Initializes a new instance of the class. /// /// The adapter to use which we want to display information for. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("CommonReactiveSource uses methods that require dynamic code generation")] - [RequiresUnreferencedCode("CommonReactiveSource uses methods that may require unreferenced code")] -#endif + /// Thrown when is . public CommonReactiveSource(IUICollViewAdapter adapter) { + ArgumentExceptionHelper.ThrowIfNull(adapter); + _adapter = adapter; _mainThreadId = Environment.CurrentManagedThreadId; + _mainDisposables = []; _sectionInfoDisposable = new SerialDisposable(); _mainDisposables.Add(_sectionInfoDisposable); + _pendingChanges = []; _sectionInfo = []; + // Avoid ObservableForProperty/WhenAnyValue (expression trees); filter by property name instead. _mainDisposables.Add( - this - .ObservableForProperty( - x => x.SectionInfo, - beforeChange: true, - skipInitial: true) - .Subscribe( - _ => SectionInfoChanging(), - ex => this.Log().Error(ex, "Error occurred whilst SectionInfo changing."))); + Changing! + .Where(static e => e.PropertyName == nameof(SectionInfo)) + .Subscribe( + _ => SectionInfoChanging(), + ex => this.Log().Error(ex, "Error occurred whilst SectionInfo changing."))); _mainDisposables.Add( - this - .WhenAnyValue, IReadOnlyList>(nameof(SectionInfo)) - .Subscribe( - SectionInfoChanged, - ex => this.Log().Error(ex, "Error occurred when SectionInfo changed."))); + Changed! + .Where(static e => e.PropertyName == nameof(SectionInfo)) + .Subscribe( + _ => SectionInfoChanged(SectionInfo), + ex => this.Log().Error(ex, "Error occurred when SectionInfo changed."))); } + /// + /// Gets or sets the current section information. + /// + /// + /// Assigning this property replaces all subscriptions associated with the prior section information. + /// public IReadOnlyList SectionInfo { get => _sectionInfo; set => this.RaiseAndSetIfChanged(ref _sectionInfo, value); } + /// + /// Gets a value indicating whether debug logging is enabled. + /// private bool IsDebugEnabled => this.Log().Level <= LogLevel.Debug; + /// + /// Returns the number of sections. + /// + /// The number of sections. public int NumberOfSections() { - Debug.Assert(Environment.CurrentManagedThreadId == _mainThreadId, "The thread is not the main thread."); + VerifyOnMainThread(); - var count = SectionInfo.Count; + var count = _sectionInfo.Count; this.Log().Debug(CultureInfo.InvariantCulture, "Reporting number of sections = {0}", count); return count; } + /// + /// Returns the number of rows in a section. + /// + /// The section index. + /// The number of rows in the specified section. public int RowsInSection(int section) { - Debug.Assert(Environment.CurrentManagedThreadId == _mainThreadId, "The thread is not the main thread."); + VerifyOnMainThread(); - var list = (IList)SectionInfo[section].Collection!; + var list = (IList)_sectionInfo[section].Collection!; var count = list.Count; + this.Log().Debug(CultureInfo.InvariantCulture, "Reporting rows in section {0} = {1}", section, count); return count; } + /// + /// Returns the item at the specified index path. + /// + /// The index path. + /// The item at the index path, or . public object? ItemAt(NSIndexPath path) { - Debug.Assert(Environment.CurrentManagedThreadId == _mainThreadId, "The thread is not the main thread."); + VerifyOnMainThread(); - var list = (IList)SectionInfo[path.Section].Collection!; + var list = (IList)_sectionInfo[path.Section].Collection!; this.Log().Debug(CultureInfo.InvariantCulture, "Returning item at {0}-{1}", path.Section, path.Row); return list[path.Row]; } + /// + /// Dequeues and configures a cell for the specified index path. + /// + /// The index path. + /// The configured view cell. public TUIViewCell GetCell(NSIndexPath indexPath) { - Debug.Assert(Environment.CurrentManagedThreadId == _mainThreadId, "The thread is not the main thread."); + VerifyOnMainThread(); this.Log().Debug(CultureInfo.InvariantCulture, "Getting cell for index path {0}-{1}", indexPath.Section, indexPath.Row); - var section = SectionInfo[indexPath.Section]; + + var section = _sectionInfo[indexPath.Section]; var vm = ((IList)section.Collection!)[indexPath.Row]; - var cell = _adapter.DequeueReusableCell(section?.CellKeySelector?.Invoke(vm) ?? NSString.Empty, indexPath); + + var key = section?.CellKeySelector?.Invoke(vm) ?? NSString.Empty; + var cell = _adapter.DequeueReusableCell(key, indexPath); if (cell is IViewFor view) { @@ -120,18 +197,34 @@ public TUIViewCell GetCell(NSIndexPath indexPath) view.ViewModel = vm; } - var initializeCellAction = section?.InitializeCellAction ?? (static _ => { }); + var initializeCellAction = section?.InitializeCellAction ?? NoOpInitializeCell; initializeCellAction(cell); return cell; } + /// + /// Disposes subscriptions and managed resources associated with this instance. + /// public void Dispose() { - _mainDisposables?.Dispose(); - _sectionInfoDisposable?.Dispose(); + _mainDisposables.Dispose(); + } + + /// + /// No-op initializer used when a section does not provide an initialization callback. + /// + /// The cell to initialize. + private static void NoOpInitializeCell(TUIViewCell cell) + { } + /// + /// Builds the list of updates for a pending collection change event. + /// + /// The pending change. + /// An enumerable of updates. + /// Thrown when the action is not supported. private static IEnumerable GetUpdatesForEvent(PendingChange pendingChange) => pendingChange.Action switch { @@ -139,6 +232,7 @@ private static IEnumerable GetUpdatesForEvent(PendingChange pendingChang Enumerable .Range(pendingChange.NewStartingIndex, pendingChange.NewItems is null ? 1 : pendingChange.NewItems.Count) .Select(Update.CreateAdd), + NotifyCollectionChangedAction.Remove => Enumerable .Range(pendingChange.OldStartingIndex, pendingChange.OldItems is null ? 1 : pendingChange.OldItems.Count) @@ -146,23 +240,26 @@ private static IEnumerable GetUpdatesForEvent(PendingChange pendingChang // Use OldStartingIndex for each "Update.Index" because the batch update processes and removes items sequentially // opposed to as one Range operation. - // For example if we are removing the items from indexes 1 to 5. - // When item at index 1 is removed item at index 2 is now at index 1 and so on down the line. NotifyCollectionChangedAction.Move => Enumerable .Range(pendingChange.OldStartingIndex, pendingChange.OldItems is null ? 1 : pendingChange.OldItems.Count) .Select(Update.CreateDelete) .Concat( - Enumerable - .Range(pendingChange.NewStartingIndex, pendingChange.NewItems is null ? 1 : pendingChange.NewItems.Count) - .Select(Update.CreateAdd)), + Enumerable + .Range(pendingChange.NewStartingIndex, pendingChange.NewItems is null ? 1 : pendingChange.NewItems.Count) + .Select(Update.CreateAdd)), + NotifyCollectionChangedAction.Replace => Enumerable .Range(pendingChange.NewStartingIndex, pendingChange.NewItems is null ? 1 : pendingChange.NewItems.Count) - .SelectMany(x => new[] { Update.CreateDelete(x), Update.CreateAdd(x) }), + .SelectMany(static x => new[] { Update.CreateDelete(x), Update.CreateAdd(x) }), + _ => throw new NotSupportedException("Don't know how to deal with " + pendingChange.Action), }; + /// + /// Called before changes. Disposes subscriptions for the current SectionInfo. + /// private void SectionInfoChanging() { VerifyOnMainThread(); @@ -171,6 +268,10 @@ private void SectionInfoChanging() _sectionInfoDisposable.Disposable = null; } + /// + /// Called when changes. Subscribes to section structure and item changes. + /// + /// The new section info. private void SectionInfoChanged(IReadOnlyList? sectionInfo) { VerifyOnMainThread(); @@ -186,15 +287,19 @@ private void SectionInfoChanged(IReadOnlyList? sectionInfo) } var notifyCollectionChanged = sectionInfo as INotifyCollectionChanged; - if (notifyCollectionChanged is null) { - this.Log().Warn(CultureInfo.InvariantCulture, "[#{0}] SectionInfo {1} does not implement INotifyCollectionChanged - any added or removed sections will not be reflected in the UI.", sectionInfoId, sectionInfo); + this.Log().Warn( + CultureInfo.InvariantCulture, + "[#{0}] SectionInfo {1} does not implement INotifyCollectionChanged - any added or removed sections will not be reflected in the UI.", + sectionInfoId, + sectionInfo); } - var sectionChanged = (notifyCollectionChanged is null ? - Observable.Never : - notifyCollectionChanged.ObserveCollectionChanges().Select(_ => Unit.Default)) + var sectionChanged = + (notifyCollectionChanged is null + ? Observable.Never + : notifyCollectionChanged.ObserveCollectionChanges().Select(static _ => Unit.Default)) .StartWith(Unit.Default); var disposables = new CompositeDisposable @@ -206,10 +311,13 @@ private void SectionInfoChanged(IReadOnlyList? sectionInfo) SubscribeToSectionInfoChanges(sectionInfoId, sectionInfo, sectionChanged, disposables); } -#if NET6_0_OR_GREATER - [RequiresDynamicCode("SubscribeToSectionInfoChanges uses methods that require dynamic code generation")] - [RequiresUnreferencedCode("SubscribeToSectionInfoChanges uses methods that may require unreferenced code")] -#endif + /// + /// Subscribes to changes in the section collection and to changes within each section's item collection. + /// + /// A correlation id for logging. + /// The current section info. + /// An observable indicating that the section set changed. + /// A disposable container for subscriptions. private void SubscribeToSectionInfoChanges(int sectionInfoId, IReadOnlyList sectionInfo, IObservable sectionChanged, CompositeDisposable disposables) { // holds a single disposable representing the monitoring of sectionInfo. @@ -218,199 +326,261 @@ private void SubscribeToSectionInfoChanges(int sectionInfoId, IReadOnlyList - { - VerifyOnMainThread(); - - this.Log().Debug(CultureInfo.InvariantCulture, "[#{0}] Calling ReloadData()", sectionInfoId); - _adapter.ReloadData(); - - // holds all the disposables created to monitor stuff inside the section - var sectionDisposables = new CompositeDisposable(); - sectionInfoDisposable.Disposable = sectionDisposables; - - // holds a single disposable for any outstanding request to apply pending changes - var applyPendingChangesDisposable = new SerialDisposable(); - sectionDisposables.Add(applyPendingChangesDisposable); - - var isReloading = _adapter - .IsReloadingData - .DistinctUntilChanged() - .Do(y => this.Log().Debug(CultureInfo.InvariantCulture, "[#{0}] IsReloadingData = {1}", sectionInfoId, y)) - .Publish(); - - var anySectionChanged = sectionInfo - .Select((y, index) => y.Collection!.ObserveCollectionChanges().Select(z => new { Section = index, Change = z })) - .Merge() - .Publish(); - - // since reloads are applied asynchronously, it is possible for data to change whilst the reload is occurring - // thus, we need to ensure any such changes result in another reload - sectionDisposables.Add( - isReloading - .Where(y => y) - .Join( - anySectionChanged, - _ => isReloading, - _ => Observable.Empty, - (_, __) => Unit.Default) - .Subscribe(_ => - { - VerifyOnMainThread(); - this.Log().Debug(CultureInfo.InvariantCulture, "[#{0}] A section changed whilst a reload is in progress - forcing another reload.", sectionInfoId); - - _adapter.ReloadData(); - _pendingChanges.Clear(); - _isCollectingChanges = false; - })); - - sectionDisposables.Add( - isReloading - .Where(y => !y) - .Join( - anySectionChanged, - _ => isReloading, - _ => Observable.Empty, - (_, changeDetails) => (changeDetails.Change, changeDetails.Section)) - .Subscribe( - y => - { - VerifyOnMainThread(); - - if (IsDebugEnabled) - { - this.Log().Debug( - CultureInfo.InvariantCulture, - "[#{0}] Change detected in section {1} : Action = {2}, OldStartingIndex = {3}, NewStartingIndex = {4}, OldItems.Count = {5}, NewItems.Count = {6}", - sectionInfoId, - y.Section, - y.Change.EventArgs.Action, - y.Change.EventArgs.OldStartingIndex, - y.Change.EventArgs.NewStartingIndex, - y.Change.EventArgs.OldItems is null ? "null" : y.Change.EventArgs.OldItems.Count.ToString(CultureInfo.InvariantCulture), - y.Change.EventArgs.NewItems is null ? "null" : y.Change.EventArgs.NewItems.Count.ToString(CultureInfo.InvariantCulture)); - } - - if (!_isCollectingChanges) - { - this.Log().Debug(CultureInfo.InvariantCulture, "[#{0}] A section changed whilst no reload is in progress - instigating collection of updates for later application.", sectionInfoId); - _isCollectingChanges = true; - - // immediately indicate to the view that there are changes underway, even though we don't apply them immediately - // this ensures that if application code itself calls BeginUpdates/EndUpdates on the view before the changes have been applied, those inconsistencies - // between what's in the data source versus what the view believes is in the data source won't trigger any errors because of the outstanding - // BeginUpdates call (calls to BeginUpdates/EndUpdates can be nested) - this.Log().Debug(CultureInfo.InvariantCulture, "[#{0}] BeginUpdates", sectionInfoId); - _adapter.BeginUpdates(); - - applyPendingChangesDisposable.Disposable = RxSchedulers.MainThreadScheduler.Schedule( - () => - { - ApplyPendingChanges(sectionInfoId); - this.Log().Debug(CultureInfo.InvariantCulture, "[#{0}] EndUpdates", sectionInfoId); - _adapter.EndUpdates(); - _isCollectingChanges = false; - applyPendingChangesDisposable.Disposable = null; - }); - } - - _pendingChanges.Add((y.Section, new PendingChange(y.Change.EventArgs))); - }, - ex => this.Log().Error(CultureInfo.InvariantCulture, "[#{0}] Error while watching section collection: {1}", sectionInfoId, ex))); - - sectionDisposables.Add(isReloading.Connect()); - sectionDisposables.Add(anySectionChanged.Connect()); - })); + sectionChanged.Subscribe( + _ => + { + VerifyOnMainThread(); + + this.Log().Debug(CultureInfo.InvariantCulture, "[#{0}] Calling ReloadData()", sectionInfoId); + _adapter.ReloadData(); + + // holds all the disposables created to monitor stuff inside the section + var sectionDisposables = new CompositeDisposable(); + sectionInfoDisposable.Disposable = sectionDisposables; + + // holds a single disposable for any outstanding request to apply pending changes + var applyPendingChangesDisposable = new SerialDisposable(); + sectionDisposables.Add(applyPendingChangesDisposable); + + var isReloading = _adapter + .IsReloadingData + .DistinctUntilChanged() + .Do(y => this.Log().Debug(CultureInfo.InvariantCulture, "[#{0}] IsReloadingData = {1}", sectionInfoId, y)) + .Publish(); + + // Merge per-section collection changes into a single stream. + // This remains a "setup path" rather than a hot per-event path. + var anySectionChanged = sectionInfo + .Select((y, index) => y.Collection!.ObserveCollectionChanges().Select(z => new { Section = index, Change = z })) + .Merge() + .Publish(); + + // since reloads are applied asynchronously, it is possible for data to change whilst the reload is occurring + // thus, we need to ensure any such changes result in another reload + sectionDisposables.Add( + isReloading + .Where(static y => y) + .Join( + anySectionChanged, + _ => isReloading, + _ => Observable.Empty, + static (_, __) => Unit.Default) + .Subscribe( + _ => + { + VerifyOnMainThread(); + + this.Log().Debug( + CultureInfo.InvariantCulture, + "[#{0}] A section changed whilst a reload is in progress - forcing another reload.", + sectionInfoId); + + _adapter.ReloadData(); + _pendingChanges.Clear(); + _isCollectingChanges = false; + })); + + sectionDisposables.Add( + isReloading + .Where(static y => !y) + .Join( + anySectionChanged, + _ => isReloading, + _ => Observable.Empty, + static (_, changeDetails) => (changeDetails.Change, changeDetails.Section)) + .Subscribe( + y => + { + VerifyOnMainThread(); + + if (IsDebugEnabled) + { + var ea = y.Change.EventArgs; + this.Log().Debug( + CultureInfo.InvariantCulture, + "[#{0}] Change detected in section {1} : Action = {2}, OldStartingIndex = {3}, NewStartingIndex = {4}, OldItems.Count = {5}, NewItems.Count = {6}", + sectionInfoId, + y.Section, + ea.Action, + ea.OldStartingIndex, + ea.NewStartingIndex, + ea.OldItems is null ? "null" : ea.OldItems.Count.ToString(CultureInfo.InvariantCulture), + ea.NewItems is null ? "null" : ea.NewItems.Count.ToString(CultureInfo.InvariantCulture)); + } + + if (!_isCollectingChanges) + { + this.Log().Debug( + CultureInfo.InvariantCulture, + "[#{0}] A section changed whilst no reload is in progress - instigating collection of updates for later application.", + sectionInfoId); + + _isCollectingChanges = true; + + // immediately indicate to the view that there are changes underway, even though we don't apply them immediately + this.Log().Debug(CultureInfo.InvariantCulture, "[#{0}] BeginUpdates", sectionInfoId); + _adapter.BeginUpdates(); + + applyPendingChangesDisposable.Disposable = + RxSchedulers.MainThreadScheduler.Schedule( + () => + { + ApplyPendingChanges(sectionInfoId); + + this.Log().Debug(CultureInfo.InvariantCulture, "[#{0}] EndUpdates", sectionInfoId); + _adapter.EndUpdates(); + + _isCollectingChanges = false; + applyPendingChangesDisposable.Disposable = null; + }); + } + + _pendingChanges.Add((y.Section, new PendingChange(y.Change.EventArgs))); + }, + ex => this.Log().Error(CultureInfo.InvariantCulture, "[#{0}] Error while watching section collection: {1}", sectionInfoId, ex))); + + sectionDisposables.Add(isReloading.Connect()); + sectionDisposables.Add(anySectionChanged.Connect()); + })); } + /// + /// Applies pending changes collected during the current update window as adapter batch operations. + /// + /// A correlation id for logging. + /// Thrown when called off the creating thread. private void ApplyPendingChanges(int sectionInfoId) { - Debug.Assert(Environment.CurrentManagedThreadId == _mainThreadId, "The thread is not the main thread."); + VerifyOnMainThread(); Debug.Assert(_isCollectingChanges, "Currently there are no changes to collect"); + this.Log().Debug(CultureInfo.InvariantCulture, "[#{0}] Applying pending changes", sectionInfoId); try { _adapter.PerformUpdates( - () => - { - if (IsDebugEnabled) - { - this.Log().Debug(CultureInfo.InvariantCulture, "[#{0}] The pending changes (in order received) are:", sectionInfoId); - - foreach (var (section, pendingChange) in _pendingChanges) - { - this.Log().Debug( - CultureInfo.InvariantCulture, - "[#{0}] Section {1}: Action = {2}, OldStartingIndex = {3}, NewStartingIndex = {4}, OldItems.Count = {5}, NewItems.Count = {6}", - sectionInfoId, - section, - pendingChange.Action, - pendingChange.OldStartingIndex, - pendingChange.NewStartingIndex, - pendingChange.OldItems is null ? "null" : pendingChange.OldItems.Count.ToString(CultureInfo.InvariantCulture), - pendingChange.NewItems is null ? "null" : pendingChange.NewItems.Count.ToString(CultureInfo.InvariantCulture)); - } - } - - foreach (var sectionedUpdates in _pendingChanges.GroupBy(x => x.section)) - { - var section = sectionedUpdates.First().section; - - this.Log().Debug(CultureInfo.InvariantCulture, "[#{0}] Processing updates for section {1}", sectionInfoId, section); - - var allSectionChanges = sectionedUpdates - .Select(x => x.pendingChange) - .ToList(); - - if (allSectionChanges.Any(x => x.Action == NotifyCollectionChangedAction.Reset)) - { - this.Log().Debug(CultureInfo.InvariantCulture, "[#{0}] Section {1} included a reset notification, so reloading that section.", sectionInfoId, section); - _adapter.ReloadSections(new NSIndexSet((nuint)section)); - continue; - } - - var updates = allSectionChanges - .SelectMany(GetUpdatesForEvent) - .ToList(); - var normalizedUpdates = IndexNormalizer.Normalize(updates); - - if (IsDebugEnabled) - { - this.Log().Debug( - CultureInfo.InvariantCulture, - "[#{0}] Updates for section {1}: {2}", - sectionInfoId, - section, - string.Join(":", updates)); - - this.Log().Debug( - CultureInfo.InvariantCulture, - "[#{0}] Normalized updates for section {1}: {2}", - sectionInfoId, - section, - string.Join(":", normalizedUpdates)); - } - - foreach (var normalizedUpdate in normalizedUpdates) - { - switch (normalizedUpdate?.Type) - { - case UpdateType.Add: - DoUpdate(_adapter.InsertItems, [normalizedUpdate.Index], section); - break; - - case UpdateType.Delete: - DoUpdate(_adapter.DeleteItems, [normalizedUpdate.Index], section); - break; - - default: - throw new NotSupportedException(); - } - } - } - }, - () => this.Log().Debug(CultureInfo.InvariantCulture, "[#{0}] Pending changes applied", sectionInfoId)); + () => + { + if (_pendingChanges.Count == 0) + { + return; + } + + if (IsDebugEnabled) + { + this.Log().Debug(CultureInfo.InvariantCulture, "[#{0}] The pending changes (in order received) are:", sectionInfoId); + + for (var i = 0; i < _pendingChanges.Count; i++) + { + var (section, pendingChange) = _pendingChanges[i]; + this.Log().Debug( + CultureInfo.InvariantCulture, + "[#{0}] Section {1}: Action = {2}, OldStartingIndex = {3}, NewStartingIndex = {4}, OldItems.Count = {5}, NewItems.Count = {6}", + sectionInfoId, + section, + pendingChange.Action, + pendingChange.OldStartingIndex, + pendingChange.NewStartingIndex, + pendingChange.OldItems is null ? "null" : pendingChange.OldItems.Count.ToString(CultureInfo.InvariantCulture), + pendingChange.NewItems is null ? "null" : pendingChange.NewItems.Count.ToString(CultureInfo.InvariantCulture)); + } + } + + // Sort by section to process per section without GroupBy allocations. + _pendingChanges.Sort(static (a, b) => a.section.CompareTo(b.section)); + + var iChange = 0; + while (iChange < _pendingChanges.Count) + { + var section = _pendingChanges[iChange].section; + + this.Log().Debug(CultureInfo.InvariantCulture, "[#{0}] Processing updates for section {1}", sectionInfoId, section); + + // Scan to determine the range [iChange, iEnd) for this section and whether any Reset is present. + var iEnd = iChange; + var hasReset = false; + + while (iEnd < _pendingChanges.Count && _pendingChanges[iEnd].section == section) + { + if (_pendingChanges[iEnd].pendingChange.Action == NotifyCollectionChangedAction.Reset) + { + hasReset = true; + } + + iEnd++; + } + + if (hasReset) + { + this.Log().Debug( + CultureInfo.InvariantCulture, + "[#{0}] Section {1} included a reset notification, so reloading that section.", + sectionInfoId, + section); + + _adapter.ReloadSections(new NSIndexSet((nuint)section)); + iChange = iEnd; + continue; + } + + // Materialize updates for this section. + // We keep using the existing normalization routine; updates list is per-section and bounded. + var updates = new List(); + + for (var j = iChange; j < iEnd; j++) + { + foreach (var update in GetUpdatesForEvent(_pendingChanges[j].pendingChange)) + { + if (update is null) + { + continue; + } + + updates.Add(update); + } + } + + var normalizedUpdates = IndexNormalizer.Normalize(updates); + + if (IsDebugEnabled) + { + this.Log().Debug( + CultureInfo.InvariantCulture, + "[#{0}] Updates for section {1}: {2}", + sectionInfoId, + section, + string.Join(":", updates)); + + this.Log().Debug( + CultureInfo.InvariantCulture, + "[#{0}] Normalized updates for section {1}: {2}", + sectionInfoId, + section, + string.Join(":", normalizedUpdates)); + } + + for (var k = 0; k < normalizedUpdates.Count; k++) + { + var normalizedUpdate = normalizedUpdates[k]; + switch (normalizedUpdate?.Type) + { + case UpdateType.Add: + DoUpdate(_adapter.InsertItems, normalizedUpdate.Index, section); + break; + + case UpdateType.Delete: + DoUpdate(_adapter.DeleteItems, normalizedUpdate.Index, section); + break; + + default: + throw new NotSupportedException(); + } + } + + iChange = iEnd; + } + }, + () => this.Log().Debug(CultureInfo.InvariantCulture, "[#{0}] Pending changes applied", sectionInfoId)); } finally { @@ -418,44 +588,116 @@ private void ApplyPendingChanges(int sectionInfoId) } } - private void DoUpdate(Action method, IEnumerable update, int section) + /// + /// Applies an adapter update method for a single index within a section. + /// + /// The adapter method. + /// The row index. + /// The section index. + private void DoUpdate(Action method, int index, int section) { - var toChange = update - .Select(x => NSIndexPath.FromRowSection(x, section)) - .ToArray(); + // Single item -> avoid IEnumerable allocations and ToArray. + var toChange = new[] { NSIndexPath.FromRowSection(index, section) }; if (IsDebugEnabled) { this.Log().Debug( - CultureInfo.InvariantCulture, - "Calling {0}: [{1}]", - method.Method.Name, - string.Join(",", toChange.Select(x => x.Section + "-" + x.Row))); + CultureInfo.InvariantCulture, + "Calling {0}: [{1}]", + method.Method.Name, + toChange[0].Section + "-" + toChange[0].Row); } method(toChange); } + /// + /// Throws if the current thread is not the creating thread. + /// + /// Thrown when called off the creating thread. private void VerifyOnMainThread() { if (Environment.CurrentManagedThreadId != _mainThreadId) { - throw new InvalidOperationException("An operation has occurred off the main thread that must be performed on it. Be sure to schedule changes to the underlying data correctly."); + throw new InvalidOperationException( + "An operation has occurred off the main thread that must be performed on it. Be sure to schedule changes to the underlying data correctly."); } } - // rather than storing NotifyCollectionChangeEventArgs instances, we store instances of this class instead - // storing NotifyCollectionChangeEventArgs doesn't always work because external code can mutate the instance before we get a chance to apply it - private sealed class PendingChange(NotifyCollectionChangedEventArgs ea) + /// + /// Snapshot of a collection change event that is resilient to external mutation of the original event args. + /// + /// + /// Rather than storing instances, we store instances of this type. + /// Storing the event args directly does not always work because external code can mutate the instance before it + /// can be applied. + /// + private sealed class PendingChange { - public NotifyCollectionChangedAction Action { get; } = ea.Action; - - public IList? OldItems { get; } = ea.OldItems?.Cast().ToList(); - - public IList? NewItems { get; } = ea.NewItems?.Cast().ToList(); + /// + /// Initializes a new instance of the class. + /// + /// The original collection change event arguments. + /// Thrown when is . + public PendingChange(NotifyCollectionChangedEventArgs ea) + { + ArgumentExceptionHelper.ThrowIfNull(ea); - public int OldStartingIndex { get; } = ea.OldStartingIndex; + Action = ea.Action; + OldItems = CopyItems(ea.OldItems); + NewItems = CopyItems(ea.NewItems); + OldStartingIndex = ea.OldStartingIndex; + NewStartingIndex = ea.NewStartingIndex; + } - public int NewStartingIndex { get; } = ea.NewStartingIndex; + /// + /// Gets the collection change action. + /// + public NotifyCollectionChangedAction Action { get; } + + /// + /// Gets the copied old items. + /// + public IList? OldItems { get; } + + /// + /// Gets the copied new items. + /// + public IList? NewItems { get; } + + /// + /// Gets the old starting index. + /// + public int OldStartingIndex { get; } + + /// + /// Gets the new starting index. + /// + public int NewStartingIndex { get; } + + /// + /// Creates a shallow copy of the items in the specified list, returning a new list containing the same + /// elements. + /// + /// The copy is shallow; reference types within the list are not cloned. The returned + /// list is always of type . + /// The list whose items are to be copied. Can be null or empty. + /// A new list containing the elements of . Returns null if + /// is null, or an empty list if is empty. + private static IList? CopyItems(IList? source) + { + if (source is null || source.Count == 0) + { + return source is null ? null : Array.Empty(); + } + + var list = new List(source.Count); + for (var i = 0; i < source.Count; i++) + { + list.Add(source[i]!); + } + + return list; + } } } diff --git a/src/ReactiveUI/Platforms/uikit-common/FlexibleCommandBinder.cs b/src/ReactiveUI/Platforms/uikit-common/FlexibleCommandBinder.cs index 4165f20903..e5fd4e884b 100644 --- a/src/ReactiveUI/Platforms/uikit-common/FlexibleCommandBinder.cs +++ b/src/ReactiveUI/Platforms/uikit-common/FlexibleCommandBinder.cs @@ -6,222 +6,556 @@ using System.Reflection; using System.Windows.Input; -using ReactiveUI.Helpers; - using UIKit; namespace ReactiveUI; /// -/// Generic command binder platform registrations. +/// Base class for platform command binders that register per-type binding factories with an affinity score. /// -/// -#if NET6_0_OR_GREATER -[RequiresDynamicCode("FlexibleCommandBinder uses reflection for property access and type checking which require dynamic code generation")] -[RequiresUnreferencedCode("FlexibleCommandBinder uses reflection for property access and type checking which may require unreferenced code")] -#endif +/// +/// +/// This type is intended for platform implementations (Android, iOS, etc.) that need to bind an +/// to UI controls with platform-specific semantics. +/// +/// +/// Threading: registrations are mutable; lookups are served from a versioned snapshot to avoid locking on +/// the common path. Binding factories are invoked outside locks. +/// +/// +/// Trimming/AOT: the default binding selection method accepts an unknown runtime target type and may call +/// reflection-based helpers (e.g., ). Reflection-based methods are annotated with +/// and where applicable. +/// Prefer the add/remove handler overload for AOT-safe event binding. +/// +/// public abstract class FlexibleCommandBinder : ICreatesCommandBinding { /// - /// Configuration map. + /// A single synchronization gate for all mutable state in this instance. + /// + private readonly object _gate = new(); + + /// + /// Mutable registration map; only accessed under . + /// + private readonly Dictionary _config = []; + + /// + /// A version counter incremented on each registration mutation. + /// + private int _version; + + /// + /// A snapshot of registrations used for lock-free reads. /// - private readonly Dictionary _config = - []; + private Entry[]? _snapshot; + + /// + /// A snapshot version that corresponds to . + /// + private int _snapshotVersion; /// - public int GetAffinityForObject(Type type, bool hasEventTarget) + public int GetAffinityForObject<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicEvents | DynamicallyAccessedMemberTypes.PublicProperties)] T>(bool hasEventTarget) { if (hasEventTarget) { return 0; } - var match = _config.Keys - .Where(x => x.IsAssignableFrom(type)) - .OrderByDescending(x => _config[x].Affinity) - .FirstOrDefault(); + var entries = GetSnapshot(); + var targetType = typeof(T); + + var bestAffinity = 0; - if (match is null) + // Scan all assignable registrations; choose highest affinity. + for (var i = 0; i < entries.Length; i++) { - return 0; + var entry = entries[i]; + if (!entry.Type.IsAssignableFrom(targetType)) + { + continue; + } + + var affinity = entry.Affinity; + if (affinity > bestAffinity) + { + bestAffinity = affinity; + } } - var typeProperties = _config[match]; - return typeProperties.Affinity; + return bestAffinity; } /// -#if NET6_0_OR_GREATER - public int GetAffinityForObject<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicEvents | DynamicallyAccessedMemberTypes.PublicProperties)] T>( - bool hasEventTarget) -#else - public int GetAffinityForObject( - bool hasEventTarget) -#endif + [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 commandParameter) + where T : class { - if (hasEventTarget) + ArgumentExceptionHelper.ThrowIfNull(target); + + var entries = GetSnapshot(); + var runtimeType = target.GetType(); + + Entry? best = null; + var bestAffinity = int.MinValue; + + for (var i = 0; i < entries.Length; i++) { - return 0; - } + var entry = entries[i]; + if (!entry.Type.IsAssignableFrom(runtimeType)) + { + continue; + } - var match = _config.Keys - .Where(x => x.IsAssignableFrom(typeof(T))) - .OrderByDescending(x => _config[x].Affinity) - .FirstOrDefault(); + if (entry.Affinity > bestAffinity) + { + bestAffinity = entry.Affinity; + best = entry; + } + } - if (match is null) + if (best is null || best.Value.Factory is null) { - return 0; + throw new NotSupportedException($"CommandBinding for {runtimeType.Name} is not supported"); } - var typeProperties = _config[match]; - return typeProperties.Affinity; + // Never invoke user code under locks; snapshot factories are safe to call directly here. + return best.Value.Factory(command, target, commandParameter) ?? Disposable.Empty; } /// - public IDisposable? BindCommandToObject(ICommand? command, object? target, IObservable commandParameter) + [RequiresUnreferencedCode("String/reflection-based event binding may require members removed by trimming.")] + public virtual IDisposable? BindCommandToObject( + ICommand? command, + T? target, + IObservable commandParameter, + string eventName) + where T : class => + Disposable.Empty; + + /// + public virtual IDisposable? BindCommandToObject<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicEvents | DynamicallyAccessedMemberTypes.NonPublicEvents)] T, TEventArgs>( + ICommand? command, + T? target, + IObservable commandParameter, + Action> addHandler, + Action> removeHandler) + where T : class + where TEventArgs : EventArgs { ArgumentExceptionHelper.ThrowIfNull(target); + ArgumentNullException.ThrowIfNull(addHandler); + ArgumentNullException.ThrowIfNull(removeHandler); - var type = target.GetType(); + // Match existing binder behavior: null command means "no binding". + if (command is null) + { + return Disposable.Empty; + } + + commandParameter ??= Observable.Return((object?)target); + + object? latestParam = null; + + void Handler(object? sender, TEventArgs e) + { + var param = Volatile.Read(ref latestParam); + if (command.CanExecute(param)) + { + command.Execute(param); + } + } - var match = _config.Keys - .Where(x => x.IsAssignableFrom(type)) - .OrderByDescending(x => _config[x].Affinity) - .FirstOrDefault() ?? throw new NotSupportedException($"CommandBinding for {type.Name} is not supported"); - var typeProperties = _config[match]; + var paramSub = commandParameter.Subscribe(x => Volatile.Write(ref latestParam, x)); + addHandler(Handler); - return typeProperties?.CreateBinding?.Invoke(command, target, commandParameter) ?? Disposable.Empty; + return new CompositeDisposable( + paramSub, + Disposable.Create(() => removeHandler(Handler))); } - /// - public IDisposable BindCommandToObject(ICommand? command, object? target, IObservable commandParameter, string eventName) - where TEventArgs : EventArgs => throw new NotImplementedException(); + /// + /// Creates a command binding from an explicit event subscription API and an enabled property. + /// + /// The target type that exposes the event. + /// The event args type. + /// The command to execute when the event fires. + /// The target object that exposes the event. + /// An observable providing the latest command parameter. + /// Adds the event handler to the target. + /// Removes the event handler from the target. + /// A property used to set enabled state (best-effort). + /// A disposable that unsubscribes the event and stops updating enabled state. + /// + /// Thrown when , , , + /// , or is . + /// + /// + /// This overload is AOT-compatible: it does not use reflection-based event subscription. + /// Enabled state synchronization still depends on the provided . + /// + protected static IDisposable ForEvent( + ICommand? command, + TTarget target, + IObservable commandParameter, + Action> addHandler, + Action> removeHandler, + PropertyInfo enabledProperty) + where TTarget : class + where TEventArgs : EventArgs + { + ArgumentExceptionHelper.ThrowIfNull(command); + ArgumentExceptionHelper.ThrowIfNull(target); + ArgumentNullException.ThrowIfNull(addHandler); + ArgumentNullException.ThrowIfNull(removeHandler); + ArgumentExceptionHelper.ThrowIfNull(enabledProperty); + + commandParameter ??= Observable.Return((object?)target); + + object? latestParam = null; + + void Handler(object? sender, TEventArgs e) + { + var param = Volatile.Read(ref latestParam); + if (command.CanExecute(param)) + { + command.Execute(param); + } + } + + // Subscribe parameter first so we have best effort latest value before the first event. + var paramSub = commandParameter.Subscribe(x => Volatile.Write(ref latestParam, x)); + + addHandler(Handler); + var eventDisp = Disposable.Create(() => removeHandler(Handler)); + + var enabledSetter = Reflection.GetValueSetterForProperty(enabledProperty); + if (enabledSetter is null) + { + return new CompositeDisposable(paramSub, eventDisp); + } + + // Initial enabled state. + enabledSetter(target, command.CanExecute(Volatile.Read(ref latestParam)), null); + + var canExecuteSub = Observable.FromEvent( + eventHandler => + { + void CanExecuteHandler(object? s, EventArgs e) => + eventHandler(command.CanExecute(Volatile.Read(ref latestParam))); + return CanExecuteHandler; + }, + h => command.CanExecuteChanged += h, + h => command.CanExecuteChanged -= h) + .Subscribe(x => enabledSetter(target, x, null)); + + return new CompositeDisposable(paramSub, eventDisp, canExecuteSub); + } /// - /// Creates a commands binding from event and a property. + /// Creates a command binding from an explicit event subscription API and an enabled property. /// - /// The binding from event. - /// Command. - /// Target. - /// Command parameter. - /// Event name. - /// Enabled Property. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("ForEvent uses Reflection.GetValueSetterForProperty which requires dynamic code generation")] - [RequiresUnreferencedCode("ForEvent uses Reflection.GetValueSetterForProperty which may require unreferenced code")] -#endif - protected static IDisposable ForEvent(ICommand? command, object? target, IObservable commandParameter, string eventName, PropertyInfo enabledProperty) + /// The target type that exposes the event. + /// The command to execute when the event fires. + /// The target object that exposes the event. + /// An observable providing the latest command parameter. + /// Adds the event handler to the target. + /// Removes the event handler from the target. + /// A property used to set enabled state (best-effort). + /// A disposable that unsubscribes the event and stops updating enabled state. + /// + /// Thrown when , , , + /// , or is . + /// + /// + /// This overload is AOT-compatible: it does not use reflection-based event subscription. + /// Enabled state synchronization still depends on the provided . + /// + protected static IDisposable ForEvent( + ICommand? command, + TTarget target, + IObservable commandParameter, + Action addHandler, + Action removeHandler, + PropertyInfo enabledProperty) + where TTarget : class { ArgumentExceptionHelper.ThrowIfNull(command); + ArgumentExceptionHelper.ThrowIfNull(target); + ArgumentNullException.ThrowIfNull(addHandler); + ArgumentNullException.ThrowIfNull(removeHandler); + ArgumentExceptionHelper.ThrowIfNull(enabledProperty); - commandParameter ??= Observable.Return(target); + commandParameter ??= Observable.Return((object?)target); object? latestParam = null; - var ctl = target!; - var actionDisp = Observable.FromEventPattern(ctl, eventName).Subscribe((_) => + void Handler(object? sender, EventArgs e) { - if (command.CanExecute(latestParam)) + var param = Volatile.Read(ref latestParam); + if (command.CanExecute(param)) { - command.Execute(latestParam); + command.Execute(param); } - }); + } + + // Subscribe parameter first so we have best effort latest value before the first event. + var paramSub = commandParameter.Subscribe(x => Volatile.Write(ref latestParam, x)); + + addHandler(Handler); + var eventDisp = Disposable.Create(() => removeHandler(Handler)); var enabledSetter = Reflection.GetValueSetterForProperty(enabledProperty); if (enabledSetter is null) { - return actionDisp; + return new CompositeDisposable(paramSub, eventDisp); } - // initial enabled state - enabledSetter(target, command.CanExecute(latestParam), null); + // Initial enabled state. + enabledSetter(target, command.CanExecute(Volatile.Read(ref latestParam)), null); - return new CompositeDisposable( - actionDisp, - commandParameter.Subscribe(x => latestParam = x), - Observable.FromEvent( - eventHandler => - { - void Handler(object? sender, EventArgs e) => eventHandler(command.CanExecute(latestParam)); - return Handler; - }, - x => command.CanExecuteChanged += x, - x => command.CanExecuteChanged -= x) - .Subscribe(x => enabledSetter(target, x, null))); + var canExecuteSub = Observable.FromEvent( + eventHandler => + { + void CanExecuteHandler(object? s, EventArgs e) => + eventHandler(command.CanExecute(Volatile.Read(ref latestParam))); + return CanExecuteHandler; + }, + h => command.CanExecuteChanged += h, + h => command.CanExecuteChanged -= h) + .Subscribe(x => enabledSetter(target, x, null)); + + return new CompositeDisposable(paramSub, eventDisp, canExecuteSub); } /// - /// Creates a commands binding from event and a property. + /// Creates a command binding from a named event and an enabled property. /// - /// The command. - /// The target object. - /// The command parameter. - /// The enabled property. - /// Returns a disposable. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("ForTargetAction uses Reflection.GetValueSetterForProperty which requires dynamic code generation")] - [RequiresUnreferencedCode("ForTargetAction uses Reflection.GetValueSetterForProperty which may require unreferenced code")] -#endif - protected static IDisposable ForTargetAction(ICommand? command, object? target, IObservable commandParameter, PropertyInfo enabledProperty) + /// The command to execute when the event fires. + /// The UI target object that exposes the event. + /// An observable providing the latest command parameter. + /// The event name to subscribe to. + /// A property to set enabled state (best-effort). + /// A disposable that unsubscribes the event and stops updating enabled state. + /// Thrown when is null. + /// + /// This helper uses reflection-based event subscription and is not trimming-safe. + /// + [RequiresUnreferencedCode("String/reflection-based event binding may require members removed by trimming.")] + [RequiresDynamicCode("String/reflection-based event binding uses reflection and may require dynamic code generation.")] + protected static IDisposable ForEvent( + ICommand? command, + object? target, + IObservable commandParameter, + string eventName, + PropertyInfo enabledProperty) { ArgumentExceptionHelper.ThrowIfNull(command); + ArgumentExceptionHelper.ThrowIfNull(target); + ArgumentExceptionHelper.ThrowIfNull(eventName); + ArgumentExceptionHelper.ThrowIfNull(enabledProperty); commandParameter ??= Observable.Return(target); object? latestParam = null; - var actionDisposable = Disposable.Empty; + var paramSub = commandParameter.Subscribe(x => Volatile.Write(ref latestParam, x)); - if (target is UIControl ctl) + var actionSub = Observable.FromEventPattern(target, eventName).Subscribe(_ => { - var eh = new EventHandler((o, e) => + var param = Volatile.Read(ref latestParam); + if (command.CanExecute(param)) { - if (command.CanExecute(latestParam)) + command.Execute(param); + } + }); + + var enabledSetter = Reflection.GetValueSetterForProperty(enabledProperty); + if (enabledSetter is null) + { + return new CompositeDisposable(paramSub, actionSub); + } + + enabledSetter(target, command.CanExecute(Volatile.Read(ref latestParam)), null); + + var canExecuteSub = Observable.FromEvent( + eventHandler => { - command.Execute(latestParam); - } - }); + void Handler(object? sender, EventArgs e) => + eventHandler(command.CanExecute(Volatile.Read(ref latestParam))); + return Handler; + }, + h => command.CanExecuteChanged += h, + h => command.CanExecuteChanged -= h) + .Subscribe(x => enabledSetter(target, x, null)); + + return new CompositeDisposable(paramSub, actionSub, canExecuteSub); + } + + /// + /// Creates a command binding for UIKit controls using + /// and an enabled property. + /// + /// The command to execute when the control is activated. + /// The target object, expected to be a . + /// An observable providing the latest command parameter. + /// The property used to set enabled state. + /// A disposable that unbinds the handler and stops updating enabled state. + /// Thrown when is null. + protected static IDisposable ForTargetAction( + ICommand? command, + object? target, + IObservable commandParameter, + PropertyInfo enabledProperty) + { + ArgumentExceptionHelper.ThrowIfNull(command); + ArgumentExceptionHelper.ThrowIfNull(target); + ArgumentExceptionHelper.ThrowIfNull(enabledProperty); + + commandParameter ??= Observable.Return(target); - ctl.AddTarget(eh, UIControlEvent.TouchUpInside); - actionDisposable = Disposable.Create(() => ctl.RemoveTarget(eh, UIControlEvent.TouchUpInside)); + if (target is not UIControl ctl) + { + return Disposable.Empty; } + object? latestParam = null; + + // Stable handler instance for deterministic unsubscribe. + void Handler(object? sender, EventArgs e) + { + var param = Volatile.Read(ref latestParam); + if (command.CanExecute(param)) + { + command.Execute(param); + } + } + + var paramSub = commandParameter.Subscribe(x => Volatile.Write(ref latestParam, x)); + + // UIKit target-action via EventHandler is supported through UIControl's AddTarget overload. + ctl.AddTarget(Handler, UIControlEvent.TouchUpInside); + var actionDisp = Disposable.Create(() => ctl.RemoveTarget(Handler, UIControlEvent.TouchUpInside)); + var enabledSetter = Reflection.GetValueSetterForProperty(enabledProperty); if (enabledSetter is null) { - return actionDisposable; + return new CompositeDisposable(paramSub, actionDisp); } - // Initial enabled state - enabledSetter(target, command.CanExecute(latestParam), null); + enabledSetter(target, command.CanExecute(Volatile.Read(ref latestParam)), null); - return new CompositeDisposable( - actionDisposable, - commandParameter.Subscribe(x => latestParam = x), - Observable.FromEvent( - eventHandler => - { - void Handler(object? sender, EventArgs e) => eventHandler(command.CanExecute(latestParam)); - return Handler; - }, - x => command.CanExecuteChanged += x, - x => command.CanExecuteChanged -= x) - .Subscribe(x => enabledSetter(target, x, null))); + var canExecuteSub = Observable.FromEvent( + eventHandler => + { + void CanExecuteHandler(object? s, EventArgs e) => + eventHandler(command.CanExecute(Volatile.Read(ref latestParam))); + return CanExecuteHandler; + }, + h => command.CanExecuteChanged += h, + h => command.CanExecuteChanged -= h) + .Subscribe(x => enabledSetter(target, x, null)); + + return new CompositeDisposable(paramSub, actionDisp, canExecuteSub); } /// - /// Registers an observable factory for the specified type and property. + /// Registers a binding factory for a type with an affinity score. /// - /// The type. - /// The affinity. - /// The create binding. - protected void Register(Type type, int affinity, Func, IDisposable> createBinding) => _config[type] = new CommandBindingInfo { Affinity = affinity, CreateBinding = createBinding }; + /// The registered type. + /// The affinity score used to select among candidates. + /// The factory that creates the binding. + /// Thrown when or is null. + protected void Register(Type type, int affinity, Func, IDisposable> createBinding) + { + ArgumentNullException.ThrowIfNull(type); + ArgumentNullException.ThrowIfNull(createBinding); - private class CommandBindingInfo + lock (_gate) + { + _config[type] = new CommandBindingInfo(affinity, createBinding); + _version++; + _snapshot = null; + } + } + + /// + /// Produces or returns a cached snapshot of registrations for lock-free reads. + /// + /// The current snapshot. + private Entry[] GetSnapshot() { - public int Affinity { get; set; } + var snapshot = Volatile.Read(ref _snapshot); + var snapshotVersion = Volatile.Read(ref _snapshotVersion); + + if (snapshot is not null && snapshotVersion == Volatile.Read(ref _version)) + { + return snapshot; + } + + lock (_gate) + { + // Recheck under lock. + var v = _version; + snapshot = _snapshot; + if (snapshot is not null && _snapshotVersion == v) + { + return snapshot; + } + + var entries = new Entry[_config.Count]; + var i = 0; + + foreach (var kvp in _config) + { + var info = kvp.Value; + entries[i++] = new Entry(kvp.Key, info.Affinity, info.CreateBinding); + } + + Volatile.Write(ref _snapshotVersion, v); + Volatile.Write(ref _snapshot, entries); + + return entries; + } + } + + /// + /// Immutable snapshot entry for a registered binding factory. + /// + private readonly record struct Entry( + Type Type, + int Affinity, + Func, IDisposable>? Factory); + + /// + /// Stores binding configuration for a registered type. + /// + private sealed class CommandBindingInfo + { + /// + /// Initializes a new instance of the class. + /// + /// The affinity score. + /// The binding factory. + public CommandBindingInfo(int affinity, Func, IDisposable> createBinding) + { + Affinity = affinity; + CreateBinding = createBinding; + } + + /// + /// Gets the affinity score for this binding. + /// + public int Affinity { get; } - public Func, IDisposable>? CreateBinding { get; set; } + /// + /// Gets the binding factory. + /// + public Func, IDisposable> CreateBinding { get; } } } diff --git a/src/ReactiveUI/Platforms/uikit-common/IUICollViewAdapter.cs b/src/ReactiveUI/Platforms/uikit-common/IUICollViewAdapter.cs index d65e33a070..62dca7a05c 100644 --- a/src/ReactiveUI/Platforms/uikit-common/IUICollViewAdapter.cs +++ b/src/ReactiveUI/Platforms/uikit-common/IUICollViewAdapter.cs @@ -23,10 +23,6 @@ internal interface IUICollViewAdapter /// /// Reloads the data. /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("ReloadData uses methods that require dynamic code generation")] - [RequiresUnreferencedCode("ReloadData uses methods that may require unreferenced code")] -#endif void ReloadData(); /// diff --git a/src/ReactiveUI/Platforms/uikit-common/PlatformRegistrations.cs b/src/ReactiveUI/Platforms/uikit-common/PlatformRegistrations.cs index c3b66df45c..d4817d19ad 100644 --- a/src/ReactiveUI/Platforms/uikit-common/PlatformRegistrations.cs +++ b/src/ReactiveUI/Platforms/uikit-common/PlatformRegistrations.cs @@ -13,18 +13,28 @@ namespace ReactiveUI; public class PlatformRegistrations : IWantsToRegisterStuff { /// - [RequiresUnreferencedCode("Uses reflection to create instances of types.")] - [RequiresDynamicCode("Uses reflection to create instances of types.")] - public void Register(Action, Type> registerFunction) + public void Register(IRegistrar registrar) { - ArgumentExceptionHelper.ThrowIfNull(registerFunction); + ArgumentExceptionHelper.ThrowIfNull(registrar); - registerFunction(static () => new PlatformOperations(), typeof(IPlatformOperations)); - registerFunction(static () => new ComponentModelTypeConverter(), typeof(IBindingTypeConverter)); - registerFunction(static () => new UIKitObservableForProperty(), typeof(ICreatesObservableForProperty)); - registerFunction(static () => new UIKitCommandBinders(), typeof(ICreatesCommandBinding)); - registerFunction(static () => new DateTimeNSDateConverter(), typeof(IBindingTypeConverter)); - registerFunction(static () => new KVOObservableForProperty(), typeof(ICreatesObservableForProperty)); + registrar.RegisterConstant(static () => new PlatformOperations()); + registrar.RegisterConstant(static () => new ComponentModelFallbackConverter()); + registrar.RegisterConstant(static () => new UIKitObservableForProperty()); + registrar.RegisterConstant(static () => new UIKitCommandBinders()); + + // DateTime ↔ NSDate converters + registrar.RegisterConstant(static () => new DateTimeToNSDateConverter()); + registrar.RegisterConstant(static () => new NullableDateTimeToNSDateConverter()); + registrar.RegisterConstant(static () => new NSDateToDateTimeConverter()); + registrar.RegisterConstant(static () => new NSDateToNullableDateTimeConverter()); + + // DateTimeOffset ↔ NSDate converters + registrar.RegisterConstant(static () => new DateTimeOffsetToNSDateConverter()); + registrar.RegisterConstant(static () => new NullableDateTimeOffsetToNSDateConverter()); + registrar.RegisterConstant(static () => new NSDateToDateTimeOffsetConverter()); + registrar.RegisterConstant(static () => new NSDateToNullableDateTimeOffsetConverter()); + + registrar.RegisterConstant(static () => new KVOObservableForProperty()); if (!ModeDetector.InUnitTestRunner()) { @@ -32,6 +42,6 @@ public void Register(Action, Type> registerFunction) RxSchedulers.MainThreadScheduler = new WaitForDispatcherScheduler(static () => new NSRunloopScheduler()); } - registerFunction(static () => new AppSupportJsonSuspensionDriver(), typeof(ISuspensionDriver)); + registrar.RegisterConstant(static () => new AppSupportJsonSuspensionDriver()); } } diff --git a/src/ReactiveUI/Platforms/uikit-common/ReactiveCollectionReusableView.cs b/src/ReactiveUI/Platforms/uikit-common/ReactiveCollectionReusableView.cs index eb441be6a7..164f9fe810 100644 --- a/src/ReactiveUI/Platforms/uikit-common/ReactiveCollectionReusableView.cs +++ b/src/ReactiveUI/Platforms/uikit-common/ReactiveCollectionReusableView.cs @@ -16,10 +16,6 @@ namespace ReactiveUI; /// (i.e. you can call RaiseAndSetIfChanged). /// [SuppressMessage("Design", "CA1010: Implement generic IEnumerable", Justification = "UI Kit exposes IEnumerable")] -#if NET6_0_OR_GREATER -[RequiresDynamicCode("ReactiveCollectionReusableView inherits from ReactiveObject which uses extension methods that require dynamic code generation")] -[RequiresUnreferencedCode("ReactiveCollectionReusableView inherits from ReactiveObject which uses extension methods that may require unreferenced code")] -#endif public abstract class ReactiveCollectionReusableView : UICollectionReusableView, IReactiveNotifyPropertyChanged, IHandleObservableErrors, IReactiveObject, ICanActivate { private readonly Subject _activated = new(); @@ -152,10 +148,6 @@ protected override void Dispose(bool disposing) /// The view model type. [SuppressMessage("Design", "CA1010: Implement generic IEnumerable", Justification = "UI Kit exposes IEnumerable")] [SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:FileMayOnlyContainASingleType", Justification = "Classes with the same class names within.")] -#if NET6_0_OR_GREATER -[RequiresDynamicCode("ReactiveCollectionReusableView inherits from ReactiveObject which uses extension methods that require dynamic code generation")] -[RequiresUnreferencedCode("ReactiveCollectionReusableView inherits from ReactiveObject which uses extension methods that may require unreferenced code")] -#endif public abstract class ReactiveCollectionReusableView : ReactiveCollectionReusableView, IViewFor where TViewModel : class { diff --git a/src/ReactiveUI/Platforms/uikit-common/ReactiveCollectionView.cs b/src/ReactiveUI/Platforms/uikit-common/ReactiveCollectionView.cs index 2f630131e0..959db7fab0 100644 --- a/src/ReactiveUI/Platforms/uikit-common/ReactiveCollectionView.cs +++ b/src/ReactiveUI/Platforms/uikit-common/ReactiveCollectionView.cs @@ -16,10 +16,6 @@ namespace ReactiveUI; /// (i.e. you can call RaiseAndSetIfChanged). /// [SuppressMessage("Design", "CA1010: Implement generic IEnumerable", Justification = "UI Kit exposes IEnumerable")] -#if NET6_0_OR_GREATER -[RequiresDynamicCode("ReactiveCollectionView inherits from ReactiveObject which uses extension methods that require dynamic code generation")] -[RequiresUnreferencedCode("ReactiveCollectionView inherits from ReactiveObject which uses extension methods that may require unreferenced code")] -#endif public abstract class ReactiveCollectionView : UICollectionView, IReactiveNotifyPropertyChanged, IHandleObservableErrors, IReactiveObject, ICanActivate, ICanForceManualActivation { private readonly Subject _activated = new(); @@ -137,10 +133,6 @@ protected override void Dispose(bool disposing) /// The view model type. [SuppressMessage("Design", "CA1010: Implement generic IEnumerable", Justification = "UI Kit exposes IEnumerable")] [SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:FileMayOnlyContainASingleType", Justification = "Classes with the same class names within.")] -#if NET6_0_OR_GREATER -[RequiresDynamicCode("ReactiveCollectionView inherits from ReactiveObject which uses extension methods that require dynamic code generation")] -[RequiresUnreferencedCode("ReactiveCollectionView inherits from ReactiveObject which uses extension methods that may require unreferenced code")] -#endif public abstract class ReactiveCollectionView : ReactiveCollectionView, IViewFor where TViewModel : class { diff --git a/src/ReactiveUI/Platforms/uikit-common/ReactiveCollectionViewCell.cs b/src/ReactiveUI/Platforms/uikit-common/ReactiveCollectionViewCell.cs index 621c84843c..34e8ab95e1 100644 --- a/src/ReactiveUI/Platforms/uikit-common/ReactiveCollectionViewCell.cs +++ b/src/ReactiveUI/Platforms/uikit-common/ReactiveCollectionViewCell.cs @@ -16,10 +16,6 @@ namespace ReactiveUI; /// (i.e. you can call RaiseAndSetIfChanged). /// [SuppressMessage("Design", "CA1010: Implement generic IEnumerable", Justification = "UI Kit exposes IEnumerable")] -#if NET6_0_OR_GREATER -[RequiresDynamicCode("ReactiveCollectionViewCell inherits from ReactiveObject which uses extension methods that require dynamic code generation")] -[RequiresUnreferencedCode("ReactiveCollectionViewCell inherits from ReactiveObject which uses extension methods that may require unreferenced code")] -#endif public abstract class ReactiveCollectionViewCell : UICollectionViewCell, IReactiveNotifyPropertyChanged, IHandleObservableErrors, IReactiveObject, ICanActivate { private readonly Subject _activated = new(); @@ -131,10 +127,6 @@ protected override void Dispose(bool disposing) /// The view model type. [SuppressMessage("Design", "CA1010: Implement generic IEnumerable", Justification = "UI Kit exposes IEnumerable")] [SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:FileMayOnlyContainASingleType", Justification = "Classes with the same class names within.")] -#if NET6_0_OR_GREATER -[RequiresDynamicCode("ReactiveCollectionViewCell inherits from ReactiveObject which uses extension methods that require dynamic code generation")] -[RequiresUnreferencedCode("ReactiveCollectionViewCell inherits from ReactiveObject which uses extension methods that may require unreferenced code")] -#endif public abstract class ReactiveCollectionViewCell : ReactiveCollectionViewCell, IViewFor where TViewModel : class { diff --git a/src/ReactiveUI/Platforms/uikit-common/ReactiveCollectionViewController.cs b/src/ReactiveUI/Platforms/uikit-common/ReactiveCollectionViewController.cs index 8e29d0cc81..22f234c397 100644 --- a/src/ReactiveUI/Platforms/uikit-common/ReactiveCollectionViewController.cs +++ b/src/ReactiveUI/Platforms/uikit-common/ReactiveCollectionViewController.cs @@ -14,10 +14,6 @@ namespace ReactiveUI; /// (i.e. you can call RaiseAndSetIfChanged). /// [SuppressMessage("Design", "CA1010: Implement generic IEnumerable", Justification = "UI Kit exposes IEnumerable")] -#if NET6_0_OR_GREATER -[RequiresDynamicCode("ReactiveCollectionViewController inherits from ReactiveObject which uses extension methods that require dynamic code generation")] -[RequiresUnreferencedCode("ReactiveCollectionViewController inherits from ReactiveObject which uses extension methods that may require unreferenced code")] -#endif public abstract class ReactiveCollectionViewController : UICollectionViewController, IReactiveNotifyPropertyChanged, IHandleObservableErrors, IReactiveObject, ICanActivate { @@ -149,10 +145,6 @@ protected override void Dispose(bool disposing) /// The view model type. [SuppressMessage("Design", "CA1010: Implement generic IEnumerable", Justification = "UI Kit exposes IEnumerable")] [SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:FileMayOnlyContainASingleType", Justification = "Classes with the same class names within.")] -#if NET6_0_OR_GREATER -[RequiresDynamicCode("ReactiveCollectionViewController inherits from ReactiveObject which uses extension methods that require dynamic code generation")] -[RequiresUnreferencedCode("ReactiveCollectionViewController inherits from ReactiveObject which uses extension methods that may require unreferenced code")] -#endif public abstract class ReactiveCollectionViewController : ReactiveCollectionViewController, IViewFor where TViewModel : class { diff --git a/src/ReactiveUI/Platforms/uikit-common/ReactiveCollectionViewSource.cs b/src/ReactiveUI/Platforms/uikit-common/ReactiveCollectionViewSource.cs index 545d9b3f14..047535d9c3 100644 --- a/src/ReactiveUI/Platforms/uikit-common/ReactiveCollectionViewSource.cs +++ b/src/ReactiveUI/Platforms/uikit-common/ReactiveCollectionViewSource.cs @@ -18,10 +18,6 @@ namespace ReactiveUI; /// View items are animated in and out as items are added. /// /// The source type. -#if NET6_0_OR_GREATER -[RequiresDynamicCode("ReactiveCollectionViewSource uses methods that require dynamic code generation")] -[RequiresUnreferencedCode("ReactiveCollectionViewSource uses methods that may require unreferenced code")] -#endif public class ReactiveCollectionViewSource : UICollectionViewSource, IReactiveNotifyPropertyChanged>, IHandleObservableErrors, IReactiveObject { private readonly CommonReactiveSource> _commonSource; @@ -34,10 +30,6 @@ public class ReactiveCollectionViewSource : UICollectionViewSource, IRe /// The notify collection changed. /// The cell key. /// The cell initialization action. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("ReactiveCollectionViewSource uses methods that require dynamic code generation")] - [RequiresUnreferencedCode("ReactiveCollectionViewSource uses methods that may require unreferenced code")] -#endif public ReactiveCollectionViewSource(UICollectionView collectionView, INotifyCollectionChanged collection, NSString cellKey, Action? initializeCellAction = null) : this(collectionView) => Data = [new CollectionViewSectionInformation(collection, cellKey, initializeCellAction) @@ -47,10 +39,6 @@ public ReactiveCollectionViewSource(UICollectionView collectionView, INotifyColl /// Initializes a new instance of the class. /// /// The ui collection view. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("ReactiveCollectionViewSource uses methods that require dynamic code generation")] - [RequiresUnreferencedCode("ReactiveCollectionViewSource uses methods that may require unreferenced code")] -#endif public ReactiveCollectionViewSource(UICollectionView collectionView) { var adapter = new UICollectionViewAdapter(collectionView); @@ -153,10 +141,6 @@ public override void ItemSelected(UICollectionView collectionView, NSIndexPath i /// /// An object that, when disposed, re-enables change /// notifications. -#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/Platforms/uikit-common/ReactiveCollectionViewSourceExtensions.cs b/src/ReactiveUI/Platforms/uikit-common/ReactiveCollectionViewSourceExtensions.cs index cd7caeda3c..7a0d17e2a5 100644 --- a/src/ReactiveUI/Platforms/uikit-common/ReactiveCollectionViewSourceExtensions.cs +++ b/src/ReactiveUI/Platforms/uikit-common/ReactiveCollectionViewSourceExtensions.cs @@ -31,10 +31,8 @@ public static class ReactiveCollectionViewSourceExtensions /// the . /// Type of the view source. /// Type of the . -#if NET6_0_OR_GREATER - [RequiresDynamicCode("BindTo uses methods that require dynamic code generation")] - [RequiresUnreferencedCode("BindTo uses methods that may require unreferenced code")] -#endif + [RequiresUnreferencedCode("Evaluates expression-based member chains via reflection; members may be trimmed.")] + [RequiresDynamicCode("Uses dynamic binding paths which may require runtime code generation or reflection-based invocation.")] public static IDisposable BindTo( this IObservable>> sectionsObservable, UICollectionView collectionView, @@ -66,10 +64,8 @@ public static IDisposable BindTo( /// the . /// Type of the source. /// Type of the . -#if NET6_0_OR_GREATER - [RequiresDynamicCode("BindTo uses methods that require dynamic code generation")] - [RequiresUnreferencedCode("BindTo uses methods that may require unreferenced code")] -#endif + [RequiresUnreferencedCode("Evaluates expression-based member chains via reflection; members may be trimmed.")] + [RequiresDynamicCode("Uses dynamic binding paths which may require runtime code generation or reflection-based invocation.")] public static IDisposable BindTo( this IObservable sourceObservable, UICollectionView collectionView, @@ -102,10 +98,8 @@ public static IDisposable BindTo( /// the . /// Type of the source. /// Type of the . -#if NET6_0_OR_GREATER - [RequiresDynamicCode("BindTo uses methods that require dynamic code generation")] - [RequiresUnreferencedCode("BindTo uses methods that may require unreferenced code")] -#endif + [RequiresUnreferencedCode("Evaluates expression-based member chains via reflection; members may be trimmed.")] + [RequiresDynamicCode("Uses dynamic binding paths which may require runtime code generation or reflection-based invocation.")] public static IDisposable BindTo( this IObservable sourceObservable, UICollectionView collectionView, diff --git a/src/ReactiveUI/Platforms/uikit-common/ReactiveNavigationController.cs b/src/ReactiveUI/Platforms/uikit-common/ReactiveNavigationController.cs index 25cf896a66..496227b568 100644 --- a/src/ReactiveUI/Platforms/uikit-common/ReactiveNavigationController.cs +++ b/src/ReactiveUI/Platforms/uikit-common/ReactiveNavigationController.cs @@ -14,10 +14,6 @@ namespace ReactiveUI; /// (i.e. you can call RaiseAndSetIfChanged). /// [SuppressMessage("Design", "CA1010: Implement generic IEnumerable", Justification = "UI Kit exposes IEnumerable")] -#if NET6_0_OR_GREATER -[RequiresDynamicCode("ReactiveNavigationController inherits from ReactiveObject which uses extension methods that require dynamic code generation")] -[RequiresUnreferencedCode("ReactiveNavigationController inherits from ReactiveObject which uses extension methods that may require unreferenced code")] -#endif public abstract class ReactiveNavigationController : UINavigationController, IReactiveNotifyPropertyChanged, IHandleObservableErrors, IReactiveObject, ICanActivate, IActivatableView { private readonly Subject _activated = new(); @@ -158,10 +154,6 @@ protected override void Dispose(bool disposing) /// The view model type. [SuppressMessage("Design", "CA1010: Implement generic IEnumerable", Justification = "UI Kit exposes IEnumerable")] [SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:FileMayOnlyContainASingleType", Justification = "Classes with the same class names within.")] -#if NET6_0_OR_GREATER -[RequiresDynamicCode("ReactiveNavigationController inherits from ReactiveObject which uses extension methods that require dynamic code generation")] -[RequiresUnreferencedCode("ReactiveNavigationController inherits from ReactiveObject which uses extension methods that may require unreferenced code")] -#endif public abstract class ReactiveNavigationController : ReactiveNavigationController, IViewFor where TViewModel : class { diff --git a/src/ReactiveUI/Platforms/uikit-common/ReactivePageViewController.cs b/src/ReactiveUI/Platforms/uikit-common/ReactivePageViewController.cs index 730b5f1958..66f1b08de5 100644 --- a/src/ReactiveUI/Platforms/uikit-common/ReactivePageViewController.cs +++ b/src/ReactiveUI/Platforms/uikit-common/ReactivePageViewController.cs @@ -14,10 +14,6 @@ namespace ReactiveUI; /// (i.e. you can call RaiseAndSetIfChanged). /// [SuppressMessage("Design", "CA1010: Implement generic IEnumerable", Justification = "UI Kit exposes IEnumerable")] -#if NET6_0_OR_GREATER -[RequiresDynamicCode("ReactivePageViewController inherits from ReactiveObject which uses extension methods that require dynamic code generation")] -[RequiresUnreferencedCode("ReactivePageViewController inherits from ReactiveObject which uses extension methods that may require unreferenced code")] -#endif public abstract class ReactivePageViewController : UIPageViewController, IReactiveNotifyPropertyChanged, IHandleObservableErrors, IReactiveObject, ICanActivate { private readonly Subject _activated = new(); @@ -183,10 +179,6 @@ protected override void Dispose(bool disposing) /// The view model type. [SuppressMessage("Design", "CA1010: Implement generic IEnumerable", Justification = "UI Kit exposes IEnumerable")] [SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:FileMayOnlyContainASingleType", Justification = "Classes with the same class names within.")] -#if NET6_0_OR_GREATER -[RequiresDynamicCode("ReactivePageViewController inherits from ReactiveObject which uses extension methods that require dynamic code generation")] -[RequiresUnreferencedCode("ReactivePageViewController inherits from ReactiveObject which uses extension methods that may require unreferenced code")] -#endif public abstract class ReactivePageViewController : ReactivePageViewController, IViewFor where TViewModel : class { diff --git a/src/ReactiveUI/Platforms/uikit-common/ReactiveTabBarController.cs b/src/ReactiveUI/Platforms/uikit-common/ReactiveTabBarController.cs index 1ffe91b898..c507e07226 100644 --- a/src/ReactiveUI/Platforms/uikit-common/ReactiveTabBarController.cs +++ b/src/ReactiveUI/Platforms/uikit-common/ReactiveTabBarController.cs @@ -14,10 +14,6 @@ namespace ReactiveUI; /// (i.e. you can call RaiseAndSetIfChanged). /// [SuppressMessage("Design", "CA1010: Implement generic IEnumerable", Justification = "UI Kit exposes IEnumerable")] -#if NET6_0_OR_GREATER -[RequiresDynamicCode("ReactiveTabBarController inherits from ReactiveObject which uses extension methods that require dynamic code generation")] -[RequiresUnreferencedCode("ReactiveTabBarController inherits from ReactiveObject which uses extension methods that may require unreferenced code")] -#endif public abstract class ReactiveTabBarController : UITabBarController, IReactiveNotifyPropertyChanged, IHandleObservableErrors, IReactiveObject, ICanActivate { private readonly Subject _activated = new(); @@ -139,10 +135,6 @@ protected override void Dispose(bool disposing) /// The view model type. [SuppressMessage("Design", "CA1010: Implement generic IEnumerable", Justification = "UI Kit exposes IEnumerable")] [SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:FileMayOnlyContainASingleType", Justification = "Classes with the same class names within.")] -#if NET6_0_OR_GREATER -[RequiresDynamicCode("ReactiveTabBarController inherits from ReactiveObject which uses extension methods that require dynamic code generation")] -[RequiresUnreferencedCode("ReactiveTabBarController inherits from ReactiveObject which uses extension methods that may require unreferenced code")] -#endif public abstract class ReactiveTabBarController : ReactiveTabBarController, IViewFor where TViewModel : class { diff --git a/src/ReactiveUI/Platforms/uikit-common/ReactiveTableView.cs b/src/ReactiveUI/Platforms/uikit-common/ReactiveTableView.cs index 0b4a20d8e3..20457b441f 100644 --- a/src/ReactiveUI/Platforms/uikit-common/ReactiveTableView.cs +++ b/src/ReactiveUI/Platforms/uikit-common/ReactiveTableView.cs @@ -16,10 +16,6 @@ namespace ReactiveUI; /// (i.e. you can call RaiseAndSetIfChanged). /// [SuppressMessage("Design", "CA1010: Implement generic IEnumerable", Justification = "UI Kit exposes IEnumerable")] -#if NET6_0_OR_GREATER -[RequiresDynamicCode("ReactiveTableView inherits from ReactiveObject which uses extension methods that require dynamic code generation")] -[RequiresUnreferencedCode("ReactiveTableView inherits from ReactiveObject which uses extension methods that may require unreferenced code")] -#endif public abstract class ReactiveTableView : UITableView, IReactiveNotifyPropertyChanged, IHandleObservableErrors, IReactiveObject, ICanActivate, ICanForceManualActivation { private readonly Subject _activated = new(); @@ -140,10 +136,6 @@ protected override void Dispose(bool disposing) /// The view model type. [SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:FileMayOnlyContainASingleType", Justification = "Classes with the same class names within.")] [SuppressMessage("Design", "CA1010: Implement generic IEnumerable", Justification = "UI Kit exposes IEnumerable")] -#if NET6_0_OR_GREATER -[RequiresDynamicCode("ReactiveTableView inherits from ReactiveObject which uses extension methods that require dynamic code generation")] -[RequiresUnreferencedCode("ReactiveTableView inherits from ReactiveObject which uses extension methods that may require unreferenced code")] -#endif public abstract class ReactiveTableView : ReactiveTableView, IViewFor where TViewModel : class { diff --git a/src/ReactiveUI/Platforms/uikit-common/ReactiveTableViewCell.cs b/src/ReactiveUI/Platforms/uikit-common/ReactiveTableViewCell.cs index 9ec1a449e0..1c7ba07592 100644 --- a/src/ReactiveUI/Platforms/uikit-common/ReactiveTableViewCell.cs +++ b/src/ReactiveUI/Platforms/uikit-common/ReactiveTableViewCell.cs @@ -16,10 +16,6 @@ namespace ReactiveUI; /// (i.e. you can call RaiseAndSetIfChanged). /// [SuppressMessage("Design", "CA1010: Implement generic IEnumerable", Justification = "UI Kit exposes IEnumerable")] -#if NET6_0_OR_GREATER -[RequiresDynamicCode("ReactiveTableViewCell inherits from ReactiveObject which uses extension methods that require dynamic code generation")] -[RequiresUnreferencedCode("ReactiveTableViewCell inherits from ReactiveObject which uses extension methods that may require unreferenced code")] -#endif public abstract class ReactiveTableViewCell : UITableViewCell, IReactiveNotifyPropertyChanged, IHandleObservableErrors, IReactiveObject, ICanActivate { private readonly Subject _activated = new(); @@ -151,10 +147,6 @@ protected override void Dispose(bool disposing) /// The view model type. [SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:FileMayOnlyContainASingleType", Justification = "Classes with the same class names within.")] [SuppressMessage("Design", "CA1010: Implement generic IEnumerable", Justification = "UI Kit exposes IEnumerable")] -#if NET6_0_OR_GREATER -[RequiresDynamicCode("ReactiveTableViewCell inherits from ReactiveObject which uses extension methods that require dynamic code generation")] -[RequiresUnreferencedCode("ReactiveTableViewCell inherits from ReactiveObject which uses extension methods that may require unreferenced code")] -#endif public abstract class ReactiveTableViewCell : ReactiveTableViewCell, IViewFor where TViewModel : class { diff --git a/src/ReactiveUI/Platforms/uikit-common/ReactiveTableViewController.cs b/src/ReactiveUI/Platforms/uikit-common/ReactiveTableViewController.cs index 68b930273b..bb4c40e714 100644 --- a/src/ReactiveUI/Platforms/uikit-common/ReactiveTableViewController.cs +++ b/src/ReactiveUI/Platforms/uikit-common/ReactiveTableViewController.cs @@ -15,10 +15,6 @@ namespace ReactiveUI; /// (i.e. you can call RaiseAndSetIfChanged). /// [SuppressMessage("Design", "CA1010: Implement generic IEnumerable", Justification = "UI Kit exposes IEnumerable")] -#if NET6_0_OR_GREATER -[RequiresDynamicCode("ReactiveTableViewController inherits from ReactiveObject which uses extension methods that require dynamic code generation")] -[RequiresUnreferencedCode("ReactiveTableViewController inherits from ReactiveObject which uses extension methods that may require unreferenced code")] -#endif public abstract class ReactiveTableViewController : NSTableViewController, IReactiveNotifyPropertyChanged, IHandleObservableErrors, IReactiveObject, ICanActivate { private readonly Subject _activated = new(); @@ -149,10 +145,6 @@ protected override void Dispose(bool disposing) /// The view model type. [SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:FileMayOnlyContainASingleType", Justification = "Classes with the same class names within.")] [SuppressMessage("Design", "CA1010: Implement generic IEnumerable", Justification = "UI Kit exposes IEnumerable")] -#if NET6_0_OR_GREATER -[RequiresDynamicCode("ReactiveTableViewController inherits from ReactiveObject which uses extension methods that require dynamic code generation")] -[RequiresUnreferencedCode("ReactiveTableViewController inherits from ReactiveObject which uses extension methods that may require unreferenced code")] -#endif public abstract class ReactiveTableViewController : ReactiveTableViewController, IViewFor where TViewModel : class { diff --git a/src/ReactiveUI/Platforms/uikit-common/ReactiveTableViewSource.cs b/src/ReactiveUI/Platforms/uikit-common/ReactiveTableViewSource.cs index d44ceba6f8..5562d96990 100644 --- a/src/ReactiveUI/Platforms/uikit-common/ReactiveTableViewSource.cs +++ b/src/ReactiveUI/Platforms/uikit-common/ReactiveTableViewSource.cs @@ -18,10 +18,6 @@ namespace ReactiveUI; /// items are animated in and out as items are added. /// /// The source type. -#if NET6_0_OR_GREATER -[RequiresDynamicCode("ReactiveTableViewSource uses methods that require dynamic code generation")] -[RequiresUnreferencedCode("ReactiveTableViewSource uses methods that may require unreferenced code")] -#endif public class ReactiveTableViewSource : UITableViewSource, IReactiveNotifyPropertyChanged>, IHandleObservableErrors, IReactiveObject { private readonly CommonReactiveSource> _commonSource; @@ -36,10 +32,6 @@ public class ReactiveTableViewSource : UITableViewSource, IReactiveNoti /// The cell key. /// The size hint. /// The initialize cell action. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("ReactiveTableViewSource uses methods that require dynamic code generation")] - [RequiresUnreferencedCode("ReactiveTableViewSource uses methods that may require unreferenced code")] -#endif public ReactiveTableViewSource(UITableView tableView, INotifyCollectionChanged collection, NSString cellKey, float sizeHint, Action? initializeCellAction = null) : this(tableView) => Data = [new TableSectionInformation(collection, cellKey, sizeHint, initializeCellAction) @@ -49,10 +41,6 @@ public ReactiveTableViewSource(UITableView tableView, INotifyCollectionChanged c /// Initializes a new instance of the class. /// /// The table view. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("ReactiveTableViewSource uses methods that require dynamic code generation")] - [RequiresUnreferencedCode("ReactiveTableViewSource uses methods that may require unreferenced code")] -#endif public ReactiveTableViewSource(UITableView tableView) { _adapter = new UITableViewAdapter(tableView); @@ -283,10 +271,6 @@ public override UIView GetViewForFooter(UITableView tableView, nint section) /// /// An object that, when disposed, re-enables change /// notifications. -#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/Platforms/uikit-common/ReactiveTableViewSourceExtensions.cs b/src/ReactiveUI/Platforms/uikit-common/ReactiveTableViewSourceExtensions.cs index 0aa82b8083..9d50e757f0 100644 --- a/src/ReactiveUI/Platforms/uikit-common/ReactiveTableViewSourceExtensions.cs +++ b/src/ReactiveUI/Platforms/uikit-common/ReactiveTableViewSourceExtensions.cs @@ -31,10 +31,8 @@ public static class ReactiveTableViewSourceExtensions /// the . /// The source type. /// Type of the . -#if NET6_0_OR_GREATER - [RequiresDynamicCode("BindTo uses methods that require dynamic code generation")] - [RequiresUnreferencedCode("BindTo uses methods that may require unreferenced code")] -#endif + [RequiresUnreferencedCode("Evaluates expression-based member chains via reflection; members may be trimmed.")] + [RequiresDynamicCode("Uses dynamic binding paths which may require runtime code generation or reflection-based invocation.")] public static IDisposable BindTo( this IObservable>> sectionsObservable, UITableView tableView, @@ -70,10 +68,8 @@ public static IDisposable BindTo( /// the . /// The source type. /// Type of the . -#if NET6_0_OR_GREATER - [RequiresDynamicCode("BindTo uses methods that require dynamic code generation")] - [RequiresUnreferencedCode("BindTo uses methods that may require unreferenced code")] -#endif + [RequiresUnreferencedCode("Evaluates expression-based member chains via reflection; members may be trimmed.")] + [RequiresDynamicCode("Uses dynamic binding paths which may require runtime code generation or reflection-based invocation.")] public static IDisposable BindTo( this IObservable sourceObservable, UITableView tableView, @@ -108,10 +104,8 @@ public static IDisposable BindTo( /// the . /// The source type. /// Type of the . -#if NET6_0_OR_GREATER - [RequiresDynamicCode("BindTo uses methods that require dynamic code generation")] - [RequiresUnreferencedCode("BindTo uses methods that may require unreferenced code")] -#endif + [RequiresUnreferencedCode("Evaluates expression-based member chains via reflection; members may be trimmed.")] + [RequiresDynamicCode("Uses dynamic binding paths which may require runtime code generation or reflection-based invocation.")] public static IDisposable BindTo( this IObservable sourceObservable, UITableView tableView, diff --git a/src/ReactiveUI/Platforms/uikit-common/RoutedViewHost.cs b/src/ReactiveUI/Platforms/uikit-common/RoutedViewHost.cs index a0218b71f3..1ebab7cc3c 100644 --- a/src/ReactiveUI/Platforms/uikit-common/RoutedViewHost.cs +++ b/src/ReactiveUI/Platforms/uikit-common/RoutedViewHost.cs @@ -48,10 +48,9 @@ namespace ReactiveUI; /// /// [SuppressMessage("Design", "CA1010: Implement generic IEnumerable", Justification = "UI Kit exposes IEnumerable")] -#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 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 class RoutedViewHost : ReactiveNavigationController { private readonly SerialDisposable _titleUpdater; @@ -62,10 +61,6 @@ public class RoutedViewHost : ReactiveNavigationController /// /// 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() { ViewContractObservable = Observable.Return(null); @@ -232,10 +227,6 @@ protected override void Dispose(bool disposing) base.Dispose(disposing); } -#if NET6_0_OR_GREATER - [RequiresDynamicCode("RoutedViewHost uses methods that require dynamic code generation")] - [RequiresUnreferencedCode("RoutedViewHost uses methods that may require unreferenced code")] -#endif private NSViewController? ResolveView(IRoutableViewModel? viewModel, string? contract) { if (viewModel is null) diff --git a/src/ReactiveUI/Platforms/uikit-common/UICollectionViewAdapter.cs b/src/ReactiveUI/Platforms/uikit-common/UICollectionViewAdapter.cs index 31b22fc048..d3a79057bb 100644 --- a/src/ReactiveUI/Platforms/uikit-common/UICollectionViewAdapter.cs +++ b/src/ReactiveUI/Platforms/uikit-common/UICollectionViewAdapter.cs @@ -13,10 +13,6 @@ namespace ReactiveUI; -#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 internal class UICollectionViewAdapter : IUICollViewAdapter, IDisposable { private readonly UICollectionView _view; diff --git a/src/ReactiveUI/Platforms/uikit-common/UITableViewAdapter.cs b/src/ReactiveUI/Platforms/uikit-common/UITableViewAdapter.cs index cf52973f8d..e85190f502 100644 --- a/src/ReactiveUI/Platforms/uikit-common/UITableViewAdapter.cs +++ b/src/ReactiveUI/Platforms/uikit-common/UITableViewAdapter.cs @@ -38,10 +38,6 @@ internal UITableViewAdapter(UITableView view) public UITableViewRowAnimation ReloadRowsAnimation { get; set; } = UITableViewRowAnimation.Automatic; -#if NET6_0_OR_GREATER - [RequiresDynamicCode("ReloadData uses methods that require dynamic code generation")] - [RequiresUnreferencedCode("ReloadData uses methods that may require unreferenced code")] -#endif public void ReloadData() { ++_inFlightReloads; diff --git a/src/ReactiveUI/Helpers/CallerArgumentExpressionAttribute.cs b/src/ReactiveUI/Polyfills/CallerArgumentExpressionAttribute.cs similarity index 89% rename from src/ReactiveUI/Helpers/CallerArgumentExpressionAttribute.cs rename to src/ReactiveUI/Polyfills/CallerArgumentExpressionAttribute.cs index fb1b7f5ad6..2914c4488f 100644 --- a/src/ReactiveUI/Helpers/CallerArgumentExpressionAttribute.cs +++ b/src/ReactiveUI/Polyfills/CallerArgumentExpressionAttribute.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. +// Polyfill implementation adapted from SimonCropp/Polyfill +// https://github.com/SimonCropp/Polyfill #if !NET5_0_OR_GREATER using System.Diagnostics; @@ -12,6 +14,7 @@ namespace System.Runtime.CompilerServices; /// /// Indicates that a parameter captures the expression passed for another parameter as a string. +/// Modification of Using SimonCropp's polyfill's library. /// [ExcludeFromCodeCoverage] [DebuggerNonUserCode] diff --git a/src/ReactiveUI/Polyfills/DoesNotReturnIfAttribute.cs b/src/ReactiveUI/Polyfills/DoesNotReturnIfAttribute.cs new file mode 100644 index 0000000000..109174f661 --- /dev/null +++ b/src/ReactiveUI/Polyfills/DoesNotReturnIfAttribute.cs @@ -0,0 +1,44 @@ +// 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. + +// Polyfill implementation adapted from SimonCropp/Polyfill +// https://github.com/SimonCropp/Polyfill +#if !NETCOREAPP3_0_OR_GREATER && !NETSTANDARD2_1_OR_GREATER + +using System.Diagnostics; + +namespace System.Diagnostics.CodeAnalysis; + +/// +/// Specifies that the method will not return if the associated +/// parameter is passed the specified value. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +[AttributeUsage(AttributeTargets.Parameter)] +internal sealed class DoesNotReturnIfAttribute : Attribute +{ + /// + /// Initializes a new instance of the + /// class with the specified parameter value. + /// + /// + /// The condition parameter value. Code after the method is considered unreachable + /// by diagnostics if the argument to the associated parameter matches this value. + /// + public DoesNotReturnIfAttribute(bool parameterValue) => + ParameterValue = parameterValue; + + /// + /// Gets a value indicating whether code after the method is considered unreachable + /// by diagnostics if the argument to the associated parameter matches this value. + /// + public bool ParameterValue { get; } +} +#else +using System.Runtime.CompilerServices; + +[assembly: TypeForwardedTo(typeof(System.Diagnostics.CodeAnalysis.DoesNotReturnIfAttribute))] +#endif diff --git a/src/ReactiveUI/Polyfills/DynamicallyAccessedMemberTypes.cs b/src/ReactiveUI/Polyfills/DynamicallyAccessedMemberTypes.cs new file mode 100644 index 0000000000..aef5a6f789 --- /dev/null +++ b/src/ReactiveUI/Polyfills/DynamicallyAccessedMemberTypes.cs @@ -0,0 +1,106 @@ +// 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. + +// Polyfill implementation adapted from SimonCropp/Polyfill +// https://github.com/SimonCropp/Polyfill +#if !NET + +namespace System.Diagnostics.CodeAnalysis; + +/// +/// Specifies the types of members that are dynamically accessed. +/// +/// This enumeration has a attribute that allows a +/// bitwise combination of its member values. +/// +[Flags] +internal enum DynamicallyAccessedMemberTypes +{ + /// + /// Specifies no members. + /// + None = 0, + + /// + /// Specifies the default, parameterless public constructor. + /// + PublicParameterlessConstructor = 0x0001, + + /// + /// Specifies all public constructors. + /// + PublicConstructors = 0x0002 | PublicParameterlessConstructor, + + /// + /// Specifies all non-public constructors. + /// + NonPublicConstructors = 0x0004, + + /// + /// Specifies all public methods. + /// + PublicMethods = 0x0008, + + /// + /// Specifies all non-public methods. + /// + NonPublicMethods = 0x0010, + + /// + /// Specifies all public fields. + /// + PublicFields = 0x0020, + + /// + /// Specifies all non-public fields. + /// + NonPublicFields = 0x0040, + + /// + /// Specifies all public nested types. + /// + PublicNestedTypes = 0x0080, + + /// + /// Specifies all non-public nested types. + /// + NonPublicNestedTypes = 0x0100, + + /// + /// Specifies all public properties. + /// + PublicProperties = 0x0200, + + /// + /// Specifies all non-public properties. + /// + NonPublicProperties = 0x0400, + + /// + /// Specifies all public events. + /// + PublicEvents = 0x0800, + + /// + /// Specifies all non-public events. + /// + NonPublicEvents = 0x1000, + + /// + /// Specifies all interfaces implemented by the type. + /// + Interfaces = 0x2000, + + /// + /// Specifies all members. + /// + All = ~None +} + +#else +using System.Runtime.CompilerServices; + +[assembly: TypeForwardedTo(typeof(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes))] +#endif diff --git a/src/ReactiveUI/Polyfills/DynamicallyAccessedMembersAttribute.cs b/src/ReactiveUI/Polyfills/DynamicallyAccessedMembersAttribute.cs new file mode 100644 index 0000000000..d25f053604 --- /dev/null +++ b/src/ReactiveUI/Polyfills/DynamicallyAccessedMembersAttribute.cs @@ -0,0 +1,54 @@ +// 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. + +// Polyfill implementation adapted from SimonCropp/Polyfill +// https://github.com/SimonCropp/Polyfill +#if !NET + +using System.Diagnostics; + +namespace System.Diagnostics.CodeAnalysis; + +using Targets = System.AttributeTargets; + +/// +/// Indicates that certain members on a specified are accessed dynamically, +/// for example through . +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +[AttributeUsage( + validOn: Targets.Class | + Targets.Field | + Targets.GenericParameter | + Targets.Interface | + Targets.Method | + Targets.Parameter | + Targets.Property | + Targets.ReturnValue | + Targets.Struct, + Inherited = false)] +internal sealed class DynamicallyAccessedMembersAttribute : Attribute +{ + /// + /// Initializes a new instance of the class + /// with the specified member types. + /// + /// The types of members dynamically accessed. + public DynamicallyAccessedMembersAttribute(DynamicallyAccessedMemberTypes memberTypes) => + MemberTypes = memberTypes; + + /// + /// Gets the which specifies the type + /// of members dynamically accessed. + /// + public DynamicallyAccessedMemberTypes MemberTypes { get; } +} + +#else +using System.Runtime.CompilerServices; + +[assembly: TypeForwardedTo(typeof(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute))] +#endif diff --git a/src/ReactiveUI/Polyfills/IsExternalInit.cs b/src/ReactiveUI/Polyfills/IsExternalInit.cs new file mode 100644 index 0000000000..aad8d6f2fb --- /dev/null +++ b/src/ReactiveUI/Polyfills/IsExternalInit.cs @@ -0,0 +1,27 @@ +// 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. + +// Polyfill implementation adapted from SimonCropp/Polyfill +// https://github.com/SimonCropp/Polyfill +#if !NET + +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +namespace System.Runtime.CompilerServices; + +/// +/// Reserved to be used by the compiler for tracking metadata. This class should not be used by developers in source code. +/// Modification of Using SimonCropp's polyfill's library. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +internal static class IsExternalInit; + +#else +using System.Runtime.CompilerServices; + +[assembly: TypeForwardedTo(typeof(IsExternalInit))] +#endif diff --git a/src/ReactiveUI/Polyfills/MaybeNullWhenAttribute.cs b/src/ReactiveUI/Polyfills/MaybeNullWhenAttribute.cs new file mode 100644 index 0000000000..d7d18bb18e --- /dev/null +++ b/src/ReactiveUI/Polyfills/MaybeNullWhenAttribute.cs @@ -0,0 +1,45 @@ +// 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. + +// Polyfill implementation adapted from SimonCropp/Polyfill +// https://github.com/SimonCropp/Polyfill +#if !NETCOREAPP3_0_OR_GREATER && !NETSTANDARD2_1_OR_GREATER + +using System.Diagnostics; + +namespace System.Diagnostics.CodeAnalysis; + +/// +/// Specifies that when a method returns , +/// the parameter may be even if the corresponding type disallows it. +/// Modification of Using SimonCropp's polyfill's library. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +[AttributeUsage(AttributeTargets.Parameter)] +internal sealed class MaybeNullWhenAttribute : Attribute +{ + /// + /// Initializes a new instance of the class. + /// + /// + /// The return value condition. If the method returns this value, + /// the associated parameter may be . + /// + public MaybeNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; + + /// + /// Gets a value indicating whether the return condition has been satisfied. + /// If the method returns this value, the associated parameter may be . + /// + public bool ReturnValue { get; } +} + +#else +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +[assembly: TypeForwardedTo(typeof(MaybeNullWhenAttribute))] +#endif diff --git a/src/ReactiveUI/Polyfills/MemberNotNullAttribute.cs b/src/ReactiveUI/Polyfills/MemberNotNullAttribute.cs new file mode 100644 index 0000000000..6f5e5c2d99 --- /dev/null +++ b/src/ReactiveUI/Polyfills/MemberNotNullAttribute.cs @@ -0,0 +1,50 @@ +// 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 !NET + +using System.Diagnostics; + +using Targets = System.AttributeTargets; + +/// +/// Specifies that the method or property will ensure that the listed field and property members have +/// not- values. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +[AttributeUsage( + validOn: Targets.Method | + Targets.Property, + Inherited = false, + AllowMultiple = true)] +internal sealed class MemberNotNullAttribute : + Attribute +{ + /// + /// Initializes a new instance of the class. + /// + /// Field or property member name. + public MemberNotNullAttribute(string member) => + Members = [member]; + + /// + /// Initializes a new instance of the class. + /// + /// Field or property member names. + public MemberNotNullAttribute(params string[] members) => + Members = members; + + /// + /// Gets field or property member names. + /// + public string[] Members { get; } +} + +#else +using System.Runtime.CompilerServices; + +[assembly: TypeForwardedTo(typeof(System.Diagnostics.CodeAnalysis.MemberNotNullAttribute))] +#endif diff --git a/src/ReactiveUI/Helpers/NotNullAttribute.cs b/src/ReactiveUI/Polyfills/NotNullAttribute.cs similarity index 83% rename from src/ReactiveUI/Helpers/NotNullAttribute.cs rename to src/ReactiveUI/Polyfills/NotNullAttribute.cs index c0113756ba..6400c56764 100644 --- a/src/ReactiveUI/Helpers/NotNullAttribute.cs +++ b/src/ReactiveUI/Polyfills/NotNullAttribute.cs @@ -3,16 +3,16 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. +// Polyfill implementation adapted from SimonCropp/Polyfill +// https://github.com/SimonCropp/Polyfill #if !NETCOREAPP3_0_OR_GREATER && !NETSTANDARD2_1_OR_GREATER -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; - namespace System.Diagnostics.CodeAnalysis; /// /// Specifies that an output is not even if the /// corresponding type allows it. +/// Modification of Using SimonCropp's polyfill's library. /// [ExcludeFromCodeCoverage] [DebuggerNonUserCode] @@ -22,8 +22,4 @@ namespace System.Diagnostics.CodeAnalysis; AttributeTargets.Property | AttributeTargets.ReturnValue)] internal sealed class NotNullAttribute : Attribute; -#else -using System.Runtime.CompilerServices; - -[assembly: TypeForwardedTo(typeof(NotNullAttribute))] #endif diff --git a/src/ReactiveUI/Polyfills/NotNullWhenAttribute.cs b/src/ReactiveUI/Polyfills/NotNullWhenAttribute.cs new file mode 100644 index 0000000000..f5292b4e26 --- /dev/null +++ b/src/ReactiveUI/Polyfills/NotNullWhenAttribute.cs @@ -0,0 +1,39 @@ +// 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. + +// Polyfill implementation adapted from SimonCropp/Polyfill +// https://github.com/SimonCropp/Polyfill +#if !NETCOREAPP3_0_OR_GREATER && !NETSTANDARD2_1_OR_GREATER + +namespace System.Diagnostics.CodeAnalysis; + +/// +/// Specifies that when a method returns , +/// the parameter will not be even if the corresponding type allows it. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +[AttributeUsage(AttributeTargets.Parameter)] +internal sealed class NotNullWhenAttribute : + Attribute +{ + /// + /// Initializes a new instance of the class. + /// + public NotNullWhenAttribute(bool returnValue) => + ReturnValue = returnValue; + + /// + /// Gets a value indicating whether it is a return value condition. + /// If the method returns this value, the associated parameter will not be . + /// + public bool ReturnValue { get; } +} + +#else +using System.Runtime.CompilerServices; + +[assembly: TypeForwardedTo(typeof(System.Diagnostics.CodeAnalysis.NotNullWhenAttribute))] +#endif diff --git a/src/ReactiveUI/Polyfills/RequiresDynamicCodeAttribute.cs b/src/ReactiveUI/Polyfills/RequiresDynamicCodeAttribute.cs new file mode 100644 index 0000000000..e576234453 --- /dev/null +++ b/src/ReactiveUI/Polyfills/RequiresDynamicCodeAttribute.cs @@ -0,0 +1,57 @@ +// 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. + +// Polyfill implementation adapted from Simon Cropp's Polyfill library +// https://github.com/SimonCropp/Polyfill +#if !NET7_0_OR_GREATER + +#nullable enable + +using Targets = System.AttributeTargets; + +namespace System.Diagnostics.CodeAnalysis; + +/// +/// Indicates that the specified method requires the ability to generate new code at runtime, +/// for example through . +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +[AttributeUsage( + validOn: Targets.Method | + Targets.Constructor | + Targets.Class, + Inherited = false)] +internal sealed class RequiresDynamicCodeAttribute : + Attribute +{ + /// + /// Initializes a new instance of the class + /// with the specified message. + /// + public RequiresDynamicCodeAttribute(string message) => + Message = message; + + /// + /// Gets or sets a value indicating whether the annotation should not apply to static members. + /// + public bool ExcludeStatics { get; set; } + + /// + /// Gets a message that contains information about the usage of dynamic code. + /// + public string Message { get; } + + /// + /// Gets or sets an optional URL that contains more information about the method, + /// why it requires dynamic code, and what options a consumer has to deal with it. + /// + public string? Url { get; set; } +} +#else +using System.Runtime.CompilerServices; + +[assembly: TypeForwardedTo(typeof(System.Diagnostics.CodeAnalysis.RequiresDynamicCodeAttribute))] +#endif diff --git a/src/ReactiveUI/Polyfills/RequiresUnreferencedCodeAttribute.cs b/src/ReactiveUI/Polyfills/RequiresUnreferencedCodeAttribute.cs new file mode 100644 index 0000000000..481e7d030b --- /dev/null +++ b/src/ReactiveUI/Polyfills/RequiresUnreferencedCodeAttribute.cs @@ -0,0 +1,52 @@ +// 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. + +// Polyfill implementation adapted from Simon Cropp's Polyfill library +// https://github.com/SimonCropp/Polyfill +#if !NET + +namespace System.Diagnostics.CodeAnalysis; + +/// +/// Indicates that the specified method requires dynamic access to code that is not referenced +/// statically, for example through . +/// +/// +/// Link: https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.codeanalysis.requiresunreferencedcodeattribute. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +[AttributeUsage( + AttributeTargets.Method | + AttributeTargets.Constructor | + AttributeTargets.Class, + Inherited = false)] +internal sealed class RequiresUnreferencedCodeAttribute : Attribute +{ + /// + /// Initializes a new instance of the class + /// with the specified message. + /// + /// A message that contains information about the usage of unreferenced code. + public RequiresUnreferencedCodeAttribute(string message) => + Message = message; + + /// + /// Gets a message that contains information about the usage of unreferenced code. + /// + public string Message { get; } + + /// + /// Gets or sets an optional URL that contains more information about the method, + /// why it requires unreferenced code, and what options a consumer has to deal with it. + /// + public string? Url { get; set; } +} + +#else +using System.Runtime.CompilerServices; + +[assembly: TypeForwardedTo(typeof(System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute))] +#endif diff --git a/src/ReactiveUI/Polyfills/UnconditionalSuppressMessageAttribute.cs b/src/ReactiveUI/Polyfills/UnconditionalSuppressMessageAttribute.cs new file mode 100644 index 0000000000..143bbe2309 --- /dev/null +++ b/src/ReactiveUI/Polyfills/UnconditionalSuppressMessageAttribute.cs @@ -0,0 +1,74 @@ +// 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. + +// Polyfill implementation adapted from Simon Cropp's Polyfill library +// https://github.com/SimonCropp/Polyfill +#if !NET + +namespace System.Diagnostics.CodeAnalysis; + +/// +/// Suppresses reporting of a specific rule violation, allowing multiple suppressions on a +/// single code artifact. +/// +/// +/// Link: https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.codeanalysis.unconditionalsuppressmessageattribute. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +[AttributeUsage( + AttributeTargets.All, + Inherited = false, + AllowMultiple = true)] +internal sealed class UnconditionalSuppressMessageAttribute : Attribute +{ + /// + /// Initializes a new instance of the + /// class, specifying the category of the tool and the identifier for an analysis rule. + /// + /// The category identifying the classification of the attribute. + /// The identifier of the analysis tool rule to be suppressed. + public UnconditionalSuppressMessageAttribute(string category, string checkId) + { + Category = category; + CheckId = checkId; + } + + /// + /// Gets the category identifying the classification of the attribute. + /// + public string Category { get; } + + /// + /// Gets the identifier of the analysis tool rule to be suppressed. + /// + public string CheckId { get; } + + /// + /// Gets or sets the scope of the code that is relevant for the attribute. + /// + public string? Scope { get; set; } + + /// + /// Gets or sets a fully qualified path that represents the target of the attribute. + /// + public string? Target { get; set; } + + /// + /// Gets or sets an optional argument expanding on exclusion criteria. + /// + public string? MessageId { get; set; } + + /// + /// Gets or sets the justification for suppressing the code analysis message. + /// + public string? Justification { get; set; } +} + +#else +using System.Runtime.CompilerServices; + +[assembly: TypeForwardedTo(typeof(System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessageAttribute))] +#endif diff --git a/src/ReactiveUI/ReactiveCommand/CombinedReactiveCommand.cs b/src/ReactiveUI/ReactiveCommand/CombinedReactiveCommand.cs index 7559dde91f..d0a5a56a7b 100644 --- a/src/ReactiveUI/ReactiveCommand/CombinedReactiveCommand.cs +++ b/src/ReactiveUI/ReactiveCommand/CombinedReactiveCommand.cs @@ -45,10 +45,6 @@ public class CombinedReactiveCommand : ReactiveCommandBaseThe scheduler where to dispatch the output from the command. /// Fires when required arguments are null. /// Fires if the child commands container is empty. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("CombinedReactiveCommand uses RxApp and ReactiveCommand which require dynamic code generation.")] - [RequiresUnreferencedCode("CombinedReactiveCommand uses RxApp and ReactiveCommand which may require unreferenced code.")] -#endif protected internal CombinedReactiveCommand( IEnumerable> childCommands, IObservable? canExecute, @@ -56,7 +52,7 @@ protected internal CombinedReactiveCommand( { ArgumentExceptionHelper.ThrowIfNull(childCommands); - _outputScheduler = outputScheduler ?? RxApp.MainThreadScheduler; + _outputScheduler = outputScheduler ?? RxSchedulers.MainThreadScheduler; var childCommandsArray = childCommands.ToArray(); @@ -65,7 +61,7 @@ protected internal CombinedReactiveCommand( throw new ArgumentException("No child commands provided.", nameof(childCommands)); } - _exceptions = new ScheduledSubject(_outputScheduler, RxApp.DefaultExceptionHandler); + _exceptions = new ScheduledSubject(_outputScheduler, RxState.DefaultExceptionHandler); var canChildrenExecute = childCommandsArray.Select(x => x.CanExecute) .CombineLatest() diff --git a/src/ReactiveUI/ReactiveCommand/ReactiveCommand.cs b/src/ReactiveUI/ReactiveCommand/ReactiveCommand.cs index 551cfcf174..0527496531 100644 --- a/src/ReactiveUI/ReactiveCommand/ReactiveCommand.cs +++ b/src/ReactiveUI/ReactiveCommand/ReactiveCommand.cs @@ -72,10 +72,6 @@ public static class ReactiveCommand /// The ReactiveCommand instance. /// /// execute. -#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 ReactiveCommand Create( Action execute, IObservable? canExecute = null, @@ -107,10 +103,6 @@ public static ReactiveCommand Create( /// The ReactiveCommand instance. /// /// execute. -#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 ReactiveCommand CreateRunInBackground( Action execute, IObservable? canExecute = null, @@ -119,7 +111,7 @@ public static ReactiveCommand CreateRunInBackground( { ArgumentExceptionHelper.ThrowIfNull(execute); - return CreateFromObservable(() => Observable.Start(execute, backgroundScheduler ?? RxApp.TaskpoolScheduler), canExecute, outputScheduler); + return CreateFromObservable(() => Observable.Start(execute, backgroundScheduler ?? RxSchedulers.TaskpoolScheduler), canExecute, outputScheduler); } /// @@ -134,10 +126,6 @@ public static ReactiveCommand CreateRunInBackground( /// The ReactiveCommand instance. /// /// execute. -#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 ReactiveCommand Create( Func execute, IObservable? canExecute = null, @@ -170,10 +158,6 @@ public static ReactiveCommand Create( /// The ReactiveCommand instance. /// /// execute. -#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 ReactiveCommand CreateRunInBackground( Func execute, IObservable? canExecute = null, @@ -182,7 +166,7 @@ public static ReactiveCommand CreateRunInBackground( { ArgumentExceptionHelper.ThrowIfNull(execute); - return CreateFromObservable(() => Observable.Start(execute, backgroundScheduler ?? RxApp.TaskpoolScheduler), canExecute, outputScheduler); + return CreateFromObservable(() => Observable.Start(execute, backgroundScheduler ?? RxSchedulers.TaskpoolScheduler), canExecute, outputScheduler); } /// @@ -196,10 +180,6 @@ public static ReactiveCommand CreateRunInBackground( /// The ReactiveCommand instance. /// /// execute. -#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 ReactiveCommand Create( Action execute, IObservable? canExecute = null, @@ -232,10 +212,6 @@ public static ReactiveCommand Create( /// The ReactiveCommand instance. /// /// execute. -#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 ReactiveCommand CreateRunInBackground( Action execute, IObservable? canExecute = null, @@ -244,7 +220,7 @@ public static ReactiveCommand CreateRunInBackground( { ArgumentExceptionHelper.ThrowIfNull(execute); - return CreateFromObservable(p => Observable.Start(() => execute(p), backgroundScheduler ?? RxApp.TaskpoolScheduler), canExecute, outputScheduler); + return CreateFromObservable(p => Observable.Start(() => execute(p), backgroundScheduler ?? RxSchedulers.TaskpoolScheduler), canExecute, outputScheduler); } /// @@ -260,10 +236,6 @@ public static ReactiveCommand CreateRunInBackground( /// The ReactiveCommand instance. /// /// execute. -#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 ReactiveCommand Create( Func execute, IObservable? canExecute = null, @@ -297,10 +269,6 @@ public static ReactiveCommand Create( /// The ReactiveCommand instance. /// /// execute. -#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 ReactiveCommand CreateRunInBackground( Func execute, IObservable? canExecute = null, @@ -309,7 +277,7 @@ public static ReactiveCommand CreateRunInBackground(p => Observable.Start(() => execute(p), backgroundScheduler ?? RxApp.TaskpoolScheduler), canExecute, outputScheduler); + return CreateFromObservable(p => Observable.Start(() => execute(p), backgroundScheduler ?? RxSchedulers.TaskpoolScheduler), canExecute, outputScheduler); } /// @@ -334,10 +302,6 @@ public static ReactiveCommand CreateRunInBackground /// The type of the command's result. /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("CreateCombined uses CombinedReactiveCommand which requires dynamic code generation.")] - [RequiresUnreferencedCode("CreateCombined uses CombinedReactiveCommand which may require unreferenced code.")] -#endif public static CombinedReactiveCommand CreateCombined( IEnumerable> childCommands, IObservable? canExecute = null, @@ -366,10 +330,6 @@ public static CombinedReactiveCommand CreateCombined /// The type of the command's result. /// -#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 ReactiveCommand CreateFromObservable( Func> execute, IObservable? canExecute = null, @@ -404,10 +364,6 @@ public static ReactiveCommand CreateFromObservable( /// /// The type of the command's result. /// -#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 ReactiveCommand CreateFromObservable( Func> execute, IObservable? canExecute = null, @@ -439,10 +395,6 @@ public static ReactiveCommand CreateFromObservable /// The type of the command's result. /// -#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 ReactiveCommand CreateFromTask( Func> execute, IObservable? canExecute = null, @@ -471,10 +423,6 @@ public static ReactiveCommand CreateFromTask( /// /// The type of the command's result. /// -#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 ReactiveCommand CreateFromTask( Func> execute, IObservable? canExecute = null, @@ -500,10 +448,6 @@ public static ReactiveCommand CreateFromTask( /// /// The ReactiveCommand instance. /// -#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 ReactiveCommand CreateFromTask( Func execute, IObservable? canExecute = null, @@ -529,10 +473,6 @@ public static ReactiveCommand CreateFromTask( /// /// The ReactiveCommand instance. /// -#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 ReactiveCommand CreateFromTask( Func execute, IObservable? canExecute = null, @@ -564,10 +504,6 @@ public static ReactiveCommand CreateFromTask( /// /// The type of the command's result. /// -#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 ReactiveCommand CreateFromTask( Func> execute, IObservable? canExecute = null, @@ -602,10 +538,6 @@ public static ReactiveCommand CreateFromTask( /// /// The type of the command's result. /// -#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 ReactiveCommand CreateFromTask( Func> execute, IObservable? canExecute = null, @@ -637,10 +569,6 @@ public static ReactiveCommand CreateFromTask( /// /// The type of the parameter passed through to command execution. /// -#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 ReactiveCommand CreateFromTask( Func execute, IObservable? canExecute = null, @@ -672,10 +600,6 @@ public static ReactiveCommand CreateFromTask( /// /// The type of the parameter passed through to command execution. /// -#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 ReactiveCommand CreateFromTask( Func execute, IObservable? canExecute = null, @@ -701,10 +625,6 @@ public static ReactiveCommand CreateFromTask( /// The ReactiveCommand instance. /// /// execute. -#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 internal static ReactiveCommand CreateFromObservableCancellable( Func Result, Action Cancel)>> execute, IObservable? canExecute = null, @@ -739,10 +659,6 @@ internal static ReactiveCommand CreateFromObservableCancellable /// /// The type of the command's result. /// -#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 internal static ReactiveCommand CreateFromObservableCancellable( Func Result, Action Cancel)>> execute, IObservable? canExecute = null, @@ -803,18 +719,14 @@ public class ReactiveCommand : ReactiveCommandBase /// Thrown if any dependent parameters are null. -#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 protected internal ReactiveCommand( Func Result, Action Cancel)>> execute, IObservable? canExecute, IScheduler? outputScheduler) { _execute = execute ?? throw new ArgumentNullException(nameof(execute)); - _outputScheduler = outputScheduler ?? RxApp.MainThreadScheduler; - _exceptions = new ScheduledSubject(_outputScheduler, RxApp.DefaultExceptionHandler); + _outputScheduler = outputScheduler ?? RxSchedulers.MainThreadScheduler; + _exceptions = new ScheduledSubject(_outputScheduler, RxState.DefaultExceptionHandler); _executionInfo = new Subject(); _synchronizedExecutionInfo = Subject.Synchronize(_executionInfo, _outputScheduler); _isExecuting = _synchronizedExecutionInfo @@ -861,10 +773,6 @@ protected internal ReactiveCommand( /// execute. /// /// Thrown if any dependent parameters are null. -#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 protected internal ReactiveCommand( Func> execute, IObservable? canExecute, @@ -883,7 +791,7 @@ protected internal ReactiveCommand( }); }, canExecute, - outputScheduler ?? RxApp.MainThreadScheduler) + outputScheduler ?? RxSchedulers.MainThreadScheduler) { } diff --git a/src/ReactiveUI/ReactiveCommand/ReactiveCommandMixins.cs b/src/ReactiveUI/ReactiveCommand/ReactiveCommandMixins.cs index 7f9f15cb03..15145b36e4 100644 --- a/src/ReactiveUI/ReactiveCommand/ReactiveCommandMixins.cs +++ b/src/ReactiveUI/ReactiveCommand/ReactiveCommandMixins.cs @@ -88,10 +88,7 @@ command is null /// The expression to reference the Command. /// An object that when disposes, disconnects the Observable /// from the command. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("InvokeCommand uses WhenAnyValue which requires dynamic code generation for expression tree analysis.")] - [RequiresUnreferencedCode("InvokeCommand uses WhenAnyValue which may reference members that could be trimmed.")] -#endif + [RequiresUnreferencedCode("Evaluates expression-based member chains via reflection; members may be trimmed.")] public static IDisposable InvokeCommand(this IObservable item, TTarget? target, Expression> commandProperty) where TTarget : class { @@ -125,10 +122,7 @@ public static IDisposable InvokeCommand(this IObservable item, TT /// The expression to reference the Command. /// An object that when disposes, disconnects the Observable /// from the command. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("InvokeCommand uses WhenAnyValue which requires dynamic code generation for expression tree analysis.")] - [RequiresUnreferencedCode("InvokeCommand uses WhenAnyValue which may reference members that could be trimmed.")] -#endif + [RequiresUnreferencedCode("Evaluates expression-based member chains via reflection; members may be trimmed.")] public static IDisposable InvokeCommand(this IObservable item, TTarget? target, Expression?>> commandProperty) where TTarget : class { diff --git a/src/ReactiveUI/ReactiveObject/IReactiveObjectExtensions.cs b/src/ReactiveUI/ReactiveObject/IReactiveObjectExtensions.cs index d64f06b200..b723823051 100644 --- a/src/ReactiveUI/ReactiveObject/IReactiveObjectExtensions.cs +++ b/src/ReactiveUI/ReactiveObject/IReactiveObjectExtensions.cs @@ -258,7 +258,7 @@ internal static IDisposable DelayChangeNotifications(this TSender react private class ExtensionState : IExtensionState where TSender : IReactiveObject { - private readonly Lazy> _thrownExceptions = new(static () => new ScheduledSubject(Scheduler.Immediate, RxApp.DefaultExceptionHandler)); + private readonly Lazy> _thrownExceptions = new(static () => new ScheduledSubject(Scheduler.Immediate, RxState.DefaultExceptionHandler)); private readonly Lazy> _startOrStopDelayingChangeNotifications = new(); private readonly TSender _sender; private readonly Lazy<(ISubject> subject, IObservable> observable)> _changing; diff --git a/src/ReactiveUI/ReactiveObject/ReactiveObject.cs b/src/ReactiveUI/ReactiveObject/ReactiveObject.cs index e7d2b46178..5d52b5e54e 100644 --- a/src/ReactiveUI/ReactiveObject/ReactiveObject.cs +++ b/src/ReactiveUI/ReactiveObject/ReactiveObject.cs @@ -100,10 +100,6 @@ void IReactiveObject.RaisePropertyChanged(PropertyChangedEventArgs args) => PropertyChangedHandler?.Invoke(this, args); /// -#if NET6_0_OR_GREATER - [RequiresUnreferencedCode("This method uses reflection to access properties by name.")] - [RequiresDynamicCode("This method uses reflection to access properties by name.")] -#endif public IDisposable SuppressChangeNotifications() => // TODO: Create Test IReactiveObjectExtensions.SuppressChangeNotifications(this); diff --git a/src/ReactiveUI/ReactiveObject/ReactiveRecord.cs b/src/ReactiveUI/ReactiveObject/ReactiveRecord.cs index 4dbc99077e..32c78f47bd 100644 --- a/src/ReactiveUI/ReactiveObject/ReactiveRecord.cs +++ b/src/ReactiveUI/ReactiveObject/ReactiveRecord.cs @@ -19,10 +19,6 @@ public record ReactiveRecord : IReactiveNotifyPropertyChanged, /// /// Initializes a new instance of the class. /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("ReactiveRecord constructor uses extension methods that require dynamic code generation")] - [RequiresUnreferencedCode("ReactiveRecord constructor uses extension methods that may require unreferenced code")] -#endif public ReactiveRecord() { } @@ -101,10 +97,6 @@ public event PropertyChangedEventHandler? PropertyChanged void IReactiveObject.RaisePropertyChanged(PropertyChangedEventArgs args) => PropertyChangedHandler?.Invoke(this, args); /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("SuppressChangeNotifications uses extension methods that require dynamic code generation")] - [RequiresUnreferencedCode("SuppressChangeNotifications uses extension methods that may require unreferenced code")] -#endif public IDisposable SuppressChangeNotifications() => // TODO: Create Test IReactiveObjectExtensions.SuppressChangeNotifications(this); @@ -112,10 +104,6 @@ public IDisposable SuppressChangeNotifications() => // TODO: Create Test /// Determines if change notifications are enabled or not. /// /// A value indicating whether change notifications are enabled. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("AreChangeNotificationsEnabled uses extension methods that require dynamic code generation")] - [RequiresUnreferencedCode("AreChangeNotificationsEnabled uses extension methods that may require unreferenced code")] -#endif public bool AreChangeNotificationsEnabled() => // TODO: Create Test IReactiveObjectExtensions.AreChangeNotificationsEnabled(this); @@ -123,10 +111,6 @@ public bool AreChangeNotificationsEnabled() => // TODO: Create Test /// Delays notifications until the return IDisposable is disposed. /// /// A disposable which when disposed will send delayed notifications. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("DelayChangeNotifications uses extension methods that require dynamic code generation")] - [RequiresUnreferencedCode("DelayChangeNotifications uses extension methods that may require unreferenced code")] -#endif public IDisposable DelayChangeNotifications() => // TODO: Create Test IReactiveObjectExtensions.DelayChangeNotifications(this); } diff --git a/src/ReactiveUI/ReactiveProperty/IReactiveProperty.cs b/src/ReactiveUI/ReactiveProperty/IReactiveProperty.cs index 53fc8ce327..a2d688faea 100644 --- a/src/ReactiveUI/ReactiveProperty/IReactiveProperty.cs +++ b/src/ReactiveUI/ReactiveProperty/IReactiveProperty.cs @@ -38,9 +38,5 @@ public interface IReactiveProperty : IObservable, ICancelable, INotifyDat /// /// Refreshes this instance. /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("Refresh uses RaisePropertyChanged which requires dynamic code generation")] - [RequiresUnreferencedCode("Refresh uses RaisePropertyChanged which may require unreferenced code")] -#endif void Refresh(); } diff --git a/src/ReactiveUI/ReactiveProperty/ReactiveProperty.cs b/src/ReactiveUI/ReactiveProperty/ReactiveProperty.cs index 0ff7652171..9c5759998e 100644 --- a/src/ReactiveUI/ReactiveProperty/ReactiveProperty.cs +++ b/src/ReactiveUI/ReactiveProperty/ReactiveProperty.cs @@ -4,6 +4,7 @@ // See the LICENSE file in the project root for full license information. using System.Collections; +using System.Runtime.CompilerServices; namespace ReactiveUI; @@ -14,36 +15,93 @@ namespace ReactiveUI; /// /// [DataContract] -#if NET6_0_OR_GREATER -[RequiresDynamicCode("ReactiveProperty initialization uses ReactiveObject and RxApp which require dynamic code generation")] -[RequiresUnreferencedCode("ReactiveProperty initialization uses ReactiveObject and RxApp which may require unreferenced code")] -#endif public class ReactiveProperty : ReactiveObject, IReactiveProperty { + /// + /// The scheduler used to marshal validation and error notifications. + /// private readonly IScheduler _scheduler; + + /// + /// Holds disposables associated with the instance lifetime. + /// private readonly CompositeDisposable _disposables = []; + + /// + /// The equality comparer used to compare incoming values to the current value. + /// private readonly EqualityComparer _checkIf = EqualityComparer.Default; + + /// + /// Publishes value changes to the validation pipeline. + /// private readonly Subject _checkValidation = new(); + + /// + /// Publishes "refresh" signals for the current value (used to force emission even when the value is unchanged). + /// private readonly Subject _valueRefereshed = new(); + + /// + /// Publishes changes without relying on reflection-based property observation. + /// + /// + /// + /// This subject replaces the prior WhenAnyValue(nameof(Value)) path and provides the source stream used by + /// . + /// + /// + /// The subject is disposed with the instance; emission sites guard against disposal using . + /// + /// + private readonly BehaviorSubject _valueChanged; + + /// + /// Holds the active validation subscription, if any. + /// private readonly SerialDisposable _validationDisposable = new(); + + /// + /// Lazily created subject that publishes the current error sequence. + /// private readonly Lazy> _errorChanged; + + /// + /// Stores validators registered via . + /// private readonly Lazy, IObservable>>> _validatorStore = new(static () => []); + + /// + /// The number of initial values to skip for subscriptions created by . + /// private readonly int _skipCurrentValue; + + /// + /// Indicates whether applies DistinctUntilChanged. + /// private readonly bool _isDistinctUntilChanged; + + /// + /// The shared observable backing . + /// private IObservable? _observable; + + /// + /// The current value backing . + /// private T? _value; + + /// + /// The current validation errors, if any. + /// private IEnumerable? _currentErrors; /// /// Initializes a new instance of the class. /// The Value will be default(T). DistinctUntilChanged is true. Current Value is published on subscribe. /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("ReactiveProperty initialization uses RxApp which requires dynamic code generation")] - [RequiresUnreferencedCode("ReactiveProperty initialization uses RxApp which may require unreferenced code")] -#endif public ReactiveProperty() - : this(default, RxApp.TaskpoolScheduler, false, false) + : this(default, RxSchedulers.TaskpoolScheduler, false, false) { } @@ -52,12 +110,8 @@ public ReactiveProperty() /// The Value will be initialValue. DistinctUntilChanged is true. Current Value is published on subscribe. /// /// The initial value. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("ReactiveProperty initialization uses RxApp which requires dynamic code generation")] - [RequiresUnreferencedCode("ReactiveProperty initialization uses RxApp which may require unreferenced code")] -#endif public ReactiveProperty(T? initialValue) - : this(initialValue, RxApp.TaskpoolScheduler, false, false) + : this(initialValue, RxSchedulers.TaskpoolScheduler, false, false) { } @@ -67,12 +121,8 @@ public ReactiveProperty(T? initialValue) /// The initial value. /// if set to true [skip current value on subscribe]. /// if set to true [allow duplicate concurrent values]. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("ReactiveProperty initialization uses RxApp which requires dynamic code generation")] - [RequiresUnreferencedCode("ReactiveProperty initialization uses RxApp which may require unreferenced code")] -#endif public ReactiveProperty(T? initialValue, bool skipCurrentValueOnSubscribe, bool allowDuplicateValues) - : this(initialValue, RxApp.TaskpoolScheduler, skipCurrentValueOnSubscribe, allowDuplicateValues) + : this(initialValue, RxSchedulers.TaskpoolScheduler, skipCurrentValueOnSubscribe, allowDuplicateValues) { } @@ -83,17 +133,17 @@ public ReactiveProperty(T? initialValue, bool skipCurrentValueOnSubscribe, bool /// The scheduler. /// if set to true [skip current value on subscribe]. /// if set to true [allow duplicate concurrent values]. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("ReactiveProperty initialization uses ReactiveObject and RxApp which require dynamic code generation")] - [RequiresUnreferencedCode("ReactiveProperty initialization uses ReactiveObject and RxApp which may require unreferenced code")] -#endif + /// Thrown if is . public ReactiveProperty(T? initialValue, IScheduler? scheduler, bool skipCurrentValueOnSubscribe, bool allowDuplicateValues) { _skipCurrentValue = skipCurrentValueOnSubscribe ? 1 : 0; _isDistinctUntilChanged = !allowDuplicateValues; _value = initialValue; - _scheduler = scheduler ?? RxApp.TaskpoolScheduler; + _scheduler = scheduler ?? RxSchedulers.TaskpoolScheduler; + + _valueChanged = new BehaviorSubject(initialValue); _errorChanged = new Lazy>(() => new BehaviorSubject(GetErrors(null))); + GetSubscription(); } @@ -122,6 +172,7 @@ public T? Value { if (!_isDistinctUntilChanged) { + // Preserve existing semantics: identical assignment produces a "refresh" emission when duplicates are allowed. _valueRefereshed.OnNext(_value); } @@ -159,31 +210,31 @@ public T? Value /// /// Creates a new instance of ReactiveProperty without requiring RequiresUnreferencedCode attributes. - /// Uses RxApp.TaskpoolScheduler as the default scheduler. + /// Uses RxSchedulers.TaskpoolScheduler as the default scheduler. /// /// A new ReactiveProperty instance. public static ReactiveProperty Create() - => new(default, RxApp.TaskpoolScheduler, false, false); + => new(default, RxSchedulers.TaskpoolScheduler, false, false); /// /// Creates a new instance of ReactiveProperty with an initial value without requiring RequiresUnreferencedCode attributes. - /// Uses RxApp.TaskpoolScheduler as the default scheduler. + /// Uses RxSchedulers.TaskpoolScheduler as the default scheduler. /// /// The initial value. /// A new ReactiveProperty instance. public static ReactiveProperty Create(T? initialValue) - => new(initialValue, RxApp.TaskpoolScheduler, false, false); + => new(initialValue, RxSchedulers.TaskpoolScheduler, false, false); /// /// Creates a new instance of ReactiveProperty with configuration options without requiring RequiresUnreferencedCode attributes. - /// Uses RxApp.TaskpoolScheduler as the default scheduler. + /// Uses RxSchedulers.TaskpoolScheduler as the default scheduler. /// /// The initial value. /// if set to true [skip current value on subscribe]. /// if set to true [allow duplicate concurrent values]. /// A new ReactiveProperty instance. public static ReactiveProperty Create(T? initialValue, bool skipCurrentValueOnSubscribe, bool allowDuplicateValues) - => new(initialValue, RxApp.TaskpoolScheduler, skipCurrentValueOnSubscribe, allowDuplicateValues); + => new(initialValue, RxSchedulers.TaskpoolScheduler, skipCurrentValueOnSubscribe, allowDuplicateValues); /// /// Creates a new instance of ReactiveProperty with a custom scheduler without requiring RequiresUnreferencedCode attributes. @@ -193,6 +244,7 @@ public static ReactiveProperty Create(T? initialValue, bool skipCurrentValueO /// if set to true [skip current value on subscribe]. /// if set to true [allow duplicate concurrent values]. /// A new ReactiveProperty instance. + /// Thrown if is . public static ReactiveProperty Create(T? initialValue, IScheduler scheduler, bool skipCurrentValueOnSubscribe, bool allowDuplicateValues) => new(initialValue, scheduler, skipCurrentValueOnSubscribe, allowDuplicateValues); @@ -204,10 +256,6 @@ public static ReactiveProperty Create(T? initialValue, IScheduler scheduler, /// /// Self. /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("AddValidationError uses code which require dynamic code generation")] - [RequiresUnreferencedCode("AddValidationError uses code which may require unreferenced code")] -#endif public ReactiveProperty AddValidationError(Func, IObservable> validator, bool ignoreInitialError = false) { _validatorStore.Value.Add(validator); @@ -263,10 +311,6 @@ public ReactiveProperty AddValidationError(Func, IObservable< /// /// Self. /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("AddValidationError uses code which require dynamic code generation")] - [RequiresUnreferencedCode("AddValidationError uses code which may require unreferenced code")] -#endif public ReactiveProperty AddValidationError(Func, IObservable> validator, bool ignoreInitialError = false) => AddValidationError(xs => validator(xs).Select(x => (IEnumerable?)x), ignoreInitialError); @@ -278,10 +322,6 @@ public ReactiveProperty AddValidationError(Func, IObservable< /// /// Self. /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("AddValidationError uses code which require dynamic code generation")] - [RequiresUnreferencedCode("AddValidationError uses code which may require unreferenced code")] -#endif public ReactiveProperty AddValidationError(Func> validator, bool ignoreInitialError = false) => AddValidationError(xs => xs.SelectMany(x => validator(x)), ignoreInitialError); @@ -293,10 +333,6 @@ public ReactiveProperty AddValidationError(Func> valid /// /// Self. /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("AddValidationError uses code which require dynamic code generation")] - [RequiresUnreferencedCode("AddValidationError uses code which may require unreferenced code")] -#endif public ReactiveProperty AddValidationError(Func> validator, bool ignoreInitialError = false) => AddValidationError(xs => xs.SelectMany(x => validator(x)), ignoreInitialError); @@ -308,10 +344,6 @@ public ReactiveProperty AddValidationError(Func> validator, /// /// Self. /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("AddValidationError uses code which require dynamic code generation")] - [RequiresUnreferencedCode("AddValidationError uses code which may require unreferenced code")] -#endif public ReactiveProperty AddValidationError(Func validator, bool ignoreInitialError = false) => AddValidationError(xs => xs.Select(x => validator(x)), ignoreInitialError); @@ -323,16 +355,17 @@ public ReactiveProperty AddValidationError(Func validator, /// /// Self. /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("AddValidationError uses code which require dynamic code generation")] - [RequiresUnreferencedCode("AddValidationError uses code which may require unreferenced code")] -#endif public ReactiveProperty AddValidationError(Func validator, bool ignoreInitialError = false) => AddValidationError(xs => xs.Select(x => validator(x)), ignoreInitialError); /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// +#if NET8_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif public void Dispose() { Dispose(true); @@ -342,15 +375,16 @@ public void Dispose() /// /// Check validation. /// +#if NET8_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif public void CheckValidation() => _checkValidation.OnNext(_value); /// /// Invoke OnNext. /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("Refresh uses RaisePropertyChanged which requires dynamic code generation")] - [RequiresUnreferencedCode("Refresh uses RaisePropertyChanged which may require unreferenced code")] -#endif public void Refresh() { SetValue(_value); @@ -405,9 +439,14 @@ protected virtual void Dispose(bool disposing) if (_disposables?.IsDisposed == false && disposing) { _disposables?.Dispose(); + _checkValidation.Dispose(); _valueRefereshed.Dispose(); _validationDisposable.Dispose(); + + _valueChanged.OnCompleted(); + _valueChanged.Dispose(); + if (_errorChanged.IsValueCreated) { _errorChanged.Value.OnCompleted(); @@ -416,26 +455,50 @@ protected virtual void Dispose(bool disposing) } } + /// + /// Sets the backing value, publishes to the validation stream, and publishes to the value-change stream. + /// + /// The new value. +#if NET8_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif private void SetValue(T? value) { _value = value; + if (!IsDisposed) { _checkValidation.OnNext(value); + _valueChanged.OnNext(value); } } + /// + /// Initializes the shared subscription backing . + /// + /// + /// + /// The stream is built from a value source that does not require reflection-based property observation. + /// + /// + /// Duplicate suppression (when enabled) applies only to value-change emissions, while explicit refresh emissions + /// always flow through to subscribers. + /// + /// private void GetSubscription() { - _observable = this.WhenAnyValue, T>(nameof(Value)) - .Skip(_skipCurrentValue); + IObservable source = _valueChanged; + + source = source.Skip(_skipCurrentValue); if (_isDistinctUntilChanged) { - _observable = _observable.DistinctUntilChanged(); + source = source.DistinctUntilChanged(); } - _observable = _observable + _observable = source .Merge(_valueRefereshed) .Replay(1) .RefCount() diff --git a/src/ReactiveUI/ReactiveProperty/ReactivePropertyMixins.cs b/src/ReactiveUI/ReactiveProperty/ReactivePropertyMixins.cs index 4b1ecbc118..07f18b1701 100644 --- a/src/ReactiveUI/ReactiveProperty/ReactivePropertyMixins.cs +++ b/src/ReactiveUI/ReactiveProperty/ReactivePropertyMixins.cs @@ -30,10 +30,11 @@ public static class ReactivePropertyMixins /// or /// self. /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("The method uses DataAnnotations validation which requires dynamic code generation.")] - [RequiresUnreferencedCode("The method uses DataAnnotations validation which may require unreferenced code.")] -#endif + /// + /// This method uses DataAnnotations validation which requires reflection and is not compatible with AOT compilation. + /// For AOT scenarios, use manual validation instead. + /// + [RequiresUnreferencedCode("DataAnnotations validation uses reflection to discover attributes and is not trim-safe. Use manual validation for AOT scenarios.")] public static ReactiveProperty AddValidation(this ReactiveProperty self, Expression?>> selfSelector) { ArgumentExceptionHelper.ThrowIfNull(selfSelector); @@ -43,7 +44,9 @@ public static ReactiveProperty AddValidation(this ReactiveProperty self var propertyInfo = (PropertyInfo)memberExpression.Member; var display = propertyInfo.GetCustomAttribute(); var attrs = propertyInfo.GetCustomAttributes().ToArray(); - var context = new ValidationContext(self) + + // Use the AOT-compatible constructor that doesn't require reflection for type discovery + var context = new ValidationContext(self, serviceProvider: null, items: null) { DisplayName = display?.GetName() ?? propertyInfo.Name, MemberName = nameof(ReactiveProperty.Value), diff --git a/src/ReactiveUI/Registration/DependencyResolverRegistrar.cs b/src/ReactiveUI/Registration/DependencyResolverRegistrar.cs new file mode 100644 index 0000000000..cf2a85505e --- /dev/null +++ b/src/ReactiveUI/Registration/DependencyResolverRegistrar.cs @@ -0,0 +1,64 @@ +// 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. + +namespace ReactiveUI; + +/// +/// An AOT-friendly implementation of that wraps +/// an from Splat. +/// +/// +/// Initializes a new instance of the class. +/// +/// The dependency resolver to wrap. +internal sealed class DependencyResolverRegistrar(IMutableDependencyResolver resolver) : IRegistrar +{ + private readonly IMutableDependencyResolver _resolver = resolver ?? throw new ArgumentNullException(nameof(resolver)); + + /// + public void RegisterConstant(Func factory, string? contract = null) + where TService : class + { + ArgumentExceptionHelper.ThrowIfNull(factory); + if (contract is null) + { + _resolver.RegisterConstant(factory()); + } + else + { + _resolver.RegisterConstant(factory(), contract); + } + } + + /// + public void RegisterLazySingleton<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TService>(Func factory, string? contract = null) + where TService : class + { + ArgumentExceptionHelper.ThrowIfNull(factory); + if (contract is null) + { + _resolver.RegisterLazySingleton(factory); + } + else + { + _resolver.RegisterLazySingleton(factory, contract); + } + } + + /// + public void Register(Func factory, string? contract = null) + where TService : class + { + ArgumentExceptionHelper.ThrowIfNull(factory); + if (contract is null) + { + _resolver.Register(factory); + } + else + { + _resolver.Register(factory, contract); + } + } +} diff --git a/src/ReactiveUI/Registration/Registrations.cs b/src/ReactiveUI/Registration/Registrations.cs index 1651af7e6b..ea1632f152 100644 --- a/src/ReactiveUI/Registration/Registrations.cs +++ b/src/ReactiveUI/Registration/Registrations.cs @@ -16,34 +16,132 @@ namespace ReactiveUI; public class Registrations : IWantsToRegisterStuff { /// - [SuppressMessage("Trimming", "IL2046:'RequiresUnreferencedCodeAttribute' annotations must match across all interface implementations or overrides.", Justification = "Does not use reflection")] - [SuppressMessage("AOT", "IL3051:'RequiresDynamicCodeAttribute' annotations must match across all interface implementations or overrides.", Justification = "Does not use reflection")] - public void Register(Action, Type> registerFunction) + public void Register(IRegistrar registrar) { - ArgumentExceptionHelper.ThrowIfNull(registerFunction); - - registerFunction(static () => new INPCObservableForProperty(), typeof(ICreatesObservableForProperty)); - registerFunction(static () => new IROObservableForProperty(), typeof(ICreatesObservableForProperty)); - registerFunction(static () => new POCOObservableForProperty(), typeof(ICreatesObservableForProperty)); - registerFunction(static () => new EqualityTypeConverter(), typeof(IBindingTypeConverter)); - 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 DefaultViewLocator(), typeof(IViewLocator)); - registerFunction(static () => new CanActivateViewFetcher(), typeof(IActivationForViewFetcher)); - registerFunction(static () => new CreatesCommandBindingViaEvent(), typeof(ICreatesCommandBinding)); - registerFunction(static () => new CreatesCommandBindingViaCommandParameter(), typeof(ICreatesCommandBinding)); + ArgumentExceptionHelper.ThrowIfNull(registrar); + + registrar.RegisterConstant(static () => new INPCObservableForProperty()); + registrar.RegisterConstant(static () => new IROObservableForProperty()); + registrar.RegisterConstant(static () => new POCOObservableForProperty()); + + // General converters + registrar.RegisterConstant(static () => new EqualityTypeConverter()); + registrar.RegisterConstant(static () => new StringConverter()); + + // Numeric → String converters + 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()); + + // String → Numeric converters + registrar.RegisterConstant(static () => new StringToByteTypeConverter()); + registrar.RegisterConstant(static () => new StringToNullableByteTypeConverter()); + registrar.RegisterConstant(static () => new StringToShortTypeConverter()); + registrar.RegisterConstant(static () => new StringToNullableShortTypeConverter()); + registrar.RegisterConstant(static () => new StringToIntegerTypeConverter()); + registrar.RegisterConstant(static () => new StringToNullableIntegerTypeConverter()); + registrar.RegisterConstant(static () => new StringToLongTypeConverter()); + registrar.RegisterConstant(static () => new StringToNullableLongTypeConverter()); + registrar.RegisterConstant(static () => new StringToSingleTypeConverter()); + registrar.RegisterConstant(static () => new StringToNullableSingleTypeConverter()); + registrar.RegisterConstant(static () => new StringToDoubleTypeConverter()); + registrar.RegisterConstant(static () => new StringToNullableDoubleTypeConverter()); + registrar.RegisterConstant(static () => new StringToDecimalTypeConverter()); + registrar.RegisterConstant(static () => new StringToNullableDecimalTypeConverter()); + + // Boolean ↔ String converters + registrar.RegisterConstant(static () => new BooleanToStringTypeConverter()); + registrar.RegisterConstant(static () => new NullableBooleanToStringTypeConverter()); + registrar.RegisterConstant(static () => new StringToBooleanTypeConverter()); + registrar.RegisterConstant(static () => new StringToNullableBooleanTypeConverter()); + + // Guid ↔ String converters + registrar.RegisterConstant(static () => new GuidToStringTypeConverter()); + registrar.RegisterConstant(static () => new NullableGuidToStringTypeConverter()); + registrar.RegisterConstant(static () => new StringToGuidTypeConverter()); + registrar.RegisterConstant(static () => new StringToNullableGuidTypeConverter()); + + // DateTime ↔ String converters + registrar.RegisterConstant(static () => new DateTimeToStringTypeConverter()); + registrar.RegisterConstant(static () => new NullableDateTimeToStringTypeConverter()); + registrar.RegisterConstant(static () => new StringToDateTimeTypeConverter()); + registrar.RegisterConstant(static () => new StringToNullableDateTimeTypeConverter()); + + // DateTimeOffset ↔ String converters + registrar.RegisterConstant(static () => new DateTimeOffsetToStringTypeConverter()); + registrar.RegisterConstant(static () => new NullableDateTimeOffsetToStringTypeConverter()); + registrar.RegisterConstant(static () => new StringToDateTimeOffsetTypeConverter()); + registrar.RegisterConstant(static () => new StringToNullableDateTimeOffsetTypeConverter()); + + // TimeSpan ↔ String converters + registrar.RegisterConstant(static () => new TimeSpanToStringTypeConverter()); + registrar.RegisterConstant(static () => new NullableTimeSpanToStringTypeConverter()); + registrar.RegisterConstant(static () => new StringToTimeSpanTypeConverter()); + registrar.RegisterConstant(static () => new StringToNullableTimeSpanTypeConverter()); + +#if NET6_0_OR_GREATER + // DateOnly ↔ String converters (.NET 6+) + registrar.RegisterConstant(static () => new DateOnlyToStringTypeConverter()); + registrar.RegisterConstant(static () => new NullableDateOnlyToStringTypeConverter()); + registrar.RegisterConstant(static () => new StringToDateOnlyTypeConverter()); + registrar.RegisterConstant(static () => new StringToNullableDateOnlyTypeConverter()); + + // TimeOnly ↔ String converters (.NET 6+) + registrar.RegisterConstant(static () => new TimeOnlyToStringTypeConverter()); + registrar.RegisterConstant(static () => new NullableTimeOnlyToStringTypeConverter()); + registrar.RegisterConstant(static () => new StringToTimeOnlyTypeConverter()); + registrar.RegisterConstant(static () => new StringToNullableTimeOnlyTypeConverter()); +#endif + + // Uri ↔ String converters + registrar.RegisterConstant(static () => new UriToStringTypeConverter()); + registrar.RegisterConstant(static () => new StringToUriTypeConverter()); + + registrar.RegisterConstant(static () => new DefaultViewLocator()); + registrar.RegisterConstant(static () => new CanActivateViewFetcher()); + registrar.RegisterConstant(static () => new CreatesCommandBindingViaEvent()); + registrar.RegisterConstant(static () => new CreatesCommandBindingViaCommandParameter()); + } + + /// + /// Helper method to register a bidirectional type converter with explicit generic instantiations. + /// + /// The source type. + /// The target type. + /// The converter type that handles both TFrom→TTo and TTo→TFrom conversions. + /// The dependency resolver to register with. + /// + /// This method registers the converter three times: + /// + /// As for TFrom→TTo conversion + /// As for TTo→TFrom conversion + /// As for affinity-based discovery + /// + /// + private static void RegisterBidirectionalConverter( + IRegistrar registrar) + where TConverter : IBindingTypeConverter, IBindingTypeConverter, new() + { + ArgumentExceptionHelper.ThrowIfNull(registrar); + + var instance = new TConverter(); + + // Register both generic directions + registrar.Register>(() => instance); + registrar.Register>(() => instance); + + // Register base interface for affinity-based discovery + registrar.RegisterConstant(() => instance); } } diff --git a/src/ReactiveUI/RegistrationNamespace.cs b/src/ReactiveUI/RegistrationNamespace.cs deleted file mode 100644 index d2de0fd911..0000000000 --- a/src/ReactiveUI/RegistrationNamespace.cs +++ /dev/null @@ -1,65 +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. - -namespace ReactiveUI; - -/// -/// Platforms or other registration namespaces for the dependency resolver to consider when initializing. -/// -public enum RegistrationNamespace -{ - /// No platform to register. - None = 0, - - /// - /// Windows Forms. - /// - Winforms, - - /// - /// WPF. - /// - Wpf, - - /// - /// Uno. - /// - Uno, - - /// - /// Uno Win UI. - /// - UnoWinUI, - - /// - /// Blazor. - /// - Blazor, - - /// - /// Drawing. - /// - Drawing, - - /// - /// Avalonia. - /// - Avalonia, - - /// - /// Maui. - /// - Maui, - - /// - /// Uwp. - /// - Uwp, - - /// - /// WinUI. - /// - WinUI, -} diff --git a/src/ReactiveUI/Routing/RoutingState.cs b/src/ReactiveUI/Routing/RoutingState.cs index fcc6dd0465..ee6f540e93 100644 --- a/src/ReactiveUI/Routing/RoutingState.cs +++ b/src/ReactiveUI/Routing/RoutingState.cs @@ -53,34 +53,20 @@ namespace ReactiveUI; /// /// [DataContract] -#if NET6_0_OR_GREATER -[RequiresDynamicCode("RoutingState uses RxApp and ReactiveCommand which require dynamic code generation")] -[RequiresUnreferencedCode("RoutingState uses RxApp and ReactiveCommand which may require unreferenced code")] -#endif public class RoutingState : ReactiveObject { [IgnoreDataMember] [JsonIgnore] private readonly IScheduler _scheduler; - /// - /// Initializes static members of the class. - /// - static RoutingState() => RxApp.EnsureInitialized(); - /// /// Initializes a new instance of the class. /// /// A scheduler for where to send navigation changes to. #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("RoutingState uses ReactiveCommand which requires dynamic code generation.")] - [RequiresUnreferencedCode("RoutingState uses ReactiveCommand which may require unreferenced code.")] -#endif public RoutingState(IScheduler? scheduler = null) -#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. { - _scheduler = scheduler ?? RxApp.MainThreadScheduler; + _scheduler = scheduler ?? RxSchedulers.MainThreadScheduler; NavigationStack = []; SetupRx(); } @@ -129,19 +115,20 @@ public RoutingState(IScheduler? scheduler = null) [JsonIgnore] public IObservable> NavigationChanged { get; protected set; } + /// + /// Sets up reactive commands and observables after deserialization. + /// + /// The streaming context for deserialization. [OnDeserialized] -#if NET6_0_OR_GREATER - [RequiresDynamicCode("RoutingState uses ReactiveCommand which requires dynamic code generation.")] [RequiresUnreferencedCode("RoutingState uses ReactiveCommand which may require unreferenced code.")] + [MemberNotNull(nameof(NavigationChanged), nameof(NavigateBack), nameof(Navigate), nameof(NavigateAndReset), nameof(CurrentViewModel))] +#if NET6_0_OR_GREATER private void SetupRx(in StreamingContext sc) => SetupRx(); #else private void SetupRx(StreamingContext sc) => SetupRx(); #endif -#if NET6_0_OR_GREATER - [RequiresDynamicCode("RoutingState uses ReactiveCommand which requires dynamic code generation.")] - [RequiresUnreferencedCode("RoutingState uses ReactiveCommand which may require unreferenced code.")] -#endif + [MemberNotNull(nameof(NavigationChanged), nameof(NavigateBack), nameof(Navigate), nameof(NavigateAndReset), nameof(CurrentViewModel))] private void SetupRx() { var navigateScheduler = _scheduler; diff --git a/src/ReactiveUI/RxApp.cs b/src/ReactiveUI/RxApp.cs deleted file mode 100644 index 78db95496f..0000000000 --- a/src/ReactiveUI/RxApp.cs +++ /dev/null @@ -1,257 +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. - -using System.Diagnostics; -using System.Runtime.CompilerServices; - -using Splat.Builder; - -namespace ReactiveUI; - -/// -/// The main registration point for common class instances throughout a ReactiveUI application. -/// -/// -/// N.B. Why we have this evil global class -/// In a WPF or UWP application, most commands must have the Dispatcher -/// scheduler set, because notifications will end up being run on another thread; -/// this happens most often in a CanExecute observable.Unfortunately, in a Unit -/// Test framework, while the MS Test Unit runner will *set* the Dispatcher (so -/// we can't even use the lack of its presence to determine whether we're in a -/// test runner or not), none of the items queued to it will ever be executed -/// during the unit test. -/// Initially, I tried to plumb the ability to set the scheduler throughout the -/// classes, but when you start building applications on top of that, having to -/// have *every single * class have a default Scheduler property is really -/// irritating, with either default making life difficult. -/// This class also initializes a whole bunch of other stuff, including the IoC container, -/// logging and error handling. -/// -public static class RxApp -{ -#if ANDROID || IOS - /// - /// The size of a small cache of items. Often used for the MemoizingMRUCache class. - /// - public const int SmallCacheLimit = 32; - - /// - /// The size of a large cache of items. Often used for the MemoizingMRUCache class. - /// - public const int BigCacheLimit = 64; -#else - /// - /// The size of a small cache of items. Often used for the MemoizingMRUCache class. - /// - public const int SmallCacheLimit = 64; - - /// - /// The size of a large cache of items. Often used for the MemoizingMRUCache class. - /// - public const int BigCacheLimit = 256; -#endif - - [ThreadStatic] - [SuppressMessage("Reliability", "CA2019:Improper 'ThreadStatic' field initialization", Justification = "By Design")] - private static IScheduler _unitTestTaskpoolScheduler = null!; - - [ThreadStatic] - [SuppressMessage("Reliability", "CA2019:Improper 'ThreadStatic' field initialization", Justification = "By Design")] - private static IScheduler _unitTestMainThreadScheduler = null!; - - [ThreadStatic] - [SuppressMessage("Reliability", "CA2019:Improper 'ThreadStatic' field initialization", Justification = "By Design")] - private static ISuspensionHost _unitTestSuspensionHost = null!; - - private static ISuspensionHost _suspensionHost = null!; - private static bool _hasSchedulerBeenChecked; - - /// - /// Initializes static members of the class. - /// - /// Default exception when we have unhandled exception in RxUI. - [SuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "Recommend using AppBuilder")] - [SuppressMessage("Trimming", "IL2026:Calling members annotated with 'RequiresUnreferencedCodeAttribute' may break functionality when trimming.", Justification = "Recommend using AppBuilder")] - static RxApp() - { -#if !PORTABLE - RxSchedulers.TaskpoolScheduler = TaskPoolScheduler.Default; -#endif - AppLocator.CurrentMutable.InitializeSplat(); - - if (!AppBuilder.UsingBuilder) - { - AppLocator.RegisterResolverCallbackChanged(() => - { - if (AppLocator.CurrentMutable is null) - { - return; - } - - AppLocator.CurrentMutable.InitializeSplat(); - AppLocator.CurrentMutable.InitializeReactiveUI(PlatformRegistrationManager.NamespacesToRegister); - }); - } - - DefaultExceptionHandler = Observer.Create(ex => - { - // NB: If you're seeing this, it means that an - // ObservableAsPropertyHelper or the CanExecute of a - // ReactiveCommand ended in an OnError. Instead of silently - // breaking, ReactiveUI will halt here if a debugger is attached. - if (Debugger.IsAttached) - { - Debugger.Break(); - } - -#pragma warning disable CA1065 // Avoid exceptions in constructors -- In scheduler. - MainThreadScheduler.Schedule(() => throw new UnhandledErrorException( - "An object implementing IHandleObservableErrors (often a ReactiveCommand or ObservableAsPropertyHelper) has errored, thereby breaking its observable pipeline. To prevent this, ensure the pipeline does not error, or Subscribe to the ThrownExceptions property of the object in question to handle the erroneous case.", - ex)); -#pragma warning restore CA1065 - }); - - _suspensionHost = new SuspensionHost(); - if (ModeDetector.InUnitTestRunner()) - { - LogHost.Default.Warn("*** Detected Unit Test Runner, setting MainThreadScheduler to CurrentThread ***"); - LogHost.Default.Warn("If we are not actually in a test runner, please file a bug\n\n"); - LogHost.Default.Warn("ReactiveUI acts differently under a test runner, see the docs\n"); - LogHost.Default.Warn("for more info about what to expect"); - - UnitTestMainThreadScheduler = CurrentThreadScheduler.Instance; - return; - } - - LogHost.Default.Info("Initializing to normal mode"); - - RxSchedulers.MainThreadScheduler ??= DefaultScheduler.Instance; - } - - /// - /// Gets or sets a scheduler used to schedule work items that - /// should be run "on the UI thread". In normal mode, this will be - /// DispatcherScheduler, and in Unit Test mode this will be Immediate, - /// to simplify writing common unit tests. - /// - /// - /// Consider using RxSchedulers.MainThreadScheduler for new code to avoid RequiresUnreferencedCode attributes. - /// - public static IScheduler MainThreadScheduler - { - get - { - if (ModeDetector.InUnitTestRunner()) - { - return UnitTestMainThreadScheduler; - } - - // If Scheduler is DefaultScheduler, user is likely using .NET Standard - if (!_hasSchedulerBeenChecked && RxSchedulers.MainThreadScheduler == Scheduler.Default) - { - _hasSchedulerBeenChecked = true; - LogHost.Default.Warn("It seems you are running .NET Standard, but there is no host package installed!\n"); - LogHost.Default.Warn("You will need to install the specific host package for your platform (ReactiveUI.WPF, ReactiveUI.Blazor, ...)\n"); - LogHost.Default.Warn("You can install the needed package via NuGet, see https://reactiveui.net/docs/getting-started/installation/"); - } - - return RxSchedulers.MainThreadScheduler!; - } - - set - { - // N.B. The ThreadStatic dance here is for the unit test case - - // often, each test will override MainThreadScheduler with their - // own TestScheduler, and if this wasn't ThreadStatic, they would - // stomp on each other, causing test cases to randomly fail, - // then pass when you rerun them. - if (ModeDetector.InUnitTestRunner()) - { - UnitTestMainThreadScheduler = value; - RxSchedulers.MainThreadScheduler ??= value; - } - else - { - RxSchedulers.MainThreadScheduler = value; - } - } - } - - /// - /// Gets or sets the a the scheduler used to schedule work items to - /// run in a background thread. In both modes, this will run on the TPL - /// Task Pool. - /// - /// - /// Consider using RxSchedulers.TaskpoolScheduler for new code to avoid RequiresUnreferencedCode attributes. - /// - public static IScheduler TaskpoolScheduler - { - get => _unitTestTaskpoolScheduler ?? RxSchedulers.TaskpoolScheduler; - set - { - if (ModeDetector.InUnitTestRunner()) - { - _unitTestTaskpoolScheduler = value; - RxSchedulers.TaskpoolScheduler ??= value; - } - else - { - RxSchedulers.TaskpoolScheduler = value; - } - } - } - - /// - /// Gets or sets a value indicating whether log messages should be suppressed for command bindings in the view. - /// - public static bool SuppressViewCommandBindingMessage { get; set; } - - /// - /// Gets or sets the Observer which signaled whenever an object that has a - /// ThrownExceptions property doesn't Subscribe to that Observable. Use - /// Observer.Create to set up what will happen - the default is to crash - /// the application with an error message. - /// - public static IObserver DefaultExceptionHandler { get; set; } = null!; - - /// - /// Gets or sets the current SuspensionHost, a - /// class which provides events for process lifetime events, especially - /// on mobile devices. - /// - public static ISuspensionHost SuspensionHost - { - get => _unitTestSuspensionHost ?? _suspensionHost; - set - { - if (ModeDetector.InUnitTestRunner()) - { - _unitTestSuspensionHost = value; - _suspensionHost ??= value; - } - else - { - _suspensionHost = value; - } - } - } - - internal static IScheduler UnitTestMainThreadScheduler - { - get => _unitTestMainThreadScheduler ??= CurrentThreadScheduler.Instance; - - set => _unitTestMainThreadScheduler = value; - } - - /// - /// Set up default initializations. - /// - [MethodImpl(MethodImplOptions.NoOptimization)] - internal static void EnsureInitialized() - { - // NB: This method only exists to invoke the static constructor - } -} diff --git a/src/ReactiveUI/RxCacheSize.cs b/src/ReactiveUI/RxCacheSize.cs new file mode 100644 index 0000000000..51e3e54697 --- /dev/null +++ b/src/ReactiveUI/RxCacheSize.cs @@ -0,0 +1,76 @@ +// 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. + +namespace ReactiveUI; + +/// +/// Provides configurable cache size limits for ReactiveUI's internal caching mechanisms. +/// These values can be configured via or will auto-initialize with platform-specific defaults. +/// +public static class RxCacheSize +{ +#if ANDROID || IOS + private const int DefaultSmallCacheLimit = 32; + private const int DefaultBigCacheLimit = 64; +#else + private const int DefaultSmallCacheLimit = 64; + private const int DefaultBigCacheLimit = 256; +#endif + + private static int _smallCacheLimit; + private static int _bigCacheLimit; + private static int _initialized; // 0 = false, 1 = true + + /// + /// Gets the small cache limit used for internal memoizing caches. + /// Default: 32 (mobile platforms) or 64 (desktop platforms). + /// + public static int SmallCacheLimit + { + get + { + EnsureInitialized(); + return _smallCacheLimit; + } + } + + /// + /// Gets the big cache limit used for internal memoizing caches. + /// Default: 64 (mobile platforms) or 256 (desktop platforms). + /// + public static int BigCacheLimit + { + get + { + EnsureInitialized(); + return _bigCacheLimit; + } + } + + /// + /// Initializes the cache size limits. Called by ReactiveUIBuilder. + /// + /// The small cache limit to use. + /// The big cache limit to use. + internal static void Initialize(int smallCacheLimit, int bigCacheLimit) + { + if (Interlocked.CompareExchange(ref _initialized, 1, 0) == 0) + { + _smallCacheLimit = smallCacheLimit; + _bigCacheLimit = bigCacheLimit; + } + } + + /// + /// Ensures cache sizes are initialized with platform defaults if not already configured. + /// + private static void EnsureInitialized() + { + if (Interlocked.CompareExchange(ref _initialized, 0, 0) == 0) + { + Initialize(DefaultSmallCacheLimit, DefaultBigCacheLimit); + } + } +} diff --git a/src/ReactiveUI/RxSchedulers.cs b/src/ReactiveUI/RxSchedulers.cs index ac08d4105f..0d05eb6419 100644 --- a/src/ReactiveUI/RxSchedulers.cs +++ b/src/ReactiveUI/RxSchedulers.cs @@ -8,32 +8,36 @@ namespace ReactiveUI; /// -/// Provides access to ReactiveUI schedulers without requiring unreferenced code attributes. -/// This is a lightweight alternative to RxApp for consuming scheduler properties. +/// Provides access to ReactiveUI schedulers. /// /// -/// This class provides basic scheduler functionality without the overhead of dependency injection -/// or unit test detection, allowing consumers to access schedulers without needing -/// RequiresUnreferencedCode attributes. For full functionality including unit test support, -/// use RxApp schedulers instead. +/// This class provides scheduler functionality without requiring unreferenced code attributes, +/// making it suitable for AOT compilation scenarios. RxApp scheduler properties delegate to +/// this class and add builder initialization checks. /// public static class RxSchedulers { +#if NET9_0_OR_GREATER + private static readonly Lock _lock = new(); +#else private static readonly object _lock = new(); +#endif private static volatile IScheduler? _mainThreadScheduler; private static volatile IScheduler? _taskpoolScheduler; + static RxSchedulers() + { + TaskpoolScheduler = TaskPoolScheduler.Default; + MainThreadScheduler ??= DefaultScheduler.Instance; + } + /// /// Gets or sets a scheduler used to schedule work items that /// should be run "on the UI thread". In normal mode, this will be /// DispatcherScheduler. This defaults to DefaultScheduler.Instance. /// - /// - /// This is a simplified version that doesn't include unit test detection. - /// For full functionality including unit test support, use RxSchedulers.MainThreadScheduler. - /// public static IScheduler MainThreadScheduler { get @@ -62,10 +66,6 @@ public static IScheduler MainThreadScheduler /// Gets or sets the scheduler used to schedule work items to /// run in a background thread. This defaults to TaskPoolScheduler.Default. /// - /// - /// This is a simplified version that doesn't include unit test detection. - /// For full functionality including unit test support, use RxApp.TaskpoolScheduler. - /// public static IScheduler TaskpoolScheduler { get @@ -100,10 +100,16 @@ public static IScheduler TaskpoolScheduler } /// - /// Set up default initializations. + /// Gets or sets a value indicating whether log messages should be suppressed for command bindings in the view. + /// Platform registrations may set this to true to reduce logging noise. + /// + public static bool SuppressViewCommandBindingMessage { get; set; } + + /// + /// Set up default initializations for static constructor. /// [MethodImpl(MethodImplOptions.NoOptimization)] - internal static void EnsureInitialized() + internal static void EnsureStaticConstructorRun() { // NB: This method only exists to invoke the static constructor if needed } diff --git a/src/ReactiveUI/RxState.cs b/src/ReactiveUI/RxState.cs new file mode 100644 index 0000000000..f7ee629bd7 --- /dev/null +++ b/src/ReactiveUI/RxState.cs @@ -0,0 +1,74 @@ +// 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; + +namespace ReactiveUI; + +/// +/// Static class that holds the default exception handler for ReactiveUI. +/// +public static class RxState +{ + private static IObserver? _defaultExceptionHandler; + private static int _exceptionHandlerInitialized; // 0 = false, 1 = true + + /// + /// Gets the default exception handler for unhandled errors in ReactiveUI observables. + /// Auto-initializes with debugger break + UnhandledErrorException if not configured via builder. + /// + public static IObserver DefaultExceptionHandler + { + get + { + if (Interlocked.CompareExchange(ref _exceptionHandlerInitialized, 0, 0) == 0) + { + InitializeDefaultExceptionHandler(); + } + + return _defaultExceptionHandler!; + } + } + + /// + /// Initializes the exception handler with a custom observer. Called by ReactiveUIBuilder. + /// + /// The custom exception handler to use. + internal static void InitializeExceptionHandler(IObserver exceptionHandler) + { + if (Interlocked.CompareExchange(ref _exceptionHandlerInitialized, 1, 0) == 0) + { + _defaultExceptionHandler = exceptionHandler ?? throw new ArgumentNullException(nameof(exceptionHandler)); + } + } + + /// + /// Initializes the default exception handler if not already configured. + /// Creates an observer that breaks debugger and throws UnhandledErrorException. + /// + private static void InitializeDefaultExceptionHandler() + { + if (Interlocked.CompareExchange(ref _exceptionHandlerInitialized, 1, 0) == 0) + { + _defaultExceptionHandler = Observer.Create(ex => + { + // NB: If you're seeing this, it means that an + // ObservableAsPropertyHelper or the CanExecute of a + // ReactiveCommand ended in an OnError. Instead of silently + // breaking, ReactiveUI will halt here if a debugger is attached. + if (Debugger.IsAttached) + { + Debugger.Break(); + } + +#pragma warning disable CA1065 // Avoid exceptions in constructors -- In scheduler. + RxSchedulers.MainThreadScheduler.Schedule(() => throw new UnhandledErrorException( + "An object implementing IHandleObservableErrors (often a ReactiveCommand or ObservableAsPropertyHelper) has errored, thereby breaking its observable pipeline. To prevent this, ensure the pipeline does not error, or Subscribe to the ThrownExceptions property of the object in question to handle the erroneous case.", + ex)); +#pragma warning restore CA1065 + }); + } + } +} diff --git a/src/ReactiveUI/RxSuspension.cs b/src/ReactiveUI/RxSuspension.cs new file mode 100644 index 0000000000..91444cb269 --- /dev/null +++ b/src/ReactiveUI/RxSuspension.cs @@ -0,0 +1,62 @@ +// 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. + +namespace ReactiveUI; + +/// +/// Provides access to ReactiveUI suspension functionality. +/// +/// +/// This class provides suspension host functionality for application lifecycle management. +/// Configure using or +/// . +/// +public static class RxSuspension +{ + private static ISuspensionHost? _suspensionHost; + private static int _suspensionHostInitialized; // 0 = false, 1 = true + + /// + /// Gets the suspension host for application lifecycle management. + /// Provides events for process lifetime events, especially on mobile devices. + /// Auto-initializes with default SuspensionHost if not configured via builder. + /// + public static ISuspensionHost SuspensionHost + { + get + { + if (Interlocked.CompareExchange(ref _suspensionHostInitialized, 0, 0) == 0) + { + InitializeDefaultSuspensionHost(); + } + + return _suspensionHost!; + } + } + + /// + /// Initializes the suspension host with a custom instance. Called by ReactiveUIBuilder. + /// + /// The custom suspension host to use. + internal static void InitializeSuspensionHost(ISuspensionHost suspensionHost) + { + if (Interlocked.CompareExchange(ref _suspensionHostInitialized, 1, 0) == 0) + { + _suspensionHost = suspensionHost ?? throw new ArgumentNullException(nameof(suspensionHost)); + } + } + + /// + /// Initializes the default suspension host if not already configured. + /// Creates a new SuspensionHost instance. + /// + private static void InitializeDefaultSuspensionHost() + { + if (Interlocked.CompareExchange(ref _suspensionHostInitialized, 1, 0) == 0) + { + _suspensionHost = new SuspensionHost(); + } + } +} diff --git a/src/ReactiveUI/Suspension/DummySuspensionDriver.cs b/src/ReactiveUI/Suspension/DummySuspensionDriver.cs index 4cf9a382b4..5788a4fb03 100644 --- a/src/ReactiveUI/Suspension/DummySuspensionDriver.cs +++ b/src/ReactiveUI/Suspension/DummySuspensionDriver.cs @@ -3,36 +3,58 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. +using System; +using System.Text.Json.Serialization.Metadata; + namespace ReactiveUI; /// -/// A suspension driver that does not do anything. -/// Useful potentially for unit testing or for platforms -/// where you don't want to use a Suspension Driver. +/// A suspension driver that does not persist any state. /// -public class DummySuspensionDriver : ISuspensionDriver +/// +/// +/// This driver is useful for unit tests and for platforms or applications where persistence is undesired. +/// +/// +/// All load operations return (or for the requested type), +/// and all save/invalidate operations complete immediately. +/// +/// +public sealed class DummySuspensionDriver : ISuspensionDriver { - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("LoadState uses methods that require dynamic code generation")] - [RequiresUnreferencedCode("LoadState uses methods that may require unreferenced code")] -#endif - public IObservable LoadState() => // TODO: Create Test - Observable.Default; + /// + [RequiresUnreferencedCode( + "Implementations commonly use reflection-based serialization. " + + "Prefer LoadState(JsonTypeInfo) for trimming or AOT scenarios.")] + [RequiresDynamicCode( + "Implementations commonly use reflection-based serialization. " + + "Prefer LoadState(JsonTypeInfo) for trimming or AOT scenarios.")] + public IObservable LoadState() + => Observable.Return((object?)null); + + /// + [RequiresUnreferencedCode( + "Implementations commonly use reflection-based serialization. " + + "Prefer SaveState(T, JsonTypeInfo) for trimming or AOT scenarios.")] + [RequiresDynamicCode( + "Implementations commonly use reflection-based serialization. " + + "Prefer SaveState(T, JsonTypeInfo) for trimming or AOT scenarios.")] + public IObservable SaveState(T state) + => Observables.Unit; + + /// + public IObservable LoadState(JsonTypeInfo typeInfo) + { + return Observable.Return(default); + } - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("SaveState uses methods that require dynamic code generation")] - [RequiresUnreferencedCode("SaveState uses methods that may require unreferenced code")] -#endif - public IObservable SaveState(object state) => // TODO: Create Test - Observables.Unit; + /// + public IObservable SaveState(T state, JsonTypeInfo typeInfo) + { + return Observables.Unit; + } - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("InvalidateState uses methods that require dynamic code generation")] - [RequiresUnreferencedCode("InvalidateState uses methods that may require unreferenced code")] -#endif - public IObservable InvalidateState() => // TODO: Create Test - Observables.Unit; + /// + public IObservable InvalidateState() + => Observables.Unit; } diff --git a/src/ReactiveUI/Suspension/SuspensionHost.cs b/src/ReactiveUI/Suspension/SuspensionHost.cs index 92e1fb451d..f2ee14b22b 100644 --- a/src/ReactiveUI/Suspension/SuspensionHost.cs +++ b/src/ReactiveUI/Suspension/SuspensionHost.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 ReactiveUI.Interfaces; + namespace ReactiveUI; /// @@ -11,12 +13,12 @@ namespace ReactiveUI; /// /// /// -/// backs and provides concrete observables that are wired up +/// backs and provides concrete observables that are wired up /// by helpers such as AutoSuspendHelper. Platform hosts push their lifecycle notifications into the /// instances exposed here and view models subscribe through . /// /// -/// Consumers rarely instantiate this type directly; instead call to access the singleton. The +/// Consumers rarely instantiate this type directly; instead call to access the singleton. The /// object is intentionally thread-safe via so events raised prior to subscription are /// replayed to late subscribers. /// @@ -143,7 +145,7 @@ public SuspensionHost() /// /// /// The value should be a serializable aggregate that represents the shell of your application. It is populated via - /// during resume and saved through + /// during resume and saved through the SuspensionDriver's SaveState /// when fires. /// public object? AppState diff --git a/src/ReactiveUI/Suspension/SuspensionHostExtensions.cs b/src/ReactiveUI/Suspension/SuspensionHostExtensions.cs index f71921f5de..cd71376aff 100644 --- a/src/ReactiveUI/Suspension/SuspensionHostExtensions.cs +++ b/src/ReactiveUI/Suspension/SuspensionHostExtensions.cs @@ -3,7 +3,11 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. +using System; using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization.Metadata; + +using ReactiveUI.Interfaces; namespace ReactiveUI; @@ -59,6 +63,12 @@ internal static ISuspensionDriver? SuspensionDriver /// Calling this method triggers a one-time load via if the state has not /// yet been materialized, ensuring late subscribers still receive persisted data. /// + [RequiresUnreferencedCode( + "This overload may invoke ISuspensionDriver.LoadState(), which is commonly reflection-based. " + + "Prefer GetAppState(ISuspensionHost) used with SetupDefaultSuspendResume(..., JsonTypeInfo, ...) for trimming/AOT scenarios.")] + [RequiresDynamicCode( + "This overload may invoke ISuspensionDriver.LoadState(), which is commonly reflection-based. " + + "Prefer GetAppState(ISuspensionHost) used with SetupDefaultSuspendResume(..., JsonTypeInfo, ...) for trimming/AOT scenarios.")] public static T GetAppState(this ISuspensionHost item) { ArgumentExceptionHelper.ThrowIfNull(item); @@ -68,6 +78,26 @@ public static T GetAppState(this ISuspensionHost item) return (T)item.AppState!; } + /// + /// Gets the current strongly-typed application state. + /// + /// The app state type. + /// The typed suspension host. + /// The app state. + /// + /// Calling this method triggers a one-time load if the state has not yet been materialized. + /// For trimming/AOT-safe persistence, use . + /// + public static TAppState GetAppState(this ISuspensionHost item) + where TAppState : class + { + ArgumentExceptionHelper.ThrowIfNull(item); + + Interlocked.Exchange(ref _ensureLoadAppStateFunc, null)?.Invoke(); + + return item.AppStateValue!; + } + /// /// Observe changes to the AppState of a class derived from ISuspensionHost. /// @@ -78,10 +108,12 @@ public static T GetAppState(this ISuspensionHost item) /// Emits the current value immediately (if available) and every subsequent assignment so downstream components can /// react to hot reloads or state restoration. /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("ObserveAppState uses WhenAny which requires dynamic code generation for expression tree analysis")] - [RequiresUnreferencedCode("ObserveAppState uses WhenAny which may reference members that could be trimmed")] -#endif + [RequiresUnreferencedCode( + "This overload uses WhenAny, which can require unreferenced/dynamic code in trimming/AOT scenarios. " + + "Prefer ObserveAppState(ISuspensionHost) for trimming/AOT scenarios.")] + [RequiresDynamicCode( + "This overload uses WhenAny, which can require unreferenced/dynamic code in trimming/AOT scenarios. " + + "Prefer ObserveAppState(ISuspensionHost) for trimming/AOT scenarios.")] public static IObservable ObserveAppState(this ISuspensionHost item) where T : class { @@ -92,6 +124,35 @@ public static IObservable ObserveAppState(this ISuspensionHost item) .Cast(); } + /// + /// Observes changes to the typed application state without using WhenAny APIs (trimming/AOT friendly). + /// + /// The application state type. + /// The typed suspension host. + /// An observable of the typed application state. + /// + /// Emits the current value immediately (if available) and every subsequent assignment to . + /// + public static IObservable ObserveAppState(this ISuspensionHost item) + where TAppState : class + { + ArgumentExceptionHelper.ThrowIfNull(item); + + return Observable.Create( + observer => + { + var current = item.AppStateValue; + if (current is not null) + { + observer.OnNext(current); + } + + return item.AppStateValueChanged + .WhereNotNull() + .Subscribe(observer); + }); + } + /// /// Setup our suspension driver for a class derived off ISuspensionHost interface. /// This will make your suspension host respond to suspend and resume requests. @@ -109,15 +170,17 @@ public static IObservable ObserveAppState(this ISuspensionHost item) /// /// /// new ShellState(); - /// RxApp.SuspensionHost.SetupDefaultSuspendResume(new FileSuspensionDriver(FileSystem.AppDataDirectory)); + /// RxSuspension.SuspensionHost.CreateNewAppState = () => new ShellState(); + /// RxSuspension.SuspensionHost.SetupDefaultSuspendResume(new FileSuspensionDriver(FileSystem.AppDataDirectory)); /// ]]> /// /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("SetupDefaultSuspendResume uses ISuspensionDriver which may require dynamic code generation for serialization")] - [RequiresUnreferencedCode("SetupDefaultSuspendResume uses ISuspensionDriver which may require unreferenced code for serialization")] -#endif + [RequiresUnreferencedCode( + "This overload may invoke ISuspensionDriver.LoadState()/SaveState(T), which are commonly reflection-based. " + + "Prefer SetupDefaultSuspendResume(..., JsonTypeInfo, ...) for trimming/AOT scenarios.")] + [RequiresDynamicCode( + "This overload may invoke ISuspensionDriver.LoadState()/SaveState(T), which are commonly reflection-based. " + + "Prefer SetupDefaultSuspendResume(..., JsonTypeInfo, ...) for trimming/AOT scenarios.")] public static IDisposable SetupDefaultSuspendResume(this ISuspensionHost item, ISuspensionDriver? driver = null) { ArgumentExceptionHelper.ThrowIfNull(item); @@ -150,16 +213,64 @@ public static IDisposable SetupDefaultSuspendResume(this ISuspensionHost item, I return ret; } + /// + /// Sets up suspend/resume using a strongly-typed host and source-generated JSON metadata (trimming/AOT friendly). + /// + /// The application state type. + /// The typed suspension host. + /// Source-generated metadata for . + /// The suspension driver. + /// A disposable which will stop responding to Suspend and Resume requests. + /// + /// This overload persists and restores state using and + /// to avoid reflection-based serialization. + /// + public static IDisposable SetupDefaultSuspendResume(this ISuspensionHost item, JsonTypeInfo typeInfo, ISuspensionDriver? driver = null) + where TAppState : class + { + ArgumentExceptionHelper.ThrowIfNull(item); + ArgumentExceptionHelper.ThrowIfNull(typeInfo); + + var ret = new CompositeDisposable(); + _suspensionDriver ??= driver ?? AppLocator.Current.GetService(); + + if (_suspensionDriver is null) + { + item.Log().Error("Could not find a valid driver and therefore cannot setup Suspend/Resume."); + return Disposable.Empty; + } + + _ensureLoadAppStateFunc = () => EnsureLoadAppState(item, _suspensionDriver, typeInfo); + + ret.Add(item.ShouldInvalidateState + .SelectMany(_ => _suspensionDriver.InvalidateState()) + .LoggedCatch(item, Observables.Unit, "Tried to invalidate app state") + .Subscribe(_ => item.Log().Info("Invalidated app state"))); + + ret.Add(item.ShouldPersistState + .SelectMany(x => _suspensionDriver.SaveState(item.AppStateValue!, typeInfo).Finally(x.Dispose)) + .LoggedCatch(item, Observables.Unit, "Tried to persist app state") + .Subscribe(_ => item.Log().Info("Persisted application state"))); + + ret.Add(item.IsResuming.Merge(item.IsLaunchingNew) + .Do(_ => Interlocked.Exchange(ref _ensureLoadAppStateFunc, null)?.Invoke()) + .Subscribe()); + + return ret; + } + /// /// Ensures one time app state load from storage. /// /// The suspension host. /// The suspension driver. /// A completed observable. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("EnsureLoadAppState uses ISuspensionDriver.LoadState which may require dynamic code generation for serialization")] - [RequiresUnreferencedCode("EnsureLoadAppState uses ISuspensionDriver.LoadState which may require unreferenced code for serialization")] -#endif + [RequiresUnreferencedCode( + "This overload may invoke ISuspensionDriver.LoadState(), which is commonly reflection-based. " + + "Prefer EnsureLoadAppState(ISuspensionHost, ISuspensionDriver?, JsonTypeInfo) for trimming/AOT scenarios.")] + [RequiresDynamicCode( + "This overload may invoke ISuspensionDriver.LoadState(), which is commonly reflection-based. " + + "Prefer EnsureLoadAppState(ISuspensionHost, ISuspensionDriver?, JsonTypeInfo) for trimming/AOT scenarios.")] private static IObservable EnsureLoadAppState(this ISuspensionHost item, ISuspensionDriver? driver = null) { if (item.AppState is not null) @@ -187,4 +298,41 @@ private static IObservable EnsureLoadAppState(this ISuspensionHost item, I return Observable.Return(Unit.Default); } + + /// + /// Ensures a one-time typed app state load from storage using source-generated JSON metadata (trimming/AOT friendly). + /// + /// The application state type. + /// The typed suspension host. + /// The suspension driver. + /// Source-generated metadata for . + /// A completed observable. + private static IObservable EnsureLoadAppState(this ISuspensionHost item, ISuspensionDriver? driver, JsonTypeInfo typeInfo) + where TAppState : class + { + if (item.AppStateValue is not null) + { + return Observable.Return(Unit.Default); + } + + _suspensionDriver ??= driver ?? AppLocator.Current.GetService(); + + if (_suspensionDriver is null) + { + item.Log().Error("Could not find a valid driver and therefore cannot load app state."); + return Observable.Return(Unit.Default); + } + + try + { + item.AppStateValue = _suspensionDriver.LoadState(typeInfo).Wait(); + } + catch (Exception ex) + { + item.Log().Warn(ex, "Failed to restore app state from storage, creating from scratch"); + item.AppStateValue = item.CreateNewAppStateTyped?.Invoke(); + } + + return Observable.Return(Unit.Default); + } } diff --git a/src/ReactiveUI/Suspension/SuspensionHost{TAppState}.cs b/src/ReactiveUI/Suspension/SuspensionHost{TAppState}.cs new file mode 100644 index 0000000000..50242e5dd8 --- /dev/null +++ b/src/ReactiveUI/Suspension/SuspensionHost{TAppState}.cs @@ -0,0 +1,338 @@ +// 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 ReactiveUI.Interfaces; + +namespace ReactiveUI; + +/// +/// Default strongly-typed implementation of . +/// +/// The application state type. +/// +/// +/// This implementation provides settable lifecycle observables and a strongly-typed . +/// +/// +/// The legacy members and +/// are implemented explicitly to preserve compatibility with +/// existing infrastructure. The explicit implementations project to/from the typed properties. +/// +/// +/// Type safety: if a consumer sets to a value not assignable to +/// , the implementation throws an . +/// +/// +public class SuspensionHost : ReactiveObject, ISuspensionHost, IDisposable +{ + /// + /// Holds the observable that signals when the application is launching new. + /// + private readonly ReplaySubject> _isLaunchingNew = new(1); + + /// + /// Holds the observable that signals when the application is resuming from a suspended state. + /// + private readonly ReplaySubject> _isResuming = new(1); + + /// + /// Holds the observable that signals when the application is activated / unpausing. + /// + private readonly ReplaySubject> _isUnpausing = new(1); + + /// + /// Holds the observable that signals when the application should persist its state. + /// + private readonly ReplaySubject> _shouldPersistState = new(1); + + /// + /// Holds the observable that signals when persisted state should be invalidated. + /// + private readonly ReplaySubject> _shouldInvalidateState = new(1); + + /// + /// Holds the observable that signals when the application is continuing from a temporarily paused state. + /// + private readonly ReplaySubject> _isContinuing = new(1); + + /// + /// Publishes changes to when assigned. + /// + private readonly Subject _appStateValueChanged = new(); + + /// + /// Stores the typed application state factory. + /// + private Func? _createNewAppStateTyped; + + /// + /// Stores the current typed application state. + /// + private TAppState? _appState; + + /// + /// Initializes a new instance of the class. + /// + /// + /// The default values throw to indicate that platform-specific suspend/resume wiring has not been installed. + /// Hosts should use AutoSuspendHelper (or an equivalent) to replace these streams. + /// + public SuspensionHost() + { +#if COCOA + const string? message = "Your AppDelegate class needs to use AutoSuspendHelper"; +#elif ANDROID + const string? message = "You need to create an App class and use AutoSuspendHelper"; +#else + const string? message = "Your App class needs to use AutoSuspendHelper"; +#endif + + IsLaunchingNew = IsResuming = IsUnpausing = IsContinuing = ShouldInvalidateState = + Observable.Throw(new Exception(message)); + + ShouldPersistState = Observable.Throw(new Exception(message)); + } + + /// + /// Gets or sets the observable which signals when the application is launching new. + /// + /// + /// Emits when the platform indicates a clean launch (for example, no saved state is available). + /// + /// Thrown when the value is . + public IObservable IsLaunchingNew + { + get => _isLaunchingNew.Switch(); + set + { + ArgumentExceptionHelper.ThrowIfNull(value); + _isLaunchingNew.OnNext(value); + } + } + + /// + /// Gets or sets the observable which signals when the application is resuming from a suspended state. + /// + /// + /// Raised when the host platform reports that the previous process image is being restored. + /// + /// Thrown when the value is . + public IObservable IsResuming + { + get => _isResuming.Switch(); + set + { + ArgumentExceptionHelper.ThrowIfNull(value); + _isResuming.OnNext(value); + } + } + + /// + /// Gets or sets the observable which signals when the application is activated / unpausing. + /// + /// + /// Fired when the app returns to the foreground without being recreated. + /// + /// Thrown when the value is . + public IObservable IsUnpausing + { + get => _isUnpausing.Switch(); + set + { + ArgumentExceptionHelper.ThrowIfNull(value); + _isUnpausing.OnNext(value); + } + } + + /// + /// Gets or sets an observable which signals when the application is continuing from a temporarily paused state. + /// + /// + /// This member exists to preserve behavior patterns where a host differentiates resume-from-tombstone vs + /// resume-from-suspend; consumers may ignore it if not applicable. + /// + /// Thrown when the value is . + public IObservable IsContinuing + { + get => _isContinuing.Switch(); + set + { + ArgumentExceptionHelper.ThrowIfNull(value); + _isContinuing.OnNext(value); + } + } + + /// + /// Gets or sets the observable which signals when the application should persist its state to disk. + /// + /// + /// The produced should be disposed once the application finishes persisting its state. + /// + /// Thrown when the value is . + public IObservable ShouldPersistState + { + get => _shouldPersistState.Switch(); + set + { + ArgumentExceptionHelper.ThrowIfNull(value); + _shouldPersistState.OnNext(value); + } + } + + /// + /// Gets or sets the observable which signals that the saved application state should be deleted. + /// + /// + /// Triggered when the host detects an unrecoverable failure; use it to delete corrupt state and log crash telemetry. + /// + /// Thrown when the value is . + public IObservable ShouldInvalidateState + { + get => _shouldInvalidateState.Switch(); + set + { + ArgumentExceptionHelper.ThrowIfNull(value); + _shouldInvalidateState.OnNext(value); + } + } + + /// + /// Gets or sets a function that can be used to create a new application state instance. + /// + /// + /// This is the typed counterpart to and is typically used when the + /// application is launching fresh or recovering from an invalidated state. + /// + public Func? CreateNewAppStateTyped + { + get => _createNewAppStateTyped; + set => this.RaiseAndSetIfChanged(ref _createNewAppStateTyped, value); + } + + /// + /// Gets or sets the current application state. + /// + /// + /// This is the typed counterpart to . + /// + public TAppState? AppStateValue + { + get => _appState; + set + { + // Keep ReactiveObject semantics for existing consumers. + this.RaiseAndSetIfChanged(ref _appState, value); + + // Publish change notification for trimming/AOT-friendly observation. + _appStateValueChanged.OnNext(value); + } + } + + /// + /// Gets an observable that signals when is assigned. + /// + /// + /// This is a trimming/AOT-friendly change signal. It is independent of ReactiveUI's WhenAny APIs. + /// + public IObservable AppStateValueChanged => _appStateValueChanged; + + /// + /// Gets or sets a function that can be used to create a new application state instance. + /// + /// + /// This is the legacy object-based API. It projects to/from . + /// + /// + /// Thrown when the factory returns a value that is not assignable to . + /// + Func? ISuspensionHost.CreateNewAppState + { + get + { + var typedFactory = _createNewAppStateTyped; + Func? returnFunc = typedFactory is null ? null : () => typedFactory.Invoke()!; + + return returnFunc; + } + + set + { + if (value is null) + { + CreateNewAppStateTyped = null; + return; + } + + CreateNewAppStateTyped = () => + { + var created = value(); + return created is TAppState typed + ? typed + : throw new InvalidCastException($"Created app state is not assignable to {typeof(TAppState).FullName}."); + }; + } + } + + /// + /// Gets or sets the current application state. + /// + /// + /// This is the legacy object-based API. It projects to/from the typed property. + /// + /// + /// Thrown when the assigned value is not assignable to . + /// + object? ISuspensionHost.AppState + { + get => _appState; + set + { + if (value is null) + { + AppStateValue = default; + return; + } + + if (value is not TAppState typed) + { + throw new InvalidCastException($"AppState is not assignable to {typeof(TAppState).FullName}."); + } + + AppStateValue = typed; + } + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Releases managed resources used by the instance. + /// + /// + /// to release managed resources; to release unmanaged resources only. + /// + protected virtual void Dispose(bool disposing) + { + if (!disposing) + { + return; + } + + _isLaunchingNew.Dispose(); + _isResuming.Dispose(); + _isUnpausing.Dispose(); + _isContinuing.Dispose(); + _shouldPersistState.Dispose(); + _shouldInvalidateState.Dispose(); + + _appStateValueChanged.Dispose(); + } +} diff --git a/src/ReactiveUI/VariadicTemplates.cs b/src/ReactiveUI/VariadicTemplates.cs index b77aac1d6a..3f6b1e1696 100644 --- a/src/ReactiveUI/VariadicTemplates.cs +++ b/src/ReactiveUI/VariadicTemplates.cs @@ -15,5954 +15,5273 @@ //------------------------------------------------------------------------------ using System; -using System.Diagnostics.CodeAnalysis; +using System.Reactive.Linq; using System.Linq; using System.Linq.Expressions; -using System.Reactive.Linq; - +using System.Diagnostics.CodeAnalysis; -namespace ReactiveUI; -/// Extension methods associated with the WhenAny/WhenAnyValue classes. -public static class WhenAnyMixin +namespace ReactiveUI { - /// - /// WhenAnyValue allows you to observe whenever the value of a - /// property on an object has changed, providing an initial value when - /// the Observable is set up, unlike ObservableForProperty(). Use this - /// method in constructors to set up bindings between properties that also - /// need an initial setup. - /// - /// The object where the property chain starts. - /// The first property chain to reference. This will be a expression pointing to a end property or field. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyValue( - this TSender? sender, - Expression> property1) - { - return sender!.WhenAny(property1, (IObservedChange c1) => c1.Value); - } + /// Extension methods associated with the WhenAny/WhenAnyValue classes. + [RequiresUnreferencedCode("Evaluates expression-based member chains via reflection; members may be trimmed.")] + public static class WhenAnyMixin + { + /// + /// WhenAnyValue allows you to observe whenever the value of a + /// property on an object has changed, providing an initial value when + /// the Observable is set up, unlike ObservableForProperty(). Use this + /// method in constructors to set up bindings between properties that also + /// need an initial setup. + /// + /// The object where the property chain starts. + /// The first property chain to reference. This will be a expression pointing to a end property or field. + public static IObservable WhenAnyValue( + this TSender? sender, + Expression> property1) + { + return sender!.WhenAny(property1, (IObservedChange c1) => c1.Value); + } - /// - /// AOT-friendly overload that avoids expression trees by using a property name. - /// - /// The object where the property chain starts. - /// The property name to observe. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyValue( - this TSender? sender, - string propertyName) - { - return sender!.ObservableForProperty(propertyName, beforeChange: false, skipInitial: false, isDistinct: true) - .Select(x => x.Value); - } + /// + /// AOT-friendly overload that avoids expression trees by using a property name. + /// + /// The object where the property chain starts. + /// The property name to observe. + public static IObservable WhenAnyValue( + this TSender? sender, + string propertyName) + { + return sender!.ObservableForProperty(propertyName, beforeChange: false, skipInitial: false, isDistinct: true) + .Select(x => x.Value); + } - /// - /// WhenAnyValue allows you to observe whenever the value of a - /// property on an object has changed, providing an initial value when - /// the Observable is set up, unlike ObservableForProperty(). Use this - /// method in constructors to set up bindings between properties that also - /// need an initial setup. - /// - /// The object where the property chain starts. - /// The first property chain to reference. This will be a expression pointing to a end property or field. - /// if set to true [is distinct]. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyValue( - this TSender? sender, - Expression> property1, - bool isDistinct) - { - return sender!.WhenAny(property1, (IObservedChange c1) => c1.Value, isDistinct); - } + /// + /// WhenAnyValue allows you to observe whenever the value of a + /// property on an object has changed, providing an initial value when + /// the Observable is set up, unlike ObservableForProperty(). Use this + /// method in constructors to set up bindings between properties that also + /// need an initial setup. + /// + /// The object where the property chain starts. + /// The first property chain to reference. This will be a expression pointing to a end property or field. + /// if set to true [is distinct]. + public static IObservable WhenAnyValue( + this TSender? sender, + Expression> property1, + bool isDistinct) + { + return sender!.WhenAny(property1, (IObservedChange c1) => c1.Value, isDistinct); + } - /// - /// AOT-friendly overload that avoids expression trees by using a property name and distinct option. - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyValue( - this TSender? sender, - string propertyName, - bool isDistinct) - { - return sender!.ObservableForProperty(propertyName, beforeChange: false, skipInitial: false, isDistinct: isDistinct) - .Select(x => x.Value); - } + /// + /// AOT-friendly overload that avoids expression trees by using a property name and distinct option. + /// + public static IObservable WhenAnyValue( + this TSender? sender, + string propertyName, + bool isDistinct) + { + return sender!.ObservableForProperty(propertyName, beforeChange: false, skipInitial: false, isDistinct: isDistinct) + .Select(x => x.Value); + } - /// - /// WhenAnyValue allows you to observe whenever the value of one or more - /// properties on an object have changed, providing an initial value when - /// the Observable is set up, unlike ObservableForProperty(). Use this - /// method in constructors to set up bindings between properties that also - /// need an initial setup. - /// - /// The object where the property chain starts. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The selector which will determine the final value from the properties. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyValue( - this TSender? sender, - Expression> property1, - Func selector) - { - return sender!.WhenAny(property1, - (c1) => - selector(c1.Value)); - } + /// + /// WhenAnyValue allows you to observe whenever the value of one or more + /// properties on an object have changed, providing an initial value when + /// the Observable is set up, unlike ObservableForProperty(). Use this + /// method in constructors to set up bindings between properties that also + /// need an initial setup. + /// + /// The object where the property chain starts. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The selector which will determine the final value from the properties. + public static IObservable WhenAnyValue( + this TSender? sender, + Expression> property1, + Func selector) + { + return sender!.WhenAny(property1, + (c1) => + selector(c1.Value)); + } - /// - /// AOT-friendly selector overload using property names. - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyValue( - this TSender? sender, - string property1Name, - Func selector) - { - var o1 = sender!.ObservableForProperty(property1Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - return o1.Select(selector); - } + /// + /// AOT-friendly selector overload using property names. + /// + public static IObservable WhenAnyValue( + this TSender? sender, + string property1Name, + Func selector) + { + var o1 = sender!.ObservableForProperty(property1Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + return o1.Select(selector); + } - /// - /// WhenAnyValue allows you to observe whenever the value of one or more - /// properties on an object have changed, providing an initial value when - /// the Observable is set up, unlike ObservableForProperty(). Use this - /// method in constructors to set up bindings between properties that also - /// need an initial setup. - /// - /// The object where the property chain starts. - /// if set to true [is distinct]. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The selector which will determine the final value from the properties. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyValue( - this TSender? sender, - Expression> property1, - Func selector, - bool isDistinct) - { - return sender!.WhenAny(property1, - (c1) => - selector(c1.Value), - isDistinct); - } + /// + /// WhenAnyValue allows you to observe whenever the value of one or more + /// properties on an object have changed, providing an initial value when + /// the Observable is set up, unlike ObservableForProperty(). Use this + /// method in constructors to set up bindings between properties that also + /// need an initial setup. + /// + /// The object where the property chain starts. + /// if set to true [is distinct]. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The selector which will determine the final value from the properties. + public static IObservable WhenAnyValue( + this TSender? sender, + Expression> property1, + Func selector, + bool isDistinct) + { + return sender!.WhenAny(property1, + (c1) => + selector(c1.Value), + isDistinct); + } - /// - /// AOT-friendly selector overload using property names and distinct option. - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyValue( - this TSender? sender, - string property1Name, - Func selector, - bool isDistinct) - { - var o1 = sender!.ObservableForProperty(property1Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - return o1.Select(selector); - } + /// + /// AOT-friendly selector overload using property names and distinct option. + /// + public static IObservable WhenAnyValue( + this TSender? sender, + string property1Name, + Func selector, + bool isDistinct) + { + var o1 = sender!.ObservableForProperty(property1Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + return o1.Select(selector); + } - /// - /// WhenAny allows you to observe whenever one or more properties on an - /// object have changed, providing an initial value when the Observable - /// is set up, unlike ObservableForProperty(). Use this method in - /// constructors to set up bindings between properties that also need an - /// initial setup. - /// - /// The object where the property chain starts. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The selector which will determine the final value from the properties. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAny uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAny may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAny( - this TSender? sender, - Expression> property1, - Func, TRet> selector) - { - return sender!.ObservableForProperty(property1, false, false).Select(selector); - } + /// + /// WhenAny allows you to observe whenever one or more properties on an + /// object have changed, providing an initial value when the Observable + /// is set up, unlike ObservableForProperty(). Use this method in + /// constructors to set up bindings between properties that also need an + /// initial setup. + /// + /// The object where the property chain starts. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The selector which will determine the final value from the properties. + public static IObservable WhenAny( + this TSender? sender, + Expression> property1, + Func, TRet> selector) + { + return sender!.ObservableForProperty(property1, false, false).Select(selector); + } - /// - /// AOT-friendly WhenAny overload using property names. - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAny uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAny may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAny( - this TSender? sender, - string property1Name, - Func, TRet> selector) - { - return sender!.ObservableForProperty(property1Name, false, false) - .Select(c1 => selector(c1)); - } + /// + /// AOT-friendly WhenAny overload using property names. + /// + public static IObservable WhenAny( + this TSender? sender, + string property1Name, + Func, TRet> selector) + { + return sender!.ObservableForProperty(property1Name, false, false) + .Select(c1 => selector(c1)); + } - /// - /// WhenAny allows you to observe whenever one or more properties on an - /// object have changed, providing an initial value when the Observable - /// is set up, unlike ObservableForProperty(). Use this method in - /// constructors to set up bindings between properties that also need an - /// initial setup. - /// - /// The object where the property chain starts. - /// if set to true [is distinct]. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The selector which will determine the final value from the properties. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAny uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAny may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAny( - this TSender? sender, - Expression> property1, - Func, TRet> selector, - bool isDistinct) - { - return sender!.ObservableForProperty(property1, false, false, isDistinct).Select(selector); - } + /// + /// WhenAny allows you to observe whenever one or more properties on an + /// object have changed, providing an initial value when the Observable + /// is set up, unlike ObservableForProperty(). Use this method in + /// constructors to set up bindings between properties that also need an + /// initial setup. + /// + /// The object where the property chain starts. + /// if set to true [is distinct]. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The selector which will determine the final value from the properties. + public static IObservable WhenAny( + this TSender? sender, + Expression> property1, + Func, TRet> selector, + bool isDistinct) + { + return sender!.ObservableForProperty(property1, false, false, isDistinct).Select(selector); + } - /// - /// AOT-friendly WhenAny overload using property names and distinct option. - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAny uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAny may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAny( - this TSender? sender, - string property1Name, - Func, TRet> selector, - bool isDistinct) - { - return sender!.ObservableForProperty(property1Name, false, false, isDistinct) - .Select(c1 => selector(c1)); - } + /// + /// AOT-friendly WhenAny overload using property names and distinct option. + /// + public static IObservable WhenAny( + this TSender? sender, + string property1Name, + Func, TRet> selector, + bool isDistinct) + { + return sender!.ObservableForProperty(property1Name, false, false, isDistinct) + .Select(c1 => selector(c1)); + } - /// - /// WhenAny allows you to observe whenever one or more properties on an - /// object have changed, providing an initial value when the Observable - /// is set up, unlike ObservableForProperty(). Use this method in - /// constructors to set up bindings between properties that also need an - /// initial setup. - /// - /// The object where the property chain starts. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The selector which will determine the final value from the properties. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyDynamic uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyDynamic may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyDynamic( - this TSender? sender, - Expression? property1, - Func, TRet> selector) - { - return ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property1, false, false).Select(selector); - } + /// + /// WhenAny allows you to observe whenever one or more properties on an + /// object have changed, providing an initial value when the Observable + /// is set up, unlike ObservableForProperty(). Use this method in + /// constructors to set up bindings between properties that also need an + /// initial setup. + /// + /// The object where the property chain starts. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The selector which will determine the final value from the properties. + public static IObservable WhenAnyDynamic( + this TSender? sender, + Expression? property1, + Func, TRet> selector) + { + return ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property1, false, false).Select(selector); + } - /// - /// WhenAny allows you to observe whenever one or more properties on an - /// object have changed, providing an initial value when the Observable - /// is set up, unlike ObservableForProperty(). Use this method in - /// constructors to set up bindings between properties that also need an - /// initial setup. - /// - /// The object where the property chain starts. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The selector which will determine the final value from the properties. - /// if set to true [is distinct]. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyDynamic uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyDynamic may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyDynamic( - this TSender? sender, - Expression? property1, - Func, TRet> selector, - bool isDistinct) - { - return ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property1, false, false, isDistinct).Select(selector); - } + /// + /// WhenAny allows you to observe whenever one or more properties on an + /// object have changed, providing an initial value when the Observable + /// is set up, unlike ObservableForProperty(). Use this method in + /// constructors to set up bindings between properties that also need an + /// initial setup. + /// + /// The object where the property chain starts. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The selector which will determine the final value from the properties. + /// if set to true [is distinct]. + public static IObservable WhenAnyDynamic( + this TSender? sender, + Expression? property1, + Func, TRet> selector, + bool isDistinct) + { + return ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property1, false, false, isDistinct).Select(selector); + } - /// - /// WhenAnyValue allows you to observe whenever the value of one or more - /// properties on an object have changed, providing an initial value when - /// the Observable is set up, unlike ObservableForProperty(). Use this - /// method in constructors to set up bindings between properties that also - /// need an initial setup. - /// - /// The object where the property chain starts. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable<(T1,T2)> WhenAnyValue( - this TSender? sender, - Expression> property1, - Expression> property2 - ) - { - return sender!.WhenAny(property1, property2, - (c1, c2) => - (c1.Value, c2.Value)); - } + /// + /// WhenAnyValue allows you to observe whenever the value of one or more + /// properties on an object have changed, providing an initial value when + /// the Observable is set up, unlike ObservableForProperty(). Use this + /// method in constructors to set up bindings between properties that also + /// need an initial setup. + /// + /// The object where the property chain starts. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + public static IObservable<(T1,T2)> WhenAnyValue( + this TSender? sender, + Expression> property1, + Expression> property2 + ) + { + return sender!.WhenAny(property1, property2, + (c1, c2) => + (c1.Value, c2.Value)); + } - /// - /// AOT-friendly tuple overloads using property names instead of expressions. - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable<(T1,T2)> WhenAnyValue( - this TSender? sender, - string property1Name, string property2Name ) - { - var o1 = sender!.ObservableForProperty(property1Name, beforeChange: false, skipInitial: false, isDistinct: true); - var o2 = sender!.ObservableForProperty(property2Name, beforeChange: false, skipInitial: false, isDistinct: true); - return Observable.CombineLatest( - o1.Select(x => x.Value), - o2.Select(x => x.Value) - , (v1,v2) => - (v1,v2) - ); - } + /// + /// AOT-friendly tuple overloads using property names instead of expressions. + /// + public static IObservable<(T1,T2)> WhenAnyValue( + this TSender? sender, + string property1Name, string property2Name ) + { + var o1 = sender!.ObservableForProperty(property1Name, beforeChange: false, skipInitial: false, isDistinct: true); + var o2 = sender!.ObservableForProperty(property2Name, beforeChange: false, skipInitial: false, isDistinct: true); + return Observable.CombineLatest( + o1.Select(x => x.Value), + o2.Select(x => x.Value) + , (v1,v2) => + (v1,v2) + ); + } - /// - /// WhenAnyValue allows you to observe whenever the value of one or more - /// properties on an object have changed, providing an initial value when - /// the Observable is set up, unlike ObservableForProperty(). Use this - /// method in constructors to set up bindings between properties that also - /// need an initial setup. - /// - /// The object where the property chain starts. - /// if set to true [is distinct]. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable<(T1,T2)> WhenAnyValue( - this TSender? sender, - Expression> property1, - Expression> property2 - , - bool isDistinct) - { - return sender!.WhenAny(property1, property2, - (c1, c2) => - (c1.Value, c2.Value), - isDistinct); - } + /// + /// WhenAnyValue allows you to observe whenever the value of one or more + /// properties on an object have changed, providing an initial value when + /// the Observable is set up, unlike ObservableForProperty(). Use this + /// method in constructors to set up bindings between properties that also + /// need an initial setup. + /// + /// The object where the property chain starts. + /// if set to true [is distinct]. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + public static IObservable<(T1,T2)> WhenAnyValue( + this TSender? sender, + Expression> property1, + Expression> property2 + , + bool isDistinct) + { + return sender!.WhenAny(property1, property2, + (c1, c2) => + (c1.Value, c2.Value), + isDistinct); + } - /// - /// AOT-friendly tuple overloads using property names with distinct option. - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable<(T1,T2)> WhenAnyValue( - this TSender? sender, - string property1Name, string property2Name , - bool isDistinct) - { - var o1 = sender!.ObservableForProperty(property1Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct); - var o2 = sender!.ObservableForProperty(property2Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct); - return Observable.CombineLatest( - o1.Select(x => x.Value), - o2.Select(x => x.Value) - , (v1,v2) => - (v1,v2) - ); - } + /// + /// AOT-friendly tuple overloads using property names with distinct option. + /// + public static IObservable<(T1,T2)> WhenAnyValue( + this TSender? sender, + string property1Name, string property2Name , + bool isDistinct) + { + var o1 = sender!.ObservableForProperty(property1Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct); + var o2 = sender!.ObservableForProperty(property2Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct); + return Observable.CombineLatest( + o1.Select(x => x.Value), + o2.Select(x => x.Value) + , (v1,v2) => + (v1,v2) + ); + } - /// - /// WhenAnyValue allows you to observe whenever the value of one or more - /// properties on an object have changed, providing an initial value when - /// the Observable is set up, unlike ObservableForProperty(). Use this - /// method in constructors to set up bindings between properties that also - /// need an initial setup. - /// - /// The object where the property chain starts. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The selector which will determine the final value from the properties. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyValue( - this TSender? sender, - Expression> property1, - Expression> property2, - Func selector) - { - return sender!.WhenAny(property1, property2, - (c1, c2) => - selector(c1.Value, c2.Value)); - } + /// + /// WhenAnyValue allows you to observe whenever the value of one or more + /// properties on an object have changed, providing an initial value when + /// the Observable is set up, unlike ObservableForProperty(). Use this + /// method in constructors to set up bindings between properties that also + /// need an initial setup. + /// + /// The object where the property chain starts. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The selector which will determine the final value from the properties. + public static IObservable WhenAnyValue( + this TSender? sender, + Expression> property1, + Expression> property2, + Func selector) + { + return sender!.WhenAny(property1, property2, + (c1, c2) => + selector(c1.Value, c2.Value)); + } - /// - /// AOT-friendly selector overload using property names. - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyValue( - this TSender? sender, - string property1Name, - string property2Name, - Func selector) - { - var o1 = sender!.ObservableForProperty(property1Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - var o2 = sender!.ObservableForProperty(property2Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - return Observable.CombineLatest( - o1, - o2 - , selector); - } + /// + /// AOT-friendly selector overload using property names. + /// + public static IObservable WhenAnyValue( + this TSender? sender, + string property1Name, + string property2Name, + Func selector) + { + var o1 = sender!.ObservableForProperty(property1Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + var o2 = sender!.ObservableForProperty(property2Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + return Observable.CombineLatest( + o1, + o2 + , selector); + } - /// - /// WhenAnyValue allows you to observe whenever the value of one or more - /// properties on an object have changed, providing an initial value when - /// the Observable is set up, unlike ObservableForProperty(). Use this - /// method in constructors to set up bindings between properties that also - /// need an initial setup. - /// - /// The object where the property chain starts. - /// if set to true [is distinct]. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The selector which will determine the final value from the properties. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyValue( - this TSender? sender, - Expression> property1, - Expression> property2, - Func selector, - bool isDistinct) - { - return sender!.WhenAny(property1, property2, - (c1, c2) => - selector(c1.Value, c2.Value), - isDistinct); - } + /// + /// WhenAnyValue allows you to observe whenever the value of one or more + /// properties on an object have changed, providing an initial value when + /// the Observable is set up, unlike ObservableForProperty(). Use this + /// method in constructors to set up bindings between properties that also + /// need an initial setup. + /// + /// The object where the property chain starts. + /// if set to true [is distinct]. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The selector which will determine the final value from the properties. + public static IObservable WhenAnyValue( + this TSender? sender, + Expression> property1, + Expression> property2, + Func selector, + bool isDistinct) + { + return sender!.WhenAny(property1, property2, + (c1, c2) => + selector(c1.Value, c2.Value), + isDistinct); + } - /// - /// AOT-friendly selector overload using property names and distinct option. - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyValue( - this TSender? sender, - string property1Name, - string property2Name, - Func selector, - bool isDistinct) - { - var o1 = sender!.ObservableForProperty(property1Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - var o2 = sender!.ObservableForProperty(property2Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - return Observable.CombineLatest( - o1, - o2 - , selector); - } + /// + /// AOT-friendly selector overload using property names and distinct option. + /// + public static IObservable WhenAnyValue( + this TSender? sender, + string property1Name, + string property2Name, + Func selector, + bool isDistinct) + { + var o1 = sender!.ObservableForProperty(property1Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + var o2 = sender!.ObservableForProperty(property2Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + return Observable.CombineLatest( + o1, + o2 + , selector); + } - /// - /// WhenAny allows you to observe whenever one or more properties on an - /// object have changed, providing an initial value when the Observable - /// is set up, unlike ObservableForProperty(). Use this method in - /// constructors to set up bindings between properties that also need an - /// initial setup. - /// - /// The object where the property chain starts. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The selector which will determine the final value from the properties. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAny uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAny may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAny( - this TSender? sender, - Expression> property1, - Expression> property2, - Func, IObservedChange, TRet> selector) - { - return Observable.CombineLatest( - sender!.ObservableForProperty(property1, false, false), - sender!.ObservableForProperty(property2, false, false), - selector - ); - } + /// + /// WhenAny allows you to observe whenever one or more properties on an + /// object have changed, providing an initial value when the Observable + /// is set up, unlike ObservableForProperty(). Use this method in + /// constructors to set up bindings between properties that also need an + /// initial setup. + /// + /// The object where the property chain starts. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The selector which will determine the final value from the properties. + public static IObservable WhenAny( + this TSender? sender, + Expression> property1, + Expression> property2, + Func, IObservedChange, TRet> selector) + { + return Observable.CombineLatest( + sender!.ObservableForProperty(property1, false, false), + sender!.ObservableForProperty(property2, false, false), + selector + ); + } - /// - /// AOT-friendly WhenAny overload using property names. - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAny uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAny may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAny( - this TSender? sender, - string property1Name, - string property2Name, - Func, IObservedChange, TRet> selector) - { - return Observable.CombineLatest( - sender!.ObservableForProperty(property1Name, false, false), - sender!.ObservableForProperty(property2Name, false, false), - selector - ); - } + /// + /// AOT-friendly WhenAny overload using property names. + /// + public static IObservable WhenAny( + this TSender? sender, + string property1Name, + string property2Name, + Func, IObservedChange, TRet> selector) + { + return Observable.CombineLatest( + sender!.ObservableForProperty(property1Name, false, false), + sender!.ObservableForProperty(property2Name, false, false), + selector + ); + } - /// - /// WhenAny allows you to observe whenever one or more properties on an - /// object have changed, providing an initial value when the Observable - /// is set up, unlike ObservableForProperty(). Use this method in - /// constructors to set up bindings between properties that also need an - /// initial setup. - /// - /// The object where the property chain starts. - /// if set to true [is distinct]. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The selector which will determine the final value from the properties. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAny uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAny may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAny( - this TSender? sender, - Expression> property1, - Expression> property2, - Func, IObservedChange, TRet> selector, - bool isDistinct) - { - return Observable.CombineLatest( - sender!.ObservableForProperty(property1, false, false, isDistinct), - sender!.ObservableForProperty(property2, false, false, isDistinct), - selector - ); - } + /// + /// WhenAny allows you to observe whenever one or more properties on an + /// object have changed, providing an initial value when the Observable + /// is set up, unlike ObservableForProperty(). Use this method in + /// constructors to set up bindings between properties that also need an + /// initial setup. + /// + /// The object where the property chain starts. + /// if set to true [is distinct]. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The selector which will determine the final value from the properties. + public static IObservable WhenAny( + this TSender? sender, + Expression> property1, + Expression> property2, + Func, IObservedChange, TRet> selector, + bool isDistinct) + { + return Observable.CombineLatest( + sender!.ObservableForProperty(property1, false, false, isDistinct), + sender!.ObservableForProperty(property2, false, false, isDistinct), + selector + ); + } - /// - /// AOT-friendly WhenAny overload using property names and distinct option. - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAny uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAny may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAny( - this TSender? sender, - string property1Name, - string property2Name, - Func, IObservedChange, TRet> selector, - bool isDistinct) - { - return Observable.CombineLatest( - sender!.ObservableForProperty(property1Name, false, false, isDistinct), - sender!.ObservableForProperty(property2Name, false, false, isDistinct), - selector - ); - } + /// + /// AOT-friendly WhenAny overload using property names and distinct option. + /// + public static IObservable WhenAny( + this TSender? sender, + string property1Name, + string property2Name, + Func, IObservedChange, TRet> selector, + bool isDistinct) + { + return Observable.CombineLatest( + sender!.ObservableForProperty(property1Name, false, false, isDistinct), + sender!.ObservableForProperty(property2Name, false, false, isDistinct), + selector + ); + } - /// - /// WhenAny allows you to observe whenever one or more properties on an - /// object have changed, providing an initial value when the Observable - /// is set up, unlike ObservableForProperty(). Use this method in - /// constructors to set up bindings between properties that also need an - /// initial setup. - /// - /// The object where the property chain starts. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The selector which will determine the final value from the properties. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyDynamic uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyDynamic may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyDynamic( - this TSender? sender, - Expression? property1, - Expression? property2, - Func, IObservedChange, TRet> selector) - { - return Observable.CombineLatest( - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property1, false, false), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property2, false, false), - selector - ); - } + /// + /// WhenAny allows you to observe whenever one or more properties on an + /// object have changed, providing an initial value when the Observable + /// is set up, unlike ObservableForProperty(). Use this method in + /// constructors to set up bindings between properties that also need an + /// initial setup. + /// + /// The object where the property chain starts. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The selector which will determine the final value from the properties. + public static IObservable WhenAnyDynamic( + this TSender? sender, + Expression? property1, + Expression? property2, + Func, IObservedChange, TRet> selector) + { + return Observable.CombineLatest( + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property1, false, false), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property2, false, false), + selector + ); + } - /// - /// WhenAny allows you to observe whenever one or more properties on an - /// object have changed, providing an initial value when the Observable - /// is set up, unlike ObservableForProperty(). Use this method in - /// constructors to set up bindings between properties that also need an - /// initial setup. - /// - /// The object where the property chain starts. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The selector which will determine the final value from the properties. - /// if set to true [is distinct]. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyDynamic uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyDynamic may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyDynamic( - this TSender? sender, - Expression? property1, - Expression? property2, - Func, IObservedChange, TRet> selector, - bool isDistinct) - { - return Observable.CombineLatest( - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property1, false, false, isDistinct), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property2, false, false, isDistinct), - selector - ); - } + /// + /// WhenAny allows you to observe whenever one or more properties on an + /// object have changed, providing an initial value when the Observable + /// is set up, unlike ObservableForProperty(). Use this method in + /// constructors to set up bindings between properties that also need an + /// initial setup. + /// + /// The object where the property chain starts. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The selector which will determine the final value from the properties. + /// if set to true [is distinct]. + public static IObservable WhenAnyDynamic( + this TSender? sender, + Expression? property1, + Expression? property2, + Func, IObservedChange, TRet> selector, + bool isDistinct) + { + return Observable.CombineLatest( + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property1, false, false, isDistinct), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property2, false, false, isDistinct), + selector + ); + } - /// - /// WhenAnyValue allows you to observe whenever the value of one or more - /// properties on an object have changed, providing an initial value when - /// the Observable is set up, unlike ObservableForProperty(). Use this - /// method in constructors to set up bindings between properties that also - /// need an initial setup. - /// - /// The object where the property chain starts. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The 3 property chain to reference. This will be a expression pointing to a end property or field. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable<(T1,T2,T3)> WhenAnyValue( - this TSender? sender, - Expression> property1, - Expression> property2, - Expression> property3 - ) - { - return sender!.WhenAny(property1, property2, property3, - (c1, c2, c3) => - (c1.Value, c2.Value, c3.Value)); - } + /// + /// WhenAnyValue allows you to observe whenever the value of one or more + /// properties on an object have changed, providing an initial value when + /// the Observable is set up, unlike ObservableForProperty(). Use this + /// method in constructors to set up bindings between properties that also + /// need an initial setup. + /// + /// The object where the property chain starts. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The 3 property chain to reference. This will be a expression pointing to a end property or field. + public static IObservable<(T1,T2,T3)> WhenAnyValue( + this TSender? sender, + Expression> property1, + Expression> property2, + Expression> property3 + ) + { + return sender!.WhenAny(property1, property2, property3, + (c1, c2, c3) => + (c1.Value, c2.Value, c3.Value)); + } - /// - /// AOT-friendly tuple overloads using property names instead of expressions. - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable<(T1,T2,T3)> WhenAnyValue( - this TSender? sender, - string property1Name, string property2Name, string property3Name ) - { - var o1 = sender!.ObservableForProperty(property1Name, beforeChange: false, skipInitial: false, isDistinct: true); - var o2 = sender!.ObservableForProperty(property2Name, beforeChange: false, skipInitial: false, isDistinct: true); - var o3 = sender!.ObservableForProperty(property3Name, beforeChange: false, skipInitial: false, isDistinct: true); - return Observable.CombineLatest( - o1.Select(x => x.Value), - o2.Select(x => x.Value), - o3.Select(x => x.Value) - , (v1,v2,v3) => - (v1,v2,v3) - ); - } + /// + /// AOT-friendly tuple overloads using property names instead of expressions. + /// + public static IObservable<(T1,T2,T3)> WhenAnyValue( + this TSender? sender, + string property1Name, string property2Name, string property3Name ) + { + var o1 = sender!.ObservableForProperty(property1Name, beforeChange: false, skipInitial: false, isDistinct: true); + var o2 = sender!.ObservableForProperty(property2Name, beforeChange: false, skipInitial: false, isDistinct: true); + var o3 = sender!.ObservableForProperty(property3Name, beforeChange: false, skipInitial: false, isDistinct: true); + return Observable.CombineLatest( + o1.Select(x => x.Value), + o2.Select(x => x.Value), + o3.Select(x => x.Value) + , (v1,v2,v3) => + (v1,v2,v3) + ); + } - /// - /// WhenAnyValue allows you to observe whenever the value of one or more - /// properties on an object have changed, providing an initial value when - /// the Observable is set up, unlike ObservableForProperty(). Use this - /// method in constructors to set up bindings between properties that also - /// need an initial setup. - /// - /// The object where the property chain starts. - /// if set to true [is distinct]. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The 3 property chain to reference. This will be a expression pointing to a end property or field. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable<(T1,T2,T3)> WhenAnyValue( - this TSender? sender, - Expression> property1, - Expression> property2, - Expression> property3 - , - bool isDistinct) - { - return sender!.WhenAny(property1, property2, property3, - (c1, c2, c3) => - (c1.Value, c2.Value, c3.Value), - isDistinct); - } + /// + /// WhenAnyValue allows you to observe whenever the value of one or more + /// properties on an object have changed, providing an initial value when + /// the Observable is set up, unlike ObservableForProperty(). Use this + /// method in constructors to set up bindings between properties that also + /// need an initial setup. + /// + /// The object where the property chain starts. + /// if set to true [is distinct]. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The 3 property chain to reference. This will be a expression pointing to a end property or field. + public static IObservable<(T1,T2,T3)> WhenAnyValue( + this TSender? sender, + Expression> property1, + Expression> property2, + Expression> property3 + , + bool isDistinct) + { + return sender!.WhenAny(property1, property2, property3, + (c1, c2, c3) => + (c1.Value, c2.Value, c3.Value), + isDistinct); + } - /// - /// AOT-friendly tuple overloads using property names with distinct option. - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable<(T1,T2,T3)> WhenAnyValue( - this TSender? sender, - string property1Name, string property2Name, string property3Name , - bool isDistinct) - { - var o1 = sender!.ObservableForProperty(property1Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct); - var o2 = sender!.ObservableForProperty(property2Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct); - var o3 = sender!.ObservableForProperty(property3Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct); - return Observable.CombineLatest( - o1.Select(x => x.Value), - o2.Select(x => x.Value), - o3.Select(x => x.Value) - , (v1,v2,v3) => - (v1,v2,v3) - ); - } + /// + /// AOT-friendly tuple overloads using property names with distinct option. + /// + public static IObservable<(T1,T2,T3)> WhenAnyValue( + this TSender? sender, + string property1Name, string property2Name, string property3Name , + bool isDistinct) + { + var o1 = sender!.ObservableForProperty(property1Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct); + var o2 = sender!.ObservableForProperty(property2Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct); + var o3 = sender!.ObservableForProperty(property3Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct); + return Observable.CombineLatest( + o1.Select(x => x.Value), + o2.Select(x => x.Value), + o3.Select(x => x.Value) + , (v1,v2,v3) => + (v1,v2,v3) + ); + } - /// - /// WhenAnyValue allows you to observe whenever the value of one or more - /// properties on an object have changed, providing an initial value when - /// the Observable is set up, unlike ObservableForProperty(). Use this - /// method in constructors to set up bindings between properties that also - /// need an initial setup. - /// - /// The object where the property chain starts. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The 3 property chain to reference. This will be a expression pointing to a end property or field. - /// The selector which will determine the final value from the properties. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyValue( - this TSender? sender, - Expression> property1, - Expression> property2, - Expression> property3, - Func selector) - { - return sender!.WhenAny(property1, property2, property3, - (c1, c2, c3) => - selector(c1.Value, c2.Value, c3.Value)); - } + /// + /// WhenAnyValue allows you to observe whenever the value of one or more + /// properties on an object have changed, providing an initial value when + /// the Observable is set up, unlike ObservableForProperty(). Use this + /// method in constructors to set up bindings between properties that also + /// need an initial setup. + /// + /// The object where the property chain starts. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The 3 property chain to reference. This will be a expression pointing to a end property or field. + /// The selector which will determine the final value from the properties. + public static IObservable WhenAnyValue( + this TSender? sender, + Expression> property1, + Expression> property2, + Expression> property3, + Func selector) + { + return sender!.WhenAny(property1, property2, property3, + (c1, c2, c3) => + selector(c1.Value, c2.Value, c3.Value)); + } - /// - /// AOT-friendly selector overload using property names. - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyValue( - this TSender? sender, - string property1Name, - string property2Name, - string property3Name, - Func selector) - { - var o1 = sender!.ObservableForProperty(property1Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - var o2 = sender!.ObservableForProperty(property2Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - var o3 = sender!.ObservableForProperty(property3Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - return Observable.CombineLatest( - o1, - o2, - o3 - , selector); - } + /// + /// AOT-friendly selector overload using property names. + /// + public static IObservable WhenAnyValue( + this TSender? sender, + string property1Name, + string property2Name, + string property3Name, + Func selector) + { + var o1 = sender!.ObservableForProperty(property1Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + var o2 = sender!.ObservableForProperty(property2Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + var o3 = sender!.ObservableForProperty(property3Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + return Observable.CombineLatest( + o1, + o2, + o3 + , selector); + } - /// - /// WhenAnyValue allows you to observe whenever the value of one or more - /// properties on an object have changed, providing an initial value when - /// the Observable is set up, unlike ObservableForProperty(). Use this - /// method in constructors to set up bindings between properties that also - /// need an initial setup. - /// - /// The object where the property chain starts. - /// if set to true [is distinct]. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The 3 property chain to reference. This will be a expression pointing to a end property or field. - /// The selector which will determine the final value from the properties. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyValue( - this TSender? sender, - Expression> property1, - Expression> property2, - Expression> property3, - Func selector, - bool isDistinct) - { - return sender!.WhenAny(property1, property2, property3, - (c1, c2, c3) => - selector(c1.Value, c2.Value, c3.Value), - isDistinct); - } + /// + /// WhenAnyValue allows you to observe whenever the value of one or more + /// properties on an object have changed, providing an initial value when + /// the Observable is set up, unlike ObservableForProperty(). Use this + /// method in constructors to set up bindings between properties that also + /// need an initial setup. + /// + /// The object where the property chain starts. + /// if set to true [is distinct]. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The 3 property chain to reference. This will be a expression pointing to a end property or field. + /// The selector which will determine the final value from the properties. + public static IObservable WhenAnyValue( + this TSender? sender, + Expression> property1, + Expression> property2, + Expression> property3, + Func selector, + bool isDistinct) + { + return sender!.WhenAny(property1, property2, property3, + (c1, c2, c3) => + selector(c1.Value, c2.Value, c3.Value), + isDistinct); + } - /// - /// AOT-friendly selector overload using property names and distinct option. - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyValue( - this TSender? sender, - string property1Name, - string property2Name, - string property3Name, - Func selector, - bool isDistinct) - { - var o1 = sender!.ObservableForProperty(property1Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - var o2 = sender!.ObservableForProperty(property2Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - var o3 = sender!.ObservableForProperty(property3Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - return Observable.CombineLatest( - o1, - o2, - o3 - , selector); - } + /// + /// AOT-friendly selector overload using property names and distinct option. + /// + public static IObservable WhenAnyValue( + this TSender? sender, + string property1Name, + string property2Name, + string property3Name, + Func selector, + bool isDistinct) + { + var o1 = sender!.ObservableForProperty(property1Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + var o2 = sender!.ObservableForProperty(property2Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + var o3 = sender!.ObservableForProperty(property3Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + return Observable.CombineLatest( + o1, + o2, + o3 + , selector); + } - /// - /// WhenAny allows you to observe whenever one or more properties on an - /// object have changed, providing an initial value when the Observable - /// is set up, unlike ObservableForProperty(). Use this method in - /// constructors to set up bindings between properties that also need an - /// initial setup. - /// - /// The object where the property chain starts. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The 3 property chain to reference. This will be a expression pointing to a end property or field. - /// The selector which will determine the final value from the properties. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAny uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAny may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAny( - this TSender? sender, - Expression> property1, - Expression> property2, - Expression> property3, - Func, IObservedChange, IObservedChange, TRet> selector) - { - return Observable.CombineLatest( - sender!.ObservableForProperty(property1, false, false), - sender!.ObservableForProperty(property2, false, false), - sender!.ObservableForProperty(property3, false, false), - selector - ); - } + /// + /// WhenAny allows you to observe whenever one or more properties on an + /// object have changed, providing an initial value when the Observable + /// is set up, unlike ObservableForProperty(). Use this method in + /// constructors to set up bindings between properties that also need an + /// initial setup. + /// + /// The object where the property chain starts. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The 3 property chain to reference. This will be a expression pointing to a end property or field. + /// The selector which will determine the final value from the properties. + public static IObservable WhenAny( + this TSender? sender, + Expression> property1, + Expression> property2, + Expression> property3, + Func, IObservedChange, IObservedChange, TRet> selector) + { + return Observable.CombineLatest( + sender!.ObservableForProperty(property1, false, false), + sender!.ObservableForProperty(property2, false, false), + sender!.ObservableForProperty(property3, false, false), + selector + ); + } - /// - /// AOT-friendly WhenAny overload using property names. - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAny uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAny may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAny( - this TSender? sender, - string property1Name, - string property2Name, - string property3Name, - Func, IObservedChange, IObservedChange, TRet> selector) - { - return Observable.CombineLatest( - sender!.ObservableForProperty(property1Name, false, false), - sender!.ObservableForProperty(property2Name, false, false), - sender!.ObservableForProperty(property3Name, false, false), - selector - ); - } + /// + /// AOT-friendly WhenAny overload using property names. + /// + public static IObservable WhenAny( + this TSender? sender, + string property1Name, + string property2Name, + string property3Name, + Func, IObservedChange, IObservedChange, TRet> selector) + { + return Observable.CombineLatest( + sender!.ObservableForProperty(property1Name, false, false), + sender!.ObservableForProperty(property2Name, false, false), + sender!.ObservableForProperty(property3Name, false, false), + selector + ); + } - /// - /// WhenAny allows you to observe whenever one or more properties on an - /// object have changed, providing an initial value when the Observable - /// is set up, unlike ObservableForProperty(). Use this method in - /// constructors to set up bindings between properties that also need an - /// initial setup. - /// - /// The object where the property chain starts. - /// if set to true [is distinct]. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The 3 property chain to reference. This will be a expression pointing to a end property or field. - /// The selector which will determine the final value from the properties. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAny uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAny may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAny( - this TSender? sender, - Expression> property1, - Expression> property2, - Expression> property3, - Func, IObservedChange, IObservedChange, TRet> selector, - bool isDistinct) - { - return Observable.CombineLatest( - sender!.ObservableForProperty(property1, false, false, isDistinct), - sender!.ObservableForProperty(property2, false, false, isDistinct), - sender!.ObservableForProperty(property3, false, false, isDistinct), - selector - ); - } + /// + /// WhenAny allows you to observe whenever one or more properties on an + /// object have changed, providing an initial value when the Observable + /// is set up, unlike ObservableForProperty(). Use this method in + /// constructors to set up bindings between properties that also need an + /// initial setup. + /// + /// The object where the property chain starts. + /// if set to true [is distinct]. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The 3 property chain to reference. This will be a expression pointing to a end property or field. + /// The selector which will determine the final value from the properties. + public static IObservable WhenAny( + this TSender? sender, + Expression> property1, + Expression> property2, + Expression> property3, + Func, IObservedChange, IObservedChange, TRet> selector, + bool isDistinct) + { + return Observable.CombineLatest( + sender!.ObservableForProperty(property1, false, false, isDistinct), + sender!.ObservableForProperty(property2, false, false, isDistinct), + sender!.ObservableForProperty(property3, false, false, isDistinct), + selector + ); + } - /// - /// AOT-friendly WhenAny overload using property names and distinct option. - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAny uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAny may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAny( - this TSender? sender, - string property1Name, - string property2Name, - string property3Name, - Func, IObservedChange, IObservedChange, TRet> selector, - bool isDistinct) - { - return Observable.CombineLatest( - sender!.ObservableForProperty(property1Name, false, false, isDistinct), - sender!.ObservableForProperty(property2Name, false, false, isDistinct), - sender!.ObservableForProperty(property3Name, false, false, isDistinct), - selector - ); - } + /// + /// AOT-friendly WhenAny overload using property names and distinct option. + /// + public static IObservable WhenAny( + this TSender? sender, + string property1Name, + string property2Name, + string property3Name, + Func, IObservedChange, IObservedChange, TRet> selector, + bool isDistinct) + { + return Observable.CombineLatest( + sender!.ObservableForProperty(property1Name, false, false, isDistinct), + sender!.ObservableForProperty(property2Name, false, false, isDistinct), + sender!.ObservableForProperty(property3Name, false, false, isDistinct), + selector + ); + } - /// - /// WhenAny allows you to observe whenever one or more properties on an - /// object have changed, providing an initial value when the Observable - /// is set up, unlike ObservableForProperty(). Use this method in - /// constructors to set up bindings between properties that also need an - /// initial setup. - /// - /// The object where the property chain starts. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The 3 property chain to reference. This will be a expression pointing to a end property or field. - /// The selector which will determine the final value from the properties. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyDynamic uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyDynamic may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyDynamic( - this TSender? sender, - Expression? property1, - Expression? property2, - Expression? property3, - Func, IObservedChange, IObservedChange, TRet> selector) - { - return Observable.CombineLatest( - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property1, false, false), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property2, false, false), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property3, false, false), - selector - ); - } + /// + /// WhenAny allows you to observe whenever one or more properties on an + /// object have changed, providing an initial value when the Observable + /// is set up, unlike ObservableForProperty(). Use this method in + /// constructors to set up bindings between properties that also need an + /// initial setup. + /// + /// The object where the property chain starts. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The 3 property chain to reference. This will be a expression pointing to a end property or field. + /// The selector which will determine the final value from the properties. + public static IObservable WhenAnyDynamic( + this TSender? sender, + Expression? property1, + Expression? property2, + Expression? property3, + Func, IObservedChange, IObservedChange, TRet> selector) + { + return Observable.CombineLatest( + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property1, false, false), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property2, false, false), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property3, false, false), + selector + ); + } - /// - /// WhenAny allows you to observe whenever one or more properties on an - /// object have changed, providing an initial value when the Observable - /// is set up, unlike ObservableForProperty(). Use this method in - /// constructors to set up bindings between properties that also need an - /// initial setup. - /// - /// The object where the property chain starts. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The 3 property chain to reference. This will be a expression pointing to a end property or field. - /// The selector which will determine the final value from the properties. - /// if set to true [is distinct]. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyDynamic uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyDynamic may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyDynamic( - this TSender? sender, - Expression? property1, - Expression? property2, - Expression? property3, - Func, IObservedChange, IObservedChange, TRet> selector, - bool isDistinct) - { - return Observable.CombineLatest( - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property1, false, false, isDistinct), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property2, false, false, isDistinct), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property3, false, false, isDistinct), - selector - ); - } + /// + /// WhenAny allows you to observe whenever one or more properties on an + /// object have changed, providing an initial value when the Observable + /// is set up, unlike ObservableForProperty(). Use this method in + /// constructors to set up bindings between properties that also need an + /// initial setup. + /// + /// The object where the property chain starts. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The 3 property chain to reference. This will be a expression pointing to a end property or field. + /// The selector which will determine the final value from the properties. + /// if set to true [is distinct]. + public static IObservable WhenAnyDynamic( + this TSender? sender, + Expression? property1, + Expression? property2, + Expression? property3, + Func, IObservedChange, IObservedChange, TRet> selector, + bool isDistinct) + { + return Observable.CombineLatest( + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property1, false, false, isDistinct), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property2, false, false, isDistinct), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property3, false, false, isDistinct), + selector + ); + } - /// - /// WhenAnyValue allows you to observe whenever the value of one or more - /// properties on an object have changed, providing an initial value when - /// the Observable is set up, unlike ObservableForProperty(). Use this - /// method in constructors to set up bindings between properties that also - /// need an initial setup. - /// - /// The object where the property chain starts. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The 3 property chain to reference. This will be a expression pointing to a end property or field. - /// The 4 property chain to reference. This will be a expression pointing to a end property or field. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable<(T1,T2,T3,T4)> WhenAnyValue( - this TSender? sender, - Expression> property1, - Expression> property2, - Expression> property3, - Expression> property4 - ) - { - return sender!.WhenAny(property1, property2, property3, property4, - (c1, c2, c3, c4) => - (c1.Value, c2.Value, c3.Value, c4.Value)); - } + /// + /// WhenAnyValue allows you to observe whenever the value of one or more + /// properties on an object have changed, providing an initial value when + /// the Observable is set up, unlike ObservableForProperty(). Use this + /// method in constructors to set up bindings between properties that also + /// need an initial setup. + /// + /// The object where the property chain starts. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The 3 property chain to reference. This will be a expression pointing to a end property or field. + /// The 4 property chain to reference. This will be a expression pointing to a end property or field. + public static IObservable<(T1,T2,T3,T4)> WhenAnyValue( + this TSender? sender, + Expression> property1, + Expression> property2, + Expression> property3, + Expression> property4 + ) + { + return sender!.WhenAny(property1, property2, property3, property4, + (c1, c2, c3, c4) => + (c1.Value, c2.Value, c3.Value, c4.Value)); + } - /// - /// AOT-friendly tuple overloads using property names instead of expressions. - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable<(T1,T2,T3,T4)> WhenAnyValue( - this TSender? sender, - string property1Name, string property2Name, string property3Name, string property4Name ) - { - var o1 = sender!.ObservableForProperty(property1Name, beforeChange: false, skipInitial: false, isDistinct: true); - var o2 = sender!.ObservableForProperty(property2Name, beforeChange: false, skipInitial: false, isDistinct: true); - var o3 = sender!.ObservableForProperty(property3Name, beforeChange: false, skipInitial: false, isDistinct: true); - var o4 = sender!.ObservableForProperty(property4Name, beforeChange: false, skipInitial: false, isDistinct: true); - return Observable.CombineLatest( - o1.Select(x => x.Value), - o2.Select(x => x.Value), - o3.Select(x => x.Value), - o4.Select(x => x.Value) - , (v1,v2,v3,v4) => - (v1,v2,v3,v4) - ); - } + /// + /// AOT-friendly tuple overloads using property names instead of expressions. + /// + public static IObservable<(T1,T2,T3,T4)> WhenAnyValue( + this TSender? sender, + string property1Name, string property2Name, string property3Name, string property4Name ) + { + var o1 = sender!.ObservableForProperty(property1Name, beforeChange: false, skipInitial: false, isDistinct: true); + var o2 = sender!.ObservableForProperty(property2Name, beforeChange: false, skipInitial: false, isDistinct: true); + var o3 = sender!.ObservableForProperty(property3Name, beforeChange: false, skipInitial: false, isDistinct: true); + var o4 = sender!.ObservableForProperty(property4Name, beforeChange: false, skipInitial: false, isDistinct: true); + return Observable.CombineLatest( + o1.Select(x => x.Value), + o2.Select(x => x.Value), + o3.Select(x => x.Value), + o4.Select(x => x.Value) + , (v1,v2,v3,v4) => + (v1,v2,v3,v4) + ); + } - /// - /// WhenAnyValue allows you to observe whenever the value of one or more - /// properties on an object have changed, providing an initial value when - /// the Observable is set up, unlike ObservableForProperty(). Use this - /// method in constructors to set up bindings between properties that also - /// need an initial setup. - /// - /// The object where the property chain starts. - /// if set to true [is distinct]. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The 3 property chain to reference. This will be a expression pointing to a end property or field. - /// The 4 property chain to reference. This will be a expression pointing to a end property or field. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable<(T1,T2,T3,T4)> WhenAnyValue( - this TSender? sender, - Expression> property1, - Expression> property2, - Expression> property3, - Expression> property4 - , - bool isDistinct) - { - return sender!.WhenAny(property1, property2, property3, property4, - (c1, c2, c3, c4) => - (c1.Value, c2.Value, c3.Value, c4.Value), - isDistinct); - } + /// + /// WhenAnyValue allows you to observe whenever the value of one or more + /// properties on an object have changed, providing an initial value when + /// the Observable is set up, unlike ObservableForProperty(). Use this + /// method in constructors to set up bindings between properties that also + /// need an initial setup. + /// + /// The object where the property chain starts. + /// if set to true [is distinct]. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The 3 property chain to reference. This will be a expression pointing to a end property or field. + /// The 4 property chain to reference. This will be a expression pointing to a end property or field. + public static IObservable<(T1,T2,T3,T4)> WhenAnyValue( + this TSender? sender, + Expression> property1, + Expression> property2, + Expression> property3, + Expression> property4 + , + bool isDistinct) + { + return sender!.WhenAny(property1, property2, property3, property4, + (c1, c2, c3, c4) => + (c1.Value, c2.Value, c3.Value, c4.Value), + isDistinct); + } - /// - /// AOT-friendly tuple overloads using property names with distinct option. - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable<(T1,T2,T3,T4)> WhenAnyValue( - this TSender? sender, - string property1Name, string property2Name, string property3Name, string property4Name , - bool isDistinct) - { - var o1 = sender!.ObservableForProperty(property1Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct); - var o2 = sender!.ObservableForProperty(property2Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct); - var o3 = sender!.ObservableForProperty(property3Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct); - var o4 = sender!.ObservableForProperty(property4Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct); - return Observable.CombineLatest( - o1.Select(x => x.Value), - o2.Select(x => x.Value), - o3.Select(x => x.Value), - o4.Select(x => x.Value) - , (v1,v2,v3,v4) => - (v1,v2,v3,v4) - ); - } + /// + /// AOT-friendly tuple overloads using property names with distinct option. + /// + public static IObservable<(T1,T2,T3,T4)> WhenAnyValue( + this TSender? sender, + string property1Name, string property2Name, string property3Name, string property4Name , + bool isDistinct) + { + var o1 = sender!.ObservableForProperty(property1Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct); + var o2 = sender!.ObservableForProperty(property2Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct); + var o3 = sender!.ObservableForProperty(property3Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct); + var o4 = sender!.ObservableForProperty(property4Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct); + return Observable.CombineLatest( + o1.Select(x => x.Value), + o2.Select(x => x.Value), + o3.Select(x => x.Value), + o4.Select(x => x.Value) + , (v1,v2,v3,v4) => + (v1,v2,v3,v4) + ); + } - /// - /// WhenAnyValue allows you to observe whenever the value of one or more - /// properties on an object have changed, providing an initial value when - /// the Observable is set up, unlike ObservableForProperty(). Use this - /// method in constructors to set up bindings between properties that also - /// need an initial setup. - /// - /// The object where the property chain starts. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The 3 property chain to reference. This will be a expression pointing to a end property or field. - /// The 4 property chain to reference. This will be a expression pointing to a end property or field. - /// The selector which will determine the final value from the properties. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyValue( - this TSender? sender, - Expression> property1, - Expression> property2, - Expression> property3, - Expression> property4, - Func selector) - { - return sender!.WhenAny(property1, property2, property3, property4, - (c1, c2, c3, c4) => - selector(c1.Value, c2.Value, c3.Value, c4.Value)); - } + /// + /// WhenAnyValue allows you to observe whenever the value of one or more + /// properties on an object have changed, providing an initial value when + /// the Observable is set up, unlike ObservableForProperty(). Use this + /// method in constructors to set up bindings between properties that also + /// need an initial setup. + /// + /// The object where the property chain starts. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The 3 property chain to reference. This will be a expression pointing to a end property or field. + /// The 4 property chain to reference. This will be a expression pointing to a end property or field. + /// The selector which will determine the final value from the properties. + public static IObservable WhenAnyValue( + this TSender? sender, + Expression> property1, + Expression> property2, + Expression> property3, + Expression> property4, + Func selector) + { + return sender!.WhenAny(property1, property2, property3, property4, + (c1, c2, c3, c4) => + selector(c1.Value, c2.Value, c3.Value, c4.Value)); + } - /// - /// AOT-friendly selector overload using property names. - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyValue( - this TSender? sender, - string property1Name, - string property2Name, - string property3Name, - string property4Name, - Func selector) - { - var o1 = sender!.ObservableForProperty(property1Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - var o2 = sender!.ObservableForProperty(property2Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - var o3 = sender!.ObservableForProperty(property3Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - var o4 = sender!.ObservableForProperty(property4Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - return Observable.CombineLatest( - o1, - o2, - o3, - o4 - , selector); - } + /// + /// AOT-friendly selector overload using property names. + /// + public static IObservable WhenAnyValue( + this TSender? sender, + string property1Name, + string property2Name, + string property3Name, + string property4Name, + Func selector) + { + var o1 = sender!.ObservableForProperty(property1Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + var o2 = sender!.ObservableForProperty(property2Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + var o3 = sender!.ObservableForProperty(property3Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + var o4 = sender!.ObservableForProperty(property4Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + return Observable.CombineLatest( + o1, + o2, + o3, + o4 + , selector); + } - /// - /// WhenAnyValue allows you to observe whenever the value of one or more - /// properties on an object have changed, providing an initial value when - /// the Observable is set up, unlike ObservableForProperty(). Use this - /// method in constructors to set up bindings between properties that also - /// need an initial setup. - /// - /// The object where the property chain starts. - /// if set to true [is distinct]. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The 3 property chain to reference. This will be a expression pointing to a end property or field. - /// The 4 property chain to reference. This will be a expression pointing to a end property or field. - /// The selector which will determine the final value from the properties. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyValue( - this TSender? sender, - Expression> property1, - Expression> property2, - Expression> property3, - Expression> property4, - Func selector, - bool isDistinct) - { - return sender!.WhenAny(property1, property2, property3, property4, - (c1, c2, c3, c4) => - selector(c1.Value, c2.Value, c3.Value, c4.Value), - isDistinct); - } + /// + /// WhenAnyValue allows you to observe whenever the value of one or more + /// properties on an object have changed, providing an initial value when + /// the Observable is set up, unlike ObservableForProperty(). Use this + /// method in constructors to set up bindings between properties that also + /// need an initial setup. + /// + /// The object where the property chain starts. + /// if set to true [is distinct]. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The 3 property chain to reference. This will be a expression pointing to a end property or field. + /// The 4 property chain to reference. This will be a expression pointing to a end property or field. + /// The selector which will determine the final value from the properties. + public static IObservable WhenAnyValue( + this TSender? sender, + Expression> property1, + Expression> property2, + Expression> property3, + Expression> property4, + Func selector, + bool isDistinct) + { + return sender!.WhenAny(property1, property2, property3, property4, + (c1, c2, c3, c4) => + selector(c1.Value, c2.Value, c3.Value, c4.Value), + isDistinct); + } - /// - /// AOT-friendly selector overload using property names and distinct option. - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyValue( - this TSender? sender, - string property1Name, - string property2Name, - string property3Name, - string property4Name, - Func selector, - bool isDistinct) - { - var o1 = sender!.ObservableForProperty(property1Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - var o2 = sender!.ObservableForProperty(property2Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - var o3 = sender!.ObservableForProperty(property3Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - var o4 = sender!.ObservableForProperty(property4Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - return Observable.CombineLatest( - o1, - o2, - o3, - o4 - , selector); - } + /// + /// AOT-friendly selector overload using property names and distinct option. + /// + public static IObservable WhenAnyValue( + this TSender? sender, + string property1Name, + string property2Name, + string property3Name, + string property4Name, + Func selector, + bool isDistinct) + { + var o1 = sender!.ObservableForProperty(property1Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + var o2 = sender!.ObservableForProperty(property2Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + var o3 = sender!.ObservableForProperty(property3Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + var o4 = sender!.ObservableForProperty(property4Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + return Observable.CombineLatest( + o1, + o2, + o3, + o4 + , selector); + } - /// - /// WhenAny allows you to observe whenever one or more properties on an - /// object have changed, providing an initial value when the Observable - /// is set up, unlike ObservableForProperty(). Use this method in - /// constructors to set up bindings between properties that also need an - /// initial setup. - /// - /// The object where the property chain starts. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The 3 property chain to reference. This will be a expression pointing to a end property or field. - /// The 4 property chain to reference. This will be a expression pointing to a end property or field. - /// The selector which will determine the final value from the properties. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAny uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAny may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAny( - this TSender? sender, - Expression> property1, - Expression> property2, - Expression> property3, - Expression> property4, - Func, IObservedChange, IObservedChange, IObservedChange, TRet> selector) - { - return Observable.CombineLatest( - sender!.ObservableForProperty(property1, false, false), - sender!.ObservableForProperty(property2, false, false), - sender!.ObservableForProperty(property3, false, false), - sender!.ObservableForProperty(property4, false, false), - selector - ); - } + /// + /// WhenAny allows you to observe whenever one or more properties on an + /// object have changed, providing an initial value when the Observable + /// is set up, unlike ObservableForProperty(). Use this method in + /// constructors to set up bindings between properties that also need an + /// initial setup. + /// + /// The object where the property chain starts. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The 3 property chain to reference. This will be a expression pointing to a end property or field. + /// The 4 property chain to reference. This will be a expression pointing to a end property or field. + /// The selector which will determine the final value from the properties. + public static IObservable WhenAny( + this TSender? sender, + Expression> property1, + Expression> property2, + Expression> property3, + Expression> property4, + Func, IObservedChange, IObservedChange, IObservedChange, TRet> selector) + { + return Observable.CombineLatest( + sender!.ObservableForProperty(property1, false, false), + sender!.ObservableForProperty(property2, false, false), + sender!.ObservableForProperty(property3, false, false), + sender!.ObservableForProperty(property4, false, false), + selector + ); + } - /// - /// AOT-friendly WhenAny overload using property names. - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAny uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAny may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAny( - this TSender? sender, - string property1Name, - string property2Name, - string property3Name, - string property4Name, - Func, IObservedChange, IObservedChange, IObservedChange, TRet> selector) - { - return Observable.CombineLatest( - sender!.ObservableForProperty(property1Name, false, false), - sender!.ObservableForProperty(property2Name, false, false), - sender!.ObservableForProperty(property3Name, false, false), - sender!.ObservableForProperty(property4Name, false, false), - selector - ); - } + /// + /// AOT-friendly WhenAny overload using property names. + /// + public static IObservable WhenAny( + this TSender? sender, + string property1Name, + string property2Name, + string property3Name, + string property4Name, + Func, IObservedChange, IObservedChange, IObservedChange, TRet> selector) + { + return Observable.CombineLatest( + sender!.ObservableForProperty(property1Name, false, false), + sender!.ObservableForProperty(property2Name, false, false), + sender!.ObservableForProperty(property3Name, false, false), + sender!.ObservableForProperty(property4Name, false, false), + selector + ); + } - /// - /// WhenAny allows you to observe whenever one or more properties on an - /// object have changed, providing an initial value when the Observable - /// is set up, unlike ObservableForProperty(). Use this method in - /// constructors to set up bindings between properties that also need an - /// initial setup. - /// - /// The object where the property chain starts. - /// if set to true [is distinct]. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The 3 property chain to reference. This will be a expression pointing to a end property or field. - /// The 4 property chain to reference. This will be a expression pointing to a end property or field. - /// The selector which will determine the final value from the properties. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAny uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAny may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAny( - this TSender? sender, - Expression> property1, - Expression> property2, - Expression> property3, - Expression> property4, - Func, IObservedChange, IObservedChange, IObservedChange, TRet> selector, - bool isDistinct) - { - return Observable.CombineLatest( - sender!.ObservableForProperty(property1, false, false, isDistinct), - sender!.ObservableForProperty(property2, false, false, isDistinct), - sender!.ObservableForProperty(property3, false, false, isDistinct), - sender!.ObservableForProperty(property4, false, false, isDistinct), - selector - ); - } + /// + /// WhenAny allows you to observe whenever one or more properties on an + /// object have changed, providing an initial value when the Observable + /// is set up, unlike ObservableForProperty(). Use this method in + /// constructors to set up bindings between properties that also need an + /// initial setup. + /// + /// The object where the property chain starts. + /// if set to true [is distinct]. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The 3 property chain to reference. This will be a expression pointing to a end property or field. + /// The 4 property chain to reference. This will be a expression pointing to a end property or field. + /// The selector which will determine the final value from the properties. + public static IObservable WhenAny( + this TSender? sender, + Expression> property1, + Expression> property2, + Expression> property3, + Expression> property4, + Func, IObservedChange, IObservedChange, IObservedChange, TRet> selector, + bool isDistinct) + { + return Observable.CombineLatest( + sender!.ObservableForProperty(property1, false, false, isDistinct), + sender!.ObservableForProperty(property2, false, false, isDistinct), + sender!.ObservableForProperty(property3, false, false, isDistinct), + sender!.ObservableForProperty(property4, false, false, isDistinct), + selector + ); + } - /// - /// AOT-friendly WhenAny overload using property names and distinct option. - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAny uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAny may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAny( - this TSender? sender, - string property1Name, - string property2Name, - string property3Name, - string property4Name, - Func, IObservedChange, IObservedChange, IObservedChange, TRet> selector, - bool isDistinct) - { - return Observable.CombineLatest( - sender!.ObservableForProperty(property1Name, false, false, isDistinct), - sender!.ObservableForProperty(property2Name, false, false, isDistinct), - sender!.ObservableForProperty(property3Name, false, false, isDistinct), - sender!.ObservableForProperty(property4Name, false, false, isDistinct), - selector - ); - } + /// + /// AOT-friendly WhenAny overload using property names and distinct option. + /// + public static IObservable WhenAny( + this TSender? sender, + string property1Name, + string property2Name, + string property3Name, + string property4Name, + Func, IObservedChange, IObservedChange, IObservedChange, TRet> selector, + bool isDistinct) + { + return Observable.CombineLatest( + sender!.ObservableForProperty(property1Name, false, false, isDistinct), + sender!.ObservableForProperty(property2Name, false, false, isDistinct), + sender!.ObservableForProperty(property3Name, false, false, isDistinct), + sender!.ObservableForProperty(property4Name, false, false, isDistinct), + selector + ); + } - /// - /// WhenAny allows you to observe whenever one or more properties on an - /// object have changed, providing an initial value when the Observable - /// is set up, unlike ObservableForProperty(). Use this method in - /// constructors to set up bindings between properties that also need an - /// initial setup. - /// - /// The object where the property chain starts. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The 3 property chain to reference. This will be a expression pointing to a end property or field. - /// The 4 property chain to reference. This will be a expression pointing to a end property or field. - /// The selector which will determine the final value from the properties. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyDynamic uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyDynamic may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyDynamic( - this TSender? sender, - Expression? property1, - Expression? property2, - Expression? property3, - Expression? property4, - Func, IObservedChange, IObservedChange, IObservedChange, TRet> selector) - { - return Observable.CombineLatest( - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property1, false, false), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property2, false, false), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property3, false, false), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property4, false, false), - selector - ); - } + /// + /// WhenAny allows you to observe whenever one or more properties on an + /// object have changed, providing an initial value when the Observable + /// is set up, unlike ObservableForProperty(). Use this method in + /// constructors to set up bindings between properties that also need an + /// initial setup. + /// + /// The object where the property chain starts. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The 3 property chain to reference. This will be a expression pointing to a end property or field. + /// The 4 property chain to reference. This will be a expression pointing to a end property or field. + /// The selector which will determine the final value from the properties. + public static IObservable WhenAnyDynamic( + this TSender? sender, + Expression? property1, + Expression? property2, + Expression? property3, + Expression? property4, + Func, IObservedChange, IObservedChange, IObservedChange, TRet> selector) + { + return Observable.CombineLatest( + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property1, false, false), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property2, false, false), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property3, false, false), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property4, false, false), + selector + ); + } - /// - /// WhenAny allows you to observe whenever one or more properties on an - /// object have changed, providing an initial value when the Observable - /// is set up, unlike ObservableForProperty(). Use this method in - /// constructors to set up bindings between properties that also need an - /// initial setup. - /// - /// The object where the property chain starts. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The 3 property chain to reference. This will be a expression pointing to a end property or field. - /// The 4 property chain to reference. This will be a expression pointing to a end property or field. - /// The selector which will determine the final value from the properties. - /// if set to true [is distinct]. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyDynamic uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyDynamic may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyDynamic( - this TSender? sender, - Expression? property1, - Expression? property2, - Expression? property3, - Expression? property4, - Func, IObservedChange, IObservedChange, IObservedChange, TRet> selector, - bool isDistinct) - { - return Observable.CombineLatest( - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property1, false, false, isDistinct), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property2, false, false, isDistinct), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property3, false, false, isDistinct), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property4, false, false, isDistinct), - selector - ); - } + /// + /// WhenAny allows you to observe whenever one or more properties on an + /// object have changed, providing an initial value when the Observable + /// is set up, unlike ObservableForProperty(). Use this method in + /// constructors to set up bindings between properties that also need an + /// initial setup. + /// + /// The object where the property chain starts. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The 3 property chain to reference. This will be a expression pointing to a end property or field. + /// The 4 property chain to reference. This will be a expression pointing to a end property or field. + /// The selector which will determine the final value from the properties. + /// if set to true [is distinct]. + public static IObservable WhenAnyDynamic( + this TSender? sender, + Expression? property1, + Expression? property2, + Expression? property3, + Expression? property4, + Func, IObservedChange, IObservedChange, IObservedChange, TRet> selector, + bool isDistinct) + { + return Observable.CombineLatest( + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property1, false, false, isDistinct), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property2, false, false, isDistinct), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property3, false, false, isDistinct), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property4, false, false, isDistinct), + selector + ); + } - /// - /// WhenAnyValue allows you to observe whenever the value of one or more - /// properties on an object have changed, providing an initial value when - /// the Observable is set up, unlike ObservableForProperty(). Use this - /// method in constructors to set up bindings between properties that also - /// need an initial setup. - /// - /// The object where the property chain starts. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The 3 property chain to reference. This will be a expression pointing to a end property or field. - /// The 4 property chain to reference. This will be a expression pointing to a end property or field. - /// The 5 property chain to reference. This will be a expression pointing to a end property or field. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable<(T1,T2,T3,T4,T5)> WhenAnyValue( - this TSender? sender, - Expression> property1, - Expression> property2, - Expression> property3, - Expression> property4, - Expression> property5 - ) - { - return sender!.WhenAny(property1, property2, property3, property4, property5, - (c1, c2, c3, c4, c5) => - (c1.Value, c2.Value, c3.Value, c4.Value, c5.Value)); - } + /// + /// WhenAnyValue allows you to observe whenever the value of one or more + /// properties on an object have changed, providing an initial value when + /// the Observable is set up, unlike ObservableForProperty(). Use this + /// method in constructors to set up bindings between properties that also + /// need an initial setup. + /// + /// The object where the property chain starts. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The 3 property chain to reference. This will be a expression pointing to a end property or field. + /// The 4 property chain to reference. This will be a expression pointing to a end property or field. + /// The 5 property chain to reference. This will be a expression pointing to a end property or field. + public static IObservable<(T1,T2,T3,T4,T5)> WhenAnyValue( + this TSender? sender, + Expression> property1, + Expression> property2, + Expression> property3, + Expression> property4, + Expression> property5 + ) + { + return sender!.WhenAny(property1, property2, property3, property4, property5, + (c1, c2, c3, c4, c5) => + (c1.Value, c2.Value, c3.Value, c4.Value, c5.Value)); + } - /// - /// AOT-friendly tuple overloads using property names instead of expressions. - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable<(T1,T2,T3,T4,T5)> WhenAnyValue( - this TSender? sender, - string property1Name, string property2Name, string property3Name, string property4Name, string property5Name ) - { - var o1 = sender!.ObservableForProperty(property1Name, beforeChange: false, skipInitial: false, isDistinct: true); - var o2 = sender!.ObservableForProperty(property2Name, beforeChange: false, skipInitial: false, isDistinct: true); - var o3 = sender!.ObservableForProperty(property3Name, beforeChange: false, skipInitial: false, isDistinct: true); - var o4 = sender!.ObservableForProperty(property4Name, beforeChange: false, skipInitial: false, isDistinct: true); - var o5 = sender!.ObservableForProperty(property5Name, beforeChange: false, skipInitial: false, isDistinct: true); - return Observable.CombineLatest( - o1.Select(x => x.Value), - o2.Select(x => x.Value), - o3.Select(x => x.Value), - o4.Select(x => x.Value), - o5.Select(x => x.Value) - , (v1,v2,v3,v4,v5) => - (v1,v2,v3,v4,v5) - ); - } + /// + /// AOT-friendly tuple overloads using property names instead of expressions. + /// + public static IObservable<(T1,T2,T3,T4,T5)> WhenAnyValue( + this TSender? sender, + string property1Name, string property2Name, string property3Name, string property4Name, string property5Name ) + { + var o1 = sender!.ObservableForProperty(property1Name, beforeChange: false, skipInitial: false, isDistinct: true); + var o2 = sender!.ObservableForProperty(property2Name, beforeChange: false, skipInitial: false, isDistinct: true); + var o3 = sender!.ObservableForProperty(property3Name, beforeChange: false, skipInitial: false, isDistinct: true); + var o4 = sender!.ObservableForProperty(property4Name, beforeChange: false, skipInitial: false, isDistinct: true); + var o5 = sender!.ObservableForProperty(property5Name, beforeChange: false, skipInitial: false, isDistinct: true); + return Observable.CombineLatest( + o1.Select(x => x.Value), + o2.Select(x => x.Value), + o3.Select(x => x.Value), + o4.Select(x => x.Value), + o5.Select(x => x.Value) + , (v1,v2,v3,v4,v5) => + (v1,v2,v3,v4,v5) + ); + } - /// - /// WhenAnyValue allows you to observe whenever the value of one or more - /// properties on an object have changed, providing an initial value when - /// the Observable is set up, unlike ObservableForProperty(). Use this - /// method in constructors to set up bindings between properties that also - /// need an initial setup. - /// - /// The object where the property chain starts. - /// if set to true [is distinct]. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The 3 property chain to reference. This will be a expression pointing to a end property or field. - /// The 4 property chain to reference. This will be a expression pointing to a end property or field. - /// The 5 property chain to reference. This will be a expression pointing to a end property or field. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable<(T1,T2,T3,T4,T5)> WhenAnyValue( - this TSender? sender, - Expression> property1, - Expression> property2, - Expression> property3, - Expression> property4, - Expression> property5 - , - bool isDistinct) - { - return sender!.WhenAny(property1, property2, property3, property4, property5, - (c1, c2, c3, c4, c5) => - (c1.Value, c2.Value, c3.Value, c4.Value, c5.Value), - isDistinct); - } + /// + /// WhenAnyValue allows you to observe whenever the value of one or more + /// properties on an object have changed, providing an initial value when + /// the Observable is set up, unlike ObservableForProperty(). Use this + /// method in constructors to set up bindings between properties that also + /// need an initial setup. + /// + /// The object where the property chain starts. + /// if set to true [is distinct]. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The 3 property chain to reference. This will be a expression pointing to a end property or field. + /// The 4 property chain to reference. This will be a expression pointing to a end property or field. + /// The 5 property chain to reference. This will be a expression pointing to a end property or field. + public static IObservable<(T1,T2,T3,T4,T5)> WhenAnyValue( + this TSender? sender, + Expression> property1, + Expression> property2, + Expression> property3, + Expression> property4, + Expression> property5 + , + bool isDistinct) + { + return sender!.WhenAny(property1, property2, property3, property4, property5, + (c1, c2, c3, c4, c5) => + (c1.Value, c2.Value, c3.Value, c4.Value, c5.Value), + isDistinct); + } - /// - /// AOT-friendly tuple overloads using property names with distinct option. - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable<(T1,T2,T3,T4,T5)> WhenAnyValue( - this TSender? sender, - string property1Name, string property2Name, string property3Name, string property4Name, string property5Name , - bool isDistinct) - { - var o1 = sender!.ObservableForProperty(property1Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct); - var o2 = sender!.ObservableForProperty(property2Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct); - var o3 = sender!.ObservableForProperty(property3Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct); - var o4 = sender!.ObservableForProperty(property4Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct); - var o5 = sender!.ObservableForProperty(property5Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct); - return Observable.CombineLatest( - o1.Select(x => x.Value), - o2.Select(x => x.Value), - o3.Select(x => x.Value), - o4.Select(x => x.Value), - o5.Select(x => x.Value) - , (v1,v2,v3,v4,v5) => - (v1,v2,v3,v4,v5) - ); - } + /// + /// AOT-friendly tuple overloads using property names with distinct option. + /// + public static IObservable<(T1,T2,T3,T4,T5)> WhenAnyValue( + this TSender? sender, + string property1Name, string property2Name, string property3Name, string property4Name, string property5Name , + bool isDistinct) + { + var o1 = sender!.ObservableForProperty(property1Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct); + var o2 = sender!.ObservableForProperty(property2Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct); + var o3 = sender!.ObservableForProperty(property3Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct); + var o4 = sender!.ObservableForProperty(property4Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct); + var o5 = sender!.ObservableForProperty(property5Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct); + return Observable.CombineLatest( + o1.Select(x => x.Value), + o2.Select(x => x.Value), + o3.Select(x => x.Value), + o4.Select(x => x.Value), + o5.Select(x => x.Value) + , (v1,v2,v3,v4,v5) => + (v1,v2,v3,v4,v5) + ); + } - /// - /// WhenAnyValue allows you to observe whenever the value of one or more - /// properties on an object have changed, providing an initial value when - /// the Observable is set up, unlike ObservableForProperty(). Use this - /// method in constructors to set up bindings between properties that also - /// need an initial setup. - /// - /// The object where the property chain starts. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The 3 property chain to reference. This will be a expression pointing to a end property or field. - /// The 4 property chain to reference. This will be a expression pointing to a end property or field. - /// The 5 property chain to reference. This will be a expression pointing to a end property or field. - /// The selector which will determine the final value from the properties. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyValue( - this TSender? sender, - Expression> property1, - Expression> property2, - Expression> property3, - Expression> property4, - Expression> property5, - Func selector) - { - return sender!.WhenAny(property1, property2, property3, property4, property5, - (c1, c2, c3, c4, c5) => - selector(c1.Value, c2.Value, c3.Value, c4.Value, c5.Value)); - } + /// + /// WhenAnyValue allows you to observe whenever the value of one or more + /// properties on an object have changed, providing an initial value when + /// the Observable is set up, unlike ObservableForProperty(). Use this + /// method in constructors to set up bindings between properties that also + /// need an initial setup. + /// + /// The object where the property chain starts. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The 3 property chain to reference. This will be a expression pointing to a end property or field. + /// The 4 property chain to reference. This will be a expression pointing to a end property or field. + /// The 5 property chain to reference. This will be a expression pointing to a end property or field. + /// The selector which will determine the final value from the properties. + public static IObservable WhenAnyValue( + this TSender? sender, + Expression> property1, + Expression> property2, + Expression> property3, + Expression> property4, + Expression> property5, + Func selector) + { + return sender!.WhenAny(property1, property2, property3, property4, property5, + (c1, c2, c3, c4, c5) => + selector(c1.Value, c2.Value, c3.Value, c4.Value, c5.Value)); + } - /// - /// AOT-friendly selector overload using property names. - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyValue( - this TSender? sender, - string property1Name, - string property2Name, - string property3Name, - string property4Name, - string property5Name, - Func selector) - { - var o1 = sender!.ObservableForProperty(property1Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - var o2 = sender!.ObservableForProperty(property2Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - var o3 = sender!.ObservableForProperty(property3Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - var o4 = sender!.ObservableForProperty(property4Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - var o5 = sender!.ObservableForProperty(property5Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - return Observable.CombineLatest( - o1, - o2, - o3, - o4, - o5 - , selector); - } + /// + /// AOT-friendly selector overload using property names. + /// + public static IObservable WhenAnyValue( + this TSender? sender, + string property1Name, + string property2Name, + string property3Name, + string property4Name, + string property5Name, + Func selector) + { + var o1 = sender!.ObservableForProperty(property1Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + var o2 = sender!.ObservableForProperty(property2Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + var o3 = sender!.ObservableForProperty(property3Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + var o4 = sender!.ObservableForProperty(property4Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + var o5 = sender!.ObservableForProperty(property5Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + return Observable.CombineLatest( + o1, + o2, + o3, + o4, + o5 + , selector); + } - /// - /// WhenAnyValue allows you to observe whenever the value of one or more - /// properties on an object have changed, providing an initial value when - /// the Observable is set up, unlike ObservableForProperty(). Use this - /// method in constructors to set up bindings between properties that also - /// need an initial setup. - /// - /// The object where the property chain starts. - /// if set to true [is distinct]. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The 3 property chain to reference. This will be a expression pointing to a end property or field. - /// The 4 property chain to reference. This will be a expression pointing to a end property or field. - /// The 5 property chain to reference. This will be a expression pointing to a end property or field. - /// The selector which will determine the final value from the properties. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyValue( - this TSender? sender, - Expression> property1, - Expression> property2, - Expression> property3, - Expression> property4, - Expression> property5, - Func selector, - bool isDistinct) - { - return sender!.WhenAny(property1, property2, property3, property4, property5, - (c1, c2, c3, c4, c5) => - selector(c1.Value, c2.Value, c3.Value, c4.Value, c5.Value), - isDistinct); - } + /// + /// WhenAnyValue allows you to observe whenever the value of one or more + /// properties on an object have changed, providing an initial value when + /// the Observable is set up, unlike ObservableForProperty(). Use this + /// method in constructors to set up bindings between properties that also + /// need an initial setup. + /// + /// The object where the property chain starts. + /// if set to true [is distinct]. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The 3 property chain to reference. This will be a expression pointing to a end property or field. + /// The 4 property chain to reference. This will be a expression pointing to a end property or field. + /// The 5 property chain to reference. This will be a expression pointing to a end property or field. + /// The selector which will determine the final value from the properties. + public static IObservable WhenAnyValue( + this TSender? sender, + Expression> property1, + Expression> property2, + Expression> property3, + Expression> property4, + Expression> property5, + Func selector, + bool isDistinct) + { + return sender!.WhenAny(property1, property2, property3, property4, property5, + (c1, c2, c3, c4, c5) => + selector(c1.Value, c2.Value, c3.Value, c4.Value, c5.Value), + isDistinct); + } - /// - /// AOT-friendly selector overload using property names and distinct option. - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyValue( - this TSender? sender, - string property1Name, - string property2Name, - string property3Name, - string property4Name, - string property5Name, - Func selector, - bool isDistinct) - { - var o1 = sender!.ObservableForProperty(property1Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - var o2 = sender!.ObservableForProperty(property2Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - var o3 = sender!.ObservableForProperty(property3Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - var o4 = sender!.ObservableForProperty(property4Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - var o5 = sender!.ObservableForProperty(property5Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - return Observable.CombineLatest( - o1, - o2, - o3, - o4, - o5 - , selector); - } + /// + /// AOT-friendly selector overload using property names and distinct option. + /// + public static IObservable WhenAnyValue( + this TSender? sender, + string property1Name, + string property2Name, + string property3Name, + string property4Name, + string property5Name, + Func selector, + bool isDistinct) + { + var o1 = sender!.ObservableForProperty(property1Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + var o2 = sender!.ObservableForProperty(property2Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + var o3 = sender!.ObservableForProperty(property3Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + var o4 = sender!.ObservableForProperty(property4Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + var o5 = sender!.ObservableForProperty(property5Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + return Observable.CombineLatest( + o1, + o2, + o3, + o4, + o5 + , selector); + } - /// - /// WhenAny allows you to observe whenever one or more properties on an - /// object have changed, providing an initial value when the Observable - /// is set up, unlike ObservableForProperty(). Use this method in - /// constructors to set up bindings between properties that also need an - /// initial setup. - /// - /// The object where the property chain starts. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The 3 property chain to reference. This will be a expression pointing to a end property or field. - /// The 4 property chain to reference. This will be a expression pointing to a end property or field. - /// The 5 property chain to reference. This will be a expression pointing to a end property or field. - /// The selector which will determine the final value from the properties. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAny uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAny may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAny( - this TSender? sender, - Expression> property1, - Expression> property2, - Expression> property3, - Expression> property4, - Expression> property5, - Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector) - { - return Observable.CombineLatest( - sender!.ObservableForProperty(property1, false, false), - sender!.ObservableForProperty(property2, false, false), - sender!.ObservableForProperty(property3, false, false), - sender!.ObservableForProperty(property4, false, false), - sender!.ObservableForProperty(property5, false, false), - selector - ); - } + /// + /// WhenAny allows you to observe whenever one or more properties on an + /// object have changed, providing an initial value when the Observable + /// is set up, unlike ObservableForProperty(). Use this method in + /// constructors to set up bindings between properties that also need an + /// initial setup. + /// + /// The object where the property chain starts. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The 3 property chain to reference. This will be a expression pointing to a end property or field. + /// The 4 property chain to reference. This will be a expression pointing to a end property or field. + /// The 5 property chain to reference. This will be a expression pointing to a end property or field. + /// The selector which will determine the final value from the properties. + public static IObservable WhenAny( + this TSender? sender, + Expression> property1, + Expression> property2, + Expression> property3, + Expression> property4, + Expression> property5, + Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector) + { + return Observable.CombineLatest( + sender!.ObservableForProperty(property1, false, false), + sender!.ObservableForProperty(property2, false, false), + sender!.ObservableForProperty(property3, false, false), + sender!.ObservableForProperty(property4, false, false), + sender!.ObservableForProperty(property5, false, false), + selector + ); + } - /// - /// AOT-friendly WhenAny overload using property names. - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAny uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAny may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAny( - this TSender? sender, - string property1Name, - string property2Name, - string property3Name, - string property4Name, - string property5Name, - Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector) - { - return Observable.CombineLatest( - sender!.ObservableForProperty(property1Name, false, false), - sender!.ObservableForProperty(property2Name, false, false), - sender!.ObservableForProperty(property3Name, false, false), - sender!.ObservableForProperty(property4Name, false, false), - sender!.ObservableForProperty(property5Name, false, false), - selector - ); - } + /// + /// AOT-friendly WhenAny overload using property names. + /// + public static IObservable WhenAny( + this TSender? sender, + string property1Name, + string property2Name, + string property3Name, + string property4Name, + string property5Name, + Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector) + { + return Observable.CombineLatest( + sender!.ObservableForProperty(property1Name, false, false), + sender!.ObservableForProperty(property2Name, false, false), + sender!.ObservableForProperty(property3Name, false, false), + sender!.ObservableForProperty(property4Name, false, false), + sender!.ObservableForProperty(property5Name, false, false), + selector + ); + } - /// - /// WhenAny allows you to observe whenever one or more properties on an - /// object have changed, providing an initial value when the Observable - /// is set up, unlike ObservableForProperty(). Use this method in - /// constructors to set up bindings between properties that also need an - /// initial setup. - /// - /// The object where the property chain starts. - /// if set to true [is distinct]. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The 3 property chain to reference. This will be a expression pointing to a end property or field. - /// The 4 property chain to reference. This will be a expression pointing to a end property or field. - /// The 5 property chain to reference. This will be a expression pointing to a end property or field. - /// The selector which will determine the final value from the properties. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAny uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAny may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAny( - this TSender? sender, - Expression> property1, - Expression> property2, - Expression> property3, - Expression> property4, - Expression> property5, - Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector, - bool isDistinct) - { - return Observable.CombineLatest( - sender!.ObservableForProperty(property1, false, false, isDistinct), - sender!.ObservableForProperty(property2, false, false, isDistinct), - sender!.ObservableForProperty(property3, false, false, isDistinct), - sender!.ObservableForProperty(property4, false, false, isDistinct), - sender!.ObservableForProperty(property5, false, false, isDistinct), - selector - ); - } + /// + /// WhenAny allows you to observe whenever one or more properties on an + /// object have changed, providing an initial value when the Observable + /// is set up, unlike ObservableForProperty(). Use this method in + /// constructors to set up bindings between properties that also need an + /// initial setup. + /// + /// The object where the property chain starts. + /// if set to true [is distinct]. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The 3 property chain to reference. This will be a expression pointing to a end property or field. + /// The 4 property chain to reference. This will be a expression pointing to a end property or field. + /// The 5 property chain to reference. This will be a expression pointing to a end property or field. + /// The selector which will determine the final value from the properties. + public static IObservable WhenAny( + this TSender? sender, + Expression> property1, + Expression> property2, + Expression> property3, + Expression> property4, + Expression> property5, + Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector, + bool isDistinct) + { + return Observable.CombineLatest( + sender!.ObservableForProperty(property1, false, false, isDistinct), + sender!.ObservableForProperty(property2, false, false, isDistinct), + sender!.ObservableForProperty(property3, false, false, isDistinct), + sender!.ObservableForProperty(property4, false, false, isDistinct), + sender!.ObservableForProperty(property5, false, false, isDistinct), + selector + ); + } - /// - /// AOT-friendly WhenAny overload using property names and distinct option. - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAny uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAny may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAny( - this TSender? sender, - string property1Name, - string property2Name, - string property3Name, - string property4Name, - string property5Name, - Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector, - bool isDistinct) - { - return Observable.CombineLatest( - sender!.ObservableForProperty(property1Name, false, false, isDistinct), - sender!.ObservableForProperty(property2Name, false, false, isDistinct), - sender!.ObservableForProperty(property3Name, false, false, isDistinct), - sender!.ObservableForProperty(property4Name, false, false, isDistinct), - sender!.ObservableForProperty(property5Name, false, false, isDistinct), - selector - ); - } + /// + /// AOT-friendly WhenAny overload using property names and distinct option. + /// + public static IObservable WhenAny( + this TSender? sender, + string property1Name, + string property2Name, + string property3Name, + string property4Name, + string property5Name, + Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector, + bool isDistinct) + { + return Observable.CombineLatest( + sender!.ObservableForProperty(property1Name, false, false, isDistinct), + sender!.ObservableForProperty(property2Name, false, false, isDistinct), + sender!.ObservableForProperty(property3Name, false, false, isDistinct), + sender!.ObservableForProperty(property4Name, false, false, isDistinct), + sender!.ObservableForProperty(property5Name, false, false, isDistinct), + selector + ); + } - /// - /// WhenAny allows you to observe whenever one or more properties on an - /// object have changed, providing an initial value when the Observable - /// is set up, unlike ObservableForProperty(). Use this method in - /// constructors to set up bindings between properties that also need an - /// initial setup. - /// - /// The object where the property chain starts. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The 3 property chain to reference. This will be a expression pointing to a end property or field. - /// The 4 property chain to reference. This will be a expression pointing to a end property or field. - /// The 5 property chain to reference. This will be a expression pointing to a end property or field. - /// The selector which will determine the final value from the properties. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyDynamic uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyDynamic may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyDynamic( - this TSender? sender, - Expression? property1, - Expression? property2, - Expression? property3, - Expression? property4, - Expression? property5, - Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector) - { - return Observable.CombineLatest( - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property1, false, false), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property2, false, false), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property3, false, false), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property4, false, false), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property5, false, false), - selector - ); - } + /// + /// WhenAny allows you to observe whenever one or more properties on an + /// object have changed, providing an initial value when the Observable + /// is set up, unlike ObservableForProperty(). Use this method in + /// constructors to set up bindings between properties that also need an + /// initial setup. + /// + /// The object where the property chain starts. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The 3 property chain to reference. This will be a expression pointing to a end property or field. + /// The 4 property chain to reference. This will be a expression pointing to a end property or field. + /// The 5 property chain to reference. This will be a expression pointing to a end property or field. + /// The selector which will determine the final value from the properties. + public static IObservable WhenAnyDynamic( + this TSender? sender, + Expression? property1, + Expression? property2, + Expression? property3, + Expression? property4, + Expression? property5, + Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector) + { + return Observable.CombineLatest( + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property1, false, false), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property2, false, false), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property3, false, false), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property4, false, false), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property5, false, false), + selector + ); + } - /// - /// WhenAny allows you to observe whenever one or more properties on an - /// object have changed, providing an initial value when the Observable - /// is set up, unlike ObservableForProperty(). Use this method in - /// constructors to set up bindings between properties that also need an - /// initial setup. - /// - /// The object where the property chain starts. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The 3 property chain to reference. This will be a expression pointing to a end property or field. - /// The 4 property chain to reference. This will be a expression pointing to a end property or field. - /// The 5 property chain to reference. This will be a expression pointing to a end property or field. - /// The selector which will determine the final value from the properties. - /// if set to true [is distinct]. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyDynamic uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyDynamic may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyDynamic( - this TSender? sender, - Expression? property1, - Expression? property2, - Expression? property3, - Expression? property4, - Expression? property5, - Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector, - bool isDistinct) - { - return Observable.CombineLatest( - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property1, false, false, isDistinct), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property2, false, false, isDistinct), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property3, false, false, isDistinct), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property4, false, false, isDistinct), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property5, false, false, isDistinct), - selector - ); - } + /// + /// WhenAny allows you to observe whenever one or more properties on an + /// object have changed, providing an initial value when the Observable + /// is set up, unlike ObservableForProperty(). Use this method in + /// constructors to set up bindings between properties that also need an + /// initial setup. + /// + /// The object where the property chain starts. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The 3 property chain to reference. This will be a expression pointing to a end property or field. + /// The 4 property chain to reference. This will be a expression pointing to a end property or field. + /// The 5 property chain to reference. This will be a expression pointing to a end property or field. + /// The selector which will determine the final value from the properties. + /// if set to true [is distinct]. + public static IObservable WhenAnyDynamic( + this TSender? sender, + Expression? property1, + Expression? property2, + Expression? property3, + Expression? property4, + Expression? property5, + Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector, + bool isDistinct) + { + return Observable.CombineLatest( + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property1, false, false, isDistinct), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property2, false, false, isDistinct), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property3, false, false, isDistinct), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property4, false, false, isDistinct), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property5, false, false, isDistinct), + selector + ); + } - /// - /// WhenAnyValue allows you to observe whenever the value of one or more - /// properties on an object have changed, providing an initial value when - /// the Observable is set up, unlike ObservableForProperty(). Use this - /// method in constructors to set up bindings between properties that also - /// need an initial setup. - /// - /// The object where the property chain starts. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The 3 property chain to reference. This will be a expression pointing to a end property or field. - /// The 4 property chain to reference. This will be a expression pointing to a end property or field. - /// The 5 property chain to reference. This will be a expression pointing to a end property or field. - /// The 6 property chain to reference. This will be a expression pointing to a end property or field. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable<(T1,T2,T3,T4,T5,T6)> WhenAnyValue( - this TSender? sender, - Expression> property1, - Expression> property2, - Expression> property3, - Expression> property4, - Expression> property5, - Expression> property6 - ) - { - return sender!.WhenAny(property1, property2, property3, property4, property5, property6, - (c1, c2, c3, c4, c5, c6) => - (c1.Value, c2.Value, c3.Value, c4.Value, c5.Value, c6.Value)); - } + /// + /// WhenAnyValue allows you to observe whenever the value of one or more + /// properties on an object have changed, providing an initial value when + /// the Observable is set up, unlike ObservableForProperty(). Use this + /// method in constructors to set up bindings between properties that also + /// need an initial setup. + /// + /// The object where the property chain starts. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The 3 property chain to reference. This will be a expression pointing to a end property or field. + /// The 4 property chain to reference. This will be a expression pointing to a end property or field. + /// The 5 property chain to reference. This will be a expression pointing to a end property or field. + /// The 6 property chain to reference. This will be a expression pointing to a end property or field. + public static IObservable<(T1,T2,T3,T4,T5,T6)> WhenAnyValue( + this TSender? sender, + Expression> property1, + Expression> property2, + Expression> property3, + Expression> property4, + Expression> property5, + Expression> property6 + ) + { + return sender!.WhenAny(property1, property2, property3, property4, property5, property6, + (c1, c2, c3, c4, c5, c6) => + (c1.Value, c2.Value, c3.Value, c4.Value, c5.Value, c6.Value)); + } - /// - /// AOT-friendly tuple overloads using property names instead of expressions. - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable<(T1,T2,T3,T4,T5,T6)> WhenAnyValue( - this TSender? sender, - string property1Name, string property2Name, string property3Name, string property4Name, string property5Name, string property6Name ) - { - var o1 = sender!.ObservableForProperty(property1Name, beforeChange: false, skipInitial: false, isDistinct: true); - var o2 = sender!.ObservableForProperty(property2Name, beforeChange: false, skipInitial: false, isDistinct: true); - var o3 = sender!.ObservableForProperty(property3Name, beforeChange: false, skipInitial: false, isDistinct: true); - var o4 = sender!.ObservableForProperty(property4Name, beforeChange: false, skipInitial: false, isDistinct: true); - var o5 = sender!.ObservableForProperty(property5Name, beforeChange: false, skipInitial: false, isDistinct: true); - var o6 = sender!.ObservableForProperty(property6Name, beforeChange: false, skipInitial: false, isDistinct: true); - return Observable.CombineLatest( - o1.Select(x => x.Value), - o2.Select(x => x.Value), - o3.Select(x => x.Value), - o4.Select(x => x.Value), - o5.Select(x => x.Value), - o6.Select(x => x.Value) - , (v1,v2,v3,v4,v5,v6) => - (v1,v2,v3,v4,v5,v6) - ); - } + /// + /// AOT-friendly tuple overloads using property names instead of expressions. + /// + public static IObservable<(T1,T2,T3,T4,T5,T6)> WhenAnyValue( + this TSender? sender, + string property1Name, string property2Name, string property3Name, string property4Name, string property5Name, string property6Name ) + { + var o1 = sender!.ObservableForProperty(property1Name, beforeChange: false, skipInitial: false, isDistinct: true); + var o2 = sender!.ObservableForProperty(property2Name, beforeChange: false, skipInitial: false, isDistinct: true); + var o3 = sender!.ObservableForProperty(property3Name, beforeChange: false, skipInitial: false, isDistinct: true); + var o4 = sender!.ObservableForProperty(property4Name, beforeChange: false, skipInitial: false, isDistinct: true); + var o5 = sender!.ObservableForProperty(property5Name, beforeChange: false, skipInitial: false, isDistinct: true); + var o6 = sender!.ObservableForProperty(property6Name, beforeChange: false, skipInitial: false, isDistinct: true); + return Observable.CombineLatest( + o1.Select(x => x.Value), + o2.Select(x => x.Value), + o3.Select(x => x.Value), + o4.Select(x => x.Value), + o5.Select(x => x.Value), + o6.Select(x => x.Value) + , (v1,v2,v3,v4,v5,v6) => + (v1,v2,v3,v4,v5,v6) + ); + } - /// - /// WhenAnyValue allows you to observe whenever the value of one or more - /// properties on an object have changed, providing an initial value when - /// the Observable is set up, unlike ObservableForProperty(). Use this - /// method in constructors to set up bindings between properties that also - /// need an initial setup. - /// - /// The object where the property chain starts. - /// if set to true [is distinct]. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The 3 property chain to reference. This will be a expression pointing to a end property or field. - /// The 4 property chain to reference. This will be a expression pointing to a end property or field. - /// The 5 property chain to reference. This will be a expression pointing to a end property or field. - /// The 6 property chain to reference. This will be a expression pointing to a end property or field. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable<(T1,T2,T3,T4,T5,T6)> WhenAnyValue( - this TSender? sender, - Expression> property1, - Expression> property2, - Expression> property3, - Expression> property4, - Expression> property5, - Expression> property6 - , - bool isDistinct) - { - return sender!.WhenAny(property1, property2, property3, property4, property5, property6, - (c1, c2, c3, c4, c5, c6) => - (c1.Value, c2.Value, c3.Value, c4.Value, c5.Value, c6.Value), - isDistinct); - } + /// + /// WhenAnyValue allows you to observe whenever the value of one or more + /// properties on an object have changed, providing an initial value when + /// the Observable is set up, unlike ObservableForProperty(). Use this + /// method in constructors to set up bindings between properties that also + /// need an initial setup. + /// + /// The object where the property chain starts. + /// if set to true [is distinct]. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The 3 property chain to reference. This will be a expression pointing to a end property or field. + /// The 4 property chain to reference. This will be a expression pointing to a end property or field. + /// The 5 property chain to reference. This will be a expression pointing to a end property or field. + /// The 6 property chain to reference. This will be a expression pointing to a end property or field. + public static IObservable<(T1,T2,T3,T4,T5,T6)> WhenAnyValue( + this TSender? sender, + Expression> property1, + Expression> property2, + Expression> property3, + Expression> property4, + Expression> property5, + Expression> property6 + , + bool isDistinct) + { + return sender!.WhenAny(property1, property2, property3, property4, property5, property6, + (c1, c2, c3, c4, c5, c6) => + (c1.Value, c2.Value, c3.Value, c4.Value, c5.Value, c6.Value), + isDistinct); + } - /// - /// AOT-friendly tuple overloads using property names with distinct option. - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable<(T1,T2,T3,T4,T5,T6)> WhenAnyValue( - this TSender? sender, - string property1Name, string property2Name, string property3Name, string property4Name, string property5Name, string property6Name , - bool isDistinct) - { - var o1 = sender!.ObservableForProperty(property1Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct); - var o2 = sender!.ObservableForProperty(property2Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct); - var o3 = sender!.ObservableForProperty(property3Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct); - var o4 = sender!.ObservableForProperty(property4Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct); - var o5 = sender!.ObservableForProperty(property5Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct); - var o6 = sender!.ObservableForProperty(property6Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct); - return Observable.CombineLatest( - o1.Select(x => x.Value), - o2.Select(x => x.Value), - o3.Select(x => x.Value), - o4.Select(x => x.Value), - o5.Select(x => x.Value), - o6.Select(x => x.Value) - , (v1,v2,v3,v4,v5,v6) => - (v1,v2,v3,v4,v5,v6) - ); - } + /// + /// AOT-friendly tuple overloads using property names with distinct option. + /// + public static IObservable<(T1,T2,T3,T4,T5,T6)> WhenAnyValue( + this TSender? sender, + string property1Name, string property2Name, string property3Name, string property4Name, string property5Name, string property6Name , + bool isDistinct) + { + var o1 = sender!.ObservableForProperty(property1Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct); + var o2 = sender!.ObservableForProperty(property2Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct); + var o3 = sender!.ObservableForProperty(property3Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct); + var o4 = sender!.ObservableForProperty(property4Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct); + var o5 = sender!.ObservableForProperty(property5Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct); + var o6 = sender!.ObservableForProperty(property6Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct); + return Observable.CombineLatest( + o1.Select(x => x.Value), + o2.Select(x => x.Value), + o3.Select(x => x.Value), + o4.Select(x => x.Value), + o5.Select(x => x.Value), + o6.Select(x => x.Value) + , (v1,v2,v3,v4,v5,v6) => + (v1,v2,v3,v4,v5,v6) + ); + } - /// - /// WhenAnyValue allows you to observe whenever the value of one or more - /// properties on an object have changed, providing an initial value when - /// the Observable is set up, unlike ObservableForProperty(). Use this - /// method in constructors to set up bindings between properties that also - /// need an initial setup. - /// - /// The object where the property chain starts. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The 3 property chain to reference. This will be a expression pointing to a end property or field. - /// The 4 property chain to reference. This will be a expression pointing to a end property or field. - /// The 5 property chain to reference. This will be a expression pointing to a end property or field. - /// The 6 property chain to reference. This will be a expression pointing to a end property or field. - /// The selector which will determine the final value from the properties. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyValue( - this TSender? sender, - Expression> property1, - Expression> property2, - Expression> property3, - Expression> property4, - Expression> property5, - Expression> property6, - Func selector) - { - return sender!.WhenAny(property1, property2, property3, property4, property5, property6, - (c1, c2, c3, c4, c5, c6) => - selector(c1.Value, c2.Value, c3.Value, c4.Value, c5.Value, c6.Value)); - } + /// + /// WhenAnyValue allows you to observe whenever the value of one or more + /// properties on an object have changed, providing an initial value when + /// the Observable is set up, unlike ObservableForProperty(). Use this + /// method in constructors to set up bindings between properties that also + /// need an initial setup. + /// + /// The object where the property chain starts. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The 3 property chain to reference. This will be a expression pointing to a end property or field. + /// The 4 property chain to reference. This will be a expression pointing to a end property or field. + /// The 5 property chain to reference. This will be a expression pointing to a end property or field. + /// The 6 property chain to reference. This will be a expression pointing to a end property or field. + /// The selector which will determine the final value from the properties. + public static IObservable WhenAnyValue( + this TSender? sender, + Expression> property1, + Expression> property2, + Expression> property3, + Expression> property4, + Expression> property5, + Expression> property6, + Func selector) + { + return sender!.WhenAny(property1, property2, property3, property4, property5, property6, + (c1, c2, c3, c4, c5, c6) => + selector(c1.Value, c2.Value, c3.Value, c4.Value, c5.Value, c6.Value)); + } - /// - /// AOT-friendly selector overload using property names. - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyValue( - this TSender? sender, - string property1Name, - string property2Name, - string property3Name, - string property4Name, - string property5Name, - string property6Name, - Func selector) - { - var o1 = sender!.ObservableForProperty(property1Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - var o2 = sender!.ObservableForProperty(property2Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - var o3 = sender!.ObservableForProperty(property3Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - var o4 = sender!.ObservableForProperty(property4Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - var o5 = sender!.ObservableForProperty(property5Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - var o6 = sender!.ObservableForProperty(property6Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - return Observable.CombineLatest( - o1, - o2, - o3, - o4, - o5, - o6 - , selector); - } + /// + /// AOT-friendly selector overload using property names. + /// + public static IObservable WhenAnyValue( + this TSender? sender, + string property1Name, + string property2Name, + string property3Name, + string property4Name, + string property5Name, + string property6Name, + Func selector) + { + var o1 = sender!.ObservableForProperty(property1Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + var o2 = sender!.ObservableForProperty(property2Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + var o3 = sender!.ObservableForProperty(property3Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + var o4 = sender!.ObservableForProperty(property4Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + var o5 = sender!.ObservableForProperty(property5Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + var o6 = sender!.ObservableForProperty(property6Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + return Observable.CombineLatest( + o1, + o2, + o3, + o4, + o5, + o6 + , selector); + } - /// - /// WhenAnyValue allows you to observe whenever the value of one or more - /// properties on an object have changed, providing an initial value when - /// the Observable is set up, unlike ObservableForProperty(). Use this - /// method in constructors to set up bindings between properties that also - /// need an initial setup. - /// - /// The object where the property chain starts. - /// if set to true [is distinct]. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The 3 property chain to reference. This will be a expression pointing to a end property or field. - /// The 4 property chain to reference. This will be a expression pointing to a end property or field. - /// The 5 property chain to reference. This will be a expression pointing to a end property or field. - /// The 6 property chain to reference. This will be a expression pointing to a end property or field. - /// The selector which will determine the final value from the properties. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyValue( - this TSender? sender, - Expression> property1, - Expression> property2, - Expression> property3, - Expression> property4, - Expression> property5, - Expression> property6, - Func selector, - bool isDistinct) - { - return sender!.WhenAny(property1, property2, property3, property4, property5, property6, - (c1, c2, c3, c4, c5, c6) => - selector(c1.Value, c2.Value, c3.Value, c4.Value, c5.Value, c6.Value), - isDistinct); - } + /// + /// WhenAnyValue allows you to observe whenever the value of one or more + /// properties on an object have changed, providing an initial value when + /// the Observable is set up, unlike ObservableForProperty(). Use this + /// method in constructors to set up bindings between properties that also + /// need an initial setup. + /// + /// The object where the property chain starts. + /// if set to true [is distinct]. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The 3 property chain to reference. This will be a expression pointing to a end property or field. + /// The 4 property chain to reference. This will be a expression pointing to a end property or field. + /// The 5 property chain to reference. This will be a expression pointing to a end property or field. + /// The 6 property chain to reference. This will be a expression pointing to a end property or field. + /// The selector which will determine the final value from the properties. + public static IObservable WhenAnyValue( + this TSender? sender, + Expression> property1, + Expression> property2, + Expression> property3, + Expression> property4, + Expression> property5, + Expression> property6, + Func selector, + bool isDistinct) + { + return sender!.WhenAny(property1, property2, property3, property4, property5, property6, + (c1, c2, c3, c4, c5, c6) => + selector(c1.Value, c2.Value, c3.Value, c4.Value, c5.Value, c6.Value), + isDistinct); + } - /// - /// AOT-friendly selector overload using property names and distinct option. - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyValue( - this TSender? sender, - string property1Name, - string property2Name, - string property3Name, - string property4Name, - string property5Name, - string property6Name, - Func selector, - bool isDistinct) - { - var o1 = sender!.ObservableForProperty(property1Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - var o2 = sender!.ObservableForProperty(property2Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - var o3 = sender!.ObservableForProperty(property3Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - var o4 = sender!.ObservableForProperty(property4Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - var o5 = sender!.ObservableForProperty(property5Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - var o6 = sender!.ObservableForProperty(property6Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - return Observable.CombineLatest( - o1, - o2, - o3, - o4, - o5, - o6 - , selector); - } + /// + /// AOT-friendly selector overload using property names and distinct option. + /// + public static IObservable WhenAnyValue( + this TSender? sender, + string property1Name, + string property2Name, + string property3Name, + string property4Name, + string property5Name, + string property6Name, + Func selector, + bool isDistinct) + { + var o1 = sender!.ObservableForProperty(property1Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + var o2 = sender!.ObservableForProperty(property2Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + var o3 = sender!.ObservableForProperty(property3Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + var o4 = sender!.ObservableForProperty(property4Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + var o5 = sender!.ObservableForProperty(property5Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + var o6 = sender!.ObservableForProperty(property6Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + return Observable.CombineLatest( + o1, + o2, + o3, + o4, + o5, + o6 + , selector); + } - /// - /// WhenAny allows you to observe whenever one or more properties on an - /// object have changed, providing an initial value when the Observable - /// is set up, unlike ObservableForProperty(). Use this method in - /// constructors to set up bindings between properties that also need an - /// initial setup. - /// - /// The object where the property chain starts. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The 3 property chain to reference. This will be a expression pointing to a end property or field. - /// The 4 property chain to reference. This will be a expression pointing to a end property or field. - /// The 5 property chain to reference. This will be a expression pointing to a end property or field. - /// The 6 property chain to reference. This will be a expression pointing to a end property or field. - /// The selector which will determine the final value from the properties. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAny uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAny may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAny( - this TSender? sender, - Expression> property1, - Expression> property2, - Expression> property3, - Expression> property4, - Expression> property5, - Expression> property6, - Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector) - { - return Observable.CombineLatest( - sender!.ObservableForProperty(property1, false, false), - sender!.ObservableForProperty(property2, false, false), - sender!.ObservableForProperty(property3, false, false), - sender!.ObservableForProperty(property4, false, false), - sender!.ObservableForProperty(property5, false, false), - sender!.ObservableForProperty(property6, false, false), - selector - ); - } + /// + /// WhenAny allows you to observe whenever one or more properties on an + /// object have changed, providing an initial value when the Observable + /// is set up, unlike ObservableForProperty(). Use this method in + /// constructors to set up bindings between properties that also need an + /// initial setup. + /// + /// The object where the property chain starts. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The 3 property chain to reference. This will be a expression pointing to a end property or field. + /// The 4 property chain to reference. This will be a expression pointing to a end property or field. + /// The 5 property chain to reference. This will be a expression pointing to a end property or field. + /// The 6 property chain to reference. This will be a expression pointing to a end property or field. + /// The selector which will determine the final value from the properties. + public static IObservable WhenAny( + this TSender? sender, + Expression> property1, + Expression> property2, + Expression> property3, + Expression> property4, + Expression> property5, + Expression> property6, + Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector) + { + return Observable.CombineLatest( + sender!.ObservableForProperty(property1, false, false), + sender!.ObservableForProperty(property2, false, false), + sender!.ObservableForProperty(property3, false, false), + sender!.ObservableForProperty(property4, false, false), + sender!.ObservableForProperty(property5, false, false), + sender!.ObservableForProperty(property6, false, false), + selector + ); + } - /// - /// AOT-friendly WhenAny overload using property names. - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAny uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAny may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAny( - this TSender? sender, - string property1Name, - string property2Name, - string property3Name, - string property4Name, - string property5Name, - string property6Name, - Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector) - { - return Observable.CombineLatest( - sender!.ObservableForProperty(property1Name, false, false), - sender!.ObservableForProperty(property2Name, false, false), - sender!.ObservableForProperty(property3Name, false, false), - sender!.ObservableForProperty(property4Name, false, false), - sender!.ObservableForProperty(property5Name, false, false), - sender!.ObservableForProperty(property6Name, false, false), - selector - ); - } + /// + /// AOT-friendly WhenAny overload using property names. + /// + public static IObservable WhenAny( + this TSender? sender, + string property1Name, + string property2Name, + string property3Name, + string property4Name, + string property5Name, + string property6Name, + Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector) + { + return Observable.CombineLatest( + sender!.ObservableForProperty(property1Name, false, false), + sender!.ObservableForProperty(property2Name, false, false), + sender!.ObservableForProperty(property3Name, false, false), + sender!.ObservableForProperty(property4Name, false, false), + sender!.ObservableForProperty(property5Name, false, false), + sender!.ObservableForProperty(property6Name, false, false), + selector + ); + } - /// - /// WhenAny allows you to observe whenever one or more properties on an - /// object have changed, providing an initial value when the Observable - /// is set up, unlike ObservableForProperty(). Use this method in - /// constructors to set up bindings between properties that also need an - /// initial setup. - /// - /// The object where the property chain starts. - /// if set to true [is distinct]. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The 3 property chain to reference. This will be a expression pointing to a end property or field. - /// The 4 property chain to reference. This will be a expression pointing to a end property or field. - /// The 5 property chain to reference. This will be a expression pointing to a end property or field. - /// The 6 property chain to reference. This will be a expression pointing to a end property or field. - /// The selector which will determine the final value from the properties. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAny uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAny may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAny( - this TSender? sender, - Expression> property1, - Expression> property2, - Expression> property3, - Expression> property4, - Expression> property5, - Expression> property6, - Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector, - bool isDistinct) - { - return Observable.CombineLatest( - sender!.ObservableForProperty(property1, false, false, isDistinct), - sender!.ObservableForProperty(property2, false, false, isDistinct), - sender!.ObservableForProperty(property3, false, false, isDistinct), - sender!.ObservableForProperty(property4, false, false, isDistinct), - sender!.ObservableForProperty(property5, false, false, isDistinct), - sender!.ObservableForProperty(property6, false, false, isDistinct), - selector - ); - } + /// + /// WhenAny allows you to observe whenever one or more properties on an + /// object have changed, providing an initial value when the Observable + /// is set up, unlike ObservableForProperty(). Use this method in + /// constructors to set up bindings between properties that also need an + /// initial setup. + /// + /// The object where the property chain starts. + /// if set to true [is distinct]. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The 3 property chain to reference. This will be a expression pointing to a end property or field. + /// The 4 property chain to reference. This will be a expression pointing to a end property or field. + /// The 5 property chain to reference. This will be a expression pointing to a end property or field. + /// The 6 property chain to reference. This will be a expression pointing to a end property or field. + /// The selector which will determine the final value from the properties. + public static IObservable WhenAny( + this TSender? sender, + Expression> property1, + Expression> property2, + Expression> property3, + Expression> property4, + Expression> property5, + Expression> property6, + Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector, + bool isDistinct) + { + return Observable.CombineLatest( + sender!.ObservableForProperty(property1, false, false, isDistinct), + sender!.ObservableForProperty(property2, false, false, isDistinct), + sender!.ObservableForProperty(property3, false, false, isDistinct), + sender!.ObservableForProperty(property4, false, false, isDistinct), + sender!.ObservableForProperty(property5, false, false, isDistinct), + sender!.ObservableForProperty(property6, false, false, isDistinct), + selector + ); + } - /// - /// AOT-friendly WhenAny overload using property names and distinct option. - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAny uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAny may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAny( - this TSender? sender, - string property1Name, - string property2Name, - string property3Name, - string property4Name, - string property5Name, - string property6Name, - Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector, - bool isDistinct) - { - return Observable.CombineLatest( - sender!.ObservableForProperty(property1Name, false, false, isDistinct), - sender!.ObservableForProperty(property2Name, false, false, isDistinct), - sender!.ObservableForProperty(property3Name, false, false, isDistinct), - sender!.ObservableForProperty(property4Name, false, false, isDistinct), - sender!.ObservableForProperty(property5Name, false, false, isDistinct), - sender!.ObservableForProperty(property6Name, false, false, isDistinct), - selector - ); - } + /// + /// AOT-friendly WhenAny overload using property names and distinct option. + /// + public static IObservable WhenAny( + this TSender? sender, + string property1Name, + string property2Name, + string property3Name, + string property4Name, + string property5Name, + string property6Name, + Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector, + bool isDistinct) + { + return Observable.CombineLatest( + sender!.ObservableForProperty(property1Name, false, false, isDistinct), + sender!.ObservableForProperty(property2Name, false, false, isDistinct), + sender!.ObservableForProperty(property3Name, false, false, isDistinct), + sender!.ObservableForProperty(property4Name, false, false, isDistinct), + sender!.ObservableForProperty(property5Name, false, false, isDistinct), + sender!.ObservableForProperty(property6Name, false, false, isDistinct), + selector + ); + } - /// - /// WhenAny allows you to observe whenever one or more properties on an - /// object have changed, providing an initial value when the Observable - /// is set up, unlike ObservableForProperty(). Use this method in - /// constructors to set up bindings between properties that also need an - /// initial setup. - /// - /// The object where the property chain starts. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The 3 property chain to reference. This will be a expression pointing to a end property or field. - /// The 4 property chain to reference. This will be a expression pointing to a end property or field. - /// The 5 property chain to reference. This will be a expression pointing to a end property or field. - /// The 6 property chain to reference. This will be a expression pointing to a end property or field. - /// The selector which will determine the final value from the properties. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyDynamic uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyDynamic may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyDynamic( - this TSender? sender, - Expression? property1, - Expression? property2, - Expression? property3, - Expression? property4, - Expression? property5, - Expression? property6, - Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector) - { - return Observable.CombineLatest( - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property1, false, false), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property2, false, false), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property3, false, false), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property4, false, false), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property5, false, false), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property6, false, false), - selector - ); - } + /// + /// WhenAny allows you to observe whenever one or more properties on an + /// object have changed, providing an initial value when the Observable + /// is set up, unlike ObservableForProperty(). Use this method in + /// constructors to set up bindings between properties that also need an + /// initial setup. + /// + /// The object where the property chain starts. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The 3 property chain to reference. This will be a expression pointing to a end property or field. + /// The 4 property chain to reference. This will be a expression pointing to a end property or field. + /// The 5 property chain to reference. This will be a expression pointing to a end property or field. + /// The 6 property chain to reference. This will be a expression pointing to a end property or field. + /// The selector which will determine the final value from the properties. + public static IObservable WhenAnyDynamic( + this TSender? sender, + Expression? property1, + Expression? property2, + Expression? property3, + Expression? property4, + Expression? property5, + Expression? property6, + Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector) + { + return Observable.CombineLatest( + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property1, false, false), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property2, false, false), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property3, false, false), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property4, false, false), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property5, false, false), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property6, false, false), + selector + ); + } - /// - /// WhenAny allows you to observe whenever one or more properties on an - /// object have changed, providing an initial value when the Observable - /// is set up, unlike ObservableForProperty(). Use this method in - /// constructors to set up bindings between properties that also need an - /// initial setup. - /// - /// The object where the property chain starts. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The 3 property chain to reference. This will be a expression pointing to a end property or field. - /// The 4 property chain to reference. This will be a expression pointing to a end property or field. - /// The 5 property chain to reference. This will be a expression pointing to a end property or field. - /// The 6 property chain to reference. This will be a expression pointing to a end property or field. - /// The selector which will determine the final value from the properties. - /// if set to true [is distinct]. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyDynamic uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyDynamic may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyDynamic( - this TSender? sender, - Expression? property1, - Expression? property2, - Expression? property3, - Expression? property4, - Expression? property5, - Expression? property6, - Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector, - bool isDistinct) - { - return Observable.CombineLatest( - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property1, false, false, isDistinct), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property2, false, false, isDistinct), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property3, false, false, isDistinct), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property4, false, false, isDistinct), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property5, false, false, isDistinct), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property6, false, false, isDistinct), - selector - ); - } + /// + /// WhenAny allows you to observe whenever one or more properties on an + /// object have changed, providing an initial value when the Observable + /// is set up, unlike ObservableForProperty(). Use this method in + /// constructors to set up bindings between properties that also need an + /// initial setup. + /// + /// The object where the property chain starts. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The 3 property chain to reference. This will be a expression pointing to a end property or field. + /// The 4 property chain to reference. This will be a expression pointing to a end property or field. + /// The 5 property chain to reference. This will be a expression pointing to a end property or field. + /// The 6 property chain to reference. This will be a expression pointing to a end property or field. + /// The selector which will determine the final value from the properties. + /// if set to true [is distinct]. + public static IObservable WhenAnyDynamic( + this TSender? sender, + Expression? property1, + Expression? property2, + Expression? property3, + Expression? property4, + Expression? property5, + Expression? property6, + Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector, + bool isDistinct) + { + return Observable.CombineLatest( + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property1, false, false, isDistinct), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property2, false, false, isDistinct), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property3, false, false, isDistinct), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property4, false, false, isDistinct), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property5, false, false, isDistinct), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property6, false, false, isDistinct), + selector + ); + } - /// - /// WhenAnyValue allows you to observe whenever the value of one or more - /// properties on an object have changed, providing an initial value when - /// the Observable is set up, unlike ObservableForProperty(). Use this - /// method in constructors to set up bindings between properties that also - /// need an initial setup. - /// - /// The object where the property chain starts. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The 3 property chain to reference. This will be a expression pointing to a end property or field. - /// The 4 property chain to reference. This will be a expression pointing to a end property or field. - /// The 5 property chain to reference. This will be a expression pointing to a end property or field. - /// The 6 property chain to reference. This will be a expression pointing to a end property or field. - /// The 7 property chain to reference. This will be a expression pointing to a end property or field. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable<(T1,T2,T3,T4,T5,T6,T7)> WhenAnyValue( - this TSender? sender, - Expression> property1, - Expression> property2, - Expression> property3, - Expression> property4, - Expression> property5, - Expression> property6, - Expression> property7 - ) - { - return sender!.WhenAny(property1, property2, property3, property4, property5, property6, property7, - (c1, c2, c3, c4, c5, c6, c7) => - (c1.Value, c2.Value, c3.Value, c4.Value, c5.Value, c6.Value, c7.Value)); - } + /// + /// WhenAnyValue allows you to observe whenever the value of one or more + /// properties on an object have changed, providing an initial value when + /// the Observable is set up, unlike ObservableForProperty(). Use this + /// method in constructors to set up bindings between properties that also + /// need an initial setup. + /// + /// The object where the property chain starts. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The 3 property chain to reference. This will be a expression pointing to a end property or field. + /// The 4 property chain to reference. This will be a expression pointing to a end property or field. + /// The 5 property chain to reference. This will be a expression pointing to a end property or field. + /// The 6 property chain to reference. This will be a expression pointing to a end property or field. + /// The 7 property chain to reference. This will be a expression pointing to a end property or field. + public static IObservable<(T1,T2,T3,T4,T5,T6,T7)> WhenAnyValue( + this TSender? sender, + Expression> property1, + Expression> property2, + Expression> property3, + Expression> property4, + Expression> property5, + Expression> property6, + Expression> property7 + ) + { + return sender!.WhenAny(property1, property2, property3, property4, property5, property6, property7, + (c1, c2, c3, c4, c5, c6, c7) => + (c1.Value, c2.Value, c3.Value, c4.Value, c5.Value, c6.Value, c7.Value)); + } - /// - /// AOT-friendly tuple overloads using property names instead of expressions. - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable<(T1,T2,T3,T4,T5,T6,T7)> WhenAnyValue( - this TSender? sender, - string property1Name, string property2Name, string property3Name, string property4Name, string property5Name, string property6Name, string property7Name ) - { - var o1 = sender!.ObservableForProperty(property1Name, beforeChange: false, skipInitial: false, isDistinct: true); - var o2 = sender!.ObservableForProperty(property2Name, beforeChange: false, skipInitial: false, isDistinct: true); - var o3 = sender!.ObservableForProperty(property3Name, beforeChange: false, skipInitial: false, isDistinct: true); - var o4 = sender!.ObservableForProperty(property4Name, beforeChange: false, skipInitial: false, isDistinct: true); - var o5 = sender!.ObservableForProperty(property5Name, beforeChange: false, skipInitial: false, isDistinct: true); - var o6 = sender!.ObservableForProperty(property6Name, beforeChange: false, skipInitial: false, isDistinct: true); - var o7 = sender!.ObservableForProperty(property7Name, beforeChange: false, skipInitial: false, isDistinct: true); - return Observable.CombineLatest( - o1.Select(x => x.Value), - o2.Select(x => x.Value), - o3.Select(x => x.Value), - o4.Select(x => x.Value), - o5.Select(x => x.Value), - o6.Select(x => x.Value), - o7.Select(x => x.Value) - , (v1,v2,v3,v4,v5,v6,v7) => - (v1,v2,v3,v4,v5,v6,v7) - ); - } + /// + /// AOT-friendly tuple overloads using property names instead of expressions. + /// + public static IObservable<(T1,T2,T3,T4,T5,T6,T7)> WhenAnyValue( + this TSender? sender, + string property1Name, string property2Name, string property3Name, string property4Name, string property5Name, string property6Name, string property7Name ) + { + var o1 = sender!.ObservableForProperty(property1Name, beforeChange: false, skipInitial: false, isDistinct: true); + var o2 = sender!.ObservableForProperty(property2Name, beforeChange: false, skipInitial: false, isDistinct: true); + var o3 = sender!.ObservableForProperty(property3Name, beforeChange: false, skipInitial: false, isDistinct: true); + var o4 = sender!.ObservableForProperty(property4Name, beforeChange: false, skipInitial: false, isDistinct: true); + var o5 = sender!.ObservableForProperty(property5Name, beforeChange: false, skipInitial: false, isDistinct: true); + var o6 = sender!.ObservableForProperty(property6Name, beforeChange: false, skipInitial: false, isDistinct: true); + var o7 = sender!.ObservableForProperty(property7Name, beforeChange: false, skipInitial: false, isDistinct: true); + return Observable.CombineLatest( + o1.Select(x => x.Value), + o2.Select(x => x.Value), + o3.Select(x => x.Value), + o4.Select(x => x.Value), + o5.Select(x => x.Value), + o6.Select(x => x.Value), + o7.Select(x => x.Value) + , (v1,v2,v3,v4,v5,v6,v7) => + (v1,v2,v3,v4,v5,v6,v7) + ); + } - /// - /// WhenAnyValue allows you to observe whenever the value of one or more - /// properties on an object have changed, providing an initial value when - /// the Observable is set up, unlike ObservableForProperty(). Use this - /// method in constructors to set up bindings between properties that also - /// need an initial setup. - /// - /// The object where the property chain starts. - /// if set to true [is distinct]. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The 3 property chain to reference. This will be a expression pointing to a end property or field. - /// The 4 property chain to reference. This will be a expression pointing to a end property or field. - /// The 5 property chain to reference. This will be a expression pointing to a end property or field. - /// The 6 property chain to reference. This will be a expression pointing to a end property or field. - /// The 7 property chain to reference. This will be a expression pointing to a end property or field. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable<(T1,T2,T3,T4,T5,T6,T7)> WhenAnyValue( - this TSender? sender, - Expression> property1, - Expression> property2, - Expression> property3, - Expression> property4, - Expression> property5, - Expression> property6, - Expression> property7 - , - bool isDistinct) - { - return sender!.WhenAny(property1, property2, property3, property4, property5, property6, property7, - (c1, c2, c3, c4, c5, c6, c7) => - (c1.Value, c2.Value, c3.Value, c4.Value, c5.Value, c6.Value, c7.Value), - isDistinct); - } + /// + /// WhenAnyValue allows you to observe whenever the value of one or more + /// properties on an object have changed, providing an initial value when + /// the Observable is set up, unlike ObservableForProperty(). Use this + /// method in constructors to set up bindings between properties that also + /// need an initial setup. + /// + /// The object where the property chain starts. + /// if set to true [is distinct]. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The 3 property chain to reference. This will be a expression pointing to a end property or field. + /// The 4 property chain to reference. This will be a expression pointing to a end property or field. + /// The 5 property chain to reference. This will be a expression pointing to a end property or field. + /// The 6 property chain to reference. This will be a expression pointing to a end property or field. + /// The 7 property chain to reference. This will be a expression pointing to a end property or field. + public static IObservable<(T1,T2,T3,T4,T5,T6,T7)> WhenAnyValue( + this TSender? sender, + Expression> property1, + Expression> property2, + Expression> property3, + Expression> property4, + Expression> property5, + Expression> property6, + Expression> property7 + , + bool isDistinct) + { + return sender!.WhenAny(property1, property2, property3, property4, property5, property6, property7, + (c1, c2, c3, c4, c5, c6, c7) => + (c1.Value, c2.Value, c3.Value, c4.Value, c5.Value, c6.Value, c7.Value), + isDistinct); + } - /// - /// AOT-friendly tuple overloads using property names with distinct option. - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable<(T1,T2,T3,T4,T5,T6,T7)> WhenAnyValue( - this TSender? sender, - string property1Name, string property2Name, string property3Name, string property4Name, string property5Name, string property6Name, string property7Name , - bool isDistinct) - { - var o1 = sender!.ObservableForProperty(property1Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct); - var o2 = sender!.ObservableForProperty(property2Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct); - var o3 = sender!.ObservableForProperty(property3Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct); - var o4 = sender!.ObservableForProperty(property4Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct); - var o5 = sender!.ObservableForProperty(property5Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct); - var o6 = sender!.ObservableForProperty(property6Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct); - var o7 = sender!.ObservableForProperty(property7Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct); - return Observable.CombineLatest( - o1.Select(x => x.Value), - o2.Select(x => x.Value), - o3.Select(x => x.Value), - o4.Select(x => x.Value), - o5.Select(x => x.Value), - o6.Select(x => x.Value), - o7.Select(x => x.Value) - , (v1,v2,v3,v4,v5,v6,v7) => - (v1,v2,v3,v4,v5,v6,v7) - ); - } + /// + /// AOT-friendly tuple overloads using property names with distinct option. + /// + public static IObservable<(T1,T2,T3,T4,T5,T6,T7)> WhenAnyValue( + this TSender? sender, + string property1Name, string property2Name, string property3Name, string property4Name, string property5Name, string property6Name, string property7Name , + bool isDistinct) + { + var o1 = sender!.ObservableForProperty(property1Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct); + var o2 = sender!.ObservableForProperty(property2Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct); + var o3 = sender!.ObservableForProperty(property3Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct); + var o4 = sender!.ObservableForProperty(property4Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct); + var o5 = sender!.ObservableForProperty(property5Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct); + var o6 = sender!.ObservableForProperty(property6Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct); + var o7 = sender!.ObservableForProperty(property7Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct); + return Observable.CombineLatest( + o1.Select(x => x.Value), + o2.Select(x => x.Value), + o3.Select(x => x.Value), + o4.Select(x => x.Value), + o5.Select(x => x.Value), + o6.Select(x => x.Value), + o7.Select(x => x.Value) + , (v1,v2,v3,v4,v5,v6,v7) => + (v1,v2,v3,v4,v5,v6,v7) + ); + } - /// - /// WhenAnyValue allows you to observe whenever the value of one or more - /// properties on an object have changed, providing an initial value when - /// the Observable is set up, unlike ObservableForProperty(). Use this - /// method in constructors to set up bindings between properties that also - /// need an initial setup. - /// - /// The object where the property chain starts. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The 3 property chain to reference. This will be a expression pointing to a end property or field. - /// The 4 property chain to reference. This will be a expression pointing to a end property or field. - /// The 5 property chain to reference. This will be a expression pointing to a end property or field. - /// The 6 property chain to reference. This will be a expression pointing to a end property or field. - /// The 7 property chain to reference. This will be a expression pointing to a end property or field. - /// The selector which will determine the final value from the properties. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyValue( - this TSender? sender, - Expression> property1, - Expression> property2, - Expression> property3, - Expression> property4, - Expression> property5, - Expression> property6, - Expression> property7, - Func selector) - { - return sender!.WhenAny(property1, property2, property3, property4, property5, property6, property7, - (c1, c2, c3, c4, c5, c6, c7) => - selector(c1.Value, c2.Value, c3.Value, c4.Value, c5.Value, c6.Value, c7.Value)); - } + /// + /// WhenAnyValue allows you to observe whenever the value of one or more + /// properties on an object have changed, providing an initial value when + /// the Observable is set up, unlike ObservableForProperty(). Use this + /// method in constructors to set up bindings between properties that also + /// need an initial setup. + /// + /// The object where the property chain starts. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The 3 property chain to reference. This will be a expression pointing to a end property or field. + /// The 4 property chain to reference. This will be a expression pointing to a end property or field. + /// The 5 property chain to reference. This will be a expression pointing to a end property or field. + /// The 6 property chain to reference. This will be a expression pointing to a end property or field. + /// The 7 property chain to reference. This will be a expression pointing to a end property or field. + /// The selector which will determine the final value from the properties. + public static IObservable WhenAnyValue( + this TSender? sender, + Expression> property1, + Expression> property2, + Expression> property3, + Expression> property4, + Expression> property5, + Expression> property6, + Expression> property7, + Func selector) + { + return sender!.WhenAny(property1, property2, property3, property4, property5, property6, property7, + (c1, c2, c3, c4, c5, c6, c7) => + selector(c1.Value, c2.Value, c3.Value, c4.Value, c5.Value, c6.Value, c7.Value)); + } - /// - /// AOT-friendly selector overload using property names. - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyValue( - this TSender? sender, - string property1Name, - string property2Name, - string property3Name, - string property4Name, - string property5Name, - string property6Name, - string property7Name, - Func selector) - { - var o1 = sender!.ObservableForProperty(property1Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - var o2 = sender!.ObservableForProperty(property2Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - var o3 = sender!.ObservableForProperty(property3Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - var o4 = sender!.ObservableForProperty(property4Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - var o5 = sender!.ObservableForProperty(property5Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - var o6 = sender!.ObservableForProperty(property6Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - var o7 = sender!.ObservableForProperty(property7Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - return Observable.CombineLatest( - o1, - o2, - o3, - o4, - o5, - o6, - o7 - , selector); - } + /// + /// AOT-friendly selector overload using property names. + /// + public static IObservable WhenAnyValue( + this TSender? sender, + string property1Name, + string property2Name, + string property3Name, + string property4Name, + string property5Name, + string property6Name, + string property7Name, + Func selector) + { + var o1 = sender!.ObservableForProperty(property1Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + var o2 = sender!.ObservableForProperty(property2Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + var o3 = sender!.ObservableForProperty(property3Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + var o4 = sender!.ObservableForProperty(property4Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + var o5 = sender!.ObservableForProperty(property5Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + var o6 = sender!.ObservableForProperty(property6Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + var o7 = sender!.ObservableForProperty(property7Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + return Observable.CombineLatest( + o1, + o2, + o3, + o4, + o5, + o6, + o7 + , selector); + } - /// - /// WhenAnyValue allows you to observe whenever the value of one or more - /// properties on an object have changed, providing an initial value when - /// the Observable is set up, unlike ObservableForProperty(). Use this - /// method in constructors to set up bindings between properties that also - /// need an initial setup. - /// - /// The object where the property chain starts. - /// if set to true [is distinct]. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The 3 property chain to reference. This will be a expression pointing to a end property or field. - /// The 4 property chain to reference. This will be a expression pointing to a end property or field. - /// The 5 property chain to reference. This will be a expression pointing to a end property or field. - /// The 6 property chain to reference. This will be a expression pointing to a end property or field. - /// The 7 property chain to reference. This will be a expression pointing to a end property or field. - /// The selector which will determine the final value from the properties. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyValue( - this TSender? sender, - Expression> property1, - Expression> property2, - Expression> property3, - Expression> property4, - Expression> property5, - Expression> property6, - Expression> property7, - Func selector, - bool isDistinct) - { - return sender!.WhenAny(property1, property2, property3, property4, property5, property6, property7, - (c1, c2, c3, c4, c5, c6, c7) => - selector(c1.Value, c2.Value, c3.Value, c4.Value, c5.Value, c6.Value, c7.Value), - isDistinct); - } + /// + /// WhenAnyValue allows you to observe whenever the value of one or more + /// properties on an object have changed, providing an initial value when + /// the Observable is set up, unlike ObservableForProperty(). Use this + /// method in constructors to set up bindings between properties that also + /// need an initial setup. + /// + /// The object where the property chain starts. + /// if set to true [is distinct]. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The 3 property chain to reference. This will be a expression pointing to a end property or field. + /// The 4 property chain to reference. This will be a expression pointing to a end property or field. + /// The 5 property chain to reference. This will be a expression pointing to a end property or field. + /// The 6 property chain to reference. This will be a expression pointing to a end property or field. + /// The 7 property chain to reference. This will be a expression pointing to a end property or field. + /// The selector which will determine the final value from the properties. + public static IObservable WhenAnyValue( + this TSender? sender, + Expression> property1, + Expression> property2, + Expression> property3, + Expression> property4, + Expression> property5, + Expression> property6, + Expression> property7, + Func selector, + bool isDistinct) + { + return sender!.WhenAny(property1, property2, property3, property4, property5, property6, property7, + (c1, c2, c3, c4, c5, c6, c7) => + selector(c1.Value, c2.Value, c3.Value, c4.Value, c5.Value, c6.Value, c7.Value), + isDistinct); + } - /// - /// AOT-friendly selector overload using property names and distinct option. - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyValue( - this TSender? sender, - string property1Name, - string property2Name, - string property3Name, - string property4Name, - string property5Name, - string property6Name, - string property7Name, - Func selector, - bool isDistinct) - { - var o1 = sender!.ObservableForProperty(property1Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - var o2 = sender!.ObservableForProperty(property2Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - var o3 = sender!.ObservableForProperty(property3Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - var o4 = sender!.ObservableForProperty(property4Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - var o5 = sender!.ObservableForProperty(property5Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - var o6 = sender!.ObservableForProperty(property6Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - var o7 = sender!.ObservableForProperty(property7Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - return Observable.CombineLatest( - o1, - o2, - o3, - o4, - o5, - o6, - o7 - , selector); - } + /// + /// AOT-friendly selector overload using property names and distinct option. + /// + public static IObservable WhenAnyValue( + this TSender? sender, + string property1Name, + string property2Name, + string property3Name, + string property4Name, + string property5Name, + string property6Name, + string property7Name, + Func selector, + bool isDistinct) + { + var o1 = sender!.ObservableForProperty(property1Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + var o2 = sender!.ObservableForProperty(property2Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + var o3 = sender!.ObservableForProperty(property3Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + var o4 = sender!.ObservableForProperty(property4Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + var o5 = sender!.ObservableForProperty(property5Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + var o6 = sender!.ObservableForProperty(property6Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + var o7 = sender!.ObservableForProperty(property7Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + return Observable.CombineLatest( + o1, + o2, + o3, + o4, + o5, + o6, + o7 + , selector); + } - /// - /// WhenAny allows you to observe whenever one or more properties on an - /// object have changed, providing an initial value when the Observable - /// is set up, unlike ObservableForProperty(). Use this method in - /// constructors to set up bindings between properties that also need an - /// initial setup. - /// - /// The object where the property chain starts. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The 3 property chain to reference. This will be a expression pointing to a end property or field. - /// The 4 property chain to reference. This will be a expression pointing to a end property or field. - /// The 5 property chain to reference. This will be a expression pointing to a end property or field. - /// The 6 property chain to reference. This will be a expression pointing to a end property or field. - /// The 7 property chain to reference. This will be a expression pointing to a end property or field. - /// The selector which will determine the final value from the properties. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAny uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAny may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAny( - this TSender? sender, - Expression> property1, - Expression> property2, - Expression> property3, - Expression> property4, - Expression> property5, - Expression> property6, - Expression> property7, - Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector) - { - return Observable.CombineLatest( - sender!.ObservableForProperty(property1, false, false), - sender!.ObservableForProperty(property2, false, false), - sender!.ObservableForProperty(property3, false, false), - sender!.ObservableForProperty(property4, false, false), - sender!.ObservableForProperty(property5, false, false), - sender!.ObservableForProperty(property6, false, false), - sender!.ObservableForProperty(property7, false, false), - selector - ); - } + /// + /// WhenAny allows you to observe whenever one or more properties on an + /// object have changed, providing an initial value when the Observable + /// is set up, unlike ObservableForProperty(). Use this method in + /// constructors to set up bindings between properties that also need an + /// initial setup. + /// + /// The object where the property chain starts. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The 3 property chain to reference. This will be a expression pointing to a end property or field. + /// The 4 property chain to reference. This will be a expression pointing to a end property or field. + /// The 5 property chain to reference. This will be a expression pointing to a end property or field. + /// The 6 property chain to reference. This will be a expression pointing to a end property or field. + /// The 7 property chain to reference. This will be a expression pointing to a end property or field. + /// The selector which will determine the final value from the properties. + public static IObservable WhenAny( + this TSender? sender, + Expression> property1, + Expression> property2, + Expression> property3, + Expression> property4, + Expression> property5, + Expression> property6, + Expression> property7, + Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector) + { + return Observable.CombineLatest( + sender!.ObservableForProperty(property1, false, false), + sender!.ObservableForProperty(property2, false, false), + sender!.ObservableForProperty(property3, false, false), + sender!.ObservableForProperty(property4, false, false), + sender!.ObservableForProperty(property5, false, false), + sender!.ObservableForProperty(property6, false, false), + sender!.ObservableForProperty(property7, false, false), + selector + ); + } - /// - /// AOT-friendly WhenAny overload using property names. - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAny uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAny may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAny( - this TSender? sender, - string property1Name, - string property2Name, - string property3Name, - string property4Name, - string property5Name, - string property6Name, - string property7Name, - Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector) - { - return Observable.CombineLatest( - sender!.ObservableForProperty(property1Name, false, false), - sender!.ObservableForProperty(property2Name, false, false), - sender!.ObservableForProperty(property3Name, false, false), - sender!.ObservableForProperty(property4Name, false, false), - sender!.ObservableForProperty(property5Name, false, false), - sender!.ObservableForProperty(property6Name, false, false), - sender!.ObservableForProperty(property7Name, false, false), - selector - ); - } + /// + /// AOT-friendly WhenAny overload using property names. + /// + public static IObservable WhenAny( + this TSender? sender, + string property1Name, + string property2Name, + string property3Name, + string property4Name, + string property5Name, + string property6Name, + string property7Name, + Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector) + { + return Observable.CombineLatest( + sender!.ObservableForProperty(property1Name, false, false), + sender!.ObservableForProperty(property2Name, false, false), + sender!.ObservableForProperty(property3Name, false, false), + sender!.ObservableForProperty(property4Name, false, false), + sender!.ObservableForProperty(property5Name, false, false), + sender!.ObservableForProperty(property6Name, false, false), + sender!.ObservableForProperty(property7Name, false, false), + selector + ); + } - /// - /// WhenAny allows you to observe whenever one or more properties on an - /// object have changed, providing an initial value when the Observable - /// is set up, unlike ObservableForProperty(). Use this method in - /// constructors to set up bindings between properties that also need an - /// initial setup. - /// - /// The object where the property chain starts. - /// if set to true [is distinct]. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The 3 property chain to reference. This will be a expression pointing to a end property or field. - /// The 4 property chain to reference. This will be a expression pointing to a end property or field. - /// The 5 property chain to reference. This will be a expression pointing to a end property or field. - /// The 6 property chain to reference. This will be a expression pointing to a end property or field. - /// The 7 property chain to reference. This will be a expression pointing to a end property or field. - /// The selector which will determine the final value from the properties. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAny uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAny may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAny( - this TSender? sender, - Expression> property1, - Expression> property2, - Expression> property3, - Expression> property4, - Expression> property5, - Expression> property6, - Expression> property7, - Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector, - bool isDistinct) - { - return Observable.CombineLatest( - sender!.ObservableForProperty(property1, false, false, isDistinct), - sender!.ObservableForProperty(property2, false, false, isDistinct), - sender!.ObservableForProperty(property3, false, false, isDistinct), - sender!.ObservableForProperty(property4, false, false, isDistinct), - sender!.ObservableForProperty(property5, false, false, isDistinct), - sender!.ObservableForProperty(property6, false, false, isDistinct), - sender!.ObservableForProperty(property7, false, false, isDistinct), - selector - ); - } + /// + /// WhenAny allows you to observe whenever one or more properties on an + /// object have changed, providing an initial value when the Observable + /// is set up, unlike ObservableForProperty(). Use this method in + /// constructors to set up bindings between properties that also need an + /// initial setup. + /// + /// The object where the property chain starts. + /// if set to true [is distinct]. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The 3 property chain to reference. This will be a expression pointing to a end property or field. + /// The 4 property chain to reference. This will be a expression pointing to a end property or field. + /// The 5 property chain to reference. This will be a expression pointing to a end property or field. + /// The 6 property chain to reference. This will be a expression pointing to a end property or field. + /// The 7 property chain to reference. This will be a expression pointing to a end property or field. + /// The selector which will determine the final value from the properties. + public static IObservable WhenAny( + this TSender? sender, + Expression> property1, + Expression> property2, + Expression> property3, + Expression> property4, + Expression> property5, + Expression> property6, + Expression> property7, + Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector, + bool isDistinct) + { + return Observable.CombineLatest( + sender!.ObservableForProperty(property1, false, false, isDistinct), + sender!.ObservableForProperty(property2, false, false, isDistinct), + sender!.ObservableForProperty(property3, false, false, isDistinct), + sender!.ObservableForProperty(property4, false, false, isDistinct), + sender!.ObservableForProperty(property5, false, false, isDistinct), + sender!.ObservableForProperty(property6, false, false, isDistinct), + sender!.ObservableForProperty(property7, false, false, isDistinct), + selector + ); + } - /// - /// AOT-friendly WhenAny overload using property names and distinct option. - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAny uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAny may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAny( - this TSender? sender, - string property1Name, - string property2Name, - string property3Name, - string property4Name, - string property5Name, - string property6Name, - string property7Name, - Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector, - bool isDistinct) - { - return Observable.CombineLatest( - sender!.ObservableForProperty(property1Name, false, false, isDistinct), - sender!.ObservableForProperty(property2Name, false, false, isDistinct), - sender!.ObservableForProperty(property3Name, false, false, isDistinct), - sender!.ObservableForProperty(property4Name, false, false, isDistinct), - sender!.ObservableForProperty(property5Name, false, false, isDistinct), - sender!.ObservableForProperty(property6Name, false, false, isDistinct), - sender!.ObservableForProperty(property7Name, false, false, isDistinct), - selector - ); - } + /// + /// AOT-friendly WhenAny overload using property names and distinct option. + /// + public static IObservable WhenAny( + this TSender? sender, + string property1Name, + string property2Name, + string property3Name, + string property4Name, + string property5Name, + string property6Name, + string property7Name, + Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector, + bool isDistinct) + { + return Observable.CombineLatest( + sender!.ObservableForProperty(property1Name, false, false, isDistinct), + sender!.ObservableForProperty(property2Name, false, false, isDistinct), + sender!.ObservableForProperty(property3Name, false, false, isDistinct), + sender!.ObservableForProperty(property4Name, false, false, isDistinct), + sender!.ObservableForProperty(property5Name, false, false, isDistinct), + sender!.ObservableForProperty(property6Name, false, false, isDistinct), + sender!.ObservableForProperty(property7Name, false, false, isDistinct), + selector + ); + } - /// - /// WhenAny allows you to observe whenever one or more properties on an - /// object have changed, providing an initial value when the Observable - /// is set up, unlike ObservableForProperty(). Use this method in - /// constructors to set up bindings between properties that also need an - /// initial setup. - /// - /// The object where the property chain starts. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The 3 property chain to reference. This will be a expression pointing to a end property or field. - /// The 4 property chain to reference. This will be a expression pointing to a end property or field. - /// The 5 property chain to reference. This will be a expression pointing to a end property or field. - /// The 6 property chain to reference. This will be a expression pointing to a end property or field. - /// The 7 property chain to reference. This will be a expression pointing to a end property or field. - /// The selector which will determine the final value from the properties. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyDynamic uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyDynamic may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyDynamic( - this TSender? sender, - Expression? property1, - Expression? property2, - Expression? property3, - Expression? property4, - Expression? property5, - Expression? property6, - Expression? property7, - Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector) - { - return Observable.CombineLatest( - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property1, false, false), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property2, false, false), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property3, false, false), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property4, false, false), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property5, false, false), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property6, false, false), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property7, false, false), - selector - ); - } + /// + /// WhenAny allows you to observe whenever one or more properties on an + /// object have changed, providing an initial value when the Observable + /// is set up, unlike ObservableForProperty(). Use this method in + /// constructors to set up bindings between properties that also need an + /// initial setup. + /// + /// The object where the property chain starts. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The 3 property chain to reference. This will be a expression pointing to a end property or field. + /// The 4 property chain to reference. This will be a expression pointing to a end property or field. + /// The 5 property chain to reference. This will be a expression pointing to a end property or field. + /// The 6 property chain to reference. This will be a expression pointing to a end property or field. + /// The 7 property chain to reference. This will be a expression pointing to a end property or field. + /// The selector which will determine the final value from the properties. + public static IObservable WhenAnyDynamic( + this TSender? sender, + Expression? property1, + Expression? property2, + Expression? property3, + Expression? property4, + Expression? property5, + Expression? property6, + Expression? property7, + Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector) + { + return Observable.CombineLatest( + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property1, false, false), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property2, false, false), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property3, false, false), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property4, false, false), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property5, false, false), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property6, false, false), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property7, false, false), + selector + ); + } - /// - /// WhenAny allows you to observe whenever one or more properties on an - /// object have changed, providing an initial value when the Observable - /// is set up, unlike ObservableForProperty(). Use this method in - /// constructors to set up bindings between properties that also need an - /// initial setup. - /// - /// The object where the property chain starts. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The 3 property chain to reference. This will be a expression pointing to a end property or field. - /// The 4 property chain to reference. This will be a expression pointing to a end property or field. - /// The 5 property chain to reference. This will be a expression pointing to a end property or field. - /// The 6 property chain to reference. This will be a expression pointing to a end property or field. - /// The 7 property chain to reference. This will be a expression pointing to a end property or field. - /// The selector which will determine the final value from the properties. - /// if set to true [is distinct]. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyDynamic uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyDynamic may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyDynamic( - this TSender? sender, - Expression? property1, - Expression? property2, - Expression? property3, - Expression? property4, - Expression? property5, - Expression? property6, - Expression? property7, - Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector, - bool isDistinct) - { - return Observable.CombineLatest( - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property1, false, false, isDistinct), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property2, false, false, isDistinct), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property3, false, false, isDistinct), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property4, false, false, isDistinct), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property5, false, false, isDistinct), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property6, false, false, isDistinct), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property7, false, false, isDistinct), - selector - ); - } + /// + /// WhenAny allows you to observe whenever one or more properties on an + /// object have changed, providing an initial value when the Observable + /// is set up, unlike ObservableForProperty(). Use this method in + /// constructors to set up bindings between properties that also need an + /// initial setup. + /// + /// The object where the property chain starts. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The 3 property chain to reference. This will be a expression pointing to a end property or field. + /// The 4 property chain to reference. This will be a expression pointing to a end property or field. + /// The 5 property chain to reference. This will be a expression pointing to a end property or field. + /// The 6 property chain to reference. This will be a expression pointing to a end property or field. + /// The 7 property chain to reference. This will be a expression pointing to a end property or field. + /// The selector which will determine the final value from the properties. + /// if set to true [is distinct]. + public static IObservable WhenAnyDynamic( + this TSender? sender, + Expression? property1, + Expression? property2, + Expression? property3, + Expression? property4, + Expression? property5, + Expression? property6, + Expression? property7, + Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector, + bool isDistinct) + { + return Observable.CombineLatest( + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property1, false, false, isDistinct), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property2, false, false, isDistinct), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property3, false, false, isDistinct), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property4, false, false, isDistinct), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property5, false, false, isDistinct), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property6, false, false, isDistinct), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property7, false, false, isDistinct), + selector + ); + } - /// - /// WhenAnyValue allows you to observe whenever the value of one or more - /// properties on an object have changed, providing an initial value when - /// the Observable is set up, unlike ObservableForProperty(). Use this - /// method in constructors to set up bindings between properties that also - /// need an initial setup. - /// - /// The object where the property chain starts. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The 3 property chain to reference. This will be a expression pointing to a end property or field. - /// The 4 property chain to reference. This will be a expression pointing to a end property or field. - /// The 5 property chain to reference. This will be a expression pointing to a end property or field. - /// The 6 property chain to reference. This will be a expression pointing to a end property or field. - /// The 7 property chain to reference. This will be a expression pointing to a end property or field. - /// The 8 property chain to reference. This will be a expression pointing to a end property or field. - /// The selector which will determine the final value from the properties. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyValue( - this TSender? sender, - Expression> property1, - Expression> property2, - Expression> property3, - Expression> property4, - Expression> property5, - Expression> property6, - Expression> property7, - Expression> property8, - Func selector) - { - return sender!.WhenAny(property1, property2, property3, property4, property5, property6, property7, property8, - (c1, c2, c3, c4, c5, c6, c7, c8) => - selector(c1.Value, c2.Value, c3.Value, c4.Value, c5.Value, c6.Value, c7.Value, c8.Value)); - } + /// + /// WhenAnyValue allows you to observe whenever the value of one or more + /// properties on an object have changed, providing an initial value when + /// the Observable is set up, unlike ObservableForProperty(). Use this + /// method in constructors to set up bindings between properties that also + /// need an initial setup. + /// + /// The object where the property chain starts. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The 3 property chain to reference. This will be a expression pointing to a end property or field. + /// The 4 property chain to reference. This will be a expression pointing to a end property or field. + /// The 5 property chain to reference. This will be a expression pointing to a end property or field. + /// The 6 property chain to reference. This will be a expression pointing to a end property or field. + /// The 7 property chain to reference. This will be a expression pointing to a end property or field. + /// The 8 property chain to reference. This will be a expression pointing to a end property or field. + /// The selector which will determine the final value from the properties. + public static IObservable WhenAnyValue( + this TSender? sender, + Expression> property1, + Expression> property2, + Expression> property3, + Expression> property4, + Expression> property5, + Expression> property6, + Expression> property7, + Expression> property8, + Func selector) + { + return sender!.WhenAny(property1, property2, property3, property4, property5, property6, property7, property8, + (c1, c2, c3, c4, c5, c6, c7, c8) => + selector(c1.Value, c2.Value, c3.Value, c4.Value, c5.Value, c6.Value, c7.Value, c8.Value)); + } - /// - /// AOT-friendly selector overload using property names. - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyValue( - this TSender? sender, - string property1Name, - string property2Name, - string property3Name, - string property4Name, - string property5Name, - string property6Name, - string property7Name, - string property8Name, - Func selector) - { - var o1 = sender!.ObservableForProperty(property1Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - var o2 = sender!.ObservableForProperty(property2Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - var o3 = sender!.ObservableForProperty(property3Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - var o4 = sender!.ObservableForProperty(property4Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - var o5 = sender!.ObservableForProperty(property5Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - var o6 = sender!.ObservableForProperty(property6Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - var o7 = sender!.ObservableForProperty(property7Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - var o8 = sender!.ObservableForProperty(property8Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - return Observable.CombineLatest( - o1, - o2, - o3, - o4, - o5, - o6, - o7, - o8 - , selector); - } + /// + /// AOT-friendly selector overload using property names. + /// + public static IObservable WhenAnyValue( + this TSender? sender, + string property1Name, + string property2Name, + string property3Name, + string property4Name, + string property5Name, + string property6Name, + string property7Name, + string property8Name, + Func selector) + { + var o1 = sender!.ObservableForProperty(property1Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + var o2 = sender!.ObservableForProperty(property2Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + var o3 = sender!.ObservableForProperty(property3Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + var o4 = sender!.ObservableForProperty(property4Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + var o5 = sender!.ObservableForProperty(property5Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + var o6 = sender!.ObservableForProperty(property6Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + var o7 = sender!.ObservableForProperty(property7Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + var o8 = sender!.ObservableForProperty(property8Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + return Observable.CombineLatest( + o1, + o2, + o3, + o4, + o5, + o6, + o7, + o8 + , selector); + } - /// - /// WhenAnyValue allows you to observe whenever the value of one or more - /// properties on an object have changed, providing an initial value when - /// the Observable is set up, unlike ObservableForProperty(). Use this - /// method in constructors to set up bindings between properties that also - /// need an initial setup. - /// - /// The object where the property chain starts. - /// if set to true [is distinct]. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The 3 property chain to reference. This will be a expression pointing to a end property or field. - /// The 4 property chain to reference. This will be a expression pointing to a end property or field. - /// The 5 property chain to reference. This will be a expression pointing to a end property or field. - /// The 6 property chain to reference. This will be a expression pointing to a end property or field. - /// The 7 property chain to reference. This will be a expression pointing to a end property or field. - /// The 8 property chain to reference. This will be a expression pointing to a end property or field. - /// The selector which will determine the final value from the properties. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyValue( - this TSender? sender, - Expression> property1, - Expression> property2, - Expression> property3, - Expression> property4, - Expression> property5, - Expression> property6, - Expression> property7, - Expression> property8, - Func selector, - bool isDistinct) - { - return sender!.WhenAny(property1, property2, property3, property4, property5, property6, property7, property8, - (c1, c2, c3, c4, c5, c6, c7, c8) => - selector(c1.Value, c2.Value, c3.Value, c4.Value, c5.Value, c6.Value, c7.Value, c8.Value), - isDistinct); - } + /// + /// WhenAnyValue allows you to observe whenever the value of one or more + /// properties on an object have changed, providing an initial value when + /// the Observable is set up, unlike ObservableForProperty(). Use this + /// method in constructors to set up bindings between properties that also + /// need an initial setup. + /// + /// The object where the property chain starts. + /// if set to true [is distinct]. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The 3 property chain to reference. This will be a expression pointing to a end property or field. + /// The 4 property chain to reference. This will be a expression pointing to a end property or field. + /// The 5 property chain to reference. This will be a expression pointing to a end property or field. + /// The 6 property chain to reference. This will be a expression pointing to a end property or field. + /// The 7 property chain to reference. This will be a expression pointing to a end property or field. + /// The 8 property chain to reference. This will be a expression pointing to a end property or field. + /// The selector which will determine the final value from the properties. + public static IObservable WhenAnyValue( + this TSender? sender, + Expression> property1, + Expression> property2, + Expression> property3, + Expression> property4, + Expression> property5, + Expression> property6, + Expression> property7, + Expression> property8, + Func selector, + bool isDistinct) + { + return sender!.WhenAny(property1, property2, property3, property4, property5, property6, property7, property8, + (c1, c2, c3, c4, c5, c6, c7, c8) => + selector(c1.Value, c2.Value, c3.Value, c4.Value, c5.Value, c6.Value, c7.Value, c8.Value), + isDistinct); + } - /// - /// AOT-friendly selector overload using property names and distinct option. - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyValue( - this TSender? sender, - string property1Name, - string property2Name, - string property3Name, - string property4Name, - string property5Name, - string property6Name, - string property7Name, - string property8Name, - Func selector, - bool isDistinct) - { - var o1 = sender!.ObservableForProperty(property1Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - var o2 = sender!.ObservableForProperty(property2Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - var o3 = sender!.ObservableForProperty(property3Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - var o4 = sender!.ObservableForProperty(property4Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - var o5 = sender!.ObservableForProperty(property5Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - var o6 = sender!.ObservableForProperty(property6Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - var o7 = sender!.ObservableForProperty(property7Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - var o8 = sender!.ObservableForProperty(property8Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - return Observable.CombineLatest( - o1, - o2, - o3, - o4, - o5, - o6, - o7, - o8 - , selector); - } + /// + /// AOT-friendly selector overload using property names and distinct option. + /// + public static IObservable WhenAnyValue( + this TSender? sender, + string property1Name, + string property2Name, + string property3Name, + string property4Name, + string property5Name, + string property6Name, + string property7Name, + string property8Name, + Func selector, + bool isDistinct) + { + var o1 = sender!.ObservableForProperty(property1Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + var o2 = sender!.ObservableForProperty(property2Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + var o3 = sender!.ObservableForProperty(property3Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + var o4 = sender!.ObservableForProperty(property4Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + var o5 = sender!.ObservableForProperty(property5Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + var o6 = sender!.ObservableForProperty(property6Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + var o7 = sender!.ObservableForProperty(property7Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + var o8 = sender!.ObservableForProperty(property8Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + return Observable.CombineLatest( + o1, + o2, + o3, + o4, + o5, + o6, + o7, + o8 + , selector); + } - /// - /// WhenAny allows you to observe whenever one or more properties on an - /// object have changed, providing an initial value when the Observable - /// is set up, unlike ObservableForProperty(). Use this method in - /// constructors to set up bindings between properties that also need an - /// initial setup. - /// - /// The object where the property chain starts. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The 3 property chain to reference. This will be a expression pointing to a end property or field. - /// The 4 property chain to reference. This will be a expression pointing to a end property or field. - /// The 5 property chain to reference. This will be a expression pointing to a end property or field. - /// The 6 property chain to reference. This will be a expression pointing to a end property or field. - /// The 7 property chain to reference. This will be a expression pointing to a end property or field. - /// The 8 property chain to reference. This will be a expression pointing to a end property or field. - /// The selector which will determine the final value from the properties. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAny uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAny may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAny( - this TSender? sender, - Expression> property1, - Expression> property2, - Expression> property3, - Expression> property4, - Expression> property5, - Expression> property6, - Expression> property7, - Expression> property8, - Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector) - { - return Observable.CombineLatest( - sender!.ObservableForProperty(property1, false, false), - sender!.ObservableForProperty(property2, false, false), - sender!.ObservableForProperty(property3, false, false), - sender!.ObservableForProperty(property4, false, false), - sender!.ObservableForProperty(property5, false, false), - sender!.ObservableForProperty(property6, false, false), - sender!.ObservableForProperty(property7, false, false), - sender!.ObservableForProperty(property8, false, false), - selector - ); - } + /// + /// WhenAny allows you to observe whenever one or more properties on an + /// object have changed, providing an initial value when the Observable + /// is set up, unlike ObservableForProperty(). Use this method in + /// constructors to set up bindings between properties that also need an + /// initial setup. + /// + /// The object where the property chain starts. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The 3 property chain to reference. This will be a expression pointing to a end property or field. + /// The 4 property chain to reference. This will be a expression pointing to a end property or field. + /// The 5 property chain to reference. This will be a expression pointing to a end property or field. + /// The 6 property chain to reference. This will be a expression pointing to a end property or field. + /// The 7 property chain to reference. This will be a expression pointing to a end property or field. + /// The 8 property chain to reference. This will be a expression pointing to a end property or field. + /// The selector which will determine the final value from the properties. + public static IObservable WhenAny( + this TSender? sender, + Expression> property1, + Expression> property2, + Expression> property3, + Expression> property4, + Expression> property5, + Expression> property6, + Expression> property7, + Expression> property8, + Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector) + { + return Observable.CombineLatest( + sender!.ObservableForProperty(property1, false, false), + sender!.ObservableForProperty(property2, false, false), + sender!.ObservableForProperty(property3, false, false), + sender!.ObservableForProperty(property4, false, false), + sender!.ObservableForProperty(property5, false, false), + sender!.ObservableForProperty(property6, false, false), + sender!.ObservableForProperty(property7, false, false), + sender!.ObservableForProperty(property8, false, false), + selector + ); + } - /// - /// AOT-friendly WhenAny overload using property names. - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAny uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAny may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAny( - this TSender? sender, - string property1Name, - string property2Name, - string property3Name, - string property4Name, - string property5Name, - string property6Name, - string property7Name, - string property8Name, - Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector) - { - return Observable.CombineLatest( - sender!.ObservableForProperty(property1Name, false, false), - sender!.ObservableForProperty(property2Name, false, false), - sender!.ObservableForProperty(property3Name, false, false), - sender!.ObservableForProperty(property4Name, false, false), - sender!.ObservableForProperty(property5Name, false, false), - sender!.ObservableForProperty(property6Name, false, false), - sender!.ObservableForProperty(property7Name, false, false), - sender!.ObservableForProperty(property8Name, false, false), - selector - ); - } + /// + /// AOT-friendly WhenAny overload using property names. + /// + public static IObservable WhenAny( + this TSender? sender, + string property1Name, + string property2Name, + string property3Name, + string property4Name, + string property5Name, + string property6Name, + string property7Name, + string property8Name, + Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector) + { + return Observable.CombineLatest( + sender!.ObservableForProperty(property1Name, false, false), + sender!.ObservableForProperty(property2Name, false, false), + sender!.ObservableForProperty(property3Name, false, false), + sender!.ObservableForProperty(property4Name, false, false), + sender!.ObservableForProperty(property5Name, false, false), + sender!.ObservableForProperty(property6Name, false, false), + sender!.ObservableForProperty(property7Name, false, false), + sender!.ObservableForProperty(property8Name, false, false), + selector + ); + } - /// - /// WhenAny allows you to observe whenever one or more properties on an - /// object have changed, providing an initial value when the Observable - /// is set up, unlike ObservableForProperty(). Use this method in - /// constructors to set up bindings between properties that also need an - /// initial setup. - /// - /// The object where the property chain starts. - /// if set to true [is distinct]. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The 3 property chain to reference. This will be a expression pointing to a end property or field. - /// The 4 property chain to reference. This will be a expression pointing to a end property or field. - /// The 5 property chain to reference. This will be a expression pointing to a end property or field. - /// The 6 property chain to reference. This will be a expression pointing to a end property or field. - /// The 7 property chain to reference. This will be a expression pointing to a end property or field. - /// The 8 property chain to reference. This will be a expression pointing to a end property or field. - /// The selector which will determine the final value from the properties. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAny uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAny may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAny( - this TSender? sender, - Expression> property1, - Expression> property2, - Expression> property3, - Expression> property4, - Expression> property5, - Expression> property6, - Expression> property7, - Expression> property8, - Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector, - bool isDistinct) - { - return Observable.CombineLatest( - sender!.ObservableForProperty(property1, false, false, isDistinct), - sender!.ObservableForProperty(property2, false, false, isDistinct), - sender!.ObservableForProperty(property3, false, false, isDistinct), - sender!.ObservableForProperty(property4, false, false, isDistinct), - sender!.ObservableForProperty(property5, false, false, isDistinct), - sender!.ObservableForProperty(property6, false, false, isDistinct), - sender!.ObservableForProperty(property7, false, false, isDistinct), - sender!.ObservableForProperty(property8, false, false, isDistinct), - selector - ); - } + /// + /// WhenAny allows you to observe whenever one or more properties on an + /// object have changed, providing an initial value when the Observable + /// is set up, unlike ObservableForProperty(). Use this method in + /// constructors to set up bindings between properties that also need an + /// initial setup. + /// + /// The object where the property chain starts. + /// if set to true [is distinct]. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The 3 property chain to reference. This will be a expression pointing to a end property or field. + /// The 4 property chain to reference. This will be a expression pointing to a end property or field. + /// The 5 property chain to reference. This will be a expression pointing to a end property or field. + /// The 6 property chain to reference. This will be a expression pointing to a end property or field. + /// The 7 property chain to reference. This will be a expression pointing to a end property or field. + /// The 8 property chain to reference. This will be a expression pointing to a end property or field. + /// The selector which will determine the final value from the properties. + public static IObservable WhenAny( + this TSender? sender, + Expression> property1, + Expression> property2, + Expression> property3, + Expression> property4, + Expression> property5, + Expression> property6, + Expression> property7, + Expression> property8, + Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector, + bool isDistinct) + { + return Observable.CombineLatest( + sender!.ObservableForProperty(property1, false, false, isDistinct), + sender!.ObservableForProperty(property2, false, false, isDistinct), + sender!.ObservableForProperty(property3, false, false, isDistinct), + sender!.ObservableForProperty(property4, false, false, isDistinct), + sender!.ObservableForProperty(property5, false, false, isDistinct), + sender!.ObservableForProperty(property6, false, false, isDistinct), + sender!.ObservableForProperty(property7, false, false, isDistinct), + sender!.ObservableForProperty(property8, false, false, isDistinct), + selector + ); + } - /// - /// AOT-friendly WhenAny overload using property names and distinct option. - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAny uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAny may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAny( - this TSender? sender, - string property1Name, - string property2Name, - string property3Name, - string property4Name, - string property5Name, - string property6Name, - string property7Name, - string property8Name, - Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector, - bool isDistinct) - { - return Observable.CombineLatest( - sender!.ObservableForProperty(property1Name, false, false, isDistinct), - sender!.ObservableForProperty(property2Name, false, false, isDistinct), - sender!.ObservableForProperty(property3Name, false, false, isDistinct), - sender!.ObservableForProperty(property4Name, false, false, isDistinct), - sender!.ObservableForProperty(property5Name, false, false, isDistinct), - sender!.ObservableForProperty(property6Name, false, false, isDistinct), - sender!.ObservableForProperty(property7Name, false, false, isDistinct), - sender!.ObservableForProperty(property8Name, false, false, isDistinct), - selector - ); - } + /// + /// AOT-friendly WhenAny overload using property names and distinct option. + /// + public static IObservable WhenAny( + this TSender? sender, + string property1Name, + string property2Name, + string property3Name, + string property4Name, + string property5Name, + string property6Name, + string property7Name, + string property8Name, + Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector, + bool isDistinct) + { + return Observable.CombineLatest( + sender!.ObservableForProperty(property1Name, false, false, isDistinct), + sender!.ObservableForProperty(property2Name, false, false, isDistinct), + sender!.ObservableForProperty(property3Name, false, false, isDistinct), + sender!.ObservableForProperty(property4Name, false, false, isDistinct), + sender!.ObservableForProperty(property5Name, false, false, isDistinct), + sender!.ObservableForProperty(property6Name, false, false, isDistinct), + sender!.ObservableForProperty(property7Name, false, false, isDistinct), + sender!.ObservableForProperty(property8Name, false, false, isDistinct), + selector + ); + } - /// - /// WhenAny allows you to observe whenever one or more properties on an - /// object have changed, providing an initial value when the Observable - /// is set up, unlike ObservableForProperty(). Use this method in - /// constructors to set up bindings between properties that also need an - /// initial setup. - /// - /// The object where the property chain starts. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The 3 property chain to reference. This will be a expression pointing to a end property or field. - /// The 4 property chain to reference. This will be a expression pointing to a end property or field. - /// The 5 property chain to reference. This will be a expression pointing to a end property or field. - /// The 6 property chain to reference. This will be a expression pointing to a end property or field. - /// The 7 property chain to reference. This will be a expression pointing to a end property or field. - /// The 8 property chain to reference. This will be a expression pointing to a end property or field. - /// The selector which will determine the final value from the properties. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyDynamic uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyDynamic may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyDynamic( - this TSender? sender, - Expression? property1, - Expression? property2, - Expression? property3, - Expression? property4, - Expression? property5, - Expression? property6, - Expression? property7, - Expression? property8, - Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector) - { - return Observable.CombineLatest( - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property1, false, false), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property2, false, false), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property3, false, false), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property4, false, false), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property5, false, false), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property6, false, false), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property7, false, false), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property8, false, false), - selector - ); - } + /// + /// WhenAny allows you to observe whenever one or more properties on an + /// object have changed, providing an initial value when the Observable + /// is set up, unlike ObservableForProperty(). Use this method in + /// constructors to set up bindings between properties that also need an + /// initial setup. + /// + /// The object where the property chain starts. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The 3 property chain to reference. This will be a expression pointing to a end property or field. + /// The 4 property chain to reference. This will be a expression pointing to a end property or field. + /// The 5 property chain to reference. This will be a expression pointing to a end property or field. + /// The 6 property chain to reference. This will be a expression pointing to a end property or field. + /// The 7 property chain to reference. This will be a expression pointing to a end property or field. + /// The 8 property chain to reference. This will be a expression pointing to a end property or field. + /// The selector which will determine the final value from the properties. + public static IObservable WhenAnyDynamic( + this TSender? sender, + Expression? property1, + Expression? property2, + Expression? property3, + Expression? property4, + Expression? property5, + Expression? property6, + Expression? property7, + Expression? property8, + Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector) + { + return Observable.CombineLatest( + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property1, false, false), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property2, false, false), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property3, false, false), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property4, false, false), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property5, false, false), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property6, false, false), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property7, false, false), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property8, false, false), + selector + ); + } - /// - /// WhenAny allows you to observe whenever one or more properties on an - /// object have changed, providing an initial value when the Observable - /// is set up, unlike ObservableForProperty(). Use this method in - /// constructors to set up bindings between properties that also need an - /// initial setup. - /// - /// The object where the property chain starts. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The 3 property chain to reference. This will be a expression pointing to a end property or field. - /// The 4 property chain to reference. This will be a expression pointing to a end property or field. - /// The 5 property chain to reference. This will be a expression pointing to a end property or field. - /// The 6 property chain to reference. This will be a expression pointing to a end property or field. - /// The 7 property chain to reference. This will be a expression pointing to a end property or field. - /// The 8 property chain to reference. This will be a expression pointing to a end property or field. - /// The selector which will determine the final value from the properties. - /// if set to true [is distinct]. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyDynamic uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyDynamic may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyDynamic( - this TSender? sender, - Expression? property1, - Expression? property2, - Expression? property3, - Expression? property4, - Expression? property5, - Expression? property6, - Expression? property7, - Expression? property8, - Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector, - bool isDistinct) - { - return Observable.CombineLatest( - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property1, false, false, isDistinct), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property2, false, false, isDistinct), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property3, false, false, isDistinct), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property4, false, false, isDistinct), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property5, false, false, isDistinct), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property6, false, false, isDistinct), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property7, false, false, isDistinct), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property8, false, false, isDistinct), - selector - ); - } + /// + /// WhenAny allows you to observe whenever one or more properties on an + /// object have changed, providing an initial value when the Observable + /// is set up, unlike ObservableForProperty(). Use this method in + /// constructors to set up bindings between properties that also need an + /// initial setup. + /// + /// The object where the property chain starts. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The 3 property chain to reference. This will be a expression pointing to a end property or field. + /// The 4 property chain to reference. This will be a expression pointing to a end property or field. + /// The 5 property chain to reference. This will be a expression pointing to a end property or field. + /// The 6 property chain to reference. This will be a expression pointing to a end property or field. + /// The 7 property chain to reference. This will be a expression pointing to a end property or field. + /// The 8 property chain to reference. This will be a expression pointing to a end property or field. + /// The selector which will determine the final value from the properties. + /// if set to true [is distinct]. + public static IObservable WhenAnyDynamic( + this TSender? sender, + Expression? property1, + Expression? property2, + Expression? property3, + Expression? property4, + Expression? property5, + Expression? property6, + Expression? property7, + Expression? property8, + Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector, + bool isDistinct) + { + return Observable.CombineLatest( + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property1, false, false, isDistinct), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property2, false, false, isDistinct), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property3, false, false, isDistinct), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property4, false, false, isDistinct), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property5, false, false, isDistinct), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property6, false, false, isDistinct), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property7, false, false, isDistinct), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property8, false, false, isDistinct), + selector + ); + } - /// - /// WhenAnyValue allows you to observe whenever the value of one or more - /// properties on an object have changed, providing an initial value when - /// the Observable is set up, unlike ObservableForProperty(). Use this - /// method in constructors to set up bindings between properties that also - /// need an initial setup. - /// - /// The object where the property chain starts. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The 3 property chain to reference. This will be a expression pointing to a end property or field. - /// The 4 property chain to reference. This will be a expression pointing to a end property or field. - /// The 5 property chain to reference. This will be a expression pointing to a end property or field. - /// The 6 property chain to reference. This will be a expression pointing to a end property or field. - /// The 7 property chain to reference. This will be a expression pointing to a end property or field. - /// The 8 property chain to reference. This will be a expression pointing to a end property or field. - /// The 9 property chain to reference. This will be a expression pointing to a end property or field. - /// The selector which will determine the final value from the properties. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyValue( - this TSender? sender, - Expression> property1, - Expression> property2, - Expression> property3, - Expression> property4, - Expression> property5, - Expression> property6, - Expression> property7, - Expression> property8, - Expression> property9, - Func selector) - { - return sender!.WhenAny(property1, property2, property3, property4, property5, property6, property7, property8, property9, - (c1, c2, c3, c4, c5, c6, c7, c8, c9) => - selector(c1.Value, c2.Value, c3.Value, c4.Value, c5.Value, c6.Value, c7.Value, c8.Value, c9.Value)); - } + /// + /// WhenAnyValue allows you to observe whenever the value of one or more + /// properties on an object have changed, providing an initial value when + /// the Observable is set up, unlike ObservableForProperty(). Use this + /// method in constructors to set up bindings between properties that also + /// need an initial setup. + /// + /// The object where the property chain starts. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The 3 property chain to reference. This will be a expression pointing to a end property or field. + /// The 4 property chain to reference. This will be a expression pointing to a end property or field. + /// The 5 property chain to reference. This will be a expression pointing to a end property or field. + /// The 6 property chain to reference. This will be a expression pointing to a end property or field. + /// The 7 property chain to reference. This will be a expression pointing to a end property or field. + /// The 8 property chain to reference. This will be a expression pointing to a end property or field. + /// The 9 property chain to reference. This will be a expression pointing to a end property or field. + /// The selector which will determine the final value from the properties. + public static IObservable WhenAnyValue( + this TSender? sender, + Expression> property1, + Expression> property2, + Expression> property3, + Expression> property4, + Expression> property5, + Expression> property6, + Expression> property7, + Expression> property8, + Expression> property9, + Func selector) + { + return sender!.WhenAny(property1, property2, property3, property4, property5, property6, property7, property8, property9, + (c1, c2, c3, c4, c5, c6, c7, c8, c9) => + selector(c1.Value, c2.Value, c3.Value, c4.Value, c5.Value, c6.Value, c7.Value, c8.Value, c9.Value)); + } - /// - /// AOT-friendly selector overload using property names. - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyValue( - this TSender? sender, - string property1Name, - string property2Name, - string property3Name, - string property4Name, - string property5Name, - string property6Name, - string property7Name, - string property8Name, - string property9Name, - Func selector) - { - var o1 = sender!.ObservableForProperty(property1Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - var o2 = sender!.ObservableForProperty(property2Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - var o3 = sender!.ObservableForProperty(property3Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - var o4 = sender!.ObservableForProperty(property4Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - var o5 = sender!.ObservableForProperty(property5Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - var o6 = sender!.ObservableForProperty(property6Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - var o7 = sender!.ObservableForProperty(property7Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - var o8 = sender!.ObservableForProperty(property8Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - var o9 = sender!.ObservableForProperty(property9Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - return Observable.CombineLatest( - o1, - o2, - o3, - o4, - o5, - o6, - o7, - o8, - o9 - , selector); - } + /// + /// AOT-friendly selector overload using property names. + /// + public static IObservable WhenAnyValue( + this TSender? sender, + string property1Name, + string property2Name, + string property3Name, + string property4Name, + string property5Name, + string property6Name, + string property7Name, + string property8Name, + string property9Name, + Func selector) + { + var o1 = sender!.ObservableForProperty(property1Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + var o2 = sender!.ObservableForProperty(property2Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + var o3 = sender!.ObservableForProperty(property3Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + var o4 = sender!.ObservableForProperty(property4Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + var o5 = sender!.ObservableForProperty(property5Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + var o6 = sender!.ObservableForProperty(property6Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + var o7 = sender!.ObservableForProperty(property7Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + var o8 = sender!.ObservableForProperty(property8Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + var o9 = sender!.ObservableForProperty(property9Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + return Observable.CombineLatest( + o1, + o2, + o3, + o4, + o5, + o6, + o7, + o8, + o9 + , selector); + } - /// - /// WhenAnyValue allows you to observe whenever the value of one or more - /// properties on an object have changed, providing an initial value when - /// the Observable is set up, unlike ObservableForProperty(). Use this - /// method in constructors to set up bindings between properties that also - /// need an initial setup. - /// - /// The object where the property chain starts. - /// if set to true [is distinct]. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The 3 property chain to reference. This will be a expression pointing to a end property or field. - /// The 4 property chain to reference. This will be a expression pointing to a end property or field. - /// The 5 property chain to reference. This will be a expression pointing to a end property or field. - /// The 6 property chain to reference. This will be a expression pointing to a end property or field. - /// The 7 property chain to reference. This will be a expression pointing to a end property or field. - /// The 8 property chain to reference. This will be a expression pointing to a end property or field. - /// The 9 property chain to reference. This will be a expression pointing to a end property or field. - /// The selector which will determine the final value from the properties. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyValue( - this TSender? sender, - Expression> property1, - Expression> property2, - Expression> property3, - Expression> property4, - Expression> property5, - Expression> property6, - Expression> property7, - Expression> property8, - Expression> property9, - Func selector, - bool isDistinct) - { - return sender!.WhenAny(property1, property2, property3, property4, property5, property6, property7, property8, property9, - (c1, c2, c3, c4, c5, c6, c7, c8, c9) => - selector(c1.Value, c2.Value, c3.Value, c4.Value, c5.Value, c6.Value, c7.Value, c8.Value, c9.Value), - isDistinct); - } + /// + /// WhenAnyValue allows you to observe whenever the value of one or more + /// properties on an object have changed, providing an initial value when + /// the Observable is set up, unlike ObservableForProperty(). Use this + /// method in constructors to set up bindings between properties that also + /// need an initial setup. + /// + /// The object where the property chain starts. + /// if set to true [is distinct]. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The 3 property chain to reference. This will be a expression pointing to a end property or field. + /// The 4 property chain to reference. This will be a expression pointing to a end property or field. + /// The 5 property chain to reference. This will be a expression pointing to a end property or field. + /// The 6 property chain to reference. This will be a expression pointing to a end property or field. + /// The 7 property chain to reference. This will be a expression pointing to a end property or field. + /// The 8 property chain to reference. This will be a expression pointing to a end property or field. + /// The 9 property chain to reference. This will be a expression pointing to a end property or field. + /// The selector which will determine the final value from the properties. + public static IObservable WhenAnyValue( + this TSender? sender, + Expression> property1, + Expression> property2, + Expression> property3, + Expression> property4, + Expression> property5, + Expression> property6, + Expression> property7, + Expression> property8, + Expression> property9, + Func selector, + bool isDistinct) + { + return sender!.WhenAny(property1, property2, property3, property4, property5, property6, property7, property8, property9, + (c1, c2, c3, c4, c5, c6, c7, c8, c9) => + selector(c1.Value, c2.Value, c3.Value, c4.Value, c5.Value, c6.Value, c7.Value, c8.Value, c9.Value), + isDistinct); + } - /// - /// AOT-friendly selector overload using property names and distinct option. - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyValue( - this TSender? sender, - string property1Name, - string property2Name, - string property3Name, - string property4Name, - string property5Name, - string property6Name, - string property7Name, - string property8Name, - string property9Name, - Func selector, - bool isDistinct) - { - var o1 = sender!.ObservableForProperty(property1Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - var o2 = sender!.ObservableForProperty(property2Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - var o3 = sender!.ObservableForProperty(property3Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - var o4 = sender!.ObservableForProperty(property4Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - var o5 = sender!.ObservableForProperty(property5Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - var o6 = sender!.ObservableForProperty(property6Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - var o7 = sender!.ObservableForProperty(property7Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - var o8 = sender!.ObservableForProperty(property8Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - var o9 = sender!.ObservableForProperty(property9Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - return Observable.CombineLatest( - o1, - o2, - o3, - o4, - o5, - o6, - o7, - o8, - o9 - , selector); - } + /// + /// AOT-friendly selector overload using property names and distinct option. + /// + public static IObservable WhenAnyValue( + this TSender? sender, + string property1Name, + string property2Name, + string property3Name, + string property4Name, + string property5Name, + string property6Name, + string property7Name, + string property8Name, + string property9Name, + Func selector, + bool isDistinct) + { + var o1 = sender!.ObservableForProperty(property1Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + var o2 = sender!.ObservableForProperty(property2Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + var o3 = sender!.ObservableForProperty(property3Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + var o4 = sender!.ObservableForProperty(property4Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + var o5 = sender!.ObservableForProperty(property5Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + var o6 = sender!.ObservableForProperty(property6Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + var o7 = sender!.ObservableForProperty(property7Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + var o8 = sender!.ObservableForProperty(property8Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + var o9 = sender!.ObservableForProperty(property9Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + return Observable.CombineLatest( + o1, + o2, + o3, + o4, + o5, + o6, + o7, + o8, + o9 + , selector); + } - /// - /// WhenAny allows you to observe whenever one or more properties on an - /// object have changed, providing an initial value when the Observable - /// is set up, unlike ObservableForProperty(). Use this method in - /// constructors to set up bindings between properties that also need an - /// initial setup. - /// - /// The object where the property chain starts. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The 3 property chain to reference. This will be a expression pointing to a end property or field. - /// The 4 property chain to reference. This will be a expression pointing to a end property or field. - /// The 5 property chain to reference. This will be a expression pointing to a end property or field. - /// The 6 property chain to reference. This will be a expression pointing to a end property or field. - /// The 7 property chain to reference. This will be a expression pointing to a end property or field. - /// The 8 property chain to reference. This will be a expression pointing to a end property or field. - /// The 9 property chain to reference. This will be a expression pointing to a end property or field. - /// The selector which will determine the final value from the properties. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAny uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAny may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAny( - this TSender? sender, - Expression> property1, - Expression> property2, - Expression> property3, - Expression> property4, - Expression> property5, - Expression> property6, - Expression> property7, - Expression> property8, - Expression> property9, - Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector) - { - return Observable.CombineLatest( - sender!.ObservableForProperty(property1, false, false), - sender!.ObservableForProperty(property2, false, false), - sender!.ObservableForProperty(property3, false, false), - sender!.ObservableForProperty(property4, false, false), - sender!.ObservableForProperty(property5, false, false), - sender!.ObservableForProperty(property6, false, false), - sender!.ObservableForProperty(property7, false, false), - sender!.ObservableForProperty(property8, false, false), - sender!.ObservableForProperty(property9, false, false), - selector - ); - } + /// + /// WhenAny allows you to observe whenever one or more properties on an + /// object have changed, providing an initial value when the Observable + /// is set up, unlike ObservableForProperty(). Use this method in + /// constructors to set up bindings between properties that also need an + /// initial setup. + /// + /// The object where the property chain starts. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The 3 property chain to reference. This will be a expression pointing to a end property or field. + /// The 4 property chain to reference. This will be a expression pointing to a end property or field. + /// The 5 property chain to reference. This will be a expression pointing to a end property or field. + /// The 6 property chain to reference. This will be a expression pointing to a end property or field. + /// The 7 property chain to reference. This will be a expression pointing to a end property or field. + /// The 8 property chain to reference. This will be a expression pointing to a end property or field. + /// The 9 property chain to reference. This will be a expression pointing to a end property or field. + /// The selector which will determine the final value from the properties. + public static IObservable WhenAny( + this TSender? sender, + Expression> property1, + Expression> property2, + Expression> property3, + Expression> property4, + Expression> property5, + Expression> property6, + Expression> property7, + Expression> property8, + Expression> property9, + Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector) + { + return Observable.CombineLatest( + sender!.ObservableForProperty(property1, false, false), + sender!.ObservableForProperty(property2, false, false), + sender!.ObservableForProperty(property3, false, false), + sender!.ObservableForProperty(property4, false, false), + sender!.ObservableForProperty(property5, false, false), + sender!.ObservableForProperty(property6, false, false), + sender!.ObservableForProperty(property7, false, false), + sender!.ObservableForProperty(property8, false, false), + sender!.ObservableForProperty(property9, false, false), + selector + ); + } - /// - /// AOT-friendly WhenAny overload using property names. - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAny uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAny may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAny( - this TSender? sender, - string property1Name, - string property2Name, - string property3Name, - string property4Name, - string property5Name, - string property6Name, - string property7Name, - string property8Name, - string property9Name, - Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector) - { - return Observable.CombineLatest( - sender!.ObservableForProperty(property1Name, false, false), - sender!.ObservableForProperty(property2Name, false, false), - sender!.ObservableForProperty(property3Name, false, false), - sender!.ObservableForProperty(property4Name, false, false), - sender!.ObservableForProperty(property5Name, false, false), - sender!.ObservableForProperty(property6Name, false, false), - sender!.ObservableForProperty(property7Name, false, false), - sender!.ObservableForProperty(property8Name, false, false), - sender!.ObservableForProperty(property9Name, false, false), - selector - ); - } + /// + /// AOT-friendly WhenAny overload using property names. + /// + public static IObservable WhenAny( + this TSender? sender, + string property1Name, + string property2Name, + string property3Name, + string property4Name, + string property5Name, + string property6Name, + string property7Name, + string property8Name, + string property9Name, + Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector) + { + return Observable.CombineLatest( + sender!.ObservableForProperty(property1Name, false, false), + sender!.ObservableForProperty(property2Name, false, false), + sender!.ObservableForProperty(property3Name, false, false), + sender!.ObservableForProperty(property4Name, false, false), + sender!.ObservableForProperty(property5Name, false, false), + sender!.ObservableForProperty(property6Name, false, false), + sender!.ObservableForProperty(property7Name, false, false), + sender!.ObservableForProperty(property8Name, false, false), + sender!.ObservableForProperty(property9Name, false, false), + selector + ); + } - /// - /// WhenAny allows you to observe whenever one or more properties on an - /// object have changed, providing an initial value when the Observable - /// is set up, unlike ObservableForProperty(). Use this method in - /// constructors to set up bindings between properties that also need an - /// initial setup. - /// - /// The object where the property chain starts. - /// if set to true [is distinct]. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The 3 property chain to reference. This will be a expression pointing to a end property or field. - /// The 4 property chain to reference. This will be a expression pointing to a end property or field. - /// The 5 property chain to reference. This will be a expression pointing to a end property or field. - /// The 6 property chain to reference. This will be a expression pointing to a end property or field. - /// The 7 property chain to reference. This will be a expression pointing to a end property or field. - /// The 8 property chain to reference. This will be a expression pointing to a end property or field. - /// The 9 property chain to reference. This will be a expression pointing to a end property or field. - /// The selector which will determine the final value from the properties. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAny uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAny may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAny( - this TSender? sender, - Expression> property1, - Expression> property2, - Expression> property3, - Expression> property4, - Expression> property5, - Expression> property6, - Expression> property7, - Expression> property8, - Expression> property9, - Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector, - bool isDistinct) - { - return Observable.CombineLatest( - sender!.ObservableForProperty(property1, false, false, isDistinct), - sender!.ObservableForProperty(property2, false, false, isDistinct), - sender!.ObservableForProperty(property3, false, false, isDistinct), - sender!.ObservableForProperty(property4, false, false, isDistinct), - sender!.ObservableForProperty(property5, false, false, isDistinct), - sender!.ObservableForProperty(property6, false, false, isDistinct), - sender!.ObservableForProperty(property7, false, false, isDistinct), - sender!.ObservableForProperty(property8, false, false, isDistinct), - sender!.ObservableForProperty(property9, false, false, isDistinct), - selector - ); - } + /// + /// WhenAny allows you to observe whenever one or more properties on an + /// object have changed, providing an initial value when the Observable + /// is set up, unlike ObservableForProperty(). Use this method in + /// constructors to set up bindings between properties that also need an + /// initial setup. + /// + /// The object where the property chain starts. + /// if set to true [is distinct]. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The 3 property chain to reference. This will be a expression pointing to a end property or field. + /// The 4 property chain to reference. This will be a expression pointing to a end property or field. + /// The 5 property chain to reference. This will be a expression pointing to a end property or field. + /// The 6 property chain to reference. This will be a expression pointing to a end property or field. + /// The 7 property chain to reference. This will be a expression pointing to a end property or field. + /// The 8 property chain to reference. This will be a expression pointing to a end property or field. + /// The 9 property chain to reference. This will be a expression pointing to a end property or field. + /// The selector which will determine the final value from the properties. + public static IObservable WhenAny( + this TSender? sender, + Expression> property1, + Expression> property2, + Expression> property3, + Expression> property4, + Expression> property5, + Expression> property6, + Expression> property7, + Expression> property8, + Expression> property9, + Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector, + bool isDistinct) + { + return Observable.CombineLatest( + sender!.ObservableForProperty(property1, false, false, isDistinct), + sender!.ObservableForProperty(property2, false, false, isDistinct), + sender!.ObservableForProperty(property3, false, false, isDistinct), + sender!.ObservableForProperty(property4, false, false, isDistinct), + sender!.ObservableForProperty(property5, false, false, isDistinct), + sender!.ObservableForProperty(property6, false, false, isDistinct), + sender!.ObservableForProperty(property7, false, false, isDistinct), + sender!.ObservableForProperty(property8, false, false, isDistinct), + sender!.ObservableForProperty(property9, false, false, isDistinct), + selector + ); + } - /// - /// AOT-friendly WhenAny overload using property names and distinct option. - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAny uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAny may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAny( - this TSender? sender, - string property1Name, - string property2Name, - string property3Name, - string property4Name, - string property5Name, - string property6Name, - string property7Name, - string property8Name, - string property9Name, - Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector, - bool isDistinct) - { - return Observable.CombineLatest( - sender!.ObservableForProperty(property1Name, false, false, isDistinct), - sender!.ObservableForProperty(property2Name, false, false, isDistinct), - sender!.ObservableForProperty(property3Name, false, false, isDistinct), - sender!.ObservableForProperty(property4Name, false, false, isDistinct), - sender!.ObservableForProperty(property5Name, false, false, isDistinct), - sender!.ObservableForProperty(property6Name, false, false, isDistinct), - sender!.ObservableForProperty(property7Name, false, false, isDistinct), - sender!.ObservableForProperty(property8Name, false, false, isDistinct), - sender!.ObservableForProperty(property9Name, false, false, isDistinct), - selector - ); - } + /// + /// AOT-friendly WhenAny overload using property names and distinct option. + /// + public static IObservable WhenAny( + this TSender? sender, + string property1Name, + string property2Name, + string property3Name, + string property4Name, + string property5Name, + string property6Name, + string property7Name, + string property8Name, + string property9Name, + Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector, + bool isDistinct) + { + return Observable.CombineLatest( + sender!.ObservableForProperty(property1Name, false, false, isDistinct), + sender!.ObservableForProperty(property2Name, false, false, isDistinct), + sender!.ObservableForProperty(property3Name, false, false, isDistinct), + sender!.ObservableForProperty(property4Name, false, false, isDistinct), + sender!.ObservableForProperty(property5Name, false, false, isDistinct), + sender!.ObservableForProperty(property6Name, false, false, isDistinct), + sender!.ObservableForProperty(property7Name, false, false, isDistinct), + sender!.ObservableForProperty(property8Name, false, false, isDistinct), + sender!.ObservableForProperty(property9Name, false, false, isDistinct), + selector + ); + } - /// - /// WhenAny allows you to observe whenever one or more properties on an - /// object have changed, providing an initial value when the Observable - /// is set up, unlike ObservableForProperty(). Use this method in - /// constructors to set up bindings between properties that also need an - /// initial setup. - /// - /// The object where the property chain starts. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The 3 property chain to reference. This will be a expression pointing to a end property or field. - /// The 4 property chain to reference. This will be a expression pointing to a end property or field. - /// The 5 property chain to reference. This will be a expression pointing to a end property or field. - /// The 6 property chain to reference. This will be a expression pointing to a end property or field. - /// The 7 property chain to reference. This will be a expression pointing to a end property or field. - /// The 8 property chain to reference. This will be a expression pointing to a end property or field. - /// The 9 property chain to reference. This will be a expression pointing to a end property or field. - /// The selector which will determine the final value from the properties. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyDynamic uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyDynamic may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyDynamic( - this TSender? sender, - Expression? property1, - Expression? property2, - Expression? property3, - Expression? property4, - Expression? property5, - Expression? property6, - Expression? property7, - Expression? property8, - Expression? property9, - Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector) - { - return Observable.CombineLatest( - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property1, false, false), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property2, false, false), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property3, false, false), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property4, false, false), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property5, false, false), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property6, false, false), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property7, false, false), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property8, false, false), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property9, false, false), - selector - ); - } + /// + /// WhenAny allows you to observe whenever one or more properties on an + /// object have changed, providing an initial value when the Observable + /// is set up, unlike ObservableForProperty(). Use this method in + /// constructors to set up bindings between properties that also need an + /// initial setup. + /// + /// The object where the property chain starts. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The 3 property chain to reference. This will be a expression pointing to a end property or field. + /// The 4 property chain to reference. This will be a expression pointing to a end property or field. + /// The 5 property chain to reference. This will be a expression pointing to a end property or field. + /// The 6 property chain to reference. This will be a expression pointing to a end property or field. + /// The 7 property chain to reference. This will be a expression pointing to a end property or field. + /// The 8 property chain to reference. This will be a expression pointing to a end property or field. + /// The 9 property chain to reference. This will be a expression pointing to a end property or field. + /// The selector which will determine the final value from the properties. + public static IObservable WhenAnyDynamic( + this TSender? sender, + Expression? property1, + Expression? property2, + Expression? property3, + Expression? property4, + Expression? property5, + Expression? property6, + Expression? property7, + Expression? property8, + Expression? property9, + Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector) + { + return Observable.CombineLatest( + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property1, false, false), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property2, false, false), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property3, false, false), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property4, false, false), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property5, false, false), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property6, false, false), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property7, false, false), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property8, false, false), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property9, false, false), + selector + ); + } - /// - /// WhenAny allows you to observe whenever one or more properties on an - /// object have changed, providing an initial value when the Observable - /// is set up, unlike ObservableForProperty(). Use this method in - /// constructors to set up bindings between properties that also need an - /// initial setup. - /// - /// The object where the property chain starts. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The 3 property chain to reference. This will be a expression pointing to a end property or field. - /// The 4 property chain to reference. This will be a expression pointing to a end property or field. - /// The 5 property chain to reference. This will be a expression pointing to a end property or field. - /// The 6 property chain to reference. This will be a expression pointing to a end property or field. - /// The 7 property chain to reference. This will be a expression pointing to a end property or field. - /// The 8 property chain to reference. This will be a expression pointing to a end property or field. - /// The 9 property chain to reference. This will be a expression pointing to a end property or field. - /// The selector which will determine the final value from the properties. - /// if set to true [is distinct]. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyDynamic uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyDynamic may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyDynamic( - this TSender? sender, - Expression? property1, - Expression? property2, - Expression? property3, - Expression? property4, - Expression? property5, - Expression? property6, - Expression? property7, - Expression? property8, - Expression? property9, - Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector, - bool isDistinct) - { - return Observable.CombineLatest( - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property1, false, false, isDistinct), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property2, false, false, isDistinct), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property3, false, false, isDistinct), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property4, false, false, isDistinct), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property5, false, false, isDistinct), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property6, false, false, isDistinct), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property7, false, false, isDistinct), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property8, false, false, isDistinct), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property9, false, false, isDistinct), - selector - ); - } + /// + /// WhenAny allows you to observe whenever one or more properties on an + /// object have changed, providing an initial value when the Observable + /// is set up, unlike ObservableForProperty(). Use this method in + /// constructors to set up bindings between properties that also need an + /// initial setup. + /// + /// The object where the property chain starts. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The 3 property chain to reference. This will be a expression pointing to a end property or field. + /// The 4 property chain to reference. This will be a expression pointing to a end property or field. + /// The 5 property chain to reference. This will be a expression pointing to a end property or field. + /// The 6 property chain to reference. This will be a expression pointing to a end property or field. + /// The 7 property chain to reference. This will be a expression pointing to a end property or field. + /// The 8 property chain to reference. This will be a expression pointing to a end property or field. + /// The 9 property chain to reference. This will be a expression pointing to a end property or field. + /// The selector which will determine the final value from the properties. + /// if set to true [is distinct]. + public static IObservable WhenAnyDynamic( + this TSender? sender, + Expression? property1, + Expression? property2, + Expression? property3, + Expression? property4, + Expression? property5, + Expression? property6, + Expression? property7, + Expression? property8, + Expression? property9, + Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector, + bool isDistinct) + { + return Observable.CombineLatest( + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property1, false, false, isDistinct), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property2, false, false, isDistinct), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property3, false, false, isDistinct), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property4, false, false, isDistinct), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property5, false, false, isDistinct), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property6, false, false, isDistinct), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property7, false, false, isDistinct), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property8, false, false, isDistinct), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property9, false, false, isDistinct), + selector + ); + } - /// - /// WhenAnyValue allows you to observe whenever the value of one or more - /// properties on an object have changed, providing an initial value when - /// the Observable is set up, unlike ObservableForProperty(). Use this - /// method in constructors to set up bindings between properties that also - /// need an initial setup. - /// - /// The object where the property chain starts. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The 3 property chain to reference. This will be a expression pointing to a end property or field. - /// The 4 property chain to reference. This will be a expression pointing to a end property or field. - /// The 5 property chain to reference. This will be a expression pointing to a end property or field. - /// The 6 property chain to reference. This will be a expression pointing to a end property or field. - /// The 7 property chain to reference. This will be a expression pointing to a end property or field. - /// The 8 property chain to reference. This will be a expression pointing to a end property or field. - /// The 9 property chain to reference. This will be a expression pointing to a end property or field. - /// The 10 property chain to reference. This will be a expression pointing to a end property or field. - /// The selector which will determine the final value from the properties. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyValue( - this TSender? sender, - Expression> property1, - Expression> property2, - Expression> property3, - Expression> property4, - Expression> property5, - Expression> property6, - Expression> property7, - Expression> property8, - Expression> property9, - Expression> property10, - Func selector) - { - return sender!.WhenAny(property1, property2, property3, property4, property5, property6, property7, property8, property9, property10, - (c1, c2, c3, c4, c5, c6, c7, c8, c9, c10) => - selector(c1.Value, c2.Value, c3.Value, c4.Value, c5.Value, c6.Value, c7.Value, c8.Value, c9.Value, c10.Value)); - } + /// + /// WhenAnyValue allows you to observe whenever the value of one or more + /// properties on an object have changed, providing an initial value when + /// the Observable is set up, unlike ObservableForProperty(). Use this + /// method in constructors to set up bindings between properties that also + /// need an initial setup. + /// + /// The object where the property chain starts. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The 3 property chain to reference. This will be a expression pointing to a end property or field. + /// The 4 property chain to reference. This will be a expression pointing to a end property or field. + /// The 5 property chain to reference. This will be a expression pointing to a end property or field. + /// The 6 property chain to reference. This will be a expression pointing to a end property or field. + /// The 7 property chain to reference. This will be a expression pointing to a end property or field. + /// The 8 property chain to reference. This will be a expression pointing to a end property or field. + /// The 9 property chain to reference. This will be a expression pointing to a end property or field. + /// The 10 property chain to reference. This will be a expression pointing to a end property or field. + /// The selector which will determine the final value from the properties. + public static IObservable WhenAnyValue( + this TSender? sender, + Expression> property1, + Expression> property2, + Expression> property3, + Expression> property4, + Expression> property5, + Expression> property6, + Expression> property7, + Expression> property8, + Expression> property9, + Expression> property10, + Func selector) + { + return sender!.WhenAny(property1, property2, property3, property4, property5, property6, property7, property8, property9, property10, + (c1, c2, c3, c4, c5, c6, c7, c8, c9, c10) => + selector(c1.Value, c2.Value, c3.Value, c4.Value, c5.Value, c6.Value, c7.Value, c8.Value, c9.Value, c10.Value)); + } - /// - /// AOT-friendly selector overload using property names. - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyValue( - this TSender? sender, - string property1Name, - string property2Name, - string property3Name, - string property4Name, - string property5Name, - string property6Name, - string property7Name, - string property8Name, - string property9Name, - string property10Name, - Func selector) - { - var o1 = sender!.ObservableForProperty(property1Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - var o2 = sender!.ObservableForProperty(property2Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - var o3 = sender!.ObservableForProperty(property3Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - var o4 = sender!.ObservableForProperty(property4Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - var o5 = sender!.ObservableForProperty(property5Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - var o6 = sender!.ObservableForProperty(property6Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - var o7 = sender!.ObservableForProperty(property7Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - var o8 = sender!.ObservableForProperty(property8Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - var o9 = sender!.ObservableForProperty(property9Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - var o10 = sender!.ObservableForProperty(property10Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - return Observable.CombineLatest( - o1, - o2, - o3, - o4, - o5, - o6, - o7, - o8, - o9, - o10 - , selector); - } + /// + /// AOT-friendly selector overload using property names. + /// + public static IObservable WhenAnyValue( + this TSender? sender, + string property1Name, + string property2Name, + string property3Name, + string property4Name, + string property5Name, + string property6Name, + string property7Name, + string property8Name, + string property9Name, + string property10Name, + Func selector) + { + var o1 = sender!.ObservableForProperty(property1Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + var o2 = sender!.ObservableForProperty(property2Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + var o3 = sender!.ObservableForProperty(property3Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + var o4 = sender!.ObservableForProperty(property4Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + var o5 = sender!.ObservableForProperty(property5Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + var o6 = sender!.ObservableForProperty(property6Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + var o7 = sender!.ObservableForProperty(property7Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + var o8 = sender!.ObservableForProperty(property8Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + var o9 = sender!.ObservableForProperty(property9Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + var o10 = sender!.ObservableForProperty(property10Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + return Observable.CombineLatest( + o1, + o2, + o3, + o4, + o5, + o6, + o7, + o8, + o9, + o10 + , selector); + } - /// - /// WhenAnyValue allows you to observe whenever the value of one or more - /// properties on an object have changed, providing an initial value when - /// the Observable is set up, unlike ObservableForProperty(). Use this - /// method in constructors to set up bindings between properties that also - /// need an initial setup. - /// - /// The object where the property chain starts. - /// if set to true [is distinct]. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The 3 property chain to reference. This will be a expression pointing to a end property or field. - /// The 4 property chain to reference. This will be a expression pointing to a end property or field. - /// The 5 property chain to reference. This will be a expression pointing to a end property or field. - /// The 6 property chain to reference. This will be a expression pointing to a end property or field. - /// The 7 property chain to reference. This will be a expression pointing to a end property or field. - /// The 8 property chain to reference. This will be a expression pointing to a end property or field. - /// The 9 property chain to reference. This will be a expression pointing to a end property or field. - /// The 10 property chain to reference. This will be a expression pointing to a end property or field. - /// The selector which will determine the final value from the properties. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyValue( - this TSender? sender, - Expression> property1, - Expression> property2, - Expression> property3, - Expression> property4, - Expression> property5, - Expression> property6, - Expression> property7, - Expression> property8, - Expression> property9, - Expression> property10, - Func selector, - bool isDistinct) - { - return sender!.WhenAny(property1, property2, property3, property4, property5, property6, property7, property8, property9, property10, - (c1, c2, c3, c4, c5, c6, c7, c8, c9, c10) => - selector(c1.Value, c2.Value, c3.Value, c4.Value, c5.Value, c6.Value, c7.Value, c8.Value, c9.Value, c10.Value), - isDistinct); - } + /// + /// WhenAnyValue allows you to observe whenever the value of one or more + /// properties on an object have changed, providing an initial value when + /// the Observable is set up, unlike ObservableForProperty(). Use this + /// method in constructors to set up bindings between properties that also + /// need an initial setup. + /// + /// The object where the property chain starts. + /// if set to true [is distinct]. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The 3 property chain to reference. This will be a expression pointing to a end property or field. + /// The 4 property chain to reference. This will be a expression pointing to a end property or field. + /// The 5 property chain to reference. This will be a expression pointing to a end property or field. + /// The 6 property chain to reference. This will be a expression pointing to a end property or field. + /// The 7 property chain to reference. This will be a expression pointing to a end property or field. + /// The 8 property chain to reference. This will be a expression pointing to a end property or field. + /// The 9 property chain to reference. This will be a expression pointing to a end property or field. + /// The 10 property chain to reference. This will be a expression pointing to a end property or field. + /// The selector which will determine the final value from the properties. + public static IObservable WhenAnyValue( + this TSender? sender, + Expression> property1, + Expression> property2, + Expression> property3, + Expression> property4, + Expression> property5, + Expression> property6, + Expression> property7, + Expression> property8, + Expression> property9, + Expression> property10, + Func selector, + bool isDistinct) + { + return sender!.WhenAny(property1, property2, property3, property4, property5, property6, property7, property8, property9, property10, + (c1, c2, c3, c4, c5, c6, c7, c8, c9, c10) => + selector(c1.Value, c2.Value, c3.Value, c4.Value, c5.Value, c6.Value, c7.Value, c8.Value, c9.Value, c10.Value), + isDistinct); + } - /// - /// AOT-friendly selector overload using property names and distinct option. - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyValue( - this TSender? sender, - string property1Name, - string property2Name, - string property3Name, - string property4Name, - string property5Name, - string property6Name, - string property7Name, - string property8Name, - string property9Name, - string property10Name, - Func selector, - bool isDistinct) - { - var o1 = sender!.ObservableForProperty(property1Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - var o2 = sender!.ObservableForProperty(property2Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - var o3 = sender!.ObservableForProperty(property3Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - var o4 = sender!.ObservableForProperty(property4Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - var o5 = sender!.ObservableForProperty(property5Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - var o6 = sender!.ObservableForProperty(property6Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - var o7 = sender!.ObservableForProperty(property7Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - var o8 = sender!.ObservableForProperty(property8Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - var o9 = sender!.ObservableForProperty(property9Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - var o10 = sender!.ObservableForProperty(property10Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - return Observable.CombineLatest( - o1, - o2, - o3, - o4, - o5, - o6, - o7, - o8, - o9, - o10 - , selector); - } + /// + /// AOT-friendly selector overload using property names and distinct option. + /// + public static IObservable WhenAnyValue( + this TSender? sender, + string property1Name, + string property2Name, + string property3Name, + string property4Name, + string property5Name, + string property6Name, + string property7Name, + string property8Name, + string property9Name, + string property10Name, + Func selector, + bool isDistinct) + { + var o1 = sender!.ObservableForProperty(property1Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + var o2 = sender!.ObservableForProperty(property2Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + var o3 = sender!.ObservableForProperty(property3Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + var o4 = sender!.ObservableForProperty(property4Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + var o5 = sender!.ObservableForProperty(property5Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + var o6 = sender!.ObservableForProperty(property6Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + var o7 = sender!.ObservableForProperty(property7Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + var o8 = sender!.ObservableForProperty(property8Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + var o9 = sender!.ObservableForProperty(property9Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + var o10 = sender!.ObservableForProperty(property10Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + return Observable.CombineLatest( + o1, + o2, + o3, + o4, + o5, + o6, + o7, + o8, + o9, + o10 + , selector); + } - /// - /// WhenAny allows you to observe whenever one or more properties on an - /// object have changed, providing an initial value when the Observable - /// is set up, unlike ObservableForProperty(). Use this method in - /// constructors to set up bindings between properties that also need an - /// initial setup. - /// - /// The object where the property chain starts. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The 3 property chain to reference. This will be a expression pointing to a end property or field. - /// The 4 property chain to reference. This will be a expression pointing to a end property or field. - /// The 5 property chain to reference. This will be a expression pointing to a end property or field. - /// The 6 property chain to reference. This will be a expression pointing to a end property or field. - /// The 7 property chain to reference. This will be a expression pointing to a end property or field. - /// The 8 property chain to reference. This will be a expression pointing to a end property or field. - /// The 9 property chain to reference. This will be a expression pointing to a end property or field. - /// The 10 property chain to reference. This will be a expression pointing to a end property or field. - /// The selector which will determine the final value from the properties. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAny uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAny may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAny( - this TSender? sender, - Expression> property1, - Expression> property2, - Expression> property3, - Expression> property4, - Expression> property5, - Expression> property6, - Expression> property7, - Expression> property8, - Expression> property9, - Expression> property10, - Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector) - { - return Observable.CombineLatest( - sender!.ObservableForProperty(property1, false, false), - sender!.ObservableForProperty(property2, false, false), - sender!.ObservableForProperty(property3, false, false), - sender!.ObservableForProperty(property4, false, false), - sender!.ObservableForProperty(property5, false, false), - sender!.ObservableForProperty(property6, false, false), - sender!.ObservableForProperty(property7, false, false), - sender!.ObservableForProperty(property8, false, false), - sender!.ObservableForProperty(property9, false, false), - sender!.ObservableForProperty(property10, false, false), - selector - ); - } + /// + /// WhenAny allows you to observe whenever one or more properties on an + /// object have changed, providing an initial value when the Observable + /// is set up, unlike ObservableForProperty(). Use this method in + /// constructors to set up bindings between properties that also need an + /// initial setup. + /// + /// The object where the property chain starts. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The 3 property chain to reference. This will be a expression pointing to a end property or field. + /// The 4 property chain to reference. This will be a expression pointing to a end property or field. + /// The 5 property chain to reference. This will be a expression pointing to a end property or field. + /// The 6 property chain to reference. This will be a expression pointing to a end property or field. + /// The 7 property chain to reference. This will be a expression pointing to a end property or field. + /// The 8 property chain to reference. This will be a expression pointing to a end property or field. + /// The 9 property chain to reference. This will be a expression pointing to a end property or field. + /// The 10 property chain to reference. This will be a expression pointing to a end property or field. + /// The selector which will determine the final value from the properties. + public static IObservable WhenAny( + this TSender? sender, + Expression> property1, + Expression> property2, + Expression> property3, + Expression> property4, + Expression> property5, + Expression> property6, + Expression> property7, + Expression> property8, + Expression> property9, + Expression> property10, + Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector) + { + return Observable.CombineLatest( + sender!.ObservableForProperty(property1, false, false), + sender!.ObservableForProperty(property2, false, false), + sender!.ObservableForProperty(property3, false, false), + sender!.ObservableForProperty(property4, false, false), + sender!.ObservableForProperty(property5, false, false), + sender!.ObservableForProperty(property6, false, false), + sender!.ObservableForProperty(property7, false, false), + sender!.ObservableForProperty(property8, false, false), + sender!.ObservableForProperty(property9, false, false), + sender!.ObservableForProperty(property10, false, false), + selector + ); + } - /// - /// AOT-friendly WhenAny overload using property names. - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAny uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAny may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAny( - this TSender? sender, - string property1Name, - string property2Name, - string property3Name, - string property4Name, - string property5Name, - string property6Name, - string property7Name, - string property8Name, - string property9Name, - string property10Name, - Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector) - { - return Observable.CombineLatest( - sender!.ObservableForProperty(property1Name, false, false), - sender!.ObservableForProperty(property2Name, false, false), - sender!.ObservableForProperty(property3Name, false, false), - sender!.ObservableForProperty(property4Name, false, false), - sender!.ObservableForProperty(property5Name, false, false), - sender!.ObservableForProperty(property6Name, false, false), - sender!.ObservableForProperty(property7Name, false, false), - sender!.ObservableForProperty(property8Name, false, false), - sender!.ObservableForProperty(property9Name, false, false), - sender!.ObservableForProperty(property10Name, false, false), - selector - ); - } + /// + /// AOT-friendly WhenAny overload using property names. + /// + public static IObservable WhenAny( + this TSender? sender, + string property1Name, + string property2Name, + string property3Name, + string property4Name, + string property5Name, + string property6Name, + string property7Name, + string property8Name, + string property9Name, + string property10Name, + Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector) + { + return Observable.CombineLatest( + sender!.ObservableForProperty(property1Name, false, false), + sender!.ObservableForProperty(property2Name, false, false), + sender!.ObservableForProperty(property3Name, false, false), + sender!.ObservableForProperty(property4Name, false, false), + sender!.ObservableForProperty(property5Name, false, false), + sender!.ObservableForProperty(property6Name, false, false), + sender!.ObservableForProperty(property7Name, false, false), + sender!.ObservableForProperty(property8Name, false, false), + sender!.ObservableForProperty(property9Name, false, false), + sender!.ObservableForProperty(property10Name, false, false), + selector + ); + } - /// - /// WhenAny allows you to observe whenever one or more properties on an - /// object have changed, providing an initial value when the Observable - /// is set up, unlike ObservableForProperty(). Use this method in - /// constructors to set up bindings between properties that also need an - /// initial setup. - /// - /// The object where the property chain starts. - /// if set to true [is distinct]. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The 3 property chain to reference. This will be a expression pointing to a end property or field. - /// The 4 property chain to reference. This will be a expression pointing to a end property or field. - /// The 5 property chain to reference. This will be a expression pointing to a end property or field. - /// The 6 property chain to reference. This will be a expression pointing to a end property or field. - /// The 7 property chain to reference. This will be a expression pointing to a end property or field. - /// The 8 property chain to reference. This will be a expression pointing to a end property or field. - /// The 9 property chain to reference. This will be a expression pointing to a end property or field. - /// The 10 property chain to reference. This will be a expression pointing to a end property or field. - /// The selector which will determine the final value from the properties. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAny uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAny may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAny( - this TSender? sender, - Expression> property1, - Expression> property2, - Expression> property3, - Expression> property4, - Expression> property5, - Expression> property6, - Expression> property7, - Expression> property8, - Expression> property9, - Expression> property10, - Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector, - bool isDistinct) - { - return Observable.CombineLatest( - sender!.ObservableForProperty(property1, false, false, isDistinct), - sender!.ObservableForProperty(property2, false, false, isDistinct), - sender!.ObservableForProperty(property3, false, false, isDistinct), - sender!.ObservableForProperty(property4, false, false, isDistinct), - sender!.ObservableForProperty(property5, false, false, isDistinct), - sender!.ObservableForProperty(property6, false, false, isDistinct), - sender!.ObservableForProperty(property7, false, false, isDistinct), - sender!.ObservableForProperty(property8, false, false, isDistinct), - sender!.ObservableForProperty(property9, false, false, isDistinct), - sender!.ObservableForProperty(property10, false, false, isDistinct), - selector - ); - } + /// + /// WhenAny allows you to observe whenever one or more properties on an + /// object have changed, providing an initial value when the Observable + /// is set up, unlike ObservableForProperty(). Use this method in + /// constructors to set up bindings between properties that also need an + /// initial setup. + /// + /// The object where the property chain starts. + /// if set to true [is distinct]. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The 3 property chain to reference. This will be a expression pointing to a end property or field. + /// The 4 property chain to reference. This will be a expression pointing to a end property or field. + /// The 5 property chain to reference. This will be a expression pointing to a end property or field. + /// The 6 property chain to reference. This will be a expression pointing to a end property or field. + /// The 7 property chain to reference. This will be a expression pointing to a end property or field. + /// The 8 property chain to reference. This will be a expression pointing to a end property or field. + /// The 9 property chain to reference. This will be a expression pointing to a end property or field. + /// The 10 property chain to reference. This will be a expression pointing to a end property or field. + /// The selector which will determine the final value from the properties. + public static IObservable WhenAny( + this TSender? sender, + Expression> property1, + Expression> property2, + Expression> property3, + Expression> property4, + Expression> property5, + Expression> property6, + Expression> property7, + Expression> property8, + Expression> property9, + Expression> property10, + Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector, + bool isDistinct) + { + return Observable.CombineLatest( + sender!.ObservableForProperty(property1, false, false, isDistinct), + sender!.ObservableForProperty(property2, false, false, isDistinct), + sender!.ObservableForProperty(property3, false, false, isDistinct), + sender!.ObservableForProperty(property4, false, false, isDistinct), + sender!.ObservableForProperty(property5, false, false, isDistinct), + sender!.ObservableForProperty(property6, false, false, isDistinct), + sender!.ObservableForProperty(property7, false, false, isDistinct), + sender!.ObservableForProperty(property8, false, false, isDistinct), + sender!.ObservableForProperty(property9, false, false, isDistinct), + sender!.ObservableForProperty(property10, false, false, isDistinct), + selector + ); + } - /// - /// AOT-friendly WhenAny overload using property names and distinct option. - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAny uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAny may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAny( - this TSender? sender, - string property1Name, - string property2Name, - string property3Name, - string property4Name, - string property5Name, - string property6Name, - string property7Name, - string property8Name, - string property9Name, - string property10Name, - Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector, - bool isDistinct) - { - return Observable.CombineLatest( - sender!.ObservableForProperty(property1Name, false, false, isDistinct), - sender!.ObservableForProperty(property2Name, false, false, isDistinct), - sender!.ObservableForProperty(property3Name, false, false, isDistinct), - sender!.ObservableForProperty(property4Name, false, false, isDistinct), - sender!.ObservableForProperty(property5Name, false, false, isDistinct), - sender!.ObservableForProperty(property6Name, false, false, isDistinct), - sender!.ObservableForProperty(property7Name, false, false, isDistinct), - sender!.ObservableForProperty(property8Name, false, false, isDistinct), - sender!.ObservableForProperty(property9Name, false, false, isDistinct), - sender!.ObservableForProperty(property10Name, false, false, isDistinct), - selector - ); - } + /// + /// AOT-friendly WhenAny overload using property names and distinct option. + /// + public static IObservable WhenAny( + this TSender? sender, + string property1Name, + string property2Name, + string property3Name, + string property4Name, + string property5Name, + string property6Name, + string property7Name, + string property8Name, + string property9Name, + string property10Name, + Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector, + bool isDistinct) + { + return Observable.CombineLatest( + sender!.ObservableForProperty(property1Name, false, false, isDistinct), + sender!.ObservableForProperty(property2Name, false, false, isDistinct), + sender!.ObservableForProperty(property3Name, false, false, isDistinct), + sender!.ObservableForProperty(property4Name, false, false, isDistinct), + sender!.ObservableForProperty(property5Name, false, false, isDistinct), + sender!.ObservableForProperty(property6Name, false, false, isDistinct), + sender!.ObservableForProperty(property7Name, false, false, isDistinct), + sender!.ObservableForProperty(property8Name, false, false, isDistinct), + sender!.ObservableForProperty(property9Name, false, false, isDistinct), + sender!.ObservableForProperty(property10Name, false, false, isDistinct), + selector + ); + } - /// - /// WhenAny allows you to observe whenever one or more properties on an - /// object have changed, providing an initial value when the Observable - /// is set up, unlike ObservableForProperty(). Use this method in - /// constructors to set up bindings between properties that also need an - /// initial setup. - /// - /// The object where the property chain starts. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The 3 property chain to reference. This will be a expression pointing to a end property or field. - /// The 4 property chain to reference. This will be a expression pointing to a end property or field. - /// The 5 property chain to reference. This will be a expression pointing to a end property or field. - /// The 6 property chain to reference. This will be a expression pointing to a end property or field. - /// The 7 property chain to reference. This will be a expression pointing to a end property or field. - /// The 8 property chain to reference. This will be a expression pointing to a end property or field. - /// The 9 property chain to reference. This will be a expression pointing to a end property or field. - /// The 10 property chain to reference. This will be a expression pointing to a end property or field. - /// The selector which will determine the final value from the properties. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyDynamic uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyDynamic may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyDynamic( - this TSender? sender, - Expression? property1, - Expression? property2, - Expression? property3, - Expression? property4, - Expression? property5, - Expression? property6, - Expression? property7, - Expression? property8, - Expression? property9, - Expression? property10, - Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector) - { - return Observable.CombineLatest( - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property1, false, false), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property2, false, false), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property3, false, false), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property4, false, false), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property5, false, false), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property6, false, false), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property7, false, false), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property8, false, false), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property9, false, false), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property10, false, false), - selector - ); - } + /// + /// WhenAny allows you to observe whenever one or more properties on an + /// object have changed, providing an initial value when the Observable + /// is set up, unlike ObservableForProperty(). Use this method in + /// constructors to set up bindings between properties that also need an + /// initial setup. + /// + /// The object where the property chain starts. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The 3 property chain to reference. This will be a expression pointing to a end property or field. + /// The 4 property chain to reference. This will be a expression pointing to a end property or field. + /// The 5 property chain to reference. This will be a expression pointing to a end property or field. + /// The 6 property chain to reference. This will be a expression pointing to a end property or field. + /// The 7 property chain to reference. This will be a expression pointing to a end property or field. + /// The 8 property chain to reference. This will be a expression pointing to a end property or field. + /// The 9 property chain to reference. This will be a expression pointing to a end property or field. + /// The 10 property chain to reference. This will be a expression pointing to a end property or field. + /// The selector which will determine the final value from the properties. + public static IObservable WhenAnyDynamic( + this TSender? sender, + Expression? property1, + Expression? property2, + Expression? property3, + Expression? property4, + Expression? property5, + Expression? property6, + Expression? property7, + Expression? property8, + Expression? property9, + Expression? property10, + Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector) + { + return Observable.CombineLatest( + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property1, false, false), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property2, false, false), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property3, false, false), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property4, false, false), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property5, false, false), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property6, false, false), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property7, false, false), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property8, false, false), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property9, false, false), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property10, false, false), + selector + ); + } - /// - /// WhenAny allows you to observe whenever one or more properties on an - /// object have changed, providing an initial value when the Observable - /// is set up, unlike ObservableForProperty(). Use this method in - /// constructors to set up bindings between properties that also need an - /// initial setup. - /// - /// The object where the property chain starts. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The 3 property chain to reference. This will be a expression pointing to a end property or field. - /// The 4 property chain to reference. This will be a expression pointing to a end property or field. - /// The 5 property chain to reference. This will be a expression pointing to a end property or field. - /// The 6 property chain to reference. This will be a expression pointing to a end property or field. - /// The 7 property chain to reference. This will be a expression pointing to a end property or field. - /// The 8 property chain to reference. This will be a expression pointing to a end property or field. - /// The 9 property chain to reference. This will be a expression pointing to a end property or field. - /// The 10 property chain to reference. This will be a expression pointing to a end property or field. - /// The selector which will determine the final value from the properties. - /// if set to true [is distinct]. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyDynamic uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyDynamic may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyDynamic( - this TSender? sender, - Expression? property1, - Expression? property2, - Expression? property3, - Expression? property4, - Expression? property5, - Expression? property6, - Expression? property7, - Expression? property8, - Expression? property9, - Expression? property10, - Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector, - bool isDistinct) - { - return Observable.CombineLatest( - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property1, false, false, isDistinct), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property2, false, false, isDistinct), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property3, false, false, isDistinct), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property4, false, false, isDistinct), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property5, false, false, isDistinct), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property6, false, false, isDistinct), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property7, false, false, isDistinct), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property8, false, false, isDistinct), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property9, false, false, isDistinct), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property10, false, false, isDistinct), - selector - ); - } + /// + /// WhenAny allows you to observe whenever one or more properties on an + /// object have changed, providing an initial value when the Observable + /// is set up, unlike ObservableForProperty(). Use this method in + /// constructors to set up bindings between properties that also need an + /// initial setup. + /// + /// The object where the property chain starts. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The 3 property chain to reference. This will be a expression pointing to a end property or field. + /// The 4 property chain to reference. This will be a expression pointing to a end property or field. + /// The 5 property chain to reference. This will be a expression pointing to a end property or field. + /// The 6 property chain to reference. This will be a expression pointing to a end property or field. + /// The 7 property chain to reference. This will be a expression pointing to a end property or field. + /// The 8 property chain to reference. This will be a expression pointing to a end property or field. + /// The 9 property chain to reference. This will be a expression pointing to a end property or field. + /// The 10 property chain to reference. This will be a expression pointing to a end property or field. + /// The selector which will determine the final value from the properties. + /// if set to true [is distinct]. + public static IObservable WhenAnyDynamic( + this TSender? sender, + Expression? property1, + Expression? property2, + Expression? property3, + Expression? property4, + Expression? property5, + Expression? property6, + Expression? property7, + Expression? property8, + Expression? property9, + Expression? property10, + Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector, + bool isDistinct) + { + return Observable.CombineLatest( + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property1, false, false, isDistinct), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property2, false, false, isDistinct), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property3, false, false, isDistinct), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property4, false, false, isDistinct), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property5, false, false, isDistinct), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property6, false, false, isDistinct), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property7, false, false, isDistinct), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property8, false, false, isDistinct), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property9, false, false, isDistinct), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property10, false, false, isDistinct), + selector + ); + } - /// - /// WhenAnyValue allows you to observe whenever the value of one or more - /// properties on an object have changed, providing an initial value when - /// the Observable is set up, unlike ObservableForProperty(). Use this - /// method in constructors to set up bindings between properties that also - /// need an initial setup. - /// - /// The object where the property chain starts. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The 3 property chain to reference. This will be a expression pointing to a end property or field. - /// The 4 property chain to reference. This will be a expression pointing to a end property or field. - /// The 5 property chain to reference. This will be a expression pointing to a end property or field. - /// The 6 property chain to reference. This will be a expression pointing to a end property or field. - /// The 7 property chain to reference. This will be a expression pointing to a end property or field. - /// The 8 property chain to reference. This will be a expression pointing to a end property or field. - /// The 9 property chain to reference. This will be a expression pointing to a end property or field. - /// The 10 property chain to reference. This will be a expression pointing to a end property or field. - /// The 11 property chain to reference. This will be a expression pointing to a end property or field. - /// The selector which will determine the final value from the properties. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyValue( - this TSender? sender, - Expression> property1, - Expression> property2, - Expression> property3, - Expression> property4, - Expression> property5, - Expression> property6, - Expression> property7, - Expression> property8, - Expression> property9, - Expression> property10, - Expression> property11, - Func selector) - { - return sender!.WhenAny(property1, property2, property3, property4, property5, property6, property7, property8, property9, property10, property11, - (c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11) => - selector(c1.Value, c2.Value, c3.Value, c4.Value, c5.Value, c6.Value, c7.Value, c8.Value, c9.Value, c10.Value, c11.Value)); - } + /// + /// WhenAnyValue allows you to observe whenever the value of one or more + /// properties on an object have changed, providing an initial value when + /// the Observable is set up, unlike ObservableForProperty(). Use this + /// method in constructors to set up bindings between properties that also + /// need an initial setup. + /// + /// The object where the property chain starts. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The 3 property chain to reference. This will be a expression pointing to a end property or field. + /// The 4 property chain to reference. This will be a expression pointing to a end property or field. + /// The 5 property chain to reference. This will be a expression pointing to a end property or field. + /// The 6 property chain to reference. This will be a expression pointing to a end property or field. + /// The 7 property chain to reference. This will be a expression pointing to a end property or field. + /// The 8 property chain to reference. This will be a expression pointing to a end property or field. + /// The 9 property chain to reference. This will be a expression pointing to a end property or field. + /// The 10 property chain to reference. This will be a expression pointing to a end property or field. + /// The 11 property chain to reference. This will be a expression pointing to a end property or field. + /// The selector which will determine the final value from the properties. + public static IObservable WhenAnyValue( + this TSender? sender, + Expression> property1, + Expression> property2, + Expression> property3, + Expression> property4, + Expression> property5, + Expression> property6, + Expression> property7, + Expression> property8, + Expression> property9, + Expression> property10, + Expression> property11, + Func selector) + { + return sender!.WhenAny(property1, property2, property3, property4, property5, property6, property7, property8, property9, property10, property11, + (c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11) => + selector(c1.Value, c2.Value, c3.Value, c4.Value, c5.Value, c6.Value, c7.Value, c8.Value, c9.Value, c10.Value, c11.Value)); + } - /// - /// AOT-friendly selector overload using property names. - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyValue( - this TSender? sender, - string property1Name, - string property2Name, - string property3Name, - string property4Name, - string property5Name, - string property6Name, - string property7Name, - string property8Name, - string property9Name, - string property10Name, - string property11Name, - Func selector) - { - var o1 = sender!.ObservableForProperty(property1Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - var o2 = sender!.ObservableForProperty(property2Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - var o3 = sender!.ObservableForProperty(property3Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - var o4 = sender!.ObservableForProperty(property4Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - var o5 = sender!.ObservableForProperty(property5Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - var o6 = sender!.ObservableForProperty(property6Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - var o7 = sender!.ObservableForProperty(property7Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - var o8 = sender!.ObservableForProperty(property8Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - var o9 = sender!.ObservableForProperty(property9Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - var o10 = sender!.ObservableForProperty(property10Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - var o11 = sender!.ObservableForProperty(property11Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - return Observable.CombineLatest( - o1, - o2, - o3, - o4, - o5, - o6, - o7, - o8, - o9, - o10, - o11 - , selector); - } + /// + /// AOT-friendly selector overload using property names. + /// + public static IObservable WhenAnyValue( + this TSender? sender, + string property1Name, + string property2Name, + string property3Name, + string property4Name, + string property5Name, + string property6Name, + string property7Name, + string property8Name, + string property9Name, + string property10Name, + string property11Name, + Func selector) + { + var o1 = sender!.ObservableForProperty(property1Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + var o2 = sender!.ObservableForProperty(property2Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + var o3 = sender!.ObservableForProperty(property3Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + var o4 = sender!.ObservableForProperty(property4Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + var o5 = sender!.ObservableForProperty(property5Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + var o6 = sender!.ObservableForProperty(property6Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + var o7 = sender!.ObservableForProperty(property7Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + var o8 = sender!.ObservableForProperty(property8Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + var o9 = sender!.ObservableForProperty(property9Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + var o10 = sender!.ObservableForProperty(property10Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + var o11 = sender!.ObservableForProperty(property11Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + return Observable.CombineLatest( + o1, + o2, + o3, + o4, + o5, + o6, + o7, + o8, + o9, + o10, + o11 + , selector); + } - /// - /// WhenAnyValue allows you to observe whenever the value of one or more - /// properties on an object have changed, providing an initial value when - /// the Observable is set up, unlike ObservableForProperty(). Use this - /// method in constructors to set up bindings between properties that also - /// need an initial setup. - /// - /// The object where the property chain starts. - /// if set to true [is distinct]. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The 3 property chain to reference. This will be a expression pointing to a end property or field. - /// The 4 property chain to reference. This will be a expression pointing to a end property or field. - /// The 5 property chain to reference. This will be a expression pointing to a end property or field. - /// The 6 property chain to reference. This will be a expression pointing to a end property or field. - /// The 7 property chain to reference. This will be a expression pointing to a end property or field. - /// The 8 property chain to reference. This will be a expression pointing to a end property or field. - /// The 9 property chain to reference. This will be a expression pointing to a end property or field. - /// The 10 property chain to reference. This will be a expression pointing to a end property or field. - /// The 11 property chain to reference. This will be a expression pointing to a end property or field. - /// The selector which will determine the final value from the properties. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyValue( - this TSender? sender, - Expression> property1, - Expression> property2, - Expression> property3, - Expression> property4, - Expression> property5, - Expression> property6, - Expression> property7, - Expression> property8, - Expression> property9, - Expression> property10, - Expression> property11, - Func selector, - bool isDistinct) - { - return sender!.WhenAny(property1, property2, property3, property4, property5, property6, property7, property8, property9, property10, property11, - (c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11) => - selector(c1.Value, c2.Value, c3.Value, c4.Value, c5.Value, c6.Value, c7.Value, c8.Value, c9.Value, c10.Value, c11.Value), - isDistinct); - } + /// + /// WhenAnyValue allows you to observe whenever the value of one or more + /// properties on an object have changed, providing an initial value when + /// the Observable is set up, unlike ObservableForProperty(). Use this + /// method in constructors to set up bindings between properties that also + /// need an initial setup. + /// + /// The object where the property chain starts. + /// if set to true [is distinct]. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The 3 property chain to reference. This will be a expression pointing to a end property or field. + /// The 4 property chain to reference. This will be a expression pointing to a end property or field. + /// The 5 property chain to reference. This will be a expression pointing to a end property or field. + /// The 6 property chain to reference. This will be a expression pointing to a end property or field. + /// The 7 property chain to reference. This will be a expression pointing to a end property or field. + /// The 8 property chain to reference. This will be a expression pointing to a end property or field. + /// The 9 property chain to reference. This will be a expression pointing to a end property or field. + /// The 10 property chain to reference. This will be a expression pointing to a end property or field. + /// The 11 property chain to reference. This will be a expression pointing to a end property or field. + /// The selector which will determine the final value from the properties. + public static IObservable WhenAnyValue( + this TSender? sender, + Expression> property1, + Expression> property2, + Expression> property3, + Expression> property4, + Expression> property5, + Expression> property6, + Expression> property7, + Expression> property8, + Expression> property9, + Expression> property10, + Expression> property11, + Func selector, + bool isDistinct) + { + return sender!.WhenAny(property1, property2, property3, property4, property5, property6, property7, property8, property9, property10, property11, + (c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11) => + selector(c1.Value, c2.Value, c3.Value, c4.Value, c5.Value, c6.Value, c7.Value, c8.Value, c9.Value, c10.Value, c11.Value), + isDistinct); + } - /// - /// AOT-friendly selector overload using property names and distinct option. - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyValue( - this TSender? sender, - string property1Name, - string property2Name, - string property3Name, - string property4Name, - string property5Name, - string property6Name, - string property7Name, - string property8Name, - string property9Name, - string property10Name, - string property11Name, - Func selector, - bool isDistinct) - { - var o1 = sender!.ObservableForProperty(property1Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - var o2 = sender!.ObservableForProperty(property2Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - var o3 = sender!.ObservableForProperty(property3Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - var o4 = sender!.ObservableForProperty(property4Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - var o5 = sender!.ObservableForProperty(property5Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - var o6 = sender!.ObservableForProperty(property6Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - var o7 = sender!.ObservableForProperty(property7Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - var o8 = sender!.ObservableForProperty(property8Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - var o9 = sender!.ObservableForProperty(property9Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - var o10 = sender!.ObservableForProperty(property10Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - var o11 = sender!.ObservableForProperty(property11Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - return Observable.CombineLatest( - o1, - o2, - o3, - o4, - o5, - o6, - o7, - o8, - o9, - o10, - o11 - , selector); - } + /// + /// AOT-friendly selector overload using property names and distinct option. + /// + public static IObservable WhenAnyValue( + this TSender? sender, + string property1Name, + string property2Name, + string property3Name, + string property4Name, + string property5Name, + string property6Name, + string property7Name, + string property8Name, + string property9Name, + string property10Name, + string property11Name, + Func selector, + bool isDistinct) + { + var o1 = sender!.ObservableForProperty(property1Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + var o2 = sender!.ObservableForProperty(property2Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + var o3 = sender!.ObservableForProperty(property3Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + var o4 = sender!.ObservableForProperty(property4Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + var o5 = sender!.ObservableForProperty(property5Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + var o6 = sender!.ObservableForProperty(property6Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + var o7 = sender!.ObservableForProperty(property7Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + var o8 = sender!.ObservableForProperty(property8Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + var o9 = sender!.ObservableForProperty(property9Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + var o10 = sender!.ObservableForProperty(property10Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + var o11 = sender!.ObservableForProperty(property11Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + return Observable.CombineLatest( + o1, + o2, + o3, + o4, + o5, + o6, + o7, + o8, + o9, + o10, + o11 + , selector); + } - /// - /// WhenAny allows you to observe whenever one or more properties on an - /// object have changed, providing an initial value when the Observable - /// is set up, unlike ObservableForProperty(). Use this method in - /// constructors to set up bindings between properties that also need an - /// initial setup. - /// - /// The object where the property chain starts. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The 3 property chain to reference. This will be a expression pointing to a end property or field. - /// The 4 property chain to reference. This will be a expression pointing to a end property or field. - /// The 5 property chain to reference. This will be a expression pointing to a end property or field. - /// The 6 property chain to reference. This will be a expression pointing to a end property or field. - /// The 7 property chain to reference. This will be a expression pointing to a end property or field. - /// The 8 property chain to reference. This will be a expression pointing to a end property or field. - /// The 9 property chain to reference. This will be a expression pointing to a end property or field. - /// The 10 property chain to reference. This will be a expression pointing to a end property or field. - /// The 11 property chain to reference. This will be a expression pointing to a end property or field. - /// The selector which will determine the final value from the properties. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAny uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAny may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAny( - this TSender? sender, - Expression> property1, - Expression> property2, - Expression> property3, - Expression> property4, - Expression> property5, - Expression> property6, - Expression> property7, - Expression> property8, - Expression> property9, - Expression> property10, - Expression> property11, - Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector) - { - return Observable.CombineLatest( - sender!.ObservableForProperty(property1, false, false), - sender!.ObservableForProperty(property2, false, false), - sender!.ObservableForProperty(property3, false, false), - sender!.ObservableForProperty(property4, false, false), - sender!.ObservableForProperty(property5, false, false), - sender!.ObservableForProperty(property6, false, false), - sender!.ObservableForProperty(property7, false, false), - sender!.ObservableForProperty(property8, false, false), - sender!.ObservableForProperty(property9, false, false), - sender!.ObservableForProperty(property10, false, false), - sender!.ObservableForProperty(property11, false, false), - selector - ); - } + /// + /// WhenAny allows you to observe whenever one or more properties on an + /// object have changed, providing an initial value when the Observable + /// is set up, unlike ObservableForProperty(). Use this method in + /// constructors to set up bindings between properties that also need an + /// initial setup. + /// + /// The object where the property chain starts. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The 3 property chain to reference. This will be a expression pointing to a end property or field. + /// The 4 property chain to reference. This will be a expression pointing to a end property or field. + /// The 5 property chain to reference. This will be a expression pointing to a end property or field. + /// The 6 property chain to reference. This will be a expression pointing to a end property or field. + /// The 7 property chain to reference. This will be a expression pointing to a end property or field. + /// The 8 property chain to reference. This will be a expression pointing to a end property or field. + /// The 9 property chain to reference. This will be a expression pointing to a end property or field. + /// The 10 property chain to reference. This will be a expression pointing to a end property or field. + /// The 11 property chain to reference. This will be a expression pointing to a end property or field. + /// The selector which will determine the final value from the properties. + public static IObservable WhenAny( + this TSender? sender, + Expression> property1, + Expression> property2, + Expression> property3, + Expression> property4, + Expression> property5, + Expression> property6, + Expression> property7, + Expression> property8, + Expression> property9, + Expression> property10, + Expression> property11, + Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector) + { + return Observable.CombineLatest( + sender!.ObservableForProperty(property1, false, false), + sender!.ObservableForProperty(property2, false, false), + sender!.ObservableForProperty(property3, false, false), + sender!.ObservableForProperty(property4, false, false), + sender!.ObservableForProperty(property5, false, false), + sender!.ObservableForProperty(property6, false, false), + sender!.ObservableForProperty(property7, false, false), + sender!.ObservableForProperty(property8, false, false), + sender!.ObservableForProperty(property9, false, false), + sender!.ObservableForProperty(property10, false, false), + sender!.ObservableForProperty(property11, false, false), + selector + ); + } - /// - /// AOT-friendly WhenAny overload using property names. - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAny uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAny may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAny( - this TSender? sender, - string property1Name, - string property2Name, - string property3Name, - string property4Name, - string property5Name, - string property6Name, - string property7Name, - string property8Name, - string property9Name, - string property10Name, - string property11Name, - Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector) - { - return Observable.CombineLatest( - sender!.ObservableForProperty(property1Name, false, false), - sender!.ObservableForProperty(property2Name, false, false), - sender!.ObservableForProperty(property3Name, false, false), - sender!.ObservableForProperty(property4Name, false, false), - sender!.ObservableForProperty(property5Name, false, false), - sender!.ObservableForProperty(property6Name, false, false), - sender!.ObservableForProperty(property7Name, false, false), - sender!.ObservableForProperty(property8Name, false, false), - sender!.ObservableForProperty(property9Name, false, false), - sender!.ObservableForProperty(property10Name, false, false), - sender!.ObservableForProperty(property11Name, false, false), - selector - ); - } + /// + /// AOT-friendly WhenAny overload using property names. + /// + public static IObservable WhenAny( + this TSender? sender, + string property1Name, + string property2Name, + string property3Name, + string property4Name, + string property5Name, + string property6Name, + string property7Name, + string property8Name, + string property9Name, + string property10Name, + string property11Name, + Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector) + { + return Observable.CombineLatest( + sender!.ObservableForProperty(property1Name, false, false), + sender!.ObservableForProperty(property2Name, false, false), + sender!.ObservableForProperty(property3Name, false, false), + sender!.ObservableForProperty(property4Name, false, false), + sender!.ObservableForProperty(property5Name, false, false), + sender!.ObservableForProperty(property6Name, false, false), + sender!.ObservableForProperty(property7Name, false, false), + sender!.ObservableForProperty(property8Name, false, false), + sender!.ObservableForProperty(property9Name, false, false), + sender!.ObservableForProperty(property10Name, false, false), + sender!.ObservableForProperty(property11Name, false, false), + selector + ); + } - /// - /// WhenAny allows you to observe whenever one or more properties on an - /// object have changed, providing an initial value when the Observable - /// is set up, unlike ObservableForProperty(). Use this method in - /// constructors to set up bindings between properties that also need an - /// initial setup. - /// - /// The object where the property chain starts. - /// if set to true [is distinct]. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The 3 property chain to reference. This will be a expression pointing to a end property or field. - /// The 4 property chain to reference. This will be a expression pointing to a end property or field. - /// The 5 property chain to reference. This will be a expression pointing to a end property or field. - /// The 6 property chain to reference. This will be a expression pointing to a end property or field. - /// The 7 property chain to reference. This will be a expression pointing to a end property or field. - /// The 8 property chain to reference. This will be a expression pointing to a end property or field. - /// The 9 property chain to reference. This will be a expression pointing to a end property or field. - /// The 10 property chain to reference. This will be a expression pointing to a end property or field. - /// The 11 property chain to reference. This will be a expression pointing to a end property or field. - /// The selector which will determine the final value from the properties. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAny uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAny may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAny( - this TSender? sender, - Expression> property1, - Expression> property2, - Expression> property3, - Expression> property4, - Expression> property5, - Expression> property6, - Expression> property7, - Expression> property8, - Expression> property9, - Expression> property10, - Expression> property11, - Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector, - bool isDistinct) - { - return Observable.CombineLatest( - sender!.ObservableForProperty(property1, false, false, isDistinct), - sender!.ObservableForProperty(property2, false, false, isDistinct), - sender!.ObservableForProperty(property3, false, false, isDistinct), - sender!.ObservableForProperty(property4, false, false, isDistinct), - sender!.ObservableForProperty(property5, false, false, isDistinct), - sender!.ObservableForProperty(property6, false, false, isDistinct), - sender!.ObservableForProperty(property7, false, false, isDistinct), - sender!.ObservableForProperty(property8, false, false, isDistinct), - sender!.ObservableForProperty(property9, false, false, isDistinct), - sender!.ObservableForProperty(property10, false, false, isDistinct), - sender!.ObservableForProperty(property11, false, false, isDistinct), - selector - ); - } + /// + /// WhenAny allows you to observe whenever one or more properties on an + /// object have changed, providing an initial value when the Observable + /// is set up, unlike ObservableForProperty(). Use this method in + /// constructors to set up bindings between properties that also need an + /// initial setup. + /// + /// The object where the property chain starts. + /// if set to true [is distinct]. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The 3 property chain to reference. This will be a expression pointing to a end property or field. + /// The 4 property chain to reference. This will be a expression pointing to a end property or field. + /// The 5 property chain to reference. This will be a expression pointing to a end property or field. + /// The 6 property chain to reference. This will be a expression pointing to a end property or field. + /// The 7 property chain to reference. This will be a expression pointing to a end property or field. + /// The 8 property chain to reference. This will be a expression pointing to a end property or field. + /// The 9 property chain to reference. This will be a expression pointing to a end property or field. + /// The 10 property chain to reference. This will be a expression pointing to a end property or field. + /// The 11 property chain to reference. This will be a expression pointing to a end property or field. + /// The selector which will determine the final value from the properties. + public static IObservable WhenAny( + this TSender? sender, + Expression> property1, + Expression> property2, + Expression> property3, + Expression> property4, + Expression> property5, + Expression> property6, + Expression> property7, + Expression> property8, + Expression> property9, + Expression> property10, + Expression> property11, + Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector, + bool isDistinct) + { + return Observable.CombineLatest( + sender!.ObservableForProperty(property1, false, false, isDistinct), + sender!.ObservableForProperty(property2, false, false, isDistinct), + sender!.ObservableForProperty(property3, false, false, isDistinct), + sender!.ObservableForProperty(property4, false, false, isDistinct), + sender!.ObservableForProperty(property5, false, false, isDistinct), + sender!.ObservableForProperty(property6, false, false, isDistinct), + sender!.ObservableForProperty(property7, false, false, isDistinct), + sender!.ObservableForProperty(property8, false, false, isDistinct), + sender!.ObservableForProperty(property9, false, false, isDistinct), + sender!.ObservableForProperty(property10, false, false, isDistinct), + sender!.ObservableForProperty(property11, false, false, isDistinct), + selector + ); + } - /// - /// AOT-friendly WhenAny overload using property names and distinct option. - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAny uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAny may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAny( - this TSender? sender, - string property1Name, - string property2Name, - string property3Name, - string property4Name, - string property5Name, - string property6Name, - string property7Name, - string property8Name, - string property9Name, - string property10Name, - string property11Name, - Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector, - bool isDistinct) - { - return Observable.CombineLatest( - sender!.ObservableForProperty(property1Name, false, false, isDistinct), - sender!.ObservableForProperty(property2Name, false, false, isDistinct), - sender!.ObservableForProperty(property3Name, false, false, isDistinct), - sender!.ObservableForProperty(property4Name, false, false, isDistinct), - sender!.ObservableForProperty(property5Name, false, false, isDistinct), - sender!.ObservableForProperty(property6Name, false, false, isDistinct), - sender!.ObservableForProperty(property7Name, false, false, isDistinct), - sender!.ObservableForProperty(property8Name, false, false, isDistinct), - sender!.ObservableForProperty(property9Name, false, false, isDistinct), - sender!.ObservableForProperty(property10Name, false, false, isDistinct), - sender!.ObservableForProperty(property11Name, false, false, isDistinct), - selector - ); - } + /// + /// AOT-friendly WhenAny overload using property names and distinct option. + /// + public static IObservable WhenAny( + this TSender? sender, + string property1Name, + string property2Name, + string property3Name, + string property4Name, + string property5Name, + string property6Name, + string property7Name, + string property8Name, + string property9Name, + string property10Name, + string property11Name, + Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector, + bool isDistinct) + { + return Observable.CombineLatest( + sender!.ObservableForProperty(property1Name, false, false, isDistinct), + sender!.ObservableForProperty(property2Name, false, false, isDistinct), + sender!.ObservableForProperty(property3Name, false, false, isDistinct), + sender!.ObservableForProperty(property4Name, false, false, isDistinct), + sender!.ObservableForProperty(property5Name, false, false, isDistinct), + sender!.ObservableForProperty(property6Name, false, false, isDistinct), + sender!.ObservableForProperty(property7Name, false, false, isDistinct), + sender!.ObservableForProperty(property8Name, false, false, isDistinct), + sender!.ObservableForProperty(property9Name, false, false, isDistinct), + sender!.ObservableForProperty(property10Name, false, false, isDistinct), + sender!.ObservableForProperty(property11Name, false, false, isDistinct), + selector + ); + } - /// - /// WhenAny allows you to observe whenever one or more properties on an - /// object have changed, providing an initial value when the Observable - /// is set up, unlike ObservableForProperty(). Use this method in - /// constructors to set up bindings between properties that also need an - /// initial setup. - /// - /// The object where the property chain starts. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The 3 property chain to reference. This will be a expression pointing to a end property or field. - /// The 4 property chain to reference. This will be a expression pointing to a end property or field. - /// The 5 property chain to reference. This will be a expression pointing to a end property or field. - /// The 6 property chain to reference. This will be a expression pointing to a end property or field. - /// The 7 property chain to reference. This will be a expression pointing to a end property or field. - /// The 8 property chain to reference. This will be a expression pointing to a end property or field. - /// The 9 property chain to reference. This will be a expression pointing to a end property or field. - /// The 10 property chain to reference. This will be a expression pointing to a end property or field. - /// The 11 property chain to reference. This will be a expression pointing to a end property or field. - /// The selector which will determine the final value from the properties. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyDynamic uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyDynamic may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyDynamic( - this TSender? sender, - Expression? property1, - Expression? property2, - Expression? property3, - Expression? property4, - Expression? property5, - Expression? property6, - Expression? property7, - Expression? property8, - Expression? property9, - Expression? property10, - Expression? property11, - Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector) - { - return Observable.CombineLatest( - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property1, false, false), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property2, false, false), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property3, false, false), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property4, false, false), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property5, false, false), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property6, false, false), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property7, false, false), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property8, false, false), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property9, false, false), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property10, false, false), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property11, false, false), - selector - ); - } + /// + /// WhenAny allows you to observe whenever one or more properties on an + /// object have changed, providing an initial value when the Observable + /// is set up, unlike ObservableForProperty(). Use this method in + /// constructors to set up bindings between properties that also need an + /// initial setup. + /// + /// The object where the property chain starts. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The 3 property chain to reference. This will be a expression pointing to a end property or field. + /// The 4 property chain to reference. This will be a expression pointing to a end property or field. + /// The 5 property chain to reference. This will be a expression pointing to a end property or field. + /// The 6 property chain to reference. This will be a expression pointing to a end property or field. + /// The 7 property chain to reference. This will be a expression pointing to a end property or field. + /// The 8 property chain to reference. This will be a expression pointing to a end property or field. + /// The 9 property chain to reference. This will be a expression pointing to a end property or field. + /// The 10 property chain to reference. This will be a expression pointing to a end property or field. + /// The 11 property chain to reference. This will be a expression pointing to a end property or field. + /// The selector which will determine the final value from the properties. + public static IObservable WhenAnyDynamic( + this TSender? sender, + Expression? property1, + Expression? property2, + Expression? property3, + Expression? property4, + Expression? property5, + Expression? property6, + Expression? property7, + Expression? property8, + Expression? property9, + Expression? property10, + Expression? property11, + Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector) + { + return Observable.CombineLatest( + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property1, false, false), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property2, false, false), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property3, false, false), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property4, false, false), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property5, false, false), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property6, false, false), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property7, false, false), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property8, false, false), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property9, false, false), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property10, false, false), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property11, false, false), + selector + ); + } - /// - /// WhenAny allows you to observe whenever one or more properties on an - /// object have changed, providing an initial value when the Observable - /// is set up, unlike ObservableForProperty(). Use this method in - /// constructors to set up bindings between properties that also need an - /// initial setup. - /// - /// The object where the property chain starts. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The 3 property chain to reference. This will be a expression pointing to a end property or field. - /// The 4 property chain to reference. This will be a expression pointing to a end property or field. - /// The 5 property chain to reference. This will be a expression pointing to a end property or field. - /// The 6 property chain to reference. This will be a expression pointing to a end property or field. - /// The 7 property chain to reference. This will be a expression pointing to a end property or field. - /// The 8 property chain to reference. This will be a expression pointing to a end property or field. - /// The 9 property chain to reference. This will be a expression pointing to a end property or field. - /// The 10 property chain to reference. This will be a expression pointing to a end property or field. - /// The 11 property chain to reference. This will be a expression pointing to a end property or field. - /// The selector which will determine the final value from the properties. - /// if set to true [is distinct]. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyDynamic uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyDynamic may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyDynamic( - this TSender? sender, - Expression? property1, - Expression? property2, - Expression? property3, - Expression? property4, - Expression? property5, - Expression? property6, - Expression? property7, - Expression? property8, - Expression? property9, - Expression? property10, - Expression? property11, - Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector, - bool isDistinct) - { - return Observable.CombineLatest( - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property1, false, false, isDistinct), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property2, false, false, isDistinct), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property3, false, false, isDistinct), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property4, false, false, isDistinct), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property5, false, false, isDistinct), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property6, false, false, isDistinct), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property7, false, false, isDistinct), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property8, false, false, isDistinct), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property9, false, false, isDistinct), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property10, false, false, isDistinct), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property11, false, false, isDistinct), - selector - ); - } + /// + /// WhenAny allows you to observe whenever one or more properties on an + /// object have changed, providing an initial value when the Observable + /// is set up, unlike ObservableForProperty(). Use this method in + /// constructors to set up bindings between properties that also need an + /// initial setup. + /// + /// The object where the property chain starts. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The 3 property chain to reference. This will be a expression pointing to a end property or field. + /// The 4 property chain to reference. This will be a expression pointing to a end property or field. + /// The 5 property chain to reference. This will be a expression pointing to a end property or field. + /// The 6 property chain to reference. This will be a expression pointing to a end property or field. + /// The 7 property chain to reference. This will be a expression pointing to a end property or field. + /// The 8 property chain to reference. This will be a expression pointing to a end property or field. + /// The 9 property chain to reference. This will be a expression pointing to a end property or field. + /// The 10 property chain to reference. This will be a expression pointing to a end property or field. + /// The 11 property chain to reference. This will be a expression pointing to a end property or field. + /// The selector which will determine the final value from the properties. + /// if set to true [is distinct]. + public static IObservable WhenAnyDynamic( + this TSender? sender, + Expression? property1, + Expression? property2, + Expression? property3, + Expression? property4, + Expression? property5, + Expression? property6, + Expression? property7, + Expression? property8, + Expression? property9, + Expression? property10, + Expression? property11, + Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector, + bool isDistinct) + { + return Observable.CombineLatest( + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property1, false, false, isDistinct), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property2, false, false, isDistinct), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property3, false, false, isDistinct), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property4, false, false, isDistinct), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property5, false, false, isDistinct), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property6, false, false, isDistinct), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property7, false, false, isDistinct), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property8, false, false, isDistinct), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property9, false, false, isDistinct), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property10, false, false, isDistinct), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property11, false, false, isDistinct), + selector + ); + } - /// - /// WhenAnyValue allows you to observe whenever the value of one or more - /// properties on an object have changed, providing an initial value when - /// the Observable is set up, unlike ObservableForProperty(). Use this - /// method in constructors to set up bindings between properties that also - /// need an initial setup. - /// - /// The object where the property chain starts. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The 3 property chain to reference. This will be a expression pointing to a end property or field. - /// The 4 property chain to reference. This will be a expression pointing to a end property or field. - /// The 5 property chain to reference. This will be a expression pointing to a end property or field. - /// The 6 property chain to reference. This will be a expression pointing to a end property or field. - /// The 7 property chain to reference. This will be a expression pointing to a end property or field. - /// The 8 property chain to reference. This will be a expression pointing to a end property or field. - /// The 9 property chain to reference. This will be a expression pointing to a end property or field. - /// The 10 property chain to reference. This will be a expression pointing to a end property or field. - /// The 11 property chain to reference. This will be a expression pointing to a end property or field. - /// The 12 property chain to reference. This will be a expression pointing to a end property or field. - /// The selector which will determine the final value from the properties. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyValue( - this TSender? sender, - Expression> property1, - Expression> property2, - Expression> property3, - Expression> property4, - Expression> property5, - Expression> property6, - Expression> property7, - Expression> property8, - Expression> property9, - Expression> property10, - Expression> property11, - Expression> property12, - Func selector) - { - return sender!.WhenAny(property1, property2, property3, property4, property5, property6, property7, property8, property9, property10, property11, property12, - (c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12) => - selector(c1.Value, c2.Value, c3.Value, c4.Value, c5.Value, c6.Value, c7.Value, c8.Value, c9.Value, c10.Value, c11.Value, c12.Value)); - } + /// + /// WhenAnyValue allows you to observe whenever the value of one or more + /// properties on an object have changed, providing an initial value when + /// the Observable is set up, unlike ObservableForProperty(). Use this + /// method in constructors to set up bindings between properties that also + /// need an initial setup. + /// + /// The object where the property chain starts. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The 3 property chain to reference. This will be a expression pointing to a end property or field. + /// The 4 property chain to reference. This will be a expression pointing to a end property or field. + /// The 5 property chain to reference. This will be a expression pointing to a end property or field. + /// The 6 property chain to reference. This will be a expression pointing to a end property or field. + /// The 7 property chain to reference. This will be a expression pointing to a end property or field. + /// The 8 property chain to reference. This will be a expression pointing to a end property or field. + /// The 9 property chain to reference. This will be a expression pointing to a end property or field. + /// The 10 property chain to reference. This will be a expression pointing to a end property or field. + /// The 11 property chain to reference. This will be a expression pointing to a end property or field. + /// The 12 property chain to reference. This will be a expression pointing to a end property or field. + /// The selector which will determine the final value from the properties. + public static IObservable WhenAnyValue( + this TSender? sender, + Expression> property1, + Expression> property2, + Expression> property3, + Expression> property4, + Expression> property5, + Expression> property6, + Expression> property7, + Expression> property8, + Expression> property9, + Expression> property10, + Expression> property11, + Expression> property12, + Func selector) + { + return sender!.WhenAny(property1, property2, property3, property4, property5, property6, property7, property8, property9, property10, property11, property12, + (c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12) => + selector(c1.Value, c2.Value, c3.Value, c4.Value, c5.Value, c6.Value, c7.Value, c8.Value, c9.Value, c10.Value, c11.Value, c12.Value)); + } - /// - /// AOT-friendly selector overload using property names. - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyValue( - this TSender? sender, - string property1Name, - string property2Name, - string property3Name, - string property4Name, - string property5Name, - string property6Name, - string property7Name, - string property8Name, - string property9Name, - string property10Name, - string property11Name, - string property12Name, - Func selector) - { - var o1 = sender!.ObservableForProperty(property1Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - var o2 = sender!.ObservableForProperty(property2Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - var o3 = sender!.ObservableForProperty(property3Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - var o4 = sender!.ObservableForProperty(property4Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - var o5 = sender!.ObservableForProperty(property5Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - var o6 = sender!.ObservableForProperty(property6Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - var o7 = sender!.ObservableForProperty(property7Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - var o8 = sender!.ObservableForProperty(property8Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - var o9 = sender!.ObservableForProperty(property9Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - var o10 = sender!.ObservableForProperty(property10Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - var o11 = sender!.ObservableForProperty(property11Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - var o12 = sender!.ObservableForProperty(property12Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); - return Observable.CombineLatest( - o1, - o2, - o3, - o4, - o5, - o6, - o7, - o8, - o9, - o10, - o11, - o12 - , selector); - } + /// + /// AOT-friendly selector overload using property names. + /// + public static IObservable WhenAnyValue( + this TSender? sender, + string property1Name, + string property2Name, + string property3Name, + string property4Name, + string property5Name, + string property6Name, + string property7Name, + string property8Name, + string property9Name, + string property10Name, + string property11Name, + string property12Name, + Func selector) + { + var o1 = sender!.ObservableForProperty(property1Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + var o2 = sender!.ObservableForProperty(property2Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + var o3 = sender!.ObservableForProperty(property3Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + var o4 = sender!.ObservableForProperty(property4Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + var o5 = sender!.ObservableForProperty(property5Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + var o6 = sender!.ObservableForProperty(property6Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + var o7 = sender!.ObservableForProperty(property7Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + var o8 = sender!.ObservableForProperty(property8Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + var o9 = sender!.ObservableForProperty(property9Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + var o10 = sender!.ObservableForProperty(property10Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + var o11 = sender!.ObservableForProperty(property11Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + var o12 = sender!.ObservableForProperty(property12Name, beforeChange: false, skipInitial: false, isDistinct: true).Select(x => x.Value); + return Observable.CombineLatest( + o1, + o2, + o3, + o4, + o5, + o6, + o7, + o8, + o9, + o10, + o11, + o12 + , selector); + } - /// - /// WhenAnyValue allows you to observe whenever the value of one or more - /// properties on an object have changed, providing an initial value when - /// the Observable is set up, unlike ObservableForProperty(). Use this - /// method in constructors to set up bindings between properties that also - /// need an initial setup. - /// - /// The object where the property chain starts. - /// if set to true [is distinct]. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The 3 property chain to reference. This will be a expression pointing to a end property or field. - /// The 4 property chain to reference. This will be a expression pointing to a end property or field. - /// The 5 property chain to reference. This will be a expression pointing to a end property or field. - /// The 6 property chain to reference. This will be a expression pointing to a end property or field. - /// The 7 property chain to reference. This will be a expression pointing to a end property or field. - /// The 8 property chain to reference. This will be a expression pointing to a end property or field. - /// The 9 property chain to reference. This will be a expression pointing to a end property or field. - /// The 10 property chain to reference. This will be a expression pointing to a end property or field. - /// The 11 property chain to reference. This will be a expression pointing to a end property or field. - /// The 12 property chain to reference. This will be a expression pointing to a end property or field. - /// The selector which will determine the final value from the properties. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyValue( - this TSender? sender, - Expression> property1, - Expression> property2, - Expression> property3, - Expression> property4, - Expression> property5, - Expression> property6, - Expression> property7, - Expression> property8, - Expression> property9, - Expression> property10, - Expression> property11, - Expression> property12, - Func selector, - bool isDistinct) - { - return sender!.WhenAny(property1, property2, property3, property4, property5, property6, property7, property8, property9, property10, property11, property12, - (c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12) => - selector(c1.Value, c2.Value, c3.Value, c4.Value, c5.Value, c6.Value, c7.Value, c8.Value, c9.Value, c10.Value, c11.Value, c12.Value), - isDistinct); - } + /// + /// WhenAnyValue allows you to observe whenever the value of one or more + /// properties on an object have changed, providing an initial value when + /// the Observable is set up, unlike ObservableForProperty(). Use this + /// method in constructors to set up bindings between properties that also + /// need an initial setup. + /// + /// The object where the property chain starts. + /// if set to true [is distinct]. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The 3 property chain to reference. This will be a expression pointing to a end property or field. + /// The 4 property chain to reference. This will be a expression pointing to a end property or field. + /// The 5 property chain to reference. This will be a expression pointing to a end property or field. + /// The 6 property chain to reference. This will be a expression pointing to a end property or field. + /// The 7 property chain to reference. This will be a expression pointing to a end property or field. + /// The 8 property chain to reference. This will be a expression pointing to a end property or field. + /// The 9 property chain to reference. This will be a expression pointing to a end property or field. + /// The 10 property chain to reference. This will be a expression pointing to a end property or field. + /// The 11 property chain to reference. This will be a expression pointing to a end property or field. + /// The 12 property chain to reference. This will be a expression pointing to a end property or field. + /// The selector which will determine the final value from the properties. + public static IObservable WhenAnyValue( + this TSender? sender, + Expression> property1, + Expression> property2, + Expression> property3, + Expression> property4, + Expression> property5, + Expression> property6, + Expression> property7, + Expression> property8, + Expression> property9, + Expression> property10, + Expression> property11, + Expression> property12, + Func selector, + bool isDistinct) + { + return sender!.WhenAny(property1, property2, property3, property4, property5, property6, property7, property8, property9, property10, property11, property12, + (c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12) => + selector(c1.Value, c2.Value, c3.Value, c4.Value, c5.Value, c6.Value, c7.Value, c8.Value, c9.Value, c10.Value, c11.Value, c12.Value), + isDistinct); + } - /// - /// AOT-friendly selector overload using property names and distinct option. - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyValue( - this TSender? sender, - string property1Name, - string property2Name, - string property3Name, - string property4Name, - string property5Name, - string property6Name, - string property7Name, - string property8Name, - string property9Name, - string property10Name, - string property11Name, - string property12Name, - Func selector, - bool isDistinct) - { - var o1 = sender!.ObservableForProperty(property1Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - var o2 = sender!.ObservableForProperty(property2Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - var o3 = sender!.ObservableForProperty(property3Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - var o4 = sender!.ObservableForProperty(property4Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - var o5 = sender!.ObservableForProperty(property5Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - var o6 = sender!.ObservableForProperty(property6Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - var o7 = sender!.ObservableForProperty(property7Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - var o8 = sender!.ObservableForProperty(property8Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - var o9 = sender!.ObservableForProperty(property9Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - var o10 = sender!.ObservableForProperty(property10Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - var o11 = sender!.ObservableForProperty(property11Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - var o12 = sender!.ObservableForProperty(property12Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); - return Observable.CombineLatest( - o1, - o2, - o3, - o4, - o5, - o6, - o7, - o8, - o9, - o10, - o11, - o12 - , selector); - } + /// + /// AOT-friendly selector overload using property names and distinct option. + /// + public static IObservable WhenAnyValue( + this TSender? sender, + string property1Name, + string property2Name, + string property3Name, + string property4Name, + string property5Name, + string property6Name, + string property7Name, + string property8Name, + string property9Name, + string property10Name, + string property11Name, + string property12Name, + Func selector, + bool isDistinct) + { + var o1 = sender!.ObservableForProperty(property1Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + var o2 = sender!.ObservableForProperty(property2Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + var o3 = sender!.ObservableForProperty(property3Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + var o4 = sender!.ObservableForProperty(property4Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + var o5 = sender!.ObservableForProperty(property5Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + var o6 = sender!.ObservableForProperty(property6Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + var o7 = sender!.ObservableForProperty(property7Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + var o8 = sender!.ObservableForProperty(property8Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + var o9 = sender!.ObservableForProperty(property9Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + var o10 = sender!.ObservableForProperty(property10Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + var o11 = sender!.ObservableForProperty(property11Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + var o12 = sender!.ObservableForProperty(property12Name, beforeChange: false, skipInitial: false, isDistinct: isDistinct).Select(x => x.Value); + return Observable.CombineLatest( + o1, + o2, + o3, + o4, + o5, + o6, + o7, + o8, + o9, + o10, + o11, + o12 + , selector); + } - /// - /// WhenAny allows you to observe whenever one or more properties on an - /// object have changed, providing an initial value when the Observable - /// is set up, unlike ObservableForProperty(). Use this method in - /// constructors to set up bindings between properties that also need an - /// initial setup. - /// - /// The object where the property chain starts. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The 3 property chain to reference. This will be a expression pointing to a end property or field. - /// The 4 property chain to reference. This will be a expression pointing to a end property or field. - /// The 5 property chain to reference. This will be a expression pointing to a end property or field. - /// The 6 property chain to reference. This will be a expression pointing to a end property or field. - /// The 7 property chain to reference. This will be a expression pointing to a end property or field. - /// The 8 property chain to reference. This will be a expression pointing to a end property or field. - /// The 9 property chain to reference. This will be a expression pointing to a end property or field. - /// The 10 property chain to reference. This will be a expression pointing to a end property or field. - /// The 11 property chain to reference. This will be a expression pointing to a end property or field. - /// The 12 property chain to reference. This will be a expression pointing to a end property or field. - /// The selector which will determine the final value from the properties. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAny uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAny may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAny( - this TSender? sender, - Expression> property1, - Expression> property2, - Expression> property3, - Expression> property4, - Expression> property5, - Expression> property6, - Expression> property7, - Expression> property8, - Expression> property9, - Expression> property10, - Expression> property11, - Expression> property12, - Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector) - { - return Observable.CombineLatest( - sender!.ObservableForProperty(property1, false, false), - sender!.ObservableForProperty(property2, false, false), - sender!.ObservableForProperty(property3, false, false), - sender!.ObservableForProperty(property4, false, false), - sender!.ObservableForProperty(property5, false, false), - sender!.ObservableForProperty(property6, false, false), - sender!.ObservableForProperty(property7, false, false), - sender!.ObservableForProperty(property8, false, false), - sender!.ObservableForProperty(property9, false, false), - sender!.ObservableForProperty(property10, false, false), - sender!.ObservableForProperty(property11, false, false), - sender!.ObservableForProperty(property12, false, false), - selector - ); - } + /// + /// WhenAny allows you to observe whenever one or more properties on an + /// object have changed, providing an initial value when the Observable + /// is set up, unlike ObservableForProperty(). Use this method in + /// constructors to set up bindings between properties that also need an + /// initial setup. + /// + /// The object where the property chain starts. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The 3 property chain to reference. This will be a expression pointing to a end property or field. + /// The 4 property chain to reference. This will be a expression pointing to a end property or field. + /// The 5 property chain to reference. This will be a expression pointing to a end property or field. + /// The 6 property chain to reference. This will be a expression pointing to a end property or field. + /// The 7 property chain to reference. This will be a expression pointing to a end property or field. + /// The 8 property chain to reference. This will be a expression pointing to a end property or field. + /// The 9 property chain to reference. This will be a expression pointing to a end property or field. + /// The 10 property chain to reference. This will be a expression pointing to a end property or field. + /// The 11 property chain to reference. This will be a expression pointing to a end property or field. + /// The 12 property chain to reference. This will be a expression pointing to a end property or field. + /// The selector which will determine the final value from the properties. + public static IObservable WhenAny( + this TSender? sender, + Expression> property1, + Expression> property2, + Expression> property3, + Expression> property4, + Expression> property5, + Expression> property6, + Expression> property7, + Expression> property8, + Expression> property9, + Expression> property10, + Expression> property11, + Expression> property12, + Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector) + { + return Observable.CombineLatest( + sender!.ObservableForProperty(property1, false, false), + sender!.ObservableForProperty(property2, false, false), + sender!.ObservableForProperty(property3, false, false), + sender!.ObservableForProperty(property4, false, false), + sender!.ObservableForProperty(property5, false, false), + sender!.ObservableForProperty(property6, false, false), + sender!.ObservableForProperty(property7, false, false), + sender!.ObservableForProperty(property8, false, false), + sender!.ObservableForProperty(property9, false, false), + sender!.ObservableForProperty(property10, false, false), + sender!.ObservableForProperty(property11, false, false), + sender!.ObservableForProperty(property12, false, false), + selector + ); + } - /// - /// AOT-friendly WhenAny overload using property names. - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAny uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAny may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAny( - this TSender? sender, - string property1Name, - string property2Name, - string property3Name, - string property4Name, - string property5Name, - string property6Name, - string property7Name, - string property8Name, - string property9Name, - string property10Name, - string property11Name, - string property12Name, - Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector) - { - return Observable.CombineLatest( - sender!.ObservableForProperty(property1Name, false, false), - sender!.ObservableForProperty(property2Name, false, false), - sender!.ObservableForProperty(property3Name, false, false), - sender!.ObservableForProperty(property4Name, false, false), - sender!.ObservableForProperty(property5Name, false, false), - sender!.ObservableForProperty(property6Name, false, false), - sender!.ObservableForProperty(property7Name, false, false), - sender!.ObservableForProperty(property8Name, false, false), - sender!.ObservableForProperty(property9Name, false, false), - sender!.ObservableForProperty(property10Name, false, false), - sender!.ObservableForProperty(property11Name, false, false), - sender!.ObservableForProperty(property12Name, false, false), - selector - ); - } + /// + /// AOT-friendly WhenAny overload using property names. + /// + public static IObservable WhenAny( + this TSender? sender, + string property1Name, + string property2Name, + string property3Name, + string property4Name, + string property5Name, + string property6Name, + string property7Name, + string property8Name, + string property9Name, + string property10Name, + string property11Name, + string property12Name, + Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector) + { + return Observable.CombineLatest( + sender!.ObservableForProperty(property1Name, false, false), + sender!.ObservableForProperty(property2Name, false, false), + sender!.ObservableForProperty(property3Name, false, false), + sender!.ObservableForProperty(property4Name, false, false), + sender!.ObservableForProperty(property5Name, false, false), + sender!.ObservableForProperty(property6Name, false, false), + sender!.ObservableForProperty(property7Name, false, false), + sender!.ObservableForProperty(property8Name, false, false), + sender!.ObservableForProperty(property9Name, false, false), + sender!.ObservableForProperty(property10Name, false, false), + sender!.ObservableForProperty(property11Name, false, false), + sender!.ObservableForProperty(property12Name, false, false), + selector + ); + } - /// - /// WhenAny allows you to observe whenever one or more properties on an - /// object have changed, providing an initial value when the Observable - /// is set up, unlike ObservableForProperty(). Use this method in - /// constructors to set up bindings between properties that also need an - /// initial setup. - /// - /// The object where the property chain starts. - /// if set to true [is distinct]. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The 3 property chain to reference. This will be a expression pointing to a end property or field. - /// The 4 property chain to reference. This will be a expression pointing to a end property or field. - /// The 5 property chain to reference. This will be a expression pointing to a end property or field. - /// The 6 property chain to reference. This will be a expression pointing to a end property or field. - /// The 7 property chain to reference. This will be a expression pointing to a end property or field. - /// The 8 property chain to reference. This will be a expression pointing to a end property or field. - /// The 9 property chain to reference. This will be a expression pointing to a end property or field. - /// The 10 property chain to reference. This will be a expression pointing to a end property or field. - /// The 11 property chain to reference. This will be a expression pointing to a end property or field. - /// The 12 property chain to reference. This will be a expression pointing to a end property or field. - /// The selector which will determine the final value from the properties. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAny uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAny may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAny( - this TSender? sender, - Expression> property1, - Expression> property2, - Expression> property3, - Expression> property4, - Expression> property5, - Expression> property6, - Expression> property7, - Expression> property8, - Expression> property9, - Expression> property10, - Expression> property11, - Expression> property12, - Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector, - bool isDistinct) - { - return Observable.CombineLatest( - sender!.ObservableForProperty(property1, false, false, isDistinct), - sender!.ObservableForProperty(property2, false, false, isDistinct), - sender!.ObservableForProperty(property3, false, false, isDistinct), - sender!.ObservableForProperty(property4, false, false, isDistinct), - sender!.ObservableForProperty(property5, false, false, isDistinct), - sender!.ObservableForProperty(property6, false, false, isDistinct), - sender!.ObservableForProperty(property7, false, false, isDistinct), - sender!.ObservableForProperty(property8, false, false, isDistinct), - sender!.ObservableForProperty(property9, false, false, isDistinct), - sender!.ObservableForProperty(property10, false, false, isDistinct), - sender!.ObservableForProperty(property11, false, false, isDistinct), - sender!.ObservableForProperty(property12, false, false, isDistinct), - selector - ); - } + /// + /// WhenAny allows you to observe whenever one or more properties on an + /// object have changed, providing an initial value when the Observable + /// is set up, unlike ObservableForProperty(). Use this method in + /// constructors to set up bindings between properties that also need an + /// initial setup. + /// + /// The object where the property chain starts. + /// if set to true [is distinct]. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The 3 property chain to reference. This will be a expression pointing to a end property or field. + /// The 4 property chain to reference. This will be a expression pointing to a end property or field. + /// The 5 property chain to reference. This will be a expression pointing to a end property or field. + /// The 6 property chain to reference. This will be a expression pointing to a end property or field. + /// The 7 property chain to reference. This will be a expression pointing to a end property or field. + /// The 8 property chain to reference. This will be a expression pointing to a end property or field. + /// The 9 property chain to reference. This will be a expression pointing to a end property or field. + /// The 10 property chain to reference. This will be a expression pointing to a end property or field. + /// The 11 property chain to reference. This will be a expression pointing to a end property or field. + /// The 12 property chain to reference. This will be a expression pointing to a end property or field. + /// The selector which will determine the final value from the properties. + public static IObservable WhenAny( + this TSender? sender, + Expression> property1, + Expression> property2, + Expression> property3, + Expression> property4, + Expression> property5, + Expression> property6, + Expression> property7, + Expression> property8, + Expression> property9, + Expression> property10, + Expression> property11, + Expression> property12, + Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector, + bool isDistinct) + { + return Observable.CombineLatest( + sender!.ObservableForProperty(property1, false, false, isDistinct), + sender!.ObservableForProperty(property2, false, false, isDistinct), + sender!.ObservableForProperty(property3, false, false, isDistinct), + sender!.ObservableForProperty(property4, false, false, isDistinct), + sender!.ObservableForProperty(property5, false, false, isDistinct), + sender!.ObservableForProperty(property6, false, false, isDistinct), + sender!.ObservableForProperty(property7, false, false, isDistinct), + sender!.ObservableForProperty(property8, false, false, isDistinct), + sender!.ObservableForProperty(property9, false, false, isDistinct), + sender!.ObservableForProperty(property10, false, false, isDistinct), + sender!.ObservableForProperty(property11, false, false, isDistinct), + sender!.ObservableForProperty(property12, false, false, isDistinct), + selector + ); + } - /// - /// AOT-friendly WhenAny overload using property names and distinct option. - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAny uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAny may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAny( - this TSender? sender, - string property1Name, - string property2Name, - string property3Name, - string property4Name, - string property5Name, - string property6Name, - string property7Name, - string property8Name, - string property9Name, - string property10Name, - string property11Name, - string property12Name, - Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector, - bool isDistinct) - { - return Observable.CombineLatest( - sender!.ObservableForProperty(property1Name, false, false, isDistinct), - sender!.ObservableForProperty(property2Name, false, false, isDistinct), - sender!.ObservableForProperty(property3Name, false, false, isDistinct), - sender!.ObservableForProperty(property4Name, false, false, isDistinct), - sender!.ObservableForProperty(property5Name, false, false, isDistinct), - sender!.ObservableForProperty(property6Name, false, false, isDistinct), - sender!.ObservableForProperty(property7Name, false, false, isDistinct), - sender!.ObservableForProperty(property8Name, false, false, isDistinct), - sender!.ObservableForProperty(property9Name, false, false, isDistinct), - sender!.ObservableForProperty(property10Name, false, false, isDistinct), - sender!.ObservableForProperty(property11Name, false, false, isDistinct), - sender!.ObservableForProperty(property12Name, false, false, isDistinct), - selector - ); - } + /// + /// AOT-friendly WhenAny overload using property names and distinct option. + /// + public static IObservable WhenAny( + this TSender? sender, + string property1Name, + string property2Name, + string property3Name, + string property4Name, + string property5Name, + string property6Name, + string property7Name, + string property8Name, + string property9Name, + string property10Name, + string property11Name, + string property12Name, + Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector, + bool isDistinct) + { + return Observable.CombineLatest( + sender!.ObservableForProperty(property1Name, false, false, isDistinct), + sender!.ObservableForProperty(property2Name, false, false, isDistinct), + sender!.ObservableForProperty(property3Name, false, false, isDistinct), + sender!.ObservableForProperty(property4Name, false, false, isDistinct), + sender!.ObservableForProperty(property5Name, false, false, isDistinct), + sender!.ObservableForProperty(property6Name, false, false, isDistinct), + sender!.ObservableForProperty(property7Name, false, false, isDistinct), + sender!.ObservableForProperty(property8Name, false, false, isDistinct), + sender!.ObservableForProperty(property9Name, false, false, isDistinct), + sender!.ObservableForProperty(property10Name, false, false, isDistinct), + sender!.ObservableForProperty(property11Name, false, false, isDistinct), + sender!.ObservableForProperty(property12Name, false, false, isDistinct), + selector + ); + } - /// - /// WhenAny allows you to observe whenever one or more properties on an - /// object have changed, providing an initial value when the Observable - /// is set up, unlike ObservableForProperty(). Use this method in - /// constructors to set up bindings between properties that also need an - /// initial setup. - /// - /// The object where the property chain starts. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The 3 property chain to reference. This will be a expression pointing to a end property or field. - /// The 4 property chain to reference. This will be a expression pointing to a end property or field. - /// The 5 property chain to reference. This will be a expression pointing to a end property or field. - /// The 6 property chain to reference. This will be a expression pointing to a end property or field. - /// The 7 property chain to reference. This will be a expression pointing to a end property or field. - /// The 8 property chain to reference. This will be a expression pointing to a end property or field. - /// The 9 property chain to reference. This will be a expression pointing to a end property or field. - /// The 10 property chain to reference. This will be a expression pointing to a end property or field. - /// The 11 property chain to reference. This will be a expression pointing to a end property or field. - /// The 12 property chain to reference. This will be a expression pointing to a end property or field. - /// The selector which will determine the final value from the properties. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyDynamic uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyDynamic may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyDynamic( - this TSender? sender, - Expression? property1, - Expression? property2, - Expression? property3, - Expression? property4, - Expression? property5, - Expression? property6, - Expression? property7, - Expression? property8, - Expression? property9, - Expression? property10, - Expression? property11, - Expression? property12, - Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector) - { - return Observable.CombineLatest( - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property1, false, false), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property2, false, false), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property3, false, false), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property4, false, false), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property5, false, false), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property6, false, false), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property7, false, false), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property8, false, false), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property9, false, false), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property10, false, false), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property11, false, false), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property12, false, false), - selector - ); - } + /// + /// WhenAny allows you to observe whenever one or more properties on an + /// object have changed, providing an initial value when the Observable + /// is set up, unlike ObservableForProperty(). Use this method in + /// constructors to set up bindings between properties that also need an + /// initial setup. + /// + /// The object where the property chain starts. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The 3 property chain to reference. This will be a expression pointing to a end property or field. + /// The 4 property chain to reference. This will be a expression pointing to a end property or field. + /// The 5 property chain to reference. This will be a expression pointing to a end property or field. + /// The 6 property chain to reference. This will be a expression pointing to a end property or field. + /// The 7 property chain to reference. This will be a expression pointing to a end property or field. + /// The 8 property chain to reference. This will be a expression pointing to a end property or field. + /// The 9 property chain to reference. This will be a expression pointing to a end property or field. + /// The 10 property chain to reference. This will be a expression pointing to a end property or field. + /// The 11 property chain to reference. This will be a expression pointing to a end property or field. + /// The 12 property chain to reference. This will be a expression pointing to a end property or field. + /// The selector which will determine the final value from the properties. + public static IObservable WhenAnyDynamic( + this TSender? sender, + Expression? property1, + Expression? property2, + Expression? property3, + Expression? property4, + Expression? property5, + Expression? property6, + Expression? property7, + Expression? property8, + Expression? property9, + Expression? property10, + Expression? property11, + Expression? property12, + Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector) + { + return Observable.CombineLatest( + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property1, false, false), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property2, false, false), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property3, false, false), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property4, false, false), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property5, false, false), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property6, false, false), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property7, false, false), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property8, false, false), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property9, false, false), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property10, false, false), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property11, false, false), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property12, false, false), + selector + ); + } - /// - /// WhenAny allows you to observe whenever one or more properties on an - /// object have changed, providing an initial value when the Observable - /// is set up, unlike ObservableForProperty(). Use this method in - /// constructors to set up bindings between properties that also need an - /// initial setup. - /// - /// The object where the property chain starts. - /// The 1 property chain to reference. This will be a expression pointing to a end property or field. - /// The 2 property chain to reference. This will be a expression pointing to a end property or field. - /// The 3 property chain to reference. This will be a expression pointing to a end property or field. - /// The 4 property chain to reference. This will be a expression pointing to a end property or field. - /// The 5 property chain to reference. This will be a expression pointing to a end property or field. - /// The 6 property chain to reference. This will be a expression pointing to a end property or field. - /// The 7 property chain to reference. This will be a expression pointing to a end property or field. - /// The 8 property chain to reference. This will be a expression pointing to a end property or field. - /// The 9 property chain to reference. This will be a expression pointing to a end property or field. - /// The 10 property chain to reference. This will be a expression pointing to a end property or field. - /// The 11 property chain to reference. This will be a expression pointing to a end property or field. - /// The 12 property chain to reference. This will be a expression pointing to a end property or field. - /// The selector which will determine the final value from the properties. - /// if set to true [is distinct]. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyDynamic uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyDynamic may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyDynamic( - this TSender? sender, - Expression? property1, - Expression? property2, - Expression? property3, - Expression? property4, - Expression? property5, - Expression? property6, - Expression? property7, - Expression? property8, - Expression? property9, - Expression? property10, - Expression? property11, - Expression? property12, - Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector, - bool isDistinct) - { - return Observable.CombineLatest( - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property1, false, false, isDistinct), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property2, false, false, isDistinct), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property3, false, false, isDistinct), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property4, false, false, isDistinct), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property5, false, false, isDistinct), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property6, false, false, isDistinct), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property7, false, false, isDistinct), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property8, false, false, isDistinct), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property9, false, false, isDistinct), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property10, false, false, isDistinct), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property11, false, false, isDistinct), - ReactiveNotifyPropertyChangedMixin - .SubscribeToExpressionChain(sender, property12, false, false, isDistinct), - selector - ); - } -} + /// + /// WhenAny allows you to observe whenever one or more properties on an + /// object have changed, providing an initial value when the Observable + /// is set up, unlike ObservableForProperty(). Use this method in + /// constructors to set up bindings between properties that also need an + /// initial setup. + /// + /// The object where the property chain starts. + /// The 1 property chain to reference. This will be a expression pointing to a end property or field. + /// The 2 property chain to reference. This will be a expression pointing to a end property or field. + /// The 3 property chain to reference. This will be a expression pointing to a end property or field. + /// The 4 property chain to reference. This will be a expression pointing to a end property or field. + /// The 5 property chain to reference. This will be a expression pointing to a end property or field. + /// The 6 property chain to reference. This will be a expression pointing to a end property or field. + /// The 7 property chain to reference. This will be a expression pointing to a end property or field. + /// The 8 property chain to reference. This will be a expression pointing to a end property or field. + /// The 9 property chain to reference. This will be a expression pointing to a end property or field. + /// The 10 property chain to reference. This will be a expression pointing to a end property or field. + /// The 11 property chain to reference. This will be a expression pointing to a end property or field. + /// The 12 property chain to reference. This will be a expression pointing to a end property or field. + /// The selector which will determine the final value from the properties. + /// if set to true [is distinct]. + public static IObservable WhenAnyDynamic( + this TSender? sender, + Expression? property1, + Expression? property2, + Expression? property3, + Expression? property4, + Expression? property5, + Expression? property6, + Expression? property7, + Expression? property8, + Expression? property9, + Expression? property10, + Expression? property11, + Expression? property12, + Func, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, IObservedChange, TRet> selector, + bool isDistinct) + { + return Observable.CombineLatest( + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property1, false, false, isDistinct), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property2, false, false, isDistinct), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property3, false, false, isDistinct), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property4, false, false, isDistinct), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property5, false, false, isDistinct), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property6, false, false, isDistinct), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property7, false, false, isDistinct), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property8, false, false, isDistinct), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property9, false, false, isDistinct), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property10, false, false, isDistinct), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property11, false, false, isDistinct), + ReactiveNotifyPropertyChangedMixin + .SubscribeToExpressionChain(sender, property12, false, false, isDistinct), + selector + ); + } + } -/// A mixin which provides support for subscribing to observable properties. -public static class WhenAnyObservableMixin -{ - /// Observe a observable which is set to a property, and automatically subscribe to the most recent emitted value. - /// The object where the property chain starts. - /// The first observable to observe. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyObservable uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyObservable may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyObservable(this TSender? sender, Expression?>> obs1) - where TSender : class - { - return sender.WhenAny(obs1, x => x.Value!.EmptyIfNull()).Switch(); - } + /// A mixin which provides support for subscribing to observable properties. + [RequiresUnreferencedCode("Evaluates expression-based member chains via reflection; members may be trimmed.")] + public static class WhenAnyObservableMixin + { + /// Observe a observable which is set to a property, and automatically subscribe to the most recent emitted value. + /// The object where the property chain starts. + /// The first observable to observe. + public static IObservable WhenAnyObservable(this TSender? sender, Expression?>> obs1) + where TSender : class + { + return sender.WhenAny(obs1, x => x.Value!.EmptyIfNull()).Switch(); + } - /// Monitor a property that is an observable, and subscribe to the most recent emitted value. - /// The object where the property chain starts. - /// The 1 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. - /// The 2 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyObservable uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyObservable may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyObservable(this TSender? sender, Expression?>> obs1, Expression?>> obs2) - where TSender : class - { - return sender.WhenAny(obs1, obs2, (o1, o2) => new[] {o1.Value!.EmptyIfNull(), o2.Value!.EmptyIfNull()}) - .Select(x => x.Merge()).Switch(); - } - /// Monitor a property that is an observable, and subscribe to the most recent emitted value. - /// The object where the property chain starts. - /// The 1 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. - /// The 2 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. - /// The 3 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyObservable uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyObservable may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyObservable(this TSender? sender, Expression?>> obs1, Expression?>> obs2, Expression?>> obs3) - where TSender : class - { - return sender.WhenAny(obs1, obs2, obs3, (o1, o2, o3) => new[] {o1.Value!.EmptyIfNull(), o2.Value!.EmptyIfNull(), o3.Value!.EmptyIfNull()}) - .Select(x => x.Merge()).Switch(); - } - /// Monitor a property that is an observable, and subscribe to the most recent emitted value. - /// The object where the property chain starts. - /// The 1 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. - /// The 2 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. - /// The 3 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. - /// The 4 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyObservable uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyObservable may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyObservable(this TSender? sender, Expression?>> obs1, Expression?>> obs2, Expression?>> obs3, Expression?>> obs4) - where TSender : class - { - return sender.WhenAny(obs1, obs2, obs3, obs4, (o1, o2, o3, o4) => new[] {o1.Value!.EmptyIfNull(), o2.Value!.EmptyIfNull(), o3.Value!.EmptyIfNull(), o4.Value!.EmptyIfNull()}) - .Select(x => x.Merge()).Switch(); - } - /// Monitor a property that is an observable, and subscribe to the most recent emitted value. - /// The object where the property chain starts. - /// The 1 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. - /// The 2 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. - /// The 3 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. - /// The 4 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. - /// The 5 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyObservable uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyObservable may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyObservable(this TSender? sender, Expression?>> obs1, Expression?>> obs2, Expression?>> obs3, Expression?>> obs4, Expression?>> obs5) - where TSender : class - { - return sender.WhenAny(obs1, obs2, obs3, obs4, obs5, (o1, o2, o3, o4, o5) => new[] {o1.Value!.EmptyIfNull(), o2.Value!.EmptyIfNull(), o3.Value!.EmptyIfNull(), o4.Value!.EmptyIfNull(), o5.Value!.EmptyIfNull()}) - .Select(x => x.Merge()).Switch(); - } - /// Monitor a property that is an observable, and subscribe to the most recent emitted value. - /// The object where the property chain starts. - /// The 1 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. - /// The 2 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. - /// The 3 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. - /// The 4 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. - /// The 5 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. - /// The 6 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyObservable uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyObservable may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyObservable(this TSender? sender, Expression?>> obs1, Expression?>> obs2, Expression?>> obs3, Expression?>> obs4, Expression?>> obs5, Expression?>> obs6) - where TSender : class - { - return sender.WhenAny(obs1, obs2, obs3, obs4, obs5, obs6, (o1, o2, o3, o4, o5, o6) => new[] {o1.Value!.EmptyIfNull(), o2.Value!.EmptyIfNull(), o3.Value!.EmptyIfNull(), o4.Value!.EmptyIfNull(), o5.Value!.EmptyIfNull(), o6.Value!.EmptyIfNull()}) - .Select(x => x.Merge()).Switch(); - } - /// Monitor a property that is an observable, and subscribe to the most recent emitted value. - /// The object where the property chain starts. - /// The 1 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. - /// The 2 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. - /// The 3 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. - /// The 4 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. - /// The 5 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. - /// The 6 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. - /// The 7 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyObservable uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyObservable may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyObservable(this TSender? sender, Expression?>> obs1, Expression?>> obs2, Expression?>> obs3, Expression?>> obs4, Expression?>> obs5, Expression?>> obs6, Expression?>> obs7) - where TSender : class - { - return sender.WhenAny(obs1, obs2, obs3, obs4, obs5, obs6, obs7, (o1, o2, o3, o4, o5, o6, o7) => new[] {o1.Value!.EmptyIfNull(), o2.Value!.EmptyIfNull(), o3.Value!.EmptyIfNull(), o4.Value!.EmptyIfNull(), o5.Value!.EmptyIfNull(), o6.Value!.EmptyIfNull(), o7.Value!.EmptyIfNull()}) - .Select(x => x.Merge()).Switch(); - } - /// Monitor a property that is an observable, and subscribe to the most recent emitted value. - /// The object where the property chain starts. - /// The 1 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. - /// The 2 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. - /// The 3 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. - /// The 4 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. - /// The 5 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. - /// The 6 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. - /// The 7 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. - /// The 8 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyObservable uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyObservable may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyObservable(this TSender? sender, Expression?>> obs1, Expression?>> obs2, Expression?>> obs3, Expression?>> obs4, Expression?>> obs5, Expression?>> obs6, Expression?>> obs7, Expression?>> obs8) - where TSender : class - { - return sender.WhenAny(obs1, obs2, obs3, obs4, obs5, obs6, obs7, obs8, (o1, o2, o3, o4, o5, o6, o7, o8) => new[] {o1.Value!.EmptyIfNull(), o2.Value!.EmptyIfNull(), o3.Value!.EmptyIfNull(), o4.Value!.EmptyIfNull(), o5.Value!.EmptyIfNull(), o6.Value!.EmptyIfNull(), o7.Value!.EmptyIfNull(), o8.Value!.EmptyIfNull()}) - .Select(x => x.Merge()).Switch(); - } - /// Monitor a property that is an observable, and subscribe to the most recent emitted value. - /// The object where the property chain starts. - /// The 1 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. - /// The 2 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. - /// The 3 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. - /// The 4 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. - /// The 5 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. - /// The 6 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. - /// The 7 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. - /// The 8 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. - /// The 9 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyObservable uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyObservable may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyObservable(this TSender? sender, Expression?>> obs1, Expression?>> obs2, Expression?>> obs3, Expression?>> obs4, Expression?>> obs5, Expression?>> obs6, Expression?>> obs7, Expression?>> obs8, Expression?>> obs9) - where TSender : class - { - return sender.WhenAny(obs1, obs2, obs3, obs4, obs5, obs6, obs7, obs8, obs9, (o1, o2, o3, o4, o5, o6, o7, o8, o9) => new[] {o1.Value!.EmptyIfNull(), o2.Value!.EmptyIfNull(), o3.Value!.EmptyIfNull(), o4.Value!.EmptyIfNull(), o5.Value!.EmptyIfNull(), o6.Value!.EmptyIfNull(), o7.Value!.EmptyIfNull(), o8.Value!.EmptyIfNull(), o9.Value!.EmptyIfNull()}) - .Select(x => x.Merge()).Switch(); - } - /// Monitor a property that is an observable, and subscribe to the most recent emitted value. - /// The object where the property chain starts. - /// The 1 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. - /// The 2 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. - /// The 3 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. - /// The 4 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. - /// The 5 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. - /// The 6 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. - /// The 7 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. - /// The 8 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. - /// The 9 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. - /// The 10 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyObservable uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyObservable may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyObservable(this TSender? sender, Expression?>> obs1, Expression?>> obs2, Expression?>> obs3, Expression?>> obs4, Expression?>> obs5, Expression?>> obs6, Expression?>> obs7, Expression?>> obs8, Expression?>> obs9, Expression?>> obs10) - where TSender : class - { - return sender.WhenAny(obs1, obs2, obs3, obs4, obs5, obs6, obs7, obs8, obs9, obs10, (o1, o2, o3, o4, o5, o6, o7, o8, o9, o10) => new[] {o1.Value!.EmptyIfNull(), o2.Value!.EmptyIfNull(), o3.Value!.EmptyIfNull(), o4.Value!.EmptyIfNull(), o5.Value!.EmptyIfNull(), o6.Value!.EmptyIfNull(), o7.Value!.EmptyIfNull(), o8.Value!.EmptyIfNull(), o9.Value!.EmptyIfNull(), o10.Value!.EmptyIfNull()}) - .Select(x => x.Merge()).Switch(); - } - /// Monitor a property that is an observable, and subscribe to the most recent emitted value. - /// The object where the property chain starts. - /// The 1 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. - /// The 2 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. - /// The 3 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. - /// The 4 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. - /// The 5 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. - /// The 6 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. - /// The 7 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. - /// The 8 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. - /// The 9 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. - /// The 10 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. - /// The 11 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyObservable uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyObservable may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyObservable(this TSender? sender, Expression?>> obs1, Expression?>> obs2, Expression?>> obs3, Expression?>> obs4, Expression?>> obs5, Expression?>> obs6, Expression?>> obs7, Expression?>> obs8, Expression?>> obs9, Expression?>> obs10, Expression?>> obs11) - where TSender : class - { - return sender.WhenAny(obs1, obs2, obs3, obs4, obs5, obs6, obs7, obs8, obs9, obs10, obs11, (o1, o2, o3, o4, o5, o6, o7, o8, o9, o10, o11) => new[] {o1.Value!.EmptyIfNull(), o2.Value!.EmptyIfNull(), o3.Value!.EmptyIfNull(), o4.Value!.EmptyIfNull(), o5.Value!.EmptyIfNull(), o6.Value!.EmptyIfNull(), o7.Value!.EmptyIfNull(), o8.Value!.EmptyIfNull(), o9.Value!.EmptyIfNull(), o10.Value!.EmptyIfNull(), o11.Value!.EmptyIfNull()}) - .Select(x => x.Merge()).Switch(); - } - /// Monitor a property that is an observable, and subscribe to the most recent emitted value. - /// The object where the property chain starts. - /// The 1 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. - /// The 2 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. - /// The 3 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. - /// The 4 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. - /// The 5 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. - /// The 6 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. - /// The 7 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. - /// The 8 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. - /// The 9 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. - /// The 10 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. - /// The 11 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. - /// The 12 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyObservable uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyObservable may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyObservable(this TSender? sender, Expression?>> obs1, Expression?>> obs2, Expression?>> obs3, Expression?>> obs4, Expression?>> obs5, Expression?>> obs6, Expression?>> obs7, Expression?>> obs8, Expression?>> obs9, Expression?>> obs10, Expression?>> obs11, Expression?>> obs12) - where TSender : class - { - return sender.WhenAny(obs1, obs2, obs3, obs4, obs5, obs6, obs7, obs8, obs9, obs10, obs11, obs12, (o1, o2, o3, o4, o5, o6, o7, o8, o9, o10, o11, o12) => new[] {o1.Value!.EmptyIfNull(), o2.Value!.EmptyIfNull(), o3.Value!.EmptyIfNull(), o4.Value!.EmptyIfNull(), o5.Value!.EmptyIfNull(), o6.Value!.EmptyIfNull(), o7.Value!.EmptyIfNull(), o8.Value!.EmptyIfNull(), o9.Value!.EmptyIfNull(), o10.Value!.EmptyIfNull(), o11.Value!.EmptyIfNull(), o12.Value!.EmptyIfNull()}) - .Select(x => x.Merge()).Switch(); - } + /// Monitor a property that is an observable, and subscribe to the most recent emitted value. + /// The object where the property chain starts. + /// The 1 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + /// The 2 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + public static IObservable WhenAnyObservable(this TSender? sender, Expression?>> obs1, Expression?>> obs2) + where TSender : class + { + return sender.WhenAny(obs1, obs2, (o1, o2) => new[] {o1.Value!.EmptyIfNull(), o2.Value!.EmptyIfNull()}) + .Select(x => x.Merge()).Switch(); + } + /// Monitor a property that is an observable, and subscribe to the most recent emitted value. + /// The object where the property chain starts. + /// The 1 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + /// The 2 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + /// The 3 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + public static IObservable WhenAnyObservable(this TSender? sender, Expression?>> obs1, Expression?>> obs2, Expression?>> obs3) + where TSender : class + { + return sender.WhenAny(obs1, obs2, obs3, (o1, o2, o3) => new[] {o1.Value!.EmptyIfNull(), o2.Value!.EmptyIfNull(), o3.Value!.EmptyIfNull()}) + .Select(x => x.Merge()).Switch(); + } + /// Monitor a property that is an observable, and subscribe to the most recent emitted value. + /// The object where the property chain starts. + /// The 1 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + /// The 2 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + /// The 3 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + /// The 4 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + public static IObservable WhenAnyObservable(this TSender? sender, Expression?>> obs1, Expression?>> obs2, Expression?>> obs3, Expression?>> obs4) + where TSender : class + { + return sender.WhenAny(obs1, obs2, obs3, obs4, (o1, o2, o3, o4) => new[] {o1.Value!.EmptyIfNull(), o2.Value!.EmptyIfNull(), o3.Value!.EmptyIfNull(), o4.Value!.EmptyIfNull()}) + .Select(x => x.Merge()).Switch(); + } + /// Monitor a property that is an observable, and subscribe to the most recent emitted value. + /// The object where the property chain starts. + /// The 1 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + /// The 2 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + /// The 3 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + /// The 4 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + /// The 5 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + public static IObservable WhenAnyObservable(this TSender? sender, Expression?>> obs1, Expression?>> obs2, Expression?>> obs3, Expression?>> obs4, Expression?>> obs5) + where TSender : class + { + return sender.WhenAny(obs1, obs2, obs3, obs4, obs5, (o1, o2, o3, o4, o5) => new[] {o1.Value!.EmptyIfNull(), o2.Value!.EmptyIfNull(), o3.Value!.EmptyIfNull(), o4.Value!.EmptyIfNull(), o5.Value!.EmptyIfNull()}) + .Select(x => x.Merge()).Switch(); + } + /// Monitor a property that is an observable, and subscribe to the most recent emitted value. + /// The object where the property chain starts. + /// The 1 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + /// The 2 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + /// The 3 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + /// The 4 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + /// The 5 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + /// The 6 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + public static IObservable WhenAnyObservable(this TSender? sender, Expression?>> obs1, Expression?>> obs2, Expression?>> obs3, Expression?>> obs4, Expression?>> obs5, Expression?>> obs6) + where TSender : class + { + return sender.WhenAny(obs1, obs2, obs3, obs4, obs5, obs6, (o1, o2, o3, o4, o5, o6) => new[] {o1.Value!.EmptyIfNull(), o2.Value!.EmptyIfNull(), o3.Value!.EmptyIfNull(), o4.Value!.EmptyIfNull(), o5.Value!.EmptyIfNull(), o6.Value!.EmptyIfNull()}) + .Select(x => x.Merge()).Switch(); + } + /// Monitor a property that is an observable, and subscribe to the most recent emitted value. + /// The object where the property chain starts. + /// The 1 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + /// The 2 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + /// The 3 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + /// The 4 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + /// The 5 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + /// The 6 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + /// The 7 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + public static IObservable WhenAnyObservable(this TSender? sender, Expression?>> obs1, Expression?>> obs2, Expression?>> obs3, Expression?>> obs4, Expression?>> obs5, Expression?>> obs6, Expression?>> obs7) + where TSender : class + { + return sender.WhenAny(obs1, obs2, obs3, obs4, obs5, obs6, obs7, (o1, o2, o3, o4, o5, o6, o7) => new[] {o1.Value!.EmptyIfNull(), o2.Value!.EmptyIfNull(), o3.Value!.EmptyIfNull(), o4.Value!.EmptyIfNull(), o5.Value!.EmptyIfNull(), o6.Value!.EmptyIfNull(), o7.Value!.EmptyIfNull()}) + .Select(x => x.Merge()).Switch(); + } + /// Monitor a property that is an observable, and subscribe to the most recent emitted value. + /// The object where the property chain starts. + /// The 1 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + /// The 2 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + /// The 3 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + /// The 4 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + /// The 5 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + /// The 6 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + /// The 7 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + /// The 8 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + public static IObservable WhenAnyObservable(this TSender? sender, Expression?>> obs1, Expression?>> obs2, Expression?>> obs3, Expression?>> obs4, Expression?>> obs5, Expression?>> obs6, Expression?>> obs7, Expression?>> obs8) + where TSender : class + { + return sender.WhenAny(obs1, obs2, obs3, obs4, obs5, obs6, obs7, obs8, (o1, o2, o3, o4, o5, o6, o7, o8) => new[] {o1.Value!.EmptyIfNull(), o2.Value!.EmptyIfNull(), o3.Value!.EmptyIfNull(), o4.Value!.EmptyIfNull(), o5.Value!.EmptyIfNull(), o6.Value!.EmptyIfNull(), o7.Value!.EmptyIfNull(), o8.Value!.EmptyIfNull()}) + .Select(x => x.Merge()).Switch(); + } + /// Monitor a property that is an observable, and subscribe to the most recent emitted value. + /// The object where the property chain starts. + /// The 1 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + /// The 2 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + /// The 3 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + /// The 4 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + /// The 5 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + /// The 6 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + /// The 7 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + /// The 8 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + /// The 9 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + public static IObservable WhenAnyObservable(this TSender? sender, Expression?>> obs1, Expression?>> obs2, Expression?>> obs3, Expression?>> obs4, Expression?>> obs5, Expression?>> obs6, Expression?>> obs7, Expression?>> obs8, Expression?>> obs9) + where TSender : class + { + return sender.WhenAny(obs1, obs2, obs3, obs4, obs5, obs6, obs7, obs8, obs9, (o1, o2, o3, o4, o5, o6, o7, o8, o9) => new[] {o1.Value!.EmptyIfNull(), o2.Value!.EmptyIfNull(), o3.Value!.EmptyIfNull(), o4.Value!.EmptyIfNull(), o5.Value!.EmptyIfNull(), o6.Value!.EmptyIfNull(), o7.Value!.EmptyIfNull(), o8.Value!.EmptyIfNull(), o9.Value!.EmptyIfNull()}) + .Select(x => x.Merge()).Switch(); + } + /// Monitor a property that is an observable, and subscribe to the most recent emitted value. + /// The object where the property chain starts. + /// The 1 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + /// The 2 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + /// The 3 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + /// The 4 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + /// The 5 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + /// The 6 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + /// The 7 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + /// The 8 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + /// The 9 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + /// The 10 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + public static IObservable WhenAnyObservable(this TSender? sender, Expression?>> obs1, Expression?>> obs2, Expression?>> obs3, Expression?>> obs4, Expression?>> obs5, Expression?>> obs6, Expression?>> obs7, Expression?>> obs8, Expression?>> obs9, Expression?>> obs10) + where TSender : class + { + return sender.WhenAny(obs1, obs2, obs3, obs4, obs5, obs6, obs7, obs8, obs9, obs10, (o1, o2, o3, o4, o5, o6, o7, o8, o9, o10) => new[] {o1.Value!.EmptyIfNull(), o2.Value!.EmptyIfNull(), o3.Value!.EmptyIfNull(), o4.Value!.EmptyIfNull(), o5.Value!.EmptyIfNull(), o6.Value!.EmptyIfNull(), o7.Value!.EmptyIfNull(), o8.Value!.EmptyIfNull(), o9.Value!.EmptyIfNull(), o10.Value!.EmptyIfNull()}) + .Select(x => x.Merge()).Switch(); + } + /// Monitor a property that is an observable, and subscribe to the most recent emitted value. + /// The object where the property chain starts. + /// The 1 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + /// The 2 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + /// The 3 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + /// The 4 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + /// The 5 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + /// The 6 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + /// The 7 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + /// The 8 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + /// The 9 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + /// The 10 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + /// The 11 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + public static IObservable WhenAnyObservable(this TSender? sender, Expression?>> obs1, Expression?>> obs2, Expression?>> obs3, Expression?>> obs4, Expression?>> obs5, Expression?>> obs6, Expression?>> obs7, Expression?>> obs8, Expression?>> obs9, Expression?>> obs10, Expression?>> obs11) + where TSender : class + { + return sender.WhenAny(obs1, obs2, obs3, obs4, obs5, obs6, obs7, obs8, obs9, obs10, obs11, (o1, o2, o3, o4, o5, o6, o7, o8, o9, o10, o11) => new[] {o1.Value!.EmptyIfNull(), o2.Value!.EmptyIfNull(), o3.Value!.EmptyIfNull(), o4.Value!.EmptyIfNull(), o5.Value!.EmptyIfNull(), o6.Value!.EmptyIfNull(), o7.Value!.EmptyIfNull(), o8.Value!.EmptyIfNull(), o9.Value!.EmptyIfNull(), o10.Value!.EmptyIfNull(), o11.Value!.EmptyIfNull()}) + .Select(x => x.Merge()).Switch(); + } + /// Monitor a property that is an observable, and subscribe to the most recent emitted value. + /// The object where the property chain starts. + /// The 1 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + /// The 2 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + /// The 3 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + /// The 4 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + /// The 5 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + /// The 6 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + /// The 7 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + /// The 8 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + /// The 9 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + /// The 10 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + /// The 11 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + /// The 12 property chain to reference which ends with an observable. This will be a expression pointing to a end property or field which must be an observable. + public static IObservable WhenAnyObservable(this TSender? sender, Expression?>> obs1, Expression?>> obs2, Expression?>> obs3, Expression?>> obs4, Expression?>> obs5, Expression?>> obs6, Expression?>> obs7, Expression?>> obs8, Expression?>> obs9, Expression?>> obs10, Expression?>> obs11, Expression?>> obs12) + where TSender : class + { + return sender.WhenAny(obs1, obs2, obs3, obs4, obs5, obs6, obs7, obs8, obs9, obs10, obs11, obs12, (o1, o2, o3, o4, o5, o6, o7, o8, o9, o10, o11, o12) => new[] {o1.Value!.EmptyIfNull(), o2.Value!.EmptyIfNull(), o3.Value!.EmptyIfNull(), o4.Value!.EmptyIfNull(), o5.Value!.EmptyIfNull(), o6.Value!.EmptyIfNull(), o7.Value!.EmptyIfNull(), o8.Value!.EmptyIfNull(), o9.Value!.EmptyIfNull(), o10.Value!.EmptyIfNull(), o11.Value!.EmptyIfNull(), o12.Value!.EmptyIfNull()}) + .Select(x => x.Merge()).Switch(); + } - /// Monitor a property that is an observable, and subscribe to the most recent emitted value. - /// The object where the property chain starts. - /// The 1 property chain to reference. - /// The 2 property chain to reference. - /// The selector which will determine the final value from the properties. This must be an observable. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyObservable uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyObservable may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyObservable(this TSender? sender, - Expression?>> obs1, - Expression?>> obs2, - Func selector) - where TSender : class - { - return sender.WhenAny(obs1, obs2, (o1, o2) => Observable.CombineLatest(o1.Value!.EmptyIfNull(), o2.Value!.EmptyIfNull(), selector)) - .Switch(); - } - /// Monitor a property that is an observable, and subscribe to the most recent emitted value. - /// The object where the property chain starts. - /// The 1 property chain to reference. - /// The 2 property chain to reference. - /// The 3 property chain to reference. - /// The selector which will determine the final value from the properties. This must be an observable. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyObservable uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyObservable may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyObservable(this TSender? sender, - Expression?>> obs1, - Expression?>> obs2, - Expression?>> obs3, - Func selector) - where TSender : class - { - return sender.WhenAny(obs1, obs2, obs3, (o1, o2, o3) => Observable.CombineLatest(o1.Value!.EmptyIfNull(), o2.Value!.EmptyIfNull(), o3.Value!.EmptyIfNull(), selector)) - .Switch(); - } - /// Monitor a property that is an observable, and subscribe to the most recent emitted value. - /// The object where the property chain starts. - /// The 1 property chain to reference. - /// The 2 property chain to reference. - /// The 3 property chain to reference. - /// The 4 property chain to reference. - /// The selector which will determine the final value from the properties. This must be an observable. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyObservable uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyObservable may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyObservable(this TSender? sender, - Expression?>> obs1, - Expression?>> obs2, - Expression?>> obs3, - Expression?>> obs4, - Func selector) - where TSender : class - { - return sender.WhenAny(obs1, obs2, obs3, obs4, (o1, o2, o3, o4) => Observable.CombineLatest(o1.Value!.EmptyIfNull(), o2.Value!.EmptyIfNull(), o3.Value!.EmptyIfNull(), o4.Value!.EmptyIfNull(), selector)) - .Switch(); - } - /// Monitor a property that is an observable, and subscribe to the most recent emitted value. - /// The object where the property chain starts. - /// The 1 property chain to reference. - /// The 2 property chain to reference. - /// The 3 property chain to reference. - /// The 4 property chain to reference. - /// The 5 property chain to reference. - /// The selector which will determine the final value from the properties. This must be an observable. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyObservable uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyObservable may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyObservable(this TSender? sender, - Expression?>> obs1, - Expression?>> obs2, - Expression?>> obs3, - Expression?>> obs4, - Expression?>> obs5, - Func selector) - where TSender : class - { - return sender.WhenAny(obs1, obs2, obs3, obs4, obs5, (o1, o2, o3, o4, o5) => Observable.CombineLatest(o1.Value!.EmptyIfNull(), o2.Value!.EmptyIfNull(), o3.Value!.EmptyIfNull(), o4.Value!.EmptyIfNull(), o5.Value!.EmptyIfNull(), selector)) - .Switch(); - } - /// Monitor a property that is an observable, and subscribe to the most recent emitted value. - /// The object where the property chain starts. - /// The 1 property chain to reference. - /// The 2 property chain to reference. - /// The 3 property chain to reference. - /// The 4 property chain to reference. - /// The 5 property chain to reference. - /// The 6 property chain to reference. - /// The selector which will determine the final value from the properties. This must be an observable. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyObservable uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyObservable may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyObservable(this TSender? sender, - Expression?>> obs1, - Expression?>> obs2, - Expression?>> obs3, - Expression?>> obs4, - Expression?>> obs5, - Expression?>> obs6, - Func selector) - where TSender : class - { - return sender.WhenAny(obs1, obs2, obs3, obs4, obs5, obs6, (o1, o2, o3, o4, o5, o6) => Observable.CombineLatest(o1.Value!.EmptyIfNull(), o2.Value!.EmptyIfNull(), o3.Value!.EmptyIfNull(), o4.Value!.EmptyIfNull(), o5.Value!.EmptyIfNull(), o6.Value!.EmptyIfNull(), selector)) - .Switch(); - } - /// Monitor a property that is an observable, and subscribe to the most recent emitted value. - /// The object where the property chain starts. - /// The 1 property chain to reference. - /// The 2 property chain to reference. - /// The 3 property chain to reference. - /// The 4 property chain to reference. - /// The 5 property chain to reference. - /// The 6 property chain to reference. - /// The 7 property chain to reference. - /// The selector which will determine the final value from the properties. This must be an observable. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyObservable uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyObservable may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyObservable(this TSender? sender, - Expression?>> obs1, - Expression?>> obs2, - Expression?>> obs3, - Expression?>> obs4, - Expression?>> obs5, - Expression?>> obs6, - Expression?>> obs7, - Func selector) - where TSender : class - { - return sender.WhenAny(obs1, obs2, obs3, obs4, obs5, obs6, obs7, (o1, o2, o3, o4, o5, o6, o7) => Observable.CombineLatest(o1.Value!.EmptyIfNull(), o2.Value!.EmptyIfNull(), o3.Value!.EmptyIfNull(), o4.Value!.EmptyIfNull(), o5.Value!.EmptyIfNull(), o6.Value!.EmptyIfNull(), o7.Value!.EmptyIfNull(), selector)) - .Switch(); - } - /// Monitor a property that is an observable, and subscribe to the most recent emitted value. - /// The object where the property chain starts. - /// The 1 property chain to reference. - /// The 2 property chain to reference. - /// The 3 property chain to reference. - /// The 4 property chain to reference. - /// The 5 property chain to reference. - /// The 6 property chain to reference. - /// The 7 property chain to reference. - /// The 8 property chain to reference. - /// The selector which will determine the final value from the properties. This must be an observable. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyObservable uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyObservable may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyObservable(this TSender? sender, - Expression?>> obs1, - Expression?>> obs2, - Expression?>> obs3, - Expression?>> obs4, - Expression?>> obs5, - Expression?>> obs6, - Expression?>> obs7, - Expression?>> obs8, - Func selector) - where TSender : class - { - return sender.WhenAny(obs1, obs2, obs3, obs4, obs5, obs6, obs7, obs8, (o1, o2, o3, o4, o5, o6, o7, o8) => Observable.CombineLatest(o1.Value!.EmptyIfNull(), o2.Value!.EmptyIfNull(), o3.Value!.EmptyIfNull(), o4.Value!.EmptyIfNull(), o5.Value!.EmptyIfNull(), o6.Value!.EmptyIfNull(), o7.Value!.EmptyIfNull(), o8.Value!.EmptyIfNull(), selector)) - .Switch(); - } - /// Monitor a property that is an observable, and subscribe to the most recent emitted value. - /// The object where the property chain starts. - /// The 1 property chain to reference. - /// The 2 property chain to reference. - /// The 3 property chain to reference. - /// The 4 property chain to reference. - /// The 5 property chain to reference. - /// The 6 property chain to reference. - /// The 7 property chain to reference. - /// The 8 property chain to reference. - /// The 9 property chain to reference. - /// The selector which will determine the final value from the properties. This must be an observable. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyObservable uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyObservable may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyObservable(this TSender? sender, - Expression?>> obs1, - Expression?>> obs2, - Expression?>> obs3, - Expression?>> obs4, - Expression?>> obs5, - Expression?>> obs6, - Expression?>> obs7, - Expression?>> obs8, - Expression?>> obs9, - Func selector) - where TSender : class - { - return sender.WhenAny(obs1, obs2, obs3, obs4, obs5, obs6, obs7, obs8, obs9, (o1, o2, o3, o4, o5, o6, o7, o8, o9) => Observable.CombineLatest(o1.Value!.EmptyIfNull(), o2.Value!.EmptyIfNull(), o3.Value!.EmptyIfNull(), o4.Value!.EmptyIfNull(), o5.Value!.EmptyIfNull(), o6.Value!.EmptyIfNull(), o7.Value!.EmptyIfNull(), o8.Value!.EmptyIfNull(), o9.Value!.EmptyIfNull(), selector)) - .Switch(); - } - /// Monitor a property that is an observable, and subscribe to the most recent emitted value. - /// The object where the property chain starts. - /// The 1 property chain to reference. - /// The 2 property chain to reference. - /// The 3 property chain to reference. - /// The 4 property chain to reference. - /// The 5 property chain to reference. - /// The 6 property chain to reference. - /// The 7 property chain to reference. - /// The 8 property chain to reference. - /// The 9 property chain to reference. - /// The 10 property chain to reference. - /// The selector which will determine the final value from the properties. This must be an observable. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyObservable uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyObservable may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyObservable(this TSender? sender, - Expression?>> obs1, - Expression?>> obs2, - Expression?>> obs3, - Expression?>> obs4, - Expression?>> obs5, - Expression?>> obs6, - Expression?>> obs7, - Expression?>> obs8, - Expression?>> obs9, - Expression?>> obs10, - Func selector) - where TSender : class - { - return sender.WhenAny(obs1, obs2, obs3, obs4, obs5, obs6, obs7, obs8, obs9, obs10, (o1, o2, o3, o4, o5, o6, o7, o8, o9, o10) => Observable.CombineLatest(o1.Value!.EmptyIfNull(), o2.Value!.EmptyIfNull(), o3.Value!.EmptyIfNull(), o4.Value!.EmptyIfNull(), o5.Value!.EmptyIfNull(), o6.Value!.EmptyIfNull(), o7.Value!.EmptyIfNull(), o8.Value!.EmptyIfNull(), o9.Value!.EmptyIfNull(), o10.Value!.EmptyIfNull(), selector)) - .Switch(); - } - /// Monitor a property that is an observable, and subscribe to the most recent emitted value. - /// The object where the property chain starts. - /// The 1 property chain to reference. - /// The 2 property chain to reference. - /// The 3 property chain to reference. - /// The 4 property chain to reference. - /// The 5 property chain to reference. - /// The 6 property chain to reference. - /// The 7 property chain to reference. - /// The 8 property chain to reference. - /// The 9 property chain to reference. - /// The 10 property chain to reference. - /// The 11 property chain to reference. - /// The selector which will determine the final value from the properties. This must be an observable. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyObservable uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyObservable may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyObservable(this TSender? sender, - Expression?>> obs1, - Expression?>> obs2, - Expression?>> obs3, - Expression?>> obs4, - Expression?>> obs5, - Expression?>> obs6, - Expression?>> obs7, - Expression?>> obs8, - Expression?>> obs9, - Expression?>> obs10, - Expression?>> obs11, - Func selector) - where TSender : class - { - return sender.WhenAny(obs1, obs2, obs3, obs4, obs5, obs6, obs7, obs8, obs9, obs10, obs11, (o1, o2, o3, o4, o5, o6, o7, o8, o9, o10, o11) => Observable.CombineLatest(o1.Value!.EmptyIfNull(), o2.Value!.EmptyIfNull(), o3.Value!.EmptyIfNull(), o4.Value!.EmptyIfNull(), o5.Value!.EmptyIfNull(), o6.Value!.EmptyIfNull(), o7.Value!.EmptyIfNull(), o8.Value!.EmptyIfNull(), o9.Value!.EmptyIfNull(), o10.Value!.EmptyIfNull(), o11.Value!.EmptyIfNull(), selector)) - .Switch(); - } - /// Monitor a property that is an observable, and subscribe to the most recent emitted value. - /// The object where the property chain starts. - /// The 1 property chain to reference. - /// The 2 property chain to reference. - /// The 3 property chain to reference. - /// The 4 property chain to reference. - /// The 5 property chain to reference. - /// The 6 property chain to reference. - /// The 7 property chain to reference. - /// The 8 property chain to reference. - /// The 9 property chain to reference. - /// The 10 property chain to reference. - /// The 11 property chain to reference. - /// The 12 property chain to reference. - /// The selector which will determine the final value from the properties. This must be an observable. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyObservable uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyObservable may reference members that could be trimmed in AOT scenarios.")] -#endif - public static IObservable WhenAnyObservable(this TSender? sender, - Expression?>> obs1, - Expression?>> obs2, - Expression?>> obs3, - Expression?>> obs4, - Expression?>> obs5, - Expression?>> obs6, - Expression?>> obs7, - Expression?>> obs8, - Expression?>> obs9, - Expression?>> obs10, - Expression?>> obs11, - Expression?>> obs12, - Func selector) - where TSender : class - { - return sender.WhenAny(obs1, obs2, obs3, obs4, obs5, obs6, obs7, obs8, obs9, obs10, obs11, obs12, (o1, o2, o3, o4, o5, o6, o7, o8, o9, o10, o11, o12) => Observable.CombineLatest(o1.Value!.EmptyIfNull(), o2.Value!.EmptyIfNull(), o3.Value!.EmptyIfNull(), o4.Value!.EmptyIfNull(), o5.Value!.EmptyIfNull(), o6.Value!.EmptyIfNull(), o7.Value!.EmptyIfNull(), o8.Value!.EmptyIfNull(), o9.Value!.EmptyIfNull(), o10.Value!.EmptyIfNull(), o11.Value!.EmptyIfNull(), o12.Value!.EmptyIfNull(), selector)) - .Switch(); - } + /// Monitor a property that is an observable, and subscribe to the most recent emitted value. + /// The object where the property chain starts. + /// The 1 property chain to reference. + /// The 2 property chain to reference. + /// The selector which will determine the final value from the properties. This must be an observable. + public static IObservable WhenAnyObservable(this TSender? sender, + Expression?>> obs1, + Expression?>> obs2, + Func selector) + where TSender : class + { + return sender.WhenAny(obs1, obs2, (o1, o2) => Observable.CombineLatest(o1.Value!.EmptyIfNull(), o2.Value!.EmptyIfNull(), selector)) + .Switch(); + } + /// Monitor a property that is an observable, and subscribe to the most recent emitted value. + /// The object where the property chain starts. + /// The 1 property chain to reference. + /// The 2 property chain to reference. + /// The 3 property chain to reference. + /// The selector which will determine the final value from the properties. This must be an observable. + public static IObservable WhenAnyObservable(this TSender? sender, + Expression?>> obs1, + Expression?>> obs2, + Expression?>> obs3, + Func selector) + where TSender : class + { + return sender.WhenAny(obs1, obs2, obs3, (o1, o2, o3) => Observable.CombineLatest(o1.Value!.EmptyIfNull(), o2.Value!.EmptyIfNull(), o3.Value!.EmptyIfNull(), selector)) + .Switch(); + } + /// Monitor a property that is an observable, and subscribe to the most recent emitted value. + /// The object where the property chain starts. + /// The 1 property chain to reference. + /// The 2 property chain to reference. + /// The 3 property chain to reference. + /// The 4 property chain to reference. + /// The selector which will determine the final value from the properties. This must be an observable. + public static IObservable WhenAnyObservable(this TSender? sender, + Expression?>> obs1, + Expression?>> obs2, + Expression?>> obs3, + Expression?>> obs4, + Func selector) + where TSender : class + { + return sender.WhenAny(obs1, obs2, obs3, obs4, (o1, o2, o3, o4) => Observable.CombineLatest(o1.Value!.EmptyIfNull(), o2.Value!.EmptyIfNull(), o3.Value!.EmptyIfNull(), o4.Value!.EmptyIfNull(), selector)) + .Switch(); + } + /// Monitor a property that is an observable, and subscribe to the most recent emitted value. + /// The object where the property chain starts. + /// The 1 property chain to reference. + /// The 2 property chain to reference. + /// The 3 property chain to reference. + /// The 4 property chain to reference. + /// The 5 property chain to reference. + /// The selector which will determine the final value from the properties. This must be an observable. + public static IObservable WhenAnyObservable(this TSender? sender, + Expression?>> obs1, + Expression?>> obs2, + Expression?>> obs3, + Expression?>> obs4, + Expression?>> obs5, + Func selector) + where TSender : class + { + return sender.WhenAny(obs1, obs2, obs3, obs4, obs5, (o1, o2, o3, o4, o5) => Observable.CombineLatest(o1.Value!.EmptyIfNull(), o2.Value!.EmptyIfNull(), o3.Value!.EmptyIfNull(), o4.Value!.EmptyIfNull(), o5.Value!.EmptyIfNull(), selector)) + .Switch(); + } + /// Monitor a property that is an observable, and subscribe to the most recent emitted value. + /// The object where the property chain starts. + /// The 1 property chain to reference. + /// The 2 property chain to reference. + /// The 3 property chain to reference. + /// The 4 property chain to reference. + /// The 5 property chain to reference. + /// The 6 property chain to reference. + /// The selector which will determine the final value from the properties. This must be an observable. + public static IObservable WhenAnyObservable(this TSender? sender, + Expression?>> obs1, + Expression?>> obs2, + Expression?>> obs3, + Expression?>> obs4, + Expression?>> obs5, + Expression?>> obs6, + Func selector) + where TSender : class + { + return sender.WhenAny(obs1, obs2, obs3, obs4, obs5, obs6, (o1, o2, o3, o4, o5, o6) => Observable.CombineLatest(o1.Value!.EmptyIfNull(), o2.Value!.EmptyIfNull(), o3.Value!.EmptyIfNull(), o4.Value!.EmptyIfNull(), o5.Value!.EmptyIfNull(), o6.Value!.EmptyIfNull(), selector)) + .Switch(); + } + /// Monitor a property that is an observable, and subscribe to the most recent emitted value. + /// The object where the property chain starts. + /// The 1 property chain to reference. + /// The 2 property chain to reference. + /// The 3 property chain to reference. + /// The 4 property chain to reference. + /// The 5 property chain to reference. + /// The 6 property chain to reference. + /// The 7 property chain to reference. + /// The selector which will determine the final value from the properties. This must be an observable. + public static IObservable WhenAnyObservable(this TSender? sender, + Expression?>> obs1, + Expression?>> obs2, + Expression?>> obs3, + Expression?>> obs4, + Expression?>> obs5, + Expression?>> obs6, + Expression?>> obs7, + Func selector) + where TSender : class + { + return sender.WhenAny(obs1, obs2, obs3, obs4, obs5, obs6, obs7, (o1, o2, o3, o4, o5, o6, o7) => Observable.CombineLatest(o1.Value!.EmptyIfNull(), o2.Value!.EmptyIfNull(), o3.Value!.EmptyIfNull(), o4.Value!.EmptyIfNull(), o5.Value!.EmptyIfNull(), o6.Value!.EmptyIfNull(), o7.Value!.EmptyIfNull(), selector)) + .Switch(); + } + /// Monitor a property that is an observable, and subscribe to the most recent emitted value. + /// The object where the property chain starts. + /// The 1 property chain to reference. + /// The 2 property chain to reference. + /// The 3 property chain to reference. + /// The 4 property chain to reference. + /// The 5 property chain to reference. + /// The 6 property chain to reference. + /// The 7 property chain to reference. + /// The 8 property chain to reference. + /// The selector which will determine the final value from the properties. This must be an observable. + public static IObservable WhenAnyObservable(this TSender? sender, + Expression?>> obs1, + Expression?>> obs2, + Expression?>> obs3, + Expression?>> obs4, + Expression?>> obs5, + Expression?>> obs6, + Expression?>> obs7, + Expression?>> obs8, + Func selector) + where TSender : class + { + return sender.WhenAny(obs1, obs2, obs3, obs4, obs5, obs6, obs7, obs8, (o1, o2, o3, o4, o5, o6, o7, o8) => Observable.CombineLatest(o1.Value!.EmptyIfNull(), o2.Value!.EmptyIfNull(), o3.Value!.EmptyIfNull(), o4.Value!.EmptyIfNull(), o5.Value!.EmptyIfNull(), o6.Value!.EmptyIfNull(), o7.Value!.EmptyIfNull(), o8.Value!.EmptyIfNull(), selector)) + .Switch(); + } + /// Monitor a property that is an observable, and subscribe to the most recent emitted value. + /// The object where the property chain starts. + /// The 1 property chain to reference. + /// The 2 property chain to reference. + /// The 3 property chain to reference. + /// The 4 property chain to reference. + /// The 5 property chain to reference. + /// The 6 property chain to reference. + /// The 7 property chain to reference. + /// The 8 property chain to reference. + /// The 9 property chain to reference. + /// The selector which will determine the final value from the properties. This must be an observable. + public static IObservable WhenAnyObservable(this TSender? sender, + Expression?>> obs1, + Expression?>> obs2, + Expression?>> obs3, + Expression?>> obs4, + Expression?>> obs5, + Expression?>> obs6, + Expression?>> obs7, + Expression?>> obs8, + Expression?>> obs9, + Func selector) + where TSender : class + { + return sender.WhenAny(obs1, obs2, obs3, obs4, obs5, obs6, obs7, obs8, obs9, (o1, o2, o3, o4, o5, o6, o7, o8, o9) => Observable.CombineLatest(o1.Value!.EmptyIfNull(), o2.Value!.EmptyIfNull(), o3.Value!.EmptyIfNull(), o4.Value!.EmptyIfNull(), o5.Value!.EmptyIfNull(), o6.Value!.EmptyIfNull(), o7.Value!.EmptyIfNull(), o8.Value!.EmptyIfNull(), o9.Value!.EmptyIfNull(), selector)) + .Switch(); + } + /// Monitor a property that is an observable, and subscribe to the most recent emitted value. + /// The object where the property chain starts. + /// The 1 property chain to reference. + /// The 2 property chain to reference. + /// The 3 property chain to reference. + /// The 4 property chain to reference. + /// The 5 property chain to reference. + /// The 6 property chain to reference. + /// The 7 property chain to reference. + /// The 8 property chain to reference. + /// The 9 property chain to reference. + /// The 10 property chain to reference. + /// The selector which will determine the final value from the properties. This must be an observable. + public static IObservable WhenAnyObservable(this TSender? sender, + Expression?>> obs1, + Expression?>> obs2, + Expression?>> obs3, + Expression?>> obs4, + Expression?>> obs5, + Expression?>> obs6, + Expression?>> obs7, + Expression?>> obs8, + Expression?>> obs9, + Expression?>> obs10, + Func selector) + where TSender : class + { + return sender.WhenAny(obs1, obs2, obs3, obs4, obs5, obs6, obs7, obs8, obs9, obs10, (o1, o2, o3, o4, o5, o6, o7, o8, o9, o10) => Observable.CombineLatest(o1.Value!.EmptyIfNull(), o2.Value!.EmptyIfNull(), o3.Value!.EmptyIfNull(), o4.Value!.EmptyIfNull(), o5.Value!.EmptyIfNull(), o6.Value!.EmptyIfNull(), o7.Value!.EmptyIfNull(), o8.Value!.EmptyIfNull(), o9.Value!.EmptyIfNull(), o10.Value!.EmptyIfNull(), selector)) + .Switch(); + } + /// Monitor a property that is an observable, and subscribe to the most recent emitted value. + /// The object where the property chain starts. + /// The 1 property chain to reference. + /// The 2 property chain to reference. + /// The 3 property chain to reference. + /// The 4 property chain to reference. + /// The 5 property chain to reference. + /// The 6 property chain to reference. + /// The 7 property chain to reference. + /// The 8 property chain to reference. + /// The 9 property chain to reference. + /// The 10 property chain to reference. + /// The 11 property chain to reference. + /// The selector which will determine the final value from the properties. This must be an observable. + public static IObservable WhenAnyObservable(this TSender? sender, + Expression?>> obs1, + Expression?>> obs2, + Expression?>> obs3, + Expression?>> obs4, + Expression?>> obs5, + Expression?>> obs6, + Expression?>> obs7, + Expression?>> obs8, + Expression?>> obs9, + Expression?>> obs10, + Expression?>> obs11, + Func selector) + where TSender : class + { + return sender.WhenAny(obs1, obs2, obs3, obs4, obs5, obs6, obs7, obs8, obs9, obs10, obs11, (o1, o2, o3, o4, o5, o6, o7, o8, o9, o10, o11) => Observable.CombineLatest(o1.Value!.EmptyIfNull(), o2.Value!.EmptyIfNull(), o3.Value!.EmptyIfNull(), o4.Value!.EmptyIfNull(), o5.Value!.EmptyIfNull(), o6.Value!.EmptyIfNull(), o7.Value!.EmptyIfNull(), o8.Value!.EmptyIfNull(), o9.Value!.EmptyIfNull(), o10.Value!.EmptyIfNull(), o11.Value!.EmptyIfNull(), selector)) + .Switch(); + } + /// Monitor a property that is an observable, and subscribe to the most recent emitted value. + /// The object where the property chain starts. + /// The 1 property chain to reference. + /// The 2 property chain to reference. + /// The 3 property chain to reference. + /// The 4 property chain to reference. + /// The 5 property chain to reference. + /// The 6 property chain to reference. + /// The 7 property chain to reference. + /// The 8 property chain to reference. + /// The 9 property chain to reference. + /// The 10 property chain to reference. + /// The 11 property chain to reference. + /// The 12 property chain to reference. + /// The selector which will determine the final value from the properties. This must be an observable. + public static IObservable WhenAnyObservable(this TSender? sender, + Expression?>> obs1, + Expression?>> obs2, + Expression?>> obs3, + Expression?>> obs4, + Expression?>> obs5, + Expression?>> obs6, + Expression?>> obs7, + Expression?>> obs8, + Expression?>> obs9, + Expression?>> obs10, + Expression?>> obs11, + Expression?>> obs12, + Func selector) + where TSender : class + { + return sender.WhenAny(obs1, obs2, obs3, obs4, obs5, obs6, obs7, obs8, obs9, obs10, obs11, obs12, (o1, o2, o3, o4, o5, o6, o7, o8, o9, o10, o11, o12) => Observable.CombineLatest(o1.Value!.EmptyIfNull(), o2.Value!.EmptyIfNull(), o3.Value!.EmptyIfNull(), o4.Value!.EmptyIfNull(), o5.Value!.EmptyIfNull(), o6.Value!.EmptyIfNull(), o7.Value!.EmptyIfNull(), o8.Value!.EmptyIfNull(), o9.Value!.EmptyIfNull(), o10.Value!.EmptyIfNull(), o11.Value!.EmptyIfNull(), o12.Value!.EmptyIfNull(), selector)) + .Switch(); + } } -internal static class ObservableExtensions -{ - public static IObservable EmptyIfNull(this IObservable @this) + internal static class ObservableExtensions { - return @this ?? Observable.Empty(); + public static IObservable EmptyIfNull(this IObservable @this) + { + return @this ?? Observable.Empty(); + } } -} \ No newline at end of file +} diff --git a/src/ReactiveUI/VariadicTemplates.tt b/src/ReactiveUI/VariadicTemplates.tt index fe5b365658..dcb37e2b88 100644 --- a/src/ReactiveUI/VariadicTemplates.tt +++ b/src/ReactiveUI/VariadicTemplates.tt @@ -35,6 +35,7 @@ int maxFuncLength = 12; namespace ReactiveUI { /// Extension methods associated with the WhenAny/WhenAnyValue classes. + [RequiresUnreferencedCode("Evaluates expression-based member chains via reflection; members may be trimmed.")] public static class WhenAnyMixin { /// @@ -46,10 +47,6 @@ namespace ReactiveUI /// /// The object where the property chain starts. /// The first property chain to reference. This will be a expression pointing to a end property or field. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif public static IObservable WhenAnyValue( this TSender? sender, Expression> property1) @@ -62,10 +59,6 @@ namespace ReactiveUI /// /// The object where the property chain starts. /// The property name to observe. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif public static IObservable WhenAnyValue( this TSender? sender, string propertyName) @@ -84,10 +77,6 @@ namespace ReactiveUI /// The object where the property chain starts. /// The first property chain to reference. This will be a expression pointing to a end property or field. /// if set to true [is distinct]. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif public static IObservable WhenAnyValue( this TSender? sender, Expression> property1, @@ -99,10 +88,6 @@ namespace ReactiveUI /// /// AOT-friendly overload that avoids expression trees by using a property name and distinct option. /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif public static IObservable WhenAnyValue( this TSender? sender, string propertyName, @@ -133,10 +118,6 @@ namespace ReactiveUI <# for(int i=1; i <= length; i++) { #> /// The <#=i#> property chain to reference. This will be a expression pointing to a end property or field. <# } #> -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif public static IObservable<(<#= String.Join(",", templParams) #>)> WhenAnyValue>( this TSender? sender, <# for(int i=1; i <= length; i++) { #> @@ -153,10 +134,6 @@ namespace ReactiveUI <# if (length != 1 && length <= 7) { #>/// /// AOT-friendly tuple overloads using property names instead of expressions. /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif public static IObservable<(<#= String.Join(",", templParams) #>)> WhenAnyValue>( this TSender? sender, <# for(int i=1; i <= length; i++) { #> @@ -188,10 +165,6 @@ namespace ReactiveUI <# for(int i=1; i <= length; i++) { #> /// The <#=i#> property chain to reference. This will be a expression pointing to a end property or field. <# } #> -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif public static IObservable<(<#= String.Join(",", templParams) #>)> WhenAnyValue>( this TSender? sender, <# for(int i=1; i <= length; i++) { #> @@ -210,10 +183,6 @@ namespace ReactiveUI <# if (length != 1 && length <= 7) { #>/// /// AOT-friendly tuple overloads using property names with distinct option. /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif public static IObservable<(<#= String.Join(",", templParams) #>)> WhenAnyValue>( this TSender? sender, <# for(int i=1; i <= length; i++) { #> @@ -245,10 +214,6 @@ namespace ReactiveUI <# for(int i=1; i <= length; i++) { #> /// The <#=i#> property chain to reference. This will be a expression pointing to a end property or field. <# } #>/// The selector which will determine the final value from the properties. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif public static IObservable WhenAnyValue>( this TSender? sender, <# for(int i=1; i <= length; i++) { #> @@ -264,10 +229,6 @@ namespace ReactiveUI /// /// AOT-friendly selector overload using property names. /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif public static IObservable WhenAnyValue>( this TSender? sender, <# for(int i=1; i <= length; i++) { #> @@ -301,10 +262,6 @@ namespace ReactiveUI <# for(int i=1; i <= length; i++) { #> /// The <#=i#> property chain to reference. This will be a expression pointing to a end property or field. <# } #>/// The selector which will determine the final value from the properties. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif public static IObservable WhenAnyValue>( this TSender? sender, <# for(int i=1; i <= length; i++) { #> @@ -322,10 +279,6 @@ namespace ReactiveUI /// /// AOT-friendly selector overload using property names and distinct option. /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyValue uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyValue may reference members that could be trimmed in AOT scenarios.")] -#endif public static IObservable WhenAnyValue>( this TSender? sender, <# for(int i=1; i <= length; i++) { #> @@ -357,12 +310,8 @@ namespace ReactiveUI /// /// The object where the property chain starts. <# for(int i=1; i <= length; i++) { #> -/// The <#=i#> property chain to reference. This will be a expression pointing to a end property or field. + /// The <#=i#> property chain to reference. This will be a expression pointing to a end property or field. <# } #>/// The selector which will determine the final value from the properties. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAny uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAny may reference members that could be trimmed in AOT scenarios.")] -#endif public static IObservable WhenAny>( this TSender? sender, <# for(int i=1; i <= length; i++) { #> @@ -385,10 +334,6 @@ namespace ReactiveUI /// /// AOT-friendly WhenAny overload using property names. /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAny uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAny may reference members that could be trimmed in AOT scenarios.")] -#endif public static IObservable WhenAny>( this TSender? sender, <# for(int i=1; i <= length; i++) { #> @@ -421,10 +366,6 @@ namespace ReactiveUI <# for(int i=1; i <= length; i++) { #> /// The <#=i#> property chain to reference. This will be a expression pointing to a end property or field. <# } #>/// The selector which will determine the final value from the properties. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAny uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAny may reference members that could be trimmed in AOT scenarios.")] -#endif public static IObservable WhenAny>( this TSender? sender, <# for(int i=1; i <= length; i++) { #> @@ -448,10 +389,6 @@ namespace ReactiveUI /// /// AOT-friendly WhenAny overload using property names and distinct option. /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAny uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAny may reference members that could be trimmed in AOT scenarios.")] -#endif public static IObservable WhenAny>( this TSender? sender, <# for(int i=1; i <= length; i++) { #> @@ -484,10 +421,6 @@ namespace ReactiveUI <# for(int i=1; i <= length; i++) { #> /// The <#=i#> property chain to reference. This will be a expression pointing to a end property or field. <# } #>/// The selector which will determine the final value from the properties. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyDynamic uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyDynamic may reference members that could be trimmed in AOT scenarios.")] -#endif public static IObservable WhenAnyDynamic( this TSender? sender, <# for(int i=1; i <= length; i++) { #> @@ -521,10 +454,6 @@ namespace ReactiveUI /// The <#=i#> property chain to reference. This will be a expression pointing to a end property or field. <# } #>/// The selector which will determine the final value from the properties. /// if set to true [is distinct]. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyDynamic uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyDynamic may reference members that could be trimmed in AOT scenarios.")] -#endif public static IObservable WhenAnyDynamic( this TSender? sender, <# for(int i=1; i <= length; i++) { #> @@ -550,15 +479,12 @@ namespace ReactiveUI } /// A mixin which provides support for subscribing to observable properties. + [RequiresUnreferencedCode("Evaluates expression-based member chains via reflection; members may be trimmed.")] public static class WhenAnyObservableMixin { /// Observe a observable which is set to a property, and automatically subscribe to the most recent emitted value. /// The object where the property chain starts. /// The first observable to observe. -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyObservable uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyObservable may reference members that could be trimmed in AOT scenarios.")] -#endif public static IObservable WhenAnyObservable(this TSender? sender, Expression?>> obs1) where TSender : class { @@ -574,10 +500,6 @@ namespace ReactiveUI <# string paramsStr = String.Join(", ", Enumerable.Range(1, length).Select(x => "Expression?>> obs" + x.ToString())); #> <# string varsStr = String.Join(", ", Enumerable.Range(1, length).Select(x => "obs" + x.ToString())); #> <# string valsStr = String.Join(", ", Enumerable.Range(1, length).Select(x => "o" + x.ToString() + ".Value!.EmptyIfNull()")); #> -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyObservable uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyObservable may reference members that could be trimmed in AOT scenarios.")] -#endif public static IObservable WhenAnyObservable(this TSender? sender, <#= paramsStr #>) where TSender : class { @@ -597,10 +519,6 @@ namespace ReactiveUI <# string varsStr = String.Join(", ", Enumerable.Range(1, length).Select(x => "obs" + x.ToString())); #> <# string valsStr = String.Join(", ", Enumerable.Range(1, length).Select(x => "o" + x.ToString() + ".Value!.EmptyIfNull()")); #> <# string selectorTypeParams = String.Join(", ", templParams); #> -#if NET6_0_OR_GREATER - [RequiresDynamicCode("WhenAnyObservable uses expression trees which require dynamic code generation in AOT scenarios.")] - [RequiresUnreferencedCode("WhenAnyObservable may reference members that could be trimmed in AOT scenarios.")] -#endif public static IObservable WhenAnyObservable>(this TSender? sender, <# for(int i=1; i <= length; i++) { #> Expression>?>> obs<#=i#>, diff --git a/src/ReactiveUI/View/DefaultViewLocator.AOT.cs b/src/ReactiveUI/View/DefaultViewLocator.AOT.cs deleted file mode 100644 index eac6f5b29e..0000000000 --- a/src/ReactiveUI/View/DefaultViewLocator.AOT.cs +++ /dev/null @@ -1,69 +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. - -using System.Collections.Concurrent; - -namespace ReactiveUI; - -/// -/// Adds an AOT-friendly mapping configuration to DefaultViewLocator so callers can avoid reflection. -/// -public sealed partial class DefaultViewLocator -{ - // Keyed by (ViewModelType, Contract). Empty string represents default contract. - private readonly ConcurrentDictionary<(Type vmType, string contract), Func> _aotMappings = new(); - - /// - /// Registers a direct mapping from a view model type to a view factory. - /// This avoids reflection-based name lookup, improving AOT and trimming support. - /// - /// View model type. - /// View type. - /// Factory that builds the view. - /// Optional contract used to disambiguate views. - /// The locator for chaining. -#if NET6_0_OR_GREATER - [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Mapping does not use reflection")] - [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "Mapping does not use dynamic code")] -#endif - public DefaultViewLocator Map(Func factory, string? contract = null) - where TViewModel : class - where TView : class, IViewFor - { - ArgumentExceptionHelper.ThrowIfNull(factory); - _aotMappings[(typeof(TViewModel), contract ?? string.Empty)] = () => factory(); - return this; - } - - /// - /// Clears a previously registered mapping for an optional contract. - /// - /// View model type. - /// Optional contract to unmap. - /// The locator for chaining. - public DefaultViewLocator Unmap(string? contract = null) - where TViewModel : class - { - _ = _aotMappings.TryRemove((typeof(TViewModel), contract ?? string.Empty), out _); - return this; - } - - private IViewFor? TryResolveAOTMapping(Type viewModelType, string? contract) - { - // Try exact contract - if (_aotMappings.TryGetValue((viewModelType, contract ?? string.Empty), out var f)) - { - return f(); - } - - // Fallback to default contract if a specific contract was requested - if (!string.IsNullOrEmpty(contract) && _aotMappings.TryGetValue((viewModelType, string.Empty), out var fDefault)) - { - return fDefault(); - } - - return null; - } -} diff --git a/src/ReactiveUI/View/DefaultViewLocator.cs b/src/ReactiveUI/View/DefaultViewLocator.cs index e6487d385f..9ed6a22711 100644 --- a/src/ReactiveUI/View/DefaultViewLocator.cs +++ b/src/ReactiveUI/View/DefaultViewLocator.cs @@ -1,245 +1,212 @@ -// Copyright (c) 2025 .NET Foundation and Contributors. All rights reserved. +// 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.Concurrent; using System.Globalization; -using System.Reflection; namespace ReactiveUI; /// -/// Default implementation of that resolves views by convention (replacing "ViewModel" with "View"). +/// Default AOT-compatible implementation of that resolves views using compile-time registrations. /// /// /// -/// This locator queries Splat's service locator for a registered using several fallbacks, including -/// the exact view type, IViewFor<TViewModel>, and interface/class naming conversions. Override -/// to customize the name translation strategy. +/// This locator uses explicit view-to-viewmodel mappings registered via . +/// When no mapping is found, it falls back to querying the service locator for IViewFor<TViewModel>. +/// +/// +/// This implementation is fully AOT-compatible and does not use reflection or runtime type discovery. +/// All view-viewmodel associations must be registered at application startup. /// /// /// /// /// new LoginView(), typeof(IViewFor)); -/// +/// // Register views at startup /// var locator = new DefaultViewLocator(); -/// var view = locator.ResolveView(new LoginViewModel()); +/// locator.Map(() => new LoginView()) +/// .Map(() => new MainView()) +/// .Map(() => new SettingsView()); +/// +/// // Resolve at runtime (fully AOT-compatible) +/// var view = locator.ResolveView(); /// ]]> /// /// -public sealed partial class DefaultViewLocator : IViewLocator +public sealed class DefaultViewLocator : IViewLocator { + // Keyed by (ViewModelType, Contract). Empty string represents default contract. + private readonly ConcurrentDictionary<(Type vmType, string contract), Func> _aotMappings = new(); + /// /// Initializes a new instance of the class. /// - /// Custom mapping from view model type name to view type name. - internal DefaultViewLocator(Func? viewModelToViewFunc = null) => - ViewModelToViewFunc = viewModelToViewFunc ?? (static vm => vm.Replace("ViewModel", "View")); + internal DefaultViewLocator() + { + } + + /// + /// Registers a direct mapping from a view model type to a view factory. + /// This is the recommended way to register views for AOT-compatible applications. + /// + /// View model type. + /// View type that implements IViewFor<TViewModel>. + /// Factory function that creates the view instance. + /// Optional contract used to disambiguate multiple views for the same view model. + /// The locator for chaining. + /// + /// + /// (() => new LoginView()) + /// .Map(() => new MainView()); + /// ]]> + /// + /// + public DefaultViewLocator Map(Func factory, string? contract = null) + where TViewModel : class + where TView : class, IViewFor + { + ArgumentExceptionHelper.ThrowIfNull(factory); + _aotMappings[(typeof(TViewModel), contract ?? string.Empty)] = () => factory(); + return this; + } /// - /// Gets or sets the function used to convert a view model type name into a view type name during resolution. + /// Removes a previously registered view mapping. /// - /// - /// The view model to view function. - /// - public Func ViewModelToViewFunc { get; set; } + /// View model type to unmap. + /// Optional contract to unmap. If null, removes the default mapping. + /// The locator for chaining. + public DefaultViewLocator Unmap(string? contract = null) + where TViewModel : class + { + _ = _aotMappings.TryRemove((typeof(TViewModel), contract ?? string.Empty), out _); + return this; + } /// - /// Returns the view associated with a view model, deriving the name of the type via , then discovering it via the - /// service locator. + /// Resolves a view for a view model type known at compile time. Fully AOT-compatible. /// - /// The type. + /// The view model type to resolve a view for. + /// Optional contract to disambiguate between multiple views for the same view model. + /// The resolved view or when no registration is available. /// /// - /// Given view model type T with runtime type RT, this implementation will attempt to resolve the following views: + /// Resolution strategy: /// - /// - /// - /// Look for a service registered under the type whose name is given to us by passing RT to (which defaults to changing "ViewModel" to "View"). - /// - /// - /// - /// - /// Look for a service registered under the type IViewFor<RT>. - /// - /// - /// - /// - /// Look for a service registered under the type whose name is given to us by passing T to (which defaults to changing "ViewModel" to "View"). - /// - /// - /// - /// - /// Look for a service registered under the type IViewFor<T>. - /// - /// - /// - /// - /// If T is an interface, change its name to that of a class (i.e. drop the leading "I"). If it's a class, change to an interface (i.e. add a leading "I"). - /// - /// - /// - /// - /// Repeat steps 1-4 with the type resolved from the modified name. - /// - /// + /// Check for explicit mapping registered via with the specified contract. + /// Query the service locator for IViewFor<TViewModel> with the specified contract. + /// If a specific contract was requested and not found, fall back to the default contract (null). /// /// /// - /// - /// The view model whose associated view is to be resolved. - /// - /// - /// Optional contract to be used when resolving from Splat. - /// - /// - /// The view associated with the given view model. - /// -#if NET6_0_OR_GREATER - [RequiresDynamicCode("View resolution uses reflection and type discovery")] - [RequiresUnreferencedCode("View resolution may reference types that could be trimmed")] -#endif - public IViewFor? ResolveView(T? viewModel, string? contract = null) + public IViewFor? ResolveView(string? contract = null) + where TViewModel : class { - ArgumentExceptionHelper.ThrowIfNull(viewModel); - - var mapped = TryResolveAOTMapping(viewModel!.GetType(), contract); - if (mapped is not null) + // Check explicit AOT mappings first + if (_aotMappings.TryGetValue((typeof(TViewModel), contract ?? string.Empty), out var factory)) { - return mapped; + this.Log().Debug(CultureInfo.InvariantCulture, "Resolved IViewFor<{0}> from explicit mapping", typeof(TViewModel).Name); + return (IViewFor)factory(); } - var view = AttemptViewResolutionFor(viewModel!.GetType(), contract) - ?? AttemptViewResolutionFor(typeof(T), contract) - ?? AttemptViewResolutionFor(ToggleViewModelType(viewModel.GetType()), contract) - ?? AttemptViewResolutionFor(ToggleViewModelType(typeof(T)), contract); - + // Fallback to service locator (still AOT-compatible as it uses generics) + var view = AppLocator.Current?.GetService>(contract); if (view is not null) { + this.Log().Debug(CultureInfo.InvariantCulture, "Resolved IViewFor<{0}> via service locator", typeof(TViewModel).Name); return view; } - this.Log().Warn(CultureInfo.InvariantCulture, "Failed to resolve view for view model type '{0}'.", typeof(T).FullName); - return null; - } - -#if NET6_0_OR_GREATER - [RequiresDynamicCode("Type resolution requires dynamic code generation")] - [RequiresUnreferencedCode("Type resolution may reference types that could be trimmed")] -#endif - private static Type? ToggleViewModelType(Type viewModelType) - { - var viewModelTypeName = viewModelType.AssemblyQualifiedName; - - if (viewModelTypeName is null) + // If specific contract requested, try default contract + if (!string.IsNullOrEmpty(contract)) { - return null; - } + if (_aotMappings.TryGetValue((typeof(TViewModel), string.Empty), out var defaultFactory)) + { + this.Log().Debug(CultureInfo.InvariantCulture, "Resolved IViewFor<{0}> from default mapping as fallback", typeof(TViewModel).Name); + return (IViewFor)defaultFactory(); + } - if (viewModelType.GetTypeInfo().IsInterface) - { -#if NET6_0_OR_GREATER - if (viewModelType.Name.StartsWith('I')) -#else - if (viewModelType.Name.StartsWith("I", StringComparison.InvariantCulture)) -#endif + var defaultView = AppLocator.Current?.GetService>(); + if (defaultView is not null) { - var toggledTypeName = DeinterfaceifyTypeName(viewModelTypeName); - return Reflection.ReallyFindType(toggledTypeName, throwOnFailure: false); + this.Log().Debug(CultureInfo.InvariantCulture, "Resolved IViewFor<{0}> via service locator (default) as fallback", typeof(TViewModel).Name); + return defaultView; } } - else - { - var toggledTypeName = InterfaceifyTypeName(viewModelTypeName); - return Reflection.ReallyFindType(toggledTypeName, throwOnFailure: false); - } + this.Log().Warn(CultureInfo.InvariantCulture, "Failed to resolve view for {0}. Use Map() or register IViewFor in the service locator.", typeof(TViewModel).Name); return null; } - private static string DeinterfaceifyTypeName(string typeName) + /// + [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 IViewFor? ResolveView(object? instance, string? contract = null) { - var idxComma = typeName.IndexOf(',', 0); - var idxPeriod = typeName.LastIndexOf('.', idxComma - 1); -#if NET6_0_OR_GREATER - return string.Concat(typeName.AsSpan(0, idxPeriod + 1), typeName.AsSpan(idxPeriod + 2)); -#else - return typeName.Substring(0, idxPeriod + 1) + typeName.Substring(idxPeriod + 2); -#endif - } - - private static string InterfaceifyTypeName(string typeName) - { - var idxComma = typeName.IndexOf(',', 0); - var idxPeriod = typeName.LastIndexOf('.', idxComma - 1); - return typeName.Insert(idxPeriod + 1, "I"); - } - -#if NET6_0_OR_GREATER - [RequiresDynamicCode("View resolution uses reflection and type discovery")] - [RequiresUnreferencedCode("View resolution may reference types that could be trimmed")] -#endif - private IViewFor? AttemptViewResolutionFor(Type? viewModelType, string? contract) - { - if (viewModelType is null) + if (instance is null) { return null; } - var viewModelTypeName = viewModelType.AssemblyQualifiedName; + var vmType = instance.GetType(); + var key = (vmType, contract ?? string.Empty); - if (viewModelTypeName is null) + // 1) Explicit AOT mappings first (no reflection beyond GetType()). + if (_aotMappings.TryGetValue(key, out var factory)) { - return null; + var view = factory(); + if (view is IViewFor viewFor) + { + viewFor.ViewModel = instance; + } + + return view as IViewFor; } - var proposedViewTypeName = ViewModelToViewFunc(viewModelTypeName); - var view = AttemptViewResolution(proposedViewTypeName, contract); + // 2) Fallback to service locator via runtime-constructed service type. + // Note: this uses MakeGenericType and is the reason for the RUC attribute. + var serviceType = typeof(IViewFor<>).MakeGenericType(vmType); + var resolved = AppLocator.Current?.GetService(serviceType, contract); - if (view is not null) + if (resolved is IViewFor resolvedViewFor) { - return view; + resolvedViewFor.ViewModel = instance; } - proposedViewTypeName = typeof(IViewFor<>).MakeGenericType(viewModelType).AssemblyQualifiedName; - return AttemptViewResolution(proposedViewTypeName, contract); - } - -#if NET6_0_OR_GREATER - [RequiresDynamicCode("View resolution uses reflection and type discovery")] - [RequiresUnreferencedCode("View resolution may reference types that could be trimmed")] -#endif - private IViewFor? AttemptViewResolution(string? viewTypeName, string? contract) - { - try + if (resolved is IViewFor typedResolved) { - var viewType = Reflection.ReallyFindType(viewTypeName, throwOnFailure: false); - if (viewType is null) - { - return null; - } + return typedResolved; + } - var service = AppLocator.Current?.GetService(viewType, contract); + // 3) If a specific contract was requested, try default contract as fallback. + if (!string.IsNullOrEmpty(contract)) + { + var defaultKey = (vmType, string.Empty); - if (service is null) + if (_aotMappings.TryGetValue(defaultKey, out var defaultFactory)) { - return null; + var view = defaultFactory(); + if (view is IViewFor viewFor) + { + viewFor.ViewModel = instance; + } + + return view as IViewFor; } - if (service is not IViewFor view) + var defaultResolved = AppLocator.Current?.GetService(serviceType); + + if (defaultResolved is IViewFor defaultResolvedViewFor) { - return null; + defaultResolvedViewFor.ViewModel = instance; } - this.Log().Debug(CultureInfo.InvariantCulture, "Resolved service type '{0}'", viewType.FullName); - - return view; - } - catch (Exception ex) - { - this.Log().Error(ex, $"Exception occurred whilst attempting to resolve type {viewTypeName} into a view."); - throw; + return defaultResolved as IViewFor; } + + return null; } } diff --git a/src/ReactiveUI/View/ViewMappingBuilder.cs b/src/ReactiveUI/View/ViewMappingBuilder.cs new file mode 100644 index 0000000000..7b44d83fef --- /dev/null +++ b/src/ReactiveUI/View/ViewMappingBuilder.cs @@ -0,0 +1,76 @@ +// 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. + +namespace ReactiveUI; + +/// +/// Fluent builder for registering AOT-compatible view-to-viewmodel mappings. +/// +public sealed class ViewMappingBuilder +{ + private readonly DefaultViewLocator _locator; + + /// + /// Initializes a new instance of the class. + /// + /// The view locator to register mappings with. + internal ViewMappingBuilder(DefaultViewLocator locator) + { + ArgumentExceptionHelper.ThrowIfNull(locator); + _locator = locator; + } + + /// + /// Maps a view model type to a view type with automatic instantiation. + /// The view must have a parameterless constructor. + /// + /// The view model type. + /// The view type implementing IViewFor<TViewModel>. + /// Optional contract to disambiguate multiple views for the same view model. + /// The builder for chaining. + public ViewMappingBuilder Map(string? contract = null) + where TViewModel : class + where TView : class, IViewFor, new() + { + _locator.Map(() => new TView(), contract); + return this; + } + + /// + /// Maps a view model type to a view type with a custom factory function. + /// Use this when the view requires constructor parameters or custom initialization. + /// + /// The view model type. + /// The view type implementing IViewFor<TViewModel>. + /// Factory function that creates the view. + /// Optional contract to disambiguate multiple views for the same view model. + /// The builder for chaining. + public ViewMappingBuilder Map(Func factory, string? contract = null) + where TViewModel : class + where TView : class, IViewFor + { + ArgumentExceptionHelper.ThrowIfNull(factory); + _locator.Map(factory, contract); + return this; + } + + /// + /// Maps a view model type to a view resolved from the service locator. + /// The view must be registered in the dependency injection container. + /// + /// The view model type. + /// The view type implementing IViewFor<TViewModel>. + /// Optional contract to disambiguate multiple views for the same view model. + /// The builder for chaining. + public ViewMappingBuilder MapFromServiceLocator(string? contract = null) + where TViewModel : class + where TView : class, IViewFor + { + _locator.Map( + () => AppLocator.Current.GetService() ?? throw new InvalidOperationException($"View {typeof(TView).Name} not registered in service locator"), + contract); + return this; + } +} diff --git a/src/RxUI.DotSettings b/src/RxUI.DotSettings deleted file mode 100644 index 314a4a12e0..0000000000 --- a/src/RxUI.DotSettings +++ /dev/null @@ -1,30 +0,0 @@ - - True - True - True - True - True - True - True - True - END_OF_LINE - False - False - False - END_OF_LINE - NEVER - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy><Descriptor Staticness="Instance" AccessRightKinds="Private" Description="Instance fields (private)"><ElementKinds><Kind Name="FIELD" /><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="aaBb" /></Policy> - <Policy><Descriptor Staticness="Static, Instance" AccessRightKinds="Private" Description="Private Fields/Methods/Properties"><ElementKinds><Kind Name="FIELD" /><Kind Name="READONLY_FIELD" /><Kind Name="CONSTANT_FIELD" /><Kind Name="METHOD" /><Kind Name="PROPERTY" /><Kind Name="EVENT" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></Policy> - <Policy><Descriptor Staticness="Static" AccessRightKinds="Private" Description="Static fields (private)"><ElementKinds><Kind Name="FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="aaBb" /></Policy> - True - True - True - True - True - True - True - True - True - True \ No newline at end of file diff --git a/src/examples/ReactiveUI.Builder.WpfApp/App.xaml.cs b/src/examples/ReactiveUI.Builder.WpfApp/App.xaml.cs index f19f6ba186..1bb3998849 100644 --- a/src/examples/ReactiveUI.Builder.WpfApp/App.xaml.cs +++ b/src/examples/ReactiveUI.Builder.WpfApp/App.xaml.cs @@ -6,6 +6,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; +using System.Reactive; using System.Reactive.Linq; using System.Windows; @@ -40,6 +41,17 @@ protected override void OnStartup(StartupEventArgs e) ////.RegisterView() ////.RegisterView() ////.RegisterView() + .WithSuspensionHost() // Configure typed suspension host + .WithCacheSizes(smallCacheLimit: 100, bigCacheLimit: 400) // Customize cache sizes + .WithExceptionHandler(Observer.Create(static ex => + { + // Custom exception handler - log unhandled reactive errors + Trace.WriteLine($"[ReactiveUI] Unhandled exception: {ex}"); + if (Debugger.IsAttached) + { + Debugger.Break(); + } + })) .WithRegistration(static r => { // Register IScreen as a singleton so all resolutions share the same Router @@ -60,7 +72,7 @@ protected override void OnStartup(StartupEventArgs e) .BuildApp(); // Setup Suspension - RxApp.SuspensionHost.CreateNewAppState = static () => new ChatState(); + RxSuspension.SuspensionHost.CreateNewAppState = static () => new ChatState(); var statePath = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), @@ -71,7 +83,7 @@ protected override void OnStartup(StartupEventArgs e) _driver = new Services.FileJsonSuspensionDriver(statePath); // Set an initial state instantly to avoid blocking UI - RxApp.SuspensionHost.AppState = new ChatState(); + RxSuspension.SuspensionHost.AppState = new ChatState(); // Load persisted state asynchronously and update UI when ready _ = _driver @@ -80,7 +92,7 @@ protected override void OnStartup(StartupEventArgs e) .Subscribe( static stateObj => { - RxApp.SuspensionHost.AppState = stateObj; + RxSuspension.SuspensionHost.AppState = stateObj; MessageBus.Current.SendMessage(new ChatStateChanged()); Trace.WriteLine("[App] State loaded"); }, @@ -125,9 +137,9 @@ protected override void OnExit(ExitEventArgs e) Trace.WriteLine($"[App] Instance exiting. Remaining={remaining} Id={Services.AppInstance.Id}"); // Only the last instance persists the final state to the central store - if (remaining == 0 && _driver is not null && RxApp.SuspensionHost.AppState is not null) + if (remaining == 0 && _driver is not null && RxSuspension.SuspensionHost.AppState is not null) { - _driver.SaveState(RxApp.SuspensionHost.AppState).Wait(); + _driver.SaveState(RxSuspension.SuspensionHost.AppState).Wait(); } } finally diff --git a/src/examples/ReactiveUI.Builder.WpfApp/Services/ChatNetworkService.cs b/src/examples/ReactiveUI.Builder.WpfApp/Services/ChatNetworkService.cs index 85a29a8ddd..57e107b3f0 100644 --- a/src/examples/ReactiveUI.Builder.WpfApp/Services/ChatNetworkService.cs +++ b/src/examples/ReactiveUI.Builder.WpfApp/Services/ChatNetworkService.cs @@ -50,13 +50,13 @@ public ChatNetworkService() // Outgoing chat messages (default contract) - only send messages originating from this instance MessageBus.Current.Listen() .Where(static m => m.InstanceId == AppInstance.Id) - .ObserveOn(RxApp.TaskpoolScheduler) + .ObserveOn(RxSchedulers.TaskpoolScheduler) .Subscribe(Send); // Outgoing room events - only send messages originating from this instance MessageBus.Current.Listen(contract: RoomsContract) .Where(static m => m.InstanceId == AppInstance.Id) - .ObserveOn(RxApp.TaskpoolScheduler) + .ObserveOn(RxSchedulers.TaskpoolScheduler) .Subscribe(Send); Trace.WriteLine("[Net] ChatNetworkService initialized."); diff --git a/src/examples/ReactiveUI.Builder.WpfApp/Services/FileJsonSuspensionDriver.cs b/src/examples/ReactiveUI.Builder.WpfApp/Services/FileJsonSuspensionDriver.cs index 49cab743f5..08f329951b 100644 --- a/src/examples/ReactiveUI.Builder.WpfApp/Services/FileJsonSuspensionDriver.cs +++ b/src/examples/ReactiveUI.Builder.WpfApp/Services/FileJsonSuspensionDriver.cs @@ -7,6 +7,7 @@ using System.Reactive; using System.Reactive.Linq; using System.Text.Json; +using System.Text.Json.Serialization.Metadata; using ReactiveUI.Builder.WpfApp.Models; @@ -38,7 +39,7 @@ public IObservable InvalidateState() => Observable.Start( File.Delete(path); } }, - RxApp.TaskpoolScheduler); + RxSchedulers.TaskpoolScheduler); /// /// Loads the application state from persistent storage. @@ -46,7 +47,7 @@ public IObservable InvalidateState() => Observable.Start( /// /// An object observable. /// - public IObservable LoadState() => Observable.Start( + public IObservable LoadState() => Observable.Start( () => { if (!File.Exists(path)) @@ -57,20 +58,55 @@ public IObservable LoadState() => Observable.Start( var json = File.ReadAllText(path); return JsonSerializer.Deserialize(json) ?? new ChatState(); }, - RxApp.TaskpoolScheduler); + RxSchedulers.TaskpoolScheduler); + + /// + /// Loads the application state from persistent storage using source-generated JSON metadata. + /// + /// The type of state to load. + /// The source-generated JSON type info. + /// An observable that produces the deserialized state. + public IObservable LoadState(JsonTypeInfo typeInfo) => Observable.Start( + () => + { + if (!File.Exists(path)) + { + return default(T); + } + + var json = File.ReadAllText(path); + return JsonSerializer.Deserialize(json, typeInfo); + }, + RxSchedulers.TaskpoolScheduler); /// /// Saves the application state to disk. /// + /// The type of state to save. /// The application state. /// /// A completed observable. /// - public IObservable SaveState(object state) => Observable.Start( + public IObservable SaveState(T state) => Observable.Start( () => { var json = JsonSerializer.Serialize(state, _options); File.WriteAllText(path, json); }, - RxApp.TaskpoolScheduler); + RxSchedulers.TaskpoolScheduler); + + /// + /// Saves the application state to disk using source-generated JSON metadata. + /// + /// The type of state to save. + /// The application state. + /// The source-generated JSON type info. + /// A completed observable. + public IObservable SaveState(T state, JsonTypeInfo typeInfo) => Observable.Start( + () => + { + var json = JsonSerializer.Serialize(state, typeInfo); + File.WriteAllText(path, json); + }, + RxSchedulers.TaskpoolScheduler); } diff --git a/src/examples/ReactiveUI.Builder.WpfApp/ViewModels/LobbyViewModel.cs b/src/examples/ReactiveUI.Builder.WpfApp/ViewModels/LobbyViewModel.cs index 5c9fa7e2fc..1c5b7e9e78 100644 --- a/src/examples/ReactiveUI.Builder.WpfApp/ViewModels/LobbyViewModel.cs +++ b/src/examples/ReactiveUI.Builder.WpfApp/ViewModels/LobbyViewModel.cs @@ -71,7 +71,7 @@ public LobbyViewModel(IScreen hostScreen) .Select(_ => Unit.Default); RoomsChanged = localRoomsChanged.Merge(remoteRoomsChanged) - .Throttle(TimeSpan.FromMilliseconds(50), RxApp.TaskpoolScheduler); + .Throttle(TimeSpan.FromMilliseconds(50), RxSchedulers.TaskpoolScheduler); this.WhenAnyObservable(x => x.RoomsChanged) .StartWith(Unit.Default) @@ -138,7 +138,7 @@ public string RoomName /// public ReactiveCommand JoinRoom { get; } - private static ChatState GetState() => RxApp.SuspensionHost.GetAppState(); + private static ChatState GetState() => RxSuspension.SuspensionHost.GetAppState(); private static void ApplyRoomEvent(Services.RoomEventMessage evt) { diff --git a/src/reactiveui.slnx b/src/reactiveui.slnx new file mode 100644 index 0000000000..d8468b595c --- /dev/null +++ b/src/reactiveui.slnx @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/tests/ReactiveUI.AOTTests/AOTCompatibilityTests.cs b/src/tests/ReactiveUI.AOTTests/AOTCompatibilityTests.cs index 14b6f19a59..d6a8e46cfb 100644 --- a/src/tests/ReactiveUI.AOTTests/AOTCompatibilityTests.cs +++ b/src/tests/ReactiveUI.AOTTests/AOTCompatibilityTests.cs @@ -36,8 +36,6 @@ public async Task ReactiveObject_PropertyChanges_WorksInAOT() /// /// A representing the asynchronous operation. [Test] - [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Testing AOT-incompatible ReactiveCommand.Create method")] - [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "Testing AOT-incompatible ReactiveCommand.Create method")] public async Task ReactiveCommand_Create_WorksInAOT() { var executed = false; @@ -53,8 +51,6 @@ public async Task ReactiveCommand_Create_WorksInAOT() /// /// A representing the asynchronous operation. [Test] - [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Testing AOT-incompatible ReactiveCommand.Create method")] - [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "Testing AOT-incompatible ReactiveCommand.Create method")] public async Task ReactiveCommand_CreateWithParameter_WorksInAOT() { string? result = null; @@ -70,8 +66,6 @@ public async Task ReactiveCommand_CreateWithParameter_WorksInAOT() /// /// A representing the asynchronous operation. [Test] - [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Testing ToProperty with string-based property names which requires AOT suppression")] - [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "Testing ToProperty with string-based property names which requires AOT suppression")] public async Task ObservableAsPropertyHelper_WorksInAOT() { var obj = new TestReactiveObject(); @@ -88,8 +82,6 @@ public async Task ObservableAsPropertyHelper_WorksInAOT() /// /// A representing the asynchronous operation. [Test] - [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Testing WhenAnyValue which requires AOT suppression")] - [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "Testing WhenAnyValue which requires AOT suppression")] public async Task WhenAnyValue_StringPropertyNames_WorksInAOT() { var obj = new TestReactiveObject(); @@ -153,8 +145,6 @@ public async Task INPCPropertyObservation_WorksInAOT() /// /// A representing the asynchronous operation. [Test] - [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Testing AOT-incompatible ReactiveCommand.CreateFromObservable method")] - [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "Testing AOT-incompatible ReactiveCommand.CreateFromObservable method")] public async Task ReactiveCommand_CreateFromObservable_WorksInAOT() { var command = ReactiveCommand.CreateFromObservable(() => Observable.Return(42)); @@ -170,8 +160,6 @@ public async Task ReactiveCommand_CreateFromObservable_WorksInAOT() /// /// A representing the asynchronous operation. [Test] - [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Testing ToProperty with string-based property names which requires AOT suppression")] - [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "Testing ToProperty with string-based property names which requires AOT suppression")] public async Task StringBasedPropertyBinding_WorksInAOT() { var obj = new TestReactiveObject(); diff --git a/src/tests/ReactiveUI.AOTTests/AdvancedAOTTests.cs b/src/tests/ReactiveUI.AOTTests/AdvancedAOTTests.cs index e39fa5ddfc..b9f986064b 100644 --- a/src/tests/ReactiveUI.AOTTests/AdvancedAOTTests.cs +++ b/src/tests/ReactiveUI.AOTTests/AdvancedAOTTests.cs @@ -21,8 +21,6 @@ public class AdvancedAOTTests /// /// A representing the asynchronous operation. [Test] - [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Testing AOT-incompatible RoutingState which uses ReactiveCommand")] - [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "Testing AOT-incompatible RoutingState which uses ReactiveCommand")] public async Task RoutingState_Navigation_WorksInAOT() { var routingState = new RoutingState(); @@ -40,8 +38,6 @@ public async Task RoutingState_Navigation_WorksInAOT() /// /// A representing the asynchronous operation. [Test] - [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Testing ReactiveProperty constructor that uses RxApp")] - [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "Testing ReactiveProperty constructor that uses RxApp")] public async Task PropertyValidation_WorksInAOT() { var property = new ReactiveProperty(string.Empty, ImmediateScheduler.Instance, false, false); @@ -61,8 +57,6 @@ public async Task PropertyValidation_WorksInAOT() /// /// A representing the asynchronous operation. [Test] - [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Testing ReactiveProperty constructor that uses RxApp")] - [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "Testing ReactiveProperty constructor that uses RxApp")] public async Task ViewModelActivation_WorksInAOT() { var viewModel = new TestActivatableViewModel(); @@ -87,8 +81,6 @@ public async Task ViewModelActivation_WorksInAOT() /// /// A representing the asynchronous operation. [Test] - [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Testing ToProperty which requires AOT suppression")] - [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "Testing ToProperty which requires AOT suppression")] public async Task ObservableAsPropertyHelper_Lifecycle_WorksInAOT() { var testObject = new TestReactiveObject(); diff --git a/src/tests/ReactiveUI.AOTTests/AssemblyHooks.cs b/src/tests/ReactiveUI.AOTTests/AssemblyHooks.cs index ae255b33b9..4c0b2ced7a 100644 --- a/src/tests/ReactiveUI.AOTTests/AssemblyHooks.cs +++ b/src/tests/ReactiveUI.AOTTests/AssemblyHooks.cs @@ -4,6 +4,8 @@ // See the LICENSE file in the project root for full license information. using System; +using ReactiveUI.Builder; +using Splat; using TUnit.Core; namespace ReactiveUI.AOTTests; @@ -21,6 +23,10 @@ public static void AssemblySetup() { // Override ModeDetector to ensure we're detected as being in a unit test runner ModeDetector.OverrideModeDetector(new TestModeDetector()); + + RxAppBuilder.CreateReactiveUIBuilder() + .WithCoreServices() + .BuildApp(); } /// diff --git a/src/tests/ReactiveUI.AOTTests/ComprehensiveAOTMarkupTests.cs b/src/tests/ReactiveUI.AOTTests/ComprehensiveAOTMarkupTests.cs index e1cb306b8f..81cdc249c9 100644 --- a/src/tests/ReactiveUI.AOTTests/ComprehensiveAOTMarkupTests.cs +++ b/src/tests/ReactiveUI.AOTTests/ComprehensiveAOTMarkupTests.cs @@ -20,8 +20,6 @@ public class ComprehensiveAOTMarkupTests /// /// A representing the asynchronous operation. [Test] - [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Testing ReactiveObject AOT-incompatible constructor")] - [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "Testing ReactiveObject AOT-incompatible constructor")] public async Task ReactiveObject_Constructor_WorksWithAOTSuppression() { var obj = new TestReactiveObject(); @@ -42,8 +40,6 @@ public async Task ReactiveObject_Constructor_WorksWithAOTSuppression() /// /// A representing the asynchronous operation. [Test] - [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Testing ReactiveProperty Refresh AOT-incompatible method")] - [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "Testing ReactiveProperty Refresh AOT-incompatible method")] public async Task ReactiveProperty_Refresh_WorksWithAOTSuppression() { var scheduler = CurrentThreadScheduler.Instance; @@ -66,8 +62,6 @@ public async Task ReactiveProperty_Refresh_WorksWithAOTSuppression() /// /// A representing the asynchronous operation. [Test] - [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Testing ReactiveProperty which requires AOT suppression")] - [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "Testing ReactiveProperty which requires AOT suppression")] public async Task PlatformSpecific_AOTMarkup_IsProperlyApplied() { // This test validates that platform-specific code has AOT attributes @@ -87,8 +81,6 @@ public async Task PlatformSpecific_AOTMarkup_IsProperlyApplied() /// /// A representing the asynchronous operation. [Test] - [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Testing comprehensive ReactiveProperty AOT scenarios")] - [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "Testing comprehensive ReactiveProperty AOT scenarios")] public async Task ReactiveProperty_ComprehensiveOperations_WorkWithAOT() { var scheduler = CurrentThreadScheduler.Instance; @@ -126,8 +118,6 @@ public async Task ReactiveProperty_ComprehensiveOperations_WorkWithAOT() /// /// A representing the asynchronous operation. [Test] - [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Testing mixed AOT scenario")] - [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "Testing mixed AOT scenario")] public async Task MixedAOTScenario_ComplexWorkflow_WorksCorrectly() { var scheduler = CurrentThreadScheduler.Instance; @@ -172,8 +162,6 @@ public async Task MixedAOTScenario_ComplexWorkflow_WorksCorrectly() /// /// A representing the asynchronous operation. [Test] - [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Testing ToProperty which requires AOT suppression")] - [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "Testing ToProperty which requires AOT suppression")] public async Task ObservableAsPropertyHelper_AOTCompatibleUsage_Works() { var obj = new TestReactiveObject(); @@ -199,8 +187,6 @@ public async Task ObservableAsPropertyHelper_AOTCompatibleUsage_Works() /// /// A representing the asynchronous operation. [Test] - [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Testing ReactiveProperty in AOT scenario with proper suppression")] - [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "Testing ReactiveProperty in AOT scenario with proper suppression")] public async Task DependencyInjection_AOTCompatiblePatterns_Work() { var resolver = Locator.CurrentMutable; @@ -239,8 +225,6 @@ public async Task DependencyInjection_AOTCompatiblePatterns_Work() /// /// A representing the asynchronous operation. [Test] - [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Testing ReactiveProperty in AOT scenario with proper suppression")] - [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "Testing ReactiveProperty in AOT scenario with proper suppression")] public async Task ViewModelActivation_AOTCompatible_WorksCorrectly() { var viewModel = new TestActivatableViewModel(); @@ -296,8 +280,6 @@ public async Task ViewModelActivation_AOTCompatible_WorksCorrectly() /// /// A representing the asynchronous operation. [Test] - [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Testing error handling in AOT scenario")] - [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "Testing error handling in AOT scenario")] public async Task ErrorHandling_AOTScenarios_WorkCorrectly() { var scheduler = CurrentThreadScheduler.Instance; diff --git a/src/tests/ReactiveUI.AOTTests/ComprehensiveAOTTests.cs b/src/tests/ReactiveUI.AOTTests/ComprehensiveAOTTests.cs index 9edcfd8581..cfe5509d02 100644 --- a/src/tests/ReactiveUI.AOTTests/ComprehensiveAOTTests.cs +++ b/src/tests/ReactiveUI.AOTTests/ComprehensiveAOTTests.cs @@ -23,8 +23,6 @@ public class ComprehensiveAOTTests /// /// A representing the asynchronous operation. [Test] - [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Testing ToProperty with string-based property names which requires AOT suppression")] - [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "Testing ToProperty with string-based property names which requires AOT suppression")] public async Task AOTCompatiblePatterns_WorkCorrectly() { // Use string-based property names for ToProperty (AOT-compatible) @@ -93,8 +91,6 @@ public async Task MessageBus_WorksInAOT() /// /// A representing the asynchronous operation. [Test] - [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Demonstrating proper suppression of AOT warnings for ReactiveCommand")] - [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "Demonstrating proper suppression of AOT warnings for ReactiveCommand")] public async Task ReactiveCommand_WithProperSuppression_WorksInAOT() { var executed = false; @@ -115,8 +111,6 @@ public async Task ReactiveCommand_WithProperSuppression_WorksInAOT() /// /// A representing the asynchronous operation. [Test] - [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Testing ReactiveProperty with explicit scheduler which requires AOT suppression")] - [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "Testing ReactiveProperty with explicit scheduler which requires AOT suppression")] public async Task ReactiveProperty_WithExplicitScheduler_WorksInAOT() { // Use explicit scheduler to avoid RxApp dependency (AOT-friendly) @@ -141,8 +135,6 @@ public async Task ReactiveProperty_WithExplicitScheduler_WorksInAOT() /// /// A representing the asynchronous operation. [Test] - [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Testing ToProperty with string-based property names which requires AOT suppression")] - [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "Testing ToProperty with string-based property names which requires AOT suppression")] public async Task ObservableAsPropertyHelper_StringBased_WorksInAOT() { var obj = new TestReactiveObject(); @@ -190,8 +182,6 @@ public async Task DependencyInjection_BasicUsage_WorksInAOT() /// /// A representing the asynchronous operation. [Test] - [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Testing ReactiveProperty constructor that uses RxApp")] - [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "Testing ReactiveProperty constructor that uses RxApp")] public async Task ViewModelActivation_PatternWorks_InAOT() { var viewModel = new TestActivatableViewModel(); diff --git a/src/tests/ReactiveUI.AOTTests/FinalAOTValidationTests.cs b/src/tests/ReactiveUI.AOTTests/FinalAOTValidationTests.cs index 816a67ebaa..2ed2f4dde1 100644 --- a/src/tests/ReactiveUI.AOTTests/FinalAOTValidationTests.cs +++ b/src/tests/ReactiveUI.AOTTests/FinalAOTValidationTests.cs @@ -21,8 +21,6 @@ public class FinalAOTValidationTests /// /// A representing the asynchronous operation. [Test] - [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Testing ReactiveProperty in AOT scenario with proper suppression")] - [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "Testing ReactiveProperty in AOT scenario with proper suppression")] public async Task CompleteAOTCompatibleWorkflow_WorksSeamlessly() { // 1. Create objects using AOT-compatible patterns @@ -72,8 +70,6 @@ public async Task CompleteAOTCompatibleWorkflow_WorksSeamlessly() /// /// A representing the asynchronous operation. [Test] - [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Demonstrating ReactiveCommand usage with proper AOT suppression")] - [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "Demonstrating ReactiveCommand usage with proper AOT suppression")] public async Task ReactiveCommand_CompleteWorkflow_WorksWithSuppression() { // Use CurrentThreadScheduler to ensure synchronous execution @@ -122,8 +118,6 @@ public async Task ReactiveCommand_CompleteWorkflow_WorksWithSuppression() /// /// A representing the asynchronous operation. [Test] - [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Testing mixed AOT scenario with ReactiveCommand")] - [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "Testing mixed AOT scenario with ReactiveCommand")] public async Task MixedAOTScenario_ComplexApplication_Works() { var scheduler = CurrentThreadScheduler.Instance; @@ -169,8 +163,6 @@ public async Task MixedAOTScenario_ComplexApplication_Works() /// /// A representing the asynchronous operation. [Test] - [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Testing ReactiveProperty in AOT scenario with proper suppression")] - [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "Testing ReactiveProperty in AOT scenario with proper suppression")] public async Task DependencyInjection_AdvancedScenarios_WorkInAOT() { var resolver = Locator.CurrentMutable; @@ -200,8 +192,6 @@ public async Task DependencyInjection_AdvancedScenarios_WorkInAOT() /// /// A representing the asynchronous operation. [Test] - [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Testing ReactiveProperty in AOT scenario with proper suppression")] - [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "Testing ReactiveProperty in AOT scenario with proper suppression")] public async Task ErrorHandlingAndDisposal_PatternsWork_InAOT() { var disposables = new CompositeDisposable(); diff --git a/src/tests/ReactiveUI.AOTTests/TestReactiveObject.cs b/src/tests/ReactiveUI.AOTTests/TestReactiveObject.cs index 50247cbe92..27807b7af2 100644 --- a/src/tests/ReactiveUI.AOTTests/TestReactiveObject.cs +++ b/src/tests/ReactiveUI.AOTTests/TestReactiveObject.cs @@ -16,8 +16,6 @@ public class TestReactiveObject : ReactiveObject /// /// Initializes a new instance of the class. /// - [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "AOT compatibility tests deliberately use AOT-incompatible methods to test suppression scenarios")] - [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "AOT compatibility tests deliberately use AOT-incompatible methods to test suppression scenarios")] public TestReactiveObject() { _computedProperty = this.WhenAnyValue(static x => x.TestProperty) @@ -31,8 +29,6 @@ public TestReactiveObject() public string? TestProperty { get => _testProperty; - [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "AOT compatibility tests deliberately use AOT-incompatible methods to test suppression scenarios")] - [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "AOT compatibility tests deliberately use AOT-incompatible methods to test suppression scenarios")] set => this.RaiseAndSetIfChanged(ref _testProperty, value); } diff --git a/src/tests/ReactiveUI.AOTTests/ViewLocatorAOTMappingTests.cs b/src/tests/ReactiveUI.AOTTests/ViewLocatorAOTMappingTests.cs index c2a6296b59..55aae10d0a 100644 --- a/src/tests/ReactiveUI.AOTTests/ViewLocatorAOTMappingTests.cs +++ b/src/tests/ReactiveUI.AOTTests/ViewLocatorAOTMappingTests.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 ReactiveUI.Builder; + namespace ReactiveUI.AOTTests; /// @@ -10,14 +12,6 @@ namespace ReactiveUI.AOTTests; /// public class ViewLocatorAOTMappingTests { - /// - /// Initializes a new instance of the class. - /// - public ViewLocatorAOTMappingTests() - { - RxApp.EnsureInitialized(); - } - /// /// Map/Resolve with contract and default fallback works. /// diff --git a/src/tests/ReactiveUI.Blazor.Tests/BlazorReactiveUIBuilderExtensionsTests.cs b/src/tests/ReactiveUI.Blazor.Tests/BlazorReactiveUIBuilderExtensionsTests.cs index fe18fe9ffb..586348d446 100644 --- a/src/tests/ReactiveUI.Blazor.Tests/BlazorReactiveUIBuilderExtensionsTests.cs +++ b/src/tests/ReactiveUI.Blazor.Tests/BlazorReactiveUIBuilderExtensionsTests.cs @@ -235,5 +235,13 @@ public IReactiveUIBuilder UsingSplatModule(T registrationModule) public IReactiveUIInstance WithInstance(Action action) => throw new NotImplementedException(); public IReactiveUIInstance WithInstance(Action action) => throw new NotImplementedException(); + + public IReactiveUIBuilder WithExceptionHandler(IObserver exceptionHandler) => throw new NotImplementedException(); + + public IReactiveUIBuilder WithSuspensionHost() => throw new NotImplementedException(); + + public IReactiveUIBuilder WithSuspensionHost() => throw new NotImplementedException(); + + public IReactiveUIBuilder WithCacheSizes(int smallCacheLimit, int bigCacheLimit) => throw new NotImplementedException(); } } diff --git a/src/tests/ReactiveUI.Builder.Maui.Tests/Activation/ActivationForViewFetcherTests.cs b/src/tests/ReactiveUI.Builder.Maui.Tests/Activation/ActivationForViewFetcherTests.cs index 1a1b41da00..907be5632f 100644 --- a/src/tests/ReactiveUI.Builder.Maui.Tests/Activation/ActivationForViewFetcherTests.cs +++ b/src/tests/ReactiveUI.Builder.Maui.Tests/Activation/ActivationForViewFetcherTests.cs @@ -29,7 +29,9 @@ public async Task PageAndChildViewActivateAndDeactivate() AppBuilder.ResetBuilderStateForTests(); var resolver = new ModernDependencyResolver(); resolver.InitializeSplat(); - resolver.InitializeReactiveUI(); + resolver.CreateReactiveUIBuilder() + .WithPlatformServices() + .BuildApp(); resolver.RegisterConstant(new ActivationForViewFetcher()); using (resolver.WithResolver()) diff --git a/src/tests/ReactiveUI.Builder.Tests/Mixins/BuilderInstanceMixinsHappyPathTests.cs b/src/tests/ReactiveUI.Builder.Tests/Mixins/BuilderInstanceMixinsHappyPathTests.cs index ada16b6155..c2819e7692 100644 --- a/src/tests/ReactiveUI.Builder.Tests/Mixins/BuilderInstanceMixinsHappyPathTests.cs +++ b/src/tests/ReactiveUI.Builder.Tests/Mixins/BuilderInstanceMixinsHappyPathTests.cs @@ -1124,6 +1124,8 @@ public MockDependencyResolver(params object[] services) return _services.TryGetValue(serviceType, out var service) ? service : null; } + public object? GetService(Type? serviceType) => GetService(serviceType, null); + public IEnumerable GetServices(Type? serviceType, string? contract = null) { if (serviceType == null) @@ -1134,6 +1136,16 @@ public IEnumerable GetServices(Type? serviceType, string? contract = nul return _services.TryGetValue(serviceType, out var service) ? [service] : []; } + public IEnumerable GetServices(Type? serviceType) => GetServices(serviceType, null); + + public T? GetService(string? contract = null) => (T?)GetService(typeof(T), contract); + + public T? GetService() => (T?)GetService(typeof(T)); + + public IEnumerable GetServices(string? contract = null) => GetServices(typeof(T), contract).Cast(); + + public IEnumerable GetServices() => GetServices(typeof(T)).Cast(); + public bool HasRegistration(Type? serviceType, string? contract = null) => serviceType != null && _services.ContainsKey(serviceType); diff --git a/src/tests/ReactiveUI.Builder.Tests/Mixins/BuilderMixinsTests.cs b/src/tests/ReactiveUI.Builder.Tests/Mixins/BuilderMixinsTests.cs index baf0fa428d..3887d4c468 100644 --- a/src/tests/ReactiveUI.Builder.Tests/Mixins/BuilderMixinsTests.cs +++ b/src/tests/ReactiveUI.Builder.Tests/Mixins/BuilderMixinsTests.cs @@ -5,6 +5,7 @@ using System.Reactive; using System.Reactive.Concurrency; +using System.Text.Json.Serialization.Metadata; using Splat.Builder; @@ -36,8 +37,7 @@ static BuilderMixinsTests() () => BuilderMixins.RegisterViewModel(null!), () => BuilderMixins.RegisterSingletonViewModel(null!), () => BuilderMixins.RegisterView(null!), - () => BuilderMixins.RegisterSingletonView(null!), - ]; + () => BuilderMixins.RegisterSingletonView(null!)]; } [Before(HookType.Test)] @@ -371,9 +371,9 @@ private sealed class SplatModuleMarker private sealed class TestRegistrationModule : IWantsToRegisterStuff { - public void Register(Action, Type> registerFunction) + public void Register(IRegistrar registrar) { - registerFunction(() => new PlatformRegistrationMarker(), typeof(PlatformRegistrationMarker)); + registrar.RegisterConstant(() => new PlatformRegistrationMarker()); } } @@ -395,7 +395,11 @@ private sealed class TestSuspensionDriver : ISuspensionDriver { public IObservable LoadState() => Observable.Return(null); - public IObservable SaveState(object state) => Observable.Return(Unit.Default); + public IObservable SaveState(T state) => Observable.Return(Unit.Default); + + public IObservable LoadState(JsonTypeInfo typeInfo) => Observable.Return(default); + + public IObservable SaveState(T state, JsonTypeInfo typeInfo) => Observable.Return(Unit.Default); public IObservable InvalidateState() => Observable.Return(Unit.Default); } diff --git a/src/tests/ReactiveUI.Builder.Tests/ReactiveUIBuilderBlockingTests.cs b/src/tests/ReactiveUI.Builder.Tests/ReactiveUIBuilderBlockingTests.cs index 2bcaf9ad11..ef90065b10 100644 --- a/src/tests/ReactiveUI.Builder.Tests/ReactiveUIBuilderBlockingTests.cs +++ b/src/tests/ReactiveUI.Builder.Tests/ReactiveUIBuilderBlockingTests.cs @@ -17,9 +17,7 @@ public async Task Build_SetsFlag_AndBlocks_InitializeReactiveUI() using var locator = new ModernDependencyResolver(); var builder = locator.CreateReactiveUIBuilder(); - builder.WithCoreServices().Build(); - - locator.InitializeReactiveUI(); + builder.WithCoreServices().BuildApp(); var observableProperty = locator.GetService(); await Assert.That(observableProperty).IsNotNull(); diff --git a/src/tests/ReactiveUI.Builder.Tests/ReactiveUIBuilderRxAppMigrationTests.cs b/src/tests/ReactiveUI.Builder.Tests/ReactiveUIBuilderRxAppMigrationTests.cs new file mode 100644 index 0000000000..1bb72c5780 --- /dev/null +++ b/src/tests/ReactiveUI.Builder.Tests/ReactiveUIBuilderRxAppMigrationTests.cs @@ -0,0 +1,254 @@ +// 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.Reactive; +using Splat.Builder; + +namespace ReactiveUI.Builder.Tests; + +/// +/// Tests for RxApp migration functionality including WithExceptionHandler, WithSuspensionHost, and WithCacheSizes. +/// +[NotInParallel] +public class ReactiveUIBuilderRxAppMigrationTests +{ + [Test] + public async Task WithExceptionHandler_Should_Set_Custom_Exception_Handler() + { + AppBuilder.ResetBuilderStateForTests(); + using var locator = new ModernDependencyResolver(); + + Exception? capturedEx = null; + var customHandler = Observer.Create(ex => capturedEx = ex); + + locator.CreateReactiveUIBuilder() + .WithExceptionHandler(customHandler) + .WithCoreServices() + .BuildApp(); + + var testException = new InvalidOperationException("Test exception"); + RxState.DefaultExceptionHandler.OnNext(testException); + + await Assert.That(capturedEx).IsEqualTo(testException); + } + + [Test] + public void WithExceptionHandler_With_Null_Handler_Should_Throw() + { + AppBuilder.ResetBuilderStateForTests(); + using var locator = new ModernDependencyResolver(); + var builder = locator.CreateReactiveUIBuilder(); + + Assert.Throws(() => builder.WithExceptionHandler(null!)); + } + + [Test] + public async Task WithSuspensionHost_NonGeneric_Should_Create_Default_Host() + { + AppBuilder.ResetBuilderStateForTests(); + using var locator = new ModernDependencyResolver(); + + locator.CreateReactiveUIBuilder() + .WithSuspensionHost() + .WithCoreServices() + .BuildApp(); + + var host = RxSuspension.SuspensionHost; + await Assert.That(host).IsNotNull(); + await Assert.That(host).IsTypeOf(); + } + + [Test] + public async Task WithSuspensionHost_Generic_Should_Create_Typed_Host() + { + AppBuilder.ResetBuilderStateForTests(); + using var locator = new ModernDependencyResolver(); + + locator.CreateReactiveUIBuilder() + .WithSuspensionHost() + .WithCoreServices() + .BuildApp(); + + var host = RxSuspension.SuspensionHost; + await Assert.That(host).IsNotNull(); + await Assert.That(host).IsTypeOf>(); + } + + [Test] + public async Task WithCacheSizes_Should_Set_Custom_Cache_Sizes() + { + AppBuilder.ResetBuilderStateForTests(); + using var locator = new ModernDependencyResolver(); + + locator.CreateReactiveUIBuilder() + .WithCacheSizes(smallCacheLimit: 128, bigCacheLimit: 512) + .WithCoreServices() + .BuildApp(); + + await Assert.That(RxCacheSize.SmallCacheLimit).IsEqualTo(128); + await Assert.That(RxCacheSize.BigCacheLimit).IsEqualTo(512); + } + + [Test] + public void WithCacheSizes_With_Zero_Or_Negative_Values_Should_Throw() + { + AppBuilder.ResetBuilderStateForTests(); + using var locator = new ModernDependencyResolver(); + var builder = locator.CreateReactiveUIBuilder(); + + Assert.Throws(() => + builder.WithCacheSizes(smallCacheLimit: 0, bigCacheLimit: 100)); + + Assert.Throws(() => + builder.WithCacheSizes(smallCacheLimit: 100, bigCacheLimit: 0)); + + Assert.Throws(() => + builder.WithCacheSizes(smallCacheLimit: -1, bigCacheLimit: 100)); + + Assert.Throws(() => + builder.WithCacheSizes(smallCacheLimit: 100, bigCacheLimit: -1)); + } + + [Test] + public async Task RxCacheSize_Should_Use_Platform_Defaults_When_Not_Configured() + { + AppBuilder.ResetBuilderStateForTests(); + using var locator = new ModernDependencyResolver(); + + locator.CreateReactiveUIBuilder() + .WithCoreServices() + .BuildApp(); + +#if ANDROID || IOS + await Assert.That(RxCacheSize.SmallCacheLimit).IsEqualTo(32); + await Assert.That(RxCacheSize.BigCacheLimit).IsEqualTo(64); +#else + await Assert.That(RxCacheSize.SmallCacheLimit).IsEqualTo(64); + await Assert.That(RxCacheSize.BigCacheLimit).IsEqualTo(256); +#endif + } + + [Test] + public async Task Builder_Should_Support_Chaining_All_RxApp_Migration_Methods() + { + AppBuilder.ResetBuilderStateForTests(); + using var locator = new ModernDependencyResolver(); + + Exception? capturedEx = null; + var customHandler = Observer.Create(ex => capturedEx = ex); + + locator.CreateReactiveUIBuilder() + .WithExceptionHandler(customHandler) + .WithSuspensionHost() + .WithCacheSizes(smallCacheLimit: 100, bigCacheLimit: 400) + .WithCoreServices() + .BuildApp(); + + // Verify exception handler + var testException = new InvalidOperationException("Test"); + RxState.DefaultExceptionHandler.OnNext(testException); + await Assert.That(capturedEx).IsEqualTo(testException); + + // Verify suspension host + var host = RxSuspension.SuspensionHost; + await Assert.That(host).IsTypeOf>(); + + // Verify cache sizes + await Assert.That(RxCacheSize.SmallCacheLimit).IsEqualTo(100); + await Assert.That(RxCacheSize.BigCacheLimit).IsEqualTo(400); + } + + [Test] + public async Task RxSchedulers_DefaultExceptionHandler_Should_Not_Be_Null() + { + AppBuilder.ResetBuilderStateForTests(); + using var locator = new ModernDependencyResolver(); + + locator.CreateReactiveUIBuilder() + .WithCoreServices() + .BuildApp(); + + var handler = RxState.DefaultExceptionHandler; + await Assert.That(handler).IsNotNull(); + } + + [Test] + public async Task RxSchedulers_SuspensionHost_Should_Not_Be_Null() + { + AppBuilder.ResetBuilderStateForTests(); + using var locator = new ModernDependencyResolver(); + + locator.CreateReactiveUIBuilder() + .WithCoreServices() + .BuildApp(); + + var host = RxSuspension.SuspensionHost; + await Assert.That(host).IsNotNull(); + } + + [Test] + public async Task WithExceptionHandler_Called_Multiple_Times_Should_Use_Last_Handler() + { + AppBuilder.ResetBuilderStateForTests(); + using var locator = new ModernDependencyResolver(); + + Exception? firstCaptured = null; + Exception? secondCaptured = null; + + var firstHandler = Observer.Create(ex => firstCaptured = ex); + var secondHandler = Observer.Create(ex => secondCaptured = ex); + + locator.CreateReactiveUIBuilder() + .WithExceptionHandler(firstHandler) + .WithExceptionHandler(secondHandler) + .WithCoreServices() + .BuildApp(); + + var testException = new InvalidOperationException("Test"); + RxState.DefaultExceptionHandler.OnNext(testException); + + await Assert.That(secondCaptured).IsEqualTo(testException); + await Assert.That(firstCaptured).IsNull(); + } + + [Test] + public async Task WithCacheSizes_Called_Multiple_Times_Should_Use_Last_Values() + { + AppBuilder.ResetBuilderStateForTests(); + using var locator = new ModernDependencyResolver(); + + locator.CreateReactiveUIBuilder() + .WithCacheSizes(smallCacheLimit: 100, bigCacheLimit: 200) + .WithCacheSizes(smallCacheLimit: 300, bigCacheLimit: 600) + .WithCoreServices() + .BuildApp(); + + await Assert.That(RxCacheSize.SmallCacheLimit).IsEqualTo(300); + await Assert.That(RxCacheSize.BigCacheLimit).IsEqualTo(600); + } + + [Test] + public async Task WithSuspensionHost_Generic_Overrides_NonGeneric() + { + AppBuilder.ResetBuilderStateForTests(); + using var locator = new ModernDependencyResolver(); + + locator.CreateReactiveUIBuilder() + .WithSuspensionHost() + .WithSuspensionHost() + .WithCoreServices() + .BuildApp(); + + var host = RxSuspension.SuspensionHost; + await Assert.That(host).IsTypeOf>(); + } + + private class TestAppState + { + public string? Name { get; set; } + + public int Counter { get; set; } + } +} diff --git a/src/tests/ReactiveUI.Maui.Tests/AutoSuspendHelperTest.cs b/src/tests/ReactiveUI.Maui.Tests/AutoSuspendHelperTest.cs index bc02bf079d..86195e5d37 100644 --- a/src/tests/ReactiveUI.Maui.Tests/AutoSuspendHelperTest.cs +++ b/src/tests/ReactiveUI.Maui.Tests/AutoSuspendHelperTest.cs @@ -31,11 +31,11 @@ public async Task Constructor_WiresUpSuspensionHost() { var helper = new AutoSuspendHelper(); - await Assert.That(RxApp.SuspensionHost.IsLaunchingNew).IsNotNull(); - await Assert.That(RxApp.SuspensionHost.IsUnpausing).IsNotNull(); - await Assert.That(RxApp.SuspensionHost.IsResuming).IsNotNull(); - await Assert.That(RxApp.SuspensionHost.ShouldPersistState).IsNotNull(); - await Assert.That(RxApp.SuspensionHost.ShouldInvalidateState).IsNotNull(); + await Assert.That(RxSuspension.SuspensionHost.IsLaunchingNew).IsNotNull(); + await Assert.That(RxSuspension.SuspensionHost.IsUnpausing).IsNotNull(); + await Assert.That(RxSuspension.SuspensionHost.IsResuming).IsNotNull(); + await Assert.That(RxSuspension.SuspensionHost.ShouldPersistState).IsNotNull(); + await Assert.That(RxSuspension.SuspensionHost.ShouldInvalidateState).IsNotNull(); } /// @@ -48,7 +48,7 @@ public async Task OnCreate_TriggersIsLaunchingNew() var helper = new AutoSuspendHelper(); var triggered = false; - RxApp.SuspensionHost.IsLaunchingNew.Subscribe(_ => triggered = true); + RxSuspension.SuspensionHost.IsLaunchingNew.Subscribe(_ => triggered = true); helper.OnCreate(); await Assert.That(triggered).IsTrue(); @@ -64,7 +64,7 @@ public async Task OnStart_TriggersIsUnpausing() var helper = new AutoSuspendHelper(); var triggered = false; - RxApp.SuspensionHost.IsUnpausing.Subscribe(_ => triggered = true); + RxSuspension.SuspensionHost.IsUnpausing.Subscribe(_ => triggered = true); helper.OnStart(); await Assert.That(triggered).IsTrue(); @@ -80,7 +80,7 @@ public async Task OnResume_TriggersIsResuming() var helper = new AutoSuspendHelper(); var triggered = false; - RxApp.SuspensionHost.IsResuming.Subscribe(_ => triggered = true); + RxSuspension.SuspensionHost.IsResuming.Subscribe(_ => triggered = true); helper.OnResume(); await Assert.That(triggered).IsTrue(); @@ -96,7 +96,7 @@ public async Task OnSleep_TriggersShouldPersistState() var helper = new AutoSuspendHelper(); var triggered = false; - RxApp.SuspensionHost.ShouldPersistState.Subscribe(_ => triggered = true); + RxSuspension.SuspensionHost.ShouldPersistState.Subscribe(_ => triggered = true); helper.OnSleep(); await Assert.That(triggered).IsTrue(); diff --git a/src/tests/ReactiveUI.Maui.Tests/BooleanToVisibilityTypeConverterTest.cs b/src/tests/ReactiveUI.Maui.Tests/BooleanToVisibilityTypeConverterTest.cs index 9323c9a90a..ac21331dab 100644 --- a/src/tests/ReactiveUI.Maui.Tests/BooleanToVisibilityTypeConverterTest.cs +++ b/src/tests/ReactiveUI.Maui.Tests/BooleanToVisibilityTypeConverterTest.cs @@ -21,7 +21,7 @@ public async Task GetAffinityForObjects_ReturnsCorrectAffinityForBoolToVisibilit { var converter = new BooleanToVisibilityTypeConverter(); - var affinity = converter.GetAffinityForObjects(typeof(bool), typeof(Visibility)); + var affinity = converter.GetAffinityForObjects(); await Assert.That(affinity).IsEqualTo(10); } @@ -35,7 +35,7 @@ public async Task GetAffinityForObjects_ReturnsCorrectAffinityForVisibilityToBoo { var converter = new BooleanToVisibilityTypeConverter(); - var affinity = converter.GetAffinityForObjects(typeof(Visibility), typeof(bool)); + var affinity = converter.GetAffinityForObjects(); await Assert.That(affinity).IsEqualTo(10); } @@ -49,7 +49,7 @@ public async Task GetAffinityForObjects_ReturnsZeroForUnsupportedTypes() { var converter = new BooleanToVisibilityTypeConverter(); - var affinity = converter.GetAffinityForObjects(typeof(int), typeof(string)); + var affinity = converter.GetAffinityForObjects(); await Assert.That(affinity).IsEqualTo(0); } @@ -63,7 +63,7 @@ public async Task TryConvert_ConvertsTrueToVisible() { var converter = new BooleanToVisibilityTypeConverter(); - var success = converter.TryConvert(true, typeof(Visibility), null, out var result); + var success = converter.TryConvertTyped(true, null, out var result); await Assert.That(success).IsTrue(); await Assert.That(result).IsEqualTo(Visibility.Visible); @@ -78,7 +78,7 @@ public async Task TryConvert_ConvertsFalseToCollapsed() { var converter = new BooleanToVisibilityTypeConverter(); - var success = converter.TryConvert(false, typeof(Visibility), null, out var result); + var success = converter.TryConvertTyped(false, null, out var result); await Assert.That(success).IsTrue(); await Assert.That(result).IsEqualTo(Visibility.Collapsed); @@ -93,7 +93,7 @@ public async Task TryConvert_WithInverseHint_InvertsConversion() { var converter = new BooleanToVisibilityTypeConverter(); - var success = converter.TryConvert(true, typeof(Visibility), BooleanToVisibilityHint.Inverse, out var result); + var success = converter.TryConvertTyped(true, BooleanToVisibilityHint.Inverse, out var result); await Assert.That(success).IsTrue(); await Assert.That(result).IsEqualTo(Visibility.Collapsed); @@ -109,7 +109,7 @@ public async Task TryConvert_WithUseHiddenHint_UsesHidden() { var converter = new BooleanToVisibilityTypeConverter(); - var success = converter.TryConvert(false, typeof(Visibility), BooleanToVisibilityHint.UseHidden, out var result); + var success = converter.TryConvertTyped(false, BooleanToVisibilityHint.UseHidden, out var result); await Assert.That(success).IsTrue(); await Assert.That(result).IsEqualTo(Visibility.Hidden); @@ -123,10 +123,10 @@ public async Task TryConvert_WithUseHiddenHint_UsesHidden() [Test] public async Task TryConvert_ConvertsVisibilityToBool() { - var converter = new BooleanToVisibilityTypeConverter(); + var converter = new VisibilityToBooleanTypeConverter(); - var successVisible = converter.TryConvert(Visibility.Visible, typeof(bool), null, out var resultVisible); - var successCollapsed = converter.TryConvert(Visibility.Collapsed, typeof(bool), null, out var resultCollapsed); + var successVisible = converter.TryConvertTyped(Visibility.Visible, null, out var resultVisible); + var successCollapsed = converter.TryConvertTyped(Visibility.Collapsed, null, out var resultCollapsed); await Assert.That(successVisible).IsTrue(); await Assert.That(successCollapsed).IsTrue(); @@ -139,18 +139,18 @@ public async Task TryConvert_ConvertsVisibilityToBool() } /// - /// Tests that TryConvert with non-Visibility input defaults to Visible. + /// Tests that TryConvert with non-bool input for boolean converter. /// /// A representing the asynchronous operation. [Test] - public async Task TryConvert_NonVisibilityInput_DefaultsToVisible() + public async Task TryConvert_BooleanConverter_HandlesInput() { var converter = new BooleanToVisibilityTypeConverter(); - var success = converter.TryConvert("some string", typeof(bool), null, out var result); + var success = converter.TryConvertTyped(false, null, out var result); await Assert.That(success).IsTrue(); - await Assert.That(result).IsEqualTo(Visibility.Visible); + await Assert.That(result).IsEqualTo(Visibility.Collapsed); } /// @@ -160,14 +160,14 @@ public async Task TryConvert_NonVisibilityInput_DefaultsToVisible() [Test] public async Task TryConvert_VisibilityToBoolWithInverse_InvertsResult() { - var converter = new BooleanToVisibilityTypeConverter(); + var converter = new VisibilityToBooleanTypeConverter(); - var success = converter.TryConvert(Visibility.Visible, typeof(bool), BooleanToVisibilityHint.Inverse, out var result); + var success = converter.TryConvertTyped(Visibility.Visible, BooleanToVisibilityHint.Inverse, out var result); await Assert.That(success).IsTrue(); // With Inverse hint, Visible should become false - await Assert.That(result).IsEqualTo(true); + await Assert.That(result).IsEqualTo(false); } #if !HAS_UNO && !HAS_WINUI && !IS_MAUI @@ -180,9 +180,8 @@ public async Task TryConvert_WithInverseAndUseHidden_WorksCorrectly() { var converter = new BooleanToVisibilityTypeConverter(); - var success = converter.TryConvert( + var success = converter.TryConvertTyped( true, - typeof(Visibility), BooleanToVisibilityHint.Inverse | BooleanToVisibilityHint.UseHidden, out var result); diff --git a/src/tests/ReactiveUI.Maui.Tests/Builder/MauiDispatcherSchedulerTest.cs b/src/tests/ReactiveUI.Maui.Tests/Builder/MauiDispatcherSchedulerTest.cs index 76e0de59cb..bdd2eebdc2 100644 --- a/src/tests/ReactiveUI.Maui.Tests/Builder/MauiDispatcherSchedulerTest.cs +++ b/src/tests/ReactiveUI.Maui.Tests/Builder/MauiDispatcherSchedulerTest.cs @@ -46,7 +46,7 @@ public async Task Dispatcher_ImmediateSchedule_ExecutesWork() builder.BuildApp(); var executed = false; - RxApp.MainThreadScheduler.Schedule(() => executed = true); + RxSchedulers.MainThreadScheduler.Schedule(() => executed = true); await Assert.That(executed).IsTrue(); } @@ -64,7 +64,7 @@ public async Task Dispatcher_NoDispatchRequired_ExecutesImmediately() builder.BuildApp(); var executed = false; - RxApp.MainThreadScheduler.Schedule(() => executed = true); + RxSchedulers.MainThreadScheduler.Schedule(() => executed = true); await Assert.That(executed).IsTrue(); } @@ -82,7 +82,7 @@ public async Task Dispatcher_DispatchRequired_ExecutesWork() builder.BuildApp(); var executed = false; - RxApp.MainThreadScheduler.Schedule(() => executed = true); + RxSchedulers.MainThreadScheduler.Schedule(() => executed = true); await Assert.That(executed).IsTrue(); } @@ -153,8 +153,8 @@ private sealed class RxAppSchedulersScope : IDisposable /// public RxAppSchedulersScope() { - _mainThreadScheduler = RxApp.MainThreadScheduler; - _taskpoolScheduler = RxApp.TaskpoolScheduler; + _mainThreadScheduler = RxSchedulers.MainThreadScheduler; + _taskpoolScheduler = RxSchedulers.TaskpoolScheduler; } /// @@ -167,8 +167,8 @@ public void Dispose() return; } - RxApp.MainThreadScheduler = _mainThreadScheduler; - RxApp.TaskpoolScheduler = _taskpoolScheduler; + RxSchedulers.MainThreadScheduler = _mainThreadScheduler; + RxSchedulers.TaskpoolScheduler = _taskpoolScheduler; _disposed = true; } } diff --git a/src/tests/ReactiveUI.Maui.Tests/ViewModelViewHostTest.cs b/src/tests/ReactiveUI.Maui.Tests/ViewModelViewHostTest.cs index c61b1f6a41..367427ac4e 100644 --- a/src/tests/ReactiveUI.Maui.Tests/ViewModelViewHostTest.cs +++ b/src/tests/ReactiveUI.Maui.Tests/ViewModelViewHostTest.cs @@ -142,6 +142,9 @@ private class TestViewModel /// private class TestViewLocator : IViewLocator { - public IViewFor? ResolveView(T? viewModel, string? contract = null) => null; + public IViewFor? ResolveView(string? contract = null) + where TViewModel : class => null; + + public IViewFor? ResolveView(object? instance, string? contract = null) => null; } } diff --git a/src/tests/ReactiveUI.NonParallel.Tests/AwaiterTest.cs b/src/tests/ReactiveUI.NonParallel.Tests/AwaiterTest.cs index 1a0cefaf37..6173ab25d8 100644 --- a/src/tests/ReactiveUI.NonParallel.Tests/AwaiterTest.cs +++ b/src/tests/ReactiveUI.NonParallel.Tests/AwaiterTest.cs @@ -10,12 +10,12 @@ namespace ReactiveUI.Tests.Core; [NotInParallel] public class AwaiterTest : IDisposable { - private RxAppSchedulersScope? _schedulersScope; + private RxSchedulersSchedulersScope? _schedulersScope; [Before(Test)] public void SetUp() { - _schedulersScope = new RxAppSchedulersScope(); + _schedulersScope = new RxSchedulersSchedulersScope(); } [After(Test)] @@ -51,7 +51,7 @@ private static async Task AwaitAnObservable() Thread.Sleep(1000); return 42; }, - RxApp.TaskpoolScheduler); + RxSchedulers.TaskpoolScheduler); return await o; } diff --git a/src/tests/ReactiveUI.NonParallel.Tests/CommandBinding/CommandBindingTests.cs b/src/tests/ReactiveUI.NonParallel.Tests/CommandBinding/CommandBindingTests.cs index 8d2e702e69..9194555790 100644 --- a/src/tests/ReactiveUI.NonParallel.Tests/CommandBinding/CommandBindingTests.cs +++ b/src/tests/ReactiveUI.NonParallel.Tests/CommandBinding/CommandBindingTests.cs @@ -4,6 +4,7 @@ // See the LICENSE file in the project root for full license information. using System; +using System.Diagnostics.CodeAnalysis; using System.Reactive; using System.Reactive.Linq; using System.Reactive.Subjects; @@ -139,9 +140,9 @@ public FakeCustomBinder() public static bool BindCalled { get; set; } - public int GetAffinityForObject(Type type, bool hasEventTarget) + public int GetAffinityForObject(bool hasEventTarget) { - if (type == typeof(FakeCustomControl)) + if (typeof(T) == typeof(FakeCustomControl)) { return 100; // High affinity } @@ -149,23 +150,25 @@ public int GetAffinityForObject(Type type, bool hasEventTarget) return 0; } - public int GetAffinityForObject(bool hasEventTarget) + [RequiresUnreferencedCode("String/reflection-based event binding may require members removed by trimming.")] + public IDisposable? BindCommandToObject(ICommand? command, T? target, IObservable commandParameter) + where T : class { - if (typeof(T) == typeof(FakeCustomControl)) - { - return 100; // High affinity - } - - return 0; + BindCalled = true; + return Disposable.Empty; } - public IDisposable? BindCommandToObject(ICommand? command, object? target, IObservable commandParameter) + [RequiresUnreferencedCode("String/reflection-based event binding may require members removed by trimming.")] + public IDisposable? BindCommandToObject(ICommand? command, T? target, IObservable commandParameter, string eventName) + where T : class { BindCalled = true; return Disposable.Empty; } - public IDisposable BindCommandToObject(ICommand? command, object? target, IObservable commandParameter, string eventName) + public IDisposable? BindCommandToObject(ICommand? command, T? target, IObservable commandParameter, Action> addHandler, Action> removeHandler) + where T : class + where TEventArgs : EventArgs { BindCalled = true; return Disposable.Empty; diff --git a/src/tests/ReactiveUI.NonParallel.Tests/Commands/ReactiveCommandTest.cs b/src/tests/ReactiveUI.NonParallel.Tests/Commands/ReactiveCommandTest.cs index 5acae20714..f0b2b5cd74 100644 --- a/src/tests/ReactiveUI.NonParallel.Tests/Commands/ReactiveCommandTest.cs +++ b/src/tests/ReactiveUI.NonParallel.Tests/Commands/ReactiveCommandTest.cs @@ -9,6 +9,7 @@ using Microsoft.Reactive.Testing; +using ReactiveUI.Builder; using ReactiveUI.Testing; using ReactiveUI.Tests.Infrastructure.StaticState; @@ -17,17 +18,17 @@ namespace ReactiveUI.Tests.Core; [NotInParallel] public class ReactiveCommandTest : IDisposable { - private RxAppSchedulersScope? _schedulersScope; + private RxSchedulersSchedulersScope? _schedulersScope; public ReactiveCommandTest() { - RxApp.EnsureInitialized(); + RxAppBuilder.EnsureInitialized(); } [Before(Test)] public void SetUp() { - _schedulersScope = new RxAppSchedulersScope(); + _schedulersScope = new RxSchedulersSchedulersScope(); } [After(Test)] diff --git a/src/tests/ReactiveUI.NonParallel.Tests/Locator/DefaultViewLocatorTests.cs b/src/tests/ReactiveUI.NonParallel.Tests/Locator/DefaultViewLocatorTests.cs index 2ac635b670..68a24c8eec 100644 --- a/src/tests/ReactiveUI.NonParallel.Tests/Locator/DefaultViewLocatorTests.cs +++ b/src/tests/ReactiveUI.NonParallel.Tests/Locator/DefaultViewLocatorTests.cs @@ -26,7 +26,9 @@ public async Task DiagnosticTestForRegistrationAndResolution() { var resolver = new ModernDependencyResolver(); resolver.InitializeSplat(); - resolver.InitializeReactiveUI(); + RxAppBuilder.CreateReactiveUIBuilder(resolver) + .WithCoreServices() + .BuildApp(); // Register resolver.Register(() => new FooView(), typeof(IViewFor)); @@ -67,7 +69,9 @@ public async Task ByDefaultViewModelIsReplacedWithViewWhenDeterminingTheServiceN var resolver = new ModernDependencyResolver(); resolver.InitializeSplat(); - resolver.InitializeReactiveUI(); + RxAppBuilder.CreateReactiveUIBuilder(resolver) + .WithCoreServices() + .BuildApp(); resolver.Register(static () => new FooView(), typeof(IViewFor)); using (resolver.WithResolver()) @@ -90,7 +94,9 @@ public async Task TheRuntimeTypeOfTheViewModelIsUsedToResolveTheView() var resolver = new ModernDependencyResolver(); resolver.InitializeSplat(); - resolver.InitializeReactiveUI(); + RxAppBuilder.CreateReactiveUIBuilder(resolver) + .WithCoreServices() + .BuildApp(); resolver.Register(static () => new FooView(), typeof(FooView)); using (resolver.WithResolver()) @@ -113,16 +119,16 @@ public async Task ViewModelToViewNamingConventionCanBeCustomized() var resolver = new ModernDependencyResolver(); resolver.InitializeSplat(); - resolver.InitializeReactiveUI(); + RxAppBuilder.CreateReactiveUIBuilder(resolver) + .WithCoreServices() + .BuildApp(); resolver.Register(static () => new FooWithWeirdConvention(), typeof(FooWithWeirdConvention)); using (resolver.WithResolver()) { - var fixture = new DefaultViewLocator - { - ViewModelToViewFunc = - static viewModelName => viewModelName.Replace("ViewModel", "WithWeirdConvention") - }; + var fixture = new DefaultViewLocator(); + + // FooWithWeirdConvention implements IFooView, use service registration instead var vm = new FooViewModel(); var result = fixture.ResolveView(vm); @@ -140,7 +146,9 @@ public async Task CanResolveViewFromViewModelClassUsingClassRegistration() var resolver = new ModernDependencyResolver(); resolver.InitializeSplat(); - resolver.InitializeReactiveUI(); + RxAppBuilder.CreateReactiveUIBuilder(resolver) + .WithCoreServices() + .BuildApp(); resolver.Register(static () => new FooView(), typeof(FooView)); using (resolver.WithResolver()) @@ -163,7 +171,9 @@ public async Task CanResolveViewFromViewModelClassUsingInterfaceRegistration() var resolver = new ModernDependencyResolver(); resolver.InitializeSplat(); - resolver.InitializeReactiveUI(); + RxAppBuilder.CreateReactiveUIBuilder(resolver) + .WithCoreServices() + .BuildApp(); resolver.Register(static () => new FooView(), typeof(IFooView)); using (resolver.WithResolver()) @@ -186,7 +196,9 @@ public async Task CanResolveViewFromViewModelClassUsingIViewForRegistration() var resolver = new ModernDependencyResolver(); resolver.InitializeSplat(); - resolver.InitializeReactiveUI(); + RxAppBuilder.CreateReactiveUIBuilder(resolver) + .WithCoreServices() + .BuildApp(); resolver.Register(static () => new FooView(), typeof(IViewFor)); using (resolver.WithResolver()) @@ -209,7 +221,9 @@ public async Task CanResolveViewFromViewModelInterfaceUsingClassRegistration() var resolver = new ModernDependencyResolver(); resolver.InitializeSplat(); - resolver.InitializeReactiveUI(); + RxAppBuilder.CreateReactiveUIBuilder(resolver) + .WithCoreServices() + .BuildApp(); resolver.Register(static () => new FooView(), typeof(FooView)); using (resolver.WithResolver()) @@ -232,7 +246,9 @@ public async Task CanResolveViewFromViewModelInterfaceUsingInterfaceRegistration var resolver = new ModernDependencyResolver(); resolver.InitializeSplat(); - resolver.InitializeReactiveUI(); + RxAppBuilder.CreateReactiveUIBuilder(resolver) + .WithCoreServices() + .BuildApp(); resolver.Register(static () => new FooView(), typeof(IFooView)); using (resolver.WithResolver()) @@ -255,7 +271,9 @@ public async Task CanResolveViewFromViewModelInterfaceUsingIViewForRegistration( var resolver = new ModernDependencyResolver(); resolver.InitializeSplat(); - resolver.InitializeReactiveUI(); + RxAppBuilder.CreateReactiveUIBuilder(resolver) + .WithCoreServices() + .BuildApp(); resolver.Register(static () => new FooView(), typeof(IViewFor)); using (resolver.WithResolver()) @@ -278,7 +296,9 @@ public async Task ContractIsUsedWhenResolvingView() var resolver = new ModernDependencyResolver(); resolver.InitializeSplat(); - resolver.InitializeReactiveUI(); + RxAppBuilder.CreateReactiveUIBuilder(resolver) + .WithCoreServices() + .BuildApp(); resolver.Register(static () => new FooView(), typeof(IViewFor), "first"); resolver.Register(static () => new FooWithWeirdConvention(), typeof(IViewFor), "second"); @@ -308,15 +328,15 @@ public async Task NoErrorIsRaisedIfATypeCannotBeFound() var resolver = new ModernDependencyResolver(); resolver.InitializeSplat(); - resolver.InitializeReactiveUI(); + RxAppBuilder.CreateReactiveUIBuilder(resolver) + .WithCoreServices() + .BuildApp(); using (resolver.WithResolver()) { - var fixture = new DefaultViewLocator - { - ViewModelToViewFunc = static viewModelName => - "DoesNotExist, " + typeof(DefaultViewLocatorTests).Assembly.FullName - }; + var fixture = new DefaultViewLocator(); + + // Don't register any views - this will cause resolution to fail var vm = new FooViewModel(); var result = fixture.ResolveView(vm); @@ -334,7 +354,9 @@ public async Task NoErrorIsRaisedIfAServiceCannotBeFound() var resolver = new ModernDependencyResolver(); resolver.InitializeSplat(); - resolver.InitializeReactiveUI(); + RxAppBuilder.CreateReactiveUIBuilder(resolver) + .WithCoreServices() + .BuildApp(); using (resolver.WithResolver()) { @@ -356,7 +378,9 @@ public async Task NoErrorIsRaisedIfTheServiceDoesNotImplementIViewFor() var resolver = new ModernDependencyResolver(); resolver.InitializeSplat(); - resolver.InitializeReactiveUI(); + RxAppBuilder.CreateReactiveUIBuilder(resolver) + .WithCoreServices() + .BuildApp(); resolver.Register(static () => "this string does not implement IViewFor", typeof(IViewFor)); using (resolver.WithResolver()) @@ -379,7 +403,9 @@ public async Task ExceptionIsThrownIfTheCreationOfTheViewFails() var resolver = new ModernDependencyResolver(); resolver.InitializeSplat(); - resolver.InitializeReactiveUI(); + RxAppBuilder.CreateReactiveUIBuilder(resolver) + .WithCoreServices() + .BuildApp(); resolver.Register(() => new FooThatThrowsView(), typeof(IViewFor)); using (resolver.WithResolver()) @@ -406,7 +432,9 @@ public void WithOddInterfaceNameDoesntThrowException() var resolver = new ModernDependencyResolver(); resolver.InitializeSplat(); - resolver.InitializeReactiveUI(); + RxAppBuilder.CreateReactiveUIBuilder(resolver) + .WithCoreServices() + .BuildApp(); using (resolver.WithResolver()) { @@ -427,7 +455,9 @@ public async Task AotMapping_WithMapMethod_ResolvesViewCorrectly() { var resolver = new ModernDependencyResolver(); resolver.InitializeSplat(); - resolver.InitializeReactiveUI(); + RxAppBuilder.CreateReactiveUIBuilder(resolver) + .WithCoreServices() + .BuildApp(); using (resolver.WithResolver()) { @@ -451,7 +481,9 @@ public async Task AotMapping_WithContract_ResolvesCorrectView() { var resolver = new ModernDependencyResolver(); resolver.InitializeSplat(); - resolver.InitializeReactiveUI(); + RxAppBuilder.CreateReactiveUIBuilder(resolver) + .WithCoreServices() + .BuildApp(); using (resolver.WithResolver()) { @@ -478,7 +510,9 @@ public async Task AotMapping_FallsBackToDefaultContract_WhenSpecificNotFound() { var resolver = new ModernDependencyResolver(); resolver.InitializeSplat(); - resolver.InitializeReactiveUI(); + RxAppBuilder.CreateReactiveUIBuilder(resolver) + .WithCoreServices() + .BuildApp(); using (resolver.WithResolver()) { @@ -502,7 +536,9 @@ public async Task Unmap_RemovesAotMapping() { var resolver = new ModernDependencyResolver(); resolver.InitializeSplat(); - resolver.InitializeReactiveUI(); + RxAppBuilder.CreateReactiveUIBuilder(resolver) + .WithCoreServices() + .BuildApp(); using (resolver.WithResolver()) { @@ -528,7 +564,9 @@ public async Task Unmap_WithContract_RemovesOnlyThatMapping() { var resolver = new ModernDependencyResolver(); resolver.InitializeSplat(); - resolver.InitializeReactiveUI(); + RxAppBuilder.CreateReactiveUIBuilder(resolver) + .WithCoreServices() + .BuildApp(); using (resolver.WithResolver()) { diff --git a/src/tests/ReactiveUI.NonParallel.Tests/Locator/Mocks/RoutableFooCustomView.cs b/src/tests/ReactiveUI.NonParallel.Tests/Locator/Mocks/RoutableFooCustomView.cs index e30f534ddc..6c9d24199f 100644 --- a/src/tests/ReactiveUI.NonParallel.Tests/Locator/Mocks/RoutableFooCustomView.cs +++ b/src/tests/ReactiveUI.NonParallel.Tests/Locator/Mocks/RoutableFooCustomView.cs @@ -8,15 +8,15 @@ namespace ReactiveUI.Tests.Core; /// /// A routable view. /// -public class RoutableFooCustomView : IViewFor +public class RoutableFooCustomView : IViewFor { /// object? IViewFor.ViewModel { get => ViewModel; - set => ViewModel = (IRoutableFooViewModel?)value; + set => ViewModel = (RoutableFooViewModel?)value; } /// - public IRoutableFooViewModel? ViewModel { get; set; } + public RoutableFooViewModel? ViewModel { get; set; } } diff --git a/src/tests/ReactiveUI.NonParallel.Tests/Locator/ViewLocatorTest.cs b/src/tests/ReactiveUI.NonParallel.Tests/Locator/ViewLocatorTest.cs index 4d3a6032c2..0b7d5f1e81 100644 --- a/src/tests/ReactiveUI.NonParallel.Tests/Locator/ViewLocatorTest.cs +++ b/src/tests/ReactiveUI.NonParallel.Tests/Locator/ViewLocatorTest.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; + using TUnit.Assertions; using TUnit.Assertions.Extensions; using TUnit.Core; @@ -50,7 +52,9 @@ public async Task Current_ReturnsRegisteredLocator_WhenLocatorIsRegistered() { var resolver = new ModernDependencyResolver(); resolver.InitializeSplat(); - resolver.InitializeReactiveUI(); + RxAppBuilder.CreateReactiveUIBuilder(resolver) + .WithCoreServices() + .BuildApp(); using (resolver.WithResolver()) { @@ -89,6 +93,18 @@ public async Task Current_ReturnsCustomLocator_WhenCustomLocatorIsRegistered() private class CustomViewLocator : IViewLocator { /// - public IViewFor? ResolveView(T? viewModel, string? contract = null) => null; + public IViewFor? ResolveView(string? contract = null) + where TViewModel : class + { + return null; + } + + /// + [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 IViewFor? ResolveView(object? instance, string? contract = null) + { + return null; + } } } diff --git a/src/tests/ReactiveUI.NonParallel.Tests/MessageBusTest.cs b/src/tests/ReactiveUI.NonParallel.Tests/MessageBusTest.cs index 2a902edb66..6347cea55f 100644 --- a/src/tests/ReactiveUI.NonParallel.Tests/MessageBusTest.cs +++ b/src/tests/ReactiveUI.NonParallel.Tests/MessageBusTest.cs @@ -73,7 +73,9 @@ public async Task MessageBusSmokeTest() public async Task ExplicitSendMessageShouldWorkEvenAfterRegisteringSource() { Locator.CurrentMutable.InitializeSplat(); - Locator.CurrentMutable.InitializeReactiveUI(); + RxAppBuilder.CreateReactiveUIBuilder(Locator.CurrentMutable) + .WithCoreServices() + .BuildApp(); var fixture = new MessageBus(); fixture.RegisterMessageSource(Observable.Never); @@ -170,7 +172,9 @@ public async Task RegisteringSecondMessageSourceShouldMergeBothSources() public async Task MessageBusThreadingTest() { Locator.CurrentMutable.InitializeSplat(); - Locator.CurrentMutable.InitializeReactiveUI(); + RxAppBuilder.CreateReactiveUIBuilder(Locator.CurrentMutable) + .WithCoreServices() + .BuildApp(); var messageBus = new MessageBus(); messageBus.RegisterScheduler(ImmediateScheduler.Instance); int? listenedThreadId = null; diff --git a/src/tests/ReactiveUI.NonParallel.Tests/Mixins/MutableDependencyResolverExtensionsTests.cs b/src/tests/ReactiveUI.NonParallel.Tests/Mixins/MutableDependencyResolverExtensionsTests.cs index 5ad29d9655..bfd58c008a 100644 --- a/src/tests/ReactiveUI.NonParallel.Tests/Mixins/MutableDependencyResolverExtensionsTests.cs +++ b/src/tests/ReactiveUI.NonParallel.Tests/Mixins/MutableDependencyResolverExtensionsTests.cs @@ -13,12 +13,12 @@ namespace ReactiveUI.Tests.Mixins; [NotInParallel] public class MutableDependencyResolverExtensionsTests : IDisposable { - private RxAppSchedulersScope? _schedulersScope; + private RxSchedulersSchedulersScope? _schedulersScope; [Before(Test)] public void SetUp() { - _schedulersScope = new RxAppSchedulersScope(); + _schedulersScope = new RxSchedulersSchedulersScope(); } [After(Test)] diff --git a/src/tests/ReactiveUI.NonParallel.Tests/PlatformRegistrationManagerTest.cs b/src/tests/ReactiveUI.NonParallel.Tests/PlatformRegistrationManagerTest.cs deleted file mode 100644 index 8a339e8fea..0000000000 --- a/src/tests/ReactiveUI.NonParallel.Tests/PlatformRegistrationManagerTest.cs +++ /dev/null @@ -1,57 +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. - -namespace ReactiveUI.Tests; - -/// -/// Tests for . -/// -[NotInParallel] -public class PlatformRegistrationManagerTest -{ - /// - /// Tests that SetRegistrationNamespaces sets the namespaces. - /// - /// A representing the asynchronous operation. - [Test] - public async Task SetRegistrationNamespaces_SetsNamespaces() - { - var originalNamespaces = PlatformRegistrationManager.NamespacesToRegister; - var newNamespaces = new[] { RegistrationNamespace.Maui }; - - try - { - PlatformRegistrationManager.SetRegistrationNamespaces(newNamespaces); - - await Assert.That(PlatformRegistrationManager.NamespacesToRegister).IsEquivalentTo(newNamespaces); - } - finally - { - // Restore original namespaces - PlatformRegistrationManager.SetRegistrationNamespaces(originalNamespaces); - } - } - - /// - /// Tests that DefaultRegistrationNamespaces is not null. - /// - /// A representing the asynchronous operation. - [Test] - public async Task DefaultRegistrationNamespaces_IsNotNull() - { - await Assert.That(PlatformRegistrationManager.DefaultRegistrationNamespaces).IsNotNull(); - await Assert.That(PlatformRegistrationManager.DefaultRegistrationNamespaces).Count().IsGreaterThan(0); - } - - /// - /// Tests that NamespacesToRegister starts with default namespaces. - /// - /// A representing the asynchronous operation. - [Test] - public async Task NamespacesToRegister_StartsWithDefaultNamespaces() - { - await Assert.That(PlatformRegistrationManager.NamespacesToRegister).IsNotNull(); - } -} diff --git a/src/tests/ReactiveUI.NonParallel.Tests/Platforms/windows-xaml/Api/XamlApiApprovalTests.Blend.DotNet10_0.verified.txt b/src/tests/ReactiveUI.NonParallel.Tests/Platforms/windows-xaml/Api/XamlApiApprovalTests.Blend.DotNet10_0.verified.txt index 5512db5f4a..01f17b8cbe 100644 --- a/src/tests/ReactiveUI.NonParallel.Tests/Platforms/windows-xaml/Api/XamlApiApprovalTests.Blend.DotNet10_0.verified.txt +++ b/src/tests/ReactiveUI.NonParallel.Tests/Platforms/windows-xaml/Api/XamlApiApprovalTests.Blend.DotNet10_0.verified.txt @@ -1,8 +1,6 @@ [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("ReactiveUI.NonParallel.Tests")] namespace ReactiveUI.Blend { - [System.Diagnostics.CodeAnalysis.RequiresDynamicCode("OnStateObservableChanged uses methods that require dynamic code generation")] - [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("OnStateObservableChanged uses methods that may require unreferenced code")] public class FollowObservableStateBehavior : Microsoft.Xaml.Behaviors.Behavior { public static readonly System.Windows.DependencyProperty StateObservableProperty; @@ -17,8 +15,6 @@ namespace ReactiveUI.Blend [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("OnStateObservableChanged uses methods that may require unreferenced code")] protected static void OnStateObservableChanged(System.Windows.DependencyObject? sender, System.Windows.DependencyPropertyChangedEventArgs e) { } } - [System.Diagnostics.CodeAnalysis.RequiresDynamicCode("ObservableTrigger uses methods that require dynamic code generation")] - [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("ObservableTrigger uses methods that may require unreferenced code")] public class ObservableTrigger : Microsoft.Xaml.Behaviors.TriggerBase { public static readonly System.Windows.DependencyProperty ObservableProperty; @@ -30,4 +26,4 @@ namespace ReactiveUI.Blend [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("OnObservableChanged uses methods that may require unreferenced code")] protected static void OnObservableChanged(System.Windows.DependencyObject sender, System.Windows.DependencyPropertyChangedEventArgs e) { } } -} \ No newline at end of file +} diff --git a/src/tests/ReactiveUI.NonParallel.Tests/Platforms/windows-xaml/MockWindow.xaml b/src/tests/ReactiveUI.NonParallel.Tests/Platforms/windows-xaml/MockWindow.xaml index 6fca989bf8..a5b8f677ed 100644 --- a/src/tests/ReactiveUI.NonParallel.Tests/Platforms/windows-xaml/MockWindow.xaml +++ b/src/tests/ReactiveUI.NonParallel.Tests/Platforms/windows-xaml/MockWindow.xaml @@ -6,10 +6,10 @@ xmlns:local="clr-namespace:ReactiveUI.Tests.Wpf" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:rxui="http://reactiveui.net" + x:TypeArguments="local:CommandBindingViewModel" Title="MockWindow" Width="800" Height="450" - x:TypeArguments="local:CommandBindingViewModel" mc:Ignorable="d">