[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(&quot;root&quot;,
					new XElement(&quot;person&quot;,
						new XElement(&quot;name&quot;, &quot;masuda&quot;))));

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

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

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

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

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

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


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

			Assert.AreEqual(&quot;person&quot;, q.First().TagName());
			// 拡張メソッドを利用
			Assert.AreEqual(&quot;yamada&quot;, q.First().Child(&quot;name&quot;).Value());
			Assert.AreEqual(&quot;20&quot;, q.First().Child(&quot;age&quot;).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 &quot;&quot;;
			}
		}
		public static string Value(this XNode n)
		{
			if (n.NodeType == System.Xml.XmlNodeType.Element)
			{
				return ((XElement)n).Value;
			}
			else
			{
				return &quot;&quot;;
			}
		}
		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 &quot;&quot;;
		}
		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() == &quot;name&quot;)
		.FirstOrDefault();
	Assert.AreEqual(&quot;masuda&quot;, q.Value());

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

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

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

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

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