マイクロソフト系技術情報 Wiki」は、「Open棟梁Project」,「OSSコンソーシアム .NET開発基盤部会」によって運営されています。

目次

概要

.NET Frameworkでは、画面の開発に必要となるコントロールが用意されている。

.NETでは、これらのコントロールの動作をカスタマイズすることで、特定プロジェクト向けにカスタマイズされた
(例えば、自動編集処理、自動入力チェック処理、サニタイジング処理、などの機能を有した)コントロール共通部品開発が可能である。

ここでは、この.NETコントロール(ASP.NET Web Forms, Windows Forms)のカスタマイズ方法について説明する。

ASP.NET Web Forms

ASP.NET Web Formsで使用するコントロールの特徴は、Webアプリケーションであるために、

  • 表示はHTMLにより行われるのでカスタマイズしやすい。
  • ただし、状態の持ち回りについて考慮する必要がある。

という点である。

クラス定義

以下は、ラベル コントロール(System.Web.UI.WebControls?.Label)を継承したカスタム ラベル コントロール(Ctrl.WebCustLabel?)のクラス定義。

[assembly: TagPrefix("Ctrl", "my_wcc")]
namespace Ctrl {
  /// <summary>System.Web.UI.Labelのカスタム・コントロール</summary>
  [DefaultProperty("Text"),
  ToolboxData("<{0}:WebCustLabel runat=server></{0}:WebCustLabel>")]
  public class WebCustLabel : Label

このクラス定義に含まれる「ToolboxData」属性は、カスタム コントロールに必要な属性で、Visual Studio(以下、VSと略す)のツールボックスからドラッグ&ドロップされるときに生成される、カスタム タグを表す文字列を指定する。また、「DefaultProperty」属性は既定のプロパティを指定するオプション属性である。これ以外に、(クラス定義には含まれないが)名前空間定義に含まれる「TagPrefix」 属性により「ToolboxData?」属性の「{0}」に設定されるデフォルトの文字列(タグ プレフィックス)を指定できる。この属性もオプション属性である。

プロパティ

カスタム プロパティの追加

本項ではカスタム プロパティの追加方法について説明する。
カスタム プロパティを追加する際は、通常通りにプロパティ プロシージャを実装すれば良い。

/// <summary>コントロール内のテキストを反転するかどうかを示す。</summary>
[DefaultValue("false"),
Category("動作"),
Description("コントロール内のテキストを反転するかどうかを示します。")]
public bool Reverse {
  set { this.ViewState["Reverse"] = value; }
  get {
    return (this.ViewState["Reverse"] == null) ?
      false : (bool)this.ViewState["Reverse"];
  }
}
  • ただし、ASP.NET Web Formsアプリケーションにおいてコントロールの状態を持ち回る(状態を複数ポストバック間において保存する)場合は、インスタンス変数は用いず、Control.ViewStateを使用する必要がある。これはPage.ViewStateとは異なるため、同じキーを使用しても衝突しない。詳しくは、「ASP.NETの状態管理方式」を参照。
  • また、ここでも同様にオプションとして、「DefaultValue」・「Category」・「Description」などの属性を定義できる。これらの属性は共に、PropertyGrid?(コントロールのデザインタイム・プロパティの設定を行うためのUIで、Visual Studioデザイナでコントロール選択時、右下のペインに表示されるグリッド)への表示方法を制御するためのものである。PropertyGrid?への表示方法を制御するための属性について、以下にまとめる。
メンバ名説明デフォルト値
DefaultValue?デフォルト値を表す。
BrowsableプロパティをPropertyGrid?に表示するかどうかを指定する。True
Categoryプロパティをまとめるカテゴリ。
カテゴリには、次の共通カテゴリがある。
・Appearance:表示
・Behavior:動作
・Data:データ
Descriptionプロパティの設定を行う際の参考としてPropertyGrid?に表示される説明。

既存プロパティのカスタマイズ

  • 本項では既存プロパティのカスタマイズ(オーバーライド)方法について説明する。既存プロパティのカスタマイズの際は、既存プロパティをオーバーライドする必要がある。カスタマイズ対象の既存のプロパティは、派生クラスでオーバーライドが可能なように、virtual / abstractキーワード(VBのキーワードでは、Overridable / MustOverride?に該当)が設定されている必要がある。
  • また、何からの処理を追加した後、「base.オーバーライドしたプロパティ」から値を取得し、その後にカスタム動作となる処理を追加する(に値を設定し、その前にカスタム動作となる処理を追加する)ようにする。

Label.Textのカスタマイズ

以下は、Textプロパティをオーバーライドしカスタム動作を実装するサンプル(テンプレート)である。

/// <summary>Textプロパティに機能を追加実装する。</summary>
public override string Text {

  // ベースのプロパティを使用する。
  
  set {
    string temp = value;
    // カスタム動作の処理を実装する。
    base.Text = temp;
  }
  
  get {
    string temp = base.Text;
    // カスタム動作の処理を実装する。
    return temp;
  }
}

TextBox?.Enabled, ReadOnly?のカスタマイズ

