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

WinForms User Control for date selection

4.50/5 (6 votes)
17 Feb 2009CPOL5 min read 43.7K   1.5K  
A WinForms User Control which provides colour-coded feedback on dates which are already assigned.

TestCal

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:

C#
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;
    // Sets the lowest year relative to the current year.

    private const int DEF_YEARRANGE_HI = 3;
    // Sets the highest year relative to the current year.

    private Color DEF_BACKCOLOR = Color.LightBlue;
    // Defines the background colour for the control.

    private Color DEF_WEEKEND = Color.LightGreen;
    // Defines the weekend colour for the control.

    private Color DEF_OTHERMONTH_BACK = Color.LightGray;
    // Defines the background colour for dates not in current month.

    private Color DEF_OTHERMONTH_FORE = Color.SteelBlue;
    // Defines the foreground colour for dates not in current month.

    private Color DEF_COLOR1 = Color.Cyan;
    // Defines background colour for reserved dates #1.

    private Color DEF_COLOR2 = Color.DarkGreen;
    // Defines background colour for reserved dates #2.

    private Color DEF_COLOR3 = Color.DarkKhaki;
    // Defines background colour for reserved dates #3.

    private int m_CurrentDate;
    // The current date that the user is selecting.

    private int m_SelectedDate;
    // The date selected by the user.

    private Boolean bNoRedraw = false;
    // Set true if we don't want to update the control.

    public event EventHandler DateSelected;
    // Delegate for handling completion events.

    ArrayList ReservedDates;
    // List of reserved dates (these are coloured to
    // indicate their status).
#endregion

#region Properties

    /// <summary>
    /// Sets and retrieves the date by/for the owner of the control.
    /// </summary>
    internal int SelectedDate
    {
        get
        {
            return m_SelectedDate;
        }
        set
        {
            m_SelectedDate = value;
            m_CurrentDate = m_SelectedDate;
        }
    }

#endregion

#region Public methods

    /// <summary>
    /// Sets the position of the control on the parent form.
    /// </summary>
    /// <param name="pt"></param>
    public void SetControlPosition(Point pt)
    {
        this.Left = pt.X;
        this.Top = pt.Y;
    }

    /// <summary>
    /// Reserve date. These dates will be shown with a different background
    /// colour. Note that only 3 different colours are defined in the control
    /// as supplied - this could very easily be expanded upon if required so that
    /// the control could show a wide range of different status values for different
    /// dates.
    /// </summary>
    /// <param name="intYyyyMmDd">Date to reserve</param>
    /// <param name="intColor">Colour to reserve (1, 2 or 3)</param>
    public void ReserveDate(int intYyyyMmDd, int intColor)
    {
        Boolean bExists = false;

        // Walk thru the current reserved list to see whether we already have
        // this date. If we do then we don't bother saving a new copy.

        for (int RowNo = 0; RowNo < ReservedDates.Count; RowNo++)
        {
            ClsDataItem oItm = (ClsDataItem)ReservedDates[RowNo];

            if (oItm.ID.ToString() == intYyyyMmDd.ToString())
            {
                // We've already got the requested date, so we don't save it
                // again.

                bExists = true;
                break;
            }
        }

        if (!bExists)
        {
            // Requested date does not already exist in our list, so add
            // this to our arraylist.

            ClsDataItem oItm = new ClsDataItem(intYyyyMmDd.ToString(), 
                                               intColor.ToString());

            ReservedDates.Add(oItm);
        }
    }

#endregion

