WPF Command 패턴의 이해 및 Command, 데이터바인딩 실습
n 전통적인 이벤트 기반 프로그래밍에서 컨트롤에 이벤트 핸들러 메소드를 코드 비하인드에서 연결하여 사용자의 이벤트를 처리했다. 그러나 이방식은 이벤트처리 핸들러를 재사용하거나 단위 테스트를 어렵게 한다.
n XAML UI에서 버튼을 클릭시 MVVM에서는 Click 이벤트 핸들러를 이용하기 보다는 Commamd를 이용하기를 권장한다. 여러 버튼에서 하나의 Command를 공유할 수 있으므로 모든 컨트롤마다 Click 이벤트를 만드는 방법 보다는 효율적이기 때문이다.
n WPF의 명령(Command)은 ICommand 인터페이스를 구현하여 만들며 ICommand는 Execute 및 CanExecute라는 두 가지 메서드와 CanExecuteChanged 이벤트를 제공한다.
n Execute 메서드는 실제 처리해야 하는 작업을 기술하고 CanExecute 메소드에서는 Execute 메소드의 코드를 실행할지 여부를 결정하는 코드를 기술한다. CanExecute가 false를 리턴하면 Execute 메소드는 호출되지 않는다.
n 즉 CanExecute 메소드는 명령을 사용 가능하게 하거나 사용 불가능하게 할 때 사용되며 명령을 사용할 수 있는지 여부를 확인하기 위해 WPF에 의해 호출된다. 이 메소드는 키보드 GET포커스, LOST포커스, 마우스 업 등과 같은 UI 상호 작용 중에 대부분 발생한다.
n 사용자 정의 명령의 경우 CanExucute 메서드가 대부분의 시나리오에서 호출되지는 않으므로 어떤 조건에 따라 버튼을 활성화, 비활성화 해야 할 수도 있는데 ICommand 구현체에서 CanExecuteChanged 이벤트를 CommandManager의 RequerySuggested 이벤트에 연결하면 된다.
n CanExecute 메소드가 호출되어 CanExecute의 상태가 변경되면 CanExecuteChanged 이벤트가 발생해야 하며, WPF는 CanExecute를 호출하고 Command에 연결된 컨트롤의 상태를 변경한다.
// CanExecuteChanged 이벤트는 해당 ICommand에 바인딩 된
// 모든 명령 소스(예 : Button 또는 MenuItem)에
// CanExecute에 의해 반환 된 값이 변경 되었음을 알린다.
// 커맨드 소스는 일반적으로 상태를 적절히 업데이트해야 하는데
// 예를 들면 CanExecute()가 false를 반환하면 버튼이 비활성화 된다).
// CommandManager.RequerySuggested 이벤트는 CommandManager가 명령 실행에
// 영향을 줄 수있는 변경 사항이 있다고 생각할 때마다 발생하며 이때마다
// CanExecute가 호출된다.
// 예를 들어, 이는 포커스의 변화 일 수 있는데. 이 이벤트가 많이 발생한다.
// 따라서 본질적으로 이 코드의 역할은 CommandManager가 명령 실행 기능이 변경되었다고 생각할 때마다(실제로 변경되지 않은 경우에도) CanExecuteChanged를 발생시키는 것이다.
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
n CommandManager.RequerySuggested 이벤트는 CanExecute 메서드를 강제로 실행할 수 있다.
n CanExecuteChanged 이벤트는 CommandManager의 RequerySuggested에 위임되어 모든 종류의 UI 상호작용을 통해 변경사항이 호출되는 정확한 알림을 제공한다.
n RequerySuggested 이벤트의 CommandManager.InvalidateRequerySuggested()를 호출하여 CommandManager의 RequerySuggested 이벤트를 발생하도록 할 수도 있는데 다음 예제를 참조 하자.
n Command 패턴에서는 몇가지 주체가 있는데 서비스를 요청하는 클라이언트(손님), 명령을 서술하는 Command Object(주문서), 명령을 요청하는 Command Invoker(웨이터), 특정 명령을 실제 처리하는 Command Receiver(Target, 요리사)가 있다.
[먼저 간단한 예제를 만들고 이해를 해보자.]
n 프로젝트 명 : CommandExam
n [실행화면]
n Model : [Emp.cs]
namespace CommandExam
{
class Emp
{
public string Ename { get; set; }
public string Job { get; set; }
public override string ToString()
{
return "[" + Ename + "," + Job + "]";
}
}
}
n Command Object : [RelayCommand.cs]
using System;
using System.Windows.Input;
namespace CommandExam
{
class RelayCommand : ICommand
{
#region Variables
Func<object, bool> canExecute;
Action<object> executeAction;
#endregion
#region Construction/Initialization
public RelayCommand(Action<object> executeAction) : this(executeAction, null)
{
}
public RelayCommand(Action<object> executeAction, Func<object, bool> canExecute)
{
//if (executeAction == null) throw new ArgumentNullException("Execute Action was null for ICommanding Operation.");
//this.executeAction = executeAction;
this.executeAction = executeAction ?? throw new ArgumentNullException("Execute Action was null for ICommanding Operation.");
this.canExecute = canExecute;
}
#endregion
#region ICommand Member
//CommandManager.RequerySuggested
이벤트가 호출될 때마다 실행
//즉 CanExecuteChanged 이벤트가 호출될 때마다 실행
public bool CanExecute(object param)
{
// 사원이름을 입력하지 않으면 Add 버튼은 비활성화 된다.
if (param?.ToString().Length == 0) return false;
bool result = this.canExecute == null ? true : this.canExecute.Invoke(param);
return result;
}
public void Execute(object param)
{
//System.Windows.MessageBox.Show(param.ToString());
this.executeAction.Invoke(param);
}
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
#endregion
}
}
n Command Receiver : [MainWindowViewModel]
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace CommandExam
{
class MainWindowViewModel : INotifyPropertyChanged
{
public Emp _SelectedEmp;
public Emp SelectedEmp
{
get
{
return _SelectedEmp;
}
set
{
_SelectedEmp = value;
OnPropertyChanged("SelectedEmp");
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string Pname = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(Pname));
}
public RelayCommand AddEmpCommand { get; set; }
// 항목을 추가 하거나 제거할 때 알림을 제공 하는 컬렉션 클래스
public ObservableCollection<Emp> Emps { get; set; }
public MainWindowViewModel()
{
Emps = new ObservableCollection<Emp>();
Emps.Add(new Emp { Ename = "홍길동", Job = "Salesman" });
Emps.Add(new Emp { Ename = "김길동", Job = "Clerk" });
Emps.Add(new Emp { Ename = "정길동", Job = "Manager" });
Emps.Add(new Emp { Ename = "박길동", Job = "Salesman" });
Emps.Add(new Emp { Ename = "성길동", Job = "Clerk" });
AddEmpCommand = new RelayCommand(AddEmp);
}
public void AddEmp(object param)
{
Emps.Add(new Emp { Ename = param.ToString(),Job="New Job"});
}
}
}
n Client : [MainWindow.xaml], 여기서 Button은 Command Invoker
<Window x:Class="CommandExam.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:CommandExam"
mc:Ignorable="d"
Title="MainWindow" SizeToContent="Width">
<Window.DataContext>
<local:MainWindowViewModel/>
</Window.DataContext>
<StackPanel>
<TextBlock>사원 이름을 입력하세요.</TextBlock>
<TextBox x:Name="txtName" Text=”{Binding SelectedEmp.Ename}”/>
<Button Command="{Binding AddEmpCommand}" CommandParameter="{Binding Text, ElementName=txtName}">Add</Button>
<ListBox ItemsSource="{Binding Emps}"
SelectedItem="{Binding SelectedEmp}"
DisplayMemberPath="Ename"
x:Name="empListBox"/>
<Label x:Name="label" Content="{Binding SelectedItem, ElementName=empListBox}"
HorizontalAlignment="Center"
Height="40"
Margin="10,0,0,0" Width="137"/>
</StackPanel>
</Window>
n 위 예제에서 Command Invoker는 MainWindow.xaml의 버튼 이며 Command Target(Receiver)은 MainWindowViewModel, ConcreteCommand Object는 RelayCommand 객체이다.