I have been playing around with the ASP.NET ListView and Repeater controls quite a bit recently, and thought that a simple calendar control with date range selection capabilities baked-in would make a good example of how these controls can be used to produce some pretty neat functionality without a great deal of code.

The System.Web.UI.WebControls namespace already contains a calendar control of course, which is itself quite feature-rich. Although it doesn’t offer support for date range selection out-of-the-box, it can quite easily be extended to do so. The built-in ASP.NET Calendar Control also has a wealth of properties which enable its layout and appearance to be tweaked as necessary. So really, the code for this article is by way of example only; the date range selection functionality is useful, or the code could be used as the starting point for a custom calendar control if you have a need to develop one.

The calendar is implemented as a UserControl, and the UserControl contains a Repeater control, nested within an HTML table. The repeater is used to generate the linkbuttons for each of the calendar days for the current month. The end result looks, as you might expect, not entirely dissimilar to the ASP.NET Calendar:

calendar 

The binding of the repeater control is performed using two classes: CalendarMonth and CalendarWeek. The CalendarMonth class represents a single month within a specific year, and the code looks like this:

 

   1: public class CalendarMonth : IEnumerable<DateTime>
   2: {
   3:  
   4:     private List<DateTime> _days;
   5:  
   6:     public CalendarMonth(int month, int year)
   7:     {
   8:         this._days = new List<DateTime>();
   9:  
  10:         DateTime date = new DateTime(year, month, 1);
  11:         while (date.Month == month)
  12:         {
  13:             this._days.Add(date);
  14:             date = date.AddDays(1);
  15:         }
  16:     }
  17:  
  18:     #region IEnumerable<DateTime> Members
  19:  
  20:     public IEnumerator<DateTime> GetEnumerator()
  21:     {
  22:         return this._days.GetEnumerator();
  23:     }
  24:  
  25:     #endregion
  26:  
  27:     #region IEnumerable Members
  28:  
  29:     System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
  30:     {
  31:         return this._days.GetEnumerator();
  32:     }
  33:  
  34:     #endregion
  35:  
  36:     /// <summary>
  37:     /// Gets a collection of calendar weeks for the current month.
  38:     /// </summary>
  39:     /// <returns></returns>
  40:     public IList<CalendarWeek> GetWeeks()
  41:     {
  42:         List<CalendarWeek> weeks = new List<CalendarWeek>();
  43:         CalendarWeek currentWeek = null;
  44:         for (int i = 0; i < this._days.Count; i++)
  45:         {
  46:             if (currentWeek == null || this._days[i].DayOfWeek == DayOfWeek.Monday)
  47:             {
  48:                 if (currentWeek != null)
  49:                     weeks.Add(currentWeek);
  50:  
  51:                 currentWeek = new CalendarWeek();
  52:             }
  53:  
  54:             currentWeek[this._days[i].DayOfWeek] = this._days[i];
  55:  
  56:             if (i == this._days.Count - 1)
  57:                 weeks.Add(currentWeek);
  58:         }
  59:  
  60:         return weeks;
  61:     }
  62: }

As you can see from the above, there isn’t a great deal of functionality exposed: just a single public method which returns a collection of CalendarWeek instances. It is the collection returned from this method which is bound to the repeater on the page. The CalendarWeek class is intended to represent a single Monday-Sunday period within a given CalendarMonth. The CalendarWeek class is coded like this:

   1: public class CalendarWeek
   2: {
   3:     private System.Collections.Specialized.ListDictionary _days;
   4:  
   5:     internal CalendarWeek()
   6:     {
   7:         this._days = new System.Collections.Specialized.ListDictionary();
   8:  
   9:         //add the days of the week
  10:         for (int i = 1; i < 7; i++)
  11:         {
  12:             this._days.Add((DayOfWeek)i, DateTime.MinValue);
  13:         }
  14:         this._days.Add(DayOfWeek.Sunday, DateTime.MinValue);
  15:     }
  16:  
  17:     /// <summary>
  18:     /// Indexer to supply access to the date values.
  19:     /// </summary>
  20:     /// <param name="day"></param>
  21:     /// <returns></returns>
  22:     public DateTime this[DayOfWeek day]
  23:     {
  24:         get { return (DateTime)this._days[day]; }
  25:         internal set { this._days[day] = value; }
  26:     }
  27: }

