Windows Phone

Windows Phone のデータ バインド

Jesse Liberty

コード サンプルのダウンロード

事実上、意味のある Windows Phone アプリにはすべてなんらかの種類のデータが含まれるため、そのデータを UI (ビュー) の要素に関連付ける機能は不可欠です。プログラムから (ユーザーの処理に合わせて値を割り当て) データを UI 要素に関連付ける方法はたくさんありますが、XAML プログラミングの強力かつ不可欠な機能の 1 つとして、データをコントロールにバインドする機能があります。

これには、次のメリットがあります。

  • 簡単に理解できる
  • 簡単に実装できる

データ バインドの例を示すため、図1 に示すページを作成しましょう。このページが読み込まれるときに、これから作成する Person オブジェクトの値が設定されます。UI の各値を Person オブジェクトのプロパティにバインドするだけで、コントロールへのデータの実際のバインドが自動的に行われます。つまり、このために C# コードは必要ありません。

The Data Binding View in Windows Phone
図 1 データ バインドによる Windows Phone の表示

作業の開始

まず、Visual Studio で新しい Windows Phone アプリを作成し、「DataBinding」という名前を付けます。次に、バインド先のデータとして機能するクラス (DataContext とも呼びます) を作成します。プロジェクトを右クリックし、[追加]、[新規作成]、[クラス] の順にクリックして、クラスに「Person.cs」という名前を付けます。

Person には、(少なくとも) ビューに表示するプロパティをすべて含めます。クラスは、1 つの列挙子と一連の自動プロパティから構成します (図 2 参照)。

図 2 Person クラス

public class Person
{
  public enum Sex
  {
    Male,
    Female,
  }
  public string Name { get; set; }
  public bool Moustache { get; set; }
  public bool Goatee { get; set; }
  public bool Beard { get; set; }
  public Sex WhichSex { get; set; }
  public double Height { get; set; }
  public DateTime BirthDate { get; set; }
  public bool Favorite { get; set; }
}

これらのプロパティが、図 1 に示すどのコントロールにマップされるかは簡単に把握できます。ブール値は、その項目が相互に排他的かどうかに応じて、CheckBoxes または RadioButtons のいずれかになります。

フォームを作成する

次は、データ バインドを使用するフォームを作成します。プロジェクトを右クリックし、[Expression Blend で開く] を選択します。通常、UI は Expression Blend で作成し、コードは Visual Studio で作成します。

6 行 2 列のコンテンツ グリッドを作成し、該当する入力コントロールにドラッグします。図 3 に、生成する XAML を示します。

図 3 フォームを作成する XAML

<Grid
  x:Name="ContentPanel"
  Grid.Row="1"
  Margin="24,0,0,0">
  <Grid.ColumnDefinitions>
    <ColumnDefinition
      Width="0.384*" />
    <ColumnDefinition
      Width="0.616*" />
  </Grid.ColumnDefinitions>
  <Grid.RowDefinitions>
    <RowDefinition
      Height="0.1*" />
    <RowDefinition
      Height="0.1*" />
    <RowDefinition
      Height="0.1*" />
    <RowDefinition
      Height="0.1*" />
    <RowDefinition
      Height="0.1*" />
    <RowDefinition
      Height="0.1*" />
    <RowDefinition
      Height="0.2*" />
  </Grid.RowDefinitions>
  <TextBlock
    x:Name="NamePrompt"
    TextWrapping="Wrap"
    Text="Name"
    Grid.Row="0"
    HorizontalAlignment="Left"
    VerticalAlignment="Center" />
  <TextBlock
    x:Name="SexPrompt"
    Grid.Row="2"
    TextWrapping="Wrap"
    HorizontalAlignment="Left"
    VerticalAlignment="Center"
    Text="Sex" />
  <TextBlock
    x:Name="HeightPrompt"
    TextWrapping="Wrap"
    Text="Height, StringFormat=F3"
    HorizontalAlignment="Left"
    Grid.Row="3"
    d:LayoutOverrides="Height"
    VerticalAlignment="Center" />
  <TextBlock
    x:Name="FavoritePrompt"
    TextWrapping="Wrap"
    Text="Favorite"
    HorizontalAlignment="Left"
    Grid.Row="4"
    d:LayoutOverrides="Height"
    VerticalAlignment="Center" />
  <TextBox
    x:Name="Name"
    TextWrapping="Wrap"
    d:LayoutOverrides="Height"
    Grid.Column="1"
    HorizontalAlignment="Left"
    Width="200"
    VerticalAlignment="Center"
    Text="{Binding Name}" />
  <StackPanel
    x:Name="BeardStackPanel"
    Grid.ColumnSpan="2"
    Grid.Row="1"
    Orientation="Horizontal">
    <CheckBox
      x:Name="Moustache"
      Content="Moustache"
      HorizontalAlignment="Left"
      VerticalAlignment="Center"
      IsChecked="{Binding Moustache}" />
    <CheckBox
      x:Name="Goatee"
      Content="Goatee"
      IsChecked="{Binding Goatee}" />
    <CheckBox
      Content="Beard"
      IsChecked="{Binding Beard}"/>
  </StackPanel>
  <StackPanel
    x:Name="SexStackPanel"
    Grid.Column="1"
    Grid.Row="2"
    Orientation="Horizontal">
    <RadioButton
      x:Name="Male"
      Content="Male"
      IsChecked="True"
      GroupName="Sex" />
    <RadioButton
      x:Name="Female"
      Content="Female"
      GroupName="Sex" />
  </StackPanel>
  <StackPanel
    x:Name="HeightStackPanel"
    Grid.Column="1"
    Grid.Row="3"
    Orientation="Horizontal">
    <TextBlock
      TextWrapping="Wrap"
      Text="{Binding Height}"
      VerticalAlignment="Center"
      HorizontalAlignment="Left"
      Margin="0,0,0,0" />
    <TextBlock
      VerticalAlignment="Center"
      HorizontalAlignment="Left"
      Margin="5,0,0,0"
      Text="meters" />
  </StackPanel>
  <ToggleButton
    x:Name="Favorite"
    Content="Favorite"
    Grid.Column="1"
    Grid.Row="4"
    d:LayoutOverrides="Width, Height"
    HorizontalAlignment="Left"
    VerticalAlignment="Center"
    IsChecked="{Binding Favorite}" />
</Grid>

バインドする

この時点で、各テキスト入力フィールドにはバインド構文を使用して設定された値が含まれるようになります。たとえば、TextBox へのバインドを指示するには、その属性の中でデータが必要なもの (この場合は Text 属性) を特定して、前述のようにバインド構文を使用します。

バインドは中括弧内に収め、Binding というキーワードを指定します。通常、キーワードの後には属性のバインド先となるプロパティの名前を続けます。たとえば、次の XAML では TextBox の Text を Name という名前のパブリック プロパティから取得することを示します。

<TextBox
  x:Name="Name"
  TextWrapping="Wrap"
  d:LayoutOverrides="Height"
  Grid.Column="1"
  HorizontalAlignment="Left"
  Width="200"
  VerticalAlignment="Center"
  Text="{Binding Name}" />

同様に、顔ひげ用のチェックボックスの場合、該当するプロパティに IsChecked プロパティをバインドします。

<CheckBox
  x:Name="Moustache"
  Content="Moustache"
  HorizontalAlignment="Left"
  VerticalAlignment="Center"
  IsChecked="{Binding Moustache}" />

どのオブジェクトがこれらのプロパティ (Name と Moustache) を含むかはまだわかりません。前述のとおり、バインド可能なプロパティを含むオブジェクトを DataContext と呼びます。他にもさまざまな例が挙げられますが、ここでは Person クラスのインスタンスを作成して、その Person オブジェクトがすべてのビューの DataContext になるように設定します。

コンテナー (この場合はページ) に DataContext を設定して、そのコンテナー内のすべての表示コントロールからその DataContext を共有できますが、別の DataContext を 1 つ以上の個別のコントロールに割り当てることもできます。

ページの分離コードの Loaded イベント ハンドラーで Person のインスタンスを作成できます。ページが読み込まれ、コントロールが初期化されると、Loaded イベントが呼び出されます (図 4 参照)。

図 4 ページが読み込まれると呼び出される Loaded イベント

private Person _currentPerson;
private Random _rand = new Random();
public MainPage()
{
  InitializeComponent();
  Loaded += MainPage_Loaded;
}
void MainPage_Loaded( object sender, RoutedEventArgs e )
{
  _currentPerson = new Person
  {
    Beard = false,
    Favorite = true,
    Goatee = false,
    Height = 1.86,
    Moustache = true,
    Name = "Jesse",
    WhichSex = Person.Sex.Male
  };
}

