Windows Forms, ASP .NET Web Form 디자이너의 경우, 디자인 타임에 대한 의존도가 상당히 높은 편입니다. 그리고 이러한 디자인 타임 상의 구현을 완료하는데에 있어서 흔히 겪게 되는 문제가 있는데, 바로 컬렉션에 대한 처리입니다.

디자인 타임을 위한 컬렉션의 초기화 방법은 달라야 합니다.

디자인 타임을 위한 컬렉션의 초기화 방법은 런타임때와는 달라야 합니다. 객체를 생성하고 호출하는 방법이 우리가 이해하는 런타임 때와는 다르며, 컬렉션은 이를 준수하기 위해서 지연된 초기화 과정을 거쳐야 합니다. 다음은 디자인 타임용 컬렉션을 초기화하는 프로퍼티의 한 예시입니다.

internal ArrayList internalObjCollection = new ArrayList(); // 나중에 설명할 부분입니다.
private ObjectCollection objCollection = null; // 생성자나 인라인 식에서 초기화하지 않습니다. 대신...

[Browsable(true)]
[Category(ForexRuntime.ForexCategoryName)]
[Editor(typeof(CollectionEditor), typeof(UITypeEditor))]
[Description("속성에 대한 설명을 여기에 지정합니다.")]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
public ObjectCollection Collection {
    get {
        if (objCollection == null) objCollection = new ObjectCollection(this);
        return objCollection;
    }
}

위의 예시에서처럼 지연된 초기화를 사용한다는 점을 기억해야 합니다. 또한, 디자인 타임 컬렉션이 갖추어야 할 추가적인 조건이 한 가지 더 있는데, 디자인 타임 컬렉션은 철저히 프록시 역할을 수행해야 한다는 점입니다. 위에서 언급한 ObjectCollection의 생성자 호출 시 부모 객체의 참조를 넘겨받는 다는 점을 유심히 살펴보아야 합니다. 그리고 ObjectCollection은 컬렉션으로서 준수해야 할 기본적인 인터페이스 구현만을 포함해야 하며 실제로 모든 객체 관리는 다시 internalObjCollection 이라는 별도의 컬렉션에서 관리가 이루어지게 된다는 점입니다.

[Serializable]
[DesignTimeVisible(true)]
public class OutputFieldDefineCollection : IList { ... }

컬렉션 클래스 자체에는, 디자인 타임과의 원활한 상호 작용을 위하여 DesignTimeVisibleAttribute 속성이 추가된 것을 확인해 둡니다. 그리고 이 컬렉션의 생성자 형식에서 수용하기로 한 원본 객체의 참조는, 원본 객체 내부의 ArrayList에 접근하기 위한 목적으로 사용되고, 이 클래스가 구현하기로 한 IList 및 다른 인터페이스는 원본 객체 내부의 ArrayList를 기준으로 컬렉션의 기능을 제공하도록 코드를 작성합니다.

생성자와 IContainer 인터페이스의 역할은 매우 중요합니다.

추가하기로 한 컴포넌트에서 각별히 신경써야 할 것은, 바로 컨테이너의 기본 생성자와 더불어서 IContainer 객체의 참조를 받는 생성자로 적어도 2가지 생성자가 항상 제공되어야 합니다. 예를 들어, BookComponent가 있다고 가정해 보겠습니다.

public class BookComponent : Component {
    public BookComponent() : this(null) { }
    public BookComponent(IContainer container) : base() {
        if (container != null) { container.Add(this); }
        // 이곳에 생성자 코드를 지정하거나, this.InitializeComponent() 메서드를 호출합니다.
    }
}

위와 같은 코드가 있다고 하였을 때, 보통 container의 Add 메서드를 별 다른 생각없이 부릅니다. 하지만 여기에 숨겨진 기능이 하나 더 있는데, Add 메서드에는 디자인 타임에서 사용할 변수의 이름을 지정할 수 있는 인자가 제공된다는 점입니다. Add 메서드의 두 번째 오버로드를 사용하면 쉽게 변수의 이름을 다시 정의할 수 있는 것입니다. 단, 중복되는 변수 이름이 발생하지 않도록 정교한 명명 규칙이 필요함을 유의해야합니다.

또한, 별도의 디자이너가 존재하는 경우, 가능하면 컴포넌트의 생성자 메서드의 실행이 끝나기 전에 필요한 모든 초기화 작업을 수행하는 것이 좋으며 여기에는 앞서 설명한 Add 메서드의 활용은 물론 컴포넌트 자체의 설정에 관한 부분들도 포함됩니다.

저작자 표시 비영리 동일 조건 변경 허락
크리에이티브 커먼즈 라이선스
Creative Commons License
Posted by Windows Azure MVP 남정현 (rkttu.com)

