Click here to Skip to main content
Click here to Skip to main content

DateRangePicker (select date ranges with a single extended Calendar)

, 14 Apr 2006
Rate this:
Please Sign up or sign in to vote.
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:

<%@ 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.

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.

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)

About the Author

Robert Ensor
Web Developer
New Zealand New Zealand
No Biography provided

Comments and Discussions

 
AnswerRe: Visual Basic PinmemberRobert Ensor7-Nov-06 10:00 
QuestionRe: Visual Basic Pinmembersi_owen7-Nov-06 21:31 
AnswerRe: Visual Basic PinmemberRobert Ensor8-Nov-06 11:04 
GeneralRe: Visual Basic Pinmembersi_owen8-Nov-06 23:24 
Generaltest for single date and clear all dates Pinmemberpimming22-May-06 10:12 
GeneralRe: test for single date and clear all dates PinmemberRobert Ensor22-May-06 12:10 
GeneralRe: test for single date and clear all dates Pinmemberpimming22-May-06 17:50 
GeneralNice PinmemberDan Letecky14-May-06 12:04 
QuestionHow do I create this control in VS 2005 Pinmembergkadery4-May-06 5:56 
AnswerRe: How do I create this control in VS 2005 PinmemberRobert Ensor4-May-06 12:02 
GeneralClick and Drag.. PinmemberMikeEast18-Apr-06 12:58 
GeneralRe: Click and Drag.. PinmemberRobert Ensor18-Apr-06 13:05 
GeneralRe: Click and Drag.. PinmemberMikeEast18-Apr-06 13:11 
GeneralRe: Click and Drag.. PinmemberRobert Ensor18-Apr-06 13:14 
GeneralRe: Click and Drag.. PinmemberRob Elsner6-Jul-07 10:18 

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

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

| Advertise | Privacy | Mobile
Web02 | 2.8.140709.1 | Last Updated 15 Apr 2006
Article Copyright 2006 by Robert Ensor
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid