Silverlight 1.1 바로 시작하기

이벤트 핸들링
준비 사항
Silverlight 개발의 기초에서 개발에 필요한 도구와 기술에 대해 설명하고 있습니다.

이벤트 핸들링 방식

이벤트 핸들러
이벤트 핸들러는 하나의 Silverlight 페이지 내에 존재하는 개체들의 이벤트를 처리하는 코드-보통은 함수-를 말합니다. Silverlight의 페이지 또는 컨트롤은 하나의 XAML 파일로 표현되며 이 마크업 내에서 표현되는 모든 개체는 자신이 노출하는 이벤트에 대한 이벤트 핸들러를 지정할 수 있습니다. 이런 개체는 코드-비하인드에서도 같은 클래스명을 가진 개체로 참조가 가능하고 반대로 코드-비하인드에서 동적으로 개체를 추가할 수도 있습니다.

Silverlight 개발의 기초; Hello Silverlight! 에서는 별다른 설명도 없이 이벤트 핸들러를 사용하였고 사실 특별한 설명이 필요 없을 정도로 직관적이었죠. 이렇게 Silverlight 내의 컨트롤의 이벤트를 처리 하는 방법은 몇 가지가 있습니다.
  • Silverlight 브라우저 컨트롤(HTML 페이지)에 의해 인터프리트되는 JavaScript 핸들러를 이용.
  • IronPython, 매니지드 JScript와 같은 다이내믹 언어로 작성한 핸들러를 이용. 이 핸들러는 런타임이 되기 전 까지 컴파일 되지 않으며 DLR(Dynamic Language Runtime)에 의해 제공됩니다. 자세한 건 http://silverlight.net/QuickStarts/Other/ProgramDlr.aspx 여기를 참조하시고요, DLR에 대해서는 아마도 다루지 않을 것 같네요.
  • C#과 VB.NET같은 매니지드 코드 언어로 작성한 핸들러를 이용. 이 핸들러는 라이브러리 파일(.dll)로 컴파일되며 XAML에서 구현된 라이브러리를 참조하여 사용합니다. 빌드시 라이브러리로 컴파일 되므로 소스코드를 공개하지 않고 배포할 수 있습니다.
이 글에서는 Silverlight 1.1에서 지원하는 C#을 사용한 매니지드 코드로 작성할 수 있는 이벤트 핸들링에 대해 설명합니다.

이벤트 핸들러의 구현 방법
매니지드 코드에서 이벤트 핸들러를 구현하는 방법은 크게 두 가지가 있습니다.
  • 마크업의 개별 엘리먼트에서 이벤트 핸들러를 지정하는 어트리뷰트에 이벤트 핸들러의 이름을 지정하고 코드-비하인드에서 해당 이름의 이벤트 핸들러 함수를 정의하는 방법.
  • 코드-비하인드에서 원하는 개체의 참조로부터 이벤트 핸들러 델리게이트를 구현하는 방법.
이 이벤트 핸들러를 구현하는 방법을 알아보기에 앞서 이벤트 핸들링의 대상이 되는 개체가 어떻게 참조되는지 알아보겠습니다.

코드-비하인드에서 개체 참조하기

개체의 이름
마크업과 코드-비하인드 모두에서 어떤 개체를 표현할 수 있고 이 개체는 '이름'을 가질 수도 있고 그렇지 않을 수도 있습니다. 각각의 예를 들어보죠.
XAML
<!-- 이름이 없는 엘리먼트 -->
<TextBlock Text="Text Content" />

<!-- 이름을 지정한 엘리먼트 -->
<TextBlock x:Name="Content" Text="Text Content" />

C#
// 이름이 없는 TextBlock을 메인 캔버스에 추가
mainCanvas.Children.Add(new TextBlock());
// 이름이 있는 TextBlock을 생성하고 속성 지정
TextBlock content = new TextBlock();
content.Text = "Text Content";

이름이 없는 개체는 다시 말해 그것이 화면에 표시되거나 어떤 기능을 가진다고 해도 코드에서 개별적으로 접근될 필요가 없다는 것을 의미합니다. 반대로 이름이 있다는 것은 그 개체를 식별하고 싶다는 것을 말하며 코드에서 언젠가는 그 개체의 이름을 불러 참조하고 싶다는 것을 의미합니다. 생각해보면 당연하겠죠?

