[MAUI 활용] BookStore 만들기 (5) – BookListViewModel 만들기

이제 본격적으로 BookStore 에서 사용할 ViewModel 을 만들어 보겠습니다. 이번에 만들 ViewModel 은 도서 리스트를 보여주는 페이지에서 사용할 예정이기 때문에 BookListViewModel 로 만들려고 합니다. BookListViewModel 은 이전 강의에서 만들었던 BaseViewModel 을 상속받습니다. 

 public partial class BookListViewModel : BaseViewModel
 {
     
 }

도서 리스트를 제공할 것이기 때문에 도서 리스트를 검색해오는 기능이 필요합니다. 이전 강의에서 BookService 라는 클래스를 만들었으며 해당 클래스는 도서 리스트를 제공하는 기능을 갖고 있습니다. BookService 의 GetBooks 메서드를  BookListViewModel 에서 호출할 예정입니다. BookListViewModel 에서는 BookService 를 참조해야 하기 때문에 생성자를 통한  “의존성 주입 (dependency injection)”  을 통해서 BookList 에 대한 참조를 할당하겠습니다.  “의존성 주입” 에 대해서는 다음 포스트에서 자세히 설명할 예정입니다. 

또한, BookService 를 통해서 조회된 도서 리스트를 저장할 속성이 필요합니다. 해당 속성은 ObservableCollection<Book> 형식으로 만들겁니다. “ObservableCollection”은  아이템이 추가, 제거, 또는 변경될 때 UI와 같은 바인딩된 데이터 대상에게 변경 사항을 자동으로 알리는 기능을 제공합니다.  즉, 이 컬렉션이 변경되면 UI가 자동으로  업데이트되게 만들겁니다. 

추가적으로 BaseViewModel 에서 정의한 title 필드를 기억하실 겁니다. title 은 페이지 이름을 설정하는데 사용됩니다. Community ToolKit 을 통해서 title 은 ObservableProperty 로 정의되었기 때문에 Title 이란 속성이 자동으로 생성이 되어 있습니다. 생성자에서 Title 속성에 값을 정의할 수 있습니다. 지금까지 설명한 내용을 반영한 코드를 확인해 보겠습니다.

 public partial class BookListViewModel : BaseViewModel
 {
     private readonly BookService bookService;
     public ObservableCollection<Book> Books { get; set; } 
     public BookListViewModel(BookService bookService)
     {
         Title = "Book List";
         this.bookService = bookService;
     }
 }

이제 ViewModel 에 도서 리스트를 조회하는 기능을 추가하겠습니다. MVVM에서는 Command를 통해 뷰(View)와 ViewModel 사이의 상호작용을 처리하며, 이를 통해 UI 이벤트(버튼 클릭 등)를 ViewModel로 전달할 수 있습니다. RelayCommand는 이런 명령을 간단하게 구현할 수 있도록 돕는 클래스입니다.  잠시 RelayCommand 에 대해서 정리해보겠습니다. 

RelayCommand의 핵심 개념

 RelayCommand는 DelegateCommand 또는 ICommand 인터페이스를 구현한 클래스이며 Commuity Toolkit 에서 제공해주고 있으며, 다른 MVVM 프레임워크에서도 제공하고 있습니다. RelayCommand 는  기본적으로 두 개의 델리게이트(함수 포인터)를 전달받습니다:

  • Execute 메서드: 명령이 실행될 때 호출되는 로직을 정의.
  • CanExecute 메서드 (선택 사항): 명령이 실행 가능한지 여부를 결정하는 조건을 정의.
public RelayCommand(Action execute, Func<bool> canExecute = null);
  • Action execute: 명령이 실행될 때 호출할 메서드를 나타냅니다.
  • Func<bool> canExecute: 명령을 실행할 수 있는지 여부를 결정하는 조건입니다. true면 실행 가능, false면 실행 불가를 나타냅니다.  이 메서드는 선택 사항이며 기본값은 항상 true입니다.

아래 예시는 RelayCommand를 사용하여 버튼 클릭 시 특정 작업을 수행하는 예제입니다.

public class MyViewModel
{
    public RelayCommand MyCommand { get; }

    public MyViewModel()
    {
        // Execute 메서드와 CanExecute 메서드를 전달해 RelayCommand 생성
        MyCommand = new RelayCommand(ExecuteMethod, CanExecuteMethod);
    }

    private void ExecuteMethod()
    {
        // 명령이 실행될 때 수행할 작업
        Console.WriteLine("Command Executed");
    }

    private bool CanExecuteMethod()
    {
        // 명령이 실행 가능한지를 결정하는 조건
        return true; // 실행 가능
    }
}

UI 바인딩: MVVM 패턴에서는 보통 XAML에서 RelayCommand를 바인딩하여 버튼 같은 UI 요소와 연결합니다. 예를 들어

<Button Content="Click Me" Command="{Binding MyCommand}" />

