6) RelayCommand
MVVM 패턴을 이용한 개발을 해보셨다면 익숙할 만한, ICommand의 보편적인 구현체인 RelayCommand입니다. CommunityToolkit.Mvvm 패키지에서는 일반적인 RelayCommand 클래스뿐 아니라 비동기 타입의 AsyncRelayCommand 클래스도 제공하며, 커맨드를 자동 생성해주는 RelayCommand 특성도 제공합니다. 이 특성은 어떠한 클래스든 partial로 선언되었다면 사용할 수 있으며, 메서드에 추가할 수 있습니다.
RelayCommand 특성을 이용하면 Command 필드를 만들어 속성으로 제공하는 과정을 간소화할 수 있습니다.
전통적인 커맨드 노출 방식
public partial class LoginViewModel
{
private RelayCommand? _loginCommand;
public ICommand LoginCommand => _loginCommand ??= new RelayCommand(Login);
private void Login()
{
}
}
RelayCommand 특성을 이용한 방식
public partial class LoginViewModel
{
[RelayCommand]
private void Login()
{
}
}
동기 메서드 뿐만 아니라 비동기 메서드에 대한 Command 생성도 지원합니다.
[RelayCommand]
private async Task LoginAsync()
{
}
이때 자동 생성되는 Command의 이름은 기본적으로 메서드 이름에 접미사 Command를 붙인 형태가 되는데, 구체적으로는 다음과 같은 과정을 거쳐 결정됩니다.
- 메서드 이름이 On으로 시작하는 경우 - On 제거
- 메서드 이름이 Async로 끝나며 return type이 Task인 경우 - Async 제거
즉 Task를 반환하는 메서드 OnMessageAsync에 RelayCommand 특성을 추가했을 때 생성되는 Command 이름은 MessageCommand가 됩니다.
유의점은 Async 접미사가 붙은 경우 이것이 제거되는지 여부는 메서드가 비동기인지 여부가 아니라, return type이 Task인지 여부에 따라 결정된다는 것입니다.
[RelayComand]
private Task OnMessageAsync() // CommandName: MessageCommand
[RelayCommand]
private async void OnMessageAsync() // CommandName: MessageAsyncCommand
또한 Command의 실행 가능 여부를 제어하는 CanExecute에 대한 핸들링도 쉽게 할 수 있습니다. 예를 들어 Id의 길이가 3 이상인 경우에만 LoginCommand를 실행할 수 있게 하고 싶다고 가정해 봅시다.
ViewModel
public partial class LoginViewModel : ObservableObject
{
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(CanLogin))]
private string? _id;
public bool CanLogin => !string.IsNullOrWhiteSpace(Id) && Id.Length >= 3;
[RelayCommand(CanExecute = nameof(CanLogin))]
private void Login()
{
}
}
View
<StackPanel Width="100"
VerticalAlignment="Center">
<TextBox Text="{Binding Id, UpdateSourceTrigger=PropertyChanged}"
Margin="0 10"/>
<Button Height="40"
Content="Login"
Command="{Binding LoginCommand}"/>
</StackPanel>
위에서 보는 바와 같이 TextBox에 값이 들어있지 않을 때 Login 버튼이 비활성화 된 것을 볼 수 있습니다.
그런데 위와 같은 상태에서 TextBox에 값을 입력해 보면 이상한 점을 발견할 수 있는데요. 분명 CanLogin은 Id 속성의 길이가 3 이상일 때 true를 반환하는데, 입력값을 3자 이상 입력하더라도 Login 버튼이 활성화되지 않습니다. _id 필드에 CanLogin 속성의 변경 알림도 함께 구현했음에도 불구하고 말이죠.
그 이유는, ObservableProperty 및 NotifyPropertyChangedFor 특성은 속성의 값 변경 관련 알림만 구현할 뿐, 특정한 Command의 CanExecute 변경 알림은 구현하지 않기 때문입니다. 사실 이 상태에서 필드나 속성은 자신이 어떤 Command와 연관이 있는지도 알지 못합니다.
그렇다면 Command의 CanExecute 값 변경을 쉽게 알리는 방법은 없을까요?
당연히 있습니다.
7) NotifyCanExecuteChangedFor
NotifyCanExecuteChangedFor 특성은 ObservableObject 클래스를 상속받거나 ObservableObject 특성이 추가된 클래스에서만 사용할 수 있으며, ObservableProperty 특성이 추가된 필드에 추가할 수 있습니다. 이 특성은 Command의 이름(string)을 매개변수로 받는데, 특성을 추가하면 속성 값 변경 시 해당 Command의 CanExecute 변경도 함께 Notify합니다.
public partial class LoginViewModel : ObservableObject
{
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(CanLogin))]
[NotifyCanExecuteChangedFor(nameof(LoginCommand))]
private string? _id;
public bool CanLogin => !string.IsNullOrWhiteSpace(Id) && Id.Length >= 3;
[RelayCommand(CanExecute = nameof(CanLogin))]
private void Login()
{
}
}
위 코드에서 Login 메서드로부터 생성된 LoginCommand는 CanLogin 속성의 반환값에 따라 실행 여부가 결정되며, CanLogin은 Id 속성에 의해 값이 결정됩니다. 따라서 Id 속성이 변경될 때마다 LoginCommand의 CanExecuteChanged가 호출되며, LoginCommand에서 CanLogin 속성의 반환값을 확인해 Command의 실행 가능 여부를 확인하게 됩니다.
이제 입력값이 3자 이상일 때 Login 버튼이 정상적으로 활성화됩니다.