読者です 読者をやめる 読者になる 読者になる

xin9le.net

Microsoft の製品/技術が大好きな Microsoft MVP な管理人の技術ブログです。

DateTimePickerのFormatStringで例外

Extended WPF Toolkit、使ってますか?XAMLは非常に柔軟なので、WinFormsとは雲泥の差と言わんばかりに簡単にUIをカスタマイズできます。が、WPFには標準提供のコントロールがそれほど多くありません。「WinFormsにはあるのにWPFにはない」、こんなコントロールはやはりあります。Extended WPF Toolkitは、そんな標準提供されていないコントロールの多くを無償で提供してくれています。もちろん、GrapeCityさんやInfragisticsさんの有償のコントロールを利用できる場合はそれを利用するのが良いのかもしれませんが、個人で簡単なツールを作る場合にはExtended WPF Toolkitは非常におすすめです。

DateTimePickerは業務アプリ開発ではないと絶対困るのに、標準では提供されていないコントロールのひとつです。今回Extended WPF Toolkitで提供されているコレを利用して少しハマッた、という事例をご紹介します。

FormatStringでXamlParseException

DateTimePickerには、FormatStringという表示する日時文字列をカスタマイズするための依存関係プロパティが提供されています。それを "2013年08月05日 01:38:52" のようなよくある形で表示するように指定しました。以下はその設定例です。

<Window x:Class="WpfApplication.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:my="clr-namespace:WpfApplication"
        xmlns:xwtk="http://schemas.xceed.com/wpf/xaml/toolkit"
        Title="MainWindow" Height="300" Width="300">
    <Window.DataContext>
        <my:MainWindowViewModel />
    </Window.DataContext>
    <StackPanel Margin="7">
        <xwtk:DateTimePicker Value="{Binding TargetDateTime}" Format="Custom" FormatString="yyyy/MM/dd HH:mm:ss" TextAlignment="Left" />
    </StackPanel>
</Window>

これを実行しても当然何のエラーも出ず、普通に動きます。しかし、ちょっとした条件を加えると次のように実行時例外が発生します。

XamlParseException

InnerExceptionの内容を確認すると、"FormatStringの有効な値ではありません" と言われています。今回の場合、何十人もが正常に動作している中、全く同じバイナリで特定の1人だけ絶対に例外が出てるような状況でした。FormatStringの値が間違っているならば全員動かないはずなので、FormatStringがおかしいハズがない。なので、何が良くないのか理由がサッパリわかりませんでした。

FormatStringError

オープンソースなのを利用する

Extended WPF Toolkitはオープンソースなプロジェクトなので、ホストされているCodePlexのプロジェクトサイトに行けばソースコードはすべて見ることができます。DateTimePickerのFormatStringプロパティ付近を見ると次のように記述されています。

public static readonly DependencyProperty FormatStringProperty = DependencyProperty.Register("FormatString", typeof(string), typeof(DateTimeUpDown), new UIPropertyMetadata(default(string), OnFormatStringChanged), IsFormatStringValid);

public string FormatString
{
    get{ return (string)GetValue(FormatStringProperty); }
    set{ SetValue(FormatStringProperty, value); }
}

internal static bool IsFormatStringValid(object value)
{
    try
    {
        // Test the format string if it is used.
        DateTime.MinValue.ToString((string)value, CultureInfo.CurrentCulture);
    }
    catch
    {
        return false;
    }
    return true;
}

private static void OnFormatStringChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
    var dateTimeUpDown = o as DateTimeUpDown;
    if (dateTimeUpDown != null)
        dateTimeUpDown.OnFormatStringChanged((string)e.OldValue, (string)e.NewValue);
}

protected virtual void OnFormatStringChanged(string oldValue, string newValue)
{
    FormatUpdated();
}

どうやら、値を設定する際にIsFormatStringValidメソッドで検証を行っているようです。検証はDateTime.MinValueにフォーマット文字列を指定して例外が発生しないかどうかで判別していることが分かりますが、このとき現在のカルチャー情報も一緒に指定しています。そして、こいつが例外を発生させる犯人です。

CultureInfoにはカレンダー情報が含まれています。CurrentCultureプロパティから取得できるCultureInfoは現在OSで設定されている実行環境固有の情報であり、そこに含まれるカレンダーも同様です。日本には西暦と和暦という2種類のカレンダーがあり、OSでそれらを選択可能です (既定値は西暦)。そしてそのカレンダーがサポートしている最小日時が異なります。システムの設定を変えて検証してみると以下のような結果になります。

var calendar = CultureInfo.CurrentCulture.Calendar;
Console.WriteLine("クラス名 : {0}", calendar.GetType().Name);
Console.WriteLine("最小日時 : {0}", calendar.MinSupportedDateTime);

//--- 西暦
// クラス名 : GregorianCalendar
// 最小日時 : 0001/01/01 0:00:00

//--- 和暦
// クラス名 : JapaneseCalendar
// 最小日時 : 明治 1/9/8 0:00:00

DateTime.MinValueは "0001年1月1日 00:00:00.0000000" を表す定数なので、和暦ではサポートされていない範囲になります。そのため偶然和暦を設定していた方の環境でのみ障害が発生していました。

まとめ

Extended WPF ToolkitのDateTimePickerでFormatStringを明示的に指定すると (DateTimeUpDownも同様)、システムのカレンダー設定が和暦の場合に例外が発生することが分かりました。商用プロダクトの場合、ユーザーさんに「このアプリは必ず西暦で使ってください」などとはとっても言いづらいので利用を避けるのが吉です。社内システムなどで融通が利く場合には積極的に利用して良いのではないかと思います。また、別の理由でCurrentCultureを利用する場合もサポート最小日時にはお気を付けください。

今回の場合、Calandar.MinSupportedDateTimeとかDateTime.Nowなどで検証してくれていれば何も問題が起こらなかったハズ。せっかくなのでExtended WPF Toolkitにフィードバックを入れておきました。修正してもらえると嬉しいですね!(英語が稚拙なのは気にしない...)

DateTimePicker.FormatString throws Exception