사용자 정의 컨트롤을 여러 개 올린 폼을 만들다 보면, 폼을 닫을 때 컨트롤마다 해야 할 정리 작업이 생깁니다. 입력 중이던 내용을 저장하거나, 열어 둔 파일·연결을 닫거나, 타이머를 멈추는 일 같은 것들이죠. 이걸 폼의 닫기 이벤트에 컨트롤별로 하나씩 적어 두면 컨트롤이 늘어날 때마다 폼 코드를 고쳐야 합니다. 그래서 정리 작업은 각 컨트롤이 Closed 라는 메서드로 스스로 갖고 있게 하고, 폼은 닫힐 때 "Closed가 있는 컨트롤이면 불러 준다"는 일만 하도록 나눴습니다.

폼이 닫힐 때 자식 컨트롤의 Closed 메서드를 호출하는 구조

코드

리플렉션을 쓰므로 파일 위에 Imports System.Reflection 이 필요합니다. fMain 은 메인 폼 이름입니다.

Private Sub fMain_FormClosed(sender As Object, e As FormClosedEventArgs) Handles Me.FormClosed
    For Each ctl As Object In Me.Controls
        If HasClosedMethod(ctl) Then
            Dim method As MethodInfo = ctl.GetType().GetMethod("Closed")
            method.Invoke(ctl, Nothing)
        Else
            Console.WriteLine("Closed 메서드가 구현되지 않은 컨트롤입니다.")
        End If
    Next
End Sub

' 컨트롤이 Closed 메서드를 구현했는지 확인하는 함수
Private Function HasClosedMethod(control As Control) As Boolean
    Dim method As MethodInfo = control.GetType().GetMethod("Closed")
    Return method IsNot Nothing AndAlso method.ReturnType Is GetType(Void)
End Function

폼이 닫히면 폼에 올라간 컨트롤을 하나씩 꺼내 HasClosedMethod 로 검사합니다. 이 함수는 컨트롤의 실제 형식에서 이름이 Closed 인 공개 메서드를 찾고, 반환값이 없는 Sub 인지까지 확인합니다. 조건에 맞으면 MethodInfo.Invoke 로 그 메서드를 실행하고, 없으면 콘솔에 메시지만 남기고 넘어갑니다. 컨트롤 쪽에서는 Public Sub Closed() 하나만 만들어 두면 폼이 알아서 불러 줍니다.

그대로 쓰면 걸리는 곳

Me.Controls 는 폼에 직접 올린 컨트롤만 돌려줍니다. Panel, GroupBox, TabControl 안에 넣은 컨트롤은 그 컨테이너의 Controls 에 들어 있어서 이 반복문에서 빠집니다. 레이아웃을 패널로 나눈 폼이라면 컨테이너 안까지 재귀로 내려가도록 바꿔야 합니다.

GetMethod("Closed") 는 매개변수가 있든 없든 이름만 보고 찾습니다. 누가 Closed(force As Boolean) 처럼 매개변수를 받게 만들면 Invoke(ctl, Nothing) 에서 매개변수 개수가 맞지 않는다는 예외가 나고, 같은 이름의 오버로드가 두 개 있으면 찾는 단계에서 모호하다는 예외가 납니다. 또 반복 변수 ctl 을 Object 로 선언해 Control 매개변수에 넘기고 있어서, Option Strict On 인 프로젝트에서는 컴파일되지 않습니다. For Each ctl As Control 로 바꾸면 해결됩니다.

인터페이스로 바꾸면 더 단단하다

리플렉션은 메서드 이름 문자열에 의존하니 오타가 나도 컴파일러가 잡아 주지 않습니다. 직접 만드는 사용자 정의 컨트롤이라면 인터페이스를 하나 정해 두고 그걸 구현하게 하는 쪽이 안전하고 빠릅니다. 컨테이너 안쪽까지 도는 재귀도 같이 넣었습니다.

Public Interface IClosable
    Sub Closed()
End Interface

Private Sub fMain_FormClosed(sender As Object, e As FormClosedEventArgs) Handles Me.FormClosed
    CallClosed(Me.Controls)
End Sub

Private Sub CallClosed(ctls As Control.ControlCollection)
    For Each ctl As Control In ctls
        Dim c = TryCast(ctl, IClosable)
        If c IsNot Nothing Then c.Closed()
        If ctl.HasChildren Then CallClosed(ctl.Controls)
    Next
End Sub

컨트롤 클래스에 Implements IClosable 을 붙이고 Public Sub Closed() Implements IClosable.Closed 를 만들면 됩니다. 반대로 소스를 고칠 수 없는 외부 컨트롤까지 대상에 넣어야 한다면 원래의 리플렉션 방식이 쓸모가 있습니다. 한 가지 더, 저장처럼 실패하면 안 되는 작업은 FormClosed 보다 한 단계 앞인 FormClosing 에서 처리해야 실패했을 때 e.Cancel = True 로 닫기를 멈출 수 있습니다.