読み取り専用の値オブジェクトがWPFのTextBoxにバインドされない(ように見える)件

最終的に、WPFのバグを踏んだのか!?と思ったけど、よく見ればきちんと例外が出ていたという話なので、今後注意するという備忘録的な記事です。

現象

こんな風な値オブジェクトを作っておいて、WPFのウィンドウにバインドします。

public class ViewModel
{
    public int ID { get; set; }
    public string Name { get; set; }
    public string NameSan { get => Name + "-san"; } 
}

NameSan プロパティは加工して表示するだけの読み取り専用のプロパティです。金額の合計値を出すとか、なにか計算結果をだすとかそういう ReadOnly な表示はよくやるパターンですね。

ViewModel _vm;
private void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
    _vm = new ViewModel();
    _vm.ID = 100;
    _vm.Name = "Masuda";
    this.DataContext = _vm;
}

ここで NameSan プロパティを表示するときに、TextBlock タグを使えばよいのですが、TextBox を使って ReadOnly=”True” にします。TextBox を使う理由としては、表示している文字列のコピーが Ctrl-C でできるからです。けれど、読み取りにしたいから、ReadOnly を付けておきます。

<TextBox Grid.Column="1" Grid.Row="0" Text="{Binding ID}" />
<TextBox Grid.Column="1" Grid.Row="1" Text="{Binding Name}" />
<TextBox Grid.Column="1" Grid.Row="2" Text="{Binding NameSan}" IsReadOnly="True" />

これをビルドして動かそうとすると実行時に例外が発生します。

System.InvalidOperationException
HResult=0x80131509
Message=TwoWay または OneWayToSource バインドは、型 ‘WpfApp4.ViewModel’ の読み取り専用プロパティ ‘NameSan’ では動作できません。
Source=PresentationFramework

どうやら、TextBox の Text プロパティは読み書き同時にできることが前提となっているので NameSan のように get しかない読み取り専用プロパティではエラーが発生するのです。IsReadOnly を付けていても駄目ですね。これは、WPFのModeがデフォルトで「TwoWay」になっており双方向になっているのが原因です。ここを明示的に「Mode=OneWay」にして表示のみにすると実行時エラーはでなくなります。

<TextBox Grid.Column="1" Grid.Row="0" Text="{Binding ID}" />
<TextBox Grid.Column="1" Grid.Row="1" Text="{Binding Name}" />
<TextBox Grid.Column="1" Grid.Row="2" Text="{Binding NameSan, Mode=OneWay}" IsReadOnly="True" />

ちなみに、UWPの場合はデフォルトが「OneWay」になっているので動作が違います。これがややこしい。

実験

あらかじめ、この現象を知っておけば Mode=OneWay をちまちまつけるのですが、どうやら手元でたくさんサブウィンドウを作っているときにうっかり OneWay をつけ忘れたのです。

すると、サブ画面ではこんな風に NameSan プロパティをバインドしている TextBox の初期値がでなくなります。

サブ画面を開くときは、こんな風に ViewModel を渡して(本当はコンストラクタのほうがいいけど)、ShowDialogメソッドで開きます。

private void clickOpen(object sender, RoutedEventArgs e)
{
    var sub = new SubWindow();
    sub.VM = _vm;
    sub.ShowDialog();
}

実行に落ちることはなく、単にバインドに失敗します。

これ、実行時の出力を注意深くみると、実は「例外」が発生しています。

メイン画面のときに失敗したとき同じように、バインド時に例外が発生しているのですが、ShowDialogメソッド内で例外を握りつぶしているので、初期値が表示できない(バインドできない)現象だけが出てサブウィンドウ自体は表示されるという現象です。

所感

EF で作った自動生成のオブジェクトは get/set が揃っているので、この現象は発生しません。LINQ で読み込んだリストとかをグリッドにバインドするとか TextBox にバインドしても get/set があるので助かるのだけど。
計算結果の表示とか、なんらかの文字列を加工した後に表示するときに get だけの読み取り専用プロパティを作ったときにはまります。しかも、これ TextBlock の場合はもともと読み取り専用(表示専用)なので代用部なんだけど、TextBox のような読み書き用のコントールだけに当てはまる現象だという、結構レアなケースですが、覚えておくとハマりにくい穴かもしれません。

カテゴリー: 開発, C#, WPF パーマリンク