Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
8405072
Fix dotnet-maui skill regressions and sharpen decisiveness (#895)
kubaflo Jul 26, 2026
b07628c
maui-collectionview: fix eval-identified loss causes
kubaflo Jul 26, 2026
8a200fa
Fix markdownlint MD012 (duplicate blank line)
kubaflo Jul 26, 2026
ad057a0
Apply multi-model review feedback (4-model consensus round 1)
kubaflo Jul 26, 2026
746679a
Apply multi-model review feedback (round 2) + Copilot comment
kubaflo Jul 26, 2026
0b86a41
Apply multi-model review feedback (round 3)
kubaflo Jul 26, 2026
df0806a
Resolve Copilot review comment: conflicting XC0022 guidance
kubaflo Jul 26, 2026
9c02b41
Fix the 3 skills that failed the official evaluation
kubaflo Jul 27, 2026
5812d5a
Address review feedback: guard the regression, pin versions, fix cros…
kubaflo Jul 28, 2026
1db8796
maui-theming: recommend a single-source palette instead of inline lit…
kubaflo Jul 28, 2026
4d61dd9
Merge remote-tracking branch 'upstream/main' into kubaflo/dotnet-maui…
kubaflo Jul 28, 2026
714b597
maui-theming: never Clear() MergedDictionaries when swapping a theme
kubaflo Jul 28, 2026
6d1201c
Eliminate the last two tying scenarios in shell-navigation and DI
kubaflo Jul 28, 2026
cf62633
Add regression-guard scenarios to theming, shell-navigation and DI evals
kubaflo Jul 29, 2026
fcb7e56
Fix SetAppTheme example: wrong BindableProperty owner (Copilot review)
kubaflo Jul 29, 2026
a1eec66
Merge branch 'main' into kubaflo/dotnet-maui-skills-eval-fixes
kubaflo Jul 29, 2026
b02c5a2
Revert accidental repo-wide runs: 1 -> 3 change in the experiment config
kubaflo Jul 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 14 additions & 10 deletions plugins/dotnet-maui/skills/maui-app-lifecycle/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,14 +156,18 @@ protected override void OnDestroying()

### iOS / Mac Catalyst

| Window Event | UIKit Callback |
|---|---|
| Created | `WillFinishLaunching` / `SceneWillConnect` |
| Activated | `DidBecomeActive` |
| Deactivated | `WillResignActive` |
| Stopped | `DidEnterBackground` |
| Resumed | `WillEnterForeground` |
| Destroying | `WillTerminate` |
| Window Event | UIKit Callback | `AddiOS` builder method |
|---|---|---|
| Created | `WillFinishLaunching` / `SceneWillConnect` | `.WillFinishLaunching()` / `.SceneWillConnect()` |
| Activated | `DidBecomeActive` | `.OnActivated()` |
| Deactivated | `WillResignActive` | `.OnResignActivation()` |
| Stopped | `DidEnterBackground` | `.DidEnterBackground()` |
| Resumed | `WillEnterForeground` | `.WillEnterForeground()` |
| Destroying | `WillTerminate` | `.WillTerminate()` |

> ⚠️ The UIKit selector names and the `AddiOS` builder method names differ for
> activation. There is **no** `.DidBecomeActive()` or `.WillResignActive()` builder
> method — use `.OnActivated()` and `.OnResignActivation()` or the code will not compile.

### Windows (WinUI)

Expand Down Expand Up @@ -192,8 +196,8 @@ builder.ConfigureLifecycleEvents(events =>
.OnDestroy(activity => Debug.WriteLine("Android OnDestroy")));
#elif IOS || MACCATALYST
events.AddiOS(ios => ios
.DidBecomeActive(app => Debug.WriteLine("iOS DidBecomeActive"))
.WillResignActive(app => Debug.WriteLine("iOS WillResignActive"))
.OnActivated(app => Debug.WriteLine("iOS OnActivated"))
.OnResignActivation(app => Debug.WriteLine("iOS OnResignActivation"))
.DidEnterBackground(app => Debug.WriteLine("iOS DidEnterBackground"))
.WillEnterForeground(app => Debug.WriteLine("iOS WillEnterForeground")));
#elif WINDOWS
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,14 +90,18 @@ protected override Window CreateWindow(IActivationState? activationState)

### iOS / Mac Catalyst

| Window event | UIKit callback |
|---|---|
| Created | `WillFinishLaunching` / `SceneWillConnect` |
| Activated | `DidBecomeActive` |
| Deactivated | `WillResignActive` |
| Stopped | `DidEnterBackground` |
| Resumed | `WillEnterForeground` |
| Destroying | `WillTerminate` |
| Window event | UIKit callback | `AddiOS` builder method |
|---|---|---|
| Created | `WillFinishLaunching` / `SceneWillConnect` | `.WillFinishLaunching()` / `.SceneWillConnect()` |
| Activated | `DidBecomeActive` | `.OnActivated()` |
| Deactivated | `WillResignActive` | `.OnResignActivation()` |
| Stopped | `DidEnterBackground` | `.DidEnterBackground()` |
| Resumed | `WillEnterForeground` | `.WillEnterForeground()` |
| Destroying | `WillTerminate` | `.WillTerminate()` |

> ⚠️ The activation builder methods are **not** named after the UIKit selectors.
> `.DidBecomeActive()` and `.WillResignActive()` do not exist on `IiOSLifecycleBuilder` —
> use `.OnActivated()` and `.OnResignActivation()`.

### Windows (WinUI)

Expand Down Expand Up @@ -129,8 +133,8 @@ builder.ConfigureLifecycleEvents(events =>
events.AddiOS(ios => ios
.WillFinishLaunching((app, options) => { Log("iOS WillFinishLaunching"); return true; })
.SceneWillConnect((scene, session, options) => Log("iOS SceneWillConnect"))
.DidBecomeActive(app => Log("iOS DidBecomeActive"))
.WillResignActive(app => Log("iOS WillResignActive"))
.OnActivated(app => Log("iOS OnActivated"))
.OnResignActivation(app => Log("iOS OnResignActivation"))
.DidEnterBackground(app => Log("iOS DidEnterBackground"))
.WillTerminate(app => Log("iOS WillTerminate")));
#elif WINDOWS
Expand Down
117 changes: 103 additions & 14 deletions plugins/dotnet-maui/skills/maui-collectionview/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ description: >
displaying scrollable data, replacing ListView.
DO NOT USE FOR: simple static layouts without scrollable data (use Grid or
StackLayout), map pin lists (use Microsoft.Maui.Controls.Maps), table-based
data entry forms, or non-MAUI list controls.
data entry forms, non-MAUI list controls, CarouselView or BindableLayout
questions, platform-specific handler or renderer customization, diagnosing
CollectionView bugs in the MAUI framework itself, or general MVVM/binding
questions that merely happen to mention a list (use maui-data-binding).
license: MIT
---

Expand All @@ -34,6 +37,40 @@ license: MIT
- Table-based data entry forms — use standard form controls
- Simple text-only lists with no interaction — consider `BindableLayout` on a `StackLayout`

## Scope Control — Answer Only What Was Asked

This skill is a **reference you consult**, not a checklist you apply. Most requests
need one or two sections from it. Pulling in the rest makes the answer worse.

**Stop conditions — do NOT act when:**

- **The user asked a narrow question.** Answer that question only. Do not append
grouping, swipe actions, empty views, snap points, or performance tips that
were not asked about.
- **The user's existing code already works.** Do not rewrite working markup to
match the examples here. Point out a concrete defect; if there is none, say so
and answer the question that was asked.
- **The change is stylistic.** Renaming, reordering attributes, or restructuring
a template that already behaves correctly is churn, not a fix.
- **The control isn't `CollectionView`.** `CarouselView`, `BindableLayout`, and
`ListView`-in-maintenance code have different rules. Do not rewrite `ListView`
code the user did not ask about — but if they ask *which* control to use, or are
migrating from Xamarin.Forms, recommend `CollectionView` (see
[Migrating from ListView](#migrating-from-listview)).
- **The problem is really a binding, DI, or navigation problem** that happens to
involve a list — defer to `maui-data-binding`, `maui-dependency-injection`, or
`maui-shell-navigation`.

**Everything below is a suggestion the agent may decline.** Only three rules are
non-negotiable, because violating them produces code that does not work:

1. Never use `ViewCell` as a `DataTemplate` root in `CollectionView`.
2. Use `ObservableCollection<T>` when the list mutates after first render.
3. Mutate the bound collection on the UI thread.

Everything else — sizing strategy, snap points, header/footer, empty views — is
optional and should be offered only when it addresses the user's actual problem.

## Inputs

- A data source (typically `ObservableCollection<T>`) bound to `ItemsSource`
Expand All @@ -42,19 +79,31 @@ license: MIT

## Basic Setup

A complete, copy-pasteable page. Note the `xmlns:models` declaration — every
`x:DataType="models:Item"` in this skill assumes it:

```xml
<CollectionView ItemsSource="{Binding Items}">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="models:Item">
<HorizontalStackLayout Padding="8" Spacing="8">
<Image Source="{Binding Icon}" WidthRequest="40" HeightRequest="40" />
<Label Text="{Binding Name}" VerticalOptions="Center" />
</HorizontalStackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:models="clr-namespace:MyApp.Models"
x:Class="MyApp.ItemsPage">
<CollectionView ItemsSource="{Binding Items}">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="models:Item">
<HorizontalStackLayout Padding="8" Spacing="8">
<Image Source="{Binding Icon}" WidthRequest="40" HeightRequest="40" />
<Label Text="{Binding Name}" VerticalOptions="Center" />
</HorizontalStackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</ContentPage>
```

Later snippets show only the `CollectionView` element. When you hand a snippet to a
user, include the matching `xmlns:` declaration for any prefix it uses, or the XAML
will not compile.

**Key rules:**

- Bind `ItemsSource` to an `ObservableCollection<T>` so the UI updates on add/remove.
Expand Down Expand Up @@ -309,12 +358,39 @@ collectionView.ScrollTo(item: myItem, position: ScrollToPosition.MakeVisible, an
- `SnapPointsType`: `None`, `Mandatory`, `MandatorySingle`
- `SnapPointsAlignment`: `Start`, `Center`, `End`

## Migrating from ListView

`ListView` still compiles, but as of .NET 10 it is marked `[Obsolete]`
("*ListView is deprecated. Please use CollectionView instead.*"). **If the user asks
which control to use, or is migrating from Xamarin.Forms, recommend
`CollectionView`** — it is faster, needs no `ViewCell`, and supports flexible
layouts. What to avoid is silently rewriting `ListView` code the user did not ask
you to touch.

| `ListView` | `CollectionView` equivalent |
|---|---|
| `ViewCell` template root | Any `View`/`Layout` root — **`ViewCell` is not supported** |
| `ItemSelected` event | `SelectionChanged` event, or `SelectionChangedCommand` |
| `ItemTapped` event | A `TapGestureRecognizer` in the item template — `SelectionChanged` only fires when the selection *changes*, so it will not re-fire on tapping the already-selected item |
| `IsPullToRefreshEnabled` + `Refreshing` | Wrap the `CollectionView` in a `RefreshView` |
| `IsGroupingEnabled` | `IsGrouped` |
| `HasUnevenRows="True"` | Default `ItemSizingStrategy="MeasureAllItems"` |
| `RowHeight` (fixed height) | Set the height in the item template. `MeasureFirstItem` only reuses the first item's measured size — it is not an explicit row height |
| `SeparatorVisibility` / `SeparatorColor` | **No equivalent** — draw a `BoxView`/`Border` in the item template |

The missing separator API is the most common migration surprise: `CollectionView`
has no built-in separators, so add one to the template yourself.

## Performance Tips

- **Use `MeasureFirstItem`** for uniform item sizes — significantly faster than `MeasureAllItems`:
Apply these only when the user reports a performance problem or explicitly asks
about performance — they are not a default checklist.

- **Use `MeasureFirstItem`** for uniform item sizes — significantly faster than `MeasureAllItems`. Set it on the `CollectionView` itself (it is declared on `StructuredItemsView`), **not** on `LinearItemsLayout` / `GridItemsLayout`:
```xml
<LinearItemsLayout Orientation="Vertical" ItemSizingStrategy="MeasureFirstItem" />
<CollectionView ItemsSource="{Binding Items}" ItemSizingStrategy="MeasureFirstItem" />
```
Only use it when every item really is the same height — with variable-height items it clips or stretches content.
- **Always use `ObservableCollection<T>`**, not `List<T>`. Swapping a `List` forces a full re-render.
Comment thread
AbhitejJohn marked this conversation as resolved.
Outdated
- **Update collections on the UI thread** — `MainThread.BeginInvokeOnMainThread(() => Items.Add(item))`.

Expand All @@ -328,9 +404,22 @@ collectionView.ScrollTo(item: myItem, position: ScrollToPosition.MakeVisible, an
| Incremental loading fires endlessly | Don't use `StackLayout` as layout; use `LinearItemsLayout` or `GridItemsLayout`. |
| EmptyView doesn't render correctly | Wrap custom empty views in `ContentView`. |
| Poor scroll performance | Use `MeasureFirstItem` sizing strategy for uniform item sizes. |
| `ItemSizingStrategy` doesn't compile | It is declared on `StructuredItemsView` — set it on `<CollectionView>`, not on `<LinearItemsLayout>` / `<GridItemsLayout>`. |
| Items clipped or stretched | `MeasureFirstItem` assumes uniform item size. Use the default `MeasureAllItems` for variable-height items. |
| Selected state not visible | Add `VisualState Name="Selected"` to the item template root element. |
| Binding errors in SwipeView commands | Use `RelativeSource AncestorType` to reach the ViewModel from inside the item template. |
| Using ListView instead of CollectionView | `CollectionView` replaces `ListView` — it has better performance, no `ViewCell`, and flexible layouts. |

## Validation

Before returning CollectionView markup you wrote or edited, confirm:

- [ ] The `DataTemplate` root is a `View`/`Layout` — **not** `ViewCell`.
- [ ] `DataTemplate` declares `x:DataType` for compiled bindings.
- [ ] `ItemsSource` is bound to `ObservableCollection<T>` if the list mutates.
- [ ] `ItemSizingStrategy` (if used) is on `<CollectionView>`, not on the layout.
- [ ] `Multiple` selection binds `SelectedItems`; `Single` binds `SelectedItem` (`TwoWay`).
- [ ] `RefreshView.IsRefreshing` is set back to `false` when the refresh completes.
- [ ] The answer covers **only** what the user asked — no unrequested sections.

## References

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,22 @@ Set `ItemsLayout` to control arrangement. Default is `VerticalList`.

## ItemSizingStrategy

Controls how items are measured. Set on `ItemsLayout`.
Controls how items are measured. Set on the **`CollectionView` element itself** — it is declared on `StructuredItemsView`, **not** on `ItemsLayout`.

| Value | Behavior |
|---|---|
| `MeasureAllItems` | Measures every item individually (default). Accurate but slower for heterogeneous sizes. |
| `MeasureFirstItem` | Measures only the first item and applies that size to all. Much faster for uniform items. |

```xml
<!-- ✅ Correct — ItemSizingStrategy is a CollectionView property -->
<CollectionView ItemsSource="{Binding Items}"
ItemSizingStrategy="MeasureFirstItem" />
```

```xml
<!-- ❌ Wrong — LinearItemsLayout/GridItemsLayout have no ItemSizingStrategy property.
This does not compile. -->
<CollectionView.ItemsLayout>
<LinearItemsLayout Orientation="Vertical"
ItemSizingStrategy="MeasureFirstItem" />
Expand Down
28 changes: 23 additions & 5 deletions plugins/dotnet-maui/skills/maui-data-binding/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,24 @@ and treat binding warnings as build errors.
- XAML pages or C# code-behind where bindings are declared
- A ViewModel class (or plan to create one)

## Rules That Change the Answer

Apply these to every binding answer — they are the differences between "it compiles"
and "it actually updates the UI".

| Situation | Do this | Not this |
|---|---|---|
| Deciding where `x:DataType` goes | Put it wherever a binding scope starts — the page/view root, and **each** `DataTemplate` | Scattering it on arbitrary children that share the parent's `BindingContext` |
| A binding falls back to reflection (XC0022 / XC0023) | Fix the path or supply the right `x:DataType` | `x:DataType="x:Object"` to silence it — this disables compile-time checking |
| A `DataTemplate` inherits `x:DataType` from an outer scope (XC0024) | Give the `DataTemplate` its **own** `x:DataType` | Leaving it to resolve against the wrong type |
| ViewModel change notification | `ObservableObject` + `[ObservableProperty]`, or implement `INotifyPropertyChanged` | A plain POCO base class — bindings will never update |
| Bindings show blank | Check `BindingContext` is actually set | Assuming the binding path is wrong |
| Enforcing compiled bindings | `<WarningsAsErrors>XC0022;XC0025</WarningsAsErrors>` | Leaving them as warnings and ignoring them |

**Do not** restructure a ViewModel or add a converter that the user did not ask for
and that fixes no real defect. Adding `x:DataType` is different: when you are
already editing a page's bindings, recommending compiled bindings is in scope.

---

## Compiled Bindings — x:DataType Placement
Expand Down Expand Up @@ -99,10 +117,10 @@ anti-pattern — it disables compile-time checking and reintroduces reflection.

| Warning | Meaning |
|---------|---------|
| **XC0022** | Binding path not found on the declared `x:DataType` |
| **XC0023** | Property is not bindable |
| **XC0024** | `x:DataType` type not found |
| **XC0025** | Binding used without `x:DataType` (non-compiled fallback) |
| **XC0022** | Binding used **without `x:DataType` in scope** — not compiled, falls back to reflection |
| **XC0023** | Binding not compiled because `x:DataType` is **explicitly `null`** |
| **XC0024** | `x:DataType` came from an **outer scope** — annotate the `DataTemplate` with its own `x:DataType` |
| **XC0025** | Binding not compiled because it has an explicit **`Source`** — enable `<MauiEnableXamlCBindingWithSourceCompilation>` |

Add to the `.csproj`:

Expand Down Expand Up @@ -370,7 +388,7 @@ MainThread.BeginInvokeOnMainThread(() => Items.Add(newItem));

| Mistake | Fix |
|---------|-----|
| Missing `x:DataType` — bindings silently fall back to reflection | Add `x:DataType` at page root and every `DataTemplate`; enable `XC0025` as error |
| Missing `x:DataType` — bindings silently fall back to reflection | Add `x:DataType` at page root and every `DataTemplate`; enable `XC0022` as error |
Comment thread
AbhitejJohn marked this conversation as resolved.
Outdated
| Forgetting to set `BindingContext` | Set in XAML (`<Page.BindingContext>`) or inject via constructor |
| Specifying redundant `Mode=OneWay` / `Mode=TwoWay` | Omit `Mode` when using the control's default |
| ViewModel does not implement `INotifyPropertyChanged` | Use `ObservableObject` from CommunityToolkit.Mvvm or implement manually |
Expand Down
18 changes: 16 additions & 2 deletions plugins/dotnet-maui/skills/maui-dependency-injection/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,20 @@ license: MIT
- Knowledge of which services, ViewModels, and Pages need registration
- Target platforms (Android, iOS, Mac Catalyst, Windows) for conditional registrations

## Rules That Change the Answer

| Situation | Do this | Why |
|---|---|---|
| Registering a Page or ViewModel | `AddTransient` | Each navigation must get a fresh instance; a Singleton page keeps stale state and can't be re-added to the visual tree |
| Registering shared/expensive state | `AddSingleton` | One instance app-wide (settings, DB connection, `HttpClient` handler) |
| Tempted to use `AddScoped` | Use `AddTransient` (or `AddSingleton` if sharing is intended) | MAUI has **no** built-in request scope like ASP.NET Core's HTTP pipeline. MAUI does create one `IServiceScope` per window, so a Scoped service lives as long as that window — and resolved from the root provider it behaves like a Singleton. Neither gives you per-navigation freshness |
| Navigating to a DI-registered page | Register the page **and** its ViewModel, then `Routing.RegisterRoute` | `Shell.Current.GoToAsync` resolves the page through DI and injects its constructor dependencies |
| Platform-specific implementation | `#if` per platform **with every platform covered** | A missing platform branch leaves the service unregistered and throws at resolution time |

**Do not** introduce DI into a project that isn't using it, swap a working service
lifetime, or add an interface purely for symmetry — only when the user asked or it
fixes a real defect.

## Workflow

1. Identify all services, ViewModels, and Pages that need to participate in dependency injection.
Expand All @@ -61,7 +75,7 @@ license: MIT

**Key rule:** Register Pages and ViewModels as **Transient**. Register shared services as **Singleton**.

> ⚠️ **Avoid `AddScoped` unless you manually manage `IServiceScope`.** MAUI has no built-in request scope like ASP.NET Core. A Scoped registration without an explicit scope silently behaves as a Singleton, leading to subtle bugs.
> ⚠️ **Avoid `AddScoped` unless you manually manage `IServiceScope`.** MAUI has no built-in request scope like ASP.NET Core. MAUI creates one `IServiceScope` per window, so a Scoped service lives as long as that window; resolved from the root provider it silently behaves as a Singleton. Neither gives per-navigation freshness.

---

Expand Down Expand Up @@ -273,7 +287,7 @@ Forgetting a platform in `#if` blocks means `GetService<T>()` returns `null` at

### 6. AddScoped Without Manual Scope

`AddScoped` in MAUI without creating `IServiceScope` manually gives Singleton behavior silently. Use `AddTransient` or `AddSingleton` instead unless you explicitly manage scopes.
See the rule table above: `AddScoped` gives you either window lifetime or Singleton behaviour, never per-navigation freshness. Use `AddTransient` or `AddSingleton` unless you explicitly create and manage an `IServiceScope`.

---

Expand Down
Loading