WPF Command -Part2 : RoutedCommand

프로그래밍 2009.01.20 21:02 Posted by 아일레프
  • 이 포스트는 기본적인 RoutedCommand의 바인딩과 CommandBinding을 통한 Handler 등록 방법을 알고 있을 때 이해하기 쉽습니다.

이전 Post에서 만든 Command 객체는 치명적인 문제를 가지고 있었죠. 그 문제를 살펴보면

  1. Command Object와 Command Target이 분리되어있지 않다.
  2. Command를 실행했을 때의 작업을 수정하려면 Command 객체를 수정하고 재 컴파일 해야한다.

결국 1,2번 문제 모두가 Command Object와 Command Target이 분리되어있지 않기 때문에 생기는 문제입니다. Command Object와 Target을 분리하는 가장 간단한 방법은 무엇일까요? 예, 맞습니다. 바로 Event를 사용하는 것이죠. Command Object가 명령을 받았을 때의 행동을 직접 정의하는 것이 아니라 단지 Command가 실행되었을 때 Command Object가 이벤트를 발생시키고 해당 이벤트를 구독하는 녀석이 실제 명령을 수행하게 하는 것입니다. 그리고 이 이벤트를 구독하는 녀석이 바로 Command Target이 되는 것입니다. 누구나 쉽게 생각할 수 있는 이 방법을 통해 WPF는 RoutedCommand라는 녀석을 미리 구현해 놓았습니다. 그런데 저에게는 그렇게 쉽지 않더군요 ^^;; 

RoutedCommand라는 녀석의 이름답게 RoutedCommand는 RoutedEvent를 사용합니다. RoutedEvent를 사용해 자신의 Command Target을 찾는 것입니다. 다음과 같은 Visual Object Tree를 생각해봅시다.

 Button에 특정 RoutedCommand가 바인딩 되어있다고 합시다.(Button이 Invoker가 됩니다.) 위Button이 눌리면 RoutedCommand 에서 ExecuteEvent를 발생시킵니다. 이렇게 발생된 이벤트는 Button -> Grid -> Windows 순으로 Routing되는 것입니다. 좀 더 일반적으로 말하자면 Command Invoker부터 Visual Tree Root까지 ExecuteEvent가 Routing됩니다.

여기까지는 쉽죠? 예 쉽습니다.

그러면 적절한 시기에 Command 객체의 "CanExecute" 메소드에 대해 알아볼까요? 이 녀석은 일반적으로 Command가 현재 실행 가능한지 가능하지 Command Invoker에게 알려주는 역할을 합니다. 예를 들어 RoutedCommand에 바인딩 된 Command Invoker객체는 자신이 Render되었을 때 RoutedCommand의 CanExecute 메소드를 실행시킨 뒤 그 결과가 true라면 자신을 활성화 시키고 그렇지 않으면 비활성화 시킵니다.

여기까지도 쉬운가요? 쉬웠으면 좋겠습니다.

 그런데 RoutedCommand에서 사용되는 RoutedEvent와 일반 RoutedEvent와의 차이점은 여기서부터 시작됩니다. 이것에 대해 설명하기 위해 이전 포스트에서 사용했던 맥도날드의 Command Pattern을 살펴보기로 하죠.  

CanExecute

자, 주문하는 사람은 Command Invoker, 웨이터는 Command Object, 햄버거 만드는 사람은 Command Target입니다. 주문하는 사람은 웨이터에게 먼저 이렇게 물어볼 것 입니다. "빅맥 햄버거를 먹을 수 있나요?" 웨이터는 현재 빅맥 햄버거를 만들 수 있는 지 알아야 합니다. 때문에 햄버거 만드는 사람들에게 물어보겠죠. "지금 빅맥 햄버거를 만들 수 있는 사람 있습니까?" 중요한 것은 빅맥 햄버거를 만들 수 있는 사람이 적어도 한 명 있으면 주문하는 사람에게 "예, 지금 햄버거를 만들 수 있어요"라고 말한다는 사실 입니다.

자, 이전에 살펴본 Visual Object Tree입니다. 이전과 마찬가지로 Button에 특정 RoutedCommand가 바인딩 되어있다고 가정해 보겠습니다. Button이 Render되면 자신이 실행 가능한 녀석인지 알기 위해 RoutedCommand의 CanExecute 메소드를 실행합니다. 그리고 RoutedCommand는 CanExecute 이벤트를 발생시키죠. 이 이벤트의 Handler의 시그니쳐는 다음과 같습니다.

void CanExecuteRoutedEventHandler(object sender, CanExecuteRoutedEventArgs e)

이 이벤트는 Button -> Grid -> Windows로 타고 올라갑니다. 그리고 만약 Handler 중에서 CanExecuteRoutedEventArgs의 CanExecute Property를 true로 만드는 녀석이 적어도 하나라도 있다면 RoutedCommand는 Button에게 "난 실행될 수 있는 Command야"라고 알려주게 됩니다. 적어도 하나라는 사실을 꼭 기억해 주시기 바랍니다.  

Execute

자 이제 맥도날드에서 주문을 해볼까요? 여러분은 웨이터(Command Object)를 통해 빅맥을 주문할 수 있다는 사실을 알았습니다.(CanExecute 메소드를 통해) 여러분이 빅맥을 주문하면 웨이터는 햄버거를 만드는 사람에게 햄버거를 만들라고 할 것입니다. 주의 해야하는 사실은 웨이터가 단 한 명의 사람에게 햄버거를 만들라고 해야 한다는 사실입니다. 만약 여러 명에게 햄버거를 만들라고 한다면 주문한 햄버거는 하나인데, 여러 개의 햄버거가 만들어져 있겠죠? RoutedCommand도 마찬가지 입니다. Button이 눌러지면 RoutedCommand의 Execute메소드가 실행되고 RoutedCommand는 ExecuteEvent RoutedEvent를 Command Invoker Node부터 발생시킬 것입니다.

이 경우도 Button -> Grid -> Windows순으로 ExecuteEvent가 발생하게 됩니다. 중요한 것은 특정 Command Target의 ExecuteEventHandler가 해당 Command를 처리한다면 e.handled를 true로 만들어 더 이상 Routing을 진행하지 않는 다는 것입니다. 즉 단 하나의 ExecuteEvent Handler만 실행된다는 사실이죠.

MSDN Magazine의 Understanding RoutedEvent and Command는 이 것을 다음과 같이 설명하고 있습니다.

The difference between routed commands and routed events is in how the command gets routed from the command invoker to the command handler. Specifically, routed events are used under the covers to route messages between the command invokers and the command handlers (through the command binding that hooks it into the visual tree).

There could be a many-to-many relationship here, but only one command handler will actually be active at any given time. The active command handler is determined by a combination of where the command invoker and command handler are in the visual tree, and where the focus is in the UI. Routed events are used to call the active command handler to ask whether the command should be enabled, as well as to invoke the command handler's Executed method handler.

위의 굵은 글씨로 표시된 부분이 바로 이를 설명하고 있는 것입니다.

위의 글에는 몇 가지 추가적인 내용이 있는데요, CommandHadler를 선택할 때 CommandInvoker의 VisualTree내의 위치와 현재 Focus가 어느 UI에 있는 지를 고려한다고 합니다. 이 부분은 다음 포스팅에서 설명하도록 하겠습니다.

신고