일반적으로 개체의 이름은 디자인 타임에만 명시적으로 지정할 수 있으며 마크업에서 정의한 엘리먼트의 이름은 해당 마크업에서 정의한 x:Class의 멤버로 선언됩니다. 하지만 코드-비하인드에서는 어떤 프로시저 내에서 선언하여 한시적인 스코프를 가질 수도 있고 마크업의 x:Class에서 정의한 클래스의 멤버로 선언하여 애플리케이션 내내 그 이름을 유지할 수도 있습니다.

개체 트리(Object Tree) 구성
Canvas와 같은 컨테이너 역할을 하는 개체는 다른 개체들을 자식으로 포함할 수 있습니다. 예를 들어 어떤 페이지에 텍스트블럭과 사각형 도형을 가진 캔버스는 다음의 마크업과 같이 표현할 수 있습니다.
XAML
<Canvas>
  <TextBlock Text="Text Content" />

  <Rectangle Width="100" Height="100" />
</Canvas>

이 때 개체들은 트리 구조를 이루게 되며 이것을 개체 트리(Object Tree)라고 합니다.

좀더 구체적으로 코드상에서 Canvas 개체는 Panel 개체를 상속하고 있으며 이 Panel 개체는 다른 개체의 컨테이너를 제공하고 컨테이너에 포함된 자식 개체들을 Children 이라는 프로퍼티를 통해 Collection으로 제공합니다.

위의 개체들을 간략하게 그려보자면 다음과 같은 트리 구조로 표현할 수 있습니다.


이름이 없는 개체 참조하기
Silverlight 페이지의 일반적인 루트 엘리먼트인 Canvas는 Panel을 상속하여 구현되었으며 Panel 클래스의 Children 프로퍼티를 사용하여 자식 엘리먼트를 열거할 수 있습니다.

따라서 특별히 개체의 이름을 지정하지 않더라도 Children 프로퍼티를 통해 다음과 같이 접근이 가능합니다. (여기에서 루트 개체는 Canvas이고 this 키워드로 접근했다고 가정합니다.)
C#
// 루트 개체로부터 자식의 인덱스로 접근
TextBlock tb = this.Children[0] as TextBlock;
Rectangle rt = this.Children[1] as Rectangle;

// 루트 개체의 모든 자식을 열거하면서 처리
foreach (Visual vi in this.Children)
{
    // 각 자식 개체를 vi로 참조하여 처리...
}

이 방법으로 개별 개체에 접근하기란 매우 불안정합니다. 보시다시피 자식의 인덱스 번호 즉, 해당 개체가 컨테이너에 추가된 순서를 미리 알고 있어야 한다는 전제 조건이 생기며 만약 디자인이 변경된 경우 이 인덱스가 바뀔 가능성이 높으므로 이런 방법은 사용하지 않는 것이 좋습니다. 앞서서 얘기 했지만, 어떤 개체에 개별적으로 접근할 때에는 반드시 이름을 지정하는 것이 좋습니다.

다만 예제의 두번째 블럭처럼 모든 자식을 열거하면서 공통적인 처리를 할 때에는 좋은 수단이 될 수 있을 것입니다.

이름이 있는 개체 참조하기1 - Children 프로퍼티를 사용
다음 마크업과 같이 특정한 이름을 지정한 경우는 좀 더 명확한 접근이 가능합니다.
XAML
<Canvas>
  <TextBlock x:Name="Content" Text="Text Content" />

  <Rectangle x:Name="Banner" Width="100" Height="100" />
</Canvas>

C#
// 루트 개체로부터 자식의 명시적인 이름으로 접근
TextBlock tb = this.Children.FindName("Content") as TextBlock;
Rectangle rt = this.Children.FindName("Banner") as Rectangle;

이름 없는 개체에 접근할 때와 비슷하지만 FindName이라는 메소드를 사용하여 좀 더 명시적인 접근이 가능합니다. 하지만 이 경우도 마크업과 코드-비하인드 간의 동기화 문제는 여전히 남아 있어서 만약 마크업을 수정한 경우 코드-비하인드 역시 수정되어야 하며 컴파일러가 해당 이름의 개체가 있는지 여부를 체크할 수 없습니다.

