ASP.NET 2.0 및 데이터 바인딩된 컨트롤: 새로운 관점 및 몇 가지 새로운 방식

 

Dino Esposito
Wintellect

적용 대상
   Microsoft ASP.NET 1.x
   Microsoft ASP.NET 2.0

요약: 사용자 지정 데이터 바인딩된 컨트롤을 빌드하는 도구가 ASP.NET 2.0에서 어떻게 발전했는지 알아봅니다.

목차

새로운 데이터 원본 모델이 필요한 이유
ASP.NET 2.0의 데이터 바인딩된 컨트롤
분석 요점
데이터 바인딩 메커니즘
목록 컨트롤
HeadlineList 샘플 컨트롤
사용자 지정 컬렉션 관리
복합 컨트롤 요약
결론

새로운 데이터 원본 모델이 필요한 이유

데이터 바인딩은 개발자들에게 있어 ASP.NET 1.x 프로그램의 가장 놀라운 기능 중 하나였습니다. Active Server Pages 데이터 액세스 지원과 비교할 때 데이터 바인딩은 사용의 간단함과 효율성이 매우 뛰어나게 결합된 기능이었습니다. 그러나 실제 개발자의 요구에 비해서는 다소 불완전한 기능이기도 했습니다. 전체 기능성 면에서는 제한이 없었지만, 개발자들은 페이징, 정렬 또는 삭제와 같이 간단하고 일반적인 작업을 처리하는 데도 많은 양의 코드를 작성해야 했습니다. 이를 보완하기 위해 ASP.NET 2.0에는 새 데이터 원본 모델이 추가되었습니다. 제가 쓴 다른 기사인 More Load, Less Code with the Data Enhancements of ASP.NET 2.0 을 참조하십시오. 이 모델은 데이터 바인딩된 컨트롤 및 데이터 컨테이너의 시각적 부분 사이의 차이점을 보완하는 UI가 없는 다수의 새 컨트롤로 구성됩니다. 기본적으로 ASP.NET 1.x를 사용할 때 개발자들이 적절하게 인수를 지정하고 만들어 직접 작성해야 했던 코드의 거의 대부분은 이제 새 컨트롤 패밀리인 데이터 원본 구성 요소에 포함됩니다(이 기사에는 영문 페이지 링크가 포함되어 있습니다).

데이터 원본 구성 요소를 사용하면 많은 이점이 제공됩니다. 무엇보다도, 완전히 선언적인 데이터 바인딩 모델을 사용할 수 있습니다. 새 모델은 ASPX 리소스에 인라인으로 삽입되거나 코드 숨김 클래스를 통해 분산되는 느슨한 코드를 줄여 줍니다. 새로운 데이터 바인딩된 아키텍처는 개발자가 엄격한 규칙을 따르도록 합니다. 또한 이 아키텍처를 통해 코드의 품질이 본질적으로 향상됩니다. 이벤트에 첨부된 긴 코드 블록이 없어지고 이를 기존 프레임워크에 바로 연결할 수 있는 구성 요소가 대체합니다. 데이터 원본 구성 요소는 추상 클래스에서 파생되고, 잘 알려진 인터페이스를 구현하며, 전체적으로 보다 높은 수준의 재사용 가능성을 제공합니다.

컨트롤 개발에 대한 Nikhil Kothari의 훌륭한 저서인 Developing Microsoft ASP.NET Server Controls and Components를 통해 수많은 개발자들이 사용자 지정 컨트롤을 빌드하고 디자인 및 구현에 있어 최적의 방식을 활용할 수 있었습니다. 그러나 아무리 훌륭한 저서라 해도 뛰어난 시스템 프레임워크를 대신할 수는 없겠죠. 또한, ASP.NET 2.0에서는 완벽하게 다시 디자인된 클래스 그래프를 활용할 수 있습니다. 이러한 클래스 그래프를 통해 기본 클래스에서 리프 클래스까지 트리 전체에 걸쳐 보다 구체적인 데이터 바인딩 기능을 추가할 수 있습니다. 데이터 바인딩된 컨트롤의 새로운 계층 구조를 통해 모든 개발자는 상속받을 올바른 클래스를 선택하고, 필요한 사용자 지정 데이터 바인딩 컨트롤을 보다 쉽게 만들 수 있습니다.

이 기사에서는 사용자 지정 컨트롤에 영향을 주는 ASP.NET 2.0 데이터 바인딩 모델의 변경 사항에 대해 미리 알아봅니다. 이 과정에서, 사용 가능한 새 기본 클래스 및 고품질의 새 사용자 지정 컨트롤에 필요한 새로운 요구 사항을 살펴봅니다.

ASP.NET 2.0의 데이터 바인딩된 컨트롤

ASP.NET 2.0 데이터 원본 모델에서는 새로운 컨트롤(예: GridViewFormView)이 필요하지 않습니다. DataGridCheckBoxList 등의 이전 스타일 컨트롤을 그대로 사용할 수 있습니다. 이것이 컨트롤 개발자에게 의미하는 것은 무엇일까요? 개발자가 다루는 데이터 원본 유형은 두 가지로 구분됩니다. DataView 및 컬렉션과 같은 기존 IEnumerable 기반 데이터 컨테이너와, SqlDataSourceObjectDataSource와 같은 데이터 원본 컨트롤이 그것입니다. 말하자면 ASP.NET 2.0 데이터 바인딩된 컨트롤은 ASP.NET 개체, 사용자 지정 컬렉션 또는 데이터 원본 구성 요소 등 원본에 관계없이 들어오는 데이터를 모두 열거 가능한 컬렉션으로 표준화할 수 있어야 합니다.

