3流プログラマのメモ書き

元開発職→社内SE→派遣で営業支援→開発戻り浦島太郎状態の三流プログラマのIT技術メモ書き。 このメモが忘れっぽい自分とググってきた技術者の役に立ってくれれば幸いです。

(vb.net)カスタムコントロール作成時に初期化処理を何でもかんでもコンストラクタに書いてはいけない

今回System.Windows.Forms.Buttonクラスを継承したカスタムコントロールを作ってます。

このとき自作コントロール側で、配置してる親フォームの名前を取得したいと思って下記のようなコードを書きました。

Public Class TButton
    Inherits System.Windows.Forms.Button
    '配置しているフォームの名前

    Private m_strFormName As String
    '....
    Public Sub New()
        ' この呼び出しは、Windows フォーム デザイナで必要です。
        InitializeComponent()
        ' InitializeComponent() 呼び出しの後で初期化を追加します。
        '親コントロール
        Dim ParentObj As Windows.Forms.Control = Me.Parent
        'フォームの名前取得
        m_strFormName = ParentObj.GetType.FullName
    End Sub
End Class

要はコンストラクタで、親オブジェクト(フォームであること前提)の名前を取得しているだけす。
しかし、これをするとデザイナを開いたときに「オブジェクト参照がオブジェクト インスタンスに設定されていません。 」というエラーが出てきます。

しかもエラー一覧には警告で「呼び出しのターゲットが例外をスローしました。」とでるだけ。
つくづく思うのですが、MSはもうちょっとわかりやすいエラーを出すようにしてほしいですね。

この「オブジェクト参照がオブジェクト インスタンスに設定されていません。 」というのは、あるオブジェクトのプロパティやらメソッドやらを触ろうとしたけど、オブジェクトがNull(VB的に言うとNothing?)だからできないという意味だそうです。

で、なぜ上記のコードの場合このエラーがでるかというと、自作コントロールが張り付けてあるフォームのDesignerファイルInitializeComponentメソッドを見るとわかります。

Private Sub InitializeComponent()
    Me.TButton1 = New TButton
    Me.SuspendLayout()
    'TButton1

    'ここで自作コントロールのプロパティを設定。
    '
    'Sample_and_Test

    '....
    Me.Controls.Add(Me.TButton1)
    '....
End Sub

VSが自動で生成するとこんな感じになっていると思うのですが、まず最初に自作コントロールインスタンスが生成されます。 しかし、実際にこのコントロールがフォーム上に配置されるのは

Me.Controls.Add(Me.TButton1)

の部分です。

つまり、コントロールインスタンス生成時には、まだコントロールはフォームに追加されていないため、コンストラクタで親フォームを探そうとしても探せない(Null)にという状態になるようです。
また、ふりっつさんのブログによると、コントラクタ内で値の初期化は2回実行、2重登録されてしまうためよろしくないようです。

これの解決策として、コントロール配置時に発生するイベントがあるみたいです。(配置時ということは親側のMe.Controls.Add(コントロールオブジェクト)が起きたタイミングか?)
それは、InitLayout()メソッド。
これはSystem.Windows.Forms.Controlに定義されており、オーバーライド可能なので、今回の場合は使えそうです。

とういことでこう書きなおしました。

Public Class TButton
    Inherits System.Windows.Forms.Button
    '配置しているフォームの名前
    Private m_strFormName As String
    ' ....
    Protected Overrides Sub InitLayout()
        '基本クラスのInitLayoutを呼ばないとおかしくなるらしい
        MyBase.InitLayout()

        '親コントロール
        Dim ParentObj As Windows.Forms.Control = Me.Parent

        'フォームの名前取得
        m_strFormName = ParentObj.GetType.FullName
    End Sub

    Public Sub New()
        ' この呼び出しは、Windows フォーム デザイナで必要です。
        InitializeComponent()
        ' InitializeComponent() 呼び出しの後で初期化を追加します。
    End Sub

End Class

こうするとデザイナでもきちんと表示されますし、実行しても正しく動くようになりました。
カスタムコントロールを作成するとデザイナがおかしくなることがよくありますが、やはり呼び出しのタイミングやバグが原因なんでしょうね。

たとえば、この自作コントロール(チェックボックス)が別の自作クラスで描画している場合、CheckedChangeイベントで自作クラスを用い描画するということがあるかもしれません。
この場合、デザイナ側でCheckedプロパティをTrueにしておくと、当然DesignerファイルInitializeComponentメソッド内で、○○.Cheked=Trueになります。
ということはこのコードが実行されるより前に自作クラスのインスタンスがないとやはりエラーになってしまいます。(これはコントロールのコンストラクタを使うくらいしか方法はないのでしょうが。。。)