【C#】XMLの要素名や値を動的に作成する方法をまとめてみた

# 概要
C#でxmlテキストを作成する際に要素とコンテンツを動的に作成する方法を、自身の勉強を兼ねて記載してみました。

# 説明
xmlテキストにシリアライズするための階層構造になっているデータクラス群があるとします。
xmlの第2階層 (ルート要素の一つ下の子要素) 用のクラスは、紐づくクラスのプロパティの値から動的に要素と値を出力し、他のクラスは自身のプロパティをそのままシリアライズする形になります。
※第2階層のみ動的に要素名と値を出力するイメージです。

# コード

前提として、シリアライズ後のxmlのイメージは以下になります。

```xml
<?xml version="1.0" encoding="utf-8"?>
<ROOT xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <HOGE>AAAA</HOGE>
  <FUGA>BBBB</FUGA>
  <PIYO>CCCC</PIYO>
  <CONTENT_LIST>
    <CONTENT>
      <TEXT>1</TEXT>
    </CONTENT>
    <CONTENT>
      <TEXT>2</TEXT>
    </CONTENT>
    <CONTENT>
      <TEXT>3</TEXT>
    </CONTENT>
  </CONTENT_LIST>
</ROOT>
```
第2階層の`<HOGE>`、`<FUGA>`、`<PIYO>`部分を動的に作成しているイメージです。

### 第2階層用のクラス
xml第2階層用のクラスです。
`Layer2Items`というプロパティに、xmlの第2階層の要素名と値のまとまりが入っています。

```C#:Layer2.cs
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml.Serialization;

namespace Sample
{
    [XmlRoot("ROOT")]
    public class Layer2
    {
        [XmlAnyElement]
        public List<Layer2Item> Layer2Items { get; set; }

        [XmlElement("CONTENT_LIST")]
        public ContentList ContentList { get; set; }

        public void Set(List<Layer2Info> layer2Infos)
        {
            // 第2階層を作成する
            var layer2Items = new List<Layer2Item>();
            foreach (var layer2Info in layer2Infos.OrderBy(x => x.Index))
            {
                var item = new Layer2Item();
                item.Set(layer2Info);

                // プロパティに情報をセット
                layer2Items.Add(item);
            }

            Layer2Items = layer2Items;

            SetContentList(new ContentList());
        }

        private void SetContentList(ContentList contentList)
        {
            contentList.Set();
            this.ContentList = contentList;
        }

        public string CreateXmlText()
        {
            // シリアライズ
            string xmlText = Serialize();

            // &のエスケープコードを消去し返却
            return xmlText.Replace("amp;", "");
        }

        private string Serialize()
        {
            var serializer = new XmlSerializer(GetType());
            var writer = new StringWriterUTF8();

            serializer.Serialize(writer, this);

            return writer.ToString();
        }

        private class StringWriterUTF8 : StringWriter
        {
            public override Encoding Encoding
            {
                get
                {
                    return Encoding.UTF8;
                }
            }
        }
    }
}
```
#### 説明
##### XmlAnyElement属性
予め定義していないXML要素を動的に処理するために使用するC#の属性です。
ここではxmlの第2階層の要素名や値を保持している`Layer2Items`プロパティに属性を付与しています。

### Layer2クラスのプロパティに出力する情報クラス

`Layer2`クラスの`Layer2Items`プロパティの型となっているクラスです。
このクラス一つ分がxmlの第2階層の要素一つ分です。
`Name`プロパティが要素名、`Value`プロパティが要素の値です。

```C#:Layer2Item.cs
using System;
using System.Linq;
using System.Xml;
using System.Xml.Schema;
using System.Xml.Serialization;

namespace Sample
{
    public class Layer2Item : IXmlSerializable
    {
        /// <summary>
        /// 要素名
        /// </summary>
        public string Name { get; set; }

        /// <summary>
        /// 値
        /// </summary>
        public string Value { get; set; }

        /// <summary>
        ///  1要素分のデータを取得する
        /// </summary>
        /// <returns>1要素分のデータ</returns>
        public void Set(Layer2Info layer2Info)
        {
            Name = layer2Info.Name;
            Value = layer2Info.Value;
        }

        public XmlSchema GetSchema()
        {
            throw new NotImplementedException();
        }

        public void ReadXml(XmlReader reader)
        {
            throw new NotImplementedException();
        }

        public void WriteXml(XmlWriter writer)
        {
            writer.WriteElementString(Name, Value);
        }
    }
}
```
### 説明
#### IXmlSerializableインターフェース
オブジェクトをXMLに変換する方法を完全に制御できるようにするインターフェースです。
`IXmlSerializable`の`WriteXml()`はオブジェクトの状態をxml形式で出力するためのメソッドです。このメソッドの中で`XmlWriter`クラスの`WriteElementString()`を使用し、要素名と値を指定してxml要素を生成します。