Each CalendarWeek instance contains a collection of seven DateTime instances which can be accessed by day of week. In order to achieve this, the class uses the System.DayOfWeek enumeration: each day of the week is added as the key to a lightweight key/value collection. The assigning of DateTime instances to each day in the CalendarWeek occurs in the GetWeeks() method of the CalendarMonth class, when the collection of CalendarWeek instances is generated. The CalendarWeek exposes a public indexer to allow each date value to be accessed. Of course, not every month starts on a Monday and ends on a Sunday, so one or more date values in the CalendarWeek instance may not be initialised- and the UI uses this to work out whether a linkbutton should be displayed for each day.

So the UI first creates a CalendarMonth instance, gets a collection of CalendarWeek instances for the month, then binds this collection to the repeater.

The markup for the UserControl (which contains the parent HTML table for the calendar, and repeater control which generates the weeks) is structured as follows:

   1: <asp:Panel ID="pnlContainer" runat="server">
   2:     <table class="<%# CalendarCssClass %>">
   3:         <tr>
   4:             <td>
   5:                 <asp:LinkButton ID="lnkBack" runat="server" OnClick="calendar_Back">&lt;&lt;</asp:LinkButton>
   6:             </td>
   7:             <td colspan="5" style="text-align:center">
   8:                 <asp:Label ID="lblMonth" runat="server"></asp:Label>
   9:             </td>
  10:             <td runat="server" id="tdForward">
  11:                 <asp:LinkButton ID="lnkForward" runat="server" OnClick="calendar_Forward">&gt;&gt;</asp:LinkButton>
  12:             </td>
  13:         </tr>
  14:         <tr>
  15:             <td><asp:Literal ID="litMonday" runat="server"></asp:Literal></td>
  16:             <td><asp:Literal ID="litTuesday" runat="server"></asp:Literal></td>
  17:             <td><asp:Literal ID="litWednesday" runat="server"></asp:Literal></td>
  18:             <td><asp:Literal ID="litThursday" runat="server"></asp:Literal></td>
  19:             <td><asp:Literal ID="litFriday" runat="server"></asp:Literal></td>
  20:             <td><asp:Literal ID="litSaturday" runat="server"></asp:Literal></td>
  21:             <td><asp:Literal ID="litSunday" runat="server"></asp:Literal></td>
  22:         </tr>
  23:         <asp:Repeater ID="rptCalendar" runat="server" 
  24:             onitemdatabound="rptCalendar_ItemDataBound" 
  25:             onitemcommand="rptCalendar_ItemCommand">
  26:             <ItemTemplate>
  27:                 <tr class="<%# CurrentRowCssClass %>">
  28:                     <td runat="server" id="tdMonday"><asp:LinkButton ID="lnkMonday" runat="server" CausesValidation="false"></asp:LinkButton></td>
  29:                     <td runat="server" id="tdTuesday"><asp:LinkButton ID="lnkTuesday" runat="server" CausesValidation="false"></asp:LinkButton></td>
  30:                     <td runat="server" id="tdWednesday"><asp:LinkButton ID="lnkWednesday" runat="server" CausesValidation="false"></asp:LinkButton></td>
  31:                     <td runat="server" id="tdThursday"><asp:LinkButton ID="lnkThursday" runat="server" CausesValidation="false"></asp:LinkButton></td>
  32:                     <td runat="server" id="tdFriday"><asp:LinkButton ID="lnkFriday" runat="server" CausesValidation="false"></asp:LinkButton></td>
  33:                     <td runat="server" id="tdSaturday"><asp:LinkButton ID="lnkSaturday" runat="server" CausesValidation="false"></asp:LinkButton></td>
  34:                     <td runat="server" id="tdSunday"><asp:LinkButton ID="lnkSunday" runat="server" CausesValidation="false"></asp:LinkButton></td>  
  35:                 </tr>
  36:             </ItemTemplate>
  37:         </asp:Repeater>
  38:     </table>
  39: </asp:Panel>

