Skip to content

feat: add main-thread sequencers for MAUI, WinUI and WinForms - #225

Merged
glennawatson merged 1 commit into
mainfrom
feat/maui-winui-winforms-main-sequencers
Sep 24, 2026
Merged

glennawatson merged 1 commit into
mainfrom
feat/maui-winui-winforms-main-sequencers

Conversation

@glennawatson

Copy link
Copy Markdown
Contributor

Summary

MAUI, WinUI and WinForms each get a shared main-thread sequencer, like DispatcherSequencer.Main on WPF.

  • MauiDispatcherSequencer.Main and .Current are new. Main binds once to the running application's dispatcher, from any thread. Before the application exists it uses the calling thread's dispatcher, and it throws when neither exists. Current returns the calling thread's dispatcher sequencer.
  • DispatcherQueueSequencer.Main and .Current are new for WinUI. Main binds to the first dispatcher queue it sees on a UI thread. After that every thread gets the same sequencer. Current returns the calling thread's dispatcher queue sequencer.
  • ControlSequencer.Main is new for WinForms. On first read it creates a hidden control whose handle belongs to the calling thread. It does this only on an STA thread, and throws on any other thread.
  • Each property is in both flavours. The ISequencer packages and the System.Reactive IScheduler packages get the same members.

Why

Today ReactiveUI finds the UI dispatcher itself on these platforms, and each lookup can bind to the wrong thread.

  • MAUI falls back to the thread-pool sequencer when the builder runs on a thread without a dispatcher, so main-thread work silently runs on the thread pool.
  • WinUI wraps the dispatcher queue lookup in a Lazy that caches its exception. One read off the UI thread breaks the scheduler for the rest of the process.
  • WinForms forces a control handle on whichever thread reads it first. On a background thread nothing pumps that thread's messages, so the work never runs.
  • With these properties in Primitives, ReactiveUI can switch to them and delete its own lookup code.

Breaking changes

None. Every change adds a member.

How this was verified

Tests cover binding, caching per thread, and the throw-without-caching rule for each new property.

  • The MAUI tests use a fake dispatcher and run on any platform.
  • I ran the WinForms and WinUI tests on a real Windows 11 machine.

Notes for the reviewer

Start with MauiDispatcherSequencer. The WinUI and WinForms files follow the same shape.

  • All three follow DispatcherSequencer on WPF: no property creates a dispatcher, and a failed lookup is never cached.
  • MauiDispatcherSequencer.Main reads the application's dispatcher from IPlatformApplication.Current.Services. That keeps the package on Microsoft.Maui.Core and avoids a dependency on Microsoft.Maui.Controls. MAUI resolves that dispatcher on the UI thread while it starts the application.
  • WinUI has no application-wide dispatcher that any thread can reach. So DispatcherQueueSequencer.Main has to be read first on a UI thread.
  • WinForms UI threads are always STA and thread-pool threads never are. ControlSequencer.Main uses that check. It still supports the usual pattern: read it in Program.Main before Application.Run.
  • The Reactive files and the PublicAPI baselines are mechanical copies of the ISequencer changes.
  • ReactiveUI.Primitives.Maui gains InternalsVisibleTo for its test project, like the Reactive project already has.
  • Out of scope: switching ReactiveUI's builders to these properties. That follows once this is released.

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

Give MAUI, WinUI and WinForms a shared main-thread sequencer, matching
DispatcherSequencer.Main on WPF, so consumers such as ReactiveUI no
longer resolve the UI dispatcher themselves. Each is added to both the
ISequencer and the System.Reactive IScheduler flavours.

MauiDispatcherSequencer.Main binds once to the running application's
dispatcher, from any thread. Until the application exists it uses the
calling thread's dispatcher. It throws when neither exists, and never
falls back to the thread pool. MauiDispatcherSequencer.Current returns
the calling thread's dispatcher sequencer, cached per thread.

DispatcherQueueSequencer.Main binds to the first dispatcher queue it
sees on a UI thread. A read from a thread without a dispatcher queue
throws, and nothing is cached. DispatcherQueueSequencer.Current returns
the calling thread's dispatcher queue sequencer, cached per thread.

ControlSequencer.Main creates its hidden control only on an STA thread,
so the handle belongs to a thread that can run a message loop. A read
from any other thread throws instead of binding to a thread that never
pumps messages.

None of these properties creates a dispatcher.
@glennawatson
glennawatson enabled auto-merge (squash) September 24, 2026 05:52
@sonarqubecloud

Copy link
Copy Markdown

@glennawatson
glennawatson merged commit 2a8b66d into main Sep 24, 2026
13 checks passed
@glennawatson
glennawatson deleted the feat/maui-winui-winforms-main-sequencers branch September 24, 2026 06:11
@codecov

codecov Bot commented Sep 24, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.78%. Comparing base (f66acd7) to head (323c274).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #225   +/-   ##
=======================================
  Coverage   99.78%   99.78%           
=======================================
  Files         785      785           
  Lines       24792    24850   +58     
  Branches     2920     2946   +26     
=======================================
+ Hits        24738    24796   +58     
  Misses         54       54           

☔ 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.

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.

2 participants