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

DataCalendar

0.00/5 (No votes)
19 Feb 2004 60  
A data-driven, templated sub-class of the ASP.NET Calendar control

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
    {
        .
        .
        .
        // Constructor   

        public DataCalendar() : base()
        {
            // since this control will be used for displaying

            // events, set these properties as a default

            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;
        
        // Support either a DataSet or DataTable object

        // for the DataSource property

        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");
            }
        }
        
        // If a DataSet is supplied for DataSource,

        // use this property to determine which

        // DataTable within the DataSet should

        // be used; if DataMember is not supplied,

        // the first table in the DataSet will

        // be used.

        public string DataMember {
            get {return _dataMember;}
            set {_dataMember = value;}
        }
        
        
        // Specify the name of the field within

        // the source DataTable that contains

        // a DateTime value for displaying in the

        // calendar.

        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) 
            {
                // determine if the datasource is a DataSet or DataTable

                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 == "")
                        // if data member isn't supplied, 

                        // default to the first table

                        _dtSource = ds.Tables[0];
                    else
                        // if data member is supplied, use it

                        _dtSource = ds.Tables[this.DataMember];            
                }
                // throw an exception if there is a problem 

                // with the data source

                if (_dtSource == null)
                    throw new Exception(
                        "Error finding the DataSource.  Please check " +
                        " the DataSource and DataMember properties.");
            }                    
            // call the base Calendar's Render method, allowing

            // OnDayRender to be executed.

            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)
        {
            // _dtSource was already set by the Render method            

            if (_dtSource != null) 
            {
                // We have the data source as a DataTable now;                

                // filter the records in the DataTable for the given day;

                // force the date format to be MM/dd/yyyy

                // to ensure compatibility with RowFilter

                // date expression syntax (#date#).

                // Also, take the possibility of time

                // values into account by specifying

                // a date range, to include the full day

                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")
                );
               
                // are there events on this day?

                if (dv.Count > 0) {
                    // there are events on this day;

                    .
                    .
                    .
                }
                else
                {
                    // no events this day;

                    .
                    .
                    .                       
                }
               
                 
            }           
           
            // call the base render method too

            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:

  1. Create a container DataCalendarItem object, constructed with the item's DataRow
  2. Instantiate the ItemTemplate in the container (using the ITemplate.InstantiateIn() method)
  3. Execute the DataBind method of the DataCalendarItem object to resolve data binding expressions (this method is inherited from Control)
  4. 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)
    {
        // given a calendar cell and a datarow, set up the

        // templated item and resolve data binding syntax

        // in the template

        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)
        {
            .
            .
            .
            // are there events on this day?

            if (dv.Count > 0) {
                // for each event on this day apply the

                // ItemTemplate, with data bound to the item's row

                // from the data source

                if (this.ItemTemplate != null)
                    for (int i=0; i<dv.Count; i++) {
                        SetupCalendarItem(cell, dv[i].Row,
                                          this.ItemTemplate);
                    }
            }
            else
            {
                // no events this day;

            }
        }           
        .
        .
        .
    }             

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
            {
                // no events this day;

                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)
            {
                .
                .
                .
                // are there events on this day?

                if (dv.Count > 0) {
                    // there are events on this day; if indicated,

                    // apply the DayWithEventsStyle to the table cell

                    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

  • 6 Feb 2004
- 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.
  • 3 Nov 2003
- Original posting

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