Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Migrate ASP.NET Web Forms User Controls (.ascx) to Blazor components using BlazorWebFormsComponents. Covers ASCX-to-Razor conversion, code-behind preservation, tag prefix resolution, property/event mapping, and partial-class base class alignment. WHEN: 'migrate ascx', 'convert user control', 'ascx to blazor', 'user control migration'. FOR SINGLE OPERATIONS: use /bwfc-migration for full page migration, /bwfc-custom-control-migration for WebControl-based controls.
.claude/skills/fritzandfriends-bwfc-ascx-migration/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-03 | ✗→✓ | ▲ Improved | 88% | 0% |
| case-01 | ✗→✓ | ▲ Improved | 8% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 93% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 6% | 0% |
| case-09 | ✗→✓ | ▲ Improved | 25% | 0% |
ASP.NET Web Forms User Controls (.ascx files) are reusable markup fragments with code-behind. They map directly to Blazor .razor components with .razor.cs code-behind files.
The webforms-to-blazor CLI tool handles L1 conversion automatically. This skill guides L2 repair for common patterns that require contextual understanding.
| Step | What Happens | |------|--------------| | 1. Directive removal | <%@ Control ... %> is stripped | | 2. Markup transforms | asp: prefixes removed, expressions converted | | 3. Code-behind preservation | Base class remapped, usings updated | | 4. @inherits injection | Added to .razor when code-behind inherits a CustomControls type | | 5. Output | .razor + .razor.cs pair in the output project |
ASCX code-behind files contain business logic that should compile unchanged. The CLI:
System.Web.UI.* usingsusing BlazorWebFormsComponents.CustomControls;UserControl base class (now mapped to BWFC's UserControl)@inherits UserControl into the .razor fileDo NOT rewrite code-behind logic. Fix compile errors by adding missing shims or stubs.
Web Forms exposes ASCX properties to parent pages. In Blazor, these become [Parameter] properties:
csharp// Web Forms (worked without attribute) public string Title { get; set; } // Blazor (requires [Parameter]) [Parameter] public string Title { get; set; }
The CLI's ParameterAttributeTransform handles this automatically for public properties.
ASCX controls that raise events to parent pages:
csharp// Web Forms public event EventHandler ItemSelected; protected void OnItemSelected(EventArgs e) => ItemSelected?.Invoke(this, e); // Blazor [Parameter] public EventCallback<EventArgs> ItemSelected { get; set; } protected async Task OnItemSelected(EventArgs e) => await ItemSelected.InvokeAsync(e);
Web Forms requires tag prefix registration in Web.config or <%@ Register %>:
xml<!-- Web.config --> <add tagPrefix="uc" src="~/Controls/StatusPanel.ascx" tagName="StatusPanel" /> <!-- Usage --> <uc:StatusPanel runat="server" Title="Dashboard" />
In Blazor, components are referenced directly by name (no prefix, no registration):
razor<StatusPanel Title="Dashboard" />
The CLI strips <%@ Register %> directives and removes tag prefixes automatically.
The .razor file and .razor.cs file form a partial class. Their base class must agree:
razor@* In the .razor file *@ @inherits UserControl
csharp// In the .razor.cs file public partial class MyControl : UserControl { }
If the CLI misses the @inherits directive, you'll get CS0263. Add it manually.
Web Forms can load user controls dynamically:
csharpvar ctrl = (StatusPanel)LoadControl("~/Controls/StatusPanel.ascx"); ctrl.Title = "Dynamic"; PlaceHolder1.Controls.Add(ctrl);
Blazor equivalent: Use RenderFragment or DynamicComponent:
razor<DynamicComponent Type="typeof(StatusPanel)" Parameters="@(new Dictionary<string, object> { ["Title"] = "Dynamic" })" />
Code-behind that uses FindControl to locate child controls works unchanged through the BWFC runtime:
csharp// This works in Blazor via BWFC's FindControl runtime var lbl = (Label)FindControl("lblStatus"); lbl.Text = "Updated";
Web Forms controls often wrap and expose child control properties:
csharp// Web Forms pattern public string StatusText { get { return lblStatus.Text; } set { lblStatus.Text = value; } }
In Blazor, this still works if lblStatus is resolved via FindControl or @ref. The BWFC runtime supports both patterns.
User control lifecycle methods auto-wire through BWFC's virtual methods:
csharp// Works unchanged — BaseWebFormsComponent provides virtual Page_Load protected override void Page_Load(object sender, EventArgs e) { if (!IsPostBack) { BindData(); } }
// Before (Web Forms)
Controls/
StatusPanel.ascx ← Markup
StatusPanel.ascx.cs ← Code-behind
StatusPanel.ascx.designer.cs ← Auto-generated (discarded)
// After (Blazor)
Components/Controls/ (or wherever the CLI places them)
StatusPanel.razor ← Converted markup + @inherits UserControl
StatusPanel.razor.cs ← Preserved code-behind (usings updated)For each ASCX file in the migration:
.razor file has @inherits UserControl (or appropriate base).razor file has @using BlazorWebFormsComponents.CustomControls[Parameter] attributeEventCallback<T>Page_Load / Page_Init overrides work (virtual methods)FindControl calls resolve at runtimeOther measured skills in the registry, with their headline benchmark lift.