ASP.NET 1.x에서는 프레임워크보다 설명서를 먼저 참조하게 됩니다. 설명서에 표준 컨트롤, 목록 컨트롤 및 복합 컨트롤의 세 가지 데이터 바인딩된 컨트롤이 정확하게 확인 및 설명되어 있습니다. DataBind 메서드 및 DataSource 속성의 비어 있지 않은 구현을 제공하는 모든 컨트롤이 첫 번째 범주에 속합니다. 목록 컨트롤은 바인딩된 각 데이터 요소에 대해 반복할 고급 레이아웃 속성(예: RepeatColumnsRepeatLayout)과 고정 및 포함 항목 템플릿을 혼합한 흥미로운 컨트롤입니다. 마지막으로, 복합 컨트롤은 하나 이상의 기존 컨트롤을 결합하여 최종 사용자 인터페이스 디자인 작업을 처리하는 컨트롤입니다. 설명서에서는 이러한 컨트롤 유형 작성에 관련된 문제점을 정확하게 지적합니다. 그러나 ASP.NET 프레임워크는 개발자 작업을 단순화하는 대부분의 기본 클래스를 제공하지 않습니다. 그림 1에는 새로운 ASP.NET 2.0의 데이터 바인딩된 컨트롤 계층 구조가 나와 있습니다. 기본 클래스는 노란색으로 표시되어 있으며 전체 트리에 분포되어 있습니다.

그림 1. ASP.NET 2.0의 데이터 바인딩된 컨트롤 계층 구조

그림 1에 표시된 기본 클래스를 살펴보면 흥미로운 점이 있습니다. 기본 클래스 목록은 표 1에 자세히 나와 있습니다.

클래스 설명
BaseDataBoundControl 데이터 바인딩된 컨트롤의 루트 클래스입니다. 데이터 바인딩을 수행하고 바인딩된 데이터의 유효성을 검사합니다.
DataBoundControl 데이터 원본 컨트롤 및 데이터 컨테이너와 통신하는 논리를 포함합니다. 이 클래스에서 상속하여 표준 데이터 바인딩된 컨트롤을 빌드합니다.
ListControl 목록 컨트롤의 기본 클래스이며 Items 컬렉션 및 고급 레이아웃 렌더링 기능을 제공합니다.
CompositeDataBoundControl 포스트백(postback) 수행 이후 viewstate에서 컨트롤의 트리를 복원하는 코드를 포함하여 복합 컨트롤이 필요로 하는 일반 코드를 구현합니다.
HierarchicalDataBoundControl 계층 구조적인 트리 기반 컨트롤의 루트 클래스입니다.

표 1. ASP.NET 2.0의 데이터 바인딩된 기본 클래스

고유한 데이터 컬렉션을 관리하고 viewstate로부터 복원 작업을 제대로 수행하는 풍부한 기능을 갖춘 데이터 바인딩된 컨트롤을 만드는 까다로운 작업을 수행해 온 개발자에게는 이러한 클래스가 더없이 반가울 것입니다. 이에 대한 예제를 이 기사에서 살펴보겠습니다.

분석 요점

ASP.NET Developer Center에서는 지난 몇 달간 ASP.NET 1.1 데이터 바인딩된 컨트롤, 즉 RssFeedDetailsView 컨트롤에 대한 두 가지 기사를 제공했습니다(각각 Building DataBound Templated Custom ASP.NET Server Controls A DetailsView Control for ASP.NET 1.x ). 두 컨트롤의 코드를 자세히 살펴보면 여러 가지 면에서 원본에 특수한 기능을 사용하고 있음을 확인할 수 있습니다. 예를 들어 이러한 코드는 페이지에 대해 포스트백(postback)을 수행한 후 viewstate로부터 컨트롤의 트리를 다시 빌드하고(즉, 포스트백(postback)을 컨트롤이 제어하지 않음), 사용자 지정 컬렉션 클래스를 통해 항목 컬렉션을 제공하며, 항목의 스타일을 지정할 수 있도록 하고, 다양한 입력 원본을 지원합니다. ASP.NET 1.1에서는 이러한 각 기능마다 코드를 작성해야 합니다. 더욱 중요한 것은 이러한 코드를 특정 순서로 작성해야 하며, 특정 기본 메서드는 재지정하고 설명서 및 앞서 언급한 뛰어난 저서인 Developing Microsoft ASP.NET Server Controls and Components의 지침 및 제안 사항을 주의 깊게 따라야 한다는 것입니다.

반면 ASP.NET 2.0에서는 두 샘플 컨트롤에 사용되는 대부분의 연결 코드가 표 1에 나열된 기본 클래스에 하드코드되어 있습니다. ASP.NET 1.1과 2.0의 데이터 바인딩된 컨트롤을 비교 및 대조하기 위해 다음 주요 논점을 중점적으로 다루겠습니다.

  • 전체 데이터 바인딩 메커니즘 및 다양한 데이터 원본 유형
  • 컬렉션 및 viewstate 관리

위 내용만으로는 완벽한 설명이 되지 않겠지만 컨트롤 개발에 대한 대략적인 내용은 파악할 수 있을 것입니다. 풍부한 기능이 포함된 사용자 지정 컨트롤을 개발하는 데 코드가 거의 필요하지 않다는 것은 정말 놀랍고도 즐거운 일입니다.

데이터 바인딩 메커니즘

