次の方法で共有


スマート タグ

Visual Studio におけるカスタム デザイナ アクションによる UI 開発の簡易化

Michael Weinhardt


翻訳元: Simplify UI Development with Custom Designer Actions in Visual Studio (英語)

この記事は、Visual Studio 2005 プレリリース バージョンに基づいています。この記事のすべての情報は変更される可能性があります。

この記事で取り上げる話題:

  • Visual Studio におけるデザイナ アクションの役割
  • デザイナ アクションのプロパティ アイテムおよびメソッド アイテム
  • 独自のカスタム デザイナ アクションの作成方法、およびスマート タグの追加

この記事で使用する技術:

  • Visual Studio 2005

サンプルコードのダウンロード:


目次

  1. デザイナ アクションの紹介
  2. デザイナ アクション リストの構築
  3. デザイナ アクション プロパティ アイテムの作成
  4. カスタム デザイナの作成
  5. デザイナ アクション プロパティ アイテムの表示
  6. デザイン時専用デザイナ アクション プロパティ アイテム
  7. 複数の値を持つデザイナ アクション プロパティ アイテム
  8. デザイナ アクション メソッド アイテム
  9. デザイナ アクション メソッド アイテムの追加
  10. デザイナ アクション メソッド アイテムの表示名の切り替え
  11. スマート タグ パネルの自動表示
  12. カテゴリおよび説明
  13. ヘッダーおよびラベル
  14. まとめ

プロパティ ウィンドウは、Windows フォームをデザインする際のもっとも主要な機能であり、コンポーネント全体の構成や制御を 1 か所から行うことを可能にします。Visual Studio 2005 では、この機能が、スマート タグと呼ばれる新しい機能によってさらに強化されます。スマート タグは、デザイン画面上に、直接、主要な構成を表示する機能で、デザイン時の生産性を全体的に高めます。多くの Windows Forms 2.0 コンポーネントには、デザイナ アクションと言われる Microsoft .NET Framework 2.0 のサブセットを使用してスマート タグが表示されます。これと同じように、カスタム コンポーネントにも、スマート タグを追加して同様の便利さを実現することができます。

1. デザイナ アクションの紹介

Windows フォームのデザイン画面でスマート タグをサポートするコンポーネントを選択すると、コンポーネントの右上端にスマート タグ アンカーが表示されます。このアンカー (ボタン) をクリックすると、スマート タグ パネルが開きます。スマート タグ パネルは、コンポーネントの構成をスマート タグ エントリのセットとして表示するデザイナ管理 UI です。図 1 に、主なこれらの要素を示します。

図 1 スマート タグ アンカー、パネル、 およびタスク

図 1 スマート タグ アンカー、パネル、 およびタスク

Visual Studio 2005 のデザイナ アクション インフラストラクチャでは、スマート タグ エントリは、デザイナ アクション アイテムとも言われ、デザイナ アクション リストと呼ばれるグループとしてデザイン時に表示されます。コンポーネントがスマート タグをサポートするには、最低 1 つのデザイナ アクション アイテムを含む、最低 1 つのデザイナ アクション リストを Windows フォーム デザイナに渡す必要があります。次に、その同じデザイナ アクション リストがスマート タグ パネルに渡されます。スマート タグ パネルは、各デザイナ アクション アイテムを対応のタスクに変換して表示します。

ページのトップへ


2. デザイナ アクション リストの構築

デザイナ アクション アイテムは、デザイナ アクション リスト内にまとめる必要があるため、スマート タグのサポートをコンポーネントに追加するには、まず、デザイナ アクション リストを構築する必要があります。デザイナ アクション インフラストラクチャは、デザイナ アクション リストを表す専用のクラス (正確には、DesignerActionList と呼ばれる) を実装しています。すべてのデザイナ アクション クラスと同様に、DesignerActionList は、System.ComponentModel.Design 名前空間に存在します。しかし、物理的には、プロジェクトで先に進む前に必ず参照する System.Design.dll アセンブリに存在します。

DesignerActionList クラスは、1 つ以上のデザイナ アクション アイテムを 1 つのコンポーネントに関連付けるために設計されています。関連付けは、そのコンストラクタを介して作成されます。コンストラクタは、コンポーネントの読み取り専用 Component プロパティとして用意されている IComponent 参照を使用します。

public class DesignerActionList {
public DesignerActionList(IComponent component);
public IComponent Component { get; }
...
}

DesignerActionList は、リストが IList、または ICollection を実装するものとすると、その意味ではリストとは言えません。しかし、DesignerActionList は、GetSortedActionItems メソッドによって DesignerActionItemCollection に保存されたデザイナ アクション アイテムのリストを返します。独自のデザイナ アクション アイテムのリストを返すには、DesignerActionList からの派生として、GetSortedActionItems をオーバーライドし、DesignerActionItemCollection インスタンスを、作成し、データを取得して、返します (オーバーライドしない場合、GetSortedActionItems のデフォルトの実装では、DesignerActionItems の public メソッドおよびプロパティをすべてラップするリフレクションを使用し、それらをアルファベット順に並べて返します)。

public class ClockControlDesignerActionList : DesignerActionList {
  ...
  public override DesignerActionItemCollection GetSortedActionItems() {

    // デザイナ アクション アイテムを保存するリストを作成
    DesignerActionItemCollection actionItems =
      new DesignerActionItemCollection();
    ...
    // デザイナ アクション アイテムのリストを取得
    // デザイナ アクション アイテムのリストを返す
    return actionItems;
  }
}

