WPF Command-Part4 : DelegateCommand

프로그래밍 2009.02.26 22:24 Posted by 아일레프

(미루고 미루다 이제야 마지막 포스팅이네요. ㅜ.ㅜ)

지난 포스팅까지 RoutedCommand에 대해서 살펴보았습니다. RoutedCommand라는 이름이 붙은 이유는 Command Target을 찾기 위해RoutedEvent를 사용하기 때문이라는 것도 알 수 있었죠. 그런데 개발자들은 다른 유형의 Command에 대한 필요성을 느끼게 되었습니다.

적절한 UI를 구성하기 위해 개발자들이 먼저 생각하는 것은 바로 View와 논리의 분리일 것입니다. 현재 UI Design Pattern으로 널리 사용되는 MVC, MVP, MVVM의 경우도 View와 Model, Logic의 결합도를 줄임으로써 확장성을 늘리고 테스트 하기 쉬운 구조를 만들어 낸 바 있습니다. 그런데 RoutedCommand의 경우는 RoutedCommand가 CommandTarget을 View의 Visual Tree내에서 찾기 때문에 View와 Logic을 완전히 분리하기 어렵습니다. 때문에 여러 개발자들은 다른 Command에 대한 필요성을 느끼게 되었고 그 결과 DelegateCommand라는 녀석을 만들어 냈습니다. 그리고 이 DelegateCommand는 Composite Application Guidance for WPF에 포함되게 됩니다. MSDN Magazine에 소개된 DelegateCommand의 구현 코드는 다음과 같습니다.

public class StringDelegateCommand : ICommand {
  Action<string> m_ExecuteTargets = delegate { };
  Func<bool> m_CanExecuteTargets = delegate { return false; };
  bool m_Enabled = false;
  public bool CanExecute(object parameter) {
    Delegate[] targets = m_CanExecuteTargets.GetInvocationList();
    foreach (Func<bool> target in targets) {
      m_Enabled = false;
      bool localenable = target.Invoke();
      if (localenable) {
        m_Enabled = true;
        break;
      }
    }

    return m_Enabled;
  }
  public void Execute(object parameter) {
    if (m_Enabled)
      m_ExecuteTargets(parameter != null ? parameter.ToString() : null);
  }
  public event EventHandler CanExecuteChanged = delegate { };
  ...
}
간단하지요? 위 녀석은 Command Parameter로 string 객체를 전달하기 때문에 StringDelegateCommand라는 이름이 붙었습니다. 좀 더 일반적인 녀석을 만들어볼까요? 다음과 같이 만들 수 있겠네요.
public class DelegateCommand<T> : ICommand
{
        Action<T> executeTargets = delegate { };
        Func<bool> canExecuteTargets = delegate { return false; };
        bool m_Enabled = false;
        public bool CanExecute(object parameter)
        {
            Delegate[] targets = canExecuteTargets.GetInvocationList();
            foreach (Func<bool> target in targets)
            {
                m_Enabled = false;
                bool localenable = target.Invoke();
                if (localenable)
                {
                    m_Enabled = true;
                    break;
                }
            }
            return m_Enabled;
        }
        public void Execute(object parameter)
        {
            if (CanExecute(parameter) == true)
                executeTargets(parameter != null ? (T)parameter : default(T));
        }
        public event EventHandler CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }
        public event Action<T> ExecuteTargets
        {
            add
            {
                executeTargets += value;
            }
            remove
            {
                executeTargets -= value;
            }
        }
        public event Func<bool> CanExecuteTargets
        {
            add
            {
                canExecuteTargets += value;
            }
            remove
            {
                canExecuteTargets -= value;
            }
        }
    }
위 녀석은 Generic으로 구현된 DelegateCommand입니다. Command Parameter로 사용하기 위한 Type을 T로 지정해 사용하면 됩니다. 그리고 이 경우 ExecuteTargets에 Handler를 등록함으로써 하나의 Command이 실행되었을 때 여러 개의 Command Target이 실행되게 할 수 있습니다.

자, 다음과 같은 질문을 해봅시다. 왜 굳이 Command를 사용하는 것이 좋을까요? 만약 특정 버튼을 눌렀을 때 "Save"란 일을 하고 싶다면 이것은 충분히 이벤트 만으로 가능해 보이는데 말입니다. 다음과 같이 말이죠. 

<Button Click="Button_Click" Content="Save"/>
private void Button_Click(object sender, RoutedEventArgs e)
{

    Save();
}


위와 똑 같은 일을 하는 녀석을 Command로 만들어 보면 다음과 같을 것입니다.

<Button Command="{Binding SaveCommand}" Content="Save"/>
public Window1()
{
      InitializeComponent();
     
this.DataContext = this;
}
public ICommand SaveCommand
{
      
getreturn saveCommand; }
} 
어떤 차이가 있는지 보이십니까? 언뜻 보면 이벤트나 Command나 거기서 거기 같아 보입니다. 그러나 중요한 사실이 있습니다. Application을 구현할 때는 Business Logic을 변경할 때 보다 View를 변경하는 일이 더욱 많다는 사실입니다. Event로 Save Button을 만들었다고 합시다. 만 약 그 후에 고객이 Save Menu를 만들어 달라고 요청했다고 가정해 보겠습니다. 이 경우 event를 사용했다면 각 메뉴항목마다 이벤트를 또 걸어주어야 합니다. 만약 Command를 사용했다면? 단순히 xaml만을 교체해 Command를 Binding하는 것으로 끝나게 될 것입니다. Command로 느슨하게 결합되어 있기 때문입니다.

