WPF/XAMLのStringFormatが効かない原因と対処法:TextBox.Text×IValueConverterで日付表示を正しく整形する完全ガイド

WPF/XAML で「TextBox.Text に DateTime をバインドし StringFormat も指定しているのに、なぜか既定の短い形式で表示される」――実務でよく遭遇する落とし穴です。本記事では、IValueConverter と StringFormat の相互作用を仕組みから解説し、再現例・原因・対処法・設計上の指針・チェックリストまでを一気にまとめます。

目次

問題の再現コードと症状

次のように、TextBox.Text に日付プロパティ LastUpdate をバインドし、表示書式を英語の「月 日, 年」にしたいとします。

<TextBox
    Language="en-US"
    Text="{Binding LastUpdate,
                   Converter={StaticResource conDate},
                   StringFormat='MMMM d, yyyy'}" />

ところが実際の表示は 9/1/2020 のような「OS 既定の短い形式」のまま。conDate は下限日付より新しければ値を返し、そうでなければ空表示にする IValueConverter です。

根本原因:Converter が string を返すと書式指定子が効かない

データ バインディングの値の流れは概ね次の通りです。

Source (LastUpdate: DateTime)
   └─> IValueConverter.Convert(...)
          └─> (ここで「返す型」が決まる)
                 └─> Binding.StringFormat で文字列整形 (Composite Formatting)
                        └─> Target (TextBox.Text: string)

ここで決定的なのは、StringFormat は「Composite Formatting」( {0:...} 形式 ) を内部で使うという点です。Composite Formatting は、引数が DateTime や double のような IFormattable を実装している場合に限って MMMM d, yyyy などの「型依存の書式指定子」を解釈します。引数が string の場合、書式指定子は解釈されず、その文字列がそのまま差し込まれます。

つまり、conDate.Convert が "9/1/2020" のような 文字列 を返してしまうと、StringFormat="MMMM d, yyyy" は適用されても効果がゼロ (元の文字列がそのまま) になります。
逆に、DateTime (または DateTime?) を返せば、MMMM d, yyyy が正しく解釈されて期待通りに整形されます。

結論:方針は 2 つに分かれる

方針実装の要点向いている場面メリット注意点
A. Converter は DateTime を返す条件を満たせば DateTime / DateTime? を返す。空表示は null や UnsetValue を返し XAML 側で TargetNullValue 等を使う。書式を XAML に集約したい/UI でロケールを切り替えるStringFormat がそのまま効く。XAML の見通しが良い。ロケール切替に強い。双方向バインド時は ConvertBack を用意 (string→DateTime)。
B. Converter 内で ToString するdt.ToString("MMMM d, yyyy", culture) で文字列化。XAML の StringFormat は外す。画面ごとに細かな書式差分が多い/コードで厳密制御したい挙動が一箇所に閉じる。TextBox / TextBlock の違いに影響されない。書式が散逸しやすい。ロケール切替は Converter 側の対応が必要。

実装例:方針 A (Converter は DateTime を返す)

XAML

<Window
    x:Class="SampleApp.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:SampleApp"
    Language="en-US">








 

C# の Converter

using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;

public sealed class NewerThanMinDateConverter : IValueConverter
{
public DateTime MinDate { get; set; } = new DateTime(2000, 1, 1);

```
// Source(DateTime?) → Target(string)
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
    if (value is DateTime dt)
    {
        // 下限を満たさないなら「表示しない」(XAML の TargetNullValue へ委ねる)
        if (dt &lt; MinDate) return null; // または DependencyProperty.UnsetValue
        return dt; // ← ここが肝。string にしない!
    }
    return null;
}

// Target(string) → Source(DateTime?)
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
    var s = value as string;
    if (string.IsNullOrWhiteSpace(s)) return null; // 空入力は null

    // ユーザー入力のパース。Language に応じた culture が渡ってくる
    if (DateTime.TryParse(s, culture, DateTimeStyles.None, out var dt))
    {
        return dt;
    }

    // 解析不能ならバリデーション等の方針により調整
    return DependencyProperty.UnsetValue;
}
```

} 

ViewModel

using System;
using System.ComponentModel;

public class MainViewModel : INotifyPropertyChanged
{
private DateTime? _lastUpdate = new DateTime(2020, 9, 1);
public DateTime? LastUpdate
{
get => _lastUpdate;
set { _lastUpdate = value; PropertyChanged?.Invoke(this, new(nameof(LastUpdate))); }
}
public event PropertyChangedEventHandler PropertyChanged;
} 

ポイント

  • Converter.Convert は string を返さない。DateTime または null を返す。
  • Language="en-US" を要素 (またはルート) に付与し、月名の言語 を制御する。WPF では Language が culture に反映されるため、MMMM の月名が英語化される。
  • 空表示は XAML の TargetNullValue='' で扱うと整理しやすい。

実装例:方針 B (Converter 内で書式化する)

XAML

