출처 : http://msdn.microsoft.com/ko-kr/magazine/cc785479.aspx

 

이 기사에서는 다음 내용에 대해 설명합니다.

  • 복합 응용 프로그램의 기본
  • 부트스트래퍼 및 모듈 초기화
  • 영역 및 RegionManager
  • 뷰, 명령 및 이벤트

이 기사에서 사용하는 기술:
WPF용 복합 응용 프로그램 지침

목차

문제: 구식 응용 프로그램
복합 응용 프로그램
복합 응용 프로그램 지침
부트스트래퍼와 컨테이너
모듈 초기화
부트스트래퍼 사용
모듈 및 서비스
영역 및 RegionManager
지역 범위를 가지는 영역

프레젠테이션의 분리
명령
이벤트
요약

WPF(Windows ® Presentation Foundation) 및 Silverlight™와 같은 기술은 풍부한 사용자 환경을 갖춘 응용 프로그램을 쉽고 빠르게 개발할 수 있는 간단하고 선언적인 방법을 제공합니다. 그러나 이러한 기술이 논리 계층에서 프레젠테이션 계층을 분리할 수 있도록 도와주는 것은 사실이지만 관리하기 쉬운 응용 프로그램을 개발하는 데 관련된 전통적인 문제까지 해결하지는 못합니다.

작은 프로젝트에서는 어느 정도의 경험을 갖춘 개발자 한 명이 응용 프로그램을 설계 및 개발하고 이를 어려움 없이 관리하고 확장하는 것이 가능합니다. 그러나 응용 프로그램이 복잡해지고 여러 개발자가 각자 맡은 기능을 개발하게 되면 프로젝트를 올바르게 제어하기가 매우 어려워집니다.

복합 응용 프로그램은 이 문제를 해결할 수 있는 방법입니다. 이 기사에서는 복합 응용 프로그램이 무엇인지 설명하고 WPF의 기능을 활용하는 복합 응용 프로그램을 작성하는 방법을 설명하겠습니다. 그리고 그 과정에서 Microsoft patterns & practices 팀에서 개발한 새로운 WPF용 복합 응용 프로그램 지침(이전 코드 이름 "Prism")을 소개하겠습니다.

문제: 구식 응용 프로그램

복합 응용 프로그램이 필요한 이유를 이해하는 데 도움이 되는 예를 살펴보겠습니다. Contoso Financial Investments라는 회사에서 사용자가 주식 투자 포트폴리오를 관리할 수 있는 응용 프로그램을 제공한다고 가정해 보겠습니다. 이 응용 프로그램을 사용하면 현재 투자 현황 및 관련 뉴스 항목을 볼 수 있으며, 조사 목록에 항목을 추가하고 매입/매도 트랜잭션을 수행할 수 있습니다.

사용자 컨트롤을 사용하여 기존 WPF 응용 프로그램으로 개발하는 경우에는 최상위 창에서 시작하여 앞서 언급한 다양한 기능을 위한 사용자 컨트롤을 추가하는 방법을 사용할 것입니다. 이 경우에 PositionGrid, PositionSummary, TrendLine 및 WatchList와 같은 사용자 컨트롤을 추가하면 됩니다(그림 1 참조). 각 사용자 컨트롤은 디자인 타임에 Expression Blend™와 같은 디자이너를 사용하거나 수동으로 XAML을 작성하여 기본 창에 배치됩니다.

그림 1 구식 응용 프로그램의 사용자 컨트롤 (더 크게 보려면 이미지를 클릭하십시오.)

그런 다음에는 RoutedEvent, RoutedCommand 및 데이터 바인딩을 사용하여 모든 항목을 연결할 수 있습니다. 이에 대한 자세한 내용은 이번 호에서 Brian Noyes의 기사 "WPF의 라우팅된 이벤트와 명령 이해"(msdn.microsoft.com/magazine/cc785480)를 참조하십시오. PositionGrid에는 선택을 처리하는 RoutedCommand가 연결되어 있으며 명령의 Execute 처리기에서는 위치가 선택될 때마다 TickerSymbolSelected 이벤트가 발생합니다. 선택된 주식 종목 코드에 따라 해당 콘텐츠를 렌더링하기 위해 TickerSymbolSelected 이벤트에는 TrendLine과 NewsReader가 연결됩니다.

이 경우에 응용 프로그램은 각 컨트롤과 밀접하게 결합됩니다. 다른 부분들을 조율하기 위한 상당히 많은 양의 논리가 UI에 포함되며 컨트롤 간에 상호 의존성도 있습니다.

이러한 의존성 때문에 다른 부분을 별도로 개발할 수 있는 형태로 응용 프로그램을 분리하기 까다롭습니다. 모든 사용자 컨트롤을 별도의 어셈블리로 분리하여 관리를 좀 더 용이하게 할 수 있지만 이렇게 하면 주 응용 프로그램의 문제를 컨트롤 어셈블리로 옮기는 것일 뿐입니다. 이 모델에서는 많은 부분을 변경하거나 새로운 기능을 도입하기가 어렵습니다.

다음은 여기에 두 가지 비즈니스 요구 사항을 추가하여 문제를 더욱 복잡하게 만들어 보겠습니다. 첫 번째는 두 번 클릭하면 선택한 펀드에 대한 개인 노트를 보여 주는 펀드 노트 화면을 추가하는 것이고 두 번째는 선택한 펀드와 관련된 하이퍼링크 목록을 표시하는 새로운 화면을 추가하는 것입니다. 촉박한 개발 일정 때문에 이러한 기능은 다른 팀에서 동시에 개발해야 합니다.

각 팀은 FundNotes와 FundLinks라는 이름으로 별도의 컨트롤을 개발할 것입니다. 같은 컨트롤 어셈블리에 컨트롤 두 개를 추가하려면 이를 모두 컨트롤 프로젝트에 추가해야 합니다. 더 중요한 것은 두 컨트롤을 기본 폼에 추가해야 한다는 것이며, 이것은 두 코드에 대한 변경 내용과 각 컨트롤의 XAML을 기본 폼과 함께 병합해야 한다는 것입니다. 이러한 종류의 작업은 상당히 까다로우며 특히 기존 응용 프로그램에 추가하는 경우에 더욱 그렇습니다.

이러한 모든 변경 내용을 기본 응용 프로그램에 적용하려면 어떻게 해야 할까요? 이 작업을 완료하려면 원본 컨트롤을 병합하는 데 상당히 많은 시간을 소비해야 할 수 있습니다. 변경 내용을 병합하는 데 실수가 생기거나 잘못된 부분을 덮어쓰면 응용 프로그램이 손상됩니다. 해결 방법은 응용 프로그램 디자인을 다시 고려하는 것입니다.

복합 응용 프로그램

복합 응용 프로그램은 런타임에 동적으로 발견되고 구성되는 느슨하게 연결된 모듈로 이루어집니다. 모듈은 시스템의 다양한 수직 조각을 나타내는 시각적 및 비시각적 구성 요소를 포함합니다(그림 2 참조). 시각적 구성 요소인 뷰는 응용 프로그램의 모든 콘텐츠의 호스트 역할을 수행하는 공통 셸의 일부가 됩니다. 복합은 이러한 모듈 수준 구성 요소를 한데 연결하는 서비스를 제공합니다. 모듈은 응용 프로그램의 특정한 기능과 연관된 추가 서비스를 제공할 수 있습니다.

그림 2 복합 응용 프로그램의 구성 요소 (더 크게 보려면 이미지를 클릭하십시오.)

