2009年9月27日日曜日

UserControl の Load イベントハンドラーが呼ばれない

.NET Framework2.0 SP2、.NET Framework3.5 SP1 で試したところ
UserControl に WebBrowser コントロールを貼り付けると、
Load イベントハンドラーが呼ばれません。

private void UserControl1_Load(object sender, EventArgs e)
{
    MessageBox.Show("呼ばれない orz");
}


このデリゲート(UserControl1_Load) を呼ぶのは、
ベースクラスの OnLoad() メソッドです。
というわけで、OnLoad() が呼ばれないのかな?と思い、
オーバーライドしてブレークを張ったところ、ちゃんと呼ばれています。

OnLoad() で追加したイベントハンドラーを呼ばれないなんてことがあるの?
など思いつつ Reflector で UserControl.OnLoad() を覗いてみました。
実装は次のようになっています。

protected virtual void OnLoad(EventArgs e)
{
  EventHandler handler = (EventHandler)base.Events[EVENT_LOAD];
  if (handler != null)
  {
    handler(this, e);
  }
}

実際、OnLoad() でブレイクして
base.Events[EVENT_LOAD] をウォッチしてみると
やっぱり null になっていました。
これで、イベントハンドラーが呼ばれないのも納得です。

だって呼んでいないんだもの (T ^ T)

では、なぜ呼ばれないか?
ブレイク張ってみると一目瞭然。張るところは当然

1.) this.Load += new System.EventHandler(this.UserControl1_Load);
2.) UserControl1.OnLoad()

UserControl に WebBrowser を貼り付けなければ
1 -> 2 と正しい順序で動きます。
一方、WebBrowser を張ると、2 -> 1 の順番で動きます。
どうやら this.Load += ... より前に
OnLoad() を呼んでしまう処理があるようです。
InitializeComponent() の先頭でブレイクし、
ステップ実行していくと・・・ありました。

this.Controls.Add(this.webBrowser1);
この後、UserControl1.OnLoad() が実行されてしまいました。

Refrelctor で今度は ControlCollection.Add() メソッドを覗いてみました。

public virtual void Add(Control value)
{
  if (value != null)
  {
    if (value.GetTopLevel())
    {
      throw new ArgumentException(SR.GetString("TopLevelControlAdd"));
    }
    if (this.owner.CreateThreadId != value.CreateThreadId)
    {
      throw new ArgumentException(SR.GetString("AddDifferentThreads"));
    }
    Control.CheckParentingCycle(this.owner, value);
    if (value.parent == this.owner)
    {
      value.SendToBack();
    }
    else
    {
      if (value.parent != null)
      {
        value.parent.Controls.Remove(value);
      }
      base.InnerList.Add(value);
      if (value.tabIndex == -1)
      {
        int num = 0;
        for (int i = 0; i < (this.Count - 1); i++)
        {
          int tabIndex = this[i].TabIndex;
          if (num <= tabIndex)
          {
            num = tabIndex + 1;
          }
        }
        value.tabIndex = num;
      }
      this.owner.SuspendLayout();
      try
      {
        Control parent = value.parent;
        try
        {
          value.AssignParent(this.owner);
        }
        finally
        {
          if ((parent != value.parent) && ((this.owner.state & 1) != 0))
          {
            value.SetParentHandle(this.owner.InternalHandle);
            if (value.Visible)
            {
              value.CreateControl();
            }
          }
        }
        value.InitLayout();
      }
      finally
      {
        this.owner.ResumeLayout(false);
      }
      LayoutTransaction.DoLayout(this.owner, value, PropertyNames.Parent);
      this.owner.OnControlAdded(new ControlEventArgs(value));
    }
  }
}

先ほど、

このデリゲート(UserControl1_Load) を呼ぶのは、
ベースクラスの OnLoad() メソッドです。


なんて言いましたが、では OnLoad() はどこから呼ばれるか?
答えは、UserControl.OnCreateControl() です。
となると、なんとなく怪しいのがマーカーを入れた value.CreateControl() で、
Visible プロパティを見て分岐していることから
実際、以下のようにすると UserControl1_Load が呼ばれるようになります。
それならと試しに、WebBrowser を継承して
OnCreateControl() をオーバーライドし、
base.OnCreateControl() を呼ぶ直前に Visible = false とすると
UserControl1_Load は呼ばれませんでした。残念。。orz

this.webBrowser1.Visible = false;
this.Controls.Add(this.webBrowser1);
this.webBrowser1.Visible = true;

てっきり WebBrowser の OnCreateControl() から
コンテナコントロールの OnCreateControl() を呼んでいるのだと
思ったのですが、見当違いだったみたいです。

間違いなく ControlCollection.Add() のどこかで
コンテナコントロールの OnCreateControl() が呼ばれるはずなのですが
長くなりすぎたし、とりあえずは解決できているので良しとします。