&lt;TextBox
    Language="en-US"
    Text="{Binding LastUpdate,
                   Converter={StaticResource conDate}, 
                   TargetNullValue=''}" /&gt;

C# の Converter

public sealed class FormatDateStringConverter : IValueConverter
{
    public DateTime MinDate { get; set; } = new DateTime(2000, 1, 1);
    public string Format { get; set; } = "MMMM d, yyyy";

```
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
    if (value is DateTime dt &amp;&amp; dt &gt;= MinDate)
    {
        return dt.ToString(Format, culture); // ここで完全に文字列化
    }
    return null; // TargetNullValue で空に
}

public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
    var s = value as string;
    if (string.IsNullOrWhiteSpace(s)) return null;
    return DateTime.TryParse(s, culture, DateTimeStyles.None, out var dt)
        ? dt
        : DependencyProperty.UnsetValue;
}
```

} 

ポイント

  • StringFormat は使わない。書式は Converter の ToString(format, culture) で決める。
  • 画面個別の書式差分が多い場合や、曜日・時刻・タイムゾーンなど複雑な整形も Converter 一つで完結する。

TextBox と TextBlock の違いでハマらないために

  • TextBlock.Text は一方向バインド (OneWay) が多く、単に表示だけなら StringFormat で十分。Converter も DateTime を返せば OK。
  • TextBox.Text は通常 TwoWay。StringFormat の効果は あくまで「表示 (Target 方向)」のみ。ユーザー入力から Source に戻す際は ConvertBack か UpdateSourceTrigger・ValidationRules などの設計が必要。

文化情報 (Culture) と言語 (Language) の正しい扱い

WPF では、FrameworkElement.Language が CultureInfo にマップされ、Converter の culture 引数や StringFormat の解釈に影響します。英語の月名が欲しいなら、対象要素またはルートに Language="en-US" を付けましょう。日本語のままにしたいなら指定は不要です。

よくある誤解として「Thread.CurrentThread.CurrentCulture を切り替えれば十分」というものがありますが、WPF の XAML バインディングは 要素の Language を優先するため、画面単位での明示指定が安定します。

空表示・既定値・エラー時の挙動を XAML で制御する

返す値 / 設定表示結果用途備考
null + TargetNullValue=""空文字値なしを明示して空表示最も扱いやすい
DependencyProperty.UnsetValueバインド無効 (未設定)値を供給しない見た目は空か既定値
Binding.DoNothing直前の表示を保持不正入力時のロールバックConvertBack で有効
FallbackValue値取得に失敗時の代替プロパティ未解決など設計により使い分け

「StringFormat が効かない」を一発で見抜くチェックリスト

  1. Converter は string を返していないか? (→ 返すのは DateTime / DateTime?)
  2. StringFormat の書式は 対象型に合っているか? (例: 日付に MMMM、数値に N2)
  3. Language (または xml:lang) は期待のカルチャか?
  4. TargetNullValue / FallbackValue は意図通り?
  5. UpdateSourceTrigger は適切? (PropertyChanged / LostFocus)
  6. 双方向バインドなら ConvertBack とパースの仕様が定義されているか?

補足:複合書式と StringFormat の関係を理解する

StringFormat に "MMMM d, yyyy" のように 波括弧を含まない形式 を書くと、内部的には "{0:MMMM d, yyyy}" として扱われます。Converter が DateTime を返す場合だけ、MMMM 等の型依存書式が効きます。Converter が string を返すと、string は IFormattable を実装していないため、MMMM を解釈できず、そのまま差し込まれるわけです。

これは TextBlock でも TextBox でも同様です。「TextBlock だと効くのに TextBox で効かない」と感じるケースは、TextBlock では Converter を通していなかった、または Converter が DateTime を返していた、といった設計差に起因することがほとんどです。

実務 Tips:保守性を高める設計パターン

  • 表示書式は可能な限り XAML に集約 (=方針 A をデフォルト)。UI デザイナとエンジニアの責務分離がしやすい。
  • 例外的な画面だけ 専用 Converter (=方針 B) を導入。Converter 名で用途が分かるように命名 (DisplayDateAsLongMonthConverter など)。
  • カルチャは XAML で宣言的に (画面単位の Language)。グローバル変更よりも影響範囲を抑制しやすい。
  • 双方向では ConvertBack を必ず実装。ユーザー入力の許容フォーマット、検証エラー時の挙動 (DoNothing / エラースタイル) を仕様化。

よくある誤りと対処

誤り症状原因対処
Converter で val.ToString() を返すStringFormat が効かない引数が string になり、MMMM 等が解釈されないDateTime を返す/書式は Converter 内に寄せる
Language 未設定で英語月名を期待「9月」などローカル言語の月名要素の Language がカルチャに反映Language="en-US" を付与
空表示を空文字で返すConvertBack で不整合空文字の扱いがバラつくnull を返し TargetNullValue で制御
ConvertBack 未実装入力が Source に反映されない/例外TwoWay でパース仕様未定義TryParse 実装+検証ルール