전체적인 관점에서 보면 복합 응용 프로그램은 다른 뷰를 자식으로 포함하며 뷰의 재귀적 UI 구조를 기술하는 복합 뷰 디자인 패턴을 구현한 것입니다. 뷰는 디자인 타임에 정적으로 구성되는 것이 아니라 일반적으로 런타임에 메커니즘에 의해 구성됩니다.

이 패턴의 장점을 알아보기 위해 주문 입력 시스템에서 주문이 여러 건 있는 경우를 예로 들어 보겠습니다. 각 인스턴스는 헤더, 세부 정보, 배송 및 영수증을 표시하기 위해 상당히 복잡해질 수 있으며 시스템이 발전하면 추가적인 정보를 표시해야 할 수 있습니다. 가령 주문의 유형에 따라 주문의 일부가 다르게 표시되는 예를 생각해 볼 수 있습니다.

화면이 정적으로 구성된다면 주문의 다른 부분을 표시하기 위해 여러 조건부 논리를 사용해야 합니다. 게다가 새로운 기능을 추가하면 기존 논리에 문제가 생길 확률도 높습니다. 이를 복합 뷰로 구현한다면 필요한 조각으로만 동적으로 주문 화면을 구성할 수 있습니다. 즉, 주문 뷰 자체를 수정할 필요가 없으며 조건부 표시 논리 사용과 새로운 자식 화면 추가가 불필요합니다.

모듈은 셸이라고도 하는 기본 복합 뷰가 생성된 위치에서 뷰에 사용됩니다. 모듈은 서로를 직접 참조하지 않으며 셸을 직접 참조하지도 않습니다. 대신 서비스를 활용하여 서로 통신하고 셸과 통신하여 사용자 동작에 반응합니다.

모듈을 사용하여 시스템을 구성하면 몇 가지 장점이 있습니다. 일단 모듈은 같은 응용 프로그램 내에서 여러 백 엔드 시스템으로부터 데이터를 집계할 수 있습니다. 또한 시간이 흐름에 따라 시스템을 발전시키기가 더 쉽습니다. 시스템 요구 사항이 변경되면 모듈식이 아닌 시스템에 비해 훨씬 매끄럽게 새로운 모듈을 시스템에 추가할 수 있으며 기존 모듈 역시 독립적으로 발전시킬 수 있기 때문에 테스트하기도 수월합니다. 마지막으로 다른 팀에서 모듈을 개발, 테스트 및 유지 관리할 수 있습니다.

복합 응용 프로그램 지침

Microsoft patterns & practices 팀은 최근에 최신 버전의 WPF용 복합 응용 프로그램 지침(microsoft.com/CompositeWPF 참조)을 발표했습니다. 새로운 지침은 WPF의 기능과 프로그래밍 모델을 활용하도록 디자인되었으며 이와 동시에 팀에서는 내부 제품 팀, 고객 및 .NET 커뮤니티의 의견을 바탕으로 이전 복합 응용 프로그램 지침의 디자인을 개선했습니다.

WPF용 복합 응용 프로그램 지침에는 참조 구현(앞서 소개한 Stock Trader 응용 프로그램), CAL(Composite Application Library), 퀵 스타트 응용 프로그램, 그리고 디자인 및 기술 설명서가 포함되어 있습니다.

CAL은 복합 응용 프로그램을 작성하기 위한 서비스와 내부 기능을 제공합니다. 사용되는 복합 모델에서는 자체 서비스를 점차적으로 사용하거나 CAL 디자인 응용 프로그램의 일부로 함께 사용할 수 있습니다. 또한 CAL을 다시 컴파일하지 않고도 각 서비스를 쉽게 대체할 수 있습니다. 예를 들어 CAL에는 종속성 주입을 위해 Unity Application Block을 사용하지만 이를 여러분의 종속성 주입 서비스로 대체할 수 있도록 허용하는 확장 기능도 제공됩니다.

퀵 스타트는 CAL 구성 요소를 사용하는 방법을 설명하는 작고 집중적인 응용 프로그램을 제공하며 이러한 응용 프로그램을 활용하면 모르는 내용이 있더라도 주요 개념을 빠르게 익힐 수 있습니다.

이 기사의 나머지 부분에서는 Stock Trader 참조 구현에서 다루어진 몇 가지 기술적인 복합의 개념을 설명하겠습니다. 이 기사에서 다루는 모든 코드는 MSDN의 WPF용 복합 응용 프로그램 지침(msdn.microsoft.com/library/cc707819) 다운로드에 포함되어 있습니다.

부트스트래퍼와 컨테이너

CAL을 사용하여 복합 응용 프로그램을 작성할 때는 먼저 몇 가지 핵심 복합 서비스를 초기화해야 합니다. 여기에 필요한 것이 부트스트래퍼입니다. 그림 3에 나와 있는 것처럼 부트스트래퍼는 복합을 수행하는 데 필요한 모든 기능을 수행합니다. 부트스트래퍼는 여러 측면에서 CAL 응용 프로그램의 Main 메서드라고 할 수 있습니다.

그림 3 부트스트래퍼의 초기화 작업 (더 크게 보려면 이미지를 클릭하십시오.)

먼저 컨테이너가 초기화됩니다. 여기에서 컨테이너는 IoC(제어 반전)/DI(종속성 주입) 컨테이너를 의미합니다. 이 용어가 생소하다면 James Kovacs의 MSDN Magazine 기사, "응용 프로그램 유연성 향상을 위한 소프트웨어 종속성 제어"(msdn.microsoft.com/magazine/cc337885)를 읽어 보십시오.

컨테이너는 CAL 응용 프로그램에서 핵심적인 역할을 수행하며 복합에 사용되는 모든 응용 프로그램 서비스를 저장합니다. 또한 이러한 서비스를 필요한 위치로 주입하는 작업도 담당합니다. 기본적으로 CAL에는 patterns & practices 팀에서 제공하는 Unity 프레임워크를 컨테이너로 사용하는 추상 UnityBootstrapper가 포함되어 있습니다. 그러나 CAL은 Windsor, Structure Map 및 Sprint.NET과 같은 다른 컨테이너를 사용할 수도 있습니다. Unity 확장을 제외하고는 CAL의 어떤 클래스도 특정 컨테이너에 의존하지 않습니다.

컨테이너가 구성되는 동안 로거와 이벤트 집계를 포함한 복합에 사용되는 몇 가지 핵심 서비스가 자동으로 등록되며 기본 부트스트래퍼는 이러한 모든 항목을 재정의할 수 있도록 허용합니다. 예를 들어 자동으로 등록되는 서비스 중 하나로 IModuleLoader가 있습니다. 부트스트래퍼에서 ConfigureContainer 메서드를 재정의하면 여러분의 모듈 로더를 등록할 수 있습니다.

코드 복사

protected override void ConfigureContainer() {
  Container.RegisterType<IModuleLoader, MyModuleLoader>();
  base.ConfigureContainer();
}

서비스가 기본적으로 등록되기를 원하지 않는 경우에는 자동 등록을 해제할 수도 있습니다. 부트스트래퍼의 Run 메서드 오버로드를 호출하고 useDefaultConfiguration 매개 변수에 False 값을 전달하면 됩니다.

다음은 영역 어댑터가 구성됩니다. 영역은 UI에서 모듈이 UIElement를 주입할 수 있는 이름이 지정된 위치이며 일반적으로 패널과 같은 컨테이너입니다. 영역 어댑터는 액세스되는 여러 영역의 유형을 연결하는 작업을 처리합니다. 이러한 어댑터는 컨테이너의 RegionAdapterMappings 단일 인스턴스에서 매핑됩니다.

