이벤트 대신 ICommand로 액션 연결하기
이벤트 대신 ICommand로 액션 연결하기
요약: 버튼 클릭을 코드비하인드가 아니라 ViewModel의 ICommand로 연결하고, CanExecute로 활성 상태를 맞추는 방법을 정리한다.
1. 왜 Command인가
Button.Click을 .xaml.cs에 두면 저장·검증이 View에 붙는다. 창을 띄우지 않고 로직을 시험하기 어렵다.
ICommand는 버튼이 실행할 일을 ViewModel에 맡기는 계약이다. XAML은 Command="{Binding SaveCommand}"만 안다. 실행 가능 여부는 CanExecute가 버튼 IsEnabled와 연결된다.
언제 이벤트가 남나: 마우스 좌표, 드래그, 패스워드 박스처럼 바인딩이 약한 접착. 저장·검색·삭제 같은 도메인 동작은 명령이다.
2. 핵심 개념
| 구성 | 한 줄 |
|---|---|
ICommand.Execute |
클릭 시 할 일 |
ICommand.CanExecute |
지금 실행 가능한지. false면 버튼 비활성 |
CanExecuteChanged |
가능 여부가 바뀌었다고 UI에 알린다 |
CommandParameter |
목록 행 등 추가 인자를 명령에 넘긴다 |
[RelayCommand] |
CommunityToolkit.Mvvm이 위 구현을 생성한다 |
ICommand는 WPF 네임스페이스 System.Windows.Input에 있다. ViewModel 프로젝트가 WPF를 참조하기 싫으면 툴킷의 IRelayCommand를 쓴다. 단일 프로젝트면 ICommand로 충분하다.
3. 동작하는 예: 저장 명령
ViewModels/MainViewModel.cs:
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace MyApp.ViewModels;
public partial class MainViewModel : ObservableObject
{
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(SaveCommand))]
private string title = "";
[ObservableProperty]
private string status = "대기";
[RelayCommand(CanExecute = nameof(CanSave))]
private void Save()
{
Status = $"저장됨: {Title.Trim()}";
}
private bool CanSave() => Title.Trim().Length > 0;
[RelayCommand]
private void Clear()
{
Title = "";
Status = "대기";
}
}
[RelayCommand]onSave→SaveCommand속성 생성CanExecute = nameof(CanSave)→ 비어 있으면 저장 버튼 비활성[NotifyCanExecuteChangedFor(nameof(SaveCommand))]→Title이 바뀔 때CanExecute를 다시 묻는다
XAML:
<StackPanel Margin="16">
<TextBox Text="{Binding Title, UpdateSourceTrigger=PropertyChanged}" />
<Button Content="저장" Command="{Binding SaveCommand}" Margin="0,8,0,0" />
<Button Content="지우기" Command="{Binding ClearCommand}" />
<TextBlock Text="{Binding Status}" Margin="0,8,0,0" />
</StackPanel>
비동기:
[RelayCommand]
private async Task SaveAsync(CancellationToken cancellationToken)
{
Status = "저장 중";
await Task.Delay(400, cancellationToken);
Status = "저장됨";
}
생성 이름은 SaveCommand다 (SaveAsyncCommand가 아니다). UI 스레드를 막지 않으려면 async 명령을 쓴다. Task.Delay 자리는 실제 I/O다.
4. 파라미터와 목록
행 삭제:
<Button Content="삭제"
Command="{Binding DataContext.RemoveCommand, RelativeSource={RelativeSource AncestorType=ListBox}}"
CommandParameter="{Binding}" />
ViewModel:
[RelayCommand]
private void Remove(UserItem item) => Users.Remove(item);
CommandParameter="{Binding}"은 그 행의 UserItem이다. 행 템플릿의 DataContext가 항목이라, 부모 ViewModel 명령은 RelativeSource로 올라간다.
코드비하인드 최소 연결이 필요하면 EventToCommand(툴킷 Behaviors)를 검토한다. 신규는 가능하면 Command 속성만 쓴다.
툴킷 없이 직접 구현하는 최소형:
public sealed class RelayCommand : ICommand
{
private readonly Action _execute;
private readonly Func<bool>? _canExecute;
public RelayCommand(Action execute, Func<bool>? canExecute = null)
{
_execute = execute;
_canExecute = canExecute;
}
public bool CanExecute(object? parameter) => _canExecute?.Invoke() ?? true;
public void Execute(object? parameter) => _execute();
public event EventHandler? CanExecuteChanged;
public void RaiseCanExecuteChanged() =>
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
직접 구현이면 Title setter에서 SaveCommand.RaiseCanExecuteChanged()를 호출해야 버튼이 살아난다. 빼먹기 쉽다. 그래서 툴킷을 기본선으로 둔다.
5. 주의 / 흔한 실수
Click과Command를 동시에. 두 번 실행되거나 한쪽이 무시된다. 하나만 둔다.CanExecute를 바꿨는데CanExecuteChanged를 안 올림. 버튼이 영원히 비활성이다.[NotifyCanExecuteChangedFor]또는NotifyCanExecuteChanged().async void이벤트 핸들러에 저장. 예외가 삼켜진다.[RelayCommand] Task를 쓴다.- 명령에서
MessageBox. View 의존. 5편 대화상자 서비스. RelayCommand를 프로퍼티 get마다new. 바인딩이 매번 새 객체다. 필드에 한 번 만들거나 툴킷 생성 명령을 쓴다.
정리
WPF 액션의 기본은 이벤트 남발이 아니라 테스트 가능한 Command다.
- 클릭 →
{Binding XxxCommand} - 활성 →
CanExecute+ 속성 변경 시 재평가 - 행 인자 →
CommandParameter
연습
Title이 비어 있으면 저장 버튼이 비활성인지, 한 글자 입력 후 활성화되는지 확인한다.ClearCommand로 제목과 상태가 초기화되는지 확인한다.Save를async Task로 바꾸고 저장 중Status가저장 중으로 보이는지 확인한다.
댓글
0아직 댓글이 없습니다. 첫 댓글을 남겨보세요.