ICommand는 WPF 컨트롤의 Command 속성에 Binding 하기 위해 사용합니다. WPF 프로젝트가 아니어도 사용할 수 있지만 WPF가 아니라면 굳이 사용할 필요가 없습니다. ICommand는 RelayCommand라는 클래스로 구현하여 사용하는 게 일반적인데 잘 사용하려면 약간의 이해가 필요합니다.
✅ ICommand와 RelayCommand
ICommand가 언제 등장했는지 알 수는 없지만 View의 사용자 명령(이벤트)을 ViewModel에 전달하기 위해 만들어진 게 아닐까 생각합니다. 다시 말해 WPF의 MVVM 패턴을 위해 존재하는 거죠. Codebehind를 사용한다면 ICommand 역시 사용하지 않아도 됩니다.
WPF의 ICommand
ICommand는 인터페이스이므로 구현해야합니다.
WPF 자체적으로 RoutedCommand라는 ICommand를 구현한 Class가 존재하지만 사용자가 ICommand를 직접 구현한 RelayCommand가 코드의 직관성, 간결함, 편의성 등의 장점이 더 많습니다. (RoutedCommand는 좀 어렵습니다.)
public interface ICommand
{
event EventHandler? CanExecuteChanged; // Excute를 실행할 수 있는 상태 확인 이벤트
bool CanExecute(object? parameter); // Excute를 실행할 수 있는 상태 확인 조건
void Execute(object? parameter); // 실행문
}
ICommand는 위와 같은 멤버로 구성된 인터페이스입니다.
위 순서도는 ICommand의 동작 방식입니다.
CanExecuteChanged 이벤트가 발생하면 CanExecute를 통해 컨트롤이 사용가능한지 체크하고 IsEnable 속성을 업데이트합니다. IsEnable이 적용되는 컨트롤은 ButtonBase, MenuItem, Hyperlink 등이 있습니다.
사용자 명령이 발생하면 IsEnable이 true로 활성화된 컨트롤은 Execute에 정의된 동작이 실행됩니다.
ICommand 예제 코드
위 그림과 같은 예제 프로그램을 만들었습니다.
좌측에는 TextBox, 우측에는 Button이 있습니다.
View (xaml)
<Window x:Class="RelayCommandTest.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:RelayCommandTest"
mc:Ignorable="d"
Title="MainWindow" Height="80" Width="400">
<Window.DataContext>
<local:MainViewModel/>
</Window.DataContext>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="3*"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBox Text="{Binding InputString, UpdateSourceTrigger=PropertyChanged}"/>
<Button Grid.Column="1" Content="Button"
Command="{Binding ButtonICommand}" HorizontalAlignment="Stretch" Height="NaN" VerticalAlignment="Stretch" Width="NaN"/>
</Grid>
</Window>
xaml 코드는 위와 같습니다.
1. DataContext를 정의
2. TextBox와 Button을 생성
3. TextBox의 Text, Button의 Command를 ViewModel에 Binding
ViewModel (MainViewModel.cs)
namespace RelayCommandTest
{
public class MainViewModel
{
private string _inputString;
public string InputString
{
get => _inputString;
set
{
_inputString = value;
}
}
public MainViewModel() { }
private class MyICommand : ICommand
{
public event EventHandler? CanExecuteChanged;
public bool CanExecute(object? parameter)
{
return true;
}
public void Execute(object? parameter)
{
MessageBox.Show("MyICommand Execute()");
}
}
private ICommand _buttonICommand;
public ICommand ButtonICommand
{
get {
_buttonICommand ??= new MyICommand();
return _buttonICommand;
}
}
}
}
MyICommand Class를 정의하고 ButtonICommand 객체를 생성했습니다.
ButtonICommand는 View의 Button의 Command와 Binding 되어있습니다.
실행 후 버튼을 클릭하면 아래와 같은 MessageBox가 나타납니다.
위 코드에서 CanExecute()의 return을 false로 바꾸면 버튼이 비활성화되는 것을 확인할 수 있습니다.
버튼이 생성될 때 CanExecute()를 통해 IsEnable을 false로 바꾸기 때문입니다.
CanExecuteChanged 이벤트
TextBox에 문자가 있으면 버튼을 활성화하고 문자가 없으면 버튼을 비활성화하는 기능을 추가합니다.
CanExecuteChanged를 발생시켜서 CanExecute()가 실행되고 그 결과에 따라 컨트롤의 IsEnable이 변경되도록 수정합니다.
위 예제의 MyICommand에 CanExecuteChanged를 호출하는 함수(CheckExecute)를 추가합니다. 매개변수로 bool 타입을 받고 멤버변수로 보관합니다. 직접 CanExecute()를 호출하면 IsEnable가 적용되지 않고 CanExecuteChanged를 통해야 합니다. 아래 코드처럼 수정합니다.
public class MyICommand : ICommand
{
public event EventHandler CanExecuteChanged;
public bool CanExecute(object? parameter)
{
return _canExecute;
}
public void Execute(object? parameter)
{
MessageBox.Show("MyICommand Execute()");
}
bool _canExecute = false;
public void CheckExecute(bool canExetue)
{
_canExecute = canExetue;
this.CanExecuteChanged(this, EventArgs.Empty);
}
}
TextBox와 Binding 된 InputString도 수정합니다.
private string _inputString;
public string InputString
{
get => _inputString;
set
{
_inputString = value;
((MyICommand)ButtonICommand).CheckExecute(!string.IsNullOrEmpty(_inputString));
}
}
TextBox에 입력하면 MyICommand에 true 또는 false를 전달하며 CanExecuteChanged가 발생합니다.
RelayCommand
위 예제처럼 ICommand를 매번 클래스로 구현하여 사용하기 번거롭기 때문에 범용적으로 사용하기 위해서 클래스로 만들어 사용하는 것이 RelayCommand입니다. WPF에서 제공하는 클래스가 아니기 때문에 만든 사람에 따라서 그 형태가 조금씩 다르지만 개념은 동일합니다.
일반적으로 Excute() 시 실행할 함수와 CanExecute()를 체크할 함수(선택)를 가지며 생성자에서 할당합니다.
RelayCommand.cs
public class RelayCommand : ICommand
{
private readonly Predicate<object>? _canExecute;
private readonly Action<object> _execute;
public event EventHandler? CanExecuteChanged;
public RelayCommand(Action<object> execute)
: this(execute, null)
{
}
public RelayCommand(
Action<object> execute,
Predicate<object>? canExecute)
{
_execute = execute;
_canExecute = canExecute;
}
public bool CanExecute(object? parameter)
{
return (_canExecute == null) || _canExecute(parameter);
}
public void Execute(object? parameter)
{
_execute(parameter);
}
public void CheckExecute()
{
CanExecuteChanged(this, EventArgs.Empty);
}
}
정리하면 위와 같은 코드가 됩니다.
RelayCommand를 예제에 적용하면
private string _inputString;
public string InputString
{
get => _inputString;
set
{
_inputString = value;
((RelayCommand)ButtonICommand).CheckExecute();
}
}
InputString은 이렇게 바뀌고
private ICommand _buttonICommand;
public ICommand ButtonICommand
{
get {
_buttonICommand ??= new RelayCommand(ButtonClicked, _ => !string.IsNullOrEmpty(_inputString));
return _buttonICommand;
}
}
private void ButtonClicked(object obj)
{
MessageBox.Show("ButtonClicked");
}
Button에 Binding 된 Command는 위와 같이 쓸 수 있습니다.
실행하면
TextBox에 문자가 있을 때만 Button이 활성화되며 Button 클릭 시 "ButtonClicked" 메시지박스가 나타납니다.
RelayCommand의 완성
위 RelayCommand의 문제는 InputString이 변경될 때마다 CheckExecute()를 호출하여 CanExecuteChanged를 발생시켜야 한다는 점입니다. 참 번거로운데 다행히도 WPF에서 좋은 방법을 제공합니다.
CommandManager를 사용하여 CanExecuteChanged를 추가하면 내부적으로 Command의 조건 변경을 체크하여 매번 CanExecuteChanged를 체크하는 부분을 사용자가 신경 쓰지 않도록 합니다.
너무 길어질 것 같으니 이 부분은 이 정도로 넘어가겠습니다.
RelayCommand.cs
public class RelayCommand : ICommand
{
private readonly Predicate<object>? _canExecute;
private readonly Action<object> _execute;
public RelayCommand(Action<object> execute)
: this(execute, null)
{
}
public RelayCommand(
Action<object> execute,
Predicate<object>? canExecute)
{
_execute = execute;
_canExecute = canExecute;
}
public bool CanExecute(object? parameter)
{
return (_canExecute == null) || _canExecute(parameter);
}
public void Execute(object? parameter)
{
_execute(parameter);
}
public event EventHandler? CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
}
CommandManager를 적용한 최종 코드입니다.
InputString의 ((RelayCommand)ButtonICommand).CheckExecute(); 는 제거합니다.
마무리
제법 장문의 포스팅이 되었네요.
RelayCommand를 generic을 적용해 RelayCommand<T>로 사용하기도 합니다.
RelayCommand는 MVVM라이브러리 등에서 워낙 흔하게 볼 수 있고 또 많이 사용하기도 하는 Class지만 의외로 살펴보지 않는 부분인데 잘 이해하고 사용하면 코드를 좀 더 간결하게 만들 수 있습니다.
✅ ICommand와 RelayCommand - 끝
관련 포스팅