ASP.NET WebForms 雖支援繼承,但無法像 WinForms 那樣進行視覺化介面繼承。為解決跨頁面共同需求(權限檢查、日誌、URL 重寫、共用屬性/方法),採用 BasePage 繼承鏈 是經典最佳實踐。
System.Web.UI.Page
│
▼
BasePage (根基類) → 無業務邏輯:URL Rewrite、全域例外處理、Log4Net、多語系初始化
│
▼
BizBasePage (業務基類) → 業務共用屬性:CurrentUser、CurrentTenant、PermissionService、DbContext
│
├──────────────────┬──────────────────┐
▼ ▼ ▼
ViewBasePage EditBasePage ListBasePage
(檢視頁) (編輯頁) (列表/查詢頁)
| 層級 | 核心職責 | 典型成員 |
|---|---|---|
| BasePage | 基礎設施、橫切關注點 | OnError、OnPreInit、RewriteUrl、WriteLog、SetCulture |
| BizBasePage | 業務上下文、權限、資料存取 | CurrentUserId、TenantId、HasPermission(string)、UnitOfWork |
| ViewBasePage | 唯讀顯示、資料繫結 | BindDetail(int id)、SetReadOnlyMode() |
| EditBasePage | 新增/編輯、驗證、儲存 | SaveEntity()、ValidateInput()、RedirectAfterSave() |
| ListBasePage | 分頁、排序、篩選、匯出 | GridView、PageSize、SortExpression、ExportToExcel() |
public abstract class QueryBasePage : BizBasePage {
protected abstract IDataAccess DataAccess { get; }
protected abstract string DefaultSortColumn { get; }
protected abstract Control GridControl { get; }
protected void BindGrid(string whereClause, string sortExpr) {
var dt = DataAccess.GetPagedData(whereClause, sortExpr, CurrentPageIndex, PageSize);
((BaseDataBoundControl)GridControl).DataSource = dt;
((BaseDataBoundControl)GridControl).DataBind();
UpdatePager(dt.Rows.Count);
}
protected virtual void btnQuery_Click(object sender, EventArgs e) {
string where = BuildWhereClause(); // 子類實作
BindGrid(where, DefaultSortColumn);
}
}
// 具體頁面
public class UserQueryPage : QueryBasePage {
protected override IDataAccess DataAccess => new UserRepository();
protected override string DefaultSortColumn => "CreateDate DESC";
protected override Control GridControl => gvUsers;
protected override string BuildWhereClause() {
var sb = new StringBuilder("1=1");
if (!string.IsNullOrEmpty(txtName.Text)) sb.Append($" AND Name LIKE '%{txtName.Text}%'");
if (!string.IsNullOrEmpty(txtEmail.Text)) sb.Append($" AND Email LIKE '%{txtEmail.Text}%'");
return sb.ToString();
}
}
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class RequirePermissionAttribute : Attribute {
public string PermissionCode { get; set; }
}
public class BasePage : Page {
protected override void OnInit(EventArgs e) {
var attr = GetType().GetCustomAttribute();
if (attr != null && !CurrentUser.HasPermission(attr.PermissionCode)) {
Response.Redirect("~/AccessDenied.aspx");
}
base.OnInit(e);
}
}
// 使用
[RequirePermission(PermissionCode = "USER_VIEW")]
public class UserListPage : ListBasePage { ... }
base_)。Page_Load 早於子類 → 使用 OnLoad 覆寫而非事件訂閱,或明確呼叫 base.OnLoad(e)。IHttpContextWrapper,單元測試時注入 Mock。BasePage 注入 IAuthService、ILogService 等服務)。Core 採用 Filter Pipeline(AuthorizationFilter、ActionFilter、ResultFilter、ExceptionFilter)取代 BasePage 繼承,配合 Dependency Injection 注入共用服務,Tag Helpers / ViewComponents 實現 UI 重用,架構更彈性、可測試。