Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Yet another DateTime...Slicker?

0.00/5 (No votes)
26 Nov 2003 1  
Everyone loves to hate the Windows Forms DateTimePicker control. This article shows how DateTimePicker can be improved without throwing the baby out with the bathwater. It describes bug fixes along with data-binding and usability enhancements to DateTimePicker.

Sample Image - DateTimeSlicker1.png

Introduction

I know, I know�you�re thinking, �Yet another DateTimePicker?� DateTimeSlicker is different and better. If you must, love it only for its oh-so-clever name.

With multiple DateTimePicker alternatives available, from .NET discussion group samples to $400 commercial controls, why would I spend time on this? My motivation�centered on improving upon the Windows Forms DateTimePicker�was threefold:

Others have reimplemented DateTimePicker, adding and removing features to produce a result that doesn�t appeal to me. DateTimePicker�s existing capabilities are many: empty values, keyboard and mouse input, culture sensitivity, XP (Luna) theming, and more. I didn�t want to take anything away; I just wanted to fix what�s broken.

Focus rectangle is drawn when it should not be

DateTimePicker paints a focus cue on its check box when the control is created, and any time the value of its Checked property changes. This means that a focus cue can appear while the control doesn�t actually have focus. This is unacceptable behavior in a well-mannered WinForms control! It�s confusing to have a control appear focused when it really isn�t.

DateTimePicker gets focus wrong

This bug has been with us since DateTimePicker was introduced years ago, and yet it doesn�t seem to get any airtime. I got tired of waiting for Redmond to fix the bug, so I decided to do something about it myself.

Lack of data-binding support for DBNull values

I hate how you can�t easily bind a DateTimePicker to a nullable ADO.NET DataColumn. Because a nullable column can contain a DBNull value, you can�t bind to DateTimePicker�s Text property nor to its Value property; these properties can accept only string values and DateTime values, respectively.

DateTimePicker can�t bind to DBNull values

You can set DateTimePicker�s Text property to null (Nothing in Visual Basic), but doing so doesn�t clear the check box; the current date and time are displayed instead. (I used Lutz Roeder�s .NET Reflector to verify this by decompiling the Windows Forms library. If you don�t have this terrific tool, download it now.)

The wonders of binding in Avalon are still months and months away, so I wanted these problems solved now in WinForms.

Unpopular display of empty values

DateTimePicker never appears empty, even when the check box is cleared. Instead, DateTimePicker�s Checked property�in addition to checking and clearing the check box�enables and disables (or �grays out�) the text. Many people despise this behavior. I myself am not fond of it because disabled text usually doesn�t look different enough from enabled text to avoid ambiguity, especially if your monitor�s contrast is not good. Users can easily mistake the disabled-text value in a DateTimePicker for an enabled-text value. It would be better to hide the text when the check box is cleared.

DateTimePicker can�t display empty values

Some have gone so far as to eliminate the check box from their DateTimePicker alternatives. I think this is a mistake, because in the effort to fix one usability problem they introduce others. How does the user clear a DateTimePicker if he can�t select all the text displayed in it? Do you really want to give up the behavior that allows only one element of the date/time to be selected at once? I toyed with the idea of replacing the check box with a delete button next to the dropdown button, but that didn�t feel right, either. Better to leave well enough alone, I think, and just try to hide the text when the check box is cleared.

Abortive attempts

Thomas Edison experimented with hundreds and hundreds of materials before he got his light bulb to work with a carbon filament. (The problem at hand is much simpler, of course, and so is the author.) In the effort to resolve all three of the aforementioned issues with DateTimePicker, I explored and abandoned multiple approaches that ranged from inheritance to containment in a UserControl to a combination of both. The root of the difficulty is that DateTimePicker�s Checked property is unreliable; in certain situations, it returns true while the check box appears to be cleared.

I tried listening to the Windows message stream (that is to say, I overrode DateTimePicker�s WndProc() method) in the attempt to keep track of the state of the check box, but the messages generated by a mouse click do not seem to correlate with the state of the check box. The message stream is exactly the same whether the check box is being checked or cleared.

I tried using reflection to monitor the state of private fields in DateTimePicker. There are two boolean fields that look promising (again, use .NET Reflector to decompile .NET executables). But neither of these, nor any other fields I could observe via reflection, reveals whether the check box is checked or cleared.

