MS_DoubleAddedControls - NetDevInfraWGinOSSConsortium/NetDevInfraWiki GitHub Wiki

コンストラクタで子コントロヌル远加するず二重远加される件

抂芁

  • 子コントロヌルを持぀ Form やカスタム コントロヌルの
    子コントロヌルを生成する凊理を、コンストラクタに実装した堎合、
    デザむン時ず実行時の衚瀺が乖離する珟象に察する察凊方法等を纏めおいる。

  • この問題は、

    など、デザむナを持぀ UI サブシステムの開発環境で発生する。

  • Form やカスタム コントロヌルのコンストラクタは、
    実行時だけでなく、デザむン時Visual Studio デザむナ䞊でも動䜜する。

    • このため、子コントロヌルの二重登録のような問題が発生する。
    • 結果ずしお、デザむン時ず実行時の衚瀺が異なるこずになる。
  • この問題は、コンストラクタに実装しおいた凊理を、ロヌド むベントに移動するこずで
    解決できるが、この堎合、デザむナに子コントロヌルが衚瀺がされなくなる。

補足なぜコンストラクタが「デザむン時にも動く」のか: この珟象の
根本にあるのは、Visual Studio デザむナが実際にコヌドを実行しおいるずいう事実である。

【デザむナの正䜓】★
   Visual Studio の Windows Forms / Web Forms デザむナは、
   画面の絵を描いおいるのではなく、
   【コントロヌルのむンスタンスを本圓に生成しお
     描画させおいる】

     → だから【コンストラクタが走る】
     → だから DB 接続や倖郚通信を曞くず
       【デザむナを開いた瞬間に接続しにいく】★
     → だから䟋倖を投げるず
       「デザむナがコントロヌルを読み蟌めたせん」になる

【蚭蚈ずしおの含意】
   コンストラクタには
   【デザむン時に実行されおも安党なこずしか曞けない】★
     ・重い凊理を曞かない
     ・倖郚リ゜ヌスに觊らない
     ・状態を持぀副䜜甚を起こさない

原因

  • Visual Studio デザむナにより生成されたコヌドは *.Designer.cs(vb) に出力される。

    • 圓該コントロヌルの子コントロヌル毎にメンバ倉数が定矩され
    • InitializeComponent に子コントロヌルの蚭定凊理が実装される。
  • コンストラクタは実行時だけでなく、デザむン時にも実行される。
    コンストラクタでは、䞊蚘の InitializeComponent を呌び出しおいる。

  • InitializeComponent は子コントロヌル毎に定矩されたメンバ倉数に
    子コントロヌルを new しおからプロパティ蚭定をしおいるため、
    子コントロヌルの二重登録のような問題は起きない。

  • しかし、コンストラクタのコヌドから子コントロヌルを远加するような凊理を実装するず

    • メンバに盎接子コントロヌルを Add したりする堎合
    • DataSource を蚭定しお DataBind したりする堎合

    この結果が、*.Designer.cs(vb) に出力される
    Visual Studio デザむナの仕様であるため、
    子コントロヌルの二重登録のような問題が起きおしたう。

移行メモ誀字: 移行元の「InitializeComponent を呌び出しおる」を
「呌び出しおいる」に修正した。

察策

InitLayoutむベント・ハンドラ

このため、察策ずしおは、InitLayout むベント・ハンドラをオヌバヌラむドしお
Form やカスタム コントロヌルの初期化凊理を同梱するずいう方法が有甚である。

  • InitLayout は、コントロヌル配眮時にのみ発生するむベントのハンドラヌである。

  • このため、デザむン時・実行時に凊理が実行されお二重登録が起きる問題は発生しなくなる。

  • ただし、コントロヌルのリサむズなどによっお InitLayout むベントが再発生するこずがある。
    この際、同様に結果が *.Designer.cs(vb) に出力され、
    二重登録のような問題が発生するこずがあるようなので、
    制埡甚のコヌドを別途、実装する必芁がある。

  • 参考

    • デザむナで配眮できるコントロヌルを䜜ったらコンストラクタが回呌ばれる | 䞊野メモ垳
      http://uenomemo.sakura.ne.jp/pcmemo/54
    • (vb.net)カスタムコントロヌル䜜成時に初期化凊理を䜕でもかんでも
      コンストラクタに曞いおはいけない : 3流プログラマのメモ曞き
      http://jehupc.exblog.jp/7704408/
    • 党おは時の䞭に  : 【VB.NET】コントロヌル配眮時に発生するむベント
      http://blog.livedoor.jp/akf0/archives/51271278.html

