Skip to content

[XAML] Make OnPlatform/OnIdiom markup extensions data-driven for custom backends#35901

Open
kubaflo wants to merge 3 commits into
dotnet:net11.0from
kubaflo:fix/35695-onplatform-onidiom-data-driven
Open

[XAML] Make OnPlatform/OnIdiom markup extensions data-driven for custom backends#35901
kubaflo wants to merge 3 commits into
dotnet:net11.0from
kubaflo:fix/35695-onplatform-onidiom-data-driven

Conversation

@kubaflo

@kubaflo kubaflo commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Note

Are you waiting for the changes in this PR to be merged?
It would be very helpful if you could test the resulting artifacts from this PR and let us know in a comment if this change resolves your issue. Thank you!

Description of Change

Fixes #35695. Part of #34099.

The {OnPlatform ...} and {OnIdiom ...} XAML markup extensions matched values with a hardcoded if-chain over DevicePlatform/DeviceIdiom, and the GTK, macOS and WPF properties on OnPlatformExtension were internal. As a result, custom/community backends (e.g. GTK/Linux) couldn't express platform-conditional values in the markup-extension form the way first-party platforms can — even though the element form (OnPlatform<T>/On) already resolves values by platform string.

This PR generalizes the markup extensions to resolve values by platform/idiom string, mirroring the element form:

  • String-based matching — values are resolved by comparing DeviceInfo.Platform.ToString() / DeviceInfo.Idiom.ToString() against the per-property keys using StringComparison.Ordinal (exact, case-sensitive). This matches DevicePlatform/DeviceIdiom equality and the compile-time SimplifyOnPlatformVisitor, so runtime and compiled XAML resolve identically. The match is a zero-allocation if-chain (no per-call dictionary/lookup allocation); the named properties are unchanged.
  • GTK, macOS and WPF are now public on OnPlatformExtension, so a backend whose DeviceInfo.Platform.ToString() is GTK, macOS or WPF can resolve those values from {OnPlatform ...}.

Scope note: this lets the markup-extension form resolve the predefined platform keys (now including the newly-public GTK/macOS/WPF). It does not add arbitrary user-defined platform/idiom keys to the markup-extension form — a backend reporting a string with no matching property (e.g. Web, Linux) still falls back to Default. Use the element form for fully arbitrary platform strings.

Backwards-compatibility is preserved:

  • UWP/WinUI precedence is unchanged: an explicit WinUI value wins, and UWP is used as the fallback for the WinUI platform when no WinUI value is provided. The legacy "UWP" platform string still resolves the UWP value.
  • Unknown platforms/idioms continue to fall back to Default.

For first-party platform builds (android/ios/macos/maccatalyst), SimplifyOnPlatformVisitor still resolves the value at compile time, so the new runtime matching path only runs for other/custom platforms — keeping the change low-risk.

Why the markup extension and not just the element form?

The element form already resolves by platform string, but the markup-extension form (which most XAML uses) hardcoded a closed set and kept GTK/macOS/WPF internal. This change closes that gap (issue options 1 + 3) without changing any first-party named-property behavior.

Issues Fixed

Fixes #35695

API Changes

Microsoft.Maui.Controls.Xaml.OnPlatformExtension:

public object GTK { get; set; }      // was internal
public object macOS { get; set; }    // was internal
public object WPF { get; set; }       // was internal

Tests

Added unit tests in MarkupExpressionParserTests and OnPlatformTests covering:

  • GTK, macOS and WPF resolving through {OnPlatform ...} (newly-public properties).
  • A custom backend platform (GTK) resolving its value, and an unknown platform (Web) falling back to Default, via the markup extension (MarkupExtensionResolvesCustomPlatform, including an end-to-end LoadFromXaml case).
  • Exact, case-sensitive (Ordinal) matching: a non-matching-case string (e.g. android) falls back to Default rather than matching Android.
  • Existing first-party platform and UWP/WinUI precedence cases still pass.

All OnPlatform/OnIdiom unit tests pass locally; no regressions in the wider XAML unit-test suite.

