diff --git a/.github/workflows/skill-review.yml b/.github/workflows/skill-review.yml new file mode 100644 index 0000000..aee21d6 --- /dev/null +++ b/.github/workflows/skill-review.yml @@ -0,0 +1,22 @@ +# Tessl Skill Review — runs on PRs that change any SKILL.md; posts scores as one PR comment. +# Docs: https://github.com/tesslio/skill-review +name: Tessl Skill Review + +on: + pull_request: + branches: [main] + paths: + - "**/SKILL.md" + +jobs: + review: + runs-on: ubuntu-latest + permissions: + pull-requests: write + contents: read + steps: + - uses: actions/checkout@v4 + - uses: tesslio/skill-review@main + # Optional quality gate (off by default — do not enable unless user asked): + # with: + # fail-threshold: 70 diff --git a/skills/dotnet-entity-framework6/SKILL.md b/skills/dotnet-entity-framework6/SKILL.md index 0bb954b..32aa7fd 100644 --- a/skills/dotnet-entity-framework6/SKILL.md +++ b/skills/dotnet-entity-framework6/SKILL.md @@ -1,8 +1,8 @@ --- name: dotnet-entity-framework6 -version: "1.0.0" -category: "Data" -description: "Maintain or migrate EF6-based applications with realistic guidance on what to keep, what to modernize, and when EF Core is or is not the right next step." +version: "1.0.1" +category: "Data, Distributed, and AI" +description: "Maintain or migrate EF6-based applications with realistic guidance on what to keep, what to modernize, and when EF Core is or is not the right next step. Use when working in an EF6 codebase or planning a data layer migration." compatibility: "Requires EF6 or a transition plan from EF6 to EF Core or modern .NET." --- @@ -10,32 +10,71 @@ compatibility: "Requires EF6 or a transition plan from EF6 to EF Core or modern ## Trigger On -- working in an EF6 codebase -- deciding whether to keep EF6, move to modern .NET, or port to EF Core +- working in an EF6 codebase on .NET Framework or modern .NET +- deciding whether to keep EF6, move to modern .NET runtime, or port to EF Core - reviewing EDMX, code-first, or legacy ASP.NET/WPF/WinForms data access +- planning a data layer migration strategy ## Workflow -1. Treat EF6 as stable and supported but no longer the innovation path; do not promise EF Core-only features to EF6 applications. -2. Decide separately between runtime migration and ORM migration; moving to modern .NET can happen before or without moving to EF Core. -3. Review advanced mapping usage, lazy loading, stored procedures, and designer-driven models before planning a port because these often drive migration cost. -4. Prefer small, validated migration slices rather than big-bang rewrites of the data layer. -5. Use `dotnet-entity-framework-core` only when the app is actually ready to adopt EF Core patterns and provider support. -6. Keep performance and behavior checks close to the real database provider, not only mock or in-memory tests. +1. **Audit current EF6 usage** before planning any migration. Identify which features the codebase depends on: + ```csharp + // Common EF6-specific patterns to inventory: + // - EDMX designer models (check for *.edmx files) + // - ObjectContext vs DbContext usage + // - Lazy loading with virtual navigation properties + // - Database.SqlQuery() for raw SQL + // - Stored procedure mappings in model + // - Spatial types (DbGeography, DbGeometry) + ``` +2. **Decide runtime vs ORM migration separately:** + + | Path | When to use | + |------|-------------| + | Keep EF6 on .NET Framework | Legacy app with no runtime pressure | + | EF6 on modern .NET | Runtime upgrade needed, ORM migration too risky | + | EF6 → EF Core | Clean data layer, no EDMX, minimal stored-procedure mapping | + +3. **For maintenance work** — keep EF6 stable: + - use repository + unit of work patterns to isolate data access (see references/patterns.md) + - prefer `DbContext` over `ObjectContext` for new code + - use `AsNoTracking()` for read-only queries + - configure concurrency tokens with `[ConcurrencyCheck]` or `IsRowVersion()` +4. **For migration work** — validate each slice: + - map EF6 features to EF Core equivalents (see references/migration.md) + - migrate one bounded context at a time, not the entire data layer + - run integration tests against the real database provider, not InMemory + - verify: `dotnet ef migrations add` succeeds, queries produce equivalent results, lazy loading behavior matches expectations +5. **Do not promise EF Core features to EF6 codebases** — EF6 is stable and supported but not on the innovation path. Keep expectations realistic. + +```mermaid +flowchart LR + A["Audit EF6 usage"] --> B{"EDMX or complex mappings?"} + B -->|Yes| C["High migration cost — consider keeping EF6"] + B -->|No| D["Evaluate EF Core migration"] + D --> E["Migrate one context at a time"] + E --> F["Integration test against real DB"] + C --> G["Modernize runtime only"] + F --> H["Validate query equivalence"] + G --> H +``` ## Deliver -- realistic EF6 maintenance or migration guidance +- realistic EF6 maintenance or migration guidance based on actual codebase audit - clear separation between runtime upgrade and ORM upgrade work +- bounded migration slices with concrete validation checkpoints - reduced risk for legacy data access changes ## Validate -- migration assumptions are backed by real feature usage -- EF6-only features are identified early -- the proposed path avoids avoidable churn +- EF6 feature inventory is complete before migration planning starts +- migration assumptions are backed by real feature usage, not guesses +- EF6-only features (EDMX, spatial types, ObjectContext patterns) are identified early +- integration tests run against the real database provider, not mocks or InMemory +- the proposed path avoids unnecessary churn in stable data access code ## References -- [EF6 to EF Core Migration Guide](references/migration.md) - decision framework, migration approaches, feature mapping, and common pitfalls when moving from EF6 to EF Core -- [EF6 Maintenance Patterns](references/patterns.md) - repository and unit of work patterns, query optimization, concurrency handling, auditing, and testing strategies for EF6 codebases +- references/migration.md - decision framework, migration approaches, EF6-to-EF Core feature mapping, and common pitfalls +- references/patterns.md - repository and unit of work patterns, query optimization, concurrency handling, auditing, and testing strategies for EF6 codebases diff --git a/skills/dotnet-managedcode-orleans-graph/SKILL.md b/skills/dotnet-managedcode-orleans-graph/SKILL.md index 6f3f333..be3ea99 100644 --- a/skills/dotnet-managedcode-orleans-graph/SKILL.md +++ b/skills/dotnet-managedcode-orleans-graph/SKILL.md @@ -1,8 +1,8 @@ --- name: dotnet-managedcode-orleans-graph -version: "1.0.0" -category: "Distributed" -description: "Use ManagedCode.Orleans.Graph when a distributed .NET application models graph-oriented relationships or traversal logic on top of Orleans grains and graph-aware integration patterns." +version: "1.0.1" +category: "Data, Distributed, and AI" +description: "Integrate ManagedCode.Orleans.Graph into an Orleans-based .NET application for graph-oriented relationships, edge management, and traversal logic on top of Orleans grains. Use when the application models graph structures in a distributed Orleans system." compatibility: "Requires a .NET application that integrates ManagedCode.Orleans.Graph or evaluates graph-style modeling with Orleans." --- @@ -11,36 +11,83 @@ compatibility: "Requires a .NET application that integrates ManagedCode.Orleans. ## Trigger On - integrating `ManagedCode.Orleans.Graph` into an Orleans-based system -- modeling graph relationships, edges, or traversal behavior with Orleans -- reviewing graph-oriented distributed workflows -- documenting how graph operations fit into an Orleans application +- modeling graph relationships, edges, or traversal behavior with Orleans grains +- reviewing graph-oriented distributed workflows on top of Orleans +- deciding whether a graph abstraction is the right fit vs relational modeling ## Workflow -1. Confirm the application really has graph-style relationships or traversal needs. -2. Identify which graph concerns belong in the library integration: - - node representation - - relationship management - - traversal or lookup patterns -3. Keep Orleans runtime concerns explicit and avoid disguising a normal CRUD model as a graph problem. -4. Document how graph operations interact with grain identity, persistence, and distributed execution. -5. Validate the actual traversal or relationship scenarios the application depends on. +1. **Install the library:** + ```bash + dotnet add package ManagedCode.Orleans.Graph + ``` +2. **Confirm the application has a real graph problem** — node-to-node relationships, directed/undirected edges, or traversal queries. If the data is tabular or hierarchical, prefer standard Orleans grain patterns instead. +3. **Model graph entities as grains:** + - nodes map to grain identities + - edges represent relationships between grains + - traversal operations query across grain boundaries +4. **Implement graph operations:** + ```csharp + // Define a graph grain interface + public interface IGraphGrain : IGrainWithStringKey + { + Task AddEdge(string targetId, string edgeType); + Task> GetNeighbors(string edgeType); + Task HasEdge(string targetId, string edgeType); + Task RemoveEdge(string targetId, string edgeType); + } + ``` +5. **Keep Orleans runtime concerns explicit:** + - grain identity determines the node identity + - persistence provider stores edge state + - grain activation lifecycle affects traversal latency +6. **Add traversal logic for multi-hop queries:** + ```csharp + // Breadth-first traversal across grains + public async Task> TraverseAsync( + IGrainFactory grainFactory, string startId, string edgeType, int maxDepth) + { + var visited = new HashSet(); + var queue = new Queue<(string Id, int Depth)>(); + queue.Enqueue((startId, 0)); + + while (queue.Count > 0) + { + var (currentId, depth) = queue.Dequeue(); + if (!visited.Add(currentId) || depth >= maxDepth) continue; + + var grain = grainFactory.GetGrain(currentId); + var neighbors = await grain.GetNeighbors(edgeType); + foreach (var neighbor in neighbors) + queue.Enqueue((neighbor, depth + 1)); + } + return visited.ToList(); + } + ``` +7. **Validate** that traversal and relationship operations work against real Orleans clusters, not only unit tests with mock grain factories. ```mermaid flowchart LR - A["Application graph request"] --> B["ManagedCode.Orleans.Graph integration"] - B --> C["Orleans grain graph model"] - C --> D["Traversal, relationship, or query result"] + A["Graph request"] --> B["Resolve node grain"] + B --> C["Query edges from grain state"] + C --> D{"Traversal needed?"} + D -->|Yes| E["Multi-hop grain calls"] + D -->|No| F["Return direct neighbors"] + E --> G["Aggregate results"] + F --> G ``` ## Deliver -- guidance on when Orleans.Graph is the right abstraction -- recommendations for keeping graph modeling explicit -- verification expectations for the graph flows the application actually runs +- concrete guidance on when Orleans.Graph is the right abstraction vs standard grain patterns +- graph grain interface patterns with edge management +- traversal implementation that respects Orleans distributed execution +- verification expectations for real graph flows ## Validate -- the application has a real graph problem, not a generic relational one +- the application has a genuine graph problem, not a generic relational one - graph integration does not blur grain identity and traversal concerns -- validation covers real traversal or relationship flows, not only setup code +- edge persistence is configured for the correct Orleans storage provider +- traversal operations are tested against a real Orleans cluster, not only mocks +- multi-hop queries have bounded depth to prevent runaway grain activations diff --git a/skills/dotnet-mcaf/SKILL.md b/skills/dotnet-mcaf/SKILL.md index 566bcaf..f924e63 100644 --- a/skills/dotnet-mcaf/SKILL.md +++ b/skills/dotnet-mcaf/SKILL.md @@ -1,8 +1,8 @@ --- name: dotnet-mcaf -version: "1.2.0" +version: "1.2.1" category: "Core" -description: "Adopt MCAF alongside the dotnet-skills catalog with the right AGENTS.md layout, repo-native docs, skill installation flow, verification rules, and non-trivial task workflow." +description: "Adopt MCAF governance in a .NET repository with the right AGENTS.md layout, repo-native docs, skill installation, verification rules, and non-trivial task workflow. Use when bootstrapping or updating MCAF alongside the dotnet-skills catalog." compatibility: "Best for repositories that want MCAF governance and also use dotnet-skills for actual .NET implementation work." --- @@ -12,81 +12,78 @@ compatibility: "Best for repositories that want MCAF governance and also use dot - bootstrapping MCAF in a new or existing repository that also contains `.NET` work - updating root or project-local `AGENTS.md` files to follow a durable repo workflow -- deciding which MCAF skills and `dotnet-*` skills a solution should install together +- deciding which MCAF governance skills and `dotnet-*` implementation skills to install together - organizing repo-native docs for architecture, features, ADRs, testing, development, and operations -- aligning AI-agent workflow with explicit build, test, format, analyze, and coverage commands ## Workflow -1. Treat the canonical bootstrap surface as URL-first: +1. Start from the canonical bootstrap surface: - tutorial: `https://mcaf.managed-code.com/tutorial` - concepts: `https://mcaf.managed-code.com/` - public MCAF skills: `https://mcaf.managed-code.com/skills` -2. Place root `AGENTS.md` at the repository or solution root. Add project-local `AGENTS.md` files when the `.NET` solution has multiple projects or bounded modules with stricter local rules. -3. Keep MCAF bootstrap small and repo-native: - - durable instructions in `AGENTS.md` - - durable engineering docs in the repository - - workflow details in skills, references, and repo docs instead of chat memory -4. Treat MCAF as its own skill catalog, not one monolithic rule file. This catalog now mirrors the net-new MCAF governance surfaces as dedicated `dotnet-mcaf-*` skills while keeping clear boundaries against overlapping `dotnet-*` implementation skills. -5. Route to the narrowest local MCAF skill once the governance problem is clear: - - delivery workflow and feedback loops: `dotnet-mcaf-agile-delivery` - - developer onboarding and local inner loop: `dotnet-mcaf-devex` - - durable docs structure and source-of-truth placement: `dotnet-mcaf-documentation` - - executable feature behaviour docs: `dotnet-mcaf-feature-spec` - - human review sequencing for large AI-generated drops: `dotnet-mcaf-human-review-planning` - - ML/AI product delivery process: `dotnet-mcaf-ml-ai-delivery` - - explicit quality attributes and trade-offs: `dotnet-mcaf-nfr` - - branch, merge, and release hygiene: `dotnet-mcaf-source-control` - - design-system, accessibility, and front-end direction: `dotnet-mcaf-ui-ux` -6. Expect partial conceptual overlap with existing `dotnet-*` skills. Keep MCAF for repo governance, documentation, delivery, and cross-cutting process policy. Keep `dotnet-*` skills for framework-specific implementation, .NET-specific quality tooling, and concrete code changes. Use the overlap map in `references/skill-map.md` before adding duplicate surfaces. -7. For `.NET` repositories, install the local MCAF governance mirrors from this catalog and install the narrow framework implementation skills from the same catalog. When the upstream MCAF catalog evolves faster than the local mirror, refer back to `https://mcaf.managed-code.com/` for the canonical source and update path. -8. Keep documentation explicit enough for direct implementation: - - `docs/Architecture.md` - - `docs/Features/` - - `docs/ADR/` - - `docs/Testing/` - - `docs/Development/` - - `docs/Operations/` -9. Encode the non-trivial task flow directly in `AGENTS.md`: root-level `.brainstorm.md`, then `.plan.md`, then implementation and validation. -10. Treat verification as part of done. The change is not complete until the full repo-defined quality pass is green, including tests, analyzers, formatters, coverage, and any architecture or security gates the repo configured. +2. Place root `AGENTS.md` at the repository or solution root. +3. Add project-local `AGENTS.md` only when the solution has multiple projects with genuinely different local rules. +4. Install MCAF governance skills (`dotnet-mcaf-*`) for process areas and `dotnet-*` implementation skills for framework work. Check `references/skill-map.md` for overlap before adding duplicate surfaces. +5. Route to the narrowest MCAF skill once the governance concern is clear: -## Architecture + | Concern | Skill | + |---------|-------| + | Delivery workflow and feedback loops | `dotnet-mcaf-agile-delivery` | + | Developer onboarding and local inner loop | `dotnet-mcaf-devex` | + | Durable docs structure and source-of-truth placement | `dotnet-mcaf-documentation` | + | Executable feature behaviour docs | `dotnet-mcaf-feature-spec` | + | Human review for large AI-generated drops | `dotnet-mcaf-human-review-planning` | + | ML/AI product delivery process | `dotnet-mcaf-ml-ai-delivery` | + | Explicit quality attributes and trade-offs | `dotnet-mcaf-nfr` | + | Branch, merge, and release hygiene | `dotnet-mcaf-source-control` | + | Design-system, accessibility, front-end direction | `dotnet-mcaf-ui-ux` | + +6. Scaffold repo-native documentation: + ``` + docs/ + ├── Architecture.md + ├── Features/ + ├── ADR/ + ├── Testing/ + ├── Development/ + └── Operations/ + ``` +7. Encode the non-trivial task flow in `AGENTS.md`: `.brainstorm.md` then `.plan.md` then implementation and validation. +8. Treat verification as part of done: tests, analyzers, formatters, coverage, and any architecture or security gates the repo configured. ```mermaid flowchart LR - A["Adopt MCAF in a repo with .NET work"] --> B["Root AGENTS.md"] - B --> C{"Multi-project solution?"} - C -->|Yes| D["Project-local AGENTS.md files"] - C -->|No| E["Keep root policy only"] - B --> F["Install local dotnet-mcaf-* governance skills"] - B --> G["Install local dotnet-* implementation skills"] - D --> H["Document local boundaries and commands"] + A["Adopt MCAF"] --> B["Root AGENTS.md"] + B --> C{"Multi-project?"} + C -->|Yes| D["Project-local AGENTS.md"] + C -->|No| E["Root policy only"] + B --> F["Install mcaf-* governance skills"] + B --> G["Install dotnet-* implementation skills"] + D --> H["Document boundaries and commands"] E --> H - F --> I["Repo-native docs and workflow scaffolds"] - G --> J["Stack-specific .NET implementation guidance"] - H --> K["Run repo-defined quality pass"] + F --> I["Repo-native docs scaffolds"] + G --> J[".NET implementation guidance"] + H --> K["Run full quality pass"] I --> K J --> K ``` ## Deliver -- a repository-ready MCAF adoption shape for the solution -- clear root and local `AGENTS.md` responsibilities -- the right split between overlapping `mcaf-*` governance skills and `dotnet-*` implementation skills -- local installable MCAF governance skills for the net-new process areas that this catalog did not cover before -- explicit repo docs and verification expectations instead of chat-only instructions +- repository-ready MCAF adoption with clear root and local `AGENTS.md` responsibilities +- correct split between `mcaf-*` governance and `dotnet-*` implementation skills +- repo-native docs and verification expectations instead of chat-only instructions ## Validate - root `AGENTS.md` exists at the repository or solution root -- project-local `AGENTS.md` files exist where the solution actually needs stricter local rules -- the repo documents exact `.NET` build, test, formatting, analyzer, and coverage commands -- durable docs exist for architecture and behavior, not only inline comments or chat context -- non-trivial work requires the brainstorm-to-plan flow before implementation -- the full relevant quality pass is part of done, not only a narrow happy-path test run +- project-local `AGENTS.md` files exist only where genuinely needed +- repo documents exact build, test, format, analyze, and coverage commands +- durable docs exist for architecture and behavior, not only inline comments +- non-trivial work follows the brainstorm-to-plan flow before implementation +- the full quality pass is part of done, not only a narrow happy-path test run ## References -- [references/adoption.md](references/adoption.md) - Canonical MCAF entry points, bootstrap rules for repos that also use dotnet-skills, and the local-mirror boundary between MCAF governance skills and `.NET` implementation skills -- [references/skill-map.md](references/skill-map.md) - Current MCAF catalog map, including the locally mirrored `dotnet-mcaf-*` skills and the overlap-vs-new split so teams can route precisely instead of treating MCAF as a single blob +- references/adoption.md - canonical MCAF entry points, bootstrap rules, and the local-mirror boundary between governance and implementation skills +- references/skill-map.md - MCAF catalog map with overlap-vs-new split for precise routing diff --git a/skills/dotnet-winforms/SKILL.md b/skills/dotnet-winforms/SKILL.md index d64d801..f2341f9 100644 --- a/skills/dotnet-winforms/SKILL.md +++ b/skills/dotnet-winforms/SKILL.md @@ -1,8 +1,8 @@ --- name: dotnet-winforms -version: "1.0.0" -category: "Desktop" -description: "Build, maintain, or modernize Windows Forms applications with practical guidance on designer-driven UI, event handling, data binding, and migration to modern .NET." +version: "1.0.1" +category: "Desktop and Mobile" +description: "Build, maintain, or modernize Windows Forms applications with practical guidance on designer-driven UI, event handling, data binding, MVP separation, and migration to modern .NET. Use when working on WinForms projects or migrating from .NET Framework." compatibility: "Requires a Windows Forms project on .NET or .NET Framework." --- @@ -15,537 +15,83 @@ compatibility: "Requires a Windows Forms project on .NET or .NET Framework." - cleaning up oversized form code or designer coupling - implementing data binding, validation, or control customization -## Documentation - -- [Windows Forms Overview](https://learn.microsoft.com/en-us/dotnet/desktop/winforms/overview/) -- [What's New in Windows Forms](https://learn.microsoft.com/en-us/dotnet/desktop/winforms/whats-new/) -- [Data Binding Overview](https://learn.microsoft.com/en-us/dotnet/desktop/winforms/controls/how-to-bind-a-windows-forms-control-to-a-type) -- [Migration Guide](https://learn.microsoft.com/en-us/dotnet/desktop/winforms/migration/) -- [Controls Reference](https://learn.microsoft.com/en-us/dotnet/desktop/winforms/controls/) - -### References - -- [patterns.md](references/patterns.md) - WinForms architectural patterns (MVP, MVVM, Passive View), data binding patterns, validation patterns, form communication, and threading patterns -- [migration.md](references/migration.md) - Step-by-step migration guide from .NET Framework to modern .NET, common issues, deployment options, and gradual migration strategies - ## Workflow -1. **Respect designer boundaries** — avoid editing generated `.Designer.cs` code directly -2. **Separate business logic** — forms should orchestrate, not contain business rules -3. **Use consistent naming** — control naming and layout should be predictable -4. **Consider MVP/MVVM patterns** — even WinForms benefits from separation -5. **Validate at runtime** — designer success alone proves very little -6. **Modernize incrementally** — choose better structure before rewriting - -## Project Structure - -``` -MyWinFormsApp/ -├── MyWinFormsApp/ -│ ├── Program.cs # Application entry -│ ├── Forms/ # Form classes -│ │ ├── MainForm.cs -│ │ └── MainForm.Designer.cs -│ ├── Presenters/ # MVP Presenters or ViewModels -│ ├── Models/ # Domain models -│ ├── Services/ # Business logic -│ ├── Controls/ # Custom user controls -│ └── Resources/ # Images, strings, etc. -└── MyWinFormsApp.Tests/ -``` - -## MVP Pattern (Model-View-Presenter) - -### View Interface -```csharp -public interface ICustomerView -{ - string CustomerName { get; set; } - string CustomerEmail { get; set; } - BindingSource CustomersBindingSource { get; } - - event EventHandler LoadRequested; - event EventHandler SaveRequested; - event EventHandler CustomerSelected; - - void ShowError(string message); - void ShowSuccess(string message); -} -``` - -### Presenter -```csharp -public class CustomerPresenter -{ - private readonly ICustomerView _view; - private readonly ICustomerService _service; - - public CustomerPresenter(ICustomerView view, ICustomerService service) - { - _view = view; - _service = service; - - _view.LoadRequested += OnLoadRequested; - _view.SaveRequested += OnSaveRequested; - _view.CustomerSelected += OnCustomerSelected; - } - - private async void OnLoadRequested(object? sender, EventArgs e) - { - try - { - var customers = await _service.GetAllAsync(); - _view.CustomersBindingSource.DataSource = customers; - } - catch (Exception ex) - { - _view.ShowError($"Failed to load: {ex.Message}"); - } - } - - private async void OnSaveRequested(object? sender, EventArgs e) - { - try - { - var customer = new Customer - { - Name = _view.CustomerName, - Email = _view.CustomerEmail - }; - await _service.SaveAsync(customer); - _view.ShowSuccess("Customer saved successfully"); - } - catch (Exception ex) - { - _view.ShowError($"Failed to save: {ex.Message}"); - } - } - - private async void OnCustomerSelected(object? sender, int customerId) - { - var customer = await _service.GetByIdAsync(customerId); - if (customer != null) - { - _view.CustomerName = customer.Name; - _view.CustomerEmail = customer.Email; - } - } -} -``` - -### Form Implementation -```csharp -public partial class CustomerForm : Form, ICustomerView -{ - private readonly CustomerPresenter _presenter; - public BindingSource CustomersBindingSource { get; } = new(); - - public string CustomerName - { - get => txtName.Text; - set => txtName.Text = value; - } - - public string CustomerEmail - { - get => txtEmail.Text; - set => txtEmail.Text = value; - } - - public event EventHandler? LoadRequested; - public event EventHandler? SaveRequested; - public event EventHandler? CustomerSelected; - - public CustomerForm(ICustomerService service) - { - InitializeComponent(); - _presenter = new CustomerPresenter(this, service); - - dgvCustomers.DataSource = CustomersBindingSource; - dgvCustomers.SelectionChanged += (s, e) => - { - if (dgvCustomers.CurrentRow?.DataBoundItem is Customer c) - { - CustomerSelected?.Invoke(this, c.Id); - } - }; - } - - private void CustomerForm_Load(object sender, EventArgs e) - => LoadRequested?.Invoke(this, EventArgs.Empty); - - private void btnSave_Click(object sender, EventArgs e) - => SaveRequested?.Invoke(this, EventArgs.Empty); - - public void ShowError(string message) - => MessageBox.Show(message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); - - public void ShowSuccess(string message) - => MessageBox.Show(message, "Success", MessageBoxButtons.OK, MessageBoxIcon.Information); -} -``` - -## Dependency Injection - -```csharp -internal static class Program -{ - [STAThread] - static void Main() - { - ApplicationConfiguration.Initialize(); - - var services = new ServiceCollection(); - ConfigureServices(services); - - using var serviceProvider = services.BuildServiceProvider(); - var mainForm = serviceProvider.GetRequiredService(); - Application.Run(mainForm); - } - - private static void ConfigureServices(IServiceCollection services) - { - // Services - services.AddSingleton(); - services.AddSingleton(); - - // Forms - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - } -} -``` - -## Data Binding - -### BindingSource Pattern -```csharp -public partial class ProductForm : Form -{ - private readonly BindingSource _bindingSource = new(); - private readonly List _products; - - public ProductForm() - { - InitializeComponent(); - SetupBindings(); - } - - private void SetupBindings() - { - // Bind list to grid - dgvProducts.DataSource = _bindingSource; - - // Bind current item to detail controls - txtName.DataBindings.Add("Text", _bindingSource, "Name", - true, DataSourceUpdateMode.OnPropertyChanged); - txtPrice.DataBindings.Add("Text", _bindingSource, "Price", - true, DataSourceUpdateMode.OnPropertyChanged, "0.00"); - - // Enable/disable based on selection - _bindingSource.CurrentChanged += (s, e) => - { - btnEdit.Enabled = _bindingSource.Current != null; - btnDelete.Enabled = _bindingSource.Current != null; - }; - } - - private async Task LoadDataAsync() - { - var products = await _productService.GetAllAsync(); - _bindingSource.DataSource = new BindingList(products.ToList()); - } -} -``` - -### INotifyPropertyChanged Support -```csharp -public class Product : INotifyPropertyChanged -{ - private string _name = string.Empty; - private decimal _price; - - public string Name - { - get => _name; - set - { - if (_name != value) - { - _name = value; - OnPropertyChanged(); - } - } - } - - public decimal Price - { - get => _price; - set - { - if (_price != value) - { - _price = value; - OnPropertyChanged(); - } - } - } - - public event PropertyChangedEventHandler? PropertyChanged; - - protected void OnPropertyChanged([CallerMemberName] string? name = null) - { - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); - } -} -``` - -## Validation - -```csharp -public partial class CustomerForm : Form -{ - private readonly ErrorProvider _errorProvider = new(); - - private void txtEmail_Validating(object sender, CancelEventArgs e) - { - if (!IsValidEmail(txtEmail.Text)) - { - _errorProvider.SetError(txtEmail, "Invalid email address"); - e.Cancel = true; - } - else - { - _errorProvider.SetError(txtEmail, string.Empty); - } - } - - private void txtName_Validating(object sender, CancelEventArgs e) - { - if (string.IsNullOrWhiteSpace(txtName.Text)) - { - _errorProvider.SetError(txtName, "Name is required"); - e.Cancel = true; - } - else - { - _errorProvider.SetError(txtName, string.Empty); - } - } - - private bool IsValidEmail(string email) - { - return !string.IsNullOrWhiteSpace(email) && - email.Contains('@') && - email.Contains('.'); - } - - private void btnSave_Click(object sender, EventArgs e) - { - if (ValidateChildren(ValidationConstraints.Enabled)) - { - // All validations passed - SaveCustomer(); - } - } -} -``` - -## Async Operations - -```csharp -public partial class DataForm : Form -{ - // Good: Use async/await properly - private async void btnLoad_Click(object sender, EventArgs e) - { - btnLoad.Enabled = false; - progressBar.Visible = true; - - try - { - var data = await LoadDataAsync(); - dgvData.DataSource = data; - } - catch (Exception ex) - { - MessageBox.Show($"Error: {ex.Message}"); - } - finally - { - btnLoad.Enabled = true; - progressBar.Visible = false; - } - } - - // Progress reporting - private async void btnProcess_Click(object sender, EventArgs e) - { - var progress = new Progress(percent => - { - progressBar.Value = percent; - lblStatus.Text = $"Processing: {percent}%"; - }); - - await ProcessDataAsync(progress); - } - - private async Task ProcessDataAsync(IProgress progress) - { - for (int i = 0; i <= 100; i += 10) - { - await Task.Delay(100); - progress.Report(i); - } - } -} -``` - -## .NET 8+ Features - -```csharp -// Button commands (.NET 8+) -public partial class ModernForm : Form -{ - private readonly ICommand _saveCommand; - - public ModernForm() - { - InitializeComponent(); - - _saveCommand = new RelayCommand( - execute: _ => Save(), - canExecute: _ => CanSave()); - - // Bind command to button - btnSave.Command = _saveCommand; - } - - private bool CanSave() => !string.IsNullOrEmpty(txtName.Text); - private void Save() { /* save logic */ } -} - -// Modern system icons (.NET 8+) -var infoIcon = SystemIcons.GetStockIcon(StockIconId.Info, StockIconOptions.Large); -pictureBox.Image = infoIcon.ToBitmap(); -``` - -## Anti-Patterns to Avoid - -| Anti-Pattern | Why It's Bad | Better Approach | -|--------------|--------------|-----------------| -| Business logic in forms | Hard to test, tight coupling | Use MVP/Presenter pattern | -| Editing Designer.cs | Changes lost on regeneration | Modify in Form.cs only | -| Synchronous I/O in events | UI freezes | Use async/await | -| Giant form classes | Unmaintainable | Split into user controls | -| Direct database calls in forms | Coupling, hard to test | Use service layer | -| Ignoring validation events | Silent failures | Use ErrorProvider, Validating | -| Manual control population | Error-prone | Use data binding | -| Nested event handler logic | Spaghetti code | Extract to methods/services | - -## Best Practices - -1. **Use User Controls for reusable UI:** +1. **Respect designer boundaries** — never edit `.Designer.cs` directly; changes are lost on regeneration. +2. **Separate business logic from forms** — use MVP (Model-View-Presenter) pattern. Forms orchestrate UI; presenters contain logic; services handle data access. ```csharp - public partial class AddressControl : UserControl + // View interface — forms implement this + public interface ICustomerView { - public string Street { get; set; } - public string City { get; set; } - public string ZipCode { get; set; } + string CustomerName { get; set; } + event EventHandler SaveRequested; + void ShowError(string message); } - ``` -2. **Implement proper disposal:** - ```csharp - protected override void Dispose(bool disposing) + // Presenter — testable without UI + public class CustomerPresenter { - if (disposing) + private readonly ICustomerView _view; + private readonly ICustomerService _service; + public CustomerPresenter(ICustomerView view, ICustomerService service) { - _bindingSource?.Dispose(); - _errorProvider?.Dispose(); - components?.Dispose(); + _view = view; + _service = service; + _view.SaveRequested += async (s, e) => + { + try { await _service.SaveAsync(_view.CustomerName); } + catch (Exception ex) { _view.ShowError(ex.Message); } + }; } - base.Dispose(disposing); } ``` - -3. **Use BindingList for observable collections:** +3. **Use DI from Program.cs** (.NET 6+): ```csharp - var bindingList = new BindingList(products); - bindingList.ListChanged += (s, e) => UpdateStatus(); - dgvProducts.DataSource = bindingList; + var services = new ServiceCollection(); + services.AddSingleton(); + services.AddTransient(); + using var sp = services.BuildServiceProvider(); + Application.Run(sp.GetRequiredService()); ``` +4. **Use data binding** via `BindingSource` and `INotifyPropertyChanged` instead of manual control population. See references/patterns.md for complete binding patterns. +5. **Use async/await** for I/O operations — disable controls during loading, use `Progress` for progress reporting. Never block the UI thread. +6. **Validate with `ErrorProvider`** and the `Validating` event. Call `ValidateChildren()` before save operations. +7. **Modernize incrementally** — prefer better structure over big-bang rewrites. Use .NET 8+ features (button commands, stock icons) when available. + +```mermaid +flowchart LR + A["Form event"] --> B["Presenter handles logic"] + B --> C["Service layer / data access"] + C --> D["Update view via interface"] + D --> E["Validate and display results"] +``` -4. **Handle high-DPI properly:** - ```xml - - - - true/pm - PerMonitorV2 - - - ``` - -5. **Configure application settings properly:** - ```csharp - ApplicationConfiguration.Initialize(); // .NET 6+ - Application.SetHighDpiMode(HighDpiMode.PerMonitorV2); - Application.EnableVisualStyles(); - Application.SetCompatibleTextRenderingDefault(false); - ``` - -## Testing - -```csharp -[Fact] -public async Task Presenter_LoadsCustomers_OnLoadRequested() -{ - var mockView = new Mock(); - var mockService = new Mock(); - var bindingSource = new BindingSource(); - - mockView.Setup(v => v.CustomersBindingSource).Returns(bindingSource); - mockService.Setup(s => s.GetAllAsync()) - .ReturnsAsync(new[] { new Customer { Name = "Test" } }); - - var presenter = new CustomerPresenter(mockView.Object, mockService.Object); - - mockView.Raise(v => v.LoadRequested += null, EventArgs.Empty); - - await Task.Delay(100); // Allow async completion - Assert.Single((IList)bindingSource.DataSource); -} - -[Fact] -public void Presenter_ShowsError_OnLoadFailure() -{ - var mockView = new Mock(); - var mockService = new Mock(); - var bindingSource = new BindingSource(); - - mockView.Setup(v => v.CustomersBindingSource).Returns(bindingSource); - mockService.Setup(s => s.GetAllAsync()).ThrowsAsync(new Exception("DB Error")); - - var presenter = new CustomerPresenter(mockView.Object, mockService.Object); - mockView.Raise(v => v.LoadRequested += null, EventArgs.Empty); +## Key Decisions - mockView.Verify(v => v.ShowError(It.IsAny()), Times.Once); -} -``` +| Decision | Guidance | +|----------|----------| +| MVP vs MVVM | Prefer MVP for WinForms — simpler with event-driven model | +| BindingSource vs manual | Always prefer BindingSource for list/detail binding | +| Sync vs async I/O | Always async — use `async void` only for event handlers | +| Custom controls | Extract reusable `UserControl` when form grows beyond ~300 lines | +| .NET Framework → .NET | Use the official migration guide; validate designer compatibility first | ## Deliver -- less brittle form code and event handling -- better separation between UI and business logic -- pragmatic modernization guidance for WinForms-heavy apps +- less brittle form code with clear UI/logic separation - MVP pattern with testable presenters +- pragmatic modernization guidance for WinForms-heavy apps +- data binding and validation patterns that reduce manual wiring ## Validate -- designer files stay stable +- designer files stay stable and are not hand-edited - forms are not acting as the application service layer -- Windows-only runtime behavior is tested -- async operations do not block the UI -- validation is implemented consistently +- async operations do not block the UI thread +- validation is implemented consistently with ErrorProvider +- Windows-only runtime behavior is tested on target + +## References + +- references/patterns.md - WinForms architectural patterns (MVP, MVVM, Passive View), data binding, validation, form communication, threading, DI setup, and .NET 8+ features +- references/migration.md - step-by-step migration from .NET Framework to modern .NET, common issues, deployment options, and gradual migration strategies diff --git a/skills/dotnet-winui/SKILL.md b/skills/dotnet-winui/SKILL.md index a2b96c4..4b508d7 100644 --- a/skills/dotnet-winui/SKILL.md +++ b/skills/dotnet-winui/SKILL.md @@ -1,8 +1,8 @@ --- name: dotnet-winui -version: "1.0.0" -category: "Desktop" -description: "Build or review WinUI 3 applications with the Windows App SDK, modern Windows desktop patterns, packaging decisions, and interop boundaries with other .NET stacks." +version: "1.0.1" +category: "Desktop and Mobile" +description: "Build or review WinUI 3 applications with the Windows App SDK, including MVVM patterns, packaging decisions, navigation, theming, windowing, and interop boundaries with other .NET stacks. Use when building modern Windows-native desktop UI." compatibility: "Requires a WinUI 3, Windows App SDK, or MAUI-on-Windows integration scenario." --- @@ -15,547 +15,81 @@ compatibility: "Requires a WinUI 3, Windows App SDK, or MAUI-on-Windows integrat - deciding between WinUI, WPF, WinForms, and MAUI for Windows work - implementing MVVM patterns in Windows App SDK applications -## Documentation - -- [WinUI 3 Overview](https://learn.microsoft.com/en-us/windows/apps/winui/winui3/) -- [Create Your First WinUI 3 App](https://learn.microsoft.com/en-us/windows/apps/winui/winui3/create-your-first-winui3-app) -- [Windows App SDK Overview](https://learn.microsoft.com/en-us/windows/apps/windows-app-sdk/) -- [MVVM Toolkit with WinUI](https://learn.microsoft.com/en-us/windows/apps/tutorials/winui-mvvm-toolkit/intro) -- [Controls Reference](https://learn.microsoft.com/en-us/windows/apps/design/controls/) - -### References - -- [patterns.md](references/patterns.md) - WinUI 3 patterns including MVVM, navigation, services, and Windows App SDK integration -- [anti-patterns.md](references/anti-patterns.md) - Common WinUI mistakes and how to avoid them - ## Workflow -1. **Confirm WinUI is the right choice** — use when modern Windows-native UI and Windows App SDK capabilities are needed -2. **Choose packaging model** — packaged (MSIX) vs unpackaged differ materially -3. **Apply MVVM pattern** — keep views dumb, logic in ViewModels -4. **Use Fluent Design** — leverage modern Windows 11 styling -5. **Handle Windows App SDK features** — windowing, app lifecycle, notifications -6. **Validate on Windows targets** — behavior depends on runtime environment - -## Project Structure - -``` -MyWinUIApp/ -├── MyWinUIApp/ -│ ├── App.xaml # Application entry -│ ├── MainWindow.xaml # Main window -│ ├── Views/ # XAML pages -│ ├── ViewModels/ # MVVM ViewModels -│ ├── Models/ # Domain models -│ ├── Services/ # Business logic -│ ├── Helpers/ # Utility classes -│ └── Assets/ # Images, fonts -├── MyWinUIApp (Package)/ # MSIX packaging project (if packaged) -└── MyWinUIApp.Tests/ -``` - -## MVVM Pattern - -### ViewModel with MVVM Toolkit -```csharp -public partial class ProductsViewModel : ObservableObject -{ - private readonly IProductService _productService; - private readonly INavigationService _navigationService; - - [ObservableProperty] - private ObservableCollection _products = []; - - [ObservableProperty] - [NotifyCanExecuteChangedFor(nameof(DeleteCommand))] - private Product? _selectedProduct; - - [ObservableProperty] - [NotifyCanExecuteChangedFor(nameof(LoadProductsCommand))] - private bool _isLoading; - - public ProductsViewModel(IProductService productService, INavigationService navigationService) - { - _productService = productService; - _navigationService = navigationService; - } - - [RelayCommand(CanExecute = nameof(CanLoadProducts))] - private async Task LoadProductsAsync() - { - IsLoading = true; - try - { - var items = await _productService.GetAllAsync(); - Products = new ObservableCollection(items); - } - finally - { - IsLoading = false; - } - } - - private bool CanLoadProducts() => !IsLoading; - - [RelayCommand(CanExecute = nameof(CanDelete))] - private async Task DeleteAsync() - { - if (SelectedProduct is null) return; - await _productService.DeleteAsync(SelectedProduct.Id); - Products.Remove(SelectedProduct); - SelectedProduct = null; - } - - private bool CanDelete() => SelectedProduct is not null; - - [RelayCommand] - private void NavigateToDetail(Product product) - { - _navigationService.NavigateTo(product); - } -} -``` - -### View Binding -```xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -``` - -### Code-Behind with x:Bind -```csharp -public sealed partial class ProductsPage : Page -{ - public ProductsViewModel ViewModel { get; } - - public ProductsPage() - { - ViewModel = App.GetService(); - InitializeComponent(); - } - - protected override async void OnNavigatedTo(NavigationEventArgs e) - { - base.OnNavigatedTo(e); - await ViewModel.LoadProductsCommand.ExecuteAsync(null); - } -} -``` - -## Dependency Injection - -```csharp -public partial class App : Application -{ - private static IHost? _host; - - public App() - { - InitializeComponent(); - - _host = Host.CreateDefaultBuilder() - .ConfigureServices((context, services) => - { - // Services - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - - // ViewModels - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - - // Views - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - }) - .Build(); - } - - public static T GetService() where T : class - => _host!.Services.GetRequiredService(); - - protected override void OnLaunched(LaunchActivatedEventArgs args) - { - m_window = GetService(); - m_window.Activate(); - } - - private Window? m_window; -} -``` - -## Navigation Service - -```csharp -public interface INavigationService -{ - bool CanGoBack { get; } - void NavigateTo(object? parameter = null) where TViewModel : class; - void GoBack(); -} - -public class NavigationService : INavigationService -{ - private readonly IServiceProvider _serviceProvider; - private Frame? _frame; - - public NavigationService(IServiceProvider serviceProvider) - { - _serviceProvider = serviceProvider; - } - - public void Initialize(Frame frame) => _frame = frame; - - public bool CanGoBack => _frame?.CanGoBack ?? false; - - public void NavigateTo(object? parameter = null) where TViewModel : class - { - var pageType = GetPageType(); - _frame?.Navigate(pageType, parameter); - } - - public void GoBack() - { - if (_frame?.CanGoBack == true) - { - _frame.GoBack(); - } - } - - private static Type GetPageType() - { - var viewModelName = typeof(TViewModel).Name; - var pageName = viewModelName.Replace("ViewModel", "Page"); - var pageType = Type.GetType($"MyWinUIApp.Views.{pageName}"); - return pageType ?? throw new ArgumentException($"Page not found for {viewModelName}"); - } -} -``` - -## Windowing - -```csharp -public sealed partial class MainWindow : Window -{ - private AppWindow _appWindow; - - public MainWindow() - { - InitializeComponent(); - - // Get AppWindow for advanced windowing - var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(this); - var windowId = Win32Interop.GetWindowIdFromWindow(hWnd); - _appWindow = AppWindow.GetFromWindowId(windowId); - - // Customize title bar - if (AppWindowTitleBar.IsCustomizationSupported()) - { - var titleBar = _appWindow.TitleBar; - titleBar.ExtendsContentIntoTitleBar = true; - titleBar.ButtonBackgroundColor = Colors.Transparent; - titleBar.ButtonInactiveBackgroundColor = Colors.Transparent; - } - - // Set window size and position - _appWindow.Resize(new SizeInt32(1200, 800)); - _appWindow.Move(new PointInt32(100, 100)); - } - - // Center window on screen - private void CenterOnScreen() - { - var displayArea = DisplayArea.GetFromWindowId(_appWindow.Id, DisplayAreaFallback.Primary); - var centerX = (displayArea.WorkArea.Width - _appWindow.Size.Width) / 2; - var centerY = (displayArea.WorkArea.Height - _appWindow.Size.Height) / 2; - _appWindow.Move(new PointInt32(centerX, centerY)); - } -} -``` - -## Theming - -```csharp -public class ThemeService -{ - public void SetTheme(ElementTheme theme) - { - if (App.MainWindow.Content is FrameworkElement rootElement) - { - rootElement.RequestedTheme = theme; - } - } - - public ElementTheme GetCurrentTheme() - { - if (App.MainWindow.Content is FrameworkElement rootElement) - { - return rootElement.RequestedTheme; - } - return ElementTheme.Default; - } -} -``` - -```xml - - - - - - - - - - - - - - -``` - -## Dialogs - -```csharp -public class DialogService : IDialogService -{ - public async Task ShowConfirmationAsync(string title, string message) - { - var dialog = new ContentDialog - { - Title = title, - Content = message, - PrimaryButtonText = "Yes", - CloseButtonText = "No", - DefaultButton = ContentDialogButton.Close, - XamlRoot = App.MainWindow.Content.XamlRoot - }; - - var result = await dialog.ShowAsync(); - return result == ContentDialogResult.Primary; - } - - public async Task ShowErrorAsync(string title, string message) - { - var dialog = new ContentDialog - { - Title = title, - Content = message, - CloseButtonText = "OK", - XamlRoot = App.MainWindow.Content.XamlRoot - }; - - await dialog.ShowAsync(); - } -} -``` - -## Packaging Options - -### Packaged (MSIX) -```xml - - - - - My WinUI App - My Company - - - - - - - - - - - - - - - -``` - -### Unpackaged -```xml - - - - WinExe - net8.0-windows10.0.19041.0 - true - None - - -``` - -## Anti-Patterns to Avoid - -| Anti-Pattern | Why It's Bad | Better Approach | -|--------------|--------------|-----------------| -| Logic in code-behind | Hard to test | Use MVVM with ViewModels | -| Ignoring x:Bind | Poor performance | Use compiled bindings | -| Blocking UI thread | Frozen UI | Use async/await | -| Hardcoded styles | Inconsistent theming | Use resource dictionaries | -| Ignoring packaging choice | Deployment issues | Choose packaged vs unpackaged early | -| Direct service access in views | Tight coupling | Use dependency injection | -| Ignoring XamlRoot | Dialog failures | Always set XamlRoot for dialogs | -| Manual property notifications | Boilerplate, errors | Use MVVM Toolkit attributes | - -## Best Practices - -1. **Use x:Bind for compiled bindings:** +1. **Confirm WinUI is the right choice** — use when modern Windows-native UI, Fluent Design, and Windows App SDK capabilities are needed. For cross-platform, consider MAUI instead. +2. **Choose packaging model early** — packaged (MSIX) vs unpackaged differ materially for deployment, identity, and API access: ```xml - + + None ``` - -2. **Implement proper navigation:** +3. **Apply MVVM pattern** with the MVVM Toolkit — keep views dumb, logic in ViewModels: ```csharp - protected override void OnNavigatedTo(NavigationEventArgs e) + public partial class ProductsViewModel : ObservableObject { - base.OnNavigatedTo(e); - if (e.Parameter is Product product) - { - ViewModel.Initialize(product); - } - } - ``` - -3. **Use InfoBar for notifications:** - ```xml - - ``` + [ObservableProperty] + private ObservableCollection _products = []; -4. **Handle app lifecycle:** - ```csharp - public App() - { - InitializeComponent(); + [ObservableProperty] + [NotifyCanExecuteChangedFor(nameof(DeleteCommand))] + private Product? _selectedProduct; - // Handle suspension - Suspending += (s, e) => + [RelayCommand(CanExecute = nameof(CanDelete))] + private async Task DeleteAsync() { - var deferral = e.SuspendingOperation.GetDeferral(); - // Save state - deferral.Complete(); - }; + if (SelectedProduct is null) return; + await _productService.DeleteAsync(SelectedProduct.Id); + Products.Remove(SelectedProduct); + } + private bool CanDelete() => SelectedProduct is not null; } ``` - -5. **Virtualize large lists:** - ```xml - - ``` - -6. **Use semantic zoom for large datasets:** +4. **Use x:Bind for compiled bindings** — better performance and compile-time checking than `{Binding}`: ```xml - - - - - - - - + ``` +5. **Wire DI through `Host.CreateDefaultBuilder`** — register services, ViewModels, and views. Resolve via `App.GetService()`. +6. **Implement navigation service** — map ViewModels to Pages by convention. See references/patterns.md for the full pattern. +7. **Handle Windows App SDK features** — windowing (AppWindow), custom title bar, app lifecycle, notifications. +8. **Always set `XamlRoot`** when showing ContentDialog — omitting this causes silent failures. +9. **Validate on Windows targets** — behavior depends on runtime, packaging model, and Windows version. + +```mermaid +flowchart LR + A["Choose WinUI"] --> B["Select packaging model"] + B --> C["MVVM + DI setup"] + C --> D["Navigation and views"] + D --> E["Windows App SDK features"] + E --> F["Validate on target runtime"] +``` -## Testing - -```csharp -[Fact] -public async Task LoadProducts_UpdatesCollection() -{ - var mockService = new Mock(); - var mockNavigation = new Mock(); - mockService.Setup(s => s.GetAllAsync()) - .ReturnsAsync(new[] { new Product { Name = "Test" } }); - - var viewModel = new ProductsViewModel(mockService.Object, mockNavigation.Object); - - await viewModel.LoadProductsCommand.ExecuteAsync(null); - - Assert.Single(viewModel.Products); - Assert.Equal("Test", viewModel.Products[0].Name); -} - -[Fact] -public void DeleteCommand_CannotExecute_WhenNoSelection() -{ - var mockService = new Mock(); - var mockNavigation = new Mock(); - var viewModel = new ProductsViewModel(mockService.Object, mockNavigation.Object); - - viewModel.SelectedProduct = null; +## Key Decisions - Assert.False(viewModel.DeleteCommand.CanExecute(null)); -} -``` +| Decision | Guidance | +|----------|----------| +| Packaged vs unpackaged | Packaged (MSIX) for Store, auto-update, and full API access; unpackaged for simpler deployment | +| x:Bind vs Binding | Always prefer x:Bind — compiled, faster, type-safe | +| MVVM Toolkit attributes | Use `[ObservableProperty]`, `[RelayCommand]` to eliminate boilerplate | +| Navigation | Convention-based ViewModel→Page mapping via navigation service | +| Theming | Use `RequestedTheme` on root element; respect system theme by default | ## Deliver - modern Windows UI code with clear platform boundaries - explicit deployment and packaging assumptions -- cleaner interop between shared and Windows-specific layers - MVVM pattern with testable ViewModels +- cleaner interop between shared and Windows-specific layers ## Validate -- WinUI is chosen for a real product reason -- Windows App SDK dependencies are explicit -- packaging and runtime assumptions are tested -- x:Bind is used for compiled bindings -- navigation and dialogs work correctly +- WinUI is chosen for a real product reason, not defaulted to +- Windows App SDK dependencies are explicit in the project file +- packaging and runtime assumptions are tested on target +- x:Bind is used for compiled bindings throughout +- navigation and ContentDialog both work with correct XamlRoot +- custom title bar renders correctly on Windows 10 and 11 + +## References + +- references/patterns.md - WinUI 3 patterns including MVVM, navigation services, DI setup, windowing, theming, dialogs, and lifecycle handling +- references/anti-patterns.md - common WinUI mistakes with explanations and corrections