mirror of
https://github.com/dotnet/skills.git
synced 2026-09-20 09:49:54 +08:00
maui-theming: never Clear() MergedDictionaries when swapping a theme
The gate had maui-theming at 3W/1T/0L, +30% [-1.8%, +61.8%] - one tie away
from passing, on "Avoid common theming mistakes". Comparing the trajectories,
baseline and skilled were near-identical and both satisfied all four rubric
items, so there was nothing to win on... except that BOTH emitted the same
real bug:
var merged = Application.Current.Resources.MergedDictionaries;
merged.Clear();
merged.Add(new DarkTheme());
The default MAUI template merges Resources/Styles/Colors.xaml and Styles.xaml
into Application.Resources. Clear() removes those too, so every implicit style,
brush and colour in the app silently disappears.
Verified at runtime in a net11.0-maccatalyst app built from the default
template:
before Clear(): merged=2 'Primary' resolves=True value=[Color Red=0.317...]
after Clear(): merged=1 'Primary' resolves=False value=<GONE>
The skill previously had a weak comment acknowledging this ("assumes theme
dictionaries are the only merged dictionaries... move them to
Application.Resources directly instead"), which is both easy to miss and poor
advice. Replaced across SKILL.md (both occurrences) and theming-api.md with a
tracked-reference swap that removes only the previous theme:
static ResourceDictionary? _currentTheme;
if (_currentTheme is not null) merged.Remove(_currentTheme);
merged.Add(theme);
_currentTheme = theme;
Also verified at runtime: app styles survive across two consecutive theme
swaps and the dictionary count stays flat (2 -> 3 -> 3, 'Primary' OK
throughout), so it neither leaks nor unstyles the app.
Measured (n=4/scenario, 16 trials, executor claude-opus-4.6, judge gpt-5.5):
before 14W/1T/1L +0.33 [+0.21, +0.44] 88% win
after 15W/1T/0L +0.38 [+0.32, +0.43] 94% win
"Avoid common theming mistakes" - the scenario that tied on the gate - now
wins 4/4, and the remaining single tie is on a different scenario. Tightest
confidence interval of any arm measured in this PR.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 58513e0c-4086-4112-9ea1-95dbd8e8e469
This commit is contained in:
@@ -205,21 +205,41 @@ Use `DynamicResource` so values update when the dictionary is swapped at runtime
|
||||
|
||||
### Step 3 — Switch Themes at Runtime
|
||||
|
||||
> 🚨 **Never call `MergedDictionaries.Clear()` to swap a theme.** The default MAUI
|
||||
> template merges `Resources/Styles/Colors.xaml` and `Styles.xaml` into
|
||||
> `Application.Resources`. `Clear()` removes **those too**, so every implicit style,
|
||||
> brush and colour in the app silently disappears — buttons, entries and labels all
|
||||
> revert to unstyled defaults. Verified: after `Clear()`, `MergedDictionaries` drops
|
||||
> from 2 to 1 and the template's `Primary` colour no longer resolves.
|
||||
|
||||
Remove only the theme you added, and leave everything else alone:
|
||||
|
||||
```csharp
|
||||
static ResourceDictionary? _currentTheme;
|
||||
|
||||
void ApplyTheme(ResourceDictionary theme)
|
||||
{
|
||||
// Assumes theme dictionaries are the only merged dictionaries.
|
||||
// If your App.xaml merges non-theme dictionaries (e.g., converters),
|
||||
// move them to Application.Resources directly instead.
|
||||
var mergedDictionaries = Application.Current!.Resources.MergedDictionaries;
|
||||
mergedDictionaries.Clear();
|
||||
mergedDictionaries.Add(theme);
|
||||
var merged = Application.Current!.Resources.MergedDictionaries;
|
||||
|
||||
// ✅ Remove ONLY the previous theme — Colors.xaml / Styles.xaml survive
|
||||
if (_currentTheme is not null)
|
||||
merged.Remove(_currentTheme);
|
||||
|
||||
merged.Add(theme);
|
||||
_currentTheme = theme;
|
||||
}
|
||||
|
||||
// Usage
|
||||
ApplyTheme(new DarkTheme());
|
||||
```
|
||||
|
||||
```csharp
|
||||
// ❌ Destroys the app's Colors.xaml and Styles.xaml along with the old theme
|
||||
var merged = Application.Current!.Resources.MergedDictionaries;
|
||||
merged.Clear();
|
||||
merged.Add(theme);
|
||||
```
|
||||
|
||||
## System Theme Detection
|
||||
|
||||
### Read the Current Theme
|
||||
@@ -327,11 +347,19 @@ diagnose this, always show the swap and the system-theme hook alongside the fix
|
||||
otherwise the user has a corrected binding that still never updates:
|
||||
|
||||
```csharp
|
||||
static ResourceDictionary? _currentTheme;
|
||||
|
||||
void ApplyTheme(bool useDark)
|
||||
{
|
||||
var merged = Application.Current!.Resources.MergedDictionaries;
|
||||
merged.Clear();
|
||||
merged.Add(useDark ? new DarkTheme() : new LightTheme());
|
||||
|
||||
// Remove only the previous theme — never Clear(), which also wipes
|
||||
// the template's Colors.xaml / Styles.xaml
|
||||
if (_currentTheme is not null)
|
||||
merged.Remove(_currentTheme);
|
||||
|
||||
_currentTheme = useDark ? new DarkTheme() : new LightTheme();
|
||||
merged.Add(_currentTheme);
|
||||
}
|
||||
|
||||
// React to the OS switching light/dark
|
||||
@@ -375,6 +403,6 @@ Every `x:Key` used in one theme dictionary must exist in all other theme diction
|
||||
- **Read OS theme** → `Application.Current.RequestedTheme`
|
||||
- **Force theme** → `Application.Current.UserAppTheme = AppTheme.Dark`
|
||||
- **Theme changes** → `RequestedThemeChanged` event
|
||||
- **Custom switching** → Swap `ResourceDictionary` in `MergedDictionaries`
|
||||
- **Custom switching** → `Remove` the old theme from `MergedDictionaries`, then `Add` the new one — **never `Clear()`**
|
||||
- **Runtime bindings** → **`DynamicResource`** (not `StaticResource`)
|
||||
- **Persist choice** → `Preferences.Set` / `Preferences.Get`
|
||||
|
||||
@@ -79,13 +79,21 @@ Use `DynamicResource` (not `StaticResource`) so values update at runtime when th
|
||||
|
||||
### Switch Themes at Runtime
|
||||
|
||||
Remove only the previous theme. `MergedDictionaries.Clear()` also removes the
|
||||
template's `Colors.xaml` / `Styles.xaml`, silently unstyling the whole app.
|
||||
|
||||
```csharp
|
||||
static ResourceDictionary? _currentTheme;
|
||||
|
||||
void ApplyTheme(ResourceDictionary theme)
|
||||
{
|
||||
// Assumes theme dictionaries are the only merged dictionaries.
|
||||
var mergedDictionaries = Application.Current!.Resources.MergedDictionaries;
|
||||
mergedDictionaries.Clear();
|
||||
mergedDictionaries.Add(theme);
|
||||
var merged = Application.Current!.Resources.MergedDictionaries;
|
||||
|
||||
if (_currentTheme is not null)
|
||||
merged.Remove(_currentTheme);
|
||||
|
||||
merged.Add(theme);
|
||||
_currentTheme = theme;
|
||||
}
|
||||
|
||||
// Usage
|
||||
|
||||
Reference in New Issue
Block a user