Skip to content

Index the gallery's sample pages so other tools can consume them - #2232

Merged
Niels Laute (niels9001) merged 17 commits into
microsoft:mainfrom
Jaylyn-Barbee:jay/sample-catalog-manifest
Sep 16, 2026
Merged

Niels Laute (niels9001) merged 17 commits into
microsoft:mainfrom
Jaylyn-Barbee:jay/sample-catalog-manifest

Conversation

@Jaylyn-Barbee

@Jaylyn-Barbee Jaylyn Barbee (Jaylyn-Barbee) commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

Makes the gallery's samples readable by other tools, by publishing an index of every page and the samples on it as a single generated JSON file.

Why

The gallery is where WinUI's sample code actually lives, and it's the answer to "show me how to use this control." But that answer is only reachable by a human browsing the app, or by a program scraping this repo's markup. Scrapers are the norm today — winapp find-ui ships one — and they break every time a page moves, a sample is renamed, or an example changes shape. The gallery has no way to know it broke them, and they have no way to know they're now showing stale or partial code.

Nothing here is a new sample. The samples are already written, already reviewed, and already correct; they're just not addressable. This makes the set the gallery already maintains available as data, so the gallery stays the single source of truth for WinUI sample code instead of being the thing everyone re-derives.

What this adds

catalog/windows-samples.json, generated from the same .xaml pages and snippet bundles that the app itself renders. 123 pages, 333 samples — 290 with XAML, 127 with C#.

Each entry carries what a consumer needs to present a sample without knowing anything about this repo's layout: the control's name and description, its docs links, its keywords, and the samples themselves with their headers and code.

{
  "id": "geometry",
  "name": "Geometry",
  "description": "Clear geometric design ensures visual coherence and structure.",
  "curatedKeywords": ["path", "vector", "figures"],
  "docs": [
    { "title": "Geometry in Windows 11", "uri": "https://learn.microsoft.com/windows/apps/design/signature-experiences/geometry" }
  ],
  "samples": [
    {
      "header": "Geometry",
      "xaml": "<Grid CornerRadius=\"{StaticResource OverlayCornerRadius}\"/>\n<Grid CornerRadius=\"{StaticResource ControlCornerRadius}\"/>"
    }
  ]
}

It's a shared format, not a gallery-specific one

The file conforms to winui-sample-index.schema.json, an index contract that's already published and already consumed for other sample sources. Emitting that shape rather than inventing one means consumers need no gallery-specific adapter, and the same shape can describe a controls index or another sample source later. $schema points at the published contract so there's one source of truth and no copy to drift.

Fields that only make sense for this repo — the page path, the snippet file, the gallery group — are grouped under a gallery object on each control and sample, so what's contract and what's ours is obvious at a glance.

What makes it dependable

An index nobody can trust is worse than no index, so the generator is built around the ways this could quietly go wrong:

It can't drift from the samples. The file is generated, and the PR validation pipeline runs a step (Verify catalog/windows-samples.json is up to date) that regenerates it from the current sources and fails the build if the committed copy differs by so much as a byte. A new sample can't land without the index following. It isn't auto-committed — the failure tells you to run dotnet run --project tools/CatalogExporter -- generate and commit the result, so the index stays a reviewable part of the diff rather than something a bot rewrites underneath you.

Sample order is stable. Consumers address samples positionally, so samples are emitted in page order — the order a reader sees them in the app. Appending is safe. Alphabetical-by-filename order was the earlier approach and is subtly wrong: adding ButtonA.txt would renumber every later sample on that page and silently repoint existing references at different code.

Nothing is published that a consumer will silently discard. Consumers drop samples whose XAML doesn't parse, which is the worst failure mode because it looks like success on our side. The generator runs the same well-formedness check the consumer does and reports what it withheld.

That withholds 12 snippets, all deliberate authored elisions like <Window ...> — right for the gallery's code viewer, impossible as XML. All 12 still publish their C#, so no sample is lost.

A second check withholds 7 more: fragments that bind an XML namespace prefix their page never declares. These parse fine in isolation, which is why they went unnoticed, and fail only when a reader pastes them alongside the imports we publish. That is strictly worse than malformed XAML, because it clears our gate and breaks at their end instead of ours. Five still publish their C#; two are XAML-only and so leave the index entirely (FlipviewShowingBoundData, LayingOutNestedItemsrepeaters). Both bind gallery-internal types through x:DataType, so no import would have rescued them — they were never portable, and withholding them is the honest result.

One further XAML-only snippet is dropped outright (OtherXamlEasingFunctions, which uses a token as an element name resolved from code-behind). All 20 are pinned by a test so the set can't grow unnoticed.

Code is published as a reader would see it. Snippets contain $(Token) placeholders bound to each page's interactive options; published verbatim, many are not valid XAML. They now resolve to the value the control starts with — what the gallery shows on load. Resolution reads only what the markup states; when the answer depends on running code, the XAML attribute carrying the token is deleted so the property falls back to its own default, and a gate asserts none survives. Publishing a value the gallery never shows would be worse than publishing none. C# is the exception and is covered below.

What maintainers are taking on

Two new top-level folders and one generated data file. Nothing existing is restructured, and the gallery app itself gains no new runtime dependency — the exporter is a build-time console tool that never ships inside the app.

