diff --git a/.ai-team/agents/beast/history-archive.md b/.ai-team/agents/beast/history-archive.md index bc36918b6..148f128a7 100644 --- a/.ai-team/agents/beast/history-archive.md +++ b/.ai-team/agents/beast/history-archive.md @@ -96,3 +96,17 @@ Team update (2026-03-04): EF Core must use 10.0.3 (latest .NET 10) directed by Jeff + +## Archived 2026-03-08 (entries from 2026-03-05 through 2026-03-07) + +### Archived Session Pointers + +- WebFormsPageBase & Page System Docs (2026-03-05) +- Skills Cross-Reference Review 7 files, 16+ fixes (2026-03-06) +- Run 8 Report Enhancement executive pattern established (2026-03-06) +- Run 9 Skill Fixes 6 RF items across 4 skill files (2026-03-07) +- Run 9 RCA Documentation path preservation + CSS verification rules (2026-03-07) + +### Summary (2026-03-05 through 2026-03-07 pre-Run 11) + +WebFormsPageBase docs and Page System rewrite shipped (2026-03-05). Skills cross-reference review found `.ai-team/skills/` drifting behind `migration-toolkit/skills/` both must be updated together. LoginView is a native BWFC component, never replace with AuthorizeView. Executive report pattern established in Run 8: blockquote bottom line -> timeline -> screenshots -> before/after code. Run 9 skill fixes: cookie auth under Interactive Server, minimal API endpoint templates, enhanced navigation guidance, DisableAntiforgery, ListView GroupItemCount. Run 9 RCA: added Static Asset Path Preservation and CSS Reference Verification rules to migration-standards. Key learning: functional tests passing != migration success visual regression is ship-blocking. diff --git a/.ai-team/agents/beast/history.md b/.ai-team/agents/beast/history.md index e31352c77..a2d36037e 100644 --- a/.ai-team/agents/beast/history.md +++ b/.ai-team/agents/beast/history.md @@ -5,33 +5,13 @@ - **Stack:** C#, Blazor, .NET, ASP.NET Web Forms, bUnit, xUnit, MkDocs, Playwright - **Created:** 2026-02-10 -## Learnings - - - - - -### Archived Sessions +## Core Context -- Core Context (2026-02-10 through 2026-02-27) -- Doc Work Summary (2026-02-27 through 2026-03-03) -- Key Team Updates (2026-02-27 through 2026-03-03) -- Migration Report Conventions (2026-03-04) -- Run 5 Benchmark Report (2026-03-05) -- Run 6 Benchmark Report (2026-03-04) -- Render Mode Placement Correction (2026-03-05) + - +Documentation & migration reporting agent. M1–M16 docs shipped (all control categories). Doc structure: title → intro → Features → NOT Supported → syntax → HTML Output → Migration Notes → Examples. mkdocs.yml nav alphabetical within categories. Migration reports: standalone `{project-name}-runNN.md` format, zero-padded. Executive report pattern: blockquote bottom line → timeline → screenshots → before/after code. Skills files in `.ai-team/skills/` and `migration-toolkit/skills/` must be updated together. LoginView is native BWFC — never replace with AuthorizeView. Functional tests passing ≠ migration success — visual regression is ship-blocking. Run 9 RCA rules: Static Asset Path Preservation + CSS Reference Verification. -- WebFormsPageBase & Page System Docs (2026-03-05) -- Skills Cross-Reference Review — 7 files, 16+ fixes (2026-03-06) -- Run 8 Report Enhancement — executive pattern established (2026-03-06) -- Run 9 Skill Fixes — 6 RF items across 4 skill files (2026-03-07) -- Run 9 RCA Documentation — path preservation + CSS verification rules (2026-03-07) - -### Summary (2026-03-05 through 2026-03-07 pre-Run 11) - -WebFormsPageBase docs and Page System rewrite shipped (2026-03-05). Skills cross-reference review found `.ai-team/skills/` drifting behind `migration-toolkit/skills/` — both must be updated together. LoginView is a native BWFC component, never replace with AuthorizeView. Executive report pattern established in Run 8: blockquote bottom line → timeline → screenshots → before/after code. Run 9 skill fixes: cookie auth under Interactive Server, minimal API endpoint templates, enhanced navigation guidance, DisableAntiforgery, ListView GroupItemCount. Run 9 RCA: added Static Asset Path Preservation and CSS Reference Verification rules to migration-standards. Key learning: functional tests passing ≠ migration success — visual regression is ship-blocking. +## Learnings ### Run 10 Failure Report (2026-03-07) @@ -65,3 +45,85 @@ WebFormsPageBase docs and Page System rewrite shipped (2026-03-05). Skills cross Team update (2026-03-08): @using BlazorWebFormsComponents.LoginControls must be in every generated _Imports.razor decided by Cyclops Team update (2026-03-08): Run 12 migration patterns: auth via plain HTML forms with data-enhance=false, dual DbContext, LoginView _userName from cascading auth state decided by Cyclops + +### Documentation Refresh — Runs 8–13 (2026-03-08) + +- **AutomatedMigration.md updated:** Added SSR-as-default section, package version pinning, enhanced navigation (`data-enhance-nav="false"`) guidance, updated pipeline table (Script layer from ~40% to ~60%), added pipeline convergence note (56% → 100% across 13 runs), expanded transform table with JS/CSS detection, placeholder conversion, and static asset copying. +- **6 new run summary pages created:** `docs/migration-tests/wingtiptoys-run{8-13}.md` — each with date, score, render mode, key changes, remaining fixes, and link to full report in dev-docs. +- **migration-tests/README.md rewritten:** Full run history table with all 10 published runs (1-4, 8-13), plus convergence summary section. +- **mkdocs.yml updated:** Added all 6 new run pages, Runs 5-6 (previously missing), and component-coverage.md to the Migration Tests nav section. +- **component-coverage.md created:** Gap analysis of 52 components vs docs. Result: 100% coverage — no gaps found. Sub-components (View, RoleGroup, MenuItem, TreeNode, etc.) are documented within parent pages. +- **Patterns followed:** Run summary pages use consistent table format (metric/value), root cause abbreviations where applicable, and link to full dev-docs report. Followed existing migration-tests page style from Runs 1-4. +- **Key decisions documented:** SSR default render mode, package version pinning, `data-enhance-nav="false"` for non-Blazor endpoints. + +### Run 14 Migration Report (2026-03-08) + +- **Report location:** `docs/migration-tests/wingtiptoys-run14.md` +- **Score:** ✅ 25/25 (third consecutive 100%) +- **Key narrative:** Layer 1 (script) reached zero manual intervention — all 3 Run 13 fixes baked in. Layer 2 still has 3 semantic code-behind fixes that can't be regex-automated. +- **New elements documented:** `-TestMode` switch, 8-component `id` rendering fix, component audit (153 components, 96% coverage), automation ceiling concept (Layer 1 mechanical vs Layer 2 semantic). +- **Format followed:** Matched Run 13 structure — metric table, executive blockquote, what's new, execution details, progression table, lessons learned. Added "Key Insight: The Automation Ceiling" section to capture the Layer 1/Layer 2 boundary discovery. +- **mkdocs.yml updated:** Added Run 14 nav entry between Run 13 and Component Coverage. + +### Run 15 Migration Report (2026-03-08) + +- Run 15 report written to dev-docs/migration-tests/ (not docs/ — internal dev docs) + + Team update (2026-03-08): Layer 2 bulk-extract from known-good commit is default approach until Layer 1 output changes structurally decided by Cyclops + + Team update (2026-03-08): Migration script gained 4 new functions (EnhancedNavDisable, ReadOnlyWarning, LoginStatus conversion, LogoutFormToLink) targets 0 manual fixes decided by Cyclops + + Team update (2026-03-08): Component audit priorities BulletedList/Panel/id-rendering fixes, field column docs, zero-touch migration script decided by Forge + +### Run 16 Migration Report & Toolkit Docs (2026-03-08) + +- **Run 16 report:** `dev-docs/migration-tests/wingtiptoys-run16.md` — fifth consecutive 100% (25/25). First Layer 2 automation attempt. Layer 1 at 2.50s (12% faster). Layer 2 script handles Pattern C (Program.cs) fully, Pattern A (code-behinds) scaffolding, Pattern B (auth) not yet detected. +- **migration-toolkit/README.md:** Updated "Latest" banner to Run 16, added `bwfc-migrate-layer2.ps1` to scripts table, updated pipeline table to reflect "Script + Overlay" for Layer 2, rewrote "What's New" section for Run 16, updated Quick Overview to include Layer 2 step. +- **migration-toolkit/METHODOLOGY.md:** Updated pipeline diagram (COPILOT-ASSISTED → SCRIPT + OVERLAY), updated intelligence/tool table, rewrote Layer 2 section to document 2-script pipeline and 3 patterns (A/B/C), updated time estimates (Layer 1 now ~3s, Layer 2 now ~3 min with agents). +- **migration-toolkit/QUICKSTART.md:** Added Step 4 (Layer 2 script), renumbered Steps 5–10, added warning about Pattern A/B partial automation. +- **Key learning:** Layer 2 documentation must distinguish between "script-automated" and "needs overlay" — the pipeline is no longer binary (automated vs manual). Three-state model: fully automated (Pattern C), partially automated (Pattern A), not yet automated (Pattern B). + + + Team update (2026-03-09): Run 16 complete 25/25 tests, Layer 2 script bugs fixed ( init, TestMode removed), known-good overlay still required. Audit consolidated with updated priorities decided by Cyclops, Forge + +### Migration-Tests Reorganization for Multi-Project Testing (2026-03-09) + +- **Scope:** Rewrote `dev-docs/migration-tests/README.md` to support multiple test projects and include all 16 runs. +- **What changed:** Added Test Projects table (WingtipToys + placeholder for next project), full 16-run history table with L1 time/L1 manual fixes/L2 fixes/score/render mode, Pipeline Evolution section with convergence summary and key milestones, updated Key Conclusions (5 consecutive 100%, Layer 2 automation frontier), "Adding a New Test Project" guidance section, and Report Archive cataloging all 12 old run folders. +- **Naming convention established:** Standalone `{project-name}-runNN.md` is the canonical report format going forward. Old run folders preserved as historical archives. +- **Data added:** Runs 5, 6, 14, 15, 16 were missing from the README — all now included with metrics extracted from their respective report files. Run 7 confirmed non-existent (skipped). +- **Key learning:** The old run folders (runs 1–6, 8–13) contain valuable raw data (screenshots, build output, scan results) that the standalone reports don't capture. Worth keeping as archives but not worth migrating to standalone format. + + Team update (2026-03-08): Three P0 HTML fidelity fixes identified CheckBox needs span wrapper, BaseValidator needs id/class, FormView needs class on table. Audit scored 87%. decided by Forge + + + Team update (2026-03-08): P0 HTML fidelity fixes complete CheckBox span wrapper, BaseValidator id/class, FormView CssClass. 1488 tests pass. decided by Cyclops, Forge + Team update (2026-03-08): Second sample project will be purpose-built 'EventManager' Control Gallery targeting ~12-15 pages with controls WingtipToys doesn't cover decided by Forge + Team update (2026-03-08): ASPX URL rewriting goes in migration-toolkit docs (RewriteOptions.AddRedirect snippet), not BWFC NuGet decided by Forge + +### Genericization of Migration Toolkit (2026-03-09) + +- **Scope:** Removed WingtipToys-specific examples from all skill files and documentation in `migration-toolkit/`. Made examples clearly generic so any Web Forms app (e-commerce, school management, healthcare, etc.) sees applicable patterns. +- **Files modified (8):** + - `skills/migration-standards/SKILL.md` — ListView example: `WingtipToys.Models.Product` → `YourApp.Models.YourEntity`, `ProductContext` → `AppDbContext`, anti-patterns genericized + - `skills/bwfc-migration/SKILL.md` — GridView, ListView, and GroupItemCount examples: `ItemType="WingtipToys.Models.Product"` → `ItemType="YourApp.Models.YourEntity"` + - `skills/bwfc-data-migration/SKILL.md` — Largest change. `ProductContext` → `AppDbContext`, `ProductService` → `EntityService`, `GetProducts` → `GetItems` across EF6, service injection, and SelectMethod mapping sections. ShoppingCart/CartService restructured: generic pattern leads, cart shown as labeled "Example (e-commerce app)" note. Route examples genericized (`ProductRoute` → `DetailRoute`). + - `CHECKLIST.md` — Tracking examples wrapped in `` comments with generic page names + - `copilot-instructions-template.md` — Control notes table and routing table now use `[YourXxx]` template format + - `README.md` — "How Long" section now says "and other sample app migrations" alongside WingtipToys PoC + - `METHODOLOGY.md` — Layer 1 table labeled "WingtipToys PoC", percentages note says "representative, not absolute", time estimates note added, cross-references clarified +- **Identity skill (`bwfc-identity-migration/SKILL.md`):** Verified clean — no WingtipToys-specific references found. +- **Pattern applied:** Generic code examples use `YourApp`, `YourEntity`, `AppDbContext`, `EntityService`, `GetItemsAsync`. Where WingtipToys-specific examples add genuine value (shopping cart session state pattern), kept but labeled explicitly as "> **Example (e-commerce app):**". +- **Principle learned:** Toolkit skill files must use domain-neutral names in primary patterns. Domain-specific examples (e-commerce, healthcare, etc.) are fine as labeled secondary examples but should never be the primary teaching example. Future skill additions should follow the `YourApp`/`YourEntity`/`AppDbContext` convention. +📌 Team update (2026-03-08): Preserve SelectMethod in migration scripts — BWFC supports it natively via SelectHandler. Stop stripping the attribute, add signature-adaptation TODO instead — decided by Forge + +📌 Team update (2026-03-08): WingtipToys hardcoding audit — 23 findings (5 CRITICAL, 3 HIGH, 10 MEDIUM, 5 LOW). Layer 2 entity detection, Program.cs template, and skill files need genericization — decided by Cyclops + +### ViewState Documentation Enhancement (2026-03-09) + +- **Scope:** Rewrote `docs/UtilityFeatures/ViewState.md` from a sparse 45-line doc to a comprehensive utility feature page. Fixed outdated migration guide. +- **ViewState.md changes:** Added Microsoft docs link, Background section with Web Forms context, detailed Blazor Implementation section showing the `[Obsolete]` dictionary pattern, `EnableViewState` parameter documentation, Web Forms vs Blazor usage comparison, migration path table, in-memory limitations table (Web Forms vs BWFC side-by-side), practical sort-direction migration example (before/after), cross-request state alternatives table (cascading values, scoped services, etc.), refactored "Moving On" with concrete code showing strongly-typed replacement, admonitions for key warnings, and See Also cross-references. +- **Migration/readme.md fix:** Line 90 previously stated "Components also do not have ViewState" and linked to GitHub issue #93 as if it were still under consideration. This was **outdated** — ViewState has been implemented. Updated to describe the existing `ViewState` property with link to the new doc page. +- **mkdocs.yml:** Already had `ViewState: UtilityFeatures/ViewState.md` entry — no change needed. +- **Key findings:** The `BaseWebFormsComponent.ViewState` property (line 146) is a `Dictionary` with `[Obsolete]` attribute. `EnableViewState` (line 75-76) is also present as a no-op parameter for markup compatibility. Decision 6915 confirmed ViewState works for ContosoUniversity Instructors sort-direction scenario. +- **Pattern followed:** Utility Feature Documentation Template from the documentation skill — Background → Web Forms Usage → Blazor Implementation → Migration Path → Limitations → Moving On → See Also. diff --git a/.ai-team/agents/colossus/history.md b/.ai-team/agents/colossus/history.md index fe4a8d825..903b464d3 100644 --- a/.ai-team/agents/colossus/history.md +++ b/.ai-team/agents/colossus/history.md @@ -99,3 +99,38 @@ Added 5 smoke tests (Timer, UpdatePanel, UpdateProgress, ScriptManager, Substitu Team update (2026-03-08): Default to SSR (Static Server Rendering) with per-component InteractiveServer opt-in; eliminates HttpContext/cookie/session problems decided by Forge Team update (2026-03-08): @using BlazorWebFormsComponents.LoginControls must be in every generated _Imports.razor decided by Cyclops + + Team update (2026-03-08): Three P0 HTML fidelity fixes identified CheckBox needs span wrapper, BaseValidator needs id/class, FormView needs class on table. Audit scored 87%. decided by Forge + + + Team update (2026-03-08): P0 HTML fidelity fixes complete CheckBox span wrapper, BaseValidator id/class, FormView CssClass. 1488 tests pass. decided by Cyclops, Forge + Team update (2026-03-08): Second sample project will be purpose-built 'EventManager' Control Gallery targeting ~12-15 pages with controls WingtipToys doesn't cover decided by Forge + +## ContosoUniversity Web Forms Setup (2026-03-08) + +Set up the ContosoUniversity ASP.NET Web Forms sample project for local development and captured baseline screenshots of all 5 pages. + +### Setup Learnings + +- **MSBuild selection**: VS 2017 BuildTools (MSBuild 15.0) lacks `Microsoft.WebApplication.targets`. Must use VS 18 Insiders MSBuild (or any VS with web workloads) for Web Forms `.csproj` builds. +- **NBGV vs legacy .NET Framework**: Repo-root `Directory.Build.props` injects Nerdbank.GitVersioning into all projects. Legacy projects with manual `AssemblyInfo.cs` get duplicate `AssemblyVersion` attributes. Fix: place empty `Directory.Build.props` at sample root to block inheritance. +- **AjaxControlToolkit NuGet**: Package version `16.1.1` creates folder `AjaxControlToolkit.16.1.1.0`. DLL is under `lib\net40\`, not `lib\net45\`. +- **LocalDB database attach**: The shipped `.mdf` (internal version 782) auto-upgrades to version 998 on LocalDB v17.0. Use `FOR ATTACH_REBUILD_LOG` when the `.ldf` is missing. +- **IIS Express**: Launch with `/path:` and `/port:` flags. Press `Q` to gracefully stop. + +### Pages & Controls Observed + +| Page | Key Web Forms Controls | +|------|----------------------| +| Home.aspx | Master Page nav menu, static content | +| About.aspx | GridView (enrollment statistics, 11 rows) | +| Students.aspx | GridView (11 students), DetailsView (Add Student form), AutoCompleteExtender | +| Courses.aspx | DropDownList (department filter), AutoCompleteExtender, GridView (empty-state) | +| Instructors.aspx | GridView (7 instructors, sortable columns) | + +### Artifacts + +- Screenshots: `dev-docs/contoso-screenshots/` (5 PNG files) +- Setup guide: `dev-docs/contoso-university-setup.md` +- Commit: `ce2e90fc` on `squad/audit-docs-perf` + Team update (2026-03-08): ContosoUniversity acceptance test patterns partial ID selectors, cascading fallbacks, CONTOSO_BASE_URL env var decided by Rogue diff --git a/.ai-team/agents/cyclops/history.md b/.ai-team/agents/cyclops/history.md index 34c543d36..b79dee9b0 100644 --- a/.ai-team/agents/cyclops/history.md +++ b/.ai-team/agents/cyclops/history.md @@ -31,166 +31,246 @@ ### Summary (2026-03-05 through 2026-03-07 pre-Run 11) -Run 6: 4 script enhancements (TFM, SelectMethod TODO, wwwroot copy, stubs). @rendermode fix: removed standalone directive from _Imports.razor scaffold — `@rendermode` is a directive *attribute* for component instances only. WebFormsPageBase: `ComponentBase` subclass with `Page => this`, Title/MetaDescription/MetaKeywords delegates, `IsPostBack => false`. WebFormsPage consolidation: merged Page.razor head rendering into WebFormsPage via Option B. LoginView script fix: `` → `` (not AuthorizeView), preserve template names. Run 9: 9 script fixes (Models copy, DbContext transform, EF6→EF Core, redirect detection, Program.cs boilerplate, Page Title extraction, QueryString/RouteData annotations, ListView GroupItemCount, csproj packages). Layer 2: full AfterWingtipToys conversion — key pattern: layout code-behind class name MUST match .razor filename. Auth pages use plain HTML forms with HTTP endpoints. - -### Run 11 — Complete WingtipToys Migration from Scratch (2026-03-07) - -**Completed:** Full fresh migration of WingtipToys from Web Forms to Blazor Server. Built from scratch (no reference to FreshWingtipToys). 0 errors, 0 warnings. - -**Approach:** -1. Created fresh `dotnet new blazor --interactivity Server --framework net10.0` project -2. Added BWFC ProjectReference + EF Core/Identity NuGet packages -3. Ran `bwfc-migrate.ps1` to temp dir, cherry-picked converted .razor pages -4. Copied static content (CSS, images, fonts, favicon) from original source preserving paths -5. Built all Layer 2 content from scratch: Models, Data, Services, Program.cs, MainLayout, all code-behinds - -**Key decisions & patterns:** -- Root-level `_Imports.razor` needed for pages outside `Components/` — the `Components/_Imports.razor` only applies within that folder. Both files must have identical usings + `@inherits WebFormsPageBase`. -- Code-behind partial classes must NOT specify `: ComponentBase` when `_Imports.razor` has `@inherits WebFormsPageBase` — causes CS0263 (different base classes in partial declarations). -- `@rendermode InteractiveServer` as a standalone directive in .razor files works for pages that need interactivity (ShoppingCart, AddToCart, AdminPage, Checkout pages). The `@using static RenderMode` in `_Imports.razor` enables the shorthand. -- Auth pages (Login, Register) use plain HTML forms posting to HTTP endpoints — SignInManager needs HTTP context, not SignalR. -- MainLayout inherits `LayoutComponentBase` (overrides `_Imports.razor` `@inherits`) and uses `` for head rendering, `` with ``/``, and code-based category list. -- Category.Description set to `string?` — seed data doesn't populate it. -- Product.UnitPrice converted from `double?` to `decimal?` for currency precision. -- CartStateService uses cookie-based cart ID instead of Session. -- Image paths preserved from source: `/Catalog/Images/Thumbs/` for list, `/Catalog/Images/` for details. - -**File count:** 105 total (27 .razor, 23 .cs, 38 .png images, 5 .css, plus fonts/config) -**Build result:** 0 errors, 0 warnings - -### Run 11 Script Fixes — Fix 1 & Fix 2 (2026-03-07) - -**Fix 1: Scripts/ folder detection and copy (`Invoke-ScriptAutoDetection`)** - -Added `Invoke-ScriptAutoDetection` function to `migration-toolkit/scripts/bwfc-migrate.ps1` (parallel to existing `Invoke-CssAutoDetection`). The function: -- Scans source project for `Scripts/` folder -- Filters out WebForms-specific JS (`*intellisense*`, `_references.js`, `WebForms/` subdir) -- Copies relevant JS files to `wwwroot/Scripts/` in output -- Injects ` + + +``` + +However, this is significantly more complex: +- Must serialize child content to HTML string +- JS must parse and inject arbitrary elements +- Script injection has security implications +- Not recommended for initial implementation + +**Recommendation:** Start with `PageStyleSheet` for CSS only. Add script support separately if needed. + +--- + +## Part 4: Interaction with Render Modes + +| Render Mode | PageStyleSheet Behavior | +|-------------|------------------------| +| **Static SSR** | JS loads CSS on enhanced navigation; no automatic unload | +| **InteractiveServer** | JS loads CSS; proper dispose cleanup | +| **InteractiveWebAssembly** | JS loads CSS; proper dispose cleanup | +| **Auto** | Works in both modes | + +### SSR Considerations + +In Static SSR with enhanced navigation: +- `OnAfterRenderAsync` fires after hydration +- CSS loads dynamically +- Disposal doesn't fire on navigation (page just updates) + +**Solution:** For SSR, consider a service that tracks loaded stylesheets and cleans up on navigation: + +```csharp +public interface IStyleSheetTracker +{ + void Track(string href); + Task CleanupOnNavigationAsync(); +} +``` + +--- + +## Part 5: Implementation Plan + +### Phase 1: Core Component (Recommended Starting Point) + +1. Add JS functions to `Basepage.module.js` +2. Create `PageStyleSheet.razor` and `PageStyleSheet.razor.cs` +3. Add to `_Imports.razor` +4. Test in AfterContosoUniversity sample + +### Phase 2: Enhanced Features + +1. Add `IStyleSheetTracker` for SSR navigation cleanup +2. Add preload support: `` +3. Add integrity/crossorigin attributes for CDN usage + +### Phase 3: Script Support (Future) + +1. Create `PageScript` component for dynamic script loading +2. Handle script dependencies and load ordering + +--- + +## Appendix: Migration Pattern for Courses.aspx + +**Original Web Forms:** +```aspx + + + + +``` + +**Migrated Blazor (with PageStyleSheet):** +```razor +@page "/Courses" + + +@* Script loading handled separately *@ + +
+ ... +
+``` + +--- + +## Decision Required + +- [ ] Approve Phase 1 implementation of `PageStyleSheet` +- [ ] Decide on SSR navigation cleanup approach +- [ ] Determine if script loading should be in scope diff --git a/dev-docs/proposals/pagestylesheet-registry-design.md b/dev-docs/proposals/pagestylesheet-registry-design.md new file mode 100644 index 000000000..1b707b820 --- /dev/null +++ b/dev-docs/proposals/pagestylesheet-registry-design.md @@ -0,0 +1,659 @@ +# PageStyleSheet Registry Architecture + +**Author:** Forge (Lead / Web Forms Reviewer) +**Date:** 2026-03-11 +**Status:** Proposal +**Reviewers:** Jeffrey T. Fritz, Cyclops + +--- + +## Executive Summary + +The current PageStyleSheet implementation has a fundamental lifecycle mismatch: CSS unloads on `DisposeAsync`, but in SSR mode dispose fires immediately after render—before the user sees the page. The current fix (static `` + smart disposal) works for SSR, but doesn't solve the broader problem of CSS lifecycle management in Blazor's enhanced navigation model. + +**This proposal introduces a registry-based "last page wins" model** where CSS persists until no PageStyleSheet component in the render tree references it anymore. + +--- + +## Part 1: Problem Analysis + +### Current Behavior (After Cyclops Fix) + +| Mode | Load | Unload | +|------|------|--------| +| Static SSR | Static `` | Browser (full page nav) | +| Prerender → Interactive | Static `` | JS on dispose | +| InteractiveServer | JS interop | JS on dispose | + +### The Problem + +In Blazor's enhanced navigation model: +1. **Layout CSS** should persist across page navigations (layout stays alive) +2. **Page CSS** should swap when pages swap +3. **Current dispose-based unload** fires at the wrong time for SSR/prerender + +### The Insight + +**The CSS lifecycle should be tied to the component tree, not dispose timing.** + +- If a PageStyleSheet component is **in the render tree** → its CSS stays loaded +- If a PageStyleSheet component is **removed from the render tree** → its CSS can be cleaned up + +This naturally handles: +- **Layout CSS** persists because the layout component stays alive across navigations +- **Page CSS** swaps because the old page leaves the tree and the new one enters + +--- + +## Part 2: Registry Architecture + +### Design Goals + +1. **Global stylesheet registry** tracks all active PageStyleSheet instances +2. **Reference counting** handles multiple components referencing the same CSS +3. **Cleanup after navigation settles** — not on individual dispose +4. **SSR compatibility** — works with static `` tags that JS takes over later + +### Where Should the Registry Live? + +#### Option A: C# Scoped Service + +```csharp +public class StyleSheetRegistry +{ + private readonly Dictionary> _activeSheets = new(); + // Key = Href, Value = Set of component IDs referencing it + + public void Register(string componentId, string href) { ... } + public void Unregister(string componentId) { ... } + public IEnumerable GetOrphanedHrefs() { ... } +} +``` + +**Pros:** +- Type-safe, testable +- Full lifecycle control +- Can integrate with NavigationManager + +**Cons:** +- Scoped to circuit (InteractiveServer) or app (WASM) +- In SSR, each request gets a new service—no persistence +- Must coordinate with JS for actual DOM manipulation + +#### Option B: JavaScript Global State + +```javascript +// In Basepage.module.js +const stylesheetRegistry = { + refs: new Map(), // href -> Set + links: new Map(), // href -> link element + + register(componentId, href) { ... }, + unregister(componentId, href) { ... }, + cleanupOrphans() { ... } +}; +``` + +**Pros:** +- Persists across enhanced navigations (same document) +- Direct DOM access +- Can use MutationObserver for automatic cleanup +- Works naturally with SSR static `` tags + +**Cons:** +- No C# compile-time safety +- Harder to unit test +- Must handle page unload edge cases + +#### **Recommendation: Hybrid (JS Primary, C# Coordination)** + +The **registry should live in JavaScript** because: +1. CSS manipulation is DOM work—JS is the natural home +2. JS state persists across enhanced navigations (SSR + interactive) +3. Static `` tags from SSR can be "adopted" by the JS registry +4. MutationObserver can detect when components leave the DOM + +C# components **register/unregister via JS interop** but don't hold the canonical state. + +--- + +## Part 3: Lifecycle Flow + +### 1. Initial Page Load (Static SSR) + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Server Render │ +├─────────────────────────────────────────────────────────────────┤ +│ 1. PageStyleSheet.razor renders static tag │ +│ │ +│ │ +│ 2. HTML sent to browser (CSS visible immediately) │ +│ │ +│ 3. Component disposes on server (no JS interop → no-op) │ +└─────────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────────┐ +│ Browser (hydration/enhancement) │ +├─────────────────────────────────────────────────────────────────┤ +│ 4. blazor.web.js enhances the page │ +│ │ +│ 5. Blazor re-renders components (same static output) │ +│ │ +│ 6. OnAfterRenderAsync runs → JS registry.register() │ +│ - Finds existing by ID → adopts it │ +│ - Records: refs["CSS/Page.css"] = Set(["bwfc-css-{guid}"]) │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 2. Enhanced Navigation (Page Swap) + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ User clicks link (enhanced navigation) │ +├─────────────────────────────────────────────────────────────────┤ +│ 1. Old page's PageStyleSheet disposes: │ +│ await JS.InvokeVoidAsync("registry.unregister", id, href) │ +│ - refs["CSS/OldPage.css"] = Set() (empty) │ +│ │ +│ 2. New page's PageStyleSheet renders: │ +│ await JS.InvokeVoidAsync("registry.register", id, href) │ +│ - refs["CSS/NewPage.css"] = Set(["bwfc-css-newguid"]) │ +│ │ +│ 3. After navigation settles (see Part 4): │ +│ registry.cleanupOrphans() │ +│ - Removes CSS/OldPage.css (ref count = 0) │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 3. Layout CSS Persistence + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Layout.razor (persists across navigations) │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ │ +│ - Registered once on first page load │ +│ - Never disposed (layout stays alive) │ +│ - refs["CSS/Layout.css"] always has "layout-css" │ +│ - Never orphaned → never unloaded │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 4. Multiple Components, Same CSS + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Page A: │ +│ Page B: │ +├─────────────────────────────────────────────────────────────────┤ +│ If both pages are in tree: │ +│ refs["shared.css"] = Set(["shared-a", "shared-b"]) │ +│ │ +│ Page A disposes: │ +│ refs["shared.css"] = Set(["shared-b"]) (still has ref) │ +│ → CSS NOT removed │ +│ │ +│ Page B disposes: │ +│ refs["shared.css"] = Set() (empty) │ +│ → CSS removed by cleanup │ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Part 4: Detecting "Navigation Settled" + +The key question: **When do we trigger `cleanupOrphans()`?** + +### Option A: Debounced Timer After Unregister + +```javascript +let cleanupTimer = null; + +function unregister(componentId, href) { + // Remove from refs... + + // Schedule cleanup with debounce + clearTimeout(cleanupTimer); + cleanupTimer = setTimeout(() => { + cleanupOrphans(); + }, 100); // 100ms debounce +} +``` + +**Pros:** Simple, handles rapid unregister/register cycles +**Cons:** Arbitrary delay, CSS briefly orphaned before cleanup + +### Option B: NavigationManager.LocationChanged (C#) + +```csharp +public class StyleSheetCleanupService : IDisposable +{ + private readonly IJSRuntime _js; + private readonly NavigationManager _nav; + + public StyleSheetCleanupService(IJSRuntime js, NavigationManager nav) + { + _js = js; + _nav = nav; + _nav.LocationChanged += OnLocationChanged; + } + + private async void OnLocationChanged(object? sender, LocationChangedEventArgs e) + { + // Small delay to let new components register + await Task.Delay(50); + await _js.InvokeVoidAsync("bwfc.stylesheetRegistry.cleanupOrphans"); + } +} +``` + +**Pros:** Explicit navigation trigger +**Cons:** Doesn't fire in static SSR, requires scoped service + +### Option C: MutationObserver in JS + +```javascript +const observer = new MutationObserver((mutations) => { + // Check if any PageStyleSheet placeholder elements were removed + // Schedule cleanup +}); + +observer.observe(document.body, { childList: true, subtree: true }); +``` + +**Pros:** Automatic, no C# coordination needed +**Cons:** Performance overhead, complex to filter relevant mutations + +### **Recommendation: Option A (Debounced Timer)** + +The debounced timer approach is: +- **Simple** — no coordination between C# and JS +- **Reliable** — works in all render modes +- **Safe** — debounce handles rapid navigations +- **Efficient** — timer only runs when unrefs happen + +The delay is invisible to users (CSS is still loaded during the 100ms window). + +--- + +## Part 5: SSR/Interactive Mode Handling + +### Static SSR (No JS) + +``` +Component renders → static tag +Component disposes → no-op (JS not available) +Browser navigation → browser handles CSS cleanup +``` + +The static `` approach from the current fix remains **unchanged**. The registry only activates when JS is available. + +### Prerender → Interactive Transition + +``` +Prerender: static tag rendered +Hydration: OnAfterRenderAsync fires + → registry.register() adopts existing + → registry now manages lifecycle +``` + +**Key behavior:** `loadStyleSheet()` checks if link already exists. If yes, it adopts it instead of creating a duplicate. + +### Interactive-Only (No Prerender) + +``` +Component renders (no static output in interactive mode) +OnAfterRenderAsync: registry.register() creates +Dispose: registry.unregister() +``` + +--- + +## Part 6: Implementation Specification + +### JavaScript: `stylesheetRegistry` (in Basepage.module.js) + +```javascript +// Stylesheet lifecycle registry +const stylesheetRegistry = { + // href -> Set + refs: new Map(), + // href -> HTMLLinkElement + links: new Map(), + + // Cleanup timer handle + cleanupTimer: null, + + /** + * Register a component's reference to a stylesheet. + * Creates the element if it doesn't exist. + */ + register(componentId, href, media, integrity, crossOrigin) { + // Get or create ref set + if (!this.refs.has(href)) { + this.refs.set(href, new Set()); + } + this.refs.get(href).add(componentId); + + // Find existing link or create new + let link = this.links.get(href); + if (!link) { + link = document.querySelector(`link[href="${href}"]`); + if (link) { + // Adopt existing static link + this.links.set(href, link); + console.debug(`[BWFC] Adopted existing stylesheet: ${href}`); + } else { + // Create new link + link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = href; + if (media) link.media = media; + if (integrity) link.integrity = integrity; + if (crossOrigin) link.crossOrigin = crossOrigin; + document.head.appendChild(link); + this.links.set(href, link); + console.debug(`[BWFC] Loaded stylesheet: ${href}`); + } + } + }, + + /** + * Unregister a component's reference. Schedules cleanup. + */ + unregister(componentId, href) { + const refs = this.refs.get(href); + if (refs) { + refs.delete(componentId); + if (refs.size === 0) { + this.refs.delete(href); + } + } + + // Schedule cleanup with debounce + this.scheduleCleanup(); + }, + + /** + * Schedule orphan cleanup after navigation settles. + */ + scheduleCleanup() { + clearTimeout(this.cleanupTimer); + this.cleanupTimer = setTimeout(() => { + this.cleanupOrphans(); + }, 100); + }, + + /** + * Remove stylesheets with no component references. + */ + cleanupOrphans() { + for (const [href, link] of this.links.entries()) { + const refs = this.refs.get(href); + if (!refs || refs.size === 0) { + link.remove(); + this.links.delete(href); + console.debug(`[BWFC] Unloaded orphan stylesheet: ${href}`); + } + } + } +}; + +// Export for module use +export { stylesheetRegistry }; + +// Also expose on window for backward compatibility +if (typeof window !== 'undefined') { + window.bwfc = window.bwfc ?? {}; + window.bwfc.stylesheetRegistry = stylesheetRegistry; +} +``` + +### C#: Updated PageStyleSheet Component + +```csharp +public partial class PageStyleSheet : ComponentBase, IAsyncDisposable +{ + [Inject] private IJSRuntime JS { get; set; } = null!; + + [Parameter, EditorRequired] public string Href { get; set; } = ""; + [Parameter] public string? Media { get; set; } + [Parameter] public string? Id { get; set; } + [Parameter] public string? Integrity { get; set; } + [Parameter] public string? CrossOrigin { get; set; } + + private string _componentId = ""; + private bool _registered; + private IJSObjectReference? _module; + + private string GetComponentId() + { + if (string.IsNullOrEmpty(_componentId)) + { + _componentId = Id ?? $"bwfc-css-{Guid.NewGuid():N}"; + } + return _componentId; + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender && !string.IsNullOrEmpty(Href)) + { + GetComponentId(); + + try + { + _module = await JS.InvokeAsync( + "import", "./_content/Fritz.BlazorWebFormsComponents/js/Basepage.module.js"); + + // Register with the global stylesheet registry + await _module.InvokeVoidAsync( + "stylesheetRegistry.register", + _componentId, Href, Media, Integrity, CrossOrigin); + + _registered = true; + } + catch (InvalidOperationException) + { + // SSR-only mode, JS not available + // Static is already in HTML + } + } + } + + public async ValueTask DisposeAsync() + { + if (_registered && _module is not null) + { + try + { + // Unregister from registry (may trigger deferred cleanup) + await _module.InvokeVoidAsync( + "stylesheetRegistry.unregister", + _componentId, Href); + } + catch (JSDisconnectedException) { } + catch (ObjectDisposedException) { } + } + + if (_module is not null) + { + try { await _module.DisposeAsync(); } + catch { } + } + } +} +``` + +### PageStyleSheet.razor (Unchanged for SSR) + +```razor +@if (!string.IsNullOrEmpty(Href) && !RendererInfo.IsInteractive) +{ + +} +``` + +--- + +## Part 7: Test Scenarios + +### Unit Tests (bUnit) + +1. **Register on render** — Verify `registry.register` called with correct args +2. **Unregister on dispose** — Verify `registry.unregister` called +3. **Multiple components, same CSS** — Verify ref counting works +4. **Empty Href** — Verify no registration +5. **SSR mode** — Verify static link rendered, no JS calls + +### Integration Tests (Playwright) + +1. **Enhanced navigation** — Navigate between pages, verify only current page's CSS loaded +2. **Layout CSS persistence** — Navigate between pages, verify layout CSS never unloads +3. **Rapid navigation** — Click multiple links quickly, verify CSS settles correctly +4. **Full page navigation** — Verify browser handles cleanup naturally + +--- + +## Part 8: Migration from Current Implementation + +### Breaking Changes + +None. The API is identical: +```razor + +``` + +### Behavioral Changes + +| Scenario | Before | After | +|----------|--------|-------| +| SSR dispose | CSS persists (browser) | CSS persists (browser) | +| Interactive dispose | CSS removed immediately | CSS removed after debounce | +| Layout CSS | Removed on page dispose | Never removed (layout alive) | +| Same CSS, multiple components | Removed on first dispose | Removed when last unregisters | + +The behavioral change for "same CSS, multiple components" is a **fix**, not a breaking change. + +--- + +## Part 9: Alternatives Considered + +### Alternative A: SectionContent Aggregation + +Use Blazor's `SectionContent/SectionOutlet` to aggregate CSS in App.razor. + +**Rejected because:** +- Requires App.razor modification +- Steeper learning curve for migrators +- Doesn't help with dynamic load/unload + +### Alternative B: C#-Only Registry (No JS State) + +Keep all state in a scoped C# service, use JS only for DOM manipulation. + +**Rejected because:** +- Scoped services reset on each SSR request +- Can't adopt static `` tags across render modes +- More complex interop + +### Alternative C: NavigationManager.LocationChanged Cleanup + +Trigger cleanup on C# navigation events instead of JS debounce. + +**Rejected because:** +- Doesn't fire in static SSR +- Adds service dependency +- JS debounce is simpler and works everywhere + +--- + +## Part 10: Open Questions + +1. **Should cleanup delay be configurable?** + - Probably not — 100ms is safe for all scenarios. + +2. **Should we support explicit "don't unload" marker?** + - Use case: CDN CSS that should persist forever. + - Consider: `` + +3. **Should we dedupe by href normalization?** + - `CSS/Page.css` vs `/CSS/Page.css` vs `./CSS/Page.css` + - Recommendation: Normalize to absolute URL in JS. + +--- + +## Deliverables for Cyclops + +### Files to Create/Modify + +1. **`src/BlazorWebFormsComponents/wwwroot/js/Basepage.module.js`** + - Add `stylesheetRegistry` object with `register`, `unregister`, `cleanupOrphans` + - Keep existing `loadStyleSheet`/`unloadStyleSheet` as legacy (or deprecate) + +2. **`src/BlazorWebFormsComponents/PageStyleSheet.razor.cs`** + - Replace `_isLoadedViaJs` with `_registered` + - Replace `loadStyleSheet`/`unloadStyleSheet` calls with `registry.register`/`unregister` + - Pass `Href` to unregister (registry needs it for ref counting) + +3. **`src/BlazorWebFormsComponents.Test/PageStyleSheetTests.cs`** + - Add tests for registry behavior + - Add tests for ref counting scenarios + +### Implementation Order + +1. Add JS `stylesheetRegistry` (additive, doesn't break existing) +2. Update C# component to use registry +3. Add tests +4. Update docs (`dynamic-css-loader-design.md`) + +--- + +## Appendix: Sequence Diagram + +``` +Page Load (SSR → Interactive) +════════════════════════════ + +Server Browser JS Registry +─────── ─────── ─────────── + │ │ │ + │──render PageStyleSheet──────────>│ │ + │ │ │ + │ │ │ + │──dispose (no JS interop)────────>│ │ + │ │ │ + │ │──blazor hydration──────────────>│ + │ │ OnAfterRenderAsync │ + │ │ │ + │ │──register("css-1", "page.css")─>│ + │ │ │ + │ │ (adopts )│ + │ │ │ + +Enhanced Navigation +═══════════════════ + +Browser JS Registry +─────── ─────────── + │ │ + │──[navigate to /new-page]────────>│ + │ │ + │ (old page disposes) │ + │──unregister("css-1","page.css")─>│ + │ │ refs[page.css] = Set() + │ │ scheduleCleanup() + │ │ + │ (new page renders) │ + │──register("css-2","new.css")────>│ + │ │ refs[new.css] = Set(["css-2"]) + │ │ + │ (100ms debounce) │ + │ │──cleanupOrphans() + │ │ page.css removed + │ │ +``` diff --git a/dev-docs/proposed-issues/blazor-ajax-toolkit-components.md b/dev-docs/proposed-issues/blazor-ajax-toolkit-components.md new file mode 100644 index 000000000..fa4b20644 --- /dev/null +++ b/dev-docs/proposed-issues/blazor-ajax-toolkit-components.md @@ -0,0 +1,96 @@ +# GitHub Issue: Create BlazorAjaxToolkitComponents companion library + +**Labels:** enhancement, help wanted + +--- + +## Summary + +Create a new companion library **BlazorAjaxToolkitComponents** (or similar name) that provides Blazor component equivalents for the [AJAX Control Toolkit](https://github.com/DevExpress/AjaxControlToolkit) controls, following the same design philosophy as BlazorWebFormsComponents. + +## Background + +The AJAX Control Toolkit was a popular extension library for ASP.NET Web Forms that provided rich UI controls and behaviors. Many Web Forms applications being migrated to Blazor use these controls extensively. Currently, our migration scripts strip `` tags with TODO comments, leaving developers to manually find replacements. + +## Proposed Approach + +Following the BWFC pattern: +1. **Same control names** — `` becomes `` +2. **Same attributes** — Property names match the original toolkit +3. **Similar HTML output** — Preserve CSS compatibility where possible +4. **NuGet package** — `Fritz.BlazorAjaxToolkitComponents` or similar + +## Priority Controls + +Based on real-world migration frequency: + +### High Priority +| AJAX Toolkit Control | Notes | +|---------------------|-------| +| `Accordion` / `AccordionPane` | Collapsible panel groups | +| `TabContainer` / `TabPanel` | Tabbed content areas | +| `ModalPopupExtender` | Modal dialogs | +| `CollapsiblePanelExtender` | Single collapsible section | +| `CalendarExtender` | Date picker (may leverage existing BWFC Calendar) | +| `AutoCompleteExtender` | Typeahead/autocomplete textbox | +| `FilteredTextBoxExtender` | Input masking/filtering | + +### Medium Priority +| AJAX Toolkit Control | Notes | +|---------------------|-------| +| `ConfirmButtonExtender` | Confirmation dialogs on button click | +| `MaskedEditExtender` | Input masks | +| `NumericUpDownExtender` | Numeric spinner | +| `SliderExtender` | Range slider | +| `ToggleButtonExtender` | Toggle state buttons | +| `PopupControlExtender` | Popup panels | +| `HoverMenuExtender` | Hover-triggered menus | + +### Lower Priority +- `AnimationExtender` +- `DragPanelExtender` +- `DropShadowExtender` +- `RoundedCornersExtender` (CSS handles this now) +- `ReorderList` +- `Rating` + +## Design Considerations + +### Extender Pattern +Many AJAX Toolkit controls are "extenders" that attach behavior to existing controls (e.g., ``). Options: +1. **Wrapper components** — `` combines TextBox + calendar +2. **Behavior components** — Use Blazor's `@ref` and JS interop to attach behaviors +3. **CSS/JS only** — Some extenders can be pure CSS/JS without Blazor components + +### JavaScript Interop +Unlike BWFC (mostly pure Blazor), many AJAX Toolkit features require JavaScript (animations, popups, drag-drop). The library should: +- Minimize JS dependencies +- Use modern CSS where possible (CSS Grid, Flexbox, CSS animations) +- Provide clean JS interop for complex behaviors + +## Integration with BWFC + +- Separate NuGet package to keep BWFC lightweight +- Share common infrastructure (WebColor, enums, base classes) if appropriate +- Coordinated versioning and documentation + +## Migration Script Integration + +Once the library exists, update `bwfc-migrate.ps1` to: +1. Recognize `` controls +2. Convert to `` equivalents (or strip prefix like BWFC) +3. Add appropriate `@using` statements + +## Success Criteria + +- [ ] NuGet package published +- [ ] Top 7 high-priority controls implemented +- [ ] Migration script recognizes and converts common controls +- [ ] Documentation with migration examples +- [ ] At least one sample app demonstrating migration + +## Related + +- BWFC migration script: `migration-toolkit/scripts/bwfc-migrate.ps1` +- Current behavior: `ajaxToolkit:` tags are stripped with `@* TODO: AjaxToolkit ... *@` comments +- Original toolkit: https://github.com/DevExpress/AjaxControlToolkit diff --git a/dev-docs/research-second-sample-and-url-rewriting.md b/dev-docs/research-second-sample-and-url-rewriting.md new file mode 100644 index 000000000..b76f22fe5 --- /dev/null +++ b/dev-docs/research-second-sample-and-url-rewriting.md @@ -0,0 +1,446 @@ +# Research: Second Sample Project & ASPX URL Rewriting + +**Author:** Forge (Lead / Web Forms Reviewer) +**Date:** 2026-03-09 +**Requested by:** Jeffrey T. Fritz + +--- + +## Part 1: Second Web Forms Sample Project Candidates + +### Problem Statement + +WingtipToys is our only migration test bed. It's an e-commerce sample that heavily exercises: +- **Data display:** GridView, ListView, FormView, DetailsView +- **Input:** TextBox, Button, CheckBox, DropDownList, FileUpload, ImageButton +- **Display:** Label, HyperLink, LinkButton, Literal, PlaceHolder +- **Validation:** RequiredFieldValidator, CompareValidator, RegularExpressionValidator, ValidationSummary +- **Login:** Login, Register, ManagePassword (via Identity templates) +- **Model binding:** `SelectMethod`, `ItemType`, bound expressions + +**Controls NOT exercised by WingtipToys:** +- TreeView, Menu, SiteMapPath, SiteMapDataSource +- Wizard (standalone — CreateUserWizard is in login, but not ``) +- MultiView/View (standalone) +- Calendar (standalone page usage) +- DataList, DataGrid (legacy controls) +- Repeater (standalone) +- AdRotator +- Panel (structural usage with GroupingText) +- BulletedList (data-bound usage) +- RadioButtonList, CheckBoxList (data-bound list usage) + +We need a second sample that stress-tests these gaps. + +### Evaluation Criteria + +1. **Must be a real, publicly available ASP.NET Web Forms application** (open source, sample, or tutorial app) +2. **Should exercise DIFFERENT controls than WingtipToys** — specifically navigation controls (TreeView, Menu, SiteMapPath), wizard flows, calendar, and list controls (DataList, Repeater, RadioButtonList, CheckBoxList) +3. **Should be small-to-medium complexity** — comparable to WingtipToys (~15 pages) +4. **Must target .NET Framework 4.x** (the migration toolkit targets 4.x Web Forms) +5. **Bonus: Has a database or data layer** (tests real data binding scenarios) + +### Market Reality + +After extensive searching across GitHub, Microsoft samples, CodeProject, and community repositories, **the landscape for comprehensive open-source Web Forms sample applications is extremely thin.** Key findings: + +- Microsoft's only maintained Web Forms tutorial sample is **WingtipToys itself** +- The old Visual Studio "Starter Kits" (Personal Website, Club Web Site, Small Business) are no longer publicly available +- Contoso University, Music Store, and most other well-known Microsoft samples are **MVC or ASP.NET Core only** — no Web Forms versions exist +- Community repos on GitHub (under the `asp-net-web-forms` topic) are mostly small demos, homework projects, or single-control samples — not integrated applications +- Third-party samples (Telerik, DevExpress, Syncfusion) use vendor-specific controls, not standard ASP.NET Web Forms controls + +### Candidates + +#### 1. PallaviKatari/ASP.NET-WEBFORMS-CONCEPTS (GitHub) + +- **Name and source URL:** ASP.NET-WEBFORMS-CONCEPTS — https://github.com/PallaviKatari/ASP.NET-WEBFORMS-CONCEPTS +- **Framework version:** .NET Framework 4.x (Visual Studio project) +- **Controls used:** MultiView, Calendar, TreeView, DataList, Repeater, RadioButtonList, CheckBoxList, BulletedList, AdRotator, Panel, Wizard (individual .aspx demo pages for each control) +- **Size:** ~30+ individual .aspx pages, each demonstrating one or two controls. Not an integrated application — more of a control showcase. +- **Data layer:** Inline data / XML files. No database. +- **Why it's a good fit:** Directly covers nearly every control WingtipToys misses. Each page is a self-contained demo with markup and code-behind. Could be adapted into a cohesive sample application. +- **Risks:** + - Not an integrated app — it's a collection of isolated demos, not a real migration scenario + - No navigation flow between pages (no master page, no site map) + - No database layer — doesn't test real data binding + - License unclear (no LICENSE file in repo) + - Would require significant rework to become a migration test bed + +#### 2. Purpose-Built "BWFC Control Gallery" (New Project) + +- **Name and source URL:** Would be created from scratch under `samples/ControlGallery/` in this repo +- **Framework version:** .NET Framework 4.8 (matching our target) +- **Controls used:** TreeView + SiteMapDataSource (navigation), Menu + SiteMapPath (breadcrumbs), Wizard (multi-step form), Calendar (event scheduling), DataList (card layout), Repeater (custom templates), RadioButtonList + CheckBoxList (survey/filter), BulletedList (data-bound lists), AdRotator (banner rotation), Panel with GroupingText (fieldsets), MultiView/View (tabbed content) +- **Size:** Target ~12-15 pages with a shared master page and Web.sitemap +- **Data layer:** SQL Server LocalDB with Entity Framework — a simple domain like "Event Management" or "Employee Directory" that naturally uses Calendar, TreeView (department hierarchy), Wizard (event registration), DataList (event cards) +- **Why it's a good fit:** + - Purpose-built to exercise exactly the controls WingtipToys doesn't cover + - We control the complexity and can ensure every BWFC component gap is tested + - Uses a real database for authentic data-binding scenarios + - Follows the same project structure patterns as WingtipToys for consistency + - Lives in our repo — no license or dependency concerns +- **Risks:** + - Requires development effort (estimated 2-3 days for Cyclops + Jubilee) + - Needs design spec and review before building + - Not a "real world" app that someone would actually migrate — it's a test harness + +#### 3. Microsoft AspNetDocs Embedded Samples (Assembled) + +- **Name and source URL:** Assembled from https://github.com/dotnet/AspNetDocs/tree/main/aspnet/web-forms/ +- **Framework version:** .NET Framework 4.5+ +- **Controls used:** Various — the AspNetDocs repo contains code snippets and partial samples for navigation controls, data controls, and security scenarios across the tutorial series +- **Size:** Hundreds of code snippets across dozens of markdown files. Would need extraction and assembly. +- **Data layer:** Various — some samples use SQL Server, some use inline data +- **Why it's a good fit:** Official Microsoft code, MIT-licensed, covers breadth of Web Forms features +- **Risks:** + - Not a runnable application — scattered code fragments embedded in documentation markdown + - Assembly effort would be very high (parsing markdown, extracting code blocks, creating project structure) + - Many snippets are incomplete or context-dependent + - Essentially building from scratch with reference material + +#### 4. Tour Management Application (GitHub Community) + +- **Name and source URL:** Tour Management ASP.NET — found under `asp-net-web-forms` GitHub topic (various forks, e.g., jaygajera17/Tour_Management_Asp.Net) +- **Framework version:** .NET Framework 4.x +- **Controls used:** GridView, TextBox, Button, Label, DropDownList, Image — predominantly data entry/display controls +- **Size:** Small (~8-10 pages), tour booking CRUD +- **Data layer:** SQL Server +- **Why it's a good fit:** Real application with database, authentication, and CRUD operations +- **Risks:** + - **Overlaps heavily with WingtipToys** — same control set (GridView, TextBox, Button, Label) + - Doesn't exercise the navigation/wizard/calendar controls we need + - Community project with unknown code quality + - Doesn't fill the control coverage gaps + +#### 5. Custom "Northwind Explorer" (Adapted from Northwind DB) + +- **Name and source URL:** Would be created using the classic Northwind sample database schema +- **Framework version:** .NET Framework 4.8 +- **Controls used:** TreeView (product category hierarchy), Menu + SiteMapPath (navigation), DataList (product cards), Repeater (order line items), Calendar (order date picker), DetailsView (single record view), RadioButtonList (filters), CheckBoxList (multi-select filters), BulletedList (related items), Panel with GroupingText +- **Size:** ~10-12 pages +- **Data layer:** SQL Server LocalDB with Northwind schema (well-known, freely available) +- **Why it's a good fit:** + - Northwind is the most recognized .NET sample database — instantly familiar to Web Forms developers + - The domain (products, categories, orders, employees) naturally maps to hierarchical controls (TreeView for categories), calendar controls (order dates), and list controls + - Database schema is pre-built and well-documented + - Signals to the migration community that BWFC handles real enterprise data +- **Risks:** + - Still requires building the Web Forms frontend from scratch + - Northwind schema is large — need to scope to a subset + - Same "test harness" concern as Candidate 2 + +### Recommendation + +**Top Pick: Candidate 2 — Purpose-Built "BWFC Control Gallery" with an Event Management domain** + +**Runner-up: Candidate 5 — Northwind Explorer** (if we want name recognition over domain fit) + +**Reasoning:** + +1. **The market has spoken** — there is no existing open-source Web Forms sample app that exercises the controls we need. Every candidate either overlaps with WingtipToys or requires building from scratch anyway. Accepting this reality means we should build exactly what we need rather than force-fitting an existing project. + +2. **Event Management domain is optimal** because: + - **Calendar** is a natural fit (event dates, scheduling) + - **TreeView** maps to event categories or venue hierarchy + - **Menu + SiteMapPath** for site navigation (Events, Venues, Attendees, Reports) + - **Wizard** for multi-step event registration (personal info → event selection → payment → confirmation) + - **DataList** for event card layout (image + title + date + description) + - **Repeater** for attendee lists with custom templates + - **RadioButtonList/CheckBoxList** for event type filters and preference selection + - **BulletedList** for event features/amenities + - **AdRotator** for sponsor banners + - **Panel with GroupingText** for form sections + +3. **Controlled complexity** — we design it to be ~12-15 pages, matching WingtipToys scale, with a LocalDB database and Entity Framework Code First. + +4. **Zero license risk** — it's our code, in our repo, MIT-licensed like everything else. + +5. **Migration pipeline validation** — building the Web Forms version first, then running our migration toolkit against it, validates the entire BWFC pipeline for the control families that WingtipToys doesn't cover. + +**Suggested name:** `EventManager` (parallel to `WingtipToys`) +**Suggested location:** `samples/EventManager/` (Web Forms original) + `samples/AfterEventManager/` (Blazor migrated) + +**Next step:** Forge to write a design spec for Cyclops (build) + Jubilee (samples) + Beast (docs) + Rogue (tests). + +--- + +## Part 2: ASPX URL Rewriting for Migration + +### Problem Statement + +When migrating a Web Forms application to Blazor, URLs change: + +| Web Forms URL | Blazor URL | +|---|---| +| `/Products.aspx` | `/Products` | +| `/Account/Login.aspx` | `/Account/Login` | +| `/Admin/AdminPage.aspx?id=5` | `/Admin/AdminPage?id=5` | +| `/Catalog/Products.aspx?cat=shoes&page=2` | `/Catalog/Products?cat=shoes&page=2` | + +Existing bookmarks, search engine indexes, external links, and hardcoded references all point to the `.aspx` URLs. We need a strategy to handle these gracefully. + +### Approaches Investigated + +#### Approach A: ASP.NET Core URL Rewriting Middleware (`Microsoft.AspNetCore.Rewrite`) + +The built-in `Microsoft.AspNetCore.Rewrite` package provides regex-based URL rewriting and redirection. + +**Transparent Rewrite (URL stays as `.aspx` in browser):** +```csharp +using Microsoft.AspNetCore.Rewrite; + +var rewriteOptions = new RewriteOptions() + .AddRewrite(@"^(.+)\.aspx$", "$1", skipRemainingRules: true); + +app.UseRewriter(rewriteOptions); +// Must be placed BEFORE app.UseRouting() +``` + +**301 Redirect (browser URL changes to clean URL):** +```csharp +var rewriteOptions = new RewriteOptions() + .AddRedirect(@"^(.+)\.aspx$", "$1", statusCode: 301); + +app.UseRewriter(rewriteOptions); +``` + +**Pros:** +- Built into ASP.NET Core — no additional packages needed (ships with the framework) +- Regex-based — handles any `.aspx` URL pattern in a single rule +- Query strings are preserved automatically (they're not part of the path the regex matches) +- Well-documented by Microsoft +- Supports both rewrite (transparent) and redirect (SEO) modes + +**Cons:** +- Regex operates on URL path only — works for our use case but limited for complex transformations +- Rewrite mode doesn't update the browser's URL bar — could confuse users if they copy the URL +- Must be placed before `UseRouting()` in the middleware pipeline + +#### Approach B: Custom Middleware (`app.Use()`) + +A lightweight inline middleware that catches `.aspx` requests: + +```csharp +app.Use(async (context, next) => +{ + var path = context.Request.Path.Value; + if (path != null && path.EndsWith(".aspx", StringComparison.OrdinalIgnoreCase)) + { + var newPath = path[..^5]; // Strip ".aspx" + var queryString = context.Request.QueryString.Value; + + context.Response.StatusCode = 301; + context.Response.Headers.Location = newPath + queryString; + return; // Short-circuit — don't call next() + } + await next(); +}); +``` + +**Pros:** +- Zero dependencies — pure middleware, no NuGet package needed +- Full control over redirect logic (can add logging, conditional behavior, URL mapping tables) +- Easy to extend for complex cases (e.g., `/Default.aspx` → `/`, page-specific rewrites) +- Query string preservation is explicit and visible + +**Cons:** +- More code to maintain than a single regex rule +- No built-in rewrite (transparent) mode — would need to manually rewrite `Request.Path` +- Less discoverable than the standard `RewriteOptions` API + +#### Approach C: Custom `IRule` Implementation + +A reusable rule class for the RewriteOptions pipeline: + +```csharp +public class AspxRewriteRule : IRule +{ + private readonly int _statusCode; + private readonly bool _redirect; + + public AspxRewriteRule(bool redirect = true, int statusCode = 301) + { + _redirect = redirect; + _statusCode = statusCode; + } + + public void ApplyRule(RewriteContext context) + { + var request = context.HttpContext.Request; + var path = request.Path.Value; + + if (path == null || !path.EndsWith(".aspx", StringComparison.OrdinalIgnoreCase)) + { + context.Result = RuleResult.ContinueRules; + return; + } + + var newPath = path[..^5]; // Strip ".aspx" + if (string.IsNullOrEmpty(newPath)) newPath = "/"; + + if (_redirect) + { + var response = context.HttpContext.Response; + response.StatusCode = _statusCode; + response.Headers.Location = newPath + request.QueryString.Value; + context.Result = RuleResult.EndResponse; + } + else + { + request.Path = newPath; + context.Result = RuleResult.SkipRemainingRules; + } + } +} + +// Usage: +var rewriteOptions = new RewriteOptions() + .Add(new AspxRewriteRule(redirect: true, statusCode: 301)); +app.UseRewriter(rewriteOptions); +``` + +**Pros:** +- Plugs into the standard `RewriteOptions` pipeline — composable with other rules +- Supports both redirect and transparent rewrite modes +- Testable as a standalone class +- Can be shipped as part of a NuGet package + +**Cons:** +- More code than Approach A's one-liner regex +- Requires understanding the `IRule` interface + +#### Approach D: Blazor `@page` Directive with `.aspx` Patterns + +Blazor components can declare multiple `@page` directives, including ones with `.aspx`: + +```razor +@page "/Products" +@page "/Products.aspx" +``` + +**Pros:** +- No middleware needed — works purely at the Blazor routing level +- Each page explicitly declares its legacy URL +- Route parameters work: `@page "/ProductDetails.aspx/{Id:int}"` + +**Cons:** +- **Does not scale** — every migrated page needs a duplicate `@page` directive +- Does not handle query strings differently (they work, but the `.aspx` stays in the URL) +- No 301 redirect — search engines see two URLs for the same content (duplicate content penalty) +- Violates DRY — migration toolkit would need to auto-generate these +- Not removable in a single place when legacy support is no longer needed + +#### Approach E: Catch-All Fallback Route + +A single Blazor component that catches all `.aspx` requests: + +```razor +@page "/{*path}" +@inject NavigationManager Nav + +@code { + [Parameter] public string? Path { get; set; } + + protected override void OnInitialized() + { + if (Path != null && Path.EndsWith(".aspx", StringComparison.OrdinalIgnoreCase)) + { + var newPath = Path[..^5]; + Nav.NavigateTo("/" + newPath + Nav.ToAbsoluteUri(Nav.Uri).Query, replace: true); + } + } +} +``` + +**Cons:** +- Catch-all routes in Blazor are greedy — can interfere with other routing +- Client-side redirect, not server-side 301 — no SEO benefit +- Extra round-trip (page loads, then redirects) +- Not recommended + +### 301 Redirect vs. Transparent Rewrite — SEO Analysis + +| Factor | 301 Redirect | Transparent Rewrite | +|---|---|---| +| **Browser URL** | Changes to clean URL | Stays as `.aspx` | +| **SEO** | ✅ Search engines update index to new URL | ⚠️ Two URLs may exist for same content | +| **Bookmarks** | Update on next visit | Stay as `.aspx` forever | +| **Performance** | Extra HTTP round-trip | No extra round-trip | +| **Migration phase** | Best for: after migration is complete | Best for: during migration (testing) | +| **Recommendation** | ✅ **Use this for production** | Use temporarily during development | + +**Verdict:** Ship with **301 redirect** as the default. Provide an option for transparent rewrite during the migration development phase. + +### Existing Solutions + +No existing NuGet package or library specifically targets ASPX-to-Blazor URL rewriting. The closest solutions are: +- `Microsoft.AspNetCore.Rewrite` — general-purpose URL rewriting (our recommended foundation) +- YARP (Yet Another Reverse Proxy) — useful for incremental migration but overkill for URL stripping +- Various blog posts describe one-off regex rules, but no reusable library exists + +### Recommended Approach + +**Ship a documented extension method in the migration-toolkit**, not as a NuGet-installable middleware in the BWFC library itself. + +**Rationale:** +1. This is a **migration concern**, not a runtime component concern — it belongs in the migration toolkit +2. It's ~20 lines of code — doesn't warrant its own NuGet package +3. Developers should understand what it does and remove it once migration is complete +4. The BWFC NuGet package should remain focused on Blazor components, not middleware + +#### Recommended Code + +Add to `migration-toolkit/scripts/` or as a documented snippet in `migration-toolkit/METHODOLOGY.md`: + +```csharp +// ============================================================ +// ASPX URL Redirect Middleware for Web Forms → Blazor Migration +// Add to Program.cs BEFORE app.UseRouting() +// Remove once all legacy URLs have been updated. +// ============================================================ + +using Microsoft.AspNetCore.Rewrite; + +// Option 1: Simple one-liner (recommended for most migrations) +var rewriteOptions = new RewriteOptions() + .AddRedirect(@"^(.+)\.aspx$", "$1", statusCode: 301); +app.UseRewriter(rewriteOptions); +app.UseRouting(); + +// Option 2: Handle Default.aspx → / (root page redirect) +var rewriteOptions = new RewriteOptions() + .AddRedirect(@"^Default\.aspx$", "/", statusCode: 301) + .AddRedirect(@"^(.+)\.aspx$", "$1", statusCode: 301); +app.UseRewriter(rewriteOptions); +app.UseRouting(); +``` + +**Query string handling:** Query strings are automatically preserved because `AddRedirect` only matches against the URL path, not the query string. A request to `/Products.aspx?cat=shoes&page=2` redirects to `/Products?cat=shoes&page=2` with no additional configuration. + +**Case sensitivity:** The regex is case-insensitive by default in `RewriteOptions`. Both `/Products.ASPX` and `/products.aspx` are handled. + +**Default.aspx special case:** Many Web Forms apps use `Default.aspx` as their home page. The recommended approach adds an explicit rule for this before the general rule, redirecting to `/` (the root). + +### Implementation Plan + +| Item | Location | Action | +|---|---|---| +| **Code snippet** | `migration-toolkit/METHODOLOGY.md` | Add "URL Preservation" section with the recommended `RewriteOptions` code | +| **Checklist item** | `migration-toolkit/CHECKLIST.md` | Add checkbox: "Add ASPX URL redirect middleware to Program.cs" | +| **Migration script** | `migration-toolkit/scripts/bwfc-migrate-layer2.ps1` | Consider auto-injecting the rewrite code into `Program.cs` as a Layer 2 transform | +| **Documentation** | `docs/Migration/UrlRewriting.md` | Full guide with all approaches, trade-offs, and examples | +| **Migration skill** | `migration-toolkit/skills/migration-standards/SKILL.md` | Update with URL preservation guidance | + +**Priority:** P2 — not blocking any current migration runs, but should be addressed before the migration toolkit is promoted as "production ready." + +**Not recommended:** Adding this to the BWFC NuGet library itself. URL rewriting is infrastructure middleware, not a Blazor component. Mixing concerns would confuse the package's purpose. + +--- + +## Summary + +| Research Item | Recommendation | Priority | +|---|---|---| +| Second sample project | Build "EventManager" — purpose-built Control Gallery with Event Management domain | P2 (after migration toolkit stabilization) | +| ASPX URL rewriting | Document `RewriteOptions.AddRedirect` in migration-toolkit; don't ship as NuGet | P2 | diff --git a/docs/DataControls/FieldColumns.md b/docs/DataControls/FieldColumns.md new file mode 100644 index 000000000..e3542c468 --- /dev/null +++ b/docs/DataControls/FieldColumns.md @@ -0,0 +1,476 @@ +# Field Column Components + +Field column components define how individual columns are rendered inside data controls such as [GridView](GridView.md), [DataGrid](DataGrid.md), and [DetailsView](DetailsView.md). In ASP.NET Web Forms, these are the ``, ``, ``, and `` controls that appear as children of a `` element. This library provides Blazor equivalents that preserve the same names and attribute signatures. + +Original Microsoft documentation: + +- [BoundField](https://docs.microsoft.com/en-us/dotnet/api/system.web.ui.webcontrols.boundfield?view=netframework-4.8) +- [TemplateField](https://docs.microsoft.com/en-us/dotnet/api/system.web.ui.webcontrols.templatefield?view=netframework-4.8) +- [ButtonField](https://docs.microsoft.com/en-us/dotnet/api/system.web.ui.webcontrols.buttonfield?view=netframework-4.8) +- [HyperLinkField](https://docs.microsoft.com/en-us/dotnet/api/system.web.ui.webcontrols.hyperlinkfield?view=netframework-4.8) + +## Shared Features (All Field Columns) + +Every field column component inherits from `BaseColumn` and supports these common parameters: + +| Parameter | Type | Description | +|-----------|------|-------------| +| `HeaderText` | `string` | Text displayed in the column header. | +| `SortExpression` | `string` | Expression used when the parent control supports sorting. | +| `Visible` | `bool` | Controls whether the column is rendered. Defaults to `true`. | + +### Blazor Notes + +- **ItemType cascading** — The generic `ItemType` parameter is automatically cascaded from the parent data control (GridView, DataGrid) to all child field columns. You do not need to specify it on each column unless you want to be explicit. +- **No `runat="server"`** — As with all Blazor components, the `runat="server"` attribute is not used. +- **Column ordering** — Columns render in the order they are declared, just like in Web Forms. + +--- + +## BoundField + +Displays the value of a data field as text. This is the most common column type and is the Blazor equivalent of ``. + +### Features Supported in Blazor + +- Bind to a data property via `DataField` +- Format output with `DataFormatString` +- Nested (dot-notation) property access (e.g., `DataField="Address.City"`) +- Read-only mode in edit scenarios via `ReadOnly` +- Sort expression defaults to `DataField` when not explicitly set + +### Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `DataField` | `string` | Name of the data property to display. Supports dot-notation for nested properties. | +| `DataFormatString` | `string` | A .NET format string applied to the value (e.g., `{0:C}` for currency, `{0:d}` for short date). | +| `ReadOnly` | `bool` | When `true`, the field renders as text even when the row is in edit mode. | +| `HeaderText` | `string` | Column header text. | +| `SortExpression` | `string` | Sort expression. Defaults to `DataField` if not set. | +| `Visible` | `bool` | Show or hide the column. | + +### Web Forms Syntax + +```html + +``` + +### Blazor Syntax + +```razor + +``` + +### Example + +```razor + + + + + + + + + +@code { + private List Products = new() + { + new Product { Id = 1, Name = "Widget", Price = 9.99m, CreatedDate = DateTime.Now }, + new Product { Id = 2, Name = "Gadget", Price = 24.50m, CreatedDate = DateTime.Now.AddDays(-7) } + }; + + public class Product + { + public int Id { get; set; } + public string Name { get; set; } + public decimal Price { get; set; } + public DateTime CreatedDate { get; set; } + } +} +``` + +--- + +## TemplateField + +Allows fully custom content inside a column using Blazor `RenderFragment` templates. This is the Blazor equivalent of ``. + +### Features Supported in Blazor + +- Custom display content via `` +- Custom edit-mode content via `` +- Access to the current row item through the template context variable +- Full Blazor component nesting inside templates + +### Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `ItemTemplate` | `RenderFragment` | Template for rendering the cell in display mode. | +| `EditItemTemplate` | `RenderFragment` | Template for rendering the cell in edit mode. Falls back to `ItemTemplate` if not specified. | +| `HeaderText` | `string` | Column header text. | +| `SortExpression` | `string` | Sort expression for the column. | +| `Visible` | `bool` | Show or hide the column. | + +### Blazor Notes + +!!! note "Context Attribute" + When using `` or ``, use the `Context` attribute to name the template variable. For example, `Context="Item"` lets you reference `@Item.PropertyName`. Without it, the default name is `@context`. + +### Web Forms Syntax + +```html + + + + + + + + +``` + +### Blazor Syntax + +```razor + + + View + + + + + +``` + +### Example + +```razor + + + + + + + @if (Item.InStock) + { + ✓ Yes + } + else + { + ✗ No + } + + + + + Details + + + + + +@code { + private List Products = new() + { + new Product { Id = 1, Name = "Widget", Price = 9.99m, InStock = true }, + new Product { Id = 2, Name = "Gadget", Price = 24.50m, InStock = false } + }; + + public class Product + { + public int Id { get; set; } + public string Name { get; set; } + public decimal Price { get; set; } + public bool InStock { get; set; } + } +} +``` + +--- + +## ButtonField + +Displays a button (push button, link button, or image button) in each row of a data control. This is the Blazor equivalent of ``. Clicking the button raises the parent control's `RowCommand` event. + +### Features Supported in Blazor + +- Three button styles via `ButtonType`: `ButtonType.Button`, `ButtonType.Link`, `ButtonType.Image` +- Command name and argument for server-side handling +- Data-bound button text with format strings +- Image buttons via `ImageUrl` + +### Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `ButtonType` | `ButtonType` | The style of button to render. Options: `ButtonType.Button`, `ButtonType.Link` (default), `ButtonType.Image`. | +| `CommandName` | `string` | The command name passed to the parent control's row command event. | +| `DataTextField` | `string` | Data field used for button text. Supports comma-separated fields when used with `DataTextFormatString`. | +| `DataTextFormatString` | `string` | Format string applied to the data-bound text. | +| `ImageUrl` | `string` | URL of the image to display when `ButtonType` is `ButtonType.Image`. | +| `Text` | `string` | Static button text. Overridden by `DataTextFormatString` if specified. | +| `HeaderText` | `string` | Column header text. | +| `SortExpression` | `string` | Sort expression for the column. | +| `Visible` | `bool` | Show or hide the column. | + +### Web Forms Syntax + +```html + +``` + +### Blazor Syntax + +```razor + +``` + +### Web Forms Features NOT Supported + +- **CausesValidation** — Validation integration is not implemented for ButtonField. +- **ValidationGroup** — Not supported; use TemplateField with a Button component for validation scenarios. + +### Example + +```razor + + + + + + + + + +@code { + private List Products = new() + { + new Product { Name = "Widget", Price = 9.99m }, + new Product { Name = "Gadget", Price = 24.50m } + }; + + private void HandleRowCommand(GridViewCommandEventArgs e) + { + var commandName = e.CommandName; // "Select" or "Delete" + var rowIndex = e.CommandArgument; // Row index + } + + public class Product + { + public string Name { get; set; } + public decimal Price { get; set; } + } +} +``` + +--- + +## HyperLinkField + +Displays a hyperlink in each row of a data control. Both the URL and the display text can be data-bound or static. This is the Blazor equivalent of ``. + +### Features Supported in Blazor + +- Static or data-bound URL via `NavigateUrl` / `DataNavigateUrlFormatString` +- Static or data-bound display text via `Text` / `DataTextFormatString` +- Multiple data fields in URL and text format strings +- Target window/frame control + +### Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `DataNavigateUrlFields` | `string` | Comma-separated list of data field names used as arguments in `DataNavigateUrlFormatString`. | +| `DataNavigateUrlFormatString` | `string` | Format string for the URL (e.g., `"/products/{0}"`). | +| `DataTextField` | `string` | Data field used for the link display text. | +| `DataTextFormatString` | `string` | Format string for the display text. | +| `NavigateUrl` | `string` | Static URL. Overridden by `DataNavigateUrlFormatString` if specified. | +| `Target` | `string` | The target window or frame (e.g., `_blank`, `_self`). | +| `Text` | `string` | Static link text. Overridden by `DataTextFormatString` if specified. | +| `HeaderText` | `string` | Column header text. | +| `SortExpression` | `string` | Sort expression for the column. | +| `Visible` | `bool` | Show or hide the column. | + +### Web Forms Syntax + +```html + +``` + +### Blazor Syntax + +```razor + +``` + +### Example + +```razor + + + + + + + + +@code { + private List Products = new() + { + new Product { Id = 1, Name = "Widget", Price = 9.99m, Category = "Tools" }, + new Product { Id = 2, Name = "Gadget", Price = 24.50m, Category = "Electronics" } + }; + + public class Product + { + public int Id { get; set; } + public string Name { get; set; } + public decimal Price { get; set; } + public string Category { get; set; } + } +} +``` + +--- + +## Field Columns Not Yet Implemented + +The following ASP.NET Web Forms field column types are **not yet available** in this library: + +- **CommandField** — Provides Edit, Delete, and Select buttons. Use a [TemplateField](#templatefield) with [Button](../EditorControls/Button.md) or [LinkButton](../EditorControls/LinkButton.md) components as an alternative. +- **CheckBoxField** — Displays a checkbox for boolean values. Use a [TemplateField](#templatefield) with a [CheckBox](../EditorControls/CheckBox.md) component instead. +- **ImageField** — Displays an image from a URL field. Use a [TemplateField](#templatefield) with an [Image](../EditorControls/Image.md) component instead. + +!!! tip "TemplateField Workaround" + For any field type that is not yet implemented, `TemplateField` can be used to achieve the same result with full control over the rendered output. + + ```razor + @* CheckBoxField equivalent *@ + + + + + + + @* ImageField equivalent *@ + + + + + + + @* CommandField equivalent *@ + + + + + + + ``` + +## Migration Notes + +### Migrating from Web Forms + +1. **Remove `asp:` prefix** — Change `` to ``, `` to ``, etc. +2. **Remove `runat="server"`** — Not needed in Blazor. +3. **Replace `Eval()` / `Bind()`** — Use the template context variable instead. For example, `<%# Eval("Name") %>` becomes `@Item.Name` (with `Context="Item"` on the template). +4. **Replace `CommandField`** — Use `TemplateField` with `Button` or `LinkButton` components. +5. **Replace `CheckBoxField`** — Use `TemplateField` with a `CheckBox` component. +6. **Update `ButtonType`** — Web Forms uses string values (`"Button"`, `"Link"`, `"Image"`). Blazor uses `ButtonType.Button`, `ButtonType.Link`, `ButtonType.Image`. + +### Before (Web Forms) + +```html + + + + + + + + + + + + +``` + +### After (Blazor with BWFC) + +```razor + + + + + + + View + + + + + + + + + + +``` + +## See Also + +- [GridView](GridView.md) — The most common data control that uses field columns +- [DataGrid](DataGrid.md) — Legacy data grid with field column support +- [DetailsView](DetailsView.md) — Detail view using BoundField and TemplateField +- [Databinder](../UtilityFeatures/Databinder.md) — The data binding utility used by field columns diff --git a/docs/Migration/AutomatedMigration.md b/docs/Migration/AutomatedMigration.md index a6fa947f2..7ac1b5290 100644 --- a/docs/Migration/AutomatedMigration.md +++ b/docs/Migration/AutomatedMigration.md @@ -9,13 +9,54 @@ The BWFC migration system uses a three-layer pipeline: | Layer | Tool | Automation | What It Handles | |-------|------|-----------|-----------------| | **1. Scanner** | `bwfc-scan.ps1` | Inventory | Analyzes your Web Forms project and reports migration readiness | -| **2. Script** | `bwfc-migrate.ps1` | ~40% | Mechanical regex transforms (strip `asp:`, fix expressions, rename files) | -| **3. Copilot Skill** | `webforms-migration` | ~45% | Structural transforms (code-behind, data binding, lifecycle methods) | -| **4. Agent** | `migration.agent.md` | ~15% | Semantic decisions (Session→DI, Identity, EF Core, architecture) | +| **2. Script** | `bwfc-migrate.ps1` | ~60% | Mechanical transforms (strip `asp:`, fix expressions, rename files, copy static assets, convert template placeholders, detect JS/CSS) | +| **3. Copilot Skill** | `webforms-migration` | ~30% | Structural transforms (code-behind, data binding, lifecycle methods) | +| **4. Agent** | `migration.agent.md` | ~10% | Semantic decisions (Session→DI, Identity, EF Core, architecture) | !!! tip "Core Principle" Strip `asp:` and `runat="server"`, keep everything else, and it just works. BWFC components match Web Forms control names, property names, and rendered HTML. +!!! info "Pipeline Maturity" + Across 13 iterative benchmark runs on the WingtipToys reference application, the pipeline improved from a 56% acceptance test pass rate to **100%** — with total migration time dropping from ~2 hours to ~22 minutes. See [Migration Test Runs](../migration-tests/README.md) for the full history. + +## Render Mode: SSR by Default + +Migrated applications use **Static Server Rendering (SSR)** as the default render mode. This is the closest Blazor equivalent to how Web Forms pages work — pages render on the server during the HTTP request/response cycle, with full access to `HttpContext`, cookies, and session state. + +| Render Mode | When to Use | HttpContext Available? | +|-------------|-------------|----------------------| +| **SSR (default)** | Most pages — especially those that read cookies, session, or auth state | ✅ Yes | +| **InteractiveServer** | Pages that need real-time UI updates (e.g., chat, live dashboards) | ❌ No (`HttpContext` is null inside SignalR circuits) | + +To opt a specific page into interactive rendering, add the directive at the top of the `.razor` file: + +```razor +@rendermode InteractiveServer +``` + +!!! warning "SSR Enhanced Navigation" + In SSR mode, Blazor's enhanced navigation intercepts `` link clicks and fetches the target URL via `fetch()` instead of performing a full browser navigation. This is seamless for links between Blazor pages, but **breaks links to server-side API endpoints** (e.g., `/AddToCart`, `/account/logout-handler`) that return 302 redirects. + + Add `data-enhance-nav="false"` to any link targeting a non-Blazor endpoint: + + ```html + Add To Cart + ``` + +## Package Version Pinning + +NuGet package references in generated `.csproj` files must use **explicit stable versions** — never preview or wildcard versions. + +```xml + + + + + +``` + +The .NET 10 SDK preview is acceptable for building; NuGet package preview versions are not. + ## Prerequisites - [.NET 10 SDK](https://dotnet.microsoft.com/download) or later @@ -89,6 +130,11 @@ pwsh scripts/bwfc-migrate.ps1 -Path ./MyWebFormsApp -Output ./MyBlazorApp | Remove `
` | `` | (removed) | | Fix type params | `ItemType="NS.Class"` | `TItem="Class"` | | Remove dead attrs | `EnableViewState`, `AutoEventWireup` | (removed) | +| Detect CSS bundles | `` for CSS | `` tags in App.razor | +| Detect JS bundles | `` for JS | `)?' + foreach ($m in $cdnScriptRegex.Matches($headInner)) { $tag = $m.Value.Trim() - # Skip if already captured as a tag - if ($tag -match '^$') { + $tag = $tag + '' + } + $headContentTags.Add(" " + $tag) + Write-TransformLog -File $RelPath -Transform 'MasterPage' -Detail "Preserved CDN script: $($tag.Substring(0, [Math]::Min(80, $tag.Length)))" + } + + # Build the PageStyleSheet block (goes at top of layout, works correctly) + if ($pageStyleSheets.Count -gt 0) { + $pageStyleSheetBlock = ($pageStyleSheets -join "`n") + "`n" + Write-TransformLog -File $RelPath -Transform 'MasterPage' -Detail "Extracted $($pageStyleSheets.Count) CSS link(s) as components" } - if ($extractedTags.Count -gt 0) { - $headContentBlock = "`n" + ($extractedTags -join "`n") + "`n" - Write-TransformLog -File $RelPath -Transform 'MasterPage' -Detail "Extracted $($extractedTags.Count) head element(s) into " + # Build HeadContent block for non-CSS items (with migration note) + if ($headContentTags.Count -gt 0) { + $headContentBlock = "@* NOTE: Move this HeadContent to App.razor — HeadContent in layouts is ignored by Blazor *@`n" + $headContentBlock += "`n" + ($headContentTags -join "`n") + "`n`n" + Write-TransformLog -File $RelPath -Transform 'MasterPage' -Detail "Extracted $($headContentTags.Count) non-CSS head element(s) into with migration note" } # Remove the entire ... section @@ -827,16 +937,20 @@ function ConvertFrom-MasterPage { Write-TransformLog -File $RelPath -Transform 'MasterPage' -Detail 'Stripped document wrapper (DOCTYPE, html, body)' # 4. Replace → @Body - $mainCphRegex = [regex]'(?si)]*ID\s*=\s*"MainContent"[^>]*>.*?' - if ($mainCphRegex.IsMatch($Content)) { - $Content = $mainCphRegex.Replace($Content, '@Body') - Write-TransformLog -File $RelPath -Transform 'MasterPage' -Detail 'ContentPlaceHolder MainContent → @Body' - } - # Self-closing MainContent - $mainCphSelfRegex = [regex]'(?i)]*ID\s*=\s*"MainContent"[^>]*/>' - if ($mainCphSelfRegex.IsMatch($Content)) { - $Content = $mainCphSelfRegex.Replace($Content, '@Body') - Write-TransformLog -File $RelPath -Transform 'MasterPage' -Detail 'ContentPlaceHolder MainContent → @Body (self-closing)' + # Also handle common variants like ContentPlaceHolder1 + $mainCphPatterns = @('MainContent', 'ContentPlaceHolder1', 'BodyContent', 'Body') + foreach ($mainCphName in $mainCphPatterns) { + $mainCphRegex = [regex]"(?si)]*ID\s*=\s*`"$mainCphName`"[^>]*>.*?" + if ($mainCphRegex.IsMatch($Content)) { + $Content = $mainCphRegex.Replace($Content, '@Body') + Write-TransformLog -File $RelPath -Transform 'MasterPage' -Detail "ContentPlaceHolder $mainCphName → @Body" + } + # Self-closing variant + $mainCphSelfRegex = [regex]"(?i)]*ID\s*=\s*`"$mainCphName`"[^>]*/>" + if ($mainCphSelfRegex.IsMatch($Content)) { + $Content = $mainCphSelfRegex.Replace($Content, '@Body') + Write-TransformLog -File $RelPath -Transform 'MasterPage' -Detail "ContentPlaceHolder $mainCphName → @Body (self-closing)" + } } # Other ContentPlaceHolders → TODO comment @@ -860,10 +974,15 @@ function ConvertFrom-MasterPage { Write-ManualItem -File $RelPath -Category 'SelectMethod' -Detail 'SelectMethod detected — will be auto-converted to TODO annotation by ConvertFrom-SelectMethod' } - # 6. Inject @inherits LayoutComponentBase and HeadContent at the top + # 6. Inject @inherits LayoutComponentBase, PageStyleSheet, and any HeadContent at the top $header = "@inherits LayoutComponentBase`n" + # PageStyleSheet components go first (CSS for the layout) + if ($pageStyleSheetBlock) { + $header += "`n" + $pageStyleSheetBlock + } + # HeadContent (meta, scripts) goes after with migration note if ($headContentBlock) { - $header += "`n" + $headContentBlock + "`n" + $header += "`n" + $headContentBlock } $Content = $header + "`n" + $Content @@ -1035,7 +1154,7 @@ function ConvertFrom-GetRouteUrl { $routeNameMatches = $routeNameRegex.Matches($Content) foreach ($m in $routeNameMatches) { $routeName = $m.Groups[1].Value - Write-ManualItem -File $RelPath -Category 'GetRouteUrl' -Detail "Replace route name '$routeName' with direct URL pattern, e.g., /ProductDetails?ProductID=@Item.ProductID" + Write-ManualItem -File $RelPath -Category 'GetRouteUrl' -Detail "Replace GetRouteUrl('$routeName', ...) with direct NavigateTo or href URL pattern for the '$routeName' route" } return $Content @@ -1048,23 +1167,363 @@ function ConvertFrom-GetRouteUrl { function ConvertFrom-SelectMethod { param([string]$Content, [string]$RelPath) - # Match SelectMethod="MethodName" in any tag, capture the method name and insert a TODO after the tag - $selectMethodRegex = [regex]'(?si)(<[^>]*?)\s+SelectMethod\s*=\s*"([^"]+)"([^>]*>)' + # Match SelectMethod="MethodName" — PRESERVE the attribute in markup (BWFC supports it natively) + # and append a TODO noting the signature adaptation from 0-param to 4-param SelectHandler + $selectMethodRegex = [regex]'(?si)(<[^>]*?\s+SelectMethod\s*=\s*"([^"]+)"[^>]*>)' $selectMethodMatches = $selectMethodRegex.Matches($Content) if ($selectMethodMatches.Count -gt 0) { $Content = $selectMethodRegex.Replace($Content, { param($m) - $tagBeforeAttr = $m.Groups[1].Value + $fullTag = $m.Groups[1].Value $methodName = $m.Groups[2].Value - $tagAfterAttr = $m.Groups[3].Value - $serviceName = 'I' + $methodName.TrimStart('Get') + 'Service' - $varName = ($methodName.TrimStart('Get')).Substring(0,1).ToLower() + ($methodName.TrimStart('Get')).Substring(1) + 'Service' - "${tagBeforeAttr}${tagAfterAttr}`n@* TODO: Replace SelectMethod=""${methodName}"" with Items=""@_data"" parameter on this BWFC data control. Load _data in OnInitializedAsync: _data = await yourDbContext.YourEntities.ToListAsync(); *@" + "${fullTag}`n@* TODO: Adapt SelectMethod ""${methodName}"" to BWFC signature:`n IQueryable ${methodName}(int maxRows, int startRowIndex, string sortByExpression, out int totalRowCount)`n — add the 4 parameters to the existing method. See BWFC docs for SelectHandler. *@" }) - Write-TransformLog -File $RelPath -Transform 'SelectMethod' -Detail "Converted $($selectMethodMatches.Count) SelectMethod attribute(s) to TODO annotations" + Write-TransformLog -File $RelPath -Transform 'SelectMethod' -Detail "Preserved $($selectMethodMatches.Count) SelectMethod attribute(s); added BWFC signature TODO(s)" foreach ($m in $selectMethodMatches) { - Write-ManualItem -File $RelPath -Category 'SelectMethod' -Detail "SelectMethod='$($m.Groups[2].Value)' removed — needs service injection and OnInitializedAsync data loading" + Write-ManualItem -File $RelPath -Category 'SelectMethod' -Detail "SelectMethod='$($m.Groups[2].Value)' preserved — adapt method signature to BWFC SelectHandler (add 4 parameters: maxRows, startRowIndex, sortByExpression, out totalRowCount)" + } + } + + return $Content +} + +#endregion + +#region --- GridView Columns Wrapper --- + +function Wrap-GridViewColumns { + <# + .SYNOPSIS + Wraps BoundField, TemplateField, and other column field elements in when they appear + directly inside GridView without the Columns wrapper. + .DESCRIPTION + In ASP.NET Web Forms, field elements (BoundField, TemplateField, CommandField, etc.) can appear + directly inside GridView: + + + + + In BWFC Blazor, these must be wrapped in a element: + + + + + + + This function finds GridView elements that contain field elements NOT already wrapped in Columns + and adds the Columns wrapper. It handles both asp:-prefixed and unprefixed tags. + + Must run BEFORE ConvertFrom-AspPrefix so asp: prefix is still present for detection. + #> + param( + [string]$Content, + [string]$RelPath + ) + + # Field types that should be inside + $fieldTypes = @( + 'BoundField', + 'TemplateField', + 'ButtonField', + 'HyperLinkField', + 'ImageField', + 'CommandField', + 'CheckBoxField' + ) + $fieldPattern = ($fieldTypes -join '|') + + # Match GridView blocks (both asp:GridView and GridView after prefix removal) + # Use a non-greedy match for the inner content + $gridViewRegex = [regex]"(?si)(<(?:asp:)?GridView\b[^>]*>)(.*?)()" + $gridViewMatches = $gridViewRegex.Matches($Content) + + if ($gridViewMatches.Count -eq 0) { return $Content } + + $wrapCount = 0 + + # Process in reverse order to preserve string positions + for ($i = $gridViewMatches.Count - 1; $i -ge 0; $i--) { + $m = $gridViewMatches[$i] + $openTag = $m.Groups[1].Value + $innerContent = $m.Groups[2].Value + $closeTag = $m.Groups[3].Value + + # Check if inner content has field elements that are NOT inside + # First, check if already exists + if ($innerContent -match '(?si) + # Strategy: Find all field elements and style elements, wrap only the fields + + # Collect field elements (opening and self-closing tags with their content) + $fieldsToWrap = [System.Collections.Generic.List[string]]::new() + $remainingContent = $innerContent + + # Match each field type — both self-closing and open+close variants + foreach ($fieldType in $fieldTypes) { + # Self-closing: + $selfClosingRegex = [regex]"(?si)<(?:asp:)?$fieldType\b[^>]*/>" + foreach ($fm in $selfClosingRegex.Matches($remainingContent)) { + $fieldsToWrap.Add($fm.Value) + } + $remainingContent = $selfClosingRegex.Replace($remainingContent, "<<>>") + + # Open+close: ... + $openCloseRegex = [regex]"(?si)<(?:asp:)?$fieldType\b[^>]*>.*?" + foreach ($fm in $openCloseRegex.Matches($remainingContent)) { + $fieldsToWrap.Add($fm.Value) + } + $remainingContent = $openCloseRegex.Replace($remainingContent, "<<>>") + } + + if ($fieldsToWrap.Count -eq 0) { + continue + } + + # Now rebuild the inner content: + # - Find where the first field placeholder is and insert before it + # - Find where the last field placeholder is and insert after it + + # Extract non-field content before, between, and after field placeholders + $parts = [regex]::Split($remainingContent, '<<>>') + + # Reconstruct: non-field-stuff + + all-fields + + remaining-non-field-stuff + $beforeFields = $parts[0] + $afterFields = if ($parts.Count -gt 1) { $parts[$parts.Count - 1] } else { '' } + + # Join all fields + $fieldsBlock = $fieldsToWrap -join "`n " + + # Determine indentation from inner content + $indent = '' + if ($innerContent -match '(?m)^(\s+)<(?:asp:)?(?:BoundField|TemplateField)') { + $indent = $Matches[1] + } + elseif ($innerContent -match '(?m)^(\s+)<') { + $indent = $Matches[1] + } + + $newInnerContent = $beforeFields.TrimEnd() + "`n$indent`n$indent " + $fieldsBlock + "`n$indent" + $afterFields + + # Replace the GridView in the content + $newGridView = $openTag + $newInnerContent + $closeTag + $Content = $Content.Substring(0, $m.Index) + $newGridView + $Content.Substring($m.Index + $m.Length) + $wrapCount++ + } + + if ($wrapCount -gt 0) { + Write-TransformLog -File $RelPath -Transform 'GridViewColumns' -Detail "Wrapped field elements in for $wrapCount GridView(s)" + } + + return $Content +} + +#endregion + +#region --- GridView/DetailsView Style Element Conversion --- + +function Convert-StyleElementsInSection { + <# + .SYNOPSIS + Helper to convert style elements in a content section, excluding nested protected areas. + #> + param( + [string]$Content, + [hashtable]$Mappings, + [string[]]$UnsupportedStyles, + [string]$RelPath, + [ref]$ConversionCount + ) + + # Protect ... and ... sections from conversion + # because style elements inside columns/fields are per-column styles, not grid-level styles + $columnsRegex = [regex]'(?si)(]*>.*?)' + $fieldsRegex = [regex]'(?si)(]*>.*?)' + + # Replace protected sections with placeholders + $protectedSections = @{} + $placeholderIndex = 0 + + foreach ($cm in $columnsRegex.Matches($Content)) { + $placeholder = "<<>>" + $protectedSections[$placeholder] = $cm.Value + $Content = $Content.Substring(0, $cm.Index) + $placeholder + $Content.Substring($cm.Index + $cm.Length) + $placeholderIndex++ + # Re-match since content changed + break + } + while ($columnsRegex.IsMatch($Content)) { + $cm = $columnsRegex.Match($Content) + $placeholder = "<<>>" + $protectedSections[$placeholder] = $cm.Value + $Content = $Content.Substring(0, $cm.Index) + $placeholder + $Content.Substring($cm.Index + $cm.Length) + $placeholderIndex++ + } + while ($fieldsRegex.IsMatch($Content)) { + $cm = $fieldsRegex.Match($Content) + $placeholder = "<<>>" + $protectedSections[$placeholder] = $cm.Value + $Content = $Content.Substring(0, $cm.Index) + $placeholder + $Content.Substring($cm.Index + $cm.Length) + $placeholderIndex++ + } + + # Now convert style elements in the unprotected content + foreach ($oldName in $Mappings.Keys) { + $mapping = $Mappings[$oldName] + $wrapperName = $mapping.Wrapper + $componentName = $mapping.Component + + # Self-closing + $selfClosingRegex = [regex]"(?s)<$oldName\b([^>]*)(/\s*>)" + while ($selfClosingRegex.IsMatch($Content)) { + $sm = $selfClosingRegex.Match($Content) + $attrs = $sm.Groups[1].Value + $replacement = "<$wrapperName><$componentName$attrs />" + $Content = $Content.Substring(0, $sm.Index) + $replacement + $Content.Substring($sm.Index + $sm.Length) + $ConversionCount.Value++ + } + + # Open+close + $openCloseRegex = [regex]"(?s)<$oldName\b([^>]*)>(.*?)" + while ($openCloseRegex.IsMatch($Content)) { + $sm = $openCloseRegex.Match($Content) + $attrs = $sm.Groups[1].Value + $innerStyleContent = $sm.Groups[2].Value + $replacement = "<$wrapperName><$componentName$attrs>$innerStyleContent" + $Content = $Content.Substring(0, $sm.Index) + $replacement + $Content.Substring($sm.Index + $sm.Length) + $ConversionCount.Value++ + } + } + + # Comment out unsupported style elements + foreach ($unsupportedStyle in $UnsupportedStyles) { + $selfClosingRegex = [regex]"(<$unsupportedStyle\b[^>]*/?>)" + if ($selfClosingRegex.IsMatch($Content)) { + $Content = $selfClosingRegex.Replace($Content, "@* TODO: <$unsupportedStyle> is not supported by BWFC — apply via CSS. `$1 *@") + Write-ManualItem -File $RelPath -Category 'UnsupportedStyle' -Detail "<$unsupportedStyle> not supported by BWFC — use CSS to style sorted columns" + } + } + + # Restore protected sections + foreach ($placeholder in $protectedSections.Keys) { + $Content = $Content.Replace($placeholder, $protectedSections[$placeholder]) + } + + return $Content +} + +function Convert-GridViewStyleElements { + <# + .SYNOPSIS + Converts GridView and DetailsView style child elements to BWFC format. + .DESCRIPTION + In ASP.NET Web Forms, GridView style elements use short names: + + + In BWFC, these must be wrapped in a *StyleContent RenderFragment containing the prefixed component: + + + + + Style elements INSIDE or are NOT converted because they are + per-column styles which BWFC doesn't support via child components. + + Must run AFTER asp: prefix has been stripped. + #> + param( + [string]$Content, + [string]$RelPath + ) + + # GridView style element mappings (Web Forms name -> wrapper name, component name) + $gridViewStyleMappings = @{ + 'AlternatingRowStyle' = @{ Wrapper = 'AlternatingRowStyleContent'; Component = 'GridViewAlternatingRowStyle' } + 'EditRowStyle' = @{ Wrapper = 'EditRowStyleContent'; Component = 'GridViewEditRowStyle' } + 'EmptyDataRowStyle' = @{ Wrapper = 'EmptyDataRowStyleContent'; Component = 'GridViewEmptyDataRowStyle' } + 'FooterStyle' = @{ Wrapper = 'FooterStyleContent'; Component = 'GridViewFooterStyle' } + 'HeaderStyle' = @{ Wrapper = 'HeaderStyleContent'; Component = 'GridViewHeaderStyle' } + 'PagerStyle' = @{ Wrapper = 'PagerStyleContent'; Component = 'GridViewPagerStyle' } + 'RowStyle' = @{ Wrapper = 'RowStyleContent'; Component = 'GridViewRowStyle' } + 'SelectedRowStyle' = @{ Wrapper = 'SelectedRowStyleContent'; Component = 'GridViewSelectedRowStyle' } + } + + # Unsupported GridView style elements (comment out with TODO) + $unsupportedGridViewStyles = @( + 'SortedAscendingCellStyle', + 'SortedAscendingHeaderStyle', + 'SortedDescendingCellStyle', + 'SortedDescendingHeaderStyle' + ) + + # DetailsView style element mappings + $detailsViewStyleMappings = @{ + 'AlternatingRowStyle' = @{ Wrapper = 'AlternatingRowStyleContent'; Component = 'DetailsViewAlternatingRowStyle' } + 'CommandRowStyle' = @{ Wrapper = 'CommandRowStyleContent'; Component = 'DetailsViewCommandRowStyle' } + 'EditRowStyle' = @{ Wrapper = 'EditRowStyleContent'; Component = 'DetailsViewEditRowStyle' } + 'EmptyDataRowStyle' = @{ Wrapper = 'EmptyDataRowStyleContent'; Component = 'DetailsViewEmptyDataRowStyle' } + 'FieldHeaderStyle' = @{ Wrapper = 'FieldHeaderStyleContent'; Component = 'DetailsViewFieldHeaderStyle' } + 'FooterStyle' = @{ Wrapper = 'FooterStyleContent'; Component = 'DetailsViewFooterStyle' } + 'HeaderStyle' = @{ Wrapper = 'HeaderStyleContent'; Component = 'DetailsViewHeaderStyle' } + 'InsertRowStyle' = @{ Wrapper = 'InsertRowStyleContent'; Component = 'DetailsViewInsertRowStyle' } + 'PagerStyle' = @{ Wrapper = 'PagerStyleContent'; Component = 'DetailsViewPagerStyle' } + 'RowStyle' = @{ Wrapper = 'RowStyleContent'; Component = 'DetailsViewRowStyle' } + } + + $totalConversions = 0 + + # Process GridView blocks + $gridViewRegex = [regex]"(?si)(]*>)(.*?)()" + $gridViewMatches = $gridViewRegex.Matches($Content) + + # Process in reverse order + for ($i = $gridViewMatches.Count - 1; $i -ge 0; $i--) { + $m = $gridViewMatches[$i] + $openTag = $m.Groups[1].Value + $innerContent = $m.Groups[2].Value + $closeTag = $m.Groups[3].Value + + $convCount = [ref]0 + $newInnerContent = Convert-StyleElementsInSection -Content $innerContent -Mappings $gridViewStyleMappings -UnsupportedStyles $unsupportedGridViewStyles -RelPath $RelPath -ConversionCount $convCount + + if ($convCount.Value -gt 0 -or $newInnerContent -ne $innerContent) { + $newGridView = $openTag + $newInnerContent + $closeTag + $Content = $Content.Substring(0, $m.Index) + $newGridView + $Content.Substring($m.Index + $m.Length) + $totalConversions += $convCount.Value + } + } + + # Process DetailsView blocks + $detailsViewRegex = [regex]"(?si)(]*>)(.*?)()" + $detailsViewMatches = $detailsViewRegex.Matches($Content) + + for ($i = $detailsViewMatches.Count - 1; $i -ge 0; $i--) { + $m = $detailsViewMatches[$i] + $openTag = $m.Groups[1].Value + $innerContent = $m.Groups[2].Value + $closeTag = $m.Groups[3].Value + + $convCount = [ref]0 + $newInnerContent = Convert-StyleElementsInSection -Content $innerContent -Mappings $detailsViewStyleMappings -UnsupportedStyles @() -RelPath $RelPath -ConversionCount $convCount + + if ($convCount.Value -gt 0 -or $newInnerContent -ne $innerContent) { + $newDetailsView = $openTag + $newInnerContent + $closeTag + $Content = $Content.Substring(0, $m.Index) + $newDetailsView + $Content.Substring($m.Index + $m.Length) + $totalConversions += $convCount.Value + } + } + + if ($totalConversions -gt 0) { + Write-TransformLog -File $RelPath -Transform 'StyleElements' -Detail "Converted $totalConversions GridView/DetailsView style element(s) to BWFC format" } return $Content @@ -1129,12 +1588,55 @@ function Remove-WebFormsAttributes { } } - # ItemType="Namespace.Class" → TItem="Class" + # ItemType="Namespace.Class" → ItemType="Class" (strip namespace only, keep attribute name) + # BWFC components (GridView, DetailsView, FormView, ListView, etc.) use ItemType, not TItem + # Only list controls (DropDownList, BulletedList, CheckBoxList, ListBox, RadioButtonList) use TItem $itemTypeRegex = [regex]'ItemType="(?:[^"]*\.)?([^"]+)"' $itemTypeMatches = $itemTypeRegex.Matches($Content) if ($itemTypeMatches.Count -gt 0) { - $Content = $itemTypeRegex.Replace($Content, 'TItem="$1"') - Write-TransformLog -File $RelPath -Transform 'Attribute' -Detail "Converted $($itemTypeMatches.Count) ItemType to TItem" + $Content = $itemTypeRegex.Replace($Content, 'ItemType="$1"') + Write-TransformLog -File $RelPath -Transform 'Attribute' -Detail "Simplified $($itemTypeMatches.Count) ItemType attribute(s) (stripped namespace)" + } + + return $Content +} + +function ConvertFrom-ButtonOnClick { + <# + .SYNOPSIS + Preserves Web Forms OnClick="HandlerName" on BWFC Button elements. + .DESCRIPTION + In Web Forms, wires the button to a server-side handler. + BWFC Button components use OnClick as an EventCallback parameter, so the attribute is preserved + as-is (no conversion to @onclick needed). + + This function logs OnClick handlers found on Button/LinkButton/ImageButton elements and reminds + the developer to verify the handler signature in the code-behind. + #> + param( + [string]$Content, + [string]$RelPath + ) + + # Match OnClick="HandlerName" within Button, LinkButton, or ImageButton tags (after asp: prefix removal) + # BWFC uses OnClick as an EventCallback parameter, so we keep it as-is + $onClickRegex = [regex]'(<(?:Button|LinkButton|ImageButton)\s+[^>]*?)OnClick="([^"]+)"' + $onClickMatches = $onClickRegex.Matches($Content) + + if ($onClickMatches.Count -gt 0) { + $handlers = @() + foreach ($m in $onClickMatches) { + $handlers += $m.Groups[2].Value + } + + # No replacement needed - BWFC Button uses OnClick as EventCallback parameter + Write-TransformLog -File $RelPath -Transform 'ButtonHandler' -Detail "Found $($onClickMatches.Count) OnClick handler(s) — preserved for BWFC EventCallback" + + # Log unique handlers as manual items for verification + $uniqueHandlers = $handlers | Select-Object -Unique + foreach ($h in $uniqueHandlers) { + Write-ManualItem -File $RelPath -Category 'ButtonHandler' -Detail "OnClick=""$h"" preserved. Verify handler exists in code-behind as: private void $h(MouseEventArgs e) { }" + } } return $Content @@ -1157,6 +1659,65 @@ function ConvertFrom-UrlReferences { } } + # Convert relative .aspx hrefs to rooted paths without extension + # e.g., href="Home.aspx" → href="/Home", href="Account/Login.aspx" → href="/Account/Login" + # But NOT hrefs with ~/ (already handled) or absolute URLs or anchors + $relativeAspxRegex = [regex]'href="(?!~/|https?://|/|#)([^"]*?)\.aspx"' + $relativeAspxMatches = $relativeAspxRegex.Matches($Content) + if ($relativeAspxMatches.Count -gt 0) { + $Content = $relativeAspxRegex.Replace($Content, 'href="/$1"') + Write-TransformLog -File $RelPath -Transform 'URL' -Detail "Converted $($relativeAspxMatches.Count) relative .aspx href(s) to rooted paths" + } + + return $Content +} + +function ConvertFrom-ColorAttributes { + <# + .SYNOPSIS + Converts Web Forms color attributes to Razor-safe @("value") syntax. + .DESCRIPTION + In Razor, an attribute like BackColor="White" is interpreted as the C# variable White, + and ForeColor="#333333" triggers a preprocessor directive error because # is a C# token. + + This function converts color attribute values to explicit Razor strings using @("value") + syntax, which the BWFC WebColor type can then parse (it has implicit string conversion). + + Handled attributes: BackColor, ForeColor, BorderColor, HeaderBackColor, HeaderForeColor, + RowBackColor, RowForeColor, AlternatingRowBackColor, AlternatingRowForeColor + #> + param( + [string]$Content, + [string]$RelPath + ) + + # Color attribute names used in Web Forms GridView, DetailsView, FormView, etc. + $colorAttributes = @( + 'BackColor', + 'ForeColor', + 'BorderColor' + ) + + $totalConverted = 0 + + foreach ($attr in $colorAttributes) { + # Match AttributeName="value" where value is not already a Razor expression + # Captures: $1 = attribute name, $2 = value (without @) + $pattern = "(?$attr)\s*=\s*`"(?[^`"@][^`"]*)`"" + $regex = [regex]$pattern + + $matches = $regex.Matches($Content) + if ($matches.Count -gt 0) { + # Replace with AttributeName=@("value") format + $Content = $regex.Replace($Content, '${attr}=@("${val}")') + $totalConverted += $matches.Count + } + } + + if ($totalConverted -gt 0) { + Write-TransformLog -File $RelPath -Transform 'ColorAttr' -Detail "Converted $totalConverted color attribute(s) to Razor-safe @(`"value`") syntax" + } + return $Content } @@ -1206,7 +1767,7 @@ function Copy-CodeBehind { $rdRegex = [regex]'\[RouteData\]' $rdMatches = $rdRegex.Matches($annotatedContent) if ($rdMatches.Count -gt 0) { - $annotatedContent = $rdRegex.Replace($annotatedContent, "[Parameter] // TODO: Verify RouteData → [Parameter] conversion — ensure @page route template has matching {parameter}") + $annotatedContent = $rdRegex.Replace($annotatedContent, "// TODO: Verify RouteData → [Parameter] conversion — ensure @page route has matching {parameter}`n[Parameter]") Write-TransformLog -File $RelPath -Transform 'ParameterAttr' -Detail "Converted $($rdMatches.Count) [RouteData] to [Parameter]" } @@ -1230,13 +1791,21 @@ function Copy-CodeBehind { function Test-UnconvertiblePage { param([string]$Content) + # Patterns that indicate the page contains complex Identity/Auth/Payment logic + # that requires manual migration (not just UI references) + # NOTE: These patterns should match code/API patterns, NOT just text content + # (e.g., "PayPal" in alt text should not trigger stubbing) $unconvertiblePatterns = @( 'SignInManager', 'UserManager', 'FormsAuthentication', - 'Session\[', - 'PayPal', - 'Checkout' + # Payment SDK/API patterns (actual code, not UI text) + # Match: PayPalService, PayPal.Api, StripeClient, etc. + # Don't match: "Check out with PayPal" (UI text) + '(PayPal|Stripe|Braintree|Square|Adyen)\.(Api|Client|Service|SDK|Gateway|Payment)', + '(PayPal|Stripe|Braintree|Square|Adyen)(Service|Client|Gateway|Handler|Api)\b', + 'new\s+(PayPal|Stripe|Braintree|Square|Adyen)', + '(ProcessPayment|ChargeCard|CreateCharge|CapturePayment|PaymentGateway|IPayment)' ) foreach ($pat in $unconvertiblePatterns) { if ($Content -match $pat) { @@ -1343,6 +1912,495 @@ function Convert-TemplatePlaceholders { return $Content } +#endregion +#region --- Nav Link ID Generation --- + +function Add-NavLinkIds { + <# + .SYNOPSIS + Adds id attributes to navigation links that don't already have them. + .DESCRIPTION + Navigation links in Web Forms master pages often have id attributes for testing and + JavaScript targeting. This function ensures all links inside common navigation + containers (