目次
はじめに
.NET付属のDateTimePicker
コントロールは、ルックアンドフィールは非常に優れていますが、ある重要な機能が欠けています。DateTimePicker
コントロールにはnull
値を入力できないのです。そのため、DateTimePicker
コントロールをデータセットにバインドすると問題が起こります。データベース内では、null
を許容するDateTime
値がよく使われるからです。この問題を回避するためのソリューションがインターネットでいくつか見つかりましたが、いずれも少々問題がありました。
- Yet another DateTime...Slicker:Nils Jonssonが作成したこのコントロールは、
DateTimePicker
のCheckBox
を使用してnull
値を示します。私はこのソリューションに賛成しません。TextBox
などと一貫した動作にするべきだと思うからです。TextBox
内にnull
値を表示するときには、それがnull
であることをCheckBox
で示したりはしません。ただ何も表示しないだけです。 - Nullable DateTimePicker:Pham Minh Triが作成したこのコントロールは、別の方法を採用し、
null
値のときは空のDateTimePicker
を表示します。この方が良いとは思いますが、このコントロールにはいくつか問題があり、思いどおりの動作をしませんでした。このコントロールをTabPage
に配置してデータ連結を行おうとしたところ、例外が発生してしまいました。このコントロールをnull
値で初期化すると、必ず次の例外が発生しました。"An unhandled exception of type 'System.ComponentModel.Win32Exception' occurred in system.windows.forms.dll." 有効なDateTime
値で初期化したときは、問題なく動作しました。 - Nullable DateTimePicker:これはAlexander Shirshovが作成したコントロールです。Pham Minh Triのコントロールによく似ていますが、こちらの方が問題は少ないと言えます。しかし、
TabControl
上で使用するとまったく同じ例外が発生しました。
つまり、ルックアンドフィールは良くてもTabPage
上で動作しないコントロールしかないということです。しかし、私はたいていのコントロールをTabPage
に配置して使用するので、これらのコントロールをTabPage
上で動作させる必要がありました。そこで、返された例外を詳しく調べるところから着手しました。調べるうちに、初期化時にDateTimePicker.Format
プロパティをCustomFormat
に変更していることに関係があることがわかってきました。さらに、Brett Zimmerannによる次のような説明を見つけました。
「null日付から非null日付へ、またはその逆へと切り替えたときは、DateTimePicker.Formatが変更されている。Formatの値を変更すると、コンポーネントが通常は問題なしという通知を生成する。しかし、この場合はコントロールがまだ表示されていないので、Formatの通知が内部的に失敗してしまう。」
そこで、これらのNullableDateTimePicker
コントロールの動作に基づきつつ、問題が発生しないような独自のNullableDateTimePicker
コントロールを作成することにしました。
ソリューション
このNullableDateTimePicker
コントロールはDateTimePicker
コントロールを継承し、DateTimePicker
の良いところをすべて受け継いでいます。基本となるDateTimePicker
のFormat
プロパティは常にDateTimePickerFormat.Custom
に設定し、変更しません(コントロールをTabPage
上で使用したときの例外を避けるため)。オリジナルのDateTimPicker
と同様にフォーマットを変更できるようにするために、このNullableDateTimePicker
では、DateTimePicker
のFormat
プロパティおよびCustomFormat
プロパティにまつわるすべてのものをエミュレートする必要があります。このコントロールの具体的な実装を次に示します。
実装
まず、NullableDateTimePicker
で使用されるさまざまな値を格納するためのプライベートフィールドをいくつか新しく定義します。
// true, when no date shall be displayed (empty DateTimePicker) private bool _isNull; // If _isNull = true, this value is shown in the DTP private string _nullValue; // The format of the DateTimePicker control private DateTimePickerFormat _format = DateTimePickerFormat.Long; // The custom format of the DateTimePicker control private string _customFormat; // The format of the DateTimePicker control as string private string _formatAsString;
フォーマット
前にも述べたとおり、DateTimePicker
からはFormat
プロパティとCustomFormat
プロパティを使用できません。そのため、これらに代わる新しいプロパティを実装する必要があります。
public new String CustomFormat { get { return _customFormat; } set { _customFormat = value; } } public new DateTimePickerFormat Format { get { return _format; } set { _format = value; SetFormat(); OnFormatChanged(EventArgs.Empty); } }
Format
プロパティのセッターを呼び出すと、新しいDateTimePickerFormat
の値が_value
に格納されます。ここで、NullableDateTimePicker
のフォーマットをこの新しいフォーマットに変更する必要があります。これはSetFormat()
メソッド内で行います。
親のDateTimePicker
のFormat
プロパティは常にDateTimePickerFormat.Custom
に設定されているので、独自クラスのFormat
値を、このフォーマットを表す文字列にマッピングする必要があります。それにより、この文字列を基になるDateTimePicker
コントロールのCustomFormat
として設定することができます。しかし、DateTimePickerFormat
の値を対応する文字列表現にマッピングするにはどうすればいいでしょうか。そのためには、現在のCultureInfo
を取得する必要があります。このCultureInfo
から、各種のDateTimePickerFormat
に対応する正しいフォーマット文字列を返すDateTimeFormatInfo
を取得します。
private void SetFormat() { CultureInfo ci = Thread.CurrentThread.CurrentCulture; DateTimeFormatInfo dtf = ci.DateTimeFormat; switch (_format) { case DateTimePickerFormat.Long: FormatAsString = dtf.LongDatePattern; break; case DateTimePickerFormat.Short: FormatAsString = dtf.ShortDatePattern; break; case DateTimePickerFormat.Time: FormatAsString = dtf.ShortTimePattern; break; case DateTimePickerFormat.Custom: FormatAsString = this.CustomFormat; break; } }
上記のメソッドでは、フォーマットの文字列表現をFormatAsString
プロパティに割り当てています。これは本稿で作成しているコントロールのプライベートプロパティです。
private string FormatAsString { get { return _formatAsString; } set { _formatAsString = value; base.CustomFormat = value; } }
FormatAsString
プロパティのセッターでは、フォーマット文字列をbase.CustomFormat
に割り当てて、最終的に親DateTimePicker
クラスのフォーマットを変更しています。
値
ここまでのコードでは、フォーマットの問題を同名の新規プロパティの陰に隠しました。したがって、このコントロールのユーザー側から見れば、これまでと何も変わっていません。次は、さらに一歩進んで、DateTime
値だけでなくnull
値も設定できるように新しいValue
プロパティを実装します。Value
プロパティのゲッターは、基本コントロールのValue
をそのまま返すか、コントロールがnull
値を表示している場合はnull
を返します。セッターは、値がnull
またはDBNull.Value
の場合は、SetToNullValue()
を呼び出してコントロールをnull
に設定します。値がDateTime
値の場合は、基本コントロールのValue
を設定します。さらに、DateTime
値を正しく表示するためにSetToDateTimeFormat()
を呼び出します。
public new Object Value { get { if (_isNull) return null; else return base.Value; } set { if (value == null || value == DBNull.Value) { SetToNullValue(); } else { SetToDateTimeValue(); base.Value = (DateTime)value; } } }
コントロールがそれまでnull
を表示していて、次にDateTime
値を表示しなければならなくなった場合、SetToDateTimeValue()
メソッドは、SetFormat()
を呼び出すことでフォーマットを現在使用中のDateTimePickerFormat
に設定し、その後にOnValueChanged()
を呼び出してValueChanged
イベントを発生させます。
private void SetToDateTimeValue() { if (_isNull) { SetFormat(); _isNull = false; base.OnValueChanged(new EventArgs()); } }
SetToNullValue()
メソッドは、コントロール内にDateTime
値の代わりにNullValue
を表示します。NullValue
は、開発者が設定できる文字列プロパティです。したがって、null
の場合に空のDateTimePicker
を表示することも、決まった文字列(「<Select date please>」など)を自由に定めて表示することもできます。
private void SetToNullValue() { _isNull = true; base.CustomFormat = (_nullValue == null || _nullValue == String.Empty) ? " " : "'" + NullValue + "'"; } public String NullValue { get { return _nullValue; } set { _nullValue = value; } }
イベント
DateTimePicker
が現在null
値を表示していて、ユーザーがこのコントロールのドロップダウンリストからDateTime
値を選択したときは、Format
を適切なDateTimePickerFormat
に戻す必要があります。
protected override void OnCloseUp(EventArgs e) { if (Control.MouseButtons == MouseButtons.None && _isNull) { SetToDateTimeValue(); _isNull = false; } base.OnCloseUp (e); }
ここまでのコードでは、ユーザーがDateTimePicker
の値をnull
に設定するという可能性を考慮していません。データ連結では正常に動作しますが、問題はユーザー操作のときです。今回のDateTimePicker
では、Deleteキーが押されたときにnull
を受け付けるようにします。そのためには、OnKeyUp
イベントをオーバーライドして、Value
をNullValue
に設定します。
protected override void OnKeyUp(KeyEventArgs e) { if (e.KeyCode == Keys.Delete) { this.Value = _dateTimeNullValue; OnValueChanged(EventArgs.Empty); } base.OnKeyUp(e); }
コンストラクタ
最後にコンストラクタを実装します。コンストラクタでは、基本クラスのFormat
プロパティをDateTimePickerFormat.Custom
に設定し、今回作成しているコントロールのFormat
プロパティをDateTimePickerFormat.Long
に設定します(Long
はこのDateTimePicker
コントロールの既定値です)。
public NullableDateTimePicker() : base() { base.Format = DateTimePickerFormat.Custom; NullValue = " "; this.Format = DateTimePickerFormat.Long; }
使い方
このコントロールの使い方は、.NETのDateTimePicker
と同じです。唯一の違いは、Value
プロパティへのアクセス方法です。新しいコントロールでは、Value
プロパティのデータ型がDateTime
からObject
になっています。したがって、値をDateTime
型にキャストする必要があります(ただし、その前にnull
かどうかをテストしてください)。
NullableDateTimePicker _dtp = new NullableDateTimePicker(); _dtp.NullValue = "Select Date"; // Set the null value of the // control (default is an empty string) // use of the NullableDateTimePicker without databinding: DateTime _date = new DateTime(); if (_dtp.Value != null) _date = (DateTime)_dtp.Value; // use of the NullableDateTimePicker with databinding (Bind it to // a DataSet): DataSet _ds = new DataSet(); DataTable _dt = _ds.Tables.Add("Table"); _dt.Columns.Add("DateTimeColumn", typeof(DateTime)); _dt.Columns[0].AllowDBNull = true; _dtp.DataBindings.Add("Value", _ds, "Table.DateTimeColumn");
CSLA.NETユーザーへの注意
私は、マルチ階層アプリケーションを開発するときにはCSLA.NETというオープンソースフレームワークを使用しています。CSLA.NETには、SmartDate
と呼ばれる独自のDateTime
型があります。SmartDate
は、DBNull.Value
をDateTime.MinValue
またはDateTime.MaxValue
に変換します。そのため、CSLA.NETで作業しているときは、上記のNullableDateTimePicker
は役に立ちません。SmartDate
クラスを扱っているときは、SmartDate
のDateTime
値がDateTime.MinValue
またはDateTime.MaxValue
の際に、DateTimePicker
がNullValue
を表示しなければならないからです。
そこで、この状況に対処する別のNullableDateTimePicker
を作成してみました。こちらの詳細は私の個人的ブログに書いてあります。興味のある方は「nullable DateTimePicker for CSLA.NET」をお読みください。