1
\$\begingroup\$

The Problem

I'm developing a game and using the XmlSerializer to store some user data.

While it isn't an immediate problem, I'm trying to conceptualize what would happen if I needed to update Save Data to a newer version of my game. Since the XmlSerializer reads as a Type, if I update my class with more variables, the save file becomes unreadable.

After doing some research, I discovered the XmlDocument class, which is able to read XML files as that type. This allows the data stored to be arbitrary, allowing me to read and write data accordingly.

What's confusing me is how exactly I would go about this. Is there an easy way to populate an XmlDocument with the Nodes/Elements of a custom class? And how can I detect when data is missing and needs to be added to the XML file?

Example

If you were confused by the question, here's a simple example of what I would like to accomplish:

<FileData version="Original File">
  <foo>14</foo>
</FileData>

Several versions later, I update my class to look like this:

class FileData {
    var foo = 14;
    var bar = 4;
}

Now I need the XML file to look like this before I can use XmlSerializer:

<FileData version="Updated File">
  <foo>14</foo>
  <bar>4</bar>
</FileData>
\$\endgroup\$
  • \$\begingroup\$ You could have a fallback system. If it detects the file is an older style save then load it in with your older loading method. Then resave in the new style and continue onwards with the new saving stuff. \$\endgroup\$ – lozzajp Oct 11 '16 at 7:26
  • \$\begingroup\$ could you use a datacontract msdn.microsoft.com/en-us/library/ms733127(v=vs.110).aspx Its kind of implemented in a way to expect different versions of serialsied objects This has worked well for me for many updates to serialisation object structure. You need to set default values on your save object using the DeserializationInitializer. And then any deserialised data will overwrite the defaults. Then on the next save any new data is stored in the new format. \$\endgroup\$ – RobM Oct 11 '16 at 9:51
  • \$\begingroup\$ @lozzajp I do save every file with a game version, but as mentioned above the XML Serializer uses a Type to load data. In order for me to load an older version of that Type/Class, I need to have an entire duplicate of the older data structure--which you can imagine is very messy. That said, my loading method doesn't read "line by line," it deserializes the entire file into a class instance. This is why I thought XML Document would help, since it can read arbitrary data, though I'm not sure how to use it properly in this case. \$\endgroup\$ – RobProductions Oct 11 '16 at 16:30
  • \$\begingroup\$ or just update your data structure ( as per your example ) and use a datacontract. Seems to solve your issues and save you writing a specific serialiser. Kinda win win. I must be missing something \$\endgroup\$ – RobM Oct 11 '16 at 19:25
  • \$\begingroup\$ @RobM not missing anything--I was replying to the first comment. I've been looking into DataContract and I can't quite figure out how it will help (I haven't used DataContract before). Will tagging data in my class with the DataMember attributes change how the XmlSerializer handles it? Could you provide a simple example of how this should work? \$\endgroup\$ – RobProductions Oct 11 '16 at 20:25
1
\$\begingroup\$

You could use a datacontract.

So below is how to use a datacontract with two versions of FileData ( v1 & v2 ). Comment in filedata version 1 and call testV1(). This will cause filedata version 1 to be saved

Then comment in filedata version 2 and call testV2() this will cause filedata version 1 to be loaded as filedata version 2

using System.Runtime.Serialization;
using System.IO;
using System.Xml;


class Program
{
    // filedata version 1
    [DataContract]
    public class FileData 
    {
        public FileData()
        {
            setDefaults();
        }

        private void setDefaults()
        {
            foo = 14;
        }

        [OnDeserializing]
        private void DeserializationInitializer(StreamingContext ctx)
        {
            setDefaults();
        }

        [DataMember]
        public int foo { get; set; }

    }

    /*
    // filedata version 2
    [DataContract]
    public class FileData
    {
        public FileData()
        {
            setDefaults();
        }

        private void setDefaults()
        {
            foo = 14;
            bar = 4;
        }

        [OnDeserializing]
        private void DeserializationInitializer(StreamingContext ctx)
        {
            setDefaults();
        }

        [DataMember]
        public int foo { get; set; }

        [DataMember]
        public int bar { get; set; }

    }
     * */


    static void Main(string[] args)
    {

        // run testv1 once. Ensure filedata version 1 is commented in
        testV1();

        // now comment out above testv1(). Ensure Filedata version 2 is commented in and run testv2()
        //testV2();         

    }

    static void testV1()
    {
        FileData saveFd = new FileData();
        saveText(serialiseToString<FileData>(saveFd), fileName());
        FileData fd = deserialiseFromString<FileData>( loadText(fileName()) );
    }

    static void testV2()
    {
        FileData fd = deserialiseFromString<FileData>(loadText(fileName()));
    }

    static String fileName()
    {
        return UserPath() + "filedata.xml";
    }

    public static String UserPath()
    {
        return Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + @"\" ;
    }

    public static String serialiseToString<T>(T obj)
    {
        String ret = "";
        DataContractSerializer ser = new DataContractSerializer(typeof(T));
        var settings = new XmlWriterSettings { Indent = true };

        MemoryStream ms = new MemoryStream();
        var w = XmlWriter.Create(ms, settings);
        using (ms)
        {

            ser.WriteObject(w, obj);
            w.Close();
            ms.Position = 0;

            System.IO.StreamReader reader = new System.IO.StreamReader(ms);
            using (reader)
            {
                ret = reader.ReadToEnd();
            }
        }

        return ret;
    }

    public static T deserialiseFromString<T>(String text)
    {
        T p;
        DataContractSerializer ser = new DataContractSerializer(typeof(T));

        byte[] byteArray = Encoding.UTF8.GetBytes(text);
        MemoryStream ms = new MemoryStream(byteArray);
        using (ms)
        {
            p = (T)(ser.ReadObject(ms));
        }

        return p;
    }


    public static String loadText(String filename)
    {
        TextReader tw = null;
        String text = null;

        try
        {
            FileStream fs = System.IO.File.OpenRead(filename);
            tw = new StreamReader(fs);
            text = tw.ReadToEnd();
            tw.Close();
        }
        catch (Exception ex)
        {
            //deal with ex
            ex.ToString();
        }
        finally
        {
            if (tw != null)
            {
                tw.Dispose();   // same as tw.Close() btw
            }
        }

        return text;
    }


    public static bool saveText(String text, String filename)
    {
        bool success = false;
        TextWriter tw = null;

        String fd = Path.GetDirectoryName(filename);
        try
        {
            if (Directory.Exists(fd) == false)
            {
                Directory.CreateDirectory(fd);
            }

            tw = new StreamWriter(filename);
            tw.Write(text);
            tw.Close();
            success = true;
        }
        catch (Exception ex)
        {
            // deal with e
            ex.ToString();
        }

        finally
        {
            if (tw != null)
            {
                tw.Dispose();   
            }
        }

        return success;
    }


}
\$\endgroup\$
  • \$\begingroup\$ Huh... Thanks a ton for that example--I definitely understand DataContracts a bit more. However, I forgot to mention I'm using Unity 5.3.5, which uses the .NET 2.0 subset. I tried the DataContract Attributes and it was throwing an error, even after importing System.Runtime.Serialization. Furthermore, the docs for DataContract only go back to .NET 3.0. Was DataContract introduced in 3.0? \$\endgroup\$ – RobProductions Oct 11 '16 at 20:48
  • \$\begingroup\$ The page for DataContracts does indeed say it was available since 3.0, so I would have to do some importing to get that working. Also, is there any way to get this working with the XmlSerializer instead of the XmlWriter? \$\endgroup\$ – RobProductions Oct 11 '16 at 20:56
  • \$\begingroup\$ Ah, unity.. Should have realised. I've not used XmlSerialiser much as DataContract has always worked for me so far. \$\endgroup\$ – RobM Oct 13 '16 at 9:50
0
\$\begingroup\$

As it happens, the XMLSerializer (and most likely DataContract and JSON as well) will add in data that is missing when Deserializing. So in my example above, the XML Serializer will actually give you this when trying to read the outdated file:

<FileData version="Updated File">
  <foo>14</foo>
  <bar>0</bar>
</FileData>

Since "Bar" is a type int, it will default to 0. This is a huge step forward, but it doesn't completely solve the issue. We still need "bar" to default to 4 when running the game.

However, since we've now learned the XMLSerializer doesn't throw an exception when reading outdated files, we can stick a version number in the file like this:

<FileData version="Original File">
  <VERSION>1.0</VERISON>
  <foo>14</foo>
</FileData>

Now whenever we read a file, we can compare it's version number to our current version, and if it's outdated, we know that "Bar" now exists as 0. Now we can easily set "Bar" to 4 before returning the deserialized data.

SIDENOTE:

If we were to now go back to version 1.0, after saving a file with extra data, the XMLSerializer will actually just ignore the new data and simply return the correct version of FileData. So if our Type doesn't include "Bar", the Serializer will just ignore it in the inputted String.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.