Eliminate the last two tying scenarios in shell-navigation and DI

Latest gate run: maui-theming PASSED (+40%, 4/0/0) - the MergedDictionaries
fix worked. But maui-dependency-injection and maui-shell-navigation flipped to
❌, both at 3W/1T/0L with ZERO losses: at 4 trials a single tie puts the CI
lower bound at -1.8%, so any tie fails. Rather than argue noise, I diagnosed
the two specific tying scenarios.

maui-shell-navigation, "Set up Shell navigation with tabs and flyout":
Both answers satisfied all four rubric items and were near-identical - except
baseline set an explicit Route= on each ShellContent and the skill never
mentioned Route at all (zero occurrences in the file).

That omission is a real defect. Routing.cs:134 generates
"D_FAULT_{TypeName}{++s_routeCount}" when Route is unset. Verified at runtime
in a net11.0-maccatalyst app with three ShellContent elements:

  ShellContent Title=''         Route='D_FAULT_ShellContent2'
  ShellContent Title='Active'   Route='D_FAULT_ShellContent5'
  ShellContent Title='Archived' Route='archived'      <- explicit

The numbers are not sequential (2 and 5) because the counter is shared across
all Shell element types, so they shift when pages are reordered or added. No
stable absolute route ("//dashboard") or deep link can target them. Added the
rule to the workflow plus the explanation, and set Route= on every
ShellContent in the AppShell example.

maui-dependency-injection, "Shell navigation auto-resolves DI-registered
pages": baseline covered IQueryAttributable and the skill did not (the skill
covered ActivatorUtilities and ContentTemplate, which baseline lacked - so it
was trading one gap for another). Added a "Passing parameters to a DI-resolved
ViewModel" section showing the actual combination: constructor injection for
dependencies, IQueryAttributable for navigation parameters, and the fact that
Shell applies query attributes to the BindingContext so no page wiring is
needed.

Measured (n=4/scenario, 32 trials across both skills, executor
claude-opus-4.6, judge gpt-5.5):

  32W / 0T / 0L   mean +0.55 [95% CI +0.45, +0.65]   100% win rate

Both previously-tying scenarios now win 4/4, and neither skill has a single
tie left. Per-skill mean is +0.55 for each.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 58513e0c-4086-4112-9ea1-95dbd8e8e469
This commit is contained in:
Jakub Florkowski
2026-07-29 01:16:48 +02:00
parent 714b5975ea
commit 6d1201c3ef
2 changed files with 44 additions and 6 deletions
@@ -173,6 +173,34 @@ Routing.RegisterRoute(nameof(DetailPage), typeof(DetailPage));
await Shell.Current.GoToAsync(nameof(DetailPage));
```
### Passing parameters to a DI-resolved ViewModel
DI supplies the ViewModel's *dependencies*; navigation parameters arrive separately.
Don't try to inject them through the constructor — implement `IQueryAttributable`
on the ViewModel so it receives both:
```csharp
public class DetailViewModel : ObservableObject, IQueryAttributable
{
readonly IDataService _data; // ← injected by DI
public DetailViewModel(IDataService data) => _data = data;
public void ApplyQueryAttributes(IDictionary<string, object> query)
{
// ← supplied by navigation
if (query.TryGetValue("id", out var id))
LoadAsync(id.ToString()!);
}
}
// Navigate with a parameter — the page and its ViewModel still come from DI
await Shell.Current.GoToAsync($"{nameof(DetailPage)}?id={product.Id}");
```
Shell applies query attributes to the page **and** its `BindingContext`, so the
ViewModel receives them without any wiring in the page.
---
## Platform-Specific Registration
@@ -100,7 +100,17 @@ You can omit intermediate wrappers. Shell auto-wraps:
2. Add `FlyoutItem` or `TabBar` elements for top-level navigation
3. Add `Tab` elements for bottom tabs; nest multiple `ShellContent` for top tabs
4. **Always use `ContentTemplate`** with `DataTemplate` so pages load on demand
5. Register detail-page routes in the `AppShell` constructor
5. **Give every `ShellContent` an explicit `Route`** (see below)
6. Register detail-page routes in the `AppShell` constructor
> **Set `Route=` on every `ShellContent`.** If you omit it, MAUI auto-generates a
> name from a shared counter — `Routing.cs` produces `D_FAULT_{TypeName}{n}`. A real
> shell with three unnamed `ShellContent` elements yields routes like
> `D_FAULT_ShellContent2` and `D_FAULT_ShellContent5`: the numbers are not
> sequential, they depend on how many Shell elements were constructed first, and they
> shift when you reorder or add pages. You cannot write a stable absolute route
> (`//dashboard`) or deep link against that. An explicit `Route="dashboard"` is stable
> forever.
```xml
<Shell xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
@@ -111,20 +121,20 @@ You can omit intermediate wrappers. Shell auto-wraps:
<FlyoutItem Title="Animals" Icon="animals.png">
<Tab Title="Cats">
<ShellContent Title="Domestic"
<ShellContent Title="Domestic" Route="domesticcats"
ContentTemplate="{DataTemplate views:DomesticCatsPage}" />
<ShellContent Title="Wild"
<ShellContent Title="Wild" Route="wildcats"
ContentTemplate="{DataTemplate views:WildCatsPage}" />
</Tab>
<Tab Title="Dogs" Icon="dogs.png">
<ShellContent ContentTemplate="{DataTemplate views:DogsPage}" />
<ShellContent Route="dogs" ContentTemplate="{DataTemplate views:DogsPage}" />
</Tab>
</FlyoutItem>
<TabBar>
<ShellContent Title="Home" Icon="home.png"
<ShellContent Title="Home" Icon="home.png" Route="home"
ContentTemplate="{DataTemplate views:HomePage}" />
<ShellContent Title="Settings" Icon="settings.png"
<ShellContent Title="Settings" Icon="settings.png" Route="settings"
ContentTemplate="{DataTemplate views:SettingsPage}" />
</TabBar>
</Shell>