[출처 : http://hoons.kr/Lecture/LectureMain.aspx?BoardIdx=45143&kind=54&view=0 ]
이번껀 너무 그대로 퍼와서.. 그냥 개인 소장용으로 퍼왔고 악의는 없다. 좀더 자세한 내용은 위에 출처를 참조..
이벤트 라우팅의 이해
WPF는 기존의 닷넷 프로그래밍 모델을 그대로 물려받아 사용하고 있다. 그 중에서 CLR의 이벤트 모델에 대한 정교한 동작들 또한 XAML의 객체 트리에서 비슷하게 동작되는 것을 볼 수 있다. 그럼 WPFControlEvents라는 새로운 WPF 프로젝트를 생성하여 이벤트 동작에 대해서 자세하게 살펴보도록 하자. WPF 프로젝트를 생성하였다면 처음에 생성된 <Grid> 개체 안으로 다음과 같은 버튼을 삽입하여 보도록 하자.
<Button Name="btnClickMe" Height="75" Width = "250" Click ="btnClickMe_Clicked"> <StackPanel Orientation ="Horizontal"> <Label Height="50" FontSize ="20">Fancy Button!</Label> <Canvas Height ="50" Width ="100" > <Ellipse Name = "outerEllipse" Fill ="Green" Height ="25" Width ="50" Cursor="Hand" Canvas.Left="25" Canvas.Top="12"/> <Ellipse Name = "innerEllipse" Fill ="Yellow" Height = "15" Width ="36" Canvas.Top="17" Canvas.Left="32"/> </Canvas> </StackPanel> </Button> |
여기서 <Button> 태그를 열어 Click 이벤트를 지정해 주었고 버튼이 클릭될 때 지정한 메서드가 호출될 것이다. Click 이벤트는 RoutedEventHandler 델리게이트를 기반으로 동작되기 때문에 첫 번째 파라메터로 Object를 두 번째 파라메터로 System.Windows.RoutedEventArgs 파라메터를 전달하게 된다.
public void btnClickMe_Clicked(object sender, RoutedEventArgs e) { // 버튼이 클릭되었을 때의 동작 MessageBox.Show("Clicked the button"); } |
다음 [그림29-4]는 현재 컨트롤을 클릭했을 때의 동작을 보여주고 있다. (다음과 같이 버튼이 정렬되는 이유는 필자는 처음에 생성된 <Grid>를 <StackPanel>로 변경하고 실행했기 때문이다.)
그럼 버튼의 구조에 대해서 살펴보도록 하자. 버튼은 UI를 표현하기 위한 여러 개의 자식 엘리먼트들을 가지고 있다. 만약 WPF가 이러한 자식 엘리먼트들에 각각의 Click 이벤트를 지정해주어야 한다고 생각한다면 굉장히 당황스러울 것이다. 결국 사용자는 버튼의 어느 영역이라도 클릭만 한다면 클릭이벤트를 발생시킬 수 있는 것이다. 뿐만 아니라 이벤트들이 분리되어 있다고 생각해보면 그 코드는 상당히 더러워질 것이다.
윈도우 폼에서 버튼을 가지고 커스텀 컨트롤을 만든다고 한다면 버튼에 추가된 모든 항목별로 Click 이벤트를 추가해주어야 한다. 하지만 WPF는 다행스럽게도 자동적으로 이벤트를 라우팅 시켜주게 된다. 즉, WPF의 이벤트 라우팅 모델은 자동으로 이벤트를 상위 객체로 라우팅 시켜주는 것이다.
특히 이벤트 라우팅은 세 가지 분야로 분리하여 정리할 수 있다. 첫 번째로 이벤트가 발생했을 때 현재개체에서 상위로 올라가면서 이벤트가 전달되는 경우를 우리는 버블링 이벤트라고 한다. 이와 반대로 이벤트가 자식 개체로 전달되는 경우를 터너링 이벤트라고 한다. 마지막으로 이벤트가 단 하나의 개체에서만 발생하게 된다면 우리는 이것을 다이렉트 이벤트라고 한다.
의존성 속성처럼 이벤트 라우팅은 WPF 구조를 위해서 생성된 타입이라고 보면 된다. 그렇기 때문에 C# 문법을 공부할 필요는 없다.
이벤트 버블링의 역할
앞에서 살펴본 예제에서 만약 사용자가 노란 타원을 클릭했을 때 Click 이벤트는 상위 계층 <Canvas>로 이벤트가 전달되고 그 다음에는 <StackPanel>으로 이벤트가 전달되고 마지막으로 버튼으로 그 벤트가 전달되는 것이다. 이와 비슷하게 만약 Label이 클릭하게 되었다면 그 이벤트는 <StackPanel>로 그리고 버튼으로 이벤트가 전달될 것이다.
이벤트 라우팅을 살펴보았듯이 우리는 Click 이벤트를 다루기 위해서 모든 개체에 이벤트를 각각 넣어주는 수고는 걱정하지 않아도 되는 것이다. 하지만 만약 이러한 클릭 이벤트를 원하는 대로 수정하여 사용하고 싶은 경우가 있을 것이다. 이 경우 또한 우리가 그렇게 수정하는 것이 가능하다. 먼저 outerEllipse이란 컨트롤을 클릭했을 때만 이벤트를 발생시키고 싶다고 가정하자. 먼저 이 객체에 MouseDown 이벤트를 설정해 두도록 하자. 참고로 그래픽 개체들은 Click 이벤트를 지원하지 않고 있지만 MouseDown, MouseUp 이벤트를 이용해서 버튼과 같은 Click 이벤트를 구현할 수 있다.
<Button Name="btnClickMe" Height="75" Width = "250" Click ="btnClickMe_Clicked"> <StackPanel Orientation ="Horizontal"> <Label Height="50" FontSize ="20">Fancy Button!</Label> <Canvas Height ="50" Width ="100" > <Ellipse Name = "outerEllipse" Fill ="Green" Height ="25" MouseDown ="outerEllipse_MouseDown" Width ="50" Cursor="Hand" Canvas.Left="25" Canvas.Top="12"/> <Ellipse Name = "innerEllipse" Fill ="Yellow" Height = "15" Width ="36" Canvas.Top="17" Canvas.Left="32"/> </Canvas> </StackPanel> </Button> |
그리고 나서 각각의 이벤트 메서드에서는 제목 표시줄의 제목을 간단하게 변경하는 로직을 작성해 보도록 하자.
public void outerEllipse_MouseDown(object sender, RoutedEventArgs e)
{ // Window의 제목 변경 이벤트 this.Title = "You clicked the outer ellipse!"; } |
이러한 방식으로 우리는 원하는 동작을 커스텀하게 작성할 수 있을 것이다.
버블링 이벤트 라우팅은 언제나 상위의 개체로 이벤트를 이동시킨다. 그렇기 때문에 이번 예제에서 innerEllipse 객체를 클릭하면 outerEllipse가 아닌 상위 Canvas에 그 이벤트가 전달될 것이다. 즉, 두 개의 Ellipse 개체는 모두 동일한 레벨에 위치되어 있기 때문이다.
연속적이거나 불완전한 이벤트 버블링
여기서 만약 사용자가 outerEllipse객체를 클릭한다면 트리거에 Ellipse 타입을 위한 MouseDown 이벤트가 등록될 것이고 이 버블링은 해당 객체를 만나게 되면 이벤트는 중지된다. 대부분의 경우 이렇게 되길 바랄 수도 있지만 어떤 경우에서는 이 이벤트가 계속 상위로 전달되기를 바랄 수도 있다. 이 때 우리는 RountedEventArgs 타입에 false를 할당해 줘서 이벤트를 계속 유지시키는 것이 가능하다.
public void outerEllipse_MouseDown(object sender, RoutedEventArgs e) { // Window 제목의 변경 this.Title = "You clicked the outer ellipse!"; // 버블링의 유지 e.Handled = false; }
|
이 경우 우리는 제목을 변경한 후에 버튼의 Click 이벤트에 전달되어 메시지 박스가 실행되는 것을 볼 수 있다. 요약하자면 이벤트 버블링을 단 하나의 이벤트로 처리되는 것을 가능하고 또한 불연속적인 이벤트를 제어하는 것 또한 가능하다.
터너링 이벤트의 역할
엄밀히 말해서 이벤트 라우팅은 실제로 버블링(Bubbling) 아니면 터너링(Tunneling) 이벤트 중에 하나이다. 터너링 이벤트들은 원래 엘리먼트가 가지고 있는 자식들 개체로 이벤트를 전달하게 된다. 대체로 WPF 클래스 라이브러리의 각각의 버블링 이벤트는 터너링 이벤트와 짝을 이루게 된다. 예를 들어 앞에서 MouseDown이 발생했을 때 PreviewMouseDown 이벤트가 먼저 발생하게 된다.
터너링 이벤트는 다른 이벤트들을 선언하는 것과 같이 선언할 수 있다. 간단하게 XAML안에 이벤트 이름을 할당하고 거기에 메서드 이름을 지정해주면 되는 것이다. 그럼 이 터너링과 버블링 이벤트들을 확인해 보기 위해서 outerEllipse에 PreviewMouseDown를 추가해 보도록 하자.
<Ellipse Name = "outerEllipse" Fill ="Green" Height ="25" MouseDown ="outerEllipse_MouseDown" PreviewMouseDown ="outerEllipse_PreviewMouseDown" Width ="50" Cursor="Hand" Canvas.Left="25" Canvas.Top="12"/> |
다음으로 현재 C# 클래스를 정의했던 메서드들을 string 문자열에 정보를 업데이트 하게 수정하자. 그리고 마지막에 이 문자열들을 한꺼번에 보여주게 수정해보도록 하자.
public partial class MainWindow : System.Windows.Window
{ // 마우스 관련 이벤트 정보를 담을 문자열 string mouseActivity = string.Empty; public MainWindow() { InitializeComponent(); } public void btnClickMe_Clicked(object sender, RoutedEventArgs e) { // 마지막으로 문자열 보여주기 mouseActivity += "Button Click event fired!\n"; MessageBox.Show(mouseActivity); // 문자열 클리어 mouseActivity = string.Empty; } public void outerEllipse_MouseDown(object sender, RoutedEventArgs e) { // 문자열 추가 mouseActivity += "MouseDown event fired!\n"; // 버블링 유지 e.Handled = false; } public void outerEllipse_PreviewMouseDown(object sender, RoutedEventArgs e) { // 문자열 추가 mouseActivity = "PreviewMouseDown event fired!\n"; // 버블링 유지 e.Handled = false; } } |
이렇게 수정한 후에 프로그램을 실행해본 후에 밖의 원을 피해서 클릭해보면 간단하게 “Button Click event fired!” 라는 이벤트가 보여지는 것을 볼 수 있을 것이다. 하지만 만약 밖의 원을 클릭해보면 다음 [그림29-5]와 같은 메시지가 보여지는 것을 볼 수 있을 것이다.
그렇다면 WPF는 왜 이렇게 한 쌍으로 이벤트를 발생시키는 것인지 궁금할 것이다. 그 이유는 이벤트 미리보기를 통해서 데이터 유효성 검사나 버블링 동작의 실행여부와 같은 로직을 좀 더 유연하게 구현할 수 있게 하기 위해서라고 보면 된다. 대부분의 경우 Preview 접두사로 이용하는 터너링 이벤트들을 많이 이용하지 않을 것이고 보다 간단한 버블링 이벤트를 사용할 것이다.
의존성 속성을 직접 손으로 구현하는 경우처럼 터너링 이벤트는 서브 클래스를 가지고 있는 WPF 컨트롤에서 일반적으로 사용된다. 만약 이벤트가 버블링 되는 커스텀 컨트롤을 만든다면 의존 속성과 비슷한 메커니즘을 이용해서 커스텀 라우팅 로직을 구현해주어야 한다. 만약 이 내용에 대한 더 자세한 정보를 보고 싶다면 .NET Framework 3.5 SDK 문서의 “How to: Create a Custom Routed Event”를 살펴볼 것을 권한다.