  • なお、以下は、テキストボックス コントロール(System.Web.UI.WebControls?.TextBox?)を継承したカスタム テキストボックス コントロール(Ctrl.WebCustTextBox?)のEnabledプロパティのカスタマイズ例である。
  • ここでは、Enabledプロパティのsetterメソッド内部で、実際にメンバ変数に値をセットする処理をReadOnly?プロパティに変更している。これは、例えばEnabled = falseで実装したものの、後工程での指摘により、ReadOnly? = true的な動作(表示)に変更する必要に迫られた場合など、最小限の工数で対応するために、このような修正を施すことがある。また、併せて、ReadOnly?プロパティをオーバーライドしカスタム動作として、背景色の変更処理を実装する。
  • このカスタマイズにより、Enabledプロパティの挙動をReadOnly?に一括変更でき、またReadOnly? = trueに設定された場合、背景色をグレーに変更するので、「読み取り専用項目」であることが、ユーザ(オペレータ)に伝わり易くなる。
/// <summary>活性・非活性を、Enabledから、ReadOnlyに変更したい場合。</summary>
public override bool Enabled {
  set {
    this.ReadOnly = !value;
  }
  // getは親のEnabledのgetを使用
}

また、背景色をグレーに変更するにあたって、元の背景色のバックアップを取っておく必要があるが、これについても、持ち回る(状態を複数ポストバック間において保存する)必要があるので、同様にインスタンス変数は用いず、Control.ViewStateを使用する必要がある。

/// <summary>ReadOnlyプロパティに背景変更処理を追加する。</summary>
public override bool ReadOnly {
  // ベースのプロパティを使用する。

  set {
    base.ReadOnly = value;

    // 背景変更
    if (base.ReadOnly) {
      this.ViewState["bkBgColor"] = this.BackColor;
      this.BackColor = System.Drawing.Color.LightGray;
    }
    else {
      this.BackColor =
        (this.ViewState["bkBgColor"] == null)
          ? this.BackColor : (System.Drawing.Color)this.ViewState["bkBgColor"];
    }
  }

  get {
    return base.ReadOnly;
  }
}

UIのカスタマイズ

ASP.NET Web Formsで使用するコントロールは、表示の制御がHTMLにより行われるのでカスタマイズしやすい。

Render系メソッド

ここで出力されるHTMLのカスタマイズは、Render系メソッドのカスタマイズにより対応する。

カスタム コントロールをControlクラスから派生させてスクラッチ開発する場合は、Render系メソッドもスクラッチ開発するが、
それ以外の場合は、必要に応じて動作変更するようにして、動作変更の必要がない場合は、処理を基本クラスの「base.xxxx」メソッドに委譲するような方法を採る。

HtmlTextWriterクラス

Render系メソッドにおいてHTMLを描画するには、引数に渡されたHtmlTextWriterクラスを使用する。

などがある。

余談となるが、HtmlTextWriter?TextWriter?のDecoratorとして設計されている。

System.Web.UI.Control.Renderメソッド

Renderメソッドには、必要に応じてHTML描画コードをオーバーライドして実装する。

RenderメソッドのHTML描画処理は、更に、以下の3つのメソッドから構成される設計となっているので、必要であれば、こちらのメソッドをオーバーライドすることで、開始タグ、タグ内テキスト、終了タグと局所的にHTML描画処理をカスタマイズできる。

例えば、次のような実装を行うと、コントロールのHTML出力として、 "<test> test </test>" が出力される。RenderContents?メソッドのみオーバーライドすれば、タグ内に出力される文字列の出力のみカスタマイズできる。必要であれば、スタイルの属性・属性値をHTMLタグに反映させること。また、inputタグの場合は、id、nameなどの属性・属性値を出力し、次のポストバックでコントロールが正しく復元されるように実装する必要がある。

/// <summary>RenderBeginTagのテスト</summary>
public override void RenderBeginTag(HtmlTextWriter output) {
  output.Write("<test>");
}

/// <summary>RenderContentsのテスト</summary>
protected override void RenderContents(HtmlTextWriter output) {
  output.Write("test");
}

/// <summary>RenderEndTagのテスト</summary>
public override void RenderEndTag(HtmlTextWriter output) {
  output.Write("</test>");
}

System.Web.UI.Control.RenderControlメソッド

RenderControl?メソッドは、上記のRenderメソッドの制御コード(前・後処理など)を実装する(Renderメソッドの呼び出し自体も実装する)。必要に応じてこれをオーバーライドする。RenderControl?メソッドには、Renderメソッドで描画されたHTMLを取得し、それを変換する処理を実装することも可能である。

DisplayRawText?カスタム プロパティの実装例(Render)

以下は、カスタム プロパティによって、HTMLの描画処理を変更するRenderメソッドの実装例である(変更の必要がない場合は、「base.Render」に処理を委譲する)。興味深いのは、(追加したカスタム プロパティである)DisplayRawText?、Hideプロパティをtrueに設定した場合である。この場合、HTTPレスポンスにコントロールのHTMLが出力されなくなるが、以降DisplayRawText?、Hideプロパティをfalseに戻した場合、コントロールが正しく復元される。これは、このコントロールの復元処理は、ViewStateにより実現されており、ViewStateの情報は、コントロールが出力するinputタグから分離されたViewState専用のHiddenタグに保存されるためである。

/// <summary>
/// Renderメソッドは、
/// ・RenderBeginTag(開始のタグ)
/// ・RenderContents(中間の部分)
/// ・RenderEndTag(終了のタグ)
/// の各メソッドをこの順に呼び出して、コントロールをクライアントに送信する。
/// </summary>
/// <remarks>
/// このメソッドは、表示中にページによって自動的に呼び出される。
/// また、このメソッドは、主にコントロールの開発者によって使用される。
/// </remarks>
protected override void Render(HtmlTextWriter output) {
  // Render処理を作り込む。

  if (this.DisplayRawText) {
    // 生テキストの表示
    output.Write(this.Text);
  }
  else if (this.Hide) {
    // 出力時に隠す(データ自体は保持)
    output.Write("");
  }
  else {
    // 通常通りの出力
    base.Render(output);
  }
}

ToUpper?, ToLower?カスタム プロパティの実装例(RenderControl?

以下は、カスタム プロパティによって、Renderメソッドにより描画されたHTML出力を編集するRenderControl?メソッドの実装例である(編集の必要がない場合は、「base.RenderControl?」に処理を委譲する)。Renderメソッドにより描画されたHTML出力を取得するには、Renderメソッド内部で使用するHtmlTextWriter?を変更する。変更内容は、HtmlTextWriter?が内部で使用するTextWriter?クラス を既定の型から、StringWriter?クラス 型に変更する。これにより、Renderメソッドにより描画されたHTML出力を、RenderControl?メソッド中で取得できる 。

/// <summary>
/// Visibleプロパティ、ページのトレースなどの
/// 制御を行い、ページにコントロールを表示する。
/// </summary>
/// <remarks>
/// このメソッドは、表示中にページによって自動的に呼び出される。
/// カスタム コントロールの開発者はこのメソッドをオーバーライドできる。
/// </remarks>
public override void RenderControl(HtmlTextWriter output) {

  if (this.ToUpper || this.ToLower) {

    // RenderControlにRenderの制御処理を作り込む。

    // StringWriterの書き出し先のStringBuilderを生成
    StringBuilder sb = new StringBuilder();
    // StringWriterを使用するStringWriterを生成
    StringWriter sw = new StringWriter(sb);

    // 上記StringWriterを使用するHtmlTextWriterを生成
    HtmlTextWriter htw = new HtmlTextWriter(sw);

    // StringWriterを使用するHtmlTextWriterを指定し、Renderメソッドを実行する。
    this.Render(htw);

    // RenderメソッドでRenderされた、コントロールのHTMLを
    // StringWriterに指定したStringBuilderから取得する。
    string html = sb.ToString();

    // HTMLの編集処理
    if (this.ToUpper) {
      html = html.ToUpper();
    }
    else if (this.ToLower)
    {
      html = html.ToLower();
    }
    else
    { 
      // ここは通らない。
    }

    // ページにコントロールを表示
    output.Write(html);
  }
  else {
    // ベースのRenderControlを使用する。
    base.RenderControl(output);
  }
}

コンストラクタ

コンストラクタについては自由な利用が可能である。

ここではスタイル関係のプロパティに初期値を設定。

/// <summary>コンストラクタ</summary>
/// <summary>コンストラクタでプロジェクトなどでの標準スタイルを適用する。</summary>
public WebCustLabel()
{
  // 初期設定のプロパティ値を設定する。
  // ※ デザインタイム・プロパティのほうが優先される。
  this.Font.Size = 12;
  this.ForeColor = System.Drawing.Color.Red;
  this.Font.Name = "MS ゴシック";
}

デザイナとコンストラクタのコードが干渉する問題

カスタム サーバ コントロールのメタデータ属性

http://msdn.microsoft.com/ja-jp/library/ms178658.aspx

Windows Forms

クラス定義

以下は、ラベル コントロール(System.Windows.Forms.Label )を継承したカスタム ラベル コントロール(Ctrl.WinCustLabel?)のクラス定義。

namespace Ctrl
{
  /// <summary>System.Windows.Forms.Labelのカスタム・コントロール</summary>
  [DefaultProperty("Text")]
  public class WinCustLabel : Label

プロパティ

Windows Formsアプリケーションのカスタム コントロールでは、カスタム プロパティのうち、表示に影響を与えるものがあったら、カスタム プロパティのsetterメソッドでRefreshメソッド を実行する必要がある。また、ASP.NET Web Formsでは、状態保持のためにViewStateを使用したが、Windows Formsアプリケーションでは通常通りメンバ変数を使用する。

/// <summary>文字列反転</summary>
private bool _Reverse = false;

/// <summary>テキストを反転するかどうかを示す。</summary>
[DefaultValue("false"),
Category("動作"),
Description("テキストを反転するかどうかを示します。")]
public bool Reverse {
  set {
    this._Reverse = value;

    // 表示に影響を与えるプロパティの
    // setterにはRefreshメソッドを仕込む。
    this.Refresh();
  }
  get { return this._Reverse; }
}

表示に影響を与えるプロパティのgetter

Windows Formsアプリケーションのカスタム コントロールのプロパティのカスタマイズは、基本的にASP.NET Web Formsのコントロールのカスタマイズ方法と変わらないが、表示に影響を与えるプロパティのgetterメソッドのみ変更するカスタマイズを施すと、コントロールが認識している描画領域・位置と合わなくなり、すべての文字が描画されないなどの問題が発生する。

故に、Windows Formsアプリケーションのカスタム コントロールでは、描画にかかわるプロパティのgetterメソッドのみをカスタマイズすることは不可能である。例えば、getterメソッド内でsetterメソッドを呼び出し、描画領域・位置の再計算・再描画させるという方法も、このRefreshメソッドによる描画領域・位置の再計算・再描画にて、getterメソッド呼び出すため、getter → setter → 再描画(Refresh) → getterの無限再起(スタック オーバーフロー)に陥る。このため、このアプローチは上手くいかない。対処としては、setterメソッドに実装を移す必要がある。

イベント

イベントハンドラの追加

リッチクライアントであるWindows Formsではウィンドウ メッセージを使用したイベントドリブンの制御を行うため、共通的な処理をカスタム コントロール内に同梱することも可能である。以下は、フォーカスが当たった際に入力されたテキストを全選択するサンプル(プログラムやタブで遷移した時だけテキストを全選択し、マウスクリックで遷移した時は選択しないように制御している)。

namespace Ctrl {
  /// <summary>System.Windows.Forms.TextBoxのカスタム・コントロール</summary>
  [DefaultProperty("Text")]
  public class WinCustTextBox : TextBox {
  /// <summary>コンストラクタ</summary>
    public WinCustTextBox() {
      this.InitializeComponent();
    }
    /// <summary>初期化</summary>
    private void InitializeComponent() {
      this.SuspendLayout();
      this.Enter += new System.EventHandler(this.WinCustomTextBox_Enter); 
      this.Leave += new System.EventHandler(this.WinCustomTextBox_Leave);
      this.MouseDown += new System.Windows.Forms.MouseEventHandler(this.WinCustomTextBox_MouseDown);
      this.ResumeLayout(false);
    }

    /// <summary>MouseDown状態の確認用フラグ</summary>
    private bool IsMouseDown = false;

    /// <summary>マウスが入った</summary>
    private void WinCustomTextBox_MouseDown(object sender, MouseEventArgs e) {
      this.IsMouseDown = true;
    }

    /// <summary>フォーカス</summary>
    private void WinCustomTextBox_Enter(object sender, EventArgs e) {
      if (!this.IsMouseDown) {
        // MouseDown状態で無ければ全選択
        this.SelectAll();
      }
    }

    /// <summary>ロスト フォーカス</summary>
    private void WinCustomTextBox_Leave(object sender, EventArgs e) {
      this.IsMouseDown = false;
    }
  }
}

カスタムイベント

カスタムイベントの作成方法

UIのカスタマイズ

オーナードロー(オーナー描画)なので難易度が高い。

  • コントロールの描画を独自で行う方法。
  • OnPaint?メソッドをオーバーライドする。

OnPaint?メソッド

ここで出力されるHTMLのカスタマイズは、Render系メソッドのカスタマイズにより対応する。

カスタム コントロールをControlクラスから派生させてスクラッチ開発する場合は、OnPaint?メソッドもスクラッチ開発するが、
それ以外の場合は、必要に応じて動作変更するようにして、動作変更の必要がない場合は、処理を基本クラスの「base.xxxx」メソッドに委譲するような方法を採る。

  • 参考
    • 方法 : カスタム イメージのボタン コントロールを作成する
      http://msdn.microsoft.com/ja-jp/library/ms172532.aspx
      // Override the OnPaint method to draw the background image and the text.
      protected override void OnPaint(PaintEventArgs e)
      {
          if(this.pressed && this.pressedImage != null)
              e.Graphics.DrawImage(this.pressedImage, 0, 0);
          else
              e.Graphics.DrawImage(this.backgroundImage, 0, 0);
      
          // Draw the text if there is any.if(this.Text.Length > 0)
          {
              SizeF size = e.Graphics.MeasureString(this.Text, this.Font);
      
              // Center the text inside the client area of the PictureButton.
              e.Graphics.DrawString(this.Text,
                  this.Font,
                  new SolidBrush(this.ForeColor),
                  (this.ClientSize.Width - size.Width) / 2,
                  (this.ClientSize.Height - size.Height) / 2);
                  }
      
          // Draw a border around the outside of the // control to look like Pocket PC buttons.
          e.Graphics.DrawRectangle(new Pen(Color.Black), 0, 0, 
              this.ClientSize.Width - 1, this.ClientSize.Height - 1);
      
          base.OnPaint(e);
      }

クリッピング

部分描画により、性能を向上させることができる。

オーナー描画をサポートするコントロール

  • 組み込みのオーナー描画サポートを備えたコントロール
    https://msdn.microsoft.com/ja-jp/library/yyfab68k.aspx

    オーナー描画によるカスタマイズを含んだ再使用可能なコントロールを作成するには、オーナー描画をサポートするコントロール クラスから派生した新しいクラスを作成します。その後、描画イベントを処理する代わりに、新しいクラス内の適切な OnEventName? メソッドのオーバーライドにオーナー描画コードを記述します。このとき、必ず基本クラスの OnEventName? メソッドを呼び出すようにしてください。これは、このコントロールのユーザーがオーナー描画イベントを処理してさらなるカスタマイズを実行できるようにするための配慮です。

コンストラクタ

コンストラクタについては自由な利用が可能である。

デザイナとコンストラクタのコードが干渉する問題

メソッド・プロパティをオーバーライドできない場合

派生クラスでオーバーライドが可能なように、virtual / abstractキーワード(VBのキーワードでは、Overridable / MustOverride?に該当)が設定されておらず、カスタマイズができない場合、この場合、newキーワード により隠蔽・置換が可能であるが、.NETフレームワークによりコントロールがベースの型によってハンドルされると、カスタマイズにより追加した機能が動作しないなどの問題が発生する。

例えば、newキーワードによりTextプロパティを上書きし、Renderメソッドのオーバーライド中で「base.Render」を呼び出した場合、「base.Render」中では「base.Text」が呼ばれてしまい、newキーワードにより上書きした「this.Text」が呼ばれなくなる。このため、思い通りに動作しなくなる。

このようなコントロールは、当該プロパティのカスタマイズをサポートした再使用可能なコントロールとして作成されておらず、また、newキーワード(VBのキーワードでは、Shadowsに該当)によるメソッド・プロパティの上書きは、問題の発見を遅れさせ、かつ原因特定などを困難にする可能性があるので、避けるようにする。

スクラッチ開発

下記のコントロールの基本クラスから継承し、カスタム コントロールをスクラッチ開発する方法がある。

参考

Webカスタム コントロールの開発

Webカスタム コントロール開発に関する参考資料

Windows Formsカスタム コントロールの開発

Windows Formsカスタム コントロール開発に関する参考資料

PropertyGrid?の活用

PropertyGrid?コントロールに関する参考資料

Open棟梁のカスタムコントロール


Tags: :.NET開発, :UIサブシステム, :Windows Forms, :ASP.NET Web Forms


トップ   編集 凍結 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2018-06-14 (木) 17:39:21 (7d)