首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> C# >> 關於C#

C# ASP.NET 自定義服務器控件開發—— 下拉日歷 DatePicker 完整實現

     編輯:關於C#

ASP.NET WebForms 時代,自定義服務器控件是封裝可復用 UI 邏輯的核心手段。本文實戰演示:繼承 WebControl、實現 IPostBackEventHandler、構建復合控件、生命周期管理、ViewState 持久化、設計器屬性、渲染輸出、客戶端腳本協作,打造一個功能完整的下拉日歷選擇器。

一、控件類別繼承鏈

public class DatePicker : WebControl, IPostBackEventHandler
{
    // 必須實現 IPostBackEventHandler 接收按鈕回發
}

二、核心字段與子控件構建

private Calendar _calendar;           // 子日歷控件
private const string BtnDefaultStyle = "border:1px solid gray;cursor:pointer;";
private const string BtnDefaultText = "...";

protected override void CreateChildControls()
{
    Controls.Clear();
    _calendar = new Calendar
    {
        ID = CalendarID,
        SelectedDate = DateTime.Parse(Text),
        TitleFormat = TitleFormat.MonthYear,
        NextPrevFormat = NextPrevFormat.CustomText,
        NextMonthText = "下月",
        PrevMonthText = "上月",
        CellSpacing = 0,
        Font = { Name = "Verdana", Size = FontUnit.Parse("9pt") },
        SelectedDayStyle = { BackColor = Color.FromArgb(0x333399), ForeColor = Color.White },
        DayStyle = { BackColor = Color.FromArgb(0xCCCCCC) },
        TodayDayStyle = { BackColor = Color.FromArgb(0x999999), ForeColor = Color.Aqua },
        DayHeaderStyle = { Font = { Bold = true, Size = FontUnit.Parse("8pt") }, ForeColor = Color.FromArgb(0x333333), Height = Unit.Parse("8pt") },
        NextPrevStyle = { Font = { Bold = true, Size = FontUnit.Parse("8pt") }, ForeColor = Color.White },
        TitleStyle = { Font = { Bold = true, Size = FontUnit.Parse("12pt") }, ForeColor = Color.White, BackColor = Color.FromArgb(0x333399), Height = Unit.Parse("12pt") },
        OtherMonthDayStyle = { ForeColor = Color.FromArgb(0x999999) }
    };
    _calendar.Style.Add("display", "none"); // 初始隱藏
    _calendar.SelectionChanged += Calendar_SelectionChanged;
    Controls.Add(_calendar);
}

三、屬性與 ViewState 持久化

[DefaultProperty("Text")]
[ToolboxData("<{0}:DatePicker runat=server>")]
public class DatePicker : WebControl, IPostBackEventHandler
{
    // 文本屬性:日期字符串
    public string Text
    {
        get
        {
            EnsureChildControls();
            object o = ViewState["Text"];
            return o?.ToString() ?? DateTime.Today.ToString("yyyy-MM-dd");
        }
        set
        {
            EnsureChildControls();
            if (DateTime.TryParse(value, out var dt))
                ViewState["Text"] = (DateFormat == CalendarEnum.LongDateTime) ? dt.ToString("yyyy-MM-dd") : dt.ToString("yyyy-M-d");
            else
                throw new ArgumentOutOfRangeException("請輸入有效日期字符串,如 2024-01-15");
        }
    }

    // 格式枚舉
    public enum CalendarEnum { LongDateTime, ShortDateTime }
    public CalendarEnum DateFormat
    {
        get => (CalendarEnum)(ViewState["DateFormat"] ?? CalendarEnum.LongDateTime);
        set { ViewState["DateFormat"] = value; Text = Text; } // 重新格式化
    }

    // 按鈕樣式/文本
    public string ButtonStyle { get => (string)ViewState["ButtonStyle"] ?? BtnDefaultStyle; set => ViewState["ButtonStyle"] = value; }
    public string ButtonText { get => (string)ViewState["ButtonText"] ?? BtnDefaultText; set => ViewState["ButtonText"] = value; }

    // Enabled 重寫:同步禁用按鈕
    public override bool Enabled
    {
        get => (bool?)(ViewState["Enabled"]) ?? true;
        set => ViewState["Enabled"] = value;
    }

    // 內部 ID 生成(確保唯一)
    public string CalendarID => ClientID + "_Cal";
    public string InputID => ClientID + "_Input";
    public string ButtonID => ClientID + "_Btn";
}

四、渲染輸出:Table + Input + Button + Calendar

protected override void Render(HtmlTextWriter w)
{
    EnsureChildControls();

    // 外層 Table 定位
    w.AddStyleAttribute("position", "absolute");
    w.AddStyleAttribute("left", Style["LEFT"]);
    w.AddStyleAttribute("top", Style["TOP"]);
    if (Width != Unit.Empty) w.AddStyleAttribute(HtmlTextWriterStyle.Width, Width.ToString());
    else w.AddStyleAttribute(HtmlTextWriterStyle.Width, "200px");

    w.RenderBeginTag(HtmlTextWriterTag.Table);
    w.RenderBeginTag(HtmlTextWriterTag.Tr);

    // 第一行:輸入框 (90%) + 按鈕 (10%)
    w.AddAttribute(HtmlTextWriterAttribute.Width, "90%");
    w.RenderBeginTag(HtmlTextWriterTag.Td);
    RenderInput(w);
    w.RenderEndTag(); // Td

    w.AddAttribute(HtmlTextWriterAttribute.Width, "*");
    w.RenderBeginTag(HtmlTextWriterTag.Td);
    RenderButton(w);
    w.RenderEndTag(); // Td

    w.RenderEndTag(); // Tr

    // 第二行:日歷跨兩列
    w.RenderBeginTag(HtmlTextWriterTag.Tr);
    w.AddAttribute(HtmlTextWriterAttribute.ColSpan, "2");
    w.RenderBeginTag(HtmlTextWriterTag.Td);
    _calendar.RenderControl(w); // 渲染子控件
    w.RenderEndTag(); // Td
    w.RenderEndTag(); // Tr

    w.RenderEndTag(); // Table
}

