[C#] XNode を使って XNavigator を作る

[C#] XElement とは違う LINQ できる XmlNode を作成する | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/3628

なところで、自前の XmlNode を使って LINQ 操作を実現した訳ですが、XNode との重複が激しいので、試しに System.Xml.Linq.XNode http://msdn.microsoft.com/ja-jp/library/system.xml.linq.xnode.aspx を使って書き直してみます、っていう実験を。

■XNavigatorを実装。

自前の XmlNavigator をそのままコピペして実装し直します。
いくつかのメソッド名を変更しますが、そのままですんなりコンパイルが通ります。

public class XNavigator : IEnumerable<XNode>
{
	XNode _root;

	public XNavigator(XNode root)
	{
		_root = root;
	}

	public IEnumerator<XNode> GetEnumerator()
	{
		return new Enumerator(_root);
	}

	System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
	{
		return new Enumerator(_root);
	}

	protected class Enumerator : IEnumerator<XNode>
	{
		XNode _root;
		XNode _cur;

		public Enumerator(XNode root)
		{
			_root = root;
		}

		public XNode Current
		{
			get { return _cur; }
		}

		public void Dispose()
		{
		}

		object System.Collections.IEnumerator.Current
		{
			get { return _cur; }
		}

		public bool MoveNext()
		{
			if (_cur == null)
			{
				_cur = _root;
				return true;
			}
			if (_cur.NodeType == System.Xml.XmlNodeType.Element &&
				((XElement)_cur).Nodes().Count() > 0)
			{
				_cur = ((XElement)_cur).FirstNode;
				return true;
			}
			if (_cur.NextNode != null)
			{
				_cur = _cur.NextNode;
				return true;
			}
			XNode cur = _cur;
			while (true)
			{
				XElement pa1 = cur.Parent;
				if (pa1 == null)
				{
					_cur = null;
					return false;
				}
				if (pa1.NextNode != null)
				{
					_cur = pa1.NextNode;
					return true;
				}
				cur = pa1;
			}
		}

		public void Reset()
		{
			_cur = null;
		}
	}
}

■テストコードを書く

XmlNavigator 用に書いたテストコードを XNavigator 用に直します。

  • XML 構築のところは、XDocument を使って書き換え。
  • XNode のままでは Where メソッドでうまく動かないので、ちょっと思案

という具合です。

XNode のままだと、実は Nodes コレクションとかが無いんですよね…このあたり、DOM の頃からそうなのですが、インターフェースを重視するあまり、利用する時に XElement にキャストをしないといけないという罠があります。
XNavigator 自体は、XElement, XText などのオブジェクトも返すので、この TagName や Value 当たりが鬼門となります。

	[TestClass]
	public class TestXNavi
	{
		[TestMethod]
		public void TestNormal1()
		{
			XDocument doc = new XDocument(
				new XElement("root",
					new XElement("person",
						new XElement("name", "masuda"))));

			var q = new XNavigator(doc.FirstNode)
				.Where(n => n.TagName() == "name")
				.FirstOrDefault();
			Assert.AreEqual("masuda", q.Value());

		}
		
		[TestMethod]
		public void TestNormal2()
		{
			XDocument doc = new XDocument(
				new XElement("root",
					new XElement("person",
						new XElement("name", "masuda"),
						new XElement("name", "yamada"),
						new XElement("name", "yamasaki"))));

			// タグが検索できた場合
			var q = new XNavigator(doc.FirstNode)
				.Where(n => n.TagName() == "name")
				.Select( n => n );

			Assert.AreEqual(3, q.Count());
			Assert.AreEqual("masuda", q.First().Value());
		}

		[TestMethod]
		public void TestNormal3()
		{
			XDocument doc = new XDocument(
				new XElement("root",
					new XElement("person",
						new XAttribute("id", "1"),
						new XElement("name", "masuda"),
						new XElement("age","44")),
					new XElement("person",
						new XAttribute("id", "2"),
						new XElement("name", "yamada"),
						new XElement("age","20")),
					new XElement("person",
						new XAttribute("id", "3"),
						new XElement("name", "tanaka"),
						new XElement("age","10"))));

			// タグが検索できた場合
			var q = new XNavigator(doc.FirstNode)
				.Where(n => n.Attrs("id") == "2")
				.FirstOrDefault();
			Assert.AreEqual("person", q.TagName());
			// 拡張メソッドを利用
			Assert.AreEqual("yamada", q.Child("name").Value());
			Assert.AreEqual("20", q.Child("age").Value());
		}
		
		[TestMethod]
		public void TestNormal4()
		{
			XDocument doc = new XDocument(
				new XElement("root",
					new XElement("person",
						new XAttribute("id", "1"),
						new XElement("name", "masuda"),
						new XElement("age","44")),
					new XElement("person",
						new XAttribute("id", "2"),
						new XElement("name", "yamada"),
						new XElement("age","20")),
					new XElement("person",
						new XAttribute("id", "3"),
						new XElement("name", "tanaka"),
						new XElement("age","10"))));


			// クエリ文にしてみる
			var q = from n in new XNavigator(doc.FirstNode)
					where n.Attrs("id") == "2"
					select n;

			Assert.AreEqual("person", q.First().TagName());
			// 拡張メソッドを利用
			Assert.AreEqual("yamada", q.First().Child("name").Value());
			Assert.AreEqual("20", q.First().Child("age").Value());
		}

	}

■拡張メソッドを追加する

で、普通にやると断念してしまうので…無理矢理、XNode に拡張メソッドを追加します。
本来は、TagName プロパティ、Value プロパティのように「プロパティ」にしたいのですが、プロパティの拡張メソッドはできないので TagName() メソッド、Value() メソッドにします。

	/// <summary>
	/// XNodeをXElement風に使う拡張クラス
	/// </summary>
	public static class XNodeExtentions
	{
		public static string TagName(this XNode n)
		{
			if (n.NodeType == System.Xml.XmlNodeType.Element)
			{
				return ((XElement)n).Name.ToString();
			}
			else
			{
				return "";
			}
		}
		public static string Value(this XNode n)
		{
			if (n.NodeType == System.Xml.XmlNodeType.Element)
			{
				return ((XElement)n).Value;
			}
			else
			{
				return "";
			}
		}
		public static string Attrs(this XNode n, string key)
		{
			if (n.NodeType == System.Xml.XmlNodeType.Element)
			{
				var attr = ((XElement)n).Attribute(key);
				if ( attr != null ) {
					return attr.Value;
				}
			}
			return "";
		}
		public static XNode Child(this XNode nd, string tag)
		{
			if (nd.NodeType == System.Xml.XmlNodeType.Element)
			{
				return ((XElement)nd).Nodes().Where(n => n.TagName() == tag).FirstOrDefault();
			}
			else
			{
				return null;
			}

		}
	
	}

すると、上記のテストコードが通るようになります。

	var q = new XNavigator(doc.FirstNode)
		.Where(n => n.TagName() == "name")
		.FirstOrDefault();
	Assert.AreEqual("masuda", q.Value());

ラムダ式の中の n が XNode なので、TagName() メソッドを使います、ってな具合。XElement の場合は Value プロパティになる訳で、ここのところ、

  • XNode.Value() メソッド
  • XElement.Value プロパティ

という「メソッド名とプロパティ名が同じ」という(多分)VB では使えないメソッドが作られていますが、まあ良しとしましょう(後で確認してみますか)。

こうすると、XDocument もツリー構造でサーチができるのでコードが簡単になりますね。って話で。
自前の XmlNode でもいいんだけど、node / “tag” のような書き方ができないから、どうしようかね、って具合です。

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