Skip to content

feat: add AOT-friendly WhenActivated for IActivatableView#4413

Merged
glennawatson merged 4 commits into
mainfrom
feature/aot-friendly-when-activated
Jul 23, 2026
Merged

feat: add AOT-friendly WhenActivated for IActivatableView#4413
glennawatson merged 4 commits into
mainfrom
feature/aot-friendly-when-activated

Conversation

@glennawatson

Copy link
Copy Markdown
Contributor

What kind of change does this PR introduce?

Feature - a trim/AOT-safe way to use WhenActivated on an IActivatableView.

What is the new behavior?

ViewForMixins gains AOT-friendly IActivatableView.WhenActivated overloads that are NOT annotated [RequiresUnreferencedCode] and produce no trim/AOT analyzer warnings. Instead of discovering the view's ViewModel by reflection, the caller supplies the ViewModel-change signal directly as an IObservable<object?>:

IDisposable WhenActivated(Func<IEnumerable<IDisposable>> block, IObservable<object?> viewModelChanged)
IDisposable WhenActivated(Action<Action<IDisposable>> block, IObservable<object?> viewModelChanged)
IDisposable WhenActivated(Action<MultipleDisposable> block, IObservable<object?> viewModelChanged)
IDisposable WhenActivated(IObservable<object?> viewModelChanged)

viewModelChanged emits the current ViewModel and every later value (e.g. null on clear). When an emitted value is an IActivatableViewModel, its activator is activated for the duration of the view's activation - exactly like the reflected overloads, but with no reflection.

Usage (AOT-safe): produce viewModelChanged from a reflection-free source such as the source-generated WhenAnyValue in ReactiveUI.SourceGenerators, or a hand-written INotifyPropertyChanged subscription. IObservable<T> is covariant, so a strongly-typed IObservable<TViewModel?> (reference-type ViewModel) can be passed directly.

Why it is AOT-safe:

  • The only trim-unsafe dependency in the activation path was the private HandleViewModelActivation calling view.WhenAnyValue<IViewFor, object?>(nameof(view.ViewModel)) (string/expression-based reflection).
  • That reflection is now isolated in a tiny [RequiresUnreferencedCode] bridge; the real work moved to a reflection-free HandleViewModelActivation(IObservable<object?>, IObservable<bool>) that both the reflected and the new AOT paths share.
  • Fetcher resolution and HandleViewActivation were already reflection-free (shared ResolveActivationEvents).
  • Confirmed by building the core ReactiveUI project (which sets IsAotCompatible/EnableTrimAnalyzer/ILLinkTreatWarningsAsErrors) with zero warnings, and by a behavioral test in the PublishAot/TrimMode=full ReactiveUI.AOT.Tests project that calls the new overload with no suppression.

What is the current behavior?

Every IActivatableView.WhenActivated(...) overload is [RequiresUnreferencedCode], because the ViewModel is observed via reflection. AOT/trim publishing flags them and there is no reflection-free alternative (unlike ReactiveUI.Binding).

Closes #4365

What might this PR break?

  • Additive public API only; the existing reflected overloads are unchanged in behavior.
  • One decision worth a reviewer's eye: the shared fetcher-resolution helper no longer contains the design-mode no-op branch that lived in the core WhenActivated(Func, IViewFor?). That branch was unreachable - the core GetIsDesignMode() extension always returns false at that call site, and the platform packages that provide a design-mode-aware GetIsDesignMode() (WPF) or design-mode-aware fetcher (WinForms) short-circuit before reaching it - so it never executed and runtime behavior is unchanged. Removing it keeps the file free of dead code and reachable at 100% line + branch coverage. If a functional core design-mode fallback is wanted later, it needs a design-mode signal that is actually reachable by the core call site.

Checklist

  • I have read the Contribute guide
  • Tests have been added or updated (for bug fixes / features)
  • Docs have been added or updated (for bug fixes / features)
  • Changes target the main branch
  • PR title follows Conventional Commits