이 시점에 셸이 생성됩니다. 셸은 영역이 정의되는 최상위 창으로서 App.Xaml에서 선언되는 것이 아니라 사용자의 응용 프로그램별 부트스트래퍼에서 CreateShell 메서드에 의해 생성됩니다. 이것은 셸이 표시되기 전에 부트스트래퍼의 초기화를 완료하기 위한 것입니다.

다소 놀라울 수 있지만 응용 프로그램에는 셸이 없어도 됩니다. 예를 들어 기존 WPF 응용 프로그램에 일부 CAL 기능을 추가하려는 경우가 있습니다. CAL이 전체 화면을 제어하도록 하는 것이 아니라 최상위 영역이 될 패널을 추가할 수 있습니다. 이 경우에는 셸을 정의할 필요가 없습니다. 부트스트래퍼는 셸이 정의되지 않은 경우 간단히 셸 표시 과정을 무시합니다.

모듈 초기화

마지막으로 모듈이 초기화됩니다. CAL 응용 프로그램의 모듈은 복합 내의 분리 단위이며 별도의 어셈블리로 배포할 수 있지만 이것이 요구 사항은 아닙니다. CAL 응용 프로그램에서 모듈은 대부분의 기능이 포함되는 위치입니다.

모듈을 로딩하는 것은 IModuleEnumerator와 IModuleLoader를 사용하는 두 단계의 과정입니다. 열거자는 사용 가능한 모듈을 찾는 역할을 담당하며 모듈에 대한 메타 데이터를 포함하는 ModuleInfo 개체의 컬렉션 여러 개를 반환합니다. UnityBootstrapper는 올바른 열거자를 반환하기 위해 재정의해야 하는 GetModuleEnumerator를 포함하며 그렇지 않으면 런타임에 예외가 발생합니다. CAL은 디렉터리 검색과 구성에서 정적으로 모듈을 찾기 위한 열거자를 포함합니다.

로딩의 경우 CAL은 UnityBootstrapper에서 기본적으로 사용하는 ModuleLoader를 포함합니다. ModuleLoader는 아직 로드되지 않은 경우 각 모듈 어셈블리를 로드하고 이를 초기화합니다. 모듈은 다른 모듈에 대한 종속성을 지정할 수 있습니다. ModuleLoader는 종속성 트리를 작성하고 이러한 사양에 따라 올바른 순서로 모듈을 초기화합니다.

부트스트래퍼 사용

UnityBootstrapper는 추상 클래스이므로 StockTraderRIBootstrapper가 이를 재정의합니다(그림 4 참조). bootstrapper에는 여러분이 응용 프로그램별 기능을 추가할 수 있는 protected virtual 메서드가 여러 개 있습니다.

그림 4 Stock Trader 부트스트래퍼

코드 복사

public class StockTraderRIBootstrapper : UnityBootstrapper {
  private readonly EntLibLoggerAdapter _logger = new EntLibLoggerAdapter();

  protected override IModuleEnumerator GetModuleEnumerator()  {
    return new StaticModuleEnumerator()
    .AddModule(typeof(NewsModule))
    .AddModule(typeof(MarketModule))
    .AddModule(typeof(WatchModule), "MarketModule")
    .AddModule(typeof(PositionModule), "MarketModule", "NewsModule");
  }

  protected override ILoggerFacade LoggerFacade  {
    get { return _logger; }
  }

  protected override void ConfigureContainer()  {
    Container.RegisterType<IShellView, Shell>();

    base.ConfigureContainer();
  }

  protected override DependencyObject CreateShell()  {
    ShellPresenter presenter = Container.Resolve<ShellPresenter>();
    IShellView view = presenter.View;
    view.ShowView();
    return view as DependencyObject;
  }
}

처음 알 수 있는 부분은 _logger 변수에 EntlibLoggerAdapter가 정의 및 저장된다는 것입니다. 코드는 그런 다음 ILoggerFacade를 구현하는 이 로거를 반환하도록 LoggerFacade 속성을 재정의합니다. 이 경우에 필자는 Enterprise Library의 로거를 사용했지만 이를 손쉽게 여러분의 어댑터로 대체할 수 있습니다.

다음은 네 개의 참조 구현 모듈로 미리 채워진 StaticModuleEnumerator를 반환하도록 GetModuleEnumerator 메서드가 재정의됩니다. 참조 구현에서는 정적 모듈 로딩을 사용하지만 디렉터리 조회와 구성을 포함하여 모듈을 열거하는 여러 가지 방법이 있습니다. 다른 열거 메서드를 사용하려면 다른 열거자를 인스턴스화하도록 이 메서드를 변경하면 됩니다.

그런 다음에는 셸을 등록하도록 ConfigureContainer가 재정의됩니다. 이 시점에는 필요한 경우 추가 서비스를 프로그래밍 방식으로 등록할 수 있습니다. 마지막으로 셸을 만드는 특정한 논리로 CreateShell이 재정의됩니다. 이 경우 코드는 MVP(Model View Presenter) 패턴을 구현하므로 셸에 프리젠터가 연결됩니다.

그림 4에 있는 부트스트래퍼는 CAL 응용 프로그램을 처음부터 작성하는, 즉 응용 프로그램별 부트스트래퍼를 만드는 일반적인 패턴을 보여 줍니다. 이 방법의 가장 큰 장점은 응용 프로그램별 부트스트래퍼 덕분에 응용 프로그램을 테스트하기가 수월하다는 것입니다. 부트스트래퍼는 DependencyObject를 제외하고는 WPF에 대한 다른 종속성은 가지지 않습니다. 응용 프로그램별 부트스트래퍼에서 상속하는 테스트 부트스트래퍼를 만들고 AutoMocking 컨테이너를 반환하도록 CreateContainer 메서드를 재정의하여 모든 서비스를 흉내 낼 수 있습니다.

또한 부트스트래퍼는 복합 초기화를 위한 단일 진입 지점을 제공하며 CAL은 사용자의 응용 프로그램에서 프레임워크 클래스로부터의 상속에 의존하지 않으므로 이전 프레임워크에서와 같은 마찰 없이 CAL을 기존 응용 프로그램에 통합할 수 있습니다. CAL 자체는 부트스트래퍼에 의존하지 않으므로 부트스트래퍼가 여러분의 필요에 맞지 않는 경우에는 사용하지 않을 수 있습니다.

모듈 및 서비스

앞에서 언급한 것처럼 CAL을 사용하여 작성한 복합 응용 프로그램에서 응용 프로그램 논리의 상당 부분은 모듈에 배치됩니다. Stock Trader 참조 구현에는 네 개의 모듈이 포함되어 있습니다.

  • NewsModule은 선택된 각 펀드에 대한 뉴스 피드를 제공합니다.
  • MarketModule은 선택된 펀드에 대한 실시간 시장 데이터를 비롯한 경향 데이터를 제공합니다.
  • WatchModule은 사용자가 모니터링하는 펀드의 목록을 보여 주는 조사 목록을 제공합니다.
  • PositionModule은 사용자가 투자한 펀드의 목록을 표시하고 매입/매도 트랜잭션을 수행할 수 있도록 해 줍니다.

CAL에서 모듈은 IModule 인터페이스를 구현하는 클래스입니다. 이 인터페이스에는 Initialize라는 메서드 하나가 있습니다. 부트스트래퍼가 응용 프로그램의 Main 메서드에 해당한다면 Initialize 메서드는 각 모듈의 Main에 해당한다고 할 수 있습니다. 예를 들어 다음 코드에서 WatchModule의 Initialize 메서드를 볼 수 있습니다.

