MVVM(Model-View-ViewModel)

이 콘텐츠는 ‘.NET MAUI를 사용하는 엔터프라이즈 애플리케이션 패턴’ eBook에서 발췌한 것으로, .NET Docs에서 제공되거나 오프라인으로 읽을 수 있는 다운로드 가능한 무료 PDF로 제공됩니다.

‘.NET MAUI를 사용하는 엔터프라이즈 애플리케이션 패턴’ eBook 표지 썸네일

.NET MAUI 개발자 환경에는 일반적으로 XAML에서 사용자 인터페이스를 만든 다음 사용자 인터페이스에서 작동하는 코드 숨김을 추가하는 작업이 포함됩니다. 앱이 수정되고 크기 및 범위가 증가함에 따라 복잡한 유지 관리 문제가 발생할 수 있습니다. 이러한 문제에는 UI 컨트롤과 비즈니스 논리 간의 긴밀한 결합이 포함되며, 이로 인해 UI 수정 비용이 증가하고 이러한 코드를 단위 테스트하기 어려워집니다.

MVVM 패턴은 애플리케이션의 비즈니스 및 프레젠테이션 논리를 UI(사용자 인터페이스)와 명확하게 구분하는 데 도움이 됩니다. 애플리케이션 논리와 UI 간의 명확한 분리를 유지 관리하면 수많은 개발 문제를 해결하고 애플리케이션을 더 쉽게 테스트, 유지 관리 및 개량할 수 있습니다. 또한 코드 재사용 기회를 크게 개선할 수 있으며 개발자와 UI 디자이너가 앱의 각 부분을 개발할 때 더 쉽게 공동 작업할 수 있습니다.

MVVM 패턴

MVVM 패턴에는 모델, 뷰 및 뷰 모델의 세 가지 핵심 구성 요소가 있습니다. 각 구성 요소는 다른 용도로 사용됩니다. 아래 다이어그램은 세 구성 요소 간의 관계를 보여 줍니다.

MVVM 패턴

각 구성 요소의 책임을 이해하는 것 외에 상호 작용하는 방법을 이해하는 것도 중요합니다. 개략적으로 뷰는 뷰 모델을 "인식"하고, 뷰 모델은 모델을 "인식"하지만 모델은 뷰 모델을 인식하지 못하고 뷰 모델은 뷰를 인식하지 못합니다. 따라서 뷰 모델은 뷰를 모델에서 격리하고 모델이 뷰와 독립적으로 발전할 수 있도록 합니다.

MVVM 패턴을 사용하면 다음과 같은 이점이 있습니다.

  • 기존 모델 구현이 기존 비즈니스 논리를 캡슐화하는 경우 변경하기가 어렵거나 위험할 수 있습니다. 이 시나리오에서 뷰 모델은 모델 클래스의 어댑터 역할을 하며 이로 인해 모델 코드를 크게 변경할 수 없습니다.
  • 개발자는 뷰를 사용하지 않고 뷰 모델 및 모델에 대한 단위 테스트를 만들 수 있습니다. 뷰 모델에 대한 단위 테스트는 뷰에서 사용하는 것과 정확히 동일한 기능을 실행할 수 있습니다.
  • 뷰 전체가 XAML 또는 C#에서 구현된 경우 뷰 모델 및 모델 코드를 건드리지 않고 앱 UI를 다시 디자인할 수 있습니다. 따라서 새 버전의 뷰가 기존 뷰 모델에서도 작동합니다.
  • 디자이너와 개발자는 개발 중에 구성 요소에 대해 독립적으로 동시에 작업할 수 있습니다. 디자이너는 뷰에 집중할 수 있고 개발자는 뷰 모델 및 모델 구성 요소에서 작업할 수 있습니다.

MVVM을 효과적으로 사용하는 관건은 앱 코드를 올바른 클래스로 팩터링하는 방법과 클래스가 상호 작용하는 방식을 이해하는 것입니다. 다음 섹션에서는 MVVM 패턴에서 각 클래스의 책임을 설명합니다.

보기

뷰는 사용자가 화면에 표시되는 구조, 레이아웃 및 모양을 정의합니다. 이상적으로 각 뷰는 비즈니스 논리를 포함하지 않는 제한된 코드 숨김을 사용하여 XAML에서 정의됩니다. 그러나 경우에 따라 코드 숨김에는 애니메이션과 같이 XAML에서 표현하기 어려운 시각적 동작을 구현하는 UI 논리가 포함될 수 있습니다.

