scrap book

( ..)φメモメモ

(C#) シリアライズ可能なオブジェクトを階層化して表示

概要

前々回と前回(以下)からの続き。

シリアライズ可能なオブジェクトをXML文字列にしたり、さらにそれをXDocumentを使って解析して、見やすい形で表示したりしてみた。

バージョン

.NET Framework 4.7.2
C# 4.7

目次

ソースコード

StringBuilder拡張クラス

表示用に拡張メソッド用意しただけ。今回の本題とはあまり関係ない。ほぼ前回からの流用。

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

namespace ObjectToXmlString
{
    /// <summary>
    /// StringBuilder拡張クラス
    /// </summary>
    public static class StringBuilderExtensions
    {
        /// <summary>
        /// 書式指定文字列追加
        /// 改行付き
        /// </summary>
        /// <param name="stringBuilder">StringBuilder</param>
        /// <param name="format">書式</param>
        /// <param name="args">可変長引数</param>
        public static void AppendFormatLine(this StringBuilder stringBuilder,
            string format, params object[] args)
        {
            stringBuilder.AppendFormat(
                CultureInfo.CurrentCulture,
                "{0}{1}", string.Format(format, args), Environment.NewLine);
        }

        /// <summary>
        /// 書式指定文字列追加
        /// 改行および先頭空白インデント付き
        /// </summary>
        /// <param name="stringBuilder">StringBuilder</param>
        /// <param name="indent">インデント</param>
        /// <param name="format">書式</param>
        /// <param name="args">可変長引数</param>
        public static void AppendFormatLine(this StringBuilder stringBuilder,
            int indent, string format, params object[] args)
        {
            stringBuilder.AppendFormat(
                CultureInfo.CurrentCulture,
                "{0}{1}{2}", new string(' ', indent), string.Format(format, args), Environment.NewLine);
        }
    }
}

XML文字列化クラス

オブジェクトをXML文字列にするだけのクラス。前回からの流用。

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="enableException">例外送出有無</param>
        /// <returns>XML文字列</returns>
        public static string GenerateXmlStirng(object obj, bool enableException = 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 (enableException)
                {
                    throw;
                }
            }

            return xmlString;
        }
    }
}

加工して表示するクラス

今回の本題。ただし説明するより実行結果見た方が早そうなので説明なし。

using System;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using System.Collections.Generic;

namespace ObjectToXmlString
{
    /// <summary>
    /// ダンプヘルパー
    /// </summary>
    public static class DumpHelper
    {
        /// <summary>
        /// XML文字列生成(軽量版)
        /// XML宣言とルート要素のXMLNS名前空間の属性を削除したXML文字列化
        /// </summary>
        /// <param name="obj">オブジェクト</param>
        /// <param name="enableException">例外送出有無</param>
        /// <returns>XML文字列</returns>
        public static string GenerateXmlStirngSlim(object obj, bool enableException = false)
        {
            var xmlString = string.Empty;

            try
            {
                XDocument xDoc = XDocument.Parse(
                    XmlStringHelper.GenerateXmlStirng(obj, enableException));

                xDoc.Root.Attributes()
                    .Where(attr => attr.Name.NamespaceName == @"http://www.w3.org/2000/xmlns/")
                    .ToList().ForEach(attr => attr.Remove());

                xmlString = xDoc.ToString();
            }
            catch
            {
                if (enableException)
                {
                    throw;
                }
            }

            return xmlString;
        }

        /// <summary>
        /// XML文字列生成(1行版)
        /// </summary>
        /// <param name="obj">オブジェクト</param>
        /// <param name="newLineReplacingString">改行置換文字列</param>
        /// <param name="enableException">例外送出有無</param>
        /// <returns>XML文字列</returns>
        public static string GenerateXmlStringOneLine(object obj, string newLineReplacingString, bool enableException = false)
        {
            var xmlString = string.Empty;

            try
            {
                xmlString =
                    GenerateXmlStirngSlim(obj, enableException)
                    .Replace(Environment.NewLine, newLineReplacingString);
            }
            catch
            {
                if (enableException)
                {
                    throw;
                }
            }

            return xmlString;
        }

        /// <summary>
        /// 階層化文字列生成
        /// </summary>
        /// <param name="obj">オブジェクト</param>
        /// <param name="enableException">例外送出有無</param>
        /// <returns>階層化文字列</returns>
        public static string GenerateLayeredString(object obj, bool enableException = false)
        {
            var sb = new StringBuilder();

            try
            {
                var xmlString = GenerateXmlStirngSlim(obj, enableException);
                XDocument xDoc = XDocument.Parse(xmlString);

                // 階層化表示用の文字列を生成
                foreach (var item in xDoc.Elements())
                {
                    sb.Append(GenerateLayeredStringOfElement(0, item));
                }
            }
            catch
            {
                if (enableException)
                {
                    throw;
                }

                sb.Clear();
            }

            return sb.ToString();
        }

