[C#] XElement 風に XML を構築するクラスを作ってみる

困ったときにそのまま使える LINQ to XML コードサンプル 18 パターン [1/2] – Web/DB プログラミング徹底解説
http://keicode.com/dotnet/linq-to-xml-101.php
XElement クラス (System.Xml.Linq)
http://msdn.microsoft.com/ja-jp/library/system.xml.linq.xelement.aspx

を参考にしながら、XML をメソッドチェーンを使って構築するクラスを作ってみます。まぁ、HtmlDom を作るときの副産物…と言うか、練習問題的に。

■使い方サンプルを作る

TDD と UIDD のハイブリッドプロセス(という命名をしておく)、ということで使い方のサンプルを先に作ります。
これを先に作っておくと、ある程度のマニュアルがなくても使える「感覚的に正しい」プログラミングコードができあがります。D.A.ノーマンの言う「ユーザーインターフェース」ってやつです。

public void test1()
{
	var root = new XmlNode("root")
		.Node("person")
		.Node("name", "masuda")
		.Root;

	var root2 = new XmlNode("root")
		.Node("person")
		.AddNode("name", "masuda")
		.AddNode("age", "44")
		.AddNode("address", "itabashi-ku")
		.Root;

	var root3 = new XmlNode("root")
		.Node("person")
		.AddAttr("name", "masuda")
		.AddAttr("age", "44")
		.SetValue("masuda tomoaki")
		.Root;


	var root4 = new XmlNode("persons")
		.Node("person").AddAttr("id", "1")
			.AddNode("name", "masuda tomoaki")
			.AddNode("age", "44")
		.Parent
		.Node("person").AddAttr("id", "2")
			.AddNode("name", "yamada taro")
			.AddNode("age", "20")
		.Root;
}

実は、.Parent と .Root は後付、SetValue はやむなくって感じですね。
最初に作ったのはこんな感じ。

	var root = new XmlNode("root")
		.Node("person")
		.Attr("name", "masuda")
		.Attr("age", "44")
		.Value("masuda tomoaki");

「動詞」を使わずに「名詞」だけで、繋げてみたかったのですが、Value プロパティと、Value メソッドが曖昧になってしまうのでパス。
あと、子ノードを連続追加する時に Node だけだと駄目で、Node.Parent.Node(“tag”) と変なことをしないといけないので、AddNode に変更。それに伴って、Attr も AddAttr に変更しています。

それと、最後に Root プロパティを入れたのは、テストコードを書いたときに、

	var root = new XmlNode("root");
	root.Node("person")
		.Attr("name", "masuda")
		.Attr("age", "44")
		.Value("masuda tomoaki");
    Assert.AreEqual("...", root.Xml );

のように「わざわざ」ルート要素を保存しないといけないという面倒があるので、ルートに戻すプロパティを用意します。

	var root = new XmlNode("root")
	    .Node("person")
		.Attr("name", "masuda")
		.Attr("age", "44")
		.Value("masuda tomoaki")
        .Root ;
    Assert.AreEqual("...", root.Xml );

こっちのほうがすっきりするし、後から意味が分かり易いのです。

■インターフェースに従って、コンパイルが通るようにコーディング

テストコードを先に書いてもいいのですが、Visual Studio のインテリセンスを利用したいので、コンパイルが通るまでのコードを書きます。
素のエディタで書くときは、逆にテストコードから書いたりします。と言うのも、素のエディタの場合は、テストコードのコンパイルエラーからメソッド/プロパティを追加することになるので、テストコードが先のほうがやり易いのです。
この違いは(私にとって)重要です。

■テストコードを書く

下記では、一気に追加していますが、実際にはひとつひとつ追加しています。
簡単なテストから複雑なテストへというのはテストの定番です。
実は Xml プロパティは、テストコードを書くときに追加しています。こうするとテストがやりやすいですからね。

	[TestMethod]
	public void TestNormal1()
	{
		var root = new XmlNode("root")
			.Node("person")
			.Node("name", "masuda")
			.Root;

		Assert.AreEqual("<root><person><name>masuda</name></person></root>", root.Xml);
	}

	[TestMethod]
	public void TestNormal2()
	{
		var root = new XmlNode("root")
			.Node("person")
			.AddNode("name", "masuda")
			.AddNode("age", "44")
			.AddNode("address", "itabashi-ku")
			.Root;

		Assert.AreEqual("<root><person><name>masuda</name><age>44</age><address>itabashi-ku</address></person></root>", root.Xml);
	}

	[TestMethod]
	public void TestNormal3()
	{
		var root = new XmlNode("root")
			.Node("person")
			.AddAttr("name", "masuda")
			.AddAttr("age", "44")
			.SetValue("masuda tomoaki")
			.Root;

		Assert.AreEqual("<root><person name=\"masuda\" age=\"44\">masuda tomoaki</person></root>", root.Xml);
	}

	[TestMethod]
	public void TestNormal4()
	{
		var root = new XmlNode("persons")
			.Node("person").AddAttr("id", "1")
				.AddNode("name", "masuda tomoaki")
				.AddNode("age", "44")
			.Parent
			.Node("person").AddAttr("id", "2")
				.AddNode("name", "yamada taro")
				.AddNode("age", "20")
			.Root;
			;

		Assert.AreEqual("<person>" + 
			"<person id=\"1\"><name>masuda tomoaki</name><age>44</age></person>" +
			"<person id=\"2\"><name>tamada taro</name><age>20</age></person>" +
			"</persons>",
			root.Xml);
	}

■コードが完成品

でもって、テストを通るようにしたのが以下、150行程度のコードです。
Xml プロパティは、HtmlDom からコピペしているのでちょっと無駄なコードが入っていますが、まぁこの程度で書けるのだから C# って便利やなぁ、と。
これを HtmlDom に組み入れるかどうか思案中。

/// <summary>
/// XML node class
/// </summary>
public class XmlNode
{
	#region property
	public string TagName { get; set; }
	public string Value { get; set; }
	public XmlNodeCollection Children { get; set; }
	public XmlAttrCollection Attrs { get; set; }
	public XmlNode Parent { get; set; }
	#endregion

	#region constractor 
	public XmlNode(string tag, string value = "")
	{
		this.TagName = tag;
		this.Value = value;
		this.Children = new XmlNodeCollection();
		this.Children.Parent = this;
		this.Attrs = new XmlAttrCollection();
	}
	#endregion

	#region creator for method chain
	public XmlNode Node(string tag, string value = "")
	{
		var nd = new XmlNode(tag, value);
		this.Children.Add(nd);
		return nd;
	}
	public XmlNode AddNode(string tag, string value = "")
	{
		this.Node(tag, value);
		return this;
	}

	public XmlNode AddAttr(string key, string value)
	{
		this.Attrs.Add(new XmlAttr(key, value));
		return this;
	}
	public XmlNode SetValue(string value)
	{
		this.Value = value;
		return this;
	}
	#endregion

	#region xml string serialize
	public string Xml
	{
		get
		{
                string s = "";
                if (this.TagName == "" || this.TagName == "#text" || this.TagName == "#comment")
                {
                    s = this.Value.Trim();
                }
                else
                {
                    s += string.Format("<{0}", this.TagName);
                    foreach (var at in this.Attrs)
                    {
                        s += string.Format(" {0}=\"{1}\"", at.Key, at.Value);
                    }
                    if (this.Children.Count > 0)
                    {
                        s += ">";
					s += this.Value;
                        foreach (var el in this.Children)
                        {
                            s += el.Xml;
                        }
                        s += string.Format("</{0}>", this.TagName);
                    }
				else if (this.Value != "")
				{
					s += ">";
					s += this.Value;
					s += string.Format("</{0}>", this.TagName);
				} 
				else 
                    {
                        s += "/>";
                    }
                }
                return s;
		}
	}
	#endregion

	public XmlNode Root
	{
		get
		{
			XmlNode nd = this;
			while (nd.Parent != null)
			{
				nd = nd.Parent;
			}
			return nd;
		}
	}
}

/// <summary>
/// XML node collection
/// </summary>
public class XmlNodeCollection : List<XmlNode> {
	public XmlNode Parent { get; set; }
	public new XmlNode Add(XmlNode nd) 
	{
		base.Add(nd);
		nd.Parent = this.Parent;
		return nd;
	}

}

/// <summary>
/// XML attribute class
/// </summary>
public class XmlAttr
{
	public string Key { get; set; }
	public string Value { get; set; }

	public XmlAttr(string key, string value)
	{
		this.Key = key;
		this.Value = value;
	}
}
/// <summary>
/// XML Attribute collection
/// </summary>
public class XmlAttrCollection : List<XmlAttr> { }

カテゴリー: C# パーマリンク