虚飾の王

2009-09-01 あめりか

[][]C#::ポリモーフィックな型のXMLシリアライズ(2) C#::ポリモーフィックな型のXMLシリアライズ(2)を含むブックマーク

前回(id:lord_hollow:20090831:p2)の続き。

手を入れられないソースライブラリ)にある配列またはリストシリアライズするんだけど、そこに派生クラスインスタンスも入っているような場合。

リスト派生クラスインスタンスを入れるには、初回の方法で[XmlArrayItem(typeof(BasicClass))]などの属性を追加すればOK。でもね。すべての場合において属性を付加できるかというとそうでもない。ソースの変更ができないライブラリを使う場合などがある。

もともとのクラス定義は前回と同じ。シリアル化を考慮されているので、すでに属性がついているものとします。

public class BasicClass{
  public string Name;
}
public class InheritedClassA : BasicClass{
  public int Value;
}
public class InheritedClassB : BasicClass{
  public string String;
}
public class ClassList{
  public BasicClass Owner;

  [XmlArrayItem(typeof(BasicClass))]
  [XmlArrayItem(typeof(InheritedClassA))]
  [XmlArrayItem(typeof(InheritedClassB))]
  public List<BasicClass> Children;
}

で、これらのクラスライブラリ定義されていて、ソースコード編集ができないとき、さらにOriginalInheritedClassなるものを作ったとしましょう。

public class OriginalInheritedClass : BasicClass{
  public double Data;
}

すると、ClassListのOwnerやらChildrenにOriginalInheritedClassのインスタンスを混ぜた時点でシリアル化できなくなります。シリアル化するためにはClassList.Childrenに[XmlArrayItem(typeof(OriginalInheritedClass))]属性をつけなければならないのですが、ClassListのコード編集が禁止されているためです。

このようなときは、属性オーバーライド設定を行います。

var attrOverride = new XmlAttributeOverrides();	//属性上書き情報

var attributes = new XmlAttributes();		//属性のリスト

//リフレクションを使用してClassList.Childrenの既存属性[XmlArrayItem]を列挙して追加
var info = typeof(ClassList).GetMember("Children")[0];
var originalAttrs = Attribute.GetCustomAttributes(info, typeof(XmlArrayItemAttribute));
foreach (var attr in originalAttrs){
  attributes.XmlArrayItems.Add(attr as XmlArrayItemAttribute);
}

//新しく追加する属性を作成して追加
attributes.XmlArrayItems.Add(new XmlArrayItemAttribute(typeof(OriginalInheritedClass)));

//今回は[XmlArrayItem]を使う。[XmlElement]を使用する場合は、
//attributes.XmlElements.Add(new XmlElement(typeof(xxx)))となる。

//どのクラスの、どのメンバーの属性を上書きするかを設定(Class.Childrenの属性をattributesで上書き)
attrOverride.Add(typeof(ClassList), "Children", attributes);

//上書き設定を使用してXmlSerializerをnewする
var ser = new XmlSerializer(list.GetType(), attrOverride);

XmlSerializerのコンストラクタに第2引数が追加されました。そこに指定するのはXmlAttributeOverridesで、その中には型とメンバー名とXmlAttributesのセットが入っています。XmlAttributesというのが、クラスにつけるシリアライズ用の属性そのものになるので、クラスにつけたときと同じように属性をセットしていけばOK。

注意点としては、こうやって上書きすると既存の属性が全部消える点。したがって、今回追加したOriginalInheritedClass以外のすでに定義されている属性についても、再度設定する必要があります。リフレクションを使って属性を取得しておけばOKです。また、デシリアライズするときにも、必要十分な属性情報(上で作成したAttributes)をセットする必要がある点にも注意してください。


この機能を使って、通常は子要素として表現されるプロパティ属性として表現することもできます。それはたとえば、さっきのコードのnew XmlSerializerの前に以下のようなコードを挿入すれば可能です。

attributes = new XmlAttributes();
attributes.XmlAttribute = new XmlAttributeAttribute();	//[XmlAttribute]相当
attrOverride.Add(typeof(BasicClass), "Name", attributes);
attrOverride.Add(typeof(InheritedClassA), "Value", attributes);
attrOverride.Add(typeof(InheritedClassB), "String", attributes);

これを使ってたとえばnew XmlSerializer(typeof(List<BasicClass>))ができるようになればいいなぁ、と夢想するんだけど、[XmlElement]もしくは[XmlArrayItem]を持たせる場所がないので出来ないのが残念です・・・。