Windows Community Tookit의 MVVM 툴킷 살펴보기

Windows Community Toolit은 유용한 도우미 기능, 사용자 지정 컨트롤 및 앱 서비스 모음입니다. 여기서 제공하는 MVVM Kit는 Windows에 종속성이 없어 다양한 플랫폼에서 사용할 수 있는데요, 특히 .NET 5 이상에서 좀 더 최적화 되었다고 합니다.

MVVM을 사용하기 위해 Windows Community Toolkit에서 제공하는 MVVM 툴킷을 사용할 수 있는데요, Microsoft.Toolkit.Mvvm 패키지로 NuGet에서 내려받아 사용할 수 있습니다. 최신 버젼은 7.1.0입니다.

MVVM을 편하게 사용하려면 IoC를 써야 하는데요, Ioc.Default를 통해 사용할 수 있고 IServiceProvider제공하는 IoC라면 ConfigureServices()를 통해 적용할 수 있습니다.

image

서비스 등록

서비스 등록은 Microsoft.Extensions.DependencyInjection에서 제공하는 ServiceCollection을 통해 합니다.

var s = new ServiceCollection();

// ViewModel, View 등록 {
s.AddTransient<MainViewModel>();
s.AddTransient<MainPage>();

s.AddTransient<SettingViewModel>();
s.AddTransient<SettingPage>();

// 어플리케이션 Context 등록
s.AddSingleton<IAppContext>(new AppContext());

// 자원 서비스 등록
s.AddSingleton<IResourceService>(new ResourceService());

Ioc.Default.ConfigureServices(s);

AddSingleton()은 싱글톤으로 하나의 개체를 계속 사용하고
AddTransient()은 서비스를 요청할 때마다 개체를 생성하는 대상을 등록합니다.

일반적으로 서비스AddSingleton()으로, 뷰모델은 AddTransient()으로 등록하나 필수는 아니며 목적에 맞게 사용할 수 있습니다.

서비스 사용

IoC가 ConfigureServices()에 의해 필요시 등록된 서비스를 사용할 수 있게 되면 등록된 서비스를 GetService()에 의해 IoC에서 가져올 때 의존성 주입(Dependency Injection)이 자동으로 이루어 집니다. 단, 주입 대상은 반드시 등록된 서비스여야 하는데요, 이런 식입니다.

| 가장 일반적인 생성자 주입(Constructor Injection)

public SettingViewModel(IConfiguration config, IStorage storage) { ... }

여기서 SettingViewModel, IConfiguration, IStorage는 모두 서비스로 등록되어 있어야 합니다.

그런데 여기서 인터페이스 형태가 보이는데요, 의존성 주입의 장점이 서비스를 등록할 때 인터페이스와 대상유형 또는 인스턴스를 매칭해서 등록하면 사용하는 쪽에서 대상을 특정하지 않고 인터페이스로 접근이 된다는 점입니다.

생성자가 아닌 속성에 Attribute를 줘서 주입하는 방법도 있습니다.

| 속성의 Attribute를 통해 의존성 주입
보통 InjectAttribute를 통해 자동으로 주입 하는데요,

[Inject]
private IConfiguration Config { get; }

MVVM 툴킷은 지원하지 않는 것 같습니다.

ObservableObject

MVVM 툴킷은 ObservableObject을 통해 관찰 가능한 개체를 생성할 수 있습니다.

INotifyPropertyChanged, INotifyPropertyChanging 인터페이스를 구현한 기본 클래스이며, 뷰모델을을 이 클래스에서 상속받아 만들 수 있습니다.

class SettingViewModel : ObservableObject
{
...
}

속성이 변경되었을 때 외부에서 관찰할 수 있게 하려면 다음과 같이 할 수 있습니다.

class SettingViewModel : ObservableObject
{
      private bool _isSaved;
      public bool IsSaved
      {
          get => _isSaved;
          set => SetProperty(ref _isSaved, value);
      }
}

SetProperty()에 의해 IsSaved값이 변경되었음을 외부에서 관찰할 수 있도록 합니다.

소스 생성기를 이용한 간결한 관찰 기능 구현

MVVM 툴킷이 이제 소스 생성기를 이용한 간결한 Attribute를 통해 관찰 가능을 손쉽게 구현할 수 있게 되었습니다.

