이전 포스트에서 우리는 Book, BookStore 에 대한 Model 클래스를 생성했습니다. 이제 Book 과 BookStore 에 관련한 ViewModel 을 만들 차례입니다. 앞서 MVVM 패턴을 쉽게 적용하기 위해서 Community Toolkit 을 설치했습니다. 우선 처음에는 해당 ToolKit 을 사용하지 않고 ViewModel 을 만들어 보겠습니다.
MVVM에서 뷰모델을 만들 때는 INotifyPropertyChanged라는 인터페이스를 상속받게 됩니다. INotifyPropertyChanged 인터페이스는 .NET에서 데이터 바인딩을 지원하기 위해 사용되는 인터페이스입니다. 이 인터페이스는 속성 값이 변경될 때 알림을 제공하여 UI가 자동으로 업데이트되도록 할 수 있는 기능을 제공하며, 주로 MVVM 패턴에서 ViewModel 클래스에 사용됩니다.
INotifyPropertyChanged 를 이용한 구현방법
- 인터페이스 상속 : 클래스에서 INotifyPropertyChanged 인터페이스를 상속합니다.
- PropertyChanged 이벤트 선언 : PropertyChanged 이벤트를 선언합니다.
- OnPropertyChanged 메서드 구현 : 속성 값이 변경될 때 PropertyChanged 이벤트를 호출하는 OnPropertyChanged 메서드를 구현합니다.
- 속성 값 변경 시 OnPropertyChanged 호출 : 속성의 set 접근자에서 OnPropertyChanged 메서드를 호출하여 변경 사항을 알립니다.
전체 소스를 한번 살펴보겠습니다.
public class BaseViewModelWithoutToolKit : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
private string title;
private bool isBusy;
public string Title
{
get => title;
set
{
if (title != value)
{
title = value;
OnPropertyChanged();
}
}
}
public bool IsBusy
{
get => isBusy;
set
{
if (isBusy != value)
{
isBusy = value;
OnPropertyChanged();
}
}
}
public void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
구현 코드에 대해서 조금 더 자세히 살펴보겠습니다.
public event PropertyChangedEventHandler? PropertyChanged;
PropertyChanged 이벤트는 속성 값이 변경될 때 이를 알리기 위해 사용됩니다. PropertyChangedEventHandler 델리게이트 타입을 사용하여 이벤트를 선언할 수 있으며, 속성 값이 변경될 때 OnPropertyChanged 메서드에서 PropertyChanged 이벤트를 호출하여 UI가 자동으로 업데이트되도록 합니다.
public void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
OnPropertyChanged 메서드는 INotifyPropertyChanged 인터페이스를 구현하는 클래스에서 속성 값이 변경될 때 이를 알리기 위해 사용됩니다. 이 메서드는 PropertyChanged 이벤트를 호출하여 바인딩된 UI 요소나 다른 구독자에게 속성 값이 변경되었음을 알립니다.
위 코드에서 OnPropertyChagned 메서드는 디폴트 파라미터로 CallerMemberName을 사용해서 호출된 속성의 이름을 자동으로 가져오게 됩니다. 이렇게 하면 속성 이름을 수동으로 전달할 필요 없이 자동으로 속성 이름을 가져올 수 있습니다.
즉, OnPropertyChanged 메서드는 속성값이 변경될 때 변경된 속성 이름과 함께 PropertyChanged 이벤트를 호출하고, 변UI 에서는 DataBinding 을 사용해서 변경된 속성의 이벤트를 감지하여 화면에 반영할 수 있는 구조입니다.
속성 정의
속성을 정의한 부분을 살펴보겠습니다. title은 현재 페이지의 제목을 나타내고, isLoading 는 페이지가 데이터를 로드하거나 작업을 수행 중인지를 나타냅니다. 작업이 완료되면 이 값을 변경합니다.
이제 속성을 정의해보겠습니다. 살펴볼 속성은 페이지의 제목을 설정하는 Title 입니다.
public string Title
{
get => title;
set
{
if (title != value)
{
title = value;
OnPropertyChanged(nameof(Title));
}
}
}
get 메서드는 필드 Title 의 값을 반환하고, set 메서드는 값을 설정하기 전에 값이 동일한지 확인합니다. 만약 동일한 값이라면 아무런 동작을 하지 않고, 그렇지 않으면 새로운 값을 설정한 후 OnPropertyChanged() 메서드를 호출해 UI에 변경 사항을 알립니다. 이러한 방식으로, 속성이 변경될 때마다 UI에 자동으로 반영될 수 있도록 처리할 수 있습니다.
그러나, 만일 속성이 많다면 이와 같은 작업을 계속해서 반복해야 하는 문제에 직면할 수 있습니다 . 다행히 여러가지 MVVM 패턴 구현을 편하게 구현할 수 있도록 도와주는 라이브러리들이 제공되고 있습니다. 우리는 이전 강의에 MS 에서 제공하는 Community ToolKit 을 설치하였으며, 이러한 라이브러리를 사용하면 우리가 작성해야 하는 코드의 양을 줄일 수 있으며, 코딩 가독성도 높일 수 있습니다. 이제 Community ToolKIt 을 사용해 보겠습니다.
Community ToolKit 사용
우선 첫 번째 변경 사항은 INotifyPropertyChanged를 상속받는 대신 Community ToolKit 에서 제공하는 기본 클래스인 ObservableObject를 상속받는 것입니다.. 이를 통해서 애플리케이션에 이 객체의 모든 것이 관찰 가능하다는 것을 알릴 수 있습니다. 우선 전체 소스를 확인해 보겠습니다.
public partial class BaseViewModel : ObservableObject
{
[ObservableProperty]
string title;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsNotLoading))]
bool isLoading;
public bool IsNotLoading => !IsLoading;
}
코드가 무척 간결해진 것을 확인할 수 있습니다. 조금 더 자세히 살펴보도록 하겠습니다.
[ObservableProperty]
string title;
우선 생성하고자 하는 BaseViewModel 클래스는 ObservableObject 를 상속받고 있는 것을 확인할 수 있습니다. 그리고, 필드를 선언하면서 ObservableProperty 라는 어노테이션을 사용하였습니다.
ObservableProperty
.NET MAUI에서 ObservableProperty는 MVVM (Model-View-ViewModel) 패턴을 구현할 때 매우 유용한 속성입니다. ObservableProperty는 INotifyPropertyChanged 인터페이스를 쉽게 구현하도록 도와주며, 속성 값이 변경될 때 자동으로 UI에 반영되도록 하는 역할을 합니다.
역할 및 작동 방식
ObservableProperty는 주로 데이터 바인딩 시 ViewModel과 View 간의 데이터 동기화를 원활하게 하기 위해 사용됩니다. 일반적으로 다음과 같은 역할을 합니다.
- 속성 변경 알림 : ViewModel에서 속성 값을 변경하면 PropertyChanged 이벤트가 자동으로 발생하여, 바인딩된 UI 요소가 변경된 값을 자동으로 업데이트할 수 있도록 합니다.
- 코드 간소화 : 수동으로 INotifyPropertyChanged를 구현하려면 속성마다 이벤트를 발생시키는 코드를 작성해야 합니다. 하지만 ObservableProperty를 사용하면 이러한 작업이 자동으로 처리됩니다.
- 데이터 바인딩에 최적화 : ObservableProperty는 XAML 파일에서 UI 요소와 바인딩될 때, 속성의 변경을 즉시 UI에 반영할 수 있는 환경을 제공하여, 사용자가 더 적은 코드로 효율적으로 바인딩을 구현할 수 있게 합니다.
isLoading 은 현재 페이지가 로딩 중이라는 것을 나타냅니다. 이번에는 isNotLoading 이라는 속성을 추가해 보겠습니다. 속성명에서 알 수 있듯이 isLoading 의 반대입니다. isLoading 값이 변경될 때 isNotLoading 도 자동으로 업데이트 되도록 코드를 수정해 보겠습니다.
public partial class BaseViewModel : ObservableObject
{
[ObservableProperty]
string title;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsNotLoading))]
bool isLoading;
public bool IsNotLoading => !IsLoading;
}
NotifyPropertyChangedFor(nameof(IsNotLoading)) 를 추가하게 되면 특정 속성이 변경될 때 다른 속성도 변경 알림을 발생시키도록 합니다. 즉, isLoading 필드가 변경될 때 IsNotLoading 속성도 변경 알림을 발생시키도록 설정한 것입니다. 이를 통해 IsLoading 속성이 변경될 때 IsNotLoading 속성도 자동으로 업데이트됩니다.
ObservableProperty 로 필드를 정의하면, 필드값 변경시 이에 대한 알림을 처리할 수 있는 구조가 자동으로 생성이 되며, 기존 소스에 있던 속성과 이벤트 , 메서드들을 더 이상 선언하지 않아도 동일한 기능을 제공할 수 있게 됩니다. 내부적으로 생성된 소스 코드를 확인할 수 있는데요, 아래 그림을 참고하시면 Community ToolKit 에서 생성한 소스를 확인할 수 있습니다. 위에서 Community ToolKit 없이 작성했던 내용과 비슷하다는 것을 알 수 있습니다.
/// <inheritdoc cref="title"/>
[global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")]
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
public string Title
{
get => title;
[global::System.Diagnostics.CodeAnalysis.MemberNotNull("title")]
set
{
if (!global::System.Collections.Generic.EqualityComparer<string>.Default.Equals(title, value))
{
OnTitleChanging(value);
OnTitleChanging(default, value);
OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Title);
title = value;
OnTitleChanged(value);
OnTitleChanged(default, value);
OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Title);
}
}
}
Community ToolKit 에서 자동으로 생성한 소스 코드의 일부입니다. 우리가 먼저 작성했던 소스와 유사하다는 것을 알 수 있습니다.
이렇게 해서 우리의 MVVM 패턴을 위한 BaseViewModel을 구현했습니다. 이제 실제로 BookStore 에서 사용하는 실제 View를 위한 ViewModel을 만드는 것을 살펴보도록 하겠습니다.