ASP.NET 2.0에서 새 데이터 바인딩된 컨트롤을 빌드하려면 우선 적합한 클래스를 선택해야 합니다. 그러나 선택은 비교적 빈 클래스인 ControlWebControl이나 ListControl 등에 국한되지는 않습니다. 그러면 실제로 사용할 수 있는 클래스를 살펴보겠습니다. BaseDataBoundControl은 모든 데이터 바인딩된 컨트롤 클래스의 루트이며 DataSourceDataSourceID 속성을 정의하고 이러한 속성에 할당된 콘텐츠의 유효성을 검사합니다. DataSource는 ASP.NET 1.x 방식으로 가져오고 할당된 열거 가능한 개체를 허용합니다.

Mycontrol1.DataSource = dataSet; _rep1.DataBind();

DataSourceID는 문자열이며 바인딩된 데이터 원본 구성 요소의 ID를 참조합니다. 컨트롤이 데이터 원본으로 바인딩되고 나면 컨트롤과 데이터 원본 간에 읽기 및 쓰기에 대한 추가 상호 작용은 컨트롤 외부에서 처리되며 보기에서 숨겨집니다. 여기에는 좋은 점도 있는 반면 좋지 않은 점도 있습니다. 코드의 양이 대폭 줄어든다는 것이 좋은, 그것도 매우 뛰어난 점입니다. ASP.NET 프레임워크는 파악된 최상의 방법에 따라 올바른 코드가 실행 및 작성되도록 합니다. 따라서 기본적으로 도중에 사소한 버그 없이 페이지를 보다 빠르게 작성할 수 있으므로 더욱 생산적입니다. 그러나 이러한 상황, 즉 대부분의 ASP.NET 1.x 개발자가 불만을 제기한 상황에 만족하지 못한다면 DataSource 속성 및 DataBind 메서드를 통한 기존 스타일의 프로그래밍을 계속 사용할 수도 있습니다. 이 경우에도 기본 클래스를 사용하면 코드 측면의 작업에는 별로 변화가 없지만 일반적인 작업은 줄일 수 있습니다.

DataBoundControl은 기존 컨트롤과 공유하는 항목이 거의 없는 표준 사용자 지정 데이터 바인딩된 컨트롤에 사용하는 클래스입니다. 고유한 데이터 항목 컬렉션을 처리하고, viewstate 및 스타일을 관리하고, 간단하지만 가장 적절한 사용자 인터페이스를 만들어야 하는 경우에는 이 클래스를 사용하는 것이 좋습니다. 재미있는 것은, DataBoundControl 클래스는 컨트롤을 데이터 원본 구성 요소에 연결하고 열거 가능한 데이터 원본 및 특별 구성 요소 간의 차이점을 API 수준에서 숨긴다는 것입니다. 요약하자면, 이 클래스에서 상속받을 때는 원본이 DataSet 개체이든, 새 데이터 원본 구성 요소이든 관계없이 데이터 컬렉션을 받는 메서드만 재지정하면 됩니다.

이는 아키텍처의 주요 변경 사항을 의미합니다. 여기에 대해 자세히 살펴보겠습니다.

BaseDataBoundControl은 원래 Control에서 정의된 DataBind 메서드를 재지정하여 보호된 추상 메서드로 표시되는 PerformSelect 메서드를 호출하도록 합니다. 이름에서 알 수 있듯이, PerformSelect는 바인딩 작업이 수행되도록 작업 데이터 컬렉션을 검색해야 합니다. 이 메서드는 구현 세부 사항을 포함하기 때문에 보호된 메서드이며, 해당 동작을 DataBoundControl 등의 파생 클래스로만 정확하게 실행할 수 있으므로 추상 메서드(Visual Basic 용어로는 MustInherit) 메서드입니다.

그러면 PerformSelect를 재지정하기 위해 DataBoundControl이 수행하는 작업은 무엇일까요?

데이터 원본 개체에 연결하여 기본 보기를 가져오는 것입니다. SqlDataSource 또는 ObjectDataSource 등의 데이터 원본 개체는 해당 select 명령을 실행하고 그 결과로 생성되는 컬렉션을 반환합니다. 데이터 검색을 실행하는 보호된 메서드인 GetDataDataSource 속성 또한 확인할 수 있습니다. DataSource가 비어 있지 않으면 바인딩된 개체는 동적으로 만들어진 데이터 원본 보기 개체에 래핑되어 반환됩니다.

다음 단계에는 컨트롤 개발자의 작업이 필요합니다. 지금까지는 완벽하게 자동화된 방식으로 기본 클래스가 ADO.NET 개체 또는 데이터 원본 구성 요소로부터 데이터를 검색했습니다. 다음 단계는 컨트롤이 수행할 작업에 따라 달라집니다. 여기서는 재지정 가능한 PerformDataBinding 메서드를 사용합니다. 다음 코드 조각은 DataBoundControl에서 구현되는 메서드를 보여 줍니다. 프레임워크가 메서드로 전달하는 IEnumerable 매개 변수에는 원본에 관계없이 바인딩할 데이터만 포함됩니다.

protected virtual void PerformDataBinding(IEnumerable data) { }

사용자 지정 데이터 바인딩된 컨트롤에서는 이 메서드만 재지정하여 대부분의 목록 컨트롤(예: CheckBoxList)의 Items 컬렉션과 같은 컨트롤 관련 컬렉션을 채우면 됩니다. 컨트롤의 사용자 인터페이스 렌더링은 컨트롤의 특성에 따라 Render 메서드 또는 CreateChildControls에서 수행됩니다. Render는 목록 컨트롤에 적합하며 CreateChildControls는 복합 컨트롤에 적합합니다.

