Compartilhar via


LINQ to XML: Implementing the Visitor Pattern using Extension Methods

[Blog Map]  This blog is inactive.  New blog: EricWhite.com/blog

We often use the Visitor pattern to separate the structure of an object tree or collection from the operations performed on that tree or collection. There are lots of ways in LINQ where you can visit some function on a collection, but when implementing this pattern as a couple of extension methods in LINQ to XML, we can pass the element depth as an argument to the delegate. This allows us to do interesting things because we know the element depth.

For instance, we can use the Visit method to transform this:

<?xml version="1.0"?>
<PurchaseOrder PurchaseOrderNumber="99503" OrderDate="1999-10-20">
<Address Type="Shipping">
<Name>Ellen Adams</Name>
<Street>123 Maple Street</Street>
<City>Mill Valley</City>
<State>CA</State>
<Zip>10999</Zip>
<Country>USA</Country>
</Address>
<Address Type="Billing">
<Name>Tai Yee</Name>
<Street>8 Oak Avenue</Street>
<City>Old Town</City>
<State>PA</State>
<Zip>95819</Zip>
<Country>USA</Country>
</Address>
<Comment>Hurry, my lawn is going wild</Comment>
<Items>
<Item PartNumber="872-AA">
<ProductName>Lawnmower</ProductName>
<Quantity>1</Quantity>
<USPrice>148.95</USPrice>
<Comment>Confirm this is electric</Comment>
</Item>
<Item PartNumber="926-AA">
<ProductName>Baby Monitor</ProductName>
<Quantity>2</Quantity>
<USPrice>39.98</USPrice>
<ShipDate>1999-05-21</ShipDate>
</Item>
</Items>
</PurchaseOrder>

Into this:

PurchaseOrder:
PurchaseOrderNumber: 99503
OrderDate: 1999-10-20
Address:
Type: Shipping
Name: Ellen Adams
Street: 123 Maple Street
City: Mill Valley
State: CA
Zip: 10999
Country: USA
Address:
Type: Billing
Name: Tai Yee
Street: 8 Oak Avenue
City: Old Town
State: PA
Zip: 95819
Country: USA
Comment: Hurry, my lawn is going wild
Items:
Item:
PartNumber: 872-AA
ProductName: Lawnmower
Quantity: 1
USPrice: 148.95
Comment: Confirm this is electric
Item:
PartNumber: 926-AA
ProductName: Baby Monitor
Quantity: 2
USPrice: 39.98
ShipDate: 1999-05-21

In contrast, we could use the DescendantsAndSelf axis and the Select operator to visit every descendant in the tree, except that there are two problems:

  • The Select operator must return a type, and if we want to execute a method where the return type of the method is void, the compiler rightly complains.
  • The delegate passed to the Select operator takes an index as an argument, but the index, of course, isn't a depth in the tree.

There are two variations on this when implemented as extension methods. The first variation has a return type of void, and takes a delegate with a return type of void, so you can call a method like Console.WriteLine in the lambda. The second variation is generic, and returns a collection of some type, projected by the lambda.

Another two variants, VisitElements, and VisitElements<T>, only visits elements, not both elements and attributes.

One note to make: In the current LINQ to XML design (which is going to be in the next CTP), a new class has been added, XObject, which is a common base class of both XElement and XAttribute. This doesn't affect most existing code, but if our Visit operator is going to visit on both XElement and XAttribute, this is the right class to pass to the visiting method. However, so that this code will work with the May CTP version of XLinq, I replaced XObject with object.

This post contains the entire source code for the implementation of these extension methods, as well as some code to exercise them. All of the samples use the depth argument to the visiting function to indent the output.

The first four samples use a couple of utility methods:

public static string GetName(object o)
{
return
o is XElement ?
(o as XElement).Name.ToString() :
(o as XAttribute).Name.ToString();
}

static string GetLeafValue(object o)
{
XElement ell = o as XElement;
if (ell != null)
if (! ell.Elements().Any())
return (string)ell;

    XAttribute att = o as XAttribute;
if (att != null)
return (string)att;

    return "";
}

There are six samples:

1. Uses the void Visit, calling Console.WriteLine. 

 

purchaseOrder.Visit(
(object o, int depth) =>
Console.WriteLine("{0}{1}{2}",
"".PadRight(depth * 4),
(GetName(o) + ": ").PadRight(14),
GetLeafValue(o)
)
);

 

2. Uses the void Visit, calling StringBuilder.Append
This uses the efficiencies of building strings using StringBuilder.
But it's not side-effect free.

 

StringBuilder sb1 = new StringBuilder();
purchaseOrder.Visit(
delegate(object o, int depth)
{
sb1
.Append(
String.Format("{0}{1}{2}",
"".PadRight(depth * 4),
(GetName(o) + ": ").PadRight(14),
GetLeafValue(o)
)
)
.Append(Environment.NewLine);
}
);
Console.WriteLine(sb1.ToString());

 

3. Uses the typed Visit, calling String.Format, returning a string.
This one is side-effect free. It is also simpler to code, but it creates lots of objects.

 

string str =
purchaseOrder.Visit(
(object o, int depth) =>
String.Format("{0}{1}{2}",
"".PadRight(depth * 4),
(GetName(o) + ": ").PadRight(14),
GetLeafValue(o)
)
)
.Aggregate("", (s, t) => s + t + Environment.NewLine);
Console.WriteLine(str);

 

4. Uses the typed Visit to use StringBuilder in a more side-effectless way, but it still can't be said to be pure. The side-effects are only observable from within the lambda passed to Aggregate.

 

StringBuilder sb2 =
purchaseOrder.Visit(
(object o, int depth) =>
String.Format("{0}{1}{2}",
"".PadRight(depth * 4),
(GetName(o) + ": ").PadRight(14),
GetLeafValue(o)
)
)
.Aggregate(new StringBuilder(),
(seedSb, s) => seedSb
.Append(s)
.Append(Environment.NewLine));
Console.WriteLine(sb2.ToString());

 

5. Uses the void VisitElements.

 

purchaseOrder.VisitElements(
(XElement e, int depth) =>
Console.WriteLine("{0}{1}{2}",
"".PadRight(depth * 4),
e.Name.ToString().PadRight(14),
e.Elements().Any() ? "" : (string)e
)
);

 

6. Uses the typed VisitElements.

 

string str2 =
purchaseOrder.VisitElements(
(XElement o, int depth) =>
String.Format("{0}{1}{2}",
"".PadRight(depth * 4),
o.Name.ToString().PadRight(14),
o.Elements().Any() ? "" : (string)o
)
)
.Aggregate("", (s, t) => s + t + Environment.NewLine);
Console.WriteLine(str2);

 

Here is the code to implement the extension methods. Below the C# code is the XML file that the sample works with, although the extension methods will work with any XML tree you like.

 

using System;
using System.Collections.Generic;
using System.Text;
using System.Query;
using System.Xml.XLinq;
using System.Data.DLinq;

namespace MyProgram
{
    public static class MySequence
    {
        public delegate T XObjectProjectionFunc<T>(object o, int depth);

        public delegate T XElementProjectionFunc<T>(XElement o, int depth);

 

        public static IEnumerable<T> Visit<T>(this XElement source, XObjectProjectionFunc<T> func)
        {
            foreach (var v in Visit(source, func, 0))
                yield return v;
        }

 

        public static IEnumerable<T> Visit<T>(XElement source, XObjectProjectionFunc<T> func, int depth)
        {
            yield return func(source, depth);
            foreach (XAttribute att in source.Attributes())
                yield return func(att, depth + 1);
            foreach (XElement child in source.Elements())
                foreach (T s in Visit(child, func, depth + 1))
                    yield return s;
        }

 

        public delegate void XObjectVisitor(object o, int depth);

 

        public static void Visit(this XElement source, XObjectVisitor func)
        {
            Visit(source, func, 0);
        }

 

