Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

BetterCalendar WebControl

0.00/5 (No votes)
27 May 2004 3  
A replacement for the System.Web.UI.WebControls.Calendar control.

Sample Image - BetterCalendar.gif

Introduction

BetterCalendar is a custom control derived from System.Web.UI.WebControls.Calendar. It is designed to correct some problems with the Calendar control and add some additional functionality.

Background

The Calendar control is quite useful but it has a few bugs and drawbacks:

  • It's impossible to set a calendar's appearance using only style sheets (i.e., assigning class names to the CssClass property on the various calendar style elements). This is because the control embeds inline style attributes for many elements when style properties are defaulted.

    For example, the HTML for a day cell might look like:

    <td class="calendarDay" align="Center" style="width:12%;">
    <a href="javascript:__doPostBack('Calendar1','1566')"
    style="color:Black">15</a></td>

    Here, the color setting of style attribute will override any color setting in the calendarDay class.

  • While you can define different styles for a day based on certain conditions - via the DayStyle, WeekendDayStyle, OtherMonthDayStyle, TodayDayStyle and SelectedDayStyle properties - only one style class is assigned even when more than one may apply.

    In other words, if today's date falls on a week end and it is selected on the control, the SelectedDayStyle style takes precedence:

    <td class="selectedDay_style" ... >

    Since an HTML element may be assigned multiple class names in its class attribute, it's possible to include all applicable style classes like this:

    <td class="day_style weekendDay_style todayDay_style selectedDay_style" ... >

    This would give you full control over how it appeared via a style sheet.

  • Using the DayRender event, you can control which days are selectable and which are not. However, when week or month selectors are active on the control, all days in the given week or month are included in the selection, regardless of whether they are individually selectable or not. You could use the SelectionChanged event to manually remove such dates, but that's doing the same work twice.

  • Lastly, there is no limit on the next and previous month navigation controls. You can pro grammatically prevent viewing of months outside some range via the VisibleMonthChanged event, but the next and previous links are always rendered on the control.

The BetterCalendar control addresses these issues.

Using the code

The control is packaged in a class library with the namespace BrainJar.Web.UI.WebControls which contains only the one class, BetterCalendar.

Within a Visual Studio web project, you can simply add a reference to the BrainJar.Web.UI.WebControls.dll file. You can then edit your web form in HTML view and add the appropriate @ Register directive and tags. A typical example is shown below (note the inclusion of an external style sheet).

<%@ Page language="c#" Codebehind="WebForm2.aspx.cs" AutoEventWireup="false"
    Inherits="Demo.WebForm2" %>
<%@ Register TagPrefix="BrainJar" Namespace="BrainJar.Web.UI.WebControls"
    Assembly="BrainJar.Web.UI.WebControls"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" > 
<HTML>
  <HEAD>
    <title>WebForm1</title>
    <meta name="GENERATOR" Content="Microsoft Visual Studio 7.0">
    <meta name="CODE_LANGUAGE" Content="C#">
    <meta name=vs_defaultClientScript content="JavaScript">
    <meta name=vs_targetSchema
          content="http://schemas.microsoft.com/intellisense/ie5">
    <meta http-equiv="Content-Type" content="text/html;charset=utf-8">
    <link href="Styles.css" type="text/css" rel="stylesheet">
  </HEAD>
  <body>
  
    <form id="WebForm1" method="post" runat="server">

    <h1>BetterCalendar Control  Demo</h1>
    <BrainJar:BetterCalendar id="BetterCalendar1" runat="server">
      <DayStyle CssClass="calendarDay"></DayStyle>
      <DayHeaderStyle CssClass="calendarDayHeader"></DayHeaderStyle>
      <NextPrevStyle CssClass="calendarNextPrev"></NextPrevStyle>
      <OtherMonthDayStyle CssClass="calendarOtherMonthDay">
      </OtherMonthDayStyle>
      <SelectedDayStyle CssClass="calendarSelectedDay"></SelectedDayStyle>
      <SelectorStyle CssClass="calendarSelector"></SelectorStyle>
      <TitleStyle CssClass="calendarTitle"></TitleStyle>
      <TodayDayStyle CssClass="calendarTodayDay"></TodayDayStyle>
      <WeekendDayStyle CssClass="calendarWeekendDay"></WeekendDayStyle>
    </BrainJar:BetterCalendar>

     </form>
  
  </body>
</HTML>

You can also add the control to the toolbox in the usual way (in Design mode, right-click on the toolbox, select "Customize Toolbox...", select the ".NET Framework Components" tab, press the "Browse" button and select the BrainJar.Web.UI.WebControls.dll file).

