困ったときにそのまま使える 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> { }