결정적으로 마크업에서 명시적으로 이름을 지정한 개체를 이렇게 접근해야 할 이유는 단 한가지도 없다고 단언할 수 있으며 그 이유는 다음과 같습니다.

이름이 있는 개체 참조하기2 - 컴파일러가 자동으로 생성하는 멤버 변수
마크업에서 x:Name 어트리뷰트로 지정된 이름은 마크업의 루트 엘리먼트가 지정한 x:Class에 해당하는 코드-비하인드 클래스의 멤버로 구현됩니다. 이 과정은 컴파일 도중 컴파일러에 의해 자동으로 구현되며 따라서 개발자는 코드-비하인드에 별도의 코드를 작성하지 않고도 x:Name에서 지정한 이름을 곧바로 사용할 수 있습니다.

이것은 매니지드 언어가 지원하는 partial class 선언 덕분에 가능한 일인데요, 이 과정을 살펴보면 다음과 같이 요약할 수 있습니다.

  1. 마크업에서 x:Name 어트리뷰트로 개체의 이름을 지정.
  2. 컴파일러는 빌드시 자동으로 x:Class에서 지정한 클래스 이름으로 partial 클래스를 생성하고 위의 개체를 클래스의 멤버 변수로 선언.
  3. 코드-비하인드에 초기화 코드를 호출.
  4. 자동으로 생성된 코드에서 FindName을 통한 개체 찾기를 통해 멤버 변수에 참조를 설정.
  5. 코드-비하인드에서는 자동으로 생성된 클래스의 멤버 변수를 직접 접근하여 사용.
개체 참조 방식 비교
Children을 사용한 개체 참조 방법은 런타임에 동적으로 일어나므로 꼭 필요할 때에만 참조를 만들게 되므로 페이지에 컨트롤 해야 할 개체가 아주 많다면 최초 페이지 로딩시 참조 생성작업이 필요 없어서 초기화 속도상의 이점이 있고 아주 약간의 메모리 절감 효과가 있습니다. 반면 실제 개체를 접근 할 때 동적으로 컬렉션을 뒤지는 작업을 해야 하므로 접근 속도가 느리고 마크업이 변경된 경우 컴파일 시점에 그 사실이 반영되었는지 알 수 없으므로 디버깅에 불리합니다.

컴파일러가 자동으로 생성해주는 멤버 변수를 사용한 개체 참조 방법은 위와 반대로 페이지에 아주 많은 개체가 있다면 초기화에 약간의 시간이 더 걸리고 참조 변수 자체의 메모리가 더 필요한 반면 개체에 대한 접근은 참조를 통해 상수적으로(Constantly) 이루어지므로 빠르게 처리할 수 있습니다.

하지만 여러가지 조건과 득실을 따져보자면 후자의 방법이 전자에 비해 월등한 이점이 있다고 봅니다. 특히 후자의 방법은 컴파일러에 의해 해당 개체가 자동으로 생성되므로 만약 마크업에서 해당 개체를 삭제한다면 그 개체를 사용했던 기존 코드는 컴파일 시점에 에러를 낼 것이고 이는 훨씬 안전하고 견고하며 빠르게 디버깅 할 수 있는 기반이 됩니다.

이벤트를 핸들링하는 방법

이벤트 핸들러의 기본 형식
Silverlight에서 모든 개체의 이벤트 핸들러는 다음과 같은 형식을 가집니다.
void EventHandler(object sender, EventArgs e)

첫번째 파라미터는 이벤트를 발생시킨 개체를 참조하며 만약 그 이벤트를 발생시킨 개체를 정확히 알고 있다면 그 개체의 타입으로 캐스팅하여 추가적인 작업을 편하게 할 수 있습니다.

두번째 파라미터는 이벤트의 추가 정보로 EventArgs 타입을 상속받아 구현되는 개체입니다. 물론 이 개체는 발생되는 이벤트의 종류에 따라 다를 것입니다. 예를 들어, 마우스 왼쪽 버튼이 눌렸음을 알리는 이벤트 개체는 MouseEventArgs 클래스이고 키보드 버튼이 눌렸음을 알리는 이벤트 개체는 KeyboardEventArgs 클래스입니다. 추가적인 정보가 전혀 없는 GotFocus 이벤트라고 해도 null을 참조하는 EventArgs 개체가 전달됩니다.