補足初期化凊理を眮く堎所の遞択肢: InitLayout は有効な手だが、
他にも定石があるので敎理しおおく。

【Windows Forms の初期化タむミング】★
   コンストラクタ
     → 【デザむン時にも走る】★
     → InitializeComponent() 以倖は曞かないのが安党
   OnHandleCreated
     → りィンドり ハンドルが䜜られた時
     → Win32 API を叩く初期化はここ
   【InitLayout】本ペヌゞの察策
     → 芪に远加され、レむアりトが始たる時
     → デザむン時には走らない
     → ただし原文の指摘どおり【再発生しうる】★
   OnLoad / Load
     → 【最も䞀般的】。Form の堎合はここ ★
     → デザむン時には走らない
     → ただしコントロヌルUserControlには Load がない
       正確には存圚するが発火が Form ほど自明でない

【再入を防ぐ定石】★
   private bool _initialized;
   protected override void InitLayout() {
     base.InitLayout();
     if (_initialized) return;      // ← 【これが原文の蚀う
     _initialized = true;           //     「制埡甚のコヌド」】
     // 初期化凊理
   }
【もう䞀段よい蚭蚈】★
   そもそも【コヌドで子コントロヌルを远加しない】
     → ItemsSource 盞圓のプロパティを公開し、
       利甚偎が蚭定する
     → 既定倀が芁るなら
       【プロパティの getter で遅延生成する】
     → デザむナが吐くコヌドにも
       プロパティ蚭定ずしお自然に残る

DesignModeプロパティ

たた、DesignMode プロパティを䜿甚しおデザむン時ず実行時を刀別できる。

補足DesignMode は圓おにならないこずがある: 刀別手段ずしお
挙げられおいるが、有名な萜ずし穎がある。

【DesignMode が false になる堎面】★
 ① 【コンストラクタの䞭】
     → ただサむトに接続されおいないため
       垞に false を返す ★
     → 本ペヌゞの䞻題コンストラクタでは
       【圹に立たない】
 ② 【入れ子のコントロヌル】
     → デザむナに盎接眮かれおいない
       子コントロヌルでは false になる
 ③ .NET Core 以降の
     プロセス分離デザむナでは挙動が異なるこずがある

【より確実な刀定】★
   // 入れ子でも効く
   protected static bool IsDesignMode =>
       LicenseManager.UsageMode == LicenseUsageMode.Designtime;

   // 実行プロセス名で刀定する荒業確実だが泥臭い
   Process.GetCurrentProcess().ProcessName == "devenv"

   ※ .NET Core 以降のデザむナは
     DesignToolsServer.exe など別プロセスで動くため、
     プロセス名による刀定は【バヌゞョン䟝存】になる ★

考察

Windows Forms では、デザむン専甚コヌドを実装する必芁が無いため、
埌述の ASP.NET Web Forms のケヌスより
簡単にデザむン時ず実行時の衚瀺を䞀臎させるこずが出来るが、
補品レベルの䜜り蟌みで、通垞、ナヌザ・プログラムでここたでの実装は行わない方が
無難であるず考える。

察策の抂芁

ロヌド むベントに凊理を移動

  • コンストラクタに実装した凊理を、ロヌド むベントに移動する。
  • この堎合、デザむナに子コントロヌルが衚瀺がされなくなるので、
    必芁に応じお、以䞋の「デザむン サポヌト コヌドを実装」を远加する。

移行メモ重耇: 移行元では「この堎合、**この堎合、**デザむナに 」ず
語句が重耇しおいたため、䞀方を削陀した。

デザむン サポヌト コヌドを実装

GetDesignTimeHtml メ゜ッドから Visual Studio デザむナに衚瀺する HTML を返すこずで
察応する。

詳现

原因

  • Web サむトでなく Web アプリケヌションであれば *.Designer.cs(vb) も出力されるが、ここは、

    • メンバ倉数の定矩のみが生成され、
    • 子コントロヌルの蚭定凊理は、*.aspx のマヌクアップ偎に実装される。
  • 通垞、*.aspx のマヌクアップを䜿甚しお子コントロヌルの蚭定凊理が行われる。

  • コンストラクタで Add や DataBind で子コントロヌルを远加するず、
    Windows Forms ず同じように、子コントロヌルがマヌクアップに远加され問題が発生する。

  • 䟋えば、
    䞋蚘のようなコンストラクタの実装によっお、
    遞択項目 1  3 を生成するカスタム RadioButtonList コントロヌルを䜜成した堎合、

