2

I need to serialize a list of strings as CDATA and thought I would follow an answer of How do you serialize a string as CDATA using XmlSerializer.

It works like a charm for serializing. My XML file looks as desired:

<?xml version="1.0" encoding="utf-8"?>
<root xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:xsd="http://www.w3.org/2001/XMLSchema">
 <tlist>
   <item><![CDATA[First string]]></item>
   <item><![CDATA[Second string]]></item>
 </tlist>
</root>

But deserialization does not work. The TestList remains empty; value in the setter has count 0. What have I missed?

[XmlRootAttribute("root")]
public class TestConfig
{
  public TestConfig()
  {
    TestList = new List<string>();
    CdataList = new List<XmlCDataSection>();
  }

  [XmlIgnore]
  public List<string> TestList { get; set; }

  [XmlArray("tlist")]
  [XmlArrayItem("item")]
  public List<XmlCDataSection> CdataList
  {
    get { return TestList.Select(a => new XmlDocument().CreateCDataSection(a)).ToList(); }
    set
    {
      TestList = value.Select(s => s.Value).ToList();
    }
  }

  public void Save(string path)
  {
    var serializer = new XmlSerializer(GetType());
    using (var stream = new StreamWriter(path))
    {
      serializer.Serialize(stream, this);
    }
  }

  public static TestConfig Load(string path)
  {
    var serializer = new XmlSerializer(typeof(TestConfig));
    using (var stream = new StreamReader(path))
    {
      return (TestConfig)serializer.Deserialize(stream);
    }
  }
}

Executing:

  var t = new TestConfig();
  t.TestList.Add("First string");
  t.TestList.Add("Second string");
  t.Save(@"C:\Test\cdatatest.xml");

  var r = TestConfig.Load(@"C:\Test\cdatatest.xml");
  Console.WriteLine("Testlist size is {0}", r.TestList.Count);
Community
  • 1
  • 1
AHalvar
  • 390
  • 2
  • 10

2 Answers2

1

I thought I would "improve" on the answer by softwariness by reducing the length of the XMLWriter subclass.

/// <summary>
/// Custom XmlWriter.
/// Wraps up another XmlWriter to intercept string writes within
/// elements and writes them as CDATA instead.
/// </summary>
public class XmlCDataWriter : XmlTextWriter
{

    public override void WriteString(string text)
    {
        if (WriteState == WriteState.Element)
        {
            WriteCData(text);
        }
        else
        {
            base.WriteString(text);
        }
    }

    /// <summary>
    /// Creates an instance of the XmlTextWriter class using the specified <see cref="T:System.IO.TextWriter"/>.
    /// </summary>
    /// <param name="w">The TextWriter to write to. It is assumed that the TextWriter is already set to the correct encoding. </param>
    public XmlCDataWriter( [NotNull] TextWriter w ) : base( w )
    {
    }
}

It can then be used with a StringBuffer like so:

using (StringWriter textWriter = new StringWriter())
{
    XmlSerializer serializer = new XmlSerializer( typeof( ... ) );
    serializer.Serialize(new XmlCDataWriter(textWriter), ... );
    return textWriter.ToString();
}

Seems to work so far for me, and it is a MUCH smaller subclass :)

Community
  • 1
  • 1
Johan Henkens
  • 153
  • 1
  • 8
  • Is there a compact way of doing this and still allowing the use of the XmlTextWriter XmlWriterSettings option? It seems that XmlWriterSettings are only available via the XmlTextWriter.Create() static method. Which returns an XmlWriter instance. – BevanWeiss Dec 25 '19 at 13:47
  • Nevermind... it seems that XmlWriter already contains a XmlWriterSettings member variable, so I just added another constructor which took the XmlWriterSettings instance and deep copied across all the content that I cared about from it. – BevanWeiss Dec 25 '19 at 14:44
0

Whilst simple proxies work with single values, you have to do a deeper proxying for collections, because of the way the .NET XML serialization machinery works:

[XmlRootAttribute("root")]
public class TestConfig
{
    public TestConfig()
    {
        TestList = new List<string>();
    }

    private List<string> testList;

    [XmlIgnore]
    public List<string> TestList
    {
        get
        {
            if (this.testList == null)
            {
                var newCollection = new List<string>();

                if (this.cdataList != null)
                {
                    foreach (var x in this.cdataList)
                    {
                        newCollection.Add(x.Value);
                    }
                }

                this.testList = newCollection;
                this.cdataList = null;
            }

            return this.testList;
        }
        set
        {
            this.testList = value;
            this.cdataList = null;
        }
    }

    private List<XmlCDataSection> cdataList;

