- 核心概念:服务器控件 vs. HTML 控件 vs. Web 用户控件
- ASP.NET 服务器控件开发详解
- 开发自定义控件 vs. 扩展现有控件
- 开发步骤与生命周期
- 关键技术点:状态管理、事件处理、渲染
- 组件开发(Web Forms 视角下的“组件”)
- 什么是组件?
- 开发自定义组件(无 UI 的逻辑单元)
- 最佳实践与高级技巧
- 现代 ASP.NET Core 中的替代方案
- 总结与学习资源
核心概念:服务器控件 vs. HTML 控件 vs. Web 用户控件
在深入开发之前,必须清楚这三者的区别。

| 类型 | 描述 | 特点 | 示例 |
|---|---|---|---|
| HTML 服务器控件 | 对应标准 HTML 元素,通过 runat="server" 属性使其可被服务器端代码访问。 |
- 保留了大部分 HTML 的原始外观和行为。 - 属性名与 HTML 属性名不完全一致(如 CssClass 而不是 class)。- 事件模型较简单。 |
<asp:TextBox runat="server" /> |
| Web 用户控件 | 将一个 .aspx 页面中的部分 UI 和代码逻辑封装成一个 .ascx 文件。 | - 组合性:可以像拖拽普通控件一样拖拽到页面上。 - 封装性:可以定义属性、事件和方法,但通常不公开给其他项目。 - 可重用性:主要用于项目内部或特定应用场景。 |
一个包含用户名和密码输入框的登录框控件。 |
| ASP.NET 服务器控件 | .NET 框架提供的功能丰富的控件,或开发者自定义的控件。 | - 丰富功能:内置复杂功能(如数据绑定、分页、模板等)。 - 抽象性:开发者关注“做什么”,而不是“如何渲染成 HTML”。 - 跨浏览器兼容性:框架自动处理不同浏览器的差异。 - 高度可定制和可扩展。 |
GridView, Repeater, Calendar, 以及你自定义的 ProductList 控件。 |
我们讨论的重点是“自定义 ASP.NET 服务器控件”和“自定义组件”。
ASP.NET 服务器控件开发详解
自定义服务器控件主要分为两类:
- 复合控件:将现有服务器控件组合在一起,形成一个功能更强大的新控件,将一个
Label、一个TextBox和一个RequiredFieldValidator组合成一个“带验证的文本框”控件。 - 派生控件:继承自
WebControl或某个现有控件(如TextBox、DataBoundControl),并重写其行为,如渲染逻辑、事件处理等。
开发自定义控件的步骤(以复合控件为例)
假设我们要创建一个名为 AddressInput 的复合控件,它包含省、市、区三个下拉框,并且可以联动。
第一步:创建类库项目