        public static void Visit(XElement source, XObjectVisitor func, int depth)
        {
            func(source, depth);
            foreach (XAttribute att in source.Attributes())
                func(att, depth + 1);
            foreach (XElement child in source.Elements())
                Visit(child, func, depth + 1);
        }

 

        public static IEnumerable<T> VisitElements<T>(this XElement source, XElementProjectionFunc<T> func)
        {
            foreach (var v in VisitElements(source, func, 0))
                yield return v;
        }

 

        public static IEnumerable<T> VisitElements<T>(XElement source, XElementProjectionFunc<T> func, int depth)
        {
            yield return func(source, depth);
            foreach (XElement child in source.Elements())
                foreach (T s in VisitElements(child, func, depth + 1))
                    yield return s;
        }

 

        public delegate void XElementVisitor(XElement o, int depth);

 

        public static void VisitElements(this XElement source, XElementVisitor func)
        {
            VisitElements(source, func, 0);
        }

 

        public static void VisitElements(XElement source, XElementVisitor func, int depth)
        {
            func(source, depth);
            foreach (XElement child in source.Elements())
                VisitElements(child, func, depth + 1);
        }

    }

 

    class Program
    {
        public static string GetName(object o)
        {
            return
                o is XElement ?
                    (o as XElement).Name.ToString() :
                    (o as XAttribute).Name.ToString();
        }

 

        static string GetLeafValue(object o)
        {
            XElement ell = o as XElement;
            if (ell != null)
                if (! ell.Elements().Any())
                    return (string)ell;

            XAttribute att = o as XAttribute;
            if (att != null)
                return (string)att;

            return "";
        }

 

        static void Main(string[] args)
        {
            XElement purchaseOrder = XElement.Load("PurchaseOrder.xml");

            Console.WriteLine("All of the examples use depth to indent.");
            Console.WriteLine("");

            Console.WriteLine("Sample 1");
            Console.WriteLine("Uses the void Visit, calling Console.WriteLine");
            Console.WriteLine("==================================================");
            purchaseOrder.Visit(
                (object o, int depth) =>
                    Console.WriteLine("{0}{1}{2}",
                        "".PadRight(depth * 4),
                        (GetName(o) + ": ").PadRight(14),
                        GetLeafValue(o)
                    )
            );

            Console.WriteLine("");
            Console.WriteLine("Sample 2");
            Console.WriteLine("Uses the void Visit, calling StringBuilder.Append");
            Console.WriteLine("This uses the efficiencies of building strings using StringBuilder.");
            Console.WriteLine("But it's not side-effect free.");
            Console.WriteLine("==================================================");
            StringBuilder sb1 = new StringBuilder();
            purchaseOrder.Visit(
                delegate(object o, int depth)
                {
                    sb1
                    .Append(
                        String.Format("{0}{1}{2}",
                            "".PadRight(depth * 4),
                            (GetName(o) + ": ").PadRight(14),
                            GetLeafValue(o)
                        )
                    )
                    .Append(Environment.NewLine);
                }
            );
            Console.WriteLine(sb1.ToString());

            Console.WriteLine("");
            Console.WriteLine("Sample 3");
            Console.WriteLine("Uses the typed Visit, calling StringBuilder.Format, returning a string.");
            Console.WriteLine("This one is side-effect free. It is also simpler to code, but it creates lots of objects.");
            Console.WriteLine("==================================================");
            string str =
                purchaseOrder.Visit(
                    (object o, int depth) =>
                        String.Format("{0}{1}{2}",
                            "".PadRight(depth * 4),
                            (GetName(o) + ": ").PadRight(14),
                            GetLeafValue(o)
                        )
                )
                .Aggregate("", (s, t) => s + t + Environment.NewLine);
            Console.WriteLine(str);

            Console.WriteLine("");
            Console.WriteLine("Sample 4");
            Console.WriteLine("Uses the typed Visit to use StringBuilder in a more side-effectless");
            Console.WriteLine("way, but it still can't be said to be pure. The side-effects are only");
            Console.WriteLine("observable from within the lambda passed to Aggregate.");
   &nbsp

Comments