.NET MAUI 애플리케이션에서 뷰는 일반적으로 ContentPage 파생 또는 ContentView 파생 클래스입니다. 그러나 뷰는 표시되는 개체를 시각적으로 표현하는 데 사용할 UI 요소를 지정하는 데이터 템플릿으로도 나타낼 수 있습니다. 뷰로서의 데이터 템플릿에는 코드 숨김이 없으며 특정 뷰 모델 형식에 바인딩되도록 설계되었습니다.

코드 숨김에서 UI 요소를 사용하도록 설정하거나 사용하지 않도록 설정하지 마세요.

뷰 모델이 명령을 사용할 수 있는지 여부 또는 작업이 보류 중임을 나타내는 표시 등 뷰 표시의 일부 측면에 영향을 주는 논리적 상태 변경을 정의하도록 해야 합니다. 따라서 코드 숨김에서 UI 요소를 사용하도록 설정하거나 사용하지 않도록 설정하는 대신 뷰 모델 속성에 바인딩하여 UI 요소를 사용하거나 사용하지 않도록 설정합니다.

뷰 모델에서 뷰 상호 작용에 대한 응답으로 코드를 실행하는 몇 가지 옵션이 있습니다(예: 단추 클릭 또는 항목 선택). 컨트롤이 명령을 지원하는 경우 컨트롤의 Command 속성은 뷰 모델의 ICommand 속성에 데이터 바인딩될 수 있습니다. 컨트롤의 명령이 호출되면 뷰 모델의 코드가 실행됩니다. 명령 외에도 동작을 뷰의 개체에 연결할 수 있으며 호출할 명령 또는 발생할 이벤트를 수신 대기할 수 있습니다. 이에 대한 응답으로 동작은 뷰 모델에서 ICommand 또는 뷰 모델의 메서드를 호출할 수 있습니다.

ViewModel

뷰 모델은 뷰가 데이터 바인딩될 수 있는 속성 및 명령을 구현하고 변경 알림 이벤트를 통해 뷰에 상태 변경을 알릴 수 있습니다. 뷰 모델에서 제공하는 속성 및 명령은 UI에서 제공할 기능을 정의하지만 뷰는 해당 기능을 표시하는 방법을 결정합니다.

비동기 작업을 사용하여 UI의 응답성을 유지합니다.

다중 플랫폼 앱은 사용자의 성능 인식을 개선하기 위해 UI 스레드를 차단 해제 상태로 유지해야 합니다. 따라서 뷰 모델에서 I/O 작업에 비동기 메서드를 사용하고 이벤트를 발생시켜 뷰에 속성 변경 내용을 비동기적으로 알립니다.

또한 뷰 모델은 필요한 모든 모델 클래스와 뷰의 상호 작용을 조정해야 합니다. 일반적으로 뷰 모델과 모델 클래스 간에 일대다 관계가 있습니다. 뷰 모델은 뷰의 컨트롤이 직접 데이터에 바인딩될 수 있도록 모델 클래스를 뷰에 직접 노출하도록 선택할 수 있습니다. 이 경우 모델 클래스는 데이터 바인딩 및 변경 알림 이벤트를 지원하도록 설계되어야 합니다.

각 뷰 모델은 뷰에서 쉽게 사용할 수 있는 형식으로 모델의 데이터를 제공합니다. 이를 위해 뷰 모델은 때때로 데이터 변환을 수행합니다. 뷰 모델에 이 데이터 변환을 배치하는 것은 뷰가 바인딩될 수 있는 속성을 제공하기 때문에 좋은 생각입니다. 예를 들어 뷰 모델은 뷰에서 더 쉽게 표시할 수 있도록 두 속성의 값을 결합할 수 있습니다.

변환 계층에서 데이터 변환을 중앙 집중화합니다.

변환기를 뷰 모델과 뷰 사이에 있는 별도의 데이터 변환 계층으로 사용할 수도 있습니다. 예를 들어 데이터에 뷰 모델이 제공하지 않는 특별한 서식이 필요한 경우 이 작업이 필요할 수 있습니다.

뷰 모델이 뷰를 사용하여 양방향 데이터 바인딩에 참여하려면 해당 속성이 PropertyChanged 이벤트를 발생시켜야 합니다. 뷰 모델은 INotifyPropertyChanged 인터페이스를 구현하고 속성이 변경되면 PropertyChanged 이벤트를 발생시켜 이 요구 사항을 충족합니다.

컬렉션의 경우 뷰 친화적인 ObservableCollection<T>이 제공됩니다. 이 컬렉션은 컬렉션 변경 알림을 구현하여 개발자가 컬렉션에서 INotifyCollectionChanged 인터페이스를 구현할 필요가 없습니다.