Path What it is
catalog/windows-samples.json The generated index (10,642 lines, 684 KB). Output, not source — never hand-edit it; run generate and commit the result.
catalog/README.md Explains the format, what each field means, and how to regenerate. Start here.
tools/CatalogExporter/Program.cs Entry point. Two commands: generate writes the index, check fails if the committed copy is stale.
tools/CatalogExporter/CatalogGenerator.cs The bulk of the logic (670 lines): reads ControlInfoData.json and the sample pages, builds the index.
tools/CatalogExporter/CatalogModels.cs The output shape. Property declaration order is the JSON field order.
tools/CatalogExporter/SourceModels.cs Typed view of ControlInfoData.json.
tools/CatalogExporter/SampleBundle.cs Parses .txt snippet bundles. Deliberately mirrors ControlExample.ParseSampleCodeSections.
tools/CatalogExporter/SubstitutionResolver.cs Resolves $(Token) placeholders to the value each page's control starts with.
tools/CatalogExporter/TokenFallback.cs Removes the attribute carrying a token the resolver could not settle, and records its name. Holds the gate that asserts no token reaches published XAML.
tools/CatalogExporter/XamlFragment.cs The well-formedness gate and namespace-prefix detection.
tests/WinUIGallery.CatalogExporter.Tests/ 73 tests across 6 files. Includes the staleness guard and the pinned list of snippets whose XAML can't be published.
14 new .txt bundles under Samples/AccessibilityKeyboard/ and Samples/AccessibilityScreenReader/ Sample code migrated out of inline page markup. Same code, same rendering — just reachable by tooling now.

Six existing files are modified, all narrowly:

Path Change
WinUIGallery.slnx Adds the two new projects.
.pipelines/azure-pipelines.yml Adds the staleness-check step described below.
ControlInfoData.json Adds an optional Catalog block to two entries (search aliases only), and drops two dangling RelatedControls references from LayoutPanel.
ControlInfoDataSchema.json Documents that optional Catalog block: Exclude, Aliases, RelatedSamples.
AccessibilityKeyboardPage.xaml, AccessibilityScreenReaderPage.xaml Inline sample code replaced with SampleDefinition references to the new bundles.

The Catalog block in ControlInfoData.json is opt-in and additive. The running gallery app never reads it, and omitting it is the normal case — it exists only for the rare sample that needs to override what the exporter would otherwise derive on its own.

What runs in CI, and when

One step is added to the existing WinUI-Gallery-PRValidation pipeline, so it runs on every PR and on pushes to main alongside the checks already there. No new pipeline, no new schedule, no new secrets, and nothing that publishes anywhere.

Step When What it does How it fails
Verify catalog/windows-samples.json is up to date Every PR build, before the app build Runs the 73 exporter tests. The key one regenerates the index from the current sample sources and byte-compares it to the committed file. Build fails with the exact command to fix it: dotnet run --project tools/CatalogExporter -- generate, then commit.

It adds roughly 7 seconds of test time plus the exporter's own build. It never writes to the repo — no bot commits, no auto-push. A stale index is surfaced as a failing check and fixed by the contributor, so the generated file stays visible in review instead of changing underneath you.

One thing worth knowing: the guard is a test, and it throws if it can't locate the repo root rather than skipping. It can't silently pass in an environment where it didn't really run.

Incidental fixes found along the way

Some samples supplied their code as inline <ControlExample.Xaml> markup instead of a snippet bundle. They rendered fine in the app but were invisible to any export, with no symptom. They're migrated to bundles, and a test fails if the inline form comes back.

ControlInfoDataSchema.json does not parse as strict JSON on main: there's a dangling comma before a closing brace (line 101). PowerShell's ConvertFrom-Json tolerates it, JSON.parse does not, and nothing in CI parses this file strictly, which is how it drifted unnoticed. It parses now — though worth flagging that the fix is positional rather than deliberate: the new optional Catalog property happens to land exactly where the dangling comma was, so the comma now separates two real properties. Validating this file in CI would be a reasonable small follow-up.

Verification

Beyond the exporter's 73 tests:

  • Validated against the published contract with ajv (draft 2020-12).
  • Every published XAML fragment clears the same well-formedness check a consumer applies, and a build-time gate fails the export outright if a $(Token) reaches published XAML. Both run on every PR rather than being spot-checked, so a consumer discards nothing: 290 XAML and 127 code blocks publish, none of the XAML carrying a placeholder.
  • The gallery app builds clean, and every SampleDefinition reference resolves — a dangling one compiles fine and shows an empty code viewer at runtime. The exporter validates the full Folder\File.txt path rather than just the file name, matching how ControlExample resolves it at runtime.

Notes for reviewers

  • The exporter began as Niels Laute (@niels9001)'s work in Export WinUI Gallery samples as a catalog/windows-samples.json manifest #2226, preserved here as the first commit; this supersedes that PR.
  • SampleBundleParser deliberately mirrors ControlExample.ParseSampleCodeSections. If the two drift, the index publishes code users never actually see, so the tests pin the rules most likely to be got wrong.
  • A prefix a page doesn't declare — a snippet using local: to mean "your namespace" — was originally omitted from xmlnsImports rather than guessed at, since an invented URI would look authoritative and not compile. That still left the fragment published but unusable, so the fragment itself is now withheld and the offending prefixes recorded in gallery.xamlOmittedUnboundPrefixes.
  • 7 pages emit zero samples because every example on them is code-less or withheld. That's legal and visible in the output rather than hidden.

$(Token) placeholders

Snippets carry $(Token) placeholders that the gallery substitutes at runtime from live control values. Published verbatim they break the contract's promise of code "exactly as it should be pasted into a project".

