Click here to Skip to main content
15,890,690 members
Articles / Web Development / ASP.NET

DateRangePicker (select date ranges with a single extended Calendar)

Rate me:
Please Sign up or sign in to vote.
4.57/5 (18 votes)
14 Apr 2006CPOL3 min read 109.4K   378   51   26
Extend a Calendar to let users pick a date range - with less than three pages of code.

Sample Image - DateRangePicker.gif

Introduction

My custom control, DateRangePicker, provides the ability to select not just single dates but arbitrary date ranges. It is a trivially extended Calendar control.

Background

I needed to provide users with the ability to select date ranges. Two calendars side by side take up a lot of space, so I had a look for custom controls. I noticed that Windows Forms let you select date ranges, but web form calendars can select days, weeks, or months. There were some commercial products on the internet, but once I saw what methods could be overridden on Calendar, I knew it would be easy enough to implement my own.

User Interface

The first time a user selects a date, that date is recorded as one end of the date range. Then, the second click on the same calendar will set the other end of the date range. A third click will clear the old date range and record a new date as the starting point of a date range, and so on.

Using the code

Once you have the code in a custom web control library project, all you have to do is drag the control from your toolbox onto the designer, just like any other custom control.

You should see something like the following code added to your page:

HTML
<%@ Register Assembly="CustomWebControls" 
      Namespace="CustomWebControls" TagPrefix="cc1" %>

...

<cc1:DateRangePicker ID="DateRangePicker1" 
     runat="server"></cc1:DateRangePicker>

How it works

There are two classes that make up the DateRangePicker control - DateRange, and DateRangePicker. DateRange is a struct to represent a range of dates, and DateRangePicker is the custom web control that extends System.Web.UI.Calendar.

DateRange

A DateRange is essentially a from date and a to date. It would be possible to implement a DateRangePicker without this, but it helps us keep logic out of the UI code. It's marked as Serializable so that the WebControl can keep it in the viewstate. The most important logic method is Include, which adds a DateTime or another DateRange to the instance. We use this when a second DateTime is clicked, and it means the user doesn't have to click the earliest date first. Equals, HashCode, and the operators == and != are all overridden for efficiency.

C#
using System;

namespace CustomWebControls
{
    [Serializable]
    public struct DateRange
    {
        public static readonly DateRange EMPTY = new DateRange();
        
        readonly DateTime from;
        readonly DateTime to;


        public DateRange(DateTime from, DateTime to)
        {
            this.from = from;
            this.to = to;
        }


        public DateTime From
        {
            get { return from; }
        }

        public DateTime To
        {
            get { return to; }
        }

        public TimeSpan TimeSpan
        {
            get
            {
                return to - from;
            }
        }
        
        public bool Contains(DateTime time)
        {
            return from <= time && time < to;
        }

        public DateRange Include(DateRange otherRange)
        {
            return Include(otherRange.From).Include(otherRange.To);
        }

        public DateRange Include(DateTime date)
        {
            if (date < from)
                return new DateRange(date, to);
            else if (date > to)
                return new DateRange(from, date);
            else 
                return this;
        }

        /// <summary>
        /// Creates a one day (24 hr) long DateRange starting at DateTime
        /// </summary>
        public static DateRange CreateDay(DateTime dateTime){
            return new DateRange(dateTime, dateTime.AddDays(1));
        }

        #region operators and overrides
        public override int GetHashCode()
        {
            return from.GetHashCode() + 29*to.GetHashCode();
        }

        public override bool Equals(object obj)
        {
            if (ReferenceEquals(this, obj)) return true;
            if (!(obj is DateRange)) return false;
            DateRange dateRange = (DateRange) obj;
            if (!Equals(from, dateRange.from)) return false;
            if (!Equals(to, dateRange.to)) return false;
            return true;
        }


        public static bool operator == (DateRange d1, DateRange d2)
        {
            return d1.Equals(d2);
        }

        public static bool operator !=(DateRange d1, DateRange d2)
        {
            return !d1.Equals(d2);
        }
        #endregion

    }
}

DateRangePicker

This is the class that overrides Calendar. It has two properties, one for styling the selected DateRange and another to store the selected DateRange.

By default, the style has a BackColor of LightSteelBlue. This is initialised in the static constructor. The style is stored in a private variable, because whenever a Page is initialised, the new style will be set by the designer code. SelectedDateRange, however, is stored in the ViewState because it needs to persist across multiple page requests. If you don't use ViewState, you will need to find another way to persist this variable.

OnSelectionChanged is where we change the DateRange. OnDayRender is where we apply the styling to a DateRange. Note the special handling of whole days in OnSelectionChanged.

C#
using System.ComponentModel;
using System.Drawing;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace CustomWebControls
{
    /// <summary>
    /// An extended Calendar that can select DateRanges as well as Dates
    /// </summary>
    [DefaultProperty("Text")]
    [ToolboxData("<{0}:DateRangePicker runat="server"></{0}:DateRangePicker>")]
    public class DateRangePicker : Calendar
    {
        static readonly TableItemStyle defaultSelectedDateRangeStyle = new TableItemStyle();


        static DateRangePicker()
        {
            //initialise a default colour for defaultSelectedDateRangeStyle
            defaultSelectedDateRangeStyle.BackColor = Color.LightSteelBlue;
        }

        TableItemStyle selectedDateRangeStyle = defaultSelectedDateRangeStyle;

        protected override void OnDayRender(TableCell cell, CalendarDay day)
        {
            if (SelectedDateRange.Contains(day.Date))
            {
                cell.ApplyStyle(selectedDateRangeStyle);
            }
        }

        protected override void OnSelectionChanged()
        {
            base.OnSelectionChanged();

            bool emptyDateRange = SelectedDateRange == DateRange.EMPTY;
            bool dateRangeAlreadyPicked = SelectedDateRange.TimeSpan.TotalDays > 1;

            if (emptyDateRange || dateRangeAlreadyPicked)
            {
                SelectedDateRange = DateRange.CreateDay(SelectedDate);
                //save this date as the first date in our date range
            }
            else
            {
                SelectedDateRange = 
                  SelectedDateRange.Include(DateRange.CreateDay(SelectedDate));
                //set the end date in our date range
            }
        }

        //DateRange gets stored in the viewstate since
        //it's a property that needs to persist across page requests.
        <Browsable(false)>
        public DateRange SelectedDateRange
        {
            get { return (DateRange) (ViewState["SelectedDateRange"]??DateRange.EMPTY); }
            set { ViewState["SelectedDateRange"] = value; }
        }

        //SelectedDateRangeStyle goes into a private
        //variable since this property is designed.
        [Category("Styles")]
        [Description("The Style that is aplied to cells within the selected Date Range")]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
        [NotifyParentProperty(true)]
        [PersistenceMode(PersistenceMode.InnerProperty)]
        public TableItemStyle SelectedDateRangeStyle
        {
            get { return selectedDateRangeStyle; }
            set { selectedDateRangeStyle = value; }
        }
    }
}

Future Work

  • Better designer support.
  • Further extensions to OnDayRender to give the user more tooltips regarding what to do.
  • Your suggestions! I value all feedback. I'd love to hear if this works well for you, or if you have any improvements you'd like to offer.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Web Developer
New Zealand New Zealand
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralRe: Click and Drag.. Pin
Rob Elsner6-Jul-07 10:18
Rob Elsner6-Jul-07 10:18 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.