Xml을 이용한 TreeView 메뉴 구성하기

현재 주소 복사
트랙백 주소 복사
방실이님의 글 (2/1/2008 2:03:38 PM) Viewing : 7651

처음에는 쓸 필요가 없을 듯 하여...(귀찮기도 하지만..ㅡ.ㅡ;) 패스 할려고 했으나..
몇몇분들의 요청이 있어서..
현재 사이트에서 사용중인 TreeView 메뉴에 대한 강좌를 시작하도록 하겠다..^^;

먼저 TreeView를 써 보신 분들은 아시겠지만...
Sitemap 을 이용해서 하게 되면..상당히 골치 아프다..ㅡ.ㅡ;
그래서 나의 경우에는 커스텀으로 xml을 만들어서 메뉴로 이용한다..

물론 TreeView가 아니어도 상관없다...
일반적인 사이트에서 메뉴의 특징을 살펴 보자.

  • 거의 변하지 않는다...
  • 카테고리마다 Left Menu가 변한다.
  • Role에 의해 Visible 속성이 변할 수 있다.

이 세가지는 거의 모든 사이트에서 이용되는 방법이라 볼수 있겠다.

이러한 경우에 XML을 이용하면 참으로 편리하다..
각 카테고리마다 UserControl을 만들어서 사용했었던 개발자가 있다면..아래 방법으로 변경을 시도해 보는 것도 좋을듯 싶다...

아래 xml은 이 사이트에서 사용되는 메뉴에 대한 xml 이다.

<?xml version="1.0" encoding="utf-8" ?>

<Menu>

    <Category url="/default.aspx" title="처음으로"  description="첫 페이지로 이동합니다.">

    </Category>

    <Category url="/AboutMe.aspx" title="쥔장은.."  description="저는 누구일까요??">

        <Item url="/AboutMe.aspx" title="쥔장 소개"  description="쥔장 소개입니다." />

        <Item url="/board/list.aspx?BoardId=31185279-904C-4BA3-9752-B3F2EA62BC37" title="쥔장 낙서장"  description="쥔장이 맘대로 끄적거린 글입니다." />

    </Category>

    <Category url="/board/list.aspx?BoardId=93a58900-87e8-4ed1-9cb9-a66ecb43ad59" title="게시판"  description="게시판 입니다.">

        <Item url="/board/list.aspx?BoardId=93a58900-87e8-4ed1-9cb9-a66ecb43ad59" title="알리는 글"  description="알리는 글입니다." />

        <Item url="/Board/List.aspx?BoardId=CA4E4AF3-083F-4D79-8079-CD42C674A57F" title="개발 히스토리"  description="개발 히스토리입니다." />

        <Item url="/Board/List.aspx?BoardId=ef3aa07e-6662-4670-8037-f89bba2d48a1" title="Microsoft 소식"  description="마이크로소프트 소식입니다." />

        <Item url="/board/list.aspx?BoardId=C783FA2B-AF07-4D26-9E15-B835DB541E6A" title="자유 게시판"  description="아무나 글을 쓰셔도 됩니다." />

        <Item url="/Board/List.aspx?BoardId=5a26d48c-a176-4130-9636-4fb07dfc565a" title="질문 게시판"  description="질문을 올려 주세요." />

        <Item url="/Board/List.aspx?BoardId=5f6a2c27-54a2-45f8-aab1-a6bfc567e550" title="사진 게시판"  description="사진 게시판입니다." />

        <Item url="/Board/List.aspx?BoardId=e37e600b-b769-4171-a655-09f4f776408e" title="커플 게시판"  description="커플 게시판입니다." roles="Couple" />

    </Category>

    <Category url="/Board/List.aspx?BoardId=da05ba1b-3e9b-4d16-b8f8-ff2e4f7514c5" title="Article"  description="아티클 입니다.">

        <Item url="/Board/List.aspx?BoardId=6cb920b1-f969-499c-a79b-f3b285eb437d" title="퍼온글들"  description="퍼온 글들입니다." />

        <Item url="/Board/List.aspx?BoardId=da05ba1b-3e9b-4d16-b8f8-ff2e4f7514c5" title="쥔장글들"  description="제가 쓴 글들입니다." />

        <Item url="/Board/List.aspx?BoardId=a905689f-c7d4-410c-a7b2-98f822b9b0ba" title="Source Files"  description="홈페이지 소스파일입니다." />

    </Category>

    <Category url="/Board/List.aspx?BoardId=bc974687-f70c-4aa9-8e90-ce201898b16f" title="자료실"  description="자료실 입니다.">

        <Item url="/Board/List.aspx?BoardId=bc974687-f70c-4aa9-8e90-ce201898b16f" title="공개 자료실"  description="공개 자료실 입니다." />

        <Item url="/Board/List.aspx?BoardId=66e51564-6372-4ddb-9d4e-28a21346191d" title="Back Up"  description="비밀 자료실" roles="Admin" />

    </Category>

    <Category url="/visit.aspx" title="방명록"  description="방명록을 남겨 주세요.">

    </Category>

</Menu>

구성을 살펴 보면 Menu라는 최상단 노드가 있으며 이 노드는 Category 라는 자식 노드를 가지고 있다.
Category 라는 노드는 url , title, description 이라는 어트리뷰트를 가지고 있다.
그리고 자식노드로 Item 이라는 노드를 가지고 있다..
이 Item 이라는 노드는 url, title, description, roles 라는 노드를 가지고 있다.

Category라는 노드는 메뉴상에서의 하나의 카테고리가 된다..
이 사이트의 경우에는 게시판, article, 자료실, 방명록 등이 되겠다.
그리고 Item이라는 노드는 그 하위 메뉴가 된다..
많약에 Depth가 하나 더 필요 하다면 노드를 하나 더 만들면 되겠다.

여기서 중요한 어트리뷰트는 url과 roles가 되겠다.
url 의 경우에는 클릭했을때 이동할 페이지의 url이 되며 roles의 경우에는 해당 메뉴에 액세스가 가능한 역할이 되겠다.

이쯤으로 xml 파일 설명을 마치고 이제 이것을 읽어오는 부분을 살펴 보자.

MenuItem 이라는 클래스는 xml에서 Item 노드에 대한 클래스이다.
MenuCategory 라는 클래스는 xml에서 Category 노드에 대한 클래스가 되겠다.
이 클래스는 Items 라는 MenuItem 클래스의 컬렉션을 속성으로 가지고 있으며 Add 라는 메서드로 이 컬렉션에 MenuItem 개체를 넣을 수 있다.
단순한 속성의 열거이니 코드는 생략한다...

이제 XmlLoader 라는 클래스에 주목하자 ..
이 클래스는 xml을 읽어 와서 MenuCategory 라는 클래스를 만드는 역할을 하게 되겠다.

    1 public static List<MenuCategory> LoadMenu()

    2 {

    3     List<MenuCategory> menu;

    4     Cache cache = HttpContext.Current.Cache;

    5 

    6     if (cache["Menu"] == null)

    7     {

    8         menu = new List<MenuCategory>();

    9         XmlLoader.ReadMenuXml(menu);

   10         cache.Insert("Menu", menu, null, System.Web.Caching.Cache.NoAbsoluteExpiration, new TimeSpan(0, 10, 0));

   11     }

   12     else

   13     {

   14         menu = (List<MenuCategory>)cache["Menu"];

   15     }

   16 

   17     return menu;

   18 }

먼저 LoadMenu라는 메서드를 살펴 보자.
이 메서드는 외부노출(public) 한 유일한 메서드이다.
이 메서드는 캐시를 조사해서 캐시되지 않았으면 새로 캐시하고 캐시된게 있으면 캐시된 개체를 반환하는 역할을 한다.

맨 처음 언급했지만 메뉴는 거의..변할 일이 없다..그러므로 캐시를 사용토록 한다.(4행)
캐시가 소멸되었을 경우에만 새로 xml 파일을 읽도록 한다.(6행)

9행의 ReadMenuXml 메서드를 이용해 체를 만들고 10행 처럼 캐시한다.

이제 ReadMenuXml 메서드를 살펴 보자.

    1 private static void ReadMenuXml(List<MenuCategory> menu)

    2 {

    3     XmlReader reader = XmlReader.Create(HttpContext.Current.Server.MapPath("~/Menu.xml"));

    4     lock (reader)

    5     {

    6         while (reader.Read())

    7         {

    8             if (reader.IsStartElement("Category"))

    9             {

   10                 MenuCategory category = new MenuCategory();

   11                 category.Title = String.IsNullOrEmpty(reader.GetAttribute("title")) ? "" : reader.GetAttribute("title");

   12                 category.Url = String.IsNullOrEmpty(reader.GetAttribute("url")) ? "" : reader.GetAttribute("url").ToLower();

   13                 category.Description = String.IsNullOrEmpty(reader.GetAttribute("description")) ? "" : reader.GetAttribute("description").ToLower();

   14                 category.Roles = String.IsNullOrEmpty(reader.GetAttribute("roles")) ? "*" : reader.GetAttribute("roles");

   15 

   16                 if (reader.ReadToDescendant("Item"))

   17                 {

   18                     do

   19                     {

   20                         MenuItem item = new MenuItem();

   21                         item.Title = String.IsNullOrEmpty(reader.GetAttribute("title")) ? "" : reader.GetAttribute("title");

   22                         item.Url = String.IsNullOrEmpty(reader.GetAttribute("url")) ? "" : reader.GetAttribute("url").ToLower();

   23                         item.Description = String.IsNullOrEmpty(reader.GetAttribute("description")) ? "" : reader.GetAttribute("description").ToLower();

   24                         item.Roles = String.IsNullOrEmpty(reader.GetAttribute("roles")) ? "*" : reader.GetAttribute("roles");

   25                         category.Add(item);

   26                     }

   27                     while (reader.ReadToNextSibling("Item"));

   28                 }

   29 

   30                 menu.Add(category);

   31             }

   32         }

   33     }

   34 }

3행에서 Xml 파일을 읽어서 XmlReader 개체를 만든다.
그리고 노드를 탐색 하면서 MenuCategory 개체와 Item 개체를 생성하고 List 개체에 Add 한다.

자 이제 이렇게 생성된 List 개체를 사용하는 법을 보도록 하겠다.
LeftMenu 라는 유저 컨트롤을 만들고 그 안에 코딩하면 되겠다.

    1 private void BindMenu()

    2 {

    3     List<MenuCategory> menu = XmlLoader.LoadMenu();

    4     foreach (MenuCategory category in menu)

    5     {

    6         TreeNode parent = new TreeNode(category.Title, "", "/Image/category.gif", category.Url, "");

    7         this.TreeView1.Nodes.Add(parent);

    8         foreach (MenuItem item in category.Items)

    9         {

   10             if (item.Roles == "*" || Roles.IsUserInRole(item.Roles))

   11             {

   12                 TreeNode child = new TreeNode(item.Title, "", "/Image/item.gif", item.Url, "");

   13                 parent.ChildNodes.Add(child);

   14             }

   15         }

   16     }

   17 }

