Enhancing the ASP.NET Calendar Control
Save Your Dates and Mark Your Calendar
October 30, 2009
A date is a special type and needs ad hoc programming tools to be managed effectively. Virtually all programming languages and frameworks provide a native date type with an appropriate set of methods and properties. In input forms, then, specifying a date is always a bit problematic. Using a text field requires careful validation and parsing to transform the typed string in a date object. You must deal with date formats, separators, and cultures, and add clear messages to the user interface to instruct the user on how to fill the date fields. For this reason, a calendar control is often used in the user interface whenever the user is expected to enter a date. Purposely,ASP.NET comes with a built-in Calendar control.
The ASP.NET Calendar control displays a single-month calendar that can be used to select a date while moving to the next or previous month, as needed. The calendar is an interactive control that posts back as you click to select a day or to move to another month. The control is not data bound, but supports a number of styles for the selected day, weekend days, another month s days, and so on. The Calendar control is mainly designed for input and supports different selection modes: day, week, month. It also features events that, properly used, allow you to modify the content of the individual date cells and format them at your leisure.
In this article, I ll enhance the basic Calendar control to make it look a bit more like the Outlook calendar component. I ll define a collection property through which you can indicate appointments and activities that will be rendered in the user interface using a different style. As a result, you can build calendar pages where the busy days show up clearly and in the style you set. Once you ve got such a calendar, the next step is quite obvious make it display multiple consecutive months. In this article, I ll provide a quick and dirty solution to this problem, as well. But let s tackle one issue at a time and start with adding activities and busy dates to the basic Calendar control.
Add Busy Dates to the Calendar
The built-in Calendar control lacks a property to recognize and set special dates in a month. Listing One presents the full source code of a custom Calendar control. The control features a new property named BusyDays. The property is a collection of scheduled activities formed by a start date and an end date, a description, and a Boolean flag to distinguish between personal and business activities.
Collection properties always require special care in ASP.NET control development. Unlike properties implemented through primitive types, collection properties are not explicitly persisted to the viewstate. The following code snippet shows the classic implementation of a control property:
public string Text{ get { Object o = ViewState["Text"]; if (o == null) return String.Empty; return (String) o; } set { ViewState["Text"] = value; }}
The value stored by the property is serialized to the viewstate and recovered from there. For collection properties, this model is problematic because it s the source of potential performance issues. A collection can be easily serialized to the viewstate, but what about the constituent type of child objects? They could be nonserializable or instances of a custom, non-primitive type. As a result, the system should use the BinaryFormatter class to serialize the state of the object. This approach is too expensive in the long run and has unacceptable costs for a repeated operation. It is much more affordable for collection classes to feature an ad hoc serialization engine that determines how to save the state to the viewstate.
Collection properties used by ASP.NET custom controls generally implement the IStateManager interface. By implementing the IStateManager interface, an object prescribes how its state should be saved to, or read from, the viewstate. Listing Two shows the implementation of the IStateManager interface in the collection class used for the BusyDays property of our calendar. The following code shows the implementation of the collection property:
private CalendarItemCollection _busyDays = null;public CalendarItemCollection BusyDays{ get { if (_busyDays == null) { _busyDays = new CalendarItemCollection(); if (base.IsTrackingViewState) _busyDays.TrackViewState(); } return _busyDays; }}
A few tailor-made attributes complete the definition of the BusyDays property, as shown in Listing One. The DesignerSerializationVisibility and PersistenceMode attributes instruct the Visual Studio 2005 designer on how to save the values assigned to the property declaratively. In particular, the settings in Listing One indicate that values will be saved as content within a child tag with the same name as the property. Furthermore, the ParseChildren attribute on the class instructs the ASP.NET parser to look inside the control tag to find the contents of the specified property.
As per the combined effect of the code in Listing One and Listing Two, you can now write the following markup code in ASP.NET pages:
The final result of the sample calendar is shown in Figure 1. To get that, some changes are required to the rendering process of the calendar control.
Figure 1: A modified Calendar control that supports busy dates.
Modifying the Rendering Process
The output of an ASP.NET Calendar control is a 7 x 6 table, where each cell represents a day in a given month. Columns indicate days of the week, whereas rows indicate weeks in the month. The control features a few events, one of which, the OnDayRender event, is fired when the control is going to render a cell:
protected override void OnDayRender(TableCell cell, CalendarDay day){ base.OnDayRender(cell, day); if (IsBusyDay(day.Date)) cell.BackColor = SelectedDayStyle.BackColor;}
In the preceding implementation, you first let the base control do its own standard job, then you kick in and check whether the just-rendered day is a busy day. If so, you use a different color to render its background. More in general, though, you might want to use a new style property to characterize busy days in the calendar.
How would you test if the day being rendered is a busy day according to the content of the BusyDays property? You simply loop through the calendar items in the collection and verify whether a day falls between each pair of start and end dates.
Note that by default the calendar operates in selection mode, meaning that it renders days using link buttons. As you click a day, the day is selected through a postback and rendered using a different style. The selection mode is controlled via the SelectionMode property. You can select by week or month, as well, or you can set up a read-only calendar. In this case, set SelectionMode to None.
Multiple Months View
With SelectionMode disabled and support established for appointments and activities, you now have a tool for calendaring functions rather than a simple input control. Because the original Calendar control is mostly an input control, it only supports a single-month view. A true calendaring tool, instead, requires a multiple-month view. How would you build that? The simplest option is composing two or more modified calendar controls in a Web user control, as shown in Listing Three.
Two neighboring single-month calendars are not the same as one single calendar designed for a multiple-month view. In particular, you might want to provide one single navigation mechanism and link to the months before and after the period shown. By default, each calendar shows its own navigation links that, if left on, would overlap (thus posing synchronization issues). For example, if you display February and March on the different calendars, you establish that the February calendar links to January and March and the March calendar links to February and April. What if you navigate back from the March calendar? Without additional work, you would have the two individual calendars hanging on the same month. All you really need, instead, is one link to January and one link to April. This has to be built manually. The simplest way is by hiding default links and adding a couple of navigation buttons handled by the user control. You hide default navigation links by setting the ShowNextPrevMonth property to false on both child calendars; here s the code you need to attach to the buttons:
protected void Button1_Click(object sender, EventArgs e) { SetVisibleDate(Calendar1.VisibleDate.AddMonths(-1));}protected void Button2_Click(object sender, EventArgs e) { SetVisibleDate(Calendar1.VisibleDate.AddMonths(1));}protected void SetVisibleDate(DateTime day) { Calendar1.VisibleDate = day; Calendar2.VisibleDate = day.AddMonths(1);}
Note that you also must ensure that all child calendars have their VisibleDate and BusyDays properties set to the same values. Ideally you d build an object model on top of the Web user control and use a new set of user control properties to let users select the visible view and define busy days. The Web user control will expose its own BusyDays property through which dates are acquired. Next, dates are forwarded to all child calendars for actual display. To force this latter step, a public update method is defined, as shown here:
protected void Button1_Click(object sender, EventArgs e){ CalendarItem item = new CalendarItem(); item.Description = "Summer break"; item.IsHoliday = true; item.StartDate = new DateTime(2007, 7, 22); item.EndDate = new DateTime(2007, 7, 31); Calendar1.BusyDays.Add(item); // Force new busy dates into child calendars Calendar1.UpdateChildCalendars();}
Figure 2 shows the sample multiple-month calendar in action. Download the accompanying source code for the complete details.
Figure 2: A multiple-month view calendar.
Conclusion
There s an important lesson to learn from this code: You can fairly easily adapt a control to support a new functionality, but you can t easily alter the overall behavior and design of a control to add new features. The ASP.NET built-in Calendar control is not designed for multi-month view. You can easily make it show busy dates, and you also can easily switch it to a multi-view control. Aggregating two or more calendars in a user control works, but it s a short-run solution. The number of displayed months, for example, is hard-coded, and synchronization between child calendars must be enforced programmatically by the developer. Next month I ll explore ways to build a new multiple-month calendar control from the ground up. Stay tuned.
The sample code accompanying this article is available for download.
Begin Listing One An extended Calendar control[ParseChildren(true, "BusyDays")]public class Calendar : System.Web.UI.WebControls.Calendar{ public Calendar() { } private CalendarItemCollection _busyDays; [DesignerSerializationVisibility( DesignerSerializationVisibility.Content)] [PersistenceMode(PersistenceMode.InnerProperty)] public CalendarItemCollection BusyDays { get { if (_busyDays == null) { _busyDays = new CalendarItemCollection(); if (base.IsTrackingViewState) _busyDays.TrackViewState(); } return _busyDays; } } protected override void OnDayRender( TableCell cell, CalendarDay day) { base.OnDayRender(cell, day); if (IsBusyDay(day.Date)) cell.BackColor = SelectedDayStyle.BackColor; } protected virtual bool IsBusyDay(DateTime day) { CalendarItem item; for (int i = 0; i < BusyDays.Count; i++) { item = _busyDays[i]; if (day >= item.StartDate && day <= item.EndDate) { return true; } } return false; }}
End Listing One
Begin Listing Two A custom scheme for viewstate serializationpublic class CalendarItem{ private DateTime _startDate; private DateTime _endDate; private string _description; private bool _isHoliday; public DateTime StartDate { get { return _startDate; } set { _startDate = value; } } public DateTime EndDate { get { return _endDate; } set { _endDate = value; } } public string Description { get { return _description; } set { _description = value; } } public bool IsHoliday { get { return _isHoliday; } set { _isHoliday = value; } }}public class CalendarItemCollection : Collection, IStateManager{ private bool _marked; public bool IsTrackingViewState { get { return _marked; } } public void LoadViewState(object state) { if (state != null) { Pair p = (Pair)state; Pair p1 = (Pair)p.First; Pair p2 = (Pair)p.Second; Clear(); string[] rgDescription = (string[])p2.Second; bool[] rgIsHoliday = (bool[])p2.First; DateTime[] rgEndDate = (DateTime[])p1.Second; DateTime[] rgStartDate = (DateTime[])p1.First; for (int i = 0; i < rgStartDate.Length; i++) { CalendarItem item = new CalendarItem(); item.Description = rgDescription[i]; item.IsHoliday = rgIsHoliday[i]; item.EndDate = rgEndDate[i]; item.StartDate = rgStartDate[i]; Add(item); } } } public object SaveViewState() { int numOfItems = Count; DateTime[] rgStartDate = new DateTime[numOfItems]; DateTime[] rgEndDate = new DateTime[numOfItems]; bool[] rgIsHoliday = new bool[numOfItems]; string[] rgDescription = new string[numOfItems]; for (int i = 0; i < numOfItems; i++) { rgStartDate[i] = this[i].StartDate; rgEndDate[i] = this[i].EndDate; rgIsHoliday[i] = this[i].IsHoliday; rgDescription[i] = this[i].Description; } return new Pair(new Pair(rgStartDate, rgEndDate), new Pair(rgIsHoliday, rgDescription)); } public void TrackViewState() { _marked = true; }}
End Listing Two
Begin Listing Three A Web user control for a multi-month calendar view<%@ Control CodeFile="MultiCalendar.ascx.cs" Inherits ="MultiCalendarControl" %><%@ Register TagPrefix="x" Namespace="Samples.More" %>
End Listing Three
About the Author
You May Also Like