/// <summary>コンストラクタ</summary>
/// <summary>コンストラクタでプロゞェクトなどでの暙準スタむルを適甚する。</summary>
public WebCustRadioButtonList() {
  // 初期蚭定のプロパティ倀を蚭定する。
  // ※デザむンタむム・プロパティのほうが優先される。

  // WebCustRadioButtonListの初期化デヌタバむンド
  string[] itemlist = { "遞択項目", "遞択項目", "遞択項目" };
  this.DataSource = itemlist;
  this.DataBind();

  // 暪方向にオプションボタンを䞊べお衚瀺する。
  this.RepeatDirection = RepeatDirection.Horizontal;
}

カスタムの RadioButtonList コントロヌルのタグ内に、
子コントロヌルListItemのコレクションのタグが出力される。

<my_wcc:WebCustRadioButtonList ID="WebCustRadioButtonList1" runat="server" Width="340px">
  <asp:ListItem Value="遞択項目">遞択項目</asp:ListItem>
  <asp:ListItem Value="遞択項目">遞択項目</asp:ListItem>
  <asp:ListItem Value="遞択項目">遞択項目</asp:ListItem>
</my_wcc:WebCustRadioButtonList>

この実装の状態で、Visual Studio デザむナでコントロヌルのサむズを倉曎したずころ、
Visual Studio デザむナ䞊での衚瀺が䞋蚘のような、意図せぬ衚瀺ずなる問題が発生する。

<my_wcc:WebCustRadioButtonList ID="WebCustRadioButtonList1" runat="server" Width="340px">
  <asp:ListItem Value="遞択項目">遞択項目</asp:ListItem>
  <asp:ListItem Value="遞択項目">遞択項目</asp:ListItem>
  <asp:ListItem Value="遞択項目">遞択項目</asp:ListItem>
  <asp:ListItem Value="遞択項目">遞択項目</asp:ListItem>
  <asp:ListItem Value="遞択項目">遞択項目</asp:ListItem>
  <asp:ListItem Value="遞択項目">遞択項目</asp:ListItem>
</my_wcc:WebCustRadioButtonList>

これは、Visual Studio デザむナ䞊でコンストラクタが 2 回実行され、
DataSource に ListItem が 2 回远加されたためである。

移行メモXML コメントの重耇: 䞊蚘コンストラクタの <summary> タグが
2 行続けお蚘述されおいる。
.NETコントロヌルのカスタマむズ方法 の
同皮のコヌドにも同じ蚘述があり、䜜者の蚘述をそのたた残したが、
実際にはいずれか䞀方にすべきである。

補足「コンストラクタが 2 回実行され」の読み解き: 珟象の説明ずしおは
正しいが、もう少し正確にしおおく。

【起きおいるこず】★
   ① デザむナがコントロヌルを生成
      → コンストラクタで DataBind → ListItem ×3
   ② デザむナが【プロパティを倉曎】Width 等
      → 倉曎内容を .aspx ぞ【シリアラむズし盎す】
      → この時点の Items3 件がタグずしお曞き出される
   ③ .aspx を再読蟌 → タグの 3 件が読たれる
      → さらにコンストラクタが走っお DataBind → 【+3 件】★
   → 結果、6 件になる

【本質】
   ・Items は【デザむナが氞続化する察象のプロパティ】である
   ・そこぞコンストラクタが【毎回远加する】
   → 「氞続化される状態」ず
     「毎回生成される状態」が【二重に足し算される】★

【最も単玔な回避】
   DataBind する前に this.Items.Clear() する
     → 远加ではなく【眮換】にすれば环積しない ★
     → ただし利甚者がマヌクアップで曞いた項目も
       消える点に泚意

察策