これで、次のようにして、ContentPanel にあるすべてのコントロールの DataContext が、(Loaded イベント ハンドラーで) インスタンスを作成した _currentPerson オブジェクトになるように設定できます。

ContentPanel.DataContext = _currentPerson;

DataContext が認識されると、TextBox は Name プロパティを解決して、表示する値 ("Jesse") を取得できます。他のすべてのコントロールについても同様で、それぞれが新しい Person オブジェクトのプロパティにバインドされます。

アプリケーションを実行すると、すべてのフィールドが適切にバインドされて表示されます。

DataContext を変更する

バインドと表示の関係を理解するために、いくつかの Person オブジェクトを作成して、それらを 1 つずつ表示してみましょう。このためには、MainPage.xaml.cs を変更して (ランダムに作成される) Persons のリストを作成し、UI 上の新しい [Next] ボタンを使用してリスト内を反復します。このボタンは、次のように一番下の行に追加します。

<Button
  Name="Next"
  Content="Next"
  Grid.Row="5"
  HorizontalAlignment="Center"
  VerticalAlignment="Center" />

[Next] ボタンを操作するよう変更したコードは、次のようになります。

void MainPage_Loaded( object sender, RoutedEventArgs e )
{
  SetDataContext();
  Next.Click += Next_Click;
}
private void SetDataContext()
{
  ContentPanel.DataContext = GeneratePerson();
}
void Next_Click( object sender, RoutedEventArgs e )
{
  SetDataContext();
}

ページの Loaded イベントと、[Next] ボタンの Click イベント ハンドラーの両方で DataContext を設定する必要があるため、イベント ハンドラーとは分けて SetDataContext というメソッドを別途作成しています。このメソッドは、Person をランダムに作成する GeneratePerson メソッドを呼び出します。

これで、分離コード内ですべての変更を行えるようになります。まず、現在のように Person をコードで結び付けるのを止め、GeneratePerson メソッドを呼び出して結び付けます。

ランダムに Person を生成する

次に、GeneratePerson メソッド全体を示します。ここでは true か false を選択するタスクを分けて、FlipCoin というメソッドを作成しています。

private Person GeneratePerson()
{
  var newPerson = new Person
  {
    Beard = FlipCoin(),
    Favorite = FlipCoin(),
    Goatee = FlipCoin(),
    Height = _rand.NextDouble() + 1,
    Moustache = FlipCoin(),
    Name = names[_rand.Next(0, names.Count - 1)]
  };
  return newPerson;
}

FlipCoin は乱数ジェネレーターを使用して、対象時間の 50 パーセントは true を返すようにします。

private  bool FlipCoin()
{
  return _rand.Next( 1, 3 ) % 2 == 0;
}

最後に、名前を選択するために、男性または女性に割り当てることができる 6 つの名前を作成し、乱数ジェネレーターを使用してオフセット選択してリストに収めます。

private readonly List<string> names = new List<string>()
{
  "Stacey",
  "Robbie",
  "Jess",
  "Robin",
  "Syd",
  "J.J.",
  "Terri",
  "Moonunit",
};

アプリを実行して、[Next] ボタンをクリックします。各 Person オブジェクトが作成されると DataContext として設定され、プロパティが UI コントロールにバインドされます。

INotifyPropertyChanged

Person オブジェクトのプロパティの 1 つが変わるとどうなるでしょう。オブジェクトがデータベースに含まれており、他のユーザーが同じオブジェクトにアクセスできれば、このような事態は簡単に起こる可能性があります。UI を更新する場合を考えてみましょう。

これを行うには、使用するクラス (DataContext) に INotifyPropertyChanged を実装する必要があります。INotifyPropertyChanged は、各プロパティの値が変化したら UI に通知できるようにする単純なインターフェイスです。通常は、イベントに少なくとも 1 つのメソッドが登録されるようにするヘルパー メソッドを作成します。このヘルパー メソッドによって、更新されたプロパティの名前を渡すイベントを発生します。

これが機能するようすを確認するために、UI に [Change] という新しいボタンを追加します。[Change] をクリックすると、Name プロパティが "Jacob" に変わります。

