WPF:ラジオボタンの選択をバインディングソースに反映させるには?[C#/VB]:.NET TIPS
WPFアプリで双方向データバインディングを使用して、ラジオボタンの選択状態を反映する方法、その問題点と解決策を解説する。
対象:.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
「SampleData」クラスには、UIコントロールとデータバインディングするために、INotifyPropertyChangedインタフェース(System.ComponentModel名前空間)を実装してある。
このデータをテキストブロックにバインドして「One」「Two」「Three」と表示するのは簡単だ。テキストブロックのTextプロパティにデータバインディングするだけである(次のコード)。
<StackPanel x:Name="RadioButtonGroup1" ……省略…… >
<TextBlock ……省略…… Text="{Binding Value}" />
</StackPanel>
ここでは、スタックパネルの中にテキストブロックを配置した。このスタックパネルには、次のコードでデータコンテキストにデータを設定する。
public MainWindow()
{
InitializeComponent();
// データバインディングする
RadioButtonGroup1.DataContext = new SampleData();
}
Public Sub New()
InitializeComponent()
' データバインディングする
RadioButtonGroup1.DataContext = New SampleData()
End Sub
ウィンドウのコンストラクタに、太字の部分を追加する。
では、ラジオボタンの状態をデータバインディングしてみよう。前述したように、データの側にも三つの選択肢に対応した三つの真偽値型プロパティを用意すればよい(次のコード)。
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
追加した三つのプロパティのうち、「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>
前述したテキストブロックと同じスタックパネル内に、ラジオボタンを三つ追加した。
コードの全体は、別途公開のサンプルをご覧いただきたい。
これで実行してみると次の画像のようになる。ラジオボタンの選択を変えるとデータに反映され、その値がデータバインディングでテキストブロックに表示されている。
ラジオボタンの選択を変えるとデータに反映される
この画像は、[選択肢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>
前述のコードに太字の部分を追加した。
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
ボタンの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
「SampleData」クラスの「Value」プロパティのセッターに、太字の部分を追記する。
これでデータの変更が正しくラジオボタンに反映されるようになる。
とはいうものの、データの側でバインディング先のコントロールの都合を考慮するというのはおかしなものだ。上の回避策はバインディング先がラジオボタンだから必要なのであって、例えばチェックボックスならば不要なのだ。そこが気になるようであれば、コンバータを使う方法を検討してほしい(ただし、.NET 4以降)。
まとめ
ラジオボタンの選択状態をデータに反映させるには、データバインディングを使う。簡易的には、ラジオボタンの選択状態に対応する真偽値のプロパティをデータのクラスに追加すればよい。ただし、この方法では逆にデータの変化をラジオボタンの選択状態に反映させようとすると、不具合の回避策が必要になる。
利用可能バージョン:.NET Framework 3.5以降
カテゴリ:WPF 処理対象:データバインディング
カテゴリ:WPF/XAML 処理対象:RadioButtonコントロール
使用ライブラリ:RadioButtonコントロール(System.Windows.Controls名前空間)
使用ライブラリ:INotifyPropertyChangedインタフェース(System.ComponentModel名前空間)
関連TIPS:WPF/UWP:ラジオボタンの選択をコードから切り替えるには?[C#/VB]
Copyright© Digital Advantage Corp. All Rights Reserved.