Ktouth Brand. on Web

け〜くんこと K.Ktouth のだらだらした日常と突発的に作るプログラムや読み物とかの雑多サイト



[2014年07月22日]

Catel のシリアライザの微妙な仕様ミス?

2014年07月23日 04:02更新 筆者:K.Ktouth

Catel なら Xmlアトリビュートも使えるんで出力がシンプル化するかなーと思っていたんですが、実際に書いてみるとうまく行きません。具体的には以下のようなことが起きます。

  • 〜Modefier を使った際になどプロパティの型と違う場合、出力時の型情報が付加される
  • ModelBase 派生クラスなどではインスタンスの重複を防ぐための識別番号が付加される
  • 配列の場合、XML名前空間arrがその都度付加される
  • 未知の型の場合、XML名前空間ctlDDDがその都度付加される
  • 先日のコードで指定した XmlAttribute つきプロパティが出力されない

上から「被害が軽い」順に並んでいます。
デシリアライズ時に型情報は必須ですから1番目は当然です。2番目の識別番号は理解できます……が、実際の出力では無くても問題ないというか重複をしないような設計にするのが基本だと思うんで正直邪魔です(ぉぃ

で、問題なのは3番目4番目、そして最後。
3番目4番目に関してはやりたいことはわかるんですが、出力される xmlns:arr および xmlns:ctlDDD (DDDは適当な番号) 名前空間は、結局の所、そのノード以下で特に使用されていない以上、名前空間の指定自体が無駄に見えます。上手く機能していないのかも。
さらに最後の問題は「機能として実装しているXMLアトリビュートがきちんと出力されない」のは実装バグもしくは仕様ミスのどちらかとしか言えません。正確にはシリアライズの基点となるルートモデル(シリアライザに渡すインスタンス)ではXMLアトリビュートの指定が正しく動作するが、そのインスタンスのプロパティ、もしくはそれらが所有するインスタンス(子孫ノードのインスタンス)では正しく出力されない、となります。

で、どうしてそんな事になるのか黙々と Catel のソースコードを眺めていたんですが。ModelBase の該当部分をようやく見つけました。

void IXmlSerializable.WriteXml(XmlWriter writer)
{
  var type = GetType();

  var element = new XElement(type.Name);
  var serializer = SerializationFactory.GetXmlSerializer();
  serializer.Serialize(this, new XmlSerializationContextInfo(element, this));

  // The serializer gives us the full element, but we only need the actual content. According to
  // http://stackoverflow.com/questions/3793/best-way-to-get-innerxml-of-an-xelement, this method is the fastest:
  var reader = element.CreateReader();
  reader.MoveToContent();
  var elementContent = reader.ReadInnerXml();

  writer.WriteRaw(elementContent);
}

Catel の XmlSerializer は、子孫ノードのシリアライズは DataContractSerializer に丸投げしています。そして DataContract が ModelBase の IXmlSerializable インターフェイス実装を呼び出し、上記コードが実行されます。
このコードでは一旦 Catel の XmlSerializer を使って XElement を作りだし、そこから改めて XmlWriter に出力しています。が……コメントに書かれているようにより高速な出力をするために子孫ノードをXML化し、直接出力する方法を採用しています。
ここで重要なのはreader.ReadInnerXml では XMLアトリビュート部分の指定はカットされるって事です。子要素では無いですから。
もしプロパティ周りの XmlAttribute 指定を想定したコードを追加するなら、上記ソースのコメントのすぐ上に……

var propertyDataManager = PropertyDataManager.Default;
var catelTypeInfo = propertyDataManager.GetCatelTypeInfo(type);
var attrs = catelTypeInfo.GetCatelProperties().Keys.Union(catelTypeInfo.GetNonCatelProperties().Keys)
        .Where(x => propertyDataManager.IsPropertyNameMappedToXmlAttribute(type, x))
        .Select(x => propertyDataManager.MapPropertyNameToXmlAttributeName(type, x))
        .Select(x => element.Attribute(x));
foreach (var attr in attrs) { writer.WriteAttributeString(attr.Name.LocalName, attr.Value); }

PropertyDataManager という Catel 式プロパティの制御クラスから Linq を使って XmlAttribute 指定されているプロパティの名前を抽出、さらに XElement の中から該当する XAttribute を取りだして XmlWriter に書き出しています。
デシリアライズの方は reader.ReadOuterXml を使っているので、これらの情報も普通に含んだ形で読み込んでいるので修正不要でした。

XmlAttribute 属性による指定自体、公式のドキュメントには載っておらずソースを見ないとわからないため「機能追加の最中」なのか「機能削除の最中」なのかが不明ですが……どちらにせよあまり使わない方が良さげって事ですかねあせ

本日のリンク元
アンテナ
その他のリンク元
検索