Click here to Skip to main content
15,885,366 members
Articles / Programming Languages / C#

# Nullable DateEdit Control

Rate me:
Please Sign up or sign in to vote.
4.13/5 (8 votes)
15 Jun 20034 min read 127.2K   812   33  
A Nullable C# DateEdit Control
using System;
using System.Windows.Forms;
using System.Text.RegularExpressions;
using System.ComponentModel;
using System.Collections;
using System.Globalization;

namespace DateEdit
{
	/// <summary>
	/// Summary description for DateEdit. The DateEdit control is designed to allow Null DateTime values
	///		to be entered by the user. 
	/// Notes: 
	///		1) DateTime.MinValue is reserved for use to represent NullValue. There are a couple of options
	///			regarding database handling. 
	///				a) Insert DateTime.NullValue into db and REMEMBER the null value (01/01/0001). 
	///				b) Convert any DateTime containing NullValue to a real null.
	///		2) InvalidDate event is fired when leaving the control with an invalid date. Use this to provide
	///			users an indication of the problem. Beep?, MessageBox?, change BackColor. ValidDate event
	///			can be used to signal good dates, or undo color changes
	///			
	///	Author: Oscar Bowyer 
	///	Revision History:
	///		Initial Release 5/22/2003
	/// </summary>
	public class DateEdit : TextBox
	{
		private string m_format;		// display format (mask with input chars replaced by input char)
		private string m_convert;		// used to convert DateTime to string
		private char m_inpChar = '_';
		private string m_regex = @"[0-9]";
		private int m_caret;
		private Hashtable m_posNdx;

		// use MinValue as NullValue
		public static readonly DateTime NullValue = DateTime.MinValue;

		// events
		public delegate void InvalidDateEventHandler(object sender, EventArgs e);
		public delegate void ValidDateEventHandler(object sender, EventArgs e);

		public event InvalidDateEventHandler InvalidDate;
		public event ValidDateEventHandler ValidDate;

		public DateEdit()
		{
			BuildFormat();
			base.MaxLength = m_format.Length;
			BuildPosNdx();
			base.Text = m_format;

			// disable context menu since it bypasses Ctrl+V handler
			this.ContextMenu = new ContextMenu();
		}

		[Category("Behavior")]
		protected void OnInvalidDate(EventArgs e)
		{
			if(InvalidDate != null)
				InvalidDate(this, e);
		}

		[Category("Behavior")]
		protected void OnValidDate(EventArgs e)
		{
			if(ValidDate != null)
				ValidDate(this, e);
		}

		[Description("Sets the Input Char default '_'"), Category("Behavior"), 
		RefreshProperties(RefreshProperties.All)]
		public char InputChar
		{
			// default '_'
			get{return m_inpChar;}
			set
			{
				m_inpChar = value;
				BuildFormat();
			}
		}

