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 < 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
<TextBox
Language="en-US"
Text="{Binding LastUpdate,
Converter={StaticResource conDate},
TargetNullValue=''}" />
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 && dt >= 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 が効かない」を一発で見抜くチェックリスト
- Converter は string を返していないか? (→ 返すのは
DateTime/DateTime?) StringFormatの書式は 対象型に合っているか? (例: 日付にMMMM、数値にN2)Language(またはxml:lang) は期待のカルチャか?TargetNullValue/FallbackValueは意図通り?UpdateSourceTriggerは適切? (PropertyChanged/LostFocus)- 双方向バインドなら
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 日」のような複合文にしたいときも、考え方は同じです。
<TextBlock
Language="ja-JP"
Text="{Binding LastUpdate, StringFormat='最終更新: yyyy年 M月 d日'}" />
また、他の値と組み合わせる場合は 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 < 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 | 期待表示 | 結果 | 結論 |
|---|---|---|---|---|
| DateTime | MMMM d, yyyy | September 1, 2020 | ◎ | 最善 |
| string (例: 9/1/2020) | MMMM d, yyyy | September 1, 2020 | × (9/1/2020 のまま) | 避ける |
| null | 任意 | 空表示 | △ (TargetNullValue 必須) | OK |
| UnsetValue | 任意 | 未設定 (前値/既定値) | △ | 用途限定 |
実装テンプレート(使い回し用)
<!-- XAML 側:基本形 -->
<TextBox
Language="en-US"
Text="{Binding LastUpdate,
Converter={StaticResource NewerThanMinDateConverter},
StringFormat='MMMM d, yyyy',
TargetNullValue=''}" />
// 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)
=> DateTime.TryParse(value as string, culture, DateTimeStyles.None, out var dt)
? dt : DependencyProperty.UnsetValue;
```
}
以上を押さえておけば、「StringFormat が適用されない」という典型的な罠は確実に回避できます。ポイントは「Converter で文字列を返さない」――これだけです。あとは XAML の StringFormat と Language、TargetNullValue を味方につけ、保守性の高い UI を組み上げていきましょう。

コメント