The {OnPlatform} and {OnIdiom} XAML markup extensions resolved values via a
hardcoded if-chain over DevicePlatform/DeviceIdiom, and the GTK, macOS and WPF
properties on OnPlatformExtension were internal. As a result, custom backends
(e.g. GTK/Linux) could not express platform-conditional values the way
first-party platforms can, even though the element form (OnPlatform<T>/On) is
already data-driven and supports arbitrary platform strings.

- Resolve values by comparing DeviceInfo.Platform.ToString() /
  DeviceInfo.Idiom.ToString() (case-insensitive) against a lookup built from the
  named properties, mirroring the data-driven element form. UWP/WinUI
  precedence is preserved.
- Make GTK, macOS and WPF public on OnPlatformExtension.

Fixes dotnet#35695

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

github-actions Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 35901

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 35901"

@github-actions github-actions Bot added the area-xaml XAML, CSS, Triggers, Behaviors label Jun 12, 2026
@kubaflo

kubaflo commented Jun 12, 2026

Copy link
Copy Markdown
Contributor Author

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 3 pipeline(s).

@kubaflo

kubaflo commented Jun 12, 2026

Copy link
Copy Markdown
Contributor Author

🤖 Multimodal code review summary

I ran an independent multi-model review of this PR with three top models — Claude Opus 4.8, GPT‑5.5, and Gemini 3.1 Pro. Consolidated findings:

✅ Verified correct (unanimous)

  • UWP/WinUI precedence preserved exactly. Opus exhaustively simulated the old if‑chain vs. the new data‑driven lookup across all 2⁹ property combinations × all platform strings (11,264 cases) and found 0 behavioral differences for real (exact‑case) platform strings — including the regression‑critical "both UWP and WinUI set, platform = WinUI ⇒ WinUI" case (covered by a test).
  • OnIdiom null sentinel is equivalent to the old Idiom ?? Default.
  • Thread‑safety, public API entries (~‑prefixed, present in all 7 TFM folders, correct GTK/macOS/WPF casing), and test coverage all clean.

🔧 Finding to address — case sensitivity (GPT‑5.5: Major, Opus: Minor)

The old chain compared via DevicePlatform/DeviceIdiom equality, which is Ordinal (case‑sensitive) (DevicePlatform.shared.cs). The new lookup used StringComparer.OrdinalIgnoreCase — the only behavioral delta (Opus: 3,328 differing cases, all attributable to casing). All three models also flagged that the code/test comments justifying this as "case‑insensitive, like the element form" are inaccurate: the element form (OnPlatform<T>IList<string>.Contains) and the compile‑time SimplifyOnPlatformVisitor (split.Trim() == target) are both case‑sensitive. Making only the runtime markup‑extension path case‑insensitive introduces a runtime/compile‑time inconsistency.

➡️ Fix: switch both lookups to StringComparer.Ordinal for exact behavioral parity and consistency with the element form + compile‑time path, and correct the comments/tests.

📝 Considered — per‑call dictionary allocation (Gemini: Critical; GPT‑5.5 & Opus: acceptable)

Gemini flagged the Dictionary allocated on each ProvideValue. GPT‑5.5 and Opus both assessed it as negligible: ProvideValue runs once per XAML inflation (not per frame), only for Windows/Tizen/runtime‑loaded XAML — android/ios/macos/maccatalyst compiled XAML is resolved at compile time by SimplifyOnPlatformVisitor and never calls it — and a ~9‑entry dictionary is dwarfed by the reflection/ConvertTo already in the method. #35695 also explicitly requests a "data‑driven lookup … the same dictionary", so I'm keeping the dictionary.

Applying the case‑sensitivity fix now and re‑reviewing afterward.

Multimodal review (Claude Opus 4.8, GPT-5.5, Gemini 3.1 Pro) found the new
lookups used StringComparer.OrdinalIgnoreCase, which was the only behavioral
delta versus the old DevicePlatform/DeviceIdiom comparison (Ordinal) and was
inconsistent with the case-sensitive element form (OnPlatform<T>/On) and the
compile-time SimplifyOnPlatformVisitor.

Switch both lookups to StringComparer.Ordinal for exact behavioral parity and
runtime/compile-time consistency, and correct the related comments and tests.

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

