ラベル シリアライズ の投稿を表示しています。 すべての投稿を表示
ラベル シリアライズ の投稿を表示しています。 すべての投稿を表示

2015年6月30日

XmlSerializer と CDATA セクション

XmlSerializerCDATA セクションを扱うのが面倒だったので、自分なりのまとめ。(ぐぐれば引っかかる内容。)
System.Xml.Serialization 名前空間には、なぜか CDATA セクションのためのカスタム属性が用意されてない・・・

CDATA セクションのみを含む要素

[XML]

<root>
  <text><![CDATA[<b>cdata</b>]]></text>
</root>
[対応クラス]

[XmlRoot("root")]
public class CDataXml1
{
    [XmlElement("text")]
    public XmlCDataSection Text { get; set; }
}

XmlCDataSection のプロパティを用意するだけ。
XmlCDataSection は new XmlDocument().CreateCDataSection("CDATA セクションの中身") で作成できる。
面倒な場合はプロパティで工夫。
※シリアライズ時、CreateCDataSection の引数が null でも <![CDATA[]]> は出力される。

XmlCDataSection の代わりに、IXmlSerializable を継承した CDATA セクション用クラスを自作するのもあり。
[CDATA セクション用クラス]

public class CDataSection : IXmlSerializable
{
    public CDataSection() { }
    public CDataSection(string text) { Text = text; }
    public string Text { get; set; }

    public virtual XmlSchema GetSchema()
    {
        return null;
    }
    public virtual void ReadXml(XmlReader reader)
    {
        Text = reader.ReadElementString();
    }
    public virtual void WriteXml(XmlWriter writer)
    {
        writer.WriteCData(Text);
    }
}

CDATA セクションと属性がある要素

[XML]

<root>
  <node attr="value"><![CDATA[<b>cdata</b>]]></node>
</root>
[対応クラス]

[XmlRoot("root")]
public class CDataXml3
{
    [XmlElement("node")]
    public CDataNode Node { get; set; }
}

public class CDataNode
{
    [XmlAttribute("attr")]
    public string Attr { get; set; }

    // データの設定・取得用
    [XmlIgnore]
    public string Content { get; set; }

    // シリアライズ・デシリアライズ用
    [XmlText]
    public XmlNode[] ContentNode
    {
        get { return new[] { new XmlDocument().CreateCDataSection(Content) }; }
        set
        {
            if( value == null ){
                Content = null;
                return;
            }
            if( value.Length != 1 ) throw new InvalidOperationException();
            Content = value[0].Value;
        }
    }
}

CDATA セクションにあたるプロパティを、XmlCDataSection を 1 つだけ含む XmlNode[] 型にして、XmlTextAttribute を付加。
そうすると、直接 CDATA セクションを出力できる。
XmlNode[] 型だとデータの設定・取得が面倒なので、別のプロパティ(上記だと Content)を用意する。

ただし、この方法はデシリアライズ時にちょっとした問題があり、CDATA セクションの代わりに普通のテキストノードでもデシリアライズできてしまう。
デシリアライズされたインスタンス(上記だと value[0])は、元が CDATA セクションかテキストノードかに関わらず XmlText になるため、型チェックではじくことができない。
OuterXmlInnerText などのプロパティにも <![CDATA[]]> の文字列が含まれないため、デシリアライズインスタンスからはおそらく判断不可能。

CDATA セクションかテキストノードかどちらでもいい場合は問題ないが、厳密にチェックしたい場合、この方法では難しいかもしれない。

参考URL

2014年8月13日

自作シリアライズの注意事項 #1 コレクション

ISerializable で自作シリアライズを実装する際、デシリアライズ処理におけるコレクション(配列やList)の扱いには注意が必要である。
コレクションのデシリアライズ直後、中身はまだデシリアライズされてない。

[ISerializable サンプル]
※本来、これらのプロパティだけなら ISerializable の実装は不要

[Serializable]
public class Node<T> : ISerializable
{
    public Node(){}
    public T Value;
    public readonly List<Node<T>> Children = new List<Node<T>>();

    private Node(SerializationInfo info, StreamingContext context)
    {
        Value = (T)info.GetValue("Value", typeof(T));
        Children = (List<Node<T>>)info.GetValue("Children", typeof(List<Node<T>>));

        Console.WriteLine("コレクションの個数 : {0}", Children.Count);
        foreach( var child in Children ) Console.WriteLine(child == null);
    }

    [SecurityPermissionAttribute(SecurityAction.Demand, SerializationFormatter=true)]
    public virtual void GetObjectData(SerializationInfo info, StreamingContext context)
    {
        info.AddValue("Value", Value);
        info.AddValue("Children", Children);
    }
}

上記オブジェクトをシリアライズ→デシリアライズしてみる。

static class TestSerialization
{
    static void Main()
    {
        var node = new Node<string>() { Value = "parent" };
        node.Children.Add(new Node<string>() { Value = "one" });
        node.Children.Add(new Node<string>() { Value = "two" });

        var mem = new MemoryStream();
        var formatter = new BinaryFormatter();
        formatter.Serialize(mem, node);

        mem.Position = 0;
        var deserialized = (Node<string>)formatter.Deserialize(mem);
        Console.WriteLine("デシリアライズ後");
        Console.WriteLine(deserialized.Children[0].Value);
        Console.WriteLine(deserialized.Children[1].Value);
    }
}

結果は、
コレクションの個数 : 2
True
True
コレクションの個数 : 0
コレクションの個数 : 0
デシリアライズ後
one
two

コレクションのデシリアライズ直後は、中身が null であることが分かる。
その後、コレクションの中身のデシリアライズが実行されている。
つまり、コレクションのデシリアライズは、コレクション本体→中身の順で行われる。

OnDeserializedAttribute

デシリアライズ処理において、コレクションの中身にいじりたい場合はどうするのか?
例えばシリアライズ対象外のプロパティ(Parentとか)を設定したり、独自のAddメソッドなどで追加したい場合など。

そのために、OnDeserializedAttribute が存在する。
これを付けたメソッドは、コレクションの中身がデシリアライズされた後に呼び出される。

[OnDeserialized を追加した ISerializable サンプル]

[Serializable]
public class Node<T> : ISerializable
{
    public Node(){}
    public T Value;
    public readonly List<Node<T>> Children = new List<Node<T>>();

    private Node(SerializationInfo info, StreamingContext context)
    {
        Value = (T)info.GetValue("Value", typeof(T));
        Children = (List<Node<T>>)info.GetValue("Children", typeof(List<Node<T>>));

        Console.WriteLine("コレクションの個数 : {0}", Children.Count);
        foreach( var child in Children ) Console.WriteLine(child == null);
    }

    [OnDeserialized]
    private void OnDeserialized(StreamingContext context)
    {
        Console.WriteLine("OnDeserialized : Value={0}", Value);
        foreach( var child in Children ) Console.WriteLine(child.Value);
    }

    [SecurityPermissionAttribute(SecurityAction.Demand, SerializationFormatter=true)]
    public virtual void GetObjectData(SerializationInfo info, StreamingContext context)
    {
        info.AddValue("Value", Value);
        info.AddValue("Children", Children);
    }
}

上と同じ方法でシリアライズ→デシリアライズした結果は、
コレクションの個数 : 2
True
True
コレクションの個数 : 0
コレクションの個数 : 0
OnDeserialized : Value=parent
one
two
OnDeserialized : Value=one
OnDeserialized : Value=two
デシリアライズ後
one
two

コレクションの中身がデシリアライズされているので、変更が可能。

検証環境

Windows 7 64bit/Visual Studio 2010 SP1/.NET 4.0

参考URL