列挙型の値をラジオボタンにバインドするには?[ユニバーサルWindowsアプリ開発]WinRT/Metro TIPS

ラジオボタンで列挙型の値を選択してもらう場合に、データコンバーターを使って、各ボタンが持つブール値と列挙型の値との間で変換を行う方法を説明する。

» 2015年07月29日 05時00分 公開
[山本康彦BluewaterSoft/Microsoft MVP for Windows Platform Development]
WinRT/Metro TIPS
業務アプリInsider/Insider.NET

powered by Insider.NET

「WinRT/Metro TIPS」のインデックス

連載目次

 WindowsランタイムアプリでUIとロジックを分離する設計をしているとき、ラジオボタンの扱いで困ってしまったことはないだろうか? 例えば、ラジオボタンで列挙型の値を選択する場合である(次の画像を参照)。バインディングソースには列挙型の値だけを持ちたい。しかし、バインドしたいラジオボタンのプロパティは、チェックされているかどうかのブール値である。このミスマッチを解決するために、本稿ではデータコンバーターを工夫する方法を紹介しよう。なお、本稿のサンプルは「Windows Store app samples:MetroTips #110」からダウンロードできる。

別途公開のサンプルコードを実行している様子
別途公開のサンプルコードを実行している様子 別途公開のサンプルコードを実行している様子
上はWindows 8.1、下はWindows Phone 8.1エミュレーターで実行した画面である。
バインディングソースは列挙型のプロパティを持っていて、列挙値「Red」/「Green」/「Blue」/「None」のいずれかの値を取る。そのプロパティをラジオボタンにバインドして、ラジオボタンの選択状態([赤]/[緑]/[青])に反映させる。また、エンドユーザーがラジオボタンの選択状態を変更した結果がプロパティに書き戻されるようにする(双方向バインディング)。それはどのようにしたら実現できるだろうか? その方法の一つとして、本稿ではデータコンバーターを使う方法を解説する。
なお、画面の下半分はテスト用のUIである。[UserData]とあるテキストボックスには、バインディングソースの値が表示されている。[Change UserData]のところにある四つのボタンは、バインディングソースの値をボタンに表示されている値に変更する。本稿では、このUIの作り方は説明しない。別途公開のサンプルコードをご覧いただきたい。

事前準備

 ユニバーサルプロジェクトを使ってWindows 8.1/Windows Phone 8.1用のユニバーサルWindowsアプリを開発するには、以下の開発環境が必要である。本稿では、無償のVisual Studio Community 2013 with Update 4を使っている。

  • SLAT対応のPC*1
  • 2014年4月のアップデート*2適用済みの64bit版Windows 8.1 Pro版以上*3
  • Visual Studio 2013 Update 2(またはそれ以降)*4を適用済みのVisual Studio 2013(以降、VS 2013)*5

*1 SLAT対応ハードウエアは、Windows Phone 8.1エミュレーターの実行に必要だ。ただし未対応でも、ソースコードのビルドと実機でのデバッグは可能だ。SLAT対応のチェック方法はMSDNブログの「Windows Phone SDK 8.0 ダウンロードポイント と Second Level Address Translation (SLAT) 対応PCかどうかを判定する方法」を参照。なお、SLAT対応ハードウエアであっても、VM上ではエミュレーターが動作しないことがあるのでご注意願いたい。

*2 事前には「Windows 8.1 Update 1」と呼ばれていたアップデート。スタート画面の右上に検索ボタンが(環境によっては電源ボタンも)表示されるようになるので、適用済みかどうかは簡単に見分けられる。ちなみに公式呼称は「the Windows RT 8.1, Windows 8.1, and Windows Server 2012 R2 update that is dated April, 2014」というようである。

*3 Windows Phone 8.1エミュレーターを使用しないのであれば、32bit版のWindows 8.1でもよい。

*4 マイクロソフトのダウンロードページから誰でも入手できる(このURLはUpdate 5のもの)。