Additional information

  • New public surface recorded in ReactiveUI and ReactiveUI.Reactive PublicAPI.Unshipped.txt for every TFM.
  • ViewForMixins.cs is at 100% line and 100% branch coverage, merged across the full ReactiveUI.Tests suite (1973 tests passing); new activation tests also close pre-existing gaps in the file.
  • Verified locally on net8.0/net9.0 (core, ReactiveUI.Reactive shim, tests, AOT tests). Windows/platform TFM (WPF/WinForms) validation is left to CI.

- Add trim/AOT-safe IActivatableView.WhenActivated overloads that take an
  IObservable<object?> viewModelChanged instead of discovering the ViewModel
  by reflection, so they carry no [RequiresUnreferencedCode] and stay clean
  under the trim/AOT analyzers (Func, Action<Action<IDisposable>>,
  Action<MultipleDisposable>, and a no-view-block overload)
- Isolate the reflection to a tiny [RequiresUnreferencedCode] bridge and route
  both the reflected and AOT paths through a shared reflection-free
  HandleViewModelActivation that consumes the supplied observable
- Extract ResolveActivationEvents; drop the unreachable design-mode no-op
  branch (the core GetIsDesignMode() is always false at this call site, so it
  never executed; designer safety is provided by the platform packages)
- Record the new public surface in the ReactiveUI and ReactiveUI.Reactive
  PublicAPI files
- Cover ViewForMixins.cs to 100% line and branch with real activation tests,
  plus a behavioral AOT-safety test in ReactiveUI.AOT.Tests

Closes #4365
@glennawatson
glennawatson marked this pull request as ready for review July 22, 2026 22:25
- Resolve ViewForMixins.cs conflict: keep the AOT-friendly ResolveActivationEvents
  helper, folding in main's #4358 design-mode no-op (helper returns null in design
  mode; both WhenActivated overloads short-circuit to EmptyDisposable.Instance) and
  main's cached CompositeFormat throw message.
- Update tests for main's adopted analyzers: route Scope.Create deactivation counters
  through StrongBox state with static lambdas (PSH1011/PSH1000), and make the AOT
  TestActivatableView fixture IDisposable so its signals are disposed (SST2315).
@glennawatson
glennawatson enabled auto-merge (squash) July 22, 2026 23:10
- Move the AOT-friendly IActivatableView.WhenActivated(..., IObservable<object?>
  viewModelChanged) overloads from PublicAPI.Unshipped.txt to PublicAPI.Shipped.txt.
- Applies to ReactiveUI and ReactiveUI.Reactive across all target frameworks
  (336 entries, 42 files); shipped stays ordinal-sorted, unshipped reset to header.
@glennawatson
glennawatson disabled auto-merge July 22, 2026 23:30
…le overload

The new WhenActivated(Func<IEnumerable<IDisposable>>, IObservable<object?>) overload
made the bare-null call in WpfViewForMixins.WhenActivated() ambiguous with the existing
(Func<...>, IViewFor?) overload (CS0121) on every WPF target framework. Cast the argument
to (IViewFor?)null to keep binding to the no-view overload.
@sonarqubecloud

Copy link
Copy Markdown

@codecov

codecov Bot commented Jul 23, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 83.33333% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 93.13%. Comparing base (e6088d3) to head (f739f8b).
⚠️ Report is 3 commits behind head on main.

Files with missing lines Patch % Lines
src/ReactiveUI.Shared/Activation/ViewForMixins.cs 82.92% 4 Missing and 3 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4413      +/-   ##
==========================================
- Coverage   93.13%   93.13%   -0.01%     
==========================================
  Files         345      345              
  Lines       14979    15008      +29     
  Branches     1566     1569       +3     
==========================================
+ Hits        13951    13977      +26     
- Misses        755      757       +2     
- Partials      273      274       +1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@glennawatson
glennawatson merged commit 2f34b56 into main Jul 23, 2026
14 of 17 checks passed
@glennawatson
glennawatson deleted the feature/aot-friendly-when-activated branch July 23, 2026 00:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

AOT friendly WhenActivated method

2 participants