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