이 버튼은 MyCommand에 바인딩되며, MyCommand가 실행 가능할 때만 활성화되고 클릭 시 ExecuteMethod가 호출됩니다.

RelayCommand  어노테이션

[RelayCommand] 어트리뷰트는  Community Toolkit에서 제공되는 기능으로, RelayCommand 를  더 간단하고 직관적으로 구현할 수 있도록 도와줍니다. [RelayCommnad] 를 사용하면 RelayCommand 객체를 수동으로 생성할 필요 없이 메서드에 직접 명령을 연결할 수 있습니다.

  • 자동 명령 생성: [RelayCommand] 어노테이션은 메서드 위에 붙여 RelayCommand 를 자동으로 생성합니다. MVVM 패턴에서 UI 이벤트(예: 버튼 클릭)를 처리하기 위해 RelayCommand 를 수동으로 생성하는 대신, 어노테이션을 붙이기만 하면 코드 생성기가 자동으로 RelayCommand를 만들어 줍니다.
public partial class MyViewModel : ObservableObject
{
    [RelayCommand]
    private void Submit()
    {
        // 명령 실행 시 호출될 로직
        Console.WriteLine("Submit Command Executed");
    }
}

Community Toolkit 을 사용하면 위와 같이 [RelayCommand] 어노테이션을 했을 때,   SubmitCommand 라는 RelayCommand 객체가 자동으로 생성됩니다. 뒤에 “Command” 라는 접미사가 자동으로 붙는 것에 유의하십시요. 이제 SubmitCommand 를 버튼 등의 UI 요소에 바인딩할 수 있습니다.

<Button Content="Submit" Command="{Binding SubmitCommand}" />

CanExecute 메서드 추가

 명령이 실행 가능한지 여부를 결정하는 CanExecute 메서드도 추가할 수 있습니다. 이를 위해서는 메서드 이름에 Can 접두사를 붙이면 됩니다.

public partial class MyViewModel : ObservableObject
{
    [RelayCommand]
    private void Submit()
    {
        Console.WriteLine("Submit Command Executed");
    }

    private bool CanSubmit()
    {
        // 명령이 실행 가능한지 여부를 결정하는 로직
        return true; // true일 때만 명령 실행 가능
    }
}

비동기 명령

[RelayCommand]는 비동기 메서드에도 사용할 수 있습니다. 비동기 명령을 정의할 때는 async 키워드를 사용하고, AsyncRelayCommand가 자동으로 생성됩니다.

public partial class MyViewModel : ObservableObject
{
    [RelayCommand]
    private async Task LoadDataAsync()
    {
        // 비동기 작업 로직
        await Task.Delay(1000); // 예시: 1초 대기
        Console.WriteLine("Data Loaded");
    }
}

이 경우, LoadDataAsyncCommand가 생성되고, 버튼 클릭 등 UI 이벤트에 비동기 명령을 쉽게 바인딩할 수 있습니다

BookListViewModel 

이제 다시 BookListViewModel 로 돌아오겠습니다. BookListViewModel 클래스에 도서 목록을 조회하는 LoadBooksAsync 라는 메서드를 선언하고 RelayCommand 로 지정하겠습니다.

 public partial class BookListViewModel : BaseViewModel
 {
     private readonly BookService bookService;
     public ObservableCollection<Book> Books { get; set; } = new ObservableCollection<Book>();
     public BookListViewModel(BookService bookService) 
     {
         Title = "Book List";
         this.bookService = bookService;
     }

     [RelayCommand]
     public async Task LoadBooksAsync() 
     {
         if (IsLoading) return;
         try
         {
             if (Books.Any()) Books.Clear();
             var books = bookService.GetBooks();
             foreach(var book in books)
             {
                 Books.Add(book);
             }   
         }
         catch (Exception ex)
         {
             Debug.Write($"LoadBookAsync error: {ex.Message}");
             await Shell.Current.DisplayAlert("Error", ex.Message, "OK");    
         }
         finally
         {
             IsLoading = false;
         }            
         IsLoading = false;
     }
 }

처음으로 할 일은 페이지가 로딩 중인지 확인하는 것입니다. 만약 로딩 중이라면 아무 작업도 수행하지 않고 리턴합니다. 페이지가 로딩중이 아니면 BookService 를 통해서 도서 리스트를 조회해 옵니다. 그리고, 조회해온 Book List 를 ObservableCollection 에 추가하고 있습니다. 만약 예외가 발생하면, 예외 메시지를 시스템에 출력하고, 사용자에게 경고 메시지를 표시합니다. 이를 위해 “Shell”을 사용하여 UI에 경고창을 띄울 수 있습니다.

그리고 마지막으로 로딩 상태를 false로 설정해줍니다. 이렇게 하면 로딩 상태가 변경될 때마다 UI가 업데이트됩니다.

이제 ViewModel이 설정되었습니다. 다음 시간에는 Dependency Injection에 대해 더 깊이 알아보고, 이 ViewModel을 메인 페이지와 연결하는 방법을 다룰 예정입니다.

댓글 달기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다