본문 바로가기

Library/.NET Framework with C#

delegate / event

System.Windows.Forms.Form의 Paint 핸들러는,

public delegate void PaintEventHandler(object objSender, PaintEventArgs pea);

의 형식으로 정의되어 있는 delegate이다. 사실, Form을 상속받는 Form을 하나 생성했을 때, 여기에 어떠한 paintEventHandler를 설치하지 않아도 기본적으로 배경은 그려진다. 이것은, 이미 설치되어 있는 PaintEventHandler가 존재한다는 말인데, 그것은 어디에 있을까? 그것은 System.Windows.Forms.Control에 이미 설치되어 있다. 즉, 다음과 같은 빈 깡통  Form을 하나 생성해보자. 이 Form이 화면에 그려질 때, 어떤 일이 발생할까?

class FormPaintTest
{
    public static void Main()
    {
        System.Windows.Forms.Form form = new System.Windows.Forms.Form();
        form.Text = "FormPaintTest";

        System.Windows.Forms.Application.Run(form);
    }
}

이 Form의 영역이 무효화될 때마다 OnPaint 메서드가 호출되며, FormPaintTest는 Control을 상속하고 있기 때문에 Control의 OnPaint 메서드가 호출된다. 좀 더 자세하게 살펴보자. Control의 화면을 갱신하는 이벤트는 Paint이며, 메서드는 OnPaint이고, 이것을 호출하는 delegate는 PaintEventHandler이다. 그리고, 이 delegate의 아규먼트는 PaintEventArgs이다. OnPaint 메서드에서는 분명히 event로 선언된 Paint를 사용하여 자신에게 중복 위임처리된 메서드를 연속적으로 호출할 것이다. 그리고, Paint 이벤트는 PaintEventHandler 형식으로 delegate 개체를 받기 때문에, Paint(PaintEventArgs)와 같은 형식으로 Invoke 처리될 것이다. 이제 다음 코드를 보자.

class HelloWorld
{
    public static void Main()
    {
        System.Windows.Forms.Form form = new System.Windows.Forms.Form();
        System.Windows.Forms.Application.Run(form);
    }

    protected override void OnPaint(System.Windows.Forms.PaintEventArgs pea)
    {
        System.Drawing.Graphics grfx = pea.Graphics;
        grfx.DrawString("HelloWorld!", Font, System.Drawing.Brushes.Black, 0, 100);
    }
}

이 코드에에서는, System.Windows.Forms.Control의 OnPaint 메서드가 호출되는게 아니라, Control을 상속받은 Form에서 OnPaint 메서드를 재정의했기 때문에 HelloWorld의 OnPaint 메서드가 호출된다. override라는 것에 주목할 필요가 있는데, 이렇게 정의하면 Control의 OnPaint 메서드를 호출하지 않고, HelloWorld.OnPaint를 호출하게 된다. 그리고, 여기서 정의한 OnPaint 메서드에서는 Paint 이벤트 개체를 호출하지 않는다.

어떤 다른 클래스에서 HelloWorld를 인스턴스화하여, HelloWorld.Paint에 새로운 PaintEventHandler를 설치했다고 하자. 그렇다면, 그 클래스의 OnPaint가 호출될 수 있을까? 이것은 HelloWorld가 부모 클래스의 OnPaint를 재정의하고 있기 때문에 결코 호출되지 않는다. 즉, 이 개체의 내부에는 Paint 이벤트 개체가 선언되어 있고, 외부에서

Paint += new PaintEventHandler(PaintEventArgs;pea);

처럼 새로운 이벤트 핸들러를 추가했다고 하더라도, 새로 정의된 OnPaint에서는 이 Paint 이벤트를 사용하여 PaintEventHandler를 호출하는 것이 아니다. 만약, 그러한 동작을 원한다면, HelloWorld의 OnPaint 메서드안에 다음과 같은 코드를 추가해주어야 할 것이다.