코드 복사

public void Initialize() {
  RegisterViewsAndServices();

  IWatchListPresentationModel watchListPresentationModel = 
    _container.Resolve<IWatchListPresentationModel>();
  _regionManager.Regions["WatchRegion"].Add(watchListPresentationModel.View);
  IAddWatchPresenter addWatchPresenter = 
    _container.Resolve<IAddWatchPresenter>();
  _regionManager.Regions["MainToolbarRegion"].Add(addWatchPresenter.View);
}

모듈에 대한 내용으로 진행하기에 앞서 _container 및 _regionManager에 대한 참조에 대해 이야기할 필요가 있습니다. 이러한 참조가 인터페이스에 정의되지 않는다면 어디에서 오는 것일까요? 이러한 종속성을 찾는 논리를 모듈 내에 하드코딩한 것일까요?

다행스럽게도 그렇지는 않습니다. 이 부분에서 사용할 수 있는 것이 IoC 컨테이너입니다. 모듈이 로드되면 지정된 모든 종속성을 모듈의 컨테이너로 주입하는 작업도 하는 컨테이너에서 모듈을 확인합니다.

코드 복사

public WatchModule(IUnityContainer container, 
  IRegionManager regionManager) {
  _container = container;
  _regionManager = regionManager;
}

여기에서 컨테이너 자체가 모듈로 주입되는 것을 볼 수 있습니다. 이것이 가능한 것은 부트스트래퍼가 자체 ConfigureContainer 메서드 내에서 컨테이너를 등록하기 때문입니다.

코드 복사

Container.RegisterInstance<IUnityContainer>(Container);

컨테이너에 대한 직접 액세스 권한이 모듈에 부여되므로 모듈은 컨테이너로부터의 종속성을 꼼꼼하게 등록 및 확인할 수 있습니다.

이 등록이 반드시 필요한 것은 아니며 모든 서비스를 전역 구성에 배치할 수도 있습니다. 이렇게 하면 컨테이너가 처음 생성될 때 모든 서비스가 등록되어야 합니다. 그러나 대부분의 모듈에는 모듈별 서비스가 있습니다. 그리고 모듈에서 등록을 처리하면 모듈에 로드된 경우에만 이러한 모듈별 서비스를 등록할 수 있습니다.

앞서 살펴본 모듈의 경우 첫 번째 호출은 RegisterViewsAndServices에 대해 수행됩니다. 이 메서드에서는 WatchModule에 대한 각각의 특정한 뷰가 인터페이스와 함께 컨테이너에서 등록됩니다.

코드 복사

protected void RegisterViewsAndServices() {
  _container.RegisterType<IWatchListService, WatchListService>(
    new ContainerControlledLifetimeManager());
  _container.RegisterType<IWatchListView, WatchListView>();
  _container.RegisterType<IWatchListPresentationModel, 
    WatchListPresentationModel>();
  _container.RegisterType<IAddWatchView, AddWatchView>();
  _container.RegisterType<IAddWatchPresenter, AddWatchPresenter>();
}

인터페이스를 지정하는 요구 사항을 적용하면 주요 항목의 분리를 촉진하고 시스템의 다른 모듈이 직접 참조 없이도 뷰와 상호 작용하도록 허용할 수 있습니다. 컨테이너에 모든 것을 배치하면 다른 개체에 대한 각각의 종속성이 자동으로 주입되도록 할 수 있습니다. 예를 들어 WatchListView는 코드에서 직접 인스턴스화되지 않으며 WatchListPresentationModel 생성자에서 종속성으로서 로드됩니다.

코드 복사

public WatchListPresentationModel(IWatchListView view...)

뷰 외에도 WatchModule은 목록 데이터를 포함하며 새 항목을 추가하는 데 사용되는 WatchListService도 등록합니다. 등록되는 세부적인 뷰는 조사 목록과 조사 목록 도구 모두입니다. 등록 후에는 영역 관리자가 사용되며 방금 등록된 두 개의 뷰가 WatchRegion과 ToolbarRegion에 추가됩니다.

영역 및 RegionManager

UI에 콘텐츠를 렌더링할 수 없는 모듈은 그다지 흥미롭지 않을 것입니다. 이전 섹션에서 Watch 모듈이 영역을 사용하여 두 개의 자체 뷰를 추가하는 것을 보았습니다. 영역을 사용하면 모듈이 UI에 대한 세부적인 참조를 가질 필요가 없으며 주입된 뷰가 어떻게 배치되고 표시되는지에 대한 정보도 필요 없습니다. 이러한 예로 그림 5에는 WatchModule이 주입되는 영역이 나와 있습니다.

그림 5 응용 프로그램으로 모듈 주입 (더 크게 보려면 이미지를 클릭하십시오.)

CAL에는 이러한 위치를 래핑하는 핸들이라고 할 수 있는 Region 클래스가 있습니다. Region 클래스에는 영역 내에 표시될 뷰의 읽기 전용 컬렉션인 Views 속성이 포함되어 있습니다. 뷰는 영역의 Add 메서드를 호출하여 영역에 추가할 수 있습니다. Views 속성은 개체의 제네릭 컬렉션을 포함하며 UIElement만 포함할 수 있는 것은 아닙니다. 이 컬렉션은 영역과 연결된 UIElement가 이에 바인딩하고 변경 내용을 감시할 수 있도록 INotifyPropertyCollectionChanged를 구현합니다.

Views 컬렉션이 UIElement 형식이 아닌 약한 형식인 이유가 궁금할 것입니다. WPF의 풍부한 템플릿 지원 덕분에 모델을 직접 영역에 추가할 수 있습니다. 그런 다음 이러한 모델에 모델의 렌더링을 정의하는 DataTemplate을 정의하고 연결할 수 있습니다. 추가된 항목이 UIElement이거나 사용자 컨트롤이면 WPF면 이를 그대로 렌더링합니다. 예를 들어 열린 주문의 탭인 영역이 있다면 사용자 지정 OrderView 사용자 컨트롤을 만들 필요 없이 OrderModel이나 OrderPresentationModel을 영역에 추가하고 사용자 지정 DataTemplate을 정의하여 표시를 제어할 수 있다는 것을 의미합니다.

영역을 등록하는 방법은 두 가지입니다. 첫 번째 방법은 UIElement에 연결된 속성 RegionName을 추가하여 XAML에서 정의하는 것입니다. 예를 들어 MainToolbarRegion을 정의하는 XAML은 다음과 같습니다.

코드 복사

<ItemsControl Grid.Row="1" Grid.Column="1" 
  x:Name="MainToolbar" 
  cal:RegionManager.RegionName="MainToolbarRegion">
  <ItemsControl.ItemsPanel>
    <ItemsPanelTemplate>
      <WrapPanel />
    </ItemsPanelTemplate>
  </ItemsControl.ItemsPanel>
</ItemsControl>

XAML을 통해 영역을 정의하면 부트스트래퍼가 등록하는 복합 서비스 중 하나인 RegionManager에 의해 런타임에 자동으로 영역이 등록됩니다. 근본적으로 RegionManager는 키는 영역의 이름이며 값은 IRegion 인터페이스의 인스턴스인 사전입니다. 연결된 속성 RegionManager는 이 인스턴스를 만들기 위해 RegionAdapter를 사용합니다.