*5 本稿に掲載したコードを試すだけなら、無償のExpressエディションやCommunityエディションで構わない。Visual Studio Express 2013 for Windows Update 5(製品版)はマイクロソフトのページから無償で入手できる。Expressエディションはターゲットプラットフォームごとに製品が分かれていて紛らわしいが、Windowsランタイムアプリの開発には「for Windows」を使う(「for Windows Desktop」はデスクトップで動作するアプリ用)。また、Visual Studio Community 2013 with Update 5(製品版)もマイクロソフトのページ(ページ左側のメニューで[Visual Studio 2013]を選ぶ)から無償で入手できる。なお、英語版がインストールされた場合には、Microsoft Visual Studio 2013 Language Packの日本語版を追加インストールし、[オプション]ダイアログで言語を切り替える


サンプルコードについて

 Visual Studio 2013 Update 2(Update 3/4/5も)では、残念なことにVB用のユニバーサルプロジェクトのテンプレートは含まれていない*6。そのため、本稿で紹介するVBのコードはユニバーサルプロジェクトではなく、PCL(ポータブルクラスライブラリ)を使ったプロジェクトのものである。

*6 VB用のユニバーサルプロジェクトは、2015年の夏にリリースされるといわれているVisual Studio 2015(開発コード「Visual Studio 14」)からの提供となるようだ(Visual Studio 2015の本体は7月20日のリリースだが、ストアアプリ用の最新SDKのリリースは7月29日になるといわれている)。プレビュー版で、すでに共有プロジェクトは利用可能になっている。「特集:次期Visual Studioの全貌を探る:Visual Basic 14の新機能ベスト10〜もう「VBだから」とは言わせない!」参照。


バインディングソースの実装

 ラジオボタンにバインドするデータとして、次のようなコードを考えてみよう。バインドしたいプロパティは、「UserData」クラスの「FavoriteColor」プロパティだ。それは「FavoriteColor」列挙型の値を持っている。「UserData」クラスはINotifyPropertyChangedインターフェース(System.ComponentModel名前空間)を実装しているので、データの変化をUIに通知できる。

using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace MetroTips110CS
{
  // 列挙型:好きな色
  public enum FavoriteColor
  {
    None,
    Red,
    Green,
    Blue,
  } 

  // ユーザーの特性データ
  public class UserData : INotifyPropertyChanged
  {
    // INotifyPropertyChangedインターフェースの実装
    public event PropertyChangedEventHandler PropertyChanged;
    public void NotifyPropertyChanged([CallerMemberName] string propertyName = null)
    {
      var handler = PropertyChanged;
      if (handler != null)
        handler(this, new PropertyChangedEventArgs(propertyName));
    }

    // データバインド可能なプロパティ:好きな色
    private FavoriteColor _favouriteColor;
    public FavoriteColor FavoriteColor
    {
      get { return _favouriteColor; }
      set
      {
        if (_favouriteColor == value)
          return;

        if (value != FavoriteColor.None)
        {
          // 有効な色がセットされたときは、いったんNoneにして通知を出す
          // (ラジオボタンの動きを正しくするため)
          _favouriteColor = FavoriteColor.None;
          NotifyPropertyChanged();
        }
        _favouriteColor = value;
        NotifyPropertyChanged();
      }
    } 
  }
}

' 列挙体:好きな色
Public Enum FavoriteColor
  None
  Red
  Green
  Blue
End Enum

' ユーザーの特性データ
Public Class UserData
  Implements INotifyPropertyChanged

  ' INotifyPropertyChangedインターフェースの実装
  Public Event PropertyChanged(sender As Object, e As PropertyChangedEventArgs) _
    Implements INotifyPropertyChanged.PropertyChanged
  Public Sub NotifyPropertyChanged(
               <CallerMemberName> Optional propertyName As String = Nothing)
    RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
  End Sub

  ' データバインド可能なプロパティ:好きな色
  Private _favouriteColor As FavoriteColor
  Public Property FavoriteColor As FavoriteColor
    Get
      Return _favouriteColor
    End Get
    Set(value As FavoriteColor)
      If (_favouriteColor = value) Then
        Return
      End If

      If (value <> FavoriteColor.None) Then
        ' 有効な色がセットされたときは、いったんNoneにして通知を出す
        ' (ラジオボタンの動きを正しくするため)
        _favouriteColor = FavoriteColor.None
        NotifyPropertyChanged()
      End If
      _favouriteColor = value
      NotifyPropertyChanged()
    End Set
  End Property