I also briefly considered writing my own DateTimePicker from scratch, but that was only a momentary lapse of reason. Losing type compatibility with the existing DateTimePicker would have been a disadvantage. More importantly, rewriting the DateTimePicker would have entailed too much work; I would have got mired in dark areas of Win32 development that I didn�t want to get into just now. Besides, it seems like overkill to reinvent the wheel when all that is needed is a little grease.

The implementation

I decided to handle the user input myself so that I would know for sure whether the check box was checked or cleared. I overrode WndProc() and handled the WM_CHAR and WM_LMOUSEDOWN messages. DateTimeSlicker hides the text by setting DateTimePicker�s Format and CustomFormat properties to show an empty string when the check box is cleared. Problem #3 solved.

DateTimeSlicker displays empty values

There are some legitimate uses for the controversial OOP feature of �shadowing�, or hiding, a base class member using the new modifier (the Shadows keyword in Visual Basic). DateTimeSlicker shadows DateTimePicker�s Value property so that it can accept either a DateTime value or a DBNull value. When the Value property is DBNull, DateTimeSlicker sets the Checked property to false. Problem #2 solved.

DateTimeSlicker binds to DBNull

Important note: Although shadowing does the job here, it only works properly if you call the new members from the interface in which they�re declared. For example, if I call the Checked, CustomFormat, Format and Value properties on a variable of type DateTimePicker, the new members will not be called, regardless of whether the object underlying my DateTimePicker variable is a DateTimeSlicker.

KlugeFocusBug() grabs focus and then focuses the previously active control again. This is admittedly not the most elegant solution (what workaround is?). But I was surprised to discover by experimentation that my managed-code approach is cleaner than posting Windows messages in order to manipulate focus. Problem #1 solved.

DateTimeSlicker gets focus right

Finally, because I interfered with some of DateTimePicker�s behavior, some events no longer fired as expected. I added code to suppress certain events and fire others.

The code

Here�s the nitty-gritty.

using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Security.Permissions;
using System.Windows.Forms;


namespace Umbrae.Windows.Forms
{
    // Author:     Nils Jonsson

    // Originated: 10/03/2003

    /// <summary>

    /// Represents an enhanced Windows date-time picker control.

    /// </summary>

    [DefaultEvent("ValueChanged")]
    [DefaultProperty("Value")]
    [ToolboxItemFilter("System.Windows.Forms")]
    public class DateTimeSlicker : DateTimePicker
    {
        /// <summary>

        /// Initializes a new instance of the <see cref="DateTimePicker" />

        /// class.

        /// </summary>

        public DateTimeSlicker() : base()
        {
            this.ParentChanged += new EventHandler(this.this_ParentChanged);
            
            // Show the check box because it is most of the raison d��tre for

            // this class.

            this.ShowCheckBox = true;
            
            this.customFormat = base.CustomFormat;
            this.format = base.Format;
        }
        
        
        /// <summary>

        /// Occurs when the value of the <see cref="Checked" /> property

        /// changes.

        /// </summary>

        [Category("Property Changed")]
        [Description("Occurs when the Checked property value is changed.")]
        public event EventHandler CheckedChanged
        {
            add { this.checkedChanged += value; }
            
            remove { this.checkedChanged -= value; }
        }
        
        /// <summary>

        /// Occurs when the value of the <see cref="CustomFormat" /> property

        /// changes.

        /// </summary>

        [Category("Property Changed")]
        [Description("Occurs when the CustomFormat property value is changed.")]
        public event EventHandler CustomFormatChanged
        {
            add { this.customFormatChanged += value; }
            
            remove { this.customFormatChanged -= value; }
        }
        
        /// <summary>

        /// Gets or sets a value indicating whether the <see cref="Value" />

        /// property has been set with a valid date-time value and the displayed

        /// value is able to be updated.

        /// </summary>

        /// <value><c>true</c> if the <see cref="Value" /> property has been set

        /// with a valid <see cref="DateTime" /> value and the displayed value

        /// is able to be updated; otherwise, <c>false</c>. The default is

        /// <c>true</c>.</value>

        true)>
        [Category("Behavior")]
        [DefaultValue(true)]
        [Description("Determines if the check box is checked, indicating that "
         + "the user has selected a value.")]
        public new bool Checked
        {
            get { return base.Checked; }
            
            set
            {
                if (value == this.Checked)
                    return;
                
                this.SetCheckBoxState(value);
                this.OnCheckedChanged();
                this.KlugeFocusBug();
            }
        }
        
