User and Custom Controls

This section documents ASP.NET Web Forms on .NET Framework 4.8.1, the last and permanent version of Web Forms — the page life cycle and postback model, ViewState and control state, server controls, validation controls, master pages and themes, data-bound controls, and the provider-based security model — as described by the official documentation at Microsoft Learn and the ASP.NET previous-versions archive, which are the reference these pages are written and verified against.

Web Forms receives security fixes only and has no forward path onto modern .NET (.NET Framework 4.8.1 is Microsoft’s last version of .NET Framework; Web Forms itself never shipped on .NET Core/.NET 5+). It remains supported for existing applications running on Windows but is not recommended for new development — see Choosing an ASP.NET Framework and Migrating to Modern ASP.NET for what that means in practice.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

Web Forms offers two ways to build a reusable piece of UI: a user control (markup-based, fast to author) and a custom server control (code-based, compiled, distributable as a standalone assembly). This page covers both, plus Web Parts, the framework built on top of custom controls for end-user page personalization.

.ascx user controls

A user control is authored exactly like a page — markup plus code-behind — but has no <html>/<form> of its own and is embedded into a hosting page via @Register:

<%-- Controls/AddressEditor.ascx --%>
<%@ Control Language="C#" CodeBehind="AddressEditor.ascx.cs" Inherits="WebFormsApp.Controls.AddressEditor" %>
<div class="address-editor">
    <asp:TextBox ID="StreetBox" runat="server" />
    <asp:TextBox ID="CityBox" runat="server" />
</div>
// Controls/AddressEditor.ascx.cs
public partial class AddressEditor : System.Web.UI.UserControl
{
    public event EventHandler AddressChanged;

    public string Street
    {
        get => StreetBox.Text;
        set => StreetBox.Text = value;
    }

    protected void StreetBox_TextChanged(object sender, EventArgs e)
        => AddressChanged?.Invoke(this, EventArgs.Empty);
}
<%@ Register TagPrefix="uc" TagName="AddressEditor" Src="~/Controls/AddressEditor.ascx" %>
<uc:AddressEditor ID="ShippingAddress" runat="server" OnAddressChanged="ShippingAddress_AddressChanged" />

Public properties (Street above) and events (AddressChanged) are exactly how the host page communicates with the control; @Reference on the host page (or Src= on @Register, as above) is what makes the control’s strongly typed members visible at design time and compile time.

Custom server controls

A custom server control is a compiled class, not markup, and lives in an ordinary class library assembly referenced by any number of applications.

Control vs. WebControl

Deriving from System.Web.UI.Control gives the bare life-cycle/tree participation with no rendering assumptions — appropriate for a control with no visual output of its own, or one whose output shape does not fit WebControl’s single-root-tag model. Deriving from `System.Web.UI.WebControl (itself a Control) adds the common styling API (CssClass, Font, BackColor, Style, …​) and a default single-tag rendering model (TagKey, defaulting to <span>).

Render/RenderContents and CreateChildControls

[ToolboxData("<{0}:RatingStars runat=server></{0}:RatingStars>")]
public class RatingStars : WebControl
{
    public int Value { get; set; }
    public int MaxStars { get; set; } = 5;

    protected override HtmlTextWriterTag TagKey => HtmlTextWriterTag.Span;

    protected override void RenderContents(HtmlTextWriter writer)
    {
        for (int i = 1; i <= MaxStars; i++)
        {
            writer.AddAttribute(HtmlTextWriterAttribute.Class, i <= Value ? "star filled" : "star");
            writer.RenderBeginTag(HtmlTextWriterTag.Span);
            writer.Write(i <= Value ? "&#9733;" : "&#9734;");
            writer.RenderEndTag();
        }
    }
}

Render controls the entire output including the wrapping tag; overriding RenderContents instead (as above) keeps WebControl’s own tag/attribute rendering and only customizes what goes inside it. `CreateChildControls is where a composite control builds its own internal control tree (rather than writing raw HTML), and is called lazily via EnsureChildControls() — typically overridden instead of OnInit so the tree is (re)built consistently across the life cycle, including after ViewState is restored.

INamingContainer

Implementing the marker interface INamingContainer makes a control’s own ClientID/UniqueID become a prefix for all of its children’s IDs, guaranteeing uniqueness when the control (and therefore its children) is repeated multiple times on one page — essential for any composite or data-bound control that generates children in a loop (a Repeater row, for instance, is itself a naming container).

Composite and templated controls

A templated control lets the page author supply the inner markup, rather than the control author baking it in — the pattern every data-bound list control (Repeater, GridView via TemplateField) is built on:

[ParseChildren(true)]
public class Callout : Control, INamingContainer
{
    private ITemplate _contentTemplate;

    [TemplateContainer(typeof(Control)), PersistenceMode(PersistenceMode.InnerProperty)]
    public ITemplate ContentTemplate
    {
        get => _contentTemplate;
        set => _contentTemplate = value;
    }

    protected override void CreateChildControls()
    {
        Controls.Clear();
        var container = new Control();
        _contentTemplate?.InstantiateIn(container);
        Controls.Add(container);
    }
}
<cc:Callout runat="server">
    <ContentTemplate>
        <p>Anything the page author wants, including <asp:Label runat="server" Text="other controls" />.</p>
    </ContentTemplate>
</cc:Callout>

ITemplate.InstantiateIn(Control container) is called during CreateChildControls to build whatever markup the page author supplied into the control’s own tree.

Designer attributes

[ToolboxData], [DefaultProperty], [Bindable], [Category], and [Description] (from System.ComponentModel) drive how a control appears in the Visual Studio Toolbox and Properties window; none affect runtime behavior.

Building a data-bound control

A minimal data-bound custom control implements IPostBackDataHandler/relies on WebControl plus a DataSource property and overrides DataBind/PerformDataBinding to build its child controls from the bound data during CreateChildControls, following the same DataBinding/bound-item events documented in Data Binding and Data Controls.

Web Parts and personalization

The Web Parts framework (System.Web.UI.WebControls.WebParts) turns custom controls into end-user reconfigurable page regions — Zones the user can drag parts between, minimize, close, and (per-user) remember, which is the layer Vogel’s Professional Web Parts and Custom Controls with ASP.NET 2.0 is written against:

<asp:WebPartManager ID="Manager1" runat="server" />
<asp:WebPartZone ID="LeftZone" runat="server">
    <ZoneTemplate>
        <uc:WeatherPart ID="Weather1" runat="server" Title="Weather" />
    </ZoneTemplate>
</asp:WebPartZone>
<asp:WebPartZone ID="RightZone" runat="server" />
public class WeatherPart : UserControl, IWebPart
{
    public string CatalogIconImageUrl { get; set; }
    public string Description { get; set; }
    public string Subtitle { get; set; }
    public string Title { get; set; }
    public string TitleUrl { get; set; }
    public bool AllowClose { get; set; } = true;
    public bool AllowHide { get; set; } = true;
    public bool AllowMinimize { get; set; } = true;
    public bool AllowZoneChange { get; set; } = true;
    public string AuthorizationFilter { get; set; }
}

WebPartManager orchestrates zones, drag-and-drop layout, and mode switching (Browse/Design/Edit); connections let two parts exchange data declared through [ConnectionProvider]/[ConnectionConsumer] methods; personalization providers (the default SqlPersonalizationProvider, backed by the ASPNETDB schema) persist each user’s per-page layout; and Profile properties (configured in web.config’s `<profile> section) give strongly typed, per-user settings storage (Profile.FavoriteColor) available anywhere in the application, independent of Web Parts specifically. See ASP.NET Web Parts Overview and ASP.NET Profile Properties Overview.