C#을 프로그래밍 언어로 사용하여 Windows Forms를 이용하여 만든 이미지 뷰어 컨트롤입니다. PictureBox 컨트롤이 내부적으로 스크롤 기능을 지원하지 않는점을 고려하여 디자인한 컨트롤이며, 다음의 기능들을 지원합니다.

 

  • 배율에 따른 이미지 확대/축소 기능
  • 이미지 축소 및 확대 시 부드럽게 이미지를 변조하는 기능
  • 키보드 스크롤, 마우스 드래그 스크롤

 

크리에이티브 커먼즈 라이선스
Creative Commons License
Posted by Windows Azure MVP 남정현 (rkttu.com)

Visual Studio, 특히 Windows Forms 기반 프로젝트를 진행하면서, 사용자 정의 컴포넌트나 사용자 정의 컨트롤을 개발할 때 쉽게 파악하기 힘든 사항 중에 하나가 현재 로드된 컴포넌트나 컨트롤이 디자인 타임 위에서 실행 중인지 런타임 위에서 실행 중인지를 파악하는 것입니다. 저 또한 이 문제 때문에 꽤 많은 고민과 테스트를 수행해보았습니다만 시원치 않은 결과들 뿐이었습니다.

 

그러다가 결론을 하나 구했고 다음과 같은 내용들입니다.

 

  • Component.DesignMode 속성은 Component.Site 속성이 null 참조가 아니고, 지정된 Site 객체의 DesignMode 속성을 읽어서 반환하는 것이므로 큰 의미는 없습니다.
  • Component.Site 속성이 null 참조를 반환하는지 검사하는 방법은 논리적인 오류가 내포되어있을 가능성이 있습니다. 무조건 이런 검사를 사용하면, 디자인 타임이 아니면서도 Site 속성을 이용할 때 문제가 발생할 수 있습니다.
  • 외국 포럼의 자료를 검색한 결과 System.ComponentModel.LicenseManager 클래스의 UsageMode 속성을 이용하는 방법을 찾을 수 있었습니다. 이 속성은, LicenseManager 클래스의 Context 객체가 null 참조가 아니고, 해당 객체의 UsageMode 속성 값을 반환하는 것이며, null 참조일 경우 런타임으로 이해합니다.
  • Site 속성에 비해 훨씬 목적이 분명하고 제한적이므로 Site 속성을 이용한 판정보다는 안전한 선택이라고 예상됩니다.
  • 다만, 초기에 컴포넌트나 컨트롤의 생성자 단계에서만 유효한 정보이므로 이 때 캐치하지 못하면 디자인 타임 위인지 런타임 위인지 판정하지 못한채 런타임으로 인지하고 실행해버리므로 별도의 변수에 보관할 필요가 있습니다.

그 결과 아래와 같이 코드를 구성할 수 있었습니다.

 

public class MyComponent : Component

{

    private readonly bool inDesign;

    public MyComponent()

    {

         // 중략

         this.inDesign = (LicenseManager.UsageMode == LicenseUsageMode.Design);

    }
}

 

디자인 타임 때 만들어진 객체의 유효 기간은, Visual Studio에서 디자인 타임 편집 창을 닫기 전까지이므로 싱글턴 패턴을 이용하지 않는다면 만들어질 여러 MyComponent 객체 간의 간섭 효과는 걱정하지 않아도 될 것입니다.

크리에이티브 커먼즈 라이선스
Creative Commons License
Posted by Windows Azure MVP 남정현 (rkttu.com)

최근 진행 중인 씨티은행의 외환업무시스템 프레임워크 업그레이드 작업은 .NET Framework를 RAD (Rapid Application Development) 도구로 사용하는 것의 난해함에 대해 많은 것을 생각하게 하고 있습니다. .NET Framework 기반의 소프트웨어는 분명히 Unmanaged Code를 기초로 하는 다른 RAD 도구와는 다른 이점이 많이 있으며, 앞으로도 Windows 7과 더불어서 Desktop Scene의 현대화를 이끌어내는 주요한 키 포인트가 될 것으로 보이지만 풀어야 할 숙제 또한 많이 있을 것입니다.

 

다들 인지하고 계시는 부분일 수도 있지만, 또한 도움이 될 수 있을것이라 생각하여 오랫만에 간단한 샘플 하나를 올려봅니다. 이 방법을 통하여, Windows Forms가 가지고 있는 특유의 장점을 살리면서도, 코드를 복잡하게 만들지 않으면서, 손쉽게 다양한 유형의 데이터 바인딩을 다룰 수 있을 것이라 생각합니다.

 

이번 글에서 소개하는 데이터 바인딩 소스는 단순한 난수 생성에 한정되지만, XML 문서 구조를 유추하여 데이터 셋을 완성해야 할 필요가 있다거나 다양한 경우에 대응되는 데이터 변형 시나리오를 구축할 수 있을 것입니다.

 

System.ComponentModel.Component를 기본 클래스로 하는 컴포넌트를 만들고, System.ComponentModel.IListSource 인터페이스를 구현하는 컴포넌트를 만듭니다. Component 클래스를 기본 클래스로 한다는 것은 Visual Studio가 제공하는 Design Time 상호 운용성을 위한 기본 틀을 마련하는 것입니다.

 

IListSource 인터페이스는 ContainsListCollection 프로퍼티와 GetList 메서드로 구현이 됩니다. GetList 메서드를 통하여, 우리가 흔히 필요로 하는 데이터 소스를 직접 반환할 수 있으며, ContainsListCollection 프로퍼티는 좀 더 복잡한 유형의 데이터 소스를 관리할 수 있는 기준을 제공합니다. 보통의 경우, ContainsListCollection 프로퍼티의 값은 항상 false를 반환하도록 하고, GetList 메서드에서는 BindingList<T> 객체나 System.Data.DataView 객체를 반환합니다.

 

다음은 IListSource 인터페이스를 구현해 놓은 예시입니다.

 

        [Browsable(false)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
        public bool ContainsListCollection
        {
            get { return false; }
        }

 

        public IList GetList()
        {
            DataTable table = new DataTable();

            if (this.DesignMode && !this.liveBinding)
                return table.DefaultView;

            for (int i = 0; i < this.columnCount; i++)
            {
                DataColumn eachColumn = table.Columns.Add();

                eachColumn.ColumnName = String.Format(
                    CultureInfo.CurrentCulture,
                    "Column{0}", (i + 1));
                eachColumn.Caption = String.Format(
                    CultureInfo.CurrentCulture,
                    "Value #{0}", (i + 1));
                eachColumn.AllowDBNull = false;
                eachColumn.DataType = typeof(int);
                eachColumn.DefaultValue = 0;
            }

            for (int i = 0; i < this.rowCount; i++)
            {
                object[] dataArray = new object[this.columnCount];

                for (int j = 0; j < this.columnCount; j++)
                {
                    dataArray[j] = this.randomizer.Next(
                        this.minimumValue,
                        this.maximumValue);
                }

                table.Rows.Add(dataArray);
            }

            return table.DefaultView;
        }

 

위에서 굵게 강조표시한 부분이 설명하고자 하는 부분들입니다. ContainsListCollection 속성을 false로 반환하여 일반적인 데이터 소스임을 알리는 부분이 있습니다. 그리고, 이제까지 커뮤니티에서 자주 회자되는 내용들 중 하나입니다만, 디자인 타임을 정확히 구분하는 방법이 여기에 있습니다. protected 접근자로 보호되고 있고, 상속하여 재정의할 수 없는 DesignMode라는 속성을 이용하여 디자인 모드에서 취할 동작을 설정할 수 있습니다.

 

DesignMode 속성을 이용하여 디자인 타임에서 동적으로 데이터를 가져오도록 만들어서 다음 동작을 취할 수 있도록 정하는 것이 가능하며, 동시에 GetList 메서드 자체는 연계하기에 따라서 디자인 타임에 노출시킬 Verb (동사)와 연동시킬 수 있으므로, 데이터 바인딩에 시간이 많이 걸리는 동작을 정의하기에 편리합니다.

 

그리고 마지막으로, IList 인터페이스와 호환되면서도 우리가 논리적으로 편리하게 생각할 수 있는 데이터 모델인 System.DataView를 반환하는 것으로 코드의 대략적인 구조는 마무리됩니다. 이와 같은 형태로 완성된 컴포넌트를 실제로 데이터 바인딩에 연결시켜보도록 하겠습니다.

 

 

위에서 보이는것처럼, 난수를 데이터 테이블의 형태로 생성하였습니다. 이와 같이, 데이터베이스 서버가 아닌 곳의 일정한 자료를 데이터 소스로 사용하는 일이 반드시 제한적이지만은 않다는 것을 알 수 있으며, 응용하기에 따라서는 BindingSource 컴포넌트를 직접 오버라이드하여 데이터베이스가 아닌 대상을 놓고 Create, Read, Update, Delete 연산을 구현하는 것도 생각해볼 수 있을 것입니다.

 

이 샘플에 사용한 소스 코드를 하나 올려봅니다. 새 프로젝트에 추가하여 테스트해볼 수 있을 것입니다.

 

 

 

크리에이티브 커먼즈 라이선스
Creative Commons License
Posted by Windows Azure MVP 남정현 (rkttu.com)