Published XAML now contains none. The exporter resolves a token from what the markup states; where it can't, it deletes the attribute carrying it, so the property falls back to its own default. A build-time gate then fails the export if any token survives in published XAML, so the guarantee is asserted rather than assumed. 0 tokens across 290 XAML samples, with 40 samples listing what was removed in gallery.xamlPlaceholdersDropped so the omission is inspectable rather than invisible.

The tradeoff is deliberate. A dropped attribute can leave a sample not matching the gallery's rendering 1:1, but it always compiles. Inference was tried and rejected: 13 sliders declare Minimum > 0, so a guessed Value silently coerces to something the gallery never shows, and several pages set real values in their constructors. Not resolving beats resolving badly.

This adds no work for anyone writing a sample. A new token nobody has taught the exporter about degrades to a dropped attribute on its own.

C# still carries 11, across 6 controls. The XAML approach has no C# equivalent, because no C# construct yields a default by being absent. The tokens appear as identifier fragments (BadgeNotificationGlyph.$(SelectedGlyph)), fixed-arity arguments (SetBorderAndTitleBar($(HasBorder), $(HasTitleBar))), and entire statements, none of which can be deleted without a syntax error or gutting the sample. Those samples publish their code as-is.

They are now declared rather than resolved. gallery.codePlaceholdersPresent lists the token names on each of those 11 samples, so a consumer knows up front that the C# is not pasteable instead of finding out at compile time.

It is deliberately not symmetric with xamlPlaceholdersDropped. That field means "removed, output is clean"; this one means "still present, output is not." A consumer conflating the two would paste non-compiling C#, which is the exact failure the field exists to prevent, so the names differ and catalog/README.md states the asymmetry explicitly. No fatal gate is added for C#, because unlike XAML these tokens are expected and allowed.

What contributors need to know once this lands

Two things will surprise someone otherwise, and both are cheap to say up front.

Adding a sample gains a second step. Deriving the index is automatic: a new Samples/{UniqueId}/ folder is picked up with no registration, no list to append to, and no mapping table. Regenerating it is not. Any PR that adds, renames, or removes a sample, or edits a snippet .txt, has to run dotnet run --project tools/CatalogExporter -- generate and commit the result, or the staleness check fails. The failure lands on a PR whose diff may look entirely unrelated to catalog/, which is the part that reads as a mystery if you haven't hit it before. The error message names the exact command to fix it.

This branch is its own worked example. #2233 and #2234 merged while it was open, and their three new controls and eight new samples flowed into the index the moment the generator was next run — including #2234's consolidation of two Windowing snippets into one, which the index tracked as a rename with no manual intervention and no mapping to maintain.

RelatedControls gains its first consumer, and its schema description is misleading. ControlInfoDataSchema.json documents the field as "UniqueIds of other controls" — accurate, and obeyed by 330 of the 332 references currently on main. It then says those values are "used to surface 'Related controls' suggestions on the control's detail page," and no such code exists: the field has no .cs or .xaml reader anywhere in WinUIGallery/. Until now a wrong value did nothing observable, so nothing caught one — and an author reading that sentence would reasonably expect a mistake to show up by running the app.

The exporter is the first thing to read the field, and it validates the cross-references so a dangling entry is a build failure rather than a silent dead link. LayoutPanel arrived from #2233 listing Panel and StackLayout, which are WinUI type names rather than gallery pages; both are dropped here, leaving ItemsRepeater. Nothing is lost, since Panel was already published through BaseClasses and Tags. Correcting that stale second sentence in the schema is a reasonable follow-up, as it is the part that would mislead the next author.

Follow-up, deliberately not in this PR

WinUIGallery/Samples/SampleCode/ is a leftover from an older layout. Of its 164 .txt files, 157 (~181 KB) are reachable by nothing — no SampleDefinition, no CodeSourceFile, no code-behind path — yet WinUIGallery.csproj globs Samples\**\*.txt, so all of them ship inside the app as dead payload.

The folder can't be deleted wholesale: 7 files are still loaded by name from code-behind, 3 under ScrollView\ (ScrollViewPage.xaml.cs) and 4 under Templates\ (TemplatesPage.xaml.cs). Those paths are built as runtime strings, so no build-time check catches them — worth knowing for whoever picks up the cleanup.

- Add tools/CatalogExporter: a small, platform-agnostic console tool/library
  that derives a machine-readable manifest from the existing
  WinUIGallery/SampleSupport/Data/ControlInfoData.json source of truth plus
  the on-disk WinUIGallery/Samples/<UniqueId>/ folders, validates it
  (unique ids, case-exact referenced paths, resolvable RelatedControls /
  Catalog.RelatedSamples references, existing SampleDefinition snippets),
  and serializes it deterministically.
- Generate catalog/windows-samples.json (120 samples) plus
  catalog/windows-samples.schema.json (JSON Schema contract) and
  catalog/README.md (design rationale + regeneration/check instructions).
- Extend ControlInfoDataSchema.json with an optional, additive Catalog
  override block (Exclude/Aliases/RelatedSamples), demonstrated on the
  Button and ScratchPad entries in ControlInfoData.json. Everything else
  is derived, so no per-item sample.yml duplication is introduced.
- Add tests/WinUIGallery.CatalogExporter.Tests (plain MSTest, no WinUI
  dependency) covering normalization, determinism, path/reference
  validation, and stale-output detection against the real repository data.
- Wire both new projects into WinUIGallery.slnx and add a fast
  'dotnet test' step to azure-pipelines.yml so a stale manifest fails CI.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Prepares the catalog export for consumption by winapp find-ui, which needs to
