MAUI backend coverage
Regenerate docs/maui-coverage.md — the .NET MAUI ⇄ Microsoft.AndroidX.Compose.Maui
backend completeness report. The report is produced by a single .NET 10
file-based program at scripts/maui-coverage.cs that:
- Reads the pinned
Microsoft.Maui.Controlsversion fromDirectory.Build.targets. - Finds the matching
Microsoft.Maui.dll(Core) in the local NuGet cache ($env:NUGET_PACKAGESif set, else the user-profile default). - Decompiles every
Microsoft.Maui.Handlers.*Handlertype viailspycmdintoobj/maui-coverage/maui/. - Regex-parses each handler's
IPropertyMapper<TVirtual, THandler>field initializer — including base-mapper chains (ViewHandler.ViewMapper, sibling sub-mappers likeTextButtonMapper/ImageButtonMapper). - Does the same for our own handlers under
src/Microsoft.AndroidX.Compose.Maui/Handlers/*.cs. - Cross-references the two by property-mapper key name and writes a markdown report with summary, per-category roll-up, status table, gap analysis, and per-handler detail.
Workflow
1. Run the script
From the repo root (Windows / PowerShell):
dotnet run scripts/maui-coverage.cs
That's it — no .csproj, no package install, no MSBuild target. The
script handles its own caching. First run shells to ilspycmd for
every handler type (~40 entries, takes ~20 seconds); subsequent runs
are [cached] … and finish in a couple of seconds.
Expected tail output:
Stock virtual-view registrations: 53
Report written: docs/maui-coverage.md
2. Verify the diff
git diff --stat docs/maui-coverage.md
The headline numbers live in the Summary block near the top:
Select-String -Path docs/maui-coverage.md -Pattern '^- \*\*' | Select-Object -First 5
If the totals didn't move when you expected them to (e.g. you added a
new handler in src/Microsoft.AndroidX.Compose.Maui/Handlers/ but the
"Handlers we override" line is unchanged), see Troubleshooting
below.
3. Commit the regenerated report alongside the change that prompted it
The report is checked in deliberately so reviewers can see coverage movement in the PR diff. Don't commit the script run on its own — pair it with the handler addition, the package bump, or whatever change motivated the refresh.
git add docs/maui-coverage.md
git commit --amend --no-edit # or a fresh commit, depending on context
Updating the MAUI version
The script reads Microsoft.Maui.Controls's version from
Directory.Build.targets via regex on the
<PackageReference Update="Microsoft.Maui.Controls" Version="..." />
line. When you bump MAUI:
- Edit
Directory.Build.targetsto the new version (e.g.10.0.20→10.0.21). - Run a normal
dotnet buildto restore the new package into the NuGet cache. - Wipe the decompile cache — the script keys cache files by
handler type name, not by MAUI version, so stale
.csfiles from the previous version would be silently reused:Remove-Item -Recurse -Force obj/maui-coverage - Re-run the script.
Troubleshooting
"microsoft.maui.core X.Y.Z not found under ..." — the package
hasn't been restored yet. Run any dotnet build in the repo first
(easiest: dotnet build src/Microsoft.AndroidX.Compose.Maui). The
script honours $env:NUGET_PACKAGES for non-default cache locations.
"ilspycmd: command not found" — install the global tool:
dotnet tool install -g ilspycmd
Headline % didn't move after adding a handler — there are two common causes:
- The handler isn't wired into
UseAndroidXCompose()insrc/Microsoft.AndroidX.Compose.Maui/Hosting/AppHostBuilderExtensions.cs. The script parses that file forhandlers.AddHandler<MauiX, YHandler>()calls; an unregistered handler file underHandlers/is invisible. - The mapper field uses a syntax the regex doesn't recognise. The
parser expects:
Variations:public static IPropertyMapper<TVirtual, THandler> Mapper = new PropertyMapper<TVirtual, THandler>(ViewHandler.ViewMapper) { [nameof(IFoo.Bar)] = MapBar, };public new static IPropertyMapper<...>(used byPageHandler), empty{ }body, and base-mapper chains across sibling sub-mappers are all handled. Custom shapes (rawDictionary<>constructions, runtime mutation) will not be picked up — keep the declarative initializer form for consistency.
LabelHandler / CheckBoxHandler aren't listed under any virtual
view — MAUI registers a handful of handlers reflectively rather than
via the regex-scannable AddHandler<TVirtual, THandler>() calls in
Microsoft.Maui.Controls.Hosting.AppHostBuilderExtensions. The script
falls back to deriving the virtual-view name from the handler class
name (XxxHandler → Xxx) for those. See SupplementWithImpliedRegistrations.
Report looks empty / 0% across the board — the decompile cache contains a half-written file. Wipe it:
Remove-Item -Recurse -Force obj/maui-coverage
dotnet run scripts/maui-coverage.cs
What the report contains
Summary — overall percentages. Three headline numbers:
- Stock MAUI handlers in scope — denominator for handler-level
coverage (excludes the abstract
ViewHandler/ElementHandlerbases). - Handlers we override — how many of those have a Compose
backend in
src/Microsoft.AndroidX.Compose.Maui/Handlers/. - Property-mapper keys covered — finer-grained signal across every property key on every covered handler. This is the most useful single number for tracking "behavioural completeness" of individual handlers over time.
- Stock MAUI handlers in scope — denominator for handler-level
coverage (excludes the abstract
Per-category coverage — handlers grouped into Pages / Navigation, Containers, Leaves, Menus / Toolbar, Shapes, App / Window, Other / advanced. Each row shows handler coverage and key coverage for the bucket.
Per-handler coverage — single table, sorted by category, with a ✅ / 🟡 / ❌ status icon for every stock handler. ✅ = all keys covered, 🟡 = partial, ❌ = no Compose backend at all.
Missing handlers worth investigating — ❌ handlers sorted by number of stock keys (biggest gaps first). These are the highest- leverage candidates for new handler work.
Partial handlers (gap analysis) — 🟡 handlers sorted by coverage % (lowest first), listing the specific missing keys. This is the prioritised TODO list for tightening existing handlers.
Per-handler property detail — one section per stock handler. Missing keys are listed inline; the full
[x]/[ ]stock-key list is in a collapsed<details>block to keep the report skimmable.Microsoft.AndroidX.Compose.Maui-only handlers — internal plumbing handlers with no stock counterpart.
Caveats — the matching heuristics in plain English. Always worth a re-read before drawing conclusions from a number — in particular,
ViewHandler.ViewMapperis shared viaRemapForCompose()so every Compose-backed handler getsOpacity/Scale*/Rotation*/etc. "for free", andCommandMapperkeys (focus, scroll-to, …) aren't included.
Don't do these things
- Don't hand-edit
docs/maui-coverage.md. It's regenerated every run and your edit will be lost. - Don't add
obj/maui-coverage/to git — it's already covered by the rootobj/gitignore entry and contains the full decompiled MAUI Core handler source. - Don't run the script from inside a subdirectory; paths in the
script (
obj/maui-coverage,docs/maui-coverage.md,src/Microsoft.AndroidX.Compose.Maui/Handlers/,Directory.Build.targets) are relative to the repo root. - Don't compare coverage % across MAUI version bumps without first
wiping
obj/maui-coverage/— the cache is keyed by handler class name, not by MAUI version.