포트폴리오2013. 10. 21. 00:52

Windows Forms는 오랜 시간이 지난 지금 매우 잘 정착하여 성공적으로 여러 시스템과 플랫폼에 걸쳐 사용되고있는 훌륭한 프레임워크입니다. 하지만 최신 프로그래밍 기법과는 잘 어울리지 못한다는 문제점이 있어서 점진적으로 사장되어가고 있다는 것은 개인적으로 참 안타깝게 생각하는 부분입니다.

여러가지 이슈들이 있을 수 있지만, Windows Forms 개발자들에게 있어 가장 근본적이면서도 가장 어려운 문제는 단일 UI 스레드 안에서 모든 코드를 처리하도록 만든 프레임워크의 특성 상 다중 스레드에서의 프로그래밍 기법이 친숙하지 않다는 것입니다. 코드를 잘못 풀어서 정리할 경우 매우 파악하기 어렵고 까다로운 코드가 되기 쉽습니다. 그리고 이러한 문제는 UI 스레드가 아닌 스레드 문맥에서 UI 요소를 제어하려고 할 때 가장 큰 문제가 됩니다.

모든 경우의 수를 다 고려할 수는 없습니다. 그렇지만 빈번하게 사용되는 속성과 메서드들에 대해서만이라도 비동기 프로그래밍이 쉬워진다면 확실한 이점이 있을 것입니다. 이번에 개인적으로 런칭한 라이브러리는 그런 부분에 있어 큰 도움을 줄 수 있을 것이라 기대합니다.

라이브러리 설치 방법

이 라이브러리는 .NET Framework 4.0 Client Profile 이상의 Desktop App을 대상으로 합니다. 따라서 최소한 Visual Studio 2010 이상의 IDE 또는 Visual C#/Basic Express IDE가 필요하며, async/await 프로그래밍을 사용하기 위해서는 Visual Studio 2012 이상의 IDE 또는 Visual Studio 2012 for Desktop Express가 필요합니다. 그리고 이 글을 작성하는 현 시점에서 가장 최신 버전은 Visual Studio 2013 또는 Visual Studio 2013 for Desktop Express입니다. Express 버전의 IDE는 http://www.microsoft.com/express 에서 무료로 다운로드할 수 있습니다.

적절한 버전의 IDE를 다운로드하여 Windows Forms 프로젝트를 열거나 새로 만든 다음, NuGet 패키지 관리자를 열어 온라인에서 검색 키워드로 wfasync를 입력하면 다음과 같이 검색 결과가 나타납니다. 검색 결과 상의 패키지를 설치합니다.

 

기능 시험해보기

이 라이브러리는 System.Windows.Forms를 기본 네임스페이스로 사용하고 있으므로, 자동으로 기존 Windows Forms 프로젝트 상의 주요 컨트롤에 관련된 확장 메서드들이 추가됩니다. Control 클래스를 기준으로 구현되는 거의 모든 속성, 메서드들에 대해서 다음의 사항들에 대한 Wrapper Method가 제공됩니다.

  • Control 클래스의 주요 인스턴스 메서드의 경우 Begin~, End~, ~Async Wrapper 메서드가 추가됩니다. 만약 메서드가 여러 버전의 오버로드를 가지고 있는 경우 정확한 구분을 위하여 Begin과 End 시리즈 메서드에는 접미사로 순번이 붙습니다. 이것은 설계에 따른 기본 설정입니다.
  • Control 클래스의 주요 속성 (인덱서 제외)의 경우 getter/setter 사용 가능 여부에 따라 GetXXX, SetXXX, BeginGetXXX, BeginSetXXX, EndGetXXX, EndSetXXX, GetXXXAsync, SetXXXAsync 확장 메서드가 자동으로 추가됩니다.
  • 해당 Form이나 Control을 Parent Window로 지정하는 MessageBox.Show 메서드에 대한 10가지 오버로드에 대한 확장 메서드가 제공됩니다.

위의 사항들 중 일부를 간단히 시험해볼 수 있는 예제는 다음과 같습니다.

아래 화면과 같이 간단한 Form을 하나 생성하고, 적당한 크기의 RichTextBox 컨트롤을 추가한 다음, 비동기 작업 분리를 위하여 BackgroundWorker 컴포넌트를 하나 추가합니다. 그리고 Form의 Load 이벤트와 BackgroundWorker 컴포넌트의 DoWork 이벤트를 다음과 같이 구현하고 연결합니다.

private void Form1_Load(object sender, EventArgs e)
{
    this.backgroundWorker1.RunWorkerAsync();
}

private async void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
    using (System.Net.WebClient wc = new System.Net.WebClient())
    using (System.Threading.AutoResetEvent are = new System.Threading.AutoResetEvent(false))
    {
        wc.Encoding = Encoding.UTF8;
        await this.richTextBox1.SetTextAsync("Performing downloads.");
        string content = await wc.DownloadStringTaskAsync(
            new Uri("http://support.xinics.com/support/index.php?document_srl=23783"));
        await this.ShowMessageBoxAsync(
            String.Format("Downloaded {0} bytes.", content.Length));
        await this.richTextBox1.SetTextAsync("Waiting five seconds.");
        are.WaitOne(5000);
        await this.richTextBox1.SetTextAsync(content);
    }
}

코드의 내용은 단순합니다. UI 스레드가 메시지 처리에만 집중할 수 있도록, 추가 스레드를 쉽게 관리할 수 있도록 해주는 BackgroundWorker 컴포넌트를 Form 시작 시 실행하도록 코드를 추가하였습니다. 그 후, DoWork 이벤트 처리기를 만났을 때 비로소 이 라이브러리의 진가가 발휘됩니다.

결과를 반환하는 메서드가 아니기 때문에, 다행히 async/await 프로그래밍 때문에 이벤트 처리기의 메서드 시그니처와 이벤트 선언 사이의 임피던스 불일치는 일어나지 않습니다. 간편하게 DoWork 이벤트 핸들러 메서드에 async 키워드를 붙여주기만 하면 async/await 프로그래밍 준비는 끝납니다.

그 다음에는 RichTextBox 컨트롤의 텍스트를 변경하도록 요청하고 있고, WebClient 클래스를 초기화하고 using 블록 안에서만 사용하도록 코드를 시작합니다. WebClient의 기본 송수신 인코딩을 UTF8로 정하고, 지정된 URL로부터 문자열을 다운로드하여 해당 인코딩 방식으로 데이터를 디코딩하여 문자열로 가져옵니다.

그리고 다운로드한 문자의 갯수를 보여주기 위하여 ShowMessageBoxAsync 메서드를 사용하였습니다. 이 메서드 안에는 많은 내용이 숨겨져 있는데, 일단은 현재 스레드가 UI 스레드가 아니라는 점을 인지하여 메시지 박스를 UI 스레드를 통해 호출하도록 하고 있으며, 따라서 메시지 박스를 닫기 전까지는 모달 상태이므로 UI는 작업이 중단됩니다. 여기에 한 가지 더, 메시지 박스를 닫을 때까지 이 비동기 스레드 역시 동결 상태가 됩니다. 이것이 이 라이브러리의 역할인데, UI 스레드와 무관하게 전개가 되어야 할 때에는 작업을 독립적으로 수행하다가도, 사용자의 입력이 필요할 때 이와 같이 현재 스레드의 상태를 동결하는 것이 가능하다는 것입니다.

그 다음으로 RichTextBox 컨트롤을 대상으로 UI 스레드가 아닌 위치에서 표시할 문자열 변경을 요청하고 있습니다. 당연히 이 작업도 UI 스레드에 의하여 온전히 처리되도록 관리되며, UI 스레드가 작업을 마무리하기 전까지 현재 스레드는 역시 동결됩니다.

위의 코드를 실행했을 때 처음 얼마간 다운로드 작업이 이루어지는 동안에도 창에 대한 이벤트나 응답이 정상적으로 처리됨을 확인할 수 있습니다. 그리고 메시지 박스를 확인하고 난 다음에 5초의 시간이 지나서 RichTextBox의 내용이 갱신될 때 까지 UI가 전혀 얼어붙지 않음을 쉽게 확인할 수 있습니다.

 

라이선스에 대하여