resolve a scenario to its source without scraping the gallery's XAML.

- Scenarios now carry a stable, source-qualified id derived from the snippet
  file name rather than their position in the page, so inserting or reordering
  scenarios never renumbers the others.
- schemaVersion is an integer, matching how the federated windows-samples
  catalog versions its own documents, and is pinned with "const" in the schema.
- Scenario source is published in a new catalog/windows-samples.code.json,
  keyed by scenario id. It is kept out of the manifest because the manifest is
  what gets imported into the federated catalog, which carries no source;
  inlining it would add ~400 KB of dead weight there. Both files are produced
  by a single generator pass so they cannot drift.
- Scenario descriptions now come from the snippet's "--- header" section, which
  is the prose the gallery already shows above each scenario.

Snippet bundles are parsed by SampleBundleParser, a deliberate mirror of
ControlExample.ParseSampleCodeSections. The catalog's promise is "this is the
code the gallery shows", so the two must agree; SampleBundleParserTests pins the
rules that are easiest to get subtly wrong.

Also fixes catalog/../ControlInfoDataSchema.json, which was invalid JSON: the
new Catalog override block left a trailing comma. Nothing in the build parsed
it strictly, so it went unnoticed.

Two scenarios appear in the manifest with no code: ContentIsland hides its code
viewer deliberately, and SystemBackdropElement still supplies code through the
legacy ControlExample.XamlSource property, which the exporter does not read yet.
A test pins that set so the gap stays visible.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9bdab9d1-bc7e-4bf2-8a04-610ad6c5a9b3
The gallery had two ways to attach code to a ControlExample: a SampleDefinition
snippet bundle, and inline <ControlExample.Xaml> / <ControlExample.CSharp>
markup. The catalog exporter only understands the first, so the 14 scenarios
still using the inline form rendered correctly in the app but were missing from
catalog/windows-samples.code.json with no visible symptom.

Move those 14 (6 in AccessibilityKeyboard, 8 in AccessibilityScreenReader) into
snippet bundles. The migration is render-identical: SampleCodePresenter already
normalizes with TrimStart('\n')/TrimEnd plus a per-line TrimEnd, which converges
with the bundle parser's Trim() for every one of these payloads, and each was
checked to confirm no block's first content line is indented. No "--- header"
sections are added, because these pages render their own heading TextBlocks and
a ControlExample header would be newly visible text.

Add RealRepository_NoSampleUsesInlineControlExampleCode to keep the single
format from eroding. It parses each page rather than text-searching it, so
commented-out markup is not mistaken for a real usage, and it also asserts every
sample page is well-formed XML.

SystemBackdropElement stays code-less on purpose. Its page swaps XamlSource at
runtime across Acrylic, Mica and MicaAlt, so no single snippet represents it -
and because SampleCodePresenter prefers Code over CodeSourceFile, giving it a
bundle would pin the code pane to one variant and break the picker.

The catalog now publishes 330 scenarios, 328 of them with code.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9bdab9d1-bc7e-4bf2-8a04-610ad6c5a9b3
Snippets can contain $(Token) placeholders that a page binds to its interactive
option controls through ControlExampleSubstitution. The gallery replaces them as
the reader moves a slider or picks from a dropdown, so a raw token is never
visible in the app - but the catalog published them verbatim, leaving 86 of 328
scenarios (243 occurrences) with code that is not valid XAML and cannot be
pasted into a project.

Resolve each token to the value its control starts with, which is exactly what
the gallery renders on first load. That drops it to 55 scenarios and 121
occurrences, and every remaining one is a case where the answer genuinely is not
in the markup.

Resolution is deliberately conservative, because publishing a value the gallery
does not show would be worse than publishing none. It reads only what the markup
states: a literal Value, an initial attribute on the bound control, or the item a
selector explicitly marks as selected via IsSelected or SelectedIndex. Converter
functions, framework defaults, and selectors with no declared selection are left
as the original token.

Two behaviours of ControlExampleSubstitution that are easy to miss are honoured:
a disabled substitution resolves to the empty string rather than its value, and a
literal keeps its surrounding whitespace, since snippets such as CommandBar's
rely on ' IsSticky="True" ' to supply its own separating spaces. Ignoring either
one produces markup the gallery never renders.

Finding the substitutions for a snippet means knowing which ControlExample owns
it, so scenario discovery now parses the page instead of regex-matching
SampleDefinition. Parsing is safe here - a test already asserts every sample page
is well-formed XML - and it also stops commented-out markup from being read as
real. Sample and scenario counts are unchanged at 120 and 330, confirming the two
approaches find the same set. Two fixtures gained the namespace declarations that
every real page has.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9bdab9d1-bc7e-4bf2-8a04-610ad6c5a9b3
The exporter now emits the index contract winappCli already publishes and
consumes (docs/winui-sample-index.schema.json), instead of a gallery-specific
shape. That makes this catalog directly ingestible by `winapp find-ui` with no
translation layer, and lets winappCli retire its HTML scraper.

What changed:

- One file, one contract. catalog/windows-samples.json now carries the
  contract's controls/samples shape, with gallery-only fields grouped under a
  `gallery` object so contract vs. extension is obvious. The sibling
  windows-samples.code.json and both gallery-local schemas are gone; `$schema`
  points at winappCli's published schema as the single source of truth.

- Code ships inline. A sample's C# now sits on the sample itself, per the
  contract, rather than in a parallel file consumers would have to join.