모델

모델 클래스는 앱의 데이터를 캡슐화하는 비 시각적 클래스입니다. 따라서 모델은 일반적으로 비즈니스 및 유효성 검사 논리와 함께 데이터 모델을 포함하는 앱의 도메인 모델을 나타내는 것으로 생각할 수 있습니다. 모델 개체의 예로는 DTO(데이터 전송 개체), POCO(Plain Old CLR Objects), 생성된 엔터티 및 프록시 개체가 있습니다.

모델 클래스는 일반적으로 데이터 액세스 및 캐싱을 캡슐화하는 서비스 또는 리포지토리와 함께 사용됩니다.

뷰 모델을 뷰에 연결

뷰 모델은 .NET MAUI의 데이터 바인딩 기능을 사용하여 뷰에 연결할 수 있습니다. 런타임에 뷰 및 뷰 모델을 생성하고 서로 연결하는 데 사용할 수 있는 여러 가지 방법이 있습니다. 이러한 방법은 뷰 우선 구성과 뷰 모델 우선 구성이라고 하는 두 가지 범주로 분류됩니다. 뷰 우선 구성과 뷰 모델 우선 구성 중에서 선택하는 것은 기본 설정 및 복잡성의 문제입니다. 그러나 모든 방법은 뷰가 BindingContext 속성에 뷰 모델을 할당한다는 동일한 목표가 있습니다.

뷰 우선 구성을 사용하면 앱은 개념적으로 사용하는 뷰 모델에 연결하는 뷰로 구성됩니다. 이 방법의 주요 이점은 뷰 모델이 뷰 자체에 의존하지 않으므로 느슨하게 결합된 단위 테스트 가능한 앱을 쉽게 생성할 수 있다는 것입니다. 또한 클래스가 만들어지고 연결되는 방법을 이해하기 위해 코드 실행을 추적할 필요 없이 시각적 구조를 따라 앱의 구조를 쉽게 이해할 수 있습니다. 또한 뷰 우선 구성은 탐색이 발생할 때 페이지 생성을 담당하는 Microsoft Maui의 탐색 시스템과 정렬되므로 뷰 모델 우선 구성은 복잡하고 플랫폼과 잘못 정렬됩니다.

뷰 모델 우선 구성을 사용하면 앱은 개념적으로 뷰 모델에 대한 뷰를 찾는 서비스를 사용하는 뷰 모델로 구성됩니다. 뷰 모델 우선 구성은 뷰 만들기를 추상화할 수 있어 앱의 논리적 비 UI 구조에 집중할 수 있으므로 일부 개발자에게 더 자연스럽게 느껴집니다. 또한 다른 뷰 모델이 뷰 모델을 만들도록 허용합니다. 그러나 이 방법은 종종 복잡하며 앱의 다양한 부분이 만들어지고 연결되는 방식을 이해하기 어려울 수 있습니다.

뷰 모델과 뷰를 독립적으로 유지합니다.

뷰의 데이터 원본 속성에 대한 바인딩은 해당 뷰 모델에 대한 뷰의 보안 주체 종속성이어야 합니다. 특히 뷰 모델에서 단추 및 ListView와 같은 뷰 형식을 참조하지 마세요. 여기에 설명된 원칙에 따라 뷰 모델을 격리하여 테스트할 수 있으므로 범위를 제한하여 소프트웨어 결함의 가능성을 줄일 수 있습니다.

다음 섹션에서는 뷰 모델을 뷰에 연결하는 주요 방법을 설명합니다.

선언적으로 뷰 모델 만들기

가장 간단한 방법은 뷰가 XAML에서 해당 뷰 모델을 선언적으로 인스턴스화하는 것입니다. 뷰가 생성되면 해당 뷰 모델 개체도 생성됩니다. 다음 코드 예제는 이 방법을 보여 줍니다.

<ContentPage xmlns:local="clr-namespace:eShop">
    <ContentPage.BindingContext>
        <local:LoginViewModel />
    </ContentPage.BindingContext>
    <!-- Omitted for brevity... -->
</ContentPage>

ContentPage가 만들어지면 LoginViewModel 인스턴스가 자동으로 생성되고 뷰의 BindingContext로 설정됩니다.

뷰에 의한 이 선언적 뷰 모델 생성 및 할당은 간단하다는 장점이 있지만 뷰 모델에서 기본(매개 변수가 없는) 생성자가 필요하다는 단점이 있습니다.

프로그래밍 방식으로 뷰 모델 만들기

뷰에서는 코드 숨김 파일에 코드가 있을 수 있으므로 뷰 모델이 해당 BindingContext 속성에 할당됩니다. 다음 코드 예제와 같이 이 작업은 뷰의 생성자에서 수행되는 경우가 많습니다.

public LoginView()
{
    InitializeComponent();
    BindingContext = new LoginViewModel(navigationService);
}

뷰의 코드 숨김 내에서 프로그래밍 방식 뷰 모델 생성 및 할당은 간단하다는 장점이 있습니다. 그러나 이 방법의 주요 단점은 뷰가 뷰 모델에 필요한 종속성을 제공해야 한다는 것입니다. 종속성 주입 컨테이너를 사용하면 뷰와 뷰 모델 간의 느슨한 결합을 유지하는 데 도움이 될 수 있습니다. 자세한 내용은 종속성 주입을 참조하세요.

기본 뷰 모델 또는 모델의 변경 내용에 대한 응답으로 뷰 업데이트

뷰에 액세스할 수 있는 모든 뷰 모델 및 모델 클래스는 INotifyPropertyChanged 인터페이스를 구현해야 합니다. 뷰 모델 또는 모델 클래스에서 이 인터페이스를 구현하면 기본 속성 값이 변경될 때 클래스가 뷰의 데이터 바인딩된 컨트롤에 변경 알림을 제공할 수 있습니다.

앱은 다음 요구 사항을 충족하여 속성 변경 알림을 올바로 사용하도록 설계되어야 합니다.

  • 공용 속성의 값이 변경되면 항상 PropertyChanged 이벤트를 발생시킵니다. XAML 바인딩이 발생하는 방식에 대한 지식 때문에 PropertyChanged 이벤트 발생을 무시할 수 있다고 가정하지 마세요.
  • 뷰 모델 또는 모델의 다른 속성에서 해당 값을 사용하는 계산 속성에 대해 항상 PropertyChanged 이벤트를 발생시킵니다.
  • 속성을 변경하는 메서드의 끝에서 또는 개체가 안전한 상태인 것으로 알려진 경우 항상 PropertyChanged 이벤트를 발생시킵니다. 이벤트가 발생하면 이벤트의 처리기를 동기적으로 호출하여 작업이 중단됩니다. 작업 중간에 발생하는 경우 부분적으로 업데이트된 안전하지 않은 상태일 때 개체가 콜백 함수에 노출될 수 있습니다. 또한 PropertyChanged 이벤트에 의해 계단식 변경이 트리거될 수 있습니다. 계단식 변경은 일반적으로 업데이트가 완료되어야 안전하게 실행될 수 있습니다.
  • 속성이 변경되지 않으면 PropertyChanged 이벤트를 발생시키지 않습니다. 즉, PropertyChanged 이벤트를 발생시키기 전에 이전 값과 새 값을 비교해야 합니다.
  • 속성을 초기화하는 경우 뷰 모델의 생성자 중에 PropertyChanged 이벤트를 발생시키지 않습니다. 뷰의 데이터 바인딩된 컨트롤은 이 시점에서 변경 알림을 수신하도록 구독하지 않습니다.
  • 클래스의 public 메서드에 대한 단일 동기 호출 내에서 속성 이름 인수가 같은 PropertyChanged 이벤트를 두 개 이상 발생시키지 않습니다. 예를 들어 NumberOfItems 속성의 백업 저장소가 _numberOfItems 필드인 경우 메서드가 루프 실행 동안 _numberOfItems를 50번 증가시키면 모든 작업이 완료된 후 NumberOfItems 속성에 대한 속성 변경 알림을 한 번만 발생시켜야 합니다. 비동기 메서드의 경우 비동기 연속 체인의 각 동기 세그먼트에서 지정된 속성 이름에 대해 PropertyChanged 이벤트를 발생시킵니다.

이 기능을 제공하는 간단한 방법은 BindableObject 클래스의 확장을 만드는 것입니다. 이 예제에서 ExtendedBindableObject 클래스는 다음 코드 예제에 표시된 변경 알림을 제공합니다.

public abstract class ExtendedBindableObject : BindableObject
{
    public void RaisePropertyChanged<T>(Expression<Func<T>> property)
    {
        var name = GetMemberInfo(property).Name;
        OnPropertyChanged(name);
    }

    private MemberInfo GetMemberInfo(Expression expression)
    {
        // Omitted for brevity ...
    }
}

.NET MAUI의 BindableObject 클래스는 INotifyPropertyChanged 인터페이스를 구현하고 OnPropertyChanged 메서드를 제공합니다. ExtendedBindableObject 클래스는 속성 변경 알림을 호출하는 RaisePropertyChanged 메서드를 제공하며, 이 경우 BindableObject 클래스에서 제공하는 기능을 사용합니다.

그러면 뷰 모델 클래스가 ExtendedBindableObject 클래스에서 파생될 수 있습니다. 따라서 각 뷰 모델 클래스는 ExtendedBindableObject 클래스의 RaisePropertyChanged 메서드를 사용하여 속성 변경 알림을 제공합니다. 다음 코드 예에서는 eShop 다중 플랫폼 앱이 람다 식을 사용하여 속성 변경 알림을 호출하는 방법을 보여 줍니다.

public bool IsLogin
{
    get => _isLogin;
    set
    {
        _isLogin = value;
        RaisePropertyChanged(() => IsLogin);
    }
}

이러한 방식으로 람다 식을 사용하면 각 호출에 대해 람다 식을 평가해야 하므로 성능 비용이 적습니다. 성능 비용이 적고 일반적으로 앱에 영향을 주지는 않지만 변경 알림이 많을 때는 비용이 발생할 수 있습니다. 그러나 이 방법의 이점은 속성 이름을 바꾸는 경우 컴파일 시간 형식 안전성 및 리팩터링 지원을 제공한다는 것입니다.

MVVM 프레임워크

MVVM 패턴은 .NET에서 잘 확립되었으며 커뮤니티는 이러한 개발을 용이하게 하는 많은 프레임워크를 만들었습니다. 각 프레임워크마다 제공하는 기능 집합이 다르지만 INotifyPropertyChanged 인터페이스 구현을 통해 공통 뷰 모델을 제공하는 것이 표준입니다. MVVM 프레임워크의 추가 기능에는 사용자 지정 명령, 탐색 도우미, 종속성 주입/서비스 로케이터 구성 요소, UI 플랫폼 통합이 포함됩니다. 이러한 프레임워크를 반드시 사용할 필요는 없지만 개발 속도를 향상시키고 표준화할 수 있습니다. eShop 다중 플랫폼 앱은 .NET 커뮤니티 MVVM 도구 키트를 사용합니다. 프레임워크를 선택할 때 애플리케이션의 요구 사항과 팀의 강점을 고려해야 합니다. 아래 목록에는 .NET MAUI에 대한 보다 일반적인 MVVM 프레임워크가 포함되어 있습니다.

명령 및 동작을 사용하는 UI 상호 작용

다중 플랫폼 앱에서 작업은 일반적으로 코드 숨김 파일에서 이벤트 처리기를 만들어 구현할 수 있는, 단추 클릭과 같은 사용자 작업에 대한 응답으로 호출됩니다. 그러나 MVVM 패턴에서 작업을 구현하는 책임은 뷰 모델에 있으며 코드를 코드 숨김에 배치하는 것은 피해야 합니다.

명령은 UI의 컨트롤에 바인딩될 수 있는 작업을 나타내는 편리한 방법을 제공합니다. 작업을 구현하는 코드를 캡슐화하고 뷰에서의 시각적 표현과 분리된 상태를 유지하는 데 도움이 됩니다. 이처럼 뷰 모델은 플랫폼의 UI 프레임워크에서 제공하는 이벤트에 대한 직접적인 종속성이 없으므로 새 플랫폼으로의 이식성이 높아집니다. .NET MAUI에는 명령에 선언적으로 연결할 수 있는 컨트롤이 포함되어 있으며, 이러한 컨트롤은 사용자가 컨트롤과 상호 작용할 때 명령을 호출합니다.

또한 동작을 사용해도 컨트롤을 명령에 선언적으로 연결할 수 있습니다. 그러나 동작은 컨트롤에 의해 발생되는 이벤트 범위와 연결된 작업을 호출하는 데 사용할 수 있습니다. 따라서 동작은 명령 사용 컨트롤과 동일한 많은 시나리오를 처리하면서 더 높은 수준의 유연성과 제어를 제공합니다. 또한 동작은 명령 개체 또는 메서드를 명령과 상호 작용하도록 특별히 설계되지 않은 컨트롤과 연결하는 데에 사용할 수도 있습니다.

명령 구현

뷰 모델은 일반적으로 ICommand 인터페이스를 구현하는 뷰에서 바인딩하기 위해 공용 속성을 노출합니다. 많은 .NET MAUI 컨트롤 및 제스처는 뷰 모델에서 제공하는 ICommand 개체에 바인딩된 데이터일 수 있는 Command 속성을 제공합니다. 단추 컨트롤은 가장 일반적으로 사용되는 컨트롤 중 하나로, 단추를 클릭하면 실행되는 명령 속성을 제공합니다.

참고

뷰 모델이 사용하는 ICommand 인터페이스(예: Command<T> 또는 RelayCommand)의 실제 구현을 노출하는 것이 가능하지만 명령을 ICommand로 공개적으로 노출하는 것이 좋습니다. 이렇게 하면 나중에 구현을 변경해야 하는 경우 쉽게 교체할 수 있습니다.

ICommand 인터페이스는 작업 자체를 캡슐화하는 Execute 메서드, 명령을 호출할 수 있는지 여부를 나타내는 CanExecute 메서드, 명령 실행 여부에 영향을 주는 변경이 발생할 때 발생하는 CanExecuteChanged 이벤트를 정의합니다. 대부분의 경우 Microsoft 명령에 대한 Execute 메서드만 제공됩니다. ICommand에 대한 자세한 개요는 .NET 명령어 문서 MAUI를 참조하세요.

NET MAUI에는 ICommand 인터페이스를 구현하는 CommandCommand<T> 클래스가 제공되며, 여기서 TExecuteCanExecute에 대한 인수의 유형입니다. CommandCommand<T>ICommand 인터페이스에 필요한 최소한의 기능 집합을 제공하는 기본 구현입니다.

참고

많은 MVVM 프레임워크는 ICommand 인터페이스의 다양한 기능을 구현합니다.

Command 또는 Command<T> 생성자에는 ICommand.Execute 메서드가 호출될 때 호출되는 Action 콜백 개체가 필요합니다. CanExecute 메서드는 선택적 생성자 매개 변수이며 부울을 반환하는 함수입니다.

eShop 다중 플랫폼 앱은 RelayCommandAsyncRelayCommand를 사용합니다. 최신 애플리케이션의 주요 이점은 AsyncRelayCommand가 비동기 작업에 더 나은 기능을 제공한다는 것입니다.

다음 코드에서는 Register 뷰 모델 메서드에 대리자를 지정하여 register 명령을 나타내는 Command 인스턴스를 생성하는 방법을 보여 줍니다.

public ICommand RegisterCommand { get; }

명령은 ICommand에 대한 참조를 반환하는 속성을 통해 뷰에 노출됩니다. Execute 메서드는 Command 개체에 호출되면 Command 생성자에 지정된 대리자를 통해 뷰 모델의 메서드에 호출을 전달하기만 합니다. 명령의 Execute 대리자를 지정할 때 비동기 및 await 키워드를 사용하여 명령에서 비동기 메서드를 호출할 수 있습니다. 이는 콜백이 Task이며 대기해야 함을 나타냅니다. 예를 들어 다음 코드는 로그인 명령을 나타내는 ICommand 인스턴스가 SignInAsync 보기 모델 메서드에 델리게이트를 지정하여 어떻게 구성되는지 보여줍니다:

public ICommand SignInCommand { get; }
...
SignInCommand = new AsyncRelayCommand(async () => await SignInAsync());

매개변수는 AsyncRelayCommand<T> 클래스를 사용하여 명령을 인스턴스화하여 ExecuteCanExecute 액션에 전달할 수 있습니다. 예를 들어 다음 코드는 AsyncRelayCommand<T> 인스턴스를 사용하여 NavigateAsync 메서드에 문자열 형식의 인수가 필요함을 나타내는 방법을 보여줍니다.

public ICommand NavigateCommand { get; }

...
NavigateCommand = new AsyncRelayCommand<string>(NavigateAsync);

RelayCommandRelayCommand<T> 클래스 모두에서 각 생성자의 CanExecute 메서드에 대한 대리자는 선택 사항입니다. 대리자를 지정하지 않으면 CommandCanExecute에 대해 true를 반환합니다. 그러나 뷰 모델은 Command 개체에서 ChangeCanExecute 메서드를 호출하여 명령의 CanExecute 상태 변경을 나타낼 수 있습니다. 이로 인해 CanExecuteChanged 이벤트가 발생합니다. 명령에 바인딩된 모든 UI 컨트롤은 데이터 바인딩된 명령의 가용성을 반영하도록 사용 상태를 업데이트합니다.

뷰에서 명령 호출

다음 코드 예제는 LoginViewGridTapGestureRecognizer 인스턴스를 사용하여 LoginViewModel 클래스의 RegisterCommand에 바인딩하는 방법을 보여줍니다:

<Grid Grid.Column="1" HorizontalOptions="Center">
    <Label Text="REGISTER" TextColor="Gray"/>
    <Grid.GestureRecognizers>
        <TapGestureRecognizer Command="{Binding RegisterCommand}" NumberOfTapsRequired="1" />
    </Grid.GestureRecognizers>
</Grid>

필요에 따라 CommandParameter 속성을 사용하여 명령 매개 변수를 정의할 수도 있습니다. 예상 인수의 형식은 ExecuteCanExecute 대상 메서드에 지정됩니다. TapGestureRecognizer는 사용자가 연결된 컨트롤과 상호 작용할 때 대상 명령을 자동으로 호출합니다. CommandParameter가 제공되면 명령의 Execute 대리자에 인수로 전달됩니다.

동작 구현

동작을 사용하면 기능을 하위 클래스 없이도 UI 컨트롤에 추가할 수 있습니다. 대신 기능은 동작 클래스에서 구현되고 컨트롤 자체의 일부였던 것처럼 컨트롤에 연결됩니다. 동작을 사용하면 코드가 컨트롤에 간결하게 첨부되고 둘 이상의 뷰 또는 앱에서 재사용하도록 패키지될 수 있도록 컨트롤의 API와 직접 상호 작용하기 때문에, 일반적으로 코드 숨김으로 작성해야 할 코드를 구현할 수 있습니다. MVVM 컨텍스트에서 동작은 명령에 컨트롤을 연결하는 데 유용한 접근 방식입니다.

연결된 속성을 통해 컨트롤에 연결된 동작을 연결된 동작이라고 합니다. 그런 다음 동작은 연결된 요소의 노출된 API를 사용하여 뷰의 시각적 트리에서 해당 컨트롤 또는 기타 컨트롤에 기능을 추가할 수 있습니다.

.NET MAUI 동작은 Behavior 또는 Behavior<T> 클래스에서 파생된 클래스입니다. 여기서 T는 동작이 적용되어야 하는 컨트롤의 형식입니다. 이러한 클래스는 OnAttachedToOnDetachingFrom 메서드를 제공하며, 이들 메서드는 동작이 컨트롤에 연결 및 분리될 때 실행될 논리를 제공하기 위해 재정의되어야 합니다.

eShop 다중 플랫폼 앱에서 BindableBehavior<T> 클래스는 Behavior<T> 클래스에서 파생됩니다. BindableBehavior<T> 클래스의 목적은 첨부된 컨트롤에 동작의 BindingContext을 설정해야 하는 .NET MAUI 동작에 대한 기본 클래스를 제공하는 것입니다.

BindableBehavior<T> 클래스는 동작의 BindingContext을 설정하는 재정의 가능한 OnAttachedTo 메서드와 BindingContext를 정리하는 재정의 가능한 OnDetachingFrom 메서드를 제공합니다.

eShop 멀티플랫폼 앱에는 MAUI 커뮤니티 툴킷에서 제공하는 이벤트 명령 동작 클래스가 포함되어 있습니다. EventToCommandBehavior는 발생하는 이벤트에 대한 응답으로 명령을 실행합니다. 이 클래스는 BaseBehavior<View> 클래스에서 파생되어 동작이 소비될 때 동작이 Command 속성으로 지정된 ICommand에 바인딩하고 실행할 수 있도록 합니다. 다음 코드 예제는 EventToCommandBehavior 클래스를 보여줍니다.

/// <summary>
/// The <see cref="EventToCommandBehavior"/> is a behavior that allows the user to invoke a <see cref="ICommand"/> through an event. It is designed to associate Commands to events exposed by controls that were not designed to support Commands. It allows you to map any arbitrary event on a control to a Command.
/// </summary>
public class EventToCommandBehavior : BaseBehavior<VisualElement>
{
    // Omitted for brevity...

    /// <inheritdoc/>
    protected override void OnAttachedTo(VisualElement bindable)
    {
        base.OnAttachedTo(bindable);
        RegisterEvent();
    }

    /// <inheritdoc/>
    protected override void OnDetachingFrom(VisualElement bindable)
    {
        UnregisterEvent();
        base.OnDetachingFrom(bindable);
    }

    static void OnEventNamePropertyChanged(BindableObject bindable, object oldValue, object newValue)
        => ((EventToCommandBehavior)bindable).RegisterEvent();

