Blog Archives

XML (part 3, basics, advanced), serialize

We are still working with the environment of part 2. We are using the embedded file “Walmart.xml”.

In part 1 we were loading an XML file the following way:

public static void LoadXml() {
    string lDesktopPath = Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory) + @"\";
    string lFile = lDesktopPath + "Walmart.xml";
 
    XDocument lXDocument = XDocument.Load(lFile);
 
    // food (using WmItem)
    WmItem[] lFood = (from lData in lXDocument.Descendants("Walmart").Descendants("food")
                        select new WmItem(
                            lData.Element("name").Value,
                            double.Parse(lData.Element("price").Value),
                            lData.Element("description").Value)
                ).ToArray();
 
    foreach (WmItem lItem in lFood) Console.WriteLine(lItem);
 
    Console.WriteLine();
 
    // electronic (quick and dirty, using var)
    var lElectronic = from lData in lXDocument.Descendants("Walmart").Descendants("electronic")
                        select lData;
 
    foreach (var lItem in lElectronic) {
        Console.WriteLine(lItem);
        Console.WriteLine();
        Console.WriteLine(lItem.Element("name").Value);
        Console.WriteLine(lItem.Element("price").Value);
        Console.WriteLine(lItem.Element("description").Value);
    }
 
    Console.ReadLine();
} //

LINQ queries were parsing the XML file and initialized the objects.
Well, there is a more direct way out there. Let the .Net framework do the dirty job. Just make use of properties. Here are some of them:

Serializable
Tells that data can be arranged in a sequence. The object can then be sent across networks or be saved.

Xml Attributes

XmlRoot
This is the root of all elements. Each XML file can only have one root element. Compared to hard disk drives on your computer it would be the “C:” directory.

XmlElement
A field or property of an object becomes an XML element during the conversion.

XmlText
A text field. Only one instance of the XmlTextAttribute can be applied in a class.

XmlAttribute
This attribute is self-explanatory. Use it whenever you want to add attributes to XML items.

Add the following two classes in your project. They define the structure of your XML file. You do not have to use attributes for all types. Attributes are optional. This means you can add as many non-Xml-Elements to a class as you like. They are simply ignored during (de-)serialization processes.

[Serializable]
[XmlRoot("Walmart", Namespace = "")]
public class WalmartWorld {
    [XmlElement("food")]
    public List<WalmartItem> Food { get; set; }

    [XmlElement("electronic")]
    public List<WalmartItem> Electronic { get; set; }

    [XmlText]
    public string Text { get; set; }
} // class

[Serializable]
[XmlRoot("Walmart", Namespace = "")]
public class WalmartItem {
    [XmlAttribute("attr")]
    public string Attribute { get; set; }

    [XmlElement("name")]
    public string Name { get; set; }

    [XmlElement("price")]
    public double Price { get; set; }

    [XmlElement("description")]
    public string Description { get; set; }

    [XmlElement("somethingElse")]
    public string SomethingElse { get; set; }

    public override string ToString() {
        return Name.PadRight(12) + 
            Price.ToString("#,##0.00").PadLeft(8) + " " + 
            Description + 
            (string.IsNullOrEmpty(SomethingElse) ? string.Empty : ("  !!! => " + SomethingElse));
    } //
} // class

Step one is to load our embedded file “Walmart.xml” using above classes. The objects will be created for you. There is no need for further parsing or instanciating.

public static WalmartWorld LoadObjects() {
    string lPath = "DemoApp.XmlFiles.Walmart.xml";
    Assembly lAssembly = Assembly.GetExecutingAssembly();

    using (Stream lStream = lAssembly.GetManifestResourceStream(lPath)) {
        XmlSerializer lXmlSerializer = new XmlSerializer(typeof(WalmartWorld));

        using (StreamReader lStreamReader = new StreamReader(lStream)) {

            return lXmlSerializer.Deserialize(lStreamReader) as WalmartWorld;
        }
    }
} //

Above method returns fully initialized classes. Nice, eh?
The code is amazingly short. Let’s convert and save objects on the desktop with this code:

public static void SaveObjects(WalmartWorld xWalmartWorld) {
    string lDesktopPath = Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory) + @"\";
    string lFile = lDesktopPath + "Walmart2.xml";

    var lXmlSerializer = new XmlSerializer(typeof(WalmartWorld));

    using (var lStreamWriter = new StreamWriter(lFile)) {
        lXmlSerializer.Serialize(lStreamWriter, xWalmartWorld);
    }    
} //

Again, the code could not be shorter.
You can test above methods with the following:

public static void SerializerTest() {
            
    // load
    WalmartWorld lWalmartWorld = LoadObjects();
    if (lWalmartWorld == null) return; // error

    // print results
    foreach (WalmartItem f in lWalmartWorld.Food) Console.WriteLine(f.ToString());
    foreach (WalmartItem e in lWalmartWorld.Electronic) Console.WriteLine(e.ToString());

    // add some stuff
    lWalmartWorld.Text = "Luis Armstrong had a fabulous voice.";
    lWalmartWorld.Food[2].Attribute = "What a wonderful world.";

    // save
    SaveObjects(lWalmartWorld);

    Console.ReadLine();
} //

Have a look at your file “Walmart2.xml”, which was saved onto your desktop. It is not exactly the same as “Walmart.xml”. We added some text and attributes.
Add “Walmart2.xml” to your Visual Studio Solution Explorer and see that you can load it without any trouble. Remember to set the file property “Build Action” to “Embedded Resource”.
You can also use a file stream to load the XML file. The program just gets shorter again, not longer:

public static WalmartWorld LoadXmlAsObject() {
    string lDesktopPath = Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory) + @"\";
    string lFile = lDesktopPath + "Walmart2.xml";

    XmlSerializer lXmlSerializer = new XmlSerializer(typeof(WalmartWorld));

    using (StreamReader lStreamReader = new StreamReader(lFile)) {
        return lXmlSerializer.Deserialize(lStreamReader) as WalmartWorld;
    }
} //

XML (part 2, basics), embedded, XDocument, XmlDocument, XmlTextReader, XPathNavigator

Embedded

We covered basic file IO for XML files yesterday. This is very useful for settings files. There are some XML files though that you want to hide from the user. I am not talking about encryption. Today we are going to embed the XML file the project itself.
We are still using yesterday’s “Walmart.xml” example. Create a folder “XmlFiles” in your Solution Explorer and drag the file into it.

XmlFileLocation

Now right-click the file “Walmart.xml” and select “Properties”. Change the “Build Action” to “Embedded Resource”.

EmbeddedResourceProperty

Use the following code to load the resource as an XDocument:

// since .Net 3.5
// namespace System.Xml.Linq
// xPath: namespace.folder.file.ext => here: "DemoApp.XmlFiles.Walmart.xml"
public static XDocument getAssemblyXDocument(string xPath) {
    Assembly lAssembly = Assembly.GetExecutingAssembly();
    using (Stream lStream = lAssembly.GetManifestResourceStream(xPath)) {
        XDocument lXDocument = XDocument.Load(lStream);
        return lXDocument;
    }
} //

You can also load the document as an XmlDocument.

// since .Net 1.1
// namespace System.Xml
// xPath: namespace.folder.file.ext => here: "DemoApp.XmlFiles.Walmart.xml"
public static XmlDocument getAssemblyXml(string xPath) {
    Assembly lAssembly = Assembly.GetExecutingAssembly();
    using (Stream lStream = lAssembly.GetManifestResourceStream(xPath)) {
        XmlDocument lXmlDocument = new XmlDocument();
        lXmlDocument.Load(lStream);
        return lXmlDocument;
    }
} //
public static void LoadXmlExample() {
    string lPath = "DemoApp.XmlFiles.Walmart.xml";
    XmlDocument lXml = getAssemblyXml(lPath);
    XDocument lXDoc = getAssemblyXDocument(lPath);
} //

Using an XmlDocument is similar to XDocument. XmlDocument does not support LINQ, which is a big disadvantage.

XmlDocument reads the entire document and initializes a tree structure, which needs a lot of memory. Processing bigger Xml files can cause issues. The tree can be accessed either by browsing through the nodes or by using XPath queries.
You can browse through documents forward/backward and carry out modifications.

XDocument (LINQ to XML) is easy to code and understand. Again the entire document is loaded into memory. To read Xml files we can use the XDocument or XElement class. Both classes support Load() methods. XElement represents an XML fragment while XDocument represents an entire XML document.

XmlTextReader is very fast and memory efficient. Unlike XmlDocument it can only read in forward direction. Searching for something specific is awkward. As the name says, there is no method to write or to modify data. And data validation is also left behind.

XPathNavigator is more complex and slower than XmlReader. XPathNavigator has to read enough data to be able to execute XPath queries. XmlDocument internally uses XPathNavigator. Using XPathNavigator directly is faster than using it via XmlDocument.

Using XmlTextReader:

public static void UseTextReader() {
    string lPath = "DemoApp.XmlFiles.Walmart.xml";
    Assembly lAssembly = Assembly.GetExecutingAssembly();

    using (Stream lStream = lAssembly.GetManifestResourceStream(lPath)) {
        using (XmlTextReader lXmlReader = new XmlTextReader(lStream)) {
            while (lXmlReader.Read()) {
                if (!lXmlReader.IsStartElement()) continue;
                switch (lXmlReader.Name) {
                    case "name":
                        Console.WriteLine("name: " +  lXmlReader.ReadString());
                        break;
                    case "price":
                        Console.WriteLine("price: " + lXmlReader.ReadString());
                        break;
                    case "description":
                        Console.WriteLine("description: " + lXmlReader.ReadString());
                        break;
                    case "somethingElse":
                        Console.WriteLine("somethingElse: " + lXmlReader.ReadString());
                        break;
                    default:
                        Console.WriteLine(Environment.NewLine + lXmlReader.Name);
                        break;
                }
            }
        }
    }
    Console.ReadLine();
} //

example output:

Walmart

food
name: Banana
price: 1.99
description: Mexican delicious

food
name: Rice
price: 0.79
description: the best you can get

food
name: Cornflakes
price: 3.85
description: buy some milk

food
name: Milk
price: 1.43
description: from happy cows

electronic
name: Kindle fire
price: 100
description: Amazon loves you
somethingElse: the perfect Xmas gift for your kids

food
name: baked beans
price: 1.35
description: very British

Using XPathNavigator:

public static void UseXPathNavigator() {
    string lPath = "DemoApp.XmlFiles.Walmart.xml";
    Assembly lAssembly = Assembly.GetExecutingAssembly();

    using (Stream lStream = lAssembly.GetManifestResourceStream(lPath)) {
        XPathDocument lXPathDoc = new XPathDocument(lStream);
        XPathNavigator lXNavi = lXPathDoc.CreateNavigator();
        lXNavi.MoveToRoot();
        lXNavi.MoveToFirstChild();

        do {
            if (lXNavi.MoveToFirstAttribute()) {
                Console.WriteLine(lXNavi.Name + "=" + lXNavi.Value);
                lXNavi.MoveToParent();
            }

            if (!lXNavi.MoveToFirstChild()) continue;
            do {
                if (!lXNavi.MoveToFirstChild()) break;
                do {
                    Console.WriteLine(lXNavi.Name + "=" + lXNavi.Value);
                } while (lXNavi.MoveToNext());
                Console.WriteLine();
                lXNavi.MoveToParent();
            } while (lXNavi.MoveToNext());
            lXNavi.MoveToParent();
        } while (lXNavi.MoveToNext());

        Console.ReadLine();
    }
} //

example output:
name=Banana
price=1.99
description=Mexican delicious

name=Rice
price=0.79
description=the best you can get

name=Cornflakes
price=3.85
description=buy some milk

name=Milk
price=1.43
description=from happy cows

name=Kindle fire
price=100
description=Amazon loves you
somethingElse=the perfect Xmas gift for your kids

name=baked beans
price=1.35
description=very British

Using XPathNavigator with query:

public static void UseXPathNavigator(string xQuery) {
    string lPath = "DemoApp.XmlFiles.Walmart.xml";
    Assembly lAssembly = Assembly.GetExecutingAssembly();

    using (Stream lStream = lAssembly.GetManifestResourceStream(lPath)) {
        XPathDocument lXPathDoc = new XPathDocument(lStream);
        XPathNavigator lXNavi = lXPathDoc.CreateNavigator();

        XPathNodeIterator lIterator = lXNavi.Select(xQuery);

        if (lIterator.Count <= 0) {
            Console.WriteLine("Nothing found!");
            return;
        }

        while (lIterator.MoveNext()) {
            Console.WriteLine(lIterator.Current.Value);
        }
    }            
} //

public static void TestQueries() {
    UseXPathNavigator(@"/Walmart/electronic");
    Console.WriteLine();
    UseXPathNavigator(@"/Walmart/food[name='Banana']");
            
    Console.ReadLine();
} //

example output:
Kindle fire100Amazon loves youthe perfect Xmas gift for your kids

Banana1.99Mexican delicious

My opinion is: Make use of LINQ to XML. It is very comfortable and can save you a lot of time. If you have a lot of data, then you should consider a database rather than XML files.

We will discuss XML object serialization tomorrow. Let C# save and load objects the nice way.