void Change_Click( object sender, RoutedEventArgs e )
{
  _currentPerson.Name = "Jacob";
}

これは、Person が INotifyPropertyChanged を実装し、Name プロパティが PropertyChanged イベントを発生しなければ、何の効果もありません (図 5 参照)。

図 5 INotifyPropertyChanged インターフェイス

public class Person : INotifyPropertyChanged
{
  public string _name;
  public string Name
  {
    get { return _name; }
    set
    {     
      _name = value;
      PropChanged( "Name" );
    }
  }
// Other properties
  public event PropertyChangedEventHandler PropertyChanged;
  private void PropChanged(string propName)
  {
    if (PropertyChanged != null)
    {
      PropertyChanged( this, new PropertyChangedEventArgs( propName ) );
    }
  }
}

これを適切に行えば、[Change] をクリックすると、UI に表示される名前がオブジェクト内の新しい名前 (Jacob) に変わります。

双方向バインド

ユーザーが UI を操作して、値を直接変更するとどうなるでしょう (たとえば、Name の TextBox に新しい名前を入力した場合)。(多くの場合) この変更を基のデータ (DataContext オブジェクト) に戻すことを考えるでしょう。これを行うには、双方向バインドを使用します。

Name プロパティで双方向バインドを使用するようにプログラムを変更するには、Name バインドを探し、次のように変更します。

<TextBox
  x:Name="Name"
  TextWrapping="Wrap"
  d:LayoutOverrides="Height"
  Grid.Column="1"
  HorizontalAlignment="Left"
  Width="200"
  VerticalAlignment="Center"
  Text="{Binding Name, Mode=TwoWay}" />

バインドには次の 3 つのモードがあります。

  1. OneTime バインドでは、データがバインドされますが、ユーザーがデータを更新しても基になるデータは更新されません。
  2. OneWay (既定値) バインドでは、データがソースから UI に取り出されますが、ソースには戻されません。
  3. TwoWay バインドでは、ソースからデータを取り出し、UI で変更が行われた場合はソースに変更を戻すことができます。

要素のバインド

[Next] ボタンを 6 行目に変更し、スライダー コントロールを 5 行目にドラッグします。スライダーには、次のような重要な設定が多数あります。

  • Minimum
  • Maximum
  • Value
  • LargeChange
  • SmallChange

これらの設定をコード内で設定するようすを図 6 に示します。

図 6 スライダーを追加する

<Slider
  x:Name="Likability"
  Grid.Row="5"
  Grid.Column="0"
  BorderBrush="White"
  BorderThickness="1"
  Background="White"
  Foreground="Blue"
  LargeChange="10"
  SmallChange="1"
  Minimum="0"
  Width="199"
  Maximum="100"
  Value="50"
  Height="90" />

Minimum と Maximum はスライダーの範囲を設定します。今回はパーセントを使用するため、それぞれ 0 と 100 に設定します。

Value はスライダーの現在値で、"Minimum <= value <= Maximum" の関係になります。

LargeChange と SmallChange はスクロールバーでよく使用し、それぞれスライダーをクリックした場合の動作量と、別のコントロール (通常は矢印) を使用してスライダーを徐々に動かす動作量を指示します。

TextBlock を設定する

右側の列に 3 つの TextBlock を使用します。最初と 3 番目は (それぞれ "Likeability:" と "%" という値を持つ) 固定のラベルとして使用します。真ん中の TextBlock はスライダーの現在位置を数値で表示します。

これを行うには、真ん中の TextBlock の Text Value プロパティをスライダーの Value プロパティにバインドし、キーワード ElementName を使用してバインド先の要素を指定します (図 7 参照)。

図 7 TextBlock の Value プロパティのバインド

<StackPanel
  x:Name="LikeabilityPercentStackPanel"
  Grid.Row="5"
  Grid.Column="1"
  Orientation="Horizontal">
  <TextBlock
    Text="Likeability: "
    HorizontalAlignment="Left"
    VerticalAlignment="Center"
    Margin="20,0,5,0" />
  <TextBlock
    x:Name="SliderValue"
    Text="{Binding Value, ElementName=Likeability, StringFormat=F3}"
    HorizontalAlignment="Left"
    VerticalAlignment="Center"
    Margin="5,0,0,0"/>
  <TextBlock
    Text="%"
    HorizontalAlignment="Left"
    VerticalAlignment="Center" />
