|
| 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) |
0 commit comments