UWPでTextBox.Textをx:Bind(TwoWay)でdoubleに直接バインドすると、地域設定が「小数点=カンマ」の環境で「12,5」が「12」に切り捨てられることがあります。原因と再現手順、NumberBoxへの置き換えやカルチャ対応の変換で確実に直す方法をまとめます。
現象:カンマ小数を入力すると値が切り捨てられる
問題が起きる典型例は、TextBoxのText(string)と、double型プロパティをx:BindでTwoWay接続しているケースです。入力欄としては自然に見えますが、内部では「文字列 → 数値」の変換が必ず発生するため、ロケール(地域設定)と変換ロジックの相性が露呈します。
<TextBox Text="{x:Bind FinancialItem2.Amount, Mode=TwoWay}" />
<TextBlock Text="{x:Bind FinancialItem2.Amount, Mode=OneWay}" />
public class FinancialItem2 : DependencyObject, INotifyPropertyChanged
{
public static readonly DependencyProperty AmountProperty =
DependencyProperty.Register(
nameof(Amount),
typeof(double),
typeof(FinancialItem2),
new PropertyMetadata(0.0));
public double Amount
{
get => (double)GetValue(AmountProperty);
set => SetValue(AmountProperty, value);
}
public event PropertyChangedEventHandler? PropertyChanged;
}
Windowsの地域設定が「小数点=カンマ(例:12,5)」の環境だと、次のような差が出ることがあります。
| TextBoxに入力 | 期待される数値 | x:Bindの結果(例) | メモ |
|---|---|---|---|
| 12,5 | 12.5 | 12 | カンマ以降が落ちる/表示も12に戻る |
| 12.5 | 12.5 | 12.5 | フォーカスアウト後に表示が12,5へ変わることがある |
同じ文字列に対して double.TryParse() を CultureInfo.CurrentCulture で実行すると正しく解釈できるのに、x:Bindの自動変換だと想定と違う結果になる――ここが混乱ポイントです。
なぜ起きるのか:TextBoxは「数値入力コントロール」ではない
TextBoxはあくまで文字列入力のコントロールです。TwoWayバインディングでdoubleに直結すると、更新タイミング(多くはフォーカスアウト)で「TextBox.Textの文字列をdoubleに変換してモデルへ押し戻す」処理が走ります。
x:Bindはコンパイル時にコード生成され、高速・型安全というメリットがあります。一方で、TextBox.Text(string)→doubleの変換は「自動変換」に任されるため、環境や入力パターンによってはロケールの小数点記号を期待通りに扱えないケースが出ます。結果として「12,5」が「12」になったり、逆に「12.5」は通って表示だけがロケールに合わせて「12,5」へ整形されたりします。
ここで押さえておきたい実務的な結論はシンプルです。
- 数値入力をTextBoxでやるなら、変換(パース/フォーマット)を自分で管理する
- 数値入力が主目的なら、数値入力向けコントロールに任せる
推奨:WinUI 2のNumberBoxに置き換える
受け入れられやすい解決策は、TextBoxをやめてWinUI 2のNumberBoxを使うことです。NumberBoxは数値入力専用で、Value(double)とText(表示文字列)を分けて持ち、入力・表示・整形の責務がコントロール側にあります。
さらにNumberBoxは、小数点のドット/カンマについて「ユーザーが入力した表記は、NumberBox側のフォーマット設定に置き換えられる」挙動が明記されており、ロケール混在入力でも破綻しにくい設計です。
導入のポイント(UWP + WinUI 2)
- UWPプロジェクトにWinUI 2(NuGetのMicrosoft.UI.Xaml)を追加
- XAMLで
xmlns:muxc="using:Microsoft.UI.Xaml.Controls"を宣言 <muxc:NumberBox />を配置し、ValueをdoubleにTwoWayバインド
<Page
...
xmlns:muxc="using:Microsoft.UI.Xaml.Controls">
<muxc:NumberBox
Header="金額"
Value="{x:Bind ViewModel.Amount, Mode=TwoWay}"
SpinButtonPlacementMode="Compact"
SmallChange="0.1"
ValidationMode="InvalidInputOverwritten" />
<TextBlock Text="{x:Bind ViewModel.Amount, Mode=OneWay}" />
</Page>
Valueにdoubleをバインドするだけで、TextBoxで悩みがちな「文字列⇔数値」の変換を自前で抱え込まずに済みます。特に、入力の正当性(InvalidInputOverwrittenなど)や、スピンボタン、フォーマッターの設定まで一気通貫で整えられるのが実務上のメリットです。
小数点以下の桁数を固定したい場合(NumberFormatter)
たとえば「小数2桁に固定」「0.25刻みで丸めたい」のような要件は、NumberBoxのNumberFormatterで扱えます。以下はDecimalFormatterを使う例です。
using Windows.Globalization.NumberFormatting;
using Windows.UI.Xaml;
private void SetupNumberFormatter()
{
// 例:小数2桁(四捨五入は要件に応じて)
var formatter = new DecimalFormatter
{
IntegerDigits = 1,
FractionDigits = 2
};
AmountNumberBox.NumberFormatter = formatter;
}
なお、NumberBoxは入力をクリアするとValueがNaNになる仕様です。空欄を「0」と見なすのか「未入力」と見なすのかは、画面要件に合わせて取り扱いを決めておくと後々のバグを減らせます。
TextBoxを使い続けるなら:文字列用プロパティを挟んで手動変換する
既存画面の都合でTextBoxのまま進めたい場合は、doubleに直結しないのがコツです。UIとは常にstringでやり取りし、モデル側でカルチャを指定してパース/フォーマットします。
XAML(TextBoxはAmountTextにTwoWay)
<TextBox Text="{x:Bind FinancialItem2.AmountText, Mode=TwoWay}" />
<TextBlock Text="{x:Bind FinancialItem2.AmountText, Mode=OneWay}" />
C#(AmountTextでCultureInfo.CurrentCultureを使う)
using System;
using System.ComponentModel;
using System.Globalization;
using System.Runtime.CompilerServices;
using Windows.UI.Xaml;
public class FinancialItem2 : DependencyObject, INotifyPropertyChanged
{
public static readonly DependencyProperty AmountProperty =
DependencyProperty.Register(
nameof(Amount),
typeof(double),
typeof(FinancialItem2),
new PropertyMetadata(0.0, OnAmountChanged));
public double Amount
{
get => (double)GetValue(AmountProperty);
set => SetValue(AmountProperty, value);
}
// UI用(文字列): ここだけをTextBoxとバインドする
public string AmountText
{
get => Amount.ToString("0.################", CultureInfo.CurrentCulture);
set
{
if (TryParseFlexibleDouble(value, out var d))
{
Amount = d;
AmountError = string.Empty;
}
else
{
// 失敗時の方針は要件次第:保持/0にする/NaNにする等
AmountError = "数値として解釈できません(例:12,5)";
// Amountは更新しない(=最後の正しい値を維持)
}
OnPropertyChanged(nameof(AmountText));
OnPropertyChanged(nameof(AmountError));
}
}
public string AmountError { get; private set; } = string.Empty;
public event PropertyChangedEventHandler? PropertyChanged;
private static void OnAmountChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is FinancialItem2 item)
{
item.OnPropertyChanged(nameof(Amount));
item.OnPropertyChanged(nameof(AmountText));
}
}
private void OnPropertyChanged([CallerMemberName] string? name = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
// カンマ文化でもドット入力を受けたい場合に備えた柔軟パース
private static bool TryParseFlexibleDouble(string? text, out double value)
{
var s = (text ?? string.Empty).Trim();
// まずは現在カルチャで素直に解析(12,5 の想定)
if (double.TryParse(s, NumberStyles.Float | NumberStyles.AllowThousands,
CultureInfo.CurrentCulture, out value))
{
return true;
}
// 次にInvariantで解析(12.5 の想定)
if (double.TryParse(s, NumberStyles.Float | NumberStyles.AllowThousands,
CultureInfo.InvariantCulture, out value))
{
return true;
}
value = 0;
return false;
}
}
この方式のメリットは、「どのカルチャで、どんな形式を許容するか」をアプリ側で明示できる点です。例えば「社内はカンマ文化だが、テンキーの都合でドット入力も許容したい」といった現場の要件に合わせて調整できます。
エラー表示もセットで用意すると運用が安定する
パース失敗時に黙って0にする・切り捨てる、といった挙動は後からトラブルになりがちです。最低限、ユーザーに気づけるフィードバックを用意するのが安全です。
<TextBox Text="{x:Bind FinancialItem2.AmountText, Mode=TwoWay}" />
<TextBlock Text="{x:Bind FinancialItem2.AmountError, Mode=OneWay}"
Margin="0,6,0,0" />
x:Bindの関数バインディングで「変換関数」を挟む
モデルにAmountTextを増やしたくない場合は、x:Bindの関数バインディングで「string⇔double」を変換する関数を噛ませる方法もあります。TwoWayの場合は、逆方向の関数をBindBackで指定します。
変換ヘルパー(staticメソッド)
using System.Globalization;
public static class AmountBinding
{
public static string ToText(double value)
=> value.ToString("0.################", CultureInfo.CurrentCulture);
public static double ToDouble(string text)
{
// 失敗時の扱いは要件に合わせて(例:0にする/例外にする等)
if (double.TryParse(text, NumberStyles.Float | NumberStyles.AllowThousands,
CultureInfo.CurrentCulture, out var d))
{
return d;
}
if (double.TryParse(text, NumberStyles.Float | NumberStyles.AllowThousands,
CultureInfo.InvariantCulture, out d))
{
return d;
}
return 0.0;
}
}
XAML(関数で変換し、BindBackで戻す)
<TextBox Text="{x:Bind local:AmountBinding.ToText(ViewModel.Amount),
Mode=TwoWay,
BindBack=local:AmountBinding.ToDouble}" />
関数バインディングは「変換」をUI層に寄せられるため、モデルを汚したくないときに便利です。特に、同じAmountを複数の表示形式(小数桁、通貨記号の有無など)で出したいケースでも拡張しやすくなります。
x:BindのConverter(IValueConverter)を使う選択肢
x:BindはConverterも指定できます。WPFのIValueConverterに慣れている場合はこちらの方が読みやすいこともあります。ConverterやConverterLanguageなどをx:Bindのプロパティとして設定できることが公式ドキュメントで案内されています。
Converter実装例
using System;
using System.Globalization;
using Windows.UI.Xaml.Data;
public sealed class FlexibleDoubleConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is double d)
{
return d.ToString("0.################", CultureInfo.CurrentCulture);
}
return string.Empty;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
var s = (value as string ?? string.Empty).Trim();
if (double.TryParse(s, NumberStyles.Float | NumberStyles.AllowThousands,
CultureInfo.CurrentCulture, out var d))
{
return d;
}
if (double.TryParse(s, NumberStyles.Float | NumberStyles.AllowThousands,
CultureInfo.InvariantCulture, out d))
{
return d;
}
// 失敗時:要件に応じて
return 0.0;
}
}
XAMLで適用
<Page.Resources>
<local:FlexibleDoubleConverter x:Key="FlexibleDoubleConverter" />
</Page.Resources>
どの方法を選ぶべきか:実務で迷わない判断基準
| 方法 | 向いている状況 | メリット | 注意点 |
|---|---|---|---|
| NumberBoxに置き換え | 数値入力が主目的/UI改善できる | Valueでdoubleを安全に扱える、ロケールや整形をコントロール側で吸収 | WinUI 2導入が必要、空欄時はNaNなど仕様確認が必要 |
| AmountText(string)を挟む | 既存TextBoxを維持したい/モデル側で制御したい | パース・フォーマットを自由に実装できる、エラー表示も組み込みやすい | プロパティ増加、通知(PropertyChanged)の設計が必要 |
| 関数バインディング(BindBack) | モデルを増やしたくない/UI側で変換を完結したい | x:Bindの型安全を保ちつつコンバーター相当を実現 | 関数の配置場所やstatic/instanceの設計に注意 |
| IValueConverter | Converter文化のチーム/既存資産がある | 役割が明確で再利用しやすい | ConvertBack失敗時の方針(0/例外/保持)を決める |
よくある落とし穴と、先回りの対策
- 金額なのにdoubleを使う:小数の丸め誤差が積み上がる可能性があります。金額はdecimalで保持し、UIだけdoubleに変換する設計も検討してください。
- 入力途中の文字列を即パースする:UpdateSourceTrigger=PropertyChangedにすると、ユーザーが「12,」まで入力した瞬間に失敗しやすいです。まずはLostFocus更新(既定)で安定させ、必要なら入力補助(プレースホルダー、説明文)を追加すると事故が減ります。
- 失敗時に黙って0へ:誤入力が気づかれずに保存されるのが最悪パターンです。エラー表示、保存ボタン無効化、最後の正しい値の維持など、画面要件に合わせて「失敗時の方針」を決めましょう。
- ドット入力を許容するか決めていない:テンキーは環境によって「.」が出ることがあります。CurrentCulture→InvariantCultureの順にTryParseするだけでも、現場のストレスがかなり下がります。
まとめ:ロケールに依存する変換は「任せる場所」を決める
TextBoxとdoubleをx:Bindで直結すると、ロケール(小数点カンマ)環境で「12,5→12」のような意図しない変換が起きることがあります。最も堅い解決策はNumberBoxに置き換えて数値入力の責務をコントロールに寄せること。置き換えが難しい場合は、stringプロパティを挟む/関数バインディングやConverterで変換を自前化し、カルチャを明示してパース・フォーマットを制御するのが安全です。

コメント