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\$
5
  • \$\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
    Commented Oct 11, 2016 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
    Commented Oct 11, 2016 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\$ Commented Oct 11, 2016 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
    Commented Oct 11, 2016 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\$ Commented Oct 11, 2016 at 20:25

2 Answers 2

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\$
4
  • \$\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\$ Commented Oct 11, 2016 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\$ Commented Oct 11, 2016 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
    Commented Oct 13, 2016 at 9:50
  • \$\begingroup\$ Could this system work after a hypothetical filedata version 3 was added and changes were made other than just adding new parameters, such as renaming a parameter? You would need a way to get from version 1 to version 3 and from version 2 to version 3. It seems this code would only take you from version N-1 to version N. \$\endgroup\$ Commented Feb 4, 2020 at 15:57
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\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .