コントロール同士をデータ・バインドするには?[Win 8/WP 8]:WinRT/Metro TIPS
データ・バインドの仕組みを使うと、データ・クラスだけでなく、コントロール同士もバインドできる。その方法を説明する。
powered by Insider.NET
前回までに紹介してきたデータ・バインドの例では、何らかのデータを表現するクラス(=データ・クラス)のプロパティをコントロールにバインドしていた。しかし、データ・バインドの仕組みを使うと、コントロール同士もバインドできるのである。そこで本稿では、コントロールとコントロールをバインドする方法を解説する。本稿のサンプルは「Windows Store app samples:MetroTips #35(Windows 8版)」と「Windows Store app samples:MetroTips #35(WP 8版)」からダウンロードできる。
なお、掲載しているコードは特記なき場合はWindowsストア・アプリとWindows Phone 8(以降、WP 8)アプリで共通である。
●事前準備
Windows 8(以降、Win 8)向けのWindowsストア・アプリを開発するには、Win 8とVisual Studio 2012(以降、VS 2012)が必要である。これらを準備するには、第1回のTIPSを参考にしてほしい。本稿では64bit版Win 8 ProとVS 2012 Express for Windows 8を使用している。
WP 8向けのアプリを開発するには、SLAT対応CPUを搭載したPC上の64bit版Win 8 Pro以上とWindows Phone SDK 8.0(無償)が必要となる。
●自分自身とバインドするには?
テキスト・ブロックに同内容のツールチップを付ける例を考えてみよう。例えば、文字列がテキスト・ブロックに表示しきれないほど長くなって末尾が切れてしまうようなときは、次の画像のように同じ文字列をツールチップで提供するのがよいだろう。
テキスト・ブロックに同内容のツールチップを設定した画面の部分(Win 8)
テキスト・ブロックに設定した文字列が長くなって末尾が切れてしまうと予想されるときは、このようにツールチップでも同じ文字列を提供するとよい。
なお、WP 8には現時点ではツールチップのコントロールが実装されていないので、ツールチップが必要ならばPopupコントロールなどを使って自作することになる。
これを素直に実装すると、次のようなXAMLコードになるだろう。
<TextBlock ……省略……
Text="WinRT/Metro TIPS #35 (CS) コントロールとデータ・バインドするには?"
ToolTipService.ToolTip="WinRT/Metro TIPS #35 (CS) コントロールとデータ・バインドするには?"
/>
これは分かりやすいが、コードビハインドから文字列を変更するときには、2箇所のプロパティを変更しなければならない。
上記のコードは、次のようにして片方をデータ・バインドにできる。バインドする対象のオブジェクトとして自分自身を指定するには、「RelativeSource={RelativeSource Mode=Self}」というキーワードを使う。これは、バインドするデータのソースが相対指定であること(=「RelativeSource」)と、相対的に自分自身を指定する(=「Mode=Self」)という設定だ。
<TextBlock ……省略……
Text="WinRT/Metro TIPS #35 (CS) コントロールとデータ・バインドするには?"
ToolTipService.ToolTip="{Binding Text, RelativeSource={RelativeSource Mode=Self}}"
/>
このようにバインドしておけば、コードビハインドからTextプロパティを変更するだけでツールチップも自動的に変わる。
この応用としては、例えば入力された文字列に応じた色を返すようなバリュー・コンバータ*1を作り、それを介してテキストボックスの前景色を自分のTextプロパティにバインドすることなどが考えられる。このようにちょっとした機能をコントロールに付加するために、自分自身のプロパティとのバインディングが利用できるのだ*2。
なお、WP 8にはツールチップのコントロールが実装されていないので上記のサンプル・コードは動作しないが、自分自身のプロパティにバインドする方法は同じである*3。
*1 バリュー・コンバータについては「WinRT/Metro TIPS: 文字列以外の値をコントロールにバインドするには?[Win 8/WP 8]」を参照。
*2 複雑になってきたら、ユーザー・コントロールや継承したカスタム・コントロールを作成した方がよい。
*3 WP 8のテキスト・ブロックでは、バリュー・コンバータを介さずに自身のプロパティとバインドするよい例を思いつかないのだが、次のようにして可能であることは確認できる。
<TextBlock Text="Red"
Foreground="{Binding Text, RelativeSource={RelativeSource Self}}" />
●ほかのコントロールにバインドするには?
ほかのコントロールへのバインドを行う場合も、自分自身にバインドするのと同じように記述できる。ただし、バインドする対象となるコントロールには名前を付けておき、バインドする対象はその名前で指定する。
ここでは、バリュー・コンバータも使ってみよう。数値の0〜255を無彩色のSolidColorBrushオブジェクト(Win 8ではWindows.UI.Xaml.Media名前空間/WP 8ではSystem.Windows.Media名前空間)に変換するバリュー・コンバータを作ると、次のコードのようになる。
最初にWin 8用のコードを示す。
public sealed class NumberToNeutralcolorConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
try
{
byte b = (byte)((double)value);
return new Windows.UI.Xaml.Media.SolidColorBrush(
Windows.UI.Color.FromArgb(255, b, b, b)
);
}
catch { }
return Windows.UI.Xaml.DependencyProperty.UnsetValue;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
return Windows.UI.Xaml.DependencyProperty.UnsetValue;
}
}
Public Class NumberToNeutralcolorConverter
Implements IValueConverter
Public Function Convert(value As Object, targetType As Type, parameter As Object,
language As String) As Object Implements IValueConverter.Convert
Try
Dim b As Byte = CByte(DirectCast(value, Double))
Return New Windows.UI.Xaml.Media.SolidColorBrush(
Windows.UI.Color.FromArgb(255, b, b, b)
)
Catch
End Try
Return Windows.UI.Xaml.DependencyProperty.UnsetValue
End Function
Public Function ConvertBack(value As Object, targetType As Type, parameter As Object,
language As String) As Object Implements IValueConverter.ConvertBack
Return Windows.UI.Xaml.DependencyProperty.UnsetValue
End Function
End Class
そして、次に示すのがWP 8用のコードだ。
public sealed class NumberToNeutralcolorConverter : System.Windows.Data.IValueConverter
{
public object Convert(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
try
{
byte b = (byte)((double)value);
return new System.Windows.Media.SolidColorBrush(
System.Windows.Media.Color.FromArgb(255, b, b, b)
);
}
catch { }
return System.Windows.DependencyProperty.UnsetValue;
}
public object ConvertBack(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
return System.Windows.DependencyProperty.UnsetValue;
}
}
Option Strict On
Public Class NumberToNeutralcolorConverter
Implements System.Windows.Data.IValueConverter
Public Function Convert(value As Object, targetType As Type, parameter As Object,
culture As Globalization.CultureInfo) As Object _
Implements System.Windows.Data.IValueConverter.Convert
Try
Dim b As Byte = CByte(DirectCast(value, Double))
Return New System.Windows.Media.SolidColorBrush(
System.Windows.Media.Color.FromArgb(255, b, b, b)
)
Catch
End Try
Return System.Windows.DependencyProperty.UnsetValue
End Function
Public Function ConvertBack(value As Object, targetType As Type, parameter As Object,
culture As Globalization.CultureInfo) As Object _
Implements System.Windows.Data.IValueConverter.ConvertBack
Return System.Windows.DependencyProperty.UnsetValue
End Function
End Class
このバリュー・コンバータを介して、次のようにスライダ・コントロールのValueプロパティをテキスト・ブロックの前景色や文字列にバインドできる。バインドする対象のコントロールを指定するためには「ElementName=slider1」という記述を使う。これは「バインド対象の要素名(=ElementName)は『slider1』である」という意味だ。要素名に指定できるのは、実行時に同じ画面に含まれているコントロールの名前である。省略部分やXAMLコードの全体の構成などは、「Sample Code - MSDN」上に公開するソース・コードを参照していただきたい。
<Grid Background="DarkOrchid">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding Value, ElementName=slider1}"
Foreground="{Binding Value, Converter={StaticResource NumberToNeutralcolorConverter}, ElementName=slider1}"
……省略…… />
<Slider x:Name="slider1" Minimum="0" Maximum="255" Grid.Column="1" ……省略…… />
</Grid>
バリュー・コンバータのインスタンス宣言部分は省略している。詳しくは「WinRT/Metro TIPS: 文字列以外の値をコントロールにバインドするには?[Win 8/WP 8]」を参照してほしい。
これを実行してみると、本記事の最後に掲載する画像「完成した画面(Win 8/WP 8)」の上部のようになる。スライダを動かすと、それにつれてテキスト・ブロックの背景色と文字列が変わっていく。
●データ・テンプレート内からコントロールにバインドするには?
ここまでは実行時の画面内でコントロール自身にバインドしたり、ほかのコントロールにバインドしたりする例を見てきた。それでは、ユーザー・コントロールやデータ・テンプレートなど、実行時の画面とは切り離してXAMLコードを記述する場合はどうだろうか? 記述するときに「見えていない」コントロールの名前を指定してデータ・バインドできるのだろうか?
答えはイエスだ。実行時に同じ画面に存在するコントロールならば、名前を使ってバインドできる。ここでは、別途定義したデータ・テンプレートの中から、画面上のコントロールにバインドしてみよう。
対象のコントロールには、先ほどと同じスライダ・コントロールを使おう。そして、次のようなリスト・コントロールとそのデータ・テンプレートを用意する。
まず、Win 8用ListViewコントロールのXAMLコードを示す。これは画面定義ファイルの中に記述する。
<ListView ……省略……
ItemTemplate="{StaticResource SampleTemplate1}"
>
<!-- あまり実用的ではないが、以下のようなデータの与え方が可能だ -->
<x:String>項目 (1)</x:String>
<x:String>項目 (2)</x:String>
<x:String>項目 (3)</x:String>
</ListView>
ItemTemplateプロパティに指定している「SampleTemplate1」は、後に示すデータ・テンプレートである。
WP 8のListBoxコントロールでは、上記のWin 8用のようなデータの与え方はできない*4ので、まずサンプル・データを提供するクラスを次のように作る。
*4 単に表示させるだけならListBoxItemクラスが使えるが、データ・テンプレートは利用できない。
public class SampleData : List<string>
{
public SampleData()
{
this.Add("項目 (1)");
this.Add("項目 (2)");
this.Add("項目 (3)");
}
}
Public Class SampleData
Inherits List(Of String)
Public Sub New()
Me.Add("項目 (1)")
Me.Add("項目 (2)")
Me.Add("項目 (3)")
End Sub
End Class
すると、WP 8用のListBoxコントロールのXAMLコードは次のように書ける。画面のリソースにSampleDataクラスのインスタンスを定義して、それを「ItemsSource=…」という属性でListBoxコントロールに与えるという記述は見慣れないかもしれないが、やっているのは前述のWin 8用のXAMLコードと同じことである。
<phone:PhoneApplicationPage
xmlns:local="clr-namespace:MetroTips035CS"
……省略……
>
<phone:PhoneApplicationPage.Resources>
……省略……
<local:SampleData x:Key="SampleData" />
</phone:PhoneApplicationPage.Resources>
……省略……
<ListBox ……省略……
ItemsSource="{StaticResource SampleData}"
ItemTemplate="{StaticResource SampleTemplate1}">
</ListBox>
……省略……
ItemTemplateプロパティに指定している「SampleTemplate1」は、次に示すデータ・テンプレートである。
次に、データ・テンプレートを示す。今回は、画面定義ファイルではなく、App.xamlファイルの中に記述する。記述しているときには、実際にテンプレートが適用されたときの画面にどんなコントロールが存在するかは分からないが、「slider1」という名前のコントロールにバインドする指定をした。
データ・テンプレート内のテキスト・ブロックのTextプロパティには、テンプレートが適用されたアイテム(=前述のSampleDataクラスの各項目)をバインドする。「"{Binding}"」とだけ記述して、バインドするプロパティを指定しなかった場合は、データ・コンテキストで割り当てられたオブジェクトそのもの(=ここではテンプレートが適用されたアイテム、すなわち、前述のSampleDataクラスの各項目)がバインドされる。
データ・テンプレート内のコンテナ・コントロール(=GridコントロールやFrameコントロール)のWidthプロパティとBackgroundプロパティには、前に説明したスライダ・コントロール「slider1」のValueオブジェクトをバインドする。Backgroundプロパティにバインドする際には、前述のバリュー・コンバータを使う。
まずWin 8用のXAMLコードを示す。
<Application
……省略……
xmlns:local="using:MetroTips035CS">
<Application.Resources>
<ResourceDictionary>
……省略……
<local:NumberToNeutralcolorConverter x:Key="NumberToNeutralcolorConverter" />
<DataTemplate x:Key="SampleTemplate1">
<Frame Width="{Binding Value, ElementName=slider1}"
Background="{Binding Value, Converter={StaticResource NumberToNeutralcolorConverter}, ElementName=slider1}" >
<TextBlock Text="{Binding}" ……省略…… />
</Frame>
</DataTemplate>
</ResourceDictionary>
</Application.Resources>
</Application>
テキスト・ブロックを囲むFrameコントロールのWidthプロパティとBackgroundプロパティを、「slider1」という名前のコントロールのValueプロパティにバインドした。テキスト・ブロックのTextプロパティは、データ・コンテキストにバインドした。
次がWP 8用のXAMLコードだ。
<Application
x:Class="MetroTips035CS.App"
……省略……
>
<!--アプリケーション リソース-->
<Application.Resources>
……省略……
<local:NumberToNeutralcolorConverter x:Key="NumberToNeutralcolorConverter" />
<DataTemplate x:Key="SampleTemplate1">
<Grid Width="{Binding Value, ElementName=slider1}"
Background="{Binding Value, Converter={StaticResource NumberToNeutralcolorConverter}, ElementName=slider1}" >
<TextBlock Text="{Binding}" ……省略…… />
</Grid>
</DataTemplate>
</Application.Resources>
……省略……
</Application>
テキスト・ブロックを囲むGridコントロールのWidthプロパティとBackgroundプロパティを、「slider1」という名前のコントロールのValueプロパティにバインドした。テキスト・ブロックのTextプロパティは、データ・コンテキストにバインドした。
このように、存在するかどうか分からないコントロールの名前を指定してバインドしても、エラーにならずコンパイルできる。
実行してみると、次の画像のように、スライダを動かすにつれてListViewコントロールの各項目の幅と背景色が変化する。
完成した画面(Win 8/WP 8)
スライダ・コントロールを動かすと、スライダ・コントロールの左側のテキスト・ブロックの文字列とその前景色、および、ListViewコントロールの各項目の幅と背景色が変化する。
ここで、画面に存在しない名前を指定してしまった場合を試しておこう。先ほどのデータ・テンプレート内の「slider1」というコントロール名を、存在しない名前(例えば「slider2」など)に書き換えてみてほしい。しかし、コンパイル時にも実行時にもエラーにはならない(もちろん、スライダ・コントロールを動かしてもListViewコントロールの項目の幅や背景色が変化することはない)。
データ・バインドでは、指定を間違えてもエラーにはならず、実行時には既定値が使用されるのだ。そのような緩やかなルールになっているからこそ、この例のように別の場所で定義したデータ・テンプレートからも画面にあるコントロールにバインドできるのである*5。
*5 ただし、可能であるからといって乱用すると、データ・テンプレートの動作が分かりにくくなり混乱を招くことになる。実際の開発では、データ・テンプレートがバインドする対象はデータ・コンテキストだけにしておくことを推奨する。上の例では、スライダの値と双方向バインドするデータ・クラスを用意し、データ・テンプレートもそのデータ・クラスにバインドされるように実装するとよい。
●まとめ
コントロールのプロパティを、ほかのコントロールや自分自身のプロパティにデータ・バインドできる。複数のコントロールの動作を連携できるのはとても便利なので、ぜひマスターしてほしい。ただし、複雑になってきたときには、ユーザー・コントロールやカスタム・コントロールを作ったり、バインドするデータ・クラスを見直したりすることで複雑さを低減できることも一緒に覚えておいてほしい。
Copyright© Digital Advantage Corp. All Rights Reserved.