DesignerActionItemCollection は、1 つ以上の DesignerActionItem オブジェクトを含むように厳密に型が決められているコレクションです。DesignerActionItem は抽象クラスで、すべてのデザイナ アクションに共通する機能を持ちます。しかし、DesignerActionItem は抽象クラスなので、直接インスタンスを生成することはできません。生成できたとしても、使用するには一般的過ぎます。代わりに、デザイナ アクション インフラストラクチャがいくつかの DesignerActionItem の派生物を提供します。各派生物は、それぞれ特定のデザイナ アクション アイテムのスタイルをサポートします。この派生物の 1 つである DesignerActionPropertyItem は、スマート タグ パネルに表示されるアイテム、つまり プロパティ エントリのできる限りもっとも一般的な型をモデルとします。

ページのトップへ


3. デザイナ アクション プロパティ アイテムの作成

サンプルとして、MSDN Magazine の April 2003 (英語) および May 2003 (英語) の 2 つのシリーズで Chris Sells と共に作成した ClockControl を使用します。ClockControl には、Face プロパティがあります。このプロパティは、ClockControl の表示をアナログ、デジタル、またはその両方のいずれかに指定します。このプロパティは、デザイン時に構成するプロパティの 1 つであるので、スマート タグでの使用に適しています。

このプロパティをデザイナ アクション プロパティ アイテムにするには、DesignerActionPropertyItem インスタンスを作成し、それを DesignerActionItemCollection に追加します。以下に例を示します。

public class ClockControlDesignerActionList : DesignerActionList {
  ...
  public override DesignerActionItemCollection GetSortedActionItems() {
    ...
    //Face デザイナ アクション プロパティ アイテムを追加
    actionItems.Add(new DesignerActionPropertyItem("Face", "Face"));
    ...
  }
  ...
}

DesignerActionPropertyItem クラス コンストラクタは、2 つの文字列引数として、メンバ名と表示名を取ります。メンバ名は、カスタム DesignerActionList によって提供されるプロパティの名前です。表示名は、スマート タグ パネルに、スマート タグ タスクのラベルとして表示されるテキスト値です。

コンポーネント自体の実装を使用するのではなく、DesignerActionList がプロパティの実装を提供するのは、スマート タグ パネルが、コンポーネント自体を直接操作する手段ではなく、DesignerActionList オブジェクトを操作する手段であるためです。このため、スマート タグ パネルが読み書きできるようにするには、DesignerActionLists が、コンポーネントのプロパティを提供するプロパティを代理として実装する必要があります。この代理のプロパティの役割を図 2 に示します。

図 2 DesignerActionList によって提供される代理プロパティの実装

図 2 DesignerActionList によって提供される代理プロパティの実装

この要件を考える場合、もうひとつ注意する点があります。代理プロパティは、コンポーネントのプロパティを以下のようなコードで直接的に set すべきではありません。

public class ClockControlDesignerActionList : DesignerActionList {
  ...
  // Face プロパティ
  public ClockFace Face {
    get { return this.ClockControl.Face; }
    set { this.ClockControl = value; }
  }
  ...

}

資料でも説明されている通り、このようなコードは、デザイン時サービス、たとえば、プロパティ変更を反映するためのプロパティ ウィンドウの再表示や、プロパティ編集の際の 「元に戻す」 のサポートを実現しません。これは、プロパティの set アクセサがプロパティ記述子を使用するようにコーディングすることで解決できます。例を図 3 に示します。これらの変更を設定するには、Windows フォーム デザイナのコンポーネント変更サービスを使用できます。ただし、資料のテクニックの説明は、おそらく、これより簡潔です。

図 3 Face プロパティの安全な実装

public class ClockControlDesignerActionList : DesignerActionList {
  ...
  // Face プロパティ
  public ClockFace Face {
    get { return this.ClockControl.Face; }
    set { SetProperty("Face", value); }
  }

  // コンポーネントのプロパティを安全に set するヘルパー メソッド
  private void SetProperty(string propertyName, object value) {
    // プロパティを取得
    PropertyDescriptor property =
    TypeDescriptor.GetProperties(this.ClockControl)[propertyName];
    // プロパティ値を set
    property.SetValue(this.ClockControl, value);
  }

  // ClockControl 参照を取得するヘルパー プロパティ
  private ClockControl ClockControl {
    get { return (ClockControl)this.Component; }
  }
}

ここでの代理 Face プロパティ、および GetSortedActionItems メソッドのどちらも、デザイナ アクション リストが必要とする最小限の実装での構成となっています。GetSortedActionItems は、スマート タグ パネルが開いたとき、および再表示されたときに、1つのデザイナ アクション プロパティ アイテムを取得する場合、およびそのアイテムをスマート タグ タスクとして表示する場合の両方において、スマート タグ パネルから呼び出されます。スマート タグ プロパティの初期値は、コンポーネントの Face プロパティから ClockControlDesignerActionList クラスの 代理プロパティを通して取得されます。逆に、スマート タグ パネルで変更を行うと、Face プロパティに設定されます。

当然、ClockControlDesignerActionList をまだ Windows フォーム デザイナに渡していないという問題があります。つまり、ClockControl は、まだデザイナ アクションのサポートは得られません。この小さな問題を埋めるのが、カスタム デザイナです。

ページのトップへ


