.NET4.0の新機能 dynamic を使用したXMLの動的読み込みクラス

修正したものは 「C# 4.0 dynamic による動的XML読み書きクラス」を参照

修正項目
UTF-8の標準的なXML
XML宣言の強制的な文字コード変換をコメント化
ほかバグをいくつか修正

                                                                                                                • -

色々と使いにくい点があるので、時間があれば修正予定

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Dynamic;
using System.Xml;
using System.Xml.Linq;
using System.Xml.XPath;
using System.IO;

namespace RLib.Common
{
    /// <summary>
    /// 文字コードが指定可能な StringWriter クラス。
    /// </summary>
    public class MultiStringWriter : StringWriter
    {
        /// <summary>
        /// 文字コード
        /// </summary>
        private Encoding _encoding = Encoding.UTF8;

        /// <summary>
        /// 指定した文字コードと StringBuilder を元に MultiStringWriter のインスタンスを作成する。
        /// </summary>
        /// <param name="sb"></param>
        /// <param name="enc"></param>
        public MultiStringWriter(StringBuilder sb, Encoding enc) : base(sb) { this._encoding = enc; }

        /// <summary>
        /// エンコードの取得
        /// </summary>
        public override Encoding Encoding
        {
            get
            {
                return this._encoding;
            }
        }
    }

    /// <summary>
    /// XElementの動的オブジェクト
    /// </summary>
    public class DElement : DynamicObject
    {
        /// <summary>
        /// XMLドキュメント
        /// </summary>
        private XElement element;

        /// <summary>
        /// CData オブジェクト
        /// </summary>
        private XCData cdata;

        /// <summary>
        /// XPath時の名前空間
        /// </summary>
        internal XmlNamespaceManager Namespace { get; set; }

        #region 初期化 (XElement)

        /// <summary>
        /// DElement を与えて初期化。
        /// </summary>
        /// <param name="element">読み取り対象の XElement。</param>
        public DElement(DElement element) { this.element = element.element; }

        /// <summary>
        /// XElement を与えて初期化。
        /// </summary>
        /// <param name="element">読み取り対象の XElement。</param>
        public DElement(XElement element) { this.element = element; }

        /// <summary>
        /// XDocument を与えて初期化。
        /// ルート要素を読み出し。
        /// </summary>
        /// <param name="doc">読み取り対象の XDocument。</param>
        public DElement(XDocument doc) : this(doc.Root) { }

        /// <summary>
        /// ファイルのパスを与えて初期化。
        /// </summary>
        /// <param name="uri">読み取り対象の XML ファイル名。</param>
        public DElement(XmlReader reader) : this(XDocument.Load(reader)) { }

        /// <summary>
        /// ファイルのパスを与えて初期化。
        /// </summary>
        /// <param name="uri">読み取り対象の XML ファイル名。</param>
        public DElement(string uri) : this(XDocument.Load(uri)) { }

        /// <summary>
        /// XElement、XmlNamespacemanager を与えて初期化。
        /// </summary>
        /// <param name="element">読み取り対象の XElement。</param>
        private DElement(XElement element, XmlNamespaceManager ns) { this.element = element; this.Namespace = ns; }

        /// <summary>
        /// XML文字列を与えて初期化されたインスタンスを受け取る。
        /// </summary>
        /// <param name="xml">XML構造の文字列</param>
        /// <returns>DElement オブジェクト</returns>
        public static DElement Create(string xml)
        {
            return Create(xml, null);
        }

        /// <summary>
        /// XML文字列を与えて初期化されたインスタンスを受け取る。
        /// </summary>
        /// <param name="xml">XML構造の文字列</param>
        /// <param name="ns">名前空間オブジェクト</param>
        /// <returns>DElement オブジェクト</returns>
        public static DElement Create(string xml, XmlNamespaceManager ns)
        {
            DElement doc = null;
            //using (var sr = new StringReader(xml))
            //{
            //    doc = new DElement(XDocument.Load(sr));
            //    doc.Namespace = ns;
            //}
            //if(xml.IndexOf
            var xdoc = XDocument.Parse(xml);
            xdoc.Declaration.Encoding = "utf-8";
            doc = new DElement(xdoc);
            doc.Namespace = ns;
            return doc;
        }

        #endregion

        #region 初期化 (XCdata)
        
        /// <summary>
        /// XCData を与えて初期化。
        /// </summary>
        /// <param name="cdata">読み取り対象の XCData</param>
        public DElement(XCData cdata) { this.cdata = cdata; }