		[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
		public override string Text
		{
			get{return base.Text;}
			set{ /* ignore set */}
		}

		[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
		public override bool Multiline
		{
			get{return base.Multiline;}
			// ignore set
		}

		[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
		public new int SelectionStart
		{
			get{return base.SelectionStart;}
		}

		[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
		public override int SelectionLength
		{
			get{return base.SelectionLength;}
		}

		public override int MaxLength
		{
			get{return base.MaxLength;}
		}

		protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
		{
			// NOTES: 
			//	1) break; causes warnings below
			//	2) m_caret tracks caret location, always the start of selected char
			//	3) code below is based on MaskedEdit, since our format is fixed
			//		there may be optimizations possible
			int strt = base.SelectionStart;
			int len = base.SelectionLength;
			int end = strt + base.SelectionLength - 1;
			string s = base.Text;
			int p;
			
			// handle startup, runs once
			if(m_format[strt] != m_inpChar)
			{
				strt = Next(-1);
				len = 1;
			}

			switch(keyData)
			{
				case Keys.Left:
				case Keys.Up:
					p = Prev(strt);
					if(p != strt)
					{
						base.SelectionStart = p;
						base.SelectionLength = 1;
					}
					m_caret = p;
					return true;
				case Keys.Left | Keys.Shift:
				case Keys.Up | Keys.Shift:
					if((strt < m_caret) || (strt == m_caret && len <= 1))
					{
						// enlarge left
						p = Prev(strt);
						base.SelectionStart -= (strt - p);
						base.SelectionLength = len + (strt - p);
					}
					else
					{
						// shrink right
						base.SelectionLength = len - (end - Prev(end));
					}
					return true;
				case Keys.Right:
				case Keys.Down:
					p = Next(strt);
					if(p != strt)
					{
						base.SelectionStart = p;
						base.SelectionLength = 1;
					}
					m_caret = p;
					return true;
				case Keys.Right | Keys.Shift:
				case Keys.Down | Keys.Shift:
					if(strt < m_caret)
					{
						// shrink left
						p = Next(strt);
						base.SelectionStart += (p - strt);
						base.SelectionLength = len - (p - strt);
					}
					else if(strt == m_caret)
					{
						// enlarge right
						p = Next(end);
						base.SelectionLength = len + (p - end);
					}
					return true;
				case Keys.Delete:
					// delete selection, replace with input format
					base.Text = s.Substring(0, strt) + m_format.Substring(strt, len) + s.Substring(strt + len);
					base.SelectionStart = strt;
					base.SelectionLength = 1;
					m_caret = strt;
					return true;
				case Keys.Home:
				case Keys.Left | Keys.Control:
				case Keys.Home | Keys.Control:
					base.SelectionStart = Next(-1);
					base.SelectionLength = 1;
					m_caret = base.SelectionStart;
					return true;
				case Keys.Home | Keys.Shift:
					if(strt <= m_caret && len <= 1)
					{
						// enlarge left
						p = Next(-1);
						base.SelectionStart -= (strt - p);
						base.SelectionLength = len + (strt - p);
					}
					else
					{
						// shrink right
						p = Next(-1);
						base.SelectionStart = p;
						base.SelectionLength = (m_caret - p) + 1;
					}
					return true;
				case Keys.End:
				case Keys.Right | Keys.Control:
				case Keys.End | Keys.Control:
					base.SelectionStart = Prev(base.MaxLength);
					base.SelectionLength = 1;
					m_caret = base.SelectionStart;
					return true;
				case Keys.End | Keys.Shift:
					if(strt < m_caret)
					{
						// shrink left
						p = Prev(base.MaxLength);
						base.SelectionStart = m_caret;
						base.SelectionLength = (p - m_caret + 1);
					}
					else if(strt == m_caret)
					{
						// enlarge right
						p = Prev(base.MaxLength);
						base.SelectionLength = len + (p - end);
					}
					return true;
				case Keys.V | Keys.Control:
				case Keys.Insert | Keys.Shift:
					// paste from clipboard
					IDataObject iData = Clipboard.GetDataObject();

					// assemble new text
					string t = s.Substring(0, strt)
						+ (string)iData.GetData(DataFormats.Text)
						+ s.Substring(strt + len);

					// check if data to be pasted is convertable to inputType
					DateTime dt;;
					try
					{
						dt = DateTime.Parse(t);
						base.Text = dt.ToString("MM/dd/yyyy");

						// reset selection
						base.SelectionStart = strt;
						base.SelectionLength = len;
					}
					catch
					{
						// do nothing
					}
					
					return true;
				default:
					return base.ProcessCmdKey(ref msg, keyData);
			}
		}

		protected override void OnMouseUp(MouseEventArgs e)
		{
			// reset selection to include input chars
			int strt = base.SelectionStart;
			int orig = strt;
			int len = base.SelectionLength;

			// reset selection start
			if(strt == base.MaxLength || m_format[strt] != m_inpChar)
			{
				// reset start
				if(Next(strt) == strt)
					strt = Prev(strt);
				else
					strt = Next(strt);

				base.SelectionStart = strt;
			}

			// reset selection length
			if(len < 1)
				base.SelectionLength = 1;
			else if(m_format[orig + len - 1] != m_inpChar)
			{
				len += Next(strt + len) - (strt + len);
				base.SelectionLength = len;
			}

			m_caret = strt;
			base.OnMouseUp(e);
		}

		[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden), Browsable(false)]
		public bool IsValid
		{
			get
			{
				try
				{
					// null is valid
					if(base.Text == m_format)
						return true;

					DateTime ret = DateTime.Parse(base.Text);
				}
				catch
				{
					return false;
				}

				return true;
			}
		}

		[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden), Browsable(false)]
		public DateTime Value
		{
			get
			{
				// TODO: What to return if Null not allowed and invalid value?
				//	a) error?
				//	b) Null?
				DateTime ret;
				try
				{
					ret = DateTime.Parse(base.Text);
				}
				catch
				{
					ret = NullValue;
				}

				return ret;
			}
			set
			{
				if(value == NullValue)
					base.Text = m_format;
				else
					base.Text = value.ToString(m_convert);	// TODO: must format using current culture!!!

				OnValidDate(EventArgs.Empty);
			}
		}

		protected override void OnLeave(EventArgs e)
		{
			// validate entry
			try
			{
				if(this.Text == m_format)
					return;
				DateTime dt = DateTime.Parse(this.Text);
				OnValidDate(EventArgs.Empty);
			}
			catch
			{
				// fire InvalidDate event
				OnInvalidDate(EventArgs.Empty);
			}
			finally
			{
				base.OnLeave(EventArgs.Empty);
			}
		}


		protected override void OnKeyPress(KeyPressEventArgs e)
		{
			int strt = base.SelectionStart;
			int len = base.SelectionLength;
			int p;

			// Handle Backspace -> replace previous char with inpchar and select
			if(e.KeyChar == 0x08)
			{
				string s = base.Text;
				p = Prev(strt);
				if(p != strt)
				{
					base.Text = s.Substring(0, p) + m_inpChar.ToString() + s.Substring(p + 1);
					base.SelectionStart = p;
					base.SelectionLength = 1;
					
				}
				m_caret = p;
				e.Handled = true;
				return;
			}
			
			// update display if valid char entered
			if(IsValidChar(e.KeyChar, (int)m_posNdx[strt]))
			{
				// assemble new text
				string t = "";
				t = base.Text.Substring(0, strt);
				t += e.KeyChar.ToString();

				if(strt + len != base.MaxLength)
				{
					t += m_format.Substring(strt + 1, len - 1);
					t += base.Text.Substring(strt + len);
				}
				else
					t += m_format.Substring(strt + 1);

				base.Text = t;

				// select next input char
				strt = Next(strt);
				base.SelectionStart = strt;
				m_caret = strt;
				base.SelectionLength = 1;
			}
			e.Handled = true;
		}

		private bool IsValidChar(char input, int pos)
		{
			// validate input char against mask
			return Regex.IsMatch(input.ToString(), m_regex);
		}

		private int Prev(int startPos)
		{
			// return previous input char position
			// returns current position if no input chars to the left
			int strt = startPos;
			int ret = strt;

			while(strt > 0)
			{
				strt--;
				if(m_format[strt] == m_inpChar)
					return strt;
			}
			return ret;			
		}

		private int Next(int startPos)
		{
			// return next input char position
			// returns current position if no input chars to the left
			int strt = startPos;
			int ret = strt;
			
			while(strt < base.MaxLength - 1)
			{
				strt++;
				if(m_format[strt] == m_inpChar)
					return strt;
			}

			return ret;			
		}

		private void BuildFormat()
		{
			// this builds the m_format string based on current regional settings
			//	EN example "M/d/yyyy" with default input char '_' produces "__/__/____"

			m_format = "";
			m_convert = "";
			string pat = CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern;
			string sep = DateTimeFormatInfo.CurrentInfo.DateSeparator;

			int pos = 0;
			string match = pat[pos].ToString();
			int cont = 0;
			while(cont < 3)
			{
				switch(match)
				{
					case "M":
						m_format += m_inpChar.ToString() + m_inpChar.ToString() + sep;
						m_convert += "MM" + sep;
						break;
					case "d":
						m_format += m_inpChar.ToString() + m_inpChar.ToString() + sep;
						m_convert += "dd" + sep;
						break;
					case "y":
						m_format += m_inpChar.ToString() + m_inpChar.ToString() + m_inpChar.ToString() + m_inpChar.ToString() + sep;
						m_convert += "yyyy" + sep;
						break;
					default:
						break;
				}

				// move to next Mdy char
				pos = pat.IndexOf(sep, pos) + 1;
				match = pat[pos].ToString();
				cont++;
			}
			
			// strip final seperator
			m_format = m_format.Substring(0, m_format.Length - 1);
			m_convert = m_convert.Substring(0, m_convert.Length - 1);
		}

		private void BuildPosNdx()
		{
			// used to build position translation map from mask string
			//	and input format
			string s = m_format;

			// reset index
			if(m_posNdx == null)
				m_posNdx = new Hashtable();
			else
				m_posNdx.Clear();

			int cnt = 0;

			for(int i = 0; i < s.Length; i++)
			{
				if(s[i] == m_inpChar)
					m_posNdx.Add(cnt, i);

				cnt++;
			}
		}
	}
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

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


Written By
Web Developer
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions