Refactoring XmlWriter
I inherited some code that make heavy use of XmlWriter. Say what you will about XmlWriter, it works, but the code using it can be ugly, hard to read, and hard to figure out where you went wrong. Case in point, there were numerous errors in the code that went unfound by the unit test, because the XmlWriter somehow rights itself. The culprit was missing WriteEndElement calls.
To keep things correct, you really need to have a WriteEndElement for every WriteStartElement, or risk future problems. This went undetected because when the project started the xml was simple, just a couple of elements, then call ToString(). As soon as you call ToString, the XmlWriter adds the nessesary end elements into the document.
Once I started looking into this, it was easy to see how this could happen. And I wanted to make it easier to NOT happen. So here is the code I started with:
1: protected void WriteInvoiceLineToAdd(XmlWriter r, OrderItem orderItem)
2: {
3: r.WriteStartElement("InvoiceLineAdd");
4: r.WriteRefNode("ItemRef", "FullName", orderItem.ProductName);
5: r.WriteElementString("Quantity", orderItem.Quantity);
6: r.WriteElementString("Rate", orderItem.Rate);
7: r.WriteEndElement();
8: }
My first refactoring was simply to add some artificial scoping:
1: protected void WriteInvoiceLineToAdd(XmlWriter r, OrderItem orderItem)
2: {
3: r.WriteStartElement("InvoiceLineAdd");
4: {
5: r.WriteRefNode("ItemRef", "FullName", orderItem.ProductName);
6: r.WriteElementString("Quantity", orderItem.Quantity);
7: r.WriteElementString("Rate", orderItem.Rate);
8: }
9: r.WriteEndElement();
10: }
Which actually worked fairly well. Unfortunately, this still left the issue open (missing EndElement calls). I was able to find the areas where I needed to fix, but it wasn’t what I considered solid yet.
Which means, back to Extension Methods and the Action delegate. I created the following Extension Methods which overload the WriteStartElement:
1: public static void WriteStartElement(this XmlWriter xe, string localName, Action action)
2: {
3: xe.WriteStartElement(localName);
4:
5: action.Invoke();
6:
7: xe.WriteEndElement();
8: }
9:
10: public static void WriteStartElement(this XmlWriter xe, string localName, Action<XmlWriter> action)
11: {
12: xe.WriteStartElement(localName);
13:
14: action.Invoke(xe);
15:
16: xe.WriteEndElement();
17: }
Which now allow me to remove the WriteEndElement altogether and refactor to this instead:
1: protected void WriteInvoiceLineToAdd(XmlWriter r, OrderItem orderItem)
2: {
3: r.WriteStartElement("InvoiceLineAdd", () => {
4: r.WriteRefNode("ItemRef", "FullName", orderItem.ProductName);
5: r.WriteElementString("Quantity", orderItem.Quantity);
6: r.WriteElementString("Rate", orderItem.Rate);
7: });
8: }
No more missing the WriteEndElement. No having to parse the code to see where the end element should have gone.
Also note, I created two overloads for WriteStartElement, the other allows you to write code like this:
1: protected void WriteInvoiceLineToAdd(XmlWriter r, OrderItem orderItem)
2: {
3: r.WriteStartElement("InvoiceLineAdd", x => {
4: x.WriteRefNode("ItemRef", "FullName", orderItem.ProductName);
5: x.WriteElementString("Quantity", orderItem.Quantity);
6: x.WriteElementString("Rate", orderItem.Rate);
7: });
8: }