첫번째 파라미터가 object 타입으로 지정되어 있으므로 동일한 작업을 수행한다면 다수의 개체의 이벤트 핸들러를 동일하게 지정할 수 있습니다. 여기에 대한 예제는 이 절의 마지막에서 설명합니다.

이벤트 핸들러는 각 개체마다 여러가지 종류가 있지만 여기서는 대부분의 개체가 지원하고 누구나 이해하기 쉬운 왼쪽 마우스 버튼을 눌렀을 때 발생하는 이벤트인 'MouseLeftButtonDown' 이벤트를 통해 알아보도록 하겠습니다.

시나리오는 TextBlock을 하나 올리고 마우스로 클릭했을 때 "Mouse Left Button Down!" 라고 표시하는 예제입니다.

마크업에서 이벤트 핸들러를 지정하는 방법
이 방법은 마크업의 이벤트 핸들러 어트리뷰트를 선언하고 이벤트 핸들러의 이름을 지정한 후 코드-비하인드에 해당 이벤트 핸들러를 작성합니다. 아마 이 방법은 HTML과 JavaScript로 이벤트 처리를 해보신 분이라면 쉽게 이해할 수 있을 것이고 실제로 코드도 거의 동일합니다.

이 예제 프로젝트는 아래에서 다운 받으세요.
EventHandling1.zip

이벤트 핸들링 방법 1



Howto:2-1. 마크업에서 이벤트 핸들러를 지정한 처리 방법
※ 각 단계에서의 코드는 아래의 XAML과 C# 코드를 참고하세요.
1. 마크업에서 원하는 엘리먼트(개체)에 MouseLeftButtonDown 어트리뷰트를 선언하고 이벤트 핸들러의 이름을 지정합니다.
    <TextBlock Text="Text Content1" MouseLeftButtonDown="OnMouseLeftButtonDown" />
2. 코드-비하인드에 위에서 지정한 이름의 이벤트 핸들러 프로시저를 작성합니다.
3. sender를 TextBlock으로 캐스팅하고 Text 속성을 변경합니다.
    void OnMouseLeftButtonDown(object sender, MouseEventHandler e)
    {
        TextBlock tb = sender as TextBlock;
        tb.Text = "Mouse Left Button Down!";
    }

XAML
<Canvas x:Name="parentCanvas"
    xmlns="http://schemas.microsoft.com/client/2007"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Loaded="Page_Loaded"
    x:Class="EventHandling.Page;assembly=ClientBin/EventHandling.dll"
    Width="640" Height="480" Background="White" >

    <TextBlock Canvas.Top="10" Text="Text Content 1" MouseLeftButtonDown="OnMouseLeftButtonDown" />
</Canvas>

C# (※ using 구문과 namespace를 생략했습니다.)
public partial class Page : Canvas
{
    public void Page_Loaded(object o, EventArgs e)
    {
        // Required to initialize variables
        InitializeComponent();
    }

    // TextBlock1
    void OnMouseLeftButtonDown(object sender, MouseEventArgs e)
    {
        TextBlock tb = sender as TextBlock;
        tb.Text = "Mouse Left Button Down!";
    }
}

이 방식은 아무런 참조 없이 코딩하기가 상당히 머리아픕니다. 예제에서 MouseLeftButtonDown 이벤트의 처리를 보였는데 만약 키보드 입력을 처리하려면 또, MouseLeftButtonUp 이벤트를 처리하려면 어떤 EventArgs 타입을 사용해야 하는지 개발자가 미리 알고 있어야 한다는 걸 전제한다는 것이죠.

다음 방법은 좀 더 경쾌한 코딩을 할 수 있게 합니다.

코드-비하인드에서 이벤트 핸들러를 붙이는(Attatch) 방법
이 방법은 마크업에서 별도로 지정하지 않고 코드-비하인드에만 코드를 작성하여 이벤트 핸들러를 작성하고 붙입니다. C#에서 일반적인 이벤트 핸들러나 델리게이트를 사용하는 방법과 동일하며 전자에 비해 인텔리센스를 통한 코드 자동완성 기능을 지원하고 이벤트 핸들러의 기본형도 자동으로 만들어 주는 이점이 있습니다.

이 예제 프로젝트는 아래에서 다운 받으세요.
EventHandling2.zip

이벤트 핸들링 방법 2