이 프로젝트의 라이선스는 Apache License v2.0 (http://www.apache.org/licenses/LICENSE-2.0)을 따릅니다. 상업적 이용에 제한을 두지 않으며, 소스 코드 공개 의무를 지니지 않으므로 편리하게 이용할 수 있습니다. 소스 코드에 대한 검토 및 의견 제안은 github를 통해서 받고 있으니 언제든 자유롭게 의견 부탁드리겠습니다.

주요 프로젝트 URL

 

Posted by Cloud Developer 남정현 (rkttu.com)

댓글을 달아 주세요

Windows + .NET2013. 2. 1. 14:44

안녕하세요. Windows Azure MVP 남정현입니다.

Windows 8 Metro Style App에 대한 이야기가 요즈음 많이 회자되고 있습니다만, 그럼에도 불구하고 전문적인 도구나 프로그램에서의 Windows Forms에 대한 수요는 끊이지 않습니다. 오늘 살펴보려고 하는 내용은 Windows Forms 환경에서 MDI를 구현할 때 어떻게 시작을 하는 것이 가장 바람직한 모양을 만들 수 있는지 간단하게 정리한 예제 코드와 함께 살펴볼까 합니다.

예제 다운로드: https://skydrive.live.com/redir?resid=318484C5AAD6B73D!2684&authkey=!AO5fx9Fp4Xr0fog

둘러보기

아래 세 장의 스크린 샷과 같은 기능을 제공하는 MDI UI를 제공하는 Desktop App이 가장 이상적일 것입니다. 이 예제는 처음 시작하자마자 몇 개의 창이 자식 창으로 등록된 상태에서 시작됩니다.

자식 창을 전체 화면으로 바꾸었을 때, 아래와 같이 MenuStrip 제일 앞에 자식 창의 아이콘이, 제일 뒤에 최소화, 최대화, 닫기 버튼이 자동으로 병합됩니다.

창 메뉴를 클릭하면 현재 이 창 안에 등록된 모든 자식 창들이 열거되고, 선택 상태가 자동으로 관리됨은 물론, 메뉴를 선택하면 창 전환까지 가능합니다.

그리고 왼쪽편에는 트리 뷰 컨트롤을 도킹시켜 MDI 창과 조화롭게 레이아웃이 구성됩니다. 보이지는 않지만, 트리뷰 바로 다음에는 Splitter 컨트롤을 도킹시켜 창의 크기도 자유자재로 조절이 가능하게 만들었습니다. 제일 하단에는 상태 표시줄까지 추가하였습니다.

깔끔한 구성이지만 사실 이렇게 만드는 것이 기본 제공되는 사항만으로는 알아내기 다소 어려운 점이 있습니다. 위와 같이 만들기 위해서 무엇을 어떻게 해야하는지 단계별로 살펴보도록 하겠습니다.

MDI 부모로 창을 만들기

MDI는 부모 창과 자식 창으로 구성되며 부모 창 1개에 자식 창 여러개가 들어갈 수 있습니다. 이렇게 하기 위해서 부모 창으로 사용할 Form을 열고 디자인 타임에서 아래와 같이 설정하거나 IsMdiContainer 속성을 코드에서 적당한 시점 (생성자나 Load 이벤트 실행 시점)에서 True를 설정합니다.

위와 같이 설정하면 디자인 타임이나 런타임 상의 폼 화면의 클라이언트 영역이 회색 배경에 움푹 파인 모양으로 바뀌는 것을 볼 수 있습니다.

MenuStrip, ToolStrip, StatusStrip 붙이기

MDI 인터페이스에서 일상적으로 쓰이는 메뉴, 도구 모음, 상태 표시줄을 차례대로 가져다 높습니다. 아래 스크린 샷에서 빨간색 사각형으로 강조한 세 가지 항목을 폼에 가져다 놓습니다.

폼에 다른 메뉴 스트립 컨트롤들이 없다면, 폼의 속성 창을 다시 살펴보았을 때 아래와 같이 자동으로 MainMenuStrip에 방금 추가한 메뉴가 포함됩니다. MDI에 관련된 다른 여러 가지 설정들은 지금 이 속성에 연결된 컨트롤 앞으로 전달되거나 응용 동작에 연결되므로 이 설정이 중요합니다.

이렇게 해서 MDI 부모 창의 기본 구성이 완료되었습니다.

자식 창 관리 기능을 만들기

방금 추가한 메뉴 스트립 컨트롤에 MDI 창의 목록을 자동으로 보이고 현재 활성화된 MDI 창 항목을 강조 표시하며 메뉴 항목을 클릭하면 자동으로 창이 앞에 나타나는 기능을 구현해보도록 하겠습니다. 직접 코드를 작성해도 상관은 없지만, 빠르고 간편하게 완성도 높은 기능을 구현하기 위함이라면 지금 소개하는 방법이 가장 편리할 것입니다.

앞서 추가한 메뉴 스트립 컨트롤을 선택하고 아래 그림과 같이 MDI 목록을 표시할 대 메뉴 항목을 하나 추가합니다.

이번엔 메뉴 스트립 컨트롤을 선택한 상태에서 속성을 살펴봅니다. 속성 중에 MdiWindowListItem 속성이 비어있는 상태로 되어있을 것인데, 이것을 방금 추가한 대 메뉴 항목의 것으로 바꿔넣습니다.

좀 더 기능을 넣을 수 있는데, 이렇게 설정된 창 메뉴에 여러분이 임의로 새로운 메뉴를 더 넣을 수 있습니다. 개발자가 추가한 메뉴보다 뒤에 이러한 기능들이 오도록 구현되므로, 계단식 정렬이나 그리드 정렬 등의 창 관리 기능, 창 모두 닫기, 현재 활성화된 창 이외에 전부 닫기 같은 기능을 마음껏 구현하실 수 있습니다. 그리고 이러한 커스텀 메뉴들 제일 끝에 구분선 메뉴를 하나 더 넣어주시면 깔끔하게 구현됩니다.

참고로, 위의 예제와 같이 자동으로 메인 메뉴 항목을 완성하기를 원한다면 메뉴 스트립 컨트롤을 디자인 타임에서 오른쪽 버튼으로 클릭하고, 나타나는 메뉴 중에서 표준 항목 삽입이라는 메뉴를 아래 그림과 같이 클릭하면 간편하게 Mock Up UI 구성을 위한 항목이 자동으로 채워집니다. 표준 Windows UI 가이드 라인에 따라 필요한 항목들을 얼추 완성해주므로 이걸 활용하면 작업이 좀 더 빨라질 수 있습니다.

메인 화면의 상/하/좌/우 활용하기

Windows 탐색기, Microsoft Office 등 우리가 잘 아는 소프트웨어들은 공통적으로 이러한 MDI 화면의 상/하/좌/우에 적당한 패널을 배치하여 사용자의 작업을 돕는 대시보드를 넣어 프로그램을 돋보이게 만듭니다. 다행스럽게도 지금 만드는 MDI 부모 창도 이러한 구성이 가능합니다. 이 예제에서는 트리 뷰를 배치하려고 합니다. 트리뷰를 부모 창의 아무 곳에서 가져다 놓고, 컨트롤을 선택한 다음 아래와 같이 Dock 속성을 원하는 위치로 설정합니다. 예제에서는 Left로 설정했습니다.

적당한 크기를 Size 속성이나 디자인 타임에서 시각적으로 설정한 다음, Splitter (SplitContainer가 아닙니다.)를 추가하고 Splitter의 Dock 속성도 위와 동일한 값으로 설정합니다. 이렇게 하면 크기를 조절하려고 하는 컨트롤이 먼저 Stack에 올라가고, 그 다음에 크기 조절을 위한 Splitter 컨트롤이 올라가는 모양이 됩니다.

자식 창을 만들고 추가하기

이제 자식 창을 만들고 추가하는 코드를 넣을 차례입니다. 자식 창의 경우에는 임의로 새로운 폼을 몇 종류 만듭니다. 다른 어떠한 설정도 필요 없으며, 구분을 위해서 간단하게 라벨을 추가하여 서로 구분될 수 있게만 꾸며주기 바랍니다. 이 과정을 거쳐 예제에서는 ChildForm1, ChildForm2, ChildForm3라는 세 개의 클래스를 프로젝트에 아래 그림과 같이 추가하였습니다.

그 후, Parent Form의 디자인 타임 영역 상의 제목 표시줄을 더블 클릭하면 Load 이벤트에 대한 이벤트 처리기가 자동으로 만들어집니다. 그리고 아래와 같이 코드를 작성합니다.

C#

    private void MainForm_Load(object sender, EventArgs e) {
        new ChildForm1() { MdiParent = this, Visible = true };
        new ChildForm2() { MdiParent = this, Visible = true };
        new ChildForm3() { MdiParent = this, Visible = true };
    }

VB.NET

    Private Sub MainForm_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        Dim Window1 As New ChildForm1()
        Window1.MdiParent = Me
        Window1.Visible = True

        Dim Window2 As New ChildForm2()
        Window2.MdiParent = Me
        Window2.Visible = True

        Dim Window3 As New ChildForm3()
        Window3.MdiParent = Me
        Window3.Visible = True
    End Sub

그리고 앞서 추가한 ToolStrip 컨트롤에 버튼을 세 개 추가하고 각각 세 종류의 창을 띄우도록 위의 코드를 응용하여 만들어 봅니다.

자식 창을 최대화했을 때의 문제점

이제 거의 다 되었습니다. 프로그램을 실행해보고 이것저것 테스트해보면 잘 됩니다. 그런데 한 가지 눈에 거슬리는 점이 보이네요. 자식 창을 최대화했더니 아래처럼 메뉴 스트립과 자식 창의 아이콘이 서로 다른 줄에 그려집니다. 왜 그럴까요?

이 문제를 해결하기 위해 엄청나게 복잡한 코드를 쓰는 경우가 있습니다. 자식 창이 최대화 되면 자식 창의 ShowIcon 속성을 끈다거나 제목 표시줄을 없앤다거나 최대화를 흉내내도록 상태를 바꾸지 않고 창 크기만 업데이트한다던가 여러가지가 있지요. 그러나 이런 방법들은 거의 열 중 아홉 이상이 사소한 문제들을 많이 일으킵니다.

위의 문제를 해결하기 위해서는 아래 사항을 확인해야 합니다.

  • 앞서 이야기한 폼의 속성 중 MainMenuStrip 속성이 정확히 메인 메뉴를 가리키고 있는지 확인해야 합니다.
  • 필요한 경우 MainMenuStrip의 RenderMode 속성을 ManagedRenderMode가 아닌 System이나 Professional로 변경해 봅니다.

올바르게 적용되었다면 앞의 그림과 같은 단순하지만 튼튼한 토대를 갖춘 MDI UI를 가진 프로그램이 완성됩니다.

사족

이러한 특성을 극복할 수 없어서 다시 Windows Forms 초기에 제공되던 MainMenu 같은 컨트롤로 롤백하시는 경우도 종종 있을 수 있습니다. 그러나 최신 버전의 .NET Framework로 업그레이드하거나 개발 도구를 변경하게 될 경우에는 가능한 스트립 계열 컨트롤로 업그레이드하시는 것이 좋습니다. 지금 소개한 아티클을 참고하여 다시 시도해보시면 좋은 결과가 있을 것입니다. :-)