    [XmlArray("tlist")]
    [XmlArrayItem("item")]
    public List<XmlCDataSection> CdataList
    {
        get
        {
            if (this.cdataList == null)
            {
                var newCollection = new List<XmlCDataSection>();

                if (this.testList != null)
                {
                    foreach (var x in this.testList)
                    {
                        newCollection.Add(new XmlDocument().CreateCDataSection(x));
                    }
                }

                this.cdataList = newCollection;
                this.testList = null;
            }

            return this.cdataList;
        }
        set
        {
            this.cdataList = value;
            this.testList = null;
        }
    }

    public void Save(string path)
    {
        var serializer = new XmlSerializer(GetType());
        using (var stream = new StreamWriter(path))
        {
            serializer.Serialize(stream, this);
        }
    }

    public static TestConfig Load(string path)
    {
        var serializer = new XmlSerializer(typeof(TestConfig));
        using (var stream = new StreamReader(path))
        {
            return (TestConfig)serializer.Deserialize(stream);
        }
    }
}

The problem is that the serialization code doesn't just get and set the collections in one go. For example, when it's deserializing, it either creates a new collection, or gets one that's already set on the property, and adds to it. If you've created a new collection here computed from the "real" collection that your application needs to deal with, then any changes to the computed collection won't be reflected in the "real" collection.

To work around this, what I've done in the code above is to transfer ownership of the collection from the "real" collection to the "proxy" collection, and back again, depending on which collection property is being accessed. The cost of transferring ownership is incurred only when switching from one property to the other, so successive accesses to the "real" TestList collection in your application won't incur that expense.

This is somewhat inelegant though if you have many such collections. If you wanted to have all your element text serialized as CDATA, you could implement a custom XmlWriter, like the following:

/// <summary>
/// Custom XmlWriter.
/// Wraps up another XmlWriter to intercept string writes within
/// elements and writes them as CDATA instead.
/// </summary>
public class XmlCDataWriter : XmlWriter
{
    XmlWriter w;

    public XmlCDataWriter(XmlWriter baseWriter)
    {
        this.w = baseWriter;
    }

    public override void Close()
    {
        w.Close();
    }

    public override void Flush()
    {
        w.Flush();
    }

    public override string LookupPrefix(string ns)
    {
        return w.LookupPrefix(ns);
    }

    public override void WriteBase64(byte[] buffer, int index, int count)
    {
        w.WriteBase64(buffer, index, count);
    }

    public override void WriteCData(string text)
    {
        w.WriteCData(text);
    }

    public override void WriteCharEntity(char ch)
    {
        w.WriteCharEntity(ch);
    }

    public override void WriteChars(char[] buffer, int index, int count)
    {
        w.WriteChars(buffer, index, count);
    }

    public override void WriteComment(string text)
    {
        w.WriteComment(text);
    }

    public override void WriteDocType(string name, string pubid, string sysid, string subset)
    {
        w.WriteDocType(name, pubid, sysid, subset);
    }

    public override void WriteEndAttribute()
    {
        w.WriteEndAttribute();
    }

    public override void WriteEndDocument()
    {
        w.WriteEndDocument();
    }

    public override void WriteEndElement()
    {
        w.WriteEndElement();
    }

    public override void WriteEntityRef(string name)
    {
        w.WriteEntityRef(name);
    }

    public override void WriteFullEndElement()
    {
        w.WriteFullEndElement();
    }

    public override void WriteProcessingInstruction(string name, string text)
    {
        w.WriteProcessingInstruction(name, text);
    }

    public override void WriteRaw(string data)
    {
        w.WriteRaw(data);
    }

    public override void WriteRaw(char[] buffer, int index, int count)
    {
        w.WriteRaw(buffer, index, count);
    }

    public override void WriteStartAttribute(string prefix, string localName, string ns)
    {
        w.WriteStartAttribute(prefix, localName, ns);
    }

    public override void WriteStartDocument(bool standalone)
    {
        w.WriteStartDocument(standalone);
    }

    public override void WriteStartDocument()
    {
        w.WriteStartDocument();
    }

    public override void WriteStartElement(string prefix, string localName, string ns)
    {
        w.WriteStartElement(prefix, localName, ns);
    }

    public override WriteState WriteState
    {
        get { return w.WriteState; }
    }

    public override void WriteString(string text)
    {
        if (WriteState == WriteState.Element)
        {
            w.WriteCData(text);
        }
        else
        {
            w.WriteString(text);
        }
    }

    public override void WriteSurrogateCharEntity(char lowChar, char highChar)
    {
        w.WriteSurrogateCharEntity(lowChar, highChar);
    }

    public override void WriteWhitespace(string ws)
    {
        w.WriteWhitespace(ws);
    }
}

You'd then use it like follows:

var serializer = new XmlSerializer(...));
using (var cdataWriter = new XmlCDataWriter(XmlWriter.Create("somepath.xml")))
{
    serializer.Serialize(cdataWriter, myDocumentObject);
}

Again, this only makes sense as an option if you want to write everything as CDATA.

softwariness
  • 4,022
  • 4
  • 33
  • 41