Since BetterCalendar is derived from the Calendar control, it inherits all of that control's properties, methods and events. In addition to those, it defines four new properties:

  • SelectAllInRange (Boolean)

    For use with week and month selectors (see the Calendar.SelectionMode property documentation). If set to true, all dates in a given week or month are selected. When set to false, only dates that are individually selectable are included. The default is false. See the demo project for an example of how this affects the control.

  • ShowLinkTitles (Boolean)

    Setting this property to true adds title attributes to the links rendered on the control with text describing their function. For example, the previous month navigation link will be given a title like "View December, 2003."

    Using titles can improve the accessibility of your web form. Many browsers render title information in a tool tip, but specialized browsers may render it in speech or Braille.

    Warning: Although day and month names and dates are generated according to the current culture, the remaining title text is in English.

  • MaxVisibleDate (DateTime)

    Sets an upper limit on the month the user can navigate to using the next month navigation link. When the currently viewed month is the same as MaxVisibleDate, the next month navigation link is omitted from the display.

  • MinVisibleDate (DateTime)

    Sets a lower limit on the month the user can navigate to using the next month navigation link. When the currently viewed month is the same as MinVisibleDate, the previous month navigation link is omitted from the display.

Note that both MixVisibleDate and MaxVisibleDate default to System.DateTime.MinValue, which is interpreted to mean that no limit is set.

Design-Time Support

BetterCalendar includes metadata for design-time support in Visual Studio .NET, so the new properties will appear in the Properties window when editing an instance of the control in Design view.

While it's not necessary in order to use the control, the source code includes a file named BrainJar.xsd that can be used to remove those embarrassing red squiggly lines when editing the control in HTML view.

To use the schema, copy BrainJar.xsd to your vs_install_directory\Common7\Packages\schemas\xml directory (where vs_install_directory is the directory Visual Studio .NET was installed in, usually C:\Program Files\Microsoft Visual Studio .NET). Then add a declaration for it in the BODY tag of your web form:

<body xmlns:BrainJar="urn:http://schemas.brainjar.com/AspNet/WebControls">

To add Intellisense support for the control when editing your code-behind, you can copy the BrainJar.Web.UI.WebControls.xml file from the project source (in the \debug directory) to your project's \bin directory.

The Demo Project

Several features of the BetterCalendar control can be seen in the demo project. The project contains a single web form which has both a regular Calendar control and a BetterCalendar control.

Both calendars are styled using only CSS classes defined in an external style sheet (the Styles.css file). Different styles are used for weekend days, days outside the current month, selected days, etc. You can immediately see how the Calendar control fails to deal with style classes properly.

In the code-behind, the BetterCalendar control is initialized to set MinVisibleDate and MaxVisibleDate to 90 days before and after the current date, respectively.

private void Page_Load(object sender, System.EventArgs e)
{
  // On the initial load, set the min and max view date to 90 days

  // before and after today's date, respectively.

  if (!this.IsPostBack)
  {
    this.BetterCalendar1.MinVisibleDate = DateTime.Today.AddDays(-90);
    this.BetterCalendar1.MaxVisibleDate = DateTime.Today.AddDays(+90);
  }
}

If you page through the months on the BetterCalendar control, you'll note that the next or previous month navigation link will disappear when you reach the corresponding date limit.

For both calendars, a DayRender event handler is set up. Within it, we compare the day's date to this same date range. Days outside the range have their IsSelectable property set to false.

private void Calendar_DayRender(object sender,
  System.Web.UI.WebControls.DayRenderEventArgs e)
{
  // Make days outside the view range nonselectable.

  if ((this.BetterCalendar1.MinVisibleDate != DateTime.MinValue &&
       e.Day.Date < this.BetterCalendar1.MinVisibleDate) ||
      (this.BetterCalendar1.MaxVisibleDate != DateTime.MinValue &&
       e.Day.Date > this.BetterCalendar1.MaxVisibleDate))
    e.Day.IsSelectable = false;
}

By navigating to the month at either end of the date range, can see that both calendars contain the same non-selectable days. Now, try clicking on the month selector (the ">>" link next to the days header). The Calendar control selects every day in the month, even the ones that we explicitly marked as non-selectable in the DayRender event handler.

If you select the same month on the BetterCalendar control however, you'll see that the dates marked as non-selectable are excluded. The same applies to weeks that contain non-selectable dates.

If you'd prefer to have BetterCalender behave like Calendar on week and month selections, you can just set the control's SelectAllInRange property to true.

Points of Interest

Control Rendering

Most of the code for BetterCalendar is dedicated to the overridden Render method. The Calendar control produces a fair amount of HTML and there are several display options like NextPrevFormat, NextMonthText, PrevMonthText, ShowTitle, TitleFormat, ShowDaysHeader, FirstDayOfWeek, etc., to account for. So, some of the work is broken out into separate functions for the sake of readability.

For the most part, the control is built using existing controls from System.Web.UI.WebControls rather than by writing raw HTML. It's much easier to use the properties and methods inherent to those controls for assigning attributes and applying styles than it is to manually write them as HTML.

Value Checking (or not)

No value checking is done when the MinVisibleDate, MaxVisibleDate or VisibleDate properties are set. It would be fairly easy to add code to throw a System.ArgumentOutOfRangeException if any one of those properties were assigned a value that makes VisibleDate less than MinVisibleDate or greater than MaxVisibleDate.

However, even if VisibleDate is out of the implied range, it causes no problems within the control. So rather than throw exceptions, it's left to the application programmer to ensure that those properties are consistent. This allows you some flexibility as you don't need to worry about setting these properties in any particular order.

Post Back Events