        /// <summary>
        /// XML要素の階層化文字列生成
        /// 再帰呼び出し用
        /// </summary>
        /// <param name="indent">インデント</param>
        /// <param name="xElm">XML要素</param>
        /// <param name="xElmIndex">XML要素の配列インデックス</param>
        /// <returns階層化文字列</returns>
        private static string GenerateLayeredStringOfElement(int indent, XElement xElm, int xElmIndex = -1)
        {
            var sb = new StringBuilder();


            // 表示用書式の生成
            //   配下のXML属性およびXML要素について最長のローカル名長を取得し
            //   "key=value"のうちkey部分の桁数合わせに使用
            var elementsNoChilds = xElm.Elements().Where(e => !e.HasAttributes && !e.HasElements);
            var maxLocalNameLength = new[] {
                xElm.HasAttributes ? xElm.Attributes().Max(a => a.Name.LocalName.Length) : 0,
                elementsNoChilds.Any() ? elementsNoChilds.Max(e => e.Name.LocalName.Length) : 0,
            }.Max();
            var keyValueFormat = $"{{0,-{maxLocalNameLength}}} = {{1}}";


            // 配下にXML属性あるいはXML要素をもつ場合、自身のローカル名を表示
            if (xElm.HasAttributes || xElm.HasElements)
            {
                // 自身が配列の構成要素の場合、末尾にインデックスを付与
                if (xElmIndex >= 0)
                {
                    sb.AppendFormatLine(indent, "{0}[{1}]", xElm.Name.LocalName, xElmIndex);
                }
                else
                {
                    sb.AppendFormatLine(indent, xElm.Name.LocalName);
                }
            }

            // XML属性の表示
            foreach (var item in xElm.Attributes())
            {
                sb.AppendFormatLine(indent + 2, keyValueFormat, item.Name.LocalName, item.Value);
            }

            // 配下のXML要素が配列か判定
            var areChildArray = (xElm.Elements().Count() > 1 && xElm.Elements().All(e => e.Name == xElm.Elements().First().Name));

            // XML要素の表示
            foreach (var (item, index) in xElm.Elements().Select((item, index) => (item, index)))
            {
                // 配下にXML属性もXML要素も持たない場合、"key=value"形式で表示
                if (!item.HasAttributes && !item.HasElements)
                {
                    sb.AppendFormatLine(indent + 2, keyValueFormat, item.Name.LocalName, item.Value);
                    continue;
                }

                // 配下のXML要素が配列の場合、配列インデックスを渡す
                if (areChildArray)
                {
                    sb.Append(GenerateLayeredStringOfElement(indent + 2, item, index));
                }
                else
                {
                    sb.Append(GenerateLayeredStringOfElement(indent + 2, item));
                }

            }

            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 enum BoxItemType
        {
            Undefined,
            Fruit,
            Fish
        }

        public class BoxItem
        {
            [XmlAttribute]
            public BoxItemType BoxItemType { get; set; }
            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(){ BoxItemType = BoxItemType.Fruit, Name = "Orange", Count = 1 },
            new BoxItem(){ BoxItemType = BoxItemType.Fish, Name = "Tuna", Count = 2 },
            new BoxItem(){ BoxItemType = BoxItemType.Undefined, Name = null, Count = 0 },
        };
    }


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

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

            //
            // 加工して表示
            //
            WriteResult("XML文字列(軽量版)", DumpHelper.GenerateXmlStirngSlim(boxA));
            WriteResult("XML文字列生成(1行版)", DumpHelper.GenerateXmlStringOneLine(boxA, "#"));
            WriteResult("階層化文字列", DumpHelper.GenerateLayeredString(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 BoxItemType="Fruit">
      <Name>Orange</Name>
      <Count>1</Count>
    </BoxItem>
    <BoxItem BoxItemType="Fish">
      <Name>Tuna</Name>
      <Count>2</Count>
    </BoxItem>
    <BoxItem BoxItemType="Undefined">
      <Count>0</Count>
    </BoxItem>
  </Items>
</Box>

--------------------------------
 XML文字列(軽量版)
--------------------------------
<Box Name="box a">
  <Volume Value="10" Unit="L" />
  <Items>
    <BoxItem BoxItemType="Fruit">
      <Name>Orange</Name>
      <Count>1</Count>
    </BoxItem>
    <BoxItem BoxItemType="Fish">
      <Name>Tuna</Name>
      <Count>2</Count>
    </BoxItem>
    <BoxItem BoxItemType="Undefined">
      <Count>0</Count>
    </BoxItem>
  </Items>
</Box>

--------------------------------
 XML文字列生成(1行版)
--------------------------------
<Box Name="box a">#  <Volume Value="10" Unit="L" />#  <Items>#    <BoxItem BoxItemType="Fruit">#      <Name>Orange</Name>#      <Count>1</Count>#    </BoxItem>#    <BoxItem BoxItemType="Fish">#      <Name>Tuna</Name>#      <Count>2</Count>#    </BoxItem>#    <BoxItem BoxItemType="Undefined">#      <Count>0</Count>#    </BoxItem>#  </Items>#</Box>

--------------------------------
 階層化文字列
--------------------------------
Box
  Name = box a
  Volume
    Value = 10
    Unit  = L
  Items
    BoxItem[0]
      BoxItemType = Fruit
      Name        = Orange
      Count       = 1
    BoxItem[1]
      BoxItemType = Fish
      Name        = Tuna
      Count       = 2
    BoxItem[2]
      BoxItemType = Undefined
      Count       = 0

所感

XDocumentを使ってみるついでに見やすい階層化表示なんてやってみたけど、試験不十分なので、下手に表示されないリスク負うくらいなら、素直にXML文字列でもいいような気もする。でも夏休みの暇つぶしにはちょうどよかった。