다음으로는 데이터 바인딩 프로세스는 어떻게 시작되는지를 설명하겠습니다. ASP.NET 1.x에서 데이터 바인딩 작업을 시작하려면 DataBind 메서드를 명시적으로 호출해야 했습니다. 그리고 ASP.NET 2.0에서도 DataSource 속성을 사용하여 데이터를 컨트롤에 바인딩하는 경우 이 작업이 필요합니다. 그러나 대신 DataSourceID 속성을 통해 데이터 원본 구성 요소를 사용하는 경우에는 이 작업을 수행하지 않아야 합니다. 데이터 바인딩 프로세스는 다음 의사 코드에 나와 있는 것처럼 DataBoundControl에서 정의된 내부 OnLoad 이벤트 처리기에 의해 자동으로 트리거됩니다.

protected override void OnLoad(EventArgs e) { this.ConnectToDataSourceView(); if (!Page.IsPostBack) base.RequiresDataBinding = true; base.OnLoad(e); }

컨트롤이 페이지로 로드될 때마다(포스트백(postback) 또는 처음으로) 데이터가 검색되어 바인딩됩니다. 쿼리를 사용하여 다시 작업할 것인지 일부 캐시된 데이터를 사용할 것인지는 데이터 원본이 결정합니다.

페이지가 처음으로 표시되는 경우 데이터 바인딩 요청을 위해 RequiresDataBinding 속성이 설정됩니다. 할당된 값이 true이면 속성 setter는 DataBind를 내부적으로 호출합니다. 아래 의사 코드는 RequiresDataBinding setter의 내부 구현을 보여 줍니다.

protected void set_RequiresDataBinding(bool value) { if (value && (DataSourceID.Length > 0)) DataBind(); else _requiresDataBinding = value; }

여기서 알 수 있는 것처럼 이전 버전과의 호환성을 위해 DataSourceID가 null이 아닌 경우, 즉 ASP.NET 2.0 데이터 원본 컨트롤만을 사용하는 경우에만 DataBind 자동 호출을 수행합니다. 그러므로 DataBind도 명시적으로 호출하는 경우에는 데이터 바인딩이 이중으로 수행됩니다.

DataSourceDataSourceID를 동시에 설정할 수는 없습니다. 동시에 설정하는 경우에는 잘못된 작업 예외가 발생합니다.

마지막으로 EnsureDataBound 보호된 메서드에 대해 간단하게 언급하겠습니다. 이 메서드는 BaseDataBoundControl 클래스에서 정의되며 컨트롤이 필요한 데이터에 올바르게 바인딩되도록 합니다. RequiresDataBinding이 true이면 이 메서드는 아래 코드 조각과 같이 DataBind를 호출합니다.

protected void EnsureDataBound() { if (RequiresDataBinding && (DataSourceID.Length > 0)) DataBind(); }

복잡하고 정밀한 데이터 바인딩된 컨트롤을 작성해 보신 적이 있다면 쉽게 이해하실 것입니다. ASP.NET 1.x에서 데이터 바인딩된 컨트롤은 보통 다음 두 시나리오 중 한 가지 방법, 즉 데이터 원본에 대한 모든 권한을 통해 또는 viewstate를 기반으로 고유한 사용자 인터페이스를 빌드하도록 구성됩니다. 페이징이 설정된 DataGrid와 같이 자체의 포스트백(postback) 이벤트를 컨트롤이 관리해야 하는 경우, 앞서 언급한 두 가지 옵션은 서로 뚜렷하게 구별됩니다. ASP.NET 1.x에서 이러한 컨트롤(예: DataGrid)은 새로 고칠 호스트 페이지에 대해 이벤트를 시작하는 방법만을 사용할 수 있었습니다. 이 방법을 사용하면 알려진 바와 같이 ASP.NET 1.x 페이지의 코드 양이 늘어나므로, 데이터 원본 구성 요소를 통해 문제를 해결해야 합니다.

ASP.NET 2.0에서는 컨트롤의 수명 동안 작업이 발생할 때마다 RequiresDataBinding을 true로 설정하여 데이터가 바인딩되도록 할 수 있습니다. 이 속성을 설정하면 데이터 바인딩 메커니즘이 트리거되어 컨트롤 내부 인프라의 업데이트된 버전이 다시 만들어집니다. 기본 제공되는 OnLoad 이벤트 처리기도 컨트롤을 데이터 원본에 연결합니다. 이 기술을 가장 효과적으로 활용하려면 데이터 캐싱 기능을 갖춘 스마트 데이터 원본 컨트롤을 사용해야 합니다. 예를 들어, SqlDataSource 컨트롤은 바인딩된 결과 집합을 일정 기간 동안 ASP.NET 캐시에 저장하는 다양한 속성을 지원합니다.

목록 컨트롤

데이터 바인딩된 컨트롤은 목록 컨트롤인 경우가 많습니다. 목록 컨트롤은 컨트롤 메인프레임 경계 내에서 바인딩된 각 데이터 항목의 고정 템플릿을 반복하여 고유한 사용자 인터페이스를 빌드합니다. 예를 들어, CheckBoxList 컨트롤은 바인딩된 각 데이터 항목에 대해 CheckBox 컨트롤만을 반복합니다. 마찬가지로 DropDownList 컨트롤은 해당 데이터 원본 전체에서 반복되어 부모 <select> 태그 내에서 새 <option> 요소를 만듭니다. ASP.NET에서는 목록 컨트롤뿐 아니라 반복 컨트롤도 제공합니다. 이 두 컨트롤의 차이점은 무엇일까요?