private void RenderInput(HtmlTextWriter w)
{
    if (!Enabled) w.AddAttribute(HtmlTextWriterAttribute.ReadOnly, "true");
    w.AddAttribute(HtmlTextWriterAttribute.Type, "text");
    w.AddAttribute(HtmlTextWriterAttribute.Id, InputID);
    w.AddAttribute(HtmlTextWriterAttribute.Name, InputID);
    w.AddAttribute(HtmlTextWriterAttribute.Value, Text);
    w.AddStyleAttribute(HtmlTextWriterStyle.Width, "100%");
    w.AddStyleAttribute(HtmlTextWriterStyle.FontFamily, Font.Name);
    w.AddStyleAttribute(HtmlTextWriterStyle.FontSize, Font.Size.ToString());
    w.AddStyleAttribute(HtmlTextWriterStyle.FontWeight, Font.Bold ? "bold" : "");
    w.AddStyleAttribute(HtmlTextWriterStyle.BackgroundColor, ColorTranslator.ToHtml(BackColor));
    w.AddStyleAttribute(HtmlTextWriterStyle.Color, ColorTranslator.ToHtml(ForeColor));
    w.RenderBeginTag(HtmlTextWriterTag.Input);
    w.RenderEndTag();
}

private void RenderButton(HtmlTextWriter w)
{
    if (!Enabled) w.AddAttribute(HtmlTextWriterAttribute.Disabled, "true");
    w.AddAttribute(HtmlTextWriterAttribute.Type, "submit");
    w.AddAttribute(HtmlTextWriterAttribute.Id, ButtonID);
    w.AddAttribute(HtmlTextWriterAttribute.Name, ButtonID);
    w.AddAttribute(HtmlTextWriterAttribute.Value, ButtonText);
    w.AddStyleAttribute(HtmlTextWriterStyle.Width, "100%");
    w.AddAttribute(HtmlTextWriterAttribute.Style, ButtonStyle);
    // 關鍵:點擊按鈕觸發回發,服務端 RaisePostBackEvent
    w.AddAttribute(HtmlTextWriterAttribute.OnClick, Page.GetPostBackEventReference(this));
    w.RenderBeginTag(HtmlTextWriterTag.Input);
    w.RenderEndTag();
}

五、回發處理:顯示/隱藏日歷、選擇日期同步

// IPostBackEventHandler 實現
public void RaisePostBackEvent(string eventArgument)
{
    OnClick(EventArgs.Empty);
}

protected virtual void OnClick(EventArgs e)
{
    EnsureChildControls();
    // 點擊按鈕:切換日歷顯示狀態
    if (_calendar.Style["display"] == "none")
    {
        _calendar.SelectedDate = DateTime.Parse(Text);
        _calendar.Style["display"] = ""; // 顯示
    }
    else
    {
        _calendar.Style["display"] = "none"; // 隱藏
    }
}

// 日歷選擇日期事件
private void Calendar_SelectionChanged(object sender, EventArgs e)
{
    Text = _calendar.SelectedDate.ToString(DateFormat == CalendarEnum.LongDateTime ? "yyyy-MM-dd" : "yyyy-M-d");
    _calendar.Style["display"] = "none"; // 選完自動隱藏
}

六、關鍵技術點總結

技術點說明
復合控件包含 Input、Button、Calendar 三個子控件
ViewState所有屬性值存入 ViewState,跨回發保持
IPostBackEventHandler接收按鈕回發,服務端控制日歷顯隱
EnsureChildControls屬性存取前確保子控件已創建
Style[display]=none隱藏日歷但保留 HTML,避免佈局跳動
ClientID/UniqueID生成唯一 ID/Name,避免多實例衝突

七、使用示例

<%@ Register Assembly="AquaCalendar" Namespace="AquaCalendar" TagPrefix="aq" %>
<aq:DatePicker ID="dp1" runat="server" Text="2024-01-15" DateFormat="LongDateTime"
    Width="220px" ButtonText="選擇" ButtonStyle="background:#333399;color:white;border:none;" />

八、現代替代方案對比

方案優點缺點
本文 WebControl服務端完整控制、ViewState 自動、設計器支持WebForms 過時、回發開銷大、移動端體驗差
ASP.NET Core + TagHelper現代、輕量、Razor 原生無 ViewState、需手動 JS 交互
jQuery UI / Flatpickr / Bootstrap Datepicker純客戶端、體驗佳、生態豐富需手動綁定、驗證、本地化
Blazor 組件C# 全棧、組件化、WebAssembly學習曲線、體積較大

九、總結

自定義服務器控件 = WebControl + 子控件構建 + ViewState 屬性 + Render 渲染 + PostBack 事件。本文 DatePicker 完整展示了這一經典模式。雖然 WebForms 已退出歷史舞台,但理解其控件生命周期、狀態管理、渲染管道,對掌握 ASP.NET Core TagHelper、Blazor 組件、甚至 React 組件設計思想都有極大啟發。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved