Index the gallery's sample pages so other tools can consume them - #2232
Niels Laute (niels9001) merged 17 commits into
Conversation
- 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
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
|
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:
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 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>
…jay/sample-catalog-manifest
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>
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>
There was a problem hiding this comment.
🟡 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
codeas C# that can be pasted directly, but this branch deliberately publishes unresolved placeholders. The committed catalog already contains values such asAppNotificationSoundEvent.$(SelectedAppNotificationSoundEvent), and the current winappCli parser has no support forgallery.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
…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>
#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>
There was a problem hiding this comment.
🟢 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
|
/azp run |
## 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
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-uiships 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.xamlpages 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.$schemapoints 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
galleryobject 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 rundotnet run --project tools/CatalogExporter -- generateand 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.txtwould 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 throughx: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.
catalog/windows-samples.jsongenerateand commit the result.catalog/README.mdtools/CatalogExporter/Program.csgeneratewrites the index,checkfails if the committed copy is stale.tools/CatalogExporter/CatalogGenerator.csControlInfoData.jsonand the sample pages, builds the index.tools/CatalogExporter/CatalogModels.cstools/CatalogExporter/SourceModels.csControlInfoData.json.tools/CatalogExporter/SampleBundle.cs.txtsnippet bundles. Deliberately mirrorsControlExample.ParseSampleCodeSections.tools/CatalogExporter/SubstitutionResolver.cs$(Token)placeholders to the value each page's control starts with.tools/CatalogExporter/TokenFallback.cstools/CatalogExporter/XamlFragment.cstests/WinUIGallery.CatalogExporter.Tests/.txtbundles underSamples/AccessibilityKeyboard/andSamples/AccessibilityScreenReader/Six existing files are modified, all narrowly:
WinUIGallery.slnx.pipelines/azure-pipelines.ymlControlInfoData.jsonCatalogblock to two entries (search aliases only), and drops two danglingRelatedControlsreferences fromLayoutPanel.ControlInfoDataSchema.jsonCatalogblock:Exclude,Aliases,RelatedSamples.AccessibilityKeyboardPage.xaml,AccessibilityScreenReaderPage.xamlSampleDefinitionreferences to the new bundles.The
Catalogblock inControlInfoData.jsonis 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-PRValidationpipeline, so it runs on every PR and on pushes tomainalongside the checks already there. No new pipeline, no new schedule, no new secrets, and nothing that publishes anywhere.Verify catalog/windows-samples.json is up to datedotnet 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.jsondoes not parse as strict JSON onmain: there's a dangling comma before a closing brace (line 101). PowerShell'sConvertFrom-Jsontolerates it,JSON.parsedoes 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 optionalCatalogproperty 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:
ajv(draft 2020-12).$(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.SampleDefinitionreference resolves — a dangling one compiles fine and shows an empty code viewer at runtime. The exporter validates the fullFolder\File.txtpath rather than just the file name, matching howControlExampleresolves it at runtime.Notes for reviewers
SampleBundleParserdeliberately mirrorsControlExample.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.local:to mean "your namespace" — was originally omitted fromxmlnsImportsrather 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 ingallery.xamlOmittedUnboundPrefixes.$(Token)placeholdersSnippets 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.xamlPlaceholdersDroppedso 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 guessedValuesilently 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 theircodeas-is.They are now declared rather than resolved.
gallery.codePlaceholdersPresentlists 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 andcatalog/README.mdstates 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 rundotnet run --project tools/CatalogExporter -- generateand commit the result, or the staleness check fails. The failure lands on a PR whose diff may look entirely unrelated tocatalog/, 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.
RelatedControlsgains its first consumer, and its schema description is misleading.ControlInfoDataSchema.jsondocuments the field as "UniqueIds of other controls" — accurate, and obeyed by 330 of the 332 references currently onmain. 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.csor.xamlreader anywhere inWinUIGallery/. 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.
LayoutPanelarrived from #2233 listingPanelandStackLayout, which are WinUI type names rather than gallery pages; both are dropped here, leavingItemsRepeater. Nothing is lost, sincePanelwas already published throughBaseClassesandTags. 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.txtfiles, 157 (~181 KB) are reachable by nothing — noSampleDefinition, noCodeSourceFile, no code-behind path — yetWinUIGallery.csprojglobsSamples\**\*.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 underTemplates\(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.