バインドできるユーザー・コントロールを作るには?[Win 8]WinRT/Metro TIPS

自作のユーザー・コントロールに独自のプロパティを実装し、そこにデータ・バインドする方法を解説する。

» 2013年07月18日 16時09分 公開
[山本康彦BluewaterSoft]
WinRT/Metro TIPS
業務アプリInsider/Insider.NET

powered by Insider.NET

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

連載目次

 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

ページのDefaultViewModelに「URLs」をキーとしてURL文字列のコレクションをセットする(上:C#、下:VB)
上はLayoutAwarePageクラスを継承した場合である。継承しない場合は、DefaultViewModelプロパティの実装を独自に行ったうえで、OnNavigatedToメソッドでセットする。

 すると、このページに配置して利用するユーザー・コントロールでは、「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>

前出のコードのURL文字列をユーザー・コントロール内部のテキスト・ブロックにバインドする(XAML)
ソリューション・エクスプローラでプロジェクトを右クリックしてコンテキスト・メニューから[追加]−[新しい項目]を選択し、[新しい項目の追加]ダイアログで[ユーザー コントロール]を選択してMyUserControl1.xamlファイルを作成した後に、上記のコードを記述する。
DataTemplateを、一度にお見せできるようにGridViewコントロールの内部に記述しているが、ページやApp.xamlファイルのリソースとして記述してもよい。
作成したユーザー・コントロールは通常のコントロールと同様に使用できる。

 このユーザー・コントロールにもページのデータ・コンテキスト(=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>

ユーザー・コントロールにProgressRingコントロールとWebViewコントロールを配置した(XAML)
先頭のUserControlタグ内にあるx:Class属性とxmlns:local属性の値は、プロジェクトの名前空間が入っているので、場合によって異なる。

 このユーザー・コントロールを利用する側からは、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

WebViewWithProgressRingクラスに依存関係プロパティを追加した(上:C#、下:VB)

 上のコードで、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クラスの完成形(上:C#、下:VB)

 これで、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>

WebViewWithProgressRingユーザー・コントロールをページに配置した(XAML)
DataTemplateを、一度にお見せできるようにGridViewコントロールの内部に記述しているが、ページやApp.xamlファイルのリソースとして記述してもよい。

 ここでも本稿の冒頭に示した例と同様に、ページのDefaultViewModelに「URLs」をキーとしてURL文字列のコレクションがセットされているものとする。すると、上のコードのGridViewコントロールのItemsSourceプロパティにはURL文字列のコレクションがバインドされ、GridViewコントロールの各アイテムのDataTemplateに配置されたWebViewWithProgressRingコントロールのSourceプロパティにはURL文字列がバインドされる。なお、URL文字列をUri型の依存関係プロパティにバインドする場合、バインディング・エンジンが自動的に文字列からUriオブジェクトへ変換してくれる(バリュー・コンバータは不要)。

 最後に、コードビハインドに冒頭で示した「ページのDefaultViewModelに“URLs”をキーとしてURL文字列のコレクションをセットする」コードを追加すれば完成だ。実行してみると、次の画像のようにWebページが表示されるまでプログレス・リングが表示される。

Webページが表示されるまでの間、プログレス・リングが回転している Webページが表示されるまでの間、プログレス・リングが回転している

まとめ

 バインディング・ソースとなるプロパティにはINotifyPropertyChangedインターフェイス(System.ComponentModel名前空間)の実装が必要であった*2。そして、バインディング・ターゲットのプロパティは、依存関係プロパティでなければならない。これでデータ・バインディングのソースとターゲットの両方を理解できた。

 なお、依存関係プロパティについて詳しくは次のドキュメントを参照してほしい。

*2INotifyPropertyChangedインターフェイスについては、「WinRT/Metro TIPS:文字列をコントロールにバインドするには?[Win 8/WP 8」を参照。


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

WinRT/Metro TIPS

Copyright© Digital Advantage Corp. All Rights Reserved.

RSSについて

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

メールマガジン登録

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