또한 제가 좋아하는 Command의 강력한 기능은 CanExecute입니다. 위 Save버튼이 활성화 되기 위한 조건을 CanExecute에 만들어 놓으면 모든 경우에 대해 버튼의 활성화 여부에 대해 걱정할 필요가 없습니다. 만약 Login 창을 만든다고 가정해 볼까요? UserID와 Password를 입력할 수 있는 TextBox가 필요하겠네요. 그리고 Login버튼이 있을 것입니다. 이 Login버튼이 활성화 될 수 있는 조건은 UserId와 Password TextBox에 적당한 Text가 쓰여졌을 때일 것입니다. 만약 Command를 사용하지 않는다면? 우리는 사용자가 새로운 문자를 UserId, Password에 입력할 때 마다 Login버튼의 활성화 여부를 확인해야 할 것입니다. 굉장히 우울한 사실이죠.

Command는 또한 Model, View, Logic을 분리하기 위해 많이 사용되는 MVP, MVVM Pattern에도 사용됩니다. MVVM패턴의 경우 직접 UIElement를 만드는 것이 아니라 보통 Template를 활용하는데 이 때 Template내에서 event를 사용하는 것이 아니라 Command를 바인딩 해주는 것이죠. 이 MVVM Pattern을 사용하면 Compile을 다시 하지 않고도 View를 교체하는 것이 가능해집니다. Event를 사용했을 때는 이와 같은 효과를 얻을 수 없습니다. Command Invoker와 Command Target이 Binding을 통해 느슨하게 결합되었기에 얻을 수 있는 이점이라 할 수 있습니다. 간단히 Person정보를 저장하는 녀석을 만들어 보겠습니다. 먼저 Model입니다.

public class PersonModel : INotifyPropertyChanged
{
        private string name;
        private string address;
        public string Name
        {
            set 
            {
                name = value
                if(PropertyChanged != null)
                {
                    PropertyChanged(this, new PropertyChangedEventArgs("Name"));
                }
            }
            get 
            {
                return name; 
            }
        }
        public string Address
        {
            set
            {
                address = value;
                if (PropertyChanged != null)
                {
                    PropertyChanged(this, new PropertyChangedEventArgs("Address"));
                }
            }
            get
            {
                return address;
            }
        }
        public event PropertyChangedEventHandler PropertyChanged;
        public void Clear()
        {
            Name= "";
            this.Address= "";
        }
    }

ViewModel은 다음과 같을 것입니다.
public class PersonViewModel
{
        PersonModel personModel = new PersonModel();
        public PersonModel PersonModel
        {
            get { return personModel; }
        }
        DelegateCommand<string> saveCommand; 
        public DelegateCommand<string> SaveCommand
        {
            get { return saveCommand; }
        } 
       
public PersonViewModel()
       
{
            saveCommand = new DelegateCommand<string>();
            saveCommand.CanExecuteTargets += () => !string.IsNullOrEmpty(personModel.Name) && !string.IsNullOrEmpty(personModel.Address);
            saveCommand.ExecuteTargets += s => this.Save(s);
        }
        private void Save(string parameter)
        {
            //Save Operation 

            //Clear PersonModel
           PersonModel.Clear();
        }
    }

View를 만들어 볼까요? 귀찮아서 그냥 윈도우로 하겠습니다.
<Window.Resources>
        <DataTemplate  DataType="{x:Type local:PersonViewModel}" >
            <StackPanel>
                <TextBox Text="{Binding PersonModel.Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
                <TextBox  Text="{Binding PersonModel.Address, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
                <Button Content="Save" Command="{Binding SaveCommand}"/>
            </StackPanel>
        </DataTemplate>
        <local:PersonViewModel x:Key="PersonViewModel"/>
    </Window.Resources>
    <Window.Content>
        <StaticResource ResourceKey="PersonViewModel"/>
    </Window.Content>

위와 같이 Template를 사용하기 싫으신 분도 있을 텐데 그러면 다음과 같이 만들어 주면 됩니다. Template를 써야 MVVM Pattern이 되는 것은 아니니까요.  

    <
Window.Resources>
        <local:PersonViewModel x:Key="PersonViewModel"/>
    </Window.Resources>
    <Window.Content>
        <StackPanel DataContext="{StaticResource PersonViewModel}">
            <TextBox Text="{Binding PersonModel.Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
            <TextBox  Text="{Binding PersonModel.Address, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
            <Button Content="Save" Command="{Binding SaveCommand}"/>
        </StackPanel>
    </Window.Content>
엄청나게 간단한 예제네요^^;; 쉽지요?

Command에 대한 Posting을 이제 끝내려고 합니다. . 생각했던 것 외로 제가 확실하게 몰랐던 부분이 있어 시간이 더 걸렸던 것 같습니다. 그러나 Command에 대한 논의는 이것으로 끝나지 않을 것 같습니다. 사실, 정확하게 WPF가 RoutedCommand를 어떻게 처리하고 CommandManager의 역할은 아직 잘 모르겠습니다. 나중에 살펴보고 제가 명확하게 알았다고 인지했을 때 여러분께 공유하도록 하겠습니다.

사족 :
사실, 예제로 Login을 들려고 했었는데 심각한 문제가 있었습니다. Password를 넣기 위해 사용되는 PasswordBox의 Password Property가 DependencyProperty가 아니라 Binding을 지원하지 않는 문제였지요. 잠시 패닉상태였습니다. ㅋ 이 부분에 대해 google에 Search를 해보니 멋진 글이 있어 소개하려 합니다. Attached Property를 이용해 PasswordBox의 Password를 Binding하는 예가 담겨있습니다. 사람들, 참 머리 좋아요~ WPF PasswordBox and Data binding
그리고 MSDN Magazine의 WPF Apps With The Model-View-ViewModel Design Pattern입니다.
마지막으로 The Build Your Own CAB Series Table of Contents 도 실로 멋지죠.

신고


 

티스토리 툴바