読者です 読者をやめる 読者になる 読者になる
ようこそ。睡眠不足なプログラマのチラ裏です。

ExpandableObjectConverterっていちいち実直に実装するのは面倒だよね。Reflectionを活用してネストしたプロパティを楽に量産してみよう。

プログラミング C#2.0 C#3.0 デザインパターン オブジェクト指向

ExpandableObjectConverterのサブクラスで実装する内容

TypeConverterAttribute属性を用いて、ExpandableObjectConverterおよび、その派生クラスを指定することで
プロパティグリッドにネスト表示されるプロパティを作成することができます。


通常、ExpandableObjectConverterから派生したサブクラスを作る場合は、
GetProperties、CanConvertFrom、CanConvertTo、ConvertFrom、ConvertTo、GetPropertiesSupported
などの各メソッドを必要に応じてオーバーライドして実装することになります。
そこで実装する内容は、プロパティの型やその項目数に応じて、
プロパティグリッドに表示する内容と、対象となる型との相互(または単方向)コンバートとなります。


ExpandableObjectConverterのサブクラスの実装を抽象的に考える

複数のExpandableObjectConverterのサブクラスを実装した経験のある方はご存知のとおり、
このサブクラスの実装でやるべきことはだいたい決まっていて、大抵似たり寄ったりな内容となりがちです。
単純に何も考えずに実装すると、型や項目数が異なるというだけで、多くのExpandableObjectConverterのサブクラスに
似たような実装を施さなくてはなりません。単純作業になりがちです。これはかなり面倒臭いです。
某グレープシティのInputManなどのコントロールのように、鬼のようにネストしたプロパティを持つ
RADなコントロールやコンポーネントを設計した場合、実直に真面目にコツコツと作ることを考えると、
それはそれは途方もない作業になることは、想像に難しくないでしょう。気が遠くなります。


そこで、ExpandableObjectConverterのサブクラスでオーバーライドして実装される、よくある実装について抽象的に考えて、
ジェネリックとリフレクションを用いた汎用的な実装を検討してみるとよさそうです。
今回の見所は、それをC#で実装したAbstractExpandableObjectConverterImplementorクラスです。ではどうぞ。



ExpandableObjectConverterの機能と実装を分離してみよう
機能と実装を分離するとは、一体どういうことでしょう。
一言で言うと、GoFデザインパターンで言うところのBridgeパターンの応用または変形と言えます。
これはいわゆるオブジェクト指向設計の基本原則である「単一責任原則 (SRP)」に基づいて、
クラスの責任を適切に分離するという考え方が基本となっています。


オブジェクト指向においては、クラスあるいはインタフェースから複数の派生が存在するような場合、
これらの派生クラスは複数の並行状態(あるいは相互に排他的な複数の状態)であり、
概念あるいは継承関係が混在(交差)している可能性があると言えます。
また、これらは常に変化する可能性も秘めています。このような場合、機能と実装を分離することを検討してみます。
機能と実装を分離することで、それぞれ独立して拡張ができるので、拡張の幅が増えた設計が期待できるからです。



まずは、Abstractな機能のクラス階層の基底クラスを作成します。
といきたいところですが、今回のケースではExpandableObjectConverterのサブクラスとしてしか機能しない
単純なもの用意すれば十分ですので、機能クラス階層の拡張は考えなくてもよいものとして、
sealedなConcreteクラスをいきなり作ってしまうことにします。(あえて機能の拡張を想定しないことにしました)
IExpandableObjectConverterImplementorインターフェイスを解して、処理を委譲しています。
ExpandableObjectConverterImplementorFactoryは、いわゆるFlyweightパターンを用いたファクトリクラスです。
ファクトリクラスは、処理を委譲される実装クラスごとにFlyweightなインスタンス生成を提供します。

using System;
using System.ComponentModel;

