Windowsストア・アプリやWindows Phone 8アプリで文字列以外の値をコントロールにデータ・バインドする方法を、DateTimeOffset型とbool型を例に解説する。
powered by Insider.NET
前回は文字列をコントロールにデータ・バインドする方法を解説した。では、文字列以外の値をデータ・バインドするにはどうしたらよいだろう?
本稿では、DateTimeOffset型とbool型(VBではBoolean型)を例としてその方法を解説する。なお、掲載しているコードは特に記載のない限り、Windowsストア・アプリとWindows Phone 8で同じである。本稿のサンプルは「Windows Store app samples:MetroTips #32(Windows 8版)」と「Windows Store app samples:MetroTips #32(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を使用している。
Windows Phone 8向けのアプリを開発するには、SLAT対応CPUを搭載したPC上の64bit版Win 8 Pro以上とWindows Phone SDK 8.0(無償)が必要となる。
●文字列と同じようにしてDateTimeOffset型をデータ・バインドしてみる
前回は簡易的な「デジタル時計」クラスを作り、string型の「NowTime」プロパティを公開し、それをテキスト・ブロックのTextプロパティにバインドした。この「NowTime」プロパティを、今回はDateTimeOffset型に変えてみよう。すると、「デジタル時計」クラス(=Clockクラス)は次のコードのようになる。
public class Clock : INotifyPropertyChanged
{
// 現在時刻を表すプロパティ ※今回はDateTimeOffset型
public DateTimeOffset NowTime { get; private set; }
// NowTimeプロパティが変化したときに発生させるイベントの定義
public event PropertyChangedEventHandler PropertyChanged;
public Clock()
{
Run(); // 時刻監視の無限ループを動かす
}
private async void Run()
{
DateTimeOffset lastTime;
while (true)
{
await Task.Delay(10); // おおよそ10ミリ秒ごとにシステム時計をチェックする
var nowTime = DateTimeOffset.Now;
if (lastTime.Second != nowTime.Second)
{
// 秒が変わったら、プロパティに時刻をセットし、イベントを発火させる
this.NowTime = nowTime;
if (this.PropertyChanged != null)
this.PropertyChanged(this, new PropertyChangedEventArgs("NowTime"));
lastTime = nowTime;
}
}
}
}
Public Class Clock
Implements INotifyPropertyChanged
' 現在時刻を表すプロパティ ※今回はDateTimeOffset型
Private _nowTime As DateTimeOffset
Public Property NowTime As DateTimeOffset
Get
Return _nowTime
End Get
Private Set(value As DateTimeOffset)
_nowTime = value
End Set
End Property
' NowTimeプロパティが変化したときに発生させるイベントの定義
Public Event PropertyChanged(sender As Object, e As PropertyChangedEventArgs) _
Implements INotifyPropertyChanged.PropertyChanged
Public Sub New()
Run() ' 時刻監視の無限ループを動かす
End Sub
Private Async Sub Run()
Dim lastTime As DateTimeOffset
While (True)
Await Task.Delay(10) ' おおよそ10ミリ秒ごとにシステム時計をチェックする
Dim nowTime = DateTimeOffset.Now
If (lastTime.Second <> nowTime.Second) Then
' 秒が変わったら、プロパティに時刻をセットし、イベントを発火させる
Me.NowTime = nowTime
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs("NowTime"))
lastTime = nowTime
End If
End While
End Sub
End Class
これをバインドするテキスト・ブロックを、次のコードのようにしてMainPage.xamlファイルにXAMLで定義する。
<TextBlock x:Name="textClock1" FontSize="120" Foreground="DarkGoldenrod"
Text="{Binding NowTime}" />
最後に、MainPageクラスのコンストラクタで、テキスト・ブロックのデータ・コンテキストにClockクラスのオブジェクトをセットする(次のコード)。
// 「デジタル時計」クラスのインスタンス
private Clock _clock1 = new Clock();
public MainPage()
{
this.InitializeComponent();
// 【1】テキスト・ブロックのデータ・コンテキストに設定
// ※ DateTimeOffset をそのままバインド (XAMLで定義)
textClock1.DataContext = _clock1;
}
' 「デジタル時計」クラスのインスタンス
Private _clock1 As Clock = New Clock()
Public Sub New()
' この呼び出しはデザイナーで必要です。
InitializeComponent()
' 【1】テキスト・ブロックのデータ・コンテキストに設定
' ※ DateTimeOffset をそのままバインド (XAMLで定義)
textClock1.DataContext = _clock1
End Sub
さて、どうなるだろうか? Textプロパティはstring型で、DateTimeOffset型とは型が合わないからエラーになるだろうか? 実行して確かめてみよう(次の画像)。
実行時エラーにはならないが、「日付け+時刻」という長い文字列が表示されている(末尾は画面の右にはみ出してしまっている)。string型のプロパティにデータ・バインドした場合は、バインドしたオブジェクトのToStringメソッドが自動的に呼び出されることから、このような結果になったのである。
●DateTimeOffset型をTextプロパティにバインドする際にフォーマットを指定するには
バインドするオブジェクトに標準で備わっているToStringメソッドで適切な表示ができるのならそれで問題無いが、上の図のようにToStringメソッドでは表示が長すぎる場合もあるし、指定したフォーマットに変換して表示したい場合もあるだろう。そのためには、指定したフォーマットにデータを変換してくれる「バリュー・コンバータ」を用意して、データ・バインド時に指定するとよい。
バリュー・コンバータは、Windows.UI.Xaml.Data名前空間(WP 8ではSystem.Windows.Data名前空間)のIValueConverterインターフェイスを継承(VBでは「実装」)して作成する。このインターフェイスには、正方向(データ→画面)の変換を行うConvertメソッドと逆方向(画面→データ)の変換を行うConvertBackメソッドが定義されているので、それらを実装する。
例えば、DateTimeOffset型のデータを「HH:mm:ss」形式の文字列にフォーマットするバリュー・コンバータのコードは、次のようになる。正方向(データ→画面)の変換しか想定しないのであれば、このコードのようにConvertメソッドだけをきちんと実装し、ConvertBackメソッドは常にDependencyProperty.UnsetValue(Windows.UI.Xaml名前空間/System.Windows名前空間)を返すようにしても構わない。まず、Win 8用のコードを示す。
class DateTimeToHhMmSsConverter : Windows.UI.Xaml.Data.IValueConverter
{
// 正方向(データ→画面)の変換
public object Convert(object value, Type targetType,
object parameter, string language)
{
if (targetType == typeof(string))
if (value is DateTimeOffset)
return ((DateTimeOffset)value).ToString("HH:mm:ss");
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 DateTimeToHhMmSsConverter
Implements Windows.UI.Xaml.Data.IValueConverter
' 正方向(データ→画面)の変換
Public Function Convert(value As Object, targetType As Type, parameter As Object, _
language As String) _
As Object Implements Windows.UI.Xaml.Data.IValueConverter.Convert
If (targetType Is GetType(String)) Then
If (TypeOf value Is DateTimeOffset) Then
Return DirectCast(value, DateTimeOffset).ToString("HH:mm:ss")
End If
End If
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 Windows.UI.Xaml.Data.IValueConverter.ConvertBack
Return Windows.UI.Xaml.DependencyProperty.UnsetValue
End Function
End Class
一方、WP8用のコードは次のようになる。
public class DateTimeToHhMmSsConverter : System.Windows.Data.IValueConverter
{
// 正方向(データ→画面)の変換
public object Convert(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
if (targetType == typeof(string))
if (value is DateTimeOffset)
return ((DateTimeOffset)value).ToString("HH:mm:ss");
return System.Windows.DependencyProperty.UnsetValue;
}
// 逆方向(画面→データ)の変換(利用を想定していない)
public object ConvertBack(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
return System.Windows.DependencyProperty.UnsetValue;
}
}
Public Class DateTimeToHhMmSsConverter
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
If (targetType Is GetType(String)) Then
If (TypeOf value Is DateTimeOffset) Then
Return DirectCast(value, DateTimeOffset).ToString("HH:mm:ss")
End If
End If
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
このバリュー・コンバータを画面のXAMLコードで使いたいのだが、それにはまず画面から使える場所にバリュー・コンバータのインスタンスを定義しなければならない。その場所とは、画面そのものかApp.xamlファイルである。ここでは、画面そのもの(MainPage.xamlファイル)に定義してみよう。次のようにページのResources要素内に記述する。
<common:LayoutAwarePage.Resources>
…… 省略 ……
<local:DateTimeToHhMmSsConverter x:Key="DateTimeValueConverter" />
</common:LayoutAwarePage.Resources>
<phone:PhoneApplicationPage
…… 省略 ……
xmlns:local="clr-namespace:MetroTips032"
>
<phone:PhoneApplicationPage.Resources>
<local:DateTimeToHhMmSsConverter x:Key="DateTimeValueConverter"/>
</phone:PhoneApplicationPage.Resources>
ここで、バリュー・コンバータのインスタンスを定義するタグの名前としては、バリュー・コンバータの名前空間/クラス名を使う*1。そして、バリュー・コンバータのインスタンスを画面のXAMLで使うときには、x:Key属性で指定された方の名前を使うのだ。上のXAMLコードでは、それを明確にするためにわざとタグ名とx:Key属性に異なる名前を使ったが、同じ名前にしても構わない。
*1 タグ名の前にある「local:」という接頭辞は名前空間を指定している。ページの先頭で、「local:」は「xmlns:local="using:MetroTips032CS"」や「xmlns:local="clr-namespace:MetroTips032"」のように名前空間のエイリアス(別名)として定義されているからだ。
テキスト・ブロックのTextプロパティにバインドするときに、このバリュー・コンバータのインスタンスを介するには、次のようにXAMLを定義する。
<TextBlock x:Name="textClock2" FontSize="120" Foreground="DarkCyan"
Text="{Binding NowTime, Converter={StaticResource DateTimeValueConverter}}"
/>
最後に、MainPageクラスのコンストラクタで、テキスト・ブロックのデータ・コンテキストにClockクラスのオブジェクトをセットすれば完成だ(次のコード)。
public MainPage()
{
this.InitializeComponent();
…… 省略 ……
// 【2】テキスト・ブロックのデータ・コンテキストに設定
// ※ バリュー・コンバータを介してDateTimeOffsetをバインド (XAMLで定義)
textClock2.DataContext = _clock1;
}
Public Sub New()
' この呼び出しはデザイナーで必要です。
InitializeComponent()
…… 省略 ……
' 【2】テキスト・ブロックのデータ・コンテキストに設定
' ※ バリュー・コンバータを介してDateTimeOffsetをバインド (XAMLで定義)
textClock2.DataContext = _clock1
End Sub
実行してみると、次の画像のようにフォーマット済みの時刻が表示される。
●string型以外のプロパティにバインドするには?
それでは、バインドするコントロールのプロパティがstring型でないときはどうだろうか? ここではbool型/Boolean型のデータをコントロールのVisibilityプロパティ(=Windows.UI.Xaml名前空間のVisibility列挙型)にバインドしてみよう。
サンプルとして、「デジタル時計」クラス(=Clockクラス)に秒が偶数か奇数かを表すプロパティを追加し、それをEllipseコントロールのVisibilityプロパティにバインドしてみよう。まず、Clockクラスを次のコードのように改修する(太字が追加部分)。
public class Clock : INotifyPropertyChanged
{
// 現在時刻を表すプロパティ
public DateTimeOffset NowTime { get; private set; }
// 秒が偶数のとき true
public bool IsEven { get; private set; }
// 秒が奇数のとき true
public bool IsOdd { get; private set; }
…… 省略 ……
private async void Run()
{
…… 省略 ……
// 秒が変わったら、プロパティに時刻をセットし、イベントを発火させる
this.NowTime = nowTime;
if (this.PropertyChanged != null)
this.PropertyChanged(this, new PropertyChangedEventArgs("NowTime"));
bool isEvenSec = (nowTime.Second % 2 == 0);
this.IsEven = isEvenSec;
if (this.PropertyChanged != null)
this.PropertyChanged(this, new PropertyChangedEventArgs("IsEven"));
this.IsOdd = !isEvenSec;
if (this.PropertyChanged != null)
this.PropertyChanged(this, new PropertyChangedEventArgs("IsOdd"));
lastTime = nowTime;
…… 省略 ……
Public Class Clock
Implements INotifyPropertyChanged
' 現在時刻を表すプロパティ
Private _nowTime As DateTimeOffset
…… 省略 ……
' 秒が偶数のとき True
Private _isEven As Boolean
Public Property IsEven As Boolean
Get
Return _isEven
End Get
Private Set(value As Boolean)
_isEven = value
End Set
End Property
' 秒が奇数のとき True
Private _isOdd As Boolean
Public Property IsOdd As Boolean
Get
Return _isOdd
End Get
Private Set(value As Boolean)
_isOdd = value
End Set
End Property
…… 省略 ……
Private Async Sub Run()
…… 省略 ……
' 秒が変わったら、プロパティに時刻をセットし、イベントを発火させる
Me.NowTime = nowTime
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs("NowTime"))
Dim isEvenSec As Boolean = (nowTime.Second Mod 2 = 0)
Me.IsEven = isEvenSec
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs("IsEven"))
Me.IsOdd = Not isEvenSec
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs("IsOdd"))
lastTime = nowTime
…… 省略 ……
この追加したプロパティを2つのEllipseコントロールのVisibilityプロパティにバインドする(次の画像とXAMLコード)。
<StackPanel Margin="0,20,0,0">
<TextBlock Text="bool を Visibility プロパティにバインド"
FontSize="21" />
<Ellipse x:Name="RedCircle" Width="90" Height="90"
Fill="DarkRed" HorizontalAlignment="Left"
Visibility="{Binding IsEven}" />
<Ellipse x:Name="GreenCircle" Width="90" Height="90"
Fill="Green" HorizontalAlignment="Left"
Visibility="{Binding IsOdd}" />
</StackPanel>
これが正しくバインドされれば、1秒ごとに表示されるEllipseコントロールが切り替わり、赤と緑の円が点滅するような表示になるはずである。コードビハインドで、ClockクラスのインスタンスをEllipseコントロールのデータ・コンテキストに結び付けてみよう(次のコード)。
public MainPage()
{
this.InitializeComponent();
…… 省略 ……
// 【3】図形のデータ・コンテキストに設定
RedCircle.DataContext = _clock1;
GreenCircle.DataContext = _clock1;
}
Public Sub New()
' この呼び出しはデザイナーで必要です。
InitializeComponent()
…… 省略 ……
' 【3】図形のデータ・コンテキストに設定
RedCircle.DataContext = _clock1
GreenCircle.DataContext = _clock1
End Sub
しかし、実行してみると、Ellipseコントロールが2つとも表示されたままになる。つまり、コントロールのVisibilityプロパティが変化しないのだ。これは、bool型/Boolean型をVisibility列挙型に変換する方法をデータ・バインディングが知らないからである。
これを解決するには、bool型/Boolean型をVisibility列挙型に変換するバリュー・コンバータを提供してやればよい。まさにこの処理を行ってくれるバリュー・コンバータが、Win 8用のプロジェクトでは「グリッド アプリケーション(XAML)」などのプロジェクト・テンプレートのCommonフォルダに「BooleanToVisibilityConverter」クラスとして入っている*2。そして、それをWP 8用に移植したのが次のコードだ。C#の場合は、WP 8用のプロジェクトにCommonフォルダを作成して、そこにBooleanToVisibilityConverterクラスを作成するか、プロジェクトの直下にクラスを作成し、明示的に名前空間Commonを宣言しておく。VBの場合はプロジェクトの直下にクラスを作成し、「Namespace Common」として名前空間を明示的に宣言しておく。
public sealed class BooleanToVisibilityConverter
: System.Windows.Data.IValueConverter
{
public object Convert(object value, Type targetType, object parameter,
CultureInfo culture)
{
return (value is bool && (bool)value)
? Visibility.Visible
: Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter,
CultureInfo culture)
{
return value is Visibility && (Visibility)value == Visibility.Visible;
}
}
Namespace Common
Public NotInheritable Class BooleanToVisibilityConverter
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
If TypeOf value Is Boolean AndAlso DirectCast(value, Boolean) Then Return Visibility.Visible
Return Visibility.Collapsed
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 TypeOf value Is Visibility AndAlso DirectCast(value, Visibility) = Visibility.Visible
End Function
End Class
End Namespace
*2 プロジェクトのCommonフォルダにBooleanToVisibilityConverterクラスが存在しない場合は、そのプロジェクトに(新しいプロジェクト項目として)[基本ページ]を追加してほしい。するとメッセージボックスが出てきて、「(前略)不足しているファイルを自動的に追加しますか?」と尋ねられるので[はい]と答えると、Commonフォルダにいくつかのファイルが自動生成される。自動生成されたファイルの中に、BooleanToVisibilityConverter.cs/.vbファイルがあるはずだ。なお、作ったページ自体は不要なので削除しておく。
また、この操作によってVS 2012の動作が不安定になる場合があるので、できるだけプロジェクトの作成直後に行ってほしい。
BooleanToVisibilityConverterバリュー・コンバータを使うために、まずそのインスタンスを画面のResources要素に定義する(次のコード)。
<common:LayoutAwarePage.Resources>
…… 省略 ……
<common:BooleanToVisibilityConverter x:Key="BoolToVisibilityConverter"/>
</common:LayoutAwarePage.Resources>
<phone:PhoneApplicationPage
…… 省略 ……
xmlns:common="clr-namespace:MetroTips032.Common"
>
<phone:PhoneApplicationPage.Resources>
…… 省略 ……
<common:BooleanToVisibilityConverter x:Key="BoolToVisibilityConverter"/>
</phone:PhoneApplicationPage.Resources>
最後に、Ellipseコントロールにバインドしている部分でバリュー・コンバータを指定する。
<StackPanel Margin="0,20,0,0">
<TextBlock
Text="bool をバリュー・コンバータを介して Visibility プロパティにバインド"
FontSize="21" />
<Ellipse x:Name="RedCircle" Width="90" Height="90"
Fill="DarkRed" HorizontalAlignment="Left"
Visibility="{Binding IsEven, Converter={StaticResource BoolToVisibilityConverter}}" />
<Ellipse x:Name="GreenCircle" Width="90" Height="90"
Fill="Green" HorizontalAlignment="Left"
Visibility="{Binding IsOdd, Converter={StaticResource BoolToVisibilityConverter}}" />
</StackPanel>
実行してみると、今度はうまくいく。秒が偶数のときと奇数のときで表示されるEllipseコントロールが切り替わり、まるでコントロールの色が変化しているように見える(次の画像)。
●まとめ
データ・バインドで、バインド時に以下に示すような状況で何らかの変換を行いたいときにはバリュー・コンバータを利用する。
なお、本稿ではXAMLコードでバリュー・コンバータを設定したが、コードビハインドでBindingオブジェクトにセットすることも可能である(BindingオブジェクトのConverterプロパティ)。
バリュー・コンバータについて詳しくは、次のドキュメントを参照してほしい。
Copyright© Digital Advantage Corp. All Rights Reserved.