And to complete the picture, the code which initialises each repeater item when the control is data bound:

   1: /// <summary>
   2: /// Invoked when a repeater item is being data bound.
   3: /// </summary>
   4: /// <param name="sender"></param>
   5: /// <param name="e"></param>
   6: protected void rptCalendar_ItemDataBound(object sender, RepeaterItemEventArgs e)
   7: {
   8:     if (e.Item.ItemType == ListItemType.Item || e.Item.ItemType == ListItemType.AlternatingItem)
   9:     {
  10:         this.InitialiseCalendarItem(e.Item);
  11:     }
  12: }
  13:  
  14: /// <summary>
  15: /// Initialise calendar repeater item.
  16: /// </summary>
  17: /// <param name="item"></param>
  18: protected void InitialiseCalendarItem(RepeaterItem item)
  19: {
  20:     CalendarWeek week = (CalendarWeek)item.DataItem;
  21:     DateTime date;
  22:     LinkButton lnk;
  23:  
  24:     for (int i = 0; i < 7; i++)
  25:     {
  26:         DayOfWeek day = (DayOfWeek)i;
  27:         date = week[day];
  28:  
  29:         //initialise th date linkbutton
  30:         lnk = (LinkButton)item.FindControl("lnk" + day.ToString());
  31:         lnk.Visible = !(date.Equals(DateTime.MinValue));
  32:         if (lnk.Visible)
  33:         {
  34:             lnk.Text = date.Day.ToString();
  35:             lnk.CommandArgument = date.ToString();
  36:         }
  37:  
  38:         if (!date.Equals(DateTime.MinValue))
  39:         {
  40:             if (this.DateSelectionMode == SelectionMode.Single)
  41:             {
  42:                 //if the current day is selected, highlight it
  43:                 DateTime selectedDate = this.SelectedDate;
  44:                 if (!selectedDate.Date.Equals(DateTime.MinValue) && date.Date.Equals(selectedDate.Date))
  45:                 {
  46:                     this.HighlightDay(item, day);
  47:                 }
  48:             }
  49:             else
  50:             {
  51:                 //if the current day is contained in the date range, highlight it
  52:                 DateRange selectedRange = this.SelectedDateRange;
  53:                 if (!selectedRange.Equals(DateRange.MinValue) && selectedRange.Contains(date))
  54:                 {
  55:                     this.HighlightDay(item, day);
  56:                 }
  57:             }
  58:         }
  59:     }
  60: }
  61:  
  62: /// <summary>
  63: /// Highlights a selected day in the current item.
  64: /// </summary>
  65: /// <param name="item"></param>
  66: /// <param name="day"></param>
  67: protected void HighlightDay(RepeaterItem item, DayOfWeek day)
  68: {
  69:     HtmlTableCell td = (HtmlTableCell)item.FindControl("td" + day.ToString());
  70:  
  71:     if (!string.IsNullOrEmpty(this.SelectedDateCssClass))
  72:     {
  73:         td.Attributes.Add("class", this.SelectedDateCssClass);
  74:     }
  75:     else
  76:     {
  77:         td.BgColor = "red";
  78:     }
  79: }

As an extra bit of sugar coating, the abbreviated day names (shown at the top of the control) are set dynamically in the code-behind, based on the current culture settings:

   1: /// <summary>
   2: /// Sets the abbreviated day names for the current culture.
   3: /// </summary>
   4: private void SetDayNames()
   5: {
   6:     Array values = Enum.GetValues(typeof(System.DayOfWeek));
   7:     DayOfWeek day;
   8:     int dayNumber;
   9:     Literal lit = null;
  10:  
  11:     foreach (object value in values)
  12:     {
  13:         day = (DayOfWeek)value;
  14:         dayNumber = (int)day;
  15:         lit = (Literal)this.FindControl("lit" + day.ToString());
  16:         lit.Text = System.Globalization.CultureInfo.CurrentUICulture.DateTimeFormat.AbbreviatedDayNames[dayNumber];
  17:     }   
  18: }

The rest of the code in the UserControl is concerned with the navigation between months, and tracking the selected date or date range, and I won’t go into any of the details here- suffice to say it’s all pretty straightforward. The UserControl also exposes a few properties (all of which persisted via ViewState) for setting CSS classes on the calendar, for setting the selected date (or date range). There’s also a property for switching the selection mode between single date and date range, and finally, a couple of events for notifying when a date or date range is selected.

In summary, we have a pretty elegant, and readily extensible, calendar control in fewer than 800 lines of code…not bad! The UserControl, class files, and a test page are included in the zip file which can be downloaded using the link at the top of the article.