namespace ClassLibrary1
{
    public sealed class ExpandableObjectConverterBridge<T>
        : ExpandableObjectConverter 
        where T : AbstractExpandableObjectConverterImplementor, new()
    {
        private static IExpandableObjectConverterImplementor ipdg = ExpandableObjectConverterImplementorFactory.Instance.Factory<T>();

        public override PropertyDescriptorCollection GetProperties(ITypeDescriptorContext context, Object value, Attribute[] attributes)
        {
            if (context != null && value != null)
                return ipdg.GetProperties(context, value, attributes);
            return base.GetProperties(context, value, attributes);
        }

        public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
        {
            if (context != null)
                return ipdg.CanConvertFrom(context, sourceType);
            return base.CanConvertFrom(context, sourceType);
        }

        public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
        {
            if (context != null)
                return ipdg.CanConvertTo(context, destinationType);
            return base.CanConvertTo(context, destinationType);
        }

        public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value)
        {
            if (context != null && value != null)
                return ipdg.ConvertFrom(context, culture, value);
            return base.ConvertFrom(context, culture, value);
        }

        public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, Type destinationType)
        {
            if (context != null && value != null)
                return ipdg.ConvertTo(context, culture, value, destinationType);
            return base.ConvertTo(context, culture, value, destinationType);
        }

        public override object CreateInstance(ITypeDescriptorContext context, System.Collections.IDictionary propertyValues)
        {
            if (context != null)
                return ipdg.CreateInstance(context, propertyValues);
            return base.CreateInstance(context, propertyValues);
        }

        public override bool GetCreateInstanceSupported(ITypeDescriptorContext context)
        {
            if (context != null)
                return ipdg.GetCreateInstanceSupported(context);
            return base.GetCreateInstanceSupported(context);
        }

        public override TypeConverter.StandardValuesCollection GetStandardValues(ITypeDescriptorContext context)
        {
            if (context != null)
                return ipdg.GetStandardValues(context);
            return base.GetStandardValues(context);
        }

        public override bool GetStandardValuesExclusive(ITypeDescriptorContext context)
        {
            if (context != null)
                return ipdg.GetStandardValuesExclusive(context);
            return base.GetStandardValuesExclusive(context);
        }

        public override bool GetStandardValuesSupported(ITypeDescriptorContext context)
        {
            if (context != null)
                return ipdg.GetStandardValuesSupported(context);
            return base.GetStandardValuesSupported(context);
        }

        public override bool IsValid(ITypeDescriptorContext context, object value)
        {
            if (context != null && value != null)
                return ipdg.IsValid(context, value);
            return base.IsValid(context, value);
        }

        public override bool GetPropertiesSupported(ITypeDescriptorContext context)
        {
            return true;
        }
    }
}

IExpandableObjectConverterImplementorインターフェイス
ExpandableObjectConverterの機能から委譲される処理が定義されています。

using System;
using System.ComponentModel;

namespace ClassLibrary1
{
    /// <summary>
    /// ExpandableObjectConverter処理委譲インターフェイス
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public interface IExpandableObjectConverterImplementor
    {
        PropertyDescriptorCollection GetProperties(ITypeDescriptorContext context, Object value, Attribute[] attributes);
        bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType);
        bool CanConvertTo(ITypeDescriptorContext context, Type destinationType);
        object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value);
        object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, Type destinationType);
        object CreateInstance(ITypeDescriptorContext context, System.Collections.IDictionary propertyValues);
        bool GetCreateInstanceSupported(ITypeDescriptorContext context);
        TypeConverter.StandardValuesCollection GetStandardValues(ITypeDescriptorContext context);
        bool GetStandardValuesExclusive(ITypeDescriptorContext context);
        bool GetStandardValuesSupported(ITypeDescriptorContext context);
        bool IsValid(ITypeDescriptorContext context, object value);
    }
}


上記で少し触れたExpandableObjectConverterImplementorFactoryの実装。
AbstractExpandableObjectConverterImplementor型についてのFlyweightを提供するファクトリクラスです。
このファクトリクラスはもちろんSingletonであるとよいでしょう。

using System;
using System.Collections.Generic;

namespace ClassLibrary1
{
    public sealed class ExpandableObjectConverterImplementorFactory
    {
        private static volatile ExpandableObjectConverterImplementorFactory _instance;
        private static object syncRoot = new Object();
        private static Dictionary<Type, AbstractExpandableObjectConverterImplementor> _dicInstance = new Dictionary<Type, AbstractExpandableObjectConverterImplementor>();

        private ExpandableObjectConverterImplementorFactory() {}

