티스토리 뷰

.NET 5, 6, 7

MVVM Toolkit 사용 가이드

Connor Park 2020. 12. 6. 16:54
반응형

MVVM Toolkit nuget package를 이용하는 방법에 대해서 알아보도록 하겠습니다.

우선 MVVM-Samples 앱을 실행해서 같이 보시면 더 도움이 될 것입니다.

 

windows-toolkit/MVVM-Samples: Sample repo for MVVM package (github.com)

 

windows-toolkit/MVVM-Samples

Sample repo for MVVM package. Contribute to windows-toolkit/MVVM-Samples development by creating an account on GitHub.

github.com

프로젝트에 추가 가능한 NuGet package 버전은 Preview 4입니다. (2020/12/01)

Preview버전이기 때문에 NuGet package 검색전에 Include prerelease를 체크하시고 microsoft.toolkit.mvvm으로 검색하시면 됩니다.

 

ObservableObject

INotifyPropertyChanged 및 INotifyPropertyChanging 인터페이스를 구현 기본 클래스입니다. 속성 변경 알림을 지원해야하는 모든 객체에 대한 시작점으로 사용할 수 있습니다.

 

How it works

  • INotifyPropertyChanged와 INotifyPropertyChanging에 대한 기본 구현을 제공하기 때문에 PropertyChanged 및 PropertyChanging 이벤트를 노출합니다.
    • - PropertyChanged는 속성 값이 변경된 후에 발생되는 이벤트이며, PropertyChanging는 속성 값이 변경되기 전에 발생되는 이벤트입니다. 일반적으로는 PropertyChanged 이벤트만 사용하여도 개발이 가능하지만, 가끔은 속성 값이 변경되기 전 이벤트도 사용이 필요합니다.
  • ObservableObject에서 상속된 속성의 값을 쉽게 설정하고 적절한 이벤트를 자동으로 발생시키는데 사용할 수 있는 몇가지 SetProperty메서드를 제공합니다.
  • SetProperty와 유사하지만 Task 속성을 설정하고 할당된 Task가 완료되면 알림 이벤트를 자동으로 발생시키는 기능이 있는, SetPropertyAndNotifyOnCompletion 메서드를 제공합니다.
  • 알림 이벤트 발생 방법을 사용자가 임으로 정의할 수 있도록 overridden 가능한 OnPropertyChanged 및 OnPropertyChanging 메서드를 노출합니다.

Simple property

public class User : ObservableObject
{
    private string name;

    public string Name
    {
        get => name;
        set => SetProperty(ref name, value);
    }
}

Wrapping a non-observable model

예를 들어 데이터베이스 아이템으로 작업을 할 때 일반적인 시나리오는 데이터베이스 모델의 속성을 중계하는 래핑 "바인딩 가능" 모델을 만들고, 필요할 때 속성 변경 알림을 발생시키는 것 입니다. 아래 예에서 User 모델은 ObservableObject를 상속받지 않은 데이터베이스 테이블에 매핑되어 있으며, 이 모델의 속성 중 Name이 변경되면, 변경 알림을 발생시킵니다.

public class ObservableUser : ObservableObject
{
    private readonly User user;

    public ObservableUser(User user) => this.user = user;

    public string Name
    {
        get => user.Name;
        set => SetProperty(() => user.Name, value);
    }
}

Handling Task<T> properties

속성이 Task인 경우 작업이 완료되면 알림을 발생시켜 바인딩이 적시에 업데이트 되도록 할 수 있습니다.

public class MyModel : ObservableObject
{
    private Task<int> requestTask;

    public Task<int> RequestTask
    {
        get => requestTask;
        set => SetPropertyAndNotifyOnCompletion(ref requestTask, () => requestTask, value);
    }

    public void RequestValue()
    {
        RequestTask = WebService.LoadMyValueAsync();
    }
}

 

Commands

RelayCommand and RelayCommand<T>

RelayCommand와 RelayCommand<T>는 메서드나 대리자를 뷰에 노출 시킬 수 있는 ICommand 구현입니다.

How they work

  • ICommand 인터페이스의 기본 구현 클래스입니다.
  • CanExecuteChanged 이벤트를 발생시키기 위해 NotifyCanExecuteChanged 메서드를 노출하는 IRelayCommand (IRelayCommand<T>) 인터페이스를 구현 했습니다.
  • 표준 메서드 및 람다식의 래핑을 허용하는 Action 및 Func<T>와 같은 대리자를 사용하는 생성자를 사용할 수 있습니다.

Working with ICommand

