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

ASP.NET Repeater 巢狀綁定實作自訂月曆控制項完整教學

     編輯:關於C#

為什麼不用內建 Calendar 控制項?

  • 樣式客製化困難(表格結構固定、CSS 類別有限)。
  • 無法直接綁定資料庫行程/事件資料。
  • 跨月導航需 PostBack,體驗不如 AJAX/前端渲染。

用 巢狀 Repeater 可完全掌控 HTML 結構、CSS、資料綁定邏輯,並輕易整合資料庫。

頁面結構:四層 Repeater 巢狀

<asp:Repeater ID="rptWeeks" runat="server" OnItemDataBound="rptWeeks_ItemDataBound">
  <ItemTemplate>
    <tr>
      <asp:Repeater ID="rptDays" runat="server" OnItemDataBound="rptDays_ItemDataBound">
        <ItemTemplate>
          <td>
            <table id="tblCell" runat="server" class="day-cell">
              <tr>
                <td class="day-number"><%# Eval("Date", "{0:dd}") %></td>
              </tr>
              <tr>
                <td class="day-events"><!-- 這裡再巢狀繫結事件 Repeater --></td>
              </tr>
            </table>
          </td>
        </ItemTemplate>
      </asp:Repeater>
    </tr>
  </ItemTemplate>
</asp:Repeater>

後端邏輯:建立月曆資料結構

public partial class CalendarPage : System.Web.UI.Page
{
    private DateTime _currentMonth;

    protected void Page_Load(object sender, EventArgs e)
    {
        if (!IsPostBack)
        {
            _currentMonth = DateTime.Today;
            if (Request.QueryString["ym"] != null)
                _currentMonth = DateTime.ParseExact(Request.QueryString["ym"], "yyyyMM", null);

            hidYearMonth.Value = _currentMonth.ToString("yyyyMM");
            lblTitle.Text = _currentMonth.ToString("yyyy年MM月");
            BindCalendar();
        }
    }

    private void BindCalendar()
    {
        // 1. 計算該月有幾週
        DateTime firstDay = new DateTime(_currentMonth.Year, _currentMonth.Month, 1);
        int weeks = 1;
        for (int d = 1; d <= DateTime.DaysInMonth(_currentMonth.Year, _currentMonth.Month); d++)
        {
            if (firstDay.AddDays(d - 1).DayOfWeek == DayOfWeek.Sunday) weeks++;
        }

        // 2. 建立週資料表(Repeater 需要 IEnumerable)
        var weekTable = new DataTable();
        weekTable.Columns.Add("WeekIndex", typeof(int));
        for (int w = 1; w <= weeks; w++)
            weekTable.Rows.Add(w);

        rptWeeks.DataSource = weekTable;
        rptWeeks.DataBind();
    }

    protected void rptWeeks_ItemDataBound(object sender, RepeaterItemEventArgs e)
    {
        if (e.Item.ItemType != ListItemType.Item && e.Item.ItemType != ListItemType.AlternatingItem) return;

        int weekIndex = (int)DataBinder.Eval(e.Item.DataItem, "WeekIndex");
        var rptDays = (Repeater)e.Item.FindControl("rptDays");
        rptDays.DataSource = GetWeekDays(weekIndex);
        rptDays.DataBind();
    }

    private DataTable GetWeekDays(int weekIndex)
    {
        DateTime firstDay = new DateTime(_currentMonth.Year, _currentMonth.Month, 1);
        int offset = (int)firstDay.DayOfWeek; // 0=Sun
        DateTime weekStart = firstDay.AddDays((weekIndex - 1) * 7 - offset);

        var dt = new DataTable();
        dt.Columns.Add("Date", typeof(DateTime));
        dt.Columns.Add("IsCurrentMonth", typeof(bool));
        dt.Columns.Add("IsToday", typeof(bool));

        for (int d = 0; d < 7; d++)
        {
            DateTime day = weekStart.AddDays(d);
            dt.Rows.Add(day, day.Month == _currentMonth.Month, day.Date == DateTime.Today);
        }
        return dt;
    }

    protected void rptDays_ItemDataBound(object sender, RepeaterItemEventArgs e)
    {
        if (e.Item.ItemType != ListItemType.Item && e.Item.ItemType != ListItemType.AlternatingItem) return;

        var tbl = (HtmlTable)e.Item.FindControl("tblCell");
        bool isCurrent = (bool)DataBinder.Eval(e.Item.DataItem, "IsCurrentMonth");
        bool isToday = (bool)DataBinder.Eval(e.Item.DataItem, "IsToday");

        if (!isCurrent) tbl.Attributes["class"] += " other-month";
        if (isToday) tbl.Attributes["class"] += " today";

        // 這裡可再巢狀綁定事件 Repeater,從資料庫讀取該日期的行程
        // var rptEvents = (Repeater)e.Item.FindControl("rptEvents");
        // rptEvents.DataSource = GetEvents((DateTime)DataBinder.Eval(e.Item.DataItem, "Date"));
        // rptEvents.DataBind();
    }
}

CSS 樣式建議

.calendar-table { border-collapse: collapse; width: 100%; }
.day-cell { width: 14.28%; height: 100px; vertical-align: top; border: 1px solid #ddd; background: #fff; }
.day-number { font-weight: bold; padding: 4px; }
.other-month .day-number { color: #ccc; }
.today { background: #fff3cd !important; }
.day-events { font-size: 0.85em; }
.event-item { background: #0d6efd; color: white; padding: 2px 4px; margin: 2px; border-radius: 3px; display: inline-block; }

跨月導航(無 PostBack,純前端跳轉)

<a href="?ym=<%# DateTime.ParseExact(hidYearMonth.Value, "yyyyMM", null).AddMonths(-1).ToString("yyyyMM") %>">上月</a>
<a href="?ym=<%# DateTime.ParseExact(hidYearMonth.Value, "yyyyMM", null).AddMonths(1).ToString("yyyyMM") %>">下月</a>

資料庫整合:行程資料綁定

private IEnumerable GetEvents(DateTime date)
{
    using (var conn = new SqlConnection(connStr))
    {
        return conn.Query(@"
            SELECT Title, StartTime, Color 
            FROM Events 
            WHERE CAST(StartTime AS DATE) = @Date", new { Date = date.Date });
    }
}

// 在 rptDays_ItemDataBound 中
var rptEvents = (Repeater)e.Item.FindControl("rptEvents");
rptEvents.DataSource = GetEvents((DateTime)DataBinder.Eval(e.Item.DataItem, "Date"));
rptEvents.DataBind();

效能優化建議

  • 一次查詢全月行程:在 BindCalendar 時撈取整個月份行程 Dictionary<DateTime, List<EventDto>> 存入 ViewState 或 Cache,ItemDataBound 直接查字典,避免 N+1 查詢。
  • 啟用 ViewState 壓縮:月曆資料量小,ViewState 可接受;大量資料改用 Session/Cache。
  • 考慮前端渲染:FullCalendar.js 等成熟庫支援拖曳、多視圖、AJAX 載入,後端僅提供 Web API。

總結

巢狀 Repeater 實作月曆的核心邏輯:外層繫結週數 → 內層繫結 7 天 → 再內層繫結行程。透過 ItemDataBound 事件逐層注入資料,配合 DataTable/List<T> 靈活構建資料結構。此方案完全掌控 HTML/CSS、易整合資料庫、無 ViewState 沉重負擔,適合中小型專案快速交付自訂月曆需求。

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