WPF Prism 연구

WPF Prism 연구


WPF 프레임워크 중 가장 대중적인 Prism에 대해 자세히 알아보고자 합니다. 그래서 본 시리즈(Slog)에서는 WPF Prism의 구조를 연구/분석하고 또 이와 관련된 이론이나 개념을 연결 지어 확장해 알아나가 보고자 합니다.

prism-library-logo

완료된 연구

13개의 좋아요

1. Prism-Sample-Wpf


Prism 기술을 빠르게 검토하고 도입하기 위한 훌륭한 방법이 여기 있습니다.

PrismLibrary/Prism-Samples-Wpf
Brian Lagunas 님을 비롯한 여러 Prism 기여자들이 GitHub를 통해 제공하고 있는 훌륭한 샘플 소스코드 입니다.

이 샘플 프로젝트의 특징으로는 특히 Prism의 주요 기술들을 각각의 프로젝트로 나누어 제공하고 있기 때문에 스텝을 순서대로 하나씩 밟아 나아가기에 안성맞춤입니다.

샘플 목록


(숫자 넘버링이 이상하지만 실제 프로젝트 이름과 동일하며 갯수는 총 29개 입니다.)

9개의 좋아요

Prism GitHub Repository


PrismLibrary organization에서 공식으로 제공하는 Prism은 WPF, Xamarin Froms, Uno 등의 플렛폼을 제공하며 핵심 공용 부분은 .NET Standard 2.0를 통해 구현되어 있으며, 그 외 플랫폼에 특화된 기능은 각각 구현되어 있습니다.

Prism은 솔루션이 이렇게 구성되어있습니다.

image

그리고 Prism의 주요 기술인 IoC Container 기술은 별도의 패키징으로 분리되어 있습니다. (WPF 기준)

  • Prism.DryIoc
  • Prism.Unity

(7.0 버전부터 IoC 관련 부분이 분리되어있다고 합니다.)

GitHub 공식 Repository

그리고 1.5k forks를 자랑하는 이 레포지토리는 확실히 프레임워크 연구/분석에 있어서 아주 핫하고 귀한 오픈소스라는 생각이 들어요. :slight_smile:

5개의 좋아요

요즘 제일 기대 되는 슬로그입니다. 감사합니다.

4개의 좋아요

UnitTest: Prism.Core.dll


Prism 프레임워크는 Unit Test를 어떻게 했을까요? 내부적으로 유닛테스트를 어떻게 구성했는지를 알게 되면 단위 기능들을 역으로 분석하고 순 기능의 의도를 이해하고 파악하는데 도움이 될 수 있습니다. 특히 프레임워크를 만들고자 하거나 유닛테스트를 도입하고자 한다면 좋은 레퍼런스가 될 수도 있을 것입니다. :smile:

사실 Prism은 다양한 여러 개의 유닛테스트가 존재 하지만 그 중에서도 Prism.Core에 대한 유닛테스트를 살펴보겠습니다. 먼저 PrismLibrary_Core.sln 솔루션을 살펴보면 두개의 프로젝트를 포함하고 있는데 항목은 아래와 같습니다.

단 프로젝트의 물리적인 위치가 각각 다르기 때문에 아래 링크를 통해 위치를 확인해보시기 바랍니다.

여기서 Prism.Core.Tests는 Prism.Core.dll 어셈블리의 유닛테스트를 담당하는 프로젝트입니다. 먼저 프로젝트 폴더 구성을 한번 살펴보겠습니다.

유닛테스트 폴더 구조…

Prism.Core.Tests
  - Commands
  - Common
  - Events
  - Extensions
  - Ioc
  - Mocks
  - Mvvm

여기서 Prism.Core 프로젝트의 폴더 구조와 비교해보면 매우 유사하게 구성되어 있는 것을 확인할 수 있습니다. 또한 유닛 테스트가 Core의 어떤 부분을 테스트 했는지도 직관적으로 쉽게 파악할 수 있는 구조이며 그렇기 때문에 역으로 분석해 나가기에도 좋습니다.

Prism.Core 구조…

image

그럼 이제 실제 구현되어있는 유닛테스트 현장을 한번 살펴보도록 하겠습니다.


유닛테스트 살펴보기


Events/EventAggregatorFixture.cs 파일에 구성된 어느 한 [Fact] 유닛테스트입니다.

using Prism.Events;
using Xunit;

namespace Prism.Tests.Events
{
    public class EventAggregatorFixture
    {
        [Fact]
        public void GetReturnsSingleInstancesOfSameEventType()
        {
            var eventAggregator = new EventAggregator();
            var instance1 = eventAggregator.GetEvent<MockEventBase>();
            var instance2 = eventAggregator.GetEvent<MockEventBase>();

            Assert.Same(instance2, instance1);
        }

