Skip to content

Commit 98cde11

Browse files
csharpfritzCopilot
andcommitted
docs, samples, tests: WebFormsForm demo, docs, and Playwright tests (#533)
- WebFormsFormDemo.razor: interactive sample page at /migration/webforms-form demonstrating WebFormsForm + SetRequestFormData + Request.Form pattern - WebFormsForm.md: component documentation with parameters, before/after comparison, dual-mode explanation, and migration path - WebFormsFormTests.cs: 4 Playwright tests (smoke, render, interaction, navigation-stays-put) + 1 smoke InlineData in ControlSampleTests - RequestShim.md: cross-reference to WebFormsForm for interactive mode - mkdocs.yml: nav entry under Utility Features - ComponentCatalog.cs: sidebar entry under Migration Helpers Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent a13cb8d commit 98cde11

7 files changed

Lines changed: 818 additions & 1 deletion

File tree

docs/UtilityFeatures/RequestShim.md

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,9 +93,37 @@ if (Request.Form.Count > 0) { /* process form */ }
9393
| Mode | Behavior |
9494
|------|----------|
9595
| SSR (Static Server Rendering) | Full access — wraps `HttpContext.Request.Form` |
96-
| Interactive Blazor Server | Returns empty — logs warning on first access |
96+
| Interactive Blazor Server | Use `<WebFormsForm>` component to enable access (see below) |
9797
| Non-form requests (JSON, etc.) | Returns empty — exceptions caught gracefully |
9898

99+
### Interactive Mode: Using WebFormsForm
100+
101+
In interactive Blazor Server mode (WebSocket-based), HTTP requests do not occur, so `Request.Form` is initially empty. To enable form submissions and `Request.Form` access in interactive pages, use the **`<WebFormsForm>` component**:
102+
103+
```razor
104+
@inherits WebFormsPageBase
105+
106+
<WebFormsForm OnSubmit="HandleSubmit">
107+
<input type="text" name="username" />
108+
<input type="password" name="password" />
109+
<button type="submit">Login</button>
110+
</WebFormsForm>
111+
112+
@code {
113+
private void HandleSubmit(FormSubmitEventArgs e)
114+
{
115+
// Inject form data into Request.Form shim
116+
SetRequestFormData(e);
117+
118+
// Now Request.Form is populated with submitted values
119+
string username = Request.Form["username"] ?? "";
120+
string password = Request.Form["password"] ?? "";
121+
}
122+
}
123+
```
124+
125+
The `<WebFormsForm>` component captures form data via JavaScript interop and injects it into the `Request.Form` shim, allowing migrated code-behind to work unchanged. See [WebFormsForm](WebFormsForm.md) for detailed documentation and examples.
126+
99127
## Graceful Degradation
100128

101129
Blazor Server components can run in two modes:
Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
# WebFormsForm
2+
3+
The **WebFormsForm** component wraps HTML form elements and provides `Request.Form` support in interactive Blazor Server mode. It bridges the gap between traditional Web Forms form submission (HTTP POST with `Request.Form`) and modern interactive Blazor (WebSocket-based with no HTTP POST).
4+
5+
Original Microsoft documentation: https://docs.microsoft.com/en-us/dotnet/api/system.web.ui.htmlcontrols.htmlform?view=netframework-4.8
6+
7+
## Background
8+
9+
In ASP.NET Web Forms, form submissions were HTTP POST requests, and `Request.Form` was populated from the POST body:
10+
11+
```csharp
12+
// Web Forms code-behind
13+
protected void btnSubmit_Click(object sender, EventArgs e)
14+
{
15+
string username = Request.Form["username"];
16+
string[] colors = Request.Form.GetValues("colors");
17+
}
18+
```
19+
20+
In interactive Blazor (WebSocket-based), there is **no HTTP POST** — all communication is bidirectional WebSocket. This means `Request.Form` would be empty. The `<WebFormsForm>` component captures form data via JavaScript interop and injects it into the `Request.Form` shim, enabling migrated code-behind to work unchanged.
21+
22+
## Use Cases
23+
24+
- **Interactive Blazor Server pages** using `<WebFormsForm>` to enable `Request.Form` access
25+
- **SSR pages** that can continue using standard `<form>` (native HTTP POST, `Request.Form` works natively)
26+
- **Gradual migration** from Web Forms forms to Blazor `EditForm` components
27+
28+
## Rendering Mode Behavior
29+
30+
| Mode | Approach | Request.Form Support |
31+
|------|----------|----------------------|
32+
| **SSR (Static Server Rendering)** | Use standard `<form>` with `[ExcludeFromInteractiveRouting]` | ✅ Native — HTTP POST populates `Request.Form` directly |
33+
| **Interactive Blazor Server** | Use `<WebFormsForm>` component | ✅ JS interop captures form data, injected into `Request.Form` shim |
34+
| **Target state (modern Blazor)** | Use `EditForm` + `@bind` | ✅ Typed binding — no need for `Request.Form` |
35+
36+
The component auto-detects the rendering mode and behaves appropriately.
37+
38+
## Parameters
39+
40+
| Parameter | Type | Default | Description |
41+
|-----------|------|---------|-------------|
42+
| `Method` | `FormMethod` | `Post` | HTTP method (`Get` or `Post`) |
43+
| `Action` | `string?` | `null` | Form action URL; `null` submits to the current page |
44+
| `OnSubmit` | `EventCallback<FormSubmitEventArgs>` || Callback fired when the form is submitted; receives `FormSubmitEventArgs` containing `FormData` (name-value pairs) |
45+
| `ChildContent` | `RenderFragment?` || Form content (input fields, buttons, etc.) |
46+
| *(unmatched)* ||| Any additional HTML attributes pass through to the `<form>` element (e.g., `enctype`, `data-*`) |
47+
48+
## Syntax Comparison
49+
50+
=== "Web Forms"
51+
52+
```html
53+
<form runat="server">
54+
<asp:TextBox ID="txtUsername" runat="server" />
55+
<asp:TextBox ID="txtPassword" TextMode="Password" runat="server" />
56+
<asp:CheckBox ID="chkRemember" runat="server" />
57+
<asp:Button ID="btnSubmit" runat="server" OnClick="btnSubmit_Click" Text="Login" />
58+
</form>
59+
```
60+
61+
```csharp
62+
// Code-behind
63+
protected void btnSubmit_Click(object sender, EventArgs e)
64+
{
65+
string username = Request.Form["txtUsername"];
66+
string password = Request.Form["txtPassword"];
67+
string remember = Request.Form["chkRemember"];
68+
69+
// Process login...
70+
}
71+
```
72+
73+
=== "Blazor (Interactive)"
74+
75+
```razor
76+
<WebFormsForm OnSubmit="HandleSubmit">
77+
<input type="text" name="txtUsername" />
78+
<input type="password" name="txtPassword" />
79+
<input type="checkbox" name="chkRemember" />
80+
<button type="submit">Login</button>
81+
</WebFormsForm>
82+
83+
@code {
84+
private void HandleSubmit(FormSubmitEventArgs e)
85+
{
86+
SetRequestFormData(e);
87+
88+
string username = Request.Form["txtUsername"];
89+
string password = Request.Form["txtPassword"];
90+
string remember = Request.Form["chkRemember"];
91+
92+
// Process login...
93+
}
94+
}
95+
```
96+
97+
## Migration Path
98+
99+
1. **Phase 1 — SSR with standard forms** (quickest path)
100+
- Keep existing `<form>` elements
101+
- Mark page with `[ExcludeFromInteractiveRouting]` to stay in SSR mode
102+
- `Request.Form` works natively via HTTP POST
103+
- No code changes needed
104+
105+
2. **Phase 2 — Interactive with WebFormsForm** (gradual adoption)
106+
- Remove `[ExcludeFromInteractiveRouting]`
107+
- Replace `<form>` with `<WebFormsForm>`
108+
- Add `OnSubmit` callback
109+
- Existing code-behind logic using `Request.Form` continues to work
110+
- Minimal rewrite
111+
112+
3. **Phase 3 — Modern Blazor** (target state)
113+
- Replace `<WebFormsForm>` with `<EditForm>`
114+
- Use `@bind` for two-way data binding
115+
- Eliminate `Request.Form` shim entirely
116+
- Full type safety and Blazor best practices
117+
118+
## Example: Login Form
119+
120+
=== "Web Forms"
121+
122+
```html
123+
<!-- LoginPage.aspx -->
124+
<form runat="server">
125+
<div>
126+
<label for="username">Username:</label>
127+
<asp:TextBox ID="username" runat="server" />
128+
</div>
129+
<div>
130+
<label for="password">Password:</label>
131+
<asp:TextBox ID="password" TextMode="Password" runat="server" />
132+
</div>
133+
<div>
134+
<asp:CheckBox ID="rememberMe" runat="server" Text="Remember me" />
135+
</div>
136+
<asp:Button ID="btnLogin" runat="server" OnClick="LoginClick" Text="Login" />
137+
<asp:Label ID="lblError" runat="server" ForeColor="Red" />
138+
</form>
139+
```
140+
141+
```csharp
142+
// LoginPage.aspx.cs
143+
public partial class LoginPage : Page
144+
{
145+
protected void LoginClick(object sender, EventArgs e)
146+
{
147+
string username = Request.Form["username"];
148+
string password = Request.Form["password"];
149+
string remember = Request.Form["rememberMe"];
150+
151+
if (ValidateCredentials(username, password))
152+
{
153+
FormsAuthentication.SetAuthCookie(username, remember == "on");
154+
Response.Redirect("~/Default.aspx");
155+
}
156+
else
157+
{
158+
lblError.Text = "Invalid credentials";
159+
}
160+
}
161+
162+
private bool ValidateCredentials(string username, string password)
163+
{
164+
// Validation logic...
165+
return true;
166+
}
167+
}
168+
```
169+
170+
=== "Blazor (Interactive)"
171+
172+
```razor
173+
<!-- Login.razor -->
174+
@page "/login"
175+
@inherits WebFormsPageBase
176+
@inject NavigationManager Nav
177+
@inject AuthenticationStateProvider AuthStateProvider
178+
179+
<div class="login-form">
180+
<h2>Login</h2>
181+
182+
<WebFormsForm OnSubmit="LoginClick">
183+
<div>
184+
<label for="username">Username:</label>
185+
<input type="text" id="username" name="username" required />
186+
</div>
187+
<div>
188+
<label for="password">Password:</label>
189+
<input type="password" id="password" name="password" required />
190+
</div>
191+
<div>
192+
<input type="checkbox" id="rememberMe" name="rememberMe" />
193+
<label for="rememberMe">Remember me</label>
194+
</div>
195+
<button type="submit">Login</button>
196+
</WebFormsForm>
197+
198+
@if (!string.IsNullOrEmpty(_errorMessage))
199+
{
200+
<p style="color: red;">@_errorMessage</p>
201+
}
202+
</div>
203+
204+
@code {
205+
private string _errorMessage = "";
206+
207+
private async Task LoginClick(FormSubmitEventArgs e)
208+
{
209+
SetRequestFormData(e);
210+
211+
string username = Request.Form["username"];
212+
string password = Request.Form["password"];
213+
string remember = Request.Form["rememberMe"];
214+
215+
if (ValidateCredentials(username, password))
216+
{
217+
// Sign in user...
218+
Nav.NavigateTo("/", forceLoad: true);
219+
}
220+
else
221+
{
222+
_errorMessage = "Invalid credentials";
223+
}
224+
}
225+
226+
private bool ValidateCredentials(string username, string password)
227+
{
228+
// Validation logic...
229+
return true;
230+
}
231+
}
232+
```
233+
234+
## How It Works
235+
236+
1. **Form Submission** — User submits the form via HTML submit button or programmatic submit
237+
2. **JavaScript Interop**`<WebFormsForm>` captures form data (all input fields) via `FormData` API
238+
3. **OnSubmit Callback** — Captured data is passed to the `OnSubmit` event callback as `FormSubmitEventArgs`
239+
4. **SetRequestFormData** — Call `SetRequestFormData(e)` to inject the captured data into the `Request.Form` shim
240+
5. **Access via Request.Form** — Code-behind logic using `Request.Form["fieldName"]` works as expected
241+
242+
## Dual-Mode Support
243+
244+
**SSR Pages:**
245+
```razor
246+
@page "/form-page"
247+
@attribute [ExcludeFromInteractiveRouting]
248+
249+
<form method="post" action="/form-page">
250+
<input type="text" name="username" />
251+
<button type="submit">Submit</button>
252+
</form>
253+
```
254+
255+
In SSR mode, the form submits via HTTP POST and `Request.Form` is automatically populated. No `<WebFormsForm>` or JavaScript interop needed.
256+
257+
**Interactive Pages:**
258+
```razor
259+
@page "/form-page"
260+
261+
<WebFormsForm OnSubmit="HandleSubmit">
262+
<input type="text" name="username" />
263+
<button type="submit">Submit</button>
264+
</WebFormsForm>
265+
```
266+
267+
In interactive mode, `<WebFormsForm>` captures data and injects it into `Request.Form`.
268+
269+
## Notes
270+
271+
- **HTML Attributes** — Unmatched HTML attributes (like `enctype`, `data-*`, `autocomplete`) are passed through to the rendered `<form>` element
272+
- **File Uploads** — When using `enctype="multipart/form-data"`, ensure file inputs have `name` attributes so they are captured
273+
- **Default Action** — If `Action` is `null`, the form submits to the current page (no full-page reload in interactive mode)
274+
- **Event Bubbling** — The form does not automatically validate child components; add validation as needed in the `OnSubmit` callback
275+
- **Accessibility** — Use standard form semantics (`<label>`, `for` attributes, `required`, `aria-*`) for accessibility
276+
277+
## Related Documentation
278+
279+
- [Request Shim](RequestShim.md) — Details on `Request.QueryString`, `Request.Cookies`, and `Request.Form` access
280+
- [WebFormsPage](WebFormsPage.md) — Base page class providing `Request`, `Response`, and other Web Forms compatibility features
281+
- [EditForm](https://learn.microsoft.com/en-us/aspnet/core/blazor/forms-and-input-components) — Modern Blazor form component (target state)

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ nav:
187187
- ViewState: UtilityFeatures/ViewState.md
188188
- ViewState and PostBack Shim: UtilityFeatures/ViewStateAndPostBack.md
189189
- WebFormsPage: UtilityFeatures/WebFormsPage.md
190+
- WebFormsForm: UtilityFeatures/WebFormsForm.md
190191
- Migration:
191192
- Getting Started: Migration/readme.md
192193
- Strangler Fig Pattern: Migration/StranglerFigPattern.md

samples/AfterBlazorServerSide.Tests/ControlSampleTests.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,7 @@ public async Task AjaxControl_Loads_WithoutErrors(string path)
250250
// Migration Shim Sample Pages
251251
[Theory]
252252
[InlineData("/migration/request-form")]
253+
[InlineData("/migration/webforms-form")]
253254
public async Task MigrationPage_Loads_WithoutErrors(string path)
254255
{
255256
await VerifyPageLoadsWithoutErrors(path);

0 commit comments

Comments
 (0)