Posted by Cloud Developer 남정현 (rkttu.com)

댓글을 달아 주세요

Windows + .NET2010. 1. 19. 19:25

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

 

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

 

Posted by Cloud Developer 남정현 (rkttu.com)

댓글을 달아 주세요

  1. 프레임쪽을 봐야했는데 많은 참조가 되었습니다. 감사합니다.

    2010.01.21 08:55 [ ADDR : EDIT/ DEL : REPLY ]

Windows + .NET2010. 1. 1. 01:29

원본: http://www.syncfusion.com/FAQ/windowsforms/faq_c40c.aspx#q694q

 

Windows Forms 관련 프로젝트를 진행하던 중, 근래에 보기 드문 자료 모음을 하나 발견하였습니다. Windows Forms, ASP .NET, Silverlight 등 다양한 소프트웨어 컴포넌트를 개발하는 Syncfusion社의 직원으로 근무하면서 받은 여러 질문과 경험 들을 George Shephered님 께서 FAQ의 형태로 웹 페이지로 정리한 페이지입니다.

 

(http://www.syncfusion.com/faq/windowsforms/default.aspx) ASP.NET과 WPF FAQ도 같이 수록되어있으며, 이 중에서 Windows Forms에 대한 부분을 먼저 번안해서 올려보고자 합니다. 번역은 지속적으로 업데이트를 해나가도록 할 예정이며, 잘못 번역된 부분이나 의미가 모호한 부분에 대한 피드백을 주시면 감사하겠습니다. 또한, 최신 버전의 Windows Forms 기술에 대한 피드백도 덧붙여주시면 반영할 수 있도록 하겠습니다. (번역 과정에서 반영하지 못한 내용도 있을 수 있습니다.)

1.1 내 응용프로그램에서 EXE 파일을 어떻게 실행합니까?

System.Diagnostics 네임스페이스의 Process 클래스를 사용하면 됩니다.

 

[C#]

     Process proc = new Process();

     proc.StartInfo.FileName = @"Notepad.exe";

     proc.StartInfo.Arguments = "";

     proc.Start();

 

[VB.NET]

     Dim proc As New Process()

     proc.StartInfo.FileName = "Notepad.exe"

     proc.StartInfo.Arguments = ""

     proc.Start()

1.2. 게시자 정책 파일을 활용하면서 발생할 수 있는 문제점은 어떤 것들이 있습니까?

1) 게시자 정책 어셈블리의 이름을 정확히 지정해야만 합니다. 예를 들어, 원본 어셈블리 파일의 이름이 “TestAssebmly.dll” 이라고 가정하였을 때, 게시자 정책 어셈블리 파일의 이름은 “policy.1.0.TestAssembly.dll” 이라고 지정되어야 하며, 이는 “1.0.*” 버전 전체에 대한 어셈블리 리디렉션을 뜻하게 됩니다.

 

2) 게시자 정책 파일 내에서 어셈블리를 지칭할 때에는 어셈블리 이름에 확장자인 DLL을 붙이지 않도록 유의합니다.

다음은 잘못된 예입니다:

<assemblyIdentity name="TestAssembly.dll" publicKeyToken="f638d0a8d5996dd4" culture="neutral" />

대신 이와 같이 지정하십시오.

<assemblyIdentity name="TestAssembly" publicKeyToken="f638d0a8d5996dd4" culture="neutral" />

3) 원본 어셈블리 파일 이름에 사용하였던 강력한 이름을 그대로 게시자 정책 어셈블리에도 지정해야 합니다.

 

4) 게시자 정책 파일을 게시자 정책 어셈블리에 맞추어 배포해야 합니다. 게시자 정책 어셈블리만을 게시자 정책 파일 없이 전역 어셈블리 캐시에 단독으로 설치하는 것으로는 충분하지 않습니다. 또한, 게시자 정책 어셈블리를 만든 이후에 가하게 되는 게시자 정책 파일의 변경 사항들은 반영되지 않습니다.

 

5) 항상 “AL” 유틸리티를 사용할 때에는 /link 스위치를 사용하여 게시자 정책 파일을 지정하도록 합니다. /embed 스위치를 사용하지 마십시오. 지원되지 않는 듯합니다.

이 내용과 관련이 있는 링크 - http://msdn.microsoft.com/ko-kr/library/dz32563a.aspx (원문에 소개된 나머지 링크 2개는 이 글이 작성되는 현 시점에서는 유효하지 않아서 제외했습니다.)

1.3. 현재 시스템에서 실행되는 모든 프로세스의 목록을 어떻게 가져옵니까?

System.Diagnostics 네임스페이스의 Process.GetProcesses() 정적 메서드를 사용하시면 됩니다.

 