아래 샘플은 RelayCommand를 사용하여 뷰모델에서 메서드를 추상화하는 간단한 명령을 설정하는 방법을 보여줍니다. ObservableObject에서 상속된 SetProperty 메소드를 통해 변경 알림을 발생시키는 속성도 있습니다. UI에는 뷰모델의 ICommand에 바인딩 된 Button 컨트롤과 Counter 속성의 값을 표시하는 TextBlock이 있습니다.

public class MyViewModel : ObservableObject
{
    public MyViewModel()
    {
        IncrementCounterCommand = new RelayCommand(IncrementCounter);
    }

    /// <summary>
    /// Gets the <see cref="ICommand"/> responsible for incrementing <see cref="Counter"/>.
    /// </summary>
    public ICommand IncrementCounterCommand { get; }

    private int counter;

    /// <summary>
    /// Gets the current value of the counter.
    /// </summary>
    public int Counter
    {
        get => counter;
        private set => SetProperty(ref counter, value);
    }

    /// <summary>
    /// Increments <see cref="Counter"/>.
    /// </summary>
    private void IncrementCounter() => Counter++;
}
<Page
    x:Class="MyApp.Views.MyPage"
    xmlns:viewModels="using:MyApp.ViewModels">
    <Page.DataContext>
        <viewModels:MyViewModel x:Name="ViewModel"/>
    </Page.DataContext>

    <StackPanel Spacing="8">
        <TextBlock Text="{x:Bind ViewModel.Counter, Mode=OneWay}"/>
        <Button
            Content="Click me!"
            Command="{x:Bind ViewModel.IncrementCounterCommand}"/>
    </StackPanel>
</Page>

AsyncRelayCommand and AsyncRelayCommand<T>

AsyncRelayCommand와 AsyncRelayCommand<T>는 비동기 작업을 지원하는 ICommand 구현입니다.

How they work

  • 작업 반환 대리자를 지원하여 라이브러리에 포함 된 비동기 명령의 기능을 확장합니다.
  • 보류중인 작업의 진행 상황을 모니터링하는 데 사용할 수 있는 ExecutionTask 속성과 작업 완료시기를 확인하는 데 사용할 수 있는 IsRunning을 노출합니다. 이 것은 로딩 표시기와 같은 UI요소에 명령을 바인딩하는 데 특히 유용합니다.
  • 이들은 IAsyncRelayCommand 및 IAsyncRelayCommand<T> 인터페이스를 구현했으며, ViewModel은 이러한 인터페이스를 사용하여 명령을 쉽게 노출하여 형식 간의 긴밀한 결합을 줄일 수 있습니다. 예를 들어, 필요한 경우 동일한 공용 API 표면을 노출하는 사용자 지정 구현으로 명령을 쉽게 대체 할 수 있습니다.

Working with asynchronous commands

RelayCommand 샘플에 설명된 시나리오와 유사하지만 비동기 작업을 실행하는 명령을 가정 해 보겠습니다.

버튼을 클릭하면 명령이 호출되고 ExecutionTask가 업데이트 됩니다. 작업이 완료되면 속성은 UI에 반영되는 알림을 발생시킵니다. 이 경우 작업 상태와 작업의 현재 결과가 모두 표시됩니다. 작업 결과를 표시하려면 TaskExtensions.GetResultOrDefault 메서드를 사용해야 합니다. 이 메서드는 스레드를 차단하지 않고(고리고 교착 상태를 유발할 수 있음) 아직 완료되지 않은 작업의 결과에 대한 액세서를 제공합니다.

public MyViewModel()
{
    DownloadTextCommand = new AsyncRelayCommand(DownloadTextAsync);
}

public IAsyncRelayCommand DownloadTextCommand { get; }

private async Task<string> DownloadTextAsync()
{
    await Task.Delay(3000); // Simulate a web request

    return "Hello world!";
}
<Page.Resources>
    <converters:TaskResultConverter x:Key="TaskResultConverter"/>
</Page.Resources>
<StackPanel Spacing="8">
    <TextBlock>
        <Run Text="Task status:"/>
        <Run Text="{x:Bind ViewModel.DownloadTextCommand.ExecutionTask.Status, Mode=OneWay}"/>
        <LineBreak/>
        <Run Text="Result:"/>
        <Run Text="{x:Bind ViewModel.DownloadTextCommand.ExecutionTask, Converter={StaticResource TaskResultConverter}, Mode=OneWay}"/>
    </TextBlock>
    <Button
        Content="Click me!"
        Command="{x:Bind ViewModel.DownloadTextCommand}"/>
    <muxc:ProgressRing
        HorizontalAlignment="Left"
        IsActive="{x:Bind ViewModel.DownloadTextCommand.IsRunning, Mode=OneWay}"/>
