System.Xaml.dllについて(その1)
1ヶ月も何も書かずに過ぎてしまった。というわけでリハビリがてら、System.Xaml.dllの話でも書こうと思う。長くなりそうなので分けて書くことにする。気が向いた時に続く。
Introduction
System.Xaml.dllはXAML 2009と言われる文法で書かれたXAMLを読み書きしてオブジェクトをXMLにしたりその逆をやったりする.NET 4.0のライブラリだ。昔はどうやらWPFのPresentationFramework.dllにload/saveだけ出来る物があったらしいが、これはその細かい動作まで規定されている感じだ。
基本的にfull profileが前提になっていて、Silverlightでは使えない…のだけど、しばらく前に昔のコードベースを無理矢理Moonlightベースでいじってみたりもしてみたので、使えるようにもなるかもしれない。(その後System.Xaml.dll自体を大幅に書き直し中なのでいずれにしろ実用には耐えない。)
この「XAML 2009」の文法については、[MS-XAML-2009]でMS-OSPの一部として文書化されている。XMLの名前空間はXAML 2006と同じだけど、文法は多少拡張されている。
ちなみにSilverlightのXAMLオブジェクトをSystem.Xaml.dllだけで読み書きすることは出来ない。それについては後で軽く触れる。WPFはmonoに存在しないので、そもそも対象になっているのかどうかも知らない。
System.Xaml.dllの内容
このアセンブリの内容は、XAMLを読み書きするためのXamlReader, XamlWriterと、そこで使われるXAML型システムを表現するクラス、そして利用者のクラスライブラリに付加する各種Attributeクラスの集合などから成る。読み書きに使われるのは以下の4つだ:
- objectをXAML型システムに基づく情報に解釈して読むXamlObjectReader
- XAML型システムの情報をXMLに出力するXamlXmlWriter
- XAMLのXML表現から型システムの情報として読むXamlXmlReader
- XAML型システムの情報から実際のオブジェクトを生成するXamlObjectWriter
最初の2つがシリアライゼーション、残りの2つがデシリアライゼーションに使われると思えばいい。
XAMLの型システムを表現する代表的な型は以下の5つだ:
- XamlType: XAML型を表す。
- XamlMember: XAML型のメンバーを表す。
- XamlDirective: XamlMemberから派生したクラスで、XAML文法の特殊な構文要素を表す。
- XamlTypeInvoker: XAML型で表された型に属するオブジェクトを操作する。
- XamlMemberInvoker: XAML型におけるメンバー定義をもとに、オブジェクトのメンバーとなるオブジェクトを操作する。
各種AttributeクラスはSystem.Windows.Markup名前空間に大量にあるので、ここでは列挙しない。(リンク先はmsdnにすると別のアセンブリのものが混じるのでmonoのgithubにした。)
型システム
System.Xaml.dllが前提とする型システムは、基本的にはリフレクションを使用しつつ、必要とあれば型システム「もどき」をカスタム定義することが出来る、というものだ。XamlReader, XamlWriterは、この型システムを読み書きする。
XAMLツリーのノードは、特定のXamlTypeに属するオブジェクト、そのメンバー(XamlMember)、そしてその値となるXAMLオブジェクトまたはプリミティブなXamlTypeの値(Value)のいずれかになる。あとNamespaceDeclarationというノードもあって、いろいろ面倒な処理を実装者(この場合わたし)に要求するのだけど、これはあまり重要ではない。
そして、実際のインスタンスを生成したり、メンバーの値を取得したり設定したりする用途でXamlTypeInvokerとXamlMemberInvokerというクラスが存在する。それぞれのInvokerプロパティで取得できる。今例に挙げたような操作は、デフォルトではリフレクションを使うように実装されているのだけど、非実在型や非実在プロパティの場合は、これらからの派生クラスを定義して、各メソッドをオーバーライドすればよい。
XamlTypeは、基本的にはCLIのSystem.Typeから構築できる。生成可能な型とそうでない型があり(IsConstructible)、生成できない型のXAMLノードを生成することは、通常は不可能だ。
XamlMemberは、基本的にはCLIのSystem.Reflection.PropertyInfoあるいはSystem.Reflection.EventInfoから構築できる。実際のツリー表現においては、特別なノードの表現が要求される場面が往々にしてあり、それらはXamlDirectiveというXamlMemberの派生クラスで表現される。リフレクションの利用経験があれば、XamlTypeとXamlMemberは、大まかな概念としてはTypeとMemberInfoに近い。ちなみに、フィールドがXamlMemberとして扱われることはない。
XamlLanguageというstaticクラスには、既定のXamlDirectiveとプリミティブ型(後述)などを表すXamlTypeを表すメンバーがある。XamlTypeには、型の全てのメンバーを返すGetAllMembers()というメソッドがある。XamlReader/XamlWriterの実装は、このメソッドから返されるメンバーの他に、状況に応じてXamlDirectiveをいくつか返すことがある。
XamlTypeとXamlMemberの多くのメンバーが、LookupXXX()という「プロパティをlookupする」仮想メソッドで挙動を変更することが出来る。独自のXamlTypeや独自のXamlMemberを実装したい場合は、これらをオーバーライドすればよい。
IsReadOnly/IsWriteOnly
XamlMemberには読み書き可否を表すフラグがあるが(IsReadOnly/IsWriteOnly)、これは必ずしもCLRプロパティのアクセス修飾子とは連動しない。特に、privateなアクセサーがある場合、IsReadOnlyあるいはIsWriterはfalseになる。該当するアクセサーが無い場合にはtrueとなる。これはハマリどころで、例えば以下の例では、FooのXamlMember.IsReadOnlyはfalseだが、BarのXamlMember.IsReadOnlyはtrueになる。
public int Foo { get; private set; } public int Bar { get { return Foo; } }
これは明らかにまずい設計で、System.Xaml.dllがシリアライズするクラスで、後から自動プロパティを自動でないプロパティに変更されると、互換性が損なわれる。
ちなみに、アクセサーがpublicかどうかは、XamlMember.IsReadPublic、XamlMember.IsWritePublicといったプロパティで知ることが出来る。
プリミティブ型
StringやInt32などプリミティブな型がトップレベルのオブジェクトとして処理される場合、そのXamlTypeは、CLRの型システム上のプロパティを反映したXamlMemberをもつことは無く、InitializationというXamlDirectiveが擬似的なツリーの子要素としてでっち上げられる。
Initialization directiveがツリー上に出現すると、その子ノードは(1つしかない)、プリミティブ型の即値をもつことになり、XamlReader/XamlWriter上ではValueノードとして処理される。
ちなみに、通常のメンバーの(トップレベルでない)値としてプリミティブな型の値が出現した場合は、いちいちInitializationノードが出現することはなく、そのままValueノードが出現する。
どの型がプリミティブかは厳密に言い難いところだが、XamlLanguageのAllTypesプロパティが返す型のうち、MarkupExtensionの派生型でないものはほとんどがそうだと言える。また、これはXamlObjectReader以外での動作は未検証だが、TypeConverterプロパティからTypeConverterが取得できるものは、内容が文字列のValueとしてシリアライズされる。
コレクション
XAMLではarrayとcollectionとdictionaryが特別扱いされる。それぞれ、XamlType.IsArray、XamlType.IsCollection、XamlType.IsDictionaryといったプロパティがtrueになる。これらはCLIのコレクションとは必ずしも一致しない。特に言えることとしては、XamlLanguage.String.IsArrayはfalseだし、IsArray = trueであるオブジェクト(つまりXAMLの配列)は、IsCollection = falseであり、IsDictionary = trueであるオブジェクトは、IsCollection = falseである(CLIとは異なる)。
通常、読み取り専用であるXamlMemberに値を出力することは出来ないので、readonlyのメンバーはXamlObjectReaderでもノードとして出現することは無い。しかし、コレクションの場合は、その要素として値を追加することができるので、メンバーとして返される。ただしこの場合、出現の仕方が多少異なる(これについてはXamlObjectReaderのところで説明する)。