연결된 속성을 사용하는 방법이 여의치 않거나 동적으로 추가 영역을 등록해야 하는 경우에는 수동으로 Region 클래스나 파생 클래스의 인스턴스를 만든 다음 이를 RegionManager의 Regions 컬렉션에 추가할 수 있습니다.

XAML 조각에서 MainToolbarRegion이 ItemsControl임을 알 수 있습니다. CAL은 부트스트래퍼로 등록할 수 있는 세 가지 영역 어댑터로 ContentControlRegionAdapter, ItemsControlRegionAdapter 및 SelectorRegionAdapter를 제공하며 어댑터를 등록하는 데는 RegionAdapterMappings 클래스를 사용합니다. 어댑터들은 모두 IRegionAdapter 인터페이스를 구현하는 RegionAdapterBase에서 상속됩니다.

그림 6에는 ItemsControlRegionAdapter 구현이 나와 있습니다. 어댑터가 구현되는 방법은 어댑터가 적용되는 UIElement 유형에 따라 완전히 달라집니다. ItemsControlRegionAdapter의 경우에는 해당 구현의 상당 부분이 Adapt 메서드에 있습니다. Adapt 메서드는 매개 변수 두 개를 받습니다. 첫 번째 매개 변수는 RegionManager가 생성하는 Region 클래스 자체의 인스턴스이며 두 번째 매개 변수는 영역을 나타내는 UIElement입니다. Adapt 메서드는 영역이 요소와 함께 작동할 수 있도록 하는 관련 내부 작업을 수행합니다.

그림 6 ItemsControlRegionAdapter

코드 복사

public class ItemsControlRegionAdapter : RegionAdapterBase<ItemsControl> {
  protected override void Adapt(IRegion region, ItemsControl regionTarget) {
    if (regionTarget.ItemsSource != null || 
      (BindingOperations.GetBinding(regionTarget, 
      ItemsControl.ItemsSourceProperty) != null))
      throw new InvalidOperationException(
        Resources.ItemsControlHasItemsSourceException);

    if (regionTarget.Items.Count > 0) {
      foreach (object childItem in regionTarget.Items) {
        region.Add(childItem);
      }
      regionTarget.Items.Clear();
    }
    regionTarget.ItemsSource = region.Views;
  }

  protected override IRegion CreateRegion() {
    return new AllActiveRegion();
  }
}

ItemsControl의 경우 어댑터는 자동으로 ItemControl 자체에서 모든 자식 항목을 제거하고 이를 영역에 추가합니다. 그런 다음 영역의 Views 컬렉션이 컨트롤의 ItemsSource에 바인딩됩니다.

재정의되는 두 번째 메서드는 새로운 AllActiveRegion 인스턴스를 반환하는 CreateRegion입니다. 영역은 활성이거나 비활성인 뷰를 포함할 수 있습니다. ItemsControl의 경우 선택의 개념이 없기 때문에 모든 해당 항목은 항상 활성입니다. 그러나 Selector와 같은 다른 유형의 영역의 경우에는 한 번에 한 항목만 선택될 수 있습니다. 뷰가 IActiveAware 인터페이스를 구현하면 선택된 해당 영역에서 알림을 받을 수 있습니다. 뷰가 선택될 때마다 해당 IsSelected 속성이 True로 설정됩니다.

복합 응용 프로그램을 개발하는 동안 영역을 추가로 만들거나 타사 공급업체의 컨트롤을 적용할 영역 어댑터를 만들어야 하는 경우가 있습니다. 새로운 영역 어댑터를 등록하려면 부트스트래퍼에서 ConfigureRegionAdapterMappings 메서드를 재정의해야 합니다. 그런 다음에는 다음과 비슷한 코드를 추가합니다.

코드 복사

protected override RegionAdapterMappings 
  ConfigureRegionAdapterMappings() {
  RegionAdapterMappings regionAdapterMappings = 
    base.ConfigureRegionAdapterMappings();

  regionAdapterMappings.RegisterMapping(typeof(Selector), 
    new MyWizBangRegionAdapter());

  return regionAdapterMappings;
}

영역이 정의되면 응용 프로그램 내의 임의의 클래스에서 RegionManager 서비스를 통해 이 영역에 액세스할 수 있습니다. CAL 응용 프로그램에서 이를 수행하는 일반적인 방법은 종속성 주입 컨테이너가 RegionManager를 필요로 하는 클래스의 생성자에 이를 주입하도록 하는 것입니다. 영역에 뷰나 모델을 추가하려면 간단히 영역의 Add 메서드를 호출하면 됩니다. 뷰를 추가할 때는 선택적인 이름을 전달할 수 있습니다.

코드 복사

_regionManager.Regions["MainRegion"].Add(
  somePresentationModel, "SomeView");

나중에 영역의 GetView 메서드에 이 이름을 사용하여 영역에서 뷰를 검색할 수 있습니다.

지역 범위를 가지는 영역

기본적으로 응용 프로그램에는 RegionManager 인스턴스가 하나만 있으므로 모든 영역이 전역 범위를 가집니다. 모든 영역이 전역 범위인 설정은 다양한 시나리오에 적용이 가능하지만 특정 범위에서만 존재하는 영역을 정의하려는 경우가 있습니다. 이러한 예로는 직원의 세부 정보를 보여 주는 뷰가 있는 응용 프로그램에서 뷰의 여러 인스턴스를 동시에 보여 주는 경우가 있습니다. 이러한 뷰는 상당히 복잡한 경우 소형 셸이나 CompositeView와 비슷하게 작동합니다. 이러한 경우에는 셸이 그러하듯이 각 뷰에 자체 영역이 있는 것이 좋습니다. CAL은 뷰의 지역 RegionManager를 정의하여 뷰 또는 해당 자식 뷰 내부에서 정의된 모든 영역이 자동으로 해당 지역 영역에 등록되도록 할 수 있습니다.

UI 복합 퀵 스타트에는 이 직원 시나리오에 대해 설명하는 지침에 포함되어 있습니다(그림 7 참조). 퀵 스타트에는 직원 목록이 있으며 각 직원을 클릭하면 연결된 직원 세부 정보가 표시됩니다. 직원을 선택할 때마다 해당 직원에 대한 EmployeeDetailsView가 생성되어 DetailsRegion(그림 8 참조)로 추가됩니다. 이 뷰에는 EmployeesController가 자체 OnEmployeeSelected 메서드에서 ProjectListView를 주입하는 로컬 TabRegion이 포함되어 있습니다.

그림 7 RegionManager를 통한 UI 복합 (더 크게 보려면 이미지를 클릭하십시오.)

그림 8 새로운 직원 뷰 만들기

코드 복사

public virtual void 
  OnEmployeeSelected(BusinessEntities.Employee employee) {
  IRegion detailsRegion = 
    regionManager.Regions[RegionNames.DetailsRegion];
  object existingView = detailsRegion.GetView(
    employee.EmployeeId.ToString(CultureInfo.InvariantCulture));

  if (existingView == null) {
    IProjectsListPresenter projectsListPresenter = 
    this.container.Resolve<IProjectsListPresenter>();
    projectsListPresenter.SetProjects(employee.EmployeeId);

    IEmployeesDetailsPresenter detailsPresenter = 
      this.container.Resolve<IEmployeesDetailsPresenter>();
    detailsPresenter.SetSelectedEmployee(employee);

    IRegionManager detailsRegionManager = 
      detailsRegion.Add(detailsPresenter.View,
      employee.EmployeeId.ToString(CultureInfo.InvariantCulture), true);

    IRegion region = detailsRegionManager.Regions[RegionNames.TabRegion];
    region.Add(projectsListPresenter.View, "CurrentProjectsView");
    detailsRegion.Activate(detailsPresenter.View);
  }
  else {
    detailsRegion.Activate(existingView);
  }
}

