XMLのDOM構築を簡易にする(C#2.0用)

System.Xml.XmlDocumentを使ってDOMを構築しようとした場合、やりたいことの大小に関わらず長い名前のメソッドを呼ばなければならず、また、XmlElementとXmlAttributeで生成・追加の方法が異なっていて戸惑います。
もっと簡易に野良XML(...)を量産できるよう、短いメソッド名とメソッドチェーンを備えたビルダーを書いてみました。

使用例

以下のXMLを構築するとします。

<?xml version="1.0" encoding="utf-16"?>
<ROOT Update="2009/01/23 22:23:47">
  <A attr1="one" attr2="two" attr3="three" />
  <B>
    <child Index="0">AAA</child>
    <child Index="1">BBB</child>
    <child Index="2">CCC</child>
    <child Index="3">DDD</child>
    <child Index="4">EEE</child>
  </B>
</ROOT>

Update属性は現在日時です。

XmlDocumentによる構築

XmlDocumentから直接構築すると、以下のようになります。

        static string[] data = new string[] { "AAA", "BBB", "CCC", "DDD", "EEE" };
        static XmlDocument normal()
        {
            XmlDocument doc = new XmlDocument();
            XmlNode root = doc.CreateElement("ROOT");
            XmlAttribute attrRoot = doc.CreateAttribute("Update");
            attrRoot.Value = DateTime.Now.ToString();
            root.Attributes.Append(attrRoot);
            doc.AppendChild(root);
            XmlNode aNode = doc.CreateElement("A");
            root.AppendChild(aNode);
            XmlAttribute attr1 = doc.CreateAttribute("attr1");
            attr1.Value = "one";
            aNode.Attributes.Append(attr1);
            XmlAttribute attr2 = doc.CreateAttribute("attr2");
            attr2.Value = "two";
            aNode.Attributes.Append(attr2);
            XmlAttribute attr3 = doc.CreateAttribute("attr3");
            attr3.Value = "three";
            aNode.Attributes.Append(attr3);
            XmlNode bNode = doc.CreateElement("B");
            root.AppendChild(bNode);
            for (int i = 0; i < data.Length; i++)
            {
                XmlNode child = doc.CreateElement("child");
                XmlAttribute attr = doc.CreateAttribute("Index");
                attr.Value = i.ToString();
                child.Attributes.Append(attr);
                child.InnerText = data[i];
                bNode.AppendChild(child);
            }
            return doc;
        }
簡易XMLビルダによる構築

簡易XMLビルダを使ってこのDOMを構築するコードです。

        static string[] data = new string[] { "AAA", "BBB", "CCC", "DDD", "EEE" };
        static XmlDocument simple()
        {
            XmlDocument doc = new XmlDocument();
            XmlBuilder b = new XmlBuilder(doc, "ROOT");
            b.Attribute("Update", DateTime.Now.ToString());
            b.Element("A")
                .Attributes(
                    "attr1", "one",
                    "attr2", "two",
                    "attr3", "three");
            b.Element("B")
                .DoTimes(data.Length, delegate(int i, XmlBuilder c)
                {
                    c.Element("child")
                        .Attribute("Index", i.ToString())
                        .InnerText(data[i]);
                });
            return doc;
        }

このコードで同じ内容のXMLが構築できます。
簡易DOMビルダーでコーディングすることで以下のようになるかと考えています。

  • 可読性が上がる。
  • タイプ量が減る。
    • 生成処理にいちいちXmlDocument#CreateElement or XmlDocument#CreateAttributeと打たなくて済む。
    • 追加処理にいちいちAppendChild or Appendしなくて済む。
    • 「値のセットは…XmlAttributeはValueに入れて、XmlElementの場合はInnerTextに入れて」ということを考えなくて済む。
  • 野良XMLを量産できる。

ソース

簡易DOMビルダーのソースです。

  • メソッド名に「Append〜」というプレフィックスを付けていません。簡易XMLビルダーが追加機能のみ提供するためです。
  • メソッドElementは、追加されたXmlElementを対象とする新たなXmlBuilderを返します。
  • Element以外のメソッドは、呼び出し元のXmlBuider自身を返します。
using System;
using System.Collections.Generic;
using System.Text;
using System.Xml;

namespace 簡易DOMビルダー
{
    public delegate void DoTimesAction(int index, XmlBuilder builder);
    public delegate void MapAction<T>(T target, XmlBuilder builder);

    public class XmlBuilder
    {
        private XmlDocument doc_;
        private XmlNode current_;

        public XmlBuilder(XmlDocument doc, String rootName)
        {
            doc_ = doc;
            current_ = doc_.CreateElement(rootName);
            doc.AppendChild(current_);
        }
        public XmlBuilder(XmlDocument doc, XmlNode current)
        {
            doc_ = doc;
            current_ = current;
        }

        public XmlBuilder Attribute(String name, String val)
        {
            XmlAttribute attr = doc_.CreateAttribute(name);
            attr.Value = val;
            current_.Attributes.Append(attr);
            return this;
        }

        public XmlBuilder Attributes(params String[] args)
        {
            if (args.Length % 2 != 0) throw new ArgumentException("引数の数が偶数個ではありません:" + String.Join(",", args));

            int index = 0;
            while (index < args.Length - 1)
            {
                Attribute(args[index], args[index + 1]);
                index = index + 2;
            }
            return this;
        }

        public XmlBuilder DoTimes(int times, DoTimesAction func)
        {
            for (int i = 0; i < times; i++)
            {
                func(i, this);
            }
            return this;
        }

        public XmlBuilder Element(String name)
        {
            return Element(name, null);
        }
        public XmlBuilder Element(String name, String val)
        {
            XmlNode n = doc_.CreateElement(name);
            if (val != null) n.InnerText = val;
            current_.AppendChild(n);
            return new XmlBuilder(doc_, n);
        }

        public XmlBuilder Elements(params String[] args)
        {
            if (args.Length % 2 != 0) throw new ArgumentException("引数の数が偶数個ではありません:" + String.Join(",", args));
            int index = 0;
            while (index < args.Length - 1)
            {
                Element(args[index], args[index + 1]);
                index = index + 2;
            }
            return this;
        }

        public XmlBuilder InnerText(String val)
        {
            current_.InnerText = val;
            return this;
        }

        public XmlBuilder Map<T>(List<T> targets, MapAction<T> func)
        {
            for (int i = 0; i < targets.Count; i++) 
            {
                func(targets[i], this);
            }
            return this;
        }
    }
}

C#3.0では

タイトルに"C#2.0"と付けたのは、#3.0ではXElementが十分にシンプルで使いやすいからです。

            XElement el = new XElement("ROOT",
                                new XElement("A",
                                    new XAttribute("attr1", "one"),
                                    new XAttribute("attr2", "two"),
                                    new XAttribute("attr3", "three")));
            XElement b = new XElement("B");
            for (int i = 0; i < data.Length; i++)
            {
                b.Add(new XElement("child", data[i],
                            new XAttribute("Index", i.ToString())));
            }
            el.Add(b);

備考

あくまで野良XMLを構築する用途のため、できることは極端に少なかったりします。(名前空間ないし型はないし)
この程度のXMLならYAMLで十分な感じですが、.NET標準でYAMLパーサが組み込まれていないので、C#で書いてる限り「野良でもXMLのが無難か」と考えています。