応用:複合文・ツールチップ・MultiBinding でも同じ発想

「最終更新: 2020 年 9 月 1 日」のような複合文にしたいときも、考え方は同じです。

&lt;TextBlock
    Language="ja-JP"
    Text="{Binding LastUpdate, StringFormat='最終更新: yyyy年 M月 d日'}" /&gt;

また、他の値と組み合わせる場合は MultiBinding+StringFormat が有効です。Converter を使うなら 最終的に DateTime を含む適切な型 を返す (または各値をそのまま渡して StringFormat で組み立てる) のがコツです。

トラブルシューティングの実例

ケース 1:Converter は正しく DateTime を返しているのに短い形式になる

  • 原因候補:Language が期待外。MMMM がカルチャに従って月名に展開されるため、英語を期待しているのに日本語になっている。
  • 対処:該当要素またはルートに Language="en-US"。または ja-JP などに明示。

ケース 2:空にしたいのに「01/01/0001」などが出る

  • 原因候補:DateTime の既定値 (MinValue) をそのまま返している。
  • 対処:Converter で null に変換し、XAML で TargetNullValue='' を設定。

ケース 3:ユーザー入力で例外になる

  • 原因候補:ConvertBack 未実装、または DateTime.Parse が例外を投げている。
  • 対処:TryParse を用い、解析不能時は UnsetValue を返し Validation を表示。UpdateSourceTrigger を LostFocus にして入力途中の例外を避ける。

単体テスト観点 (抜粋)

  • Converter が DateTime を返す:LastUpdate=2020-09-01 で Convert が DateTime(2020,9,1) を返すこと。
  • 閾値未満:Convert が null を返し、XAML 側で空表示になること。
  • ロケール:Language="en-US" と ja-JP の両ケースで MMMM の展開が期待通りであること。
  • ConvertBack:"September 1, 2020" → DateTime(2020,9,1)、空文字 → null、不正文字列 → UnsetValue。

最終まとめ

  • StringFormat が効かない最大の理由は「Converter が string を返している」こと。
  • 対処は A) Converter は DateTime を返して XAML に書式を集約、または B) Converter で ToString の二者。デフォルトは A を推奨。
  • カルチャは Language で明示し、空表示は TargetNullValue で完結させると設計が綺麗。
  • TwoWay の場合は ConvertBack と Validation を忘れない。

付録:VB 版 Converter (方針 A)

Imports System.Globalization
Imports System.Windows
Imports System.Windows.Data

Public Class NewerThanMinDateConverterVB
Implements IValueConverter

```
Public Property MinDate As DateTime = New DateTime(2000, 1, 1)

Public Function Convert(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) _
    As Object Implements IValueConverter.Convert

    If TypeOf value Is DateTime Then
        Dim dt = CType(value, DateTime)
        If dt &lt; MinDate Then
            Return Nothing ' TargetNullValue へ委ねる
        End If
        Return dt ' ← 文字列にしない
    End If
    Return Nothing
End Function

Public Function ConvertBack(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) _
    As Object Implements IValueConverter.ConvertBack

    Dim s = TryCast(value, String)
    If String.IsNullOrWhiteSpace(s) Then Return Nothing

    Dim dt As DateTime
    If DateTime.TryParse(s, culture, DateTimeStyles.None, dt) Then
        Return dt
    End If

    Return DependencyProperty.UnsetValue
End Function
```

End Class 

付録:代表的な組み合わせ早見表

Converter の戻り値StringFormat期待表示結果結論
DateTimeMMMM d, yyyySeptember 1, 2020◎最善
string (例: 9/1/2020)MMMM d, yyyySeptember 1, 2020× (9/1/2020 のまま)避ける
null任意空表示△ (TargetNullValue 必須)OK
UnsetValue任意未設定 (前値/既定値)△用途限定

実装テンプレート(使い回し用)

&lt;!-- XAML 側:基本形 --&gt;
&lt;TextBox
    Language="en-US"
    Text="{Binding LastUpdate,
                   Converter={StaticResource NewerThanMinDateConverter},
                   StringFormat='MMMM d, yyyy',
                   TargetNullValue=''}" /&gt;
// Converter 側:基本形
public sealed class NewerThanMinDateConverter : IValueConverter
{
    public DateTime MinDate { get; set; } = new(2000, 1, 1);
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        => value is DateTime dt ? (dt >= MinDate ? dt : null) : null;

```
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    =&gt; DateTime.TryParse(value as string, culture, DateTimeStyles.None, out var dt)
       ? dt : DependencyProperty.UnsetValue;
```

} 

以上を押さえておけば、「StringFormat が適用されない」という典型的な罠は確実に回避できます。ポイントは「Converter で文字列を返さない」――これだけです。あとは XAML の StringFormat と Language、TargetNullValue を味方につけ、保守性の高い UI を組み上げていきましょう。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次