Consume a ThemeContribution's Styles as a library selector-style leg#12
Merged
Conversation
…tyle leg A control library could register keyed control themes + resources via ThemeContributions, but its ResourceDictionary.Styles selector channel was explicitly NOT consumed (§11.8) — so a library had no way to ship app-level selector styles, only Type-keyed themes. The StyleEngine now gathers each registered contribution's top-level Styles as a third leg of the Theme layer, ordered: - ABOVE the BuiltIn framework leg (a library refines the framework default), and - BELOW the app.Theme leg and UIApplication.Styles (the app always wins). Each contribution gets a distinct 4096-wide order slot in registration order, so a later-registered library wins a same-target tie (the resource-tier "last wins", applied to styles); the slot is clamped one stride below AppThemeOrderBase so a contribution can never reach the app-theme leg. A contribution that ships only Type-keyed themes (no top-level Styles) is skipped, and the whole leg short-circuits when nothing is contributed — zero cost in the common case. Late registration (rare — contributions normally register at module init) re-arms the engine app-wide via OnThemeStylesInvalidated, alongside the existing resource catch-all. Contributed styles reference the library's own controls with TYPED selectors (Selectors.OfType(typeof(BarButton))), so no string parsing or ISelectorTypeResolver is needed for this path. Only the dictionary's own top-level Styles are consumed; Styles nested in a contribution's MergedDictionaries are not flattened (v1) — same contract as the app.Theme leg. The ancestor-interesting fast path leaves the whole Theme layer out (BuiltIn + app.Theme + contributions alike), unchanged. Docs: reversed the §11.8 note on ThemeContributions and updated ResourceDictionary.Styles. Tests: Section15_ContributedStyles — match+apply, App overrides, app.Theme overrides, later-contribution-wins, no-Styles-skipped, late-registration re-arm (a global-registry save/restore hook keeps them isolated). Mutation-verified. UI 2957 / Bars 296 green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0175wtwXrafD4fWAZ3i5THu6
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
A control library could contribute Type-keyed control themes + resources via
ThemeContributions, but itsResourceDictionary.Stylesselector channel was explicitly not consumed (§11.8). So a library had no way to ship app-level selector styles — only implicit,ControlThemeKey-resolved themes. Adding arbitrary styles to a contribution silently did nothing.Fix
The
StyleEnginenow gathers each registered contribution's top-levelStylesas a third leg of the Theme layer, ordered:BuiltInframework leg (a library refines the framework default), andapp.Themeleg andUIApplication.Styles(the app always wins).Each contribution gets a distinct 4096-wide order slot in registration order, so a later-registered library wins a same-target tie (the resource-tier "last wins", applied to styles); the slot is clamped one stride below
AppThemeOrderBaseso a contribution can never reach the app-theme leg no matter how many register.Styles) is skipped; the whole leg short-circuits when nothing is contributed — zero cost in the common case.OnThemeStylesInvalidated, alongside the existing resource catch-all.Stylesare consumed;Stylesnested in a contribution'sMergedDictionariesare not flattened (v1) — same contract as theapp.Themeleg. The ancestor-interesting fast path leaves the whole Theme layer out (BuiltIn + app.Theme + contributions alike), unchanged.On the type-resolver question
This path needs no
ISelectorTypeResolver: a library contributing styles in code builds typed selectors —Selectors.OfType(typeof(BarButton))/Selectors.Is<Ribbon>()— no string parsing. The resolver plumbing (for string/XAML-authored selectors) stays a separate, deferred concern.Docs / tests
Reversed the §11.8 note on
ThemeContributionsand updatedResourceDictionary.Styles.Section15_ContributedStylescovers match+apply, App overrides, app.Theme overrides, later-contribution-wins, no-Styles-skipped, and late-registration re-arm — with a global-registry save/restore hook for isolation. Mutation-verified (removing the leg fails the match test). UI 2957 / Bars 296 green.🤖 Generated with Claude Code