</StackPanel>

プログラムを実行します。スライダーを調整すると、TextBlock の値がすぐに更新されます。

データ コンバーター

プロパティの中には所定の UI コントロールに適切にバインドされないものがあります。また、値の表示方法をさらに細かく制御したい場合もあるでしょう。簡単な例として、ボタンを下に 1 行分動かして、プロンプト ("Birth date") と Person の BirthDate が含まれる行を挿入し、ユーザーの誕生日を表示してみましょう。

これを行うには、有効な BirthDate が生成されるように、MainPage.xaml.cs の GeneratePerson メソッドを修正する必要があります。次の行を追加して、過去 20 年間のランダムな BirthDate が作成されるようにします。

BirthDate = DateTime.Now - TimeSpan.FromDays(_rand.Next(1,365*20)),

BirthDate プロパティに単にバインドした場合は、誕生日が時刻付きで表示されます。しかし、ここでは時刻は必要なく、短い日付形式の誕生日だけが必要です。これを行うには、DataConverter が必要です。

DataConverter は IValueConverter を実装するクラスです。このインターフェイスには、図 8 に示すように 2 つのメソッドが必要です。

図 8 IValueConverter インターフェイス

public object Convert(
  object value,
  Type targetType,
  object parameter,
  System.Globalization.CultureInfo culture )
{
  throw new NotImplementedException();
}
public object ConvertBack(
  object value,
  Type targetType,
  object parameter,
  System.Globalization.CultureInfo culture )
{
  throw new NotImplementedException();
}

今回の場合、2 つのメソッドのうち、最初のメソッドのみが必要です (2 番目のメソッドが呼び出されることはありません)。このメソッドを実装するのは非常に簡単ですが、この場合対象の型が文字列で値の型が DateTime です。そのため、値を取得して、DateTime にキャストしてから、ToShortDateString を呼び出します (図 9 参照)。

図 9 DateTime を短い形式にする変換メソッド

public object Convert(
  object value,
  Type targetType,
  object parameter,
  System.Globalization.CultureInfo culture )
{
  if (targetType == typeof( string ) &&
    value.GetType() == typeof( DateTime ))
  {
    return (( DateTime ) value).ToShortDateString();
  }
  else  // Unable to convert
  {
    return value;
   }
}
public object ConvertBack(
  object value,
  Type targetType,
  object parameter,
  System.Globalization.CultureInfo culture )
{
  throw new NotImplementedException();
}

これを適切に行うには、XAML が値コンバーターにアクセスする方法が必要ですが、コンバーターをリソースにすることでこれが可能になります。App.xaml を開き、アプリの名前空間を基に、次のようにコンバーターの名前空間を追加します。

xmlns:mine="clr-namespace:DataBinding">

次に、同じファイルで <Application.Resource> セクションを探し、値コンバーターのリソースを追加します。

<Application.Resources>
  <mine:DateConverter   x:Key="dateConverter" />
</Application.Resources>

これで、XAML ファイルでキーを使用できるようになります。BirthDate がリソースを使用するようにバインドを更新します。

<TextBlock
  Grid.Row="6"
  Grid.Column="1"
  VerticalAlignment="Center"
  Text="{Binding BirthDate, Converter={StaticResource dateConverter}}" />

プログラムを実行すると、短い日付形式で誕生日が表示されます。

強力なアプリ

データ バインドによって、基になるデータとコントロールの関係と、そのデータを表示するビューを確実に管理する強力な Windows Phone アプリを作成できます。今回は、簡単なデータ バインドと双方向データ バインドを作成する方法、要素をバインドする方法、およびデータ コンバーターを使用して目的の形式にデータを変換する方法を説明しました。

Jesse Liberty は、Windows Phone チームのシニア開発者コミュニティ エバンジェリストです。彼は有名な Yet Another Podcast (jesseliberty.com/podcast、英語) をホストしており、ブログ (jesseliberty.com/、英語) も充実しています。『Programming Reactive Extensions and LINQ』(Apress、2011 年) や『Migrating to Windows Phone』(Apress、2011 年) など、数多くのベストセラーの著者でもあります。ツイッターは twitter.com/JesseLiberty (英語) からフォローできます。

この記事のレビューに協力してくれた技術スタッフの Jason Shaver に心より感謝いたします。