- Samples are emitted in page order, not alphabetically by filename. The
  contract assigns sample ids positionally, so appending must be safe and
  reordering must not happen; filename order would renumber every later sample
  whenever one is added.

- Fragments are gated for well-formedness before publishing, byte-compatibly
  with the consumer's own check, because the contract says consumers drop
  samples whose XAML does not parse. 12 snippets use deliberate authored
  elisions (`<Window ...>`) that read well in the gallery but are not XML;
  their C# still publishes, so no sample is lost. One XAML-only snippet that
  uses a token as an element name is dropped, and that set is pinned by test.

- Unset boolean substitutions gated on IsChecked/IsOn/IsSticky/IsOpen now
  resolve to false rather than being left unresolved, matching what the gallery
  itself renders. This recovered 6 samples.

Result: 120 controls, 327 samples. Verified valid against winappCli's schema,
and ingested end to end by winappCli's own SampleIndexParser and
ScenarioSanitizer with zero silent loss (289 XAML in / 289 out, 124 code in /
124 out, none emptied by sanitization).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9bdab9d1-bc7e-4bf2-8a04-610ad6c5a9b3
@Jaylyn-Barbee Jaylyn Barbee (Jaylyn-Barbee) changed the title Export the gallery's samples as a machine-readable index Index the gallery's sample pages so other tools can consume them Sep 14, 2026
The step was renamed to "catalog files" when the exporter briefly emitted a
second code file. It emits one file again, so the plural label no longer
matches what the step checks. Restore the original wording, which names the
artifact and so says something useful in a failed build log.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9bdab9d1-bc7e-4bf2-8a04-610ad6c5a9b3
Review of the index against the published contract turned up three fields
whose emitted value did not match what the schema promises a consumer.

Emit control-level `usings`. The contract defines it so a consumer can
prepend `using X;` and have a snippet compile on its own, but it was never
populated: 51 controls ship C# and none carried it, so BreadcrumbBar
published `ObservableCollection<Folder>` with no way to resolve it. They are
taken from the imports each page's code-behind was written against. The
gallery's own namespaces are filtered out, since prepending
`using WinUIGallery.Helpers;` would cause the failure the field prevents.

Resolve `relatedControls` to display names. ControlInfoData.json stores
UniqueIds there and they were copied straight through, so 29 of 322 entries
rendered an internal id: Binding listed "XamlResources" and "XamlStyles"
where a reader expects "Resources" and "Style".

Fix namespace detection inside prefixed attributes. The scan consumed the
"=" while matching an attribute name, so a prefixed type in that attribute's
value was never seen: x:DataType="local:Contact" found only "x" and dropped
the one import the reader needs. 13 samples were published with an
undeclared prefix. The remaining 7 are pages that genuinely never declare
the prefix, which is left alone rather than guessed at.

Also document regenerating the index when adding a control page, so a
contributor following the repo's own instructions does not open a PR that
fails CI with no hint why.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9bdab9d1-bc7e-4bf2-8a04-610ad6c5a9b3
@Jaylyn-Barbee
Jaylyn Barbee (Jaylyn-Barbee) marked this pull request as ready for review September 14, 2026 17:14
@Jaylyn-Barbee

Copy link
Copy Markdown
Contributor Author

Niels Laute (@niels9001) — picking this up from #2226 with your blessing; your exporter commit is preserved as the first commit here, and this supersedes that PR.

Since your draft, the main change is that the index now targets a shared, versioned contract rather than a gallery-shaped format, so winapp (and anything else) can consume it without scraping the repo. I then reviewed the emitted JSON field-by-field against that contract and fixed three places where what we published didn't match what the contract promises:

  • usings was never emitted. 51 controls ship C# and none carried it, so BreadcrumbBar published ObservableCollection<Folder> with nothing to resolve it. Now taken from each page's code-behind imports, with the gallery's own namespaces filtered out.
  • relatedControls shipped UniqueIds instead of display names. Binding listed XamlResources and XamlStyles; it now reads Resources and Style. 29 of 322 entries were affected.
  • Namespace detection missed prefixed types inside prefixed attributes. x:DataType="local:Contact" found only x, so 13 samples published XAML with an undeclared prefix.

There's one thing I'd rather not decide alone, and it's in the description under One open question: the 49 samples that still carry an unresolved $(Token). The exporter resolves what it can statically, but things like $(SelectedAppNotificationSoundEvent) bind to controls populated in code-behind, so there's no static answer. Publishing as-is is still better than today's scraping, which resolves nothing — but it doesn't match the contract's "exactly as it should be pasted" wording, and dropping the affected fields would delete 41 samples outright. You know the substitution system far better than I do, so I'd value your read.

Everything else is verified rather than asserted: 51 exporter tests pass, the index validates against the published schema with ajv, the gallery app builds clean, and CI regenerates and byte-compares the index on every PR so it can't silently drift.

Gallery snippets use $(Token) substitutions that the running app replaces from
its interactive option controls. A static index has no runtime, so the resolver
must decline any token whose value depends on running code, and those tokens
were published verbatim -- leaving the index advertising XAML that does not
paste.

Resolve this by deletion rather than inference. TokenFallback removes the
attribute carrying an unresolvable token, which leaves that property at its own
default; where the token binds the demo control's own property, that is exactly
what the gallery shows on load. A token standing in for a whole attribute is
removed the same way, so it no longer renders the fragment unparseable. Never
guessing a value is the point: not resolving beats resolving badly.

CatalogGenerator re-checks each fragment after stripping and fails the build if
a token survived, so a published xaml value never contains a placeholder. The
names that were dropped are reported in gallery.xamlPlaceholdersDropped, so the
loss is visible rather than silent.