The Calendar control uses post back links to handle the next and previous month navigation and date selection. When viewing a calendar in a browser, you can hover over a date link and see something like:

javascript:__doPostBack('Calendar1','1646')

for the URI. 'Calendar1' is the control ID and, obviously, '1646' somehow refers to a date. After a little guesswork and experimentation, it turns out that these values represent the number of days between a given date and January 1, 2000.

In order to use the same scheme in BetterCalendar, a DateTime constant and two methods are defined:

private static readonly DateTime DayCountBaseDate = new DateTime(2000, 1, 1);

//

// Returns the number of days between the given DateTime value and the

// base date.

//

private int DayCountFromDate(DateTime date)
{
  return ((TimeSpan) (date - BetterCalendar.DayCountBaseDate)).Days;
}

//

// Returns a DateTime value equal to the base date plus the given number

// of days.

//

private DateTime DateFromDayCount(int dayCount)
{
  return BetterCalendar.DayCountBaseDate.AddDays(dayCount);
}

For the next and previous month post back links, the argument is prefixed with the letter 'V':

javascript:__doPostBack('Calendar1','V1613')

which means "change the visible month to June 1, 2004".

For the post back links to select a week or the entire month, the argument begins with the letter 'R' followed by the day count number, and finally, two digits representing the number of total number of days to be selected:

javascript:__doPostBack('Calendar1','R164607')

which means "select seven consecutive days starting with July 4, 2004".

The RaisePostBackEvent event implemented in BetterCalendar handles parsing this argument and performing the appropriate action (changing the visible month or selecting a date or range of dates).

Storing Non-selectable Dates

Within the BetterCalendar.Render method, a System.Web.UI.WebControls.CalendarDay and a System.Web.UI.WebControls.TableCell object is created for each day in the calendar display. The OnDayRender event is raised and these two objects are passed to any DayRender event handler assigned to the control, just like Calendar does.

After control returns from the event handler, the CalendarDay.IsSelectable property is checked. If it was set to false, that day's date is added to an ArrayList.

// Create a list for storing nonselectable dates.

ArrayList nonselectableDates = new ArrayList();

for (...)

  for (...)

    ...

    // Create a CalendarDay and a TableCell for the date.

    CalendarDay day = this.Day(date);
    TableCell cell = this.Cell(day);

    // Raise the OnDayRender event.

    this.OnDayRender(cell, day);

    // If the day was marked nonselectable, add it to the list.

    if (!day.IsSelectable)
      nonselectableDates.Add(day.Date.ToShortDateString());

The obvious thing to do would be to save this list to the view state. However, since the control is being rendered, this will not work because the control's SaveViewState method has already been called.

Instead, the data is stored in a hidden field added to the page. A couple of methods are defined to handle the conversion of the dates in the ArrayList to a suitable string value that can be stored in a hidden form field and subsequently restored.

//

// Saves a list of dates to the hidden form field.

//

private void SaveNonselectableDates(ArrayList dates)
{
  // Build a string array by converting each date to a day count

  // value.

  string[] list = new string[dates.Count];
  for (int i = 0; i < list.Length; i++)
    list[i] =
      this.DayCountFromDate(DateTime.Parse(dates[i].ToString())).ToString();

  // Get the hidden field name.

  string fieldName  = this.GetHiddenFieldName();

  // For the field value, create a comma-separated list from the day

  // count values.

  string fieldValue =
    HttpUtility.HtmlAttributeEncode(String.Join(",", list));

  // Add the hidden form field to the page.

  this.Page.RegisterHiddenField(fieldName, fieldValue);
}

//

// Returns a list of dates stored in the hidden form field.

//

private ArrayList LoadNonselectableDates()
{
  // Get the value stored in the hidden form field.

  string fieldName  = this.GetHiddenFieldName();
  string fieldValue = this.Page.Request.Form[fieldName];

  // Extract the individual day count values.

  string[] list = fieldValue.Split(',');

  // Convert those values to dates and store them in an array list.

  ArrayList dates = new ArrayList();
  foreach (string s in list)
    dates.Add(this.DateFromDayCount(Int32.Parse(s)));

return dates;
}

//

// Returns the name of the hidden field used to store nonselectable

// dates on the form.

//

private string GetHiddenFieldName()
{
  // Create a unique field name.

  return String.Format("{0}_NonselectableDates", this.ClientID);
}

Within the RaisePostBackEvent, when a range of dates is being selected, we can load this list and use it to remove those dates.

...

this.SelectedDates.Clear();
this.SelectedDates.SelectRange(date, date.AddDays(n - 1));

// If SelectAllInRange is false, remove any dates found

// in the nonselectable date list.

if (!this.SelectAllInRange)
{
  ArrayList nonselectableDates = this.LoadNonselectableDates();
  foreach(DateTime badDate in nonselectableDates)
    this.SelectedDates.Remove(badDate);
}

History

  • April 19, 2004
    • Initial version.
  • May 14, 2004
    • Update to improve globalization support. Month and day names and abbreviations, date formatting and the default first day of the week are now based on the current culture.
  • May 24, 2004
    • Fix for a bug where the SelectionChanged event was not always being raised when a week or month was selected.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here