영역은 TabControl로 렌더링되며 정적 콘텐츠와 동적 콘텐츠가 모두 포함되어 있습니다. General과 Location 탭은 XAML에서 정적으로 정의됩니다. Current Projects 탭에는 자체 뷰가 주입됩니다.

코드를 보면 새로운 RegionManager 인스턴스가 detailsRegion.Add 메서드에서 반환되는 것을 알 수 있습니다. 또한 뷰의 이름을 전달하고 createRegionManagerScope 매개 변수를 True로 설정하는 Add의 오버로드를 사용하고 있다는 것도 확인할 수 있습니다. 이렇게 하면 자식에 정의된 임의의 영역에서 사용될 지역 RegionManager 인스턴스가 생성됩니다. TabRegion 자체는 EmployeeDetailsView의 XAML에 정의됩니다.

코드 복사

<TabControl AutomationProperties.AutomationId="DetailsTabControl" 
  cal:RegionManager.RegionName="{x:Static local:RegionNames.TabRegion}" .../>

지역 영역을 사용하면 인스턴스 영역을 사용하지 않더라도 추가적인 이점이 있습니다. 모듈이 자동으로 자체 영역을 환경의 나머지 부분에 공개하지 않도록 최상위 경계를 정의하는 데 이를 사용할 수 있다는 점입니다. 이를 위해서는 해당 모듈의 최상위 뷰를 영역에 추가하고 자체 범위를 가지도록 이를 지정하면 됩니다. 이렇게 하면 해당 모듈의 영역을 환경의 나머지 부분으로부터 보호할 수 있습니다. 액세스가 완전히 불가능한 것은 아니지만 훨씬 어렵습니다.

뷰가 없다면 복합도 필요 없을 것입니다. 뷰는 복합 응용 프로그램 내에서 작성하는 가장 중요한 단일 요소로서 여러분의 사용자를 응용 프로그램이 제공하는 환경으로 안내하는 게이트웨이 역할을 합니다.

뷰는 일반적으로 응용 프로그램의 화면입니다. 뷰는 다른 뷰를 포함하여 복합 뷰가 될 수 있습니다. 뷰의 다른 용도로는 메뉴와 도구 모음이 있습니다. Stock Trader를 예로 들면 OrdersToolbar는 Submit(전송), Cancel(취소), Submit All(모두 전송) 및 Cancel All(모두 취소) 단추를 포함하는 뷰입니다.

WPF에서는 기존의 Windows Forms 환경보다 뷰에 있어 훨씬 풍부한 개념을 지원합니다. Windows Forms에서는 기본적으로 시각적 표현에 컨트롤을 사용해야 하는 제한이 있었습니다. WPF에서도 이 모델이 여전히 지원되며 사용자 지정 사용자 컨트롤을 만들어 다른 화면을 나타낼 수 있습니다. Stock Trader 응용 프로그램을 보면 뷰를 정의하는 주요 메커니즘으로 이 모델이 사용되고 있음을 알 수 있습니다.

다른 방법은 모델을 사용하는 것입니다. WPF는 UI에 임의의 모델을 바인딩하고 DataTemplate을 사용하여 이를 렌더링하도록 허용합니다. 템플릿은 재귀적으로 렌더링되므로 템플릿이 모델의 속성에 바인딩된 요소를 렌더링하면 해당 속성은 사용이 가능한 경우 템플릿을 사용하여 렌더링됩니다.

작동 원리를 보여 주는 예로 다음 코드 샘플을 살펴보겠습니다. 이 샘플은 복합 퀵 스타트와 동일한 UI를 구현하지만 완전하게 모델과 DataTemplate을 사용합니다. 전체 프로젝트에 사용자 컨트롤이 전혀 없습니다. 그림 9에는 EmployeeDetailsView가 처리되는 방법이 나와 있습니다. 이제 뷰는 ResourceDictionary에 정의된 세 개의 DataTemplate으로 구성된 집합입니다. 모든 것은 EmployeeDetailsPresentationModel로 시작되며 해당 템플릿은 해당 항목이 TabControl로서 렌더링되어야 한다는 것을 선언합니다. 템플릿의 일부로서 TabControl의 ItemsSource를 EmployeeDetailsPresentationModel의 EmployeeDetails 컬렉션 속성으로 바인딩합니다. 이 컬렉션은 직원 세부 정보가 구성될 때 두 가지 정보로 채워집니다.

코드 복사

public EmployeesDetailsPresentationModel() {
  EmployeeDetails = new ObservableCollection<object>();
  EmployeeDetails.Insert(0, new HeaderedEmployeeData());
  EmployeeDetails.Insert(1, new EmployeeAddressMapUrl());
  ...
}

그림 9 EmployeeDetailsView 만들기

코드 복사

<ResourceDictionary 
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:EmployeesDetailsView=
    "clr-namespace:ViewModelComposition.Modules.Employees.Views.EmployeesDetailsView">

  <DataTemplate 
    DataType="{x:Type EmployeesDetailsView:HeaderedEmployeeData}">
    <Grid x:Name="GeneralGrid">
      <Grid.ColumnDefinitions>
        <ColumnDefinition></ColumnDefinition>
        <ColumnDefinition Width="5"></ColumnDefinition>
        <ColumnDefinition Width="*"></ColumnDefinition>
      </Grid.ColumnDefinitions>
      <Grid.RowDefinitions>
        <RowDefinition></RowDefinition>
        <RowDefinition></RowDefinition>
        <RowDefinition></RowDefinition>
        <RowDefinition></RowDefinition>
      </Grid.RowDefinitions>
      <TextBlock Text="First Name:" Grid.Column="0" Grid.Row="0">
      </TextBlock>
      <TextBlock Text="Last Name:" Grid.Column="2" Grid.Row="0">
      </TextBlock>
      <TextBlock Text="Phone:" Grid.Column="0" Grid.Row="2"></TextBlock>
      <TextBlock Text="Email:" Grid.Column="2" Grid.Row="2"></TextBlock>
      <TextBox x:Name="FirstNameTextBox" 
        Text="{Binding Path=Employee.FirstName}" 
        Grid.Column="0" Grid.Row="1"></TextBox>
      <TextBox x:Name="LastNameTextBox" 
        Text="{Binding Path=Employee.LastName}" 
        Grid.Column="2" Grid.Row="1"></TextBox>
      <TextBox x:Name="PhoneTextBox" Text="{Binding Path=Employee.Phone}" 
        Grid.Column="0" Grid.Row="3"></TextBox>
      <TextBox x:Name="EmailTextBox" Text="{Binding Path=Employee.Email}" 
        Grid.Column="2" Grid.Row="3"></TextBox>
    </Grid>
  </DataTemplate>

  <DataTemplate 
    DataType="{x:Type EmployeesDetailsView:EmployeeAddressMapUrl}">
    <Frame Source="{Binding AddressMapUrl}" Height="300"></Frame>
  </DataTemplate>

  <DataTemplate DataType="{x:Type
    EmployeesDetailsView:EmployeesDetailsPresentationModel}">
    <TabControl x:Name="DetailsTabControl" 
      ItemsSource="{Binding EmployeeDetails}" >
      <TabControl.ItemContainerStyle>
        <Style TargetType="{x:Type TabItem}" 
          BasedOn="{StaticResource RoundedTabItem}">
          <Setter Property="Header" Value="{Binding HeaderInfo}" />
        </Style>
      </TabControl.ItemContainerStyle>
    </TabControl>
  </DataTemplate>
