Avalonia UI에서 MVVM 패턴 구현: 데이터 바인딩과 명령 처리

MVVM(Model-View-ViewModel) 아키텍처는 사용자 인터페이스(UI) 로직과 비즈니스 로직을 명확하게 분리하여 유지보수성과 테스트 가능성을 높이는 표준 패턴입니다. Avalonia 프레임워크에서는 CommunityToolkit.Mvvm 라이브러리를 활용하여 이 패턴을 간결하게 구현할 수 있습니다.

1. 프로젝트 준비 및 기본 구조 설정

시작하기 전에 NuGet 패키지 관리자 또는 CLI를 통해 CommunityToolkit.Mvvm 패키지를 프로젝트에 추가해야 합니다. 이 툴킷은 소스 생성기(Source Generators)를 사용하여 반복적인 보일러플레이트 코드를 자동화합니다.

프로젝트 내에서 모든 ViewModel의 공통 기반이 될 클래스를 정의합니다. 이는 추후 다른 ViewModel들이 상속받을 부모 클래스 역할을 수행합니다.

// ViewModels/ViewModelBase.cs
using CommunityToolkit.Mvvm.ComponentModel;

namespace MyApp.ViewModels;

/// <summary>
/// 앱 내 모든 ViewModel의 기본 클래스
/// INotifyPropertyChanged 구현을 위해 ObservableObject를 상속받음
/// </summary>
public abstract class ViewModelBase : ObservableObject
{
    // 추가적인 공통 유틸리티 메서드나 속성을 여기에 정의할 수 있음
}

ObservableObject는 속성 변경 시 자동으로 이벤트를 발생시키는 기능을 제공합니다. 이를 상속받은 클래스는 복잡한 INotifyPropertyChanged 인터페이스 구현 없이도 UI와의 동기화를 처리할 수 있습니다.

2. 메인 화면을 위한 ViewModel 작성

메인 윈도우의 상태와 동작을 관리하는 구체적인 ViewModel을 생성합니다. 여기서는 사이드 메뉴의 확장 여부를 제어하는 예제를 다룹니다.

// ViewModels/MainViewModel.cs
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;

namespace MyApp.ViewModels;

// partial 키워드는 소스 생성기가 컴파일 타임에 코드를 주입할 수 있도록 허용
public partial class MainViewModel : ViewModelBase
{
    // [ObservableProperty]는 아래 필드에 대해 public 프로퍼티 SideMenuExpanded를 자동 생성함
    // 값이 변경되면 자동으로 PropertyChanged 이벤트가 발생하여 UI 업데이트 트리거
    [ObservableProperty]
    private bool _isSidebarOpen = true;

    // [RelayCommand]는 아래 메서드를 ICommand로 래핑하여 SidebarToggleCommand 생성
    // UI 버튼 클릭 등에 직접 바인딩 가능
    [RelayCommand]
    private void ToggleSidebar()
    {
        // 현재 상태 반전
        IsSidebarOpen = !IsSidebarOpen;
    }
}
  • partial 클래스: C#의 부분적 클래스 분할 기능을 사용하여, 개발자가 작성한 코드와 툴킷이 생성한 코드가 하나의 클래스로 통합되도록 합니다.
  • [ObservableProperty]: 백킹 필드(Private Field)에 적용하면 Public Property와 NotifyChanged 로직을 자동으로 생성합니다. 수동으로 getter/setter를 작성할 필요가 없습니다.
  • [RelayCommand]: 메서드를 실행 가능한 명령 객체로 변환합니다. 이를 통해 View는 특정 컨트롤의 이벤트 핸들러를 직접 호출하지 않고, 선언적으로 명령을 연결할 수 있어 결합도가 낮아집니다.

3. 뷰(View)와 뷰모델(ViewModel) 연결

생성된 ViewModel 인스턴스를 View의 DataContext로 할당하여 데이터 흐름의 통로를 엽니다.

3.1 애플리케이션 진입점 설정

// App.axaml.cs
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using MyApp.ViewModels;

namespace MyApp;

public partial class App : Application
{
    public override void Initialize()
    {
        AvaloniaXamlLoader.Load(this);
    }

    public override void OnFrameworkInitializationCompleted()
    {
        if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
        {
            // MainWindow의 DataContext로 MainViewModel 인스턴스 주입
            desktop.MainWindow = new Views.MainWindow
            {
                DataContext = new MainViewModel()
            };
        }

        base.OnFrameworkInitializationCompleted();
    }
}