        public class MockEventBase : EventBase { }
    }
}

Assert.same(instance2, instance1); 바로 이 구문에서 유추할 수 있듯이 .GetEvent<T> 를 호출할 때 내부적으로 최초 타입인 경우 인스턴스를 생성하고 주기를 지속적으로 관리하며 이후에 타입이 다시 들어올 경우 이미 생성된 인스턴스를 반환하도록 되어 있는 기능(Fact)을 쉽게 이해하고 확인할 수 있습니다.

그러므로 Assert.Same 결과를 통해 ,

아GetEvent(); 처리가 내부적으로 typeof(T)를 Key로 하는 Dictionary를 사용하겠구나…

이런 식으로 의도를 알아가고 기능 처리 방식 등을 유추하거나 또 직접적인 확인하기까지 손쉽게 가능합니다.
실제 GetEvent 소스코드도 한번 살펴볼까요? 여기

준비된 유닛테스트를 잘 활용한다면 재미있는 연구 방법이 될 것 같습니다. 또 리뷰 하면 좋을 것 같은 부분이 있다면 가끔씩 내용을 추가해나갈 예정입니다.

읽어주셔서 감사합니다.
:smile:

6개의 좋아요

Singleton Management


Prism은 IContainerRegistry를 통해 Singleton 인스턴스 객체를 지속 관리할 수 있습니다. IContainerRegistry는 기본적으로 PrismApplication 클래스로 부터 강제 구현되기 때문에 어려움 없이 사용할 수 있습니다. (abstract)

override 타입인 RegisterTypes 메서드를 통해 IContainerRegister를 사용할 수 있습니다.

internal class App : PrismApplication
{
    ...
    protected override void RegisterTypes(IContainerRegistry containerRegistry)
    {
        // 여기!!
    }
}

허나! 기존 Application을 유지하면서 PrismBootstrapper를 사용할 경우도 있겠죠. 이 경우 PrismBootstrapper 에서도 또한 RegisterTypes 메서드를 override할 수 있으므로 동일하다 생각하면 됩니다. 크게 다를 것이 없기에, 이 부분은 나중에 좀 더 다뤄보도록 하겠습니다.

IContainerRegistry

이제 Singleton 인스턴스를 관리하기 위해 해당 객체를 등록해야 합니다. 이 때 사용되는 메서드가 바로 RegisterSingleton 입니다. 그럼 RegisterSingleton 메서드를 어떻게 사용할 수 있는지 한번 살펴보겠습니다.


바로생성하기

Application 생성 과정에서 바로 인스턴스를 생성하는 경우입니다. 실제 new() 생성이 발생하기 때문에 Singleton을 등록하는 과정에서 굳이 생성할 필요가 없거나 하지 말아야 하는 경우를 잘 판단해 보는 것이 좋습니다.

containerRegistry.RegisterSingleton(new AvatarView());

아마도 변수가 필요한 생성자 이거나 무언가 셋팅이 필요하거나, 여러 시점 상의 이유가 있을 수 있겠죠.
그리고 인스턴스를 문자열로 접근할 때 원하는 이름으로 찾을 수 있도록 부여하는 것도 가능합니다. (overriding)

containerRegistry.RegisterSingleton(new BlogView(), "BlogPage");
containerRegistry.RegisterSingleton<AvatarView>(new AvatarView(), "BlogPage");

두번째 줄은 <T> 타입 단순화가 가능하죠!

사실 기본적으로 이름을 작성하지 않는다면 객체 이름과 동일한 AvatarView 문자열이 부여되지만, 이렇게 인스턴스명과 다르게 등록할 수도 있겠습니다. 아마도 보통 접두 단어를 붙이거나 할 때 사용되겠죠.

타입등록하기

바로 생성하는 방법과는 달리 Singleton 인스턴스를 등록하는 과정에서 객체를 생성할 필요가 없거나 ViewModel 의존성 주입, Resolve, Region 등에서 호출이 될 때 생성되어야 할 경우에는 바로 이 방법에 해당하게 됩니다. 물론 시점 상 더 다양한 경우가 있겠습니다. :joy:

container.Registry.RegisterSingleton(typeof(BlogView));

앞서 인스턴스를 생성하는 것과는 달리 <T> 타입을 넘겨줌으로써 먼 훗날… BlogView를 찾는 상황이 생길 때 Singleton 관리 객체가 생성될 것입니다. 그리고 이번에도 마찬가지로 name을 원하는 문자열로 부여할 수 있습니다.

container.Registry.RegisterSingleton(typeof(BlogView), "BlogViewPage");

이렇게 하면 Type을 통해 Singleton 객체를 등록만 해서 어디선가 사용되기를 기다리기만 하면 됩니다.

Singleton 사용

