이번 포스팅에서는 상세 페이지로 이동하는 기능을 구현하려고 합니다. 지금까지 구현된 것은 단순하게 도서 리스트를 조회하여 메인 페이지에 도서 정보들을 보여주는 기능입니다. 이 과정에서 아래와 같은 기본적인 내용들을 학습하였습니다.
- MVVM 기본 개요
- Community Toolkit 기본 기능
- DataBinding
- 의존성 주입 (Dependency Injection)
이번에는 도서 기본 정보를 클릭했을 때 도서 상세 페이지로 화면을 전환하는 기능에 대해서 포스팅 하겠습니다. 아주 간단한 시나리오지만 몇가지 중요한 개념에 대해서 배우게 됩니다.
- 페이지간 파라미터 전달
- 페이지 Routing 구현
- Relative Binding – Relative Binding 에 대해서는 아래 포스팅에서 확인할 수 있습니다.
[MAUI 기본] 데이터바인딩 – Relative Binding
우선, 아래와 같은 구조로 진행할 예정입니다.
- BookListViewModel 에 사용자가 도서 정보를 탭 또는 클릭했을 때 실행되는 Command 생성 (GetBookDeatilCommand)
- 실행되는 Command 에서 넘겨준 파라미터를 받는 ViewModel 구현 (BookDetailViewModel)
- 도서 리스트 화면에서 특정 도서를 탭 (클릭) 했을 때 위에서 생성한 Command 호출
- 도서 상세 내용을 표시할 View 구현
GetBookDeatilCommand 생성
BookListViewModel 에 GetBookDeatilCommand 를 생성해 보겠습니다.
[BookListViewModel.cs]
[RelayCommand]
public async Task GetBookDetail(Book book)
{
if (book == null) return;
await Shell.Current.GoToAsync(nameof(BookDetailPage), true, new Dictionary<string, object>
{
{nameof(Book), book }
});
}
여기서는 단순히 Book 객체를 파라미터로 받은 후에 앞으로 만들 BookDetailPage 로 이동하도록 코드를 작성하였습니다. GoToAsync 로 이러한 기능을 구현할 수 있습니다. 또한 GoToAsync 에서 해당 페이지로 이동할 때 넘겨줄 파라미터도 설정할 수 있습니다. 위의 예제에서는 선택된 book 개체를 nameof(Book) 이란 파라미터명(“Book”) 으로 전달하고 있습니다.
- 페이지 네비게이션 : Shell.Current.GoToAsync 메서드를 사용하여 지정된 페이지로 비동기적으로 이동합니다.
- nameof(BookDetailPage)는 이동할 페이지의 이름을 지정합니다.
- 매개변수 중 true는 애니메이션을 사용하여 페이지를 전환할지 여부를 나타냅니다.
- new Dictionary<string, object>는 네비게이션 시 전달할 매개변수를 포함하는 딕셔너리입니다.
- {nameof(Book), book}는 Book 객체를 BookDetailPage로 전달합니다.
- nameof(Book)는 전달할 매개변수의 이름을 지정하고, book은 전달할 객체를 나타냅니다.
BookDetailViewModel 생성
MAUI 에서는 이렇게 전달된 파라미터가 BookDeatilPage 에서 BindingContext 로 정의한 ViewModel 로 전달될 수 있습니다. BookDetailPage 에서 BindingContext 로 정의할 BookDatailViewModel 에서 어떻게 전달된 파라미터를 받을 수 있는지 확인해 보도록 하겠습니다.
[BookDetailViewModel.cs]
[QueryProperty(nameof(Book), "Book")]
public partial class BookDetailViewModel : BaseViewModel
{
[ObservableProperty]
Book book;
public BookDetailViewModel()
{
}
}
위 코드에서 주목할 부분은 QueryProperty 로 정의한 부분입니다.
[QueryProperty(nameof(Book), "Book")]
- [QueryProperty(nameof(Book), “Book”)]는 페이지 네비게이션 시 쿼리 매개변수를 ViewModel의 속성에 매핑하는 데 사용됩니다.
- nameof(Book) 은 매핑할 ViewModel 의 속성 이름입니다. 위 소스에서는 book 이라는 필드가 ObservableProperty 로 선언되었기 때문에 자동으로 Book 이라는 속성이 생성됩니다. 결국 넘어온 파라미터는 Book 이라는 속성에 매핑됩니다.
- “Book” 은 매개변수로 넘어오는 파라미터의 이름입니다. 앞의 소스에서 GoToAsync 에서 nameof(Book) 이라는 구문으로 파라미터명을 설정했습니다. nameof(Book) 은 “Book” 을 나타냅니다.
요약하면 “Book” 이라는 파라미터명으로 넘어온 book 객체를 Book 속성에 매핑하는 기능을 수행한다고 이해하시면 됩니다.
Command 호출
이제 도서 리스트를 보여주는 메인 페이지에서 특정 도서 목록을 클릭했을 때 BookListViewModel 의 GetBookDetailCommand 를 호출하는 부분을 작성해 보겠습니다. 기존 MainPage.xaml 에 추가 코드를 작성하겠습니다.
[MainPage.xaml.cs]
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage
x:Class="MyBookStore.MainPage"
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:model="clr-namespace:MyBookStore.Models"
xmlns:viewmodel="clr-namespace:MyBookStore.ViewModels"
Title="{Binding Title}"
x:DataType="viewmodel:BookListViewModel">
<Grid
ColumnDefinitions="*,*"
ColumnSpacing="5"
RowDefinitions="*, Auto">
<RefreshView
Grid.ColumnSpan="2"
Command="{Binding LoadBooksCommand}"
IsRefreshing="{Binding IsRefreshing}">
<CollectionView ItemsSource="{Binding Books}" SelectionMode="Single">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="model:Book">
<Frame HeightRequest="90">
<Frame.GestureRecognizers>
<TapGestureRecognizer Command="{Binding Source={RelativeSource AncestorType={x:Type viewmodel:BookListViewModel}}, Path=GetBookDetailCommand}" CommandParameter="{Binding .}" />
</Frame.GestureRecognizers>
<HorizontalStackLayout Padding="10" Spacing="5">
<Label Text="{Binding Id}" />
<Label Text="{Binding Title}" />
</HorizontalStackLayout>
</Frame>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</RefreshView>
<Button
Grid.Row="1"
Grid.Column="0"
Command="{Binding LoadBooksCommand}"
IsEnabled="{Binding IsNotLoading}"
Text="Get Books" />
</Grid>
</ContentPage>
여기서 추가된 부분은 아래와 같습니다.
<Frame.GestureRecognizers>
<TapGestureRecognizer Command="{Binding Source={RelativeSource AncestorType={x:Type viewmodel:BookListViewModel}}, Path=GetBookDetailCommand}" CommandParameter="{Binding .}" />
</Frame.GestureRecognizers>
Frame 요소에서 책을 클릭 했을 때 실행될 Command 를 Relative Binding 을 이용해서 BookListViewModel 의 “GetBookDetailCommand” 로 바인딩 시켰습니다. DataTemplate 내에서 x:DataType 을 이용해서 현재 컨텍스트에서 사용하는 바인딩 소스를 Model 로 변경했기 때문에 다시 BookListViewModel 에 선언된 메서드와 바인딩 하려면 Relative Binding 기능을 사용해야 합니다. Relative Binding 에 대해서는 이전 포스팅에서 자세히 설명하였습니다.
상세 페이지 (View) 생성
이제 마지막 단계로 도서 상세 페이지를 표시할 BookDetailPage 를 만들어 보겠습니다.
[BookDetailPage.xaml]
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage
x:Class="MyBookStore.Views.BookDetailPage"
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:viewmodel="clr-namespace:MyBookStore.ViewModels"
Title="{Binding Book.Title}"
x:DataType="viewmodel:BookDetailViewModel">
<StackLayout HorizontalOptions="Center" VerticalOptions="Center">
<Label Padding="10" Text="{Binding Book.Id}" />
<Label Padding="10" Text="{Binding Book.Genre}" />
<Label Padding="10" Text="{Binding Book.BookTitle}" />
<Label Padding="10" Text="{Binding Book.Author}" />
<Label Padding="10" Text="{Binding Book.Year}" />
</StackLayout>
</ContentPage>
[BookDetail.xaml.cs]
public partial class BookDetailPage : ContentPage
{
public BookDetailPage(BookDetailViewModel bookDetailViewModel)
{
InitializeComponent();
BindingContext = bookDetailViewModel;
}
}
코드는 아주 간단하게 구성하였습니다. 우선 코드 비하인드 파일에서 BookDetailPage 생성자에서 BindingContext 로 bookDetailViewModel 을 의존성 주입 받았습니다. 그리고, XAML 에서 Label 의 Text 속성으로 Book 의 속성들을 지정하여 화면에 표시하고 있습니다. 아주 간단한 내용으로 구성해보았습니다.
BookDetailPage 는 페이지 전환용으로 사용되기 때문에 AppShell.xaml.cs 파일에 아래 내용도 추가되어야 하는 것을 잊지마세요.
[AppShell.xaml.cs]
public AppShell()
{
InitializeComponent();
Routing.RegisterRoute(nameof(BookDetailPage), typeof(BookDetailPage));
}
그리고, 의존성 주입을 통해서 BookDetailViewModel 이 BindingContext 로 정의되었기 때문에 MauiProgram.cs 에도 아래 코드가 추가되어야 합니다.
[MauiProgram.cs]
builder.Services.AddTransient<BookDetailPage>();
builder.Services.AddTransient<BookDetailViewModel>();
아래는 프로그램을 실행하고 상세페이지로 이동한 모습입니다.