📝[WPF] CommunityToolkit.Mvvm을 활용한 RelayCommand 구현
📌1. RelayCommand란?
RelayCommand는 WPF MVVM 패턴에서 사용자 인터랙션(버튼 클릭, 메뉴 선택 등)을 처리하기 위한 핵심 구현체이다.
ViewModel과 View 간의 느슨한 결합을 유지하면서도 효과적인 명령 처리를 가능하게 해준다.
Microsoft의 CommunityToolkit.Mvvm 패키지에서 제공하는 라이브러리로서 해당 어트리뷰트를 사용하면
Source Generator가 컴파일 타임에 필요한 코드를 자동 생성해주어 매우 편리하다.
CommunityToolkit.Mvvm (MVVM 라이브러리)란?
- Microsoft가 관리하는 .NET Community Toolkit의 일부
- 기능: Source Generator를 통한 보일러플레이트 코드 생성
- WPF UI 문서에서 "Dependency Injection과 MVVM을 기반으로 한 프로젝트를 자동 생성하는 Visual Studio 플러그인"을 제공 NuGet Gallery | WPF-UI 4.0.3하기 때문에, 개발자들이 두 라이브러리를 함께 사용하는 경우가 많다고 한다.
💠다른 언어와의 비교: Java Lombok과의 유사성
C#의 [RelayCommand], [ObservableProperty] 등의 어트리뷰트는 Java의 Lombok 라이브러리와
매우 유사한 철학을 가지고 있다.
Java (Lombok)C# (CommunityToolkit.Mvvm)목적
@Getter / @Setter | [ObservableProperty] | 속성 자동 생성 |
@Data | ObservableObject 상속 | 보일러플레이트 제거 |
@NoArgsConstructor | Source Generator | 생성자 자동 생성 |
컴파일 타임 어노테이션 처리 | Source Generator | 코드 자동 생성 |
Java Lombok 예시:
@Data
public class PersonModel {
@Getter @Setter
private String name;
@Getter @Setter
private int age;
}
C# CommunityToolkit 예시:
public partial class PersonViewModel : ObservableObject
{
[ObservableProperty]
private string name;
[ObservableProperty]
private int age;
}
두 방식 모두 어노테이션/어트리뷰트 기반 메타프로그래밍을 통해 반복적인 코드 작성을 제거하고, 개발자가 비즈니스 로직에 집중할 수 있게 된다.
📌2. CommunityToolkit.Mvvm 패키지 사용하지 않는다면?
WPF-UI 프레임워크를 사용하지 않고 직접 구현하면 아래와 같다.
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Input;
public class OldSchoolViewModel : INotifyPropertyChanged
{
private string _text;
public string Text
{
get => _text;
set { _text = value; OnPropertyChanged(); }
}
private int _counter;
public int Counter
{
get => _counter;
set { _counter = value; OnPropertyChanged(); }
}
public ICommand CounterIncrementCommand { get; }
public OldSchoolViewModel()
{
CounterIncrementCommand = new RelayCommand(OnCounterIncrement);
}
private void OnCounterIncrement()
{
Counter++;
Text = "test임";
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string name = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}
- 많은 보일러플레이트 코드
- 수동 CanExecute 관리의 복잡성
CanExecute란?
CanExecute는 ICommand 인터페이스의 핵심 구성 요소로, 명령(Command)이 현재 실행 가능한 상태인지를
판단하는 메서드이다.
// ICommnad 인터페이스 구조
public interface ICommand
{
// 명령을 실행하는 메서드
void Execute(object parameter);
// 명령이 실행 가능한지 판단하는 메서드
bool CanExecute(object parameter);
// CanExecute 상태가 변경되었을 때 발생하는 이벤트
event EventHandler CanExecuteChanged;
}
📌3. 반면 WPF-UI FrameWork를 사용한다면?
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
public partial class ToolkitViewModel : ObservableObject
{
[ObservableProperty]
private string? text;
[ObservableProperty]
private int counter;
[RelayCommand]
private void CounterIncrement()
{
Counter++;
Text = "test임";
}
}
위와 같이 간단하게 작성하면 끝난다.
그리고 실제로 CommunityToolkit SourceGenerator라는 녀석은 아래와 같은 코드를 생성해준다.
생성된 코드를 전체적으로 확인하면 아래와 같다.
// <auto-generated/>
#pragma warning disable
#nullable enable
namespace UiDesktopAppTest.ViewModels.Pages
{
/// <inheritdoc/>
partial class DashboardViewModel
{
/// <summary>The backing field for <see cref="TextChangedCommand"/>.</summary>
[global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator", "8.4.0.0")]
private global::CommunityToolkit.Mvvm.Input.RelayCommand? textChangedCommand;
/// <summary>Gets an <see cref="global::CommunityToolkit.Mvvm.Input.IRelayCommand"/> instance wrapping <see cref="OnTextChanged"/>.</summary>
[global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator", "8.4.0.0")]
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
public global::CommunityToolkit.Mvvm.Input.IRelayCommand TextChangedCommand => textChangedCommand ??= new global::CommunityToolkit.Mvvm.Input.RelayCommand(new global::System.Action(OnTextChanged));
}
}