이 BindMenu라는 메서드만 유저 컨트롤에서 호출하면 된다..
먼저 TreeView1이라는 트리뷰 컨트롤을 하나 생성했다.

XmlLoader.LoadMenu() 메서드로 List 개체를 얻는 것은 이미 설명 하였다.
이제 이 리스트를 루프 돌면서 하나씩 Add 해 주면 되겠다.
단지 10행에 보면 역할에 따라 Add 할것인지 말것인지 결정하는 부분이 있다.
Roles 라는 클래스는 RoleProvider를 구현해야지 사용할 수 있는 클래스이다. 
10행의 조건문은 해당 Role이 없으면 화면에 렌더시키지 않게끔 하는 코드가 되겠다.

이제 왼쪽과 같은 트리뷰 메뉴를 볼 수 있을 것이다..

참고-- 실무에서는 트리뷰보다는 아마도 이미지라든지 플래시 같은 경우를 많이 쓸듯 한데..
이러한 체계가 이해가 된다면 응용하는 것은 쉽다...

메뉴 구성에 있어서 더 좋은 방법이 있거나 사용하고 있다면...조언 부탁..^^;


마지막 업데이트 : (2/1/2008 4:54:21 PM)

TAG : 없음



Trackback 보기 (0)
댓글 보기 (12)
이석님의 글 (3/27/2008 12:53:07 PM)
BindMenu() 8번째 줄 foreach 돌릴 때요
char형을 MenuItem형식으로 변환 할 수 없다는 에러가 나는데
제가 멀 잘 못 했을까요..ㅜㅜ
그리고 MenuCategory 클래스의 ADD메서드는 그냥 명시만 해주면 되나요???



이방은님의 글 (3/27/2008 1:20:41 PM)
^^;
코드는 아래와 같습니다.
아마 형변환오류의 원인은 값을 Add 할때 잘못 했기 때문이 아닐까요...
아래 클래스를 참고 하시고 다시 질문 올려 주세요..
using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using System.Collections.Generic;

namespace Bangsil.pe.kr
{
public class MenuCategory
{
private string title;
private string url;
private string description;
private string roles = "*";
private List<MenuItem> items = new List<MenuItem>();

public string Roles
{
get
{
return this.roles;
}
set
{
this.roles = value;
}
}

public string Description
{
get
{
return this.description;
}
set
{
this.description = value;
}
}

public string Url
{
get
{
return this.url;
}
set
{
this.url = value;
}
}

public string Title
{
get
{
return this.title;
}
set
{
this.title = value;
}
}

public List<MenuItem> Items
{
get
{
return this.items;
}
}

public void Add(MenuItem item)
{
this.items.Add(item);
}
}
}



이석님의 글 (3/27/2008 6:49:53 PM)
하하~ 성공했습니다.
감사합니다.
낼 소스 천천히 둘러봐야겠습니다.
좋은 밤 되세요



이방은님의 글 (3/27/2008 7:30:22 PM)
ㅎㅎ 다행이네요..^^



김진성님의 글 (7/12/2008 12:07:03 AM)
방실님 전체 소스좀 올려 주세요^^



이방은님의 글 (7/14/2008 2:10:39 PM)
안녕 하세요 진성님..
전체 소스 라면 홈페이지 전체 소스 말인가요???
제 홈페이지는 코드를 많이 바꾼 상태라서..ㅎㅎ 지금 빌드가 안되는 상태랍니다..ㅋ
언젠가는 다 오픈 하겠지만..아직은 좀 부족하네요..^^;



김진성님의 글 (7/21/2008 5:05:00 PM)
ㅋ^^ 아닙니다... 사실은 ?boardId=DA05BA1B-3E9B-4D16-B8F8-FF2E4F7514C5&PostId=8d6c0b40-3dbd-40cf-be73-82be76170e4c

이게 궁금해서요^^

아 그리고 방실님 홈페이지에 트리메뉴를 3단계로 하려면 Item 밑에 하나더 주면 되나요?



이방은님의 글 (7/23/2008 11:02:01 AM)
단순히 아이템만 하나 추가 하면 depth 설정이 되지 않겠지요..
중간에 예를 들면 SubMenu 라는걸 만들고 이에 해당하는 Collection(List<T> 같은)을 만드셔야 겠네요..



김진성님의 글 (7/24/2008 10:16:18 AM)
MenuSubItem 클래스 만들고

그러면 XmlLoader 에
if (reader.ReadToDescendant("Item"))
{
do
{
MenuItem item = new MenuItem();
item.Title = String.IsNullOrEmpty(reader.GetAttribute("title")) ? "" : reader.GetAttribute("title");
item.Url = String.IsNullOrEmpty(reader.GetAttribute("url")) ? "" : reader.GetAttribute("url").ToLower();
item.Description = String.IsNullOrEmpty(reader.GetAttribute("description")) ? "" : reader.GetAttribute("description").ToLower();
category.Add(item);
}
while (reader.ReadToNextSibling("Item"));
}

do while 사이에 SubMenu 부분이 들어가나요?



이방은님의 글 (7/24/2008 4:10:56 PM)
깊이를 보자면
Category
- MenuItem
-- MenuSubItem
이라는 형식 같군요..
맞나요??
이 계층 구조가 맞다면 item 이라는 노드 아래(자식노드로) subItem 이 존재하게 되니
진성님 말처럼 구현하여야 합니다.
앞서 말씀 드렸듯이
3뎁스로 가게 되면 현재 MenuCategory 클래스에 List<MenuItem> 형식의 속성이 있는 것 처럼
MenuItem 클래스에 List<MenuSubItem> 이라는 속성이 있어야 합니다. 예를 들어 SubItems 라고 하죠..
pubic List<MenuSubItem> SubItems
{get;set;}
그래서 위 루프에서 List<MenuSubItem> 형식의 개체를 만들고 이를
MenuItem의 SubItems 에 Add 하시면 되겠네요..
그렇게 구성하시면 3뎁스의 트리 구조가 나옵니다..^^



김진성님의 글 (7/24/2008 11:18:49 PM)
네... 방실님 말처럼 했는데... 에러가 나더라구요.... 어느부분이 잘못됐는지 똑같이 했는데...ㅡㅡ



이방은님의 글 (7/25/2008 9:13:53 AM)
오류 나는 부분만..질문게시판에 올려 주세요..
함 봐볼께요..
전체 소스 주시지 마시고요...메뉴 관련부분만 발췌해서요
컴파일 가능한 상태의 프로젝트로 부탁합니다.



댓글 쓰기

Configuration을 Custom으로 사용해 보자 - 마지막

현재 주소 복사
트랙백 주소 복사
방실이님의 글 (1/3/2008 10:23:06 AM) Viewing : 3670

Configuration 관련 마지막 강좌가 되겠다..
지금까지 잘 따라온 구독자들에게 경의를 표한다..사실 쓰잘데기 없는 글이지만서도..쿨럭...

여튼..앞서 말했듯이 이번에는 각각의 Element의 속성이 다를 경우에는 어떻게 사용하는지...그 방법에 대해 알아볼 생각이다.
앞서 보여 주었던 방법은 아마도 천편일률적으로 같을 것이나 지금 보여 주는 방법은...뭐...사람마다 다를 것으로 판단된다..ㅡ.ㅡ;
다른 더 좋은 방법들이 있다면..소개도..아울러 부탁 드리는 바이다...음트트트트....

 


 

각 Element 들의 속성이 다르다면 Section 개체를 부르게 될때 예외를 던지게 된다. 해당 속성을 찾을수 없다는 예외이다.
이 문제 때문에 상당히 고민을 많이 했었으나...
Provider 는 되지 않던가...라는 생각을 하게 되었고 Provider를 분석하여 Provider와 유사한 동작을 하게끔 하여 구현을 하였다..

<VideoSettings card="DS4004HCI">
    <ServerSettings>
        <add name="Quality" type="BIT.TM.DreamCare.Configurations.VideoSettings.QualityElement" level="middle"/>
        <add name="Parameter" type="BIT.TM.DreamCare.Configurations.VideoSettings.ParameterElement" brightness="1" contrast="2" saturation="3" hue="4" />
        <add name="Audio" type="BIT.TM.DreamCare.Configurations.VideoSettings.AudioElement"  state="true" volume="255" />
    </ServerSettings>
    <ClientSettings>
        <add name="Quality" type="BIT.TM.DreamCare.Configurations.VideoSettings.QualityElement" level="middle"/>
        <add name="Parameter" type="BIT.TM.DreamCare.Configurations.VideoSettings.ParameterElement" brightness="5" contrast="6" saturation="7" hue="8" />
        <add name="Audio" type="BIT.TM.DreamCare.Configurations.VideoSettings.AudioElement"  state="true" volume="255" />
    </ClientSettings>
</VideoSettings>

 

 위 형제 Element 들에게서 공통점을 찾아 보자면 name과 type 이라는 속성이 있다. 역시 provider와 비슷하지 않은가?? ㅋㅋㅋ
각각의 Element 들은 type이라는 속성에서 나머지 속성들을 Property로 가지고 있는 클래스의 전체 형식을 정의 한다.
결국 이 Element들은 load가 될때 type이라는 속성을 읽어서 이 문자열로 해당 클래스의 개체를 만들어 나머지 속성의 정의 값들을 매핑하는 구조이다.
결국 사용자는 ServerSettings 컬렉션이 있는 Quality라는 Element의 속성인 level의 값을 사용하는 것이 아니라 (이 Element 개체에는 level이라는 속성은 없다.) Quality라는 Element의 속성인 type에 정의되어 있는 형식의 개체의 level이라는 속성의 값을 사용하는 것이 되겠다. 

