!!! warning "Migration Shim — Not a Destination"
ViewState and IsPostBack are migration compatibility features. They exist so your Web Forms code-behind logic compiles and runs correctly in Blazor with minimal changes. Once your application is running, you should refactor toward native Blazor patterns — [Parameter] properties, component fields, and cascading values. See Graduating Off ViewState below.
The ViewState and PostBack shim features enable seamless migration of ASP.NET Web Forms applications to Blazor by emulating the familiar ViewState dictionary and IsPostBack pattern. These features bridge the gap between traditional stateless HTTP POST workflows and Blazor's component-based stateful architecture.
- ViewStateDictionary — A state management class that persists data across postbacks, with automatic serialization for SSR and in-memory storage for ServerInteractive mode
- Mode-Adaptive IsPostBack — A property that automatically detects postback scenarios based on your render mode
- Hidden Field Persistence — Automatic round-tripping of ViewState through protected form fields in SSR
- Form State Continuity — Seamless state management across SSR and ServerInteractive transitions
The original Web Forms ViewState was a source of well-known problems: page bloat, security vulnerabilities, and invisible performance costs. Our implementation is fundamentally different:
| Concern | Web Forms ViewState | BWFC ViewState Shim |
|---|---|---|
| Default behavior | On for every control, always serialized | Off by default — opt-in per component |
| Scope | Single __VIEWSTATE blob for the entire page |
Per-component isolated fields (__bwfc_viewstate_{ID}) |
| Serialization | Every render, even unchanged controls | Dirty tracking — skips serialization when nothing changed |
| Security | Unencrypted until .NET 4.5.2 MAC patch | Encrypted + signed by default (ASP.NET Core Data Protection, AES-256) |
| Format | Opaque binary (LosFormatter) |
JSON — human-readable, debuggable |
| Visibility | No insight into payload size | Size warnings logged when threshold exceeded |
| Interactive mode | N/A | In-memory only — no serialization overhead |
ViewStateDictionary is a dictionary-based state store that emulates the ASP.NET Web Forms ViewState pattern. It implements IDictionary<string, object?> with additional convenience methods for type-safe access.
In Web Forms, ViewState was serialized and embedded as a hidden field. In BlazorWebFormsComponents, ViewStateDictionary adapts to your rendering mode:
- SSR (StaticSSR) — Serializes to a protected hidden form field for round-tripping through HTTP POSTs
- ServerInteractive — Persists in component instance memory (equivalent to a private field)
// Store a value
viewState["foo"] = bar;
// Retrieve a value (returns null if missing, no KeyNotFoundException)
object? value = viewState["foo"];// Store with automatic type conversion
ViewState.Set<string>("Name", "John");
// Retrieve with optional default value
string name = ViewState.GetValueOrDefault<string>("Name", "Anonymous");
int count = ViewState.GetValueOrDefault<int>("Count", 0);// Returns true if the dictionary has been modified
internal bool IsDirty { get; }
// Resets the dirty flag after serialization
internal void MarkClean();Serialization (SSR Hidden Field Round-Trip)
// Serialize to protected string for hidden form field
internal string Serialize(IDataProtector protector);
// Deserialize from protected string after form POST
internal static ViewStateDictionary Deserialize(string protectedPayload, IDataProtector protector);
// Merge state from another dictionary
internal void LoadFrom(ViewStateDictionary other);@page "/counter"
@inherits BaseWebFormsComponent
<div>
<p>Count: @(ViewState.GetValueOrDefault<int>("Counter", 0))</p>
<Button Text="Increment" @onclick="OnIncrement" />
</div>
@code {
private void OnIncrement()
{
int count = ViewState.GetValueOrDefault<int>("Counter", 0);
ViewState.Set<int>("Counter", count + 1);
}
}Migration Note: In Blazor, prefer a simple int field instead:
private int counter = 0;
private void OnIncrement()
{
counter++;
}Form with Hidden Field Persistence (SSR)
This example shows how ViewStateDictionary round-trips through SSR form POSTs.
Web Forms (Before):
<%@ Page Language="C#" %>
<form runat="server">
<asp:TextBox ID="ProductNameTextBox" runat="server" />
<asp:Button ID="AddButton" Text="Add" runat="server" OnClick="AddButton_Click" />
<asp:GridView ID="ProductsGrid" runat="server" />
<script runat="server">
protected void AddButton_Click(object sender, EventArgs e)
{
var products = (List<Product>)ViewState["Products"] ?? new();
products.Add(new Product { Name = ProductNameTextBox.Text });
ViewState["Products"] = products;
ProductsGrid.DataSource = products;
ProductsGrid.DataBind();
}
</script>
</form>Blazor SSR (After):
@page "/products"
@inherits WebFormsPageBase
<form method="post">
<TextBox ID="ProductNameTextBox" @bind-Value="_productName" />
<Button ID="AddButton" Text="Add" @onclick="OnAddProduct" />
<GridView ID="ProductsGrid" DataSource="@_products" AutoGenerateColumns="true" />
<RenderViewStateField />
</form>
@code {
private string _productName = "";
private List<Product> _products = new();
protected override void OnInitialized()
{
// Load products from ViewState if this is a postback
if (IsPostBack)
{
var stored = ViewState.GetValueOrDefault<List<Product>>("Products");
if (stored != null)
{
_products = stored;
}
}
}
private void OnAddProduct()
{
if (!string.IsNullOrEmpty(_productName))
{
_products.Add(new Product { Name = _productName });
ViewState.Set("Products", _products);
_productName = "";
}
}
public class Product
{
public string Name { get; set; }
}
}@page "/wizard"
@inherits BaseWebFormsComponent
@if (CurrentStep == 1)
{
<WizardStep1 @ref="_step1" />
}
else if (CurrentStep == 2)
{
<WizardStep2 @ref="_step2" />
}
else
{
<WizardSummary Data="@_wizardData" />
}
<div>
@if (CurrentStep > 1)
{
<Button Text="Back" @onclick="GoBack" />
}
@if (CurrentStep < 3)
{
<Button Text="Next" @onclick="GoNext" />
}
</div>
@code {
private WizardData _wizardData = new();
private int CurrentStep
{
get => ViewState.GetValueOrDefault<int>("CurrentStep", 1);
set => ViewState.Set("CurrentStep", value);
}
protected override void OnInitialized()
{
// On first render (not postback), step is 1
// On postback, step is restored from ViewState
if (!IsPostBack)
{
_wizardData = new WizardData();
ViewState.Set("WizardData", _wizardData);
}
else
{
_wizardData = ViewState.GetValueOrDefault<WizardData>("WizardData", new());
}
}
private void GoNext()
{
CurrentStep++;
ViewState.Set("WizardData", _wizardData);
}
private void GoBack()
{
CurrentStep--;
}
public class WizardData
{
public string Step1Data { get; set; }
public string Step2Data { get; set; }
}
}IsPostBack is a boolean property that indicates whether the current render is a postback or an initial render. It automatically adapts to your rendering mode:
- SSR (StaticSSR) — Returns
truewhen the HTTP request method is POST (form submission) - ServerInteractive — Returns
trueafter the first initialization, indicating a re-render triggered by user interaction or state change
protected override void OnInitialized()
{
if (!IsPostBack)
{
// First render: load initial data
LoadData();
}
else
{
// Postback / re-render: restore from ViewState
RestoreState();
}
}/// <summary>
/// Returns <c>true</c> when the current request is a postback (form POST in SSR mode)
/// or after the first initialization (in ServerInteractive mode).
/// Matches the ASP.NET Web Forms <c>Page.IsPostBack</c> semantics.
/// </summary>
public bool IsPostBack { get; }/// <summary>
/// Always returns false. Blazor has no postback model.
/// Exists so that if (!IsPostBack) { ... } compiles and executes correctly —
/// the guarded block always runs, which is the correct behavior for
/// OnInitialized (first-render) context.
/// </summary>
public bool IsPostBack => false;Note:
WebFormsPageBase.IsPostBackalways returnsfalsebecause pages in Blazor don't have HTTP-level postbacks. For page-level logic, useOnInitializedto detect first render instead.
When running in SSR (pre-render or StaticSSR), IsPostBack checks the HTTP request method:
if (HttpContextAccessor?.HttpContext is { } context)
return HttpMethods.IsPost(context.Request.Method);This allows forms submitted with <form method="post"> to be detected as postbacks.
When running in ServerInteractive (interactive WebSocket mode), IsPostBack tracks component initialization:
private bool _hasInitialized = false;
public bool IsPostBack => _hasInitialized;
protected override void OnInitialized()
{
if (!_hasInitialized)
{
_hasInitialized = true;
}
}This enables the first render check (if (!IsPostBack)) to work as in Web Forms, guarding one-time initialization code.
Hidden Field Persistence (SSR)
In SSR mode, ViewState is automatically serialized to a protected hidden form field and round-tripped through HTTP POSTs. This happens transparently without requiring manual form field management.
- Component Renders — During SSR pre-render, ViewStateDictionary serializes its contents
- Hidden Field Emitted — A protected hidden input is rendered (encrypted, HMAC-signed)
- Form Submits — User submits
<form method="post"> - Hidden Field Recovered — The form POST is processed, hidden field contents are decrypted and verified
- ViewState Restored — The deserialized ViewState is loaded into the component instance
- Component Re-renders — Business logic runs with ViewState state restored
ViewState is protected using IDataProtectionProvider:
- Encryption — AES-256 (via IDataProtector)
- Authentication — HMAC-SHA256 signature (via IDataProtector)
- No Tampering — Encrypted payload fails decryption if modified
If decryption fails (corrupted or tampered data), a graceful fallback occurs and an empty ViewState is used.
In rare cases, you may need to manually emit the ViewState hidden field. Use the RenderViewStateField utility:
<form method="post">
<TextBox ID="Name" @bind-Value="_name" />
<Button Text="Submit" @onclick="OnSubmit" />
<!-- Manually emit protected ViewState hidden field -->
<RenderViewStateField />
</form>
@code {
private string _name = "";
private void OnSubmit()
{
// Access ViewState as normal
ViewState.Set("LastName", _name);
}
}Note:
RenderViewStateFieldis typically called automatically by the framework. Manual use is only needed for custom form layouts.
A common migration pattern is to start with SSR (traditional form posts) and gradually add interactive regions via ServerInteractive. ViewStateDictionary enables seamless state sharing:
- Start with SSR Form — Traditional HTML form with server-side POST handlers
- Add Interactive Button — Replace a submit button with an interactive button
- Share State — Both SSR form fields and interactive components read/write to ViewState
- Transition Gradually — Move more logic to ServerInteractive over time
@page "/order"
@inherits BaseWebFormsComponent
@rendermode InteractiveServer
<form method="post">
<div>
<Label Text="Item Count:" />
<!-- Update ViewState when user submits form -->
<TextBox ID="ItemCount" @bind-Value="_itemCountText" />
</div>
<div>
<!-- Or use interactive component to update ViewState -->
<Label Text="Quick Add:" />
<Button Text="+" @onclick="OnQuickAdd" />
<Label Text="@(ViewState.GetValueOrDefault<int>("QuickCount", 0))" />
</div>
<Button ID="SubmitButton" Text="Place Order" @onclick="OnSubmit" Type="submit" />
</form>
@code {
private string _itemCountText = "1";
protected override void OnInitialized()
{
if (IsPostBack)
{
// Form was submitted: read values posted by form fields
if (int.TryParse(_itemCountText, out var count))
{
ViewState.Set("ItemCount", count);
}
}
else
{
// First render: restore from ViewState or use defaults
var itemCount = ViewState.GetValueOrDefault<int>("ItemCount", 1);
_itemCountText = itemCount.ToString();
}
}
private void OnQuickAdd()
{
var count = ViewState.GetValueOrDefault<int>("QuickCount", 0);
ViewState.Set("QuickCount", count + 1);
}
private void OnSubmit()
{
// Save state for next postback
if (int.TryParse(_itemCountText, out var count))
{
ViewState.Set("ItemCount", count);
}
}
}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.
In BWFC, AutoPostBack is mode-adaptive:
| Render Mode | Behavior |
|---|---|
| SSR (Static) | Emits onchange="this.form.submit()" on the HTML element — mimics the Web Forms postback |
| Interactive Server | Uses Blazor's native @onchange event binding — no form submission needed |
AutoPostBack is supported on these controls:
DropDownListCheckBoxTextBoxRadioButtonListBoxCheckBoxListRadioButtonList
Set AutoPostBack="true" on any supported control. In SSR mode, changing the control value submits the enclosing <form>:
<form method="post" @formname="filter-form" @onsubmit="HandleFilter">
<AntiforgeryToken />
<DropDownList ID="ddlDepartment"
DataSource="@departments"
DataTextField="Name"
DataValueField="Id"
AutoPostBack="true" />
<p>Selected: @selectedDepartment</p>
</form>
@code {
private List<Department> departments = new();
private string selectedDepartment = "";
protected override void OnInitialized()
{
departments = DepartmentService.GetAll();
}
private void HandleFilter()
{
// This runs on form submit (triggered by AutoPostBack in SSR)
selectedDepartment = Request.Form["ddlDepartment"];
}
}The BaseWebFormsComponent.GetAutoPostBackAttributes() method returns a dictionary of HTML attributes when AutoPostBack conditions are met:
- The control has
AutoPostBack = true - The current render mode is SSR (not Interactive)
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.
When either condition is false, the method returns an empty dictionary and no extra attributes are emitted.
AutoPostBack and ViewState work together naturally in SSR:
- User changes a DropDownList value
onchange="this.form.submit()"submits the form- The form POST includes both the new value AND the ViewState hidden fields
- On the server,
IsPostBackistrue, ViewState is deserialized, and the new value is available
This mirrors the Web Forms lifecycle without requiring any JavaScript interop or SignalR connection.
// Page code-behind
public partial class ProductPage : System.Web.UI.Page
{
protected void Page_Load()
{
if (!IsPostBack)
{
ViewState["Products"] = LoadProducts();
}
}
protected void AddButton_Click(object sender, EventArgs e)
{
var products = (List<Product>)ViewState["Products"];
products.Add(new Product { Name = ProductNameTextBox.Text });
ViewState["Products"] = products;
}
}@page "/products"
@inherits WebFormsPageBase
<form method="post">
<TextBox ID="ProductNameTextBox" @bind-Value="_productName" />
<Button ID="AddButton" Text="Add" @onclick="OnAddProduct" />
<RenderViewStateField />
</form>
@code {
private string _productName = "";
protected override void OnInitialized()
{
if (!IsPostBack)
{
ViewState["Products"] = LoadProducts();
}
}
private void OnAddProduct()
{
var products = (List<Product>)ViewState["Products"];
products.Add(new Product { Name = _productName });
ViewState["Products"] = products;
}
private List<Product> LoadProducts() => new();
public class Product
{
public string Name { get; set; }
}
}@page "/products"
@inherits BaseWebFormsComponent
@rendermode InteractiveServer
<div>
<TextBox ID="ProductNameTextBox" @bind-Value="_productName" />
<Button Text="Add" @onclick="OnAddProduct" />
</div>
@code {
private string _productName = "";
private List<Product> _products = new();
protected override void OnInitialized()
{
if (!IsPostBack)
{
_products = LoadProducts();
}
}
private void OnAddProduct()
{
if (!string.IsNullOrEmpty(_productName))
{
_products.Add(new Product { Name = _productName });
_productName = "";
}
}
private List<Product> LoadProducts() => new();
public class Product
{
public string Name { get; set; }
}
}- Use ViewState for state that survives postbacks — Ideal for SSR migration where forms round-trip
- Use IsPostBack to guard one-time initialization — Load data on first render, restore on postback
- Prefer typed fields for new code —
private int counteris clearer thanViewState["counter"] - Scope ViewState to the component — Each component instance has its own ViewState
- Use
GetValueOrDefault<T>— Type-safe retrieval with optional defaults
- Don't use ViewState for large objects — Serialization overhead grows with data size
- Don't store non-serializable objects — ViewState uses JSON serialization
- Don't rely on ViewState in ServerInteractive after navigation — ViewState is per-component-instance
- Don't mix Web Forms Page-level ViewState with component ViewState — They are separate
- Don't assume ViewState survives component disposal — Values are lost when component unmounts
| Aspect | SSR (StaticSSR) | ServerInteractive |
|---|---|---|
| HTTP Context | Available | Unavailable |
| ViewState Storage | Protected hidden field | Component memory |
| ViewState Serialization | Automatic JSON + encryption | In-memory only |
| IsPostBack Detection | HttpMethods.IsPost() |
_hasInitialized flag |
| Form Submission | Traditional HTML POST | JavaScript event, no form submission |
| Use Case | Legacy form migration | Interactive features |
ViewState gets your Web Forms code running in Blazor. The next step is refactoring to native Blazor patterns. Here's how to migrate each common ViewState usage:
=== "ViewState (Migration)"
```csharp
// Web Forms pattern preserved during migration
public int SelectedDepartmentId
{
get => ViewState.GetValueOrDefault<int>("SelectedDepartmentId");
set => ViewState.Set("SelectedDepartmentId", value);
}
```
=== "Native Blazor (Target)"
```csharp
// Refactored: simple field, no serialization overhead
private int _selectedDepartmentId;
```
=== "ViewState (Migration)"
```csharp
protected override void OnInitialized()
{
if (!IsPostBack)
{
ViewState["Products"] = LoadProducts();
}
}
```
=== "Native Blazor (Target)"
```csharp
// OnInitialized already runs once per component instance
protected override void OnInitialized()
{
_products = LoadProducts();
}
```
=== "ViewState (Migration)"
```csharp
// Parent stores state in ViewState, child reads it
ViewState["SelectedCategory"] = category;
```
=== "Native Blazor (Target)"
```razor
<!-- Parent cascades value to children -->
<CascadingValue Value="@_selectedCategory">
@ChildContent
</CascadingValue>
```
ViewState remains useful for SSR form round-trips where you need state to survive an HTTP POST without JavaScript. This is a legitimate pattern in SSR Blazor — similar to hidden fields in any web framework. The key difference from Web Forms: you're choosing to use it, not having it imposed on every control.
- WebFormsPage — Page-level wrapper combining naming scope and theming
- ResponseRedirect — Response.Redirect shim for page navigation
- Migration Guides — Step-by-step Web Forms to Blazor conversion patterns
- Blazor Render Modes — Microsoft Learn guide