        public static ExpandableObjectConverterImplementorFactory Instance
        {
            get 
            {
                if (_instance != null) return _instance;
                lock (syncRoot) 
                {
                   if (_instance == null)
                       _instance = new ExpandableObjectConverterImplementorFactory();
                }
                return _instance;
            }
        }

        #region メソッド
        /// <summary>
        /// 指定の型インスタンスを取得します。
        /// </summary>
        /// <typeparam name="T">
        /// 指定の型 T :制約 AbstractExpandableObjectConverterImplementorのサブクラス
        /// </typeparam>
        /// <returns>指定した型のインスタンス</returns>
        public T Factory<T>() where T : AbstractExpandableObjectConverterImplementor, new()
        {
            lock (syncRoot)
            {
                if (!_dicInstance.ContainsKey(typeof(T)))
                {
                    T instance = (T)Activator.CreateInstance(typeof(T), true);
                    _dicInstance.Add(typeof(T), instance);
                }
                return (T)_dicInstance[typeof(T)];
            }
        }
        #endregion  
    }
}

Reflectionを用いて、ExpandableObjectConverterImplementorの一般的な実装を提供する

Abstractな実装クラス階層の基本クラス、AbstractExpandableObjectConverterImplementorを作成します。
AbstractExpandableObjectConverterImplementorでは、前述したIExpandableObjectConverterImplementorを実装します。
このクラスが今回のエントリーのメインディッシュとなります。リファクタリング不足ですが、参考にしてみてください。
以下にサンプルコードを提示することで、詳しい説明は省略します。

using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.Design.Serialization;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Windows.Forms;

namespace ClassLibrary1
{
    public abstract class AbstractExpandableObjectConverterImplementor
        : IExpandableObjectConverterImplementor
    {
        private static TypeConverter _tc = new TypeConverter();
        private static Dictionary<Type, ConstructorInfo> _dicTypeConstructorInfo = new Dictionary<Type, ConstructorInfo>();

        public virtual PropertyDescriptorCollection GetProperties(ITypeDescriptorContext context, Object value, Attribute[] attributes)
        {
            return TypeDescriptor.GetProperties(value);
        }

        public virtual bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
        {
            if (sourceType == typeof(string)) return true;
            return _tc.CanConvertFrom(context, sourceType);
        }

        public virtual bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
        {
            if (destinationType == typeof(string)) return true;
            if (destinationType == typeof(InstanceDescriptor)) return true;
            return _tc.CanConvertTo(context, destinationType);
        }

        public virtual object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
        {
            Type t = context.PropertyDescriptor.PropertyType;
            try
            {
                if (value is string)
                {
                    List<string> v = new List<string>();
                    Regex reg = new Regex("^(?:\\[(?<value>.*)\\],?|(?<value>[^,]+),?|,)*");

                    GroupCollection gc = reg.Match((string)value).Groups;
                    foreach (Capture c in gc["value"].Captures)
                        v.Add(c.Value);

                    PropertyInfo[] pInfoCollection =
                        t.GetProperties(BindingFlags.Public
                                      | BindingFlags.Instance);

                    List<PropertyInfo> pInfoList = pInfoCollection.ToList();
                    pInfoList.Sort(new PropertySortComparer());

                    List<Type> pType = new List<Type>();
                    List<object> pValue = new List<object>();

                    foreach (var p in pInfoList.Select((x, i) => new { Value = x, Index = i }))
                    {
                        pType.Add(p.Value.PropertyType);

                        object parseObject = v[p.Index];
                        MethodInfo mi = p.Value.PropertyType.GetMethod("Parse"
                            , BindingFlags.Public | BindingFlags.Static | BindingFlags.InvokeMethod
                            , null
                            , new Type[] { typeof(string) }
                            , null);

                        if (mi == null) continue;
                        parseObject = mi.Invoke(p.Value.PropertyType
                                              , BindingFlags.Public | BindingFlags.Static | BindingFlags.InvokeMethod
                                              , null
                                              , new object[] { v[p.Index] }
                                              , culture);

                        pValue.Add(parseObject);
                    }

                    if (_dicTypeConstructorInfo.ContainsKey(t))
                        return new InstanceDescriptor(_dicTypeConstructorInfo[t], pValue.ToArray()).Invoke();

                    System.Reflection.ConstructorInfo ctor = t.GetConstructor(pType.ToArray());
                    if (ctor != null)
                    {
                        _dicTypeConstructorInfo.Add(t, ctor);
                        return new InstanceDescriptor(ctor, pValue.ToArray()).Invoke();
                    }
                    throw new ArgumentException("Can not convert '" + (string)value + "' to type " + t.Name);
                }

            }
            catch (Exception ex)
            {
                throw new ArgumentException("Can not convert '" + (string)value + "' to type " + t.Name);
            }
            return _tc.ConvertFrom(context, culture, value);
        }

        public virtual object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destType)
        {
            if (destType == typeof(string))
            {
                List<string> pValue = new List<string>();
                StringBuilder sb = new StringBuilder();

                PropertyDescriptorCollection pdc = TypeDescriptor.GetProperties(value);
                pdc = SortPropertyDescriptor(pdc, value);

                foreach (PropertyDescriptor pd in pdc)
                {
                    FlagNumberBase obj = pd.GetValue(value) as FlagNumberBase;
                    if (obj != null)
                    {
                        sb.AppendFormat("{0},", ((FlagNumberBase)pd.GetValue(value)).ToString());
                        continue;
                    }
                    sb.AppendFormat("{0},", pd.GetValue(value).ToString());
                }
                return Regex.Replace(sb.ToString(), ",$", "");
            }

            if (destType == typeof(InstanceDescriptor))
            {
                List<Type> pType = new List<Type>();

                PropertyDescriptorCollection pdc = TypeDescriptor.GetProperties(value);
                pdc = SortPropertyDescriptor(pdc, value);

                foreach (PropertyDescriptor p in pdc)
                    pType.Add(p.PropertyType);

                List<object> pValue = new List<object>();
                foreach (PropertyDescriptor p in pdc)
                    pValue.Add(p.GetValue(value));

                Type t = value.GetType();

                if (_dicTypeConstructorInfo.ContainsKey(t))
                    return new InstanceDescriptor(_dicTypeConstructorInfo[t], pValue.ToArray());

                System.Reflection.ConstructorInfo ctor = t.GetConstructor(pType.ToArray());
                if (ctor != null)
                {
                    _dicTypeConstructorInfo.Add(t, ctor);
                    return new InstanceDescriptor(ctor, pValue.ToArray());
                }
            }
            return _tc.ConvertTo(context, culture, value, destType);
        }

        public virtual object CreateInstance(ITypeDescriptorContext context, IDictionary propertyValues)
        {
            return _tc.CreateInstance(context, propertyValues);
        }

        public virtual bool GetCreateInstanceSupported(ITypeDescriptorContext context)
        {
            return _tc.GetCreateInstanceSupported(context);
        }

        public virtual TypeConverter.StandardValuesCollection GetStandardValues(ITypeDescriptorContext context)
        {
            return _tc.GetStandardValues(context);
        }

        public virtual bool GetStandardValuesExclusive(ITypeDescriptorContext context)
        {
            return _tc.GetStandardValuesExclusive(context);
        }

        public virtual bool GetStandardValuesSupported(ITypeDescriptorContext context)
        {
            return _tc.GetStandardValuesSupported(context);
        }

        public virtual bool IsValid(ITypeDescriptorContext context, object value)
        {
            return _tc.IsValid(context, value);
        }

        public PropertyDescriptorCollection SortPropertyDescriptor(PropertyDescriptorCollection pdc, object value)
        {
            PropertyDescriptorCollection result = pdc;
            PropertyInfo sdpInfo = value.GetType().GetProperty("PropertySortDefinition"
                                                             , BindingFlags.NonPublic
                                                             | BindingFlags.Instance);

            if (sdpInfo == null || sdpInfo.PropertyType != typeof(string[])) return result;
            string[] sortDefinition = (string[])sdpInfo.GetValue(value, null);
            if (sortDefinition == null) return result;
            return result.Sort(sortDefinition);
        }
    }
}


Propertyをソートする順序をメタデータに定義しておくための属性

using System;

namespace ClassLibrary1
{
    [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
    public sealed class PropertySortIndexAttribute : Attribute
    {
        private int _index = default(int);
        public PropertySortIndexAttribute(int index)
        {
            _index = index;
        }
        public int Index
        {
            get { return _index; }
        }
    }
}

PropertyInfoについて、PropertySortIndexAttributeの値に応じて
ソートするためのヘルパークラス。

using System.Collections;
using System.Collections.Generic;
using System.Reflection;

namespace ClassLibrary1
{
    public class PropertySortComparer : IComparer, IComparer<PropertyInfo>
    {
        #region IComparer メンバ

        public int Compare(object x, object y)
        {
            return Compare((PropertyInfo)x, (PropertyInfo)y);
        }

        #endregion

        #region IComparer<PropertyDescriptor> メンバ

        public int Compare(PropertyInfo x, PropertyInfo y)
        {
            PropertySortIndexAttribute xpropAttr = null;
            PropertySortIndexAttribute ypropAttr = null;

            PropertySortIndexAttribute[] xpropAttributes = ((PropertySortIndexAttribute[])x.GetCustomAttributes(typeof(PropertySortIndexAttribute), false));
            if (xpropAttributes.Length > 0)
                xpropAttr = xpropAttributes[0];

            PropertySortIndexAttribute[] ypropAttributes = ((PropertySortIndexAttribute[])y.GetCustomAttributes(typeof(PropertySortIndexAttribute), false));
            if (ypropAttributes.Length > 0)
                ypropAttr = ypropAttributes[0];

            if (xpropAttr == null && ypropAttr == null) return 0;
            if (ypropAttr == null) return 1;
            if (xpropAttr == null) return -1;

            if (xpropAttr.Index > ypropAttr.Index) return 1;
            if (xpropAttr.Index < ypropAttr.Index) return -1;
            return 0;
        }

        #endregion
    }
}

以上で基本的な下ごしらえはおしまいです。


2階層ネストしたプロパティの実装例

FlagプロパティとNumberプロパティを持つ、ネストしたプロパティのベースを作成します。
PropertySortDefinitionは、PropertyDescriptorCollectionのソートに用いる定義を返します。
Parse(static)メソッドは、文字列からのパースを提供します。
これはAbstractExpandableObjectConverterImplementorの仕様上、必須のメソッドとなります。
TypeConverterAttribute属性について、今回作成したExpandableObjectConverterBridgeを適用しています。

using System;
using System.ComponentModel;

namespace ClassLibrary1
{
    [Serializable()]
    [Category("拡張プロパティ")]
    [TypeConverter(typeof(ExpandableObjectConverterBridge<FlagNumberExpandableObjectConverterImplementor>))]
    public class FlagNumberBase
    {
        private bool _flag = true;
        private int _number = 5;

        public FlagNumberBase() { }
        public FlagNumberBase(bool flag, int number)
        {
            _flag = flag;
            _number = number;
        }

        [PropertySortIndex(0)]
        [Description("Numberプロパティの使用可否を取得または設定します。")]
        [RefreshProperties(RefreshProperties.All)]
        [DefaultValue(true)]
        public virtual bool Flag
        {
            get { return _flag; }
            set { _flag = value; }
        }

        [PropertySortIndex(1)]
        [Description("番号を取得または設定します。")]
        [RefreshProperties(RefreshProperties.All)]
        [DefaultValue(5)]
        public virtual int Number
        {
            get { return _number; }
            set
            {
                if (!Flag) return;
                _number = value;
            }
        }

        protected virtual string[] PropertySortDefinition
        {
            get { return new string[] { "Flag", "Number" }; }
        }

        public static FlagNumberBase Parse(string value)
        {
            string[] v = value.Split(new char[] { ',' });
            return new FlagNumberBase(bool.Parse(v[0]), int.Parse(v[1]));
        }

        public override string ToString()
        {
            return "[" + this.Flag.ToString() + "," + this.Number.ToString() + "]";
        }
    }
}

ExpandableObjectConverterBridgeに処理を委譲される
FlagNumberExpandableObjectConverterImplementorクラス。
ここでは、Flagプロパティがfalseのとき、Numberプロパティに
ReadOnlyAttribute属性を適用することで、プロパティグリッド上で編集不可となるように実装しています。
ConvertFromメソッドやConvertToメソッドをオーバーライドしていないことに注目してください。
それらのメソッドで行うべき処理については、すべて抽象クラスに任せています。


using System;
using System.ComponentModel;
using System.Collections.Generic;
using System.Reflection;

namespace ClassLibrary1
{
    public sealed class FlagNumberExpandableObjectConverterImplementor
        : AbstractExpandableObjectConverterImplementor
    {
        public override PropertyDescriptorCollection GetProperties(ITypeDescriptorContext context, Object value, Attribute[] attributes)
        {
            PropertyDescriptorCollection pdc = TypeDescriptor.GetProperties(value);

            List<PropertyDescriptor> pdList = new List<PropertyDescriptor>();
            foreach (PropertyDescriptor pd in pdc)
            {

                if (pd.Name == "Number")
                {
                    PropertyInfo pInfo = value.GetType().GetProperty("Flag");
                    if (pInfo == null || pInfo.PropertyType != typeof(bool)) continue;
                    if (!(bool)pInfo.GetValue(value, null))
                    {
                        List<Attribute> attrList = new List<Attribute>();
                        foreach (Attribute attr in pd.Attributes)
                            attrList.Add(attr);

                        attrList.Add(new ReadOnlyAttribute(true));
                        pdList.Add(TypeDescriptor.CreateProperty(value.GetType(), pd, attrList.ToArray()));
                        continue;
                    }
                }
                pdList.Add(pd);
            }
            return SortPropertyDescriptor(new PropertyDescriptorCollection(pdList.ToArray()), value);
        }
    }
}


2重にネストした入れ子なプロパティを表現する、拡張したFlagNumber

using System;
using System.ComponentModel;

namespace ClassLibrary1
{
    [Serializable()]
    [Category("拡張プロパティ")]
    [Description("FlagでNumberの使用可否を制御しているサンプルです。")]
    [TypeConverter(typeof(ExpandableObjectConverterBridge<FlagNumberExpandableObjectConverterImplementor>))]
    public class FlagNumberExtend : FlagNumberBase
    {
        private FlagNumberBase _FlagNumberBase = new FlagNumberBase();

        public FlagNumberExtend() : base() { }
        public FlagNumberExtend(bool flag, int number, FlagNumberBase flgnumberBase)
            : base(flag, number)
        {
            _FlagNumberBase = flgnumberBase;
        }

        [PropertySortIndex(2)]
        [DisplayName("FlagNumnerChild")]
        [Description("入れ子のサンプル")]
        [RefreshProperties(RefreshProperties.All)]
        public FlagNumberBase FlagNumber
        {
            get { return _FlagNumberBase; }
            set { _FlagNumberBase = value; }
        }

        [EditorBrowsable(EditorBrowsableState.Never)]
        public bool ShouldSerializeFlagNumber()
        {
            if (!_FlagNumberBase.Flag) return true;
            if (_FlagNumberBase.Number != 5) return true;
            return false;
        }

        [EditorBrowsable(EditorBrowsableState.Never)]
        public void ResetFlagNumber()
        {
            _FlagNumberBase = new FlagNumberBase(true, 5);
        }

        public new static FlagNumberExtend Parse(string value)
        {
            string[] v = value.Split(new char[] { ',' });
            return new FlagNumberExtend(bool.Parse(v[0]), int.Parse(v[1])
                   , new FlagNumberBase(bool.Parse(v[2]), int.Parse(v[3])));
        }

