Silverlightの流儀 その1

この前のわんくま勉強会のWPF*1関連のセッションで学んだ事をメモっていく。

Silverlightにおける型変換(データと表示の分離)

SilverlightというかXAMLをいじっていて困ったことは、TextBlockとかTextBoxなんかのコントロールに値をバインドする時に例えば日付を表示させる場合、DateTime型のままでバインドすると「4/29/2008 8:31:10 AM」というようなカレントのカルチャーに依存した形式で表示されることになる。
この表示形式をデータバインド時にカスタマイズする方法がわからなくて、わざわざ任意の形式の文字列に変換したり、文字列に変換するプロパティを作ってそれにバインドしたりしていた。

こんなの作ってた
class Entity {
    public DateTime Date {
        get;
        set;
    }
    // 任意の形式に変えた文字列を返すプロパティ
    public string DateString {
        get {
            return Date.ToString("yyyy/MM/dd", CultureInfo.InvariantCulture);
        }
    }
}

これがすごく野暮ったい事だとはわかっていたけど、Silverlightの流儀がよくわからなかったので放置していたけど、今回の勉強会でやっとこさやり方がわかった。

その方法はIValueConverterというインターフェースを使ったやり方。これは.NETのTypeConverterとよく似た仕組みなのですんなり理解できた。

どう使うかというと、まずIValueConverterインターフェースを実装した「DateTimeConverter」というクラスを作る。

DateTimeConverter.cs

using System;
using System.Windows.Data;
using System.Globalization;
using System.Diagnostics;

namespace SilverlightApplication1 {    
   public class DateTimeConverter : IValueConverter {
       public DateTimeConverter() {
       }

       public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
           if(value != null && targetType == typeof(string)) {
               var format = parameter != null ? parameter.ToString() : "yyyy/MM/dd";

               return ((DateTime)value).ToString(format, culture);
           }
           // 本当は↓を返したいけどSilverlightにはまだ?ない。
           // return DependencyProperty.UnsetValue;
           return null;
       }

       public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
           return null;
       }
   }

}

Convertメソッドで対象の型に変換して、ConvertBackでその逆の事を行う。ここではtargetTypeが文字列の時だけ「yyyy/MM/dd」の形式で日付を書式設定している。

これの使い方はというと、まず以下のように現在の日付を返すプロパティを持つ「PageModel」というクラスを定義する。

PageModel.cs

using System;
using System.ComponentModel;
using System.Diagnostics;

namespace SilverlightApplication1 {
   public class PageModel : INotifyPropertyChanged {
       private DateTime date = DateTime.Now;

       /// <summary>
       /// 日付を取得、設定します。
       /// </summary>
       public DateTime Date {
           get { return date; }
           set {
               date = value;

               OnPropertyChanged(new PropertyChangedEventArgs("Date"));
           }
       }

       public PageModel() {
       }

       /// <summary>
       /// プロパティの値が変更された時に呼び出されます。
       /// </summary>
       public event PropertyChangedEventHandler PropertyChanged;
       /// <summary>
       /// PropertyChangedイベントを呼び出します。
       /// </summary>
       /// <param name="e">イベント引数</param>
       protected virtual void OnPropertyChanged(PropertyChangedEventArgs e) {
           if(PropertyChanged != null) PropertyChanged(this, e);
       }
   }
}

INotifyPropertyChangedインターフェースを実装して、Dateプロパティが変更された時にPropertyChangedイベントを発生させるようにしている。

これをすると外部にプロパティの値が変更されたことを通知できることはわかるけど、今のところこれがどういう意味を持っているのかはわからない。あとで調べる。

[追記]
kazzzさんがINotifyPropertyChangedインターフェースを実装する理由をわかりやすく説明してた。
カスタム属性を用いたデータバインディングの省力化(INotifyPropertyChanged実装編) - Kazzzの日記

.NET Framework2.0のデータバインディングは、自身に格納されたオブジェクトがINotifyPropertyChangedインタフェースを実装していることを検知すると、このイベントハンドラによりバインドされているコントロールのプロパティに変更にされた値を反映してくれるのである。(BindingSourceの無い、.NET1.1ではプロパティ名+Changedという名前のイベントをプロパティ個々に用意する必要があった)

で、こいつを画面にバインドする。

Page.xaml
<UserControl x:Class="SilverlightApplication1.Page"
   xmlns="http://schemas.microsoft.com/client/2007"
   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   xmlns:my="clr-namespace:SilverlightApplication1">
    
   <UserControl.DataContext>
       <my:PageModel />
   </UserControl.DataContext>
    
   <Grid Margin="4">
       <TextBlock Text="{Binding Date}" />
   </Grid>
    
</UserControl>

DataContextプロパティにPageModelという宣言を追加することで、PageModelクラスをインスタンス化してプロパティに設定してくれる。これはまるっきりDIの方法論。こんな事ができるのにはちょっとびっくりした。

あとはTextBlockのTextプロパティにDateプロパティをバインドしている。

これを表示すると「4/29/2008 8:31:10 AM」というような形式の文字列が表示されると思う。

これに先程作ったDateTimeConverterを適用するには、バインディングの設定を以下のように変更する。

Page.xaml

<UserControl x:Class="SilverlightApplication1.Page"
   xmlns="http://schemas.microsoft.com/client/2007"
   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   xmlns:my="clr-namespace:SilverlightApplication1">
    
   <UserControl.Resources>
       <my:DateTimeConverter x:Key="dateTimeConverter" />
   </UserControl.Resources>
    
   <UserControl.DataContext>
       <my:PageModel />
   </UserControl.DataContext>
    
   <Grid Margin="4">
       <TextBlock Text="{Binding Date, Converter={StaticResource dateTimeConverter}}" />
   </Grid>
    
</UserControl>

リソースにDateTimeConverterを宣言して、バインディング式にConverterという設定を追加している。

これで表示すると「2008/4/29」という形式の文字列が表示される。

こんな感じでデータの実体と表示形式を分離することができる。ちょっと面倒くさいけどね。

他にもConverterParameterやConverterCultureなんてのも渡せるみたいなので、汎用的なコンバータを作りやすくなっている。

う〜ん、奥が深いなぁ。

*1:WPFの流儀の多くはSilverlightでも通用するだろうからこのタイトルにした