Introduction
This article presents a sub-class of the ASP.NET Calendar
control, to function as a data-driven display calendar with template support for item layout. Though there are limitations to the approach of sub-classing Calendar
, it is a useful alternative to creating such a control from scratch, as significant functionality can be inherited. The DataCalendar
class is described with specific attention to data binding, templates, and styles.
Background
The ASP.NET Calendar
control offers the ability to navigate a monthly calendar and select dates. But the standard Calendar
control lacks support for data binding, so extra steps are required to display items from a database. The standard control does support a DayRender
event, which provides a means to customize the display of individual days. I was interested in a control that functions more closely like a Repeater
or DataList
, with the ability to bind to a data source and use templates to control item display. Sub-classing the Calendar
control seemed a good place to start.
To support data binding, a custom control will typically expose a DataSource
property, override the Control.DataBind
method, and construct a control hierarchy within CreateChildControls
. This also usually involves maintaining ViewState
for each of the child controls so explicit binding does not have to occur with each page postback. The Microsoft .NET SDK documentation offers source examples describing how to develop a data-bound control. The book Developing Microsoft ASP.NET Server Controls and Components by Nikhil Kothari and Vandana Datye (Microsoft Press, 2003) is also recommended for a full description of developing data-bound controls.
There are problems using the existing Calendar
control this way. When a Repeater
is bound to a data source, all items of that source are enumerated to generate child controls. With a Calendar
, the display layout is dependent on a fixed set of days. Creating child controls for each item with ViewState
maintained, particularly for those items that don't fall within the displayed month, doesn't seem practical. As it is, the Calendar
control already creates TableRows
and TableCells
for child controls; if that were to be overridden, the month-based layout would be lost. If we can't take advantage of the layout functionality provided by the Calendar
control, there isn't much point to inheriting from it.
Seeing these kinds of issues makes clear why the DayRender
event is offered in the first place. DayRender
is fired once for each day displayed during the Render
event of the Calendar
control. It becomes possible to simulate a data-bound control by exposing a DataSource
property, then enumerating that data listing in an overridden version of the Calendar.OnDayRender
method.
This approach has some important ramifications. The Render
event comes relatively late in the control's lifecycle, and isn't usually where child controls are created. Child controls added at this stage will not fire events. We can't, for example, add a Button
control in each day cell and code for its Click
event - the Click
event will not fire. On the other hand, content for display such as literal HTML or Label
controls may be added without penalty. If we can tolerate a lack of event-firing controls rendered in day cells, the approach of sub-classing the Calendar
control offers some terrific benefits: built-in styling capabilities, month-to-month navigation, day/week/month selection, a useful layout - all in all, there are enough benefits to this approach to make it worthwhile.
The DataCalendar
Class
The DataCalendar
class inherits from Calendar
and implements INamingContainer
. The constructor sets some defaults that make sense for a calendar whose primary purpose is to display entries as static content.
public class DataCalendar : Calendar, INamingContainer
{
.
.
.
public DataCalendar() : base()
{
this.SelectionMode = CalendarSelectionMode.None;
this.ShowGridLines = true;
}
.
.
.
}
Simulating Data Binding
The essential properties for making this a data-driven control are defined as DataSource
, DataMember
, and DayField
. DataSource
is implemented here as either a DataSet
or DataTable
object, representing the listing of calendar items. If a DataSet
object is supplied, then the DataMember
property is implemented to allow the user to specify which table in the set to use. DayField
is the name of the column in DataSource
that represents the event date.
private object _dataSource;
private string _dataMember;
private string _dayField;
public object DataSource {
get {return _dataSource;}
set
{
if (value is DataTable || value is DataSet)
_dataSource = value;
else
throw new Exception("The DataSource property " +
"of the DataCalendar control must be a " +
"DataTable or DataSet object");
}
}
public string DataMember {
get {return _dataMember;}
set {_dataMember = value;}
}
public string DayField {
get {return _dayField;}
set {_dayField = value;}
}
Often a data-bound control will support several types of objects for its DataSource
property, such as those objects that implement the IEnumerable
interface. For simplicity I chose to stay with a DataTable
(or a table within a DataSet
). The need for a DayField
property to specify a date element within the data source also lends well to using a DataTable
as opposed to other types of IEnumerable
lists.
We'll use the Render
method to inspect the DataSource
property at run-time, determining if a DataSet
or DataTable
has been specified. We can then set up the private variable _dtSource
to point to the appropriate DataTable
object, and allow the base class Render
method to execute.
private DataTable _dtSource;
protected override void Render(HtmlTextWriter html)
{
_dtSource = null;
if (this.DataSource != null && this.DayField != null)
{
if (this.DataSource is DataTable)
_dtSource = (DataTable) this.DataSource;
if (this.DataSource is DataSet)
{
DataSet ds = (DataSet) this.DataSource;
if (this.DataMember == null || this.DataMember == "")
_dtSource = ds.Tables[0];
else
_dtSource = ds.Tables[this.DataMember];
}
if (_dtSource == null)
throw new Exception(
"Error finding the DataSource. Please check " +
" the DataSource and DataMember properties.");
}
base.Render(html);
}
As DataCalendar
inherits from Calendar
, we override the OnDayRender
method to customize the display of individual days. The argument cell represents the TableCell
being rendered and acts as a placeholder for additional content. The specific date in question is derived from the CalendarDay
argument day. We'll use the _dtSource
private variable previously set by Render
. With this we'll create a DataView
object to filter the DataTable
, extracting only those items that match the day argument based on the value of the DayField
column. The code forces a "MM/dd/yyyy" date format when constructing the RowFilter
as required for date comparisons in such expressions. The filter is also constructed to take into account the possibility of time values within the DayField
column.
protected override void OnDayRender(TableCell cell, CalendarDay day)
{
if (_dtSource != null)
{
DataView dv = new DataView(dtSource);
dv.RowFilter = string.Format(
"{0} >= #{1}# and {0} < #{2}#",
this.DayField,
day.Date.ToString("MM/dd/yyyy"),
day.Date.AddDays(1).ToString("MM/dd/yyyy")
);
if (dv.Count > 0) {
.
.
.
}
else
{
.
.
.
}
}
base.OnDayRender(cell, day);
}
Supporting Templates
Another goal of the
DataCalendar
is to make use of
templates. Templates allow for content layout to be defined within the HTML portion of an .aspx page by a page designer, rather than hard-coded into the class by a developer. A template contains HTML elements and ASP.NET controls, within which data binding expressions may be applied and resolved. The
Repeater
control for example supports, among others, a
HeaderTemplate
,
ItemTemplate
, and
FooterTemplate
. The
DataCalendar
control will support an
ItemTemplate
that works in the .aspx page like this:
<dc:DataCalendar id="cal1" runat="server" width="100%"
DayField="EventDate" >
<ItemTemplate>
<b><%# Container.DataItem["EventTime"] %></b>
<%# Container.DataItem["EventTitle"] %>
</ItemTemplate>
</dc:DataCalendar>
Data binding expressions <%# . . . %>
should be resolved as the template is applied. To support this feature we need to define a container object for the calendar item. The syntax Container.DataItem["..."]
refers to a DataItem
property of this container object. Given that the source for the DataCalendar
is a DataTable
object, the DataItem
property (a single item within the source) will logically be of type DataRow
. The following code shows the class DataCalendarItem
defined to serve as this container object:
public class DataCalendarItem : Control, INamingContainer
{
private DataRow _dataItem;
public DataCalendarItem(DataRow dr) {
_dataItem = dr;
}
public DataRow DataItem {
get {return _dataItem;}
set {_dataItem = value;}
}
}
DataCalendarItem
implements the INamingContainer
interface, a requirement for template containers. This ensures that control names remain unique when templates are applied through multiple iterations of data items.
The next step is to define the ItemTemplate
property in the DataCalendar
class. This property is of type ITemplate
, and is marked with the attribute TemplateContainer
. The TemplateContainer
attribute identifies which class will function as the container for an instance of the template. In our case, this will be the DataCalendarItem
we just defined.
public class DataCalendar : Calendar, INamingContainer
{
.
.
.
private ITemplate _itemTemplate;
[TemplateContainer(typeof(DataCalendarItem))]
public ITemplate ItemTemplate
{
get {return _itemTemplate; }
set {_itemTemplate = value;}
}
.
.
.
}
With these template definitions, we can now return to the OnDayRender
method of the DataCalendar
class. Each iteration through the data source gives us a single calendar item in the form of a DataRow
. With that item we need to perform the following tasks:
- Create a container
DataCalendarItem
object, constructed with the item's DataRow
- Instantiate the
ItemTemplate
in the container (using the ITemplate.InstantiateIn()
method)
- Execute the
DataBind
method of the DataCalendarItem
object to resolve data binding expressions (this method is inherited from Control
)
- Add the
DataCalendarItem
control to the TableCell
that represents the given day
These tasks are handled through the private helper function SetupCalendarItem
, which is executed from OnDayRender
once for each calendar item:
private void SetupCalendarItem(TableCell cell, DataRow r, ITemplate t)
{
DataCalendarItem dti = new DataCalendarItem(r);
t.InstantiateIn(dti);
dti.DataBind();
cell.Controls.Add(dti);
}
protected override void OnDayRender(TableCell cell, CalendarDay day)
{
if (_dtSource != null)
{
.
.
.
if (dv.Count > 0) {
if (this.ItemTemplate != null)
for (int i=0; i<dv.Count; i++) {
SetupCalendarItem(cell, dv[i].Row,
this.ItemTemplate);
}
}
else
{
}
}
.
.
.
}
With support for an ItemTemplate
in place, it is a simple matter to implement a NoEventsTemplate
as well, also instantiated from the OnDayRender
method. If a day in the data source has no calendar items, the NoEventsTemplate
is applied.
.
.
.
else
{
if (this.NoEventsTemplate != null)
SetupCalendarItem(cell, null,
this.NoEventsTemplate);
}
.
.
.
Styles
As described before, a downside of the approach of sub-classing the Calendar
control is that events from controls in our templates will not fire. A great upside to this approach however is the strong support the Calendar
control offers for styles. Without any more code, we can make use of the properties DayStyle
, TodayDayStyle
, WeekdayDayStyle
, OtherMonthDayStyle
, and several others to customize the calendar's appearance.
In the DataCalendar
class we will define one more data-relevant property of type TableCellStyle
: DayWithEventsStyle
. This property is applied in the OnDayRender
event for each day that has data items. This allows the page designer the ability, for example, to set a different background color for days with events. The following shows the relevant code from DataCalendar
to support DayWithEventsStyle
:
public class DataCalendar : Calendar, INamingContainer
{
.
.
.
private TableItemStyle _dayWithEventsStyle;
public TableItemStyle DayWithEventsStyle {
get {return _dayWithEventsStyle;}
set {_dayWithEventsStyle = value;}
}
.
.
.
protected override void OnDayRender(TableCell cell, CalendarDay day)
{
if (_dtSource != null)
{
.
.
.
if (dv.Count > 0) {
if (this.DayWithEventsStyle != null)
cell.ApplyStyle(this.DayWithEventsStyle);
.
.
.
}
else
{
.
.
.
}
}
.
.
.
}
}
About the Examples
The examples in the sample project demonstrate different applications of the DataCalendar
control. Since the displayed entries are not maintained through ViewState
(as would be typical of a true data-bound control) other means of caching the calendar data are offered, including Session
variables and the application Cache
.
DataCalendar1.aspx
This example uses a DataTable
object constructed in code, and shows a simple DataCalendar
without much formatting.
DataCalendar2.aspx
This example demonstrates using data from an OLEDB data source, in this case an Access table. The <ItemTemplate>
of this DataCalendar
displays a hyperlink for each event, with a small image identifying the event category. The data source is cached in a Session
variable.
DataCalendar3.aspx
The data source for this example is an XML document, using the XmlDataDocument
class to get to the DataTable
. This DataCalendar
uses a little more formatting than the previous two. After loading, the data source is stored in the application Cache
.
DataCalendar4.aspx
This example uses an OLEDB data source, without caching the results. Instead, the data source is queried with each postback, but a SQL Where clause limits the results to events for the displayed month.
DataCalendar5.aspx
This example shows the DataCalendar
functioning more like a regular Calendar
for selecting a date. No <ItemTemplate>
is used, but the DayWithEventsStyle
attribute is applied to highlight those days with events. A Repeater
control is used to display events for the selected date.
Summary
The existing ASP.NET Calendar
control is great for selecting dates but lacks data binding support. Sub-classing the Calendar
control and overriding the OnDayRender
method can simulate such support. Though controls that fire events can't be included in day cells using this approach, static content may be used as can the full contingent of Style properties inherited from the Calendar
control. The DataCalendar
class presented here may be used for displaying items from a DataTable
, with layout customized by page designers through the use of its ItemTemplate
and NoEventsTemplate
properties. The DataCalendar
class may itself serve as a base class for additional database-specific implementations.
History
|
- |
Updated to include support for DataSet objects and added the DataMember property; also updated OnDayRender() to better handle internationally formatted dates and dates with time values. |
|
- |
Original posting |