@kubaflo kubaflo left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🤖 Round-2 multimodal review — all clear

Re-reviewed the case-sensitivity fix with Claude Opus 4.8 and GPT-5.5:

  • Case-sensitivity finding resolved. Both confirm the switch to StringComparer.Ordinal restores exact parity with the old DevicePlatform/DeviceIdiom (Ordinal) comparison and is consistent with the element form + compile-time SimplifyOnPlatformVisitor.
  • Exhaustive parity (Opus): old if-chain vs. new lookup across 15,360 platform + 640 idiom combinations → 0 mismatches.
  • ✅ Tests updated correctly (case-variant strings → Default); comments accurate; the 7 PublicAPI.Unshipped.txt entries correct; no new issues.
  • 📝 Dictionary allocation (Gemini round-1 Critical): both round-2 reviewers confirm it's acceptable — ProvideValue runs once per inflation and is dominated by the surrounding reflection/ConvertTo, and #35695 explicitly asks for a data-driven dictionary. Kept intentionally.

Blocking issues: none. Local tests: all 145 OnPlatform/OnIdiom cases pass; no regressions (the 2 unrelated failures in the wider suite are pre-existing culture-dependent decimal-binding tests).

Inline notes on the key lines below. 👇

// Keyed by platform string using Ordinal (case-sensitive) comparison, matching
// DevicePlatform equality, the element form (OnPlatform<T>/On) and the compile-time
// SimplifyOnPlatformVisitor, so runtime and compiled XAML resolve identically.
var lookup = new Dictionary<string, object>(StringComparer.Ordinal);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Round-2 fix (multimodal review). Switched from OrdinalIgnoreCase to Ordinal after GPT-5.5 (Major) and Opus (Minor) flagged that case-insensitive matching was the only behavioral delta vs. the old DevicePlatform comparison and was inconsistent with the case-sensitive element form (OnPlatform<T>) and compile-time SimplifyOnPlatformVisitor.

Opus exhaustively simulated the old if-chain vs. this lookup across 15,360 platform cases → 0 mismatches. The dictionary is kept intentionally: #35695 asks for a data-driven lookup, and the per-inflation allocation is negligible next to the reflection/ConvertTo already in ProvideValue.

// "UWP" still matches a custom backend reporting the legacy "UWP" platform string.
lookup["UWP"] = UWP;

// UWP is a backwards-compatible alias for WinUI: only fall back to it for the

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Reviewers verified this preserves the original precedence exactly: explicit WinUI wins (added first; this UWP block only fills the WinUI key via !ContainsKey), UWP is the fallback for the WinUI platform when WinUI is unset, and the legacy "UWP" platform string still resolves UWP. Covered by the WinUI=25, UWP=20 → 25 test case.