    void RegisterEvent()
    {
        UnregisterEvent();

        var eventName = EventName;
        if (View is null || string.IsNullOrWhiteSpace(eventName))
        {
            return;
        }

        eventInfo = View.GetType()?.GetRuntimeEvent(eventName) ??
            throw new ArgumentException($"{nameof(EventToCommandBehavior)}: Couldn't resolve the event.", nameof(EventName));

        ArgumentNullException.ThrowIfNull(eventInfo.EventHandlerType);
        ArgumentNullException.ThrowIfNull(eventHandlerMethodInfo);

        eventHandler = eventHandlerMethodInfo.CreateDelegate(eventInfo.EventHandlerType, this) ??
            throw new ArgumentException($"{nameof(EventToCommandBehavior)}: Couldn't create event handler.", nameof(EventName));

        eventInfo.AddEventHandler(View, eventHandler);
    }

    void UnregisterEvent()
    {
        if (eventInfo is not null && eventHandler is not null)
        {
            eventInfo.RemoveEventHandler(View, eventHandler);
        }

        eventInfo = null;
        eventHandler = null;
    }

    /// <summary>
    /// Virtual method that executes when a Command is invoked
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="eventArgs"></param>
    [Microsoft.Maui.Controls.Internals.Preserve(Conditional = true)]
    protected virtual void OnTriggerHandled(object? sender = null, object? eventArgs = null)
    {
        var parameter = CommandParameter
            ?? EventArgsConverter?.Convert(eventArgs, typeof(object), null, null);

        var command = Command;
        if (command?.CanExecute(parameter) ?? false)
        {
            command.Execute(parameter);
        }
    }
}

OnAttachedToOnDetachingFrom 메서드는 EventName 속성에 정의된 이벤트에 대한 이벤트 처리기를 등록 및 등록 취소하는 데 사용됩니다. 그런 다음 이벤트가 발생하면 OnTriggerHandled 메서드가 호출되어 명령을 실행합니다.

이벤트가 발생할 때 EventToCommandBehavior를 사용하여 명령을 실행하는 이점은 명령과 상호 작용하도록 설계되지 않은 컨트롤과 명령을 연결할 수 있다는 점입니다. 또한 이벤트 처리 코드를 뷰 모델로 이동하므로 단위 테스트를 수행할 수 있습니다.

뷰에서 동작 호출

EventToCommandBehavior은(는) 명령을 지원하지 않는 컨트롤에 명령을 연결하는 데 특히 유용합니다. 예를 들어 LoginView는 다음 코드와 같이 EventToCommandBehavior를 사용하여 사용자가 암호 값을 변경할 때 ValidateCommand를 실행합니다.

<Entry
    IsPassword="True"
    Text="{Binding Password.Value, Mode=TwoWay}">
    <!-- Omitted for brevity... -->
    <Entry.Behaviors>
        <mct:EventToCommandBehavior
            EventName="TextChanged"
            Command="{Binding ValidateCommand}" />
    </Entry.Behaviors>
    <!-- Omitted for brevity... -->
</Entry>

런타임 시 EventToCommandBehaviorEntry와의 상호 작용에 응답합니다. 사용자가 Entry 필드에 입력하면 TextChanged 이벤트가 발생하여 LoginViewModel에서 ValidateCommand이 실행됩니다. 기본적으로 이벤트에 대한 이벤트 인수는 명령에 전달됩니다. 필요한 경우 EventArgsConverter 속성을 사용하여 이벤트에서 제공하는 EventArgs를 명령이 입력으로 예상하는 값으로 변환할 수 있습니다.

동작에 대한 자세한 내용은 .NET MAUI 개발자 센터의 동작 를 참조하세요.

요약

MVVM(Model-View-ViewModel) 패턴은 애플리케이션의 비즈니스 및 프레젠테이션 논리를 UI(사용자 인터페이스)와 명확하게 구분하는 데 도움이 됩니다. 애플리케이션 논리와 UI 간의 명확한 분리를 유지 관리하면 수많은 개발 문제를 해결하고 애플리케이션을 더 쉽게 테스트, 유지 관리 및 개량할 수 있습니다. 또한 코드 재사용 기회를 크게 개선할 수 있으며 개발자와 UI 디자이너가 앱의 각 부분을 개발할 때 더 쉽게 공동 작업할 수 있습니다.

MVVM 패턴을 사용할 때 앱의 UI와 기본 프레젠테이션 및 비즈니스 논리는 UI 및 UI 논리를 캡슐화하는 뷰, 프레젠테이션 논리 및 상태를 캡슐화하는 뷰 모델, 앱의 비즈니스 논리 및 데이터를 캡슐화하는 모델의 세 가지 개별 클래스로 구분됩니다.