icrosoft .NET Framework 버전 1.0은 융통성이 뛰어난 디자인 타임 아키텍처를 제공하지만 실제로 디자이너를 만들고 호스트하는 코드가 거의 구현되어 있지 않았습니다. 모든 호스트 로직은 Visual Studio .NET에서 구현되어 타사에서 이 복잡한 로직을 모두 다시 작성해야 했습니다. 이제 모든 것이 바뀌었습니다. .NET Framework 2.0에는 디자이너를 호스트하는 데 사용할 수 있는 클래스 집합이 기본적으로 제공됩니다.
그림 1 런타임
.NET Framework 디자이너 작동 방식을 이해하려면 디자이너 사용 방법을 알아야 합니다. 디자이너는 디자인 타임에만 존재하는 개체이며, 일반적으로 런타임에 존재하는 개체에 연결됩니다. 프레임워크에서는 이 두 개체를 연결하고 디자인 타임 개체가 런타임 개체의 동작을 제어할 수 있는 통로를 제공합니다. 런타임에 양식과 양식에 있는 단추는 두 컨트롤 간의 모/자 관계를 통해서만 연결됩니다(그림 1). 이 두 컨트롤의 수명을 제어하는 다른 개체는 없습니다.
그림 2
디자인 타임에는 그림이 더 복잡합니다. 양식과 단추 모두 연결된 디자이너가 있습니다. 또한 두 개체 모두 상위 호스트 컨테이너(그림 2)에 연결되어 있습니다. 호스트 컨테이너는 개체와 디자이너에서 사용할 수 있는 서비스를 제공합니다. 이러한 서비스에는 디자인 타임에 하나 이상의 개체를 선택하는 선택 서비스, 메시지를 표시하고 도움말을 호출하며 개발 환경과 상호 작용하는 UI 서비스 등이 포함됩니다.
호스트 컨테이너는 많은 일을 담당합니다. 호스트 컨테이너는 구성 요소를 만들어 디자이너에 바인딩하고 구성 요소와 구성 요소를 유지하는 디자이너에 서비스를 제공합니다. 또한 일종의 지속적인 상태에서 디자이너를 로드하고 다시 해당 상태로 저장합니다. 호스트 컨테이너는 실행 취소, 클립보드 기능 등의 논리와 디자이너가 강력한 디자인 타임 환경을 제공하기 위해 의존하는 많은 서비스를 제공합니다.
그림 3 디자이너 호스트
호스트 컨테이너는 디자이너 상태를 유지할 수 있습니다. 이를 위해 디자이너 로더를 사용합니다. 계속해서 디자이너 로더는 직렬기(serializer)를 사용하여 구성 요소를 직렬화합니다. (그림 3 참조).
서비스의 확장성
.NET Framework 디자이너 아키텍처는 확장성이 있습니다. 이 확장성을 위해 서비스에서는 다양한 디자이너에서 사용할 수 있는 기능을 향상시키는 기능을 제공합니다. 서비스는 형식 기준으로 쿼리할 수 있는 개체입니다. 일반적으로 서비스를 나타내는 몇 가지 추상 클래스나 인터페이스를 정의한 다음 해당 서비스를 구현할 수 있습니다. 서비스 컨테이너라고 하는 개체를 통해 서비스를 추가하고 제거할 수 있습니다. 디자이너의 기본 호스트 인터페이스인 IDesignerHost가 서비스 컨테이너입니다. 서비스는 서로 다른 사용자가 작성한 구성 요소에서 공유할 수 있는 기능입니다. 이 때문에 서비스를 사용하고 만들 때 특정 규칙을 따라야 합니다.
서비스는 보장되지 않습니다. GetService 메서드를 호출하여 서비스를 요청할 때마다 항상 GetService에서 유효한 개체를 반환하는지 여부를 확인해야 합니다. 모든 플랫폼에서 모든 서비스를 사용할 수 있는 것은 아니며 이전에 사용한 서비스를 미래에는 사용할 수 없을 수도 있습니다. 따라서 서비스를 사용할 수 없게 될 경우에 해당 서비스가 필요한 기능을 사용하지 않도록 설정하여 쉽게 수정할 수 있는 우아한 코드를 작성해야 합니다.
서비스를 추가한 경우 디자이너가 제거될 때 서비스를 제거해야 합니다. 디자이너 호스트는 때때로 디자이너를 파괴하거나 다시 생성합니다. 서비스를 제거하지 못하면 이전 디자이너가 메모리에 남아 있게 됩니다.
DesignSurface 및 DesignSurfaceManager
.NET Framework 2.0에는 디자이너를 호스트하고 디자인에 서비스를 제공하는 데 사용할 수 있는 두 가지 클래스 DesignSurface 및 DesignSurfaceManager가 도입되었습니다. DesignSurface는 사용자가 디자이너로 인식하는 부분으로 사용자가 디자인 타임 기능을 변경하기 위해 조작하는 UI입니다. DesignSurface는 독립 실행형 디자이너로 사용하거나 다중 DesignSurface를 호스트하는 응용 프로그램을 구현할 수 있는 공용 구현을 제공하는 DesignSurfaceManager와 함께 사용할 수 있습니다.
DesignSurface는 기본적으로 여러 가지 디자인 타임 서비스를 제공합니다(그림 4 참조). 서비스 컨테이너에서 이러한 서비스를 대부분 재정의할 수 있습니다. 대체할 수 없는 서비스는 서로 종속되어 있기 때문에 바꾸지 못하게 되어 있습니다. IDisposable을 구현한 서비스 컨테이너에 추가된 모든 서비스는 디자인 화면이 제거될 때 제거됩니다.
DesignSurface는 기본 서비스와 함께 구성 요소의 사이트를 통해 사용할 수 있는 IDictionaryService를 제공합니다. 구성 요소에 대한 추상 데이터를 저장하는 데 사용할 수 있는 키/값 쌍의 범용 사전을 제공하는 이 서비스는 각 구성 요소에 고유합니다. 사이트 단위로 서비스를 대체할 방법이 없기 때문에 이러한 서비스는 대체하는 것은 불가능합니다.
디자이너 컨테이너로 사용되는 DesignSurfaceManager는 디자이너, 속성 창 및 기타 글로벌 개체 간의 이벤트 라우팅을 처리하는 공통 서비스를 제공합니다. DesignSurfaceManager를 사용하는 것은 선택적이지만 여러 개의 디자이너 창이 있는 경우 사용하는 것이 좋습니다.
DesignSurfaceManager도 기본적으로 여러 가지 디자인 타임 서비스를 제공합니다(그림 5 참조). 이러한 서비스 각각은 protected ServiceContainer 속성 값을 변경하여 재정의할 수 있습니다. DesignSurface와 마찬가지로, IDisposable을 구현한 서비스 컨테이너에 추가된 모든 DesignSurfaceManager 서비스는 디자이너 응용 프로그램이 제거될 때 제거됩니다.
IDesignerEventService는 매우 유요한 서비스입니다. 이 서비스를 사용하면 응용 프로그램에서 디자이너가 활성화된 시기를 알 수 있습니다. IDesignerEventService는 디자이너 컬렉션을 제공하며 속성 창과 같은 글로벌 개체가 선택 항목 변경 이벤트를 수신 대기할 수 있는 단일 위치입니다.
호스팅 양식
디자이너를 호스트하는 것이 얼마나 쉬운지 보여 주기 위해 기본 Windows Forms 디자이너를 만들고 표시하는 예제 코드를 작성했습니다. // Create the DesignSurface and load it with a form DesignSurface ds = new DesignSurface(); ds.BeginLoad(typeof(Form)); // Get the View of the DesignSurface, host it in a form, and show it Control c = ds.View as Control; Form f = new Form(); c.Parent = f; c.Dock = DockStyle.Fill; f.Show();
이 코드 조각에서 Form과 함께 DesignSurface를 로드했습니다. 마찬가지로 루트 디자이너에서 사용할 수 있게 만들려는 아무 구성 요소와 함께 DesignSurface를 로드할 수 있습니다. 예를 들어 Component 대신 UserControl을 로드할 수 있습니다.
이 기사에서 다운로드할 수 있는 코드에 제공된 예제에서는 네 가지 서로 다른 루트 구성 요소 Form, UserControl, Component 및 MyTopLevelComponent(그래프 디자이너)를 호스트합니다. 예제를 실행하면 셸 UI가 열립니다. 그림 6에서 볼 수 있는 것처럼 이 인터페이스에는 도구 상자, 속성 브라우저, 디자이너를 호스트하는 탭 컨트롤, 출력 창 및 솔루션 탐색기가 포함되어 있습니다. 메뉴에서 파일 | 새로 만들기 | 양식을 선택하면 Windows Forms 디자이너와 함께 새 디자이너 호스트가 열립니다. 디자이너를 로드하기 위해 앞서 설명한 코드가 사용됩니다. 예제 응용 프로그램에서는 Form을 로드하는 대신 UserControl이나 Component를 로드하는 방법을 보여 줍니다.
그림 6 Windows Forms 디자이너 호스트
루트 구성 요소를 만들려면 IRootDesigner를 구현하는 디자이너를 만든 다음 구성 요소와 해당 디자이너를 연결합니다. 루트 구성 요소의 View 속성은 사용자에게 표시되는 보기를 지정합니다.
DesignSurface가 제공하는 기본 서비스 중 하나가 IDesignerHost입니다. 이 서비스는 디자이너와 컨트롤에서 형식, 서비스 및 트랜잭션에 액세스할 때 사용되는 마스터 인터페이스입니다. 구성 요소를 만들고 파괴할 때에도 이 서비스를 사용할 수 있습니다. 앞서 만든 Windows Forms 디자이너에 단추를 추가하기 위해 필요한 모든 작업은 그림 7에서 볼 수 있는 것처럼 DesignSurface에서 IDesignerHost 서비스를 가져와 단추를 만드는 데 사용하는 것입니다.
IToolboxUser는 디자이너에서 도구 상자에 컨트롤을 추가하는 것을 지원하도록 지정합니다. 이것은 ToolboxService를 구현하는 도구 상자가 있는 경우 IToolboxUser 인터페이스를 사용하여 루트 구성 요소에 컨트롤을 추가할 수 있다는 의미입니다. 다음 예를 참조하십시오. /* Add a Button to the Form using IToolboxUser */ IDesignerHost idh = (IDesignerHost)ds.GetService(typeof(IDesignerHost)); IToolboxUser itu = (IToolboxUser)idh.GetDesigner(idh.RootComponent); itu.ToolPicked(new ToolboxItem(typeof(Button))); 도구 상자에서 항목을 두 번 클릭하여 예제 응용 프로그램의 사용자 지정 RootDesigner에 컨트롤을 추가하면 RootDesigner 보기가 업데이트되어 그림 8과 같은 원형 차트가 표시됩니다. GraphStyle 링크를 클릭하면 보기가 막대 그래프로 변경됩니다.
그림 8 사용자 지정 RootDesigner 업데이트
도구 상자
MyRootDesigner는 IToolboxUser 인터페이스를 구현합니다. 이 인터페이스에는 GetToolSupported 및 ToolPicked의 두 가지 메서드가 있습니다. GetToolSupported를 사용하여 디자이너에 추가할 수 있는 항목을 필터링할 수 있습니다. ToolPicked는 계속해서 ToolboxItem의 CreateComponents 메서드(이름에서 알 수 있는 것처럼 구성 요소 생성을 담당함)를 호출합니다.
디자이너에 컨트롤과 구성 요소를 추가했습니다. 이제 도구 상자 구현 방법을 자세히 살펴 보겠습니다. 먼저 도구 상자에서 IToolboxService를 구현해야 합니다. 이 서비스는 서비스 컨테이너에 추가되고 필요한 모든 사용자가 액세스할 수 있습니다. IToolboxService의 기본 기능은 그림 9에 표시되어 있습니다.
마우스나 키보드를 사용하여 도구 상자의 항목을 디자이너에 추가할 수 있도록 예제의 도구 상자에서는 KeyDown 및 MouseDown 이벤트를 후크합니다. Enter 키나 마우스 두 번 클릭의 경우에는 IToolboxUser.ToolPicked가 호출됩니다. 예제에서는 마우스 한 번 클릭 이벤트가 발생할 경우 ToolboxItem을 DataObject 및 DoDragDrop에 직렬화하는 방법을 보여 줍니다. 마우스 단추를 떼면 IToolboxService.SerializeToolboxItem이 호출되고 항목이 디자이너에 추가됩니다.
디자이너에 새 컨트롤이나 구성 요소를 추가할 때 INameCreationService를 구현하여 컨트롤의 사용자 지정 이름을 제공할 수 있습니다. 예제 응용 프로그램에서는 CreateName, ValidateName 및 IsValidName을 사용하는 작업에서 이 서비스의 예를 보여 줍니다.
다중 DesignSurfaces
다중 DesignSurface를 관리할 경우 DesignSurfaceManager를 사용하는 것이 좋습니다. DesignSurfaceManager를 사용하면 다중 DesignSurface를 쉽게 관리할 수 있습니다. (DesignSurfaceManager의 서비스를 DesignSurface에서 사용할 수 있습니다.)
DesignSurfaceManager.CreateDesignSurface를 호출하면 CreateDesignSurfaceCore가 호출됩니다. 이 함수를 재정의하여 사용자 지정 DesignSurface를 만들고 서비스에 추가할 수 있습니다. 예제 응용 프로그램에서는 HostSurfaceManager 클래스에서 이 함수를 재정의하여 사용자 지정 HostSurface를 만듭니다. protected override DesignSurface CreateDesignSurfaceCore( IServiceProvider parentProvider) { return new HostSurface(parentProvider); } 그런 후 다음과 같이 ActiveDesignSurfaceChanged 이벤트를 후크하여 HostSurfaceManager 클래스에서 출력 창을 업데이트할 수 있습니다. void HostSurfaceManager_ActiveDesignSurfaceChanged( object sender, ActiveDesignSurfaceChangedEventArgs e) { ToolWindows.OutputWindow o = this.GetService(typeof(ToolWindows.OutputWindow)) as ToolWindows.OutputWindow; o.RichTextBox.Text += "New host added.\n"; }
DesignerLoaders
지금까지 DesignSurfaces를 만들고 디자이너를 호스트하고 컨트롤을 추가하고 도구 상자를 구현하고 OutputWindow와 같은 서비스를 추가하고 액세스했습니다. 다음 단계는 디자이너를 유지하는 것입니다. 디자이너 로더는 특정 유지 상태에서 디자이너를 로드하는 역할을 합니다. 단순하고 융통성 있는 디자이너 로더에는 소수의 요구 사항이 있습니다. 사실, 단순히 System.Windows.Forms.Form의 인스턴스를 만드는 한 줄짜리 디자이너 로더로 Windows Forms 디자이너의 인스턴스를 만들 수 있습니다.
디자이너 로더는 양식 디자인 로드뿐만 아니라 디자인 저장도 담당합니다. 저장은 선택적인 동작이므로 디자이너 로더는 디자이너 호스트의 변경 이벤트를 수신 대기하여 해당 이벤트에 따라 자동으로 상태를 저장합니다.
.NET Framework 2.0에는 사용자 지정 로더를 작성할 수 있는 새로운 클래스인 BasicDesignerLoader 및 CodeDomDesignerLoader가 도입되었습니다. 예제 응용 프로그램에서는 두 로더 형식의 구현을 모두 보여 줍니다. 앞서 구성 요소 형식을 전달하여 루트 구성 요소와 함께 DesignSurface를 로드하는 예를 보여 주었습니다. 하지만 로더를 사용할 경우 로더를 통해 디자인 화면을 로드해야 합니다. 다음과 같은 로더를 사용할 경우 BeginLoad 코드 조각이 필요합니다. // Load it using a Loader ds.BeginLoad(new MyLoader());
DesignerLoader는 DesignSurface에서 루트 구성 요소를 로드하며 모든 구성 요소를 만듭니다. 새 양식이나 다른 루트 구성 요소를 만들 경우 단순히 로더가 해당 양식이나 구성 요소를 로드합니다. 코드 파일에서 로드하거나 다른 저장소에서 로드하는 경우와 비교하면 로더는 파일이나 저장소의 구문 분석과 다른 필수 구성 요소와 함께 루트 구성 요소를 다시 만드는 일을 담당합니다.
.NET Framework는 영구적인 저장소의 디자이너를 로드하거나 저장하는 DesignerLoader 추상 기본 클래스를 정의합니다. 이 기본 클래스는 추상 클래스이므로 모든 유형의 영구적 모델을 사용할 수 있지만 클래스 구현이 훨씬 복잡합니다.
BasicDesignerLoader는 영구적인 형식과 관련된 정보를 제외한 디자이너 로더의 완전한 공용 구현을 제공합니다. 이 클래스는 DesignerLoader와 마찬가지로 추상 클래스이며 영구적인 형식과 관련된 기능이 없습니다. 하지만 BasicDesignerLoader는 저장 시점을 인식하고 다시 로드하는 방법을 인식하며 디자이너의 변경 알림을 추적하는 표준 작업을 처리합니다. 이 클래스에는 다중 로드 종속성, 변경 사항 저장의 필요성을 나타내는 수정 비트 추적 및 지연 유휴 시간 다시 로드를 지원하기 위한 기능이 포함되어 있습니다.
그림 10 에서 볼 수 있는 서비스는 BasicDesignerLoader에 의해 디자이너 호스트의 서비스 컨테이너에 추가됩니다. 다른 서비스와 마찬가지로 protected LoaderHost 속성 값을 편집하여 대체 가능한 서비스를 변경할 수 있습니다. 예제 응용 프로그램에서는 상태를 XML 형식으로 유지하는 BasicDesignerLoader를 구현합니다. 이 응용 프로그램의 작동을 확인하려면 파일 | 형식 | BasicDesignerLoader를 선택하십시오. 그런 다음 파일 | 새로 만들기 | 양식을 선택하여 새 양식을 만듭니다. 생성되는 XML 코드를 확인하려면 보기 | 코드 | XML을 선택하십시오. 응용 프로그램에서 생성되는 XML 코드는 다음과 같습니다. <Object type="System.Windows.Forms.Form, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" name="Form1" children="Controls"> <Property name="Name">Form1</Property> <Property name="DataBindings"> <Property name="DefaultDataSourceUpdateMode">OnValidation</Property> </Property> <Property name="ClientSize">292, 273</Property> </Object> PerformFlush 및 PerformLoad는 각각 serialize와 deserialize를 위해 구현한 BasicDesignerLoader의 두 추상 함수입니다.
CodeDomDesignerLoader디자인 타임 serialization은 소스 코드를 생성하여 처리합니다. 코드 생성 스키마의 문제 중 하나가 여러 언어를 처리하는 것입니다. .NET Framework는 다양한 언어로 작동하도록 설계되어 있으므로 디자이너를 여러 언어로 내보내고 싶습니다. 이 문제를 해결하는 두 가지 방법이 있습니다. 첫 번째 방법은 각 언어 공급업체에게 해당 언어에 사용할 코드 생성 엔진을 작성하도록 요구하는 것입니다. 불행하게도 제3의 구성 요소 공급업체에서 필요한 다양한 코드 생성 요구 사항을 충족시킬 수 있는 언어 공급업체가 없습니다. 두 번째 방법은 각 구성 요소 공급업체에 지원하려는 언어별로 코드를 생성하도록 요구하는 것입니다. 지원 언어 수가 일정하지 않기 때문에 이것도 쉬운 방법은 아닙니다.
이 문제를 해결하기 위해 .NET Framework는 CodeDOM(Code Document Object Model)이라는 개체 모델을 정의했습니다. 모든 소스 코드는 본질적으로 기본 요소로 나눌 수 있으며 CodeDOM은 그와 같은 요소를 위한 개체 모델입니다. 코드가 CodeDOM을 따르는 경우 생성된 개체 모델을 나중에 특정 언어 코드 작성자에게 보내 적절한 코드로 개정할 수 있습니다.
.NET Framework 2.0에는 BasicDesignerLoader에서 상속된 CodeDomDesignerLoader가 도입되었습니다. CodeDomDesignerLoader는 CodeDOM을 읽고 작성할 수 있는 완전한 로더입니다. 이것은 턴키 방식의 디자이너 로더이므로 CodeDOM만 있으면 됩니다.
예제 응용 프로그램에서 파일 | 형식 | CodeDomDesigner-Loader를 선택하여 실제로 동작하는 CodeDOM 예제를 볼 수 있습니다. 파일 | 새로 만들기 | 양식을 선택하여 새 양식을 만듭니다. 이렇게 하면 DesignSurface가 생성되고 CodeDomDesignerLoader를 사용하여 로드됩니다. 코드를 확인하려면 보기 | 코드 | C#을 선택하여 양식 코드의 C# 버전을 확인하거나 보기 | 코드 | VB를 선택하여 Visual Basic 버전을 확인하십시오.
코드를 생성하기 위해서 예제 응응 프로그램에서는 CSharpCodeProvider 및 VBCodeProvider를 사용합니다. 또한 코드 공급자를 사용하여 코드를 컴파일하고 실행 파일을 실행합니다(그림 11 참조).
CodeDomDesignerLoader를 사용할 때 필요한 ITypeResolutionService는 형식 확인을 담당합니다. 형식을 확인하기 위해 이 서비스가 호출되는 한 가지 시나리오의 예를 들면 도구 상자의 컨트롤을 디자이너에 추가하는 경우입니다. 예제 응용 프로그램은 System.Windows.Forms 어셈블리에서 모든 형식을 확인하므로 도구 상자의 Windows Forms 탭에서 컨트롤을 추가할 수 있습니다.
결론지금까지 살펴 본 것처럼 .NET Framework는 강력하고 융통성 있는 호스트 인프라를 제공합니다. 디자이너는 이전 버전의 Visual Studio가 지원한 것보다 훨씬 고급의 시나리오와 더 특수한 요구 사항을 처리할 수 있는 직접적인 확장성을 제공합니다. 물론 Visual Studio 외부에서도 쉽게 디자이너를 호스트할 수 있습니다. 지금 예제 응용 프로그램을 다운로드하여 코드를 살펴 보고 자신만의 고유한 디자이너를 구현해 보십시오.