4. カスタム デザイナの作成

デザイナ アクションは、デザイン時のみの機能として分類されます。つまり、デザイナ アクションは、デザイン時環境内でのみ、コンポーネントに提供され、使用されます。デザイン時インフラストラクチャ全般に詳しいユーザーであれば、このようなデザイン時専用機能の提供は、デザイナ クラスの役割であることに気がつくことでしょう。デフォルトでは、すべてのコンポーネントは、独自のカスタム デザイナを使用する場合を除いて、Windows フォーム デザイナで使用されるときのデフォルト デザイナが決まっています。カスタム デザイナは、IDesigner インターフェイスを実装するクラスですが、ほとんどは、コンポーネントを対象とする ComponentDesigner や、コントロールを対象とする ControlDesigner など、コンポーネントの種類ごと用意されている、.NET Framework 提供の IDesigner の基本的実装から派生します。カスタム デザイナは、DesignerAttribute を使用するコンポーネントに関連付けれられます。ClockControl の当初の実装では、デザイン時専用プロパティを ClockControl に追加するために ClockControlDesigner が作成されました。以下は、主要な実装の詳細です。

public class ClockControlDesigner : ControlDesigner { ... }

[Designer(typeof(ClockControlDesigner))]
public class ClockControl : Control, ... { ... }

.NET Framework 2.0 およびスマート タグ サポートになってから、ComponentDesigner クラスは、次に示す、新しい読み取り専用 ActionLists プロパティに変更されました。

public class ComponentDesigner : IDesigner, ... {
  ...
  public virtual DesignerActionListCollection ActionLists { get; }
  ...
}

ActionLists プロパティはの型は、DesignerActionListCollection (1 つ、または複数の DesignerActionList オブジェクトを保持するオブジェクト) です。複数のデザイナ アクション リストについての詳細は、この記事では省略しますが、そのサポートによって、1 つのカスタム デザイナ アクション リストをいくつかの別々のリストに分割することが可能になります。たとえば、最初のリストが大きくなり過ぎてメンテナンスが難しくなった場合や、デザイナ アクション アイテムをコンポーネントのデザイン時の状況に応じて一部だけを選択して表示したい場合などには分割することが考えられます。

デフォルトでは、基本 ComponentDesigner クラスの ActionList プロパティは、空の DesignerActionListCollection を返します。この場合、Windows フォーム デザイナには、そのコンポーネントに対して "Smart Tags Not Required" という表示がされます。つまり、ActionList をオーバーライドし、独自のカスタム DesignerActionList の実装を追加した、独自の DesignerActionListCollection を返すようにする必要があります。ClockControlDesigner は、ComponentDesigner から ControlDesigner を介して、非直接的に派生しているので、ActionLists を直接オーバーライドすることができます。図 4 に例を示します。

図 4 ActionLists のオーバーライド

public class ClockControlDesigner : ControlDesigner {
  ...
  public override DesignerActionListCollection ActionLists {
    get {
      // アクション リスト コレクションを作成
      DesignerActionListCollection actionLists =
        new DesignerActionListCollection();

      // カスタム アクション リストを追加
      actionLists.Add(
        new ClockControlDesignerActionList(this.Component));

      // デザイナ アクション サービスに返す
      return actionLists;
    }
  }
  ...
}

これで、スマート タグ パネルからプロパティを編集するために必要な実装はすべてです。更新した ClockControl ソリューションをリビルドしてから、この新しい ClockControl をフォームにドラッグすると、図 5 のように、スマート タグ パネルで Face プロパティを編集できるはずです。

図 5 Face デザイナ アクション プロパティ アイテム

図 5 Face デザイナ アクション プロパティ アイテム

スマート タグ パネルのタイトル表示は変更できません。Windows フォーム デザイナが、デフォルトで、"ComponentTypeName Tasks" という規則によってタイトルを表示します。また、デザイナ アクション プロパティ アイテムの表示、および編集に使用するコントロールも、Windows フォーム デザイナが決定します。

ページのトップへ


5. デザイナ アクション プロパティ アイテムの表示

図 5 から、スマート タグ パネルは、Face プロパティのような列挙型をドロップダウン リストで表示することがわかります。他の型のプロパティをスマート タグ パネルでコントロールに変換してみると、次のようになります。

  • プロパティが UI 型エディタに関連付けられている場合は、そのエディタが表示されます。エディタは、プロパティの型によって自動的に指定される場合と、EditorAttribute を代理プロパティに適用して手動で指定する場合があります。
  • プロパティが Boolean 型である場合は、チェックボックスが表示されます。
  • 列挙型でも Boolean 型でもないプロパティは、テキストボックスで表示されます。プロパティの型が文字列からの変換をサポートしない場合は、型の ToString メソッドから得られたテキストが読み取り専用で表示されます。

ユーザーは、この変換に対してはプロパティの型を選択する以外の制御はできませんが、プロパティの編集サポートを強化するために、同じデザイン時専用属性で多少の変更を加えることが可能です。この属性の 1 つが EditorAttribute です。これは、開発者が専用の UI をプロパティに関連付けるためのものです。ClockControl ライブラリには、ClockFace 型のプロパティ専用に設計された FaceEditor という UI 型エディタが既に含まれています。Face プロパティは、その ClockFace 型のプロパティであるので、ClockControl に実装される Face プロパティは、EditorAttribute で FaceEditor に関連付けることができます。また、そうすることをお勧めします。ただし、スマート タグ パネルは、ClockControlDesignerActionList クラスの代理 Face プロパティを参照するので、このことを認識しません。ただしこの問題は、以下のように、代理 Face プロパティに、EditorAttribute を追加することで解決できます。

