エクステンダでコード拡張

CheckBox に StoreXPath プロパティが

.NET のデザイン機能のエクステンダは(IDEのサポートが微妙にアヤシイ割に)かなり便利で優秀な機能だと思う。いわゆる Mix-in 的な機能をデザイン時に設定し、実行時に解決する機能だ。
手元にある .NET 1.1 用のコンポーネントのうち実に6割がエクステンダになっている。
エクステンダって何って方のために簡単に書くと、標準のエクステンダとしては Tooltip や ErrorProvider がある。たとえば空のフォームの上に TextBox と Button を配置し、Tooltip コンポーネントをフォームの上に配置すると、TextBox と Button 両方に Tooltip Text プロパティが追加され、このプロパティを設定するだけで TextBox と Button にツールチップテキストを表示する機能が追加される。こういったコンポーネントがエクステンダに相当する。
エクステンダはコンテナ単位に動作し、コンテナ自身およびコンテナ内にある IExtenderProvider を実装するコンポーネントすべてが提供することができる。ToolTip や ErrorProvider は基本的にエクステンダとしての機能が全てだが、エクステンダを提供する Button を作成することもできるし、コンテナ自身から提供することができるため、フォームやユーザコントロールに実装することもできる。
自作のエクステンダとしては、たとえば MenuItem に対して Tag プロパティを追加したり*1 Image プロパティを追加して、MenuItem の左側にアイコンを表示したりする機能を提供するものがある。

コンテナが実装する

例えば、環境設定タブコントロールの上で表示される各ページを、機能別にユーザコントロールとして個別に作成したとして、各設定値は XML ファイルに保存されるとしよう。すべてのユーザコントロール

public class EnvironmentPage : UserControl
{
  public virtual void SaveToXml(XmlWriter writer)
  { throw new NotImplementException(); }

  public virtual void LoadFromXml(XmlReader reader)
  { throw new NotImplementException(); }
}

みたいな共通の親クラスを持つことにしよう。各ページには設定項目を表示/編集する TextBox や CheckBox などが配置される。保存処理はある程度の共通化を計ることはできるかもしれないが、

public class Page1 : EnvironmentPage
{
  private TextBox username;
  private CheckBox showSplash;

  public override void SaveToXml(XmlWriter writer)
  {
    writer.WriteStartElement("Page1")
    writer.WriteAttributeString("Splash", this.showSplash.Checked);
    writer.WriteElementString("Username", this.username.Text);
    writer.WriteEndElement();
  }
}

たとえばこんな感じになり、コントロールと保存処理と読込処理の対応をどんどんと実装していかなければならない。ここで、EnvironmentPage に IExtenderProvider を実装し、TextBox や CheckBox に保存先を XPath 形式で指定するプロパティを提供させるとどうなるだろうか?
Page1 をビジュアルデザイナで開くと、すべての TextBox や Button に XPath 形式で保存先を指定することができるだけでなく、SaveToXml() メソッドの実装すら自動で行われるようになる。

コードサンプル?

このままだと、GetStoreValue() あたりが使い物にならんわけですが。

   // code coloring/formated by http://manoli.net/csharpformat/
    [ProvideProperty("XmlStorePath", typeof(Control))]
    public partial class EnvironmentPage : UserControl, IExtenderProvider
    {
        private Dictionary<Control, string> storePath = new Dictionary<Control, string>();

        public EnvironmentPage()
        {
            InitializeComponent();
        }

        public virtual void SaveToXml(XPathNavigator nav)
        {
            foreach (KeyValuePair<Control, string> pair in this.storePath)
            {
                string value = GetStoreValue(pair.Key);

                nav.SelectSingleNode(pair.Value).SetValue(value);
            }
        }

        public virtual string GetStoreValue(Control c)
        {
            TextBox text = c as TextBox;
            if (text != null)
            {
                return text.Text;
            }

            CheckBox check = c as CheckBox;
            if (check != null)
            {
                return check.ThreeState.ToString();
            }

            throw new NotSupportedException();
        }

        #region IExtenderProvider メンバ

        bool IExtenderProvider.CanExtend(object extendee)
        {
            return (extendee is CheckBox) || (extendee is TextBox);
        }

        [DefaultValue("")]
        public string GetXmlStorePath(Control c)
        {
            Debug.Assert( (this as IExtenderProvider).CanExtend(c));

            string path;
            return storePath.TryGetValue(c, out path) ? path : string.Empty;
        }

        public void SetXmlStorePath(Control c, string value)
        {
            Debug.Assert( (this as IExtenderProvider).CanExtend(c));

            if ( (value == null) || (value.Length == 0))
            {
                storePath.Remove(c);
            }
            else
            {
                storePath[c] = value;
            }
        }

        #endregion
    }

あれ

本題のコード拡張まで話が進まなかった(笑

*1:.NET 2.0 の MenuItem クラスには Tag プロパティが新設されているのでこのエクステンダはもう必要ない。