mikeo_410


ListBoxのバインディング

バインディングのサンプル


  お菓子検索のサンプルをWPFブラウザ・アプリケーションで作った時には、ほとんどコードを書かずに表示を行うことができました。Silverlightアプリケーションではそうはいかないようです。    お菓子検索のサンプルプログラムは、左図をクリックするとポップアップウインドウを開いて実行します。
  お菓子の虜 Web API の検索機能を使っています。

  これを元に、バインディングのことだけの検討ができるようなサンプルを作りました。

WPFのサンプル

サーバー上のデータ

  URLで参照できるように、httpサーバに以下のものを置きます。

  1. イメージファイル
    kuri.png などのイメージファイルを、http://localhost/PNG/kuri.png でアクセスできる位置に置きました。
  2. fruit.xml を、http://localhost/fruit.xml で参照できる位置に置きました。
    1. <fluits>
    2.     <plate>
    3.         <color>DeepPink</color> 
    4.         <content>http://localhost/PNG/kuri.png</content>
    5.     </plate>
    6.     <plate>
    7.         <color>Turquoise</color> 
    8.         <content>http://localhost/PNG/strawberry.png</content>
    9.     </plate>
    10.     <plate>
    11.         <color>Gold</color> 
    12.         <content>http://localhost/PNG/muscat.png</content>
    13.     </plate>
    14. </fluits>

作成したサンプルの概要


  前述の、fruit.xml を読み込んで、左図のような表示をします。
  fruit.xml に、3回 plate があるので、ListBoxに3行表示されます。

  fruit.xml では、plate の色と果物が指定されています。

  この表示は、すべてXAMLで記述されており、ビハインドコードの記述なしに表示されます。

  このプログラムに修正を加えて、行を選ぶと、乗っている果物だけが入れ替わるようにします。
  これは、ビハインドコードで行います。これによって、データを変更すると、表示まで変わることを確認します。

  ほとんど、コードを書かずに表示までできるのは、WPFアプリケーションだからです。
  XmlDataProvider の Source にXMLファイルの URL を指定すると、自動的に取得して内容をによってバインディングを行います。
  初期表示だけでなく、データが変更されればダイナミックに表示に反映もします。

  XmlDataProvider は、Silvelight にはありません。このあたりの差異を明確にして置きたいと思います。

XAML の記述

  XAMLでは、ルートの Grid に、ListBox を配置しています。
  その前の、DataContext に XmlDataProvider を指定していることが要点で、ここに記述したURLからデータを取得して自動的に ListBox の表示が行われます。
  ListBoxに、SelectionChanged イベントハンドラが設定されていますが、後述する動的変更のテストためのもので、XMLファイルの内容と表示のバインディングには関係ありません。

  1. <Grid>
  2.     <!--データプロバイダ-->
  3.     <Grid.DataContext>
  4.         <XmlDataProvider x:Name="xmlDataProvider1" XPath="fluits/plate" Source="http://localhost/fruit.xml"/>
  5.     </Grid.DataContext>
  6.     <!--ListBox-->
  7.     <ListBox Name="listBox1" ItemsSource="{Binding}" ItemTemplate="{DynamicResource ListItemTemplate}" 
  8.              IsSynchronizedWithCurrentItem="True" SelectionChanged="ListBox_SelectionChanged"/>
  9. </Grid>

  ListBoxの各行の表示スタイルは、以下のように記述してあります。このリソースを ListBox の ItemTemplate で参照しています。

  1. <Window.Resources>
  2.     <DataTemplate x:Key="ListItemTemplate">
  3.         <Border Margin="2" Height="100" BorderBrush="Blue" BorderThickness="1" CornerRadius="3">
  4.             <Grid>
  5.                 <Grid.ColumnDefinitions>
  6.                     <ColumnDefinition Width="100" />
  7.                     <ColumnDefinition Width="220" />
  8.                 </Grid.ColumnDefinitions>
  9.                 <!--絵-->
  10.                 <Border CornerRadius="48" BorderBrush="Blue" BorderThickness="1" Background="{Binding XPath=color}">
  11.                 <Image Grid.Column="0"  Margin="2,2,2,2" Source="{Binding XPath=content}" Stretch="None"/>
  12.                 </Border>
  13.                 <!--項目値-->
  14.                 <StackPanel Grid.Column="1">
  15.                     <TextBlock Height="40" Text="{Binding XPath=content}"/>
  16.                     <TextBlock Height="40" Text="{Binding XPath=color}"/>
  17.                 </StackPanel>
  18.             </Grid>
  19.         </Border>
  20.     </DataTemplate>
  21. </Window.Resources>

動的変更

  データバインディングの目的は、初期表示だけでなく、操作などでデータが変更されたら自動的で表示に反映することも含まれます。
  このテストに、行選択を使います。ListBox で行の選択が変更されたら、データーを変えて見ます。

  このサンプルは、XAMLだけで表示していてデータセットを記述していません。データを変えると言っても明瞭ではありません。
  調べてみると、ListBox の Items(行のコレクション)には、XmlDocument の ノードが、行として格納されています。
  このノードを入れ替えて、「データの変更」とすることにしました。

  行が選択されると、その前の行と、果物を交換して見ます。行の選択変更イベントハンドラに以下のように書きました。

  1. private void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
  2. {
  3.     if (listBox1.SelectedIndex < 0) return;
  4.     //選択された行の content ノードを取り外しておく(ChildNodesの[0]は、color)
  5.     XmlElement current = listBox1.Items[listBox1.SelectedIndex] as XmlElement;
  6.     XmlNode current_node = current.RemoveChild(current.ChildNodes[1]);
  7.     //一つ前の行を見つける。今が先頭行なら最終行を採る。
  8.     int pi = listBox1.SelectedIndex - 1;
  9.     if (pi < 0) pi = listBox1.Items.Count - 1;
  10.     XmlElement prev = listBox1.Items[pi] as XmlElement;
  11.     //一つ前の行の content ノードを取り外しておく
  12.     XmlNode prev_node = prev.RemoveChild(prev.ChildNodes[1]);
  13.     //入れ替えて追加
  14.     current.AppendChild(prev_node);
  15.     prev.AppendChild(current_node);
  16. }

  表示は、記述した動作通りに変化します。

Silverlight のサンプル

XAML記述の差異

  最初に、WPFのサンプルに使った XAML のリソースやコントロールを、Silverlight のXAMLに移して実行できるようにします。
  コンパイルできるようにモジュールを追加するアプローチも可能なのかもしれませんが、今回は標準で可能なものに置き換えました。

  1. XPath は、Path に
  2. ItemTemplate="{DynamicResource ListItemTemplate}の DynamicResource は、StaticResourceに
  3. IsSynchronizedWithCurrentItem="True"は、初期化でエラーになるので削除
  4. XmlDataProvider は Grid.DataContex ブロックごと削除

XMLファイルのダウンロード

  Silverlight アプリケーションの場合は、XmlDataProvider を記述できなかったので、プログラムでXMLファイルをダウンロードします。
  ルートの Grid の Loaded イベントで、XMLファイルをダウンロードします。
  受信完了ハンドラには、文字列が渡されます。XDocument にして置きます。

  1. private void LayoutRoot_Loaded(object sender, RoutedEventArgs e)
  2. {
  3.     WebClient wc = new WebClient();
  4.     wc.DownloadStringCompleted += new DownloadStringCompletedEventHandler(wc_DownloadStringCompleted);
  5.     wc.DownloadStringAsync(new Uri("http://localhost/fruit.xml"));
  6. }
  7. void wc_DownloadStringCompleted(object sender, DownloadStringCompletedEventArgs e)
  8. {
  9.     XDocument doc = XDocument.Parse(e.Result);
  10.     
  11. }

XMLのパースとデータセット

  受け取ったXMLドキュメントは単なる文字列なので、構造をもった形式に変換することが必要です。
  前のリストのようにして、XDocument にしました。
  このドキュメントは、ListBoxのソースには代入できませんでした。
  表示と結びついたデータセットを作る必要があります。以下のようなクラスの List を使うことにしました。

  1. public class DataFromHost
  2. {
  3.     public string color { get; set; }
  4.     public string content { get; set; }
  5. }
  6. List<DataFromHost> list = new List<DataFromHost>();

  XDocument から list に移して、ListBox のソースにするように、受信完了ハンドラを以下のようにしました。

  1. void wc_DownloadStringCompleted(object sender, DownloadStringCompletedEventArgs e)
  2. {
  3.     XDocument doc = XDocument.Parse(e.Result);
  4.     foreach (XElement xe in doc.Root.Elements("disk"))
  5.     {
  6.         DataFromHost data = new DataFromHost();
  7.         data.color = (string)xe.Element("color");
  8.         data.content = (string)xe.Element("content");
  9.         list.Add(data);
  10.     }
  11.     listBox1.ItemsSource = list;
  12. }

ここまでの結果

  ここまでの修正で実行して見ると、果物の絵の出ない状態で表示されます。
  Image に最初から URL を指定しても表示されないことから、何かアクセスするための条件が満たされていないようです。
  コードではダウンロードできるので、イメージをバインドするようにします。

ItemsSource と Items.Add

  ListBox に表示するには、ItemsSource に コレクションを代入したり、行ごとに Items.Add() します。
  ItemsSource が null でない時には、Items は読み取り専用に設定され、Items.Add()、Items.Clear() などができません。
  これは、ItemsSource にコレクションを設定することが、コレクションのデータセットを操作することの意思表示と解釈されるだと思います。
  できるだけXAMLで記述すると言うWPFのサンプルとは異なりますが、より実用てきな、データセットで操作する方向でサンプルを作ります。
  このことと、イメージの扱いのために、DataFromHost クラスを変更します。

  DataFromHostクラス

  1. public class DataFromHost : INotifyPropertyChanged
  2. {
  3.     string _color;
  4.     string _content;
  5.     BitmapImage _image;
  6.     #region INotifyPropertyChanged メンバ
  7.     public event PropertyChangedEventHandler PropertyChanged;
  8.     #endregion
  9.     void SetEvent(string propertyName)
  10.     {
  11.         if (PropertyChanged != null)
  12.             PropertyChanged.Invoke(this, new PropertyChangedEventArgs(propertyName));
  13.     }
  14.     //プロパティの操作
  15.     public string color
  16.     {
  17.         get { return _color; }
  18.         set { _color = value; SetEvent("color"); }
  19.     }
  20.     public string content
  21.     {
  22.         get { return _content; }
  23.         set { _content = value; LoadImage();  SetEvent("content"); }
  24.     }
  25.     public BitmapImage image 
  26.     {
  27.         get { return _image; }
  28.         set { _image = value; SetEvent("image"); }
  29.     }
  30.     //イメージをダウンロード
  31.     void LoadImage()
  32.     {
  33.         WebClient wc = new WebClient();
  34.         wc.OpenReadCompleted += new OpenReadCompletedEventHandler(wc_OpenReadCompleted);
  35.         wc.OpenReadAsync(new Uri(content));
  36.     }
  37.     //イメージ受信完了時の非同期な処理
  38.     void wc_OpenReadCompleted(object sender, OpenReadCompletedEventArgs e)
  39.     {
  40.         if (e.Error == null)
  41.         {
  42.             BitmapImage bi = new BitmapImage();
  43.             bi.SetSource(e.Result);
  44.             image = bi;
  45.         }
  46.     }
  47. }

  INotifyPropertyChanged インタフェースを持ち、XAMLで定義した表示コントロールへプロパティ変更を通知します。
  content プロパティの set で、イメージのダウンロードを行います。
  XAMLは以下の通りです。

  XAML

  1. <UserControl x:Class="sla_xml_bind.MainPage"
  2.     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
  3.     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  4.     xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
  5.     mc:Ignorable="d" d:DesignWidth="640" d:DesignHeight="480">
  6.     <UserControl.Resources>
  7.         <DataTemplate x:Key="ListItemTemplate">
  8.             <Border Margin="2" Height="100" BorderBrush="Blue" BorderThickness="1" CornerRadius="3">
  9.                 <Grid>
  10.                     <Grid.ColumnDefinitions>
  11.                         <ColumnDefinition Width="100" />
  12.                         <ColumnDefinition Width="220" />
  13.                     </Grid.ColumnDefinitions>
  14.                     <!--絵-->
  15.                     <Border CornerRadius="48" BorderBrush="Blue" BorderThickness="1" Background="{Binding Path=color}" >
  16.                         <Image Name="image1" Grid.Column="0"  Margin="2,2,2,2" Source="{Binding Path=image}" Stretch="None"/>
  17.                     </Border>
  18.                     <!--項目値-->
  19.                     <StackPanel Grid.Column="1">
  20.                         <TextBlock Height="40" Text="{Binding Path=content}"/>
  21.                         <TextBlock Height="40" Text="{Binding Path=color}"/>
  22.                     </StackPanel>
  23.                 </Grid>
  24.             </Border>
  25.         </DataTemplate>
  26.     </UserControl.Resources>
  27.     <Grid x:Name="LayoutRoot" Loaded="LayoutRoot_Loaded" Width="340">
  28.         <!--ListBox-->
  29.         <ListBox Name="listBox1" ItemTemplate="{StaticResource ListItemTemplate}" ItemsSource="{Binding}"
  30.                  SelectionChanged="ListBox_SelectionChanged"/>
  31.     </Grid>
  32. </UserControl>

動的な表示変更

  ListBox の行の選択が変わった時に、皿の上の果物だけを交換して見ます。
  ListBox の ItemsSource に設定した、list コレクションを操作して、content を入れ替えます。

  1. private void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
  2. {
  3.     if (listBox1.SelectedIndex < 0) return;
  4.     int pi = listBox1.SelectedIndex - 1;
  5.     if (pi < 0) pi = listBox1.Items.Count - 1;
  6.     string tmp = list[listBox1.SelectedIndex].content;
  7.     list[listBox1.SelectedIndex].content = list[pi].content;
  8.     list[pi].content = tmp;
  9. }

  その後、いつの間にか、左図をクリックしても枠しか表示されないようになっているのに気が付きました。
  この例では実行時に動的に、xmlファイルやイメージファイルをダウンロードしますが、正しく参照できていませんでした。

1..xml を、xapと同じサーバ上のディレクトリ階層に置きました。
     .xapと同じ階層に、PNGディレクトリを作成、イメージ(.png)を入れました。
2.参照に使うURIを絶対形式にしました。
     Application.Current.Host.Source.OriginalString
     が、.xap のURLを示すので、これから絶対形式のURIを作ります。


mikeo_410@hotmail.com