[WPF] RadioButton の IsChecked バインディングがうまく動かない

Pocket

ラジオボタンのデータバインディングを、チェックボタンと同様にやっているはずなのに、なぜか途中で動かなくなったりして、かなり詰まっていた。
解決方法…というか、回避策をメモしておこう。

問題

ラジオボタンのうち、同じグループの別のラジオボタンにチェックが入り、地震のラジオボタンのチェックが外れる段階で、どうもバインドが外れてしまうらしい。

下のような XAML を起動してみるとすぐわかる。

<StackPanel>
    <RadioButton Name="rdo01" Content="01" IsChecked="{Binding ElementName = chk01, Path = IsChecked}" />
    <RadioButton Name="rdo02" Content="02" IsChecked="{Binding ElementName = chk02, Path = IsChecked}" />
    <RadioButton Name="rdo03" Content="03" IsChecked="{Binding ElementName = chk03, Path = IsChecked}" />
    <CheckBox Name="chk01" Content="01" />
    <CheckBox Name="chk02" Content="02" />
    <CheckBox Name="chk03" Content="03" />
</StackPanel>

rdo01 の RadioButton にチェックをつけると、バインドされている chk01 にもチェックが付くが、rdo02 にチェックが付いて、rdo01 のチェックが外れるのに、chk01 のチェックが外れない。
こうなると、手動で chk01 のチェックを外した後、rdo01 のチェックを再びつけ直しても、chk01 のチェックが付くことはなくなってしまう。
要は、バインドが切れてしまっているのだ。

原因

ネットでいろいろ調べていたら、同じ現象で困っている人を発見。
TabControlとRadioButtonをBinding生成するとRadioButtonのIsChecked値が突然すべてFalseに・・・

スレッドのレスを読んでいると、原因は

つまり、 同じグループ内の RadioButton で IsChecked が True になっているものが、 一瞬でも複数存在する状態になると、 バインディングが 「外れる」みたいです。

↑これっぽい。

欠陥じゃないのかこれ…

対策案1

レスでは、対策案として、

外観をRadioButton風にしたCheckBoxにする。

なんてのも出ているが、外観を RadioButton 風に書き直すのが面倒だし、ほかの対策案を考えてみる。

要は、同じ RadioButton グループ内で、同時に複数 IsChecked フラグが立っていなければよいので、Checked イベント捕まえて手動でほかのラジオボタンの IsChecked をおろしてしまってはどうだろうか。

具体的には、以下の様な感じ

<StackPanel>
    <RadioButton Name="rdo01" Content="01" IsChecked="{Binding ElementName = chk01, Path = IsChecked}" Checked="rdo01_Checked" />
    <RadioButton Name="rdo02" Content="02" IsChecked="{Binding ElementName = chk02, Path = IsChecked}" Checked="rdo02_Checked" />
    <RadioButton Name="rdo03" Content="03" IsChecked="{Binding ElementName = chk03, Path = IsChecked}" Checked="rdo03_Checked" />
    <CheckBox Name="chk01" Content="01" />
    <CheckBox Name="chk02" Content="02" />
    <CheckBox Name="chk03" Content="03" />
</StackPanel>
private void rdo01_Checked(object sender, RoutedEventArgs e) {
    if (((RadioButton)sender).IsChecked == true) {
        rdo02.IsChecked = false;
        rdo03.IsChecked = false;
    }
}

private void rdo02_Checked(object sender, RoutedEventArgs e) {
    if (((RadioButton)sender).IsChecked == true) {
        rdo01.IsChecked = false;
        rdo03.IsChecked = false;
    }
}

private void rdo03_Checked(object sender, RoutedEventArgs e) {
    if (((RadioButton)sender).IsChecked == true) {
        rdo01.IsChecked = false;
        rdo02.IsChecked = false;
    }
}

結果を言うと、失敗
Checked イベントが発行されたときにはすでにバインディングが外れてしまっているようだ。
まぁ、チェックが付いてしまっているのだからそりゃそうか…

対策案2

