Open棟梁Project - マイクロソフト系技術情報 Wiki
.NET Frameworkでは、画面の開発に必要となるコントロールが用意されている。
.NETでは、これらのコントロールの動作をカスタマイズすることで、特定プロジェクト向けにカスタマイズされた
(例えば、自動編集処理、自動入力チェック処理、サニタイジング処理、などの機能を有した)コントロール共通部品開発が可能である。
ここでは、この.NETコントロール(ASP.NET Web Forms, Windows Forms)のカスタマイズ方法について説明する。
ASP.NET Web Formsで使用するコントロールの特徴は、Webアプリケーションであるために、
という点である。
以下は、ラベル コントロール(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> /// <summary>コンストラクタでプロジェクトなどでの標準スタイルを適用する。</summary> public WebCustLabel() { // 初期設定のプロパティ値を設定する。 // ※ デザインタイム・プロパティのほうが優先される。 this.Font.Size = 12; this.ForeColor = System.Drawing.Color.Red; this.Font.Name = "MS ゴシック"; }
コントロールのコンストラクタは、VSデザイナ上で機能するアーキテクチャとなっている。
このため、下記のようなコンストラクタの実装によって、選択項目1~3を生成するカスタムのRadioButtonList?コントロールを作成した場合、
/// <summary>コンストラクタ</summary> /// <summary>コンストラクタでプロジェクトなどでの標準スタイルを適用する。</summary> public WebCustRadioButtonList() { // 初期設定のプロパティ値を設定する。 // ※デザインタイム・プロパティのほうが優先される。 // WebCustRadioButtonListの初期化(データバインド) string[] itemlist = { "選択項目1", "選択項目2", "選択項目3" }; this.DataSource = itemlist; this.DataBind(); // 横方向にオプションボタンを並べて表示する。 this.RepeatDirection = RepeatDirection.Horizontal; }
カスタムのRadioButtonList?コントロールのタグ内に、子コントロール(ListItem?)のコレクションのタグが出力される。
<my_wcc:WebCustRadioButtonList ID="WebCustRadioButtonList1" runat="server" Width="340px"> <asp:ListItem Value="選択項目1">選択項目1</asp:ListItem> <asp:ListItem Value="選択項目2">選択項目2</asp:ListItem> <asp:ListItem Value="選択項目3">選択項目3</asp:ListItem> </my_wcc:WebCustRadioButtonList>
この実装の状態で、VSデザイナでコントロールのサイズを変更したところ、VSデザイナ上での表示が下記のような、意図せぬ表示となる問題が発生する。
<my_wcc:WebCustRadioButtonList ID="WebCustRadioButtonList1" runat="server" Width="340px"> <asp:ListItem Value="選択項目1">選択項目1</asp:ListItem> <asp:ListItem Value="選択項目2">選択項目2</asp:ListItem> <asp:ListItem Value="選択項目3">選択項目3</asp:ListItem> <asp:ListItem Value="選択項目1">選択項目1</asp:ListItem> <asp:ListItem Value="選択項目2">選択項目2</asp:ListItem> <asp:ListItem Value="選択項目3">選択項目3</asp:ListItem> </my_wcc:WebCustRadioButtonList>
/// <summary>WebCustRadioButtonListのVSデザインサポートを実装する</summary> internal class WebCustRadioButtonListDesigner : ControlDesigner { /// <summary>メンバ変数にWebCustRadioButtonListを保持</summary> protected WebCustRadioButtonList wcrbl; /// <summary>初期化</summary> public override void Initialize(IComponent component) { if (component is WebCustRadioButtonList) { base.Initialize(component); this.wcrbl = (WebCustRadioButtonList)component; } } /// <summary>VSデザイナに表示するHTMLを返す。</summary> public override string GetDesignTimeHtml() { try { // 非常に単純なコードであるため、 // デザインなどの情報を含めることはしていない。 string ret = "" + "<table>" + " <tr>" + " <td>" + " <input type=\"radio\" value=\"選択項目1\" />" + " <label >選択項目1</label>" + " </td>" + " <td>" + " <input type=\"radio\" value=\"選択項目2\" />" + " <label>選択項目2</label>" + " </td>" + " <td>" + " <input type=\"radio\" value=\"選択項目3\" />" + " <label >選択項目3</label>" + " </td>" + " </tr>" + "</table>"; return ret; } catch (Exception ex) { // エラーの場合 return String .Concat("<h3>Error</h3>Stack Trace:<br>", ex.StackTrace); } } }
最後に、上記のデザイナ クラスを使用するように、カスタム ラベル コントロール(Ctrl.WebCustLabel?)のクラス定義に「Designer」 属性を使用してデザイナ クラスを指定する。
/// <summary>System.Web.UI.RadioButtonListのカスタム・コントロール</summary> [Designer("Ctrl.WebCustRadioButtonListDesigner"), ToolboxData("<{0}:WebCustRadioButtonList runat=server></{0}:WebCustRadioButtonList>")] public class WebCustRadioButtonList : RadioButtonList
ただし、このサンプルは非常に簡素な実装であるため、文字のフォント、サイズ、色など、スタイル指定は一切反映されない。このため、スタイルの属性・属性値をHTMLタグに反映させる場合は、このControlDesigner?クラスのGetDesignTimeHtml?メソッドのオーバーライドに、スタイルのサポート コードを実装する必要がある。しかし、WebアプリケーションにおけるVSデザイン サポート コード は、かなり複雑な実装が必要になるため、プロジェクト部品では、プロパティの設定により可変となる「実際のWebアプリケーション上での外観」と、「VSデザイナ上での外観」を一致させようと考えないほうがよい。また、製品レベルの作り込みであっても、これらを「完全に」一致させるようなコードを実装するのは、無駄を含むため、避けた方が良い。
本項ではカスタム プロパティの追加方法について説明する。
カスタム プロパティを追加する際は、通常通りにプロパティ プロシージャを実装すれば良い。
/// <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"]; } }
メンバ名 | 説明 | デフォルト値 |
DefaultValue? | デフォルト値を表す。 | - |
Browsable | プロパティをPropertyGrid?に表示するかどうかを指定する。 | True |
Category | プロパティをまとめるカテゴリ。 カテゴリには、次の共通カテゴリがある。 ・Appearance:表示 ・Behavior:動作 ・Data:データ | - |
Description | プロパティの設定を行う際の参考としてPropertyGrid?に表示される説明。 | - |
以下は、Textプロパティをオーバーライドしカスタム動作を実装するサンプル(テンプレート)である。
/// <summary>Textプロパティに機能を追加実装する。</summary> public override string Text { // ベースのプロパティを使用する。 set { string temp = value; // カスタム動作の処理を実装する。 base.Text = temp; } get { string temp = base.Text; // カスタム動作の処理を実装する。 return temp; } }
/// <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; } }
ASP.NET Web Formsで使用するコントロールの特徴は、Webアプリケーションであるために、表示はHTMLにより行われるのでカスタマイズしやすい。
出力されるHTMLは、Render系メソッドのカスタマイズにより対応する。
カスタム コントロールをスクラッチ開発する場合は、これらのメソッドもスクラッチ開発するが、それ以外の場合は、必要に応じて動作変更するようにして、動作変更の必要がない場合は、処理を基本クラスの「base.xxxx」メソッドに委譲するような方法を採る。
Render系メソッドにおいてHTMLを描画するには、引数に渡されたHtmlTextWriterクラスを使用する。
などがある。
余談となるが、HtmlTextWriter?は TextWriter?のDecoratorとして設計されている。
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>"); }
RenderControl?メソッドは、上記のRenderメソッドの制御コード(前・後処理など)を実装する(Renderメソッドの呼び出し自体も実装する)。必要に応じてこれをオーバーライドする。RenderControl?メソッドには、Renderメソッドで描画されたHTMLを取得し、それを変換する処理を実装することも可能である。
以下は、カスタム プロパティによって、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); } }
以下は、カスタム プロパティによって、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); } }
http://msdn.microsoft.com/ja-jp/library/ms178658.aspx
××カスタムイベント
派生クラスでオーバーライドが可能なように、virtual / abstractキーワード(VBのキーワードでは、Overridable / MustOverride?に該当)が設定されておらず、カスタマイズができない場合、この場合、newキーワード により隠蔽・置換が可能であるが、.NETフレームワークによりコントロールがベースの型によってハンドルされると、カスタマイズにより追加した機能が動作しないなどの問題が発生する。
例えば、newキーワードによりTextプロパティを上書きし、Renderメソッドのオーバーライド中で「base.Render」を呼び出した場合、「base.Render」中では「base.Text」が呼ばれてしまい、newキーワードにより上書きした「this.Text」が呼ばれなくなる。このため、思い通りに動作しなくなる。
newキーワード(VBのキーワードでは、Shadowsに該当)によるメソッド・プロパティの上書きは、問題の発見を遅れさせ、かつ原因特定などを困難にする可能性があるので、避けるようにする。
下記のコントロールの基本クラスから継承し、カスタム コントロールをスクラッチ開発する方法がある。
Webカスタム コントロール開発に関する参考資料
Windowsカスタム コントロール開発に関する参考資料
PropertyGrid?コントロールに関する参考資料