        public override string ToString()
        {
            return this.Flag.ToString() + "," + this.Number.ToString();
        }
    }
}


TextBoxExにネストプロパティを適用する
ShouldSerializeプロパティ名のメソッドにより、DefaultValue値の定義を実装しています。
Resetプロパティ名のメソッドにより、プロパティグリッド上でのリセット処理を実装しています。


参考:ShouldSerialize メソッドと Reset メソッドによる既定値の定義

using System.ComponentModel;
using System.Windows.Forms;

namespace ClassLibrary1
{
    public sealed class TextBoxEx
        : TextBox
    {
        private FlagNumberExtend _FlagNumber = new FlagNumberExtend();

        [DisplayName("FlagNumnerParent")]
        [RefreshProperties(RefreshProperties.All)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
        public FlagNumberExtend FlagNumber
        {
            get { return _FlagNumber; }
            set { _FlagNumber = value; }
        }

        [EditorBrowsable(EditorBrowsableState.Never)]
        public bool ShouldSerializeFlagNumber()
        {
            if (!_FlagNumber.Flag) return true;
            if (_FlagNumber.Number != 5) return true;
            if (!_FlagNumber.FlagNumber.Flag) return true;
            if (_FlagNumber.FlagNumber.Number != 5) return true;
            return false;
        }

        [EditorBrowsable(EditorBrowsableState.Never)]
        public void ResetFlagNumber()
        {
            _FlagNumber = new FlagNumberExtend(true, 5, new FlagNumberBase(true, 5));
        }
    }
}


他の実装サンプル(おまけ)

using System;
using System.ComponentModel;

namespace ClassLibrary1
{
    [Serializable()]
    [Category("拡張プロパティ")]
    [TypeConverter(typeof(ExpandableObjectConverterBridge<DateTimeFromToExpandableObjectConverterImplementor>))]
    public class DateTimeFromTo
    {
        private DateTime _from;
        private DateTime _to;

        protected DateTimeFromTo() { }
        public DateTimeFromTo(DateTime from, DateTime to)
        {
            _from = from;
            _to = to;
        }

        [PropertySortIndex(0)]
        [Description("自日付")]
        [RefreshProperties(RefreshProperties.All)]
        public virtual DateTime From
        {
            get { return _from; }
            set
            {
                if (_to < value) throw new ArgumentException("From <= Toでなくてはなりません。");
                _from = value;
            }
        }

        [PropertySortIndex(1)]
        [Description("至日付")]
        [RefreshProperties(RefreshProperties.All)]
        public virtual DateTime To
        {
            get { return _to; }
            set
            {
                if (_from > value) throw new ArgumentException("From <= Toでなくてはなりません。");
                _to = value;
            }
        }

        protected virtual string[] PropertySortDefinition
        {
            get { return new string[] { "From", "To" }; }
        }

        public static DateTimeFromTo Parse(string value)
        {
            string[] v = value.Split(new char[] { ',' });
            return new DateTimeFromTo(DateTime.Parse(v[0]), DateTime.Parse(v[1]));
        }

        public override string ToString()
        {
            return "[" + this.From.ToString() + "," + this.To.ToString() + "]";
        }
    }
}
using System;
using System.ComponentModel;

namespace ClassLibrary1
{
    public sealed class DateTimeFromToExpandableObjectConverterImplementor
        : AbstractExpandableObjectConverterImplementor
    {
        public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value)
        {
            object result = base.ConvertFrom(context, culture, value);
            if (!IsValid(context, result))
                throw new ArgumentException("From <= Toでなくてはなりません。");
            return result;
        }

        public override bool IsValid(ITypeDescriptorContext context, object value)
        {
            if (value.GetType() == typeof(DateTimeFromTo))
            {
                DateTimeFromTo dtft = (DateTimeFromTo)value;
                if (dtft.From > dtft.To) return false;
                return true;
            }
            return base.IsValid(context, value);
        }
    }
}


ComboBoxに実装しても意味ないけど

using System;
using System.ComponentModel;
using System.Windows.Forms;

namespace ClassLibrary1
{
    public sealed class ComboBoxEx
        : ComboBox
    {
        private DateTimeFromTo _FromTo = new DateTimeFromTo(default(DateTime), default(DateTime));

        [DisplayName("DateTimeFromTo")]
        [RefreshProperties(RefreshProperties.All)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
        public DateTimeFromTo FromTo
        {
            get { return _FromTo; }
            set { _FromTo = value; }
        }

        [EditorBrowsable(EditorBrowsableState.Never)]
        public bool ShouldSerializeFromTo()
        {
            if (_FromTo.From != default(DateTime)) return true;
            if (_FromTo.To != default(DateTime)) return true;
            return false;
        }

        [EditorBrowsable(EditorBrowsableState.Never)]
        public void ResetFromTo()
        {
            _FromTo = new DateTimeFromTo(default(DateTime), default(DateTime));
        }
    }
}