// Face 代理プロパティ
[Editor(typeof(FaceEditor), typeof(UITypeEditor))]
public ClockFace Face {
  get { return this.ClockControl.Face; }
  set { SetProperty("Face", value); }
}

FaceEditor は、UI 型エディタでドロップダウン スタイルを提供しますが、同じ技術によって、ClockControl の DigitalTimeFormat プロパティが関連付けられている DigitalTimeFormatEditor のようなモーダル UI 型エディタを使用することもできます。図 6 に例を示します。モーダル UI 型エディタは、スマート タグ パネル上でも、プロパディ ウィンドウと同じように動作します。

代理プロパティが、日付型のような、そもそも .NET Framework 2.0 で UI 型 エディタが関連付けられている型である場合、スマート タグ パネルは、自動的に UI 型エディタを使用します。ClockControl の BackupAlarm プロパティ、および PrimaryAlarm プロパティはこのケースにあたります。

UI 型エディタをデザイナ アクション プロパティ アイテムに追加するのは比較的簡単ですが、別の方法もサポートされています。ここではそのいくつかについて説明します。

図 6 DigitalTimeFormatEditor の使用

public class ClockControlDesignerActionList : DesignerActionList {
  ...
  public override DesignerActionItemCollection GetSortedActionItems() {
    ...
    // DigitalTimeFormat デザイナ アクション プロパティ アイテムを追加
    actionItems.Add(new DesignerActionPropertyItem(
        "DigitalTimeFormat","Digital Time Format"));
    ...
  }
  ...
  [Editor(typeof(DigitalTimeFormatEditor), typeof(UITypeEditor))]
  public string DigitalTimeFormat {
    get { return this.ClockControl.DigitalTimeFormat; }
    set { SetProperty("DigitalTimeFormat", value); }
  }
  ...
}

ページのトップへ


6. デザイン時専用デザイナ アクション プロパティ アイテム

ClockControl の当初のデザイナは、次のように、デザイン時専用の ShowBorder プロパティを提供していました。

public class ClockControlDesigner : ControlDesigner {
  ...
  //作成された ShowBorder プロパティ用のストレージを提供する
  //ShowBorder を実装
  [CategoryAttribute("Design")]
  [DesignOnlyAttribute(true)]
  [DefaultValueAttribute(true)]
  [DescriptionAttribute("Show/Hide a border at design-time.")]
  public bool ShowBorder { get; set; }
  ...
}

ShowBorder は、コンポーネントではなくデザイナに実装するため、ShowBorder のデザイナ アクション プロパティ アイテムへの変換は、これまでのサンプルとは多少異なります。しかし、次のように、DesignerActionPropertyItem の作成は同じです。

public class ClockControlDesignerActionList : DesignerActionList {
  ...
  public override DesignerActionItemCollection GetSortedActionItems() {
    ...
    // ShowBorder デザイナ アクション プロパティ アイテム
    actionItems.Add(
      new DesignerActionPropertyItem("ShowBorder", "Show Border"));
    ...
  }
  ...
}

ここでの違いは、代理 ShowBorder プロパティは、ClockControlDesigner のプロパティ実装に対して、読み取り、および書き込みを行う必要があるということです。つまり、まずは、ClockControlDesignerActionList から ClockControlDesigner への参照を取得することになります。DesignerActionList には、本来、このようなサポートはありませんが、次のように簡単に追加することができます。

public class ClockControlDesignerActionList : DesignerActionList {
  ...
  // ClockControlDesigner の参照を取得するヘルパー メソッド
  private ClockControlDesigner Designer {
    get {
      IDesignerHost designerHost = (IDesignerHost)
        this.ClockControl.Site.Container;
      return (ClockControlDesigner)
        designerHost.GetDesigner(this.ClockControl);
    }
  }
  ...
}

これで、次のように、代理 ShowBorder プロパティは、ClockControlDesigner クラスの実際の ShowBorder プロパティと簡単にやりとりすることができます。

public class ClockControlDesignerActionList : DesignerActionList {
  ...
  // ShowBorder 代理プロパティ
  public bool ShowBorder {
    get { return this.Designer.ShowBorder; }
    set { this.Designer.ShowBorder = value; }
  }
  ...
}

この代理プロパティが、より簡単に導入できる SetProperty ヘルパーを使用していないことに注意してください。その代わりに、実際の ShowBorder の実装が、デザイン時のコンポーネント変更サービスを使用して、同様の効果を達成しています (図 7 を参照してください)。

図 7 ShowBorder の実装

public class ClockControlDesigner : ControlDesigner {
  ...
  public bool ShowBorder {
    get { return _showBorder; }
    set {
      // プロパティ値を変更
      PropertyDescriptor property =
        TypeDescriptor.GetProperties(
          typeof(ClockControl))["ShowBorder"];
      this.RaiseComponentChanging(property);
      _showBorder = value;
      this.RaiseComponentChanged(property, !_showBorder, _showBorder);
      ...
    }
  }
  ...
}

あとは、ソリューションをリビルドすれば完成です。結果を 図 8 に示します。

図 8 ShowBorder デザイナ

図 8 ShowBorder デザイナ