        #endregion

        #region 動的メソッド(取得)

        /// <summary>
        /// 動的Getプロパティ
        /// </summary>
        /// <param name="binder">動的実行時情報</param>
        /// <param name="result">返却する値</param>
        /// <returns>動的実行を成功させる場合は true。それ以外は false。</returns>
        public override bool TryGetMember(System.Dynamic.GetMemberBinder binder, out object result)
        {
            var name = binder.Name;

            // InnerText的なもの
            if (name.StartsWith("__Value"))
            {
                if (this.element != null) result = this.element.Value;
                else result = this.cdata.Value;
                return true;
            }

            // XElementのオブジェクトが存在しない場合は処理を中断する。
            if (this.element == null)
            {
                result = null;
                return true;
            }

            // 属性値は _属性名 で取得。文字列として返す。
            if (name.StartsWith("_"))
            {
                var attName = name.Substring(1);
                var att = element.Attribute(attName);
                if (att != null)
                {
                    result = att.Value;
                    return true;
                }
                result = (string)null;
                return true;
            }

            var subElements = element.Elements(name).ToList();

            // 要素がないときは null 返す。
            if (subElements.Count == 0)
            {
                result = (string)null;
                return true;
            }

            // 要素が1個だけの時は素直にその要素を返す。
            if (subElements.Count == 1)
            {
                var e = subElements[0];

                result = new DElement(e, this.Namespace);
                return true;
            }

            // 要素が複数ある時はリストで要素一覧を返す。
            var es = subElements.Select(x => new DElement(x, this.Namespace));
            result = es.ToList();
            return true;
        }

        /// <summary>
        /// 動的キャスト
        /// </summary>
        /// <param name="binder">動的実行時情報</param>
        /// <param name="result">返却する値</param>
        /// <returns>動的実行を成功させる場合は true。それ以外は false。</returns>
        public override bool TryConvert(ConvertBinder binder, out object result)
        {
            // string へのキャストで、要素の値を取得。
            if (binder.Type == typeof(string))
            {
                if (this.element != null) result = element.Value;
                else result = cdata.Value;
                return true;
            }
            // int へのキャストで int.Parse。
            // Parse できないときは例外丸投げ。
            if (binder.Type == typeof(int))
            {
                if (this.element != null) result = int.Parse(element.Value);
                else result = int.Parse(cdata.Value);
                return true;
            }

            // 要素単体に対して foreach やっちゃったときでもエラーにならないように、IEnumerable へのキャストを定義。
            // これやっとかないと、元々複数要素あったのに XML を修正して要素が1個だけになった時に挙動おかしくなる。
            if (binder.Type == typeof(System.Collections.IEnumerable))
            {
                result = new[] { this };
                return true;
            }

            result = null;
            return false;
        }

        /// <summary>
        /// 動的メソッドの実行
        /// </summary>
        /// <param name="binder">動的実行時情報</param>
        /// <param name="args">メソッドに渡す値</param>
        /// <param name="result">返却する値</param>
        /// <returns>動的実行を成功させる場合は true。それ以外は false。</returns>
        public override bool TryInvokeMember(System.Dynamic.InvokeMemberBinder binder, object[] args, out object result)
        {
            switch (binder.Name)
            {
                case "CData":
                    result = this.CData();
                    return true;

                case "Name": // Name() で要素名を取得。
                    result = element.Name.ToString();
                    return true;

                case "GetEnumerator": // IEnumerable へのキャストと同様の理由。
                    result = new[] { this }.GetEnumerator();
                    return true;

                case "All": // All() 呼び出しで、子要素を全部取得できるようにする。
                    result = element.Elements().Select(x => new DElement(x, this.Namespace)).ToList();
                    return true;

                case "XPath": // XPath(string, string) で要素を取得出来るようにする。
                    if (this.XPath(args, out result)) return true;
                    break;
            }

            return base.TryInvokeMember(binder, args, out result);
        }

        #region TryInvokeMember - 元関数
        /// <summary>
        /// CDataセクションのXMLタグオブジェクトを取得する。
        /// </summary>
        /// <returns>XMLタグオブジェクト</returns>
        private DElement CData()
        {
            if (this.element.Nodes().Count<XNode>() == 1)
            {
                var node = this.element.FirstNode;
                if (node.NodeType == XmlNodeType.CDATA)
                {
                    return new DElement((XCData)node);
                }
            }
            return this;
        }

        /// <summary>
        /// カレント XML に対し、XPath を実行する。
        /// </summary>
        /// <param name="args">XPath</param>
        /// <param name="result">XPath結果</param>
        /// <returns>XPathが成功した場合は true。それ以外は false。</returns>
        private bool XPath(object[] args, out object result)
        {
            result = null;
            if (args.Length == 1)
            {
                IEnumerable<XElement> elements = null;
                try
                {
                    if (this.Namespace != null)
                    {
                        elements = element.XPathSelectElements(Convert.ToString(args[0]), this.Namespace);
                    }
                    else
                    {
                        elements = element.XPathSelectElements(Convert.ToString(args[0]));
                    }
                }
                catch
                {
                    System.Diagnostics.Trace.WriteLine("XPathエラー");
                    return true;
                }

                // 要素の変換
                if (elements != null && 0 < elements.Count())
                {
                    if (1 <= elements.Count())
                    {
                        var list = elements
                                    .Select(x => new DElement(x, this.Namespace))
                                    .Select(x => x.CData())
                                    .ToList();
                        if (list.Count() == 1) result = list[0];
                        else result = list;
                    }
                    return true;
                }
                return true;
            }
            // 引数が一致しない場合はXPathを実行しない
            return false;
        }

        #endregion

        #endregion

        #region 動的メソッド(設定)

        /// <summary>
        /// 動的Setプロパティ
        /// </summary>
        /// <param name="binder">動的実行時情報</param>
        /// <param name="value">設定する値</param>
        /// <returns>動的実行を成功させる場合は true。それ以外は false。</returns>
        public override bool TrySetMember(SetMemberBinder binder, object value)
        {
            var name = binder.Name;

            // InnerText的なもの
            if (name.StartsWith("__Value"))
            {
                if (value.GetType() == typeof(string))
                {
                    if(this.element != null) this.element.Value = Convert.ToString(value);
                    else this.cdata.Value = Convert.ToString(value);
                    return true;
                }
                return false;
            }

            // XElementのオブジェクトが存在しない場合は処理を中断する。
            if (this.element == null)
            {
                return false;
            }

            // 属性値は _属性名 で取得。文字列として返す。
            if (name.StartsWith("_"))
            {
                var attName = name.Substring(1);
                var att = this.element.Attribute(attName);
                if (att != null)
                {
                    att.Value = Convert.ToString(value);
                    return true;
                }
                return false;
            }

            // 要素数のチェック
            var subElements = element.Elements(name).ToList();
            if (subElements.Count == 0) return false;

            // 代入する値が null の場合は要素を削除する。
            if (value == null)
            {
                subElements.RemoveAll(x => true);
            }

            // 要素数が1の場合
            if (subElements.Count == 1)
            {
                // DElement または XElement オブジェクトが渡された場合は、子要素と置き換える。
                if (value.GetType() == typeof(DElement))
                {
                    value = ((DElement)value).element;
                }
                if (value.GetType() == typeof(XElement))
                {
                    subElements[0].ReplaceAll(value);
                    return true;
                }

                // 他の型はそのまま突っ込む
                subElements[0].ReplaceAll(value);
                return true;
            }

            // 要素数が複数ある場合は代入を禁止する。
            return false;
        }

        #endregion

        #region 非動的メソッド
        
        /// <summary>
        /// XMLドキュメントを保存する。
        /// </summary>
        /// <param name="writer">保存用Stream</param>
        public void Save(XmlWriter writer)
        {
            if(this.element != null)
                this.element.Save(writer);
        }

        /// <summary>
        /// XMLドキュメントを保存する。
        /// </summary>
        /// <param name="path">保存先パス</param>
        public void Save(string path)
        {
            if (this.element != null)
                this.element.Save(path);
        }

        #endregion

        #region override メソッド

        /// <summary>
        /// 現在参照している Xmlタグ を文字列として受け取る。
        /// </summary>
        /// <returns>文字列</returns>
        public override string ToString()
        {
            if (this.cdata != null) return this.cdata.ToString();

            var sb = new StringBuilder();
            using (var stringWrite = new MultiStringWriter(sb, Encoding.UTF8))
            using (var xw = XmlWriter.Create(stringWrite))
            {
                this.element.Save(xw);
            }
            return sb.ToString();
        }

        #endregion
    }
}
      • -