IExtenderProviderの実装方法

  • C#
  • Windows Forms

IExtenderProviderとは

IExtenderProvider(拡張プロバイダー)とは、Windows Formsのコンポーネントに新たなプロパティを追加することができるものです。
これを使って追加したプロパティは、デザイナー上であたかもそのコンポーネントに始めから存在したかのような振る舞いをします。
例を挙げると、TableLayoutPanelコントロール上に子コントロールを配置したときに現れるCellRowRowSpanColumnColumnSpanプロパティや、ErrorProviderコンポーネントの配置時に現れるErrorプロパティなどがIExtenderProviderで実装されています。

IExtenderProviderの実装

IExtenderProviderの実装方法を順を追って説明していきます。
今回は例として、コントロールとラベルを紐づけるようなプロパティを追加して、ラベルをクリックしたら紐づいているコントロールにフォーカスがあたるようにしてみたいと思います。
イメージ的にはHTMLの<label for="">みたいなものです。

環境

  • Windows Forms
  • .NET 6.0
  • Visual Studio Community 2022 64bit Version 17.0.1
  1. ComponentまたはComponentから派生したクラスを継承したクラスを作成します。
    ビルドしてツールボックスにコンポーネントが表示されればOKです。コンポーネント名はご自由に。

    using System;
    using System.ComponentModel;
       
    public class LabelProvider : Component
    {
    }
    

  2. 作成したコンポーネントにProvideProperty属性を付与します。
    第一引数には 追加するプロパティの名前 を、第二引数には 追加対象のコンポーネント型 を指定します。
    今回はコントロールに対してLabelプロパティを追加したいので、以下のようにします。

    using System;
    using System.ComponentModel;
    using System.Windows.Forms;
       
    [ProvideProperty("Label", typeof(Control))]
    public class LabelProvider : Component
    {
    }
    
  3. 作成したコンポーネントにIExtenderProviderインターフェースを実装します。
    CanExtendメソッドはデザイナー上に配置した全てのコンポーネントに対して呼ばれるメソッドで、extendeeに入ってくるコンポーネントがプロパティを追加したいコントロールであればtrueを返すように実装します。
    基本的にはコンポーネントの型がProvideProperty属性の第二引数で指定したコンポーネント型と一致しているかを確認する程度でOKだと思いますが、
    今回はコントロールに対するラベルの紐づけが目的で、ラベル同士を紐づける必要は無いため、ラベル自体は除外しています。

    using System;
    using System.ComponentModel;
    using System.Windows.Forms;
       
    [ProvideProperty("Label", typeof(Control))]
    public class LabelProvider : Component, IExtenderProvider
    {
        public bool CanExtend(object extendee) => extendee is Control && extendee is not Label;
    }
    
  4. 作成したコンポーネントに、追加するプロパティのセッターメソッドとゲッターメソッドを定義します。
    IExtenderProviderで提供するプロパティは通常のプロパティではなく、
    Set{プロパティ名}メソッドとGet{プロパティ名}メソッドを自前で定義する必要があります。
    シグネチャは以下のようにしてください。

    • セッター : public void Set{プロパティ名}(追加対象コンポーネントの型, 追加プロパティの型)
    • ゲッター : public 追加プロパティの型 Get{プロパティ名}(追加対象コンポーネントの型)

    プロパティ名と追加対象コンポーネントの型は、それぞれProvideProperty属性で指定した値と同じになる様にします。
    追加プロパティの型は、その名の通り追加するプロパティ名の型を指定します。今回だとLabelです。

    using System;
    using System.ComponentModel;
    using System.Collections.Generic;
    using System.Windows.Forms;
       
    [ProvideProperty("Label", typeof(Control))]
    public class LabelProvider : Component, IExtenderProvider
    {
        // プロパティ値の保持用
        private Dictionary<Control, Label?> targets = new ();
    
        public bool CanExtend(object extendee) => extendee is Control && extendee is not Label;
       
        public Label? GetLabel(Control control)
        {
             ArgumentNullException.ThrowIfNull(control);
             // プロパティ値取得の例
             targets.TryGetValue(control, out Label? label));
             return label;
        }
    
        public void SetLabel(Control control, Label? label)
        {
             ArgumentNullException.ThrowIfNull(control);
             // プロパティ値設定の例
             targets[control] = label;
        }
    }
    

上記の説明を踏まえつつ、プロパティの取得・設定をもう少し真面目に書いて、ラベルクリック時に紐づけたコントロールにフォーカスするようにした完成コードが以下になります。

using System;
using System.ComponentModel;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;

[ProvideProperty("Label", typeof(Control))]
public class LabelProvider : Component, IExtenderProvider
{
    private Dictionary<Control, Label?> targets = new ();

    public bool CanExtend(object extendee) => extendee is Control && extendee is not Label;

    public Label? GetLabel(Control control)
    {
        ArgumentNullException.ThrowIfNull(control);

        targets.TryGetValue(control, out Label? label);
        return label;
    }

    public void SetLabel(Control control, Label? label)
    {
        ArgumentNullException.ThrowIfNull(control);

        if (targets.ContainsValue(label))
        {
            throw new InvalidOperationException();
        }

        targets.TryGetValue(control, out Label? oldLabel);
        if (oldLabel == label)
        {
            return;
        }

        if (oldLabel is not null)
        {
            oldLabel.Click -= Label_Click;
        }

        if (label is not null)
        {
            label.Click += Label_Click;
        }

        targets[control] = label;
    }

    private void Label_Click(object? sender, EventArgs e)
    {
        var label = (Label)sender!;
        Control? control = targets.FirstOrDefault(t => t.Value == label).Key;
        control?.Select();
    }
}

動作確認してみます。
デザイナー上でBのラベルをそのすぐ下のテキストボックスと紐づけます。

画面上でAのラベルとBのラベルをそれぞれクリックしてみます。
Aのラベルをクリックしたときは何も起こりませんが、Bのラベルをクリックしたときは紐づけたテキストボックスにフォーカスが当たっていますね。

このように、IExtenderProviderを使うと既存のコンポーネントにプロパティを追加して機能を拡張することができます。
Windows Forms自体、最近はそんなに使われていないと思いますがどなたかの参考になれば幸いです。