Click here to Skip to main content
15,881,600 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.1K   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

 
BugError in page Pin
Vandana8718-Jul-17 20:59
Vandana8718-Jul-17 20:59 
GeneralThanks! Exactly what I was looking for. Pin
danishdudeous20-Jul-09 10:07
danishdudeous20-Jul-09 10:07 
Generalinsert DateRangePicker into flash cs4 Pin
Leonado Zuzzi24-Jun-09 10:57
Leonado Zuzzi24-Jun-09 10:57 
Questionhow to apply styles. Pin
medasatheesh1-Apr-09 10:56
medasatheesh1-Apr-09 10:56 
GeneralGreat Control! Pin
planetregin21-Oct-08 10:15
planetregin21-Oct-08 10:15 
QuestionHow to display selcted date range? Pin
Member 16601859-Mar-08 22:34
Member 16601859-Mar-08 22:34 
GeneralThe To Date is 1 day to much Pin
Marckus_E5-Sep-07 1:26
Marckus_E5-Sep-07 1:26 
GeneralOnSelectionChanged implementation Pin
xoph13-Mar-07 12:41
xoph13-Mar-07 12:41 
I noticed OnSelectionChanged fires a call to its base class first (base.OnSelectionChanged();) and then does what it needs to do. This makes this event out of sync: if you call SelectedDateRange from a OnSelectionChanged event handler, its viewstate has not been updated yet and you will not get the latest date range selected.
I suggest to simply move the call to base.OnSelectionChanged() to the end of the method.

xoph

Better active today than radioactive tomorrow.

QuestionVisual Basic Pin
si_owen3-Nov-06 5:23
si_owen3-Nov-06 5:23 
AnswerRe: Visual Basic Pin
Robert Ensor6-Nov-06 10:10
Robert Ensor6-Nov-06 10:10 
QuestionRe: Visual Basic Pin
si_owen7-Nov-06 4:20
si_owen7-Nov-06 4:20 
AnswerRe: Visual Basic Pin
Robert Ensor7-Nov-06 10:00
Robert Ensor7-Nov-06 10:00 
QuestionRe: Visual Basic Pin
si_owen7-Nov-06 21:31
si_owen7-Nov-06 21:31 
AnswerRe: Visual Basic Pin
Robert Ensor8-Nov-06 11:04
Robert Ensor8-Nov-06 11:04 
GeneralRe: Visual Basic Pin
si_owen8-Nov-06 23:24
si_owen8-Nov-06 23:24 
Generaltest for single date and clear all dates Pin
pimming22-May-06 10:12
pimming22-May-06 10:12 
GeneralRe: test for single date and clear all dates Pin
Robert Ensor22-May-06 12:10
Robert Ensor22-May-06 12:10 
GeneralRe: test for single date and clear all dates Pin
pimming22-May-06 17:50
pimming22-May-06 17:50 
GeneralNice Pin
Dan Letecky14-May-06 12:04
Dan Letecky14-May-06 12:04 
QuestionHow do I create this control in VS 2005 Pin
gkadery4-May-06 5:56
gkadery4-May-06 5:56 
AnswerRe: How do I create this control in VS 2005 Pin
Robert Ensor4-May-06 12:02
Robert Ensor4-May-06 12:02 
GeneralClick and Drag.. Pin
MikeEast18-Apr-06 12:58
MikeEast18-Apr-06 12:58 
GeneralRe: Click and Drag.. Pin
Robert Ensor18-Apr-06 13:05
Robert Ensor18-Apr-06 13:05 
GeneralRe: Click and Drag.. Pin
MikeEast18-Apr-06 13:11
MikeEast18-Apr-06 13:11 
GeneralRe: Click and Drag.. Pin
Robert Ensor18-Apr-06 13:14
Robert Ensor18-Apr-06 13:14 

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.