XmlSerializer はオブジェクトを XML にシリアル化してくれるありがたいクラスであるが、くせがあるので覚え書きを残す。
なお、クラス自体の使い方は参考サイトを参照のこと。
シリアル化したいクラスに IEnumerable<T> や IEnumerable インタフェースを実装してやると 、下記のような意味不明な例外を吐かれる。
XML シリアル化を可能にするには、IEnumerable から継承される型は、 継承階層のすべてのレベルで Add(System.Object) が実装される必要があります。
たしかに MSDN の XmlSerializer クラスの説明を見るとこうと書いてある。
XmlSerializer は、IEnumerable または ICollection を実装するクラスは特別に対処します。
はっきり言って特別な対処などしてくれなくてよいのだが、そうなっているもんだから仕方ない。これに対応するには void Add(object o) のような要素追加メソッドを定義してやればいい。
しかし、そうはしたくない場合だってあるのだ。独自に定義したコレクションクラスで、要素数が決まっているような局面では、Add のような汎用メソッドは定義したくないのである(それでも IEnumerable<T> としての特長を享受したいがために実装した)。
ちなみにここでとりあげている対象のクラスは 要素数が 12 固定のジェネリック型である。
こういったイレギュラーな場合は IXmlSerializable というインタフェースを実装し、自前でシリアライズ/デシリアライズ処理を書いてやる必要がある。
IXmlSerializable には GetSchema, ReadXml, WriteXml メソッドが定義されている。名前からしてそれぞれ スキーマを返す、XML を読み取る、XML を書き込む メソッドなのだが、いかんせんこれらの情報はあまり多くない。MSDN には最低限のコードが示されている。
public void WriteXml (XmlWriter writer)
{
writer.WriteString(personName);
}
public void ReadXml (XmlReader reader)
{
personName = reader.ReadString();
}
public XmlSchema GetSchema()
{
return null;
}
ここで personName は IXmlSerializable を実装したクラスのフィールドである。シリアライズしたい値が一つの場合はこれで事足りる。また、GetSchema でスキーマ定義の必要がないときは null を返しても問題ないことがわかる。ただし、コレクションクラスのような複数の値を個別のタグに格納してシリアル化しなければならない場合はこれでは実現できない。また、シリアライズするフィールドが文字列だとも ToString を実装しているとも限らない。
そのような場合は ReadXml, WriteXml 内で再度 XmlSerializer を生成し、個別にシリアライズしてやればいいことがわかった(こちらのサイトの UserInfoList クラスが詳しい)。
public XmlSchema GetSchema()
{
return null;
}
public void ReadXml(XmlReader reader)
{
var serializer = new XmlSerializer(typeof(T));
reader.Read();
// 要素数がわからない場合は while ↓
// while (reader.NodeType != System.Xml.XmlNodeType.EndElement)
for (int i = 0; i < 12; i++)
{
this[i] = (T)serializer.Deserialize(reader);
}
// 後始末
reader.ReadEndElement();
}
public void WriteXml(XmlWriter writer)
{
var serializer = new XmlSerializer(typeof(T));
// 空の名前空間
var namespaces = new XmlSerializerNamespaces();
namespaces.Add(string.Empty, string.Empty);
for (int i = 0; i < 12; i++)
{
serializer.Serialize(writer, this[i], namespaces);
}
}
このクラスはジェネリック型のため、T という型を使用しているが型指定されている場合は、ここは適宜変更すればよい。また、reader.Read() は一度だけ呼び出せば、あとは Deserialize の度に次の要素に進む。
変更点は、ReadXml の最後に ReadEndElement() メソッドを呼び出していることと、WriteXml で空の名前空間を定義して XmlSerializer に渡していることである。
前者はなくても動作する。理由はこのクラスのインスタンスの他に要素をいくつか含むようなクラスをデシリアライズした場合になぜか後ろに続くデシリアライズが処理されないという不具合が発生したためである。なお、Close() メソッドも試したが効果はなく、Read() メソッドが false になるまで while を回すか、ReadEndElement() メソッドを呼び出すと解消されることがわかった。
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"
後者は上記の長ったらしい名前空間宣言が要素ごとに挿入されてしまい、可読性を損なうため、空の名前空間を渡すことによって、追加されないようにしている。なお、これによってデシリアライズができなくなることはない。
Kenz Yamada(山田研二)。1984年生。大阪。ちょっとずつ好きなプログラム作ってます。
好きなものはカメラと旅行。ガジェットや身の回り、ちょっとこだわります。
詳しくは Web mixi で。