#region Private methods

    /// <summary>
    /// Select the requested value for the combobox.
    /// </summary>
    /// <param name="oCmb">The combobox to operate on</param>
    /// <param name="strVal">The value to select</param>
    private void ComboSetValue(ComboBox oCmb, string strVal)
    {
        // De-select the current selection.

        oCmb.SelectedIndex = -1;

        // Walk thru the combobox items to identify the one
        // we require.

        for (int RowNo = 0; RowNo < oCmb.Items.Count; RowNo++)
        {
            ClsDataItem oItm = (ClsDataItem)oCmb.Items[RowNo];

            if (oItm.ID == strVal)
            {
                // This is the item we want.

                oCmb.SelectedIndex = RowNo;
                break;
            }
        }

        // If we haven't found the item we want and there are items
        // available, select the first one.

        if ((oCmb.SelectedIndex < 0) && (oCmb.Items.Count > 0))
            oCmb.SelectedIndex = 0;
    }

    /// <summary>
    /// Get the value of the currently selected item in the combobox.
    /// Defaults to zero if no item is currently selected.
    /// </summary>
    /// <param name="oCmb">The combobox to operate on</param>
    /// <returns>Return value (zero if none currently selected)</returns>
    private int ComboGetValue(ComboBox oCmb)
    {
        int intRetVal = 0;

        if (oCmb.SelectedIndex >= 0)
        {
            // We do have a selected item in the combobox.

            ClsDataItem oItm = (ClsDataItem)oCmb.Items[oCmb.SelectedIndex];

            intRetVal = Str2Int(oItm.ID);
        }

        return intRetVal;
    }

    /// <summary>
    /// Get the year from the YyyyMmDd parameter.
    /// </summary>
    /// <param name="YyyyMmDd">Date value to extract year from</param>
    /// <returns>Year number</returns>
    private int YyyyMmDdGetYear(int YyyyMmDd)
    {
        int intRetVal = 0;

        intRetVal = YyyyMmDd / 10000;

        return intRetVal;
    }

    /// <summary>
    /// Get the month from the YyyyMmDd parameter.
    /// </summary>
    /// <param name="YyyyMmDd">Date value to extract month from</param>
    /// <returns>Month number (zero default)</returns>
    private int YyyyMmDdGetMonth(int YyyyMmDd)
    {
        int intRetVal = 0;

        intRetVal = YyyyMmDd / 100 - (100 * YyyyMmDdGetYear(YyyyMmDd));

        return intRetVal;
    }

    /// <summary>
    /// Get the day from the YyyyMmDd parameter.
    /// </summary>
    /// <param name="YyyyMmDd">Date value to extract day from</param>
    /// <returns>Day number (zero default)</returns>
    private int YyyyMmDdGetDay(int YyyyMmDd)
    {
        int intRetVal = 0;

        intRetVal = YyyyMmDd - (10000 * YyyyMmDdGetYear(YyyyMmDd)) - (100 *
            YyyyMmDdGetMonth(YyyyMmDd));

        return intRetVal;
    }

    /// <summary>
    /// Convert string to an integer. If the string doesn't contain a valid
    /// definition for an integer zero is returned instead.
    /// </summary>
    /// <param name="strVal"></param>
    /// <returns></returns>
    private int Str2Int(string strVal)
    {
        int intRetVal = 0;

        if (!int.TryParse(strVal, out intRetVal))
            intRetVal = 0;

        return intRetVal;
    }

    /// <summary>
    /// Show calendar contents.
    /// </summary>
    private void ShowMonth()
    {
        int ColNo = 0;
        DateTime DateVal;
        int YearNo = YyyyMmDdGetYear(m_CurrentDate);
        int MonthNo = YyyyMmDdGetMonth(m_CurrentDate);
        int DoW;
        int RowNo;

        // Clear current rows from grid.

        gridCalendar.Rows.Clear();

        DateVal = new DateTime(YearNo, MonthNo, 1);
        gridCalendar.Rows.Add();

        // We start painting the day numbers according to the day of the week
        // on which the 1st of the month falls.

        DoW = (int)DateVal.DayOfWeek;
        ColNo = DoW - 1;

        if (ColNo < 0)
            ColNo = 6;

        // Walk thru the days of the month, defining the day numbers on the grid.

        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)))
            {
                // We finished the columns in the current row. Add another row
                // to the grid.

                ColNo = 0;
                gridCalendar.Rows.Add();
            }
        }

        // If the 1st of the month isn't a Monday show dates for previous month.

        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");
        }

        // If the last day of the month doesn't fall on a Sunday show dates
        // for next month.

        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++;
        }

        // Set background colour for weekends.

        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;
        }

        // Show reserved dates by defining background colour for cells.

        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;
                        }
                    }
                }
            }
        }

        // Select calendar cell if the current date is current displayed in the grid.

        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;
                    }
                }
            }
        }

        // Set up physical dimensions according to the data shown in the grid.

        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;

        // Set background colour for control.

        this.BackColor = DEF_BACKCOLOR;
        gridCalendar.BackgroundColor = DEF_BACKCOLOR;
    }

#endregion

#region Form events

    /// <summary>
    /// Form instantiator.
    /// </summary>
    public UsrCalendar()
    {
        InitializeComponent();
        ReservedDates = new ArrayList();

        // Set default selected date to today's date. The user can change this
        // by setting
        // the current date property.

        m_SelectedDate = Str2Int(DateTime.Now.ToString("yyyyMMdd"));
    }

    /// <summary>
    /// Form load event. Prepare control.
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void UsrCalendar_Load(object sender, EventArgs e)
    {
        // To begin with the currently displayed month is the same as the
        // date handed down by the form on which the control resides.

        m_CurrentDate = m_SelectedDate;

        // Set up month combobox.

        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")));
        }

        // Set up year combobox.

        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()));

        // Define columns for grid.

        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;

        // Set initial values for comboboxes. This action will automatically draw the
        // grid.

        ComboSetValue(cmbMonth, YyyyMmDdGetMonth(m_CurrentDate).ToString().PadLeft(2, '0'));
        ComboSetValue(cmbYear, YyyyMmDdGetYear(m_CurrentDate).ToString());
    }

#endregion

#region Button events

    /// <summary>
    /// Go to previous year.
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void btnYearPrev_Click(object sender, EventArgs e)
    {
        if (cmbYear.SelectedIndex > 0)
        {
            cmbYear.SelectedIndex--;
        }
    }

    /// <summary>
    /// Go to previous month.
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    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);
        }
    }

    /// <summary>
    /// Go to today.
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    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();
    }

    /// <summary>
    /// Go to next month.
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    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);
        }
    }

    /// <summary>
    /// Go to next year.
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void btnYearNext_Click(object sender, EventArgs e)
    {
        if (cmbYear.SelectedIndex < (cmbYear.Items.Count - 1))
        {
            cmbYear.SelectedIndex++;
        }
    }

    /// <summary>
    /// Cancel date selection. We signify this action by setting the
    /// selected date to zero.
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void btnCancel_Click(object sender, EventArgs e)
    {
        // Set selected date to zero to signify cancellation.

        m_SelectedDate = 0;

        if (DateSelected != null)
            DateSelected(this, e);
    }

    /// <summary>
    /// Select the date and return control to the parent form.
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void btnSelect_Click(object sender, EventArgs e)
    {
        if (DateSelected != null)
            DateSelected(this, e);
    }

#endregion

#region Combobox events

    /// <summary>
    /// Changed month.
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    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();
        }
    }

    /// <summary>
    /// Changed year.
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    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

    /// <summary>
    /// User has clicked on a date.
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    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"));
        }
    }

    /// <summary>
    /// User has double-clicked on a date.
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    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.

License

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