Skip to content

feat: P1-P5 Custom Controls Framework Drop-in Replacement for Web Forms Controls#489

Merged
csharpfritz merged 33 commits intoFritzAndFriends:devfrom
csharpfritz:feature/ascx-sample-milestone
Mar 23, 2026
Merged

feat: P1-P5 Custom Controls Framework Drop-in Replacement for Web Forms Controls#489
csharpfritz merged 33 commits intoFritzAndFriends:devfrom
csharpfritz:feature/ascx-sample-milestone

Conversation

@csharpfritz
Copy link
Copy Markdown
Collaborator

@csharpfritz csharpfritz commented Mar 21, 2026

Summary

Complete implementation of the P1-P5 Custom Controls "drop-in replacement" framework, enabling ASP.NET Web Forms custom controls to work in Blazor with minimal code changes. The primary migration change is using System.Web.UI; using BlazorWebFormsComponents.CustomControls;.

What's Included

Phase 1 (P2): TagKey + AddAttributesToRender (#492 )

  • WebControl.TagKey property (default: Span, matching Web Forms)
  • AddAttributesToRender(HtmlTextWriter) virtual method auto-adds ID, CssClass, Style, ToolTip, Enabled
  • RenderBeginTag / RenderEndTag pipeline refactored to match Web Forms Render() flow

Phase 2 (P3): HtmlTextWriter Enum Expansion (#493 )

  • +57 tags (HTML5 semantic: nav, section, article, header, footer, etc.)
  • +43 attributes (HTML5 form: placeholder, required, autofocus + ARIA: role, aria-label, etc.)
  • +65 styles (Flexbox, Grid, Transform, Animation, CSS3)

Phase 3 (P1): DataBoundWebControl (#490 )

  • Non-generic DataBoundWebControl + generic DataBoundWebControl<T> base classes
  • Bridges DataSource parameter binding with HtmlTextWriter rendering pipeline
  • PerformDataBinding(IEnumerable) virtual method for subclass consumption

Phase 4 (P4): CompositeControl + Shim Types (#491 )

  • LiteralControl renders raw text/HTML (no outer tag)
  • Panel renders <div> with child controls
  • PlaceHolder invisible container (no tag output)
  • HtmlGenericControl renders any HTML tag
  • INamingContainer marker interface
  • CompositeControl fixed for graceful non-WebControl child handling

Phase 5 (P5): TemplatedWebControl (#494 )

  • TemplatedWebControl base class with placeholder-based RenderFragmentHtmlTextWriter interleaving
  • RenderTemplate(HtmlTextWriter, RenderFragment) helper using <!--BWFC_TPL_N--> markers
  • Enables controls that mix HtmlTextWriter output with Blazor template regions

Phase 6: FindControlRecursive (#496 )

  • FindControlRecursive(string id) on BaseWebFormsComponent
  • Deep traversal search for nested controls by ID

Focus() Method

  • public virtual void Focus() on BaseWebFormsComponent
  • Fire-and-forget JS interop (bwfc.Page.Focus(clientId))
  • SSR-safe (null-guards for JsRuntime)

Test Coverage

  • 48 new bUnit tests across 4 test files
  • 2515 total tests passing, 0 failures
  • Tests cover TagKey pipeline, data binding, shim controls, template interleaving

Documentation

  • dev-docs/proposals/p1-p5-custom-controls-framework.md 661-line comprehensive developer doc
  • docs/Migration/User-Controls.md ASCX Razor migration guide
  • docs/Migration/FindControl-Migration.md FindControl patterns
  • docs/Migration/CustomControl-BaseClasses.md Base class migration reference

Breaking Changes

None. All changes are additive. Existing WebControl subclasses that override Render() continue to work unchanged.

Related Issues

Closes #490, #491, #492, #493, #494, #496
Progress on #495

csharpfritz and others added 4 commits March 21, 2026 09:52
Forge scoped a .NET Framework 4.8 sample app featuring 12 ASCX user
controls, 3 custom base classes, and 14 pages to test migration
toolkit coverage for enterprise Web Forms patterns.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ssion

Session: 2026-03-21T14-35-35Z-custom-controls-plan
Requested by: Jeffrey T. Fritz

Changes:
- Logged orchestration: Forge session expanding DepartmentPortal with 6 custom server controls
- Logged session: Custom controls design (WebControl, CompositeControl, templated, data-bound, postback, custom events)
- Merged decisions: User directive + Forge decision for custom controls milestone scope
- Merged 2 decision files from inbox into decisions.md, deleted inbox files
… specs

Added section 3.7 DepartmentBreadcrumb  inherits directly from System.Web.UI.Control.
Demonstrates pure Render() override, zero ViewState, custom BreadcrumbEventArgs.
Updated executive summary: 7 custom server controls covering Control, WebControl,
CompositeControl, DataBoundControl, ITemplate, IPostBackEventHandler, and custom events.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Phase 1 Foundation (28 files):
- .NET Framework 4.8 Web Application Project with old-style .csproj
- 6 model POCOs + static PortalDataProvider (in-memory data)
- 3 base classes (BasePage, BaseMasterPage, BaseUserControl)
- Site.Master with Bootstrap 3 CDN, Default/Login/Dashboard pages
- Site.css with portal component styles

Phase 2 ASCX Controls (24 files):
- 12 user controls: Breadcrumb, PageHeader, Footer, AnnouncementCard,
  EmployeeList, TrainingCatalog, SearchBox, DepartmentFilter, Pager,
  DashboardWidget, ResourceBrowser, QuickStats
- Patterns: data-bound, event-driven, nested ASCX, web.config tagPrefix
- Custom EventArgs: SearchEventArgs, NotificationEventArgs, BreadcrumbEventArgs

Builds successfully with MSBuild. Phases 3-4 (custom server controls,
remaining pages) to follow.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<system.web>
<compilation debug="true" targetFramework="4.8" />

Check failure

Code scanning / CodeQL

Creating an ASP.NET debug binary may reveal sensitive information High

The 'debug' flag is set for an ASP.NET configuration file.
csharpfritz and others added 11 commits March 21, 2026 20:06
Phase 3 Custom Server Controls (7 files):
- StarRating.cs (WebControl)  1-5 star rating display
- EmployeeCard.cs (CompositeControl)  programmatic child controls
- SectionPanel.cs (ITemplate)  templated container with Header/Content/Footer
- PollQuestion.cs (IPostBackEventHandler)  interactive voting control
- NotificationBell.cs (WebControl + custom events)  bell icon with drawer
- EmployeeDataGrid.cs (DataBoundControl)  searchable/sortable/pageable grid
- DepartmentBreadcrumb.cs (bare Control)  pure Render() override

Phase 4 Pages (22 files, 11 pages):
- Employees.aspx  directory with search, filter, EmployeeDataGrid
- EmployeeDetail.aspx  single employee with EmployeeCard + StarRating
- Announcements.aspx  listing with SectionPanel wrapper
- AnnouncementDetail.aspx  single announcement view
- Training.aspx  catalog with PollQuestion, enrollment to Session
- MyTraining.aspx  enrolled courses from Session
- Resources.aspx  library with ResourceBrowser + SectionPanel
- ResourceDetail.aspx  single resource view
- Admin/ManageAnnouncements.aspx  admin CRUD
- Admin/ManageTraining.aspx  admin CRUD
- Admin/ManageEmployees.aspx  admin grid with EmployeeDataGrid

All custom controls registered in Web.config. Builds with 0 errors.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Renamed App_Code/ to Code/ (prevents ASP.NET double-compilation)
- Switched all pages from CodeFile to CodeBehind (WAP model)
- Added back protected field declarations for code-behind controls
- Restored code-behind files as Compile items in .csproj
- Fixed SectionPanel: removed non-existent IsCollapsible/IsExpanded attrs
- Fixed PollQuestion: Question  QuestionText, OnAnswerSubmitted  OnVoteSubmitted
- Fixed TrainingCatalog event handler: EventArgs  int (EnrollmentRequested)

All 14 pages now return 200 OK under IIS Express.
Tested: Default, Login, Dashboard, Employees, EmployeeDetail,
Announcements, AnnouncementDetail, Training, MyTraining, Resources,
ResourceDetail, Admin/ManageAnnouncements, Admin/ManageTraining,
Admin/ManageEmployees.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Session: 20260322T003035Z-departmentportal-phases3-5
Requested by: Scribe

Changes:
- Logged orchestration: Phase 3 custom controls, Phase 4 ASPX pages, IIS fixes
- Logged session: DepartmentPortal phases 3-5 completion summary
- Merged decision inbox (4 phase decisions) into decisions.md
- Deleted inbox files after merge
- No duplicates found; all 4 phase decisions are unique

Outcome: All phases (3, 4, IIS fixes) complete. 7 custom controls, 11 ASPX pages,
12 ASCX controls. All 14 pages return HTTP 200 OK in IIS Express.
Nerdbank.GitVersioning from Directory.Build.props auto-generates
AssemblyVersion and AssemblyFileVersion attributes. The manual ones
in AssemblyInfo.cs caused CS0579 duplicate attribute errors.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…binding

- Site.Master: Replace asp:LoginStatus with session-aware HyperLink/LinkButton
  for login state display (FormsAuthentication not configured)
- SectionPanel.cs: Add [ParseChildren(true)] attribute so ASP.NET page compiler
  treats <ContentTemplate> as ITemplate property instead of literal HTML
- SectionPanel.cs: Add public EnsureChildControls() wrapper for page code access
- Announcements.aspx.cs: Use OnPreRender + EnsureChildControls + FindControl
  pattern for SectionPanel template data binding
- Resources.aspx.cs: Same PreRender pattern; fix category filters to use
  FileType (PDF/DOCX/XLSX/PPTX) instead of non-existent Category values
- EmployeeList.ascx.cs: Move grid binding to Page_PreRender so parent page
  search events (which fire after Page_Load) take effect
- TrainingCatalog.ascx.cs: Same PreRender binding fix
- Training.aspx.cs: Use protected fields instead of FindControl (which fails
  across MasterPage naming containers); move binding to OnPreRender

All pages verified: 200 OK with data, search works on Employees/Announcements.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…OnPreRender

Same pattern as Training.aspx.cs fix  FindControl fails across MasterPage
naming containers. Protected fields are auto-wired by ASP.NET runtime.
Moved BindEmployees() to OnPreRender; event handlers now only set state.

Verified: search for 'Alice' correctly filters from 20 to 1 employee.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…r DepartmentPortal analysis

Session: 2026-03-22-departmentportal-migration-analysis
Requested by: Scribe

Changes:
- Added 3 orchestration logs (Jubilee, Forge, Bishop) to .squad/orchestration-log/
- Added session summary log to .squad/log/
- Merged 2 decision files from inbox into .squad/decisions.md
- Deleted merged inbox files
- Deduplication: No duplicates found

Agents: Jubilee (bug fix), Forge (control gap analysis), Bishop (prescan analysis)
Decision: DepartmentPortal migration readiness 7.5/10  GO
…ployeeList

- EmployeeDataGrid.RenderContents now reads actual Employee properties from bound dataItems instead of hardcoded placeholder text
- Removed duplicate EmployeeList ASCX from Employees.aspx
- Removed unused EmployeeListControl field from code-behind

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…nalysis

- AfterDepartmentPortal: 31 files, net10.0 Blazor SSR with BWFC reference
  - 7 stub pages (Dashboard, Employees, Announcements, Training, Resources, details)
  - 12 shared components migrated from ASCX user controls
  - Models + PortalDataProvider identical to DepartmentPortal Before app
  - MainLayout.razor from Site.Master, full project builds clean
- Solution file updated with AfterDepartmentPortal project
- 44KB migration analysis by Forge covering ASCX + custom control gaps
- Top BWFC improvements identified: DataBoundWebControl<T>, TagKey/AddAttributesToRender

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Session: 2026-03-22-migration-docs-and-issues
Requested by: User

Changes:
- Orchestration logs for Beast (migration docs) and Forge (upstream issues)
- Session log documenting documentation completion + issue specifications
- Merged Beast and Forge decisions from inbox to decisions.md
- Updated forge/history.md with upstream issue numbers (FritzAndFriends#490-FritzAndFriends#496)
- Deleted merged inbox files
Implement 6 phases of custom control shimming for Web Forms  Blazor migration:

Phase 1 (P2 FritzAndFriends#492): TagKey + AddAttributesToRender on WebControl
- TagKey property (default Span), TagName accessor
- AddAttributesToRender virtual method (ID, CssClass, Style, ToolTip, Enabled)
- RenderBeginTag/RenderEndTag pipeline matching Web Forms rendering lifecycle
- Backward compatible with controls overriding Render() directly

Phase 2 (P3 FritzAndFriends#493): HtmlTextWriter enum expansion
- 57 new HtmlTextWriterTag members (HTML5 semantic, media, structural)
- 43 new HtmlTextWriterAttribute members (form, ARIA, state, linking)
- 65 new HtmlTextWriterStyle members (flexbox, grid, visual, position, text)
- Fallback ToString().ToLowerInvariant() for resilience

Phase 3 (P1 FritzAndFriends#490): DataBoundWebControl + DataBoundWebControl<T>
- Bridges WebControl rendering (TagKey) with data binding (DataSource)
- PerformDataBinding(IEnumerable) virtual for subclass data consumption
- Generic version provides TypedDataItems for compile-time type safety

Phase 4 (P4 FritzAndFriends#491): CompositeControl fix + shim types
- LiteralControl/Literal: raw text rendering without outer tag
- Panel (div), PlaceHolder (invisible), HtmlGenericControl (any tag)
- INamingContainer marker interface
- CompositeControl.RenderChildren graceful fallback for non-WebControl children

Phase 5 (P5 FritzAndFriends#494): TemplatedWebControl (ITemplate  RenderFragment bridge)
- RenderTemplate() inserts RenderFragment into HtmlTextWriter output
- Placeholder-based interleaving in BuildRenderTree
- Null template graceful no-op

Phase 6 (FritzAndFriends#496): FindControlRecursive
- Deep search across naming container boundaries
- Added to BaseWebFormsComponent alongside existing flat FindControl

Tests: 48 new bUnit tests (123 total custom control tests passing)
Docs: 3 migration guides (User-Controls, FindControl, CustomControl-BaseClasses)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
// Emit any remaining markup after the last placeholder
if (!string.IsNullOrEmpty(remaining))
{
builder.AddMarkupContent(sequence++, remaining);

Check warning

Code scanning / CodeQL

Useless assignment to local variable Warning

This assignment to
sequence
is useless, since its value is never read.
{
public class SimpleDataList : DataBoundWebControl
{
private List<string> _items = new();

Check notice

Code scanning / CodeQL

Missed 'readonly' opportunity Note test

Field '_items' can be 'readonly'.
csharpfritz and others added 6 commits March 22, 2026 13:20
Session: 2026-03-22-p1p5-custom-controls
Requested by: Squad (Cyclops, Rogue, Beast, Coordinator)

Changes:
- Orchestration logs for Cyclops, Rogue, Beast
- Session log documenting P1-P5 framework completion (33 tests, 4 source files, 2515 total tests passing)
- Merged 9 architectural decisions from forge-p1p5-plan.md and copilot-directive inbox
- Deleted inbox files after merge (forge-p1p5-plan.md, copilot-directive-2026-03-22T14-48-37Z.md)
- Deduplicated decisions (no overlaps detected)
Add comprehensive developer documentation for the P1-P5 drop-in
replacement framework covering:
- Architecture and class hierarchy
- API reference for all 9 classes/interfaces
- 5 migration patterns with code examples
- Design decisions and rationale
- DepartmentPortal validation results
- Test coverage map (48 new tests)
- Upstream issue tracking (FritzAndFriends#490-FritzAndFriends#496)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace pessimistic 'What Can''t Be Shimmed' with evidence-backed
'Shimming & Migration Compatibility' covering:
- ViewState Dictionary shim (already working)
- Lifecycle event mapping (OnInit/OnLoad/OnPreRender/OnUnload/OnDisposed)
- Theming system (ThemeProvider, ControlSkin, SkinBuilder)
- Focus() via JS interop (pattern proven in validators)
- BWFC001-BWFC014 Roslyn analyzer suite

Only PostBack and DataSourceID remain as true architectural mismatches.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Session: 2026-03-22-scribe-merge
Requested by: Beast (Spawn Manifest)

Changes:
- Merged beast-p1p5-devdocs.md: P1-P5 Developer Documentation Scope decision
- Merged copilot-directive-2026-03-22T17-57-30Z.md: User directive on Section 6 shimming
- Deleted inbox files after merge
- decisions.md now contains both decisions

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Mirrors System.Web.UI.Control.Focus() API using fire-and-forget JS interop.
- public virtual void Focus() on BaseWebFormsComponent
- Null-safe for SSR (no JsRuntime) and missing ID
- bwfc.Page.Focus(elementId) JS function in both Basepage.js and module
- Uses proven pattern from validator SetFocus implementation

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@csharpfritz csharpfritz changed the title feat: DepartmentPortal Web Forms sample ASCX controls, custom server controls, base classes feat: P1-P5 Custom Controls Framework Drop-in Replacement for Web Forms Controls Mar 22, 2026
@csharpfritz csharpfritz marked this pull request as ready for review March 22, 2026 18:24
csharpfritz and others added 5 commits March 22, 2026 14:25
- Cyclops history.md updated with Focus() learnings
- Decision inbox from Focus() method implementation

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…BWFC

Migrated controls using BWFC CustomControls base classes:
- StarRating (WebControl)  star display with TagKey, AddAttributesToRender
- NotificationBell (WebControl)  bell icon with badge and drawer
- EmployeeCard (WebControl)  composite employee display card
- EmployeeDataGrid (DataBoundWebControl)  data-bound grid with paging/sorting
- DepartmentBreadcrumb (WebControl)  hierarchical breadcrumb with EventCallback
- PollQuestion (WebControl)  radio poll with vote EventCallback
- SectionPanel (TemplatedWebControl)  template-based section with RenderFragment

Updated Dashboard.razor and Employees.razor to use migrated controls.
Added CustomControls namespace to _Imports.razor.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
New analyzers:
- BWFC020: ViewStatePropertyPattern  detects ViewState-backed properties,
  code fix converts to [Parameter] auto-property
- BWFC021: FindControlUsage  detects FindControl calls,
  code fix replaces with FindControlRecursive
- BWFC022: PageClientScriptUsage  detects Page.ClientScript usage
- BWFC023: IPostBackEventHandlerUsage  detects IPostBackEventHandler impl

9 new tests, 139 total analyzer tests passing.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Session: 2026-03-22T18-28-00Z-migration-analyzers
Requested by: Copilot CLI (Scribe)

Changes:
- Merged 3 inbox decisions into decisions.md (no duplicates detected)
- Logged Cyclops orchestration: DepartmentPortal migration (7 controls  Blazor, 10 new components, build passing)
- Logged Colossus orchestration: BWFC020-023 analyzers (4 new, 2 code fixes, 9 tests, 139 total passing)
- Logged session summary: Migration & Analyzers completion

Files:
- .squad/decisions.md  merged (3 inbox files processed)
- .squad/decisions/inbox/  cleaned (3 files deleted)
- .squad/orchestration-log/2026-03-22T18-28-00Z-cyclops.md  created
- .squad/orchestration-log/2026-03-22T18-28-00Z-colossus.md  created
- .squad/log/2026-03-22T18-28-00Z-migration-analyzers.md  created

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
csharpfritz and others added 2 commits March 22, 2026 15:11
- Analyzer now uses semantic analysis to check whether FindControl is
  called on a BaseWebFormsComponent subclass (BWFC's own recursive
  implementation) and skips those  only flags non-BWFC types.
- Updated diagnostic message to reference BWFC's FindControl on
  BaseWebFormsComponent with recursive search.
- Code fix provider gutted: no automatic rename since the method IS
  called FindControl on BWFC. Migration requires inheriting from
  BaseWebFormsComponent, which is too complex for an automated fix.
- Tests updated: added 3 negative tests for BWFC types, removed 2
  obsolete code-fix rename tests, restored FindControlRecursive test.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…atibility

Web Forms Control.FindControl is the API developers know. Our implementation
already does recursive search  renaming to match the original API means
zero code changes needed during migration.

- Merged shallow + recursive into single FindControl method
- Updated BWFC021 analyzer to skip calls on BWFC base class types
- Updated dev-docs and migration guide references
- 2515 main tests + 139 analyzer tests passing

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment on lines +75 to +80
foreach (var candidate in symbolInfo.CandidateSymbols)
{
if (candidate is IMethodSymbol candidateMethod &&
InheritsFromOrIs(candidateMethod.ContainingType, "BaseWebFormsComponent"))
return true;
}

Check notice

Code scanning / CodeQL

Missed opportunity to use Where Note

This foreach loop
implicitly filters its target sequence
- consider filtering the sequence explicitly using '.Where(...)'.
csharpfritz and others added 3 commits March 22, 2026 15:34
…page

- Switch App.razor from local Bootstrap CSS to Bootstrap 5.3.3 CDN
- Add Bootstrap Icons CDN for NotificationBell icon support
- Copy Site.css from DepartmentPortal to wwwroot/css/site.css
- Create Home.razor welcome page at /home with navigation cards
- Fix SectionPanel duplicate CssClass parameter (was shadowing base class)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Home.razor now serves / and /home as the landing page.
Dashboard.razor serves /dashboard.
MainLayout nav link updated to /dashboard.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Design strategy for extracting CSS/JS from NuGet packages that use the
legacy Web Forms pattern (packages.config + BundleConfig.cs + Content/
Scripts folders). Recommends hybrid approach: CDN for known OSS packages,
extraction tool for custom packages, with bwfc migrate-assets CLI command.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@csharpfritz csharpfritz merged commit a4cac1d into FritzAndFriends:dev Mar 23, 2026
3 of 4 checks passed
@csharpfritz csharpfritz deleted the feature/ascx-sample-milestone branch March 25, 2026 17:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add DataBoundWebControl<T> base class (P1)

2 participants