Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WinForms

A New Concept in Date Range Picking: the DateRangeComboBox

3.11/5 (10 votes)
2 Nov 2008CPOL6 min read 38.7K   235  
It could take 11 clicks or 20 keystrokes to choose two dates representing last year. Why not choose Last Year in a ComboBox?

The Problem

One of the most common requirements as you construct reporting components is to specify a start and end date, so you can view data for a specific period. This usually requires four controls: two labels and two date-time pickers. From the user's point of view, it can require quite a number of actions, selecting one or two of the pickers, setting days, months, and years. In truth, a number of the date-time pickers available are poorly designed, so the process of choosing a date can be arduous and non-intuitive if a user-friendly picker isn't provided.

A similar challenge arises with UI development for data editing. If you want to filter or search for data based on some kind of date criteria, you may need to provide something similar.

The Solution in a Nutshell

As our company was developing the Habanero framework, we added a nifty new control called a DateRangeComboBox. It provides a set of common date ranges for the user, and then calculates the correct dates to suit the chosen range. The control has been designed on the premise that users often filter data with common date spans in mind.

For instance, a user thinks: "What were the sales like last year?" Before, he would select both date pickers and choose Jan 1, 2007 and Dec 31, 2007. That would add up to something like 11 clicks, perhaps less if you type in the dates directly, which would be about 20 keystrokes and three clicks. With the DateRangeComboBox, the user selects the drop-down, chooses "Previous Year", and clicks "Run Report". Three clicks.

The DateRangeComboBox comes equipped with a number of standard date ranges, and these can be swapped in and out of the available list of options. Developers can also add their own personalised options.

sample-daterangecombobox.jpg

In its Context

This control was released as part of Habanero, a free open source Enterprise Application Framework for .NET. Habanero streamlines the development pipeline by using object relational mapping to provide a domain object layer, and then serves these objects to a runtime-generated presentation layer. With the release of version 2, Habanero expanded its presentation layer to support generation for multiple environments from the same application. A CodeProject article describes how applications are developed with Habanero for multiple release environments, and can be accessed here.

The significance of this context is that the logic has been extracted out of the control so that you can adapt the given code to any control that needs it. The code provided includes support for the WinForms ComboBox and the Visual WebGui ComboBox.

Sample Usage

Let's look at some simple usage. The following code instantiates the control and indicates how you might process its output (note that the start date is inclusive and the end date exclusive):

C#
DateRangeComboBoxWin comboBox = new DateRangeComboBoxWin();
comboBox.SelectionChangeCommitted += delegate
    {
        DateTime inclusiveStartDate = comboBox.StartDate;
        DateTime exclusiveEndDate = comboBox.EndDate;
        //do something with the dates - create report, filter grid
    };

The default list of date ranges is suited for a system that doesn't consider time. You can easily specify a custom list and pass this list through to the constructor:

C#
List<DateRangeOptions> options = new List<DateRangeOptions>();
options.Add(DateRangeOptions.Today);
options.Add(DateRangeOptions.Current3Years);
DateRangeComboBoxWin comboBox = new DateRangeComboBoxWin(options);

You can add your own options just by accessing the Items property and adding text strings to the ComboBox:

C#
DateRangeComboBoxWin comboBox = new DateRangeComboBoxWin();
string myOption = "Last 30,000 Years";
comboBox.Items.Add(myOption);
comboBox.SelectionChangeCommitted += delegate
{
    DateTime start;
    DateTime end;
    if (comboBox.Text == myOption)
    {
        start = DateTime.Now.AddYears(-30000);
        end = DateTime.Now;
    }
    else
    {
        start = comboBox.StartDate;
        end = comboBox.EndDate;
    }
    //do something with the dates
};

Additional Features

The DateRangeComboBox has been designed to accommodate several customisations. For instance, a factory may run in shifts from 6am to 6am, implying that "Yesterday" runs in that period, rather than midnight to midnight. This can be accommodated by setting the MidnightOffset to a positive TimeSpan of 6 hours. Similar properties are included to recognise a different first day of the week, first day of the month, or first month of the year.

The control can be constructed to ignore time by setting IgnoreTime to false, which will round all DateTime values back to the most recent midnight. An option has also been included to work against a fixed DateTime as the "Now" value. While this was necessary for testing, it might have use in an application as well - simply set UseFixedNowDate to true, and set your chosen DateTime value to FixedNowDate, which will replace any call to DateTime.Now.

A View of the Code

The code included with this article is specifically designed for the Habanero framework. We'll have a look at some of the code first, and then we'll review the different ways to use or adapt the code for your project. There is a fair amount of code included, so we'll keep it brief for now. The source includes full commenting, with all methods, properties, and enumerations.

Firstly, the IDateRangeComboBox provides the basic command set. In addition to the features discussed above, it provides utilities to amend the list of options or to change the text of the options as they appear by default, which is obviously useful if you are programming for a non-English audience.

C#
public interface IDateRangeComboBox : IComboBox
{
    List<DateRangeOptions> OptionsToDisplay { get; set; }
    bool IgnoreTime { get; set; }
    TimeSpan MidnightOffset { get; set; }
    int WeekStartOffset { get; set; }
    int MonthStartOffset { get; set; }
    int YearStartOffset { get; set; }
    bool UseFixedNowDate { get; set; }
    DateTime FixedNowDate { get; set; }
    void UseAllDateRangeOptions();
    void SetTopComboBoxItem(string displayString);
    string GetDateRangeString(DateRangeOptions option);
    void SetDateRangeString(DateRangeOptions option, string newDisplayString);
    void RemoveDateOption(DateRangeOptions option);
    void AddDateOption(DateRangeOptions option);
    DateTime StartDate { get; }
    DateTime EndDate { get; }
}

A standard list of date ranges is provided in the DateRangeOptions enumeration, all with associated logic:

C#
public enum DateRangeOptions
{
    ThisHour,
    PreviousHour,
    Current60Minutes,
    Today,
    Yesterday,
    Current24Hours,
    ThisWeek,
    PreviousWeek,
    Previous7Days,
    ThisMonth,
    PreviousMonth,
    Previous30Days,
    Previous31Days,
    ThisYear,
    PreviousYear,
    Previous365Days,
    Current2Years,
    Current3Years,
    Current5Years,
    Previous2Years,
    Previous3Years,
    Previous5Years
}

The StartDate and EndDate properties call CalculateDates to calculate the dates that correspond to the selected date range. Here is a code snippet to calculate some date ranges:

C#
switch (option)
{
    case DateRangeOptions.ThisHour:
    {
        _startDate = HourStart(Now);
        _endDate = Now;
        break;
    }
    case DateRangeOptions.PreviousHour:
    {
        _startDate = HourStart(Now).AddHours(-1);
        _endDate = HourStart(Now);
        break;
    }
    case DateRangeOptions.PreviousMonth:
    {
        _startDate = MonthStart(Now).AddMonths(-1);
        _endDate = MonthStart(Now);
        break;
    }
    case DateRangeOptions.Previous5Years:
    {
        _startDate = YearStart(Now).AddYears(-5);
        _endDate = YearStart(Now);
        break;
    }
    default:
    {
        _startDate = DateTime.MinValue;
        _endDate = DateTime.MaxValue;
        break;
    }
}

The code calls on a Now property which provides either DateTime.Now or a fixed now value as provided by the developer. The Hour/Day/Month/YearStart methods calculate the start of a period based on the given DateTime and on any offsets provided by the developer. Here is an example:

C#
private DateTime YearStart(DateTime date)
{
    DateTime first = new DateTime(date.Year, 1, 1);
    first = first.AddMonths(YearStartOffset).
        AddDays(MonthStartOffset).Add(MidnightOffset);
    if (first > date)
    {
        first = first.AddYears(-1);
    }
    if (MidnightOffset < new TimeSpan(0, 0, 0, 0, 0))
    {
        DateTime closer = new DateTime(date.Year + 1, 1, 1);
        closer = closer.AddMonths(YearStartOffset).
             AddDays(MonthStartOffset).Add(MidnightOffset);
        if (closer < date) return closer;
    }
    return first;
}

Adapting the Code for your Project

Given that the DateRangeComboBox is a Habanero-specific control, there are three ways to use it in your project.

Firstly, the easiest way is to reference the four Habanero DLLs included in this download (.Base, .Util, .UI.Base, .UI.Win). At this point, you can simply use the sample usage code listed earlier.

Secondly, if your control is not System.Windows.Forms.ComboBox, then you can create two intermediate classes. The first would be something like ComboBoxMine, which inherits from your control and from Habanero's IComboBox. The second would be DateRangeComboBoxMine, which closely mimics DateRangeComboBoxWin with calls to the manager, but inherits from ComboBoxMine instead.

Lastly, you could adapt the given source code, editing the DateRangeComboBoxManager by replacing the IComboBox types with your own.

Further Possible Usages

Admittedly, a user might want to work with very specific dates for a range that is not provided. Here, the developer can provide both the DateRangeComboBox and the start and end date pickers. If a date range is chosen, the values in the pickers can be automatically adjusted. If this approach causes clutter in the interface, an extra option like "Custom date range..." could be added to the bottom of the drop-down, causing the hidden pickers to become visible. This provides simplicity in the base case, but still allows full control for those needing it.

Further Improvements

If you're discerning, you'll pick up a potential flaw in the current code where the start and end dates recalculate every time their properties are called. The implication is that there might be a small time gap between the calls to get the start and end date, which could have implications for time-sensitive data. One solution is to change the code so that the developer first calls a method like CalculateDates which creates fixed values to be read by StartDate and EndDate. Another approach could use a time-gap check to see if a refresh of the values is required. A third approach would require the developer to call CalculateDates directly, passing start and end DateTime arguments by reference and having them modified in the code.

If you come up with further innovations on this control, we would love to include them in future releases of Habanero. The official Habanero website provides forums and support features to communicate your changes with the development team.

License

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