        /// <summary>

        /// Gets or sets the custom date-time format string.

        /// </summary>

        /// <value>A string that represents the custom date-time format. The

        /// default is a null reference (<c>Nothing</c> in Visual

        /// Basic).</value>

        [Category("Behavior")]
        [DefaultValue(null)]
        [Description("The custom format string used to format the date and/or "
         + "time displayed.")]
        [RefreshProperties(RefreshProperties.Repaint)]
        public new string CustomFormat
        {
            get { return this.customFormat; }
            
            set
            {
                if (value == this.CustomFormat)
                    return;
                
                if (this.Checked)
                    base.CustomFormat = value;
                this.customFormat = value;
                this.OnCustomFormatChanged();
            }
        }
        
        /// <summary>

        /// Gets or sets the format of the date and time displayed in the

        /// control.

        /// </summary>

        /// <value>One of the <see cref="DateTimePickerFormat" /> values. The

        /// default is <see cref="DateTimePickerFormat.Long" />.</value>

        /// <exception cref="InvalidEnumArgumentException">The value assigned is

        /// not one of the <see cref="DateTimePickerFormat" />

        /// values.</exception>

        [Category("Appearance")]
        [DefaultValue(DateTimePickerFormat.Long)]
        [Description("Determines whether the date and/or time is displayed "
         + "using standard or custom formatting.")]
        [RefreshProperties(RefreshProperties.Repaint)]
        public new DateTimePickerFormat Format
        {
            get { return this.format; }
            
            set
            {
                if (value == this.Format)
                    return;
                
                if (this.Checked)
                {
                    // Calls OnFormatChanged().

                    base.Format = value;
                }
                this.format = value;
                if (!(this.Checked))
                    this.OnFormatChanged();
            }
        }
        
        /// <summary>

        /// Gets or sets the text associated with this control.

        /// </summary>

        /// <value>A <see cref="String" /> object that represents the text

        /// associated with this control.</value>

        /// <exception cref="ArgumentNullException">Argument is a null reference

        /// (<c>Nothing</c> in Visual Basic).</exception>

