The Page system in BlazorWebFormsComponents recreates the System.Web.UI.Page code-behind experience for Blazor. It lets converted pages use Page.Title, Page.MetaDescription, Page.MetaKeywords, and if (!IsPostBack) with zero syntax changes from Web Forms.
Original Microsoft implementation: System.Web.UI.Page
In ASP.NET Web Forms, the Page object provided a central place for page-level properties and functionality. Every code-behind file inherited from System.Web.UI.Page, giving automatic access to Page.Title, Page.MetaDescription, Page.MetaKeywords, and IsPostBack:
// Web Forms code-behind (.aspx.cs)
public partial class Products : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
Page.Title = "Products - Contoso";
Page.MetaDescription = "Browse our product catalog";
Page.MetaKeywords = "products, catalog, shopping";
}
}
}These properties automatically updated the HTML <title> and <meta> elements in the rendered page.
The Page system uses three complementary pieces:
┌──────────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ WebFormsPageBase │ │ IPageService │ │ WebFormsPage │
│ (code-behind API) │──────▶│ (scoped bridge) │──────▶│ (layout wrapper)│
│ │ writes│ │ reads │ │
│ • Title │ │ • Title │ │ • <PageTitle> │
│ • MetaDescription │ │ • MetaDescription │ │ • <meta> tags │
│ • MetaKeywords │ │ • MetaKeywords │ │ • NamingContainer│
│ • IsPostBack │ │ • Change events │ │ • ThemeProvider │
│ • Page (self-ref) │ │ │ │ │
└──────────────────────┘ └──────────────────┘ └──────────────────┘
Your pages inherit DI service In your layout
| Piece | Role | Where it lives |
|---|---|---|
WebFormsPageBase |
Abstract base class for converted pages. Provides Title, MetaDescription, MetaKeywords, IsPostBack, and Page (self-reference). This is the code-behind API your pages inherit. |
@inherits in _Imports.razor |
IPageService / PageService |
Scoped service that bridges the base class and the renderer. Property setters fire change events. | Registered by AddBlazorWebFormsComponents() |
WebFormsPage |
Unified layout wrapper that provides NamingContainer (ID mangling), ThemeProvider (skin cascading), AND page head rendering (<PageTitle> + <meta> tags). Subscribes to IPageService events. |
<WebFormsPage> wrapping @Body in layout |
Key point: WebFormsPageBase and WebFormsPage are complementary, not redundant. WebFormsPageBase writes data to IPageService; WebFormsPage reads from IPageService and renders HTML. Both are needed.
!!! tip "All-in-one layout component"
<WebFormsPage> combines naming, theming, and head rendering into a single component. You no longer need a separate <Page /> component — <WebFormsPage> handles everything.
Two things need to happen once in your application:
// Program.cs
builder.Services.AddBlazorWebFormsComponents();This registers IPageService as a scoped service.
@* MainLayout.razor *@
@inherits LayoutComponentBase
<WebFormsPage ID="MainContent">
@Body
</WebFormsPage><WebFormsPage> provides three features in one component:
- NamingContainer — Web Forms-style
ctl00ID mangling for child controls - ThemeProvider — Cascades
ThemeConfigurationto child components (optionalThemeparameter) - Page head rendering — Subscribes to
IPageServiceand renders<PageTitle>+<meta>tags
!!! note "RenderPageHead parameter"
If you need to handle head rendering separately (e.g., with a standalone <Page /> component), set RenderPageHead="false" on <WebFormsPage> to disable head rendering.
!!! note "Standalone Page.razor"
The <BlazorWebFormsComponents.Page /> component is still available as a standalone head renderer for apps that don't use <WebFormsPage>. However, when using <WebFormsPage>, the standalone <Page /> is unnecessary — don't use both, or you'll get duplicate head content.
For converted Web Forms pages, inherit from WebFormsPageBase. This is the recommended approach because it preserves your existing code-behind patterns with zero syntax changes.
Add to your _Imports.razor:
@inherits BlazorWebFormsComponents.WebFormsPageBase!!! note "Scope"
The @inherits directive applies to all .razor files in the same directory and subdirectories. If only some pages need it, place a separate _Imports.razor in the pages folder.
With WebFormsPageBase as your base class, Web Forms code-behind patterns work unchanged:
@page "/products"
<h1>Products</h1>
@code {
protected override void OnInitialized()
{
if (!IsPostBack)
{
Page.Title = "Products - Contoso";
Page.MetaDescription = "Browse our product catalog";
Page.MetaKeywords = "products, catalog, shopping";
}
}
}This works because:
Pagereturnsthis(a self-reference), soPage.Titleresolves tothis.TitleTitle,MetaDescription, andMetaKeywordsdelegate to the injectedIPageServiceIsPostBackalways returnsfalse— soif (!IsPostBack)blocks always execute, matching first-load behavior
| Property | Type | Description |
|---|---|---|
Title |
string |
Gets/sets the page title. Delegates to IPageService.Title. |
MetaDescription |
string |
Gets/sets the meta description. Delegates to IPageService.MetaDescription. |
MetaKeywords |
string |
Gets/sets the meta keywords. Delegates to IPageService.MetaKeywords. |
IsPostBack |
bool |
Always returns false. Blazor has no postback model. |
Page |
WebFormsPageBase |
Returns this. Enables Page.Title syntax. |
The following System.Web.UI.Page members are deliberately omitted to encourage proper Blazor migration:
Page.Request— UseNavigationManagerorHttpContextinsteadPage.Response— UseNavigationManager.NavigateTo()for redirectsPage.Session— UseProtectedSessionStorage,ProtectedLocalStorage, or a scoped servicePage.Server— Use standard .NET APIs (Path,HttpUtility, etc.)Page.Cache— UseIMemoryCacheorIDistributedCache
!!! warning "Dead Code: if (IsPostBack)"
Code guarded by if (IsPostBack) (without !) will never execute because IsPostBack is always false. During migration, search for if (IsPostBack) (without the negation) and flag those blocks for review — they likely contain logic that needs to be reimplemented as Blazor event handlers.
For components that don't inherit from WebFormsPageBase, you can inject IPageService directly. This is useful for:
- Non-page components that need to set page metadata
- Shared components used across pages
- Developers who prefer explicit dependency injection
@inject IPageService PageService
@code {
protected override void OnInitialized()
{
PageService.Title = "My Page Title";
PageService.MetaDescription = "Description for search engines";
PageService.MetaKeywords = "keyword1, keyword2";
}
}!!! tip "Naming Convention"
If you name the injection @inject IPageService Page, the syntax matches Web Forms exactly: Page.Title = "...". However, this may conflict with the Page property on WebFormsPageBase, so use PageService as the variable name when both patterns appear in the same project.
In Web Forms, the Page object was automatically available in all code-behind files:
<%@ Page Language="C#" Title="Static Title"
MetaDescription="Page description"
MetaKeywords="keyword1, keyword2" %>// Code-behind (.aspx.cs)
public partial class MyPage : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
Page.Title = GetTitleFromDatabase();
Page.MetaDescription = GetDescriptionFromDatabase();
Page.MetaKeywords = GetKeywordsFromDatabase();
}
}
}<%@ Page Language="C#" MasterPageFile="~/Site.Master"
MetaDescription="Customer details page"
MetaKeywords="customer, details, crm"
CodeBehind="CustomerDetails.aspx.cs"
Inherits="MyApp.CustomerDetails" %>// CustomerDetails.aspx.cs
public partial class CustomerDetails : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
var customer = GetCustomer(Request.QueryString["id"]);
Page.Title = "Customer Details - " + customer.Name;
Page.MetaDescription = $"View details for {customer.Name}";
}
}
}@page "/customer/{Id:int}"
<h1>Customer Details</h1>
@if (customer != null)
{
<p>@customer.Name</p>
}
@code {
[Parameter]
public int Id { get; set; }
private Customer? customer;
protected override async Task OnInitializedAsync()
{
if (!IsPostBack)
{
customer = await GetCustomer(Id);
Page.Title = "Customer Details - " + customer.Name;
Page.MetaDescription = $"View details for {customer.Name}";
Page.MetaKeywords = "customer, details, crm";
}
}
}The only changes from the original code-behind:
- Removed
System.Web.UI.Pageinheritance (replaced byWebFormsPageBasevia_Imports.razor) - Changed
Request.QueryString["id"]to a Blazor[Parameter] - Made the method
async(optional — depends on data access)
Everything else — if (!IsPostBack), Page.Title, Page.MetaDescription — works unchanged.
- Get/Set: Read and write the page title dynamically
- Event-Driven:
TitleChangedevent fires when title is updated - Reactive: The
Page.razorcomponent automatically updates the browser title when the property changes
- Get/Set: Read and write the meta description dynamically
- SEO-Friendly: Appears in search engine results (recommended 150-160 characters)
- Event-Driven:
MetaDescriptionChangedevent fires when description is updated - Reactive: The
Page.razorcomponent automatically updates the<meta>tag when the property changes
- Get/Set: Read and write the meta keywords dynamically
- SEO Support: Helps categorize page content for search engines
- Event-Driven:
MetaKeywordsChangedevent fires when keywords are updated - Reactive: The
Page.razorcomponent automatically updates the<meta>tag when the property changes
The IPageService interface can be extended in future versions to support additional Page object features:
- Open Graph meta tags for social media
- Page-level client script registration
- Page-level CSS registration
- Custom meta tags
| Web Forms | Blazor (WebFormsPageBase) | Blazor (inject IPageService) | Notes |
|---|---|---|---|
Page.Title |
Page.Title ✅ |
PageService.Title |
Identical syntax with base class |
Page.MetaDescription |
Page.MetaDescription ✅ |
PageService.MetaDescription |
.NET 4.0+ |
Page.MetaKeywords |
Page.MetaKeywords ✅ |
PageService.MetaKeywords |
.NET 4.0+ |
if (!IsPostBack) |
if (!IsPostBack) ✅ |
N/A | Always false — block always runs |
Inherits System.Web.UI.Page |
Inherits WebFormsPageBase |
No inheritance needed | Set via _Imports.razor |
| Available automatically | Available automatically | Must inject IPageService |
Base class injects for you |
Page.Request, Page.Response |
Not available | Not available | Use Blazor equivalents |
While the Page system provides familiar Web Forms compatibility, consider these Blazor-native approaches as you refactor:
Use the built-in Blazor components directly:
@page "/about"
<PageTitle>About Us - My Company</PageTitle>
<HeadContent>
<meta name="description" content="Learn about our company" />
<meta name="keywords" content="about, company, team" />
</HeadContent>
<h1>About Us</h1>The Page system approach is appropriate when:
- Metadata depends on data loaded asynchronously
- Metadata changes based on user actions
- Metadata is set in response to events
- You want Web Forms-style programmatic control
For simpler scenarios, you can use built-in Blazor components with bound variables:
<PageTitle>@currentTitle</PageTitle>
<HeadContent>
<meta name="description" content="@currentDescription" />
</HeadContent>
@code {
private string currentTitle = "Default Title";
private string currentDescription = "Default description";
private void UpdateTitle(string newTitle)
{
currentTitle = newTitle;
}
}- Set Title Early: Set the title in
OnInitializedAsyncorOnParametersSetto ensure it's available before first render - SEO Considerations: Provide meaningful, descriptive titles for better search engine optimization
- User Context: Include relevant context in the title (e.g., customer name, product name)
- Length: Keep titles under 60 characters for optimal display in browser tabs and search results
- Consistent Pattern: Use a consistent title format across your application (e.g., "Page Name - Site Name")
- One Renderer: Place
<BlazorWebFormsComponents.Page />in the layout once — do not add it to individual pages
- WebFormsPage — Different component: provides NamingContainer + ThemeProvider
- Live Sample
- Microsoft Docs: System.Web.UI.Page
- Blazor PageTitle Component