Introduction
I have designed and implemented shareware for several years, and for my sins, am a life member of the Association of Shareware Professionals. And for the last half dozen years, I have been working with Microsoft .NET technologies, so it came to pass that I would one day develop a new shareware application using the .NET platform. And so it has been with the recent software products available on my website.
I have to be honest. In knowing .NET really well, I expected that I would find a .NET component in my toolbox complete with all the bells and whistles for managing dates. I was surprised, because there didn't seem to be anything to provide me with the functionality I desired. What I wanted was fairly simple - a .NET toolbox component which could not only allow the user to select a date from a convenient menu/dialog arrangement, but which would also paint the day colours differently for dates on which appointments had already been made. I couldn't believe it, but there was no such component available - unless I paid many hundreds of dollars for a commercial product.
So, I figured I would develop my own component to manage dates the way I wanted. Nothing fancy - the usual matrix of days for a month where users could select the dates they wanted, but with the benefit of the matrix indicating which dates already had allocations against.
TestCal is a .NET user control written in C# version 2.0. It uses the DataGridView
component to do the presentation. You can just drop it into a .NET project and use it as-is, or if you fancy, you could do some fairly trivial updating to provide additional functionality.
Background
Even though Visual Studio provides fairly rich date/time picker controls, they stopped short of providing the functionality that I really needed. That functionality was the ability for a user to see which dates already had commitments made against them. And believe it or not, those default components supplied out of the box by Microsoft don't provide that functionality.
Using the Code
Unpack the zip file, making sure to maintain the folder structure of the .NET project. Then, load the TestCal.sln file from Visual Studio 2005 (or 2008), and go from there.
The project provided includes a simple form from which the calendar user control is instantiated with a button click. There is nothing really difficult about the implementation, anyone who has got past page 2 of the "Become A .NET Developer In 21 Days" should be on the home straight, very quickly.
Dates used by TestCal are defined as simple integers, which have the format 'YyyyMmDd', for example '20090121' would be 21 January 2009. I decided to steer clear of using actual DateTime
objects because if there's one thing which can be very confusing to maintain, it's DateTime
objects. Converting between a DateTime
and integer is simple - functions are provided in the code to do just that.
There is no point listing every piece of source code provided in the project; all files are in the zip file containing the source code. Just unzip that file to a folder on your hard drive, then load the TestCal.sln solution file, and you should have a fully operational test project which you can debug at leisure. However, for completeness, the UsrCalendar
control which uses a DataGridView
component is listed below:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;
namespace TestCal
{
public partial class UsrCalendar : UserControl
{
#region Declarations
private const int DEF_YEARRANGE_LO = 50;
private const int DEF_YEARRANGE_HI = 3;
private Color DEF_BACKCOLOR = Color.LightBlue;
private Color DEF_WEEKEND = Color.LightGreen;
private Color DEF_OTHERMONTH_BACK = Color.LightGray;
private Color DEF_OTHERMONTH_FORE = Color.SteelBlue;
private Color DEF_COLOR1 = Color.Cyan;
private Color DEF_COLOR2 = Color.DarkGreen;
private Color DEF_COLOR3 = Color.DarkKhaki;
private int m_CurrentDate;
private int m_SelectedDate;
private Boolean bNoRedraw = false;
public event EventHandler DateSelected;
ArrayList ReservedDates;
#endregion
#region Properties
internal int SelectedDate
{
get
{
return m_SelectedDate;
}
set
{
m_SelectedDate = value;
m_CurrentDate = m_SelectedDate;
}
}
#endregion
#region Public methods
public void SetControlPosition(Point pt)
{
this.Left = pt.X;
this.Top = pt.Y;
}
public void ReserveDate(int intYyyyMmDd, int intColor)
{
Boolean bExists = false;
for (int RowNo = 0; RowNo < ReservedDates.Count; RowNo++)
{
ClsDataItem oItm = (ClsDataItem)ReservedDates[RowNo];
if (oItm.ID.ToString() == intYyyyMmDd.ToString())
{
bExists = true;
break;
}
}
if (!bExists)
{
ClsDataItem oItm = new ClsDataItem(intYyyyMmDd.ToString(),
intColor.ToString());
ReservedDates.Add(oItm);
}
}
#endregion
#region Private methods
private void ComboSetValue(ComboBox oCmb, string strVal)
{
oCmb.SelectedIndex = -1;
for (int RowNo = 0; RowNo < oCmb.Items.Count; RowNo++)
{
ClsDataItem oItm = (ClsDataItem)oCmb.Items[RowNo];
if (oItm.ID == strVal)
{
oCmb.SelectedIndex = RowNo;
break;
}
}
if ((oCmb.SelectedIndex < 0) && (oCmb.Items.Count > 0))
oCmb.SelectedIndex = 0;
}
private int ComboGetValue(ComboBox oCmb)
{
int intRetVal = 0;
if (oCmb.SelectedIndex >= 0)
{
ClsDataItem oItm = (ClsDataItem)oCmb.Items[oCmb.SelectedIndex];
intRetVal = Str2Int(oItm.ID);
}
return intRetVal;
}
private int YyyyMmDdGetYear(int YyyyMmDd)
{
int intRetVal = 0;
intRetVal = YyyyMmDd / 10000;
return intRetVal;
}
private int YyyyMmDdGetMonth(int YyyyMmDd)
{
int intRetVal = 0;
intRetVal = YyyyMmDd / 100 - (100 * YyyyMmDdGetYear(YyyyMmDd));
return intRetVal;
}
private int YyyyMmDdGetDay(int YyyyMmDd)
{
int intRetVal = 0;
intRetVal = YyyyMmDd - (10000 * YyyyMmDdGetYear(YyyyMmDd)) - (100 *
YyyyMmDdGetMonth(YyyyMmDd));
return intRetVal;
}
private int Str2Int(string strVal)
{
int intRetVal = 0;
if (!int.TryParse(strVal, out intRetVal))
intRetVal = 0;
return intRetVal;
}
private void ShowMonth()
{
int ColNo = 0;
DateTime DateVal;
int YearNo = YyyyMmDdGetYear(m_CurrentDate);
int MonthNo = YyyyMmDdGetMonth(m_CurrentDate);
int DoW;
int RowNo;
gridCalendar.Rows.Clear();
DateVal = new DateTime(YearNo, MonthNo, 1);
gridCalendar.Rows.Add();
DoW = (int)DateVal.DayOfWeek;
ColNo = DoW - 1;
if (ColNo < 0)
ColNo = 6;
for (int DayNo = 1; DayNo <= DateTime.DaysInMonth(YearNo, MonthNo); DayNo++)
{
RowNo = gridCalendar.Rows.Count - 1;
gridCalendar.Rows[RowNo].Cells[ColNo].Value = DateVal.Day.ToString();
gridCalendar.Rows[RowNo].Cells[ColNo].Tag = DateVal.ToString("yyyyMMdd");
ColNo++;
DateVal = DateVal.AddDays(1);
if ((ColNo > 6) && (DayNo < DateTime.DaysInMonth(YearNo, MonthNo)))
{
ColNo = 0;
gridCalendar.Rows.Add();
}
}
ColNo = 0;
while (gridCalendar.Rows[0].Cells[ColNo].Value == null)
ColNo++;
DateVal = new DateTime(YyyyMmDdGetYear(m_CurrentDate),
YyyyMmDdGetMonth(m_CurrentDate), 1);
while (ColNo > 0)
{
DateVal = DateVal.AddDays(-1);
ColNo--;
gridCalendar.Rows[0].Cells[ColNo].Value = DateVal.Day.ToString();
gridCalendar.Rows[0].Cells[ColNo].Style.BackColor = DEF_OTHERMONTH_BACK;
gridCalendar.Rows[0].Cells[ColNo].Style.ForeColor = DEF_OTHERMONTH_FORE;
gridCalendar.Rows[0].Cells[ColNo].Tag = DateVal.ToString("yyyyMMdd");
}
RowNo = gridCalendar.Rows.Count - 1;
ColNo = 0;
while (gridCalendar.Rows[RowNo].Cells[ColNo].Value != null)
{
DateVal = new DateTime(YyyyMmDdGetYear(m_CurrentDate),
YyyyMmDdGetMonth(m_CurrentDate),
Str2Int(gridCalendar.Rows[RowNo].Cells[ColNo].Value.ToString()));
ColNo++;
if (ColNo > 6)
break;
}
while (ColNo < 7)
{
DateVal = DateVal.AddDays(1);
gridCalendar.Rows[RowNo].Cells[ColNo].Value = DateVal.Day.ToString();
gridCalendar.Rows[RowNo].Cells[ColNo].Style.BackColor = DEF_OTHERMONTH_BACK;
gridCalendar.Rows[RowNo].Cells[ColNo].Style.ForeColor = DEF_OTHERMONTH_FORE;
gridCalendar.Rows[RowNo].Cells[ColNo].Tag = DateVal.ToString("yyyyMMdd");
ColNo++;
}
for (RowNo = 0; RowNo < gridCalendar.Rows.Count; RowNo++)
{
gridCalendar.Rows[RowNo].Cells[5].Style.BackColor = DEF_WEEKEND;
gridCalendar.Rows[RowNo].Cells[6].Style.BackColor = DEF_WEEKEND;
}
for (RowNo = 0; RowNo < gridCalendar.Rows.Count; RowNo++)
{
for (ColNo = 0; ColNo < 7; ColNo++)
{
for (int ItmNo = 0; ItmNo < ReservedDates.Count; ItmNo++)
{
ClsDataItem oItm = (ClsDataItem)ReservedDates[ItmNo];
if (oItm.ID == gridCalendar.Rows[RowNo].Cells[ColNo].Tag.ToString())
{
switch (oItm.Text)
{
case "1":
gridCalendar.Rows[RowNo].Cells[ColNo].Style.BackColor =
DEF_COLOR1;
break;
case "2":
gridCalendar.Rows[RowNo].Cells[ColNo].Style.BackColor =
DEF_COLOR2;
break;
case "3":
gridCalendar.Rows[RowNo].Cells[ColNo].Style.BackColor =
DEF_COLOR3;
break;
}
}
}
}
}
if (gridCalendar.SelectedCells.Count > 0)
gridCalendar.SelectedCells[0].Selected = false;
for (RowNo = 0; RowNo < gridCalendar.Rows.Count; RowNo++)
{
for (ColNo = 0; ColNo < 7; ColNo++)
{
if (gridCalendar.Rows[RowNo].Cells[ColNo].Tag != null)
{
if (gridCalendar.Rows[RowNo].Cells[ColNo].Tag.ToString() ==
m_SelectedDate.ToString())
{
gridCalendar.Rows[RowNo].Cells[ColNo].Selected = true;
break;
}
}
}
}
gridCalendar.Left = 2;
cmbMonth.Left = 2;
gridCalendar.Width = 14 + gridCalendar.Columns.GetColumnsWidth(
DataGridViewElementStates.Visible);
gridCalendar.Height = 25 + gridCalendar.Rows.GetRowsHeight(
DataGridViewElementStates.Visible);
this.Height = cmbMonth.Height + gridCalendar.Height;
cmbYear.Left = gridCalendar.Right - cmbYear.Width - 13;
this.BackColor = DEF_BACKCOLOR;
gridCalendar.BackgroundColor = DEF_BACKCOLOR;
}
#endregion
#region Form events
public UsrCalendar()
{
InitializeComponent();
ReservedDates = new ArrayList();
m_SelectedDate = Str2Int(DateTime.Now.ToString("yyyyMMdd"));
}
private void UsrCalendar_Load(object sender, EventArgs e)
{
m_CurrentDate = m_SelectedDate;
cmbMonth.Items.Clear();
for (int MonthNo = 1; MonthNo < 13; MonthNo++)
{
DateTime oDate = new DateTime(2009, MonthNo, 1);
cmbMonth.Items.Add(new ClsDataItem(MonthNo.ToString().PadLeft(2, '0'),
oDate.ToString("MMMM")));
}
cmbYear.Items.Clear();
for (int YearNo = DateTime.Now.Year - DEF_YEARRANGE_LO; YearNo <=
DateTime.Now.Year + DEF_YEARRANGE_HI; YearNo++)
cmbYear.Items.Add(new ClsDataItem(YearNo.ToString(), YearNo.ToString()));
gridCalendar.Left = this.Left;
gridCalendar.Columns.Add("Mon", "Mo");
gridCalendar.Columns.Add("Tue", "Tu");
gridCalendar.Columns.Add("Wed", "We");
gridCalendar.Columns.Add("Thu", "Th");
gridCalendar.Columns.Add("Fri", "Fr");
gridCalendar.Columns.Add("Sat", "Sa");
gridCalendar.Columns.Add("Sun", "Su");
for (int ColNo = 0; ColNo < 7; ColNo++)
{
gridCalendar.Columns[ColNo].SortMode = DataGridViewColumnSortMode.NotSortable;
gridCalendar.Columns[ColNo].Width = gridCalendar.Width / 7;
gridCalendar.Columns[ColNo].DefaultCellStyle.Alignment =
DataGridViewContentAlignment.MiddleCenter;
}
gridCalendar.ColumnHeadersDefaultCellStyle.Alignment =
DataGridViewContentAlignment.MiddleCenter;
ComboSetValue(cmbMonth, YyyyMmDdGetMonth(m_CurrentDate).ToString().PadLeft(2, '0'));
ComboSetValue(cmbYear, YyyyMmDdGetYear(m_CurrentDate).ToString());
}
#endregion
#region Button events
private void btnYearPrev_Click(object sender, EventArgs e)
{
if (cmbYear.SelectedIndex > 0)
{
cmbYear.SelectedIndex--;
}
}
private void btnMonthPrev_Click(object sender, EventArgs e)
{
if (cmbMonth.SelectedIndex > 0)
{
cmbMonth.SelectedIndex--;
}
else
{
cmbMonth.SelectedIndex = cmbMonth.Items.Count - 1;
btnYearPrev_Click(sender, e);
}
}
private void btnToday_Click(object sender, EventArgs e)
{
m_CurrentDate = Str2Int(DateTime.Now.ToString("yyyyMMdd"));
bNoRedraw = true;
ComboSetValue(cmbMonth, DateTime.Now.Month.ToString().PadLeft(2, '0'));
ComboSetValue(cmbYear, DateTime.Now.Year.ToString());
bNoRedraw = false;
ShowMonth();
}
private void btnMonthNext_Click(object sender, EventArgs e)
{
if (cmbMonth.SelectedIndex < (cmbMonth.Items.Count - 1))
{
cmbMonth.SelectedIndex++;
}
else
{
cmbMonth.SelectedIndex = 0;
btnYearNext_Click(sender, e);
}
}
private void btnYearNext_Click(object sender, EventArgs e)
{
if (cmbYear.SelectedIndex < (cmbYear.Items.Count - 1))
{
cmbYear.SelectedIndex++;
}
}
private void btnCancel_Click(object sender, EventArgs e)
{
m_SelectedDate = 0;
if (DateSelected != null)
DateSelected(this, e);
}
private void btnSelect_Click(object sender, EventArgs e)
{
if (DateSelected != null)
DateSelected(this, e);
}
#endregion
#region Combobox events
private void cmbMonth_SelectedIndexChanged(object sender, EventArgs e)
{
if (!bNoRedraw)
{
m_CurrentDate = Str2Int(string.Format("{0}{1}01",
YyyyMmDdGetYear(m_CurrentDate),
ComboGetValue(cmbMonth).ToString().PadLeft(2, '0')));
ShowMonth();
}
}
private void cmbYear_SelectedIndexChanged(object sender, EventArgs e)
{
if (!bNoRedraw)
{
m_CurrentDate = Str2Int(string.Format("{0}{1}01",
ComboGetValue(cmbYear).ToString(),
YyyyMmDdGetMonth(m_CurrentDate).ToString().PadLeft(2, '0')));
ShowMonth();
}
}
#endregion
#region Grid events
private void gridCalendar_CellClick(object sender, DataGridViewCellEventArgs e)
{
if (e.RowIndex >= 0)
{
DateTime oDate = new DateTime(ComboGetValue(cmbYear), ComboGetValue(cmbMonth),
Str2Int(
gridCalendar.Rows[e.RowIndex].Cells[e.ColumnIndex].Value.ToString()));
m_SelectedDate = Str2Int(oDate.ToString("yyyyMMdd"));
}
}
private void gridCalendar_CellDoubleClick(object sender, DataGridViewCellEventArgs e)
{
if (e.RowIndex >= 0)
{
DateTime oDate = new DateTime(ComboGetValue(cmbYear), ComboGetValue(cmbMonth),
Str2Int(
gridCalendar.Rows[e.RowIndex].Cells[e.ColumnIndex].Value.ToString()));
m_SelectedDate = Str2Int(oDate.ToString("yyyyMMdd"));
}
btnSelect_Click(sender, e);
}
#endregion
}
}
The UsrCalendar
class shown above is the mainstay of this project. It contains all of the functionality needed to support the creation and maintenance of a popup calendar, which the user interacts with to select a date.
Note that the user control uses a delegate event in order to tell the host form on which it is created that the user has finished selecting a date from the popup calendar. You don't need to spend a whole lot of time trying to understand how delegates work; the logic involved can be a little daunting even for experienced developers like me - just follow the yellow brick road of comments in the source code provided, and everything should fall into place.
This is a user control which you can popup over a form. It doesn't do anything really fancy, this isn't the component you can earn the big bucks with (if you do, please remember who helped make that possible!); it just makes use of the DataGridView
component which comes as standard with Visual Studio 2005. There is quite a lot of potential for adding new functionality, for example, to expose the colours used to paint the control via properties - as provided, these colours are hardcoded in the UsrCalender
class, but could so easily be implemented as properties to make the functionality even more useful.
My primary objective with this submission to the CodeProject website was to use the KISS principle, which everyone knows stands for "Keep It Simple, Stupid!". This is a bare-bones project which almost certainly needs a little refinement on the part of the user. However, it is also a fully working project which should work without any additional effort on the part of the user. I hope it will be a starting point for others who feel just as frustrated as I have been at not finding a basic component in the Visual Studio toolbox which can at least give users the simplest of feedback about what dates are already allocated!
As befits the developer community, there's only one thing left to say - Enjoy!
Points of Interest
The time needed to implement this project was less than a day. A fair proportion of that time related to defining the logic for presenting the matrix of calendar days for a single month - everything else was fairly straightforward.