Skip to content

Commit cb535bb

Browse files
csharpfritzCopilot
andauthored
docs: ViewState Phase 5 analyzer docs, AutoPostBack guide, sample updates (FritzAndFriends#522)
* docs: ViewState Phase 5 analyzer docs, AutoPostBack guide, sample updates - Analyzers.md: BWFC002/003 severity updated to Info, added BWFC025 section, updated prioritization guide and .editorconfig examples - ViewStateAndPostBack.md: Added AutoPostBack (SSR) section with usage guide - ViewState sample page: Modernized to showcase ViewStateDictionary type-safe API, IsPostBack detection, and graduating-off-ViewState patterns - Added analyzer screenshot placeholders with capture instructions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: Add AnalyzerDemo page for VS screenshot captures Adds AnalyzerDemo.razor.cs in AfterDepartmentPortal with patterns that trigger BWFC002, BWFC003, and BWFC025 analyzers. Also adds direct analyzer ProjectReference so diagnostics appear in VS. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs: Add VS analyzer screenshots for BWFC002 and BWFC003 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: Update Playwright test for renamed ViewState button The ViewState sample page was modernized button changed from 'Click Me (ViewState)' to 'Increment'. Update test selector to match. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 95f47b2 commit cb535bb

File tree

10 files changed

+359
-130
lines changed

10 files changed

+359
-130
lines changed

docs/Migration/Analyzers.md

Lines changed: 116 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,16 @@ Once installed, diagnostics appear automatically in Visual Studio's Error List a
3030
| Rule ID | Severity | Name | What It Detects |
3131
|---------|----------|------|-----------------|
3232
| [BWFC001](#bwfc001-missing-parameter-attribute) | ⚠️ Warning | Missing `[Parameter]` Attribute | Public properties on WebControl subclasses without `[Parameter]` |
33-
| [BWFC002](#bwfc002-viewstate-usage) | ⚠️ Warning | ViewState Usage | `ViewState["key"]` access patterns |
34-
| [BWFC003](#bwfc003-ispostback-usage) | ⚠️ Warning | IsPostBack Usage | `IsPostBack` or `Page.IsPostBack` checks |
33+
| [BWFC002](#bwfc002-viewstate-usage) | ℹ️ Info | ViewState Usage | `ViewState["key"]` access patterns |
34+
| [BWFC003](#bwfc003-ispostback-usage) | ℹ️ Info | IsPostBack Usage | `IsPostBack` or `Page.IsPostBack` checks |
3535
| [BWFC004](#bwfc004-responseredirect-usage) | ⚠️ Warning | Response.Redirect Usage | `Response.Redirect()` calls |
3636
| [BWFC005](#bwfc005-session-state-usage) | ⚠️ Warning | Session State Usage | `Session["key"]` and `HttpContext.Current` access |
3737
| [BWFC010](#bwfc010-required-attribute-missing) | ℹ️ Info | Required Attribute Missing | BWFC components instantiated without critical properties |
3838
| [BWFC011](#bwfc011-event-handler-signature) | ℹ️ Info | Event Handler Signature | Methods with `(object sender, EventArgs e)` signature |
3939
| [BWFC012](#bwfc012-runatserver-leftover) | ⚠️ Warning | runat="server" Leftover | String literals containing `runat="server"` |
4040
| [BWFC013](#bwfc013-response-object-usage) | ⚠️ Warning | Response Object Usage | `Response.Write()`, `Response.WriteFile()`, `Response.Clear()`, `Response.Flush()`, `Response.End()` |
4141
| [BWFC014](#bwfc014-request-object-usage) | ⚠️ Warning | Request Object Usage | `Request.Form[]`, `Request.Cookies[]`, `Request.Headers[]`, `Request.Files`, `Request.QueryString[]`, `Request.ServerVariables[]` |
42+
| [BWFC025](#bwfc025-non-serializable-viewstate-type) | ⚠️ Warning | Non-Serializable ViewState Type | `ViewState["key"]` assignments with non-JSON-serializable types |
4243

4344
---
4445

@@ -90,16 +91,16 @@ Adds `[Parameter]` to the property and inserts `using Microsoft.AspNetCore.Compo
9091

9192
## BWFC002: ViewState Usage
9293

93-
**Severity:** ⚠️ Warning
94+
**Severity:** ℹ️ Info
9495
**Category:** Usage
9596

9697
### What It Detects
9798

98-
`ViewState["key"]` and `this.ViewState["key"]` element-access patterns in code-behind. Blazor has no ViewState mechanism — it uses in-memory component state instead.
99+
`ViewState["key"]` and `this.ViewState["key"]` element-access patterns in code-behind. This diagnostic is now informational because the BWFC `ViewStateDictionary` shim provides working ViewState support during migration. The analyzer still highlights these patterns to encourage eventual adoption of native Blazor state management.
99100

100101
### Why It Matters
101102

102-
ViewState was the persistence mechanism for Web Forms page lifecycle. In Blazor, components live in memory on the server (Blazor Server) or in the browser (Blazor WebAssembly). State is naturally preserved across re-renders as regular C# fields and properties — no serialization needed. Any leftover `ViewState` access will either fail at compile time or use the BWFC compatibility shim, which is meant as a stepping stone, not a long-term solution.
103+
ViewState is now supported as a working migration shim via `ViewStateDictionary` — your `ViewState["key"]` code will compile and run correctly in Blazor. However, native Blazor patterns (component fields, `[Parameter]` properties, cascading values) are preferred for new code because they are type-safe, avoid serialization overhead, and are idiomatic Blazor.
103104

104105
### Example
105106

@@ -142,20 +143,27 @@ private string SortDirection { get; set; } = "ASC";
142143
!!! note "See Also"
143144
The [ViewState utility documentation](../UtilityFeatures/ViewState.md) explains the BWFC compatibility shim and the recommended path to component state.
144145

146+
!!! info "Severity Reduced (Phase 4)"
147+
This analyzer was reduced from Warning to Info because the BWFC `ViewStateDictionary` shim now provides working ViewState support. The code fix still suggests migrating to native Blazor patterns, which remains the recommended long-term approach.
148+
149+
<!-- TODO: Add Visual Studio screenshot showing BWFC002 Info squiggle on ViewState["key"] usage -->
150+
<!-- Screenshot should show: the green info squiggle under ViewState["SortDirection"], the lightbulb, and the tooltip message -->
151+
![BWFC002 Info squiggle in Visual Studio](../images/analyzers/bwfc002-info-squiggle.png){ .analyzer-screenshot }
152+
145153
---
146154

147155
## BWFC003: IsPostBack Usage
148156

149-
**Severity:** ⚠️ Warning
157+
**Severity:** ℹ️ Info
150158
**Category:** Usage
151159

152160
### What It Detects
153161

154-
References to `IsPostBack` and `Page.IsPostBack` in code-behind. Blazor does not use the postback model — it uses component lifecycle methods instead.
162+
References to `IsPostBack` and `Page.IsPostBack` in code-behind. This diagnostic is now informational because BWFC provides mode-adaptive `IsPostBack` support that works correctly during migration.
155163

156164
### Why It Matters
157165

158-
In Web Forms, `IsPostBack` distinguished between the initial page load and subsequent postbacks. In Blazor, this distinction maps to lifecycle methods:
166+
`IsPostBack` is now supported as a working migration shim — in SSR mode it checks `HttpMethods.IsPost()`, and in Interactive mode it tracks initialization state. Your existing `if (!IsPostBack)` guards will work correctly. However, the idiomatic Blazor approach is to use lifecycle methods directly:
159167

160168
| Web Forms Pattern | Blazor Equivalent |
161169
|-------------------|-------------------|
@@ -199,6 +207,13 @@ protected override void OnInitialized()
199207
}
200208
```
201209

210+
!!! info "Severity Reduced (Phase 4)"
211+
This analyzer was reduced from Warning to Info because BWFC now provides mode-adaptive `IsPostBack` support (SSR checks HTTP POST method, Interactive tracks initialization state). The code fix still suggests lifecycle methods, which is the idiomatic Blazor approach.
212+
213+
<!-- TODO: Add Visual Studio screenshot showing BWFC003 Info squiggle on IsPostBack usage -->
214+
<!-- Screenshot should show: the green info squiggle under IsPostBack, the lightbulb, and the tooltip message -->
215+
![BWFC003 Info squiggle in Visual Studio](../images/analyzers/bwfc003-info-squiggle.png){ .analyzer-screenshot }
216+
202217
---
203218

204219
## BWFC004: Response.Redirect Usage
@@ -709,6 +724,79 @@ Use the `InputFile` component:
709724

710725
---
711726

727+
## BWFC025: Non-Serializable ViewState Type
728+
729+
**Severity:** ⚠️ Warning
730+
**Category:** Usage
731+
732+
### What It Detects
733+
734+
Assignments to `ViewState["key"]` or `ViewState.Set<T>()` where the value type is unlikely to be JSON-serializable. The analyzer flags:
735+
736+
| Type Pattern | Why It's Flagged |
737+
|---|---|
738+
| `IDisposable` implementations | Streams, DB connections, HttpClient — not safe to serialize |
739+
| Delegates and event handlers | Cannot be serialized to JSON |
740+
| `System.Data.DataSet` / `DataTable` | Common Web Forms pattern, but complex object graphs fail with System.Text.Json |
741+
| `System.Web.*` types | ASP.NET classic types not available in .NET 8+ |
742+
| `System.IO.Stream*` types | OS handles cannot be serialized |
743+
| `System.Net.*` types | Network objects cannot be serialized |
744+
745+
### Why It Matters
746+
747+
In SSR mode, BWFC serializes `ViewStateDictionary` contents to JSON using `System.Text.Json` and embeds them in a hidden form field. Types that lack a parameterless constructor, have non-public properties, or represent OS handles will fail at runtime with a `JsonException`.
748+
749+
In Interactive Server mode, ViewState is kept in memory and serialization is skipped — so the same code may work interactively but fail in SSR. This analyzer catches the problem at compile time.
750+
751+
### Example
752+
753+
=== "Before (triggers BWFC025)"
754+
```csharp
755+
protected override void OnInitialized()
756+
{
757+
// ⚠️ MemoryStream implements IDisposable — not JSON-serializable
758+
ViewState["ReportData"] = new MemoryStream();
759+
760+
// ⚠️ DataTable has complex internal structure — fails with System.Text.Json
761+
ViewState["Results"] = new DataTable();
762+
}
763+
```
764+
765+
=== "After (fixed)"
766+
```csharp
767+
protected override void OnInitialized()
768+
{
769+
// ✅ Store the byte array, not the stream
770+
ViewState.Set<byte[]>("ReportData", GenerateReport());
771+
772+
// ✅ Convert to a serializable collection
773+
ViewState.Set<List<Dictionary<string, object>>>("Results",
774+
ConvertDataTableToList(dataTable));
775+
}
776+
```
777+
778+
### No Code Fix
779+
780+
This analyzer does not have an automatic code fix because the correct replacement depends on your specific data structure. Common strategies:
781+
782+
- **Streams** → Store `byte[]` or `string` (base64)
783+
- **DataTable** → Convert to `List<T>` of POCOs
784+
- **Complex objects** → Create a DTO with public properties and a parameterless constructor
785+
- **Delegates** → Don't store in ViewState; use component fields instead
786+
787+
!!! tip "Check Serialization"
788+
You can verify a type serializes correctly by testing:
789+
```csharp
790+
var json = System.Text.Json.JsonSerializer.Serialize(myValue);
791+
var roundTrip = System.Text.Json.JsonSerializer.Deserialize<MyType>(json);
792+
```
793+
794+
<!-- TODO: Add Visual Studio screenshot showing BWFC025 Warning squiggle on ViewState assignment with DataTable -->
795+
<!-- Screenshot should show: the yellow warning squiggle under the assignment, tooltip showing "ViewState assignment stores 'System.Data.DataTable' which may not be JSON-serializable" -->
796+
![BWFC025 Warning squiggle in Visual Studio](../images/analyzers/bwfc025-warning-squiggle.png){ .analyzer-screenshot }
797+
798+
---
799+
712800
## Using Analyzers in CI/CD
713801

714802
The analyzers integrate seamlessly with `dotnet build` and CI/CD pipelines. You can configure severity levels per rule and fail the build on violations.
@@ -720,18 +808,19 @@ The analyzers integrate seamlessly with `dotnet build` and CI/CD pipelines. You
720808

721809
# Mandatory rules: fail the build
722810
dotnet_diagnostic.BWFC001.severity = error
723-
dotnet_diagnostic.BWFC003.severity = error
724811
dotnet_diagnostic.BWFC004.severity = error
725812

726813
# Important patterns: treat as warnings
727-
dotnet_diagnostic.BWFC002.severity = warning
728814
dotnet_diagnostic.BWFC005.severity = warning
729815
dotnet_diagnostic.BWFC011.severity = warning
730816
dotnet_diagnostic.BWFC012.severity = warning
731817
dotnet_diagnostic.BWFC013.severity = warning
732818
dotnet_diagnostic.BWFC014.severity = warning
819+
dotnet_diagnostic.BWFC025.severity = warning
733820

734821
# Informational patterns: visible but don't block build
822+
dotnet_diagnostic.BWFC002.severity = suggestion
823+
dotnet_diagnostic.BWFC003.severity = suggestion
735824
dotnet_diagnostic.BWFC010.severity = suggestion
736825
```
737826

@@ -770,10 +859,10 @@ These patterns prevent your components from working at all:
770859
- Components silently ignore bound values → visual regression
771860
- Usually quick fixes (add one attribute per property)
772861

773-
2. **BWFC003**`IsPostBack` checks
774-
- Affects page initialization and event handling
775-
- Core logic may depend on this check
776-
- Need to refactor to `OnInitialized` / event handlers
862+
2. **BWFC003**`IsPostBack` checks (ℹ️ now Info)
863+
- IsPostBack works via the BWFC mode-adaptive shim during migration
864+
- Refactor to `OnInitialized` / event handlers when ready to adopt native patterns
865+
- Severity reduced from Warning to Info — IsPostBack is a working migration shim
777866

778867
3. **BWFC004**`Response.Redirect()` calls
779868
- All navigation breaks → user can't move between pages
@@ -787,9 +876,10 @@ These patterns prevent your components from working at all:
787876

788877
These affect how data flows through your app:
789878

790-
5. **BWFC002**`ViewState` usage
791-
- Replace with component fields/properties
792-
- May require refactoring persistence logic
879+
5. **BWFC002**`ViewState` usage (ℹ️ now Info)
880+
- ViewState works via the BWFC shim during migration
881+
- Replace with component fields/properties when ready to adopt native patterns
882+
- Severity reduced from Warning to Info — ViewStateDictionary is a working migration shim
793883

794884
6. **BWFC005**`Session` and `HttpContext` access
795885
- Replace with scoped services, protected storage, etc.
@@ -799,19 +889,23 @@ These affect how data flows through your app:
799889
- Replace with route parameters, `@bind`, `InputFile`, etc.
800890
- Usually straightforward per-instance fixes
801891

892+
8. **BWFC025** — Non-serializable ViewState types (⚠️ Warning)
893+
- Catches types that will fail JSON serialization in SSR mode
894+
- Fix before deploying to SSR — these cause runtime exceptions
895+
802896
### Phase 3: Output & Response Patterns (Fix Last)
803897

804898
These are less common and more specialized:
805899

806-
8. **BWFC013**`Response.Write()`, `Response.WriteFile()`, etc.
900+
9. **BWFC013**`Response.Write()`, `Response.WriteFile()`, etc.
807901
- Less common in modern applications
808902
- Usually isolated to reporting/export features
809903

810-
9. **BWFC012**`runat="server"` in strings
811-
- Pure string cleanup, no logic impact
812-
- Can be done last as a polish pass
904+
10. **BWFC012**`runat="server"` in strings
905+
- Pure string cleanup, no logic impact
906+
- Can be done last as a polish pass
813907

814-
10. **BWFC010** — Missing required attributes
908+
11. **BWFC010** — Missing required attributes
815909
- Usually caught by testing; low risk
816910
- Fix as you discover missing data
817911

docs/UtilityFeatures/ViewStateAndPostBack.md

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,89 @@ A common migration pattern is to start with SSR (traditional form posts) and gra
483483

484484
---
485485

486+
## AutoPostBack (SSR)
487+
488+
### What It Is
489+
490+
In Web Forms, `AutoPostBack="true"` on controls like `<asp:DropDownList>`, `<asp:CheckBox>`, and `<asp:TextBox>` caused the form to automatically submit when the user changed the value. This triggered a full page postback.
491+
492+
In BWFC, AutoPostBack is mode-adaptive:
493+
494+
| Render Mode | Behavior |
495+
|---|---|
496+
| **SSR (Static)** | Emits `onchange="this.form.submit()"` on the HTML element — mimics the Web Forms postback |
497+
| **Interactive Server** | Uses Blazor's native `@onchange` event binding — no form submission needed |
498+
499+
### Supported Controls
500+
501+
AutoPostBack is supported on these controls:
502+
503+
- `DropDownList`
504+
- `CheckBox`
505+
- `TextBox`
506+
- `RadioButton`
507+
- `ListBox`
508+
- `CheckBoxList`
509+
- `RadioButtonList`
510+
511+
### Usage
512+
513+
Set `AutoPostBack="true"` on any supported control. In SSR mode, changing the control value submits the enclosing `<form>`:
514+
515+
```razor
516+
<form method="post" @formname="filter-form" @onsubmit="HandleFilter">
517+
<AntiforgeryToken />
518+
519+
<DropDownList ID="ddlDepartment"
520+
DataSource="@departments"
521+
DataTextField="Name"
522+
DataValueField="Id"
523+
AutoPostBack="true" />
524+
525+
<p>Selected: @selectedDepartment</p>
526+
</form>
527+
528+
@code {
529+
private List<Department> departments = new();
530+
private string selectedDepartment = "";
531+
532+
protected override void OnInitialized()
533+
{
534+
departments = DepartmentService.GetAll();
535+
}
536+
537+
private void HandleFilter()
538+
{
539+
// This runs on form submit (triggered by AutoPostBack in SSR)
540+
selectedDepartment = Request.Form["ddlDepartment"];
541+
}
542+
}
543+
```
544+
545+
### How It Works
546+
547+
The `BaseWebFormsComponent.GetAutoPostBackAttributes()` method returns a dictionary of HTML attributes when AutoPostBack conditions are met:
548+
549+
1. The control has `AutoPostBack = true`
550+
2. The current render mode is SSR (not Interactive)
551+
552+
When both conditions are true, the method returns `{ "onchange": "this.form.submit()" }`, which is applied to the HTML input element via Blazor's `@attributes` splatting.
553+
554+
When either condition is false, the method returns an empty dictionary and no extra attributes are emitted.
555+
556+
### Combining with ViewState
557+
558+
AutoPostBack and ViewState work together naturally in SSR:
559+
560+
1. User changes a DropDownList value
561+
2. `onchange="this.form.submit()"` submits the form
562+
3. The form POST includes both the new value AND the ViewState hidden fields
563+
4. On the server, `IsPostBack` is `true`, ViewState is deserialized, and the new value is available
564+
565+
This mirrors the Web Forms lifecycle without requiring any JavaScript interop or SignalR connection.
566+
567+
---
568+
486569
## Migration Path: From Web Forms ViewState
487570

488571
### Before (Web Forms)

docs/images/analyzers/README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Analyzer Screenshots
2+
3+
This directory should contain Visual Studio screenshots showing the BWFC Roslyn analyzer experience.
4+
5+
## Required Screenshots
6+
7+
| File | Description |
8+
|---|---|
9+
| `bwfc002-info-squiggle.png` | BWFC002 — Green info squiggle on `ViewState["key"]` usage, showing tooltip message |
10+
| `bwfc003-info-squiggle.png` | BWFC003 — Green info squiggle on `IsPostBack` check, showing tooltip message |
11+
| `bwfc025-warning-squiggle.png` | BWFC025 — Yellow warning squiggle on `ViewState["key"] = new DataTable()`, showing tooltip message |
12+
13+
## How to Capture
14+
15+
1. Open a C# file with the relevant pattern in Visual Studio 2022
16+
2. Hover over the squiggle to show the tooltip
17+
3. Use Windows Snipping Tool (Win+Shift+S) to capture the editor area
18+
4. Save as PNG, approximately 800x200 pixels
19+
5. Place in this directory with the filename from the table above
77.1 KB
Loading
132 KB
Loading

samples/AfterBlazorServerSide.Tests/InteractiveComponentTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1605,8 +1605,8 @@ public async Task ViewState_Counter_IncrementsOnClick()
16051605
Timeout = 30000
16061606
});
16071607

1608-
// Find the ViewState increment button (not the property-based one)
1609-
var viewStateButton = page.GetByRole(AriaRole.Button, new() { Name = "Click Me (ViewState)" });
1608+
// Find the ViewState increment button
1609+
var viewStateButton = page.GetByRole(AriaRole.Button, new() { Name = "Increment" });
16101610
await viewStateButton.WaitForAsync(new() { Timeout = 5000 });
16111611

16121612
// Click once — counter should go to 1

0 commit comments

Comments
 (0)