PropertyGrid で TypeConverter と PropertyDescriptor の理解を深める

# はじめに

[System.Xml.Linq.**XDocument**](https://docs.microsoft.com/ja-jp/dotnet/api/system.xml.linq.xdocument) クラスの内容をプロパティグリッドに表示する。

### やりたいこと
![xml.png](https://qiita-image-store.s3.amazonaws.com/0/298163/6f16cd7b-768e-4bb2-3f84-508d39decea1.png)
　↑ この XML を ↓ このように表示
![pg.png](https://qiita-image-store.s3.amazonaws.com/0/298163/a9aa7d17-fa1c-0994-15ee-ecaf9d295ecd.png)

XML の階層構造をプロパティグリッドにそのまま表示するように、カスタム型コンバーターを作成してみる。

## PropertyGrid について

[System.Windows.Forms.**PropertyGrid**](https://docs.microsoft.com/ja-jp/dotnet/api/system.windows.forms.propertygrid) コントロールを使うとクラスのインスタンスを簡単に表示・編集できる。

### 属性一覧
表示内容は属性指定でカスタマイズ可能で、主な属性は下記

- [CategoryAttribute](https://docs.microsoft.com/ja-jp/dotnet/api/system.componentmodel.categoryattribute)
- [DisplayNameAttribute](https://docs.microsoft.com/ja-jp/dotnet/api/system.componentmodel.displaynameattribute)
- [DescriptionAttribute](https://docs.microsoft.com/ja-jp/dotnet/api/system.componentmodel.descriptionattribute)
- [BrowsableAttribute](https://docs.microsoft.com/ja-jp/dotnet/api/system.componentmodel.browsableattribute)
- [DefaultValueAttribute](https://docs.microsoft.com/ja-jp/dotnet/api/system.componentmodel.defaultvalueattribute)
- [ReadOnlyAttribute](https://docs.microsoft.com/ja-jp/dotnet/api/system.componentmodel.readonlyattribute)

⇒ 型コンバーターを自作することでプロパティグリッドに表示する内容をカスタマイズできる。

## XML について

XML は [System.Xml.Linq.**XDocument**](https://docs.microsoft.com/ja-jp/dotnet/api/system.xml.linq.xdocument) クラスで扱うこととする。

```:System.Xml.Linq
XObject
 ┣ XDeclaration 
 ┣ XNode
 ┃  ┣ XProcessingInstruction
 ┃  ┣ XDocumentType
 ┃  ┣ XContainer
 ┃  ┃  ┣ XDocument
 ┃  ┃  ┗ XElement : 要素
 ┃  ┣ XText
 ┃  ┃  ┗ XCData
 ┃  ┗ XComment
 ┗ XAttribute : 属性
```

### [code-0] 準備

XML 要素と属性の名前などを取得する用に、XObject の拡張メソッドを用意しておく。

```C#:XObjectExtensions.cs
static class XObjectExtensions
{
	public static bool HasElements(this XObject obj) => (obj as XElement)?.HasElements ?? false;

	public static string GetName(this XObject obj)
	{
		switch (obj.NodeType)
		{
			case XmlNodeType.Element: return (obj as XElement).Name.LocalName;
			case XmlNodeType.Attribute: return "@" + (obj as XAttribute).Name.LocalName;
			default: throw new NotSupportedException();
		}
	}

	public static string GetValue(this XObject obj)
	{
		switch (obj.NodeType)
		{
			case XmlNodeType.Element: return (obj as XElement).Value;
			case XmlNodeType.Attribute: return (obj as XAttribute).Value;
			default: throw new NotSupportedException();
		}
	}

	public static void SetValue(this XObject obj, string value)
	{
		switch (obj.NodeType)
		{
			case XmlNodeType.Element: (obj as XElement).Value = value; break;
			case XmlNodeType.Attribute: (obj as XAttribute).Value = value; break;
			default: throw new NotSupportedException();
		}
	}
}
```

## 型コンバーターを作成

[System.ComponentModel.**TypeConverter**](https://docs.microsoft.com/ja-jp/dotnet/api/system.componentmodel.typeconverter) から派生したクラスを作成することで、プロパティグリッドに表示する項目を自由に指定できる。

### [code-1] XmlConverter クラス

TypeConverter クラスを継承して XmlConverter クラスを作成する。

```C#:XmlConverter.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Xml;
using System.Xml.Linq;

class XmlConverter : TypeConverter
{
	protected readonly Type type;
	public XmlConverter(Type type)
	{
		this.type = type;
	}

	//todo ここへ後述のコードを追記します
}
```

### [code-2] XmlPropertyDescriptor クラス

SimplePropertyDescriptor クラスを継承して内部クラスを作成する。
このクラスが、プロパティグリッドの１項目に相当

```C#:XmlConverter.cs
	class XmlPropertyDescriptor : SimplePropertyDescriptor
	{
		protected readonly XObject obj;

		public XmlPropertyDescriptor(Type componentType, XObject obj, params Attribute[] attributes) : base(componentType, obj.GetName(), obj.GetType(), attributes)
		{
			this.obj = obj;
			this.Converter = this.HasElements ? this.CreateConverter(componentType) : new StringConverter();
		}

		private bool HasElements => this.obj.HasElements();

		public override TypeConverter Converter { get; }

		protected virtual TypeConverter CreateConverter(Type componentType) => new XmlConverter(componentType);

		public override object GetValue(object component)
		{
			return this.HasElements ? this.obj : (object)this.obj.GetValue();
		}

		public override void SetValue(object component, object value)
		{
			if (!this.HasElements) this.obj.SetValue((string)value);
		}
	}
```

### [code-3] CreateDescriptors メソッド

XmlPropertyDescriptor のインスタンスを作成するメソッドを実装する。

```C#:XmlConverter.cs
	private IEnumerable<PropertyDescriptor> CreateDescriptors(XElement element)
	{
		foreach (var attribute in element.Attributes())
		{
			yield return this.CreateDescriptor(attribute); // 属性を列挙
		}
		foreach (var child in element.Elements())
		{
			yield return this.CreateDescriptor(child); // 子要素を列挙
		}
	}

	private PropertyDescriptor CreateDescriptor(XObject obj)
	{
		var category = obj.Parent.Name.LocalName;
		return new XmlPropertyDescriptor(this.type, obj, new CategoryAttribute(category));
	}
```

### [code-4] プロパティ列挙

プロパティグリッドへの表示項目を列挙するメソッドをオーバーライドする。

```C#:XmlConverter.cs
	public override bool GetPropertiesSupported(ITypeDescriptorContext context) => true;
	public override PropertyDescriptorCollection GetProperties(ITypeDescriptorContext context, object value, Attribute[] attributes)
	{
		var element = value as XElement;
		if (element != null) return new PropertyDescriptorCollection(this.CreateDescriptors(element).ToArray());
		return base.GetProperties(context, value, attributes);
	}
```

## 型コンバーターを紐付

[TypeConverterAttribute](https://docs.microsoft.com/ja-jp/dotnet/api/system.componentmodel.typeconverterattribute) 属性で、使用するコンバーターを指定する。

```C#
using System.ComponentModel;
using System.Xml.Linq;

[TypeConverter(typeof(XmlConverter))]
class CustomXDocument : XDocument
{
}
```