自作のユーザー・コントロールに独自のプロパティを実装し、そこにデータ・バインドする方法を解説する。
powered by Insider.NET
UserControlクラス(Windows.UI.Xaml.Controls名前空間)を継承したコントロール(以降、ユーザー・コントロール)を作り、それにデータ・バインドしたいことがある。さらに、ユーザー・コントロールに独自のプロパティを実装し、そこにバインドしたいときはどうしたらよいだろうか? 本稿では、その方法を解説する。本稿のサンプルは「Windows Store app samples:MetroTips #45(Windows 8版)」からダウンロードできる。
なお、本稿では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を使用している。
問題
ユーザー・コントロールの中でもデータ・コンテキストが使えるので、それをバインドするのならば簡単だ。しかしそれでは汎用性に欠けるという問題がある。
汎用的なユーザー・コントロールとするためには、利用するページの側でユーザー・コントロールのプロパティに明示的にバインドできる必要がある。本稿では、まずユーザー・コントロールの中でデータ・コンテキストを利用する方法を示し、それからバインド可能なプロパティを実装する方法を解説する。
ユーザー・コントロールのデータ・コンテキスト
ユーザー・コントロールにも、それが属するビジュアル・ツリーのデータ・コンテキストが適用される。すなわち、ユーザー・コントロールの中でもデータ・コンテキストを利用できるのだ。
例えば、ページのデータ・コンテキストにDefaultViewModelプロパティが設定されており*1、その「URLs」キーにはURL文字列のコレクションが設定されているとする(次のコード)。
private string[] _urls
= {
"http://www.atmarkit.co.jp/fdotnet/chushin/readyforwin8app_01/readyforwin8app_01_01.html",
"http://www.atmarkit.co.jp/fdotnet/chushin/readyforwin8app_02/readyforwin8app_02_01.html",
"http://www.atmarkit.co.jp/ait/subtop/features/da/ap_winrttips_index.html",
};
protected override void LoadState(Object navigationParameter, Dictionary<String, Object> pageState)
{
base.DefaultViewModel.Add("URLs",
new System.Collections.ObjectModel.ObservableCollection<string>(_urls));
}
Private _urls As String() = _
{ _
"http://www.atmarkit.co.jp/fdotnet/chushin/readyforwin8app_01/readyforwin8app_01_01.html",
"http://www.atmarkit.co.jp/fdotnet/chushin/readyforwin8app_02/readyforwin8app_02_01.html",
"http://www.atmarkit.co.jp/ait/subtop/features/da/ap_winrttips_index.html"
}
Protected Overrides Sub LoadState(navigationParameter As Object, pageState As Dictionary(Of String, Object))
MyBase.DefaultViewModel.Add("URLs", _
New System.Collections.ObjectModel.ObservableCollection(Of String)(_urls))
End Sub
*1DefaultViewModelプロパティについては「WinRT/Metro TIPS:複数のバインディング・ソースを画面にバインドするには?[Win 8]」を参照。
すると、このページに配置して利用するユーザー・コントロールでは、「URLs」をキーとして上のコードの3つのURL文字列をバインドできる。例えば次のようになる。
<UserControl
x:Class="MetroTips045CS.MyUserControl1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:MetroTips045CS"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignHeight="300"
d:DesignWidth="400">
<Grid>
<GridView ItemsSource="{Binding URLs}" Height="40" SelectionMode="None" >
<!-- 上のItemsSourceプロパティには、URL文字列のコレクションがバインドされる -->
<GridView.Resources>
<DataTemplate x:Key="DataTemplate1">
<Grid Margin="0" Height="20">
<TextBlock Text="{Binding}" FontSize="9" TextTrimming="WordEllipsis" />
<!-- 上のTextプロパティには、URL文字列がバインドされる -->
</Grid>
</DataTemplate>
</GridView.Resources>
<GridView.ItemTemplate >
<StaticResource ResourceKey="DataTemplate1"/>
</GridView.ItemTemplate>
</GridView>
</Grid>
</UserControl>
このユーザー・コントロールにもページのデータ・コンテキスト(=DefaultViewModelプロパティ)が適用される。すると、内部のGridViewコントロールのItemsSourceプロパティには、DefaultViewModelプロパティが持っているデータの中から「URLs」というキーで識別されるデータ、すなわちURL文字列のコレクションがバインドされることになる。ということは、GridViewコントロールの各アイテムにはそれぞれのURL文字列がバインドされるので、データ・テンプレートの中でパスを指定せずにただ「{Binding}」と記述しているところでは、URL文字列がバインドされることになる。
以上のように、ユーザー・コントロールにもページのデータ・コンテキストが伝播(でんぱ)されることを利用すれば簡単にバインドできる。ただし、この方法では汎用性に欠ける。データ・コンテキストの内容が違うページでは利用できないからだ。逆にいえば、データ・コンテキストの内容が違うページでも、ページの側でバインドするデータを設定できれば、汎用的に利用できる。そのためには、ユーザー・コントロールのプロパティにバインドできればよい(次項)。
ユーザー・コントロールのプロパティをバインド可能にするには?
コントロールのプロパティを「依存関係プロパティ」として実装すればよい。
ここでは例として、WebViewコントロール(Windows.UI.Xaml.Controls名前空間)をラップしたユーザー・コントロールを作ってみよう。わざわざユーザー・コントロールとして仕立てる意味としては、Webページが表示されるまでの待ち時間にプログレス・リングを出しておきたいものとする。
まず、VS 2012でWindowsストア・アプリのプロジェクトを作成し、続いてメニューバーから[プロジェクト]−[新しい項目の追加]を選択する。すると、[新しい項目の追加]ダイアログが表示されるので[ユーザー コントロール]を選んでユーザー・コントロールをプロジェクトに追加し、「WebViewWithProgressRing.xaml」という名前を付けておく。
次に、作成した「WebViewWithProgressRing.xaml」ファイルに、ProgressRingコントロール(Windows.UI.Xaml.Controls名前空間)とWebViewコントロールを配置する(次のコード)。
<UserControl
x:Class="MetroTips045CS.WebViewWithProgressRing"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:MetroTips045CS"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignHeight="500"
d:DesignWidth="500">
<Grid>
<ProgressRing x:Name="progress" Width="200" Height="200" />
<WebView x:Name="webView" />
</Grid>
</UserControl>
このユーザー・コントロールを利用する側からは、UriオブジェクトをコントロールのSourceプロパティにバインドしてもらうことにしよう。このSourceプロパティは、普通のプロパティではなく依存関係プロパティとして実装する。コードビハインドの「WebViewWithProgressRing.xaml.cs/.vb」ファイルに、次のようにコードを追加する。
// 通常のプロパティ(依存関係プロパティのラッパー)
public Uri Source
{
get { return (Uri)GetValue(SourceProperty); }
set { SetValue(SourceProperty, value); }
}
// 依存関係プロパティ
public static readonly DependencyProperty SourceProperty =
DependencyProperty.Register(
"Source", typeof(Uri), typeof(WebViewWithProgressRing),
new PropertyMetadata(null, new PropertyChangedCallback(OnSourceChanged))
);
// 依存関係プロパティに値がセットされたときに呼び出されるメソッド
private static void OnSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
// ……後述……
}
' 通常のプロパティ(依存関係プロパティのラッパー)
Public Property Source As Uri
Get
Return CType(GetValue(SourceProperty), Uri)
End Get
Set(value As Uri)
SetValue(SourceProperty, value)
End Set
End Property
' 依存関係プロパティ
Public Shared ReadOnly SourceProperty As DependencyProperty = _
DependencyProperty.Register( _
"Source", GetType(Uri), GetType(WebViewWithProgressRing), _
New PropertyMetadata(Nothing, New PropertyChangedCallback(AddressOf OnSourceChanged)) _
)
' 依存関係プロパティに値がセットされたときに呼び出されるメソッド
Private Shared Sub OnSourceChanged(d As DependencyObject, e As DependencyPropertyChangedEventArgs)
' ……後述……
End Sub
上のコードで、DependencyProperty型の静的メンバ「SourceProperty」が、依存関係プロパティの本体だ。さらに、データ・バインディングではなくコードから直接プロパティを操作するために、Uri型のSourceプロパティも必要だ。Sourceプロパティは、SourceProperty依存関係プロパティをラップするように記述する。また、今回はSourceProperty依存関係プロパティに値がセットされたときにメソッドを呼び出したいので、DependencyProperty.Registerメソッドの第4引数でOnSourceChangedメソッドが呼び出されるように登録している。
このOnSourceChangedメソッドでは、ユーザー・コントロールに置いたWebViewコントロールを隠した上で新しいページへのアクセスを開始させるとともに、ProgressRingをアクティブにする。また、WebViewコントロールがロードを完了した時点で、逆にWebViewコントロールを表示し、ProgressRingを止める(次のコード)。
public sealed partial class WebViewWithProgressRing : UserControl
{
public WebViewWithProgressRing()
{
this.InitializeComponent();
this.webView.LoadCompleted += webView_LoadCompleted;
this.webView.NavigationFailed += webView_NavigationFailed;
}
// 通常のプロパティ(依存関係プロパティのラッパー)
……省略(前出)……
// 依存関係プロパティ
……省略(前出)……
// 依存関係プロパティに値がセットされたときに呼び出されるメソッド
private static void OnSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var thisInstance = d as WebViewWithProgressRing;
var uri = e.NewValue as Uri;
thisInstance.StartLoading(uri);
}
private async void StartLoading(Uri newUri)
{
// webViewを隠し、progressを動かす
this.webView.Visibility = Windows.UI.Xaml.Visibility.Collapsed;
this.progress.IsActive = true;
// ProgressRingの表示を確認しやすいように、わざと遅延を入れる
await System.Threading.Tasks.Task.Delay((new Random()).Next(1000, 3000));
// webViewにWebページへのアクセスを開始させる
this.webView.Navigate(newUri);
}
// webViewでロードが完了したときのイベント・ハンドラ
void webView_LoadCompleted(object sender, NavigationEventArgs e)
{
// webViewを表示し、progressを止める
this.webView.Visibility = Windows.UI.Xaml.Visibility.Visible;
this.progress.IsActive = false;
}
// webViewで表示に失敗したときのイベント・ハンドラ
void webView_NavigationFailed(object sender, WebViewNavigationFailedEventArgs e)
{
// ……省略……
}
}
Public NotInheritable Class WebViewWithProgressRing
Inherits UserControl
Public Sub New()
InitializeComponent()
AddHandler Me.webView.LoadCompleted, AddressOf webView_LoadCompleted
AddHandler Me.webView.NavigationFailed, AddressOf webView_NavigationFailed
End Sub
' 通常のプロパティ(依存関係プロパティのラッパー)
……省略(前出)……
' 依存関係プロパティ
……省略(前出)……
' 依存関係プロパティに値がセットされたときに呼び出されるメソッド
Private Shared Sub OnSourceChanged(d As DependencyObject, e As DependencyPropertyChangedEventArgs)
Dim thisInstance = CType(d, WebViewWithProgressRing)
Dim uri = CType(e.NewValue, Uri)
thisInstance.StartLoading(uri)
End Sub
Private Async Sub StartLoading(newUri As Uri)
' webViewを隠し、progressを動かす
Me.webView.Visibility = Windows.UI.Xaml.Visibility.Collapsed
Me.progress.IsActive = True
' ProgressRingの表示を確認しやすいように、わざと遅延を入れる
Await System.Threading.Tasks.Task.Delay((New Random()).Next(1000, 3000))
' webViewにWebページへのアクセスを開始させる
Me.webView.Navigate(newUri)
End Sub
' webViewで表示が完了したときのイベント・ハンドラ
Private Sub webView_LoadCompleted(sender As Object, e As Navigation.NavigationEventArgs)
' webViewを表示し、progressを止める
Me.webView.Visibility = Windows.UI.Xaml.Visibility.Visible
Me.progress.IsActive = False
End Sub
' webViewで表示に失敗したときのイベント・ハンドラ
Private Sub webView_NavigationFailed(sender As Object, e As WebViewNavigationFailedEventArgs)
' ……省略……
End Sub
End Class
これで、WebViewWithProgressRingユーザー・コントロールのSourceプロパティにWebページのUriをセットすると、まずプログレス・リングが表示され、Webページのロードが終わるとWebViewコントロールの表示に切り替わるようになる。
最後に、完成したユーザー・コントロールをページに配置してみよう。次のXAMLコードのように記述できる。
<GridView Grid.Row="1" ItemsSource="{Binding URLs}" Padding="120,0,0,20">
<GridView.Resources>
<DataTemplate x:Key="WebViewGridDataTemplate">
<Grid Margin="0">
<Border Background="Navy" >
<local:WebViewWithProgressRing Source="{Binding}"
Width="500" Height="600" />
</Border>
</Grid>
</DataTemplate>
</GridView.Resources>
<GridView.ItemTemplate>
<StaticResource ResourceKey="WebViewGridDataTemplate"/>
</GridView.ItemTemplate>
</GridView>
ここでも本稿の冒頭に示した例と同様に、ページのDefaultViewModelに「URLs」をキーとしてURL文字列のコレクションがセットされているものとする。すると、上のコードのGridViewコントロールのItemsSourceプロパティにはURL文字列のコレクションがバインドされ、GridViewコントロールの各アイテムのDataTemplateに配置されたWebViewWithProgressRingコントロールのSourceプロパティにはURL文字列がバインドされる。なお、URL文字列をUri型の依存関係プロパティにバインドする場合、バインディング・エンジンが自動的に文字列からUriオブジェクトへ変換してくれる(バリュー・コンバータは不要)。
最後に、コードビハインドに冒頭で示した「ページのDefaultViewModelに“URLs”をキーとしてURL文字列のコレクションをセットする」コードを追加すれば完成だ。実行してみると、次の画像のようにWebページが表示されるまでプログレス・リングが表示される。
まとめ
バインディング・ソースとなるプロパティにはINotifyPropertyChangedインターフェイス(System.ComponentModel名前空間)の実装が必要であった*2。そして、バインディング・ターゲットのプロパティは、依存関係プロパティでなければならない。これでデータ・バインディングのソースとターゲットの両方を理解できた。
なお、依存関係プロパティについて詳しくは次のドキュメントを参照してほしい。
*2INotifyPropertyChangedインターフェイスについては、「WinRT/Metro TIPS:文字列をコントロールにバインドするには?[Win 8/WP 8」を参照。
Copyright© Digital Advantage Corp. All Rights Reserved.