목록 컨트롤 및 반복 컨트롤은 각 데이터 항목에 적용되는 반복 가능한 템플릿에서 허용되는 사용자 지정 수준 측면에서 서로 다릅니다. CheckBoxList 컨트롤과 같이, Repeater 컨트롤은 바인딩된 데이터 항목에서 고유한 작업을 수행하며 사용자 정의 템플릿을 적용합니다. Repeater 및 보다 복잡한 DataList 컨트롤은 매우 융통성이 뛰어나지만 코드를 모듈식 및 계층이 지정된 상태로 유지하는 데 큰 도움을 주지는 않습니다. Repeater를 사용하려면 페이지 또는 외부 사용자 컨트롤에서 템플릿을 정의하고 ASPX 원본에서 데이터 바인딩된 속성을 소비해야 합니다. 이는 빠르고 효과적이며 때로는 필요한 과정이지만, 간단하거나 뛰어난 방법은 아닙니다.

ASP.NET 1.x에서 모든 목록 컨트롤은 ListControl에서 상속됩니다. 이는 표 1의 클래스 중 1.x에도 이미 정의되어 있는 유일한 클래스입니다. 그러면 직접 코드를 살펴보면서 ASP.NET 2.0의 데이터 바인딩된 컨트롤을 익혀 보겠습니다. 먼저 각 데이터 항목에 대해 두 줄의 데이터 바인딩된 텍스트를 렌더링하는 HeadlineList 컨트롤을 빌드합니다. 이 컨트롤은 또한 수직 또는 수평 렌더링 등의 일부 레이아웃 기능을 제공합니다.

HeadlineList 샘플 컨트롤

앞서 언급한 것처럼 ListControl은 ASP.NET 1.x 및 2.0에서 모든 목록 컨트롤의 기본 클래스입니다. 다행히도, 여기서 ASP.NET 2.0용으로 작성한 HeadlineList 컨트롤을 매우 쉽게 ASP.NET 1.x로 다시 이식할 수 있습니다. 몇몇 이유로 인하여 헤드라인 목록을 빌드할 때는 먼저 Repeater를 사용하게 됩니다. Repeater를 사용하면 작업을 매우 간단하게 수행할 수 있습니다.

<asp:Repeater runat="server"> <HeaderTemplate> <table> </HeaderTemplate> <ItemTemplate> <tr><td> <%# DataBinder.Eval(Container.DataItem, "Title") %> <hr> <%# DataBinder.Eval(Container.DataItem, "Abstract") %> </td></tr> </ItemTemplate> <FooterTemplate> </table> </FooterTemplate> </asp:Repeater>

이 코드는 어디가 잘못되었을까요? 보다 정확하게 말하자면, 이 코드에서 어떤 부분을 개선할 수 있을까요?

참고: ASP.NET 2.0에서는 DataBinder.Eval(Container.DataItem, field)Page 클래스의 새 public 메서드인 Eval을 활용하는 보다 짧은 식으로 대체할 수 있습니다. 새 식의 형식은 Eval(field)입니다. 내부적으로 EvalDataBinder 클래스에서 정적 Eval 메서드를 호출하여 사용할 올바른 바인딩 컨텍스트를 결정합니다.

필드 이름은 ASPX 페이지에서 하드코드됩니다. 재사용은 잘라 내서 붙여 넣는 방법을 통해서만 가능합니다. 리피터 동작을 향상시키기 위해 코드를 추가하면 할수록 솔루션 자체 및 페이지와 프로젝트에 걸친 재사용 가능성은 낮아집니다. 헤드라인 목록 컨트롤이 필요한 경우에는 대신 다음 방법을 사용해 보십시오.

public class HeadlineList : ListControl, IRepeatInfoUser { : }

ListControlCheckBoxList, DropDownList 및 유사 컨트롤과 같은 패밀리인 목록 컨트롤의 기본 클래스입니다. IRepeatInfoUser는 열과 행, 수평 또는 수직으로 렌더링하기 위해 이러한 컨트롤 대부분이 구현하는 거의 알려지지 않은 인터페이스입니다. ListControlIRepeatInfoUser는 ASP.NET 1.x에도 있으며 2.0에서와 거의 같은 방식으로 작동합니다.

목록 컨트롤은 반복할 컨트롤로 구성됩니다. 이 컨트롤 또는 컨트롤 그래프는 클래스 속성이며 로드할 때 인스턴스화되어 CPU 사용량을 다소 줄입니다. 다음은 private ControlToRepeat 속성의 구현입니다.

private Label _controlToRepeat;

private Label ControlToRepeat

{ get { if (_controlToRepeat == null) { _controlToRepeat = new Label(); _controlToRepeat.EnableViewState = false; Controls.Add(_controlToRepeat); } return _controlToRepeat; } }

이 경우 반복할 컨트롤인 헤드라인은 첫 번째 읽을 때 인스턴스화되는 Label입니다. HeadlineList 컨트롤은 또한 사용자가 RepeatLayout, RepeatColumnsRepeatDirection과 같은 다양한 속성을 통해 모양을 바꿀 수 있도록 해야 합니다. 이러한 속성은 대부분의 표준 목록 컨트롤에서 정의되므로 개발자에게는 익숙합니다. 속성의 구현은 서로 비슷하며 아래 코드와 같이 나타납니다.

public virtual RepeatDirection RepeatDirection { get { object o = ViewState["RepeatDirection"]; if (o != null) return (RepeatDirection) o; return RepeatDirection.Vertical; } set { ViewState["RepeatDirection"] = value; } }

HeadlineList 컨트롤을 완료하기 위해 작성해야 하는 다른 코드는 렌더링 시 필요한 코드입니다. IRepeatInfoUser 인터페이스는 렌더링 프로세스를 제어할 수 있는 다양한 속성을 확인합니다. 그 예로는 HasHeader, HasFooterHasSeparator 부울 속성이 있습니다. 이러한 속성은 다른 일반 속성과 같은 방식으로 구현하며, RenderItem 인터페이스 메서드에서 필요한 경우 사용합니다.

public void RenderItem(ListItemType itemType, int repeatIndex, RepeatInfo repeatInfo, HtmlTextWriter writer) { string format = "<b>{0}</b><hr style='solid 1px black'>{1}"; Label lbl = ControlToRepeat; int i = repeatIndex; lbl.ID = i.ToString(); string text = String.Format(format, Items[i].Text, Items[i].Value); lbl.Text = text; lbl.RenderControl(writer); }

RenderItem은 최종적으로 페이지에 제공되는 출력을 처리합니다. 이 메서드는 반복할 컨트롤을 가져와 태그로 렌더링합니다. RenderItemRender에서 호출됩니다.

protected override void Render(HtmlTextWriter writer) { if (Items.Count >0) { RepeatInfo ri = new RepeatInfo(); Style controlStyle = (base.ControlStyleCreated ? base.ControlStyle : null); ri.RepeatColumns = RepeatColumns; ri.RepeatDirection = RepeatDirection; ri.RepeatLayout = RepeatLayout; ri.RenderRepeater(writer, this, controlStyle, this); } }

RepeatInfo는 기존 그래프 컨트롤을 반복하여 새 컨트롤을 빌드하도록 특수하게 디자인된 도우미 개체입니다. 필요한 것은 이것이 전부입니다. 샘플 페이지를 정렬하여 컨트롤을 테스트해 봅시다.

<expo:headlinelist id="HeadlineList1" runat="server" repeatlayout="Table" repeatdirection="Vertical" repeatcolumns="2" datatextfield="LastName" datavaluefield="Notes" />

그림 2에서 실제로 적용된 컨트롤을 볼 수 있습니다.

그림 2. HeadlineList 데이터 바인딩된 컨트롤

이 컨트롤은 더 이상 코드를 넣지 않아도 디자인 타임에서 제대로 작동합니다. 그러나 이 코드의 가장 큰 장점은 자유로운 디자인 타임 지원이 아닙니다. 개인적으로는 이 코드가 DataTable이나 DataSet 같은 ADO.NET 데이터 원본 개체 및 SqlDataSource 같은 데이터 원본 구성 요소에서 작동한다는 점이 가장 큰 장점이라고 생각합니다. 이 코드를 가져와 ASP.NET 1.x 프로젝트로 컴파일하면 IEnumerable 기반 데이터 원본에서 사용할 수 있습니다. 또한 ASP.NET 2.0 프로젝트로 이 코드를 가져와도 변경하지 않은 상태로 데이터 원본 개체에서 사용 가능합니다.

이것이 의미하는 것은 무엇일까요?

ASP.NET 1.x에서 ListControl 클래스는 유용하기는 하지만 어디까지나 예외입니다. ASP.NET 2.0에서는 이와 비슷한 간단하지만 효율적인 방식을 통해 데이터 바인딩된 컨트롤을 빌드할 수 있습니다. 이 과정에서 대부분의 복잡한 기능을 통합하며 알려져 있는 효과적 방법을 대부분 하드코드하는 새 기본 클래스를 활용합니다.

사용자 지정 컬렉션 관리

ListControlPerformSelect, OnDataBindingPerformDataBinding 등의 메서드를 재지정하지 않으면 제어할 수 없는 고정된 방식으로 데이터 바인딩을 수행하는 매우 특수화된 클래스입니다. 이 클래스는 또한 미리 정의된 Items 컬렉션 속성을 제공합니다. 보다 낮은 수준에서 ASP.NET 2.0의 데이터 바인딩을 살펴보고 다음과 같은 작업을 수행하는 ButtonList 컨트롤을 디자인해 봅니다.

  • 사용자 지정 컬렉션 클래스를 사용하여 구성 항목을 보관함
  • 사용자 지정 방식으로 viewstate를 관리함

ButtonList 컨트롤은 바인딩된 각 데이터 항목에 대해 누름 단추를 출력하는 또 하나의 목록 컨트롤입니다. 이 컨트롤이 ListControl에서 상속되도록 할 수 있습니다. 또한 HeadlineList의 소스 코드를 가져와 Label을 Button으로 대체해도 컨트롤은 제대로 작동합니다. 이번에는 다른 방식으로 DataBoundControl의 동작을 설명해 보겠습니다. 또한 간단히 하기 위해 IRepeatInfoUser 인터페이스에 대한 설명은 생략합니다.

public class ButtonList : System.Web.UI.WebControls.DataBoundControl { : }

캡션 및 명령 이름은 각 단추의 특성을 나타냅니다. 이 정보는 DataTextFieldDataCommandField 사용자 지정 속성을 통해 바인딩된 데이터 원본에서 가져옵니다. 또한 이와 유사한 속성을 쉽게 추가하여 데이터 바인딩된 도구 설명이나 URL에 제공할 수 있습니다.

public virtual string DataCommandField { get { object o = ViewState["DataCommandField"]; if (o == null) return ""; return (string) o; } set { ViewState["DataCommandField"] = value; } }

바인딩된 각 단추에 대해 발견되는 모든 정보는 Items 속성을 통해 제공되는 사용자 지정 개체 컬렉션에 채워집니다. 여기서 Items는 이 속성의 기본적인 표준 임의 이름일 뿐입니다.

[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)] [PersistenceMode(PersistenceMode.InnerProperty)] public virtual ButtonItemCollection Items { get { if (_items == null) { _items = new ButtonItemCollection(); if (base.IsTrackingViewState) _items.TrackViewState(); } return _items; } }

Items 컬렉션은 ButtonItem 개체 컬렉션인 사용자 지정 ButtonItemCollection 클래스의 인스턴스입니다. ButtonItem 클래스는 바인딩된 단추에 대한 주요 정보인 TextCommandName 속성과 두 개의 생성자, 그리고 ToString 메서드만을 저장합니다. ListItem 클래스가 일반 목록 컨트롤이듯이 ButtonItem 클래스는 일반 단추 컨트롤입니다. 다음 예제를 참고하십시오.

public class ButtonItem { private string _text; private string _command; public ButtonItem(string text, string command) { _text = text; _command = command; } public string Text { get {return _text;} set {_text = value;} } public string CommandName { get { return _command; } set { _command = value; } } public override string ToString() { return "Button [" + Text + "]"; } }

그러면 이제 ButtonItem 개체 컬렉션은 어떻게 만들면 될까요? ASP.NET 1.x에서는 CollectionBase에서 상속되며 최소한 두 개의 메서드를 재지정하는 사용자 지정 컬렉션 클래스를 빌드해야 합니다. 그러나 사용자 지정 컬렉션은 사실상 액세스 속도 측면에서는 아무런 이점이 없는 ArrayList 개체 주위의 래퍼일 뿐입니다. 그리고 사실상 캐스팅도 여전히 필요합니다. .NET 2.0의 Generics 기능이 이에 대해 중요한 전환점을 제공합니다. ButtonItem 개체 컬렉션을 빌드하려면 다음 코드가 필요합니다.

public class ButtonItemCollection : Collection<ButtonItem> { }

이는 컴파일러가 실제로 수행하는 작업으로 인해 보다 효과적으로 수행됩니다. ButtonList 컨트롤에는 두 개의 재지정된 메서드, 즉 RenderPerformDataBinding만 있으면 됩니다. RenderItems 컬렉션이 채워져 있는 것으로 가정하므로 태그 코드를 단순히 반복 및 출력합니다.

protected override void Render(HtmlTextWriter writer) { for(int i=0; i<Items.Count; i++) { ButtonItem item = Items[i]; Button btn = new Button(); btn.Text = item.Text; btn.CommandName = item.CommandName; btn.RenderControl(writer); } }

Items 컬렉션이 중요한 이유는 무엇일까요? 다음 두 가지 결과를 얻을 수 있도록 해 주므로 중요합니다. 첫째로, 수동으로 추가한 항목으로 목록 컨트롤을 채울 수 있으며 둘째로, 컬렉션이 viewstate에서 지속되는 경우 데이터에 바인딩하지 않고도 포스트백(postback)에서 컨트롤의 사용자 인터페이스를 다시 빌드할 수 있습니다. 그러면 데이터가 바인딩될 때 Items 컬렉션이 채워지는 위치는 어디이며 무엇으로 채워질까요? 이 작업에 필요한 것이 바로 PerformDataBinding입니다. 이 메서드는 원본에 관계없이 열거 가능한 데이터 목록을 가져와 Items 컬렉션을 채우는 데 사용합니다.

protected override void PerformDataBinding(IEnumerable dataSource) { base.PerformDataBinding(dataSource); string textField = DataTextField; string commandField = DataCommandField; if (dataSource != null) { foreach (object o in dataSource) { ButtonItem item = new ButtonItem(); item.Text = DataBinder.GetPropertyValue(o, textField, null); item.CommandName = DataBinder.GetPropertyValue(o, DataCommandField, null); Items.Add(item); } } }

데이터 바인딩이 필요할 때마다 이 메서드는 Items 컬렉션이 채워지도록 합니다. 그러면 포스트백(postback)에서는 어떤 작업이 수행될까요? 이 경우에는 Items 컬렉션을 viewstate에서 다시 구성해야 합니다. IStateManager 인터페이스에 있는 메서드를 통해 사용자 지정 컬렉션 클래스에 이 기능을 제공합니다. 다음은 이 인터페이스의 주요 메서드입니다.

public void LoadViewState(object state) { if (state != null) { Pair p = (Pair) state; Clear(); string[] rgText = (string[])p.First; string[] rgCommand = (string[])p.Second; for (int i = 0; i < rgText.Length; i++) Add(new ButtonItem(rgText[i], rgCommand[i])); } } public object SaveViewState() { int numOfItems = Count; object[] rgText = new string[numOfItems]; object[] rgCommand = new string[numOfItems]; for (int i = 0; i < numOfItems; i++) { rgText[i] = this[i].Text; rgCommand[i] = this[i].CommandName; } return new Pair(rgText, rgCommand); }

클래스는 일종의 최적화된 두 가지 위치 배열인 Pair 개체를 사용하여 viewstate로 자신을 serialize합니다. 각 단추의 텍스트와 명령 이름을 보관하려면 두 개의 개체 배열을 만들어야 합니다. 그런 다음 두 배열은 한 쌍으로 압축되어 viewstate에 삽입됩니다. viewstate가 복원될 때 이 쌍의 압축이 풀리고 Items 컬렉션은 이전에 저장한 정보로 다시 채워집니다. 공간과 시간적인 면에서 기존 이진 포맷터는 성능이 나쁘므로, 이 방법을 사용하는 것이 ButtonItem 클래스를 serialize 가능하도록 만드는 것보다 더 좋습니다.

그러나 컬렉션에 viewstate 지원을 추가하는 것만으로는 충분하지 않습니다. ButtonList 컨트롤 또한 컬렉션의 serialization 기능을 활용할 수 있도록 향상되어야 합니다. 이를 위해 컨트롤 클래스에서 LoadViewStateSaveViewState를 재지정합니다.

protected override void LoadViewState(object savedState) { if (savedState != null) { Pair p = (Pair) savedState; base.LoadViewState(p.First); Items.LoadViewState(p.Second); } else base.LoadViewState(null); } protected override object SaveViewState() { object baseState = base.SaveViewState(); object itemState = Items.SaveViewState(); if ((baseState == null) && (itemState == null)) return null; return new Pair(baseState, itemState); }

컨트롤의 viewstate는 기본 컨트롤의 viewstate와 Items 컬렉션의 두 가지 요소로 만들어집니다. 이 두 개체는 Pair 개체에 압축됩니다. Pair 개체와 더불어 세 개체의 배열인 Triplet 개체를 사용할 수 있으며, Pair 또는 Triplet 쌍을 사용하여 원하는 만큼 개체를 만들 수 있습니다.

이러한 방식으로 디자인한 사용자 지정 컬렉션은 디자인 타임에서도 활용할 수 있습니다. Visual Studio 2005에 포함된 기본 컬렉션 편집기는 컬렉션을 인식하고 그림 3과 같이 팝업 대화 상자를 표시합니다.

그림 3. 디자인 타임의 ButtonList Items 컬렉션

ASP.NET 2.0에서는 일부 데이터 바인딩된 컨트롤을 사용하면 데이터 바인딩된 항목을 Items 컬렉션을 통해 프로그래밍 방식으로 추가된 항목과 구분할 수 있습니다. 부울 AppendDataBoundItems 속성이 컨트롤 프로그래밍 인터페이스의 이러한 측면을 제어합니다. 이 속성은 DataBoundControl이 아닌 ListControl에서 정의되며 기본값은 false입니다.

복합 컨트롤 요약

CompositeDataBoundControl 클래스는 복합 컨트롤이 만들어지는 시작점입니다. 개인적으로는 데이터 바인딩 컨트롤이라고 하면 복합 컨트롤을 먼저 떠올린다고 생각합니다. 복합 컨트롤은 다음과 같은 작업을 수행해야 합니다.

  • 명명 컨테이너 역할을 함
  • CreateChildControls 메서드를 통해 고유한 사용자 인터페이스를 만듦
  • 포스트백(postback) 이후 자신의 자식 요소 계층 구조를 복원할 특정 논리를 구현함

마지막 논점은 Nikhil Kothari의 저서에 잘 설명되어 있으며, ASP.NET 1.x의 모든 기본 제공 컨트롤에서 구현됩니다. 아직 이 개념을 완전히 이해하지 못했다면 이젠 그냥 다 잊어버리셔도 됩니다. 이제는 모든 것이 CompositeDataBoundControl 클래스에 하드코드되어 있기 때문입니다. 이제는 컨트롤의 자식 디자인에 주의를 기울여야 합니다. 이는 다음과 같이 정의된 새 메서드를 재정의함으로써 수행할 수 있습니다.

protected abstract int CreateChildControls( IEnumerable dataSource, bool dataBinding);

CompositeDataBoundControlDataBoundControl로부터 상속되므로 이 기사에서 컬렉션, 바인딩 및 viewstate에 대해 논의한 대부분의 사항은 복합 컨트롤에도 적용됩니다.

결론

데이터 바인딩 및 데이터 바인딩된 컨트롤은 ASP.NET 1.x의 비약적 발전을 보여 주었습니다. 그러나 아직 설명되지 않은 부분과 해결되지 않은 의문이 남아 있습니다. Nikhil Kothari의 저서는 모든 개발자에게 최고의 권위 있는 지침서였습니다. ASP.NET 2.0에서는 이 저서에서 소개하는 유용한 방법 중 주로 ASP.NET 1.x에서 이미 실제로 구현되어 있는 일부 방법을 데이터 바인딩된 컨트롤에 사용할 수 있는 새로운 개체 모델 및 재사용 가능한 클래스로 전환하여 제공합니다.

이 기사에서는 ASP.NET 1.x 및 ASP.NET 2.0 간의 주요 변경 사항을 중점적으로 다루었으며 이러한 변경 사항이 개발에 주는 영향을 두 가지 실제 예제를 통해 대략적으로 살펴보았습니다. 다음 번에는 ASP.NET 2.0 컨트롤 개발의 스타일 및 테마를 다뤄 보고자 합니다. 조만간 다음 기사에서 해당 내용에 대해 알아보도록 하겠습니다. 기다려 주십시오.

관련 서적

 


저자 소개

Dino Esposito는 이탈리아에 살고 있는 Wintellect  강사이자 컨설턴트입니다. Microsoft Press 에서 출간된 Programming ASP.NET 및 최신 Introducing Microsoft ASP.NET 2.0 의 저자인 그는 주로 ASP.NET 및 ADO.NET에 대한 강의와 회의 강연을 하고 있습니다. Dino의 블로그(http://weblogs.asp.net/despos )를 방문해 보십시오.

+ Recent posts