</ResourceDictionary>

컬렉션의 각 항목을 위해 별도의 탭이 렌더링됩니다. 첫 번째 항목이 렌더링되는 동안 WPF는 HeaderedEmployeeData에 지정된 DataTemplate을 사용합니다. HeaderedEmployeeData 모델에는 직원 이름과 연락처 정보가 포함되어 있습니다. 연결된 템플릿은 정보를 표시하는 일련의 레이블로 모델을 렌더링합니다. 두 번째 항목은 EmployeeAddressMapUrl에 지정된 템플릿을 사용하여 렌더링되며 이 경우에는 직원이 거주하는 지역의 지도가 있는 웹 페이지를 포함하는 프레임을 렌더링합니다.

여러분이 이전에 아는 내용에 의하면 모델과 이에 해당하는 연결된 템플릿의 조합 동안에만 런타임에 뷰가 존재한다는 것을 의미하므로 이것은 상당한 패러다임의 변화입니다. Stock Trader에서 보여 주고 있는 것처럼 두 가지 방법을 혼합하여 내부에 컨트롤이 포함된 사용자 컨트롤을 사용하고 이를 템플릿을 통해 렌더링되는 모델에 바인딩할 수 있습니다.

프레젠테이션의 분리

이 기사 앞부분에서 필자는 복합 응용 프로그램의 장점 중 하나로 코드의 관리와 테스트가 더욱 쉬워진다는 점을 언급했습니다. 여러분의 뷰 내에 적용하여 이러한 장점을 실현할 수 있는 몇 가지 입증된 프레젠테이션 패턴이 있습니다. WPF용 복합 응용 프로그램 지침 전체에서 UI에 프레젠테이션 모델과 통제 컨트롤러라는 두 가지 패턴이 여러 차례 사용되는 것을 볼 수 있을 것입니다.

프레젠테이션 모델 패턴에서는 UI의 동작과 데이터가 모두 모델에 포함된다고 가정합니다. 그러면 뷰는 프레젠테이션 모델의 상태를 "유리판"으로 투사합니다.

내부적으로 모델은 비즈니스 및 도메인 모델과 상호 작용합니다. 모델에는 선택된 항목이나 요소 선택 여부와 같은 추가적인 상태 정보도 포함됩니다. 그런 다음 뷰는 프레젠테이션 모델에 직접 바인딩하고 이를 렌더링합니다(그림 10 참조). 데이터 바인딩, 템플릿 및 명령에 대한 WPF의 풍부한 지원 덕분에 프레젠테이션 모델 패턴은 개발을 위한 매력적인 옵션이라고 할 수 있습니다.

그림 10 프레젠테이션 모델 패턴 (더 크게 보려면 이미지를 클릭하십시오.)

Stock Trader 응용 프로그램은 위치 요약에서와 같이 현명하게 프레젠테이션 모델을 사용하고 있습니다.

코드 복사

public class PositionSummaryPresentationModel :
  IPositionSummaryPresentationModel, INotifyPropertyChanged {
  public PositionSummaryPresentationModel(
    IPositionSummaryView view,...) {
    ...
  }

  public IPositionSummaryView View { get; set; }
  public ObservableCollection<PositionSummaryItem> 
    PositionSummaryItems { 
    get; set; }
}

여기에서 PositionSummaryPresentationModel은 모든 변경 내용을 뷰에 알리기 위해 INotifyPropertyChanged를 구현합니다. 뷰 자체는 컨테이너로부터 PositionSummaryPresentationModel이 확인되는 동안 자체 IPositionSummaryView 인터페이스를 통해 생성자로 주입됩니다. 인터페이스는 단위 테스트에서 뷰의 모형을 만들 수 있도록 허용합니다. 프레젠테이션 모델에서는 PositionSummaryItems의 관찰 가능한 컬렉션을 공개합니다. 이러한 항목은 PostionSummaryView에 바인딩되고 렌더링됩니다.

통제 컨트롤러 패턴에는 그림 11에 나와 있는 것처럼 모델, 뷰 및 프리젠터가 존재합니다. 모델은 데이터이며 비즈니스 개체가 아닌 경우가 많습니다. 뷰는 모델이 직접 바딩인되는 UIElement입니다. 마지막으로 프리젠터는 UI 논리를 포함하는 클래스입니다. 이 패턴에서 뷰는 프리젠터로 위임하고 프리젠터의 콜백에 응답하여 컨트롤을 표시 또는 숨기는 것을 포함한 간단한 작업을 수행하는 것을 제외하면 매우 간단한 논리를 포함합니다.

그림 11 통제 컨트롤러 패턴 (더 크게 보려면 이미지를 클릭하십시오.)

통제 컨트롤러 패턴은 Stock Trader 응용 프로그램에서도 프레젠테이션 모델을 위해 몇 차례 사용되고 있습니다. 이러한 예로 추세선(Figure 12)이 있습니다. PositionSummaryPresentationModel과 비슷하게 TrendLinePresenter는 ITrendLineView 인터페이스를 통해 TrendLineView와 함께 주입됩니다. 프리젠터는 뷰가 해당 대리 논리를 통해 호출하는 OnTickerSymbolSelected 메서드를 공개합니다. 이 메서드에서 프리젠터는 뷰를 다시 호출하여 뷰의 UpdateLineChart와 SetChartTitle 메서드를 호출합니다.

그림 12 추세선 제공

코드 복사

public class TrendLinePresenter : ITrendLinePresenter {
  IMarketHistoryService _marketHistoryService;

  public TrendLinePresenter(ITrendLineView view, 
    IMarketHistoryService marketHistoryService) {
    this.View = view;
    this._marketHistoryService = marketHistoryService;
  }

  public ITrendLineView View { get; set; }

  public void OnTickerSymbolSelected(string tickerSymbol) {
    MarketHistoryCollection historyCollection = 
      _marketHistoryService.GetPriceHistory(tickerSymbol);
    View.UpdateLineChart(historyCollection);
    View.SetChartTitle(tickerSymbol);
  }
}

프레젠테이션 분리를 구현할 때 해결해야 하는 과제 중 하나로 뷰와 프레젠테이션 모델 또는 프리젠터 간의 통신이 있습니다. 이를 처리하는 방법은 몇 가지가 있습니다. 그 중 한 가지 방법은 자주 구현되는 방법 중 하나는 뷰의 이벤트 처리기가 프레젠테이션 모델이나 프리젠터를 직접 호출하거나 이에 대해 이벤트를 발생시키는 것입니다. 상태 변화나 사용 권한에 따라 프리젠터 호출을 시작하는 동일한 UIElement를 활성화 또는 비활성화해야 하는 경우가 많이 있습니다. 따라서 이러한 요소를 비활성화하기 위해 호출할 수 있는 메서드가 필요합니다.

다른 방법은 WPF 명령을 사용하는 것입니다. 명령을 사용하면 이러한 모든 상황을 앞뒤 논리를 사용하지 않고도 깔끔하게 해결할 수 있습니다. WPF의 요소는 명령에 바인딩하여 실행 논리 및 요소 활성화/비활성화를 모두 처리할 수 있습니다. UIElement가 명령에 바인딩되면 명령의 CanExecute 속성이 False일 때마다 자동으로 비활성화됩니다. 명령은 선언적으로 XAML에 바인딩될 수 있습니다.

WPF는 RoutedUICommands를 기본적으로 제공합니다. 이러한 명령을 사용하려면 뷰의 코드 숨김 내의 Execute와 CanExecute 메서드에 대한 처리기가 필요합니다. 즉, 앞뒤 통신을 위한 코드 수정이 여전히 필요하다는 것을 의미합니다. RoutedUICommands에는 수신기가 WPF의 논리적 트리에 있어야 한다는 다른 제약 조건이 있으며 이러한 제약 조건은 복합을 작성할 때 문제가 될 수 있습니다.

다행스럽게도 RoutedUICommands는 명령의 한 구현일 뿐입니다. WPF는 ICommand 인터페이스를 제공하며 이를 구현하는 모든 명령에 바인딩합니다. 즉, 여러분의 필요에 맞는 사용자 지정 명령을 만들 수 있으며 코드 숨김은 건드릴 필요가 없습니다. 단점은 SaveCommand, SubmitCommand 및 CancelCommand와 같은 사용자 지정 명령을 여러 곳에서 구현해야 한다는 것입니다.

CAL에는 생성자에서 Execute 및 CanExecute 메서드의 두 대리자를 지정할 수 있게 허용하는 DelegateCommand<T>와 같은 새로운 명령이 포함되어 있습니다. 이 명령을 사용하면 뷰 자체에 정의된 메서드를 통해 위임하거나 각 작업을 위한 사용자 지정 명령을 만들 필요 없이 뷰를 연결할 수 있습니다.

Stock Trader 응용 프로그램에서는 조사 목록을 포함한 여러 곳에 DelegateCommand가 사용되고 있습니다. WatchListService는 조사 목록에 항목을 추가하는 데 이 명령을 사용합니다.

코드 복사

public WatchListService(IMarketFeedService marketFeedService) {
  this.marketFeedService = marketFeedService;
  WatchItems = new ObservableCollection<string>();
  AddWatchCommand = new DelegateCommand<string>(AddWatch);
}

뷰와 프리젠터 또는 프레젠테이션 모델 간의 명령 라우팅 외에도 이벤트 게시와 같이 복합에서 처리해야 하는 다른 유형의 통신이 있습니다. 이러한 경우에 게시자는 구독자와 완전히 분리됩니다. 예를 들어 서버에서 알림을 수신하는 웹 서비스 끝점을 공개하는 모듈이 있을 수 있습니다. 알림을 수신한 다음에는 구독할 수 있는 같은 또는 다른 모듈 내의 구성 요소에 대해 이벤트를 발생시켜야 합니다.

CAL에는 이 기능을 지원하기 위해 컨테이너와 함께 등록되는 EventAggregator 서비스가 있습니다. 이벤트 집계 패턴의 구현인 이 서비스를 사용하면 게시자와 구독자가 느슨하게 연결된 방법으로 통신할 수 있습니다. EventAggregator 서비스에는 추상 EventBase 클래스의 인스턴스인 이벤트 리포지토리가 포함되어 있습니다. 서비스에는 이벤트 인스턴스를 검색하기 위한 GetEvent<TEventType> 메서드 하나가 있습니다.

CAL에는 EventBase에서 상속되며 WPF을 위한 세부 지원을 제공하는 CompositeWPFEvent<TPayload> 클래스가 포함되어 있습니다. 이 클래스는 게시를 위해 완전한 .NET 이벤트 대신 대리자를 사용합니다. 내부적으로는 약한 대리자로 작동하는 DelegateReference 클래스를 사용합니다. 약한 대리자에 대한 자세한 내용은 msdn.microsoft.com/library/ms404247을 참조하십시오. 약한 대리자를 사용하면 명시적으로 구독 취소하지 않은 경우에도 이를 가비지 수집할 수 있습니다.

CompositeWPFEvent 클래스에는 Publish, Subscribe 및 Unsubscribe 메서드가 있습니다. 이러한 각 메서드는 게시자가 올바른 매개 변수(TPayload)를 전달하고 Subscriber 속성이 이를 수신(Action<TPayload>)할 수 있도록 이벤트의 제네릭 형식 정보를 사용합니다. Subscribe 메서드에서는 PublisherThread, UIThread 또는 BackgroundThread로 설정할 수 있는 ThreadOption을 전달할 수 있습니다. 이 옵션은 구독 대리자가 호출되는 스레드를 결정합니다. 또한 Subscribe 메서드 오버로드 중에는 필터가 충족되는 경우에만 구독자가 이벤트에 대한 알림을 받도록 Predicate<T> 필터를 전달할 수 있는 것이 있습니다.

Stock Trader 응용 프로그램에서는 시세 화면에서 주식 종목 코드가 선택될 때마다 브로드캐스트하기 위해 EventAggregator가 사용되었습니다. News 모듈은 이 이벤트를 구독하고 해당 펀드에 대한 뉴스를 표시합니다. 이 기능은 다음과 같이 구현됩니다.

코드 복사

public class TickerSymbolSelectedEvent : 
  CompositeWpfEvent<string> {
}

먼저 StockTraderRI.Infrastructure 어셈블리에서 이벤트를 정의합니다. 이것은 모든 모듈이 참조하는 공유 어셈블리입니다.

코드 복사

public void Run() {
  this.regionManager.Regions["NewsRegion"].Add(
    articlePresentationModel.View);
  eventAggregator.GetEvent<TickerSymbolSelectedEvent>().Subscribe( 
    ShowNews, ThreadOption.UIThread);
}

public void ShowNews(string companySymbol) {
  articlePresentationModel.SetTickerSymbol(companySymbol);
}

News 모듈의 NewsController는 자신의 Run 메서드에서 이 이벤트를 구독합니다.

코드 복사

private void View_TickerSymbolSelected(object sender, 
  DataEventArgs<string> e) {
  _trendLinePresenter.OnTickerSymbolSelected(e.Value);

  EventAggregator.GetEvent<TickerSymbolSelectedEvent>().Publish(
    e.Value);
}

그러면 PositionSummaryPresentation 모델은 주식 종목 코드가 선택될 때마다 이벤트를 발생시킵니다.

요약

지침은 microsoft.com/compositewpf에서 다운로드할 수 있습니다. .NET Framework 3.5만 설치되어 있으면 코드를 실행할 수 있습니다.

지침에는 작업을 지원하는 도구가 포함되어 있습니다. 퀵 스타트는 복합을 작성하는 다양한 측면에 초점을 맞춘 이해하기 쉬운 샘플을 제공합니다. 참조 구현은 다양한 모든 측면을 활용하는 포괄적인 예를 제공합니다. 마지막으로 설명서에서는 세부적인 작업에 대한 방법 및 실습 모음을 통해 배경 정보를 제공합니다.

지침을 사용하는 동안 도움이 될 만한 의견이 있으면 CodePlex 포럼을 이용하거나 cafbk@microsoft.com으로 전자 메일을 보내 주십시오.

Glenn Block은 .NET Framework 4.0의 새로운 MEF(Managed Extensibility Framework)를 담당하고 있는 프로젝트 관리자입니다. MEF를 담당하기 전에는 patterns & practices 팀에서 Prism과 다른 클라이언트 지침을 담당하는 제품 기획자로 일했습니다. Glenn은 타고난 기술 전문가이며 업계 회의나 ALT.NET와 같은 그룹을 통해 전문 기술을 알리기 위해 노력하고 있습니다.

 

 

 

http://msdn.microsoft.com/ko-kr/magazine/cc785479.aspx

+ Recent posts