ShowBorder プロパティは、Boolean 型であるため、スマート タグ パネルではチェックボックスで表示されます。サンプルからは、ShowBorder デザイナ アクション プロパティを切り替えると、枠線の表示、非表示が切り替わるだけでなく、プロパティ ウィンドウの ShowBorder フィールドにもその変更がすぐに反映されていることがわかるでしょう。これが、実際の ShowBorder プロパティのコンポーネント変更サービスを統合したことにより得られる効果です。

ページのトップへ


7. 複数の値を持つデザイナ アクション プロパティ アイテム

次に説明するデザイナ アクション プロパティ アイテムは、複数の値を持つアイテムです。たとえば、ClockControl が実装する HourHand、MinuteHand、および SecondHand のプロパティがこれにあたります。これらをスマート タグ パネルで表示すると、図 9 のようになります。

図 9 複数値プロパティ

図 9 複数値プロパティ

プロパティを展開して編集するスタイルは、プロパティ ウィンドウではサポートされていますが、スマート タグ パネルでは使用できません。スマート タグ パネルが使用できるのは、カスタム Hand クラスの型変換機能である HandConverter です。これは、Hand オブジェクトと、Hand オブジェクトを表す複数値の文字列との変換をサポートします。しかし、図 9 からわかるように、文字列の表示は、あまり、ユーザー フレンドリとは言えません。

スマート タグ パネルでは、展開可能プロパティ編集はサポートされていないので、文字列での編集で不十分な場合には、何らかの代わりの方法でそれを補う必要があります。たとえば、プロパティの要素ごとに別のデザイナ アクション プロパティ アイテムを作成することが考えられます。これは、正常に機能しますが、多くの UI プログラムを使用するということには注意が必要です。これは、スマート タグ パネルの最善の使用法とは言えません。また、Hand 型専用に新しく UI 型エディタを作成することもできます。これならば、現在のソリューションが使用している以上に UI プログラムを使用することにはなりません。

ページのトップへ


8. デザイナ アクション メソッド アイテム

HourHand、MinuteHand、および SecondHand のプロパティの物理的構成を考え、1 つの構成論理単位で実際にそれを表現しようとした場合、方向性を変えて、デザイナ アクション メソッド アイテムを代わりに使用することも可能です。デザイナ アクション メソッド アイテムを使用すると、複数のプロパティ セットを含むような複雑な機能に、シングルクリックでアクセスできます。この機能は、複雑さを軽減するため、専用の UI で表示されるのが一般的です。デザイナ アクション メソッド アイテムは、3 本の針用のプロパティの物理的構成を、1 つの簡単な論理ステップにまとめるのに最適な手段です。

すべての時計の針をまとめて編集できるタスクをスマート タグ パネルに追加するには、まず、カスタム DesignerActionList クラスの GetSortedActionItems を更新します。ここでは、DesignerActionMethodItem オブジェクトを DesignerActionItemCollection に追加します。次に例を示します。

public class ClockControlDesignerActionList : DesignerActionList {
  ...
  public override DesignerActionItemCollection GetSortedActionItems() {
    ...
  // EditClockHands デザイナ アクション メソッド アイテム
    actionItems.Add(new DesignerActionMethodItem(
        this, "EditClockHands",
        "Edit Clock Hands"));
    ...
  }
  ...
}

DesignerActionMethodItem クラスのコンストラクタには、3 つの引数があります。1 つは、デザイナ アクション メソッド アイテムが関連付けられるデザイナ アクション リスト オブジェクトへの参照です。2 つ目は、カスタム デザイナ アクション リスト内に存在するメソッド実装の名前です。3 つ目は、スマート タグ パネルでのこのタスクを示すテキストです。今回の場合、スマート タグ ウィンドウは、必要なすべての処理を実行するためにこのメソッドを呼び出すので、このメソッド実装は、実際には、代理メソッドではありません。内部的には、このメソッドは、タスクを実行する必要に応じて、プロパティの読み取り、および書き込みをすべて行います。時計の 3 本の針をまとめて構成することを考えてみると、ClockControl の 3 本の針のプロパティ値を取得し、それらを編集するためにカスタム フォームに渡し、プロパティ値が変更された場合には、その変更を ClockControl に書き込みます。これらのステップを実行するコードを図 10 に示します。

図 10 時計の 3 本の針すべての構成

public class ClockControlDesignerActionList : DesignerActionList {
  ...
  private void EditClockHands() {
    // フォームを作成
    HandsEditorForm form = new HandsEditorForm();

    //現在の針の値をセット
    form.HourHand = _ this.ClockControl.HourHand;
    form.MinuteHand = _ this.ClockControl.MinuteHand;
    form.SecondHand = _ this.ClockControl.SecondHand;

    // OK ボタンがクリックされたら、新しい針の値で更新
    if( form.ShowDialog() == DialogResult.OK ) {
      SetProperty("HourHand", form.HourHand);
      SetProperty("MinuteHand", form.MinuteHand);
      SetProperty("SecondHand", form.SecondHand);
    }
  }
  ...
}

DesignerActionMethodItem オブジェクト、およびメソッドの実装を適切に設定し、リビルドをすれば、デザイナ アクション メソッド アイテムがスマート タグ パネルに表示されるようになります。

