File Saving/Loading

Conceptually, I thought about the serialization process, and how I used the XmlSerializer object to serialize my objects into XML previously.  However, this wouldn't work because of how the attributes work in the collection; at least, from what I know, it wouldn't work.

So I created a MetadataSerializer, which serializes a MetadataElement derived type by looping through the attributes and saving them/loading them to/from XML.  First, to load the XML, I created two public interfaces to load XML; one to load a string, and one to load a file.

public T LoadFromXml(string xml)
{
    //Create a document and load the XML
    XmlDocument doc = new XmlDocument();
    doc.LoadXml(xml);
    return this.LoadXml(doc);
}

public T LoadFromXmlFile(string path)
{
    XmlDocument doc = new XmlDocument();
    doc.Load(path);
    return this.LoadXml(doc);
}

The LoadXml method actually performs the work, using an XMLDocument object.  It loops through the child nodes of the root, returning an instantiated object of type T (the generic defined in the root class):

private T LoadXml(XmlDocument doc)
{
    //if no document element or children, return null
    if (doc.DocumentElement == null || doc.DocumentElement.ChildNodes.Count == 0)
        return null;

    //Get the name node, to pass to the constructor
    XmlNode nameNode = doc.SelectSingleNode("//Attribute[@Name='Name']");
    //If not provided, throw an error
    if (nameNode == null)
        throw new ArgumentNullException("Name", "Name cannot be null in the attributes list");

    //Create a new instancee of the item using the Activator, passing
    //the name in the parameter constructs
    T item = (T)Activator.CreateInstance(typeof(T), new object[] { nameNode.InnerText });

    foreach (XmlNode node in doc.DocumentElement.ChildNodes)
    {
        //Get the parameters of the node
        string name = node.Attributes["Name"].InnerText;
        Type type = Type.GetType(node.Attributes["Type"].InnerText);
        //Change the value to a specific type
        object value = node.InnerText;

        //Don't duplicate add the name again
        if (name != "Name")
        {
            //Create a new attribute with the parameters
            MetadataAttribute attribute = new MetadataAttribute(name);
            attribute.Value = value;
            //Add the attribute to the item
            item.Attributes.Add(attribute);
        }
    }

    return item;
}

So from this, we have a way to load an object from an XML string/file.  How about saving?  I created the same two equivalent methods to save the object to an XML string:

public string SaveToXml(T item)
{
    XmlDocument doc = new XmlDocument();
    //Create the root node to save the attributes to
    doc.AppendChild(doc.CreateElement("Element"));

    foreach (MetadataAttribute attribute in item.Attributes)
    {
        XmlElement attributeElement = doc.CreateElement("Attribute");
        //Create the name attribute
        XmlAttribute nameAttribute = doc.CreateAttribute("Name");
        nameAttribute.InnerText = attribute.Name;
        //Append the name attribute to the element
        attributeElement.Attributes.Append(nameAttribute);

        //Create the type attribute
        XmlAttribute typeAttribute = doc.CreateAttribute("Type");
        typeAttribute.InnerText = attribute.Type.FullName;
        //Append the type attribute to the element
        attributeElement.Attributes.Append(typeAttribute);

        //Append the text to the element
        attributeElement.InnerText = attribute.Value.ToString();
        //Append the element to the root element
        doc.DocumentElement.AppendChild(attributeElement);
    }

    return doc.OuterXml;
}

public void SaveToXmlFile(T item, string path)
{
    //Get the xml from the item
    string xml = this.SaveToXml(item);
    //Save the XML to the file
    using (StreamWriter writer = new StreamWriter(path, false))
        writer.Write(xml);
}

These two methods save the XML to a string/file by looping through the attributes, adding them to the XML root object, and then returning the outer XML.

I constructed this before testing; remember previously that I said that I believe testing using an actual constructed object instead of spending much more energy using a dummy object to test the same thing.  I used this philosophy, but not without testing it of course.  Here is the test:

[TestFixture()]
public class MetadataSerializationTest
{
    MetadataSerialization<Task> serialization = new MetadataSerialization<Task>();
    readonly string PATH = AppDomain.CurrentDomain.BaseDirectory + "test.xml";

    [Test()]
    public void TestLoadingXml()
    {
        //Create a text XML string
        string xml = @"
        <Element>
            <Attribute Name='Name' Type='System.String'>Test</Attribute>
        </Element>";

        //Deserialize the object
        Task task = serialization.LoadFromXml(xml);
        //Assert is true
        Assert.IsTrue(task.Name == "Test");
        Assert.AreEqual(1, task.Attributes.Count);
    }

    [Test()]
    public void TestSavingXml()
    {
        //Create a new task and add an attribute
        Task task = new Task("Test");
        task.Attributes.Add(new MetadataAttribute("IsReoccurring", false));

        //Get the XML string for the object
        string xml = serialization.SaveToXml(task);
        //Write out the XML
        Console.WriteLine("TestSavingXML:  " + xml);
        //Make sure there is an XML string
        Assert.IsFalse(string.IsNullOrEmpty(xml));
        Assert.IsTrue(xml.Contains("Name=\"Name\""));
        Assert.IsTrue(xml.Contains("Type=\"System.String\""));
        Assert.IsTrue(xml.Contains("Name=\"IsReoccurring\""));
    }

    [Test()]
    public void TestLoadingAndSavingXmlToFile()
    {
        //Create a new task and add an attribute
        Task task = new Task("Test", "Basic");
        task.Attributes.Add(new MetadataAttribute("IsReoccurring", false, typeof(bool)));
        task.Attributes.Add(new MetadataAttribute("ReoccurrenceMeasurement", 5, typeof(int)));
        task.Attributes.Add(new MetadataAttribute("ReoccurrenceUnit", "miles", typeof(string)));

        //Save the object to a file
        serialization.SaveToXmlFile(task, PATH);

        //Make sure the file exists
        Assert.IsTrue(File.Exists(PATH));
        task = null;

        //Load the file into a new task
        Task newTask = serialization.LoadFromXmlFile(PATH);
        Assert.IsNotNull(newTask);
        Assert.AreEqual("Test", newTask.Name);
        Assert.AreEqual("Basic", newTask.Category);
        Assert.IsTrue(newTask.Attributes.Contains("IsReoccurring"));
        Assert.AreEqual(false, newTask.Attributes["IsReoccurring"].Value);
    }

    [Test()]
    public void TestTypeCorrectnessAfterLoading()
    {
        Task task = serialization.LoadFromXmlFile(PATH);
        MetadataAttribute isReoccurringAttribute = task.Attributes["IsReoccurring"];
        MetadataAttribute measureAttribute = task.Attributes["ReoccurrenceMeasurement"];
        MetadataAttribute unitAttribute = task.Attributes["ReoccurrenceUnit"];

        Assert.IsTrue(isReoccurringAttribute.Value is bool);
        Assert.AreEqual(false, isReoccurringAttribute.Value);
        Assert.IsTrue(measureAttribute.Value is int);
        Assert.AreEqual(5, measureAttribute.Value);
        Assert.IsTrue(unitAttribute.Value is string);
        Assert.AreEqual(unitAttribute.Value, "miles");
    }
}

This test worked successfully, and so the object worked.  However, as I looked at the test, I did note that there were some things I could have tested, without first creating the object.  I could have tested that inputting an XML string worked without creating the object first.  I could have tested saving the object to an XML string successfully as well.

This made me wonder about my personal philosophy; isn't there something we can test before writing the object?  I believe so, but in advance, it is hard to make that determination.

Comments

No Comments