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.