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.
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):
DateRangeComboBoxWin comboBox = new DateRangeComboBoxWin();
comboBox.SelectionChangeCommitted += delegate
{
DateTime inclusiveStartDate = comboBox.StartDate;
DateTime exclusiveEndDate = comboBox.EndDate;
};
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:
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
:
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;
}
};
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.
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:
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:
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:
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.