그럼 이제 ViewModel이나 Resolve<T>를 통해 등록된 Singleton 객체를 가져와볼까요?


ViewModel에서 IoC Container 주입을 통한 사용방법입니다.

public class JamesViewModel(BlogView blogView)
{

}

Resolve를 통한 사용 방법도 크게 다르지 않습니다.

BlogView blogView = _container.Resolve<BlogView>("BlogViewPage");
BlogView galleryView = _container.Resolve<GalleryView>();
BlogView aboutView = _container.Resolve<AboutView>();

Region에 추가하거나 목적에 맞게 다양하게 활용하겠습니다. (Singleton 관리가 필요한 View인 경우 재미있게 응용할 수 있죠.)

Interface 활용

사실 Singleton을 사용하기 위해서는 등록에 사용된 객체 타입을 알고 있어야만 하는 구조적인 불편함이 생깁니다. 프로젝트 구조상, 별 상관없는 경우도 있겠지만 복잡하게 여러 프로젝트를 참조해야 하는 경우에는 이 활용이 필요합니다.


기존 Singleton 방식과 거의 동일 하면서 대표 인터페이스(또는 클래스)를 정의할 수 있습니다.

container.Registry.RegisterSingleton<IViewable, BlogView>("Blog");
container.Registry.RegisterSingleton<IViewable, GalleryView>("Gallery");
container.Registry.RegisterSingleton<IViewable, AboutView>("About");

이렇게 인터페이스 또는 상위 상속클래스를 대표 타입으로 정의한다면 아래처럼 어떠한 참조 없이도 대표 타입을 통해 객체를 가져올 수 있습니다.

_container.Resolve<IViewable>("Blog");
_container.Resolve<IViewable>("Gallery");
_container.Resolve<IViewable>("About");

이를 통해 프로젝트간 참조 관계와 상관 없이도 Singleton을 편하게 사용할 수 있습니다.

읽어주셔서 감사합니다. :smile:

4개의 좋아요

AutoWireViewModelChanged


안녕하세요. 이재웅입니다.

이 긴 이름을 가진 메서드는 ViewModelLocationProvider를 통해 제공됩니다. 그리고 이름에서 알 수 있듯이 Prism 설정에 의해 View가 생성될 때 대상 ViewModel을 Injection 주입을 통해 자동으로 생성하고 그 결과를 콜백 메서드로 반환하는 기능을 제공하고 있습니다. 또한 콜백 메서드는 object 타입의 View와 object 타입의 ViewModel을 반환합니다.

Prism에서는 ViewModelLocationProvider 클래스를 통해 한 가지 유용한 이벤트(콜백함수 호출 방식)를 제공합니다.

사용 방법은 심플합니다. 먼저 이를 간단하게 테스트 하기 위해 간단한 UI 클래스를 만들었습니다. 이 클래스를 만든 이유는 모든 View에 공통으로 적용하기 위함입니다.

public class SmartView : ContentControl 
{
}

그 다음 ViewModelLocationProvider를 통한 AutoWireViewModelChanged 메서드와 콜백 메서드를 구현해보도록 하겠습니다.

public class SmartView : ContentControl
{
    public SmartView()
    {
        ViewModelLocationProvider
            .AutoWireViewModelChanged(this, AutoWireViewModelChanged);
    }
    private void AutoWireViewModelChanged(object view, object dataContext)
    {
        // Prism을 통해 생성된 Injection 주입된 ViewModel이 생성될 때 마다
        // 이 콜백 메서드가 호출됩니다!
    }
}

이 기능을 통해 Prism View와 ViewModel의 생성과 흐름을 좀 더 명확히 인지하고, View와 ViewModel이 모두 생성된 이 시점에서 다양한 확장 응용 작업을 할 수 있는 타이밍을 확보할 수 있습니다.

예를 들어 인터페이스를 활용한 이런 작업도 간단하게 가능하겠죠? (예를 들어!!!, 여러분의 MVVM 규칙에 맞게…)

public class SmartView : ContentControl
{
    public SmartView()
    {
        ViewModelLocationProvider
            .AutoWireViewModelChanged(this, AutoWireViewModelChanged);
    }
    private void AutoWireViewModelChanged(object view, object dataContext)
    {
        if (dataContext is IViewCreatable ui) 
        {
            ui.OnInitialized();
        }
    } 
}

그 밖에도 프로젝트 구조나 특성에 맞게 다양한 목적과 의도로 AutoWireViewModelChanged 메서드를 를 활용할 수 있을 것입니다.

제가 준비한 내용은 여기까지 입니다.
읽어주셔서 감사합니다. :smile:

3개의 좋아요

완료 시점 이후에 샘플들이 추가 된 것 같아요!
추가된 샘플 추가합니다

추가 목록


4개의 좋아요