Stripping also makes five previously malformed fragments parse, so the index
grows from 322 to 327 samples. C# is unchanged and may still carry tokens in
positions where deletion would not leave valid code; catalog/README.md now
states that split guarantee explicitly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
PR microsoft#2229 replaced the CreateMultipleWindows sample folder with Windowing, so the committed index no longer matched a fresh generate. 327 -> 329 samples; the no-placeholder guarantee still holds.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@niels9001
Niels Laute (niels9001) requested a balanced review from Copilot September 15, 2026 17:30

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Published XAML is guaranteed placeholder-free: an unresolvable token takes its
enclosing attribute with it, the property falls back to its own default, and a
build-time gate fails if one survives. C# has no equivalent. There is no
construct whose absence yields a default, and the tokens that actually occur sit
in identifier fragments (BadgeNotificationGlyph.$(SelectedGlyph)), in
fixed-arity argument lists (SetBorderAndTitleBar($(HasBorder), $(HasTitleBar))),
and in whole statements ($(TxtFileType)$(JsonFileType)). Deleting any of them
leaves code that does not compile or a sample with its subject cut out, and
inferring a value is worse than publishing none: a Slider with a Minimum above
zero silently coerces what it is handed, and several of these tokens bind to
controls a constructor initializes.

So the tokens ship verbatim, which until now was undeclared - code looked like
ordinary pasteable C# to any consumer. Emit gallery.codePlaceholdersPresent with
the distinct token names a sample's code still carries, omitted when there are
none.

The name is deliberately not symmetric with xamlPlaceholdersDropped, and the
asymmetry is the point: that field reports tokens already removed and output
that is clean, this one reports tokens still present and output that is not.
A consumer conflating the two pastes non-compiling C#, which is the failure this
exists to prevent.

No gate is added for C# - these tokens are expected and allowed. The XAML path
is untouched and still publishes zero tokens across 291 fragments. Eleven
samples across six controls gain the field.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The exporter currently publishes non-pasteable samples and can validate paths the gallery cannot load.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (1)

tools/CatalogExporter/CatalogGenerator.cs:533

  • The shared contract defines code as C# that can be pasted directly, but this branch deliberately publishes unresolved placeholders. The committed catalog already contains values such as AppNotificationSoundEvent.$(SelectedAppNotificationSoundEvent), and the current winappCli parser has no support for gallery.codePlaceholdersPresent, so these brace-balanced snippets survive sanitization and are presented as broken C#. Please either resolve/omit templated code or add consumer-supported placeholder semantics to the shared contract before publishing it.
  • Files reviewed: 39/40 changed files
  • Comments generated: 3
  • Review effort level: Balanced

Comment thread tools/CatalogExporter/XamlFragment.cs
Comment thread tools/CatalogExporter/CatalogGenerator.cs Outdated
Comment thread tools/CatalogExporter/CatalogGenerator.cs
…ion whole

Two gaps found in review, both of which let the exporter validate something the
runtime or the consumer would then fail on.

Published XAML promised to be pasteable, but DetectImports quietly skipped any
prefix the page did not declare rather than inventing a URI for it. That left
seven fragments referencing a namespace nothing binds. IsWellFormed cannot catch
this: it synthesizes a declaration for every prefix it sees, deliberately, so
that it agrees with the consumer's parser - so such a fragment parsed on both
sides and broke only once a reader pasted it with the imports we published.
These prefixes (local:, common:, l:, data:) name gallery-internal types, so no
import would make them portable. The XAML is omitted instead, the prefixes are
recorded in gallery.xamlOmittedUnboundPrefixes, and
RealRepository_EveryPublishedFragmentDeclaresThePrefixesItUses asserts the
guarantee as a property rather than a pinned list.

Two of the seven carry no C# and therefore leave the index entirely:
FlipviewShowingBoundData.txt and LayingOutNestedItemsrepeaters.txt. Accepted
deliberately - both were only ever publishable as markup a consumer could not
compile.

Narrowing this check mattered as much as adding it. "using" is the scheme half
of a XAML namespace URI, not a prefix, and a fragment that declares xmlns on its
own root needs nothing from its page; treating either as unresolved cost five
more samples their XAML for no reason.

Separately, SampleDefinition was validated on its file name alone, while
ControlExample loads it as "Samples/<SampleDefinition>". A value naming the
wrong folder passed whenever a file of that name happened to sit next to the
page, and failed only in the running app as an empty code viewer. The whole
relative path is now checked against the sample's own folder. All 332 current
values already conform, so the index is unchanged by this half.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
LayoutPanel arrived in microsoft#2233 listing "Panel" and "StackLayout" under
RelatedControls, which holds UniqueIds of other gallery sample pages. Neither is
one: they are WinUI type names, and git history shows no commit ever added
either as a sample. The exporter validates cross-references so a rename never
silently produces a dead link, so Generate threw and catalog/windows-samples.json
could not be regenerated by anyone on this branch - 10 tests failed on it.

Nothing is lost by removing them. "Panel" is already published twice over: it is
in LayoutPanel's BaseClasses, which feeds keywords, and in its Tags, which feed
curatedKeywords. "StackLayout" is an ItemsRepeater layout rather than a base
class of LayoutPanel, so moving it to BaseClasses would have published something
false into a searchable field. ItemsRepeater, the one entry that names a real
sample, stays.