</StackPanel>

 

Messenger

메싲너 클래스(함께 제공되는 IMessenger 인터페이스 포함)를 사용하여 서로 다른 객체간에 메시지를 교환 할 수 있습니다. 이는 참조되는 유형에 대한 강력한 참조(strong references)를 유지할 필요없이 응용프로그램을 모듈로 분리하여 개발하는데 유용합니다. 토큰으로 고유하게 식별되는 특정 채널에 메시지를 보내고 응용 프로그램의 다른 섹션에 다른 메신저를 사용할 수도 있습니다.

 

How it works

Messenger 유형은 메시지 처리기를 사용하여 수신자(메시지 수신자)와 등록된 메시지 유형 간의 링크를 유지 관리합니다. 모든 객체는 메시지 처리기를 사용하여 주어진 메시지 유형에 대한 수신자로 등록 할 수 있으며, 이는 해당 유형의 메시지를 보내는 데 Messenger인스턴스를 사용할 때마다 호출됩니다. 또한 특정 통신 채널 (각각 고유한 토큰으로 식별 됨)을 통해 메시지를 보낼 수 있으므로 여러 모듈이 출돌을 일으키지 않고 동일한 유형의 메시지를 교환 할 수 있습니다. 토큰없이 보낸 메시지는 기본 공유 채널을 사용합니다.

 

메시지 등록을 수행하는 방법에는 IRecipient<TMessage> 인터페이스를 통하거나 메시지 처리기 역할을 하는 Action<TMessage> 대리자를 사용하는 두가지 방법이 있습니다. 첫번째는 RegisterAll 확장에 대한 단일 호출로 모든 핸들러를 등록 할 수 있게하여, 선언된 모든 메시지 핸들러의 수신자를 자동으로 등록하는 반면 두번째는 더 많은 유연성이 필요하거나 간단한 람다 표현식 메시지 핸들로러 사용하려는 경우에 유용합니다.

 

Ioc클래스와 유사하게 Messenger는 패키지에 내장 된 스레드로부터 안전한 구현을 제공하는 Default 속성을 노출합니다. 필요한 경우 메신저 인스턴스를 여러개 만들 수도 있습니다. 예를 들어 DI 서비스 공급자가 다른 메신저 인스턴스를 앱의 다른 모듈에 삽입하는 경우에 사용할 수 있습니다.

Sending messages

Sending and receiving messages

// Create a message
public class LoggedInUserChangedMessage : ValueChangedMessage<User>
{
    public LoggedInUserChangedMessage(User user) : base(user)
    {        
    }
}

// Register a message in some module
Messenger.Default.Register<LoggedInUserChangedMessage>(this, m =>
{
    // Handle the message here
});

// Send a message from some other module
Messenger.Default.Send(new LoggedInUserChangedMessage(user));

현재 로그인 한 사용자의 사용자 이름과 프로필 이미지가 포함된 헤더, 대화 목록이 포함 된 패널 및 현재 대화의 메시지가 포함 된 다른 패널(있는 경우)을 표시하는 간단한 메시징 응용프로그램에서 메시지 유형을 사용한다고 가정 해 보겠습니다.

이 시나리오에서 LoggedInUserChangedMessage 메시지는 로그인 작업이 완료된 후 HeaderViewModel에 의해 전송 될 수 있으며, 다른 뷰모델은 처리기를 등록하여 메시지를 수신하고 후속 처리를 진행할 수 있습니다.

 

Messenger 클래스는 등록된 모든 수신자에게 메시지를 전달합니다. 수신자는 특정 유형의 메시지를 구독 할 수 있습니다. 상속된 메시지 유형은 기본 Messenger 구현에 등록되지 않습니다. 수신자가 더 이상 필요하지 않은 경우 등록을 취소하여, 메시지 수신을 중지해야 합니다. 메시지 유형, 등록 토큰 또는 수신자별로 등록을 취소 할 수 있습니다.

// Unregisters the recipient from a message type
Messenger.Default.Unregister<LoggedInUserChangedMessage>(this);

// Unregister the recipient from all messages, across all channels
Messenger.Default.UnregisterAll(this);
주의! : Messenger 구현은 강력한 참조를 사용하여 등록된 수신자를 추적합니다. 이는 성능상의 이유로 수행되며 메모리 누수를 방지하기 위해 등록된 각 수신자를 수동으로 등록 취소해야 함을 의미합니다. 즉, 수신자가 등록되어있는 한 사용중인 Messenger 인스턴스는 해당 인스턴스에 대한 활성 참조를 유지하므로 가비지 수집기가 해당 인스턴스를 수집 할 수 없습니다. 그래서, 이를 수동으로 등록 취소하거나 ObservableRecipient에서 상속을 받아서 사용하면 인스턴스가 비활성화되었을 때 등록을 자동으로 제거합니다.
public UserSenderViewModel SenderViewModel { get; } = new UserSenderViewModel();