portected override void OnPaint(object objSender, PaintEventArgs pea)
{
    base.OnPaint(pea);
    ...
}

이 코드를 추가하면, 부모 클래스의 OnPaint 메서드를 호출하면서, Paint 이벤트 개체에 등록되어 있는 중복 위임자를 모두 호출해줄 것이다. 그렇다면, 애초에 기대했던 동작을 할 것이다. 다음 코드를 참고하라:

class InstantiateHelloWorld
{
    public static void Main()
    {
        System.Windows.Forms.Form form = new HelloWorld();
        form.Text = "InstatiateHelloWorld" + form.Text;
        form.Paint += new PaintEvnetHandler(MyPaintHandler);

        System.Windows.Forms.Application.Run(form);
    }

    public static void MyPaintHandler(object objSender, PaintEventArgs pea)
    {
        System.Windows.Forms.Form form = (System.Windows.Forms.Form)objSender;
        System.Drawing.Graphics grfx = pea.Graphics;

        grfx.DrawString("Hello from InstantiateHelloWorld",
            form.Font, System.Drawing.Brushes.Black, 0, 100);
    }
}

이 코드에서는 HelloWorld를 인스턴스화하여, 이것의 Paint 이벤트 개체에 MyPaintHandler 메서드를 등록했지만, HelloWorld 클래스의 Paint 이벤트는 이미 새로 정의한 OnPaint 메서드가 재정의되어 있고, 이것은 Paint 이벤트 개체를 호출하지 않기 때문에 HelloWorld의 OnPaint 메서드가 호출되는 것으로 끝이다. MyPaintHandler 메서드가 호출되기를 원한다면, HelloWorld의 OnPaint 메서드가 Paint 이벤트 개체를 호출하도록, 즉 이미 정의된 Control의 OnPaint 메서드를 호출하도록 해주어야하며, 그렇게 하는 코드가 바로 base.OnPaint(pea)라는 것이다.


C#에서 가장 복잡한 구조를 꼽으라면 이 delegate / event 구조가 빠지지 않을텐데, 이것은 delegate / event사 한정자(qualifier)로 보이지만 사실은 함수자를 고도로 추상화한 것이라 실체를 파악하기가 쉽지 않기 때문이다. 특히 event에 의한 콜백함수 처리 부분은, 디자인적인 면에서 봤을 때는, Observer 패턴과 유사하다. Observer 패턴을 Publish / Subscribe라고도 하는데, 이벤트를 발생시키는 Publish 개체가 있고, 특정 이벤트를 받기를 원하는 Subscribe 개체들이 존재한다. 특정 이벤트를 수신을 원하는 개체들을 등록하려면, 위임자를 사용하여 이벤트 개체에 등록한다. 그리고, 이벤트는 어떤 형식의 위임자를 사용하는지 그 형식을 밝혀야 할 것이다. C#에서는, 모든 것은 하나의 개체에서 파생된다는 것을 기억하라. 따라서, 어떤 이벤트를 받아서 콜백 형식으로 처리하고 싶다면 다음과 같은 코드를 작성하게 될 것이다.

public delegate void CustomEventHandler();
public event CustomEventHandler CustomEvent;
public void OnCustomEvent()
{
    if(CustomEvent != null)
    {
        CustomEvent();
    }
}
...

CustomEvent += new CustomEventHandler(new InstanceOfMethod1);
CustomEvent += new CustomEventHandler(new InstanceOfMethod2);
OnCustomEvent();
...

C#의 delegate와 event는 콜백 처리를 간단하게 포장한 것이며, 실체는 Observer 패턴을 활용하는 함수자다. 모든 타입이 하나의 슈퍼 타입에서 파생되는 C#의 특성과 약간의 추상화 때문에 함수자와 패턴에 익숙하지 않다면 이해하기 쉽지 않을 것이다.



Reference, Programming Microsoft Windows with C#, Charles Petzold, Microsoft Press