This also picks up the regeneration the merge in cf665c3 left outstanding:
123 controls and 333 samples, including the new experimental controls. None of
the samples added by microsoft#2233 or microsoft#2234 bind an undeclared namespace prefix.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Jaylyn Barbee (Jaylyn-Barbee) added a commit to microsoft/winappCli that referenced this pull request Sep 15, 2026
#852)

`find-ui` promises code you can paste. For Gallery-style samples it
could not keep that promise: those snippets carry `$(Name)` substitution
placeholders that the Gallery resolves at runtime from live option
controls, and nothing downstream was checking for them.

### The two ways this went wrong

**On the scraping path**, a placeholder in an attribute value was
rewritten to the literal `"..."`. That keeps the markup well-formed,
which is why it survived review as "cosmetic" — but it is not pasteable.
Decompressing the previous bake and counting shows **140 attributes**
set to `"..."`, and they are overwhelmingly *typed* properties:

| Property | Count | Why `"..."` fails |
| --- | --- | --- |
| `StrokeThickness`, `Height`, `Width`, `RadiusX/Y`, `Value` | 24 | not
a `double` |
| `Orientation`, `SelectionMode`, `PaneDisplayMode` | 13 | not an enum
member |
| `IsChecked` | 2 | not a `bool` |

Every one of those is a compile error the moment someone pastes it.

**On the index path**, nothing looked for placeholders at all, so a
sample could be served with its tokens intact.

### What this changes

A shared detector, `SampleSubstitutionPlaceholder`, is now the single
definition of what a placeholder is. Both paths use it.

**Scraping path.** `GalleryFetcher.NormalizeMarkupSubstitutions`
resolves a token by the position it occupies rather than
blanket-replacing it:

- *attribute-name position* (`<Button $(Background)/>`) — the whole
attribute is removed
- *content position* — becomes a comment
- *value position* (`Value="$(X)"`) — **the whole attribute is
dropped**, so the property falls back to its own default

The last one is the behaviour change. Dropping the attribute is what the
Gallery-side exporter already does, and it is the only option that
compiles.

Two supporting fixes were needed to make that actually take effect:

- `ExtractInlineCode` flattened XAML tokens to `"..."` *before*
`CleanGalleryContent` ran, so the position-aware pass never saw them. It
no longer pre-flattens.
- `CleanGalleryContent` now runs the known `IsOpen` / `Severity`
rewrites *before* normalization, so those real values survive instead of
having their attribute dropped.

**Index path.** `SampleIndexParser` suppresses a language block that
still contains a placeholder and drops the sample only if neither block
survives. `ScenarioSanitizer` enforces the same rule on the way out, so
a placeholder cannot reach a caller through either route.

**Cache.** `CacheVersion` goes to `22`. Same input, different output, so
without the bump an existing cache keeps serving the broken attributes
this change exists to remove — the trap entry `"20"` already documents.
The snapshot is re-baked accordingly.

### Result

Measured against a fresh bake, decompressing each snapshot and parsing
the JSON:

| Source | Scenarios | XAML | Attributes set to `"..."` | Raw tokens |
| --- | --- | --- | --- | --- |
| gallery | 330 | 310 | 0 (was 140) | 0 |
| toolkit | 48 | 48 | 0 | 0 |
| reactor | 95 | 0 | 0 | 0 |

No content was lost making this safe: gallery XAML went **up**, 302 →
310, because fragments that previously failed the sanitizer's
well-formedness check now survive.

### Schema

The contract gains the three `gallery` fields the WinUI Gallery exporter
actually emits, which were undocumented:

- `xamlPlaceholdersDropped` — tokens already removed; the XAML pastes as
published
- `xamlOmittedAsMalformed` — XAML withheld because it did not parse (12
samples upstream)
- `codePlaceholdersPresent` — tokens **still present** in `code`; it
does not compile as published

Read the first and last as opposites, not companions. One records
cleanup already done, the other warns that cleanup was not possible. A
consumer conflating them pastes broken C#, which is what the field
exists to prevent — the description says so explicitly.

C# gets no deletion equivalent, and that is a property of the language
rather than unfinished work: no C# construct yields a default by being
absent. The tokens that actually occur are identifier fragments
(`BadgeNotificationGlyph.$(SelectedGlyph)`), fixed-arity arguments
(`SetBorderAndTitleBar($(HasBorder), $(HasTitleBar))`), and whole
statements. None can be cut out without a syntax error or removing the
sample's subject.

### Testing

Full suite: **5437 tests, 12 failures**, all pre-existing and
environmental — 11 NuGet live-source tests that need `api.nuget.org`,
and 1 crash-dump test. Same 12 fail on a clean checkout.

Directly affected classes re-run green after the schema change:
`SampleIndexTests` 25/25 (includes the schema/reader drift guard),
`GalleryFetcherCleanContentTests` 7/7, `FindUiSearchTests` 27/27,
`ScenarioSanitizerTests` 23/23, `EmbeddedSnapshotTests` 19/19.

New tests cover each token position, the attribute-drop behaviour, and a
guard that the `IsOpen`/`Severity` rewrites still win over attribute
dropping — the regression the reordering could have caused.

### Related

Related to microsoft/WinUI-Gallery#2232, which publishes the sample
index and guarantees its XAML is placeholder-free. The two are
**technically independent and can merge in either order** — the
Gallery's index validates against this repo's schema both before and
after this PR, because the contract sets `additionalProperties: true` at
every level. Confirmed with ajv (draft 2020-12) against both the current
`main` schema and the one here: valid in both cases.

