Another one? Yep, I'm afraid it is just that: another date time picker control. I was using the default datetimepicker provided in the .net framework and liked a lot of its default features, but, like others, came upon some issues that made the dayly use of it unacceptable for the target audience.
The control itself is very simple, the .net 2.0 maskededitbox does most of the work, but the way the format is handled makes sure the input can take any part entered (eg only month and year, or just a day, or just a time). One of the things I liked about the standard control was the 'block' functionality to quickly switch between day,month, etc, so that feature had to added too. Of course the default calendar dropdown is also included.
While typing and filling a block (eg, the day), the next block (eg, the month) is selected automatically. Up and down arrows work as in the standard control: increasing or decreasing the selected block, but unlike the standard counterpart, the days are not forced to a value range (in this version, the range isn't forced at all...). Also didn't like to use a checkbox to indicate null as in the standard control, so null now is simply no input.
namespace DataLayer.Controls
{
using System;
using System.Collections.Generic;
using System.Text;
using System.ComponentModel;
using System.Windows.Forms;
using System.Drawing;
using System.Globalization;
[DefaultBindingProperty("BindingValue")]
[DefaultEvent("BindingValueChanged")]
[ToolboxBitmap(typeof(System.Windows.Forms.DateTimePicker))]    
public partial class DateTimePicker:UserControl
{
public DateTimePicker()
{
InitializeComponent();
ResetFormat();
}
#region Formatting
string DefaultFormat;
public class MaskFormatInfo
{
public
readonly string Format;
public readonly string Mask;
public readonly bool ContainsTime;
public readonly bool ContainsDate;
public MaskFormatInfo(
string DateTimeFormat)
{
const string dtchars = "dMyHhms"
char[] fchars =
new char[DateTimeFormat.Length * 2], mchars =
new char[fchars.Length];
int j = 0, index;
for (
int i = 0; i < DateTimeFormat.Length; i++)
{
char c = DateTimeFormat[i];
if (c ==
'\\' && i < DateTimeFormat.Length - 1)
{
fchars[j] = mchars[j++] = c;
}
else if ((index = dtchars.IndexOf(c)) != -1)
{
//ensure double entries to enable full user input
for (
int k = 0; k < 2; k++)
{
fchars[j] = c;
mchars[j++] = '0';
}
if (i < DateTimeFormat.Length - 1 && DateTimeFormat[i + 1] == c) i++;
if (index > 2)
ContainsTime = true;
else
ContainsDate = true;
continue;
}
fchars[j] = mchars[j++] = c;
}
this.Format = new string(fchars, 0, j);
this.Mask = new string(mchars, 0, j);    
}
}
private
string format ;
public string Format
{
get {
return format; }
set{
if (
string.IsNullOrEmpty(
value))
{
ResetFormat();
return;
}
MaskFormatInfo mf =
new MaskFormatInfo(
value);
format = mf.Format;
msk.Mask = mf.Mask;
pnlButton.Visible = showdropdown && mf.ContainsDate;
} }
bool ShouldSerializeFormat()
{return !UsesDefaultFormat;
}
public void ResetFormat()
{DateTimeFormatInfo formatinfo = DateTimeFormatInfo.CurrentInfo;
string format = null;
if (showdate)
format = formatinfo.ShortDatePattern;
if (showtime)
{
if (showdate) format += dtseperator;
format += formatinfo.ShortTimePattern;
}
Format = format;
DefaultFormat = this.format;}
public
bool UsesDefaultFormat
{
get
{
return format == DefaultFormat;
} }private
string dtseperator=
" "
[
DefaultValue(
" ")]
[
Description(
"This seperator used between date and time if both are shown. If a custom Format is set, this property is ignored")]
public string SeperatorDateTime
{
get {
return dtseperator; }
set{
bool hasdefault = UsesDefaultFormat;
if (
string.IsNullOrEmpty(
value))
dtseperator = " "
else
dtseperator = value;
if (hasdefault) ResetFormat();
} }private
bool showdate=
true;
[
DefaultValue(
true)]
[
Description(
"Gets or sets if the Date part is shown. If a custom Format is set, this property is ignored")]
public bool ShowDate
{
get {
return showdate; }
set{
bool hasdefault = UsesDefaultFormat;
showdate = value;
if (!value && !showtime) showtime = true;
if (hasdefault) ResetFormat();
} }
private
bool showtime =
true;
[
DefaultValue(
true)]
[
Description(
"Gets or sets if the Time part is shown. If a custom Format is set, this property is ignored")]
public bool ShowTime
{
get {
return showtime; }
set{
bool hasdefault = UsesDefaultFormat;
showtime = value;
if (!value && !showdate) showdate= true;
if (hasdefault) ResetFormat();
} }[
DesignerSerializationVisibility(
DesignerSerializationVisibility.Hidden)]
[
Browsable(
false)]
public override string Text
{
get
{
return msk.Text;
}
set
{msk.Text = value;
} }#endregion
#region Value
private
DateTime? value;
[
Browsable(
false)]
[
DesignerSerializationVisibility(
DesignerSerializationVisibility.Hidden)]
public DateTime Value
{
get
{
if (value ==
null)
{
if (IsValid || DesignMode)
//if IsValid: null allowed, return minvalue to prevent exceptionsreturn minvalue;
elsethrow new Exception("No valid value entered");
}
return value.Value; }
set
{this.value = value;
this.msk.Text = value.ToString(format);
SetValid();
OnValueChanged();                
} }
public event EventHandler ValueChanged;protected
virtual void OnValueChanged()
{
OnBindingValueChanged();
if (ValueChanged !=
null)
ValueChanged(this, EventArgs.Empty);
}
//[Browsable(false)]
[
DesignerSerializationVisibility(
DesignerSerializationVisibility.Hidden)]
[
TypeConverter(
typeof(
DateTimePicker.
BindingValueConverter))]
[
Editor(
"System.ComponentModel.Design.DateTimeEditor",
"System.Drawing.Design.UITypeEditor")]
//,typeof(UITypeEditor))]
public DateTime? BindingValue
{
get
{
return value;
}
set
{if (
this.value ==
value)
return;
if (
value !=
null)
Value = value.Value;
else
{
this.value = null;
msk.Text = null;
SetValid();
OnBindingValueChanged();
}
} }
bool ShouldSerializeBindingValue()
{return value != null;
}
public void ResetBindingValue()
{this.BindingValue = null;
}public
event EventHandler BindingValueChanged;
protected virtual void OnBindingValueChanged()
{
if (BindingValueChanged !=
null)
BindingValueChanged(this, EventArgs.Empty);
}bool allownull;
[
DefaultValue(
false)]
public bool AllowNull
{
get
{
return allownull;
}
set
{allownull = value;
SetValid();
} }[
DefaultValue(
false)]
public bool IsNull
{
get { return value==null; }
}#endregion
#region Designer Generated
private MaskedTextBox msk;
private Panel pnlButton;
private
void InitializeComponent()
{
this.msk = new System.Windows.Forms.MaskedTextBox();
this.pnlButton = new System.Windows.Forms.Panel();
this.SuspendLayout();
//
// msk
//
this.msk.Dock = DockStyle.Fill;
this.msk.BorderStyle = System.Windows.Forms.BorderStyle.None;
this.msk.HidePromptOnLeave = true;
this.msk.Location = new System.Drawing.Point(3, 2);
this.msk.Mask = "00/00/0000   00:00"
this.msk.Name = "msk"
this.msk.PromptChar = ' ';
this.msk.Size = new System.Drawing.Size(104, 13);
this.msk.TabIndex = 0;
this.msk.TextAlign = HorizontalAlignment.Center;
this.msk.ValidatingType = typeof(System.DateTime);
this.msk.Validating += new System.ComponentModel.CancelEventHandler(this.msk_Validating);
this.msk.KeyUp += new System.Windows.Forms.KeyEventHandler(this.msk_KeyUp);
this.msk.Click += new System.EventHandler(this.msk_Click);
//
// pnlButton
//
this.pnlButton.BackColor = System.Drawing.SystemColors.Control;
this.pnlButton.Dock = System.Windows.Forms.DockStyle.Right;
this.pnlButton.Location = new System.Drawing.Point(98, 0);
this.pnlButton.Name = "pnlButton"
this.pnlButton.Size = new System.Drawing.Size(19, 18);
this.pnlButton.TabIndex = 1;
this.pnlButton.Click += new System.EventHandler(this.pnlButton_Click);
this.pnlButton.Paint += new System.Windows.Forms.PaintEventHandler(this.pnlButton_Paint);
//
// DateTimePicker
//
this.BackColor = System.Drawing.Color.White;
this.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
this.Controls.Add(this.msk);
this.Controls.Add(this.pnlButton);            
this.Name = "DateTimePicker"
this.Padding = new System.Windows.Forms.Padding(3, 0, 0, 0);
this.Size = new System.Drawing.Size(117, 18);
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
#region Calendar dropdown
private
void pnlButton_Paint(
object sender,
PaintEventArgs e)
{
ControlPaint.DrawComboButton(e.Graphics, pnlButton.ClientRectangle,
ButtonState.Normal);
}private
void pnlButton_Click(
object sender,
EventArgs e)
{
ShowCalendar();
}
void ShowCalendar()
{
new MontViewer(this).Show(this);
}private
bool showdropdown=
true;
[
DefaultValue(
true)]
[
Description(
"Gets or sets if the dropdown button is shown. If the format contains no date part, the button is always hidden")]
public bool ShowDropDownButton
{
get {
return showdropdown; }
set{
if (showdropdown == value) return;
showdropdown = value;
Format = format;
} }
///
<summary>
/// Drop down calendar form for the DateTimePicker
/// </summary>
class MontViewer:
Form{
new
DateTimePicker Owner;
MonthCalendar mc =
new MonthCalendar();
public MontViewer(
DateTimePicker Owner)
{
this.Owner = Owner;
this.FormBorderStyle = FormBorderStyle.None;
this.ShowInTaskbar = false;
this.KeyPreview = true;
this.Size = mc.Size;
Controls.Add(mc);
if (!Owner.IsNull)
mc.SelectionStart = mc.SelectionEnd = Owner.Value;
mc.DateSelected +=
new DateRangeEventHandler(mc_DateSelected);
Rectangle r = Owner.RectangleToScreen(Owner.ClientRectangle);
this.StartPosition = FormStartPosition.Manual;
this.Location = new Point(r.Right - Width, r.Bottom);
}
protected
override void OnKeyDown(
KeyEventArgs e)
{
if (e.KeyCode ==
Keys.Escape)
Close();
else
if (e.KeyCode ==
Keys.Enter || e.KeyCode ==
Keys.Space)
EnterDate();
else
base.OnKeyDown(e);
}
void EnterDate()
{DateTime res = mc.SelectionStart;
if (!Owner.IsNull)
//make sure time part is not overwritten
res = res.Add(Owner.Value.TimeOfDay);
Owner.Value = res;
Close(); }void mc_DateSelected(
object sender,
DateRangeEventArgs e)
{
EnterDate();
}
protected
override void OnDeactivate(
EventArgs e)
{
base.OnDeactivate(e);
Close();
}protected
override void OnClosed(
EventArgs e)
{
Owner.Focus();
base.OnClosed(e);
}
}
#endregion
#region Validation
public
bool IsValid
{
get
{
if (value !=
null)
return InBounds;
if (allownull &&
string.IsNullOrEmpty(text))
return true;
return false;
} }bool InBounds
{
get
{
return value.Value >= minvalue && value.Value <= max;
} }void SetValid()
{
if (DesignMode)
return;
Color c;
if (IsValid)
{
if (IsNull)
c = Color.AntiqueWhite;
else
c = Color.White;
lastException =
null;
}
else
{if (value !=
null && !InBounds)
c = Color.Orange;
else
c = Color.DarkOrange;
}
this.BackColor = c; }Exception lastException;
string text;
private
void msk_Validating(
object sender,
CancelEventArgs e)
{
ValidateInput();
if (blockinvalid && !IsValid)
e.Cancel = true;
}
void ValidateInput()
{
DateTime? value =
null;
text = msk.Text;
if (
string.IsNullOrEmpty(text))
{
}
else{                
try
{
string format =
this.format;
if (!msk.MaskFull)
{
char[] textchars =
new char[format.Length],
formatchars = new char[textchars.Length];
int ci = 0;
for (
int i = 0; i < text.Length; i++)
{
if (
char.IsDigit(text[i]))
{
textchars[ci] = text[i];
formatchars[ci++] = format[i];
}
}
if (ci > 0)
{
//check for single input chars
int samecount = 1;
for (
int i = 1; i <= ci; i++)
{
if (formatchars[i] != formatchars[i - 1])
{
if (samecount == 1)
{
//insert extra
for (
int j = ci++; j >= i; j--)
{
formatchars[j] = formatchars[j - 1];
textchars[j] = textchars[j - 1];
}
textchars[i - 1] =
'0';
i++;
}
samecount = 1;
}
text = new string(textchars, 0, ci);
format = new string(formatchars, 0, ci);
}
elsetext = format = null;
}
if (text !=
null)
{
value =
DateTime.ParseExact(text, format,
null);
if (usecurrentmonth && format.IndexOf(
'M') == -1)
value = value.Value.AddMonths(DateTime.Today.Month - 1);
}
}
catch(
Exception ex)
{
lastException = ex;
value = null;
}
}
if (value !=
null)
this.Value = value.Value;
else if (
this.value !=
null)
{
this.value = null;
OnValueChanged();
}
SetValid();}
private
bool blockinvalid =
false;
[
DefaultValue(
false)]
[
Description(
"Normally invalid input is allowed but indicated as invalid. If this property is set to true, focus is held until the user enters a valid date")]
public bool BlockInvalidInput
{
get { return blockinvalid; }
set { blockinvalid = value; }
}
static
DateTime DefaultMin =
new DateTime(1950, 1, 1);
private DateTime minvalue = DefaultMin;
public DateTime MinDate
{
get {
return minvalue; }
set{
minvalue = value;
SetValid();
}
}
bool ShouldSerializeMinDate()
{return minvalue != DefaultMin;
}
void ResetMinDate()
{MinDate= DefaultMin;
}static
DateTime DefaultMax =
new DateTime(2500, 12, 31);
private DateTime max = DefaultMax;
public DateTime MaxDate
{
get {
return max; }
set{
max = value;
SetValid();
}
}
bool ShouldSerializeMaxDate()
{return max != DefaultMax;
}
void ResetMaxDate()
{MaxDate = DefaultMax;
}
#endregion
#region date block handling
void SelectBlock(
int Offset)
{
int pos = msk.SelectionStart;
string mask = msk.Mask;
int len = 1;
while (
true)
{
if (pos >= mask.Length)
pos = mask.Length - 1;
while (pos > 0 && mask[pos - 1] ==
'0')
--pos;
len = 1;
while (len + pos < mask.Length && mask[len + pos] ==
'0')
len++;
if (Offset > 0)
{
pos += len + 1;
while (pos < mask.Length && mask[pos] !=
'0')
pos++;
Offset--;
}
else if (Offset < 0)
{
while (--pos >= 0 && mask[pos] !=
'0') { }
if (pos <= 0)
{
pos = 0;
break;
}
Offset++;
}
elsebreak;
}msk.Select(pos, len);
}
void IncCurrent(
int diff)
{
SelectBlock();
string text = msk.SelectedText.Trim();
int cur = text.Length == 0 ? 0 :
int.Parse(text);
cur += diff;
if (cur < 0)
cur = 0;
int start = msk.SelectionStart, len = msk.SelectionLength;
msk.SelectedText = cur.ToString(
"d" + len);
msk.Select(start, len);
}
#endregion
#region ui events
protected
override bool ProcessCmdKey(
ref Message msg,
Keys keyData)
{
if (HandleKey(keyData)) return true;
return base.ProcessCmdKey(ref msg, keyData);
}
protected override bool IsInputKey(Keys keyData)
{if (keyData ==
Keys.Left || keyData ==
Keys.Right)
return true;
if (useupdown && (keyData ==
Keys.Up || keyData ==
Keys.Down))
return true;
return base.IsInputKey(keyData);
}bool HandleKey(
Keys k)
{
if (k ==
Keys.F3 || k == (
Keys.Control |
Keys.Down))
{
ShowCalendar();
}
else if (useupdown && (k ==
Keys.Up || k ==
Keys.Down))
{
IncCurrent(k == Keys.Up ? 1 : -1);
}
else if (k ==
Keys.Add)
{
IncCurrent(1);
}
else if (k ==
Keys.Subtract)
{
IncCurrent(-1);
}
else if (k ==
Keys.Left || k ==
Keys.Right)
{
SelectBlock(k == Keys.Right ? 1 : -1);
}
else if (IsDigit(k) && msk.SelectionLength > 1)
{
clearblock();
return false;
}
else if (k ==
Keys.Delete || k==
Keys.Back)
{
clearblock();
}
else if (k == (
Keys.T |
Keys.Control))
SetToday();
else
if (k ==
Keys.Enter)
{
Validate();
return false;
}
else if (k ==
Keys.End)
{
msk.SelectionStart = format.Length - 1;
SelectBlock();
}
else if (k ==
Keys.Home)
{
msk.SelectionStart = 0;
SelectBlock();
}
elsereturn false;
return true;
}void clearblock()
{
int i = msk.SelectionStart;
msk.SelectedText = new string(' ', msk.SelectionLength);
msk.SelectionStart = i;
}public
void SetToday()
{
Value = DateTime.Today;
msk.SelectionStart = 0;
SelectBlock();
}
private
void msk_Click(
object sender,
EventArgs e)
{
SelectBlock();
}
private
void msk_KeyUp(
object sender,
KeyEventArgs e)
{
if (msk.SelectionLength == 0 && IsDigit(e.KeyCode))
{
int i = msk.SelectionStart;
if (i < msk.Mask.Length && msk.Mask[i] !=
'0')
{
SelectBlock(1);
}
} }bool IsDigit(
Keys k)
{
if (k >=
Keys.D0 && k <=
Keys.D9)
return true;
return k >=
Keys.NumPad0 && k <=
Keys.NumPad9;
}void SelectBlock()
{
SelectBlock(0);
}
private bool useupdown=true;
[
DefaultValue(
true)]
[
Description(
"When this value is set to true, the up and down arrows are used to increment or decrement the selected value")]
public bool HandleUpDown
{
get { return useupdown; }
set { useupdown = value; }
}private
bool usecurrentmonth =
true;
[
DefaultValue(
true)]
[
Description(
"Specifies that the current month should be used if no month was supplied.")]
public bool UseCurrentMonth
{
get { return usecurrentmonth; }
set { usecurrentmonth = value; }
}
protected
override void OnEnter(
EventArgs e)
{
msk.Select();
msk.SelectionStart = 1;
SelectBlock();
base.OnEnter(e);
}
public
override Color BackColor
{
get
{
return base.BackColor;
}
set
{base.BackColor = value;
msk.BackColor = value;
} }
bool ShouldSerializeBackColor()
{return BackColor != Color.White;
}
public new void ResetBackColor()
{this.BackColor = Color.White;
}#endregion
#region Designer
class
BindingValueConverter :
DateTimeConverter{
public
override object ConvertFrom(
ITypeDescriptorContext context,
CultureInfo culture,
object value)
{                
if (value
is string)
{
string s = value as string;
if (string.IsNullOrEmpty(s)) return (DateTime?)null;                  
}
return base.ConvertFrom(context, culture, value); } }#endregion
}
}
. . .
PS. the main reasons the default didn't work for our scenario:
- The day's maximum was forced by the chosen month. Now for the default American notation where the month is entered before the day, this doesn't pose a problem. But since our regional settings fill the day first, the number entered was dependant on the month set in a previous step. For example: if the date was set to the fourth of april and the user starts typing a new date over it, the day cannot go beyond 30. Now to enter may 31st, the user would have to change the month field first and then go back to the day, which is not acceptable for quick data entry. Worse than that: the day would be set to 30 and the user might not even notice.
-The block did not switch to the next part when entering. When entering part of the date (eg. the day), the cursor would not jump to the next part (eg, the month) when filled. The user would have to press cursor keys first to switch. Again not acceptable for quick data entry. Now this is perhaps something that is solvable within the standard control, but didn't look into it that far, since the decision to create a new one had already been taken by that time ;-)
-No null binding property. This is of course a very simple one to overcome since it could be added to an inherited version, but to mention it none the less: a nullable field should be bindable directly to a property. If that field can be null, the property would have to be able to take null. Since .net 2.0 has that great new nullable syntax (DateTime?), that was easily enough done :D
-No Checkbox for null: this might be just personal preference, but for me null is nothing in the control and not changing a checkbox first. Besides that I had the feeling it would be confusing to use. Users might think that the date value was stored, but just not active or something of the kind.
-Take strange dates. Since the source can be unknown data, the control would have to be able take faulty text or strange input. The control would of course have to indicate that it is wrong, but the user shouldn't be stuck until he chooses. As long as he chooses before the data is stored to the database.
--------update 20-7-7---------
Some minor changes:
-Culture dependant formatting. The default format is now set to whatever the culture's default setting is.
-ShowTime and ShowDate properties. Using the default formatting, the date or time part can be hidden with these properties without fixing the format.
-Made the bindingvalue editable in designer instead of value, so the start value can be reset to null