図 11 のように、デザイナ アクション メソッド アイテムは、スマート タグ パネルにはリンク ラベルとして表示されます。それらは、プロパティ ウィンドウのコマンド パネルにも表示されます。コマンド パネルが非表示の場合は、プロパティ ウィンドウを右クリックし、[コマンド] を選択します。

図11 時計の針用デザイナ アクション メソッド アイテムの編集図11 時計の針用デザイナ アクション メソッド アイテムの編集

図11 時計の針用デザイナ アクション メソッド アイテムの編集

デザイナ アクション プロパティ アイテムと異なり、デザイナ アクション メソッド アイテムのメソッド実装は、private、 protected、internal、または public に設定することができます。また、このメソッドは、一切の引数を使用しません。スマート タグ パネルには、デザイナ アクション メソッド実装に対して、引数を取得したり渡したりするメカニズムがありません。反対に、デザイナ アクション メソッド実装が値を戻すこともありません。スマート タグ パネルは、値を受け取ったり、処理したりすることができません。

ページのトップへ


9. デザイナ アクション メソッド アイテムの追加

デザイナ アクション メソッド アイテムが使用される範囲は、スマート タグ パネルのみではありません。実際には、デザイン時のコンポーネントのコンテキスト メニューの表示、およびプロパティ ウィンドウでの自動的な表示が行われるように構成することができます。このようなことは、DesignerActionMethodItem クラスのコンストラクタ オーバーロードの 1 つによって実現されます。このオーバーロードには、Boolean 型の引数が 1 つ追加されています。true の場合、デザイナ アクション インフラストラクチャは、メニュー アイテムをコンポーネントの基本的なコンテキスト メニューに追加し、また、プロパティ ウィンドウの説明ペインにはリンク ラベルを追加します。現在のソリューションを、以下のように、多少調整する必要があります。

public override DesignerActionItemCollection GetSortedActionItems() {
  ...
  // EditClockHands デザイナ アクション メソッド アイテム
  actionItems.Add(new DesignerActionMethodItem(
      this, "EditClockHands", "Edit Clock Hands" , true));
  ...
}

カスタム デザイナには、コンテキスト メニュー、およびプロパティ ウィンドウの追加や更新をするための Verbs プロパティが実装されていることに注意してください。.NET Framework バージョン 2.0 よりも前のバージョンでカスタム デザイナを作成すると、Windows フォーム デザイナは、ユーザーが何もしなくても自動的に、カスタム デザイナをスマート タグ メソッドに変換します。ただし、この後で説明する、本来のデザイナ アクション アイテムのように、分類したり、適切にレイアウトしたりすることはできません。

ページのトップへ


10. デザイナ アクション メソッド アイテムの表示名の切り替え

Windows Forms 2.0 の四角形のコントロールによく見られるデザイナ アクション メソッド アイテムの 1 つとして、これらのコントロールの親コンテナに対するドッキングの状態を、スマート タグ パネルで使用可能なデザイナ アクション メソッド アイテムから切り替える機能があります (このスマート タグ アイテムは、コントロールに DockingAttribute を追加することにより、すべてのコントロールに自動的に提供されます)。Dock および Undock プロパティの特徴は、コンポーネントの現在のドッキングの状態を反映して表示名が切り替わることです。これは、デザイナ アクション メソッド アイテムのリンクラベルがクリックされた場合もまったく同じで、図 12 のようになります。

図 12 Dock および Undock プロパティ

図 12 Dock および Undock プロパティ

まず、デザイナ アクション メソッド アイテムを新しく作成する必要があります。そこに、Dock プロパティを、DockStyle の Fill と None で切り替えるメソッド実装も追加します。図 13 を参照してください。

図 13 Dock プロパティの切り替え

public class ClockControlDesignerActionList : DesignerActionList {
  ...
  public override DesignerActionItemCollection GetSortedActionItems() {
    ...
    // Dock および Undock デザイナ アクション メソッド アイテム
    actionItems.Add(new DesignerActionMethodItem(
        this, "ToggleDockStyle", "Dock/Undock in parent container"));
    ...
  }
  ...
  private void ToggleDockStyle() {

    // ClockControl の Dock プロパティを切り替える
    if( _clockControl.Dock != DockStyle.Fill ) {
      SetProperty("Dock", DockStyle.Fill);
    }
    else {
      SetProperty("Dock", DockStyle.None);
    }
  }
  ...
}

デザイナ アクション メソッド アイテムの表示名を切り替えるには、2 つの処理が必要です。1 つは、適切なテキストを決定し、返すヘルパー メソッドです。もう 1 つは、そのメソッドをドッキングの状態が変更されたときに呼び出す処理です。後者の処理について、スマート タグ パネルは、DesignerActionService.Refresh メソッドを使用すると、その次には GetSortedActionItems 呼び出しが行われ、必ず再表示されます。つまり、ヘルパー メソッドを DesignerActionMethodItem の コンストラクタから呼び出すことが可能です。更新したコンストラクタ、および新しいヘルパー メソッドを図 14 に示します。

図 14 コンストラクタ、およびヘルパー メソッド

public class ClockControlDesignerActionList : DesignerActionList {
  ...
  public override DesignerActionItemCollection GetSortedActionItems() {
    ...
    // Dock および Undock デザイナ アクション メソッド アイテムの
    // 表示名を GetDockStyleText ヘルパー メソッドで生成
    actionItems.Add(new DesignerActionMethodItem(
        this, "ToggleDockStyle", GetDockStyleText()));
    ...
  }
  ...
  // ClockControl の現在の Dock プロパティ値に応じて
  // 適切な Dock および Undock プロパティの表示名を
  // 返すヘルパー メソッド
  private string GetDockStyleText() {
    if( this.ClockControl.Dock == DockStyle.Fill ) {
      return "Undock in parent container";
    }
    else {
      return "Dock in parent container";
    }
  }
  ...
}