What this PR adds is description rather than permission. On `main`,
`sample.gallery` declares no properties at all, so the three fields that
carry the pasteability contract are tolerated but unexplained. Landing
this first simply means `codePlaceholdersPresent` is described by the
published schema as soon as the Gallery starts emitting it, rather than
shortly after.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The exporter serialized through reflection while every other data path in the
repository uses a JsonSerializerContext - WinUIGallery/Models/ControlInfoData.cs
and WinUIGallery.SourceGenerator/ControlInfoData.cs both define one for this same
input file. Being the only reflection-based reader of ControlInfoData.json is a
difference with no reason behind it, so this removes it.

CatalogReadContext covers the input, CatalogWriteContext the output, and both are
attached as TypeInfoResolver rather than carrying JsonSourceGenerationOptions of
their own. Behaviour stays on ReadOptions and WriteOptions so there is one
definition of it: WriteOptions in particular decides the published field names,
and ContractConformanceTests derives the names it expects from that object, so
keeping it authoritative is what lets the test verify the writer instead of a
second copy of the writer's configuration.

JsonSerializerIsReflectionEnabledByDefault is set to false in both projects, which
turns the contexts into something enforced rather than conventional: a type the
serializer reaches that no context covers now throws instead of silently falling
back. Verified by removing the write resolver, which fails with
ThrowInvalidOperationException_JsonSerializerIsReflectionDisabled, and by the
feature switch appearing in the generated runtimeconfig.json.

The published index is byte-identical, which the staleness test asserts directly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The exporter is comprehensively validated, prior findings are resolved, and no remaining correctness issues were identified.

Review details
  • Files reviewed: 40/41 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@niels9001

Copy link
Copy Markdown
Collaborator

/azp run

@niels9001
Niels Laute (niels9001) merged commit abb8cb4 into microsoft:main Sep 16, 2026
2 checks passed
Nikola Metulev (nmetulev) added a commit to microsoft/winappCli that referenced this pull request Sep 22, 2026
## Description

Replaces the WinUI Gallery scraper with the machine-readable sample
index now published by microsoft/WinUI-Gallery#2232 at
`catalog/windows-samples.json`.

The old path reconstructed the corpus by downloading Gallery metadata
and hundreds of individual source files. This change consumes the
upstream index in one request and deletes `GalleryFetcher.cs` entirely.
A shared `SampleIndexFetcher` now handles the Gallery and Reactor
indexes.

The published index contains Gallery source verbatim, so
`GalleryProvider` preserves the pasteability normalization users relied
on: Gallery-private page types and namespaces become app-local
placeholders, calls to Gallery's internal `UIHelper` are removed, and
XAML event attributes are retained only when the emitted C# defines the
handler. Samples remain complete rather than being truncated; `find-ui`
retrieves one selected sample at a time, and measurement showed
recovering a truncated sample costs more tokens than serving it whole.

Gallery `xmlnsImports` are now rendered in the `**Setup:**` line instead
of being discarded by the Toolkit-only formatter gate.

## Usage Example

```powershell
winapp find-ui "animated icon"
winapp find-ui --id gallery-animatedicon-2
```

The selected sample now includes the namespace declaration published by
Gallery:

```text
**Setup:** `xmlns:animatedvisuals="using:Microsoft.UI.Xaml.Controls.AnimatedVisuals"`
```

A cold corpus refresh fetches the Gallery index once rather than making
up to 433 requests.

## Related Issue

Closes: #809

Upstream index: microsoft/WinUI-Gallery#2232

Depends on the index-consumption foundation from #852.

## Type of Change

- ♻️ Refactor
- ⚡ Performance
- 🐛 Bug fix

## Checklist

- [x] New tests added for the published-index corpus contract and
pasteability guards
- [x] Tested against the live WinUI Gallery index
- [x] Tested locally on Windows
- [x] Full build and packaging completed
- [x] No command syntax or workflow documentation changes required

## Additional Notes

**Before → after:**

| Metric | Scraper | Published index |
|---|---:|---:|
| Gallery HTTP requests on a cold refresh | up to 433 | 1 |
| Scenarios | 330 | 327 |
| Controls | 114 | 115 |
| XAML samples | 295 | 290 |
| C# samples | 107 | 116 |
| Scenarios with no usable code | 5 | 0 |

The three removed scenarios are upstream omissions rather than parser
loss. Search enrichment preserves exact tag parity across all 127
controls represented by the shared Gallery tag data.

**Validation:**

- `scripts/build-cli.ps1 -SkipTests` completed successfully, including
NativeAOT, npm, NuGet, and MSIX packaging.
- 179/179 find-ui, Gallery, sample-index, snapshot, formatter, Toolkit,
and Reactor tests passed.
- A fresh live bake produced 327 Gallery, 48 Toolkit, and 95 Reactor
scenarios at cache version 23.
- Runtime checks confirmed unbacked handlers and Gallery-private helpers
are removed, Gallery namespace imports are printed, and Toolkit/Reactor
output remains intact.

The full CLI suite was not used as the local signal because unrelated
network/UI-dependent tests block on this machine; PR CI remains the
complete repository gate.

## AI Description

<!-- ai-description-start -->
_This section is auto-generated by AI when the PR is opened or updated.
To opt out, delete this entire section including the marker comments._
<!-- ai-description-end -->

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Nikola Metulev <nmetulev@users.noreply.github.com>
Copilot-Session: 034ffdc8-3308-4286-96d8-e02406110630
Copilot-Session: 883032eb-8c56-4340-b3d7-4c38bb1916b8
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.

3 participants