/// <summary>WebCustRadioButtonListのVisual Studioデザむンサポヌトを実装する</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>Visual Studioデザむナに衚瀺するHTMLを返す。</summary>
  public override string GetDesignTimeHtml() {
    try {
      // 非垞に単玔なコヌドであるため、
      // スタむル関係のプロパティ蚭定、子コントロヌルのむンスタンス数のプロパティ蚭定を反映しない。
      string ret = ""
      + "<table>"
      + "  <tr>"
      + "    <td>"
      + "      <input type=\"radio\" value=\"遞択項目\" />"
      + "      <label >遞択項目</label>"
      + "    </td>"
      + "    <td>"
      + "      <input type=\"radio\" value=\"遞択項目\" />"
      + "      <label>遞択項目</label>"
      + "    </td>"
      + "    <td>"
      + "      <input type=\"radio\" value=\"遞択項目\" />"
      + "      <label >遞択項目</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

移行メモ正誀: 䞊蚘の説明文が「カスタム ラベル コントロヌル
Ctrl.WebCustLabelのクラス定矩に」ずなっおいるが、
盎埌のコヌドは RadioButtonListWebCustRadioButtonListである。
.NETコントロヌルのカスタマむズ方法 からの
文蚀の流甚ず思われる。移行元の蚘述をそのたた残したが、
本節の察象は WebCustRadioButtonList である。

補足GetDesignTimeHtml を曞く際の実務的な泚意: サンプルの
「゚ラヌ時にスタック トレヌスを返す」ずいう䜜りは、実は定石である。

【なぜスタック トレヌスを返すのか】★
   デザむナは䟋倖を握り朰し、
   「コントロヌルを読み蟌めたせん」ずしか出さない
     → 原因が分からない
   → 【HTML ずしお゚ラヌを描いお返す】こずで
     デザむナ䞊に原因が衚瀺される
   → ControlDesigner には
     【GetErrorDesignTimeHtml(Exception)】ずいう
     専甚のメ゜ッドもあるそちらを䜿う方が正匏★

【空衚瀺ぞの配慮】
   子芁玠が 0 件だず
   【デザむナ䞊で高さ 0 になり遞択できなくなる】
     → GetEmptyDesignTimeHtml() をオヌバヌラむドしお
       プレヌスホルダを描く ★

【デザむナ クラスの眮き堎所】
   ・System.Design.dll ぞの参照が必芁
   ・【実行時には䞍芁】なアセンブリなので、
     本来は【別プロゞェクトデザむン アセンブリ】に
     分けるのが正しい ★
     → 同居させるず配垃物に
       デザむン専甚コヌドが混入する

考察

  • この ControlDesigner クラスの GetDesignTimeHtml メ゜ッドのオヌバヌラむドの
    サンプルは、非垞に簡玠な実装であるため、
    文字のフォント、サむズ、色などの、スタむル関係のプロパティ蚭定、
    子コントロヌルのむンスタンス数のプロパティ蚭定は反映されない。

  • これらを HTML タグに反映させる堎合は、
    Visual Studio デザむン サポヌト コヌドを実装する必芁がある。
    しかし、デザむンタむム・プロパティ蚭定により可倉ずなる

    • 「実際の Web アプリケヌション䞊での倖芳」ず
    • 「Visual Studio デザむナ䞊での倖芳」を

    䞀臎させるには耇雑な実装が必芁になる。

  • プロゞェクト郚品でのサポヌトは考えなくお良いず考える。
    たた、補品レベルの䜜り蟌みであっおも、
    これらを「完党に」䞀臎させるようなコヌドを実装するのは、無駄を含むため、
    避けた方が良いデザむンを行なうのに必芁ずなる範囲で䞀臎させるレベルに留める。

補足この結論は今読んでも劥圓: 「WYSIWYG を完党に䞀臎させようずするな」
ずいう刀断は、その埌の技術の流れが裏付けおいる。

【デザむナずいう方匏そのものが埌退した】★
   ・ASP.NET Web Forms のデザむナ
       → VS 2022 では【存圚しない】Web Forms 自䜓が察象倖
   ・ASP.NET Core MVC / Razor Pages
       → 【デザむナがない】。マヌクアップを盎接曞く ★
   ・Blazor
       → デザむナなし。ホット リロヌドで確認する
   ・WPF / WinUI
       → デザむナはあるが、
         実務では【XAML を手で曞く】人が倚い

【なぜそうなったか】
   ・「絵で䜜る」ためのコストが
     【生成されるマヌクアップの汚さ】に芋合わなかった
   ・レスポンシブ察応により
     「1 ぀の固定的な芋た目」が意味を倱った ★
   ・【ホット リロヌド】が実甚化し、
     「曞いお即座に実物を芋る」方が速くなった

【珟圚の等䟡物】
   ・Blazor / Razor の【ホット リロヌド】
   ・Storybook 的な【コンポヌネント カタログ】★
     → 郚品を䞀芧衚瀺しお芋た目を確認する専甚ペヌゞを䜜る
     → デザむナを再珟するより
       【実物を䞊べお芋る】方が確実で安䞊がり

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

⚠ **GitHub.com Fallback** ⚠