WPF:ラジオボタンの選択をバインディングソースに反映させるには?[C#/VB].NET TIPS

WPFアプリで双方向データバインディングを使用して、ラジオボタンの選択状態を反映する方法、その問題点と解決策を解説する。

» 2016年01月27日 05時00分 公開
[山本康彦BluewaterSoft/Microsoft MVP for Windows Development]
.NET TIPS
Insider.NET

 

「.NET TIPS」のインデックス

連載目次

対象:.NET 3.5以降


 UIコントロールの状態の変化をデータに反映させたい場合がある。そんなとき、XAMLでは双方向のデータバインディングを利用するのが便利だ。例えば、エンドユーザーがテキストボックスに入力した文字列は、データバインディングによってバインディングソースの文字列型プロパティに自動的に反映させられる。あるいは、スライダーの「つまみ」の位置を浮動小数点数のプロパティに反映できる。では、ラジオボタンの選択状態を列挙型や整数型のプロパティに反映できないだろうか? その主な方法には2通りあるのだが、本稿では簡単な方を解説する。

 なお、本稿のサンプルは「Windows desktop code samples:.NET Tips #1126」からダウンロードできる*1

*1 本稿の内容はUWPアプリにも適用できる。ただし、後述するようにデータの変更をラジオボタンに反映できないという欠点がある。UWPアプリでは、データバインディングにコンバータを介する方法を使うべきである。そのため、UWPアプリについては本稿でも扱わないし、サンプルコードにも含めていない。


ラジオボタンの選択をデータに反映させる2通りの方法

 RadioButtonコントロール(System.Windows.Controls名前空間)の選択状態は、真偽値型のIsCheckedプロパティで示される。例えば、三つの選択肢に対応した三つのラジオボタンがあるとする。しかしそこには「三つの選択肢のどれが選択されているか」を表すプロパティは存在しない。あるのは、三つのIsCheckedプロパティだけなのだ。三つの状態を取り得る列挙型のデータとバインドしたくても、直接バインドすることはできない。

 そこで考えられる解決策の一つは、三つの選択肢に対応した三つ真偽値型プロパティをデータの側にも用意する方法だ。考え方は簡単ではあるが、データの変更をラジオボタンに反映させる方法に難があるという弱点がある。本稿では、この方法を解説する。

 もう一つは、.NET 4以降に限定されるが、データバインディングにコンバータを介する方法だ。こちらは別稿に譲りたい。

例題

 例として、次のコードのようなデータを考えてみよう。三つの状態を取れる列挙型のプロパティを持っているクラスだ。

public enum SampleEnum
{
  Undefined,
  One = 1, Two, Three,
}

public class SampleData : System.ComponentModel.INotifyPropertyChanged
{
  private SampleEnum _value; // One,Two,Three
  public SampleEnum Value
  {
    get { return _value; }
    set 
    {
      if (_value == value)
        return;

      if (!Enum.IsDefined(typeof(SampleEnum),value))
        throw new ArgumentOutOfRangeException();

      var handler = PropertyChanged;
      _value = value;
      if (handler != null)
      {
        handler.Invoke(this,
          new System.ComponentModel.PropertyChangedEventArgs("Value"));
      }
    }
  }

  #region INotifyPropertyChanged メンバー
  public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;
  #endregion
}

Public Enum SampleEnum
  Undefined
  One = 1
  Two
  Three
End Enum

Public Class SampleData
  Implements System.ComponentModel.INotifyPropertyChanged

  Private _value As SampleEnum ' One,Two,Three
  Public Property Value As SampleEnum
    Get
      Return _value
    End Get
    Set(value As SampleEnum)
      If (_value = value) Then
        Return
      End If

      If (Not [Enum].IsDefined(GetType(SampleEnum), value)) Then
        Throw New ArgumentOutOfRangeException()
      End If

      _value = value
      RaiseEvent PropertyChanged(Me,
                 New System.ComponentModel.PropertyChangedEventArgs("Value"))
    End Set
  End Property

#Region "INotifyPropertyChanged メンバー"
  Public Event PropertyChanged(sender As Object,
                               e As ComponentModel.PropertyChangedEventArgs) _
               Implements ComponentModel.INotifyPropertyChanged.PropertyChanged
#End Region
End Class

データを表す列挙体とクラス(上:C#、下:VB)
「SampleData」クラスには、UIコントロールとデータバインディングするために、INotifyPropertyChangedインタフェース(System.ComponentModel名前空間)を実装してある。

 このデータをテキストブロックにバインドして「One」「Two」「Three」と表示するのは簡単だ。テキストブロックのTextプロパティにデータバインディングするだけである(次のコード)。

<StackPanel x:Name="RadioButtonGroup1" ……省略…… >
  <TextBlock ……省略…… Text="{Binding Value}" />
</StackPanel>

データをバインドするテキストブロック(XAML)
ここでは、スタックパネルの中にテキストブロックを配置した。このスタックパネルには、次のコードでデータコンテキストにデータを設定する。

public MainWindow()
{
  InitializeComponent();

  // データバインディングする
  RadioButtonGroup1.DataContext = new SampleData();
}

Public Sub New()
  InitializeComponent()

  ' データバインディングする
  RadioButtonGroup1.DataContext = New SampleData()
End Sub

「SampleData」インスタンスをデータバインディングする(上:C#、下:VB)
ウィンドウのコンストラクタに、太字の部分を追加する。

 では、ラジオボタンの状態をデータバインディングしてみよう。前述したように、データの側にも三つの選択肢に対応した三つの真偽値型プロパティを用意すればよい(次のコード)。

public class SampleData : System.ComponentModel.INotifyPropertyChanged
{
  ……省略……

  public SampleEnum Value
  {
    get { return _value; }
    set 
    {
      ……省略……
      var handler = PropertyChanged;
      _value = value;
      if (handler != null)
      {
        handler.Invoke(this,
          new System.ComponentModel.PropertyChangedEventArgs("Value"));
        handler.Invoke(this,
          new System.ComponentModel.PropertyChangedEventArgs("Is1"));
        handler.Invoke(this,
          new System.ComponentModel.PropertyChangedEventArgs("Is2"));
        handler.Invoke(this,
          new System.ComponentModel.PropertyChangedEventArgs("Is3"));
      }
    }
  }

  public bool Is1 
  {
    get { return Value == SampleEnum.One; }
    set
    {
      if (value)
        Value = SampleEnum.One;
      else
        Value = SampleEnum.Undefined;
    }
  }

  ……省略……
}

Public Class SampleData
  Implements System.ComponentModel.INotifyPropertyChanged
  ……省略……

  Public Property Value As SampleEnum
    Get
      Return _value
    End Get
    Set(value As SampleEnum)
      ……省略……

      _value = value
      RaiseEvent PropertyChanged(Me,
                 New System.ComponentModel.PropertyChangedEventArgs("Value"))
      RaiseEvent PropertyChanged(Me,
                 New System.ComponentModel.PropertyChangedEventArgs("Is1"))
      RaiseEvent PropertyChanged(Me,
                 New System.ComponentModel.PropertyChangedEventArgs("Is2"))
      RaiseEvent PropertyChanged(Me,
                 New System.ComponentModel.PropertyChangedEventArgs("Is3"))
    End Set
  End Property

  Public Property Is1 As Boolean
    Get
      Return Value = SampleEnum.One
    End Get
    Set(value As Boolean)
      If (value) Then
        Me.Value = SampleEnum.One
      Else
        Me.Value = SampleEnum.Undefined
      End If
    End Set
  End Property

  ……省略……
End Class

データに真偽値型プロパティを三つ追加する(上:C#、下:VB)
追加した三つのプロパティのうち、「Is1」プロパティのみを示す。同様にして、「Is2」「Is3」も実装する。また、太字で示したように、「Value」プロパティのセッターにもコードを追加する。
コードの全体は、別途公開のサンプルをご覧いただきたい。

 そして、次のコードのようにラジオボタンとバインドする。

<StackPanel x:Name="RadioButtonGroup1" ……省略…… >
  <RadioButton Tag="1" Content="選択肢1" IsChecked="{Binding Is1}" />
  <RadioButton Tag="2" Content="選択肢2" IsChecked="{Binding Is2}" />
  <RadioButton Tag="3" Content="選択肢3" IsChecked="{Binding Is3}" />
  ……省略……
  <TextBlock ……省略…… />
  ……省略……
</StackPanel>

データをバインドするラジオボタン(XAML)
前述したテキストブロックと同じスタックパネル内に、ラジオボタンを三つ追加した。
コードの全体は、別途公開のサンプルをご覧いただきたい。

 これで実行してみると次の画像のようになる。ラジオボタンの選択を変えるとデータに反映され、その値がデータバインディングでテキストブロックに表示されている。

ラジオボタンの選択を変えるとデータに反映される ラジオボタンの選択を変えるとデータに反映される
この画像は、[選択肢2]のラジオボタンをクリックしたところ。その変化がバインドされている「SampleData」クラスに反映され、その結果として「SampleData」クラスをバインドしているテキストブロックの表示も「Two」に変化している。

この方法の欠点

 この方法は簡単ではあるが、このままではデータの変更をラジオボタンに反映できない。

 画面にボタンを追加し、そのクリックイベントでデータを変更するようにしてみよう(次のコード)。

<StackPanel x:Name="RadioButtonGroup1" ……省略…… >
  <RadioButton Tag="1" Content="選択肢1" IsChecked="{Binding Is1}" />
  ……省略……
  <TextBlock ……省略…… />
  ……省略……
  <StackPanel Orientation="Horizontal" Margin="0,16,0,0">
    <Button Click="SelectButton1_Click" Tag="1">1</Button>
    <Button Click="SelectButton1_Click" Tag="2">2</Button>
    <Button Click="SelectButton1_Click" Tag="3">3</Button>
  </StackPanel>
</StackPanel>

データ変更用のボタン(XAML)
前述のコードに太字の部分を追加した。

private void SelectButton1_Click(object sender, RoutedEventArgs e)
{
  string tag = (e.Source as Button).Tag as string;
  SampleEnum value
    = (SampleEnum)Enum.Parse(typeof(SampleEnum), tag);
  (RadioButtonGroup1.DataContext as SampleData).Value = value;
}

Private Sub SelectButton1_Click(sender As Object, e As RoutedEventArgs)
  Dim tag As String = CType(CType(e.Source, Button).Tag, String)
  Dim value As SampleEnum _
    = CType([Enum].Parse(GetType(SampleEnum), tag), SampleEnum)
  CType(RadioButtonGroup1.DataContext, SampleData).Value = value
End Sub

クリックされたボタンに応じてデータを変更する(上:C#、下:VB)
ボタンのTagプロパティには「1」「2」「3」という数字が設定されている。ボタンクリックのイベントハンドラ(=このコード)で、Tagプロパティの値を「SampleEnum」列挙型に変換し、データバインドされている「SampleData」クラスの「Value」プロパティにセットする。
これで、ボタンに応じた値にデータが変更されると、そのデータの変化がラジオボタンに反映されるはずなのだ(実際には、次に述べるような不具合が発生する)。

 実行してみると、次のような不具合が現れるはずだ。ボタンを[1]→[2]→[3]とクリックするだけでなく、[2]→[1]や[1]→[3]などと、しばらく操作を続けてみてほしい。

  • しばらく操作を続けるとバインディングが機能しなくなる(.NET 3.5)
  • 選択されているものより上の選択肢を1回で選択状態にできない(.NET 4.0以降)

問題の回避策

 この問題を回避するには、データを切り替えるときに、いったん範囲外の値にすればよい(次のコード)。

public SampleEnum Value
{
  get { return _value; }
  set 
  {
    ……省略……
    var handler = PropertyChanged;

    // いったん範囲外の値にする
    _value = SampleEnum.Undefined;
    if (handler != null)
    {
      handler.Invoke(this,
        new System.ComponentModel.PropertyChangedEventArgs("Is1"));
      handler.Invoke(this,
        new System.ComponentModel.PropertyChangedEventArgs("Is2"));
      handler.Invoke(this,
        new System.ComponentModel.PropertyChangedEventArgs("Is3"));
    }

    _value = value;
    if (handler != null)
    {
      handler.Invoke(this,
        new System.ComponentModel.PropertyChangedEventArgs("Value"));
      ……省略……
    }
  }
}

Public Property Value As SampleEnum
  Get
    Return _value
  End Get
  Set(value As SampleEnum)
    ……省略……

    ' いったん範囲外の値にする
    _value = SampleEnum.Undefined
    RaiseEvent PropertyChanged(Me,
               New System.ComponentModel.PropertyChangedEventArgs("Is1"))
    RaiseEvent PropertyChanged(Me,
               New System.ComponentModel.PropertyChangedEventArgs("Is2"))
    RaiseEvent PropertyChanged(Me,
               New System.ComponentModel.PropertyChangedEventArgs("Is3"))

    _value = value
    RaiseEvent PropertyChanged(Me,
               New System.ComponentModel.PropertyChangedEventArgs("Value"))
    ……省略……
  End Set
End Property

不具合を回避するコード(上:C#、下:VB)
「SampleData」クラスの「Value」プロパティのセッターに、太字の部分を追記する。

 これでデータの変更が正しくラジオボタンに反映されるようになる。

 とはいうものの、データの側でバインディング先のコントロールの都合を考慮するというのはおかしなものだ。上の回避策はバインディング先がラジオボタンだから必要なのであって、例えばチェックボックスならば不要なのだ。そこが気になるようであれば、コンバータを使う方法を検討してほしい(ただし、.NET 4以降)。

まとめ

 ラジオボタンの選択状態をデータに反映させるには、データバインディングを使う。簡易的には、ラジオボタンの選択状態に対応する真偽値のプロパティをデータのクラスに追加すればよい。ただし、この方法では逆にデータの変化をラジオボタンの選択状態に反映させようとすると、不具合の回避策が必要になる。

利用可能バージョン:.NET Framework 3.5以降
カテゴリ:WPF 処理対象:データバインディング
カテゴリ:WPF/XAML 処理対象:RadioButtonコントロール
使用ライブラリ:RadioButtonコントロール(System.Windows.Controls名前空間)
使用ライブラリ:INotifyPropertyChangedインタフェース(System.ComponentModel名前空間)
関連TIPS:WPF/UWP:ラジオボタンの選択をコードから切り替えるには?[C#/VB]


「.NET TIPS」のインデックス

.NET TIPS

Copyright© Digital Advantage Corp. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。