
目次
はじめに
.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」をお読みください。