public UserReceiverViewModel ReceiverViewModel { get; } = new UserReceiverViewModel();

// Simple viewmodel for a module sending a username message
public class UserSenderViewModel : ObservableRecipient
{
    private string username = "Bob";

    public string Username
    {
        get => username;
        private set => SetProperty(ref username, value);
    }

    public void SendUserMessage()
    {
        Username = Username == "Bob" ? "Alice" : "Bob";

        Messenger.Send(new UsernameChangedMessage(Username));
    }
}

// Simple viewmodel for a module receiving a username message
public class UserReceiverViewModel : ObservableRecipient
{
    private string username = "";

    public string Username
    {
        get => username;
        private set => SetProperty(ref username, value);
    }

    protected override void OnActivated()
    {
        Messenger.Register<UsernameChangedMessage>(this, m => Username = m.Value);
    }
}

// A sample message with a username value
public sealed class UsernameChangedMessage : ValueChangedMessage<string>
{
    public UsernameChangedMessage(string value) : base(value)
    {
    }
}
<StackPanel Spacing="8">

    <!--Sender module-->
    <Border BorderBrush="#40FFFFFF" BorderThickness="2" CornerRadius="4" Padding="8">
        <StackPanel Spacing="8">
            <TextBlock Text="{x:Bind ViewModel.SenderViewModel.Username, Mode=OneWay}"/>
            <Button
                Content="Click to send a message!"
                Click="{x:Bind ViewModel.SenderViewModel.SendUserMessage}"/>
        </StackPanel>
    </Border>

    <!--Receiver module-->
    <Border BorderBrush="#40FFFFFF" BorderThickness="2" CornerRadius="4" Padding="8">
        <StackPanel Spacing="8">
            <TextBlock Text="{x:Bind ViewModel.ReceiverViewModel.Username, Mode=OneWay}"/>
        </StackPanel>
    </Border>
</StackPanel>

Request messages

Using request messages

메신저 인스턴스의 또 다른 유용한 기능은 모듈에서 다른 모듈로 값을 요청하는 데 사용할 수도 있다는 것입니다. 이를 위해 패키지에서 사용할 수 있는 기본 RequestMessage<T> 클래스가 포함됩니다.

// Create a message
public class LoggedInUserRequestMessage : RequestMessage<User>
{
}

// Register the receiver in a module
Messenger.Default.Register<LoggedInUserRequestMessage>(this, m =>
{
    m.Reply(CurrentUser); // Assume this is a private member
});

// Request the value from another module
User user = Messenger.Default.Send<LoggedInUserRequestMessage>();

RequestMessage<T> 클래스에는 LoggedInUserRequestMessage에서 포함 된 User 객체로의 변환을 가능하게하는 암시적 변환기가 포함되어 있습니다. 또한 메시지에 대한 응답이 수신되었는지 확인하고, 그렇지 않은 경우 예외를 throw합니다. 이 필수 응답 보장없이 요청 메시지를 보낼 수도 있습니다. 반환 된 메시지를 로컬 변수에 저장 한 다음 응답값을 사용할 수 있는지 수동으로 확인합니다. 이렇게하면 Send메서드가 반활 될 때 응답이 수신되지 않으면 자동 예외가 트리거되지 않습니다.

동일한 네임 스페이스에는 AsyncRequestMessage<T>, CollectionRequestMessage<T> 및 AsyncCollectionRequestMessage<T>와 같은 다른 시나리오에 대한 기본 요청 메시지도 포함되어있습니다.

비동기 요청 메시지를 사용하는 방법은 다음과 같습니다.

// Create a message
public class LoggedInUserRequestMessage : AsyncRequestMessage<User>
{
}

// Register the receiver in a module
Messenger.Default.Register<LoggedInUserRequestMessage>(this, m =>
{
    m.Reply(GetCurrentUserAsync()); // We're replying with a Task<User>
});

// Request the value from another module (we can directly await on the request)
User user = await Messenger.Default.Send<LoggedInUserRequestMessage>();
// Simple viewmodel for a module responding to a request username message.
// Don't forget to set the IsActive property to true when this viewmodel is in use!
public class UserSenderViewModel : ObservableRecipient
{
    public string Username { get; private set; } = "Bob";

