C#::XmlSerializer関連Tips

サンプルのusing宣言は省略しながら適当に・・・。
プロパティ名とは違う要素名にするときはSystem.Xml.Serialization.XmlElement属性を使う。

[XmlElement("name")]
public string Name{get; set;}

IEnumerableなコンテナ(Listとか)をシリアライズするとき、子要素の名前(デフォルトではクラス名)を変更するには、System.Xml.Serialization.XmlArrayItem属性を使う。

[XmlArrayItem("item")]
public List<StatusValue<double>> Items{get; set;}

あるいは、リストの子になるクラスにXmlType属性で無理やり変える事もできるけど、これは全体に影響が出るのであまりよくないと思う。

[XmlType("item")]
public class ListItem{}

プロパティをXML要素の属性にするには、System.Xml.Serialization.XmlAttribute属性を使う。

public class xxx{
  [XmlAttribute("name")]
  public string Name{get; set;}	//<xxx name="..." />になる
}

Getしかないプロパティやシリアライズ前後の値が異なるときはXmlIgnoreで隠してシリアライズ専用プロパティを作る。bool型のStatusプロパティを"OK"か"NG"でシリアライズしたいときを例にすると・・・

[XmlIgnore]
public bool Status{get; protected set;}
[XmlElement("Status")]
public string Status_{
    get {return Status ? "OK" : "NG"; }
    set {Status = (value==OK);}
}

さらに上記"Status_"はSystem.ComponentModel.EditorBrowsableでIDEから不可視にするといいらしいんだけど自分の環境では不可視にならないわよなんでだよ?
同じ事をお行儀よくするにはIXmlSerializeを実装すればいい。同種のプロパティが大量にあったり、全部のプロパティにXmlAttributeをつけるならやる価値はある。その場合もプロパティになる型にstringを一個引数にとるコンストラクタを追加するとか、変換関数を用意するとか、デシリアライズを楽にする工夫が必要と思います。

public class StatusValue : IXmlSerializable {
  public bool Status{get; protected set;}	//これは属性に
  public string Value{get; set;}		//これは子ノードにする
  public System.Xml.Schema.XmlSchema GetSchema(){return null;}
  public void WriteXml(XmlWriter writer){
    writer.WriteAttributeString("status", (Status ? "OK" : "NG"));
    writer.WriteString(Value);
  }
  public void ReadXml(XmlReader reader){
    Status = reader.GetAttribute("status") == "OK";
    reader.Read();
    Value = reader.ReadContentAsString();
    reader.Read();
  }
}

上記で、子要素が絡んでくる部分(Value)がstringでないとき(特にクラスの時)はXmlSerializerに任せるのが無難。

public class StatusValue<T> : IXmlSerializable{
  public bool Status { get; set; }
  public T Value { get; set; }
  public System.Xml.Schema.XmlSchema GetSchema() { return null; }
  public void WriteXml(System.Xml.XmlWriter writer){
    writer.WriteAttributeString("status", (Status ? "OK" : "NG"));
    XmlSerializer serializer = new XmlSerializer(typeof(T));
    serializer.Serialize(writer, Value);
  }
  public void ReadXml(System.Xml.XmlReader reader){
    Status = reader.GetAttribute("status") == "OK";
    reader.Read();
    XmlSerializer serializer = new XmlSerializer(typeof(T));
    Value = (T)serializer.Deserialize(reader);
    reader.Read();
  }
}

子要素になるプロパティが複数になったりするともう大変。要素名でswitchして代入文を書く必要がある。属性にならないプロパティ(XML入れ子関係になるもの)はひとつのクラスにまとめてしまうべきでしょう。既存のでっかいクラスの塊を凝ったXMLに落とすときは、一旦シリアライズ専用のデータ構造を持つオブジェクトに落としてから処理したほうが簡単なパターンもあるよね。

ポリモーフィックな型を使いたいときは、XMLの要素なり属性で型を識別してswitch-caseする感じになるのかな?

public List<BaseClass> Items{get; set;}
public void ReadXml(System.Xml.XmlReader reader){
  Items = new List<BaseClass>();
  reader.Read();
  while (reader.NodeType != System.Xml.XmlNodeType.EndElement){
    XmlSerializer serializer = null;
    string el = reader.LocalName;
    switch (el){
      case "BaseClass": serializer = new XmlSerializer(typeof(BaseClass));break;
      case "InheritedClassA": serializer = new XmlSerializer(typeof(InheritedClassA));break;
      case "InheritedClassB": serializer = new XmlSerializer(typeof(InheritedClassB)); break;
    }
    if (serializer != null){
      BaseClass bc = serializer.Deserialize(reader) as BaseClass;
      if(bc != null)Items.Add(bc);
    }
    else reader.Skip();	//知らないクラスは無視(普通は例外にするんだろう)
  }
  reader.Read();
}
public void WriteXml(System.Xml.XmlWriter writer){
  XmlSerializer serializer = null;
  var ns = new XmlSerializerNamespaces();
  ns.Add("", "");
  foreach (var i in Items){
    serializer = new XmlSerializer(i.GetType());
    serializer.Serialize(writer, i, ns);
  }
}

でもこれだと子要素の型名を知っておく必要があるので、System.Activatorあたりと絡めていくのが正解か?でもそれだと型の完全修飾名を取得できる手段が要るなぁ・・・まぁとにかく大変だ。XmlSerializer内部ではコンパイラを呼び出してるとか聞いた気がするから、CSharpCodeProviderを使ったりするのかも。