3.2 XAML에서의 데이터 바인딩

XAML 파일 내에서 {Binding} 구문을 사용하여 ViewModel의 속성과 UI 요소의 특성을 연결합니다. 설계 시(Design-time) 미리보기 기능을 활성화하기 위해 네임스페이스와 데이터 타입을 명시하는 것이 좋습니다.

<!-- Views/MainWindow.axaml -->
<Window xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:vm="clr-namespace:MyApp.ViewModels"
        x:Class="MyApp.Views.MainWindow"
        x:DataType="vm:MainViewModel"
        Title="MVVM Example">

    <!-- Design.DataContext: Visual Studio 등 IDE의 디자인 모드에서 미리보기 렌더링을 지원 -->
    <Design.DataContext>
        <vm:MainViewModel />
    </Design.DataContext>

    <StackPanel Spacing="10" Margin="20">
        
        <!-- 텍스트 가시성 제어: IsSidebarOpen이 true일 때만 표시 -->
        <TextBlock Text="Welcome to the Expanded Menu" 
                   IsVisible="{Binding IsSidebarOpen}" />
                   
        <!-- 이미지 크기 조정: 바인딩 값을 논리 연산자로 사용 가능 (예시용) -->
        <Image Source="/Assets/icon.png" 
               Width="{Binding IsSidebarOpen, Converter={StaticResource BoolToWidthConverter}}" />

        <!-- 버튼 커맨드 바인딩: 클릭 시 ToggleSidebar 메서드 실행 -->
        <Button Content="Toggle Sidebar" 
                Command="{Binding ToggleSidebarCommand}" />
                
    </StackPanel>
</Window>
  • x:DataType: 컴파일러에게 해당 뷰의 DataContext가 어떤 타입인지 알려주어, 바인딩 경로에 대한 정적 분석(IntelliSense 및 오류 체크)을 가능하게 합니다.
  • IsVisible 바인딩: Boolean 속성이 직접 UI 가시성에 매핑됩니다. 값이 변경되면 Avalonia 레이아웃 엔진이 해당 요소를 그리거나 숨기는 작업을 즉시 수행합니다.
  • Command 바인딩: 버튼의 Command 속성에 ViewModel의 ToggleSidebarCommand를 연결합니다. 별도의 코드 비하인드(Code-behind) 이벤트 핸들러 없이 로직 실행이 가능합니다.

3.3 복잡한 상호작용 처리 (코드 비하인드 활용)

순수 MVVM 원칙상 코드 비하인드 사용을 지양하지만, 마우스 더블 클릭이나 키보드 단축키와 같은 저수준 입력 이벤트는 여전히 코드 비하인드에서 처리하고 ViewModel의 명령을 호출하는 방식이 권장될 수 있습니다.

// Views/MainWindow.axaml.cs
using Avalonia.Controls;
using Avalonia.Input;
using MyApp.ViewModels;

namespace MyApp.Views;

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    // XAML에서 PointerPressed="OnPointerPressed" 등으로 연결된 경우
    private void OnPointerPressed(object? sender, PointerPressedEventArgs e)
    {
        // 더블 클릭 여부 확인
        if (e.ClickCount != 2) return;

        // DataContext를 안전한 방식으로 캐스팅하여 명령 실행
        if (DataContext is MainViewModel vm)
        {
            vm.ToggleSidebarCommand.Execute(null);
        }
    }
}

이 패턴은 View가 구체적인 비즈니스 로직을 알지 못한 채, 단순히 '사용자의 의도(명령)'를 ViewModel에게 전달하는 역할만 수행하도록 보장합니다.

데이터 흐름 요약

  1. 입력: 사용자가 UI 요소(버튼, 제스처 등)와 상호작용합니다.
  2. 전달: View는 바인딩된 ICommand를 실행하거나, 코드 비하인드를 통해 ViewModel의 메서드를 호출합니다.
  3. 처리: ViewModel 내부 로직이 실행되어 상태(ObservableProperty)가 변경됩니다.
  4. 알림: 상태 변경 시 PropertyChanged 이벤트가 발생합니다.
  5. 갱신: 바인딩된 UI 요소들이 이벤트를 수신하고 자신의 시각적 표현(IsVisible, Text, Color 등)을 자동으로 업데이트합니다.

태그: Avalonia MVVM CommunityToolkit.Mvvm data binding WPF-like Architecture

10월 3일 21:41에 게시됨