class partial SettingViewModel : ObservableObject
{
      [ObservableProperty]
      private bool _isSaved;
}

소스 생성기의 정책에 의해 개발자의 코드를 수정하지는 않습니다. 그래서 자동 생성된 코드와 잘 결합하도록 하려면 클래스에 partial을 줘야 하고 해당 Attribute를 필드에 줘야 합니다. isSaved 또는 _isSaved 필드의 [ObservablePropery] Attribute를 통해 IsSaved 속성을 생성하고 관찰 가능한 코드를 자동 생성하게 됩니다.

좋아요 4

Microsoft MVP이신 Connor Park님의 참고 되는 글입니다.

좋아요 2

아 Toolkit이 계속 개발되고 있었군요…저도 이거 써볼까했지만 지원중단 글을 본거같아서 배우길 포기했는데…ㅎㅎ

좋아요 2

여럿 MVVM 라이브러리를 써봤고 어떤 것은 자동으로 알아서 많이 해줘 처음엔 흥미로워서 써봤지만 결국엔 유지보수가 힘든 경험도 했어서… 딱 MVVM 툴킷 정도가 적절한 라이브러리로 보여져 이것을 사용하고 있습니다. ^^

좋아요 2

이거 좋아요 >ㅁ<b ! 이거 진짜 대박임. Ioc 구현도 잘 되어 있고 엄청 유용한 기능들 많아요.

근데 한 가지 아쉬운 건…

이거 제가 못 찾는 건지 원래 없는 건지는 모르겠는데 EventToCommand 가 없다는 거…
혹시 제가 못 찾는 거라면 누가 좀 알려주…

좋아요 2

IoC는 그냥 Generic Host같은데 자체 구현된 다른 IoC 인가요??

좋아요 2

위의 소스코드를 보시면 그냥 ConfigureServices()에 의해 호스팅만 해주는 것을 알 수 있습니다. 본문을 수정해야 겠네요.

좋아요 2

이 부분 같습니다.

소스는 보지 않고 Generic Host랑 형태가 완전히 같아서 완전 같은 줄 알았는데, 그래도 다른 네임스페이스를 사용하면서 고대로 Generic Host에 토스해주긴 하네요.

좋아요 3

맞아욤. Ioc 클래스 자체가 그냥 IServiceProvider 구현체이기도 한데, 실제 IServiceProvider 객체는 밖에서 던져주는 걸 쓰도록 되어 있지요.
그 밖에서 던져주는 건 Microsoft.Extensions.DependencyInjection 요기 있는 ServiceProvider 객체를 쓰면 됩니당. 딱 그 AppBuilder 스타일의 세팅이면 되욤. @ㅂ@


namespace MVVM.Entry.Bootstrap
{
    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.Toolkit.Mvvm.DependencyInjection;
    using System;

    public static class AppLoader
    {
        static AppLoader()
        {
            var services = ConfigureServices();
            Ioc.Default.ConfigureServices(services);
        }

        /// <summary>
        /// Configures the services for the application.
        /// </summary>
        private static IServiceProvider ConfigureServices()
        {
            var services = new ServiceCollection();

            services.AddSingleton<IProductRepository, ProductRepository>();

            return services.BuildServiceProvider();
        }

        public static void Load(string productPath, string layoutPath)
        {
            var repository = Ioc.Default.GetService<IProductRepository>();
            repository.LoadProduct(productPath, layoutPath);
        }
    }
}

요딴식으루 쓰면 됩니당 ~ㅂ~

좋아요 3

ObservableRecipient 사용하기

