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