- 在 Visual Studio 中创建一个新的 "类库" 项目。
- 引入对
System.Web程序集的引用。
第二步:创建控件类
using System;
using System.Web.UI;
using System.Web.UI.WebControls;
namespace MyCustomControls
{
// 1. 继承自 CompositeControl,这是复合控件的基类
[ToolboxData("<{0}:AddressInput runat=server></{0}:AddressInput>")]
public class AddressInput : CompositeControl
{
// 2. 声明子控件
private DropDownList _ddlProvince;
private DropDownList _ddlCity;
private DropDownList _ddlDistrict;
// 3. 定义公共属性
public string SelectedProvince
{
get { return _ddlProvince.SelectedValue; }
set { _ddlProvince.SelectedValue = value; }
}
// ... 类似地定义 SelectedCity 和 SelectedDistrict ...
// 4. 重写 CreateChildControls 方法,这是复合控件的核心
protected override void CreateChildControls()
{
Controls.Clear(); // 清除旧的子控件
_ddlProvince = new DropDownList();
_ddlCity = new DropDownList();
_ddlDistrict = new DropDownList();
// 这里可以初始化数据,例如从数据库或缓存中加载省份数据
// 为了演示,我们使用硬编码
_ddlProvince.Items.Add(new ListItem("请选择省份", ""));
_ddlProvince.Items.Add(new ListItem("广东省", "GD"));
_ddlProvince.Items.Add(new ListItem("湖南省", "HN"));
// ... 城市和区县的数据加载逻辑 ...
// 将子控件添加到 Controls 集合中
Controls.Add(_ddlProvince);
Controls.Add(new LiteralControl(" ")); // 添加一个空格
Controls.Add(_ddlCity);
Controls.Add(new LiteralControl(" "));
Controls.Add(_ddlDistrict);
}
// 5. 重写 RenderContents 方法,控制控件的 HTML 渲染
protected override void RenderContents(HtmlTextWriter writer)
{
// RenderContents 只负责渲染子控件,不渲染外层标签
// 如果需要渲染外层标签(如 div),可以使用 Render 方法
_ddlProvince.RenderControl(writer);
_ddlCity.RenderControl(writer);
_ddlDistrict.RenderControl(writer);
}
}
}
第三步:编译并使用
- 编译这个类库项目,会生成一个
.dll文件。 - 在你的 ASP.NET Web Forms 项目中,添加对这个 DLL 的引用。
- 在页面的顶部注册控件:
<%@ Register Assembly="MyCustomControls" Namespace="MyCustomControls" TagPrefix="cc" %>
- 在页面中使用它:
<form id="form1" runat="server"> <div> <cc:AddressInput ID="AddressInput1" runat="server" /> </div> </form>
关键技术点
- 控件生命周期:控件的生命周期非常复杂,包括
Init,Load,PreRender,Render等多个阶段,开发时必须理解在哪个阶段应该做什么事。CreateChildControls通常在PreRender阶段被调用。 - 状态管理:
- ViewState:用于在页面回发时保持控件的内部状态,默认情况下,复合控件的子控件状态会自动保存在 ViewState 中,你可以通过重写
SaveViewState和LoadViewState来实现自定义的状态管理。 - ControlState:与 ViewState 类似,但即使 ViewState 被禁用,ControlState 仍然有效,适用于控件运行所必需的核心状态。
- ViewState:用于在页面回发时保持控件的内部状态,默认情况下,复合控件的子控件状态会自动保存在 ViewState 中,你可以通过重写
- 事件处理:
- 服务器控件的事件分为“回发事件”(如
Click)和“回传事件”(如TextChanged)。 - 你可以声明性地(在 .aspx 中)或编程式地(在代码后端)为控件的事件绑定处理方法。
- 对于自定义控件,可以通过
OnEventName模式或RaisePostBackEvent方法来触发和冒泡事件。
- 服务器控件的事件分为“回发事件”(如
- 数据绑定:如果要让控件支持数据绑定(如
Eval,Bind),需要实现System.Web.UI.INamingContainer接口(这会创建一个新的命名容器)以及处理DataBinding事件。
组件开发(Web Forms 视角下的“组件”)
在 Web Forms 中,“组件”通常指没有用户界面、只包含业务逻辑或数据访问功能的可重用单元,它们不是控件,但可以在页面或控件中被调用。
一个 EmailService 类,它负责发送邮件,或者一个 ProductRepository 类,负责从数据库获取产品数据。

为什么需要组件?
- 关注点分离:将业务逻辑从 UI 层剥离出来。
- 可测试性:没有 UI 依赖的逻辑更容易进行单元测试。
- 可重用性:可以在项目的任何地方,甚至多个项目中重用。
示例:一个简单的组件
// 文件: Services/EmailService.cs
namespace MyWebApp.Services
{
public class EmailService
{
public void SendWelcomeEmail(string toAddress, string userName)
{
// 实际的邮件发送逻辑,例如使用 SmtpClient
// 这里只是模拟
Console.WriteLine($"正在向 {toAddress} 发送欢迎邮件,用户名: {userName}");
// MailMessage mail = new MailMessage(...);
// SmtpClient smtp = new SmtpClient(...);
// smtp.Send(mail);
} 