日付は表示されていた。モデルは null だった。

フォームには日付が選択表示されていた。保存しようとすると、その日付は必須だとエラーになった。

これはバリデーションの不具合のように聞こえるが、実際はバリデーターが正しいことを報告していた。nullable の .NET プロパティは依然として null のままだった。ブラウザーとアプリケーションが静かに乖離していたのである。

最近コミットされた修正により、根本的な教訓が異常に明確になった。ネイティブ HTML の date 入力とコンポーネントのコンバーターは、それぞれ妥当な動作をしていたが、同じ書式化契約を共有していなかった。コントロールは、モデルが受け取ったことのない状態を表示していた。

この教訓は 1 つのコンポーネントを超えている。UI 上に見える状態は、バインドされたアプリケーション状態の証明にはならない。

1 つのフィールドに 3 つの表現があり得る

日付フィールドは、しばしば 3 つの異なる表現を横断する。

  1. ブラウザーが描画する、人間向けの値。
  2. ネイティブ入力がやり取りする、ワイヤー上の値。
  3. .NET モデルに保存される、型付きの値。

<input type="date"> の場合、ブラウザーは馴染みのあるローカル形式で表示するかもしれないが、その値の契約は 2026-08-01 のような ISO 形式のカレンダー日付である。表示は変わり得るが、ワイヤー上の形式は変わらない。

周囲の Blazor コンポーネントは、異なる既定値を持つ場合がある。そのコンバーターは、現在のカルチャの short-date パターンを使って DateTime? を書式化・解析する。カルチャによっては、スラッシュ・フィールド順序・区切り文字が異なるパターンになる。

どちらの動作も単独では妥当だ。しかし、明示的なアダプターなしに組み合わせると、意図しないプロトコルが生まれる。

不具合は双方向で発生する

まずブラウザー→モデルの方向を考えてみよう。

ユーザーが日付を選択する。ネイティブコントロールは ISO 値を送出する。カルチャ対応のコンバーターは、現在の short-date 形式を期待している。変換に失敗し、nullable プロパティは更新されない。ブラウザーは依然として自身が所有する選択を表示できるため、次のバリデーションメッセージが不条理に見える。

今度は逆方向を見てみよう。

編集フォームが既存の日付をモデルから読み込む。コンバーターはカルチャ形式の文字列を生成する。ネイティブ date 入力は ISO 形式の値しか受け付けないため、その文字列を拒否し、空のコントロールを描画する。この状態から保存すると、アプリケーションが空欄を意図的なクリアとみなした場合、表示の問題がデータ消失に変わり得る。

これが、バリデーターやマッパー、サービスだけをテストしても欠陥を見逃す理由である。それらのレイヤーは、ブラウザーの契約とコンポーネントのコンバーターの間の不一致を検証しない。

境界の所有権を 1 つにまとめる

レビューされた修正では、ピッカー UI と型付きバインディング契約の両方を所有する date-picker コンポーネントを選択した。それは周囲のアプリケーションでも確立された慣習であり、新規性を減らし、修正の規模を小さくした。

これは 1 つの有効な回答であって、唯一のものではない。ネイティブ date 入力が重要なら、橋渡しを明示的に行う。カスタムの InputBase<DateOnly?>、明示的な get/set バインディングアダプター、またはネイティブの ISO 形式を意図的に使うコンバーターなど、いずれも機能する。

重要な設計上の問いかけは所有権である。

  • 誰がモデルの状態をコントロール向けに書式化するのか?
  • 誰がコントロールの値をモデルに戻し解析するのか?
  • 双方向で同じ契約が使われているか?
  • 変換失敗はどこで可視化されるのか?

日付のみを扱う業務概念では、DateOnly を用いることで、深夜の DateTime よりも意図を正確に表現できる。ワイヤー形式の問題は解決しないが、タイムゾーンや時刻の意味論がカレンダー専用のワークフローに不要に入り込むのを防ぐ。

バインディングをテストする(文字列だけでなく)

有用な回帰スイートは、代表的なカルチャ下でフィールド境界全体を検証する。

以下のケースから始めよう。

  • ユーザーの選択が型付きモデルのプロパティを更新する。
  • 既存のモデル値がコントロールに再描画される。
  • 省略可能な日付をクリアすると null になる。
  • 必須の日付が、モデルが空のままなのに「受け付けられた」ように見えることがない。
  • 再レンダリング後も有効な値が保持される。

双方向を複数のカルチャで実行する。ネイティブ ISO 形式と異なる short-date 形式を持つカルチャも含める。コンバーターの単体テストは役立つが、レンダリングされたコンポーネントテストの方が、バインディングイベントとコンポーネント状態を含むため、より強力である。ネイティブコントロールが所有する動作については、小さなブラウザーテストがさらに強力である。

不変条件は単純である。

displayed date = wire date = model date

Enter fullscreen mode Exit fullscreen mode

文字列は人間にとって同一である必要はない。意図的で可逆的なマッピングが必要である。

トレードオフを明示する

ネイティブコントロールは軽量で、馴染みがあり、アクセシブルで、ブラウザーサポートがある。一方、コンポーネント所有のピッカーは JavaScript・スタイリング・依存関係の重量・もう 1 つの UI 抽象化を追加する。

答えは「ネイティブ入力を使わない」ことではない。変換境界のコストを正直に見積もることである。周囲のコンポーネントがネイティブコントロールの値プロトコルを確実に話せない場合、見かけの単純さは将来のデバッグ時間から借りているに過ぎない。

同様に、カルチャマトリックスとレンダリングされたコンポーネントテストは、単一の単体テストよりもコストが高い。それらは境界に焦点を当て、すべてのフォームシナリオを複製しないようにする。双方向の契約テストをわずかに行う方が、UI プロトコルに触れない多数のサービステストよりも高い信頼性を得られることが多い。

実践的なレビューチェックリスト

型付きフォームコントロールをレビューする際は、以下を問う。

  1. ブラウザーは何を表示しているか?
  2. コントロールは正確にどのような値をやり取りしているか?
  3. モデルが必要とする型と null のセマンティクスは何か?
  4. 各方向の変換をどのコンポーネントが所有しているか?
  5. どのカルチャが検証されているか?
  6. 変換失敗が、古い UI をそのまま残す可能性はないか?

日付はこの境界を視認しやすくするが、同じパターンは小数点区切り・パーセント・通貨・列挙型・タイムゾーンでも現れる。

画面はブラウザーが描画したものの証拠である。モデルはアプリケーションが受け入れたものの証拠である。優れたバインディングコードと優れたテストは、この 2 つが接続されたままであることを証明する。

あなたの UI のどこで、値がモデルに到達したことがないのに有効に見えることがあるだろうか?