HotDog's Blog

Hotdog (Robert Verpalen) about C# and vb.net

This blog hosted by:
http://blogs.vbcity.com      
  Home :: Syndication  :: Login

MarApril 2007May
SMTWTFS
25262728293031
1234567
891011121314
15161718192021
22232425262728
293012345

Articles

Archives

Topics

CONTACT

Fun but useful linkies

General

VS 2005

Wolfenstein ET

Wednesday, April 04, 2007 #

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.

 

Code CopyHideScrollFull

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 exceptions
return minvalue;
else
throw 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;
}
else
samecount++;
}
text = new string(textchars, 0, ci);
format = new string(formatchars, 0, ci);
}
else
text = 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++;
}
else
break;
}
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();
}
else
return 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
posted @ 3:25 AM | Feedback (4)