ページのトップへ


11. スマート タグ パネルの自動表示

開発者がコンポーネントをフォームにドラッグしたらすぐに、親コンテナとのドッキング状態や、その他の構成の設定が表示されるようにするには、DesignerActionList クラスの AutoShow プロパティを使用します。デフォルトでは、AutoShow の基本的な実装は false、つまり AutoShow の無効を返します (ユーザーは、[ツール] の [ オプション] ページから AutoShow を完全に無効にするこができます)。つまり、次のように、この設定を変更する必要があります。

public class ClockControlDesignerActionList : DesignerActionList {
  ...
  public ClockControlDesignerActionList(ClockControl clockControl)
    : base(clockControl) {
    // デザイン時、Windows フォーム デザイナにコンポーネントをドロップしたときに、
    // 自動的にスマート タグ パネルが表示されるようにする
    this.AutoShow = true;
    ...
  }
  ...
}

スマート タグ パネルが自動的に表示される場合でも、要求に応じて表示される場合でも、さらなる使いやすさが求められます。デザイナ アクション インフラストラクチャには、スマート タグ パネルの動作を改善するさまざまなサポートが用意されています。

デザイナ アクション アイテムは、GetSortedActionItems で DesignerActionItemCollection に追加された順番でしかリストされません。この既定の表示では、各デザイナ アクション アイテムがどのようなものであるかを見た目から判断することは難しくなります。しかし、いくつかのデザイナ アクションの技術を使用すれば、スマート タグ パネルのこのような使いにくさを改善できます。

ページのトップへ


12. カテゴリおよび説明

デフォルトで、デザイナ アクションのプロパティ アイテム、およびメソッド アイテムは、デフォルト カテゴリに配置されます。開発者が簡単に、各デザイナ アクションを区別し、そのアクションの処理を判断できるようにするため、デザイナ アクションのカスタム カテゴリによる分類、および説明用のテキストの添付が可能です。DesignerActionPropertyItem クラス、および DesignerActionMethodItem クラスのどちらにも、この 2 つを実現するコンストラクタ オーバーロードが提供されています。

public override DesignerActionItemCollection GetSortedActionItems() {
  ...
  // DigitalTimeFormat デザイナ アクション プロパティを追加
  actionItems.Add(new DesignerActionPropertyItem( "DigitalTimeFormat",
      "Digital Time Format", "Appearance", // カテゴリを示す文字列引数
      "The digital time format, ..." // 説明を示す文字列引数
    ));
  ...
}

カテゴリが適用された場合、スマート タグ パネルにデザイナ アクション アイテムが表示される順序は、最初にカテゴリで判断され、次に、各アイテムがデザイナ アクション アイテム コレクションに追加された順序で判断されます。説明は、マウスがタスクの上に来たときに、ツールチップとして表示されます。

通常、ClockControl のようなカスタム コンポーネントでは、プロパティ ウィンドウでの表示を考慮して、CategoryAttribute、および DescriptionAttribute をそのプロパティに適用します。つまり、これらのプロパティのデザイナ アクション リスト クラスの代理実装は、その同じ文字列値を使用することになります。リフレクションによってこれらを取得する方法を図 15 に示します。

図 15 リフレクションの使用

public class ClockControlDesignerActionList : DesignerActionList {
  ...
  public override DesignerActionItemCollection GetSortedActionItems() {
    ...
    // DigitalTimeFormat デザイナ アクション プロパティを追加
    actionItems.Add(new DesignerActionPropertyItem(
        "DigitalTimeFormat", "Digital Time Format",
        GetCategory(this.ClockControl, "DigitalTimeFormat"),
        GetDescription(this.ClockControl, "DigitalTimeFormat")));
  ...
  }

  // 特定のオブジェクトが提供するプロパティに割り当てられた
  // CategoryAttribute からカテゴリの文字列を返すヘルパー メソッド
  private string GetCategory(object source, string propertyName) {
    PropertyInfo property = source.GetType().GetProperty(propertyName);
    CategoryAttribute attribute = (CategoryAttribute)
      property.GetCustomAttributes(typeof(CategoryAttribute), false)[0];
    if( attribute == null ) return null;
    return attribute.Category;
  }

  // 特定のオブジェクトが提供するプロパティに割り当てられた
  // DescriptionAttribute から説明の文字列を返す
  // ヘルパー メソッド
  private string GetDescription(object source, string propertyName) {
    PropertyInfo property = source.GetType().GetProperty(propertyName);
    DescriptionAttribute attribute = (DescriptionAttribute)
      property.GetCustomAttributes(
        typeof(DescriptionAttribute), false)[0];
    if( attribute == null ) return null;
    return attribute.Description;
  }
}

