Silverlight 2でTodoアプリを作ってみる その1

VS2008もAmazonから届いて、やっとこさSilverlight 2の開発環境が整ったのでなんか作ってみることにした。

Silverlight Application」プロジェクトでプロジェクトを新規作成する。名前は「SLTodo」にする。

作成した直後のプロジェクト構成

画面的には、Todoの一覧を表示するリストとアイテムを追加するボタンが欲しい。

イメージ


メイン画面の作成

一番最初に表示されるメイン画面にあたるのが「Page.xaml」なので、このファイルを以下のように変更する。

Page.xaml
<UserControl x:Class="SLTodo.Page"
    xmlns="http://schemas.microsoft.com/client/2007" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    Width="400" Height="300">
    <Grid Background="White" Margin="8">
        <Grid.RowDefinitions>
            <RowDefinition Height="30" />
            <RowDefinition />
            <RowDefinition Height="40" />
        </Grid.RowDefinitions>
        
        <TextBlock Text="Silverlight Todo Application" VerticalAlignment="Center" />
        
        <ListBox x:Name="ItemList" Grid.Row="1" DisplayMemberPath="Title">
        </ListBox>
        
        <Button x:Name="AddButton" Content="Add" Grid.Row="2"
                Height="30" Width="60" HorizontalAlignment="Left" />
    </Grid>
</UserControl>
解説

Gridはレイアウト用のコンテナコントロールで、コントロールをテーブル状にレイアウトすることができる。

  • Background属性で背景色を、Margin属性で外側の隙間を指定できる(このへんはHTMLでおなじみなのでわかりやすい)。
<Grid Background="White" Margin="8">
...
</Grid>

ここでは、「タイトル用の行」「アイテム一覧用の行」「ボタン用の行」の三行が必要なので、Grid.RowDefinitions要素を追加し、その下のRowDefinition要素で行を定義している。

  • Height属性で行高さを指定できる。指定しないと自動調整される(なので二行目は残りの領域全部になる)。
<Grid.RowDefinitions>
    <RowDefinition Height="30" />
    <RowDefinition />
    <RowDefinition Height="40" />
</Grid.RowDefinitions>

TextBlockでタイトルを表示する。

  • Text属性で表示するテキストを、VerticalAlignment属性で垂直配置を指定できる。
<TextBlock Text="Silverlight Todo Application" VerticalAlignment="Center" />

ListBoxでTodoアイテムの一覧を表示する。データの表示自体は.NETでおなじみのデータバインドを使うので、その部分はコードで書く。

  • x:Name属性でコントロールのIDを指定できる。これを指定するとコードからこのIDでアクセスできる。
  • Grid.Row属性で何行目に表示するかを指定できる。
  • DisplayMemberPath属性でバインドされたオブジェクトのどのプロパティを表示するかを指定できる。ここではTitleプロパティを指定する(バインドするクラスはのちほど作る)。
<ListBox x:Name="ItemList" Grid.Row="1" DisplayMemberPath="Title">
</ListBox>

Buttonで追加ボタンを表示する。

  • Content属性でボタンのテキストを指定できる(なんでTextプロパティじゃないんだぜ?)。
  • Height、Widthはみたまんま、縦高さ、横幅をそれぞれ指定できる。
  • HorizontalAlignmentは水平配置を指定できる。
<Button x:Name="AddButton" Content="Add" Grid.Row="2"
        Height="30" Width="60" HorizontalAlignment="Left" />

ここまでで、一度ビルドして実行してみる。

実行結果

う〜ん、XAMLって今までいまいち使いにくいイメージがあったけど、案外使いやすい。これなら複雑なUIもそんなに手間をかけずに作れそう。でも、デザイナを使って編集するというのはいまいちイメージがわかない。エディタじゃないと逆に編集しにくいような気がする。

Todoアイテムの一覧を表示する

まずはTodoアイテムの情報を格納するクラスを定義する。ListBoxにバインドするクラスね。

TodoItem.cs
using System;
using System.Diagnostics;

namespace SLTodo {
    /// <summary>
    /// アイテムの情報を格納するクラス
    /// </summary>
    [DebuggerStepThrough]
    public class TodoItem {
        /// <summary>
        /// タイトルを取得、設定します。
        /// </summary>
        public string Title { get; set; }
        /// <summary>
        /// 詳細を取得、設定します。
        /// </summary>
        public string Description { get; set; }
        /// <summary>
        /// 完了したかどうかを取得、設定します。
        /// </summary>
        public bool Complete { get; set; }
    }
}

プロパティの定義が楽すぎるw

で、このオブジェクトの情報をどこから取得するかだけど、Silverlightアプリはクライアントサイドで動作するため、サーバーと通信して情報を取得する必要がある。Siliverlightは現段階では単純なHTTP問い合わせ(GETだけ?)しかサポートしていないので、クライアントからのリクエストに対してレスポンスをXMLで返すようなASP.NETページを作ってやれば、XMLデータからオブジェクトの情報を取得できる。

レスポンスとして返すXMLのフォーマットは以下。このファイルを「App_Data」フォルダ配下に保存しておく。

App_Data/items.xml
<?xml version="1.0" encoding="utf-8" ?> 
<?xml version="1.0" encoding="utf-8" ?> 
<Items>
    <Item>
        <Title>ご飯を食べる</Title>
        <Description>ご飯を食べに行く。焼肉が希望。</Description>
        <Complete>false</Complete>
    </Item>
    <Item>
        <Title>石鹸を買いに行く</Title>
        <Description>薬局に石鹸を買いに行く。ボディソープね。</Description>
        <Complete>false</Complete>
    </Item>
</Items>

「ListItem.aspx」というASP.NETページを新規作成して、コードビハインドファイルに以下の処理を追加する。

ListItem.aspx.cs
using System;
using System.IO;
using System.Text;

namespace SLTodo_Web {
    public partial class ListItem : System.Web.UI.Page {
        protected void Page_Load(object sender, EventArgs e) {
            string path = Server.MapPath("~/App_Data/items.xml");

            Encoding encoding = Encoding.UTF8;
            Response.ContentType = "text/xml";
            Response.ContentEncoding = encoding;
            Response.Output.Write(File.ReadAllText(path, encoding));
            Response.End();
        }
    }
}

「App_Data/items.xml」ファイルを読み込んでレスポンスストリームに書き込んでいる。

あとはSilverlight側からこのページにアクセスして、Todoアイテムの一覧を表示するだけ。

「Page.xaml」のルート要素のUserControlのLoadedイベントにUserControl_Loadedというイベントハンドラを追加する。

Page.xaml
<UserControl x:Class="SLTodo.Page"
    xmlns="http://schemas.microsoft.com/client/2007" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    Width="400" Height="300" Loaded="UserControl_Loaded">
    ...
</UserControl>

そのイベントハンドラに以下の処理を追加する。

Page.xaml.cs
using System;
using System.Net;
using System.Xml.Linq;
using System.Linq;
using System.Windows;
using System.Windows.Controls;

namespace SLTodo {
    public partial class Page : UserControl {
        public Page() {
            InitializeComponent();
        }

        private void UserControl_Loaded(object sender, RoutedEventArgs e) {
            WebClient webClient = new WebClient();
            webClient.DownloadStringCompleted += (_s, _e) => {
                XDocument xml = XDocument.Parse(_e.Result);

                var items = from item in xml.Descendants("Item")
                            select new TodoItem {
                                Title = (string)item.Element("Title"),
                                Description = (string)item.Element("Description"),
                                Complete = (bool)item.Element("Complete")
                            };

                ItemList.ItemsSource = items;
            };
            webClient.DownloadStringAsync(new Uri("http://localhost:1030/ListItem.aspx"));
        }
    }
}

解説

WebClientクラスを使うと非同期でHTTPリクエストを送ることができる。
DownLoadStringAsyncメソッドで「ListItem.aspx」ページにアクセスして、DownLoadStringCompletedイベントでレスポンスを取得する。

WebClient webClient = new WebClient();
webClient.DownloadStringCompleted += (_s, _e) => {
    ...
};
webClient.DownloadStringAsync(new Uri("http://localhost:1030/ListItem.aspx"));

イベント引数のResultプロパティからレスポンス出力を取得できるので、そこからLINQ to XMLを使って「Item」要素を検索しTodoItemオブジェクトを生成している(ちなみに匿名クラスを生成するとデータバインドで例外が出た)。

XDocument xml = XDocument.Parse(_e.Result);

var items = from item in xml.Descendants("Item")
            select new TodoItem {
                Title = (string)item.Element("Title"),
                Description = (string)item.Element("Description"),
                Complete = (bool)item.Element("Complete")
            };

あとはこの結果をListBoxのItemSourceプロパティに連結すればデータバインド完了。

ItemList.ItemsSource = items;

ビルドして実行する。

実行結果

う〜ん、素晴らしい!!