WPF のコントロールには日付入力用の DatePicker はありますが、日時を入力する DateTimePiker が無いので、Windows フォーム用のものをラッパーを通して使ったりしています。前回の記事で書いた ValidationViewModelBase を利用すると、すっきり書けそうな感じがしたので、DateTimePicker なユーザーコントロールを書いてみました。動作テスト用に作ったアプリは、こんな感じになります。
必須項目が入力されていないときは、こんな感じになります。エラーメッセージはツールチップに表示させています。
数字の指定範囲内にない文字が入力されたときのもの。
そして、アトリビュート指定ではない、業務ロジックでのエラー発生の例。
DateTimePicker のプロパティ(依存関係プロパティ)の仕様は次のとおりです。
DateTimePicker
日付及び時刻の設定を行うユーザーコントロール
プロパティ
-
DateTime DateTimeValue
- 日時を取得または設定します。
-
DatePickerFormat SelectedDateFormat
- 選択した日付を表示するために使用される形式(Long または Short)を取得または設定します。
-
bool IsDisplayedWeek
- 曜日表示の有無を取得または設定します。(デフォルト値: true)
-
string DateTimeString
- 日時の文字列を取得します。(表示形式はスレッドのカルチャに依存します。)
ソースコード一式と動作確認用の WPF プロジェクト(Visual Studio Express 2012 for Windows Desktop で作成しています。)の zip ファイルをダウンロードできます。
まず、前提として、利用する .NET Framework のバージョンは 4 とします。
プロジェクトに次の参照を追加します(ValidationViewModelBase は前回の記事で作成したDLL(もちろん DLL 参照ではなくてプロジェクト中にコードを書いても OK 😉 ))。
- System.ComponentModel.DataAnnotations
- ValidationViewModelBase
コードは次のとおりです。
まずはプロジェクトにユーザーコントロール用のフォルダ「UserControls」を作成します。
時刻入力用のテキストボックスにフォーカスが来た時に、既存テキストを全選択状態にしたいので、最初に、ビヘイビア用のフォルダ「Behaviors」を「UserControls」の下に作り、当該フォルダに「TextBoxHelper」クラスを作ります。
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Threading;
namespace DateTimePicker.UserControls.Behaviors
{
internal class TextBoxHelper
{
/// <summary>
/// フォーカス取得時のテキスト全選択の有無を設定します(true で全選択)。
/// </summary>
public static readonly DependencyProperty IsFocusSelectProperty = DependencyProperty.RegisterAttached(
"IsFocusSelect", typeof(bool), typeof(TextBoxHelper),
new UIPropertyMetadata(false, IsFocusSelectChanged));
private static void IsFocusSelectChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
var textBox = (TextBox)sender;
if (textBox == null) return;
// 設定値を見てイベントを登録・削除
var newValue = (bool)e.NewValue;
var oldValue = (bool)e.OldValue;
if (newValue == oldValue) return;
if (oldValue)
{
textBox.GotFocus -= textBox_GotFocus;
}
if (newValue)
{
textBox.GotFocus += textBox_GotFocus;
}
}
private static void textBox_GotFocus(object sender, RoutedEventArgs e)
{
var textBox = (TextBox)e.OriginalSource;
if (textBox == null) return;
// 非同期で全選択処理を実行する
Action selectAction = textBox.SelectAll;
Dispatcher.CurrentDispatcher.BeginInvoke(selectAction, DispatcherPriority.Background);
}
[AttachedPropertyBrowsableForType(typeof(TextBox))]
public static bool GetIsFocusSelect(DependencyObject obj)
{
return (bool)obj.GetValue(IsFocusSelectProperty);
}
[AttachedPropertyBrowsableForType(typeof(TextBox))]
public static void SetIsFocusSelect(DependencyObject obj, bool value)
{
obj.SetValue(IsFocusSelectProperty, value);
}
}
}
次に「UserControls」の下に「ViewModels」フォルダを作成し、当該フォルダに「DateTimePickerViewModel」クラスを作ります。
using System;
using System.ComponentModel.DataAnnotations;
using MakCraft.ViewModels;
namespace DateTimePicker.UserControls.ViewModels
{
public class DateTimePickerViewModel : ValidationViewModelBase
{
private string _hour;
private string _minute;
private string _second;
public DateTimePickerViewModel()
{
setTime(DateTime.Now);
}
// DateTimePicker の依存関係プロパティとのバインドは、コードビハインド側で行なっている
[Required(ErrorMessage = "この項目は必須項目です。")]
[Range(0, 23, ErrorMessage = "0 から 23 の数字を入力してください。")]
public string Hour
{
get { return _hour; }
set
{
var propertyName = PropertyHelper.GetName(() => Hour);
base.RemoveItemValidationError(propertyName); // 項目編集以前のエラーメッセージをクリア
_hour = value;
if (!base.IsPropertyAnnotationError(propertyName)) // Hour のデータ検証を確認
{
base.RaisePropertyChanged(propertyName);
}
}
}
// DateTimePicker の依存関係プロパティとのバインドは、コードビハインド側で行なっている
[Required(ErrorMessage = "この項目は必須項目です。")]
[Range(0, 59, ErrorMessage = "0 から 59 の数字を入力してください。")]
public string Minute
{
get { return _minute; }
set
{
var propertyName = PropertyHelper.GetName(() => Minute);
base.RemoveItemValidationError(propertyName);
_minute = value;
if (!base.IsPropertyAnnotationError(propertyName)) // Minute のデータ検証を確認
{
base.RaisePropertyChanged(propertyName);
}
}
}
// DateTimePicker の依存関係プロパティとのバインドは、コードビハインド側で行なっている
[Required(ErrorMessage = "この項目は必須項目です。")]
[Range(0, 59, ErrorMessage = "0 から 59 の数字を入力してください。")]
public string Second
{
get { return _second; }
set
{
var propertyName = PropertyHelper.GetName(() => Second);
base.RemoveItemValidationError(propertyName);
_second = value;
if (!base.IsPropertyAnnotationError(propertyName)) // Second のデータ検証を確認
{
base.RaisePropertyChanged(propertyName);
}
}
}
private void setTime(DateTime dateTime)
{
Hour = dateTime.ToString("HH");
Minute = dateTime.ToString("mm");
Second = dateTime.ToString("ss");
}
}
}
次に「UserControls」の下にユーザーコントロール「DateTimePicker」を作ります(「UserControls」を右クリック、「追加」、「ユーザーコントロール」を選択)。
<UserControl x:Class="DateTimePicker.UserControls.DateTimePicker"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:bhvr="clr-namespace:DateTimePicker.UserControls.Behaviors"
xmlns:viewModel="clr-namespace:DateTimePicker.UserControls.ViewModels"
mc:Ignorable="d" >
<UserControl.Resources>
<viewModel:DateTimePickerViewModel x:Key="DateTimePickerViewModel" />
<Style TargetType="TextBox" x:Key="ToolTipErrorStyle">
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="True">
<Setter Property="ToolTip">
<Setter.Value>
<Binding RelativeSource="{RelativeSource Self}"
Path="(Validation.Errors)[0].ErrorContent" />
</Setter.Value>
</Setter>
</Trigger>
</Style.Triggers>
</Style>
</UserControl.Resources>
<Border BorderBrush="Aqua" BorderThickness="2">
<StackPanel Name="BasePanel" Orientation="Horizontal" Margin="2"
DataContext="{DynamicResource DateTimePickerViewModel}">
<DatePicker Name="DatePicker" SelectedDateFormat="Long" FontSize="12" MinWidth="132" />
<TextBlock Name="DisplayWeek" FontSize="12" VerticalAlignment="Center" />
<TextBox Text="{Binding Path=Hour, ValidatesOnDataErrors=True}" FontSize="12" MinWidth="25"
Style="{StaticResource ToolTipErrorStyle}" bhvr:TextBoxHelper.IsFocusSelect="True"
VerticalAlignment="Center" VerticalContentAlignment="Center" />
<TextBlock Text=":" FontSize="12" VerticalAlignment="Center" />
<TextBox Text="{Binding Path=Minute, ValidatesOnDataErrors=True}" FontSize="12" MinWidth="25"
Style="{StaticResource ToolTipErrorStyle}" bhvr:TextBoxHelper.IsFocusSelect="True"
VerticalAlignment="Center" VerticalContentAlignment="Center" />
<TextBlock Text=":" FontSize="12" VerticalAlignment="Center" />
<TextBox Text="{Binding Path=Second, ValidatesOnDataErrors=True}" FontSize="12" MinWidth="25"
Style="{StaticResource ToolTipErrorStyle}" bhvr:TextBoxHelper.IsFocusSelect="True"
VerticalAlignment="Center" VerticalContentAlignment="Center" />
</StackPanel>
</Border>
</UserControl>
最後に「DateTimePicker.xaml.cs」を開いて、依存関係プロパティやバインドを書きます。
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using DateTimePicker.UserControls.ViewModels;
namespace DateTimePicker.UserControls
{
/// <summary>
/// DateTimePicker.xaml の相互作用ロジック
/// </summary>
public partial class DateTimePicker : UserControl
{
public DateTimePicker()
{
InitializeComponent();
// DatePicker へのバインド
var datePickerBind = new Binding("SelectedDate");
datePickerBind.Source = DatePicker;
datePickerBind.Mode = BindingMode.TwoWay;
this.SetBinding(SelectedDateProperty, datePickerBind);
// ViewModel の Hour へのバインド
var hourBind = new Binding("Hour");
hourBind.Source = (DateTimePickerViewModel)BasePanel.DataContext;
hourBind.Mode = BindingMode.TwoWay;
this.SetBinding(InputtedHourProperty, hourBind);
// ViewModel の Minute へのバインド
var minuteBind = new Binding("Minute");
minuteBind.Source = (DateTimePickerViewModel)BasePanel.DataContext;
minuteBind.Mode = BindingMode.TwoWay;
this.SetBinding(InputtedMinuteProperty, minuteBind);
// ViewModel の Second へのバインド
var secondBind = new Binding("Second");
secondBind.Source = (DateTimePickerViewModel)BasePanel.DataContext;
secondBind.Mode = BindingMode.TwoWay;
this.SetBinding(InputtedSecondProperty, secondBind);
DatePicker.SelectedDate = DateTime.Now;
}
public static readonly DependencyProperty DateTimeValueProperty = DependencyProperty.Register(
"DateTimeValue", typeof(DateTime), typeof(DateTimePicker),
new FrameworkPropertyMetadata // メタデータ
{
DefaultValue = DateTime.Now, // デフォルト値
BindsTwoWayByDefault = true,
PropertyChangedCallback = new PropertyChangedCallback(onDateTimeValueChanged)
});
// 依存関係プロパティのラッパー
public DateTime DateTimeValue
{
get { return (DateTime)GetValue(DateTimeValueProperty); }
set { SetValue(DateTimeValueProperty, value); }
}
private static void onDateTimeValueChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
var userControl = (DateTimePicker)sender;
if ((DateTime)e.OldValue != (DateTime)e.NewValue)
{
var newDateTime = (DateTime)e.NewValue;
userControl.InputtedHour = newDateTime.ToString("HH");
userControl.InputtedMinute = newDateTime.ToString("mm");
userControl.InputtedSecond = newDateTime.ToString("ss");
userControl.SelectedDate = newDateTime;
userControl.DateTimeString = newDateTime.ToString("F");
}
}
public static readonly DependencyProperty SelectedDateFormatProperty = DependencyProperty.Register(
"SelectedDateFormat", typeof(DatePickerFormat), typeof(DateTimePicker),
new FrameworkPropertyMetadata // メタデータ
{
DefaultValue = DatePickerFormat.Long,
PropertyChangedCallback = new PropertyChangedCallback(onSelectedDateFormatChanged)
});
// 依存関係プロパティのラッパー
public DatePickerFormat SelectedDateFormat
{
get { return (DatePickerFormat)GetValue(SelectedDateFormatProperty); }
set { SetValue(SelectedDateFormatProperty, value); }
}
private static void onSelectedDateFormatChanged(DependencyObject sender,
DependencyPropertyChangedEventArgs e)
{
var userControl = (DateTimePicker)sender;
userControl.DatePicker.SelectedDateFormat = (DatePickerFormat)e.NewValue;
}
public static readonly DependencyProperty IsDislayedWeekProperty = DependencyProperty.Register(
"IsDislayedWeek", typeof(bool), typeof(DateTimePicker),
new FrameworkPropertyMetadata // メタデータ
{
DefaultValue = true,
PropertyChangedCallback = new PropertyChangedCallback(onIsDislayedWeekChanged)
});
// 依存関係プロパティのラッパー
public bool IsDislayedWeek
{
get { return (bool)GetValue(IsDislayedWeekProperty); }
set { SetValue(IsDislayedWeekProperty, value); }
}
private static void onIsDislayedWeekChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
var userControl = (DateTimePicker)sender;
setWeek(userControl, userControl.DateTimeValue);
}
public static readonly DependencyProperty DateTimeStringProperty = DependencyProperty.Register(
"DateTimeString", typeof(string), typeof(DateTimePicker),
new FrameworkPropertyMetadata()); // メタデータ
// 依存関係プロパティのラッパー
public string DateTimeString
{
get { return (string)GetValue(DateTimeStringProperty); }
private set { SetValue(DateTimeStringProperty, value); }
}
// 内部処理用の依存関係プロパティ(DatePicker コントロールとバインド)
private static readonly DependencyProperty SelectedDateProperty = DependencyProperty.Register(
"SelectedDate", typeof(DateTime), typeof(DateTimePicker),
new FrameworkPropertyMetadata // メタデータ
{
BindsTwoWayByDefault = true,
PropertyChangedCallback = new PropertyChangedCallback(onSelectedDateChanged)
});
private DateTime SelectedDate
{
get { return (DateTime)GetValue(SelectedDateProperty); }
set { SetValue(SelectedDateProperty, value); }
}
private static void onSelectedDateChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
var userControl = (DateTimePicker)sender;
if ((DateTime)e.OldValue != (DateTime)e.NewValue)
{
var newdate = (DateTime)e.NewValue;
var dateTime = string.Format("{0}/{1}/{2} {3}:{4}:{5}",
newdate.Year, newdate.Month, newdate.Day,
userControl.InputtedHour, userControl.InputtedMinute, userControl.InputtedSecond);
userControl.DateTimeValue = DateTime.Parse(dateTime);
setWeek(userControl, (DateTime)e.NewValue);
}
}
// 画面表示の曜日をセット
private static void setWeek(DateTimePicker userControl, DateTime date)
{
if (userControl.IsDislayedWeek)
{
userControl.DisplayWeek.Text = string.Format("({0})", date.ToString("ddd"));
}
else
{
userControl.DisplayWeek.Text = string.Empty;
}
}
// 内部処理用の依存関係プロパティ(TextBox(Hour) コントロールとバインド)
private static readonly DependencyProperty InputtedHourProperty = DependencyProperty.Register(
"InputtedHour", typeof(string), typeof(DateTimePicker),
new FrameworkPropertyMetadata // メタデータ
{
DefaultValue = "00", // デフォルト値
BindsTwoWayByDefault = true,
PropertyChangedCallback = new PropertyChangedCallback(onInputtedHourChanged)
});
private string InputtedHour
{
get { return (string)GetValue(InputtedHourProperty); }
set { SetValue(InputtedHourProperty, value); }
}
private static void onInputtedHourChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
var userControl = (DateTimePicker)sender;
if ((string)e.OldValue != (string)e.NewValue)
{
var date = userControl.DateTimeValue;
var dateTime = string.Format("{0}/{1}/{2} {3}:{4}:{5}",
date.Year, date.Month, date.Day,
(string)e.NewValue, userControl.InputtedMinute, userControl.InputtedSecond);
userControl.DateTimeValue = DateTime.Parse(dateTime);
}
}
// 内部処理用の依存関係プロパティ(TextBox(Minute) コントロールとバインド)
private static readonly DependencyProperty InputtedMinuteProperty = DependencyProperty.Register(
"InputtedMinute", typeof(string), typeof(DateTimePicker),
new FrameworkPropertyMetadata // メタデータ
{
DefaultValue = "00", // デフォルト値
BindsTwoWayByDefault = true,
PropertyChangedCallback = new PropertyChangedCallback(onInputtedMinuteChanged)
});
private string InputtedMinute
{
get { return (string)GetValue(InputtedMinuteProperty); }
set { SetValue(InputtedMinuteProperty, value); }
}
private static void onInputtedMinuteChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
var userControl = (DateTimePicker)sender;
if ((string)e.OldValue != (string)e.NewValue)
{
var date = userControl.DateTimeValue;
var dateTime = string.Format("{0}/{1}/{2} {3}:{4}:{5}",
date.Year, date.Month, date.Day,
userControl.InputtedHour, (string)e.NewValue, userControl.InputtedSecond);
userControl.DateTimeValue = DateTime.Parse(dateTime);
}
}
// 内部処理用の依存関係プロパティ(TextBox(Second) コントロールとバインド)
private static readonly DependencyProperty InputtedSecondProperty = DependencyProperty.Register(
"InputtedSecond", typeof(string), typeof(DateTimePicker),
new FrameworkPropertyMetadata // メタデータ
{
DefaultValue = "00", // デフォルト値
BindsTwoWayByDefault = true,
PropertyChangedCallback = new PropertyChangedCallback(onInputtedSecondChanged)
});
// 依存関係プロパティのラッパー
private string InputtedSecond
{
get { return (string)GetValue(InputtedSecondProperty); }
set { SetValue(InputtedSecondProperty, value); }
}
private static void onInputtedSecondChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
var userControl = (DateTimePicker)sender;
if ((string)e.OldValue != (string)e.NewValue)
{
var date = userControl.DateTimeValue;
var dateTime = string.Format("{0}/{1}/{2} {3}:{4}:{5}",
date.Year, date.Month, date.Day,
userControl.InputtedHour, userControl.InputtedMinute, (string)e.NewValue);
userControl.DateTimeValue = DateTime.Parse(dateTime);
}
}
}
}
次回は、DateTimePicker を使った業務アプリらしきものについて書いてみようと思います。
「DateTimePicker なユーザーコントロールを書いてみました」への2件のフィードバック