Skip to content

fix: stop the WinForms hosts relying on WhenAny, and generate them in their own namespace - #504

Merged
glennawatson merged 1 commit into
mainfrom
fix/winforms-hosts-without-whenany
Sep 26, 2026
Merged

glennawatson merged 1 commit into
mainfrom
fix/winforms-hosts-without-whenany

Conversation

@glennawatson

Copy link
Copy Markdown
Contributor

Summary

The WinForms [RoutedControlHost] and [ViewModelControlHost] hosts no longer call WhenAny, and are generated into the host's own namespace and containing types.

  • The generated hosts follow their own properties through a nested PropertyObservable over their PropertyChanged event. It switches to the selected observable on each change, as WhenAnyObservable does, so the hosts no longer call WhenAny, WhenAnyValue or WhenAnyObservable.
  • The hosts' PropertyChanged and PropertyChanging events call SubscribePropertyChangedEvents and SubscribePropertyChangingEvents on first subscription. This is what ReactiveObject and the [IReactiveObject] generator do; without it ReactiveUI never raised the events for a host.
  • A host in the global namespace, or nested in another type, gets its members on that type. The hosts open the namespace and containing types as the other generators do, instead of writing namespace {TargetNamespace} as-is.
  • No generated code calls an API ReactiveUI.Binding has to intercept. The emitters and stored snapshots contain no WhenAny*, Bind, OneWayBind, BindCommand or ToProperty call.

Why

With ReactiveUI built on ReactiveUI.Binding, constructing either host threw No generated WhenAny dispatch matched this call site.

  • Binding's generator dispatches WhenAny calls it can see in source, and a generator cannot see another generator's output, so the calls in the generated host constructors had no dispatch.
  • A host declared in the global namespace was generated into an unnamed namespace (CS1001), so its members landed on a different type; a nested host was generated as a separate top-level type.
  • Found while making ReactiveUI.SourceGenerators a dependency of ReactiveUI.Core (feat!: bring ReactiveUI.SourceGenerators with ReactiveUI.Core ReactiveUI#4516).

Breaking changes

None. The hosts' public members are unchanged.

How this was verified

New tests compile both hosts against ReactiveUI and the desktop stubs, construct them, and check they show their default content and then the routed or hosted view; they also cover global-namespace and nested hosts and assert no WhenAny in the output.

  • A scratch app on the ReactiveUI build that depends on ReactiveUI.Binding 8.1, in both the lean and .Reactive flavours, throws the dispatch error with 4.0.0 and shows the default content, routed view and view-model view with this change.

Notes for the reviewer

Start with ControlHostWriter.cs, then the two hosts' WriteConstructor/WriteSetupBindings.

  • TestCompilationReferences.WindowsDesktopStubs now enumerates a copy of the control collection, as the real collection tolerates removal during enumeration, which the view-model host relies on.
  • The host tests read RxState.DefaultExceptionHandler once up front: ReactiveUI sets its initialized flag before assigning the handler, so two hosts constructed concurrently on a cold start can read null. That race is in ReactiveUI and is not fixed here.

Checklist

  • I have read the Contribute guide
  • The PR title follows Conventional Commits
  • Tests cover this change, or the summary says why they do not
  • New or changed public API has XML documentation

… their own namespace

The [RoutedControlHost] and [ViewModelControlHost] constructors called
WhenAny, WhenAnyValue and WhenAnyObservable. With ReactiveUI built on
ReactiveUI.Binding, those calls are dispatched by Binding's generator, which
cannot see call sites in another generator's output, so constructing a host
threw "No generated WhenAny dispatch matched this call site".

The hosts now follow their own properties through a nested PropertyObservable
over their PropertyChanged event, switching to the selected observable on each
change as WhenAnyObservable does. The events call
SubscribePropertyChangedEvents and SubscribePropertyChangingEvents on first
subscription, as ReactiveObject and the [IReactiveObject] generator do; without
that ReactiveUI never raised them for a host. No generated code now calls an
API ReactiveUI.Binding has to intercept.

The hosts were also written into `namespace {TargetNamespace}` as-is, so a host
in the global namespace produced an unnamed namespace and a separate type, and
a nested host produced a top-level type. They now open the namespace and any
containing types as the other generators do.
@codecov

codecov Bot commented Sep 26, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.55%. Comparing base (2a33b09) to head (05091d4).

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #504      +/-   ##
==========================================
+ Coverage   96.45%   96.55%   +0.10%     
==========================================
  Files          71       72       +1     
  Lines        3578     3653      +75     
  Branches      506      506              
==========================================
+ Hits         3451     3527      +76     
+ Misses        127      126       -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.

@glennawatson
glennawatson merged commit ac6d0b7 into main Sep 26, 2026
12 checks passed
@glennawatson
glennawatson deleted the fix/winforms-hosts-without-whenany branch September 26, 2026 13:50
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.

1 participant