{
// Keyed by idiom string using Ordinal (case-sensitive) comparison, matching
// DeviceIdiom equality and the data-driven element form.
var lookup = new Dictionary<string, object>(StringComparer.Ordinal);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Mirror of the OnPlatform change — Ordinal for case-sensitive parity with DeviceIdiom equality and the data-driven element form. AddIfSet skips null, preserving the old Idiom ?? Default fallback (verified across all idiom combinations).

@kubaflo

kubaflo commented Jun 13, 2026

Copy link
Copy Markdown
Contributor Author

Automated fleet code review — PR #35901

Reviewed head SHA: e328453687977d27102d1eb6b60941eef0396c93

Verdict: NEEDS_DISCUSSION
Confidence: medium

Independent assessment

The code replaces hardcoded DevicePlatform/DeviceIdiom if chains in {OnPlatform} / {OnIdiom} markup extensions with string-keyed lookups using StringComparer.Ordinal, makes the existing GTK, macOS, and WPF OnPlatformExtension properties public, and preserves UWP as a WinUI fallback only when WinUI is not explicitly set. This matches the existing exact-case DevicePlatform/DeviceIdiom equality semantics and the compile-time SimplifyOnPlatformVisitor behavior.

Findings

❌ Errors

None found in the changed code.

⚠️ Warnings

  • The PR description is stale after the case-sensitivity fix: it still says matching is "case-insensitive" and says tests cover case-insensitive matching. The implementation and tests now intentionally use exact Ordinal matching, which looks correct, but the narrative should be updated before merge.
  • Required CI is red. I do not see a direct correlation between the failures and these XAML markup-extension changes, but Build Analysis reports them as unmatched, so this cannot be treated as green/known-infra from the available evidence.

💡 Suggestions

  • Consider tightening the OnIdiomExtension.GetValue() comment. The lookup is data-driven internally, but {OnIdiom ...} still only exposes the fixed built-in property names (Phone, Tablet, Desktop, TV, Watch); a truly custom idiom name still cannot be expressed in the markup-extension form and falls back to Default.

CI note

gh pr checks shows required maui-pr and Build Analysis failing for build 1462356. Failed areas include RunOniOS_MauiRelease*, AOT macOS, and Build windows; logs show integration/template build failures such as AppleTemplateTests.RunOniOS_MauiReleaseTrimFull*, AOTTemplateTest.PublishNativeAOTRootAllMauiAssemblies, ResizetizerTests.CollectsAssets, plus a Provision JDK failure. Unit-test and pack/build legs for the touched XAML code passed, but the required pipeline remains red.

Devil's advocate

The main behavior-risk I checked was parity with the old branch order and sentinel semantics: UWP/WinUI precedence is preserved, explicit Default handling still distinguishes unset from set-to-null for OnPlatformExtension, and OnIdiomExtension preserves the prior null-as-unset behavior. The new dictionary allocation is on XAML inflation, not a hot layout/render path.

This is an automated non-approval review and not a substitute for human review.

A third multimodal review round (Claude Opus 4.8, GPT-5.5, Gemini 3.1 Pro)
noted the per-call Dictionary built in ProvideValue: it allocates on the runtime
resolution path (Windows/Tizen, and any runtime-loaded XAML, since
SimplifyOnPlatformVisitor only compile-simplifies android/ios/macos/maccatalyst),
including the DataTemplate inflation path. It also added no extensibility over a
plain comparison, because the markup-extension parser only accepts properties
declared on the type.

Replace the Dictionary with a zero-allocation, string-keyed if-chain over the
cached DeviceInfo.Platform.ToString() / DeviceInfo.Idiom.ToString() value. This
keeps the data-driven string matching (Ordinal, matching DevicePlatform/
DeviceIdiom equality and the element form), preserves exact behavior including
UWP/WinUI precedence, and removes the now-unused System.Collections.Generic
import. All 145 OnPlatform/OnIdiom unit tests still pass.

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

@kubaflo kubaflo left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🤖 Round-3 multimodal review + CI triage

Ran a third independent review with Claude Opus 4.8, GPT-5.5, and Gemini 3.1 Pro, plus a full CI investigation.

CI failures — NOT caused by this PR ✅

The 7 red checks are pre-existing net11.0 issues, not this change:

  • AOT macOS + RunOniOS_MauiRelease / TrimFull / TrimFull_CoreCLR fail in BuildWarningsUtilities.AssertWarnings — the AOT/trim expected-warnings baseline drifted because the template now emits HybridWebViewHandler IL2026/IL3050 (System.Text.Json) warnings unrelated to OnPlatform/OnIdiom. The identical failures appear on unrelated net11.0 PRs (#35892, #35891, #35870).
  • Build windows was canceled, not failed (known issue dnceng#3008).
  • All Build, Pack, and Helix Unit Test jobs (which run the XAML unit tests exercising this change, under trim/AOT analyzers + TreatWarningsAsErrors) passed.

All three models independently concluded the change cannot explain the AOT/Trim/Windows failures (ref-type generic instantiation is AOT-safe, static local function lowers to a private static method, additive public API).

Finding addressed — per-call Dictionary allocation

Gemini reaffirmed Critical; Opus rated it Minor but agreed "a zero-alloc string-keyed if-chain is a reasonable micro-opt"; GPT-5.5 had no objection. I verified Gemini's two supporting claims:

  1. SimplifyOnPlatformVisitor only compile-simplifies android/ios/macos/maccatalyst → {OnPlatform}/{OnIdiom} do run at runtime on Windows/Tizen (incl. the DataTemplate inflation path).
  2. MarkupExtensionParser only accepts properties declared on the type → the dictionary added no extensibility over a direct comparison.

➡️ Fix applied (commit 7a59777): replaced the per-call Dictionary with a zero-allocation, string-keyed if-chain over the cached DeviceInfo.Platform.ToString() / DeviceInfo.Idiom.ToString(). Keeps the data-driven Ordinal matching and exact UWP/WinUI precedence; removed the now-unused using. Source builds with 0 warnings under warnings-as-errors; all 145 OnPlatform/OnIdiom tests still pass.

Everything else — clean

Correctness, UWP/WinUI precedence, OnIdiom null sentinel, nullability/NRT, trimming/AOT, SourceGen↔runtime consistency, public API (7 TFMs), and edge cases (Unknown/empty/tvOS/watchOS → Default, Converter path) all verified across the three models. No remaining blocking issues.

// a value as long as DeviceInfo.Platform.ToString() matches one of the keys below.
// Ordinal (case-sensitive), matching DevicePlatform equality and the compile-time
// SimplifyOnPlatformVisitor, so runtime and compiled XAML resolve identically.
var platform = DeviceInfo.Platform.ToString();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Round-3 fix. Replaced the per-call Dictionary<string,object> with this zero-allocation, string-keyed if-chain over the cached DeviceInfo.Platform.ToString().

Rationale (verified): SimplifyOnPlatformVisitor only compile-simplifies android/ios/macos/maccatalyst, so this path runs at runtime on Windows/Tizen including DataTemplate inflation; and MarkupExtensionParser only accepts declared properties, so the dictionary added no extensibility over direct comparison — it was pure allocation overhead.

Behavior is identical: Ordinal (matches DevicePlatform equality + the element form), and UWP/WinUI precedence preserved (explicit WinUI wins; UWP falls back for the WinUI platform only when WinUI is unset; legacy "UWP" string still matches). Locked in by the existing 145 tests.

@kubaflo

kubaflo commented Jun 13, 2026

Copy link
Copy Markdown
Contributor Author

Automated fleet code review — PR #35901

Reviewed head SHA: 7a5977777d72f72f727a2cbb9575c8a78dad9491

Verdict: NEEDS_DISCUSSION
Confidence: high for code correctness; medium for merge readiness because required CI is red with unmatched failures.

Independent assessment

The new commit replaces the prior per-call lookup/dictionary approach with a zero-allocation string-keyed if-chain. The behavior remains exact-case (StringComparison.Ordinal) and preserves first-party platform/idiom behavior, Default fallback semantics, and WinUI/UWP precedence. Making GTK, macOS, and WPF public is reflected in all relevant XAML PublicAPI.Unshipped.txt files, and the added parser/LoadFromXaml tests cover the new publicly-settable platform properties and fallback behavior.

Reconciliation with prior review / PR narrative

Round22's allocation concern appears addressed by the new zero-allocation matching commit. However, the PR description is still stale: it says matching is case-insensitive and that tests cover android matching Android, while the code and tests now intentionally assert exact-case matching and fallback to Default for android/gtk. The narrative also still overstates arbitrary custom platform/idiom support: the markup-extension form can now express the newly-public predefined GTK/macOS/WPF keys, but it still cannot express an arbitrary Web/Linux/custom idiom value except via Default.

Findings by severity

❌ Errors

None found in the changed code.

⚠️ Warnings

  • PR description/test summary must be updated before merge to match the current implementation: matching is case-sensitive/Ordinal, not case-insensitive.
  • Required maui-pr / Build Analysis are currently failing with unmatched failures, so I would not treat this as merge-ready from CI alone.

💡 Suggestions

  • Consider tightening the OnIdiomExtension.GetValue() comment: the refactor is behavior-preserving, but {OnIdiom ...} still exposes only the fixed built-in idiom properties, so a truly custom idiom still falls back to Default.

CI note

gh pr checks 35901 reports required maui-pr and Build Analysis failing for build 1462790. Build Analysis reports no known issue matches and unmatched failures in AOT macOS, RunOniOS_MauiRelease, RunOniOS_MauiReleaseTrimFull, and RunOniOS_MauiReleaseTrimFull_CoreCLR. Evidence includes AOTTemplateTest.PublishNativeAOTRootAllMauiAssemblies failing on an unexpected ILC ... apply-preserve-attribute.xml warning file, plus AppleTemplateTests.RunOniOS_MauiReleaseTrimFull* template project build failures. The touched XAML unit-test/build legs appear green, and I do not see a direct XAML-markup correlation, but the required pipeline is still red/unmatched.

Devil's advocate

I specifically re-checked sentinel behavior (s_notset vs null), OnIdiom prior null-as-unset semantics, exact DevicePlatform/DeviceIdiom.ToString() names, UWP/WinUI fallback ordering, and compile-time SimplifyOnPlatformVisitor parity. The implementation looks sound; the remaining concerns are documentation/narrative accuracy and unresolved CI status.

This is an automated non-approval review and not a substitute for human review.

@kubaflo

kubaflo commented Jun 14, 2026

Copy link
Copy Markdown
Contributor Author

✅ Final multimodal merge-readiness review — unanimous MERGEABLE

Ran the full fix → review loop with four top models on the current head (7a5977777d):

Model Verdict
Claude Opus 4.8 MERGEABLE
Claude Opus 4.6 MERGEABLE
Gemini 3.1 Pro MERGEABLE
GPT‑5.5 MERGEABLE

What all four models independently confirmed:

  • Behavioral equivalence is exhaustive — Opus 4.8 simulated 13,312 {OnPlatform} combinations (13 platform strings × all 2¹⁰ set/unset states) and 576 {OnIdiom} combinations against the old hardcoded if‑chain → 0 mismatches.
  • UWP/WinUI precedence preserved exactlyWinUI wins over UWP; UWP still matches both the WinUI platform and a legacy "UWP" string. (Including the trap that DevicePlatform.UWP's internal string is actually "WinUI".)
  • Ordinal is the correct comparison — it matches DevicePlatform/DeviceIdiom.Equals (which use StringComparison.Ordinal), the runtime element form (List<string>.Contains on DeviceInfo.Platform.ToString()), and the compile‑time SimplifyOnPlatformVisitor (case‑sensitive ==). Case‑variant inputs correctly fall through to Default — covered by new "android"/"gtk"/"phone" tests.
  • OnIdiom null‑sentinel maps exactly to the old ?? Default coalescing; matching is allocation‑free (ToString() returns the backing string field; the Dictionary from an earlier round was replaced by a zero‑alloc if‑chain).
  • Public GTK/macOS/WPF is justified, not speculative — these are functional value properties wired into TryGetValueForPlatform, sitting alongside already‑public Android/iOS/MacCatalyst/WinUI/UWP, enabling community backends via {OnPlatform GTK=…}; new tests demonstrate real resolution. PublicAPI.Unshipped.txt is correct and identical across all 7 TFMs.

CI note: red AOT‑macOS / RunOniOS legs are the pre‑existing net11 HybridWebViewHandler baseline drift shared by sibling PRs; Build / Pack / Helix Unit Tests are green, and 145 OnPlatform/OnIdiom unit tests pass locally.

@kubaflo

kubaflo commented Jun 16, 2026

Copy link
Copy Markdown
Contributor Author

@StephaneDelcroix — design question before we take this further. cc @PureWeen

Context. This PR makes the {OnPlatform} / {OnIdiom} markup extensions resolve their value by string-comparing DeviceInfo.Platform.ToString() against the per-platform keys (Ordinal, matching DevicePlatform equality and SimplifyOnPlatformVisitor), and it makes the existing GTK / macOS / WPF properties public so more first-party-ish backends can be expressed.

The limitation I want your call on. The matching is now data-driven, but the property surface of the markup extension is still a fixed set of CLR properties (Android, iOS, macOS, MacCatalyst, Tizen, WinUI, GTK, WPF, UWP). For a brand-new backend — say a Web head that registers an IDeviceInfo/DevicePlatform returning "Web" — there is no Web property to set, so {OnPlatform Web=...} can't be authored. The PR's own test confirms this: an unknown "Web" platform falls back to Default.

The element form already supports arbitrary platforms todayOn.Platform is an IList<string> matched via Contains(DeviceInfo.Platform.ToString()), so a custom head can do:

<Label.Text>
  <OnPlatform x:TypeArguments="x:String">
    <On Platform="Web" Value="..." />
  </OnPlatform>
</Label.Text>

So the gap is specifically the attribute markup-extension syntax {OnPlatform Web=...}, where, as far as I can tell, the XAML parser maps attributes to CLR properties and there's no hook to accept arbitrary keys.

Questions:

  1. Is opening up the predefined property surface (this PR) the intended ceiling for the markup-extension form, with the element form as the documented escape hatch for truly-custom platforms/idioms?
  2. Or do we want the markup extension to support arbitrary keys — e.g. a dictionary-style member or a parser hook so {OnPlatform Web=...; iOS=...} works for any registered DevicePlatform? If so, is there an existing XAML mechanism you'd point me at, or does this need new parser support?

Same question applies to {OnIdiom} for custom DeviceIdiom.Create(...) values. Happy to implement whichever direction you prefer — just want the design decision before building a speculative API.

@kubaflo kubaflo requested a review from StephaneDelcroix June 16, 2026 13:28
@kubaflo

kubaflo commented Jun 16, 2026

Copy link
Copy Markdown
Contributor Author

@StephaneDelcroix — concrete follow-up to the dynamic-key question, now traced through all three code paths so we can pick a direction.

Goal: let a custom backend (e.g. a future Web that registers an IDeviceInfo/DevicePlatform returning "Web") author {OnPlatform Web=..., Default=...} — not just the hardcoded Android/iOS/WinUI/… property surface on OnPlatformExtension/OnIdiomExtension.

Where the hardcoded surface bites:

  1. Runtime (LoadFromXaml)MarkupExtensionParser.SetPropertyValue resolves attributes via GetRuntimeProperty(prop).SetMethod; an unknown Web returns null → NRE.
  2. Compiled, android/ios/macos/maccatalystSimplifyOnPlatformVisitor matches the target by string in node.Properties and replaces the node before any CLR-property check, so arbitrary keys would already survive here.
  3. Compiled, Windows/Tizen/WebTarget is null, no simplification, and SetPropertiesVisitor.SetPropertyValue throws BuildException(MemberResolution) (SetPropertiesVisitor.cs:1281) on the unknown Web member.

So the attribute form is blocked on paths (1) and (3); the element form <On Platform="Web" Value=.../> already works everywhere (On.Platform is IList<string>).

Proposed design (opt-in, scoped to these two extensions):

interface IAcceptArbitraryKeys { bool TrySetKey(string key, object value, IServiceProvider sp); }

OnPlatformExtension/OnIdiomExtension back it with a Dictionary<string,object>; ProvideValue looks up DeviceInfo.Platform.ToString() (resp. idiom) there, merged with the existing typed props for back-compat. Integration points:

  • Runtime: in MarkupExtensionParser.SetPropertyValue, when GetRuntimeProperty(prop) == null and the extension implements the interface, route into TrySetKey instead of NRE-ing.
  • XamlC: at SetPropertiesVisitor.cs:1281, before throwing MemberResolution, if the parent type implements the interface emit a call to TrySetKey(key, value). This IL emit is the only genuinely new/risky piece.
  • SimplifyOnPlatformVisitor: unchanged (already string-keyed; only simplifies the 4 known TFMs).

Questions for you:

  1. Are you comfortable with the XamlC SetPropertiesVisitor IL-emit hook, or would you rather keep the compiler strict and steer custom backends to the element form as the documented path?
  2. If we add the dict, do you want unknown keys allowed on any markup extension that opts in, or hard-restricted to OnPlatform/OnIdiom?
  3. Case sensitivity / collision rules when a key matches both a typed prop and a dict entry — typed prop wins?

Happy to implement whichever direction you prefer on fix/35695-onplatform-onidiom-data-driven.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-xaml XAML, CSS, Triggers, Behaviors

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants