XAML へのシリアライズ (その 1)

WPF をはじめとするユーザー インターフェイスを記述するための言語として紹介されることの多い XAML ですが、
XAML 自体は WPF などに依存しているわけではなく、例えば WF のアクティビティを記述するために利用されたり、
さらには設定情報や簡易的なデータを保存しておくためのストアとして利用されたりすることもあります。

この記事では、オブジェクトを XAML にシリアライズするために必要な情報をまとめていきます。
今回はまず、ツリー型のデータ構造を持つオブジェクトを取り扱います。

 

■ シリアライズの例

先に具体例を示します。
プロパティの定義や属性の指定の注意点については後ほど解説します。

クラス ライブラリ プロジェクトを作成し、参照に System.Xaml.dll を追加します。
次のようにクラスを定義します。

Preference.cs


using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Windows.Markup;

namespace XamlClassLib
{
    // コレクションをコンテンツとするクラス。
    [DebuggerDisplay(@"\{{Name}\}")]
    [ContentProperty("Cities")]
    public class Preference
    {
        public string Name { get; set; }
        Collection<City> cities = new Collection<City>();
        public Collection<City> Cities { get { return cities; } }
    }

    // 単一オブジェクトをコンテンツとするクラス。
    [DebuggerDisplay(@"\{{Name}\}")]
    [ContentProperty("Mayor")]
    public class City
    {
        public string Name { get; set; }
        public Coordinate Coordinate { get; set; }
        public Person Mayor { get; set; }
        public Statistics Statistics { get; set; }
        public string[] Products { get; set; }
        Collection<Person> people = new Collection<Person>();
        public Collection<Person> People { get { return people; } }
    }

    // コンテンツが存在しないクラス。
    [DebuggerDisplay(@"\{{Name}\}")]
    public class Person
    {
        public string Name { get; set; }
        public DateTime Birthday { get; set; }
    }

    // 既定値を利用するクラス。
    public class Statistics
    {
        [DefaultValue(1)]
        public int Area { get; set; }
        [DefaultValue(false)]
        public bool HasAirport { get; set; }

        public Statistics()
        {
            Area = 1;
        }
    }

    // 型コンバーターを利用するクラス。
    [TypeConverter(typeof(ConventionalStringConverter<Coordinate>))]
    public class Coordinate
    {
        public double Latitude { get; set; }
        public double Longitude { get; set; }

        public static Coordinate Parse(string value)
        {
            var s = value.Split(‘,’);
            return new Coordinate { Latitude = double.Parse(s[0]), Longitude = double.Parse(s[1]), };
        }

        public override string ToString()
        {
            return string.Format("{0},{1}", Latitude, Longitude);
        }
    }

    // シリアライズには ToString メソッドを、デシリアライズには Parse メソッドを利用する型コンバーター。
    public class ConventionalStringConverter<T> : TypeConverter
    {
        public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
        {
            return sourceType == typeof(string);
        }

        public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
        {
            var v = (string)value;
            var parse = typeof(T).GetMethod("Parse", BindingFlags.Static | BindingFlags.Public);
            if (parse == null) throw new InvalidOperationException(string.Format("{0} クラスに Parse メソッドが見つかりません。", typeof(T).Name));
            return parse.Invoke(null, new object[] { v });
        }

        public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
        {
            return context.Instance is T && destinationType == typeof(string);
        }

        public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
        {
            return value.ToString();
        }
    }
}


 

さらに、AssemblyInfo.cs に、次の属性を追加します。
このようにすると、XML 名前空間を定義できます。この定義は必須というわけではありません。
XML 名前空間については、XAML 上での XML 名前空間の使用を参照するとよいでしょう。

[assembly: XmlnsDefinition("http://schemas.saka-pon.net/xamlsample", "XamlClassLib")]
[assembly: XmlnsPrefix("http://schemas.saka-pon.net/xamlsample", "xs")]

 

次に、コンソール アプリケーション プロジェクトを作成します。
適当なデータでオブジェクトを構築して、XAML にシリアライズします。

Program.cs


using System;
using System.Collections.Generic;
using System.Linq;
using System.Xaml;
using XamlClassLib;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            var chuo = new City
            {
                Name = "中央区",
                Coordinate = new Coordinate { Latitude = 35.7, Longitude = 139.8 },
                Mayor = new Person { Name = "月島さん", Birthday = new DateTime(1950, 1, 1) },
                Statistics = new Statistics { Area = 1, HasAirport = false },
                Products = new[] { "人形焼", "佃煮" },
            };
            chuo.People.Add(new Person { Name = "晴海さん" });
            chuo.People.Add(new Person { Name = "築地さん" });

            var ota = new City
            {
                Name = "大田区",
                Coordinate = new Coordinate { Latitude = 35.6, Longitude = 139.7 },
                Mayor = new Person { Name = "蒲田さん", Birthday = new DateTime(1950, 1, 1) },
                Statistics = new Statistics { Area = 2, HasAirport = true },
                Products = new string[0],
            };

            var tokyo = new Preference { Name = "東京都" };
            tokyo.Cities.Add(chuo);
            tokyo.Cities.Add(ota);

            XamlServices.Save("Tokyo.xaml", tokyo);
        }
    }
}


 

XamlServices.Save メソッドにより、次のようにシリアライズされます。
逆にこれを、XamlServices.Load メソッドでデシリアライズできます。

Tokyo.xaml


<Preference Name="東京都"
  xmlns="http://schemas.saka-pon.net/xamlsample"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
  <City Coordinate="35.7,139.8" Name="中央区">
    <City.People>
      <Person Birthday="0001-01-01" Name="晴海さん" />
      <Person Birthday="0001-01-01" Name="築地さん" />
    </City.People>
    <City.Products>
      <x:Array Type="x:String">
        <x:String>人形焼</x:String>
        <x:String>佃煮</x:String>
      </x:Array>
    </City.Products>
    <City.Statistics>
      <Statistics />
    </City.Statistics>
    <Person Birthday="1950-01-01" Name="月島さん" />
  </City>
  <City Products="{x:Array Type=x:String}" Coordinate="35.6,139.7" Name="大田区">
    <City.Statistics>
      <Statistics Area="2" HasAirport="True" />
    </City.Statistics>
    <Person Birthday="1950-01-01" Name="蒲田さん" />
  </City>
</Preference>


 

以下では、オブジェクトを XAML にシリアライズおよびデシリアライズする際の指針や注意点について解説します。

■ プロパティの定義

(1) 非コレクション型のプロパティ

コレクション型以外のプロパティ (配列型を含む) は、get と set の対で宣言します。
上記の例のような自動実装プロパティである必要はありません。

(2) コレクション型のプロパティ

コレクション型 (ICollection<T> インターフェイスを実装した型) のプロパティは、get のみを宣言します。
プロパティの型宣言は ICollection<T> でもかまいません。
例えば、次のようにします。

Collection<City> cities = new Collection<City>();
public Collection<City> Cities { get { return cities; } }

以下は注意点です。

  • { get; private set; } と宣言した場合、
    XAML ファイルをコンパイルすると、set にアクセスできないという理由で失敗してしまうことがあります。
  • { get; set; } と宣言した場合、
    オブジェクトをシリアライズすると、上記の例では <City> の親要素として
    <sco:Collection x:TypeArguments="City"> が出力されてしまうことがあります。

 

■ 属性の指定

(1) TypeConverter 属性

オブジェクトを XML 要素としてではなく、XML 属性として出力させることができます。
TypeConverter を継承したクラスを作成しておく必要があります。
上記の例の Coordinate クラスのように、文字列表現が容易なオブジェクトに対して適用すると効果的です。

ただし、場合により次の問題点があります。
TypeConverter の CanConvertFrom メソッドでは変換前の型は渡されますが、値は渡されないため、
本当に変換できるかどうかは ConvertFrom メソッドが実行されてからでないとわからないことがあります。

このような場合、ValueSerializer を継承したクラスを追加し、ValueSerializer 属性を追加します。
ValueSerializer 属性は単独で指定しても効果を発揮しないようで、TypeConverter 属性と併用する必要があります。

TypeConverter では文字列に変換されるとは限りません (XAML 以外の用途でも利用される) が、
ValueSerializer は XAML 上での文字列表現に特化されたコンバーターです。

(2) ContentProperty 属性

対象のクラスに、コンテンツを表すプロパティを指定します。
コンテンツとして指定されたプロパティは、直下の子要素としてシリアライズされます。
例えば、<Preference> の直下に、<Preference.Cities> のような要素を挟まずに <City> が出力されます。

(3) DefaultValue 属性

対象のプロパティに、既定値を指定します。
シリアライズの際、プロパティの値がこの既定値と等しい場合は XML 属性が出力されなくなります。

ただし、逆に XML 属性のないプロパティをデシリアライズする際には既定値を設定してくれません。
したがって、既定値が自動で設定されないプロパティ (依存関係プロパティでないなど) の場合、
コンストラクターで既定値を設定しなければなりません。

(4) DesignerSerializationVisibility 属性

確か Visual Studio 2008 の頃の環境では、コレクション型のプロパティに

[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]

が指定されていないと開発ツールでうまくシリアライズされなかった気がしますが、
現在の環境ではこの属性を指定しなくても問題なさそうに見えます。

 

次回の XAML へのシリアライズ (その 2) では、
オブジェクト参照を用いて、XAML をオブジェクト指向データストアとして利用する方法を紹介します。

バージョン情報
.NET Framework 4.5

参照
XAML の概要 (WPF)
XamlServices クラス
XAML 上での XML 名前空間の使用

カテゴリー: .NET Framework. タグ: . 1 Comment »

コメント / トラックバック1件 to “XAML へのシリアライズ (その 1)”

  1. XAML へのシリアライズ (その 2) | Do Design Space Says:

    […] XAML へのシリアライズ (その 1) […]


コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト / 変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト / 変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト / 変更 )

Google+ フォト

Google+ アカウントを使ってコメントしています。 ログアウト / 変更 )

%s と連携中

%d人のブロガーが「いいね」をつけました。