Search

'RoutedCommand'에 해당되는 글 2건

  1. 2009.02.02 WPF Command-Part3 : RoutedCommand2
  2. 2009.01.20 WPF Command -Part2 : RoutedCommand

WPF Command-Part3 : RoutedCommand2

프로그래밍 2009.02.02 20:01 Posted by 아일레프

이번 포스팅도 RoutedCommand가 Invoked되었을 때 RotedCommand 객체가 Command Target을 찾는 과정을 설명하도록 하겠습니다.

Command Target을 찾는 다는 것은 곧 ExecuteEvent Handler를 찾는다는 것을 의미하며, 일반적으로 Command가 Invoke되었을 때 ExecuteEvent 이벤트는 VisualTree내에서 Command를 Invoke를 시킨 Object부터 Tree의 루트까지 타고 올라가게 됩니다.

위와 같은 VisualTree에서 Button1이 특정 RoutedCommand를 Invoke를 했다면 ExecuteEvent는 Button1 -> Grid -> Window로 타고 올라가게 되는 것이고 이는 Button2에서도 마찬가지 입니다. 이것이 가장 일반적인 RouteCommand의 라우팅 메커니즘입니다. 하지만 이 Routing 방법으로는 충분하지 않은 경우가 있습니다. 다음과 같은 경우를 생각해봅시다.

 
위 프로그램은 아래와 같은 VisualTree로 구성 되어있습니다.

코드는 아래와 같습니다.

<Window x:Class="CommandTest.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:loacl="clr-namespace:CommandTest"
Title="Window1" Height="300" Width="300">
<Grid
    <Grid.RowDefinitions
        <RowDefinition Height="Auto"/> 
        <RowDefinition/> 
    </Grid.RowDefinitions
    <Menu
       
<MenuItem Header="Cut" Command="ApplicationCommands.Cut"/> 
    </Menu
    <TextBox Height="20" Grid.Row="1"/> 
</Grid>
</Window>

TextBox에 글자를 쓰고 선택한 후 MenuItem의 "Cut"을 누르면 선택된 글자가 잘려지는 것을 확인할 수 있습니다. 그런데 두 가지 이상한 점이 있습니다. 일단 ExecuteEventHandler를 등록하는 CommandBinding객체가 정의 되어있지 않네요. 일반적으로 사용자는 Cut, Copy, Paste Command에 CommandBinding으로 ExecuteEventHandler를 등록할 필요가 없는데요, 이는 TextBox가 기본적으로 Cut, Copy, Paste Command에 대한 CommandBinding객체를 내장하고 있기 때문입니다. 따라서 이 질문은 해결 되었네요. 두 번째로 Cut이벤트가 발생했을 때 ApplicationCommands.Cut Command객체는 어떻게 TextBox의 ExecuteEventHandler를 찾아갈 수 있었을 까요? Visual Tree를 따라가 보면 다음과 같이 Routing이 진행될 것입니다.

MenuItem->Menu->Grid->Window

Tree의 Root에 이를 때 까지 ApplicationCommands.Cut에 바인딩 되어있는 Command Target을 찾지 못했으므로 아무 일도 벌어지지 않아야 할 것 같은데 분명히 "Cut" Command가 잘 동작하고 있으니 이상한 일입니다.
이는 RoutedCommand의 Routing의 두 번째 원칙 때문입니다. RoutedCommand는 CommandInvoker 부터 Tree의 Root에 이를 때 까지 CommandBinding된 객체를 찾지 못하면 CommandInvoker와 Tree Root가 동일한 Focus Scope에 있는지 확인 합니다. 만약 CommandInvoker와 Tree의 Root가 동일한 Focus Scope에 있지 않다면 RoutedCommand는 Tree Root의 Logical Focus를 가지고 있는 컨트롤을 기점으로 해서 다시 Routing을 시작합니다. 즉 다음과 같이 Routing이 되는 것입니다.

첫 번째 Routing : MenuItem->Menu->Grid->Window ---- CommandTarget 찾기 실패 
 
MenuItem과 Window가 동일한 Focus Scope에 있지 않으므로 Window의 Local Focus를 가지는 Control을 기점으로 다시 Routing시작

두 번째 Routing: TextBox -> CommandTarget찾기 성공

위와 같은 방법으로 RoutedCommand는 CommandTarget을 찾을 수 있었던 겁니다. 위의 일이 가능하게 하기 위해서는 MenuItem과 Window가 다른 FocusScope내에 있어야 하는데 WPF에서 Menu와 ToolBar와 같은 Control은 기본적으로 FocusScope을 가지고 있기 때문입니다.

이번에는 다음과 같은 시나리오를 생각해 보겠습니다.
 

<Window x:Class="CommandHandler.Window1"

    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

    Title="Window1" Height="300" Width="300" >

   

    <Grid>

        <StackPanel>

            <Button Command="ApplicationCommands.Cut">Cut</Button>

            <TextBox Name="textBox"/>

        </StackPanel>       

    </Grid>

</Window>

위 프로그램에서 TextBox에 글자를 쓰고 선택해도 Cut 버튼은 절대 활성화 되지 않습니다.
 
Button -> TextBox->Grid->Window
 
Tree의 Root에 이르기까지 아무런 ApplicationCommands.Cut에 바인딩 되어있는 객체를 찾지 못했으므로 Cut버튼이 활성화 되지 않는 것입니다. 이를 해결하기 위해서는? 예, 맞습니다. Button을 Window와 다른 Focus Scope내에 두어야 합니다. 다음과 같은 코드로 이것이 가능합니다.
<Button Command="ApplicationCommands.Cut" FocusManager.IsFocusScope="True">Cut</Button>
 
다시 프로그램을 실행하면 기대하는 결과를 볼 수 있을 겁니다.
 
Command에 대한 이슈는 이것이 끝이 아닙니다. 앞으로 두 번 정도 더 포스팅 할 계획인데, 다음 포스팅에서는 DelegateCommand라는 것을 소개할 것이고, 그 다음 포스팅에서는 Command가 event에 비해 가지는 장점에 대해 알아보도록 하겠습니다.
신고

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에 있는 지를 고려한다고 합니다. 이 부분은 다음 포스팅에서 설명하도록 하겠습니다.

신고


 

티스토리 툴바