GetCategory、および GetDescription のどちらにも、2 つの引数があります。1 つは、オブジェクトで、もう 1 つは、そのオブジェクトが実装する、CategoryAttribute、または Description が付加されているプロパティの名前です。また、型記述子は、意図する属性を取得し、文字列値を返すために使用されます。GetCategory、および GetDescription のどちらも、元のオブジェクトのプロパティ実装に、該当の属性が付加されていない場合には、null を返します。null 文字列が DesignerActionPropertyItem クラスのコンストラクタに渡されると、それは、提供されなかったものとして処理されます。この場合、該当のスマート タグ タスクは、デフォルト カテゴリに配置され、そのツールチップは表示されません。一般的に、独自に作成したコンポーネントが提供する、デザイン時に構成可能なプロパティには、CategoryAttribute、および DescriptionAttribute の両方を付加します。これは、特にプロパティ ウィンドウで同じように使用され、同様の効果が得られます。

GetCategory、および GetDescription の両メソッド が、ClockControl パラメータ のようにより限定的なものではなく、元となるオブジェクトを示すオブジェクト パラメータを使用するのは、ClockControlDesigner.ShowBorder のような異なる種類のものに存在するプロパティにも対応するためです。以下に、GetCategory、および GetDescription に、ClockControlDesigner への参照を渡す方法を示します。

public override DesignerActionItemCollection GetSortedActionItems() {
  ...
  // ShowBorder デザイナ アクション プロパティ アイテム
  actionItems.Add(new DesignerActionPropertyItem(
      "ShowBorder", "Show Border",
      GetCategory(this.Designer, "ShowBorder"),
      GetDescription(this.Designer, "ShowBorder")));
  ...
}

一般的に、CategoryAttribute、または DescriptionAttribute は、デザイン時に構成可能なコンポーネント、またはデザイナのすべてのプロパティに付加されるので、この技術は、デザイナ アクション プロパティに適しています。一方、デザイナ アクション メソッドは、元になる実装の代理というようりも、カスタム デザイナ アクション リストに全体が実装されます。そのため、カテゴリ、および説明の文字列は、次のように、DesignerActionMethodItem のインスタンスを作成するときに指定する必要があります。

public override DesignerActionItemCollection
  GetSortedActionItems() {
  ...
  // EditClockHands デザイナ アクション メソッド アイテム
  actionItems.Add(new DesignerActionMethodItem(
      this, "EditClockHands",
      "Edit Clock Hands","Appearance",
      "Configure the ClockControl's hour,
      minute and second hands.",
      true));
  ...
}

これが、別の場所で繰り返されることはほとんどありません。繰り返す場合でも、GetCategory、および GetDescription の両メソッドを簡単にリファクタリングして、プロパティ、およびメソッドを操作することができます。

ページのトップへ


13. ヘッダーおよびラベル

カテゴリをさらにわかりやすくするには、もうひとつのデザイナ アクション アイテム DesignerActionHeaderItem を使用して、カテゴリそれぞれに、テキスト ヘッダーを割り当てることができます。DesignerActionHeaderItem のコンストラクタは、次のように、1 つの文字列値を使用します。

public override DesignerActionItemCollection GetSortedActionItems() {
  ...
  actionItems.Add(new DesignerActionHeaderItem("Appearance"));
  // Appearance カテゴリのデザイナ アクション アイテムをここで追加 ...
  actionItems.Add(new DesignerActionHeaderItem("Behavior"));
  // Behavior カテゴリのデザイナ アクション アイテムをここで追加 ...
  actionItems.Add(new DesignerActionHeaderItem("Design"));
  // Design カテゴリ のデザイナ アクション アイテムをここで追加 ...
  ...
}

説明のテキストがあれば、さらにわかりやすくなります。それには、DesignerActionTextItem を使用します。DesignerActionTextItem のコンストラクタは、次のように、文字列のカテゴリ名値と、テキスト文字列を使用します。

public override DesignerActionItemCollection GetSortedActionItems() {
  ...
  actionItems.Add(new DesignerActionHeaderItem("Appearance"));
  actionItems.Add(new DesignerActionTextItem(
      "Properties that affect how the ClockControl looks.",
      "Appearance"));
  // Appearance カテゴリのデザイナ アクション アイテムをここで追加 ...
  ...
}

この 2 つのパラメータ付きコンストラクタによって、ラベルは、まずそれが割り当てられているカテゴリの順序で並べられ、次に、デザイナ アクション アイテム コレクションに追加された順序で並べられるようになります。

図 16 ClockControl スマート タグ ペイン

図 16 ClockControl スマート タグ ペイン

カテゴリ、説明、ヘッダー、およびラベルを適用すれば、図 16 のような使いやすいスマート タグ パネルとなります。これらの UI 要素を選択して組み合わせれば、さまざまな状況に対応したサポートを簡単に提供できます。

ページのトップへ


14. まとめ

カスタム コンポーネントは、.NET Framework 2.0 デザイナ アクション インフラストラクチャを活用して、スマート タグをサポートできます。この作業は比較的簡単です。特に、ラベルのテキストの切り替え、コンテキスト メニューやプロパティ ウィンドウとの統合、スマート タグ パネルの使いやすさの改善など、新しい状況のサポートも簡単です。スマート タグによって、カスタム コンポーネントにも、高度で、最新の、生産性の高いデザイン時エクスペリエンスが提供されます。これらの技術を習得することを強くお勧めします。


Michael Weinhardt は、現在、『Windows Forms Programming in C#』を Chris Sells と共に改訂しております。また、MSDN online の Wonders of Windows Forms に毎月コラムを執筆しています。詳細については、www.mikedub.net (英語) を参照してください。


この記事は、MSDN マガジン - 2005 年 7 月号からの翻訳です。

ページのトップへ