diff --git a/plugins/dotnet-maui/skills/maui-app-lifecycle/SKILL.md b/plugins/dotnet-maui/skills/maui-app-lifecycle/SKILL.md index ffb1912c40..886a445b53 100644 --- a/plugins/dotnet-maui/skills/maui-app-lifecycle/SKILL.md +++ b/plugins/dotnet-maui/skills/maui-app-lifecycle/SKILL.md @@ -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) @@ -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 diff --git a/plugins/dotnet-maui/skills/maui-app-lifecycle/references/lifecycle-api.md b/plugins/dotnet-maui/skills/maui-app-lifecycle/references/lifecycle-api.md index 8f4a936b05..8b6f01c824 100644 --- a/plugins/dotnet-maui/skills/maui-app-lifecycle/references/lifecycle-api.md +++ b/plugins/dotnet-maui/skills/maui-app-lifecycle/references/lifecycle-api.md @@ -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) @@ -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 diff --git a/plugins/dotnet-maui/skills/maui-collectionview/SKILL.md b/plugins/dotnet-maui/skills/maui-collectionview/SKILL.md index a48083fd06..2158a64325 100644 --- a/plugins/dotnet-maui/skills/maui-collectionview/SKILL.md +++ b/plugins/dotnet-maui/skills/maui-collectionview/SKILL.md @@ -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 --- @@ -34,6 +37,42 @@ 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`. + +**The API sections below are a reference, not a checklist — offer them only when +relevant.** Four rules are non-negotiable, because violating them produces code that +does not work or silently loses compile-time checking: + +1. Never use `ViewCell` as a `DataTemplate` root in `CollectionView`. +2. Use `ObservableCollection` when the list mutates after first render. +3. Mutate the bound collection on the UI thread. +4. Set `x:DataType` on every `DataTemplate` (and on the page root) for compiled bindings. + +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`) bound to `ItemsSource` @@ -42,19 +81,42 @@ license: MIT ## Basic Setup +A complete, copy-pasteable page. Two things are load-bearing: the `xmlns:models` +declaration that every `x:DataType="models:Item"` in this skill assumes, and the +**root `x:DataType`** — without it the outer `ItemsSource` binding is not compiled: + ```xml - - - - - - - - - + + + + + + + + + + + + + + ``` +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. + +The inline `` above keeps the example self-contained. In +an app that uses dependency injection, register the ViewModel instead and assign it +through constructor injection (`BindingContext = vm;`) — see the +**maui-dependency-injection** skill. + **Key rules:** - Bind `ItemsSource` to an `ObservableCollection` so the UI updates on add/remove. @@ -309,13 +371,66 @@ 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.*"). It is **not** +obsolete on .NET 9 and earlier, so check the project's target framework before +describing it as deprecated. **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 the default + `MeasureAllItems`, which measures every item individually. Set it on the `CollectionView` + itself (it is declared on `StructuredItemsView`), **not** on `LinearItemsLayout` / + `GridItemsLayout`: ```xml - + + + + + + + + + ``` -- **Always use `ObservableCollection`**, not `List`. Swapping a `List` forces a full re-render. + **When `MeasureFirstItem` is the wrong choice** — keep the default `MeasureAllItems` if: + - Items vary in height (wrapping text, optional rows, images of differing aspect) — the + first item's size is applied to all, so the rest are clipped or stretched. + - A `DataTemplateSelector` returns different templates — the first item won't represent + the others. + - The first item is atypical (a header-like or "featured" row) — every item inherits its + size. Fixing this by reordering data is a smell; use `MeasureAllItems` instead. + - Item size depends on runtime data that isn't loaded yet when the first item is measured. +- **Use `ObservableCollection` when the list mutates after first render.** It implements + `INotifyCollectionChanged`, so in-place `Add`/`Remove`/`Insert` update the UI incrementally. + A `List` is fine for a list that never changes after it is bound. Note that *replacing* + `ItemsSource` re-renders everything regardless of the collection type — so mutate the bound + collection in place rather than reassigning it. - **Update collections on the UI thread** — `MainThread.BeginInvokeOnMainThread(() => Items.Add(item))`. ## Common Pitfalls @@ -328,9 +443,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 ``, not on `` / ``. | +| 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` if the list mutates. +- [ ] `ItemSizingStrategy` (if used) is on ``, 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 diff --git a/plugins/dotnet-maui/skills/maui-collectionview/references/collectionview-api.md b/plugins/dotnet-maui/skills/maui-collectionview/references/collectionview-api.md index 75371b2a95..2b8af5701b 100644 --- a/plugins/dotnet-maui/skills/maui-collectionview/references/collectionview-api.md +++ b/plugins/dotnet-maui/skills/maui-collectionview/references/collectionview-api.md @@ -33,7 +33,7 @@ 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 | |---|---| @@ -41,6 +41,14 @@ Controls how items are measured. Set on `ItemsLayout`. | `MeasureFirstItem` | Measures only the first item and applies that size to all. Much faster for uniform items. | ```xml + + +``` + +```xml + diff --git a/plugins/dotnet-maui/skills/maui-data-binding/SKILL.md b/plugins/dotnet-maui/skills/maui-data-binding/SKILL.md index 766ebe0b8a..ecec154fbc 100644 --- a/plugins/dotnet-maui/skills/maui-data-binding/SKILL.md +++ b/plugins/dotnet-maui/skills/maui-data-binding/SKILL.md @@ -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) | Add the right `x:DataType` for that binding scope; for XC0023 remove the explicit `x:DataType="{x:Null}"` | `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 | Set `MauiEnableXamlCBindingWithSourceCompilation` to `true`, **then** `XC0022;XC0025` | Promoting `XC0025` without the switch if the project uses `Source=` / `RelativeSource` bindings | + +**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 @@ -99,17 +117,29 @@ 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 `` | + +> These four codes are **verified against .NET 10 / .NET 11 MAUI** +> (`Build.Tasks/BuildException.cs`, `ErrorMessages.resx`). Diagnostic numbering is +> SDK-band-sensitive — re-check against `BuildException.cs` before relying on it on a +> newer SDK. Add to the `.csproj`: ```xml + +true XC0022;XC0025 ``` +If you promote `XC0025` without enabling that switch, make sure the project has no +`Source=` / `RelativeSource` bindings — otherwise they will be reported. + --- ## Binding Modes @@ -370,7 +400,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`; promote `XC0022` (see [Enforce binding warnings as errors](#enforce-binding-warnings-as-errors)) | | Forgetting to set `BindingContext` | Set in XAML (``) 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 | diff --git a/plugins/dotnet-maui/skills/maui-dependency-injection/SKILL.md b/plugins/dotnet-maui/skills/maui-dependency-injection/SKILL.md index 71fd3e4ec7..17c12d8590 100644 --- a/plugins/dotnet-maui/skills/maui-dependency-injection/SKILL.md +++ b/plugins/dotnet-maui/skills/maui-dependency-injection/SKILL.md @@ -39,6 +39,33 @@ 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 | Prefer `AddTransient` | A fresh instance per navigation avoids stale state, and a Singleton page cannot be re-added to the visual tree after it is removed. Singleton is defensible for a genuinely single-instance page (e.g. a root tab you want to keep warm) | +| 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. + +**Answer narrowly, but completely.** When you recommend a lifetime change, show the +registration code, and give the realistic alternatives rather than a single verdict — +for a unit-of-work or `DbContext` question that means `AddTransient`, an explicit +`IServiceScopeFactory.CreateScope()`, **and** the factory pattern +(`AddDbContextFactory`), with a note on when each fits. A one-line prescription is +usually a worse answer than a short menu with trade-offs. + +```csharp +// Explicit scope when you genuinely need unit-of-work semantics +using var scope = scopeFactory.CreateScope(); +var db = scope.ServiceProvider.GetRequiredService(); +``` + ## Workflow 1. Identify all services, ViewModels, and Pages that need to participate in dependency injection. @@ -57,11 +84,11 @@ license: MIT |---|---|---| | `AddSingleton()` | Shared state, expensive to create, app-wide config | `HttpClient` factory, settings service, database connection | | `AddTransient()` | Lightweight, stateless, or needs a fresh instance per use | Pages, ViewModels, per-call API wrappers | -| `AddScoped()` | Per-scope lifetime with manually created `IServiceScope` | Scoped unit-of-work (rare in MAUI) | +| `AddScoped()` | Per-window lifetime, or a manually created `IServiceScope` | Scoped unit-of-work (rare in MAUI) | -**Key rule:** Register Pages and ViewModels as **Transient**. Register shared services as **Singleton**. +**Key rule:** Register Pages and ViewModels as **Transient** by default. 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. --- @@ -146,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 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 @@ -219,19 +274,20 @@ builder.Services.AddSingleton(); builder.Services.AddTransient(); ``` -### 2. Unregistered Page Silently Skips Injection +### 2. ContentTemplate Pages Are Not Created Through DI -If a Page appears in Shell XAML via `` but is **not** registered in `builder.Services`, MAUI creates it with the parameterless constructor. Dependencies are silently `null` — no exception is thrown. +Pages declared in Shell XAML via `` are instantiated with `Activator.CreateInstance` (`ElementTemplate.cs`), **not** through the service provider. Constructor injection does not run on that path: if the page's only constructor takes dependencies, you get a `MissingMethodException` — not a silently `null` dependency. -```csharp -// ❌ Missing — injection silently skipped -// builder.Services.AddTransient(); +Pages reached through `Routing.RegisterRoute` + `GoToAsync` are different: they go through `ActivatorUtilities.GetServiceOrCreateInstance` (`Routing.cs`), which injects registered dependencies even if the page type itself was never registered, and **throws** if a required dependency cannot be resolved. -// ✅ Always register pages that need injection +```csharp +// Registering the page and its dependencies keeps both paths working builder.Services.AddTransient(); builder.Services.AddTransient(); ``` +If you need DI for a tab/flyout page, give it a parameterless constructor that resolves what it needs, or navigate to it by route instead of embedding it in `ContentTemplate`. + ### 3. XAML Resource Parsing vs. DI Timing XAML resources in `App.xaml` are parsed during `InitializeComponent()` — before the container is fully available. Defer service-dependent work to `CreateWindow()`: @@ -273,7 +329,7 @@ Forgetting a platform in `#if` blocks means `GetService()` 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`. --- @@ -285,7 +341,7 @@ Forgetting a platform in `#if` blocks means `GetService()` returns `null` at - [ ] Interfaces defined for services that need test substitution - [ ] Platform-specific `#if` registrations cover all target platforms or include a fallback - [ ] Service-dependent work deferred to `CreateWindow()`, not run during XAML parse -- [ ] `AddScoped` only used alongside manually created `IServiceScope` +- [ ] `AddScoped` used only when window lifetime is intended, or alongside a manually created `IServiceScope` ## References diff --git a/plugins/dotnet-maui/skills/maui-safe-area/SKILL.md b/plugins/dotnet-maui/skills/maui-safe-area/SKILL.md index a751b0c974..a47af06db9 100644 --- a/plugins/dotnet-maui/skills/maui-safe-area/SKILL.md +++ b/plugins/dotnet-maui/skills/maui-safe-area/SKILL.md @@ -123,9 +123,9 @@ In .NET 9, Android `ContentPage` behaved like `Container`. In .NET 10, the defau ``` -### WindowSoftInputModeAdjust.Resize removed +### WindowSoftInputModeAdjust.Resize superseded -If you used `WindowSoftInputModeAdjust.Resize` in .NET 9, replace it with `SafeAreaEdges="All"` on the ContentPage for equivalent keyboard avoidance. +`WindowSoftInputModeAdjust.Resize` still exists and still compiles (it is not removed and not obsolete), but it is Android-only. For cross-platform keyboard avoidance prefer `SafeAreaEdges="All"` (or the `SoftInput` region) on the ContentPage. ## Usage Patterns @@ -271,7 +271,7 @@ Available CSS environment variables: `env(safe-area-inset-top)`, `env(safe-area- | `Layout.IgnoreSafeArea="True"` | `SafeAreaEdges="None"` | | `WindowSoftInputModeAdjust.Resize` | `SafeAreaEdges="All"` on ContentPage | -The legacy properties still compile but are marked obsolete. `IgnoreSafeArea="True"` maps internally to `SafeAreaRegions.None`. +The legacy `ios:Page.UseSafeArea` and `Layout.IgnoreSafeArea` properties still compile but are marked obsolete. `IgnoreSafeArea="True"` maps internally to `SafeAreaRegions.None`. `WindowSoftInputModeAdjust.Resize` is **not** obsolete — it remains supported, but is Android-only. ```xaml diff --git a/plugins/dotnet-maui/skills/maui-shell-navigation/SKILL.md b/plugins/dotnet-maui/skills/maui-shell-navigation/SKILL.md index 1866a01f40..a9a9e9cccf 100644 --- a/plugins/dotnet-maui/skills/maui-shell-navigation/SKILL.md +++ b/plugins/dotnet-maui/skills/maui-shell-navigation/SKILL.md @@ -40,6 +40,31 @@ Implement page navigation in .NET MAUI apps using Shell. Shell provides URI-base - Pages (`ContentPage`) to navigate between - Route names for detail pages not in the visual hierarchy +## Rules That Change the Answer + +These are the Shell-specific decisions that are easy to get wrong. Apply them +whenever they are relevant to what the user asked. + +| Situation | Do this | Not this | +|---|---|---| +| Declaring pages in `AppShell.xaml` | With `xmlns:views="clr-namespace:MyApp.Views"` declared: `` — the page is created on first navigation | ``, which constructs **every** page at startup | +| Navigating to a page not in the visual hierarchy | `Routing.RegisterRoute("details", typeof(DetailsPage))` first | Calling `GoToAsync("details")` unregistered — it throws at runtime | +| Receiving navigation parameters | Implement `IQueryAttributable` on the **ViewModel** | Implementing it on the Page, which splits state from the BindingContext | +| Passing a whole object | `ShellNavigationQueryParameters` | Serialising the object into the query string | +| Any `GoToAsync` call | `await` it | Fire-and-forget — exceptions are swallowed and navigation races | +| Confirming before back navigation | `ShellNavigatingEventArgs.GetDeferral()` … `deferral.Complete()` | Blocking synchronously on the dialog task | +| Detecting back navigation | Check `e.Source == ShellNavigationSource.Pop` | Assuming every navigation is a back action | + +**Do not** propose `NavigationPage` / `PushAsync` solutions for a Shell app, and do +not restructure a working `AppShell` hierarchy unless the user asked. + +**Answer narrowly, but completely.** Staying on topic does not mean being terse. When +you show a navigation change, include the pieces needed to run it: the `AppShell.xaml` +markup *and* the `Routing.RegisterRoute` call, or the `GoToAsync` call *and* the +receiving `IQueryAttributable` / `[QueryProperty]` code. Where two approaches are both +valid (query string vs `ShellNavigationQueryParameters`), show both and say when each +fits — a single snippet the user still has to complete is a worse answer. + ## Shell Visual Hierarchy Shell uses a four-level hierarchy. Each level wraps the one below it: @@ -75,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 - - - + - - @@ -172,16 +207,23 @@ public class AnimalDetailsViewModel : ObservableObject, IQueryAttributable ### Option 2: QueryProperty Attribute -Apply directly on the page class: +Apply on the **ViewModel** class (or the page, if it genuinely owns the state). +Prefer `IQueryAttributable` on the ViewModel — it keeps navigation state with the +`BindingContext` and handles multiple parameters in one call: ```csharp [QueryProperty(nameof(AnimalId), "id")] -public partial class AnimalDetailsPage : ContentPage +public partial class AnimalDetailsViewModel : ObservableObject { - public string AnimalId { get; set; } + [ObservableProperty] + private string _animalId = string.Empty; } ``` +Shell applies query attributes *after* the page constructor sets `BindingContext`, +so the property must raise change notification — a plain auto-property leaves the +binding stuck on its initial value. + ### Option 3: Complex Objects via ShellNavigationQueryParameters Pass objects without serializing to strings: diff --git a/plugins/dotnet-maui/skills/maui-theming/SKILL.md b/plugins/dotnet-maui/skills/maui-theming/SKILL.md index b812429e69..292bbe126f 100644 --- a/plugins/dotnet-maui/skills/maui-theming/SKILL.md +++ b/plugins/dotnet-maui/skills/maui-theming/SKILL.md @@ -50,6 +50,30 @@ Apply light/dark mode support, custom branded themes, and runtime theme switchin 7. Verify Android `ConfigChanges.UiMode` is set on `MainActivity` to avoid activity restarts on theme change. 8. Test both light and dark themes on at least one target platform, confirming all UI elements respond correctly. +## Rules That Change the Answer + +Check these rules against the user's scenario, and apply **only** the ones that +affect what they asked. `UiMode` and dictionary swapping matter for *runtime theme +switching*; they are noise in a question about setting up `AppThemeBinding`. + +**Answer narrowly, but completely.** Completeness means showing the code that +implements *what you recommended* — not adding adjacent topics. If you recommend +`DynamicResource`, show the dictionary swap that makes it update. If the user asks +for light/dark colours in C#, show **both** `SetAppThemeColor` (colours) and the +generic `SetAppTheme` (any bindable property type), and prefer resource keys over +scattered hardcoded colours. Do not tack on platform configuration the question +didn't raise. + +| Rule | Do this | Not this | Why | +|---|---|---|---| +| **Runtime-swapped values must be dynamic** | `{DynamicResource Key}` | `{StaticResource Key}` | `StaticResource` resolves once at load and never updates when dictionaries are swapped. | +| **Android must declare `UiMode`** *(only for runtime/system theme switching)* | Include `ConfigChanges.UiMode` in the `ConfigurationChanges` list on `MainActivity` | Omitting it | Without it Android restarts the activity on theme change — navigation state is lost and it looks like a crash. Irrelevant to a static `AppThemeBinding` setup | +| **Force a theme via `UserAppTheme`** | `Application.Current.UserAppTheme = AppTheme.Dark` | Manually re-assigning colors | `UserAppTheme` overrides the OS; `AppTheme.Unspecified` returns to following the system. | + +**Do not** replace a working `AppThemeBinding` setup with ResourceDictionary +swapping (or vice versa) unless the user needs what the other approach provides — +more than two themes, or a user-selectable theme. + ## Choosing an Approach | Approach | Best for | Limitation | @@ -62,30 +86,83 @@ Apply light/dark mode support, custom branded themes, and runtime theme switchin `AppThemeBinding` selects a value based on the current system theme. It supports `Light`, `Dark`, and an optional `Default` fallback. -### XAML +### Define the palette once — don't scatter literals + +Putting `{AppThemeBinding Light=#333333, Dark=#FFFFFF}` on every element is the +single most common theming mistake: the palette ends up duplicated across dozens of +files and cannot be changed in one place. **Recommend this shape as the final +answer**, not inline literals: + +```xml + + + + + + #FFFFFF + #1E1E1E + #333333 + #E0E0E0 + + + + + + + + +``` + +Pages then need **no theming markup at all** — they pick the styles up implicitly. +Use an inline `AppThemeBinding` only for genuine one-offs, and even then reference +`{StaticResource}` keys rather than literal hex. + +### XAML (inline form, for one-offs) ```xml