Howto:2-2. 코드-비하인드에서 이벤트 핸들러를 붙이는 방법
※ 각 단계에서의 코드는 아래의 XAML과 C# 코드를 참고하세요.
※ 코드는 위에서 작성한 코드에 이어서 작성합니다.
1. 마크업에는 단지 제어할 엘리먼트만 올려놓습니다. 이 엘리먼트에 x:Name 어트리뷰트를 선언하는 것은 선택적이지만 가급적 선언하는 것이 코드를 작성하기 수월합니다. 여기에서는 x:Name을 지정한 걸로 진행합니다.
    <TextBlock x:Name="text2" Text="Text Content2" />
2. 코드-비하인드의 초기화 코드에서 위의 엘리먼트를 참조하는 개체에 이벤트 핸들러를 작성합니다. 인텔리센스의 코드 자동완성 기능을 사용하면 다음 문장까지 입력하고 탭, 탭을 누르면 됩니다.
    text2.MouseLeftButtonDown += 
3. 자동으로 작성된 이벤트 핸들러에 코드를 기입합니다.
    text2.Text = "Mouse Left Button Down!";

XAML
<Canvas x:Name="parentCanvas"
    xmlns="http://schemas.microsoft.com/client/2007"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Loaded="Page_Loaded"
    x:Class="EventHandling.Page;assembly=ClientBin/EventHandling.dll"
    Width="640" Height="480" Background="White" >

    <TextBlock Canvas.Top="10" Text="Text Content 1" MouseLeftButtonDown="OnMouseLeftButtonDown" />
    <TextBlock x:Name="text2" Canvas.Top="30" Text="Text Content 2" />
</
Canvas>

C# (※ using 구문과 namespace를 생략했습니다.)
public partial class Page : Canvas
{
    public void Page_Loaded(object o, EventArgs e)
    {
        // Required to initialize variables
        InitializeComponent();
        text2.MouseLeftButtonDown += new MouseEventHandler(text2_MouseLeftButtonDown)
    }

    // TextBlock1
    void OnMouseLeftButtonDown(object sender, MouseEventArgs e)
    {
        TextBlock tb = sender as TextBlock;
        tb.Text = "Mouse Left Button Down!";
    }

    // 코드 자동완성 기능에서 만들어준 핸들러에 처리할 내용만 직접 작성
    void text2_MouseLeftButtonDown(object sender, MouseEventArgs e)
    {
        text2.Text = "Mouse Left Button Down!";    // 이미 대상 개체 참조를 알고 있으므로 캐스팅할 필요 없음
    }
}

Note
※ VS를 익숙하게 써보셨다면 위의 코드 자동완성에 대해 잘 아시겠지만 잘 감이 안오시는 분들을 위해 작업을 동영상으로 캡쳐해봤습니다.
정말이지 이런 기능 없을 땐 어떻게 살았나 싶네요. ^_^

이벤트 핸들러 방식의 비교
이 두가지 방식의 이벤트 핸들러 처리는 앞서 얘기했던 Children을 통해 개체의 참조를 획득하느냐 아니면 x:Name을 통해 개체의 참조를 획득하느냐의 차이와 비슷한 차이점을 가지고 있습니다.

그런데 이 이벤트 핸들링 방식의 경우 후자의 방식이 모든 면에서 완전히 더 유용하고 편리하고 정확하다고 생각합니다. 아마도 전자의 방법은 기존의 HTML/JavaScript 문법과 유사한 경험을 제공하기 위한 일종의 Legacy 지원이라고 봐도 될 것 같습니다.

그런 이유로 앞으로 진행하는 모든 Silverlight 프로젝트는 이벤트 핸들링에 있어서 만큼은 후자의 방식을 사용할 예정입니다.

참고

Silverlight 공식 QuickStarts 참고:
http://silverlight.net/QuickStarts/BuildUi/ControlEventHandlers.aspx
http://silverlight.net/QuickStarts/BuildUi/CallJavascript.aspx

Silverlight 레퍼런스 Events Overview:
http://msdn2.microsoft.com/en-us/library/bb412396.aspx

WPF에서 XAML 코드가 컴파일 될 때 내부 동작 참고:
http://gongdo.tistory.com/74
http://gongdo.tistory.com/82

신고
Posted by gongdo


티스토리 툴바