[C#]

     using System.Diagnostics;

     ...

     foreach ( Process p in Process.GetProcesses() )

          Console.WriteLine( p ); // string s = p.ToString();

 

[VB.NET]

     Imports System.Diagnostics

     ...

     Dim p As Process

     For Each p In Process.GetProcesses()

          Console.WriteLine(p) ' string s = p.ToString()

     Next p

1.4. 현재 시스템에서 GUI 형태로 실행되는 프로그램들 (단순한 창 목록 나열이 아닌)의 목록을 어떻게 가져옵니까?

플랫폼 호출 기능 (P/Invoke)을 이용하여 EnumWindows Win32 API를 호출할 수도 있지만, System.Diagnostics 네임스페이스의 Process.GetProcesses() 정적 메서드를 활용하면 플랫폼 호출 기능으로 발생하는 비용을 최소화하면서 원하는 목적을 쉽게 달성할 수 있습니다.

 

[C#]

     using System.Diagnostics;

     ...

     foreach ( Process p in Process.GetProcesses(System.Environment.MachineName) )

     {

          if( p.MainWindowHandle != IntPtr.Zero)

          {

               //this is a GUI app

               Console.WriteLine( p ); // string s = p.ToString();

          }

     }

 

[VB.NET]

     Imports System.Diagnostics

     ...

     Dim p As Process

     For Each p In Process.GetProcesses(System.Environment.MachineName)

          If p.MainWindowHandle <> IntPtr.Zero Then

               'this is a GUI app

               Console.WriteLine(p) ' string s = p.ToString();

          End If

     Next p

1.5. 프로그램을 하나만 실행시킬 수 있도록 하려면 어떻게 해야 합니까?

C# Corner 웹 사이트에 Saar Carmi님이 올려주신 샘플 (http://www.codeproject.com/KB/cs/restricting_instances.aspx) 의 내용에서는, System.Diagnostics 네임스페이스 안의 Process 클래스를 활용하여 이를 구현하고 있습니다.

 

역자 주) 이 방법은 인터넷 상에 알려져 있는 여러 방법들 중 하나를 소개하는 것입니다.

 

[C#]

public static Process RunningInstance()

{

     Process current = Process.GetCurrentProcess();

     Process[] processes = Process.GetProcessesByName (current.ProcessName);

      //Loop through the running processes in with the same name

      foreach (Process process in processes)

      {

          //Ignore the current process

           if (process.Id != current.Id)

          {

               //Make sure that the process is running from the exe file.

                if (Assembly.GetExecutingAssembly().Location.Replace("/", "\\") == current.MainModule.FileName)

                {

                    //Return the other process instance.

                    return process;

                }

           }

      }

      //No other instance was found, return null.

      return null;

}

 

[VB.NET]

Public Shared Function RunningInstance() As Process

     Dim current As Process = Process.GetCurrentProcess()

     Dim processes As Process() = Process.GetProcessesByName(current.ProcessName)

     'Loop through the running processes in with the same name

     Dim process As Process

     For Each process In processes

          'Ignore the current process

          If process.Id <> current.Id Then

               'Make sure that the process is running from the exe file.

               If [Assembly].GetExecutingAssembly().Location.Replace("/", "\") = current.MainModule.FileName Then

                    'Return the other process instance.

                    Return process

               End If

          End If

     Next process

     'No other instance was found, return null.

     Return Nothing

End Function 'RunningInstance

1.6. 현재 실행 중인 운영 체제를 파악하려면 어떻게 합니까?

System 네임스페이스의 Environment 클래스를 활용하면 운영 체제 정보를 가져올 수 있습니다.

 

[C#]

     string versionText = Environment.OSVersion.Version.ToString();

 

[VB.NET]

     Dim versionText As String = Environment.OSVersion.Version.ToString()

Version 속성은 Major, Minor와 같이 상세한 정보를 표현하는 멤버를 가지고 있습니다. 참고로, Windows XP의 버전은 5.1입니다.

1.7. 실행 중인 컴퓨터의 모든 IP 주소를 가져오려면 어떻게 합니까?

[C#]

     string s ="";

     System.Net.IPAddress[] addressList = Dns.GetHostByName(Dns.GetHostName()).AddressList;

     for (int i = 0; i < addressList.Length; i ++)

     {

          s += addressList[i].ToString() + "\n";

     }

     textBox1.Text = s;

 

[VB.NET]

     Dim s As String = ""

     Dim addressList As System.Net.IPAddress() = Dns.GetHostByName(Dns.GetHostName()).AddressList

     Dim i As Integer

     For i = 0 To addressList.Length - 1

          s += addressList(i).ToString() + ControlChars.Lf

     Next i

     textBox1.Text = s

1.8. 배포 대상 컴퓨터에 .NET Framework가 설치되어있지 않습니다. 이러한 경우 개발된 Windows Forms 응용프로그램을 실행할 수 있습니까?

아니오. 개발한 Windows Forms 응용프로그램을 실행하려면 반드시 배포 대상 컴퓨터에 .NET Framework가 설치되어있어야만 합니다. Microsoft에서 .NET 런타임 플랫폼 설치 패키지를 재 배포가 가능한 형태로 배포를 하고 있으니 참고하시기 바랍니다.

1.9. 실행 중인 EXE 파일의 경로를 가져오려면 어떻게 합니까?

System.Windows.Forms 네임스페이스의 Application.ExecutablePath 정적 속성을 활용하면 알 수 있습니다.

 

[C#]

     textBox1.Text = Application.ExecutablePath;

 

[VB.NET]

     TextBox1.Text = Application.ExecutablePath

1.10. 현재 실행 중인 코드가 어떤 어셈블리로부터 시작되어 실행 중인지 파악하려면 어떻게 합니까?

아래의 코드 단편은 어떤 어셈블리로부터 시작되어 코드가 실행 중에 있는지 파악하는 방법을 보여줍니다.

 

[C#]

MessageBox.Show(System.Reflection.Assembly.GetEntryAssembly().GetName().Name);

 

[VB.NET]

MessageBox.Show(System.Reflection.Assembly.GetEnTryAssembly().GetName().Name)

1.11. 전역 어셈블리 캐시에 어떠한 항목들이 설치되어있는지 어떻게 확인할 수 있습니까?

Windows 탐색기를 활용하여 %windir%\Assembly 폴더 (%windir%은 현재 실행 중인 Windows 운영 체제의 기본 설치 경로를 저장하는 시스템 환경 변수입니다.)를 열어보면 알 수 있습니다. 만약 .NET Framework가 설치되어있다면, 이 폴더에 대해 Windows 탐색기가 전용 폴더 보기 화면을 띄워줄 것입니다. 자세히 보기로 보기 설정을 바꾸면 설치된 항목들의 세부 사항을 한눈에 볼 수 있습니다.

1.12. System.Windows.Forms.Application.CompanyName 속성으로 반환되는 값을 수정하려면 어떻게 합니까?

이것은 어셈블리 특성으로부터 가져오는 값입니다. Visual Studio 개발 환경 아래에서는 프로젝트 내에 자동 생성되는 AssemblyInfo 파일 (C# 프로젝트의 경우 AssemblyInfo.cs, Visual Basic .NET 프로젝트의 경우 AssemblyInfo.vb)에 설정됩니다. 여기에는 회사 이름과 더불어 버전 번호 및 여러 가지 세부 설정을 바꿀 수 있습니다. 다음은 코드 예시입니다.

 

[C#]

[assembly: AssemblyCompany("Syncfusion, Inc.")]

[VB.NET]

<Assembly: AssemblyCompany("Syncfusion, Inc.")>

Posted by Cloud Developer 남정현 (rkttu.com)

댓글을 달아 주세요

Windows + .NET2009. 12. 20. 15:51

네이버 오픈 API를 데이터 소스로 사용하여 Windows Forms에 Binding하는 샘플을 간단히 만들어보았습니다. 지난번에 예를 들었던 난수 생성을 위한 데이터 소스에서 언급했던 IListSource 인터페이스의 사용법을 기초로 발전시킨 샘플입니다. :-)

 

using System;
using System.IO;
using System.Net;
using System.Xml;
using System.Data;
using System.Text;
using System.Collections;
using System.Globalization;
using System.ComponentModel;

namespace OpenApiBinding
{
    [ToolboxItem(true)]
    [DesignTimeVisible(true)]
    public partial class NaverSearchBinding : Component, IListSource
    {
        public NaverSearchBinding()
            : this(null)
        {
        }

        public NaverSearchBinding(Container container)
            : base()
        {
            if (container != null)
                container.Add(this);

            this.InitializeComponent();

            this.key = String.Empty;
            this.target = NaverSearchTarget.WebDocuments;
            this.query = String.Empty;
            this.display = 100;
            this.start = 1;
            this.sort = NaverSortMode.Similarity;
        }

        private string key;
        private NaverSearchTarget target;
        private string query;
        private int display;
        private int start;
        private NaverSortMode sort;

        [NonSerialized]
        private const string RequestUrl = "http://openapi.naver.com/search";

        [Browsable(true)]
        [DefaultValue("")]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
        public string Key
        {
            get { return this.key; }
            set { this.key = (value ?? String.Empty).Trim(); }
        }

        [Browsable(true)]
        [DefaultValue(NaverSearchTarget.WebDocuments)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
        public NaverSearchTarget Target
        {
            get { return this.target; }
            set { this.target = value; }
        }

        [Browsable(true)]
        [DefaultValue("")]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
        public string Query
        {
            get { return this.query; }
            set { this.query = (value ?? String.Empty).Trim(); }
        }

        [Browsable(true)]
        [DefaultValue(100)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
        public int Display
        {
            get { return this.display; }
            set { this.display = value; }
        }

        [Browsable(true)]
        [DefaultValue(1)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
        public int Start
        {
            get { return this.start; }
            set { this.start = value; }
        }

        [Browsable(true)]
        [DefaultValue(NaverSortMode.Similarity)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
        public NaverSortMode Sort
        {
            get { return this.sort; }
            set { this.sort = value; }
        }

        public override string ToString()
        {
            return String.Format(
                CultureInfo.CurrentCulture,
                "{{ Target: '{0}', Query: '{1}' }}",
                this.target,
                this.query);
        }

        private Uri CreateUri()
        {
            if (String.IsNullOrEmpty(this.key))
                return null;

            StringBuilder buffer = new StringBuilder();
            buffer.AppendFormat("key={0}&", this.key);

            switch (this.target)
            {
                case NaverSearchTarget.Blog:
                    buffer.Append("target=blog&");
                    break;

                case NaverSearchTarget.Cafe:
                    buffer.Append("target=cafe&");
                    break;

                case NaverSearchTarget.CafeArticles:
                    buffer.Append("target=cafearticle&");
                    break;

                case NaverSearchTarget.News:
                    buffer.Append("target=news&");
                    break;

                default:
                case NaverSearchTarget.WebDocuments:
                    buffer.Append("target=webkr&");
                    break;
            }

            buffer.AppendFormat("query={0}&", this.query);
            buffer.AppendFormat("display={0}&", this.display);
            buffer.AppendFormat("start={0}&", this.start);

            switch (this.sort)
            {
                default:
                case NaverSortMode.Similarity:
                    buffer.Append("sort=sim");
                    break;

                case NaverSortMode.Date:
                    buffer.Append("sort=date");
                    break;

                case NaverSortMode.Member:
                    buffer.Append("sort=member");
                    break;

                case NaverSortMode.NewArticles:
                    buffer.Append("sort=newArticles");
                    break;

                case NaverSortMode.Rank:
                    buffer.Append("sort=rank");
                    break;
            }

            return new Uri(String.Concat(RequestUrl, '?', buffer.ToString()));
        }

        public IList GetList()
        {
            Uri requestUri = this.CreateUri();

            if (requestUri == null)
                return null;

            WebRequest webRequest = WebRequest.Create(requestUri);

            using (WebResponse webResponse = webRequest.GetResponse())
            {
                using (Stream webResponseStream = webResponse.GetResponseStream())
                {
                    XmlDocument xmlDocument = new XmlDocument();
                    xmlDocument.Load(webResponseStream);

                    DataTable inferencedTable = new DataTable(this.target.ToString());

                    switch (this.target)
                    {
                        case NaverSearchTarget.Blog:
                            inferencedTable.Columns.Add("title", typeof(string));
                            inferencedTable.Columns.Add("link", typeof(string));
                            inferencedTable.Columns.Add("description", typeof(string));
                            inferencedTable.Columns.Add("bloggername", typeof(string));
                            inferencedTable.Columns.Add("bloggerlink", typeof(string));
                            break;

                        case NaverSearchTarget.Cafe:
                            inferencedTable.Columns.Add("title", typeof(string));
                            inferencedTable.Columns.Add("link", typeof(string));
                            inferencedTable.Columns.Add("description", typeof(string));
                            inferencedTable.Columns.Add("ranking", typeof(string));
                            inferencedTable.Columns.Add("member", typeof(string));
                            inferencedTable.Columns.Add("totalarticles", typeof(string));
                            inferencedTable.Columns.Add("newarticles", typeof(string));
                            break;

                        case NaverSearchTarget.CafeArticles:
                            inferencedTable.Columns.Add("title", typeof(string));
                            inferencedTable.Columns.Add("link", typeof(string));
                            inferencedTable.Columns.Add("description", typeof(string));
                            inferencedTable.Columns.Add("cafename", typeof(string));
                            inferencedTable.Columns.Add("cafeurl", typeof(string));
                            break;

                        case NaverSearchTarget.News:
                            inferencedTable.Columns.Add("title", typeof(string));
                            inferencedTable.Columns.Add("link", typeof(string));
                            inferencedTable.Columns.Add("description", typeof(string));
                            inferencedTable.Columns.Add("originallink", typeof(string));
                            inferencedTable.Columns.Add("pubDate", typeof(string));
                            break;

                        default:
                        case NaverSearchTarget.WebDocuments:
                            inferencedTable.Columns.Add("title", typeof(string));
                            inferencedTable.Columns.Add("link", typeof(string));
                            inferencedTable.Columns.Add("description", typeof(string));
                            break;
                    }

                    foreach (XmlNode eachNode in xmlDocument.SelectNodes("/rss/channel/item"))
                    {
                        XmlElement eachElement = eachNode as XmlElement;

                        if (eachElement == null)
                            continue;

                        int i = 0;
                        object[] values = new object[inferencedTable.Columns.Count];

                        foreach (DataColumn eachColumn in inferencedTable.Columns)
                            values[i++] = eachElement.SelectSingleNode(eachColumn.ColumnName).InnerXml;

                        inferencedTable.Rows.Add(values);
                    }

                    return inferencedTable.DefaultView;
                }
            }
        }

        public bool ContainsListCollection
        {
            get { return true; }
        }
    }

    [Serializable]
    public enum NaverSearchTarget : int
    {
        Blog,
        Cafe,
        CafeArticles,
        WebDocuments,
        News
    }

    [Serializable]
    public enum NaverSortMode : int
    {
        Similarity,
        Date,
        Member,
        NewArticles,
        Rank
    }
}

 

위의 소스 코드를 프로젝트에 추가하여 Windows Forms – 또는 – ASP.NET 프로젝트에 연결하거나, 독립적으로 객체를 만들어서 사용하는 것이 가능합니다. 다음은 Windows Forms 디자인 타임에서의 사용 예시입니다.

 

 

컴포넌트에서 API Key를 Key 프로퍼티에 설정하고, 검색어를 Query 프로퍼티에 설정하여 검색 결과를 수신할 수 있습니다. 데이터 그리드를 이용하여 수신한 데이터를 곧바로 보여줄 수도 있습니다.

Posted by Cloud Developer 남정현 (rkttu.com)

댓글을 달아 주세요

  1. 아 순간적으로 X Windows라길래 X.org X Server라고 생각 [..]
    오픈API를 많이 공개한다고는 해도 어쨌건 네이버에 대한 제 안좋은 인상은 아직 남아있네요;;

    2009.12.20 16:07 [ ADDR : EDIT/ DEL : REPLY ]
    • 다음의 오픈 API도 다룰 수 있으면 참 좋겠지만 Windows Forms에서 다음의 오픈 API와 인증을 마치는 부분이 사실 상당히 어려운듯 합니다. 일반적인 웹 브라우저가 아닌 HTTP 모듈을 이용하여 세션을 처리할 수 있게 되면 같은 샘플을 한 번 더 올려봐야겠습니다. ㅎㅎ

      2009.12.20 23:32 [ ADDR : EDIT/ DEL ]

Windows + .NET2009. 12. 10. 00:30

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 객체 간의 간섭 효과는 걱정하지 않아도 될 것입니다.

Posted by Cloud Developer 남정현 (rkttu.com)

댓글을 달아 주세요

Windows + .NET2009. 12. 6. 00:09

최근 진행 중인 씨티은행의 외환업무시스템 프레임워크 업그레이드 작업은 .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 연산을 구현하는 것도 생각해볼 수 있을 것입니다.

 

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

 

 

 

Posted by Cloud Developer 남정현 (rkttu.com)

댓글을 달아 주세요

Windows + .NET2009. 10. 20. 10:30

닷넷 프레임워크를 통하여 응용프로그램을 개발할 때, 특히 Windows Forms 기반의 다중 스레드 프로그램을 구성할 때 자주 적용되는 패턴이 하나 있다면, 단일 메서드를 가지고 PreCondition/PostCondition으로 양분하는 패턴이 있을 것입니다. 예를 들면 다음과 같습니다.

 

private void MyForm_Load(object sender, EventArgs e)
{

    if (this.InvokeRequired)

    {

         this.Invoke(new EventHandler(MyForm_Load), new object[] { sender, e });

         return;

    }

    // 실제 메서드 코딩
}

 

위와 같이 조건에 따라서, 같은 메서드를 상태에 변화를 가하여 다시 호출하는 기법은 Windows Forms 뿐만 아니라 일반적인 응용프로그램에서도 자주 활용될 수 있는 기법이 될 것입니다. 사실, 위와 같은 코드는 어려울 것이 없겠습니다만 저는 이를 일반화할 수 있는 방법을 고민하던 중에, Reflection과 Interop을 이용할 수 있다는 것을 발견하였습니다.

 

using System;
using System.Reflection;

namespace dotForex.Test
{
    class Program
    {
        static bool needInvoke = true;

        static void Main(string[] args)
        {
            if (needInvoke)
            {
                Action<string[]> func = Delegate.CreateDelegate(
                    typeof(Action<string[]>),
                    MethodInfo.GetCurrentMethod() as MethodInfo) as Action<string[]>;
                needInvoke = false;
                func(args);
                return;
            }
            else
            {
                Console.WriteLine("Test");
            }

            return;
        }
    }
}

 

위의 코드에서 강조표시된 부분이 일반화에서 핵심이 되는 부분들입니다. Delegate 클래스의 CreateDelegate 메서드를 통하여, 생성할 대리자의 형식을 지정하고, 현재 메서드의 정보를 가져와서 이를 대입하는 방법입니다.

 

위와 같은 기법을 통하여 아래와 같이 Windows Forms의 스레드 안정성을 고려한 이벤트 핸들러 처리를 일반화하는 것도 가능합니다.

 

using System;
using System.Reflection;
using System.Windows.Forms;
using System.Runtime.InteropServices;

namespace dotForex.Application
{
    public sealed partial class ErrorReportForm : Form
    {
        public ErrorReportForm()
            : base()
        {
            this.InitializeComponent();
        }

        private object EnsureThreadSafe(MethodInfo targetMethod, Type targetDelegateType, params object[] arguments)
        {
            if (!this.InvokeRequired)
                throw new InvalidOperationException("EnsureThreadSafe is not required at this time.");

            if (targetMethod == null)
                throw new ArgumentNullException("targetMethod");

            if (targetDelegateType == null)
                throw new ArgumentNullException("targetDelegateType");

            if (!targetDelegateType.IsSubclassOf(typeof(Delegate)))
                throw new ArgumentException("Selected type is not a delegate type.", "targetDelegateType");

            Delegate methodDelegate = Delegate.CreateDelegate(
                targetDelegateType,
                targetMethod);

            if (methodDelegate == null)
                throw new Exception("Method cannot converted as delegate.");

            return this.Invoke(methodDelegate, arguments);
        }

        private void ErrorReportForm_Load(object sender, EventArgs e)
        {
            if (this.InvokeRequired)
            {
                this.EnsureThreadSafe(MethodInfo.GetCurrentMethod() as MethodInfo, typeof(EventHandler), sender, e);
                return;
            }

            // Method Coding Here
        }
    }
}

 

 

Posted by Cloud Developer 남정현 (rkttu.com)

댓글을 달아 주세요

Windows + .NET2008. 8. 7. 13:14

TransparencyKey를 통해서 편리한 도우미 유틸리티를 하나 만들어볼까 합니다. 바로 자동 클리핑 윈도우입니다. Window Region API를 이용하여 계산하고 자르는 작업을 하지 않고서도 간단하고 깔끔하게 이런 작업을 해낼 수 있습니다.

[Flags]
[Serializable]
public enum TransformOptions : int
{
    None = 0,
    HideAllControls = None + 1,
    RemoveWindowText = HideAllControls * 2,
    HideFromTaskbar = RemoveWindowText * 2,
    All = (HideAllControls | RemoveWindowText | HideFromTaskbar)
}

public static void TransformToCustomRegion(Form targetForm, Color transparentColor, TransformOptions options)
{
    if (targetForm == null)
        throw new ArgumentNullException("targetForm");

    if (targetForm.IsDisposed)
        throw new ObjectDisposedException("targetForm");

    if (targetForm.Visible)
        targetForm.Visible = false;

    targetForm.FormBorderStyle = FormBorderStyle.None;
    targetForm.BackColor = transparentColor;
    targetForm.TransparencyKey = targetForm.BackColor;

    if (((int)options & (int)TransformOptions.HideAllControls) != 0)
        foreach (Control c in targetForm.Controls)
            c.Visible = false;

    if (((int)options & (int)TransformOptions.RemoveWindowText) != 0)
        targetForm.Text = String.Empty;

    if (((int)options & (int)TransformOptions.HideFromTaskbar) != 0)
        targetForm.ShowInTaskbar = false;

    if (!targetForm.Visible)
        targetForm.Visible = true;
}

public static void TransformToCustomRegion(Form targetForm, Color transparentColor)
{
    TransformToCustomRegion(targetForm, transparentColor, TransformOptions.None);
}

// 실제 적용

private void Form1_Paint(object sender, PaintEventArgs e)
{
    e.Graphics.FillPie(Brushes.Aqua, 140, 0, 400, 400, 30, 80);
}

private void Form1_Load(object sender, EventArgs e)
{
    TransformToCustomRegion(this, Color.Empty);
}

여기서 TransformToCustomRegion 메서드가 동작하는 원리를 살펴보면, BackColor와 TransparencyKey의 값을 일치시켜주는 것과 함께, 창의 구성요소들을 제거하는 것입니다. 이로서, 창의 다른 구성 요소가 제거된 상태에서 순수한 컨텐츠만 Paint 이벤트를 통해서 그려지게 되는데, 이 중 색이 겹치지 않는 내용만이 남아서 창으로 존재하게 되고 나머지는 잘립니다.

이와 같은 원리를 이용하여 가운데에 구멍이 뚫려있는 창도 만들 수 있고, 예전 노턴 크래쉬가드 같은 프로그램처럼 방패모양 창도 구현할 수 있습니다. Hit-Test 구현만 정확히 되어주면 기존 제목 표시줄도 대체가 가능합니다. :-)

Posted by Cloud Developer 남정현 (rkttu.com)

댓글을 달아 주세요

  1. 잘봤습니다.
    새로운 용어를 알게되었네요. "Hit-Test" (적중 테스트)
    msdn 에 관련 정보가 있군요. 한번 봐야겠습니다.

    2009.09.28 13:09 [ ADDR : EDIT/ DEL : REPLY ]

Windows + .NET2008. 7. 20. 13:21

Paint 이벤트에서 그리는 함수가 아니지만 Paint 이벤트와 마찬가지로 작동하기 위해서는 그리는 방법을 다르게 구현해야 합니다. 이 문제 때문에 고민하시는 분이 꽤 있을 것으로 생각됩니다. 이 문제를 해결해줄 수 있는 클래스를 하나 소개합니다. 바로 Replay 클래스입니다.

Replay 클래스를 이용하여 멤버 메서드를 호출하면 모든 기록이 Replay 클래스 안에 보관되고, 나중에 ReplayWithoutResults 메서드를 호출하여 모든 메서드가 다시 호출될 수 있도록 하는 방식입니다. 이런 방법을 통하여 Paint 이벤트에서 그린 그림이 아니라도 복원이 가능합니다.

다음은 예제입니다.

// Graphics 객체를 Paint 이벤트가 아닌 곳에서 얻을 때,

// Replay 객체에 바로 집어넣습니다.

private void Form1_Load(object sender, EventArgs e)
{
    this.g = new Replay<Graphics>(Graphics.FromHwnd(this.panel1.Handle));
}

// Paint 이벤트에서는 Replay 객체가 가지고 있는 모든 메서드들을 호출합니다.

// g 객체의 ReplayWithoutResults 대신 정적 메서드를 부를 수도 있는데

// 이것은 Cross-Thread 호출을 위한 것입니다.

private void panel1_Paint(object sender, PaintEventArgs e)
{
    Replay<Graphics>.ReplayWithoutResults(this, this.g);
}

private Random r = new Random();
private Replay<Graphics> g;

// Graphics.DrawLine을 Replay 컨테이너를 통해서 호출하도록 합니다.

private void timer1_Tick(object sender, EventArgs e)
{
    g.Invoke("DrawLine", new object[] {
        new Pen(Color.FromArgb(r.Next(255), r.Next(255), r.Next(255))),
        new Point(r.Next(this.panel1.Width), r.Next(this.panel1.Height)),
        new Point(r.Next(this.panel1.Width), r.Next(this.panel1.Height)) });
}

이 소스 코드는 Apache License 버전 2.0 조건 아래에서 배포됩니다.

Posted by Cloud Developer 남정현 (rkttu.com)

댓글을 달아 주세요

Windows + .NET2008. 7. 7. 14:00

프로그래밍을 하다보면 Windows Forms나 Windows Presentation Foundation과 같이 한 단락내에 한 객체에 대해서 여러 속성을 동시에 지정해야 하는 경우가 꼭 있기 마련입니다. VB.NET이나 Object Pascl의 경우 With 절을 이용하여 이런 일을 손쉽게 할 수 있도록 해줍니다만 C#의 경우 마땅히 좋은 방법이 없습니다. 게다가, 이렇게 여러 속성을 나열해놓는 코드를 작성하다보면 코드가 어지럽혀지기 쉬운듯 합니다.

Windows Forms나 Windows Presentation Foundation의 경우 대개 디자이너를 이용하여 작업하는 경우가 많으므로 별 다른 문제가 안되지만 가끔 컨트롤을 직접 추가해야 하거나 디자이너가 지원되지 않는 GTK# 등의 환경에서 저 개인적으로 요긴하게 쓰는 방식이 있어서 소개해봅니다.

Panel myPanel = new Panel();
{
    myPanel.BackColor = Color.Violet;
    // ...
    this.Controls.Add(myPanel);
}

위와 같이 myPanel을 최초로 생성하는 줄 다음에 별 다른 의미 없이 공 Bracket을 열고 myPanel에 관한 코드를 집어넣은 뒤 관련 처리가 끝나면 공 Bracket을 닫는 방식입니다. 이렇게 정리를 해두면 #region이나 #endregion보다 훨씬 읽기 편한것 같습니다. :-)

Posted by Cloud Developer 남정현 (rkttu.com)

댓글을 달아 주세요

Windows + .NET2008. 6. 15. 00:05

닷넷 프레임워크는 MSIL 기반의 어셈블리 코드를 생성하며, 기본적으로 이 어셈블리 코드는 실행 주체에 관계없이 다양한 방법으로 불러들여지고 실행될 수 있습니다. 대개의 경우, 라이브러리 코드를 DLL로 컴파일하여 GAC에 등록하거나 같은 설치 미디어에 배포하는 방법을 주로 택합니다. 하지만, EXE 파일은 라이브러리와는 다르게 단독 실행을 기준으로 프로그래밍하는 일이 많기 때문에 기술적으로는 참조가 가능하나 실제로 EXE 파일을 재 사용하여 프로그래밍하는 것은 예외 상황을 쉽게 일으킵니다.

DLL에 있는 코드를 재 사용하는 것과 달리 EXE에 있는 코드를 재 사용하는 것은 여러모로 가치있습니다. 특히, EXE에 있는 코드를 재사용하기 위하여 Public 형식으로 Windows Forms 및 Windows Forms Control을 노출해두는 경우 다양한 방법으로 외부에서 제어할 수 있기 때문에, 완성된 .NET 기반 일반 응용프로그램을 또 다른 프로그램의 일부 구성 요소처럼 다루는 것이 가능합니다.

이를 위해서 몇 가지 고려해야 할 사항이 있습니다.

첫 번째는, System.Windows.Forms.Application 클래스와 System.Environment 클래스의 메서드를 사용하는 코드를 보완해야 합니다. 이들 클래스는 현재 실행 중인 어셈블리의 환경을 다루기 때문에, EXE를 참조하는 또 다른 EXE 프로그램에 투영됩니다. 이 경우, 상호 간섭이 발생하므로 의도하지 않은 결과가 연출될 수 있습니다. 재 사용하고자 하는 EXE 상의 코드에서 위의 클래스를 이용하는 경우 다음과 같은 정적 프로퍼티를 두고 이 프로퍼티의 값이 True일 경우에만 해당 메서드와 프로퍼티를 제어하도록 수정해야 합니다.

using System.Reflection;
// ...
return Assembly.GetExecutingAssembly().Equals(typeof(/* 재 사용할 어셈블리 내의 형식명 */).Assembly);

위의 판정 방법을 통하여 선택적으로 호출해야 하는 멤버들은 다음과 같습니다.

System.Windows.Forms.Application 클래스: 전체 멤버
System.Environment 클래스: Exit 메서드, ExitCode 프로퍼티, FailFast 메서드

STAThreadAttribute나 MTAThreadAttribute가 적용된 Main 메서드는 CLR에 의하여 실행될 때에만 의미가 있습니다. 만약, 로드할 대상 EXE가 MTA 스레드 모드로 시작된 EXE에 의하여 실행되어야 하는 경우 별도로 스레드를 분리하는 등의 방법을 통하여 간접 실행해야 합니다. (특히 Windows Forms나 GUI 관련 응용프로그램들)

Posted by Cloud Developer 남정현 (rkttu.com)

댓글을 달아 주세요

기술 소식2007. 12. 14. 00:01

오랜 기간을 거쳐서 Mono 1.2.6이 새로 런칭했습니다. 매번 더 진화하는 모습을 보여주고 있는데 이번에는 Silverlight에 대한 구체적인 지원 방안을 포함한 것이 특징입니다.

주요 변경 사항

* Microsoft Silverlgith 1.1 응용프로그램을 완전히 제작할 수 있도록 구현되었습니다.
* X11 Server 없이 사용할 수 있는 Mac OS X용 네이티브 System.Windows.Forms 드라이버가 구현되었습니다.
* ASP.NET AJAX API와 컨트롤이 추가되었습니다.
* FastCGI 배포 기능이 지원되므로 lighttpd나 Apache에 보다 쉽게 통합할 수 있습니다.
* Mozilla 브라우저를 Embedding하는 WebControl이 추가되었습니다. (주: WebBrowserControl은 Internet Explorer를 Embedding 합니다.)
* 제네릭 및 다양한 분야에 걸친 최적화로 이전보다 더 적은 메모리 소비량을 나타내며 CoreCLR 보안 시스템이 구현되었습니다.
* C# 컴파일러가 C# 3.0 기본 언어 사양을 대부분 지원합니다.
* LDAP를 정식으로 지원합니다.

홈페이지: http://www.mono-project.com


Posted by Cloud Developer 남정현 (rkttu.com)

댓글을 달아 주세요

Windows + .NET2007. 2. 15. 22:24

Windows Forms는 사실 이미 주 스레드가 메시지 루프 처리를 위하여 이미 루프를 돌고 있는 상태이다. 그렇기 때문에 사실 별 생각없이 부르는 함수나 정의해 놓은 루프 구문은 주 스레드의 메시지 루프 처리를 방해하게 된다. 물론, 필요에 의해서 정의해놓은 함수나 루프 구문이 방해의 요소라고 하는 것은 너무한 일이지만 생각보다 이렇게 정의해놓은 함수나 루프 구문이 우리가 생각하는 것보다는 훨씬 무겁다는 점이다.

그렇다면 Windows Forms에서는 원하는 일을 마음껏 한다는게 불가능한 일인가? 그렇지 않다. 지금 이 글에서 소개하려는 비동기 패턴을 이용한다면 전혀 걱정할 필요가 없다.

1단계: 병목 현상을 일으키는 반복 구문을 찾아보기

병목 현상을 일으키는 반복 구문을 선정하는 것이 중요하다. 지금 소개하려는 패턴도 결국은 무한정 사용할 수 있는 리소스는 아니기 때문에 이 단계에서 어떤 루프를 선택하느냐가 중요하다. 그렇지만 생각보다 루프를 선정하는 일은 간단하다.

쉬운 설명을 위하여 아래의 "말도 안되는" 쓰레기 코드를 잠깐 살펴보기로 한다.

for (int i = 0; i < Int32.MaxValue; i++)
{
 StringBuilder oBuffer = new StringBuilder();

 for (int j = 0; j < 16; j++)
 {
  Random oRandom = new Random();
  oBuffer.Append(((char)oRandom.Next('a', 'z')).ToString());
 }

 this.Text = oBuffer.ToString();
}



이 코드는 보이는 그대로 21억회 이상의 루프를 돌며 매번 StringBuilder 객체를 만들고 여기에 16개의 문자를 찍어서 창의 제목을 바꾸는 일을 한다. 터무니없는 코드이지만 메시지 루프를 방해하기에 이정도면 충분하다. 우리가 이 글에서 시험해 볼 코드 역시 바로 이 코드이다.

2단계: 반복 구문을 Thread Pool 비동기 패턴으로 변환하기

2단계 제목에 쓰여있듯이 이 글에서 살펴보려는 그 해결책의 키워드는 바로 Thread Pool이다. Thread Pool은 시스템의 Thread Pool을 이용해도 좋고 직접 만든 Thread Pool을 이용해도 좋을 것이다. 하지만 여기서는 .NET Framework가 제공하는 Thread Pool을 써보기로 한다.

위의 코드를 비동기 패턴으로 바꾸면서 중요한 몇 가지가 있다.

1. Thread Pool의 비동기 패턴은 "예약"의 개념이다. - Thread Pool은 콜백 개체들을 큐에 쌓아놓고 큐에서 꺼내어 처리하는 방식이다. 물론, 한 번에 하나씩 꺼내어 처리하는 것이 아니라 몇 개의 예약된 스레드를 이용하여 분할 처리를 한다. 따라서 큐에서 없어진 작업은 그걸로 끝이다. 같은 작업을 또 시키려면 어떻게 해야 할까? 그렇다. 반복적인 예약이 그 답이다. 예약을 하기 위하여 다시 같은 함수를 호출하기 때문에 흡사 "재귀 호출"처럼 보일 수 있지만 실제로는 "재귀 호출"이 아니다. 왜냐면 예약이라는 작업 자체는 단지 큐에 콜백 객체를 더하고 나오는 것 뿐이기 때문이다.

2. 비동기는 어쨌든 다중 스레드인데 Windows Forms하고는 궁합이 안맞잖아? - 하지만 .NET Framework 2.0의 무명 대리자와 System.Action<T> 및 System.Predicate<T>, Form.Invoke 메서드를 이용하면 생각하는 것 만큼 궁합이 나쁘지 않다. 왜 그런지는 3단계의 실제 코드에서 살펴보기로 하자.

3. 다중 스레드에 대한 처리가 필요하지 않을까? - 병목 구간을 치환할 곳이 많아지고, 이렇게 치환된 곳이 동시에 실행될 필요가 있거나 그럴 확률이 높다면 필요할 수 있겠다. 하지만 비동기로 치환하였다고 할지라도 "이벤트"를 사용하여 비동기 작업 전체가 완료되었다는 것을 통지할 수 있기 때문에 주 스레드가 결과를 수신할 수 있도록 이벤트를 노출시킨다면, "이벤트 지향 프로그래밍"의 원리를 살릴 수도 있을 것이다.

3단계: 실전! 병목 구간을 비동기 패턴으로 바꾸기

[NonSerialized()]
private readonly WaitCallback HardWorkDelegate = new WaitCallback(this.HardWorkCallback);

private int m_nCount = 0;

private void HardWorkCallback(object oArgument)
{
 if (this.m_nCount.Equals(Int32.MaxValue - 1))
  return;

 StringBuilder oBuffer = new StringBuilder();

 for (int i = 0; i < 16; i++)
 {
  Random oRandom = new Random();
  oBuffer.Append(((char)oRandom.Next('a', 'z')).ToString());
 }

 this.Invoke(new Action<string>(delegate(string strText)
 { this.Text = strText ?? String.Empty; }), oBuffer.ToString());

 ThreadPool.QueueUserWorkItem(this.HardWorkDelegate);
}



위의 루프와 비교했을 때 바뀐 점이 무엇이 있을까? 기본적으로 하는 일은 같다. 하지만 메서드의 시작 부분에 조건절을 두어 조건을 만족하였을 경우 함수를 나가도록 만든 점이 다르다. 그리고 조건에 만족하지 않는 경우에는 나머지 작업을 실행하되, ThreadPool 클래스의 QueueUserWorkItem 메서드를 호출하여 작업을 다시 예약한다. 이렇게 만듦으로서 루프가 아닌 비동기 패턴으로 완벽하게 바꿀 수 있다.

그러나 문제가 있다. Windows Forms는 다중 스레드에 의한 액세스가 취약하고 실제로도 이것을 검사하여 예외를 발생시키는 루틴까지 있다. 그렇다면 방법이 없는가? 물론 있다. 무명 메서드와 Action<T>, Predicate<T> 대리자, Invoke 메서드가 그 답이다.

System.Action<T> 대리자는 T 형식의 인수를 받는 대리자 틀이다. T에 올 수 있는 형식에는 제약이 없기 때문에 편리한 것을 집어넣으면 되고 이 점을 이용하여 창의 제목을 바꾸는 대리자를 Action<T>에 맞게 바꾼 것이 위의 예이다. 대리자 안에 서술되는 익명 함수는 결과적으로 해당 클래스의 일반 멤버로 편성되기 때문에 함수 하나를 새로 정의한다는 기분으로 this 키워드를 쓸 수 있는 것이다. 만약, 올바르게 실행되었는가를 판정하는 부분이 필요하다면 System.Predicate<T> 대리자를 이용하면 된다. System.Action<T> 대리자와 같지만 Boolean 반환 값이 있다는 점이 다르다. Predicate 대리자로 반환한 값은 Invoke 메서드의 반환 객체로 전달받을 수 있다.

4단계: 테스트와 생각해 볼 문제

위의 코드를 테스트해보면 아주 현란하게 바뀌는 창 제목을 볼 수 있을 것이다. 하지만 기왕 처리하는 거 좀 더 완벽하게 하고 싶은데 그런 생각으로 보면 걱정되는게 하나 있다. 바로 CPU 점유율이다. 이렇게 빨리 바뀌는거면 CPU 점유율이 너무 높이 치솟는것은 아닐까 걱정이 된다. 그렇다면 스레드의 우선 순위를 낮출 방법을 생각해 볼 수 있다. 무식한 방법이지만 Thread.Sleep을 이용하여 스레드의 우선 순위를 떨어뜨려가며 CPU 점유율을 낮출 수 있다. 하지만 더 좋은 방법이 얼마든지 있으므로 충분히 연구해봐야 할 문제이다.

Posted by Cloud Developer 남정현 (rkttu.com)

댓글을 달아 주세요

Windows + .NET2007. 1. 20. 11:33

IDataReader 인터페이스는 Database Command 객체로부터 데이터 결과 집합을 반환받을 때 주로 사용하는 ExecuteReader 함수의 결과 객체이다. 하지만 이 객체는 "커서"의 개념을 사용하고 있어서 디버거에서 사용하기가 매우 불편하다. 내용을 한꺼번에 살펴볼 수 있는 방법이 없어서 디버거나 NUnit에서 불편했던 적이 많다. 이런 점을 해결하기 위해서 개인적으로 IDataReader 뷰어 폼을 만들어보았다. 참고로 이것은 Windows Forms를 위한 것이다.

public partial class DataReaderViewer : Form
    {
        public DataReaderViewer()
            : this(null)
        {
        }

        public DataReaderViewer(IDataReader oReader)
        {
            this.m_oReader = oReader;
            this.InitializeComponent();
        }

        private IDataReader m_oReader;

        private void DataReaderViewer_Load(object sender, EventArgs e)
        {
            this.RefreshDataView();
        }

        [Browsable(true)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
        public IDataReader Reader
        {
            get { return this.m_oReader; }
            set
            {
                this.m_oReader = value;
                this.RefreshDataView();
            }
        }

        public bool RefreshDataView()
        {
            if (this.m_oReader != null && !this.m_oReader.IsClosed)
            {
                DataTable oTable = new DataTable();
                oTable.Load(this.m_oReader);
                this.oViewer.DataSource = oTable;
                return true;
            }

            return false;
        }

        public static DialogResult ShowViewer(IDataReader oReader)
        {
            return (new DataReaderViewer(oReader)).ShowDialog();
        }
    }

위의 코드에서 oViewer라고 하는 이름의 컨트롤을 추가하여 디자인하는 것은 실제로 적용할 때의 몫이 되겠다. 하지만 개략적인 원리는 간단하며, 디버거에서 사용하기 편리하게 하기 위하여 ShowViewer라고 하는 정적 함수를 두었다. 또한, 필요하다면 네임스페이스를 따로 두지는 말자. 직접 실행창이나 디버거에서 네임스페이스를 인식할 수 업는 상황도 자주 오기 때문이다.

Posted by Cloud Developer 남정현 (rkttu.com)

댓글을 달아 주세요

Windows + .NET2007. 1. 17. 23:53
.NET Framework 2.0에서 구현이 잘 된 컴포넌트로 손에 꼽을 수 있는 것이 바로 WebBrowser 컨트롤이다. 이 컨트롤은 단순히 Internet Explorer를 가져온 것이 아니다. 대표적인 예로 특별한 노력을 기울이지 않아도 상호 운용이 가능하다는 점이다.

1. JScript에서 Host 프로그램에 액세스하기

JScript에서 Host 프로그램을 접근하고 사용하는 방법은 간단하다. 문서를 로드하기 전에 WebBrowser 컨트롤 객체의 속성 중 하나인 ObjectForScripting에 원하는 객체를 지정해 주면 된다. 단, 여기에 지정할 수 있는 객체는 반드시 System.Runtime.InteropServices.ComVisibleAttribute가 지정되어있어야 하고 물론 값을 True로 설정해 놓은 상태여야 한다.

로드할 문서 안의 자바스크립트에서는 window.external 객체를 통하여 Host 프로그램이 노출한 객체를 사용할 수 있다. 만약 HelloWorld라는 클래스를 노출하였고 이 클래스의 멤버 함수 ShowMessage 함수를 실행하려 한다면 window.external.ShowMessage() 와 같이 기술하면 되겠다.

2. Host 프로그램에서 JScript에 액세스하기

문서가 완전히 로드가 되었을 때를 전제로 한다. 문서가 완전히 로드된 상태에서 JScript 함수를 단독으로 실행하고 그 결과값을 되돌려 받는 방법은 간단하다. WebBrowser.Document.InvokeScript 함수를 이용하는 방법이다. InvokeScript 함수는 두 가지 버전이 있는데, 매개 변수를 받는 버전과 받지 않는 버전이다. 매개 변수는 System.Object의 배열 형태로 전달되고 결과값은 System.Object 형태로 반환된다. 반환 값이 없는 함수의 경우 null 참조 (Visual Basic의 Nothing)를 반환할 것이다.

3. 신경써야 할 것들

COM과 닷넷 사이가 완전히 자유로운 것은 아니다. 그러므로 몇 가지 챙겨야 할 것이 있다. 되도록 함수와 프로퍼티 수준에서 연동을 마무리하는 것이 좋으며 콜백 등을 구현하고자 한다면 테스트해야 할 것이 많을 것으로 생각된다. 또한 닷넷 2.0의 제네릭은 COM으로 전달이 불가능하다.
Posted by Cloud Developer 남정현 (rkttu.com)

댓글을 달아 주세요

Windows + .NET2006. 12. 6. 01:40
올려놓고보니 Windows Forms를 NUnit으로 테스트하는 것이 굉장히 부적절한것처럼 보입니다. 그리고 문득 드는 생각은, NUnit이 다중 스레드 환경 아래에서 사용되어도 괜찮은가에 대한 의문입니다. 별도의 분리된 스레드에서도 NUnit GUI가 올바르게 테스트 결과를 수집할 수 있게 하려면 무언가 별도의 테스트 프레임워크를 더 만들어야 하지 않나 싶습니다.
Posted by Cloud Developer 남정현 (rkttu.com)

댓글을 달아 주세요

Windows + .NET2006. 12. 4. 23:35

NUnit을 좀 더 일반적으로 사용하기 위해서 꼭 짚고 넘어가야 할 부분이 하나 있습니다. 바로 NUnit GUI와 Windows Forms의 ActiveX Interop 기능 사이의 충돌입니다. 그저 다음처럼 코드를 작성하고 테스트를 실행하게 되면 "빨간 막대"를 만납니다.

[CODE]
[Test]
public void Test()
{
  Form f = new Form();
  f.Controls.Add(new WebBrowser());
  f.ShowDialog();
}
[/CODE]

NUnit GUI는, 아직 자세히 살펴보지는 않았지만, 모든 실행 루틴을 다중 스레드에 의하여 실행하는 것처럼 보입니다. 위와 같은 테스트 코드는 빨간 막대를 만나지만 결코 의미있는 빨간 막대가 될 수 없습니다. 왜냐면, 아무런 진행이 되지 않기 때문이죠. 구체적으로, 위와 같은 코드를 NUnit GUI 상에서 실행하게되면, ActiveX 컨트롤의 인스턴스를 스레드 모델 규정에 따르자면 만들 수 없다는 이야기를 하게 됩니다. 그러면, 방법이 없는걸까요? 아닙니다! 아래 코드는 .NET Framework 2.0을 기준으로 설명하는 것입니다.

[CODE]
[Test]
public void Test()
{
  Thread t = new Thread(delegate (object o)
  {
       Form f = new Form();
       f.Load += new new EventHandler(m_oTestForm_Load);
       Application.Run(f);
  }

  t.TrySetApartmentState(ApartmentState.STA);
  t.Start(null);
  t.Join();
}

private void m_oTestForm_Load(object sender, EventArgs e)
{
  Form f = (Form)sender;
  f.Controls.Add(new WebBrowser());
}
[/CODE]

이처럼 코드를 고치면 Windows Forms의 스레드는 NUnit GUI의 스레드가 아닌 새로운 스레드에서 인스턴스를 만들게 되며, 이 때 만난 새로운 스레드는 STA 모드이므로 Windows Forms를 비롯하여 Web Browser 컨트롤이 정상적으로 생성될 수 있는 환경이 됩니다. 그리고 NUnit GUI가 실행하게될 테스트 메서드의 마지막에 위치한 Join 메서드로 폼이 닫히기 전까지 테스트를 동결시킬 수 있습니다. 만약, 동결이 필요하지 않다면 Join 메서드를 사용하지 않으면 되겠지요. :-)

Posted by Cloud Developer 남정현 (rkttu.com)

댓글을 달아 주세요

Web Programming2006. 12. 3. 02:52

Visual Basic Extension, Object Linking Embedded, Component Object Model, Binary Behavior, Distributed Component Object Model, Component Object Model Plus, Windows DNA 등 Microsoft가 C++과 객체 지향 기술을 이용하여 개발해왔던 다양한 프레임워크들 중 하나인 ActiveX가 이제는 그 수명을 다하려는듯한 결정적인 루머를 내고 있다.

당장 내년 1월에 새롭게 출시될 Windows Vista에 포함된 IE7에서는 ActiveX 컨트롤 자체가 완벽하게 Sand-Box 처리가 될 것이라고 한다. ActiveX 인스턴스는 어차피 Internet Explorer와 함께 그 수명을 같이하는 것이므로 이상할 것이 없지만 Sand-Box 라는 것은 다소 생소하실 분들이 많겠다.

요약하자면, ActiveX 컨트롤을 PC에 다운로드하여 Install하는 작업을 하지 않고 마치 Java Applet이나 Flash Content 또는 Script 처럼 동작하게 만들겠다는 의미가 된다. 보안 상으로는 틀림없이 이득이 될테지만 ActiveX를 개발하여 배포해왔던 업체들에게는 정말 큰 일인셈이다.

퍼블릭 릴리즈로 나온 IE7을 먼저 써본 사람들은 새로운 모드인 "안전 모드"에 대하여 궁금해 했을 것이라 생각한다. 이 "안전 모드"가 바로 이 루머와도 관련이 있겠다. 단순히 설치된 ActiveX 컨트롤 전체를 사용하지 않는 것으로 시작해서 유입되는 모든 ActiveX 컨트롤이 하드 디스크에 저장되거나 설치되는 것을 통제하고, 퍼미션 조절을 통하여 작업에 제약을 걸어놓는 모드이다. 정식으로 나올 Windows Vista의 IE7에서는 아마도 이 "안전 모드"를 기본 모드로 사용하도록 권하기 때문에 이런 루머가 나온 것이 아닐까 싶다.

더 재미있는 사실은, Windows Vista가 이제는 .NET Framework 3.0을 기본으로 내장하고 있다는 사실이다. 이것이 의미하는 바는 ActiveX의 운명을 더욱 확실하게 만들어주고 있다. .NET Framework 2.0부터 강화된 ClickOnce는 Windows Forms로 개발된 .NET Application을 Internet Explorer와 연동할 수 있도록 도울 것이며, Windows Presentation Framework의 XAML은 Internet Explorer 7.0이 직접 렌더링하는 것도 가능하다. 또한 Windows Presentation Framework Everywhere는 경량화된 응용프로그램을 지원할 것이다. 지금 언급한 이 세 가지 기술을 적극 유치하고 유도하는 것이 MS의 실제 목표다. ActiveX는 보안 상의 문제가 많다는 점을 이용하여 오히려 뒤로 빼려는 셈이다.

또한 이제는 ActiveX보다는 Flash, Java를 이용한 개발을 더욱 선호하는 추세이다. 컨텐츠 프로바이더의 입장에서도 ActiveX는 메리트가 없는 셈이다. 그리고 기억하는 사람들도 아직 있으리라 생각되는 이올라스社의 소송 승리도 ActiveX의 숨통을 조르고 있다.

Posted by Cloud Developer 남정현 (rkttu.com)

댓글을 달아 주세요