コンテンツへスキップ
ものがたり
戻る

System.Xaml.dllについて(その2)

前回からさっぱり書く気が起きず放置していたのだけど、適当に続きを書いてみようと思う。今回はXamlObjectReaderについて。

XamlObjectReaderのノード出現フロー

XamlObjectReaderは任意のobjectをXAMLノードとして読むためのXamlReaderの実装だ。

XAMLノードは、XAMLオブジェクトとそのメンバーとその値から成る、という話を前回書いたけど、これをXMLのXmlReaderと同じようにノードをRead()で次々に返していくものだと理解すればいい。XmlReaderにおけるNodeTypeに相当するXamlNodeTypeというものがXamlReaderにもある。基本的には、1つのXAMLノードは、StartObject -> StartMember -> StartObject .. EndObject あるいは Value -> EndMember -> 次のStartMember … -> EndObject という流れで読み取られる。Valueノードはプリミティブな(メンバーを持たない)型の値である場合に返される。オブジェクトの型はStartObjectの時にXamlReaderのTypeプロパティとして、あるいはメンバーの型であればStartMemberの時にXamlMemberのTypeプロパティとして、知ることが出来る。なお、後述するMarkupExtensionのXamlLanguage.PositionalParametersについては、この流れが当てはまらないので注意が必要だ。

ルートあるいはコレクションの要素(これを便宜上トップレベルと呼ぼう)としてプリミティブな型が出現した場合は、StartObjectの型として知ることが出来る。この時、値を含むXamlMemberとしてはXamlLanguage.InitializationというXamlDirectiveが出現するのみで、これは特定の型をもたない。トップレベル以外の場合はStartMemberで型を知ることが出来る。

GetObject

基本的な流れとは異なる特異な例として、ReadOnlyなメンバーの値となるXAMLオブジェクトの場合は、StartObjectの代わりにGetObjectというノードが流れてくることがある。これはメンバーの値となるオブジェクトの開始を表すもので、オブジェクトの「生成」を指示するものではなく「取得」を指示することになり、最後にはEndObjectで終わることになる。XamlObjectReaderにおいては、StartObjectもGetObjectもメンバーの値をリフレクションで取得することになるが、例えばXamlObjectWriterにおいてはこれらの処理は明確に異なり、StartObjectにおいては新しいインスタンスが生成され、GetObjectにおいては現在生成中のインスタンスからメンバーの値を取得する。GetObjectは、例えば型がコレクションであるメンバーについて用いられる(getterがあってsetterが無い場合が多いだろう)。

GetObjectが使用されるパターンがもうひとつある。それはXAMLノードツリーで既に出現したオブジェクトを「参照」する場合だ。あるノードxの子孫がそのプロパティ値としてxへの参照をもっていた場合、これをもしXamlObjectReaderが単純にStartObject..EndObjectで処理しようとすると、永久ループに入ってしまう。そうならないよう、XAMLツリーで一度出現したオブジェクトは、二度書かれることは無い。代わりに、二度目に出現した時は、XamlLanguage.Reference(x:Reference)という特殊な型のオブジェクトとなる。これは他のメンバーを名前で参照する。この「名前」は、その型の中でSystem.Windows.Markup.RuntimeNamePropertyAttributeを設定されたプロパティが存在すればその値が、無ければXamlLanguage.Name(x:Name)というXamlDirectiveの値として一意に自動生成された文字列が、それぞれ使用される。自動生成の場合は問題にならないが、もし名前文字列が一意な値を返さなかった場合はエラーだ。

出現しうるメンバー

XAMLオブジェクトのメンバーは、通常はその型のプロパティに対応するXamlMemberの集合ということになり、そのXamlTypeでGetAllMembers()で返される。それらのメンバーのうち、値がnullでないものが、StartMember .. EndMemberの一連のノードとなって返され、それがメンバーの数だけ続く。

しかし、このパターンに沿わないものがいくつかある:

ちなみにメンバーの出現順は、コンストラクタ引数など先に出現しなければならないものが先行し、以降はアルファベット順で出現し、XamlLanguage.Itemsは(わたしの知る限り)最後に出現する。XamlLanguage.Keyなどは普通にアルファベット順で出現したりするので、実装する側(この場合わたし)としてはハマりどころだ。

メンバーの値

XAMLオブジェクトのメンバーの値は、通常はそのプロパティの値そのものとなる。しかし、そうならない場合がたまに存在する。

他にもメンバー値が特別な型になるものが存在するかもしれないが、わたしは把握していない。

ちなみに、これに関連する話題として、XamlObjectReaderのInstanceプロパティについて触れておこうと思う。これは通常は現在のプロパティの値(あるいはルートオブジェクトならそのもの)を返すのだけど、上記のArrayExtensionやTypeExtensionのようなオブジェクトが返されることはなく、元のArrayやTypeが返される2。ただしさらにややこしいことに、どうやらTypeがルートオブジェクトとして渡された場合には、Instanceプロパティの値はTypeではなくTypeExtensionになるようだ。ArrayはArrayExtensionにならないので、Typeだけ特別扱いしているように見える(バグなんじゃないかとも思う)。

追記: IXmlSerializable/XDataの時はどうなるのだろうかと疑問だったが、実験してみたところ、この場合はXDataでもIXmlSerializableでもなくnullが返るようだ(!)

NamespaceDeclarations

さて、最後に説明するのはやや順番がおかしいのだけど、XamlObjectReaderは、StartObjectを返す前に、一連のNamespaceDeclarationをノードとして返す(XamlNodeTypeにはNamespaceDeclarationも存在する)。このNamespaceDeclaration群は、XAMLオブジェクトのオブジェクトグラフから、使用されるXamlTypeおよびXamlMemberをあらかじめ巡回しておいて、使用されるNamespace (PreferredXamlNamespace)を収集して、それぞれに一意のPrefixを割り当てたものだ。このため、実はオブジェクトグラフは、NamespaceDeclarationの収集と実際のRead()の処理で、(少なくとも)2回は巡回されている(!)。何でこんな動作になっているのかは必ずしも明確ではないが、XamlXmlWriterはあらかじめNamespaceDeclarationが宣言されていないNamespaceがXamlTypeあるいはXamlMemberで出現したらエラーとなるので、その関係かもしれない(StartObjectやStartMemberの前にチェックするより2回巡回した方が早いのかもしれない)。

最後に

以上でXamlObjectReaderの動作の説明は終わりだ。XamlReader, XamlWriterを実装するとき、最初に実装すべきクラスだったのだけど(XamlObjectReaderでオブジェクトを読み、XamlXmlWriterでXMLに出力し、XamlXmlReaderでそのXMLを読み、XamlObjectWriterでデシリアライズしていた)、その経験から言えばこのクラスが一番ややこしいものだった。

本当はこれにattached propertiesなどが加わってさらにややこしいことになるのかもしれないけど、WPFの無いわたしにはそこまで実験する術がないので、今回はここまで。

Footnotes

  1. 未確認だがSystem.Windows.Markup.DictionaryKeyPropertyAttributeをもつメンバーがあれば、これは出現しないと考えられる。

  2. 書いていて気づいたけどXDataの時はどうなるのか把握していない


この記事を共有:

前の記事
2010年にはまった作品まとめ
次の記事
日本でMonoハッカーの求人