        false)>
        [DesignerSerializationVisibility(
         DesignerSerializationVisibility.Hidden)]
        [EditorBrowsable(EditorBrowsableState.Advanced)]
        public override string Text
        {
            get
            {
                if (this.Checked)
                    return base.Text;
                else
                    return string.Empty;
            }
            
            set
            {
                if (value == null)
                {
                    throw new ArgumentNullException(null,
                     "Argument cannot be null.");
                }
                
                if (value == this.Text)
                    return;
                
                if (value == string.Empty)
                {
                    this.OnTextChanged();
                    this.Checked = false;
                }
                else
                {
                    // Calls OnTextChanged().

                    base.Text = value;
                }
            }
        }
        
        /// <summary>

        /// Gets or sets the date-time value assigned to the control.

        /// </summary>

        /// <value>The <see cref="DateTime" /> value assigned to the

        /// control.</value>

        /// <exception cref="ArgumentNullException">Argument is a null reference

        /// (<c>Nothing</c> in Visual Basic).</exception>

        /// <exception cref="ArgumentException">Argument is neither a

        /// <see cref="DateTime" /> nor a <see cref="DBNull" />

        /// value.</exception>

        true)>
        [Category("Behavior")]
        [Description("The date and/or time value (also can be DBNull).")]
        [RefreshProperties(RefreshProperties.All)]
        public new object Value
        {
            get
            {
                if (this.Checked)
                    return base.Value;
                else
                    return DBNull.Value;
            }
            
            set
            {
                if (value == null)
                {
                    throw new ArgumentNullException(null,
                     "Argument cannot be null.");
                }
                
                if (value == this.Value)
                    return;
                
                if (value == DBNull.Value)
                {
                    this.Checked = false;
                    this.OnValueChanged();
                }
                else if (value is DateTime)
                {
                    // Calls OnValueChanged().

                    base.Value = (DateTime)value;
                    this.Checked = true;
                }
                else
                {
                    throw new ArgumentException(
                     "Argument must be a DateTime or DBNull.Value.");
                }
            }
        }
        
        
        // Estimates the right edge of the area in which a mouse click will

        // toggle the check box. This varies with the font used to display the

        // text of the DateTimePicker.

        private int GetCheckBoxExtent()
        {
            // Use the Height property because the check box is square.

            return
             (int)this.CreateGraphics().MeasureString("X", this.Font).Height;
        }
        
        // DateTimePicker paints a focus cue on the check box when the control

        // is created, and any time the Checked property changes. This means

        // that a focus cue can appear while the control does not actually have

        // focus. This method works around that behavior.

        [UIPermission(SecurityAction.Demand)]
        private void KlugeFocusBug()
        {
            // The kluge is not necessary if this DateTimeSlicker has the focus.

            if (this.Focused)
                return;
            
            Control parent = this.Parent;
            while ((parent != null) && !(parent is ContainerControl))
                parent = parent.Parent;
            
            ContainerControl parentContainer = parent as ContainerControl;
            if (parentContainer == null)
                return;
            
            Control activeControl = parentContainer.ActiveControl;
            // Set focus on this DateTimeSlicker.

            this.Focus();
            // Set focus back on the previously active control.

            if (activeControl != null)
                activeControl.Focus();
        }
        
        /// <summary>

        /// Raises the <see cref="CheckedChanged" /> event.

        /// </summary>

        /// <param name="e">An <see cref="EventArgs" /> that contains the event

        /// data.</param>

        /// <exception cref="ArgumentNullException"><paramref name="e" /> is a

        /// null reference (<c>Nothing</c> in Visual Basic).</exception>

        protected virtual void OnCheckedChanged(EventArgs e)
        {
            if (e == null)
            {
                throw new ArgumentNullException("e",
                 "Argument e cannot be null.");
            }
            
            if (this.checkedChanged != null)
                this.checkedChanged(this, e);
        }
        
        private void OnCheckedChanged()
        {
            this.OnCheckedChanged(EventArgs.Empty);
        }
        
        /// <summary>

        /// Raises the <see cref="CustomFormatChanged" /> event.

        /// </summary>

        /// <param name="e">An <see cref="EventArgs" /> that contains the event

        /// data.</param>

        /// <exception cref="ArgumentNullException"><paramref name="e" /> is a

        /// null reference (<c>Nothing</c> in Visual Basic).</exception>

        protected virtual void OnCustomFormatChanged(EventArgs e)
        {
            if (e == null)
            {
                throw new ArgumentNullException("e",
                 "Argument e cannot be null.");
            }
            
            if (this.customFormatChanged != null)
                this.customFormatChanged(this, e);
        }
        
        private void OnCustomFormatChanged()
        {
            this.OnCustomFormatChanged(EventArgs.Empty);
        }
        
        /// <summary>

        /// Raises the <see cref="DateTimePicker.FormatChanged" /> event.

        /// </summary>

        /// <param name="e">An <see cref="EventArgs" /> that contains the event

        /// data.</param>

        /// <exception cref="ArgumentNullException"><paramref name="e" /> is a

        /// null reference (<c>Nothing</c> in Visual Basic).</exception>

        protected override void OnFormatChanged(EventArgs e)
        {
            // Suppress the event if Format changed in order to hide the text.

            if (!(this.showingOrHidingText))
                base.OnFormatChanged(e);
        }
        
        private void OnFormatChanged()
        {
            this.OnFormatChanged(EventArgs.Empty);
        }
        
        /// <summary>

        /// Raises the <see cref="Control.KeyDown" /> event.

        /// </summary>

        /// <param name="e">A <see cref="KeyEventArgs" /> that contains the event

        /// data.</param>

        /// <exception cref="ArgumentNullException"><paramref name="e" /> is a

        /// null reference (<c>Nothing</c> in Visual Basic).</exception>

        protected override void OnKeyDown(KeyEventArgs e)
        {
            if (((e.KeyCode == Keys.F4) && !(e.Alt))
             || (e.KeyCode == Keys.Down) && e.Alt)
            {
                // Check the check box because the user dropped down the

                // calendar via keyboard input.

                this.Checked = true;
            }
            
            base.OnKeyDown(e);
        }
        
        private void OnTextChanged()
        {
            this.OnTextChanged(EventArgs.Empty);
        }
        
        private void OnValueChanged()
        {
            this.OnValueChanged(EventArgs.Empty);
        }
        
        private void SetCheckBoxState(bool value)
        {
            base.Checked = value;
            if (this.Checked)
            {
                this.showingOrHidingText = true;
                base.CustomFormat = this.customFormat;
                base.Format = this.format;
                this.showingOrHidingText = false;
            }
            else
            {
                // Tweak CustomFormat and Format in order to make the text

                // portion appear empty.

                this.showingOrHidingText = true;
                base.CustomFormat = " ";
                base.Format = DateTimePickerFormat.Custom;
                this.showingOrHidingText = false;
            }
        }
        
        private void Parent_Paint(object sender, PaintEventArgs e)
        {
            Debug.Assert(sender == this.Parent,
             string.Format("Unexpected sender {0}.", sender));
            
            // Run once for this Parent.

            this.Parent.Paint -= new PaintEventHandler(this.Parent_Paint);
            
            this.KlugeFocusBug();
        }
        
        private void this_ParentChanged(object sender, EventArgs e)
        {
            Debug.Assert(sender == this,
             string.Format("Unexpected sender {0}.", sender));
            
            if (this.Parent != null)
                this.Parent.Paint += new PaintEventHandler(this.Parent_Paint);
        }
        
        /// <summary>

        /// This member overrides <see cref="Control.WndProc" />.

        /// </summary>

        /// <param name="m">The Windows <see cref="Message" /> to

        /// process.</param>

        /// <exception cref="ArgumentNullException"><paramref name="m" /> is a

        /// null reference (<c>Nothing</c> in Visual Basic).</exception>

        [SecurityPermission(SecurityAction.Demand)]
        protected override void WndProc(ref Message m)
        {
            switch (m.Msg)
            {
                case 0x102: // WM_CHAR

                    int charAsInt32 = m.WParam.ToInt32();
                    char charAsChar = (char)charAsInt32;
                    if (charAsInt32 == (int)Keys.Space)
                    {
                        // Toggle the check box because the user pressed the

                        // space bar.

                        this.OnKeyPress(new KeyPressEventArgs(charAsChar));
                        this.Checked = !(this.Checked);
                    }
                    else
                    {
                        // Forward the message to DateTimePicker because a key

                        // other than the space bar was pressed.

                        base.WndProc(ref m);
                    }
                    break;
                case 0x201: // WM_LBUTTONDOWN

                    // The X value of the mouse position is stored in the

                    // low-order bits of LParam, and the Y value in the

                    // high-order bits.

                    int x = (m.LParam.ToInt32() << 16) >> 16;
                    int y = m.LParam.ToInt32() >> 16;
                    if (x <= this.GetCheckBoxExtent())
                    {
                        // Toggle the check box because the user clicked (near)

                        // the check box.

                        this.OnMouseDown(
                         new MouseEventArgs(MouseButtons.Left, 1, x, y, 0));
                        this.Checked = !(this.Checked);
                        // Grab focus because we are eating the message.

                        this.Focus();
                    }
                    else
                    {
                        bool checkedChange = !(this.Checked);
                        if (checkedChange)
                        {
                            // Check the check box because the user clicked

                            // somewhere within the control while it was not

                            // checked.

                            this.SetCheckBoxState(true);
                        }
                        // Forward the message to DateTimePicker so that mouse

                        // events will be fired, focus will be taken, etc.

                        base.WndProc(ref m);
                        if (checkedChange)
                            this.OnCheckedChanged();
                    }
                    break;
                default:
                    // Forward the message to DateTimePicker because it pertains

                    // to neither the left mouse button nor a pressed key.

                    base.WndProc(ref m);
                    break;
            }
        }
        
        
        private bool showingOrHidingText;
        
        private EventHandler checkedChanged;
        private EventHandler customFormatChanged;
        
        private string  customFormat;
        private DateTimePickerFormat format;
    }
}

History

Monday, November 24th, 2003: Article first submitted.

Thursday, November 27th, 2003: Fixed a problem with dropping down and navigating the calendar via the keyboard (F4 or Alt+Down) when the check box is not checked. The control�s response to a drop-down request via the keyboard is now consistent with its response to a drop-down request via the mouse; the check box becomes checked if it is not checked, and the user must issue a second drop-down request in order to get the drop-down to occur. This behavior is not identical to that of DateTimePicker, but it�s close�I�ll get around to conforming it at some point.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here