End Class

ラジオボタンにバインドするデータの例(上:C#/下:VB)
この「UserData」クラスの「FavoriteColor」プロパティを、ラジオボタンのIsCheckedプロパティにバインドしたい。
このコードでは、FavoriteColorプロパティのセッター内でいったんFavoriteColor.Noneをセットして通知を出している(=NotifyPropertyChangedメソッドを呼び出している)。それからあらためて指定された色をセットし直して通知を出している。これはラジオボタンにバインドする都合によるものだ。ラジオボタンは、グループ内で選択状態になれるのは一つだけである。そのため、すでに選択状態になっているラジオボタンが存在するとき、他のラジオボタンをさらに選択状態にすることができない。先に選択状態になっているラジオボタンを非選択状態にしてから、目的のラジオボタンを選択状態に変える必要がある。それには、現在選択状態になっているラジオボタンを探し出して非選択状態に変えてもよいが、ここではFavoriteColor.Noneをセットすることで全てのラジオボタンを非選択状態にしているのだ。なお、このような「細工」も嫌だという場合は、ラジオボタンを組み込んだユーザーコントロールを作り、FavoriteColor列挙型の値を直接バインドできるようにするとよい(その場合はデータコンバーターを工夫する必要はなくなる)。

 この「UserData」クラスの「FavoriteColor」プロパティを、ラジオボタンのIsCheckedプロパティにバインドしたいのである(次の図)。テキストボックスにバインドしてプロパティの値を表示することや、ボタンのクリックイベントでプロパティを書き換えることは、問題なくできるだろう。しかし、ラジオボタンのIsCheckedプロパティに双方向バインドすることは可能なのだろうか?

実現したいデータバインディング 実現したいデータバインディング
「UserData」クラスの「FavoriteColor」プロパティをラジオボタンに双方向バインドしたい。例えば、「FavoriteColor」プロパティがFavoriteColor.Redのとき、[赤]のラジオボタンが選択状態になってほしい。また、そこでエンドユーザーが[緑]のラジオボタンを選んだときには、「FavoriteColor」プロパティがFavoriteColor.Greenに変わってほしい。

 なお参考までに付け加えておくと、このようなバインディングを実現するには、「UserData」クラスに「IsRedChecked」/「IsGreenChecked」/「IsBlueChecked」といったブール型のプロパティを実装することでも可能だ。ただし、かなり実装が面倒になる。また、ビュー(=画面)を抽象化したデータというには、ちょっと具体的に過ぎるだろう(上のコードでFavoriteColorプロパティのセッター内でラジオボタンの都合に合わせて実装した部分を嫌だと思えるならなおさらだ)。

データコンバーターを作る

 上の図のようなデータバインディングを実現するには、列挙型の値とブール値とを変換するデータコンバーターを作ればよい。

 例えば、プロパティ値がFavoriteColor.Redのとき、[赤]のラジオボタンに対してはTrueを、他のラジオボタンに対してはFalseを返すようなデータコンバーターが実現できればよいのである。それには、XAMLコードでバインディングを記述するときに指定できるConverterParameterオプションを利用する。XAMLコードでConverterParameterオプションに指定した値は、データコンバーターの変換メソッドに第3引数parameterとして渡される。このパラメーターに「Red」/「Green」/「Blue」のいずれかの文字列を渡すことにすると、そのようなデータコンバーター「EnumToBoolConverter」クラスは次のコードのように書ける。

using System;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Data;
namespace MetroTips110CS
{
  public class EnumToBoolConverter : IValueConverter
  {
    // プロパティ値→ブール値へ変換する
    public object Convert(object value, Type targetType, object parameter, string language)
    {
      // XAMLでのデータバインドの記述にあるパラメーターを文字列として取り出す
      // そこには、Enumのいずれかの値が入っているはずである
      string param = parameter as string;
      if (param == null)
        return DependencyProperty.UnsetValue;

      // パラメーターを列挙型に変換する(例:"Red"→Enum.Red)
      object paramValue = Enum.Parse(typeof(FavoriteColor), param);

      // バインディングソースの値(value)とパラメーターが等しかったらtrueを返す
      return paramValue.Equals(value); 
    }

    // ブール値→プロパティ値へ逆変換する
    public object ConvertBack(object value, Type targetType, object parameter, string language)
    {
      string param = parameter as string;
      if (parameter == null)
        return DependencyProperty.UnsetValue;

      // バインディングソースに書き戻すときは、パラメーター文字列に見合った列挙型を返せばよい
      return Enum.Parse(typeof(FavoriteColor), param); 
    }
  }
}

Public Class EnumToBoolConverter
  Implements IValueConverter

  ' プロパティ値→ブール値へ変換する
  Public Function Convert(value As Object, targetType As Type, parameter As Object, language As String) As Object Implements IValueConverter.Convert
    ' XAMLでのデータバインドの記述にあるパラメーターを文字列として取り出す
    ' そこには、Enumのいずれかの値が入っているはずである
    Dim param As String = parameter
    If (param Is Nothing) Then
      Return DependencyProperty.UnsetValue
    End If

    ' パラメーターを列挙型に変換する(例:"Red"→Enum.Red)
    Dim paramValue As Object = FavoriteColor.Parse(GetType(FavoriteColor), param)

    ' バインディングソースの値(value)とパラメーターが等しかったらtrueを返す
    Return paramValue.Equals(value)
  End Function

  ' ブール値→プロパティ値へ逆変換する
  Public Function ConvertBack(value As Object, targetType As Type, parameter As Object, language As String) As Object Implements IValueConverter.ConvertBack
    Dim param As String = parameter
    If (parameter Is Nothing) Then
      Return DependencyProperty.UnsetValue
    End If

    ' バインディングソースに書き戻すときは、パラメーター文字列に見合った列挙型を返せばよい
    Return FavoriteColor.Parse(GetType(FavoriteColor), param)
  End Function
End Class

列挙型をラジオボタンにバインドするためのデータコンバーター(上:C#/下:VB)

データバインドする

 以上で作成した「UserData」クラスと「EnumToBoolConverter」クラスを使うと、XAMLコードのデータバインディングする部分は次のコードのように書ける。

<Grid ……省略……>
  <Grid.Resources>
    <local:UserData x:Key="userData" />
    <local:EnumToBoolConverter x:Key="E2B" />
  </Grid.Resources>
  <StackPanel DataContext="{StaticResource userData}" ……省略……>
    <TextBlock FontSize="24" FontFamily="Yu Gothic">好きな色は?</TextBlock>
    <RadioButton
      IsChecked="{Binding Path=FavoriteColor, Converter={StaticResource E2B},
                  ConverterParameter=Red, Mode=TwoWay}"
      Foreground="Red"></RadioButton>
    <RadioButton
      IsChecked="{Binding Path=FavoriteColor,Converter={StaticResource E2B},
                  ConverterParameter=Green, Mode=TwoWay}"
      Foreground="Green"></RadioButton>
    <RadioButton
      IsChecked="{Binding Path=FavoriteColor,Converter={StaticResource E2B},
                  ConverterParameter=Blue, Mode=TwoWay}"
      Foreground="Blue"></RadioButton>
    ……省略……
  </StackPanel>
</Grid>

「UserData」オブジェクトをラジオボタンにバインドする(XAML)
バインディングの記述において、「Converter」でデータコンバーターのインスタンスを指定し、データコンバーターに渡すパラメーターを「ConverterParameter」として指定する。

 以上で完成である。これで、図「実現したいデータバインディング」で示した動きが実現できたはずだ。

まとめ

 列挙型などのプロパティはラジオボタンの選択状態に直接バインドできない。また、ラジオボタンの選択状態をコードから変えようとすると意外に厄介だ。その解決策として、本稿ではデータコンバーターを使う方法を解説した。

「WinRT/Metro TIPS」のインデックス

WinRT/Metro TIPS

Copyright© Digital Advantage Corp. All Rights Reserved.

RSSについて

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

メールマガジン登録

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