자 이제 코드를 살펴 보도록 하자.
SectionGroup의 경우엔 앞서의 강좌에서 살펴 보았으니 Section 부터 살펴 보도록 하겠다.

    /// <summary>
    /// Video 세팅을 위한 섹션 클래스 Ver 1.0
    /// </summary>
    /// <remarks>
    /// <para>07.12.31 - Prototype - 민성욱</para>
    /// </remarks>
    public class VideoSettingsSection : ConfigurationSection
    {
        /// <summary>
        /// <see cref="CustomElementCollection"/> 개체를 가져옵니다.
        /// </summary>
        /// <value><see cref="CustomElementCollection"/> 개체입니다</value>
        [ConfigurationProperty("ServerSettings")]
        public CustomElementCollection ServerSettings
        {
            get
            {
                return (CustomElementCollection)base["ServerSettings"];
            }
        }
 
        /// <summary>
        /// <see cref="CustomElementCollection"/> 개체를 가져옵니다.
        /// </summary>
        /// <value><see cref="CustomElementCollection"/> 개체입니다.</value>
        [ConfigurationProperty("ClientSettings")]
        public CustomElementCollection ClientSettings
        {
            get
            {
                return (CustomElementCollection)base["ClientSettings"];
            }
        }
 
        /// <summary>
        /// 해당 요소의 화상카드 이름을 가져오거나 설정합니다
        /// </summary>
        /// <value>화상카드의 이름입니다</value>
        [ConfigurationProperty("card")]
        public bool Card
        {
            get
            {
                return (bool)base["card"];
            }
        }
 
        /// <summary>
        ///    <c>name</c> 값으로 <see cref="CustomElementCollection"/> 클래스의 컬렉션에서 <see cref="ElementBase"/>개체를 얻어 옵니다.
        /// </summary>
        /// <param name="name">검색할 이름(키)입니다.</param>
        /// <returns><see cref="ElementBase"/>개체입니다.</returns>
        public ElementBase GetClientElement(string name)
        {
            IEnumerator enumerator = this.ClientSettings.GetEnumerator();
            while (enumerator.MoveNext())
            {
                CustomElement video = (CustomElement)enumerator.Current;
                if (video.Name == name)
                {
                    Type instance = Type.GetType(video.Type);
                    ElementBase item = ((ElementBase)Activator.CreateInstance(instance));
                    item.Initialize(video.Parameters);
                    return item;
                }
            }
            return null;
        }
 
        /// <summary>
        ///    <c>name</c> 값으로 <see cref="CustomElementCollection"/> 클래스의 컬렉션에서 <see cref="ElementBase"/>개체를 얻어 옵니다.
        /// </summary>
        /// <param name="name">검색할 이름(키)입니다.</param>
        /// <returns><see cref="ElementBase"/>개체입니다.</returns>
        public ElementBase GetServerElement(string name)
        {
            IEnumerator enumerator = this.ServerSettings.GetEnumerator();
            while (enumerator.MoveNext())
            {
                CustomElement video = (CustomElement)enumerator.Current;
                if (video.Name == name)
                {
                    Type instance = Type.GetType(video.Type);
                    ElementBase item = ((ElementBase)Activator.CreateInstance(instance));
                    item.Initialize(video.Parameters);
                    return item;
                }
            }
            return null;
        }
    }

 

역시 하위 노드인 ElementCollection 개체를 얻는 프라퍼티가 2개 정의되었다. 
천천히 살펴 보면서 앞서의 강좌와 코드를 비교해 보기 바란다.

GetServerElement라는 메서드를 살펴 보자..똑같은 이름의 메서드는 앞선 강좌에도 존재 했었다.
ElementCollection에는 CustomElement라는 개체가 들어 있고 이 중에 type이라는 속성의 값을 가져와 인스턴스를 만드는 과정을 볼 수 있다.
어쨋든 여기서 조금전에 말했던 type속성에 정의된 개체를 만든다는 것을 알게 되었다.
아직은 잘 모르겠지만 해당 item(이하 착오를 줄이기 위해 ConfigurationElement 를 상속 받는 Element 들은 Element 라고 칭하고 그 Element의 type 속성에 정의된 클래스는 item 이라고 칭하겠다.헤깔리지 않도록..)
은 Initialize라는 초기화 메서드를 호출하는 것을 볼 수 있다.

이제 ElementCollection을 살펴 보자 이전 강좌의 그것과 동일하다.

    /// <summary>
    /// Element 요소 컬렉션 클래스 Ver 1.0
    /// </summary>
    /// <remarks>
    /// <para>http://www.bangsil.net</para>
    /// <para>07.12.31 - Prototype - 방실</para>
    /// </remarks>
    public class CustomElementCollection : ConfigurationElementCollection
    {
        /// <summary>
        /// 파생 클래스에서 재정의할 때 새 <see cref="T:System.Configuration.ConfigurationElement"></see>를 만듭니다.
        /// </summary>
        /// <returns>
        /// 새 <see cref="T:System.Configuration.ConfigurationElement"></see>입니다.
        /// </returns>
        protected override ConfigurationElement CreateNewElement()
        {
            return new CustomElement();
        }
 
        /// <summary>
        /// 파생 클래스에서 재정의될 때 지정된 구성 요소의 요소 키를 가져옵니다.
        /// </summary>
        /// <param name="element">키를 반환할 <see cref="T:System.Configuration.ConfigurationElement"></see>입니다.</param>
        /// <returns>
        /// 지정된 <see cref="T:System.Configuration.ConfigurationElement"></see>의 키로 사용되는 <see cref="T:System.Object"></see>입니다.
        /// </returns>
        protected override object GetElementKey(ConfigurationElement element)
        {
            return ((CustomElement)element).Name;
        }
    }

역시 같은 2개의 메서드만 재정의 되어 있다. 

이제 CustomElement클래스를 살펴 보자.

     /// <summary>
     /// Element 요소의 속성값 읽기 클래스 Ver 1.0
     /// </summary>
     /// <remarks>
     /// <para>http://www.bangsil.net</para>
     /// <para>07.12.31 - Prototype - 방실</para>
     /// </remarks>
     public class CustomElement : ConfigurationElement
     {
         private ConfigurationPropertyCollection properties;
         private ConfigurationProperty name;
         private ConfigurationProperty type;
         private NameValueCollection propertyNameCollection;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="CustomElement"/> class.
         /// </summary>
         public CustomElement()
         {
             this.name = new ConfigurationProperty("name", typeof(string), string.Empty, ConfigurationPropertyOptions.IsKey | ConfigurationPropertyOptions.IsRequired);
             this.type = new ConfigurationProperty("type", typeof(string), string.Empty, ConfigurationPropertyOptions.IsRequired);
             this.properties = new ConfigurationPropertyCollection();
             this.properties.Add(name);
             this.properties.Add(type);
         }
 
         /// <summary>
         /// 해당 요소의 이름(키)를 가져옵니다.
         /// </summary>
         /// <value>이름(키)입니다.</value>
         [ConfigurationProperty("name", IsRequired = true, IsKey = true)]
         public string Name
         {
             get
             {
                 return (string)base["name"];
             }
         }
 
         /// <summary>
         /// 해당 요소와 연결된 타입(클래스)을 가져옵니다
         /// </summary>
         /// <value>타입(클래스)입니다</value>
         [ConfigurationProperty("type", IsRequired = true)]
         public string Type
         {
             get
             {
                 return (string)base["type"];
             }
         }
 
         /// <summary>
         /// 해당 요소들의 속성(Property) 및 값(Value)의 컬렉션을 가져옵니다
         /// </summary>
         /// <value>속성(Property) 및 값(Value) 컬렉션입니다</value>
         internal NameValueCollection Parameters
         {
             get
             {
                 if (this.propertyNameCollection == null)
                 {
                     lock (this)
                     {
                         this.propertyNameCollection = new NameValueCollection(StringComparer.Ordinal);
                         foreach (ConfigurationProperty property in this.properties)
                         {
                             if ((property.Name.ToLower() != "name") && (property.Name.ToLower() != "type"))
                             {
                                 this.propertyNameCollection.Add(property.Name, (string)base[property]);
                             }
                         }
                     }
                 }
                 return this.propertyNameCollection;
             }
         }
 
         /// <summary>
         /// deserialize하는 동안 알 수 없는 특성을 발견했는지 여부를 나타내는 값을 가져옵니다.
         /// </summary>
         /// <param name="name">인식할 수 없는 특성의 이름입니다.</param>
         /// <param name="value">인식할 수 없는 특성의 값입니다.</param>
         /// <returns>
         /// deserialize하는 동안 알 수 없는 특성을 발견했으면 true입니다.
         /// </returns>
         protected override bool OnDeserializeUnrecognizedAttribute(string name, string value)
         {
             ConfigurationProperty property = new ConfigurationProperty(name, typeof(string), value);
             this.properties.Add(property);
             this.Parameters[name] = value;
             return true;
         }
 
         /// <summary>
         /// 파생 클래스에서 구현될 때 이 구성 요소가 마지막으로 저장되거나 로드된 이후 수정되었는지 여부를 나타냅니다.
         /// </summary>
         /// <returns>요소가 수정되었으면 <c>true</c>이고, 그렇지 않으면 <c>false</c>입니다.</returns>
         protected override bool IsModified()
         {
             if (!this.UpdatePropertyCollection())
             {
                 return base.IsModified();
             }
             return true;
         }
 
         /// <summary>
         /// 속성 컬렉션의 요소 및 값을 읽어와 컬렉션을 갱신합니다
         /// </summary>
         /// <returns><see cref="propertyNameCollection"/>객체에 값을 갱신여부입니다</returns>
         internal bool UpdatePropertyCollection()
         {
             bool flag = false;
             List<string> list = null;
             if (this.propertyNameCollection != null)
             {
                 foreach (ConfigurationProperty property in this.properties)
                 {
                     if ((property.Name == "name") || (property.Name == "type") || (this.propertyNameCollection.Get(property.Name) != null))
                     {
                         continue;
                     }
                     if (list == null)
                     {
                         list = new List<string>();
                     }
                     list.Add(property.Name);
                     flag = true;
 
                 }
                 if (list != null)
                 {
                     foreach (string listElement in list)
                     {
                         this.properties.Remove(listElement);
                     }
                 }
                 foreach (string propertyName in this.propertyNameCollection)
                 {
                     string propertyValue = this.propertyNameCollection[propertyName];
                     string basePropertyValue = this.GetProperty(propertyName);
                     if ((basePropertyValue == null) || (propertyValue != basePropertyValue))
                     {
                         this.SetProperty(propertyName, propertyValue);
                         flag = true;
                     }
                 }
             }
             this.propertyNameCollection = null;
             return flag;
         }
 
         /// <summary>
         /// 속성 컬렉션을 가져옵니다.
         /// </summary>
         /// <value></value>
         /// <returns>요소에 대한 속성의 <see cref="T:System.Configuration.ConfigurationPropertyCollection"></see> 컬렉션입니다.</returns>
         protected override ConfigurationPropertyCollection Properties
         {
             get
             {
                 this.UpdatePropertyCollection();
                 return properties;
             }
         }
 
         /// <summary>
         /// Property의 값을 설정합니다
         /// </summary>
         /// <param name="propName">속성(property)의 이름입니다</param>
         /// <param name="propValue">속성의 값(Value) 입니다</param>
         /// <returns>값 설정 시 <c>true</c>, 실패시 <c>false</c>입니다 </returns>
         private bool SetProperty(string propName, string propValue)
         {
             ConfigurationProperty property = null;
             if (this.properties.Contains(propName))
             {
                 property = this.properties[propName];
             }
             else
             {
                 property = new ConfigurationProperty(propName, typeof(string), null);
                 this.properties.Add(property);
             }
             if (property != null)
             {
                 base[property] = propValue;
                 return true;
             }
             return false;
         }
 
         /// <summary>
         /// Property의 값을 가져옵니다
         /// </summary>
         /// <param name="propName">속성(property)의 이름입니다</param>
         /// <returns><c>propName</c>이 존재 하지 않으면 null을 반환합니다.</returns>
         private string GetProperty(string propName)
         {
             if (this.properties.Contains(propName))
             {
                 ConfigurationProperty property = this.properties[propName];
                 if (property != null)
                 {
                     return (string)base[property];
                 }
             }
             return null;
         }
     }

얼핏 보기에도 이전 강좌에서 보았던 Element 보다 상당히 많은 메서드들이 재정의 되었다.
하나씩 살펴 보도록 하자.
앞서 강좌에서 말했듯이 이번에는 정적변수가 아니라 필드로 정의해 보았다.
이 클래스에서 앞서 강좌의 Element와 마찬가지로 properties 라는 변수를 만들고 name과 type 속성을 만들어 properties 컬렉션이 넣었다.
그러나 추가로 생긴 필드가 있으니 propertyNameCollection 이라는 변수이다. 
이 변수는 config파일의 이 Element에서 정의된 name과 type이외의 속성들에 대한 name과 value를 담고 있는 NameValueCollection의 개체이다.

57행의 Parameters 라는 속성이 보이는가? 
이 속성은 propertyNameCollection에 name과 type 속성이 아닌 나머지 속성들을 name과 value로 저장하여 이를 반환하는 역할을 한다.
Quality의 경우에는 level이라는 name과 middle이라는 value가 들어 가게 되는 것이다.
물론 Audio라는 Element에는 state라는 name과 volumn이라는 name이 각각의 value와 키/값 쌍으로 들어 가게 되겠다.

87행의 OnDeserializeUnrecongnizedAttribute 라는 메서드를 살펴 보자.
이 메서드는 config 파일에서는 존재하나 해당 Element 클래스에서는 정의되지 않은 속성이 발견되면 내부적으로 호출되는 메서드이다.
이 메서드에서 인식 할 수 없는 name/value가 Parameters 속성에 의해 동적으로 properties 필드에 추가 하게 되고 (90행) NameValueCollection에 저장되게 된다.(91행)

112행의 UpdatePropertyCollection 메서드를 살펴 보자.
이 메서드에서 실제로 속성의 값이 변경된다면 적용을 하는 코드가 들어 있다.
propertyNameCollection이 null 이 아닐 경우에는 (즉 Parameters Property에서 값이 정의되었을 경우) 해당 루틴을 타게 된다.
즉 정상적인 경우에는 124행~138행의 루틴은 타지 않는다..!!!
161행의 foreach루틴은 속성의 값이 변경된다면 타는 루틴이 되겠다. 즉 값만 가져오는 것이라면 foreach 문 안에 있는 if문은 항상 false를 반환한다. 
결론적으로 값이 변경된점이 없다면 이 메서드는 false를 반환한다.
이 메서드는 데이터가 갱신이 되는 시점이 어느곳이든지 상관 없이 많은 메서드와 프라퍼티에서 호출은 하지만 실제적으로 데이터가 없데이트 되는 곳은 99행의 IsModified 메서드에서이다.

99행의 IsModified 메서드를 보자.
값이 수정되었다면 이 메서드는 항상 true를 반환한다. 즉 UpdatePropertyCollection메서드는 true를 반환한다는 의미이다 base.IsModified메서드는 항상...false만 반환한다.

이제 이 Element의 type 속성에서 정위된 item 클래스를 살펴 보도록 하자.
이 item 클래스는 ElementBase 라는 추상 클래스를 상속 받도록 되어 있다.

/// <summary>
/// Element 요소의 상위 클래스 Ver 1.0
/// </summary>
/// <remarks>
/// <para>http://www.bangsil.net</para>
/// <para>07.12.31 - Prototype - 방실</para>
/// </remarks>
public abstract class ElementBase
{
    private NameValueCollection parameter;

    /// <summary>
    /// <see cref="CustomElement"/> 클래스에서 가져온 속성,값 쌍을 각 하위 Element에 초기화한다
    /// </summary>
    /// <param name="parameter">속성(property)및 값(Value)의 컬렉션입니다</param>
    public virtual void Initialize(NameValueCollection parameter)
    {
        this.parameter = parameter;
    }

    /// <summary>
    /// 값(value)를 가져옵니다
    /// </summary>
    /// <param name="key">요소의 키입니다</param>
    /// <returns>해당하는 키의 값(value)입니다</returns>
    public object GetValue(string key)
    {
        return this.parameter[key];
    }

    /// <summary>
    /// 값(value)를 설정합니다
    /// </summary>
    /// <param name="key">요소의 키입니다</param>
    /// <param name="value">설정할 값(value)입니다</param>
    public void SetValue(string key, object value)
    {
        this.parameter[key] = value.ToString();
    }
}

먼저 Initialize 메서드가 보인다 이 메서드는 VideoSettingsSection 이라는 Section 클래스에서 사용되었었다.
매개변수로 NameValueCollection을 받으며 이것을 내부 필드로 가지고 있을 뿐이다. 그리고 이 값은 값을 가져오거나(GetValue) 설정할 때(SetValue) 사용된다.
또 이 NameValueCollection형의 매개변수는 앞서 살펴 보았던 CustomElement 클래스의 Paramters 속성을 사용한다.

자 이제 이 추상 클래스를 상속받는 item 클래스를 살펴 보도록 하자.

/// <summary>
/// AudioElement 요소의 클래스 Ver 1.0
/// </summary>
/// <remarks>
/// <para>07.12.31 - Prototype - 민성욱</para>
/// </remarks>
public class AudioElement : ElementBase
{
    /// <summary>
    /// <see cref="AudioElement"/> 요소의 속성 및 값을 설정합니다
    /// </summary>
    /// <param name="parameter">속성(Property) 및 값(Value)의 컬렉션입니다</param>
    public override void Initialize(NameValueCollection parameter)
    {
        if (parameter["volume"] != null)
        {
            int result;
            if (Int32.TryParse(parameter["volume"].ToString(), out result))
            {
                if (result < 0 || result > 255)
                {
                    throw new ArgumentOutOfRangeException("volume이 범위를 넘어섰습니다.");
                }
            }
            else
            {
                throw new FormatException("volume 타입이 잘못되었습니다.");
            }
        }
        else
        {
            throw new ArgumentNullException("volume 값이 없습니다.");
        }

        if (parameter["state"] != null)
        {
            try
            {
                bool.Parse(parameter["state"].ToString());
            }
            catch (FormatException)
            {
                throw new FormatException("state 값이 형식에 맞지 않습니다.");
            }
        }
        else
        {
            throw new ArgumentNullException("state 값이 없습니다.");
        }
        base.Initialize(parameter);
    }
}

Audio 클래스를 보겠다.
이 클래스를 보면 조금 전에 정의한 ElementBase 라는 추상 클래스를 상속 받고 단지 Initialize() 메서드만 호출 했다..
게다가 이 메서드의 용도를 보면..단순히 입력된 값의 유효성 검증만 하고 있다. 
(이런 유효성 검증 코드를 사용 하고 있는가?????? 상당히 귀찮은 작업이지만..중요하다고 생각된다..)

휴...정말 긴 글이었다..

이제 마지막으로 사용법을 알아 보자..
사용법은 이전 강좌에서 보여주었던 것과 똑같은 패턴이다...

 

ConfigurationHelper.VideoSettingsSection.GetClientElement("Quality").GetValue("level").ToString();
ConfigurationHelper.VideoSettingsSection.GetClientElement("Quality").SetValue("level", "high");
ConfigurationHelper.Save();

Save 메서드를 호출하지 않으면 메모리상에서만 값이 변경된다..즉 config파일에 쓰지 않게 된다.

Configuration을 처음 접해본 사람이라면 조금 어려울 수도 있는 강좌이지만..
이것 역시 한번 접해 본다면..아무것도 아닌 것을 느끼게 될것이다.

설정들을 파일에 저장하거나 레지스트리에 저장하거나 하는 것보다..어째 폼나 보이지 않는가??? ㅋㄷㅋㄷ


마지막 업데이트 : (10/5/2010 1:01:57 PM)

TAG : Configuration 



Trackback 보기 (0)
댓글 보기 (0)
댓글 쓰기

Configuration을 Custom으로 사용해 보자 - 두번째

현재 주소 복사
트랙백 주소 복사
방실이님의 글 (1/2/2008 6:01:15 PM) Viewing : 4054

앞서 언급했던 2가지 방식중 같은 속성을 가진 Element를 가지고 있는 Section을 구성해 보도록 하겠다.
(첨부 파일이 없기에 모든 코드를 제시한다..보기에 불편 하더라도..이해를..^^)

    ///

    /// 최상위 섹션 그룹 클래스 Ver 1.0

    ///

    ///

    /// http://www.bangsil.net

    /// 07.12.14 - Prototype - 방실

    /// 07.12.31 - VideoSettings 추가 - 민성욱

    ///

    public class AncestorSectionGroup : ConfigurationSectionGroup

    {

        ///

        /// 개체를 가져옵니다.

        ///

        /// 사용하지 않습니다.

        [ConfigurationProperty("IPSettings", IsRequired = true)]

        public IPSettingsSection IPSettingsSection

        {

            get

            {

                return (IPSettingsSection)base.Sections["IPSettings"];

            }

        }

        ///

        /// 개체를 가져옵니다.

        ///

        /// 사용하지 않습니다.

        [ConfigurationProperty("VideoSettings", IsRequired = true)]

        public VideoSettingsSection VideoSettingsSection

        {

            get

            {

                return (VideoSettingsSection)base.Sections["VideoSettings"];

            }

        }

    }

먼저 SectionGroup을 구성해 보겠다.
SectionGroup클래스는 ConfigurationSectionGroup 클래스를 상속 받아야 한다.
이 클래스에서 정의된 부분은 이 그룹에 속한 Section 개체를 얻어 오는 프라퍼트 뿐이다.

이는 Sections라는 인덱서에서 섹션 이름으로 조회해서 가져오는 방식이다.

ConfigurationProperty 라는 어트리뷰트는 실제 xml 파일에서 표현되는 속성의 이릅이다, 이는 대소문자를 구분한다.

이제 Section 클래스를 구성해 보자.

    ///

    /// IP세팅을 위한 섹션 클래스 Ver 1.0

    ///

    ///

    /// http://www.bangsil.net

    /// 07.12.14 - Prototype - 방실

    ///

    public class IPSettingsSection : ConfigurationSection

    {

        ///

        /// 개체를 가져옵니다.

        ///

        /// 개체입니다

        [ConfigurationProperty("ServerSettings")]

        public ServerElementCollection ServerSettings

        {

            get

            {

                return (ServerElementCollection)base["ServerSettings"];

            }

        }

 

        ///

        /// 개체를 가져옵니다.

        ///

        /// 개체입니다.

        [ConfigurationProperty("ClientSettings")]

        public ClientElementCollection ClientSettings

        {

            get

            {

                return (ClientElementCollection)base["ClientSettings"];

            }

        }

 

        ///

        ///    name 값으로 클래스의 컬렉션에서 개체를 얻어 옵니다.

        ///

        /// 검색할 이름(키)입니다.

        /// 개체입니다.

        public ClientElement GetClientElement(string name)

        {

            IEnumerator enumerator = this.ClientSettings.GetEnumerator();

            while (enumerator.MoveNext())

            {

                if (name == ((ClientElement)enumerator.Current).Name)

                {

                    return (ClientElement)enumerator.Current;

                }

            }

            return null;

        }

 

        ///

        ///    name 값으로 클래스의 컬렉션에서 개체를 얻어 옵니다.

        ///

        /// 검색할 이름(키)입니다.

        /// 개체입니다.

        public ServerElement GetServerElement(string name)

        {

            IEnumerator enumerator = this.ServerSettings.GetEnumerator();

            while (enumerator.MoveNext())

            {

                if (name == ((ServerElement)enumerator.Current).Name)

                {

                    return (ServerElement)enumerator.Current;

                }

            }

            return null;

        }

    }

Section 클래스 또한 ConfigurationSection 클래스를 상속 받아야 한다.
마찬 가지로 하위 노드의 개체를 얻을 수 있는 속성을 가지고 있어야 한다.
ServetSettings와 ClientSettings 가 그것이다.

그리고 마지막으로 클라이언트(UI 단계)에서 Element 클래스에 접근 할 수 있도록 Get~ Element 메서드를 생성한다.
이 메서드는 ElementCollection의 Enumerator에서 이름속성의 값으로 조회한다.

이제 다음은 ElementCollection 클래스를 정의 한다.

    ///

    /// 클래스의 컬렉션 클래스 Ver 1.0

    ///

    ///

    /// http://www.bangsil.net

    /// 07.12.14 - Prototype - 방실

    ///

    public class ClientElementCollection : ConfigurationElementCollection

    {

        ///

        /// 파생 클래스에서 재정의할 때 새 를 만듭니다.

        ///

        ///

        ///입니다.

        ///

        protected override ConfigurationElement CreateNewElement()

        {

            return new ClientElement();

        }

 

        ///

        /// 파생 클래스에서 재정의될 때 지정된 구성 요소의 요소 키를 가져옵니다.

        ///

        /// 키를 반환할 입니다.

        ///

        /// 지정된 의 키로 사용되는 입니다.

        ///

        protected override object GetElementKey(ConfigurationElement element)

        {

            return ((ClientElement)element).Name;

        }

    }

이 ElementCollection 클래스 역시 ConfigurationElementCollectoin 클래스를 상속 받으며 2개의 메서드를 재정의 해야 한다.
이 메서드는 실제 사용자 코드에서는 사용되지 않지만, 내부적으로 호출 되는 메서드이며 반드시 정의하여야 한다.

이제 마지막으로 Element 클래스만이 남았다.

    ///

    /// ClientSettings 요소의 클래스 Ver 1.0

    ///

    ///

    /// http://www.bangsil.net

    /// 07.12.14 - Prototype - 방실

    /// 07.12.17 - portForVideo 속성 추가 - 민성욱

    /// 08.01.01 - 구조수정 - 민성욱

    ///

    public class ClientElement : ConfigurationElement

    {

        private static ConfigurationPropertyCollection properties;

        private static readonly ConfigurationProperty name;

        private static readonly ConfigurationProperty ipAddress;

        private static readonly ConfigurationProperty portForFileSharing;

        private static readonly ConfigurationProperty portForVideo;

 

        ///

        /// 클래스의 정적 생성자입니다.

        ///

        static ClientElement()

        {

            name = new ConfigurationProperty("name", typeof(string), string.Empty, ConfigurationPropertyOptions.IsKey | ConfigurationPropertyOptions.IsRequired);

            ipAddress = new ConfigurationProperty("ipAddress", typeof(string), string.Empty, ConfigurationPropertyOptions.IsRequired);

            portForFileSharing = new ConfigurationProperty("portForFileSharing", typeof(Int32), 9098, ConfigurationPropertyOptions.IsKey | ConfigurationPropertyOptions.IsRequired);

            portForVideo = new ConfigurationProperty("portForVideo", typeof(Int32), 6050, ConfigurationPropertyOptions.IsKey | ConfigurationPropertyOptions.IsRequired);

            properties = new ConfigurationPropertyCollection();

            properties.Add(name);

            properties.Add(ipAddress);

            properties.Add(portForFileSharing);

            properties.Add(portForVideo);

        }

 

        ///

        /// 클래스의 생성자입니다.

        ///

        internal ClientElement() { }

 

        ///

        /// 해당 요소의 이름(키)를 가져옵니다.

        ///

        /// 이름(키)입니다.

        [ConfigurationProperty("name", IsRequired = true, IsKey = true)]

        [StringValidator(InvalidCharacters = "[a-zA-Z]*", MinLength = 1, MaxLength = 255)]

        public string Name

        {

            get

            {

                return (string)base[name];

            }

        }

 

        ///

        /// 해당 요소의 아이피 주소를 가져오거나 설정합니다.

        ///

        /// 아이피 주소입니다.

        [ConfigurationProperty("ipAddress", IsRequired = true)]

        [StringValidator(InvalidCharacters = @"[0-9]*\.[0-9]*\.[0-9]*\.[0-9]*", MinLength = 1, MaxLength = 255)]

        public string IPAddress

        {

            get

            {

                return ((string)base[ipAddress]).Trim();

            }

            set

            {

                base[ipAddress] = value.Trim();

            }

        }

 

        ///

        /// 해당 요소의 파일 공유를 위한 포트 번호를 가져오거나 설정합니다.

        ///

        /// 파일 공유를 위한 포트 번호입니다.

        [ConfigurationProperty("portForFileSharing", IsRequired = true, IsKey = true)]

        [IntegerValidator(ExcludeRange = true, MinValue = 0, MaxValue = 65535)]

        public Int32 PortForFileSharing

        {

            get

            {

                return (Int32)base[portForFileSharing];

            }

            set

            {

                base[portForFileSharing] = value;

            }

        }

 

        ///

        /// 해당 요소의 비디오를 위한 포트 번호를 가져오거나 설정합니다.

        ///

        /// 비디오를 위한 포트 번호입니다.

        [ConfigurationProperty("portForVideo", IsRequired = true, IsKey = true)]

        [IntegerValidator(ExcludeRange = true, MinValue = 0, MaxValue = 65535)]

        public Int32 PortForVideo

        {

            get

            {

                return (Int32)base[portForVideo];

            }

            set

            {

                base[portForVideo] = value;

            }

        }

 

        ///

        /// 속성 컬렉션을 가져옵니다.

        ///

        ///

        /// 요소에 대한 속성의 컬렉션입니다.

        protected override ConfigurationPropertyCollection Properties

        {

            get

            {

                return properties;

            }

        }

 

        ///

        /// Configuration 요소의 속성 Value를 가져옵니다

        ///

        /// 속성(Property)의 이름입니다

        /// 해당 속성의 값입니다

        public object GetValue(string propertyName)

        {

            return base[propertyName];

        }

 

        ///

        /// Configuration 속성 Value값을 설정합니다

        ///

        /// 속성(Property)의 이름입니다

        /// 요소에 들어갈 값(Value)입니다

        public void SetValue(string propertyName, object value)

        {

            base[propertyName] = value;

        }

    }

마지막으로 정의해야 할 Element 클래스 역시 ConfigurationElement 클래스를 상속 받아야 한다.
앞서와 마찬가지로 사용되는 속성에 대하여 Property를 정의 해야 한다.
속성을 정의 함에 있어 사용된 IntegerValidator와 StringValidator 등의 어트리뷰트는 속성의 입력값에 대한 검증을 해준다.

정적 변수 및 정적 생성자를 이용한 이유는...이유없다..그냥..이랄까..다음 강좌에서는 Field 로 정의 한것을 보여 주도록 하겠다.

잘 살펴 보아야 하는 개체는 ConfigurationPropertyCollection 형의 properties 라는 개체이다.
이 개체는 정의된 속성들을 담고 있는 컬렉션이 되겠다.
이 컬렉션은 Properties 라는 속성을 재정의 하여 properties 라는 값을 가져온다.
이 속성 역시 내부적으로 사용되는 속성이면 반드시 재정의 하여야 하겠다.

마지막으로 Element의 값을 가져오기위한 GetValue와 SetValue 메서드를 만들어 놓았다.

자 이제 사용하기 위한 모든 조건이 완성 되었다. 이 설정을 코드에서 가져오고 새로운 값으로 저장해 보도록 하자.
바로 사용하여도 되지만..^^;
이것을 사용하기 쉽게 하기 위해 Helper 클래스를 하나 만들어 보자..어렵지 않다.

    ///

    /// 환경 설정 파일 도우미 클래스 Ver 1.0

    ///

    ///

    /// http://www.bangsil.net

    /// 07.12.14 - Prototype - 방실

    /// 07.12.31 - VideoSettingsSection, Save 메소드 추가 - 민성욱

    ///

    public class ConfigurationHelper

    {

        private static AncestorSectionGroup ancestor;

        private static Configuration config;

 

        ///

        /// 클래스의 정적 생성자입니다.

        ///

        static ConfigurationHelper()

        {

            config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);

            ancestor = (AncestorSectionGroup)(config.GetSectionGroup("BIT.TM.DreamCare"));

        }

 

        ///

        /// IPSettings 섹션요소의 값을 가져옵니다

        ///

        /// IP에 관련된 값의 섹션입니다

        public static IPSettingsSection IPSettingsSection

        {

            get

            {

                return ancestor.IPSettingsSection;

            }

        }

 

        ///

        /// VideoSettings 섹션요소의 값을 가져옵니다

        ///

        /// Video에 관련된 값의 섹션입니다

        public static VideoSettingsSection VideoSettingsSection

        {

            get

            {

                return ancestor.VideoSettingsSection;

            }

        }

 

        ///

        /// 변경된 Configuration의 값을 저장합니다

        ///

        public static void Save()

        {

            config.Save();

        }

    }

Helper 클래스의 정적 생성자를 보면 ConfigurationManager 클래스를 이용해서 config 파일을 읽어 오는 모습을 볼수 있다.
만약 사용자의 config 파일 depth의 구조가 SectionGroup 이 없는 즉, 상위 레벨의 노드가 Section이라면 GetSection 이라는 메서드를 통해서 해당 섹션을 가져 올 수 있다.
그러나 위 예시의 경우에는 SectionGroup을 사용하였기에 GetSectionGroup 메서드를 이용하여 SectionGroup 개체를 만들도록 하였다.
그리고 2개의 프라퍼티는 이 SectionGroup이 자식으로 가지고 있는 Section을 리턴하는 구조이다.
마지막으로 Save메서드는 사용자의 코드에 의해 변경된 config 값을 config 파일에 쓰도록 하는 메서드이다. 이 메서드를 호출하지 않으면 사용자가 새로운 값을 할당하였을때 메모리 상에서만 이 값이 반영되어 프로그램이 Reset 되었을 때는 다시 원래의 값을 읽혀 지게 되겠다.

자 이제 이렇게 구성한 파일들은 어떻게 사용하는지 살펴 보도록 하자.

Convert.ToInt32(ConfigurationHelper.IPSettingsSection.GetClientElement("MainServer").GetValue("ipAddress"));

ConfigurationHelper.IPSettingsSection.GetClientElement("MainServer").IPAddress;

위 처럼 2가지 방식으로 사용할 수 있다.
첫번째 방식은 Element에서 만들었던 GetValue() 메서드를 사용해서 문자열로 입력되는 속성이름을 넣고 값을 가져오는 방식이며 object형으로 리턴된다.
일반적으로는 2번째 방식을 사용하게 될 터이다.
Element 클래스에서 정의 했던 Property를 사용하는 방법이다..물론 Property를 사용하기 때문에 형도 엄격하게 정해 진다.

굳이 GetValue 라는 메서드를 만든 이유는 다음 강좌에서 살펴볼 속성의 종류가 다른 Element 일때에도 같은 메서드를 사용하여 코드의 일관성을 유지 하고 싶어서이다...@.@
값을 변경하고 저장하는 방법도 쉽다.

ConfigurationHelper.IPSettingsSection.GetClientElement("MainServer").SetValue("ipAddress","127.0.0.1");

ConfigurationHelper.Save();

SerValue() 메서드로 키과 값을 입력 하고 단순히 Save() 메서드만 호출 하면 되겠다...

자..설명이 어설픈면이 없잖아 있지만..

오늘 아티클을 이해해야 다음 아티클을 이해 할 수 있겠다...


마지막 업데이트 : (10/5/2010 12:49:30 PM)

TAG : Configuration 



Trackback 보기 (0)
댓글 보기 (0)
댓글 쓰기

Configuration을 Custom으로 사용해 보자 - 첫번째

현재 주소 복사
트랙백 주소 복사
방실이님의 글 (1/2/2008 5:32:25 PM) Viewing : 3717

정말 오랜만에..
아티클을 쓴다...
혹시나 기다렸던 분이 계신다면..(있을려나 흘흘흘) 죄송..

이번 아티클은 Configuration 을 사용자가 임의로 설정하여 사용하는 법을 알아 보도록 하겠다.
이르자면...아래와 같은 방법이다.
 
 
 
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <sectionGroup name="BIT.TM.DreamCare" type="BIT.TM.DreamCare.Configurations.AncestorSectionGroup, BIT.TM.DreamCare.Configurations">
      <section name="IPSettings" type="BIT.TM.DreamCare.Configurations.IPSettings.IPSettingsSection, BIT.TM.DreamCare.Configurations" />
      <section name="VideoSettings" type ="BIT.TM.DreamCare.Configurations.VideoSettings.VideoSettingsSection, BIT.TM.DreamCare.Configurations"/>
    </sectionGroup>
  </configSections>

  <BIT.TM.DreamCare>
    <IPSettings>
      <ServerSettings>
        <!--사용 예 : ConfigurationHelper.VideoSection.GetClientSettings("Audio").GetValue("State").ToString()-->
        <add name ="MainServer" ipAddress="124.198.16.131" port="1000"/>
        <add name ="ChatServer" ipAddress="124.198.16.131" port="1001"/>
      </ServerSettings>
      <ClientSettings>
        <add name="Client" ipAddress="124.198.16.131" portForFileSharing="9098" portForVideo="6050"/>
      </ClientSettings>
    </IPSettings>
    <VideoSettings card="DS4004HCI">
      <ServerSettings>
        <add name="Quality" type="BIT.TM.DreamCare.Configurations.VideoSettings.QualityElement" level="middle"/>
        <add name="Parameter" type="BIT.TM.DreamCare.Configurations.VideoSettings.ParameterElement" brightness="1" contrast="2" saturation="3" hue="4" />
        <add name="Audio" type="BIT.TM.DreamCare.Configurations.VideoSettings.AudioElement"  state="true" volume="255" />
      </ServerSettings>
      <ClientSettings>
        <add name="Quality" type="BIT.TM.DreamCare.Configurations.VideoSettings.QualityElement" level="middle"/>
        <add name="Parameter" type="BIT.TM.DreamCare.Configurations.VideoSettings.ParameterElement" brightness="5" contrast="6" saturation="7" hue="8" />
        <add name="Audio" type="BIT.TM.DreamCare.Configurations.VideoSettings.AudioElement"  state="true" volume="255" />
      </ClientSettings>
    </VideoSettings>
  </BIT.TM.DreamCare>
</configuration>
 
 
웹의 경우에는 web.config 라는 파일이 이에 해당되고 윈도우즈응용프로그램의 경우는 app.config 파일이 이에 해당된다.

보통 connectionString 이라는 것을 많이 사용하지 않는가?? 바로 이것이다.
위 샘플 예제는 윈도우즈응용프로그램에서 사용되는 app.config 의 한 예이다.
 
구성을 보게 되면
최상위 노드가 configuration이 되겠다.
이 상위 노드의 아래에 커스컴 Section들을 정의 하는 configSections 라는 노드가 있다.
바로 이 노드에서 사용되는 Section과 SectionGroup 들을 정의 하게 된다.
 
이번 아티클에서는 이 configuration을 사용하는 두가지 방법을 제시 해 볼까 한다.
그러므로 두개의 섹션을 정의해 보았다.
 
먼저 이 구조를 설명해 보자면
add 라고 붙어 있는 하나의 최하위 노드를 Element라고 일컫는다.
그 Element 의 부모 노드는 ElementCollection 이 되겠다.
그리고 이 ElementCollection의 집합인 부모 노드가 Section이 되며 이 Section들의 집합이 SectionGroup이 되겠다.
이해가 되는가??
 
위의 예제를 보게 되면
BIT.TM.DreamCare 라는 사용자정의 최상위 노드가 SectionGroup이 되며
그 아래에 있는 IPSettings와 VideoSettings가 Section이 될 것이며,
그 아래에 있는 ServerSettings와 ClientSettings는 ElementCollection이 되며
마지막으로 add되어 있는 것은 Element가 되겠다.
 
configSections 노드에서는 사용된 Section과 SectionGroup들을 정의 하게 된다.
이에 대한 부분은 MSDN을 참고 하도록 한다.
 
이제 우리가 만들어야 하는 클래스들은 위에서 언급했던
SectionGroup 클래스 각각의 Section 클래스, 각각의 ElementCollection 클래스와 Element 클래스이다.
위 샘플을 보게 되면 IPSettings와 VideoSettings 라는 2개의 섹션이 있는 이 둘사이에서는 커다란 차이가 있다
IPSections 의 경우에는 하위 Element의 속성(Property)가 fix 되어 있다.
즉 형제 Element가 가지고 있는 속성들은 모두 같다는 의미가 되겠다
MainServer Element와 ChatServer Element의 속성은 name, ipAddress, port 라는 것을 가지고 있으며...종류가 똑같다는 말이다.
 
그에 반에 VideoSettings 섹션을 보게 되면
Quality와 Parameter 라는 Element는 형제 요소이지만 그들이 가지고 있는 속성은 모두 다르다...
 
이게 가장 큰 차이점이 되겠다.
다음 아티클에서는 이 두가지를 정의하고 사용하는 방법을 알아 보도록 하겠다.
 

마지막 업데이트 : (10/5/2010 12:47:09 PM)

TAG : Configuration 



Trackback 보기 (0)
댓글 보기 (0)
댓글 쓰기

프로그램에서 엑셀(Excel)을 다루어 보자.(4)

현재 주소 복사
트랙백 주소 복사
방실이님의 글 (9/18/2007 5:07:00 PM) Viewing : 10594

Jakarta.POI 를 이용한 Excel Writing

자 이제 마지막 시간이다..^^;

http://jakarta.apache.org/poi/

위 URL을 참조 하면 jakarta.poi 에 대해서 정보를 얻을 수 있다.
이 어셈블리를 사용하기 위해서는 jakarta.poi.dll이 필요하며 웹의 경우 서버에 J#재배포패키지가 설치되어야 한다.
그리고 JSharplib.dll 파일을 참조 추가 하여야 한다.

자 이제 준비가 되었으니 사용법을 알아 보자.

먼저 Enum.cs 파일을 보게 되면 셀과 폰트에서 사용되는 열거형들이 정의되어 있다.
주석은 없지만...ㅡ.ㅡ;;;

외부에 노출되는 타입은 Enum.cs 에 정의되어 있는 열거형들과 ExcelHandler class, ExcelCellStyle class 이다.
ExcelCellSettingBase class는 internal로 정의되어 내부에서만 사용되는 클래스이다.
ExcelHandler 클래스는 엑셀파일을 생성하기위한 기본 클래스이다. 
ExcelCellStyle 클래스는 선택된 시트의 셀 영역에 적용할 스타일에 대한 클래스가 되겠다. 이르자면 폰트에 관련된 속성같은 것 말이다.

이 어셈블리의 구조를 먼저 설명을 해야 할 것 같다.

ExcelHandler 클래스는 내부적으로 List 형의 리스트를 가진다.  이 리스트는 DataSet형의 컬렉션인데 이 DataSet이 바로 하나의 Sheet이다.
즉 List.Count 는 시트의 갯수와 같다는 의미가 되겠다.
이제 이 DataSet의 구조를 살펴 보자.
이 DataSet에는 최소 1개 이상의 DataTable을 가진다.
이 DataTable은 하나의 데이터영역이다.
이르자면..타이틀영역, 빈공간, 데이터 넣는 곳 등등,,,
하나의 시트에 바인딩되는 데이터의 형태는 반드시 DataTable형태가 되어야 하며 이 DataTable들은 여러가지 형태로 시트에 표현이 될 수 있다.
샘플을 보게 되면 2개의 시트가 있는데 첫번째 시트의 경우에는 DataTable이 3개가 있다.  뒷 부분에서 설명하도록 하겠다.
그리고 시트의 설정에 대한 부분도 포함되어 있다. Split이라든지 Group 같은 기능들이 있다.

ExcelCellStyle 클래스는 데이터와 상관없이 셀의 스타일을 정의한다.
이 클래스는 테두리의 선 및 색상정의, 폰트의 종류,색상,크기등의 정의 등등의 속성이 있다.
이 클래스로 만들어진 스타일 인스턴스는 ExcelHandler.SetCellStyle()메서드로  시트에 임베디드된다.

이제 ExcelCellStyle 클래스부터 살펴 보겠다.

클릭하면 더 큰 그림으로 볼 수가 있을껄요

위 클래스 다이어그램을 보는 바와 같이 ExcelCellStyle 클래스는 모두 필드와 속성으로만 정의되어 있으며 내부 클래스인 Font 클래스를 가지고 있다.
이 클래스는 FontStyle이라는 속성으로 사용된다.
Align, Rotation, CellColor, Border, Font를 설정할 수 있다.

이제 핵심 클래스인 ExcelHandler 클래스를 살펴보자.

   11 using org.apache.poi.hssf.util;

   12 using org.apache.poi.hssf.usermodel;

이 어셈블리를 사용하기 위해서는 위처럼 2개의 네임스페이스를 등록하여야 한다.

   48     public delegate void RowCount(object sender, RowCountEventArgs e);

   49     public event RowCount OnRowCount;

앞서 OleExcelWriter에서 설명한 바와 같이 OnRowCount 라는 이벤트와 델리게이트를 생성하였다.
앞서 설명을 하였기 때문에 패스..

public ExcelHandler();

public ExcelHandler(List<string> sheetName);

public ExcelHandler(string fullName);

public ExcelHandler(string fullName, List<string> sheetName);

public ExcelHandler(string path, string fileName);

public ExcelHandler(string path, string fileName, List<string> sheetName);

생성자는 위와 같이 정의 되어 있다.
2개 이상의 시트가 있거나 시트의 이름을 정해주고자 할 때는 sheetName 매개변수를 이용하도록 한다. 주의사항은 한글이름은 지원이되지 않는다..ㅡ.ㅠ;

  789 ///

  790 /// 개체 생성

  791 ///

  792 private void Create()

  793 {

  794   this.wb = new HSSFWorkbook();

  795   foreach (string sheet in this.sheetName)

  796   {

  797     this.wb.createSheet(sheet);

  798   }

  799 }

생성자에서는 위의 Create()메서드를 호출하며 이 메서드에서는 마치 Automation과 마찬가지로 워크북을 생성한다음에  sheetName 리스트에서 할당된 갯수만큼 시트를 만든다.

이제 시트를 만들었다면 Data를 입력해야할 차례이다..다음 메서드를 살펴보자.

  506 public void InsertTable(int sheetIndex, bool hasTitle, DataTable dt)

  507 {

  508   if (this.dsList.Count == sheetIndex)

  509   {

  510     DataSet ds = new DataSet();

  511     this.dsList.Add(ds);

  512   }

  513   this.dsList[sheetIndex].Tables.Add(dt);

  514

  515   if (this.hasTitleList.Count == sheetIndex)

  516   {

  517     StringCollection sc = new StringCollection();

  518     this.hasTitleList.Add(sc);

  519   }

  520   this.hasTitleList[sheetIndex].Add(hasTitle.ToString());

  521 }

매개변수를 보자면.

  1. sheetIndex - 데이터를 넣을 시트의 인덱스
  2. hasTitle - 데이터테이블의 경우 해당 컬럼의 이름을 데이터의 상단에 렌더 할수 있다. 이 제목의 사용 여부이다.
  3. dt - 시트에 입력될 데이터테이블

처음에 구조를 설명하였듯이...
dsList 라는 제너릭리스트는 하나의 워크북을 의미하고 그 안에 있는 ds 라는 데이터셋은 하나의 시트를 의미하며 그 안에 입력되는 dt는 하나의 데이터영역이다.
hasTitleList는 hasTitle의 bool 값을 저장하고 있는 문자열컬렉션(StringCollection)의 제너릭리스트이다. 
dsList와 hasTitleList의 인덱스는 정확히..매칭된다.

  530 public void InsertBlankRow(int sheetIndex, int rowCount)

  531 {

  532   DataTable dt = new DataTable();

  533   dt.Columns.Add(new DataColumn());

  534   for (int i = 0; i < rowCount; i++)

  535   {

  536     DataRow dr = dt.NewRow();

  537     dr[0] = "";

  538     dt.Rows.Add(dr);

  539   }

  540   this.InsertTable(sheetIndex, false, dt);

  541 }

InsertBlackRow() 메서드는 해당 시트에 빈 공간(row)을 삽입하는 메서드이다.
몇 row의 빈칸을 삽입할지 결정하여 호출 하면 540라인에서 보듯이 InsertTable메서드를 호출하며 해당 DataSet(Sheet)에 값이 없는 (스키마만 있는) DataTable을 삽입하게 된다.

이제 데이터를 바인딩 하기 전에 바딩딩 할때 적용되로 스타일을 세팅 하도록 한다.

public void SetCellStyle(int sheetIndex, short startColumnIndex, short endColumnIndex, int startRowIndex, int endRowIndex, CellType type);

public void SetCellStyle(int sheetIndex, short startColumnIndex, short endColumnIndex, int startRowIndex, int endRowIndex, ExcelCellStyle cellStyle);

public void SetCellStyle(int sheetIndex, short startColumnIndex, short endColumnIndex, int startRowIndex, int endRowIndex, ExcelCellStyle cellStyle, CellType type);

스타일을 세팅하기위한 메서드는 SetCellStyle 메서드인데 3가지로 overload되어 있다.
매개변수를 살펴 보자.

  • sheetIndex - 스타일을 적용할 시트의 인덱스
  • startColumnIndex - 스타일을 적용할 영역의 시작 컬럼 인덱스
  • endColumnIndex - 스타일을 적용할 영역의 끝 컬럼 인덱스
  • startRowIndex - 스타일을 적용할 영역의 시작 로우 인덱스
  • endRowIndex - 스타일을 적용할 영역의 끝 로우 인덱스
  • cellStyle - ExcelCellStyle의 개체참조
  • type - 셀의 타입, 현재 정의된 타입은 string, int, datetime 형 3가지이다.

  301 public void SetCellStyle(int sheetIndex, short startColumnIndex, short endColumnIndex, int startRowIndex, int endRowIndex, ExcelCellStyle cellStyle, CellType type)

  302 {

  303   if (this.wb == null)

  304   {

  305     throw new Exception("워크북이 생성되지 않았습니다.");

  306   }

  307   else

  308   {

  309     if (this.wb.getSheetAt(sheetIndex) == null)

  310     {

  311       throw new Exception(sheetIndex + "인덱스에 시트가 생성되지 않았습니다.");

  312     }

  313     else

  314     {

  315       HSSFCellStyle style = this.wb.createCellStyle();

  316       this.AttachStyle(ref style, cellStyle, type);

  317       this.settingList.Add(new ExcelCellSettingBase(sheetIndex, startColumnIndex, endColumnIndex, startRowIndex, endRowIndex, style));

  318     }

  319   }

  320 }

위 메서드를 살펴 보자.
먼저 HSSFCellStyle 형의 셀의 스타일 개체를 생성한다.
그리고 ExcelCellStyle 개채의 값을 style 개체에 attatch를 하고 ExcelCellSettingBase 형의 개체를 생성하고 settingList 라는 제너릭리스트에 Add 한다.
ExcelCellSettingBase 클래스는 스타일이 정의된 영역에 대한 정보를 가지고 있다. 그러므로 settingList 라는 제너릭 리스트는 이 스타일이 정의된 영역에 대한 정보를 가진 컬렉션이다.

이제 바인딩을 해보자.

  548 public void DataBinding()

  549 {

  550   if (this.dsList.Count == 0)

  551   {

  552     throw new Exception("데이터가 없습니다.");

  553   }

  554   else

  555   {

  556     //하나의 DataSet이 하나의 시트

  557     for (int j = 0; j < dsList.Count; j++)

  558     {

  559       this.sheetIndex = j;

  560       int totalRowCountInSheet = this.SumOfTableRowsCount(j);

  561

  562       int rowCount = 0;

  563

  564       //데이터 갯수와 헤더의 갯수(있을거라고 가정)가 65536을 넘을 수 없다.

  565       //헤더는 각각의 테이블당 한개의 row를 차지하므로 한개의 시트에 속하는 테이블의 갯수와 동일하다.

  566       //하나의 시트의 Row가 65535를 넘게 되면 오류...

  567       totalRowCountInSheet += dsList[j].Tables.Count;

  568

  569       if (totalRowCountInSheet > 65535)

  570       {

  571         throw new ArgumentOutOfRangeException("데이터의 수가 65535건이 넘었습니다.");

  572       }

  573       else

  574       {

  575         for (int i = 0; i < dsList[j].Tables.Count; i++)

  576         {

  577           if (Convert.ToBoolean(this.hasTitleList[j][i]))

  578           {

  579             this.CreateTitle(j, dsList[j].Tables[i].Columns, rowCount);

  580             rowCount++;

  581           }

  582           this.DataBinding(dsList[j].Tables[i], rowCount);

  583           rowCount += dsList[j].Tables[i].Rows.Count;

  584         }

  585       }

  586     }

  587   }

  588 }

지금까지 정의 된 값을 토대로 하여 데이터를 바인딩 한다.
560라인의 SumOfTableRowCount()메서드는 해당 시트에 바인딩 될 총 row의 합계이다.
이를 조사하는 이유는..
엑셀의 경우 하나의 시트에 65536행까지만 허용되기 때문이다.
그러나 Excel 2007의 경우에는 1백만행*65536컬럼 까지 지원한다고 한다..
579라인에 보면 CreateTitle()메서드가 보이는데 제목을 가진 DataTable이라면(577라인의 조건절) 제목을 시트에 쓴다.
그리고 582라인의 DataBinding 메서드에서 실제로 시트에 쓴다.

먼저 CreateTitle()메서드를 살펴보자.

  745 HSSFSheet sheet = this.wb.getSheetAt(sheetIndex);

  746 HSSFRow row = sheet.createRow(rowCount);

  747 for (short i = 0; i < dcc.Count; i++)

  748 {

  749   if (!dcc[i].ColumnName.StartsWith("Column"))

  750   {

  751     HSSFCell cell = row.createCell(i);

  752     cell.setEncoding(1);

  753     this.currentRowIndex = rowCount;

  754     this.currentColumnIndex = i;

  755     ExcelCellSettingBase cellStyle = this.settingList.Find(FindStyle);

  756     if (cellStyle != null)

  757     {

  758       HSSFCellStyle cs = cellStyle.CellStyle;

  759       cell.setCellStyle(cs);

  760     }

  761     cell.setCellValue(dcc[i].ColumnName);

  762   }

  763 }

발췌했다.
745라인에서 sheet개체를 얻고 746라인에서 createRow()메서드로 row를 만들었다.
749라인의 조건절은 자동생성된 컬럼을 걸르기 위함이다. 자동으로 생성된 컬럼은 의미가 없기 때문이다 . 자동생성된 컬럼의 이름패턴은 Column1, Column2..... 처럼 생성된다.
755라인에 주목하자. 
제너릭리스트의 Find()메서드를 사용하였다. 닷넷 2.0에서 새로나온 개념이다..^^; 
이 Find 메서드는 settingList 라는 제너릭 리스트에서 해당 조건에 맞는 첫번째 아이템을 반환한다.

  806 private bool FindStyle(ExcelCellSettingBase setting)

  807 {

  808   if (setting.SheetIndex == this.sheetIndex && setting.StartRowIndex <= this.currentRowIndex && setting.EndRowIndex >= this.currentRowIndex && setting.StartColumnIndex <= this.currentColumnIndex && setting.EndColumnIndex >= this.currentColumnIndex)

  809   {

  810     return true;

  811   }

  812   else

  813   {

  814     return false;

  815   }

  816 }

FindStyle 메서드를 살펴 보면 앞서 정의했던 settingList 제너릭 리스트에서 현재 Cell의 영역이 포함되어 있는 첫번째 ExcelCellSettingBase 개체를 반환한다.
음..한번도 안써보신 분이라면..약간 헤깔릴 수도....

어쨋든 해당되는 부분을 찾았으면 759라인처럼 셀에 스타일을 정의한 후에 761라인 처럼 값을 입력한다.

이제 582라인의 DataBinding 메서드를 살펴 보자.

  620 HSSFCellStyle cs = cellStyle.CellStyle;

  621 c.setCellStyle(cs);

  622 object o = dt.Rows[rowIndexInCurrentTable][currentColumnIndex];

  623 if (o.ToString() != "")

  624 {

  625   switch ((CellType)cs.getDataFormat())

  626   {

  627     case CellType.CELL_TYPE_NUMERIC:

  628       c.setCellValue(Convert.ToDouble(o));

  629       break;

  630     case CellType.CELL_TYPE_DATETIME:

  631       c.setCellValue(new java.util.Date(Convert.ToDateTime(o).ToLongTimeString().Trim()));

  632       break;

  633     case CellType.CELL_TYPE_STRING:

  634       c.setCellValue(o.ToString().Trim());

  635       break;

  636   }

  637 }

  638 else

  639 {

  640   c.setCellValue("");

  641 }

역시 발췌했다.
CreateTitle() 메서드와 마찬가지로 Find 메서드로 해당되는 ExcelCellSettingBase 개체를 반환하여 적용한다.
그리고 추가로 작업을 하나 더 하는데..
바로 셀의 타입을 결정하는 부분이다.
셀의 타입 역시 HSSFCellStyle 개체에 포함되어 있는 부분이다.
그렇기에 셀의 형식에 맞게끔 데이터를 형변환해서 입력해주어야 한다.
그 부분이 switch ~case 구문이다.
631라인의 경우에는 datetime형식을 표현하는 방식이 닷넷과 자바가 달라서..저런 방법을 취할수 밖에 없었다...쿨럭. 그리고 마지막으로 이벤트를 발생 시킨다.

  655 this.OnRowCount(this, new RowCountEventArgs(rowIndexInCurrentTable + 1, dt.TableName));

지금까지 작업을 하였다면 하나의 워크북에 시트와 데이터가 모두 만들어 졌을 것이다.
이제 약간의 추가 작업을 선택적으로 해주면 되겠다.

추가적인 기능은

public void ColumnWidth(int sheetIndex, short columnIndex, short width);

public void GroupColumn(int sheetIndex, short startColumn, short endColumn);

public void GroupRow(int sheetIndex, int startRow, int endRow);

public void Merge(int sheetIndex, short startColumnIndex, short endColumnIndex, int startRowIndex, int endRowIndex);

public void RowHeight(int sheetIndex, int startRowIndex, int endRowIndex, int height);

public void Split(int sheetIndex, int colSplit, int rowSplit);

위와 같다.

  • ColumnWidth - 컬럼의 폭을 결정한다, 모든 단위는 px 단위이다.
  • GroupColumn - 컬럼을 그룹화한다.
  • GroupRow - 로우를 그룹화한다.
  • Merge - 정의된 셀영역을 합친다.
  • RowHeight - 로우의 높이를 결정한다.
  • Split - 문서를 스플릿한다.

주의 할 것은 이 메서드들은 반드시 바인딩 후에 호출하여야 한다.는 것이다.

이제 마지막 단계이다.
이렇게 워크북을 만들었다면..이제 출력을 해야 할것이다.
출력은 2가지가 지원한다.
하나는 파일 출력이며 하나는 스트림출력이다.

  664 public void OutPut()

  665 {

  666   string fPath = this.fullName;

  667   if (fPath == "")

  668   {

  669     fPath = this.path + this.fileName;

  670   }

  671   using (Stream fileOut = new FileStream(fPath, FileMode.Create))

  672   {

  673     this.wb.write(fileOut);

  674   }

  675 }

OutPut()메서드는 파일 출력이다. 673라인의 write() 메서드로 정의된 파일명으로 파일이 생성된다.
윈도우즈 응용프로그램에서 사용하기 적합하다.

  681 public byte[] OutPutToWeb()

  682 {

  683   Stream fileOut = new MemoryStream();

  684   this.wb.write(fileOut);

  685   byte[] buffer = ((MemoryStream)fileOut).GetBuffer();

  686   fileOut.Close();

  687   return buffer;

  688 }

이 메서드는 byte[] 배열형으로 출력한다.  
웹의 경우 서버에 파일을 만들필요가 없기에 스트림으로 내려 주는 방식이 좋다. 샘플 예제는 웹방식으로 적용해 보았다.

지금까지...어셈블리에 대해서 알아 보았다.
이제 샘플코드를 보고..어떻게 사용하는지 알아보자.

   23 List<string> sheetNames = new List<string>(2);

   24 sheetNames.Add("sheet1");

   25 sheetNames.Add("sheet2");

   26

   27 ExcelHandler handler = new ExcelHandler(sheetNames);

   28 handler.OnRowCount += new ExcelHandler.RowCount(handler_OnRowCount);

   29

   30 handler.InsertTable(0, false, this.MakeTitle());

   31 handler.InsertBlankRow(0, 3);

   32 handler.InsertTable(0, true, this.MakeDataTable("테스트"));

   33 handler.InsertTable(1, false, this.MakeTitle());

   34 handler.InsertTable(1, true, this.MakeDataTable("테스트"));

   35

   36 handler.SetCellStyle(0, 0, 0, 0, 0, this.MakeCellStyle1());

   37 handler.SetCellStyle(0, 0, 0, 10, 50, this.MakeCellStyle2(), CellType.CELL_TYPE_NUMERIC);

   38 handler.SetCellStyle(0, 1, 1, 10, 50, this.MakeCellStyle2(), CellType.CELL_TYPE_STRING);

   39 handler.SetCellStyle(0, 2, 2, 10, 50, this.MakeCellStyle2(), CellType.CELL_TYPE_DATETIME);

   40

   41 handler.DataBinding();

   42

   43 handler.RowHeight(0, 0, 0, 100);

   44 handler.ColumnWidth(0, 0, 300);

   45 handler.Merge(0, 0, 2, 0, 0);

   46 handler.Split(0, 0, 5);

   47 handler.GroupRow(0, 5, 10);

   48

   49 //파일 다운로드

   50 this.Response.Clear();

   51 this.Response.AddHeader("Content-Disposition", "attachment; filename=exceltest.xls");

   52 this.Response.ContentType = "application/vnd.ms-excel ";

   53 this.Response.BinaryWrite(handler.OutPutToWeb());

   54 this.Response.End();

22~25 라인에서 시트의 이름을 생성했다. 다시 말하지만 한글은 지원하지 않는다.
27라인에서 생성자를 만들고
28라인에서 이벤트를 등록하였다.(웹에서는 사실 이 이벤트가 소용없으나 선언된것이기 때문에 정의하지 않으면 예외를 던진다.)

30~34라인에서 DataTable을 각각의 시트에 입력하였다. 첫번째 시트에는 공란도 입력하였다. 

36~39 라인에서 스타일과 타입을 정의 하였다. 영역이 중복되어도 상관없지만..먼저 정의된것만 적용된다.^^;  테스트를 위하여 각각 숫자형, 문자형, 일시형을 정의해 보았다.

그리고 41라인 처럼 바인딩을 한다.

43~47라인에는 시트의 설정을 변경해 보았다. 어떻게 출력이 될까 하고 예측해보자...

50~54 라인에서는 웹상에서 출력하는 방법을 보여준 예이다.
주목 할 것은 바로 53라인이다.
Response.BinaryWriter() 메서드로 출력하는 것을 확인하기 바란다.

이제 샘플코드를 실행해서 엑셀파일을 다운 받아 보면..아래와 같은 결과가 나오겠다.

그리고 ..

마지막으로 스타일을 정의 하는 부분은 같이 업로드된 파일 처럼 코드스니핏을 이용하면 편하다.
혹시 이용법을 모르는 사람을 위해..
첨부된 excelStyle.snippet 파일을 C:\Documents and Settings\사용자이름\My Documents\Visual Studio 2005\Code Snippets\Visual C#\My Code Snippets 경로에 붙여 넣기를 한다.
그리고 VS2005를 재시동한다.

이제 화면에서 excel이라고 치면 인텔리센스가 뜬다...그게 코드 스니핏이다.
excelStyle을 선택하고 엔터..치면..기본값이 정의된 스타일 코드가 에디터에 표현이 된다...


마지막 업데이트 : (9/15/2010 12:08:31 PM)

TAG : 엑셀 excel 



Trackback 보기 (0)
댓글 보기 (2)
ojinni님의 글 (6/27/2008 11:48:15 AM)
POI 개체를 이용해서 Excel Reading도 가능한가요?



이방은님의 글 (6/27/2008 12:50:17 PM)
물론 가능 합니다..ㅎㅎ
그런데..도우미 클래스는 제가 안만들어 놓았군요..^^;
직접 만드셔야 할 듯 합니다..
그런데 대부분 읽는 것은 Ole 를 이용하시면 될듯 한데요..
이게 제일 쉽고..가장 빠르거든요...



댓글 쓰기



<< < 1 2 3 4 5 6 > >>