DateTimePicker なユーザーコントロールを書いてみました

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件のフィードバック

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です