ならば、RadioButton の IsChecked が更新される前、バインドされた方の値の変更イベントをキャッチしてみる。
バインドされた CheckBox の Checked イベント → 変更した RadioButton の Checked イベント
の順に呼ばれるからだ。

<StackPanel>
    <RadioButton Name="rdo01" Content="01" IsChecked="{Binding ElementName = chk01, Path = IsChecked}" />
    <RadioButton Name="rdo02" Content="02" IsChecked="{Binding ElementName = chk02, Path = IsChecked}" />
    <RadioButton Name="rdo03" Content="03" IsChecked="{Binding ElementName = chk03, Path = IsChecked}" />
    <CheckBox Name="chk01" Content="01" Checked="chk01_Checked" />
    <CheckBox Name="chk02" Content="02" Checked="chk02_Checked" />
    <CheckBox Name="chk03" Content="03" Checked="chk03_Checked" />
</StackPanel>
private void chk01_Checked(object sender, RoutedEventArgs e) {
    if (((CheckBox)sender).IsChecked == true) {
        rdo02.IsChecked = false;
        rdo03.IsChecked = false;
    }
}

private void chk02_Checked(object sender, RoutedEventArgs e) {
    if (((CheckBox)sender).IsChecked == true) {
        rdo01.IsChecked = false;
        rdo03.IsChecked = false;
    }
}

private void chk03_Checked(object sender, RoutedEventArgs e) {
    if (((CheckBox)sender).IsChecked == true) {
        rdo01.IsChecked = false;
        rdo02.IsChecked = false;
    }
}

こうすることで、見事にバインドが外れなくなった!
ちゃんとほかの RadioButton にチェックが入ると、それと連動して RadioButton と CheckBox のチェックが外れる。

対策案3

上記は要素同士のバインドの場合だったが、データとのバインドをする場合についても書いておく。
チェックプロパティとバインドするために、 System.ComponentModel.INotifyPropertyChanged インターフェースを実装した、rdoData クラスを作成し、その IsChecked プロパティが true に変更された際、ほかの RadioButton のチェックを外すようにする。

class rdoData : INotifyPropertyChanged {
    /// <summary>INotifyPropertyChanged インターフェース実装</summary>
    public event PropertyChangedEventHandler PropertyChanged;
    void NotifyPropertyChanged(string propName){
        if(PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(propName));
    }

    /// <summary>チェック変更イベント</summary>
    public event Action<bool> OnIsChecked;

    /// <summary>チェックプロパティ</summary>
    public bool IsChecked {
        set {
            _isChecked = value;
            if (OnIsChecked != null) OnIsChecked(value);
            NotifyPropertyChanged("IsChecked");
        }
        get { return _isChecked; }
    }
    private bool _isChecked;

}

private void Window_Loaded(object sender, RoutedEventArgs e) {
    var tgglList = new[] { new {rdo = rdo01, chk = chk01}, new {rdo = rdo02, chk = chk02}, new{rdo = rdo03, chk = chk03} };

    // バインドと、データ変更イベントハンドラの設定
    foreach (var tggl in tgglList) {
        var data = new rdoData();

        // デリゲートで使用の為、ループ内スコープで宣言
        var curRdo = tggl.rdo;
        var curChk = tggl.chk;

        // ラジオボタンとバインドする
        var bind = new Binding("IsChecked") { Source = data };
        curRdo.SetBinding(RadioButton.IsCheckedProperty, bind);

        // データ変更イベントハンドラの設定
        data.OnIsChecked += newvalue => {
            if (newvalue) {
                // 自身以外の RadioButton のチェックを外す
                tgglList
                    .Select(i => i.rdo)
                    .Where(r => r != curRdo)
                    .ToList().ForEach(r => {
                        r.IsChecked = false;
                    });
            }

            // 目視確認できるように、CheckBox にも表示
            curChk.IsChecked = newvalue;
        };
    }
}

結論

RadioButton グループに、一瞬でも IsCHecked == true となる RadioButton が2つ存在しないように、イベントを捕まえてあげればよいようだ。

コメントを残す

メールアドレスが公開されることはありません。

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください