scrap book

( ..)φメモメモ

(C#) XDocumentと戯れる

ソースコード

XML文字列化クラス(前回の流用)

オブジェクトをXML文字列にするだけのクラス。

using System;
using System.IO;
using System.Xml.Serialization;

namespace ObjectToXmlString
{
    /// <summary>
    /// XML文字列ヘルパー
    /// </summary>
    public static class XmlStringHelper
    {
        /// <summary>
        /// XML文字列化
        /// </summary>
        /// <param name="obj">オブジェクト</param>
        /// <param name="throwException">例外送出状態</param>
        /// <returns>XML文字列</returns>
        public static string ConvertToXmlStirng(object obj, bool throwException = false)
        {
            var xmlString = string.Empty;

            try
            {
                XmlSerializer serializer = new XmlSerializer(obj.GetType());
                using (StringWriter writer = new StringWriter())
                {
                    serializer.Serialize(writer, obj);
                    xmlString = writer.ToString();
                }
            }
            catch
            {
                if (throwException)
                {
                    throw;
                }
            }

            return xmlString;
        }
    }
}

StringBuilder拡張クラス

StringBuilderに用意されているメソッドが「書式指定付きで追加」か「改行付きで追加」しか無いので、両方付きのメソッドを用意しただけ。今回の主題とは何も関係ない。

using System;
using System.Globalization;
using System.Text;

namespace ObjectToXmlString
{
    public static class StringBuilderExtensions
    {
        public static void AppendFormatLine(this StringBuilder stringBuilder, string format, params object[] args)
        {
            stringBuilder.AppendFormat(
                CultureInfo.CurrentCulture,
                "{0}{1}", string.Format(format, args), Environment.NewLine);
        }
    }
}

XDocumentお試しコード1~4

ここからが本題。

お試し1 .. XML文字列 から XDocumentインスタンス生成

まずはXML文字列からXDocumentの生成。XDocument#Parse()で可能だった。このほかにも、XDocument#Load()でファイルから読み込めたりする。
ついでにそのまま XDocument.ToString() して分かった((2)の部分)けど、 ToString() ではXML本文のみが出力されるみたい。XML宣言部は (1) で出力できる。

using System.Linq;
using System.Text;
using System.Xml.Linq;

namespace ObjectToXmlString
{
    public static class XDocumentTrial
    {
        /// <summary>
        /// XML文字列 から XDocumentインスタンス生成
        /// &文字列出力
        /// </summary>
        public static string Trial1(object obj)
        {
            var sb = new StringBuilder();


            // オブジェクトをXML文字列化
            var xmlString = XmlStringHelper.ConvertToXmlStirng(obj);

            // XML文字列を解析
            XDocument xDoc = XDocument.Parse(xmlString);

            // (1)
            sb.AppendLine("[ Declaration ]");
            sb.AppendLine(xDoc.Declaration.ToString());
            sb.AppendLine();

            // (2)
            sb.AppendLine("[ ToString() ]");
            sb.AppendLine(xDoc.ToString());

            return sb.ToString();
        }
    }
}
お試し2 .. 属性(Attribute)や要素(Element)の表示

属性や要素の表示を試したコード。それぞれXElement.Attributes()、XElement.Elements()でコレクションとして取得できた。ほか、XElement.Annotations()で注釈(コメントのこと?)も取得できそう。
ちなみに再帰的に辿らずにRoot要素にだけ使ってみたら、リストの部分は値が繋がってしまった。これについてはお試し4でうまく表示できた。

/// <summary>
/// 属性(Attribute)や要素(Element)の表示
/// </summary>
public static string Trial2(object obj)
{
    var sb = new StringBuilder();

    var xmlString = XmlStringHelper.ConvertToXmlStirng(obj);
    XDocument xDoc = XDocument.Parse(xmlString);

    // (1) 属性の出力
    foreach (var (item, index) in xDoc.Root.Attributes().Select((item, index) => (item, index)))
    {
        sb.AppendFormatLine("Attributes[{0}]", index);
        sb.AppendFormatLine("  Name.NamespaceName = {0}", item.Name.NamespaceName);
        sb.AppendFormatLine("  Name.LocalName     = {0}", item.Name.LocalName);
        sb.AppendFormatLine("  Value              = {0}", item.Value);
    }
    sb.AppendLine();

    // (2) 要素の出力
    foreach (var (item, index) in xDoc.Root.Elements().Select((item, index) => (item, index)))
    {
        sb.AppendFormatLine("Elements[{0}]", index);
        sb.AppendFormatLine("  Name.NamespaceName = {0}", item.Name.NamespaceName);
        sb.AppendFormatLine("  Name.LocalName     = {0}", item.Name.LocalName);
        sb.AppendFormatLine("  Value              = {0}", item.Value);
        sb.AppendFormatLine("  HasAttributes      = {0}", item.HasAttributes);
        sb.AppendFormatLine("  HasElements        = {0}", item.HasElements);
    }

    return sb.ToString();
}
お試し3 .. 属性の削除

出力結果を見ていて、xmlns名前空間の属性が邪魔だったので、消してみた。(1)の部分。

/// <summary>
/// xmlns名前空間の属性を削除
/// </summary>
public static string Trial3(object obj)
{
    var sb = new StringBuilder();

    var xmlString = XmlStringHelper.ConvertToXmlStirng(obj);
    XDocument xDoc = XDocument.Parse(xmlString);

    // (1) 名前空間が"xmlns"の属性をすべて削除
    xDoc.Root.Attributes()
        .Where(attr => attr.Name.NamespaceName == @"http://www.w3.org/2000/xmlns/")
        .ToList().ForEach(attr => attr.Remove());

    // 属性の出力
    foreach (var (item, index) in xDoc.Root.Attributes().Select((item, index) => (item, index)))
    {
        sb.AppendFormatLine("Attributes[{0}]", index);
        sb.AppendFormatLine("  Name.NamespaceName = {0}", item.Name.NamespaceName);
        sb.AppendFormatLine("  Name.LocalName     = {0}", item.Name.LocalName);
        sb.AppendFormatLine("  Value              = {0}", item.Value);
    }

    return sb.ToString();
}
お試し4 .. 再帰的に走査して階層化表示

属性を再帰的に走査して、いい感じに階層化して表示してみた。十分なパターンで試験していないけど、おおむねいい感じ。

/// <summary>
/// 再帰的に走査して階層化表示
/// </summary>
public static string Trial4(object obj, bool throwException = false)
{
    var sb = new StringBuilder();

    try
    {
        var xmlString = XmlStringHelper.ConvertToXmlStirng(obj, throwException);
        XDocument xDoc = XDocument.Parse(xmlString);

        // 名前空間が"xmlns"の属性をすべて削除
        xDoc.Root.Attributes()
            .Where(attr => attr.Name.NamespaceName == @"http://www.w3.org/2000/xmlns/")
            .ToList().ForEach(attr => attr.Remove());

        // 再帰的に走査して階層化表示
        foreach (var item in xDoc.Elements())
        {
            sb.Append(Trial4Core(item));
        }
    }
    catch
    {
        if (throwException)
        {
            throw;
        }

        sb.Clear();
    }

    return sb.ToString();
}

private static string Trial4Core(XElement xElement, int offset = 0)
{
    var sb = new StringBuilder();
    var spaces = new string(' ', offset);


    // Append Attributes
    foreach (var (item, index) in xElement.Attributes().Select((item, index) => (item, index)))
    {
        sb.AppendFormatLine("{0}Attributes[{1}]", spaces, index);
        sb.AppendFormatLine("{0}  Name.LocalName = {1}", spaces, item.Name.LocalName);
        sb.AppendFormatLine("{0}  Value          = {1}", spaces, item.Value);
    }

    // Append Elements
    foreach (var (item, index) in xElement.Elements().Select((item, index) => (item, index)))
    {
        // Append Attributes and Elements recursively
        if (item.HasAttributes || item.HasElements)
        {
            sb.AppendFormatLine("{0}{1}", spaces, item.Name.LocalName);
            sb.Append(Trial4Core(item, offset + 2));
            continue;
        }

        sb.AppendFormatLine("{0}Elements[{1}]", spaces, index);
        sb.AppendFormatLine("{0}  Name.LocalName = {1}", spaces, item.Name.LocalName);
        sb.AppendFormatLine("{0}  Value          = {1}", spaces, item.Value);
    }

    return sb.ToString();
}

呼び出し側コード

using System;
using System.Xml.Serialization;
using System.Collections.Generic;

namespace ObjectToXmlString
{
    //
    // 文字列化対象
    //
    [Serializable]
    public class Box
    {
        public class BoxVolume
        {
            [XmlAttribute]
            public int Value { get; set; } = 10;
            [XmlAttribute]
            public string Unit { get; set; } = "L";
        }

        public class BoxItem
        {
            public string Name { get; set; }
            public int Count { get; set; }
        }

        [XmlAttribute]
        public string Name { get; set; } = "box a";
        public BoxVolume Volume { get; set; } = new BoxVolume();
        public List<BoxItem> Items { get; } = new List<BoxItem>() {
            new BoxItem(){ Name = "Orange", Count = 1 },
            new BoxItem(){ Name = "Apple", Count = 2 },
            new BoxItem(){ Name = null, Count = 0 },
        };
    }


    //
    // 実行コード
    //
    class Program
    {
        static void Main(string[] args)
        {
            Box boxA = new Box();

            //
            // XML文字列
            //
            WriteResult("XML文字列",
                XmlStringHelper.ConvertToXmlStirng(boxA));

            //
            // XDocumentお試し
            //
            WriteResult("Trial1",
                XDocumentTrial.Trial1(boxA));
            WriteResult("Trial2",
                XDocumentTrial.Trial2(boxA));
            WriteResult("Trial3",
                XDocumentTrial.Trial3(boxA));
            WriteResult("Trial4",
                XDocumentTrial.Trial4(boxA));


            Console.ReadKey();
        }

        static void WriteResult(string title, string result)
        {
            Console.WriteLine("----------------");
            Console.WriteLine($"{title}");
            Console.WriteLine("----------------");
            Console.WriteLine($"{result}");
            Console.WriteLine();
        }
    }
}

実行結果

----------------
XML文字列
----------------
<?xml version="1.0" encoding="utf-16"?>
<Box xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" Name="box a">
  <Volume Value="10" Unit="L" />
  <Items>
    <BoxItem>
      <Name>Orange</Name>
      <Count>1</Count>
    </BoxItem>
    <BoxItem>
      <Name>Apple</Name>
      <Count>2</Count>
    </BoxItem>
    <BoxItem>
      <Count>0</Count>
    </BoxItem>
  </Items>
</Box>

----------------
Trial1
----------------
[ Declaration ]
<?xml version="1.0" encoding="utf-16"?>

[ ToString() ]
<Box xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" Name="box a">
  <Volume Value="10" Unit="L" />
  <Items>
    <BoxItem>
      <Name>Orange</Name>
      <Count>1</Count>
    </BoxItem>
    <BoxItem>
      <Name>Apple</Name>
      <Count>2</Count>
    </BoxItem>
    <BoxItem>
      <Count>0</Count>
    </BoxItem>
  </Items>
</Box>


----------------
Trial2
----------------
Attributes[0]
  Name.NamespaceName = http://www.w3.org/2000/xmlns/
  Name.LocalName     = xsi
  Value              = http://www.w3.org/2001/XMLSchema-instance
Attributes[1]
  Name.NamespaceName = http://www.w3.org/2000/xmlns/
  Name.LocalName     = xsd
  Value              = http://www.w3.org/2001/XMLSchema
Attributes[2]
  Name.NamespaceName =
  Name.LocalName     = Name
  Value              = box a

Elements[0]
  Name.NamespaceName =
  Name.LocalName     = Volume
  Value              =
  HasAttributes      = True
  HasElements        = False
Elements[1]
  Name.NamespaceName =
  Name.LocalName     = Items
  Value              = Orange1Apple20
  HasAttributes      = False
  HasElements        = True


----------------
Trial3
----------------
Attributes[0]
  Name.NamespaceName =
  Name.LocalName     = Name
  Value              = box a


----------------
Trial4
----------------
Attributes[0]
  Name.LocalName = Name
  Value          = box a
Volume
  Attributes[0]
    Name.LocalName = Value
    Value          = 10
  Attributes[1]
    Name.LocalName = Unit
    Value          = L
Items
  BoxItem
    Elements[0]
      Name.LocalName = Name
      Value          = Orange
    Elements[1]
      Name.LocalName = Count
      Value          = 1
  BoxItem
    Elements[0]
      Name.LocalName = Name
      Value          = Apple
    Elements[1]
      Name.LocalName = Count
      Value          = 2
  BoxItem
    Elements[0]
      Name.LocalName = Count
      Value          = 0

解説?は上述したので割愛。
ところでBoxItem[2].Nameにnull入れてみたのだけど表示されない(XML文字列の時点で無い)。nullならnullと出て欲しいんだけどどうにかならないかな。。たしかNullable<int>やら自前の型なら<Name value=nil />みたいな文字列になったと思うんだけどなぁ。。記憶違いかな。。

参考(その他)

他の方法。XDocumentが使いやすそうだったので今回はパスしたけどメモ。

  1. XPathを使ってXMLファイルをパースする (XmlDocumentを利用) (C#プログラミング)
  2. XML をパースする(C++/CX, C#版) - Qiita