ObservableObject로도 관찰 가능한 개체를 만들 수 있지만 ObservableObject에서 파생된ObservableRecipient를 이용하면 ObservableRecipient`끼리 메시지를 보내거나 받을 수 있습니다. MVVM에서는 ViewModel 끼리 강한 결합을 지양하는데요, 이것은 목표 ViewModel를 지정해야만 하기 때문에 ViewModel간의 상호 결합력이 생기고, 또한 ViewModel 생명주기에 의해서 이 작업의 예외가 충분히 피곤하게 하는 요소이기 떄문이기도 합니다.

메신저를 통해 메시지를 주고 받게 되면, 자신이 필요하다면 메시지 수신을 등록하고, 소비하면 되는 심플한 구조가 되는 것이죠.

메시지의 수신 등록을 수동으로 할 수도 있지만 인터페이스를 통해 좀 더 편리하게 수신할 수도 있습니다.

public RecipientViewModel : ObservableRecipient, IRecipient<Message>
{
    public void Receive(Message message)
    {
        var messageName = message.Name;
    }
}

IRecipient<Message>에 의해 Message를 받을 수 있게 됩니다. 여기서 Message는 클래스라면 무엇이나 대상이 될 수 있어요. 이 방식의 장점은, 메시지 수신 등록메시지 수신 해제를 자동으로 해준다는데 있습니다. 사용하기도 편하고요.

이제 해당 메시지를 보내는 측을 다음과 같이 구현할 수 있습니다.

public SenderViewModel : ObservableRecipient
{
    public FuncMessageSendTest()
    {
        Messenger.Send(new Message { Name = "TestMessage" });
    }
}

ObservableRecipient에서는 속성이 변경될 때 외부에서 변경내용을 수신할 수 있도록 하는 메소드도 제공합니다.

ObservableRecipient.cs

...
protected bool SetProperty<T>([NotNullIfNotNull("newValue")] ref T field, T newValue, bool broadcast, [CallerMemberName] string? propertyName = null)
...

여기서 broadcast 인자가 true일 때 속성이 변경되면서 PropertyChangedMessage<T> 메시지를 발생 시킵니다. 이를 메시지를 받고자 하는 곳에서 IRecipient<PropertyChangedMessage<Message>> 등으로 인터페이스를 구현하면 메시지를 수신 받을 수 있게 됩니다.

그런데, 완전 자동이 아니에요. View이 활성화 될 때 [ViewModel 인스턴스].IsActive = true를 줘서 메시지 수신 등록을 해줘야 하고 View가 비활성화 될 때 [ViewModel 인스턴스].IsActive = false를 통해 메시지 수신을 해제해줘야 합니다.

이는 IsActive가 true일 때 ObservableRecipientOnActivated()가 호출되고, false일 때 OnDeactivated()가 호출되는데 이 두 가상 함수에서 메시지 수신 등록해제가 구현되어 있기 때문입니다.

좋아요 3

RelayCommand 사용하기

View에서 ViewModel의 명령을 실행하기 위해서 일반적으로 사용하는 방법이ICommand 인터페이스 입니다. ICommandSystem.ObjectModel에서 정의되어 있으며 다음과 같습니다.

    public interface ICommand
    {
        event EventHandler? CanExecuteChanged;
        bool CanExecute(object? parameter);
        void Execute(object? parameter);
    }

일반 메소드 호출과 다른 점이, View와 상호 작용을 한다는 점인데, CanExecuteChanged 이벤트에 의한 것입니다. 즉, 이 명령이 실행 가능한가? CanExecute() 불가능 하다면 버튼 등의 입력 컨트롤이 비활성화 됩니다. 그런데 어떤 상태에 따라 불가능 하다가 가능하게 되는데 가령, 시작을 해야 중지 버튼이 활성화 되는 것 같은 상황입니다.

이럴 때 CanExecuteChanged 이벤트를 발생시키면 ICommand처리를 잘 구현한 View와 상호작용할 수 있게 됩니다. 이런 뷰로는 WPF, UWP, WinUI3 등등이 되겠지요.

그런데 매번 ICommand 인터페이스를 구현한 클래스를 만드는 것은 소모적입니다. 그래서 커뮤니티 MVVM 툴킷은 RelayCommand를 제공합니다.

RelayCommand는 명령인자가 없는 RelayCommand와 명령인자가 있는 RelayCommand<T>를 사용할 수 있습니다. 일반적인 사용법은 다음과 같습니다.

public RelayCommand RunCommand { get; }

생성자에서,
RunCommand = new RelayCommand(() =>
{
    ....
}, () => 실행가능유무);

그런데 상태가 변해 처음에는 실행가능유무 false였다가 true로 바꿔야 한다고 칩시다.

그려면 RunCommand.NotifyCanExecuteChanged()를 한번 호출해줘 View가 이를 처리할 수 있도록 해주면 됩니다.

좋아요 3

View-first vs ViewModel-first ?

MVVM은 이미 10년을 훌쩍 넘는 역사가 있습니다. MVVM을 전개하는 방법 중

View를 생성하면서 ViewModel과 관계를 맺어 MVVM를 전개할 것이냐 vs
ViewModel를 생성하면서 View와 관계를 맺어 MVVM를 전개할 것이냐

인데, View-first는 구조가 단순하고 코드의 흐름이 화면 -> 구현으로 전개되어 코드 해독이 좋은 반면, 화면부터 전개하기 때문에 ViewModel을 뷰와 독립적으로 설계하기가 어렵게 되는 단점이 있고,

ViewModel-first는 뷰와 독립적인 이상적 모델링이 가능하다는 큰 장점이 있으나, ViewModel에서 View로 접근하기 위한 다양한 인터페이스를 View에 구현해줘야 하는 고단함이 있고, 이를 자동화 하는 라이브러리를 이용할 수도 있으나 화면 구현이 이런 인터페이스를 지켜줘야 하는 어려움이 생깁니다.

그래서 저는 처음에는 ViewModel-first를 지향했다가 너무나 고단해서 요즘에는 View-first로 구현하고 있습니다.

Community Toolkit에서 제공하는 MVVM 기능은 이러한 방법론에 종속되어 있지 않습니다. 그냥 원하는 대로 구현할 수 있도록 가장 기본적인 MVVM 기능만 제공한다고 생각하시면 됩니다. (결국엔 단순한게 최고)

정리

이렇게 Windows Community Toolkit 살펴봤습니다. 좀 더 자세한 내용은 아래의 문서를 통해 살펴볼 수 있습니다.

Windows Community Toolkit Documentation - Windows Community Toolkit | Microsoft Docs.

좋아요 3

개인적으로는…

태초에 데이터가 있었으니…

MVVM 으로 뭔가 만든다는 것은 결국 UI 가 있는 프로그램을 만드는 것인데, 데이터가 존재하지 않는 영역의 UI 프로그램은 사실 MVVM 이 큰 의미가 없을 가능성이 높죠.

개발자가 전체 개발과정에 적극적인 개입을 할 수 있는 상황 여부가 중요할 것 같습니다.(특히 기획 과정에서!)
무엇을 만들지, 어떻게 만들지. 결국 기획에 따라 개발 방향은 바뀔 수밖에 없겠지요.
(작고 소듕 기획느님… =ㅂ=)

결국 모든 것이 기획에 달려 있다면, 존재하는 데이터를 어떻게 보여 줄 지 여부 역시 기획에 기반한 것이므로
사실 이런 프로세스라면 ViewModel first 가 거의 가능하지 않다고 봅니다…(스트레스와의 전쟁…;ㅂ;)

게다가 MVVM 의 기본적인 목표는, view 의 추상화를 통한 model 과의 의존성 분리이기 때문에
결국엔 view 없이 시작하는 것은 매우 어려운(뜬구름 잡는 것에 가까운…) 과정이 되겠지요…

그리고 또, 뭐… 직접 개발하는 사람들이 쉽게 인지하고 생각을 확장시켜나갈 수 있느냐 역시 중요한 문제이므로
아무래도 눈에 보이는 view 를 기준으로 설계와 구조 생성 작업을 하는 게 매끄러운 프로젝트 진행을 위해서도 좋을 않을까 싶습니다.

사실… ViewModel first 는 그냥 너무 어려워요…-ㅁ-;;;

네. 저도 경험해보니 ViewModel-first는 이상적인 접근 방법인 것 같습니다. 의도한 대로라면 잘 설계된 ViewModel이 데스크탑 앱에도 그대로 사용되고, 웹 앱에도 그대로 사용되는 일이 벌어져야 하는데, 경험적으로 그렇게는 안되더라고요. 이것을 설계의 문제다 라고 하기엔 View가 그것을 잘 수용하지도 못했고요. 그냥 View-first로 최대한 화면 코드와 로직 코드를 분리해서 유지보수에 초점을 두는 활용이 현실 세계에서는 가장 맞는 것 같습니다.

좋아요 1