### Layer2クラスのプロパティに出力する情報クラス一つ分
上記の`Layer2Item`はこのデータクラスの値を使用しています。
このデータクラスには、ユーザー操作等でxml出力するための要素名や値があらかじめ格納されているイメージです。
`Name`プロパティが要素名、`Value`プロパティが要素の値です。
`Index`プロパティを使用して連番とすることで、このデータクラス群 (`Layer2Infos`) のどの順番でxml出力するかも操作できます。

```C#:Layer2Info.cs
    public class Layer2Info
    {
        public int Index { get; set; }

        public string Name { get; set; }

        public string Value { get; set; }
    }
```

### `CONTENT_LIST`用のクラス

xmlの`CONTENT_LIST`に紐づくクラスです。
`contents`プロパティに`CONTENT`用のクラス群を保持しています。

```C#:ContentList.cs
using System.Collections.Generic;
using System.Xml.Serialization;

namespace Sample
{
    public class ContentList
    {
        [XmlElement("CONTENT")]
        public List<Content> contents { get; set; }

        public void Set()
        {
            var contents = new List<Content>();

            // ここではnewしているが、ここは本来はxml出力する何かしらの値を取得する
            var texts = new List<string>();

            foreach (string text in texts)
            {
                var content = new Content();
                content.Set(text);

                contents.Add(content);
            }

            this.contents = contents;
        }
    }
}

```

### `CONTENT`用のクラス

xmlの`CONTENT`に紐づくクラスです。
`Text`プロパティが`CONTENT`の子要素として出力する値です。

```C#:Content.cs
using System.Xml.Serialization;

namespace Sample
{
    public class Content
    {
        [XmlElement("TEXT")]
        public string Text { get; set; }

        public void Set(string text)
        {
            Text = text;
        }
    }
}
```

### xmlデータの出力
xmlデータの出力処理です。
先に出力するデータをセットしておき、その後に`Layer2`クラスの`CreateXmlText()`を呼出してシリアライズされたxmlのテキストを取得し、その後に任意の場所に出力しています。

```C#
        // 出力するデータをセット
        List<Layer2Info> layer2Infos = (第2階層に出力するLayer2Infoクラスのリスト);
        var layer2 = new Layer2();
        layer2.Set(layer2Infos);

        // XMLテキストを作成
        string xmlText = layer2.CreateXmlText();

        // XMLを出力
        using (var sw = new StreamWriter("出力先パス", false, new System.Text.UTF8Encoding(false)))
        {
            sw.Write(xmlText);
        }

```

### シリアライズ後のxmlデータ
最初にも記載しましたが、以下がシリアライズ後のxmlデータです。
`<HOGE>`、`<FUGA>`、`<PIYO>`要素の要素名と値が、それぞれ`Layer2Item`クラスの`Name`プロパティや`Value`プロパティに入っていた形です。
また、`ContentList`クラスと`Content`クラスはプロパティの値をそのまま出力できています。
動的に要素名を生成する`Layer2`クラスと、属性名と値を指定してそのままシリアライズしている`ContentList`クラスと`Content`クラスのように、組み合わせることもできます。

```xml
<?xml version="1.0" encoding="utf-8"?>
<ROOT xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <HOGE>AAAA</HOGE>
  <FUGA>BBBB</FUGA>
  <PIYO>CCCC</PIYO>
  <CONTENT_LIST>
    <CONTENT>
      <TEXT>1</TEXT>
    </CONTENT>
    <CONTENT>
      <TEXT>2</TEXT>
    </CONTENT>
    <CONTENT>
      <TEXT>3</TEXT>
    </CONTENT>
  </CONTENT_LIST>
</ROOT>
```

# 終わりに
`XmlAnyElement`属性や`IXmlSerializable`インターフェースを使用すると、柔軟にxmlを作成できることがわかりました。

# 参考
- [XmlSerializerを使ってインスタンスをXML形式でシリアライズする](https://www.pine4.net/Memo/Article/Archives/60)