    protected override void OnActivated()
    {
        Messenger.Register<CurrentUsernameRequestMessage>(this, m => m.Reply(Username));
    }
}

private string username;

public string Username
{
    get => username;
    private set => SetProperty(ref username, value);
}

// Sends a message to request the current username, and updates the property
public void RequestCurrentUsername()
{
    Username = Messenger.Default.Send<CurrentUsernameRequestMessage>();
}

// Resets the current username
public void ResetCurrentUsername()
{
    Username = null;
}

// A sample request message to get the current username
public sealed class CurrentUsernameRequestMessage : RequestMessage<string>
{
}
<StackPanel Spacing="8">
    <TextBlock Text="{x:Bind ViewModel.Username, Mode=OneWay}"/>
    <Button
        Content="Click to request the username!"
        Click="{x:Bind ViewModel.RequestCurrentUsername}"/>
    <Button
        Content="Click to reset the local username!"
        Click="{x:Bind ViewModel.ResetCurrentUsername}"/>
</StackPanel>

 

Inversion of control(IoC)

Ioc (Inversion of control)

Ioc 클래스는 IServiceProvider의 사용을 용이하게하는 타입입니다. Microsoft.Extensions.DependencyInjection 패키지로 구동되며, 완전한 기능을 갖춘 강력한 DI API 세트를 제공하고 IServiceProvider를 쉽게 설정하고 사용할 수 있습니다.

Configure and resolve services

주요 진입점은 다음과 같이 사용할 수 있는 ConfigureServices 메서드입니다.

// Register the services at startup
Ioc.Default.ConfigureServices(services =>
{
    services.AddSingleton<IFilesService, FilesService>();
    services.AddSingleton<ISettingsService, SettingsService>();
    // Other services here...
});

// Retrieve a service instance when needed
IFilesService fileService = Ioc.Default.GetService<IFilesService>();

Ioc.Default 속성은 응용프로그램의 모든 위치에서 서비스를 확인하는데 사용할 수 있는 스레드로부터 안전한 IServiceProvide 인스턴스를 제공합니다. ConfigureService 메서드는 해당 서비스의 초기화를 처리합니다. 다른 Ioc인스턴스를 생성하고 각각 다른 서비스로 초기화 할 수도 있습니다.

Constructor injection

사용 가능한 강력한 기능 중 하나는 "생성자 주입(constructor injection)"입니다. 이는 DI 서비스 공급자가 요청되는 유형의 인스턴스를 만들 때 이미 등록된 서비스 간의 간접 종속성을 자동으로 해결할 수 있을을 의미합니다. 다음 코드를 참고합니다.

public class ConsoleLogger : ILogger
{
    private readonly IFileService FileService;
    private readonly IConsoleService ConsoleService;

    public ConsoleLogger(
        IFileService fileService,
        IConsoleService consoleService)
    {
        FileService = fileService;
        ConsoleService = consoleService;
    }

    // Methods for the IFileLogger interface here...
}

여기에는 ILogger 인터페이스를 구현하고 IFileService 및 IConsoleService 인스턴스가 필요한 ConsoleLogger가 있습니다. 생성자 주입은 DI 서비스 공급자가 인스턴스를 생성할 때 필요한 모든 서비스를 "자동으로" 주입한다는 것을 의미합니다.

// Register the services at startup
Ioc.Default.ConfigureServices(services =>
{
    services.AddSingleton<IFileService, FileService>();
    services.AddSingleton<IConsoleService, ConsoleService>();
    services.AddSingleton<ILogger, ConsoleLogger>();
});

// Retrieve a logger service with constructor injection
ILogger consoleLogger = Ioc.Default.GetService<ILogger>();

DI 서비스 공급자는 필요한 모든 서비스가 등록되었는지 자동으로 확인한 다음, 이를 검색하고 등록된 ILogger와 연결된 유형에 대한 생성자를 호출하여 반환할 인스턴스를 가져옵니다. 모두 자동으로 완료됩니다.

반응형

'.NET 5, 6, 7' 카테고리의 다른 글

The future of .NET Standard  (0) 2021.01.04
What’s new in Windows Forms runtime in .NET 5.0  (0) 2020.12.23
Introducing the MVVM Toolkit, a .NET Standard library  (0) 2020.12.03
.NET Conf 2020 Youtube  (0) 2020.11.19
Get started Blazor  (1) 2019.07.26
댓글
댓글쓰기 폼
반응형
Total
712,874
Today
7
Yesterday
311
«   2022/10   »
            1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31          
10-07 01:42
글 보관함