12 프로젝트유형 C# 으로어떤종류의프로그램을만들수있는지확인하고싶다면비주얼스튜디오에서 파일 / 새 프로젝트 메뉴를선택했을때나타나는내용을보면된다. Visual Studio Express 2012 를설치했다 면그림 12.1 처럼나오고, Ultimate 버전등을설치했다면좀더

Similar documents
4장기본프로그래밍2

MVVM 패턴의 이해

Microsoft PowerPoint - CSharp-10-예외처리

PowerPoint Template

Visual Basic 기본컨트롤

Visual Basic 반복문

gnu-lee-oop-kor-lec06-3-chap7

Visual Basic Visual Basic 소개

[ 그림 8-1] XML 을이용한옵션메뉴설정방법 <menu> <item 항목ID" android:title=" 항목제목 "/> </menu> public boolean oncreateoptionsmenu(menu menu) { getme

Microsoft PowerPoint - chap02-C프로그램시작하기.pptx

학습목표 함수프로시저, 서브프로시저의의미를안다. 매개변수전달방식을학습한다. 함수를이용한프로그래밍한다. 2

C# Programming Guide - Types

Javascript

제 1장 C#의 개요

PowerPoint Presentation

PowerPoint Presentation

슬라이드 1

쉽게 풀어쓴 C 프로그래밍

Microsoft PowerPoint - chap01-C언어개요.pptx

PowerPoint 프레젠테이션

Microsoft PowerPoint UI-Event.Notification(1.5h).pptx

Microsoft Word - ntasFrameBuilderInstallGuide2.5.doc

Microsoft PowerPoint - Java7.pptx

오버라이딩 (Overriding)

(Microsoft PowerPoint - C#\260\355\261\3363\(WinForm\).ppt)

Windows 8에서 BioStar 1 설치하기

JAVA 프로그래밍실습 실습 1) 실습목표 - 메소드개념이해하기 - 매개변수이해하기 - 새메소드만들기 - Math 클래스의기존메소드이용하기 ( ) 문제 - 직사각형모양의땅이있다. 이땅의둘레, 면적과대각

제이쿼리 (JQuery) 정의 자바스크립트함수를쉽게사용하기위해만든자바스크립트라이브러리. 웹페이지를즉석에서변경하는기능에특화된자바스크립트라이브러리. 사용법 $( 제이쿼리객체 ) 혹은 $( 엘리먼트 ) 참고 ) $() 이기호를제이쿼리래퍼라고한다. 즉, 제이쿼리를호출하는기호

JAVA PROGRAMMING 실습 08.다형성

13 주차 - MDI, Exception, WebBrowser, RichTextBox, AlarmProgram 대림대학 년도 1 학기홍명덕

ISP and CodeVisionAVR C Compiler.hwp

학습목표 텍스트파일을다룰수있다. 스트림읽기, 쓰기를안다. 2

슬라이드 1

<4D F736F F F696E74202D20B5A5C0CCC5CDBAA3C0CCBDBA5F3130C1D6C2F75F32C2F7BDC32E >

(Microsoft Word - C# \304\332\265\371 \277\254\275\300.docx)

다른 JSP 페이지호출 forward() 메서드 - 하나의 JSP 페이지실행이끝나고다른 JSP 페이지를호출할때사용한다. 예 ) <% RequestDispatcher dispatcher = request.getrequestdispatcher(" 실행할페이지.jsp");

Microsoft PowerPoint - 07-C#-12-GDI+.ppt [호환 모드]

iii. Design Tab 을 Click 하여 WindowBuilder 가자동으로생성한 GUI 프로그래밍환경을확인한다.

어댑터뷰

API - Notification 메크로를통하여어느특정상황이되었을때 SolidWorks 및보낸경로를통하여알림메시지를보낼수있습니다. 이번기술자료에서는메크로에서이벤트처리기를통하여진행할예정이며, 메크로에서작업을수행하는데유용할것입니다. 알림이벤트핸들러는응용프로그램구현하는데있어

PowerPoint 프레젠테이션

DataBinding

C# 입문 : 이론과 실습

PowerPoint Presentation

쉽게 풀어쓴 C 프로그래밍

C# 입문 : 이론과 실습

경우 1) 80GB( 원본 ) => 2TB( 복사본 ), 원본 80GB 는 MBR 로디스크초기화하고 NTFS 로포맷한경우 복사본 HDD 도 MBR 로디스크초기화되고 80GB 만큼포맷되고나머지영역 (80GB~ 나머지부분 ) 은할당되지않음 으로나온다. A. Window P

tiawPlot ac 사용방법

안드로이드기본 11 차시어댑터뷰 1 학습목표 어댑터뷰가무엇인지알수있다. 리스트뷰와스피너를사용하여데이터를출력할수있다. 2 확인해볼까? 3 어댑터뷰 1) 학습하기 어댑터뷰 - 1 -

임베디드 시스템 소프트웨어

윈도우시스템프로그래밍

C# 입문 : 이론과 실습

윈도우즈프로그래밍(1)

UI TASK & KEY EVENT

q 이장에서다룰내용 1 객체지향프로그래밍의이해 2 객체지향언어 : 자바 2

Microsoft PowerPoint Android-SDK설치.HelloAndroid(1.0h).pptx

2) 활동하기 활동개요 활동과정 [ 예제 10-1]main.xml 1 <LinearLayout xmlns:android=" 2 xmlns:tools="

슬라이드 1

Microsoft Word - 3부A windows 환경 IVF + visual studio.doc

PowerPoint Template

var answer = confirm(" 확인이나취소를누르세요."); // 확인창은사용자의의사를묻는데사용합니다. if(answer == true){ document.write(" 확인을눌렀습니다."); else { document.write(" 취소를눌렀습니다.");

<4D F736F F D20C1A4BAB8B0FAC7D0C8B85F FC7CFB0E85F446F63>

지도상 유의점 m 학생들이 어려워하는 낱말이 있으므로 자세히 설명해주도록 한다. m 버튼을 무리하게 조작하면 고장이 날 위험이 있으므로 수업 시작 부분에서 주의를 준다. m 활동지를 보고 어려워하는 학생에게는 영상자료를 접속하도록 안내한다. 평가 평가 유형 자기 평가

Microsoft Word - Crackme 15 from Simples 문제 풀이_by JohnGang.docx

PowerPoint Presentation

04장 메시지 처리 유형

윈도우시스템프로그래밍

제8장 자바 GUI 프로그래밍 II

2009년 상반기 사업계획

Microsoft PowerPoint - web-part02-ch16-이벤트.pptx

C# 입문 : 이론과 실습

gnu-lee-oop-kor-lec10-1-chap10

Studuino소프트웨어 설치

Microsoft PowerPoint - e pptx

1

C++ Programming

Microsoft PowerPoint - ÀÚ¹Ù08Àå-1.ppt

A SQL Server 2012 설치 A.1 소개 Relational DataBase Management System SQL Server 2012는마이크로소프트사에서제공하는 RDBMS 다. 마이크로소프트사는스탠다드 standard 버전이상의상업용에디션과익스프레스 exp

Microsoft PowerPoint - hci2-lecture2.ppt [호환 모드]

adfasdfasfdasfasfadf

예제 2) Test.java class A intvar= 10; void method() class B extends A intvar= 20; 1"); void method() 2"); void method1() public class Test 3"); args) A

Microsoft PowerPoint - hci2-lecture12 [호환 모드]

Microsoft PowerPoint - additional01.ppt [호환 모드]

PowerPoint 프레젠테이션

이장에서다룰내용 테두리를제어하는스타일시트 외부여백 (Margin) 과내부여백 (Padding) 관련속성 위치관련속성 2

Microsoft 을 열면 깔끔한 사용자 중심의 메뉴 및 레이아웃이 제일 먼저 눈에 띕니다. 또한 은 스마트폰, 테블릿 및 클라우드는 물론 가 설치되어 있지 않은 PC 에서도 사용할 수 있습니다. 따라서 장소와 디바이스에 관계 없이 언제, 어디서나 문서를 확인하고 편집

1. auto_ptr 다음프로그램의문제점은무엇인가? void func(void) int *p = new int; cout << " 양수입력 : "; cin >> *p; if (*p <= 0) cout << " 양수를입력해야합니다 " << endl; return; 동적할

BMP 파일 처리

<4D F736F F D20BEBEBCA520C4DAB5F920BFACBDC0202D20B8D6C6BC20BEB2B7B9B5E5BFCD20C0CCBAA5C6AE2E646F6378>

목차 윈도우드라이버 1. 매뉴얼안내 운영체제 (OS) 환경 윈도우드라이버준비 윈도우드라이버설치 Windows XP/Server 2003 에서설치 Serial 또는 Parallel 포트의경우.

<4D F736F F F696E74202D20C1A63236C0E520BED6C7C3B8B428B0ADC0C729205BC8A3C8AF20B8F0B5E55D>

PowerPoint Presentation

PowerPoint Template

3ÆÄÆ®-14

4S 1차년도 평가 발표자료

. 스레드 (Thread) 란? 스레드를설명하기전에이글에서언급되는용어들에대하여알아보도록하겠습니다. - 응용프로그램 ( Application ) 사용자에게특정서비스를제공할목적으로구현된응용프로그램을말합니다. - 컴포넌트 ( component ) 어플리케이션을구성하는기능별요

Microsoft PowerPoint - chap06-1Array.ppt

Microsoft PowerPoint - chap06-2pointer.ppt

PowerPoint 프레젠테이션

Microsoft PowerPoint - hci2-lecture8.ppt [호환 모드]

Transcription:

03 닷넷응용프로그램 1부와 2부를통해 C# 언어를익혔다면이제분명뭔가를만들고싶을것이다. 그런데과연어떤프로그램을만들수있을까? 프로그램의종류에따라만드는방법이다르기도하지만처음에는어디서부터어떻게시작해야할지막막할수있다. 그래서 3부에서는 C# 언어로만들수있는다양한응용프로그램사례를설명한다. 이를통해여러분이만들고싶은프로그램을어떤종류의프로젝트로시작해야할지익힐수있을것이다.

12 프로젝트유형 C# 으로어떤종류의프로그램을만들수있는지확인하고싶다면비주얼스튜디오에서 파일 / 새 프로젝트 메뉴를선택했을때나타나는내용을보면된다. Visual Studio Express 2012 를설치했다 면그림 12.1 처럼나오고, Ultimate 버전등을설치했다면좀더다양한템플릿이나온다. 그림 12.1: C# 새프로젝트 2 3 부 _ 닷넷응용프로그램

그림 12.1 의프로젝트템플릿종류를정리하면다음과같다. 1. Windows Forms 응용프로그램 2. WPF 응용프로그램 3. 콘솔응용프로그램 4. 클래스라이브러리 5. 빈프로젝트 이가운데 3번의콘솔응용프로그램은이미 1부와 2부의예제에서사용했던유형이다. 콘솔프로그램은대부분컴퓨터를잘다루는사용자층을대상으로제작되며, 배치작업 (batch process) 으로자동화할수있기때문에다중작업을손쉽게처리할수있다는장점이있다. 따라서서버측관리프로그램을만들때자주콘솔응용프로그램형식이사용되지만, 아쉽게도윈도우운영체제를구매한거의모든일반사용자층에서는선호하지않는유형이다. 4번의클래스라이브러리는이미 5.2 프로젝트구성 절에서자세히다뤘다. 이장에서는아직설명한적이없었던 1번 Windows Forms 및 2번의 WPF 응용프로그램의개발과함께서비스, 웹, 모바일폰을위한프로젝트유형까지살펴본다. 12.1 Windows Forms 응용프로그램 윈도우폼응용프로그램 은윈도우운영체제에서일상적으로실행하는모든응용프로그램이속하는프로젝트유형이다. 예를들어, 윈도우탐색기 는콘솔프로그램이아니고 윈도우프로그램 인데, 이렇게윈도우 (Window) 를가진응용프로그램을만들고싶다면 C# 에서 윈도우폼응용프로그램 템플릿을선택해서시작하면된다. 새프로젝트 메뉴를이용해윈도우폼프로젝트를만들어보자. 그럼비주얼스튜디오는 C# 프로젝트에일련의파일을추가하고중앙에는디자인화면을보여준다. 12. 프로젝트유형 3

그림 12.2: 비주얼스튜디오의윈도우폼프로젝트 이상태에서 Ctrl + F5 를눌러실행해보면다음과같이윈도우하나가실행되는것을확인할수있다. 그림 12.3: 윈도우폼프로그램 보다시피비주얼스튜디오의디자인화면에서본윈도우와동일한모습으로실행된다. 어떻게이것이 가능할수있는지이제부터그비밀을파헤쳐보자. 4 3 부 _ 닷넷응용프로그램

우선그림 12.2의우측솔루션탐색기영역을보면 WindowsFormsApplicaiton1 C# 프로젝트아래에콘솔프로젝트와는다르게 Form1.cs 파일이있음을확인할수있다. 그파일을마우스로 2번누르면나오는창이바로그림 12.2 중앙의 Form1.cs [ 디자인 ] 창이다. 디자인창이아닌코드파일을보고싶다면 Form1.cs 파일을마우스우측버튼으로누른다음 코드보기 (View code) 메뉴를선택하거나단축키 F7을누르면된다. 예제 12.1: Form1.cs 기본코드 // [using 문생략 ] using System.Windows.Forms; namespace WindowsFormsApplication1 public partial class Form1 : Form public Form1() InitializeComponent(); 이것으로비밀의절반을알게됐다. 윈도우폼프로젝트를실행했을때 윈도우 가나타난것은바로 Form 클래스를상속받은 Form1 덕분이다. Form 인스턴스 1 개는윈도우한개에대응한다. 그런데 Form1 클래스를언제생성한것일까? 이를확인하려면 Program.cs 파일을열어봐야한다. // [using 문생략 ] using System; using System.Windows.Forms; namespace WindowsFormsApplication1 static class Program [STAThread] static void Main() Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new Form1()); 12. 프로젝트유형 5

보다시피 Main 메서드가실행되는시점에 new Form1 코드를통해윈도우를생성하고있다. Application 타입은윈도우폼프로그램의기본적인처리를담당하는정적메서드를제공하는데, 이가운데 Run 정적메서드는윈도우가닫히기전까지응용프로그램을계속실행해두는역할을한다. 즉, 스레드는 Main 메서드의 Application.Run 내부의코드를윈도우가닫힐때까지실행하느라 Main 메서드를벗어나지않는다. Program.cs 파일의내용은거의바뀌지않기때문에 Form1 객체가 Run 메서드의인자로전달되는정도만알아두고넘어가자. 여기서새롭게나온 Application 타입과 Form 타입은모두 System. Windows.Forms 네임스페이스에속해있고, System.Windows.Forms.dll 어셈블리에구현돼있다. 여러분이윈도우폼프로그램을만들기위해별도로어셈블리참조를하지않아도됐던것은 Windows Forms 응용프로그램 템플릿에이미그런작업이돼있었기때문이다. 만약 빈프로젝트 템플릿으로프로젝트를시작했다면 System.Windows.Forms.dll 어셈블리를명시적으로참조해야만한다. 이제디자인창에대해좀더알아보자. Form1 디자인창에서마우스를이용해윈도우의크기를변경하고다시 Ctrl+F5를눌러실행해본다. 그림 12.4: 디자인창에서변경한크기가반영됨 6 3 부 _ 닷넷응용프로그램

디자인에서변경된창의크기 (Width, Height) 값이어딘가에코드로반영됐을것이고, 그래서프로그램을실행하면설정된크기대로윈도우가나타나는것을짐작할수있다. 그런데프로그램을종료하고, Form1.cs 코드파일을열면 (F7) 예제 12.1의내용과전혀다르지않다. 그럼, 도대체바뀐크기정보는어디에저장된것일까? 이를알려면솔루션탐색기에서 Form1.cs 노드를펼쳐 Form1. Designer.cs 파일을열어보면된다. 그림 12.5: Form1.Designer.cs 파일명은 Form1.Designer.cs이지만내부에정의된타입명은 Form1으로예제 12.1의 Form1과같다. 동일한이름의타입이가능한이유는 C# 2.0에소개된부분클래스 (partial class) 가적용됐기때문이다. 따라서 C# 컴파일러는 Form1.cs와 Form1.Designer.cs 파일을컴파일시에결합해단하나의 Form1 타입을만든다. 비주얼스튜디오는디자인창에서변경되는모든내용을그림 12.5의하단에보이는 Windows Form 디자이너에서생성한코드 영역에반영한다. 그부분의코드를펼쳐보면다음과같이 InitializeComponent 메서드내에변경된창의크기정보가포함된것을확인할수있다. private void InitializeComponent() this.suspendlayout(); // // Form1 // 12. 프로젝트유형 7

this.autoscaledimensions = new System.Drawing.SizeF(7F, 12F); this.autoscalemode = System.Windows.Forms.AutoScaleMode.Font; this.clientsize = new System.Drawing.Size(266, 83); this.name = "Form1"; this.text = "Form1"; this.resumelayout(false); 비주얼스튜디오는디자인창과 InitializeComponent 메서드의내용을동기화한다. 디자인창에서내용을변경하면 InitializeComponent에반영되고, InitializeComponent의내용을변경해도디자인창에반영된다. 예를들어, this.text 속성의값을 테스트폼 이라고변경해서저장한후디자인창을보면그림 12.6과같이윈도우캡션영역의문자열이변경돼있는것을볼수있다. 그림 12.6: 변경된윈도우타이틀 디자인창에서직접적으로변경할수있는것은윈도우의크기뿐이다. 그렇다면그외의윈도우속성값을변경하려면 Form1.Designer.cs 파일을열어서편집해야할까? 이런불편함을덜기위해비주얼스튜디오는그림 12.2의우측하단에보이는 속성 (Properties) 창을별도로제공한다. Form1 디자인창에서윈도우영역을마우스로선택하면그림 12.7처럼자동으로 Form 타입의속성을속성창에나열하고직접변경할수있다. 그림 12.7: 디자인창에선택된객체의속성을보여주는속성창 속성창을통해 Form의속성값을자유롭게바꿀수있고변경사항을다시디자인창을통해실시간으로확인할수있다. 최종적으로윈도우폼프로젝트의동작과정을정리해보자. 프로그램을실행하면맨먼저 Program.cs 파일의 Main 메서드가실행되고, new Form1 코드를통해 Form1 타입의인스턴스가생성된다. 예제 12.1에따라 Form1 생성자는 InitializeComponent 메서드를호출하는데, 이코드는 8 3 부 _ 닷넷응용프로그램

Form1.Designer.cs 파일에비주얼스튜디오의디자인창과속성창의내용이코드로반영돼있다. 결 국개발자가지정한크기, 타이틀, 색상등이설정돼그림 12.6 의윈도우가실행된다. 12.1.1 메시지와이벤트윈도우폼프로그램의가장큰특징은메시지 (message) 를기반으로동작한다는점이다. 이는닷넷이아닌윈도우운영체제자체의특징에서기인한다. 예를들어, 그림 12.8처럼실행된윈도우폼위에서마우스버튼을한번눌러보자. 그럼내부적으로윈도우운영체제는 마우스버튼이눌렸다. 라는메시지를해당윈도우폼에전달한다. Form 타입은이러한메시지가발생했을때 C# 클래스의이벤트 (event) 를발생시킨다. 그림 12.8: 윈도우폼이벤트발생 마우스클릭 윈도우운영체제가마우스눌림을윈도우에전달 Form1.Click 이벤트발생 따라서마우스가눌릴때개발자가원하는코드를실행하고싶다면 Form1 타입의 Click 이벤트에대 응하는코드를작성하면된다. 예제 12.2: 마우스버튼이눌릴때실행되는코드 using System.Windows.Forms; namespace WindowsFormsApplication1 public partial class Form1 : Form public Form1() InitializeComponent(); this.click += Form1_Click; 12. 프로젝트유형 9

void Form1_Click(object sender, System.EventArgs e) MessageBox.Show(" 마우스눌림 "); MessageBox 타입은 System.Windows.Forms 네임스페이스에정의된것으로, 사용자에게간단한메시지창을하나보여주고싶을때사용한다. 따라서예제 12.2는윈도우위에서사용자가마우스버튼을누를때마다 마우스눌림 이라는대화상자가나타난다. Form 타입은 Click 이벤트를제공하고, 개발자는 Click 이벤트가발생했을때처리하는메서드를만든다. 이런메서드의코드를이벤트처리기 (event handler) 라고한다. 예전에는이벤트에반응하기위해매번메서드를작성해야했지만익명메서드 / 람다식이지원된후부터는다음과같이간단한유형의이벤트처리기도만들수있게됐다. public Form1() InitializeComponent(); this.click += (s, e) => MessageBox.Show(" 마우스눌림 "); ; 이벤트에대응하는또다른방법이있다. Form 타입은윈도우메시지에대응되는방법을이벤트뿐아니라메서드재정의로도제공한다. 대부분의유명한이벤트는그동작에대응되는가상메서드 (virtual method) 를정의해뒀기때문에 Click 이벤트를사용하지않고다음과같이처리하는방법도가능하다. using System.Windows.Forms; namespace WindowsFormsApplication1 public partial class Form1 : Form public Form1() 10 3 부 _ 닷넷응용프로그램

InitializeComponent(); // 일반적으로이벤트이름에 "On" 접두사가붙어정의된다. protected override void OnClick(System.EventArgs e) base.onclick(e); MessageBox.Show(" 마우스눌림 "); 어떤방식을사용하느냐는개발자의취향문제일뿐크게중요하지는않지만가독성을위해한가지방법으로통일하는것이바람직하다. 이벤트처리기는비주얼스튜디오의디자인창에서도생성할수있다. 그림 12.2에서디자인영역에보이는윈도우내부를마우스로두번연속해서눌러보자. 그러면자동으로비주얼스튜디오는 Form1_Load 이벤트처리기를작성하고 Form1.cs 코드파일을보여준다. 다음은그렇게생성된 Load 이벤트처리기에일부러 MessageBox 코드를넣어봤다. using System.Windows.Forms; namespace WindowsFormsApplication1 public partial class Form1 : Form public Form1() InitializeComponent(); private void Form1_Load(object sender, System.EventArgs e) MessageBox.Show(" 폼이보이기전입니다."); 12. 프로젝트유형 11

Load 이벤트는디자인창에서생성한것이므로 this.load += Form1_Load 코드는 InitializeComponent 내부에생성돼있다. 여기서한가지알수있는사실은이벤트의종류가많다는점이다. 윈도우의상태변화, 마우스이동 / 눌림, 키보드눌림등의모든상황에대해윈도우는메시지를통해 Form에게알려주고이를이벤트로노출시킨다. 표 12.1에 Form 타입에정의된 25개의이벤트중일부가정리돼있다. 표 12.1: Form 타입의이벤트 이벤트 FormClosed FormClosing Load ResizeBegin ResizeEnd Shown SizeChanged 설명폼이닫히고나서발생하는이벤트폼이닫히기바로전발생하는이벤트폼이보이기바로전발생하는이벤트, 일반적으로 Form 에서사용될객체들의초기화를 Load 이벤트처리기에작성함. 폼의크기가변경되기시작했음을알리는이벤트폼의크기가변경됐음을알리는이벤트폼이화면에처음보일때발생하는이벤트폼의크기가변경되는동안매번발생하고최소화 / 최대화시킬때도발생 물론 Form 타입의상속관계를고려했을때더많은이벤트가있다. Form ContainerControl ScrollableControl Control 타입 Component 타입 ContainerControl에정의된이벤트 1개 ScrollableControl에정의된이벤트 1개 Control에정의된이벤트 68개 Component에정의된이벤트 1개 이중에서 Control 에정의된이벤트중자주사용되는것을표 12.2 에정리했다. 표 12.2: Control 타입의이벤트 이벤트 BackColorChanged Click DoubleClick DragEnter DragLeave DragDrop 설명컨트롤의배경색이바뀔때발생하는이벤트컨트롤위에서클릭동작이발생했을때컨트롤위에서더블클릭동작이발생했을때마우스로끌기시작한객체가컨트롤에진입했을때마우스로끌기시작한객체가컨트롤의경계를벗어났을때마우스의끌어서놓기작업이완료됐을때

이벤트 DragOver KeyDown KeyUp KeyPress LocationChanged MouseClick MouseDoubleClick MouseDown MouseUp MouseEnter MouseHover MouseLeave MouseMove MouseWheel Move Paint Resize SizeChanged TextChanged 설명마우스로끌기시작한객체가컨트롤위에서머무르는동안발생키보드의키가눌렸을때키보드의키가눌렸다가떼어졌을때키보드의키입력이발생한경우로서, 한번의 KeyDown + KeyUp 조합이있을때마다함께발생하는이벤트컨트롤의위치가변경됐을때컨트롤위에서마우스클릭이발생했을때, Click 이벤트보다더빨리발생하며오직마우스호환장치에의해서만클릭이됐을때발생컨트롤위에서마우스호환장치에의해더블클릭이발생했을때마우스의버튼이눌렸을때마우스의버튼이눌렸다떼어졌을때컨트롤위에마우스가진입했을때컨트롤위에마우스가머무르는동안발생컨트롤의경계를마우스가벗어났을때컨트롤위에서마우스를이동하는동안발생마우스의휠 (Wheel) 을조작했을때컨트롤의위치가이동하는동안발생컨트롤의내부를새로그릴때발생컨트롤의크기가변경되는동안발생컨트롤의크기가변경이완료됐을때발생컨트롤의타이틀내용이변경됐을때발생 윈도우폼응용프로그램을잘만들고싶다면어떤종류의이벤트가지원되는지정도는틈틈이익혀두 는것이좋다. 12.1.2 컨트롤윈도우프로그램의대표적인구성요소는 폼 (Form) 과 컨트롤 (Control) 이있다. 폼은윈도우에대응되고, 컨트롤은폼내부에포함된 자식윈도우 에해당한다. 예를들어, 그림 12.9의계산기프로그램을보면단일윈도우하나가폼이고, 그내부에포함된버튼등의구성요소가컨트롤이다. 12. 프로젝트유형 13

그림 12.9: 윈도우와컨트롤 기본적인용도의컨트롤은마이크로소프트가이미만들어뒀으며, 비주얼스튜디오의디자인창에 서 보기 (VIEW) / 도구상자 (Toolbox) ( 단축키 : Ctrl+ W, X) 를통해확인할수있다. ( 참조 : 그림 12.10) 그림 12.10: 윈도우폼에서사용가능한컨트롤 사용법은간단하다. 도구상자에있는특정항목을마우스로끌어다디자인창의윈도우폼위에놓으 면된다. 예를들기위해통신기능은없는채팅프로그램의외관을만들어보자. 필자의경우다음과 같이 3 개의컨트롤을장식해놓았다. 14 3 부 _ 닷넷응용프로그램

그림 12.11: 채팅프로그램의디자인 디자인화면에서각컨트롤을선택하면그에따른설정을속성창에서할수있다. 그림 12.12 에서는 디자인창에서 TextBox 컨트롤을선택했을때그것의속성을나열하는 속성창 을보여준다. 그림 12.12: 선택된컨트롤의속성창정보 속성창을이용해각컨트롤에서필자가변경한속성을표 12.3 에정리했다. 12. 프로젝트유형 15

표 12.3: 채팅윈도우의컨트롤속성 컨트롤변경된속성값 TextBox (Name) txtview Multiline True TextBox (Name) txtchat Button (Name) btninput Text 입력 표 12.3 의 (Name) 항목은 InitializeComponent 메서드내에다음과같은식으로반영된다. private System.Windows.Forms.TextBox txtview; private System.Windows.Forms.TextBox txtchat; private System.Windows.Forms.Button btninput; 즉, Form1.cs 코드파일에서 (Name) 으로변경된값을변수로해서해당컨트롤을접근할수있게된다. 비록통신이안되는채팅프로그램에불과하지만실행했을때 txtchat 영역에글을입력하고 입력 버튼을눌렀을때 txtview 영역에글이출력되게할수는있다. 이를위해반응해야하는이벤트는 입력 버튼의 Click 이벤트다. 따라서디자인창에서 입력 버튼을마우스로두번클릭하면자동으로 btninput_click 이벤트처리기가생성되고, 내부에예제 12.3 처럼작성할수있다. 예제 12.3: btninput 버튼의 Click 이벤트처리기 // txtview로 txtchat에입력된문자열을추가 private void btninput_click(object sender, EventArgs e) string oldtext = txtview.text; string newtext = string.format("you said: 01", txtchat.text, Environment.NewLine); txtview.text = oldtext + newtext; TextBox 컨트롤은문자열을설정하거나가져올수있는 Text 공용속성을제공한다. 이를이용하면 사용자가입력한 txtchat 컨트롤의입력내용을 btninput 버튼이눌린시점에가져와서 txtview 컨 트롤에이어붙일수있다. 이제실행해보고결과를확인하자. 16 3 부 _ 닷넷응용프로그램

그림 12.13: 실행중인예제프로그램 이처럼, 윈도우폼프로그램의기본은다양한컨트롤의사용법을익히는것과관련된다. 지면관계상 다른컨트롤의사용법을모두설명할수는없지만시간날때마다비주얼스튜디오의도구상자에있 는컨트롤을하나씩익히기를권장한다. 12.1.3 메뉴거의모든윈도우폼프로그램은기본적으로메뉴 (Menu) 를가지고있다. 여러분이이책의실습을위해실행한비주얼스튜디오도그림 12.14처럼 FILE, EDIT, 등의메뉴를제공하고, 마우스로메뉴를선택하면아래로펼쳐져서브메뉴 (Sub Menu) 가나타난다. 이렇게아래로펼쳐진다고해서흔히풀-다운메뉴 (pull-down menu) 라고도한다. 그림 12.14: 비주얼스튜디오의펼쳐진파일 (FILE) 메뉴 12. 프로젝트유형 17

이외에도마우스오른쪽버튼을이용해메뉴를나타나게하는것도있다. 이런방식의메뉴는원하는어떤위치에서든서브메뉴를펼칠수가있는데, 일반적으로는상황 (context) 에따라보여준다고해서컨텍스트메뉴 (context menu) 라고한다. 비주얼스튜디오의경우그림 12.15처럼코드편집창에서마우스오른쪽버튼을눌렀을때나오는메뉴가바로이런유형에속한다. 그림 12.15: 비주얼스튜디오편집창의컨텍스트메뉴 이중에서우선가장쉬운풀다운메뉴를먼저구현해보자. 비주얼스튜디오의윈폼디자인창을열고 도구상자 (Toolbox) 의 MenuStrip 을끌어다놓으면그림 12.16 처럼 여기에입력 (Type Here) 이라 는바 (Bar) 영역이생긴다. 그림 12.16: 윈폼디자인화면에 MenuStrip 배치

마우스로 여기에입력 영역을누르면텍스트를입력할수있게바뀌는데, 간단한실습을위해그림 12.17 과같이구성한다 ( 물론원한다면메뉴를자유롭게더추가해도된다.) 그림 12.17: 파일 메뉴와서브메뉴 2 개구성 이상태에서 Ctrl+F5를눌러프로젝트를실행해보면그림 12.17에서구성한메뉴가보이는것을확인할수있다. 하지만 새로고침, 종료 메뉴를선택해도현재는아무런반응도하지않는다. 왜냐하면해당메뉴를선택했을때실행돼야할코드가지정되지않았기때문이다. 실행을종료하고, 그림 12.18의화면과같이비주얼스튜디오의디자인창으로돌아가서 새로고침 메뉴를선택하면속성창에서해당메뉴의 (Name) 을바꿀수있다. 그림 12.18의경우 menurefresh 로설정했다. 그림 12.18: 새로고침 메뉴의 (Name) 항목편집 12. 프로젝트유형 19

마찬가지방법으로 종료 메뉴에대해서도 (Name) 값을 menuexit 로설정한후 F7 키를눌러코 드편집창으로넘어가서각각의메뉴에대해예제 12.4 와같이이벤트처리기를연결해준다. 예제 12.4: 이벤트처리기가연결된메뉴 using System; using System.Windows.Forms; namespace WindowsFormsApplication1 public partial class Form1 : Form public Form1() InitializeComponent(); this.menurefresh.click += menurefresh_click; this.menuexit.click += menuexit_click; void menurefresh_click(object sender, EventArgs e) MessageBox.Show(" 새로고침버튼눌림 "); void menuexit_click(object sender, EventArgs e) Application.Exit(); // 윈폼프로그램을종료하는메서드 이제프로그램을실행하고 새로고침, 종료 버튼을눌러보면각각 menurefresh_click, menuexit_click 이벤트처리기의코드가실행되는것을확인할수있다. 이것이풀다운메뉴의기본적인사용법이다. 다음으로, 컨텍스트메뉴의사용법을알아보자. 컨텍스트메뉴는그특성상마우스오른쪽버튼이눌린위치에서펼쳐지기때문에기본적으로마우스이벤트처리기를하나작성해야한다. 그리고내부에는 ContextMenu 타입을이용해각메뉴항목을 MenuItem 타입으로생성해컨텍스트메뉴로펼쳐질내용을구성하면된다. 20 3 부 _ 닷넷응용프로그램

using System; using System.Windows.Forms; namespace WindowsFormsApplication1 public partial class Form1 : Form public Form1() InitializeComponent(); // 마우스우측버튼이눌린경우실행되는 Form 재정의메서드 protected override void OnMouseClick(MouseEventArgs e) base.onmouseclick(e); // 마우스우측버튼이눌린경우 if (e.button == System.Windows.Forms.MouseButtons.Right) // 컨텍스트메뉴객체를만들고 ContextMenu ctxmenu = new ContextMenu(); // 컨텍스트메뉴에들어갈 2개의 MenuItem을생성해서추가 MenuItem menuitem = new MenuItem(" 새로고침 "); menuitem.click += menurefresh_click; ctxmenu.menuitems.add(menuitem); menuitem = new MenuItem(" 종료 "); menuitem.click += menuexit_click; ctxmenu.menuitems.add(menuitem); // 최종구성된컨텍스트메뉴를마우스가눌린위치에서팝업 ctxmenu.show(this, e.location); void menurefresh_click(object sender, EventArgs e) MessageBox.Show(" 새로고침버튼눌림 "); 12. 프로젝트유형 21

void menuexit_click(object sender, EventArgs e) Application.Exit(); 이코드를실행하면그림 12.19 와같이윈도우폼내부의아무위치에서나마우스오른쪽버튼을누르 면컨텍스트메뉴가나타나고각메뉴를선택하면연결된이벤트처리기의코드가실행되는것을확인 할수있다. 그림 12.19: 컨텍스트메뉴예제실행 12.1.4 Graphics 윈도우는내부에그래픽을출력할수있는데, 이에대한명령어가 Graphics 타입에제공된다. 예제 12.5 는간단하게윈도우내부에선을그리는코드를보여준다. 예제 12.5: 폼에 (0,0) (100,100) 좌표로선을그리는방법 using System; using System.Drawing; using System.Windows.Forms; namespace WindowsFormsApplication1 public partial class Form1 : Form 22 3 부 _ 닷넷응용프로그램

public Form1() InitializeComponent(); protected override void OnShown(EventArgs e) base.onshown(e); using (Graphics g = this.creategraphics()) g.drawline(pens.black, 0, 0, 100, 100); 프로그램을실행하면그림 12.20 과같은결과를확인할수있다. 그림 12.20: 검은색선이그려진윈도우 DrawLine 메서드의첫번째인자로 Pen 타입의인스턴스가전달되는데, Pens 타입에는대표적으로 141 개에해당하는색상을가진 Pen 인스턴스가미리만들어져있으므로원하는색상이존재한다면그 대로가져다써도된다. 물론직접생성하는것도가능하다. using (Graphics g = this.creategraphics()) using (Pen bluepen = new Pen(Brushes.Blue)) g.drawline(bluepen, 0, 0, 100, 100); // 파란색선이그어진다. 12. 프로젝트유형 23

Pen 생성자의첫번째인자는색상을나타내는 Brush 타입의인스턴스를받는것말고도펜의굵기 도지정할수있다. using (Pen bluepen = new Pen(Brushes.Blue, 10)) g.drawline(bluepen, 0, 0, 100, 100); // 10픽셀의굵기로파란색선이그어진다. 그림 12.21: 굵기가지정된선 new Pen( ) 생성자의첫번째인자인 Brush 타입을다시확인해보면, 이것역시 141 개의브러시 가 Brushes 타입에미리만들어져있기때문에가져다재사용할수있고, 원한다면객체를직접생성 하는것도가능하다. using (Graphics g = this.creategraphics()) using (Brush brush = new SolidBrush(Color.Brown)) using (Pen pen = new Pen(brush)) g.drawline(pen, 0, 0, 100, 100); SolidBrush 타입은단일색상을갖는브러시를의미하고, 생성자에서는 Color 타입의인스턴스를받 는데, 대표적인 141 개의색상을정적속성으로제공하므로대부분의경우그대로재사용하면된다. 원 하는색상이없는경우당연히직접만들어쓸수있다. using (Graphics g = this.creategraphics()) Color color = Color.FromArgb(0x0F, 0xF0, 0x10); using (Brush brush = new SolidBrush(color)) using (Pen pen = new Pen(brush)) 24 3 부 _ 닷넷응용프로그램

g.drawline(pen, 0, 0, 100, 100); Color 타입의 FromArgb 정적메서드는빛의 3원색 ( 빨강, 초록, 파랑 ) 에해당하는값을차례대로 0 ~ 255 범위로받아색상을표현한다. 빛의 3원색이므로 (Red, Green, Blue) 값이각각 (0,0,0) 이면검은색을, (255, 255, 255) 이면흰색을나타낸다. 또한 Argb의 A 는 Alpha 값으로투명도를의미하는데, 기본값인 255는불투명을나타내고, 값이작아지면서점차투명해지다가 0을지정하면완전히투명해져서아예화면에나타나지않는다. 따라서반투명효과를내고싶다면알파값을절반으로주면된다. Color color = Color.FromArgb(0xFF / 2, 0x0F, 0xF0, 0x10); Pen, Brush, Color 타입은 Graphics 타입에서제공되는그외의모든그리기메서드에동일한규칙 으로적용된다. 참고로표 12.4 에대표적인그리기메서드를정리했으니참고하자. 표 12.4: Graphics 타입에제공되는그리기메서드 메서드 설명 DrawArc 원호를그린다. DrawBezier 베지어 (Bezier) 곡선을그린다. DrawCurve 카디널스플라인 (Cardinal Spline) 을그린다. DrawEllipse 타원을그린다. DrawImage Image 객체에담긴그림을출력한다. DrawLine 선을그린다. DrawPie 부채꼴모양의원호를그린다. DrawRectangle 사각형을그린다. DrawString 인자로전달된문자열을그린다. 윈도우에그리는방법까지알았으니이제는그려진그림을유지하는방법을알아볼차례다. 이해를돕 기위해예제 12.5 를실행하고그림 12.22 와같이윈도우의크기를줄였다가다시늘여보자. 12. 프로젝트유형 25

그림 12.22: 줄어든영역의라인이사라지는현상 최초윈도우에서는 (0,0) (100,100) 까지그려졌던선이크기가축소된영역만큼없어진채로남아있는것을확인할수있다. 이것은윈도우운영체제가실행돼있는프로그램들의윈도우화면을관리하는방식에따라나타나는현상이다. 윈도우운영체제는윈도우영역에그려진정보를보관하지않으며, 이에대한책임을각윈도우에맡긴다. 그렇다면개발자는사라진그림영역을언제다시그려야할지를알수있을까? 이를위해윈도우운영체제는윈도우화면이무효화 (Invalidate) 됐을때 WM_PAINT라는윈도우메시지를이용해각윈도우에그사실을통보한다. 닷넷의윈도우폼프로그램에서는 WM_PAINT 메시지가발생할때마다 OnPaint 메서드를실행하는것으로이를처리한다. 따라서예제 12.5를윈도우크기가변하는것에상관없이라인을그대로유지하고싶다면코드를 OnPaint 메서드내부로옮겨야한다. using System; using System.Drawing; using System.Windows.Forms; namespace WindowsFormsApplication1 public partial class Form1 : Form public Form1() InitializeComponent(); 26 3 부 _ 닷넷응용프로그램

protected override void OnPaint(PaintEventArgs e) base.onpaint(e); using (Pen pen = new Pen(Brushes.Black)) e.graphics.drawline(pen, 0, 0, 100, 100); 정리하자면, OnPaint 메서드는내부를다시그려야할필요가있을때마다자동으로다시호출된다. 또한그인자로 PaintEventArgs 타입을받으며, 여기에는 Graphics 속성이제공되기때문에별도로 CreateGraphics 메서드를이용해생성하지않아도된다. 12.1.5 폼과대화상자엄밀히말해서폼 (Form) 과대화상자 (Dialog) 는윈도우폼응용프로그램에서 Form 타입으로동일하게구현된다. 단지 Form을어떻게사용하느냐에따라폼 (Form) 이될수도있고, 대화상자 (Dialog) 가될수도있다. 기본윈도우폼프로젝트를생성한상태에서솔루션탐색기의프로젝트항목을마우스오른쪽버튼으로누른다음 추가 (Add) / 새항목 (New Item) 을차례로선택한후 Windows Forms 범주에서 Windows Form 항목으로 SubForm.cs 파일을지정해서추가한다. 간단하게사용자에게서이름을입력받을수있는 Label 및 TextBox와두개의 Button을폼위에올려놓고각속성을설정한다 ( 그림 12.23과표 12.5 참고 ). 그림 12.23: SubForm 디자인 12. 프로젝트유형 27

표 12.5: SubForm의컨트롤속성 컨트롤 변경된속성 값 TextBox (Name) txtname Button (Name) btnok Text OK Button (Name) btncancel Text Cancel Form (Name) SubForm AcceptButton CancelButton btnok btncancel OK 버튼에대해서는마우스클릭을두번해서이벤트처리기를생성하고예제 12.6 처럼코드를작성 한다. 예제 12.6: SubForm.cs 의 btnok 이벤트처리기 using System; using System.Windows.Forms; namespace WindowsFormsApplication1 public partial class SubForm : Form public string Result get; private set; public SubForm() InitializeComponent(); private void btnok_click(object sender, EventArgs e) this.result = this.txtname.text; this.dialogresult = System.Windows.Forms.DialogResult.OK; 28 3 부 _ 닷넷응용프로그램

보다시피 SubForm 타입이하는일이라고는 OK 버튼이눌린경우사용자가입력한텍스트박스 (txtname) 의내용을 Result 속성에저장해두고 DialogResult 속성에사용자가한행위의의도를설정하는것이전부다. 여기서는정상적으로입력했으므로 OK를지정했고 DialogResult에값이대입되는코드가실행되면자동적으로대화상자가닫힌다는점을알아두자. 반면 btncancel의경우에는 Close 메서드를호출하는이벤트처리기를굳이만들지않아도된다. 왜냐하면표 12.5에서이미버튼을소유하고있는폼 (SubForm) 의 CancelButton 속성에 btncancel을지정했으므로윈도우는해당버튼이눌리면자동으로 DialogResult의값을 Cancel로설정하기때문이다. 물론 Cancel로설정되는것도 DialogResult에는값이대입되는것이므로대화상자는마찬가지로닫히게된다. SubForm을모두만들었으니이제이를활용하는코드를 Form1에넣어보자. 우선 Button과 Label 을각각그림 12.24, 표 12.6과같이구성하고 그림 12.24: Form1 디자인 표 12.6: Form1 의컨트롤속성 컨트롤변경된속성값 Label (Name) lblname Text (None) Button (Name) btninput Text 입력 디자인창에서 입력 버튼을마우스로두번눌러 btninput 의이벤트처리기를생성한후다음과같 이 SubForm 을띄우는코드를작성한다. using System; using System.Windows.Forms; namespace WindowsFormsApplication1 12. 프로젝트유형 29

public partial class Form1 : Form public Form1() InitializeComponent(); private void btninput_click(object sender, EventArgs e) SubForm subform = new SubForm(); if (subform.showdialog() == System.Windows.Forms.DialogResult.OK) this.lblname.text = subform.result; btninput 버튼이눌리면 SubForm 인스턴스를하나생성하고 ShowDialog 메서드를불러폼을화면에보이게만들었다. ShowDialog 메서드는호출된폼이닫힐때까지제어를반환하지않기때문에사용자는계속진행하려면반드시폼을닫아야한다. 물론 SubForm에는 OK, Cancel이라는두가지버튼이있기때문에이름을입력하는것과관계없이아무버튼이나눌리면 ShowDialog는그결과를반환한다. 결과적으로사용자가 OK 버튼을누른경우에한해서 lblname 라벨에는사용자가입력한이름값이들어간다. 이렇게폼이동작하는것을대화상자 (Dialog) 라고한다. 대화상자는일단표시되면사용자로하여금부모 (Parent) 창 ( 예제에서는 Form1) 을사용할수없게만든다. 사용자는반드시대화상자가의도한동작을수행하고 OK/Cancel과같은버튼을눌러야만부모창으로되돌아가다음작업을수행할수있다. SubForm이보이는상태에서부모폼 (Form1) 을마우스를이용해선택해보자. 아무리해도선택되지않는다는것을알게될것이다. 이때스피커가켜져있다면효과음이함께재생되면서요청한작업이불가능하다는것을알린다. 물론폼을대화상자로사용하지않게만들수도있다. 그차이를테스트해보기위해 btninput_click 의코드를다음과같이변경해보자. 30 3 부 _ 닷넷응용프로그램

using System; using System.Windows.Forms; namespace WindowsFormsApplication1 public partial class Form1 : Form public Form1() InitializeComponent(); private void btninput_click(object sender, EventArgs e) SubForm subform = new SubForm(); subform.show(); Show 메서드는 ShowDialog와는다른동작을수행한다. 이메서드는 SubForm을화면에보여주고금방제어를반환한다. 따라서 DialogResult 값도구할수없다. 왜냐하면사용자가어떤동작을취하기도전에 Show 메서드의실행은끝나버린상태이고, 실제로 OK/Cancel 버튼이눌린시점에는 btninput_click의메서드에있는코드에서스레드가머물러있지않기때문이다. 위의코드를실행해보면 2개의폼 (Form1, SubForm) 이화면에떠있고사용자는마우스를이용해원하는폼을활성화해서사용할수있다. Show 또는 ShowDialog를언제사용해야하는지에대해서는폼의목적에따라다르다. 사용자에게서반드시입력을받고진행해야할필요성이있다면 ShowDialog를호출하면되고, 부모폼과함께연동되면서떠있을필요가있다면 Show를호출하면된다. 일례로, 비주얼스튜디오에서 새프로젝트 를선택하는폼은 ShowDialog로호출된경우다. 반면 Ctrl + F 키를눌러뜨는 찾기 창은 Show 메서드로호출될필요가있는전형적인사례다. 사용자에게메시지를보여주는목적으로사용된 MessageBox.Show( ) 도따지고보면전형적인 대화상자의하나에해당한다. 12. 프로젝트유형 31

엄격히말하면대화상자는 Form 으로만들어지기때문에이를좀더구분하기위해 Show 로뜨는경 우 모드리스 (Modeless) 대화창 이라하고, ShowDialog 로뜨는경우를 모달 (Modal) 대화창 이라 고한다. 12.1.6 공통대화상자 System.Windows.Forms.dll 어셈블리에는미리만들어진유용한대화상자들이제공되는데이를일반적으로 공통대화상자 (common dialog) 라고한다. 여기서는대표적으로 5개의대화상자사용법을알아보겠다. 우선폴더를열람하면서이미생성돼있는파일을선택하는 OpenFileDialog의사용법을보자. 예제 12.7: OpenFileDialog 사용법 using System; using System.Windows.Forms; namespace WindowsFormsApplication1 public partial class Form1 : Form public Form1() InitializeComponent(); private void button1_click(object sender, EventArgs e) using (OpenFileDialog opendlg = new OpenFileDialog()) if (opendlg.showdialog() == System.Windows.Forms.DialogResult.OK) MessageBox.Show(" 선택한파일 : " + opendlg.filename); 32 3 부 _ 닷넷응용프로그램

이대화상자의사용법은앞에서배운것에서크게벗어나지않는다. ShowDialog를호출한후사용자가확인 (OK) 버튼을눌렀는지여부를검사하고, 만약파일이선택됐다면해당파일의전체경로를 FileName 속성으로제공한다. 그림 12.25는 ShowDialog를호출했을때나타나는 파일열기대화상자 다. 그림 12.25: 파일열기대화상자 기존파일을선택하는경우 파일열기대화상자 를사용할수있겠지만, 새파일 을생성했다가저장 하거나 다른이름으로저장 과같은기능을구현하려면별도로 파일저장대화상자 도필요하다. 이 는 SaveFileDialog 타입으로구현되고다음과같이사용할수있다. using (SaveFileDialog savedlg = new SaveFileDialog()) if (savedlg.showdialog() == System.Windows.Forms.DialogResult.OK) MessageBox.Show(" 새파일이름 : " + savedlg.filename); 저장대화상자 는 열기대화상자 와외관이거의비슷한데단지저장할파일을선택하는것이므로 사용자가입력한파일이존재하지않아도정상적으로반환받을수있게해준다는차이점이있다. 12. 프로젝트유형 33

파일에대한열기 / 저장대화상자가있다면당연히폴더를선택하는대화상자도있을것이다. 이는 FolderBrowserDialog 타입으로제공되고이대화상자를사용하는코드와대화상자의외양은각각 다음과같다. using (FolderBrowserDialog folderdlg = new FolderBrowserDialog()) if (folderdlg.showdialog() == System.Windows.Forms.DialogResult.OK) MessageBox.Show(" 선택한폴더 : " + folderdlg.selectedpath); 그림 12.26: 폴더선택대화상자 파일 / 폴더와상관없는다른공통대화상자로는폰트 (FontDialog), 색상 (ColorDialog) 을선택하는 대화상자가있다. 폰트의경우대화상자안에서폰트의종류와크기, 효과, 색을모두선택할수있고 반환받은 Font 속성을곧바로코드에서사용할수있다. using (FontDialog fontdlg = new FontDialog()) if (fontdlg.showdialog() == System.Windows.Forms.DialogResult.OK) using (Graphics g = this.creategraphics()) g.drawstring("this is test", fontdlg.font, Brushes.Blue, 0, 0); 34 3부 _ 닷넷응용프로그램

그림 12.27: 폰트대화상자 마지막으로아래는컬러대화상자의사용법을보여준다. using (ColorDialog colordlg = new ColorDialog()) colordlg.color = this.backcolor; if (colordlg.showdialog() == System.Windows.Forms.DialogResult.OK) this.backcolor = colordlg.color; ShowDialog 를호출하기전에윈도우폼의배경색 (BackColor) 값을미리대화상자에설정했다. 이 렇게하면대화상자가나타났을때미리해당색상을선택된채로사용자에게보여줄수있다. 12. 프로젝트유형 35

그림 12.28: 색상선택대화상자 지금까지소개한 5개의대화상자는윈도우운영체제에서제공하는대화상자다. 물론프로그램을만들다보면이밖에도많은대화상자가필요해지는데, 그런경우에는폼 (Form) 을하나만들고내부의구성요소를적절하게배치한다음 ShowDialog 메서드를호출하면된다. 직접만든대화상자코드의재사용성을높인다면다른프로젝트를할때도가져다쓸수있다. 12.1.7 사용자지정컨트롤폼과대화상자를사용자가새롭게만들었던것과마찬가지로, Form 내부에배치되는컨트롤도직접만들수있다. 컨트롤은윈도우 (Window) 자원의일종이므로지금까지배운메시지, 메뉴, Graphics를모두사용할수있고, 이를이용해원하는동작을수행하는컨트롤을만들어재사용할수있다. 간단한예를들어보기위해이번절에서는점선으로둘러싸인사각형에문자열을출력하는 DottedBoxLabel 컨트롤을만들어볼텐데, 그림 12.29처럼보이게만들것이다. 그림 12.29: DottedBoxLabel 의모습 36 3 부 _ 닷넷응용프로그램

솔루션탐색기의프로젝트항목을마우스오른쪽버튼으로누른다음 추가 (Add) / 새항목 (New Item) 을선택한후나오는대화상자의 Windows Forms 범주에서 사용자지정컨트롤 (Custom Control) 항목으로 DottedBoxLabel.cs 파일을추가한다. 그림 12.30: DottedBoxLabel.cs 사용자지정컨트롤추가 생성된 DottedBoxLabel.cs 파일의코드를보면다음과같은기본구성을볼수있다. 예제 12.8: 사용자지정컨트롤의기본코드 using System.Drawing; using System.Windows.Forms; namespace WindowsFormsApplication1 public partial class DottedBoxLabel : Control public DottedBoxLabel() InitializeComponent(); protected override void OnPaint(PaintEventArgs pe) base.onpaint(pe); 12. 프로젝트유형 37

컨트롤로서동작하기위한모든코드를직접작성할수도있겠지만, 다행히 System.Windows. Forms.dll 어셈블리에는사용자지정컨트롤의기본동작을구현한 Control 클래스가제공되므로대부분이를상속받아구현하면된다. 따라서기본적인윈도우메시지처리작업은신경쓸필요없이원하는동작에집중할수있다. 우리가만드는컨트롤은사각형박스가점선으로이뤄져있으므로 OnPaint에다음과같은작업을추가한다. protected override void OnPaint(PaintEventArgs pe) base.onpaint(pe); // 컨트롤의크기를 ClientRectangle 속성으로구하고, Rectangle rectarea = this.clientrectangle; // Inflate 메서드는 Rectangle 크기를조정한다. // width와 height 인자에모두 -1을지정했기때문에사각형크기가 1씩줄어든다. // 이렇게하는이유는선을그리기위한영역으로 1 픽셀씩차지하기때문이다. rectarea.inflate(-1, -1); using (Pen dottedpen = new Pen(Brushes.Black, 1)) // Pen의선스타일을 Dot로변경 dottedpen.dashstyle = System.Drawing.Drawing2D.DashStyle.Dot; pe.graphics.drawrectangle(dottedpen, rectarea); 일단이렇게만코드를작성하고프로젝트를빌드한다. 그러면도구상자 (Toolbox) 영역에 DottedBoxLabel 컨트롤이생성되는것을볼수있다. 이를 Form 디자인창에끌어다놓으면그림 12.31 처럼점선으로된사각형을그리는컨트롤이나타난다. 38 3 부 _ 닷넷응용프로그램

그림 12.31: 도구상자에추가된 DottedBoxLabel 컨트롤 라벨컨트롤이라서당연히출력해야할텍스트가필요하다. DottedBoxLabel 타입에 Text 속성을정 의해야하지만부모클래스인 Control 타입은이속성을기본적으로제공하므로단순히 OnPaint 에 서이를출력하면된다. protected override void OnPaint(PaintEventArgs pe) base.onpaint(pe); // [ 생략 ] // 문자열이그려질때사용할폰트를생성 using (Font normalfont = new Font(FontFamily.GenericSansSerif, 10)) // 문자열을점선으로그려진사각형내부에출력한다. pe.graphics.drawstring(this.text, normalfont, Brushes.Black, rectarea); 프로젝트를다시빌드하면그림 12.32 처럼 Form 디자인창에서컨트롤을마우스로선택한후 속성 (Properties) 창에서 Text 속성을바꿀수도있고, Form1.cs 코드창에서해당컨트롤의변수를이용 해직접 Text 속성을바꿀수도있다. 12. 프로젝트유형 39

그림 12.32: 속성창을통해컨트롤의 Text 상태값변경 텍스트의출력위치가아직정교하지않은데, 원한다면다음과같이중앙으로올수있게변경할수도 있다. protected override void OnPaint(PaintEventArgs pe) base.onpaint(pe); // [ 생략 ] using (Font normalfont = new Font(FontFamily.GenericSansSerif, 10)) // 지정된폰트로출력될텍스트의크기를먼저계산하고, SizeF outputsize = pe.graphics.measurestring(this.text, normalfont); // 사각형의중간에출력되는위치를구한다. PointF pt = new PointF(); pt.x = (rectarea.width - outputsize.width) / 2; pt.y = (rectarea.height - outputsize.height) / 2; pe.graphics.drawstring(this.text, normalfont, Brushes.Black, pt); 위와같이코드를작성하고실행하면최종적으로그림 12.29 처럼컨트롤의외양이출력되는모습을 볼수있다. 40 3 부 _ 닷넷응용프로그램

이렇게해서간단하게재사용할수있는사용자지정컨트롤을만들어봤는데, 실제로현업에서사용할 만한컨트롤을만들려면더많은윈도우폼관련프로그래밍지식이필요하다. 지면관계상모든내용 을설명할수는없고, 더자세한사항들은시간을두고꾸준히학습하길바란다. 12.1.8 사용자정의컨트롤 윈도우폼프로그램에서재사용가능한컨트롤을만드는방법은두가지가있다. 사용자지정컨트롤 (Custom Control) Control 타입을상속받아단일컨트롤로서의동작을정의 사용자정의컨트롤 (User Control) UserControl 타입을상속받고, 다양한컨트롤을조합해서재사용가능한컨트롤을정의 이름이다소비슷해서혼동이올수있지만, 이번에배우는것은기존컨트롤을조합해서새로운복합컨트롤을정의하는 사용자정의컨트롤 을만드는방법이다. 여기서실습할사용자정의컨트롤은파일을선택하면좌측의텍스트박스에그경로를보여주는 FileChooser 타입인데, 그림 12.33에서보다시피두가지컨트롤 (TextBox, Button) 이하나의사용자정의컨트롤로동작하게된다. 그림 12.33: FileChooser 사용자정의컨트롤 본격적인실습을위해새롭게윈도우폼프로젝트를하나만들고솔루션탐색기의프로젝트항목을마우스오른쪽버튼으로누른다음 추가 (Add) / 새항목 (New Item) 을선택한후나오는대화상자의 Windows Forms 범주에서 사용자정의컨트롤 (UserControl) 항목으로 FileChooser.cs 파일명을지정해서추가한다. 그러면그림 12.34처럼 Form 디자인화면과유사한화면이제공되는데, 이것이바로 사용자지정컨트롤 과비교했을때가장큰차이점이다. 12. 프로젝트유형 41

그림 12.34: 디자인화면이제공되는사용자정의컨트롤 디자인영역의크기를적당히조정하고그림 12.35, 표 12.7 에따라 TextBox, Button 컨트롤두개를 추가한후적절하게속성값을입력한다. 그림 12.35: FileChooser 사용자정의컨트롤디자인 표 12.7: FileChooser의내부컨트롤속성 컨트롤 변경된속성 값 TextBox (Name) txtfilename Button (Name) btnfile 이제 파일 버튼이눌렸을때파일선택대화상자를띄우고, 사용자가파일을선택한경우텍스트박 스에파일경로를출력하는코드를작성한다. 예제 12.9: FileChooser.cs 코드파일 using System; using System.Windows.Forms; namespace WindowsFormsApplication1 public partial class FileChooser : UserControl public event EventHandler FileSelected; // 파일이선택됐을때이벤트발생 // 외부에서선택된파일경로를가져갈수있게읽기속성을제공 string _selectedfilepath; 42 3 부 _ 닷넷응용프로그램

public string SelectedFilePath get return _selectedfilepath; public FileChooser() InitializeComponent(); private void btnfile_click(object sender, EventArgs e) using (OpenFileDialog opendlg = new OpenFileDialog()) if (opendlg.showdialog() == DialogResult.OK) this.txtfilename.text = opendlg.filename; this._selectedfilepath = opendlg.filename; if (FileSelected!= null) // 외부에서이벤트를걸어뒀다면파일이선택됐음을통보한다. FileSelected(this, EventArgs.Empty); 프로젝트를빌드하면도구상자에새롭게 FileChooser 항목이나타나는데, Form1.cs 디자인창을 열고 FileChooser 를끌어서폼위에추가한다 ( 그림 12.36 과표 12.8 참고 ). 그림 12.36: Form1 디자인창에 FileChooser 사용자정의컨트롤추가 12. 프로젝트유형 43

표 12.8: FileChooser 속성 컨트롤변경된속성값 FileChooser (Name) filechooser 그림 12.36 에서볼수있듯이두개의컨트롤 (TextBox, Button) 이 Form 디자인에추가됐지만 Form1.cs 코드파일에서는단일컨트롤처럼다룰수있다. 따라서 Form1.cs 코드에서 FileSelected 이벤트를추가하고선택된파일의경로를구하는것까지할수있다. using System; using System.Windows.Forms; namespace WindowsFormsApplication1 public partial class Form1 : Form public Form1() InitializeComponent(); private void Form1_Load(object sender, EventArgs e) // 이벤트처리기를추가하고, this.filechooser.fileselected += filechooser_fileselected; void filechooser_fileselected(object sender, EventArgs e) // 파일이선택됐음을통지받아처리한다. MessageBox.Show(this.fileChooser.SelectedFilePath); 정리하자면사용자정의컨트롤을반드시알아야할필요는없다. 왜냐하면굳이사용자정의컨트롤 로만들지않아도직접폼위에표 12.7 에나열된컨트롤을올려놓고사용해도되기때문이다. 그럼에 도일반적으로사용자정의컨트롤을만드는데는두가지이유가있다. 44 3 부 _ 닷넷응용프로그램

재사용가능한컨트롤묶음정의서로연관이있는컨트롤을하나의 사용자정의컨트롤 로만들어재사용성을높인다. 수많은상업용컨트롤라이브러리가이처럼재사용을염두에두고만들어져배포되고있다. Form 내부의컨트롤에대한구획화 Form 안에너무많은컨트롤이내장되는경우, 이러한컨트롤에연계되는코드까지더해지면 Form 타입의코드복잡도가증가한다. 따라서적절하게사용자정의컨트롤로분리해 Form 내부의코드를분산하면유지보수하는데도도움이된다. 정리윈도우가포함된응용프로그램은광범위하게사용된다. 가장흔한예로 일반사용자를대상으로한응용프로그램 은거의대부분윈도우프로그램이다. 또한윈도우안에서다양한그래픽처리를할수있기때문에시각화처리가필요한응용프로그램에도필수적으로쓰인다. 심지어 UI가필요없는서버측프로그램을주로만드는개발자에게도해당프로그램을모니터링하는용도의응용프로그램으로는 UI를제공하는윈도우프로그램으로만드는것이일반적이다. 이처럼다양한활용사례를기대할수있기때문에어떤식으로든 윈도우프로그램 을만들줄아는것은개발자입장에서놓칠수없는중요한능력중의하나다. 그리고필자가아는한, 윈도우프로그램 을만들수있는가장쉬운방법은닷넷의 윈도우폼응용프로그램 이다. 특히기존의 Visual C++ 에서 MFC 라이브러리를이용해윈도우프로그램을만들던개발자라면얼마나편리해졌는지충분히느꼈을것이다. 이때문에닷넷프레임워크는 Visual C++ 개발자에게날개를달아주는것과같다. 번잡한 UI 구성은닷넷언어를이용해쉽게작성하고보호가필요한핵심코드는 Visual C++ 에맡기는식으로프로그램을만들면극적인생산성향상을기대할수있다. 12.2 WPF 응용프로그램 마이크로소프트는닷넷프레임워크 3.0에서 XAML(Extensible Application Markup Language) 의한사례로 WPF(Windows Presentations Framework) 를소개했다. 여기서 XAML이란간단하게말해닷넷객체를 XML로표현하는기술이라고보면된다. 이해를돕기위해 6.3 직렬화 / 역직렬화 절에서배운내용을다시살펴보자. 예를들어, 다음코드를실행하면 12. 프로젝트유형 45

using System; using System.IO; using System.Text; using System.Xml.Serialization; public class Person public string Name; public int Age; class Program static void Main(string[] args) Person p1 = new Person Age = 37, Name = "Anderson" ; MemoryStream ms = new MemoryStream(); XmlSerializer xs = new XmlSerializer(typeof(Person)); xs.serialize(ms, p1); ms.position = 0; Console.WriteLine(Encoding.UTF8.GetString(ms.GetBuffer())); 화면에는 Person 객체의내용이 XML 형식으로출력된다. <?xml version="1.0"?> <Person xmlns:xsi=" " xmlns:xsd=" "> <Name>Anderson</Name> <Age>37</Age> </Person> 결국위의 XML과 Person p1 = new Person Age = 37, Name = Anderson ; 이라는 C# 코드는완전히등가적인역할을하는셈이다. 마이크로소프트는이런관계를사용자 UI 구성요소를표현하는데적용하기시작했고그결과물로나온것이바로 WPF다. 이때문에 WPF 응용프로그램에서는 UI 구성요소를코드뿐아니라 XML 표기로도생성하는방식을지원한다. 46 3 부 _ 닷넷응용프로그램

이쯤에서 WPF 프로젝트를하나만들어보면더욱쉽게이해할수있다. 비주얼스튜디오에서 파일 / 새프로젝트 메뉴를선택했을때나온그림 12.1 의 WPF 응용프로그램 (Application) 을지정해 프로젝트를생성하면그림 12.37 과같은초기화면을볼수있다. 그림 12.37: WPF 프로젝트 기본적으로 MainWindow.xaml 파일이열려있는데, 중앙에는디자인화면이보이고하단에는 xaml 파일의원본내용이보인다. 하단의 xaml 내용을자세히보면다음과같은 XML 형식인데, <Window x:class="wpfapplication1.mainwindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="350" Width="525"> <Grid> </Grid> </Window> 이는아래의코드와동일한역할을수행한다. WpfApplication1.MainWindow window = new WpfApplication1.MainWindow(); window.title = "MainWindow"; window.height = 350; window.width = 525; 12. 프로젝트유형 47

Grid grid = new Grid(); window.content = grid; 솔루션탐색기에서 MainWindow.xaml 을펼쳐보면 MainWindow.xaml.cs 파일을선택할수있고 기본내용은그림 12.38 과같다. 그림 12.38: MainWindow.xaml.cs 파일 코드파일에는 WpfApplication1 네임스페이스아래에 MainWindow 타입이정의돼있고생성자에 는 InitializeComponent 메서드가호출되고있다. 이관계를예제 12.1 의윈도우폼응용프로그램과 비교해보면표 12.9 처럼정리할수있다. 표 12.9: WPF 와윈도우폼의대응관계 윈도우폼 WPF 코드파일 Form1.cs MainWindow.xaml.cs 디자인파일 Form1.Designer.cs MainWindow.xaml 즉, 디자인을위한코드파일이 XML 형식의 xaml 파일로아예대체돼버린것이다. 기본적인디자인방법은윈도우폼과비교해서크게다르지않다. 도구상자의 Common WPF 컨트롤 또는 All WPF 컨트롤 영역에있는항목을 xaml 디자인화면에끌어다놓으면되는데, 그림 12.39에서는버튼을하나추가한모습을볼수있다. 48 3 부 _ 닷넷응용프로그램

그림 12.39: Button 컨트롤을추가 xaml 디자인화면에방금추가한버튼이하나보이고, 하단의 xaml 파일내용에는새롭게 <Button /> 태그가추가돼있다. 윈도우폼과마찬가지로디자인화면에서버튼컨트롤을마우스로두번클릭하면하단의 XAML 파일에는 Click 속성과함께이벤트처리기의메서드이름이추가되고동시에 MainWindow.xaml.cs 파일이열리면서 Click 이벤트처리기가추가된다. <Window [ 생략 ] Title="MainWindow" Height="350" Width="525"> <Grid> <Button Content="Button" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top" Width="75" Click="Button_Click"/> </Grid> </Window> 테스트를위해간단하게예제 12.10 과같이 MessageBox 를추가하고실행해보면버튼이눌릴때마 다메시지창이나타나는것을알수있다. 예제 12.10: WPF 의버튼이벤트처리기 using System.Windows; namespace WpfApplication1 12. 프로젝트유형 49

public partial class MainWindow : Window public MainWindow() InitializeComponent(); private void Button_Click(object sender, RoutedEventArgs e) MessageBox.Show("Clicked!"); 물론디자인화면에서버튼을두번누르는방법대신 XAML 파일에서직접 <Button /> 태그속성에 Click 텍스트를입력하고이벤트처리기로정의될메서드이름을임의로지정한후, MainWindow. xaml.cs 파일을열어그이름과일치하는메서드를직접정의해도무방하다. 어떤방식을이용하느냐는개인적인취향의문제다. 이렇게디자인요소를기존의코드파일에서 XML 형식의 XAML로바뀌어서좋은점이있다면디자인을개발자가아닌디자이너에게맡길수있다는점이다. 실제로마이크로소프트에서는 WPF 응용프로그램의 XAML 디자인을위해디자이너에게익숙한포토샵 (Photoshop) 프로그램과유사한 익스프레션블렌드 (Expression Blend) 라는응용프로그램을개발했다. 이프로그램은 WPF 프로젝트파일을읽어들일수있으며, 디자이너가 XAML 파일을대상으로코드에영향을주지않고도프로그램의외양을바꿀수있게해준다. 게다가 TFS(Team Foundation Server) 형상관리시스템까지지원하므로디자이너와개발자가자연스럽게소스컨트롤하에서협업하는것도가능하다. 12.2.1 WPF 렌더링방식 WPF 응용프로그램에서 XAML이디자인을담당한다는것과함께또한가지크게달라진점이있다면내부컨트롤이더는윈도우를기반으로동작하지않는다는점이다. 예를들어, Button 컨트롤이포함된윈도우폼응용프로그램의동작은 Form과 Button이각각운영체제에서제공되는윈도우 (Window) 자원으로구현된다. 즉, 화면에는두개의윈도우가부모 / 자식관계로구성되어각윈도우는독립적으로메시지에반응하도록구현된다 ( 그림 12.40 참고 ). 50 3 부 _ 닷넷응용프로그램

그림 12.40: 윈도우폼의구성 부모윈도우 2 개의윈도우 자식윈도우 하지만 WPF에서는컨트롤이더이상윈도우가아니다. 오직 Window 태그에해당하는것만운영체제의윈도우로구현되고, 내부의모든자식컨트롤은윈도우의일정영역을할당받아그리는역할만한다. 이때문에컨트롤이라는이름외에도요소 (element) 라는이름으로더자주불린다. 화면에보이게되는출력방식도독특하다. 요소들은 WPF 내부에할당된메모리영역에자신의영역을그리고, 그려진모든자식요소들의이미지는차례대로합쳐져서윈도우영역의이미지와합쳐져서그려진다 ( 그림 12.41 참고 ). 그림 12.41: WPF 의출력구성 윈도우 메모리 영역을메모리에그린다. 요소 (Element) 영역을메모리에그린다. 메모리 메모리상의그림이합쳐져서하나의윈도우출력이완성된다. 렌더링방식을새롭게바꾼데는그만한이유가있다. 왜냐하면다음과같은이점이있기때문이다. 그래픽가속가능 GPU 의그래픽가속기능을사용함으로써렌더링으로인한 CPU 부하를줄인다. 12. 프로젝트유형 51

다중윈도우를사용하지않기때문에부하감소윈도우자원을유지하는것은시스템에부하를준다. 간단한비교를위해윈도우폼응용프로그램에서 1,000개의버튼컨트롤을자식으로갖게만들고, 동일하게 1,000개의버튼요소가있는 WPF 응용프로그램을만들어각각실행해보자. 후자의 WPF 응용프로그램이좀더빨리실행되는것을확인할수있다. 왜냐하면 WPF는 1,000개의버튼모양을그리는것에불과한반면, 윈도우폼응용프로그램은운영체제로부터 1,000개의윈도우자원을생성하도록요청을한후에그안에다시그리는작업을하기때문이다. 자유로운 UI 표현가능메모리에그려진이미지가조합돼표현되기때문에요소간의중첩된이미지처리가가능해져자유로운렌더링효과를얻는다. 한가지분명한점은 WPF 응용프로그램에서는기존의윈도우폼과비교했을때컨트롤의외양을바 꾸는것이매우자유롭다는점이다. 이로인해대체로 WPF 용컨트롤이윈도우폼용컨트롤에비해더 욱화려하다는특징이있다. 12.2.2 데이터바인딩바인딩 (Binding) 이란일반적으로어떤대상과묶이는것을의미한다. WPF에서의데이터바인딩은데이터를담고있는코드와 UI 요소가연결되는것을의미한다. 사실데이터바인딩이없었어도우리는그동안정상적으로데이터를화면에표시해왔다. 예를들어, 화면에시간을보여주는 WPF 응용프로그램을만들어보자. 예제 12.11: WPF로만드는시계프로그램 // ================== MainWindow.xaml ============================ <Window x:class="wpfapplication1.mainwindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="200" Width="300"> <Grid> <Label x:name="lbltime" VerticalAlignment="Center" HorizontalAlignment="Center"></Label> </Grid> </Window> // ================== MainWindow.xaml.cs =========================== using System; using System.Windows; 52 3 부 _ 닷넷응용프로그램

using System.Windows.Threading; namespace WpfApplication1 public partial class MainWindow : Window DispatcherTimer _timer; public MainWindow() InitializeComponent(); _timer = new DispatcherTimer(); _timer.tick += _timer_tick; _timer.interval = new TimeSpan(0, 0, 1); // 1초마다 Tick 이벤트발생 _timer.start(); void _timer_tick(object sender, EventArgs e) this.lbltime.content = DateTime.Now.ToLongTimeString(); 윈도우폼과마찬가지로 Label 컨트롤을사용했고, x:name 속성에이름을주면, xaml.cs 코드파일에서그이름으로해당컨트롤에접근할수있다. MainWindow.xaml.cs 파일에는 DispatcherTimer를이용해지정된 Interval 시간마다 Tick 이벤트를발생시키게했고, 따라서 _ timer_tick 이벤트처리기는 1초마다실행되어 lbltime.content 속성에현재시간을설정한다. 한가지특이한면이있다면윈도우폼에서는 Label에보여줄텍스트를 Text 속성을이용해지정했던반면, WPF의 Label에서는 Content 속성으로바뀌었다는정도의차이가있다. 이것이바로데이터바인딩을사용하지않고 UI 요소와코드의실행을연결한경우다. 이제데이터바인딩을이용하는예제로코드를바꿔보자. 우선 xaml 파일은다음과같이바뀐다. <Window x:class="wpfapplication1.mainwindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:name="thiswindow" Title="MainWindow" Height="200" Width="300"> 12. 프로젝트유형 53

<Grid> <Label DataContext="Binding ElementName=thisWindow" Content="Binding Path=Time" VerticalAlignment="Center" HorizontalAlignment="Center"></Label> </Grid> </Window> Window 태그에 x:name을지정하고, Label 컨트롤에서는 DataContext 속성을이용해 Binding 설정을했는데, 연결될 ElementName의대상으로 Window의 x:name에지정된값을전달했다. 그리고는 Content 속성에도역시 Binding 구문을이용해 Path로 Time이라는문자열을전달한다. 궁금한것이많겠지만일단 xaml.cs 파일까지변경해야만설명을계속할수있다. using System; using System.ComponentModel; using System.Windows; using System.Windows.Threading; namespace WpfApplication1 public partial class MainWindow : Window, INotifyPropertyChanged DispatcherTimer _timer; string _time; public string Time get return _time; set _time = value; PropertyChanged(this, new PropertyChangedEventArgs("Time")); public MainWindow() InitializeComponent(); 54 3 부 _ 닷넷응용프로그램

_timer = new DispatcherTimer(); _timer.tick += _timer_tick; _timer.interval = new TimeSpan(0, 0, 1); // 1초마다발생 _timer.start(); void _timer_tick(object sender, EventArgs e) this.time = DateTime.Now.ToLongTimeString(); public event PropertyChangedEventHandler PropertyChanged; 우선 _timer_tick 메서드를보면단순히값을 Time 속성값에넣기만한다. 그리고 Time 속성의 set 구문을보면특이하게 PropertyChanged라는이벤트를 Time 문자열로초기화한 PropertyChangedEventArgs 인스턴스와함께발생시킨다. 이예제를실행해보면예제 12.11과동일하게수행되는것을확인할수있다. 어째서그런것일까? 그이면에는바로 WPF의데이터바인딩이있기때문이다. 보다시피 xaml.cs 파일에서는 Time 속성값이변경될때마다 INotifyPropertyChanged 인터페이스의 PropertyChanged 이벤트를발생시킨다. 한편, xaml에정의된 Label 요소에서는이를 Content= Binding Path=Time 이라는구문을통해 PropertyChanged 이벤트를기다리다가그인자에 Path로지정된 Time 이라는문자열과일치하는경우그값을가져다가 Content 속성에대입하는역할을한다. 그런데 Label은 Time이라는속성값을어디서가져와야하는지알수있을까? 이를위해필요한설정값이바로 DataContext다. Label의 DataContext에는 Binding 구문으로 ElementName의대상이 thiswindow로지정됐고, xaml 내에서는 thiswindow로식별되는요소가 <Window x:name= thiswindow /> 이기때문에 MainWindow 인스턴스가선택되는것이다. 따라서코드상에서직접 Label.Content 속성에값을넣지않아도데이터바인딩구문을통해코드의속성값을변경하는것만으로도그림 12.42처럼 UI 요소까지값이전달될수있는것이다. 12. 프로젝트유형 55

그림 12.42: WPF 데이터바인딩 DataContext 속성값은상위요소에서하위요소로전파되기때문에 Label 에직접지정하지않고 Window 요소나 Grid 에지정해도이예제에서는동일한실행결과를얻을수있다. <Window x:class="wpfapplication1.mainwindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:name="thiswindow" DataContext="Binding ElementName=thisWindow" Title="MainWindow" Height="200" Width="300"> <Grid> <Label Content="Binding Path=Time" VerticalAlignment="Center" HorizontalAlignment="Center"></Label> </Grid> </Window> 위와같이변경하는경우 Window 요소는 DataContext 의 Binding 대상으로자기자신을가리키게 되는데, WPF 에서는이런상황에서사용할수있는 RelativeSource 구문을지원한다. 최종적으로다 56 3 부 _ 닷넷응용프로그램

음과같은관용적인표현을통해 xaml.cs 에정의된클래스의인스턴스를가리키도록 DataContext 를 지정할수있다. <Window x:class="wpfapplication1.mainwindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" DataContext="Binding RelativeSource=RelativeSource Self" Title="MainWindow" Height="200" Width="300"> <Grid> <Label Content="Binding Path=Time" VerticalAlignment="Center" HorizontalAlignment="Center"></Label> </Grid> </Window> 아직까지는이렇게복잡한데이터바인딩을사용하기보다는예제 12.11처럼직접 UI 요소의 x:name 을통해 UI 요소와상호연동하는방식이더편하다고느껴질것이다. 하지만데이터바인딩에대해조금만더알아보자. 사실, 데이터바인딩의진짜매력을알려면 Label과같은요소보다는 TextBox 처럼편집이가능한예제를작성해봐야한다. 예를들어, xaml에 Button과 TextBox를넣고 Button 이눌린경우 TextBox의내용을출력하는예제를다음과같이작성할수있다. <Window x:class="wpfapplication1.mainwindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" DataContext="Binding RelativeSource=RelativeSource Self" Title="MainWindow" Height="200" Width="300"> <Grid> <Button Content="Button" Click="Button_Click" HorizontalAlignment="Left" Margin="207,136,0,0" VerticalAlignment="Top" Width="75" Height="23"/> <TextBox Text="Binding Path=InputText" HorizontalAlignment="Left" Height="23" Margin="10,136,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="192"/> </Grid> </Window> 12. 프로젝트유형 57

using System; using System.ComponentModel; using System.Windows; using System.Windows.Threading; namespace WpfApplication1 public partial class MainWindow : Window, INotifyPropertyChanged string _inputtext; public string InputText get return _inputtext; set _inputtext = value; PropertyChanged(this, new PropertyChangedEventArgs("InputText")); public MainWindow() InitializeComponent(); public event PropertyChangedEventHandler PropertyChanged; private void Button_Click(object sender, RoutedEventArgs e) MessageBox.Show(this.InputText); TextBox의 Text 속성을 MainWindow 타입의 InputText 속성과데이터바인딩시키고있는데, 이를통해값의전파가쌍방향으로이뤄진다. 즉, TextBox에서값을입력하면 MainWindow의 InputText 변수에그결과가반영된다. 따라서 Button_Click 이벤트처리기에서는 TextBox로부터 Text 값을가져올필요없이곧바로 InputText 속성을사용함으로써사용자가입력한텍스트에접근할수있다. 58 3 부 _ 닷넷응용프로그램

게다가데이터바인딩은 xaml 요소간에도적용할수있다. 예를들어, TextBox 에서입력하는내용 을 Label 에곧바로출력하고싶다면부가적인코드없이단순히다음과같은 xaml 만으로도표현할수 있다. <Window x:class="wpfapplication1.mainwindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="200" Width="300"> <Grid> <TextBox x:name="txtbox" HorizontalAlignment="Left" Height="23" Margin="10,136,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="272"/> <Label Content="Binding ElementName=txtBox, Path=Text" HorizontalAlignment="Left" Margin="10,105,0,0" VerticalAlignment="Top" Width="272"/> </Grid> </Window> 위의 xaml을보면 Label.Content 속성에는그위의 TextBox의 x:name 값으로 Binding 대상이지정돼있으며, Path 값으로는 TextBox 타입이가진 Text 속성을지정했다. 따라서 TextBox에사용자가내용을입력할때마다그결과가곧바로 Label의 Content에연결되어화면에출력된다. 여기서한가지중요한점은이기능을코드한줄없이구현했다는것이다. 지금까지데이터바인딩을사용해봤는데쓸만하다고느껴졌을는지모르겠다. 처음에는 INotifyPropertyChanged 인터페이스를구현하는것이번거로울수있지만일단속성에 PropertyChanged 이벤트가적용만되면데이터바인딩을이용해 xaml 디자인요소와의상호연동이물흐르듯이자연스럽게이뤄진다는매력이있다. 12.2.3 레이아웃을위한패널여기서레이아웃 (layout) 은컨트롤의배치를의미한다. 고정된윈도우화면크기에서의컨트롤배치는단순히 x, y 위치값을고정시켜지정할수있지만, 문제는 유동적인윈도우화면크기 에서발생한다. 이번에는예제로이미지뷰어프로그램을 WPF로만들어보자. 그림 12.43처럼디자인은간단하게파일을선택할수있는 Button과이미지를출력하는 Image 컨트롤로구성하고 12. 프로젝트유형 59

그림 12.43: Image 컨트롤과 Button 컨트롤로구성된이미지뷰어 예제 12.12: 컨트롤의위치를고정 <Window x:class="wpfapplication1.mainwindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" DataContext="Binding RelativeSource=RelativeSource Self" Title="MainWindow" Height="200" Width="300"> <Grid> <Image Source="Binding Path=BitmapImage" HorizontalAlignment="Left" Height="149" Margin="10,10,0,0" VerticalAlignment="Top" Width="200"/> <Button Content="Load" Click="Button_Click" HorizontalAlignment="Left" Margin="227,10,0,0" VerticalAlignment="Top" Width="55"/> </Grid> </Window> 예제 12.12 와같이 Button 컨트롤에 Click 이벤트를연결하는 XAML 코드를완성한다. 그런다음, 예 제 12.13 과같이 Button_Click 이벤트처리기에 OpenFileDialog 를띄워그림파일을선택할수있 게한후, 이파일을 Image 컨트롤의 Source 속성에데이터바인딩시킨 BitmapImage 에지정한다. 예제 12.13: 데이터바인딩으로연결된 BitmapImage 속성에그림파일을지정 using System; using System.ComponentModel; using System.IO; using System.Windows; using System.Windows.Media.Imaging; using System.Windows.Threading; using Microsoft.Win32; 60 3 부 _ 닷넷응용프로그램

namespace WpfApplication1 public partial class MainWindow : Window, INotifyPropertyChanged BitmapImage _bitmapimage; public BitmapImage BitmapImage get return _bitmapimage; set _bitmapimage = value; OnPropertyChanged("BitmapImage"); public MainWindow() InitializeComponent(); public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged(string name) if (string.isnullorempty(name) == true) return; if (PropertyChanged!= null) PropertyChangedEventArgs arg = new PropertyChangedEventArgs(name); PropertyChanged(this, arg); private void Button_Click(object sender, RoutedEventArgs e) OpenFileDialog ofd = new OpenFileDialog(); if (ofd.showdialog() == true) string txt = ofd.filename; 12. 프로젝트유형 61

this.bitmapimage = new BitmapImage(new Uri(txt)); 우선, OpenFileDialog가 11.1 Windows Forms 응용프로그램 절에서배운것과는이름만같을뿐다른타입이라는사실을알필요가있다. 윈도우폼응용프로그램이사용하는 System.Windows. Forms 네임스페이스에포함된 OpenFileDialog 타입을이용하려면별도로 System.Windows. Forms.dll 어셈블리를포함시켜야하는데 WPF 응용프로그램에또다시윈도우폼응용프로그램을위한어셈블리를내장하는것은성능면에서그다지바람직하지않다. 이러한이유로마이크로소프트에서는 WPF 응용프로그램의기본참조어셈블리인 PresentationFramework.dll에 Microsoft. Win32 네임스페이스로 OpenFileDialog를추가했다. WPF와윈도우폼응용프로그램은닷넷으로 GUI 응용프로그램을만드는서로다른방법을제공하기때문에이처럼동일한기능이양측에제공되는경우가종종있다는점을알아두자. 이제프로그램을실행하고 Load 버튼을눌러여러분의하드디스크에저장된그림을선택하면 Image 컨트롤의영역에맞게이미지가축소 / 확대되어나타나는것을확인할수있다. 그런데이미지를크게보고싶어서윈도우를확장하면어떻게될까? XAML 내부에지정된컨트롤의위치가고정돼있으므로그림 12.44처럼공간낭비가발생한다. 그림 12.44: 윈도우크기조정에영향을받지않는내부컨트롤 이런문제를해결하기위해 Grid 레이아웃컨트롤을활용할수있다. 방법은윈도우에상관없이좌측 의 Load 버튼에대해서만고정된크기를할당하고이미지영역을자유롭게축소 / 확장되게끔지정하 면된다. 다음은이를반영한 XAML 코드다. 62 3 부 _ 닷넷응용프로그램

예제 12.14: 컨트롤의위치를 Grid 레이아웃에맡긴 XAML <Window x:class="wpfapplication1.mainwindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" DataContext="Binding RelativeSource=RelativeSource Self" Title="MainWindow" Height="200" Width="300"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="*" /> <ColumnDefinition Width="70" /> </Grid.ColumnDefinitions> <Image Grid.Column="0" Source="Binding Path=BitmapImage" HorizontalAlignment="Left" VerticalAlignment="Top"/> <Button Grid.Column="1" Content="Load" Click="Button_Click" HorizontalAlignment="Left" VerticalAlignment="Top" Width="55"/> </Grid> </Window> 예제 12.14 에서는수직으로영역을분할하는 ColumnDefinition 만사용하는데, Grid 는수평분할에 대한 RowDefinition 도지원하므로상황에맞게사용하면된다. 사실, 예제 12.14 는 1 개의 Row 를가 지고있는것이나마찬가지이기때문에다음과같이정의해도무방하다. <Grid.ColumnDefinitions> <ColumnDefinition Width="*" /> <ColumnDefinition Width="70" /> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="*" /> </Grid.RowDefinitions> 0 번째칼럼 (ColumnDefinition) 의 Width 값이별표 (*) 로지정된것에유의하자. Width 값에는 double 형의숫자값외에 *, Auto 값이예외적으로허용된다. * 로설정된경우나머지전체크기 를의미하며, Auto 로설정된경우해당칼럼에위치하는자식컨트롤들의크기로자동지정된다. 12. 프로젝트유형 63

이렇게해서 2개의수직영역이정의됐으므로개별컨트롤이어느영역에들어가야할지를지정할필요가있다. 이런목적으로 Grid.Column 속성이 Image 컨트롤에는 0을지정해 0번째칼럼에들어가게하고, Button 컨트롤에는 1을지정해 1번째칼럼에서보여지게만들었다. 따라서예제 12.14는 Button 컨트롤이위치한 1번째칼럼으로 70이라는크기가할당되고그밖의나머지모든크기값이 0 번째칼럼에할당된다. 고정크기를지정한예제 12.12와함께 Grid 레이아웃컨트롤을이용해가변크기를지원하는예제 12.14를각각실행한후윈도우크기를마우스로변경해보면지금까지설명한내용을좀더쉽게이해할수있다. WPF에는 Grid 외에 Canvas, UniformGrid, DockPanel, StackPanel, WrapPanel 같은기본레이아웃컨트롤이제공되며, 이것들은모두공통적으로 System.Windows.Controls.Panel 타입을상속받는다. 일반적으로 Panel 상속컨트롤들은또다른컨트롤을담고있는역할을하기때문에 컨테이너컨트롤 이라고한다. 이름은다르지만레이아웃컨트롤은윈도우폼응용프로그램에서도유사한방식으로제공된다. 12.2.4 Content 속성레이아웃에사용되는 컨테이너컨트롤 은내부에 2개이상의컨트롤을포함시키는것이가능하다. 하지만일반적인다른컨트롤은그것의부모인 ContentControl 타입에서제공하는 Content 속성에단일컨트롤을담는것만허용된다. Content 속성의타입은 object인데, 여기에 WPF의 UI 요소가아닌다른타입에해당하는인스턴스를지정하면자동으로해당객체의 ToString 메서드를호출한문자열을보여준다. 다음예제를보자. <Window [ 생략 ] DataContext="Binding RelativeSource=RelativeSource Self" Title="MainWindow" Height="200" Width="300"> <Grid> <Button Content="Binding SampleText" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top" Width="156" Height="23"/> <Button Content="Binding SampleDate" 64 3 부 _ 닷넷응용프로그램

</Grid> </Window> HorizontalAlignment="Left" Margin="10,38,0,0" VerticalAlignment="Top" Width="156" Height="23"/> <Button Content="Binding SampleControl" HorizontalAlignment="Left" Margin="10,66,0,0" VerticalAlignment="Top" Width="156" Height="23"/> 예제 12.15: Content 속성사용예 using System; using System.ComponentModel; using System.Windows; using System.Windows.Controls; namespace WpfApplication1 public partial class MainWindow : Window, INotifyPropertyChanged string _sampletext; public string SampleText get return _sampletext; set _sampletext = value; OnPropertyChanged("SampleText"); DateTime _sampledate; public DateTime SampleDate get return _sampledate; set _sampledate = value; OnPropertyChanged("SampleDate"); UIElement _samplecontrol; 12. 프로젝트유형 65

public UIElement SampleControl get return _samplecontrol; set _samplecontrol = value; OnPropertyChanged("SampleControl"); public MainWindow() InitializeComponent(); this.sampletext = "This text"; this.sampledate = DateTime.Now; TextBox txtbox = new TextBox(); txtbox.text = "[sample]"; this.samplecontrol = txtbox; // PropertyChanged / OnPropertyChanged 구현생략 : 예제 12.13 참조 예제 12.15 를실행하면그림 12.45 와같은결과를확인할수있다. 그림 12.45: Button 의 Content 에지정된 string, DateTime, UIElement 인스턴스 첫번째버튼은단순히 string 타입인 SampleText의내용을그대로출력하고, 두번째버튼은 DateTime 인스턴스이므로 ToString 메서드를호출한결과를 Content에보여준다. 특이한것은세번째버튼이다. UIElement를상속받은 TextBox 컨트롤을 Content 속성에지정함으로써마치자식컨트롤을포함하고있는것과같은효과를낸다. 66 3 부 _ 닷넷응용프로그램

Content 속성은 XAML 에서도지정할수있는데, 문법은 < 컨트롤타입이름.Content> 로설정한다. 예 제 12.15 의세번째버튼을 XAML 에서다음과같이동일하게표현할수있다. <Button HorizontalAlignment="Left" Margin="10,94,0,0" VerticalAlignment="Top" Width="156" Height="31"> <Button.Content> <TextBox Text="[sample]"></TextBox> </Button.Content> </Button> 또는좀더간단하게, Content 가기본속성이므로아예생략하는것도가능하다. <Button HorizontalAlignment="Left" Margin="10,94,0,0" VerticalAlignment="Top" Width="156" Height="31"> <TextBox Text="[sample]"></TextBox> </Button> 위의구문을이해한다면 WPF 프로젝트를처음생성했을때기본생성되는 MainWindow.xaml 의 구조도이제파악할수있을것이다. <Window x:class="wpfapplication2.mainwindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="350" Width="525"> <Grid> </Grid> </Window> Window 타입에는 Content 속성이제공되며, 위의코드는 <Window.Content> 가생략된채로하위에 Grid 패널을포함하고있는것이다. Content 속성은 1개의자식요소만포함할수있지만, Panel 타입이 UIElement를상속받은것임을감안한다면이를활용해다중컨트롤을 Content에지정하는것도가능하다. 다음은 Button 내부에 CheckBox와 Label 컨트롤두개를 Panel을이용해포함하는방법을보여준다. 12. 프로젝트유형 67

<Button HorizontalAlignment="Left" Margin="10,94,0,0" VerticalAlignment="Top" Width="156" Height="31"> <Button.Content> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="20"></ColumnDefinition> <ColumnDefinition Width="*"></ColumnDefinition> </Grid.ColumnDefinitions> <CheckBox VerticalAlignment="Center"></CheckBox> <Label Grid.Column="1">Button with CheckBox</Label> </Grid> </Button.Content> </Button> 컨트롤내부에포함하는내용으로 Content 속성이항상사용되는것은아니다. 가령 TextBox의경우는명시적으로 Text 속성을통해내부의 편집문자열 을지정한다. 즉, Content 속성은 UIElement 까지자식으로담을필요가있을때사용하는반면, 그밖의특정타입으로값이고정되는경우에는그에맞는이름의속성을정의해두는것이일반적인관례다. 12.2.5 Padding과 Margin 컨트롤의위치를세밀하게제어하려면 Padding과 Margin 속성의차이점을알아둘필요가있다. 이용어들은윈도우폼응용프로그램및 HTML 웹페이지의 CSS에도동일하게적용되므로이번기회에알고넘어가면도움될것이다. 이해를돕기위해비주얼스튜디오의 XAML 디자인화면을이용해보자. 우선, MainWindow.xaml 디자인화면에 Button 하나를추가한다. <Window x:class="wpfapplication1.mainwindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Test" Height="350" Width="525"> <Grid> <Button>Click</Button> </Grid> </Window> 68 3 부 _ 닷넷응용프로그램

그럼화면에는 Window 요소하위에 Grid 요소를채우고, 이어서 Button 컨트롤을 Grid 에채워넣는 다 ( 그림 12.46 참고 ). 그림 12.46: Window, Grid, Button 컨트롤배치 이상태에서 Button 요소에 Margin 과 HorizontalAlignment, VerticalAlignment 를설정해보자. <Button HorizontalAlignment="Left" VerticalAlignment="Top" Margin="10, 10, 0, 0">Click</Button> Margin 의 4 개숫자는해당컨트롤의부모요소로부터각 4 면에서얼마나떨어져배치할것인지를지 정한다. 각각 [ 좌 ] [ 위 ] [ 우 ] [ 아래 ] 를의미하며, 따라서예제에서는 10, 10, 0, 0 으로지정했으므로 그림 12.47 처럼 Grid 의좌로부터 10, 위로부터 10 단위만큼 Button 컨트롤을떨어뜨리게된다. 그림 12.47: Margin= 10, 10, 0, 0 을지정한컨트롤위치 12. 프로젝트유형 69

HorizontalAlignment와 VerticalAlignment를지정하지않으면기본적으로중앙 (Center) 정렬이되기때문에 10 정도의 Margin 값은거의차이가없다. 이제 Padding 값을지정할차례다. 이역시 4개의값을줄수있는데, Margin과마찬가지로 [ 좌 ] [ 위 ] [ 우 ] [ 아래 ] 를의미하며, 이를지정해해당컨트롤과그것이소유하는 Content 요소간의간격을제어할수있다. 예를들어, 그림 12.47의 Button에 Width=200, Height=100를지정해크기를좀더키운후, HorizontalContentAlignment= Left, VerticalContentAlignment= Top 과함께 Padding 값을주면 Content에해당하는 Click 텍스트가버튼컨트롤내에서그만큼의간격을유지한상태로출력되는모습을확인할수있다 ( 그림 12.48 참고 ). 그림 12.48: Padding= 30, 50, 0, 0 을지정한컨트롤 <Button HorizontalAlignment="Left" VerticalAlignment="Top" Margin="10, 10, 0, 0" HorizontalContentAlignment="Left" VerticalContentAlignment="Top" Width="200" Height="100" Padding="30, 50, 0, 0">Click</Button> HorizontalContentAlignment와 VerticalContentAlignment의효과는 Content에영향을미친다는점을제외하면각각 HorizontalAlignment와 VerticalAlignment의역할과동일하다. 마찬가지로기본값역시중앙정렬이기때문에 Left, Top으로지정하지않으면 Padding의효과가의도한대로나타나지않는다. 70 3 부 _ 닷넷응용프로그램

간단히정리하면 Margin 은컨트롤과그컨트롤의부모와의간격을제어하는데사용하고, Padding 은그컨트롤이소유한 Content 와의간격을제어하는용도로쓰인다. 12.2.6 ItemsControl 타입 WPF의진수를맛보려면 ItemsControl을살펴봐야한다. 일반적으로 ItemsControl은컬렉션에담긴데이터를보여주는용도로사용한다. 즉, 목록 을보여주는역할을하는가장기본적인코드를 WPF에서는 ItemsControl 타입에정의해두고, 그것을상속받은여러컨트롤을이용해쉽게목록을화면에보여줄수있다. ItemsControl을상속받은컨트롤들은많지만여기서는그중에서도자주사용되는 ListBox 컨트롤의사용법을예로든다. 우선, 기본적으로 3개의항목을보여주는 ListBox는예제 12.16의 XAML 코드로구성되고, 이를실행한모습은그림 12.49와같다. 예제 12.16: ListBox 기본사용예 <ListBox> <ListBoxItem>Yellow</ListBoxItem> <ListBoxItem>Green</ListBoxItem> <ListBoxItem>Blue</ListBoxItem> </ListBox> 그림 12.49: 기본 ListBox 를실행한화면 여기서중요한것은 ListBoxItem 의내부에지정된값이다름아닌 Content 라는점이다. <ListBoxItem> <ListBoxItem.Content>Yellow</ListBoxItem.Content> </ListBoxItem> 12. 프로젝트유형 71

Content 의사용법을잘이해하고있다면여기서더재미있는표현도가능하다는것을짐작할수있 다. 예를들어, 예제 12.17 처럼각항목의좌측에 CheckBox 를넣는것도가능하다. 예제 12.17: ListBoxItem의 Content에다중컨트롤설정 <ListBox> <ListBoxItem> <ListBoxItem.Content> <WrapPanel> <CheckBox VerticalAlignment="Center"></CheckBox> <Label>Yellow</Label> </WrapPanel> </ListBoxItem.Content> </ListBoxItem> <ListBoxItem> <ListBoxItem.Content> <WrapPanel> <CheckBox VerticalAlignment="Center"></CheckBox> <Label>Green</Label> </WrapPanel> </ListBoxItem.Content> </ListBoxItem> <ListBoxItem> <ListBoxItem.Content> <WrapPanel> <CheckBox VerticalAlignment="Center"></CheckBox> <Label>Blue</Label> </WrapPanel> </ListBoxItem.Content> </ListBoxItem> </ListBox> 그림 12.50: ListBoxItem 내부에 CheckBox 와 Label 을포함 72 3 부 _ 닷넷응용프로그램

ListBoxItem 을 XAML 에직접사용하는대신, 코드에서정의한컬렉션변수를 ListBox XAML 에데 이터바인딩시켜적용하는것도가능하다. ListBox 는목록을보여주는컨트롤이기때문에컬렉션타 입을받는 ItemsSource 속성을제공하며, 이를이용해바인딩을설정한다. <Window x:class="wpfapplication1.mainwindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Form1" Height="350" Width="525" DataContext="Binding RelativeSource=RelativeSource Self"> <Grid> <ListBox ItemsSource="Binding Path=ColorList"> </ListBox> </Grid> </Window> 그럼당연히 xaml.cs 코드파일에는 ColorList 속성을제공해야한다. using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; using System.Windows; namespace WpfApplication1 public partial class MainWindow : Window public MainWindow() InitializeComponent(); _colorlist.add("yellow"); _colorlist.add("green"); _colorlist.add("blue"); ObservableCollection<string> _colorlist = new ObservableCollection<string>(); public ObservableCollection<string> ColorList 12. 프로젝트유형 73

get return _colorlist; 그런데이번에는 INotifyPropertyChanged 인터페이스가 MainWindow에구현되지않았다. _ colorlist 값은 MainWindow 인스턴스가생성되는순간부터변하지않고고정되기때문에굳이 INotifyPropertyChanged 인터페이스처리를할필요가없는것이다 ( 물론구현해도무방하다 ). 대신 ColorList의타입을일반적인 List<T> 자료형을사용하지않고 ObservableCollection<T> 타입으로대체하고있다. 이것은 ColorList 컬렉션의항목이추가 / 삭제될때마다바인딩된 ListView 컨트롤에통지를보내야하는데, List<T> 타입은이것이구현돼있지않기때문이다. 반면 ObservableCollection<T> 타입은컬렉션에항목이추가 / 삭제될때마다그것에대한변경사항을통지하는 INotifyCollectionChanged 인터페이스를상속받아구현한것으로 WPF의데이터바인딩을위해닷넷프레임워크 3.0 BCL에함께포함됐다. 이렇게구현된상태에서프로그램을실행하면그림 12.49와같은모습을볼수있다. 말로만설명한 ObservableCollection<T> 의동작을확인하기위해 Button 컨트롤 2개를추가하고각이벤트처리기에 ColorList 컬렉션에항목을더하고빼는기능을구현해보자. <Window x:class="wpfapplication1.mainwindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Form1" Height="350" Width="525" DataContext="Binding RelativeSource=RelativeSource Self"> <Grid> <ListBox ItemsSource="Binding Path=ColorList" Margin="0,0,0,35"> </ListBox> <Button Content="Add" HorizontalAlignment="Left" Margin="10,289,0,0" VerticalAlignment="Top" Width="75" Click="Add_Click"/> <Button Content="Remove" HorizontalAlignment="Left" Margin="90,289,0,0" VerticalAlignment="Top" Width="75" Click="Remove_Click"/> </Grid> </Window> 74 3 부 _ 닷넷응용프로그램

예제 12.18: XAML에바인딩된컬렉션의항목추가 / 삭제 using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; using System.Reflection; using System.Windows; using System.Windows.Media; namespace WpfApplication1 public partial class MainWindow : Window List<string> _samplecolors = new List<string>(); public MainWindow() InitializeComponent(); _colorlist.add("yellow"); _colorlist.add("green"); _colorlist.add("blue"); // Colors 타입에 static 속성으로정의된컬러값을 // 리플렉션을이용해가져온다. foreach (PropertyInfo propinfo in typeof(colors).getproperties( BindingFlags.Static BindingFlags.Public)) _samplecolors.add(propinfo.name); ObservableCollection<string> _colorlist = new ObservableCollection<string>(); public ObservableCollection<string> ColorList get return _colorlist; private void Add_Click(object sender, RoutedEventArgs e) // _samplecolors 에서임의의요소를추가 12. 프로젝트유형 75

Random rand = new Random((int)DateTime.Now.Ticks); int colorindex = rand.next(_samplecolors.count); _colorlist.add(_samplecolors[colorindex]); private void Remove_Click(object sender, RoutedEventArgs e) _colorlist.removeat(_colorlist.count - 1); // 마지막요소를제거 코드를살펴보면 Add/Remove 버튼이눌렸을때단지 ColorList 컬렉션에항목을추가하고삭제하는것밖에없다. 그런데예제코드를실행해보면버튼을누름에따라 ListBox 화면에서동시에색상이추가 / 삭제되는것을확인할수있다. 왜냐하면 ObservableCollection<T> 타입이컬렉션에항목이추가 / 삭제될때마다이벤트를발생시키는것과함께, ListBox의 ItemsSource 속성이해당이벤트를받아 ListBoxItem을그에맞게추가 / 삭제하는동작을수행하기때문이다. 이번에는 ItemsSource 바인딩을이용해그림 12.50처럼 CheckBox도함께보이도록실행하는방법을알아보자. 물론기본적으로는 UIElement를컬렉션으로구성하면그와동일하게실행시킬수는있다. using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Reflection; using System.Windows; using System.Windows.Controls; using System.Windows.Media; namespace WpfApplication1 public partial class MainWindow : Window List<string> _samplecolors = new List<string>(); public MainWindow() InitializeComponent(); 76 3 부 _ 닷넷응용프로그램

. AddElement("Yellow"); AddElement("Green"); AddElement("Blue"); foreach (PropertyInfo propinfo in typeof(colors).getproperties( BindingFlags.Static BindingFlags.Public)) _samplecolors.add(propinfo.name); // UIElement 컬렉션구성 ObservableCollection<UIElement> _colorlist = new ObservableCollection<UIElement>(); public ObservableCollection<UIElement> ColorList get return _colorlist; private void Add_Click(object sender, RoutedEventArgs e) Random rand = new Random((int)DateTime.Now.Ticks); int colorindex = rand.next(_samplecolors.count); AddElement(_sampleColors[colorIndex]); // ' 예제 12.17' 의 ListBoxItem.Content XAML을코드로구성 private void AddElement(string colorname) WrapPanel panel = new WrapPanel(); CheckBox box = new CheckBox(); box.verticalalignment = System.Windows.VerticalAlignment.Center; Label lbl = new Label(); lbl.content = colorname; panel.children.add(box); panel.children.add(lbl); _colorlist.add(panel); 12. 프로젝트유형 77

private void Remove_Click(object sender, RoutedEventArgs e) _colorlist.removeat(_colorlist.count - 1); 그런데이방법은코드에서직접 UI 구성을함으로써서로강하게결합관계를맺는다는문제를낳는다. 그보다는기존의 ObservableCollection<string> 데이터구성은순수하게유지하고 UI를담당하는 XAML에서표현을변경하는방법이권장된다. WPF의모든 ItemsControl 및그것을상속한타입은 ItemTemplate 속성을제공하는데, 여기에개별항목의 UI 요소를임의로구성하는것이가능하다. 따라서예제 12.18의 C# 코드를그대로재사용하면서 XAML만예제 12.19와같이바꾸면그림 12.50처럼체크박스가함께나오게된다. 예제 12.19: ItemTemplate 사용자정의 <Window x:class="wpfapplication1.mainwindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Form1" Height="350" Width="525" DataContext="Binding RelativeSource=RelativeSource Self"> <Grid> <ListBox ItemsSource="Binding Path=ColorList" Margin="0,0,0,35"> <ListBox.ItemTemplate> <DataTemplate> <WrapPanel> <CheckBox VerticalAlignment="Center"></CheckBox> <Label Content="Binding Path=."></Label> </WrapPanel> </DataTemplate> </ListBox.ItemTemplate> </ListBox> <Button Content="Add" HorizontalAlignment="Left" Margin="10,289,0,0" VerticalAlignment="Top" Width="75" Click="Add_Click"/> <Button Content="Remove" HorizontalAlignment="Left" Margin="90,289,0,0" VerticalAlignment="Top" Width="75" Click="Remove_Click"/> </Grid> </Window> 78 3 부 _ 닷넷응용프로그램

ItemTemplate에사용된 Label 컨트롤의바인딩구문에 Path 값이점 (.) 으로지정된것은해당 ItemsSource에바인딩된 ObservableCollection<string> 컬렉션의개별항목을의미한다. 방금실습한예제가바로 WPF 응용프로그램만이가진매력포인트다. 코드파일은변경하지않은상태에서 XAML 디자인을어떻게하느냐에따라프로그램의외관이자유롭게바뀔수있기때문에개발자는업무코드에집중하고디자인요소는분리해서디자이너에게맡김으로써분업이가능해진다. 계속해서예제 12.18에서 ObservableCollection의형식매개변수를일반적인클래스로대체해서넣어보자. 이를위해 ColorData 클래스를하나정의하고 using System.ComponentModel; using System.Windows.Media; namespace WpfApplication1 public class ColorData : INotifyPropertyChanged Brush _color; public Brush Color get return _color; set _color = value; OnPropertyChanged("Color"); string _name; public string Name get return _name; set _name = value; OnPropertyChanged("Name"); public event PropertyChangedEventHandler PropertyChanged; 12. 프로젝트유형 79

protected virtual void OnPropertyChanged(string name) if (PropertyChanged == null) return; PropertyChanged(this, new PropertyChangedEventArgs(name)); 예제 12.18 의 ObservableCollection 의형식매개변수를 ColorData 로교체한다. using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Reflection; using System.Windows; using System.Windows.Controls; using System.Windows.Media; namespace WpfApplication1 public partial class MainWindow : Window List<string> _samplecolors = new List<string>(); public MainWindow() InitializeComponent(); AddColor("Yellow"); AddColor("Green"); AddColor("Blue"); foreach (PropertyInfo propinfo in typeof(colors).getproperties( BindingFlags.Static BindingFlags.Public)) _samplecolors.add(propinfo.name); 80 3 부 _ 닷넷응용프로그램

private void AddColor(string colorname) ColorData item = new ColorData(); item.name = colorname; Color color = (Color)ColorConverter.ConvertFromString(colorName); item.color = new SolidColorBrush(color); _colorlist.add(item); ObservableCollection<ColorData> _colorlist = new ObservableCollection<ColorData>(); public ObservableCollection<ColorData> ColorList get return _colorlist; private void Add_Click(object sender, RoutedEventArgs e) Random rand = new Random((int)DateTime.Now.Ticks); int colorindex = rand.next(_samplecolors.count); AddColor(_sampleColors[colorIndex]); private void Remove_Click(object sender, RoutedEventArgs e) _colorlist.removeat(_colorlist.count - 1); 그림 12.51 은이코드를실행한결과를보여준다. 12. 프로젝트유형 81

그림 12.51: ColorData 타입으로바꾼후실행한모습 항목이 WpfApplication1.ColorData 라는문자열로출력되는것을볼수있다. 왜냐하면 XAML의 <Label Content= Binding Path=. ></Label> 에서 Binding Path를점 (.) 으로설정했기때문이다. 이는 ItemsSource에지정된컬렉션의항목하나를대표하므로결국 ColorData 인스턴스와대응되고해당인스턴스의 ToString 메서드가호출된결과를보여주기때문에나타나는현상이다. 따라서원하는대로색상명을나타내고싶다면 ColorData 타입의 Name 속성을 Binding Path에지정하면된다. 즉, <Label Content= Binding Path=Name ></Label> 로변경하면그림 12.50과같이원래의도한결과를볼수있다. 여기서조금재미있는효과를더내보자. ColorData 타입에는 Color 속성이 System.Windows. Media.Brush 타입으로정의돼있다. 이타입은 WPF에서정의된각종컨트롤의색상을나타내는속성으로도사용할수있기때문에원한다면 Binding을이용해 XAML에서활용할수도있다. 예를들어, ItemTemplate을다음과같이변경하면 <ListBox.ItemTemplate> <DataTemplate> <WrapPanel> <CheckBox VerticalAlignment="Center"></CheckBox> <Label Foreground="Binding Path=Color" Content="Binding Path=Name"></Label> </WrapPanel> </DataTemplate> </ListBox.ItemTemplate> 82 3 부 _ 닷넷응용프로그램

Label 의텍스트색상이바뀌어서실행되는것을확인할수있다 ( 그림 12.52 참고 ). 그림 12.52: Foreground 속성에바인딩된글자색 Button 하나를더추가해서이미 ColorList 에있는항목의속성을바꿔보자. <Button Content="Red" HorizontalAlignment="Left" Margin="170,289,0,0" VerticalAlignment="Top" Width="75" Click="Red_Click"/> private void Red_Click(object sender, RoutedEventArgs e) foreach (var item in _colorlist) item.color = new SolidColorBrush(Colors.Red); item.name = "Red"; 프로그램을실행한다음 Red 버튼을누르면 ListBox가담고있는모든항목이빨간색으로바뀐다. 왜냐하면 ColorData 타입은 INotifyPropertyChanged 인터페이스를구현하고있으므로속성이바뀐경우이사실을데이터바인딩된요소에통보하게되고, 결과적으로 Label의내용이바뀌는것이다. 이절의내용을통해알수있듯이 WPF의가장큰매력은개발자가 UI보다는데이터처리에좀더집중할수있게만들어준다는점이다. 데이터만있다면 UI는 XAML 수준에서자유롭게표현할수있고심지어디자이너가그과정을도와줄수있는기반을제공한다. 12. 프로젝트유형 83

정리한가지분명한점은 WPF 응용프로그램을만드는방법이윈도우폼응용프로그램을만드는방법과비교해더어렵다는점이다. 만약 윈도우운영체제 에서실행되는 윈도우응용프로그램 만작성한다면 WPF 응용프로그램을굳이배우지않고 윈도우폼응용프로그램 으로프로그램을만들어도좋다. 하지만 WPF는배워둘만한가치가충분히있다. 왜냐하면 WPF 응용프로그램을만드는 XAML 방식은 실버라이트응용프로그램, 윈도우폰응용프로그램, 윈도우 8 스토어앱 과같은프로그램에도적용되기때문이다. 이것이 WPF를공부해야하는가장확실한이유다. WPF 및 XAML과같은프로그램이재미있는또한가지이유는디자이너와의협업이가능하다는점이다. 솔직히말해서 WPF가처음나왔을때필자역시아무리 WPF라고해도응용프로그램을개발할때디자이너와협업할수있다는사실을믿지않았다. 하지만그생각이잘못됐다는것을확인하는데는그리오랜시간이걸리지않았는데, 필자와함께일한디자이너가익스프레션블렌드를이용해 WPF 요소의디자인을자유자재로다루는모습을봤기때문이다. 개발팀에포토샵을다루는디자이너가있다면지금당장익스프레션블렌드책을한권사주고 WPF/ 실버라이트 / 윈도우폰 / 스토어앱을협업해서만들어보라. 필자가느꼈던놀라움을머지않아똑같이느끼게될것이다. 12.3 서비스응용프로그램 컴퓨터가켜지면윈도우운영체제가초기화과정을마치고로그인할사용자계정을묻는단계가나온다. 로그인하기전까지여러분은어떠한프로그램도실행할수없다. 그런데이렇게사용자가로그인하지않은상태에서도어떤특정용도의프로그램은실행돼야할때가있다. 예를들어, 네이버홈페이지서비스를담당하는 HTTP 소켓서버프로그램은사용자가해당컴퓨터에로그인하지않아도자동으로시스템이부팅되면서실행될필요가있다. 윈도우운영체제에서는전통적으로 NT 서비스 라는유형으로, 유닉스 / 리눅스계열에서는데몬 (Daemon) 프로세스라고알려져있는데, 이런프로그램의특징은사용자가로그인하지않은상태에서도실행될수있다는것이다. 제어판 / 관리도구 / 서비스 를실행하면여러분의컴퓨터에설치된각종 NT 서비스프로그램 을볼수있다. 84 3 부 _ 닷넷응용프로그램

그림 12.53: 서비스목록 그림 12.53에서 시작유형 칼럼의값이 자동 으로설정된서비스는운영체제가시작될때실행된다. 한가지눈여겨봐야할것이있다면 다음사용자로로그온 칼럼에보이는 Local System, Local Service, Network Service 와같은특수한계정들이다. 윈도우운영체제에서실행되는모든 EXE 프로세스는해당프로세스가가진권한을 사용자계정 으로묶기때문에반드시해당프로세스와연관돼야할사용자계정을명시해야한다. 사실이문제는로그온한후에실행하는프로그램 ( 예 : 웹브라우저 ) 에는문제가되지않는다. 왜냐하면그러한프로그램은여러분이로그온한계정에서실행하는것이므로자연스럽게해당사용자계정이가진권한을사용하면되기때문이다. 하지만서비스프로그램은사용자가로그온하지않은상태에서실행되므로반드시실행권한을확보하기위해사용자계정을명시해야한다. 물론일반적인사용자계정으로지정하는것도가능하지만윈도우운영체제는서비스를위한세가지특별한사용자계정을제공한다. Local System 관리자그룹 (Administrators) 에준하는높은수준의권한을지닌서비스용계정으로서보안상특별한이유가없다면사용하지않는것을권장한다. Local Service Local System 계정이지닌높은수준의권한을제거한계정으로, 일반적인용도의서비스라면이사용자계정을사용하는것을권장한다. Network Service Local Service 계정과완전하게동일한수준의권한을가진다. 하지만 Local Service 계정은액티브디렉터리내의다른컴퓨터로접근하는경우 익명 (Anonymous) 권한을갖지만, Network Service는 컴퓨터계정 (Computer account) 권한으로접근한다. 12. 프로젝트유형 85

액티브디렉터리 (AD: Active Directory) 를간단히설명하면윈도우서버운영체제에서제공하는통합계정관리서비스를의미한다. 액티브디렉터리에계정을하나등록하면액티브디렉터리에참여하는모든컴퓨터에해당사용자계정권한으로접근하는것이가능하다. 선택의기준은명확하다. 높은권한이필요하다면 Local System, 낮은권한으로충분하지만액티브디렉터리내의다른컴퓨터로접근해야한다면 Network Service, 그렇지않다면 Local Service를지정하면된다. 액티브디렉터리가구성되지않은환경이라면 Network Service와 Local Service의권한은사실상같다. 서비스프로그램으로등록되려면우선그프로그램을레지스트리에등록해야한다. 그러면윈도우운영체제에포함된 SCM( 서비스컨트롤관리자 : Service Control Manager) 은그프로그램을인식하게되고, 그림 12.53의목록에도나타난다. 이후에윈도우시스템이부팅될때마다 SCM은등록된서비스중에 자동 으로설정된항목을찾아서레지스트리에설정된 EXE의경로에해당하는프로그램을실행한다. 실습을위해 TCP 서버소켓을이용한서비스를하나만들어보자. 1. 새프로젝트 창에서 빈프로젝트 유형의템플릿을선택하고이름을 MyService 로지정한다. 2. System.ServiceProcess 어셈블리와 System.Configuration.Install 어셈블리를참조추가한다. 3. 프로젝트속성창을열고 응용프로그램 (Application) 탭에서 출력형식 (Output type) 을 Windows 응용프로그램 으로변경한다. 4. 프로젝트에 클래스 유형으로 Program.cs 파일과 EchoServer.cs 파일을각각하나씩추가한다. 먼저 MyService 프로젝트가 서비스용프로그램 으로구동하기위한최소한의조건을만족하는코드 를추가하면예제 12.20 과같다. 예제 12.20: 서비스를위한기본코드 // EchoServer.cs using System.ServiceProcess; namespace MyService // 서비스를위한기본구현이포함된 ServiceBase 타입을상속받는다. public class EchoServer : ServiceBase 86 3 부 _ 닷넷응용프로그램

public EchoServer() this.servicename = "MyEchoServer"; // 서비스이름을지정 // 서비스시작시실행되는메서드 protected override void OnStart(string[] args) // 서비스중지시실행되는메서드 protected override void OnStop() // Program.cs using System.ServiceProcess; namespace MyService static class Program static void Main() ServiceBase[] ServicesToRun; ServicesToRun = new ServiceBase[] new EchoServer() ; ServiceBase.Run(ServicesToRun); EXE 프로세스가구동될때가장먼저실행되는 Program.cs의 Main 메서드를보면 ServiceBase 배열에 EchoServer 객체를담아 ServiceBase.Run 메서드에전달하는것을볼수있다. 따라서하나의 EXE 프로세스안에는여러개의서비스를담을수있는데, 실제로그림 12.53 목록에서보여지는많은서비스가각각하나의 EXE로실행되는것이아니고, 어떤서비스의경우에는하나의 EXE를공 12. 프로젝트유형 87

유하기도한다. EchoServer.cs 파일의경우에는아무일도하지않는기본서비스코드가포함돼있다. 이서비스가레지스트리에등록되면그림 12.53과같은화면이나타나고메뉴를이용해서비스를시작하게만들면 EchoServer 타입의 OnStart 메서드가 SCM에의해실행된다. 마찬가지로서비스를중지하면 OnStop 메서드가실행된다. 위의과정은 Express 버전의비주얼스튜디오를사용하는경우에만해당한다. Professional 이상의유료버전을사용하고있다면다음과같이간단하게서비스프로젝트를생성할수있다. 1. 새프로젝트 창에서 Windows 범주의 Windows Service 유형의템플릿을선택하고이름을 MyService 로 지정한다. 2. 기본생성된 Service1.cs 파일의이름을 EchoServer.cs 로변경한다. 기본적인동작만수행하는서비스프로젝트를생성했으니, 이제레지스트리에등록해서서비스관리자를이용해실제로 시작 / 중지 시킬수있다. 이작업을레지스트리편집기 (regedit.exe) 를이용해직접할수도있지만닷넷프레임워크에서는이과정을수월하게만들어주는 InstallUtil.exe 라는프로그램이제공되므로이프로그램을사용하길권장한다. 32비트운영체제인경우 32비트서비스등록 : "C:\Windows\Microsoft.NET\Framework\v2.0.50727\InstallUtil.exe" 64비트운영체제인경우 32비트서비스등록 : "C:\Windows\Microsoft.NET\Framework\v2.0.50727\InstallUtil.exe" 64비트서비스등록 : "C:\Windows\Microsoft.NET\Framework64\v2.0.50727\InstallUtil.exe" InstallUtil.exe 프로그램을이용하면서비스등록 / 해지를다음과같이간단하게할수있다. 서비스등록 InstallUtil.exe MyService.exe 서비스해지 InstallUtil.exe /u MyService.exe 하지만아직은 InstallUtil.exe로등록할수없는상태다. 왜냐하면 EXE 프로그램제작자는 InstallUtil.exe에게서비스등록과관계된몇가지정보를전달해야하기때문이다. 그리고그방법은클래스를통해제공할수있다. 따라서 MyService 프로젝트에새롭게 MyServiceInstaller.cs 코드파일을추가하고다음과같은내용을작성한다. 88 3 부 _ 닷넷응용프로그램

using System.ServiceProcess; using System.Configuration.Install; namespace MyService [RunInstaller(true)] public class MyServiceInstaller : Installer private ServiceProcessInstaller _processinstaller; private ServiceInstaller _serviceinstaller; public MyServiceInstaller() this._processinstaller = new ServiceProcessInstaller(); this._serviceinstaller = new ServiceInstaller(); this._processinstaller.account = ServiceAccount.LocalService; this._serviceinstaller.servicename = "MyEchoServer"; this._serviceinstaller.description = "My First Service Program"; this._serviceinstaller.starttype = ServiceStartMode.Automatic; this.installers.addrange(new Installer[] this._processinstaller, this._serviceinstaller); 한가지중요한것은 Installer 타입을상속받고반드시 RunInstaller 특성을지정해야한다는점이다. 그렇게만하면 InstallUtil.exe 는자동으로이클래스를감지하고서비스설치와관련된정보를알아 낸다. 나머지변수는그림 12.54 에서볼수있듯이개별적인의미를갖는다. 12. 프로젝트유형 89

그림 12.54: 서비스관리자에설정되는변수의값 드디어등록준비가모두끝났다. 예제프로젝트를빌드하고생성된 EXE 폴더에서 관리자권한 으로 실행한명령행창을이용해 InstallUtil.exe 를실행하면다음과같은출력결과를확인할수있고, 서 비스 관리도구를실행하면그림 12.54 처럼 MyEchoServer 항목이생성된것을볼수있다. C:\Windows\system32>cd D:\temp\MyService\MyService\bin\Debug C:\Windows\system32>d: D:\temp\MyService\MyService\bin\Debug>installutil MyService.exe Microsoft (R).NET Framework Installation utility Version 4.0.30319.18010 Copyright (C) Microsoft Corporation. All rights reserved. 트랜잭트설치를실행하고있습니다. 설치의 Install 단계를시작하고있습니다. D:\temp\MyService\MyService\bin\Debug\MyService.exe 어셈블리의진행상황을보려면로그파일내용을검토하십시오. 파일은 D:\temp\MyService\MyService\bin\Debug\MyService.InstallLog 위치에있습니다. 어셈블리 'D:\temp\MyService\MyService\bin\Debug\MyService.exe' 을 ( 를 ) 설치하고있습니다. 영향을받는매개변수 : logtoconsole = logfile = D:\temp\MyService\MyService\bin\Debug\MyService.InstallLog assemblypath = D:\temp\MyService\MyService\bin\Debug\MyService.exe MyEchoServer 서비스를설치하고있습니다... MyEchoServer 서비스가설치되었습니다. EventLog 소스 MyEchoServer을 ( 를 ) 로그 Application에만들고있습니다... 90 3 부 _ 닷넷응용프로그램

Install 단계는완료되었으며 Commit 단계를시작하고있습니다. D:\temp\MyService\MyService\bin\Debug\MyService.exe 어셈블리의진행상황을보려면로그파일내용을검토하십시오. 파일은 D:\temp\MyService\MyService\bin\Debug\MyService.InstallLog 위치에있습니다. 어셈블리 'D:\temp\MyService\MyService\bin\Debug\MyService.exe' 을 ( 를 ) 커밋하고있습니다. 영향을받는매개변수 : logtoconsole = logfile = D:\temp\MyService\MyService\bin\Debug\MyService.InstallLog assemblypath = D:\temp\MyService\MyService\bin\Debug\MyService.exe Commit 단계가완료되었습니다. 트랜잭트설치가완료되었습니다. D:\temp\MyService\MyService\bin\Debug> 하지만이작업으로는서비스가설치만됐을뿐실행중인것은아니다. 직접 서비스 관리도구에서 시작 메뉴를선택하거나컴퓨터를다시부팅하면서비스가실행된다. 이제본격적으로서비스를위한코드를추가해보자. 서비스응용프로그램은사용자가실행하지않고 SCM에의해실행되는데, 그진입점은예제 12.20에서구현한 OnStart 메서드다. 따라서서비스가 시작 할때실행해야할코드는 OnStart에넣어두고, 마찬가지로서비스를 중지 시킬때실행될코드는 OnStop에넣으면된다. 한가지특이한점이있다면 SCM은 OnStart 메서드를실행한후해당코드가모두실행이완료될때까지대기한다는것이다. 기본적으로 30초를기다리게되며, 만약그시간내에 OnStart 메서드가실행을반환하지않으면강제로프로세스를종료한다. 이런이유로일반적으로는서비스를위한코드를별도로스레드함수로분리하고 OnStart에서는스레드를시작하는식으로처리한다. 따라서우리가구현하는 EchoServer의코드역시그와같은구조를따른다. Thread _workerthread; bool _exitthread = false; protected override void OnStart(string[] args) _workerthread = new Thread(echoFunc); _workerthread.isbackground = true; _workerthread.start(); // 스레드를시작하는것으로 OnStart의역할을끝낸다. 12. 프로젝트유형 91

protected override void OnStop() _exitthread = true; if (_workerthread!= null) if (_workerthread.join(5000) == false) // 5초간스레드종료대기 _workerthread.abort(); _workerthread = null; private void echofunc(object obj) // echo server 코드 echofunc 의내부코드에서는 예제 6.38 TCP 서버측소켓을구현 의코드를그대로복사해넣으면 된다. 단지, OnStop 에서서비스중지가처리될수있게 _exitthread 불린형변수를이용해루프를 제어하는코드를추가하는식으로변경한다. private void echofunc(object obj) using (Socket srvsocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)) IPEndPoint endpoint = new IPEndPoint(IPAddress.Any, 11201); srvsocket.bind(endpoint); srvsocket.listen(10); while (_exitthread == false) Socket clntsocket = srvsocket.accept(); byte[] recvbytes = new byte[1024]; int nrecv = clntsocket.receive(recvbytes); string txt = Encoding.UTF8.GetString(recvBytes, 0, nrecv); 92 3 부 _ 닷넷응용프로그램

byte[] sendbytes = Encoding.UTF8.GetBytes("Hello: " + txt); clntsocket.send(sendbytes); clntsocket.close(); 물론 Socket 타입의 Accept 메서드가동기형식의블록킹호출이기때문에클라이언트가접속하지않는경우 Accept 단계에서스레드가계속머물러있게된다. 그런경우에는 OnStop 메서드에서 _ exitthread 변수를 true로변경해도스레드가종료되는것은아니므로이럴때는강제로 Thread. Abort 메서드를호출하는방식으로해결한다. 참고로 Thread.Abort 메서드의사용은권장되지않으므로좀더코드를개선하고싶다면 Accept를비동기방식으로변경해서처리하는편이좋다. 이렇게완성된 Echo 서비스는컴퓨터가재부팅되고나서사용자가로그인하지않아도 Local Service 계정의권한으로자동실행된다. 소위 서버 라고하는제품은모두이같은방식으로제작돼있다는점을알아두자. 예를들어, 6.8 데이터베이스 절에서배운 SQL 서버를비롯해 HTTP 웹서버, FTP 서버, 메일서버와같은프로그램들이 NT 서비스유형의대표적인사례다. 12.4 웹응용프로그램 앞에서 예제 6.42 TCP 소켓으로구현한 HTTP 서버 를통해간단하게 HTTP 통신을하는서버를만들어봤다. 사실그예제는 HTTP 프로토콜의기능중극히일부분만구현한것에불과하고그밖의모든기능을넣으려면더욱많은코드를추가해야만한다. 다행인점은 HTTP 프로토콜명세를충실하게구현한제품들이이미많이공개돼있기때문에대부분의개발자는그제품을기반으로동작하는 서비스 를만드는데더욱집중할수있다. 여기서말하는 서비스 가바로 웹응용프로그램 에해당한다. 처음 HTTP(Hyper Text Transport Protocol) 서비스가등장했던시절에는그이름에맞게단순히링크로엮어진문서 (Hyper Text) 를웹브라우저가요청하면서버측에서전송 (Transport) 해주는규약 (Protocol) 에불과했다. HTML 텍스트를담은문서와각종이미지파일, CSS, 자바스크립트로구성된파일들은정적콘텐츠 (Static Content) 라하고웹서버는요청된콘텐츠를가공없이그대로전송한다. 12. 프로젝트유형 93

지금까지의설명을직접실습해보자. 윈도우운영체제에서쉽게사용할수있는무료웹서버는대표 적으로 인터넷정보서비스 (IIS: Internet Information Services) 가있는데, 아래의방법에따라간 단하게설치해서사용할수있다. 1. 제어판 을실행하고 프로그램및기능 (Programs and Features) 프로젝트 항목을선택한후좌측상단의 Windows 기능켜기 / 끄기 (Turn Windows features on or off) 링크를누른다. 2. 선택창에서 인터넷정보서비스 (Internet Information Services) 항목을펼치면그아래에 World Wide Web 서비스 와 웹관리도구 가나타나는데, 모두선택하고 확인 버튼을누른다. IIS 웹서버설치가완료되면 C:\inetpub\wwwroot 폴더가생성된것을확인할수있다. 메모장 등의편집기를이용해다음과같이간단한 HTML 문서를만들어 wwwroot 폴더에 mywebpage. html 이라는이름으로복사해넣고 mywebpage.html <!DOCTYPE html> <html> <head> <title> 웹페이지테스트 </title> </head> <body style="background-color: yellow;"> 안녕하세요. <b>hello World</b><br /> <br /> 94 3 부 _ 닷넷응용프로그램

<a href="http://www.daum.net"> 이링크를누르면다음 (Daum) 사이트로이동합니다.</a> <br /> </body> </html> 이장에서는독자가기본적인 HTML 문법을알고있다고가정한다. 하지만처음접한다고해서문제될것은없는데, WPF의 XAML 구문에서다룬 XML 표기방식이그대로사용되기때문이다. 웹브라우저의주소표시줄에 http://localhost/mywebpage.html 이라고입력하면그림 12.55 와같 은화면이나타난다. 그림 12.55: mywebpage.html 을웹브라우저에서연모습 이때윈도우운영체제의작업관리자를통해목록을확인해보면 w3wp.exe라는이름의프로세스가실행된것을볼수있다. 예제 6.42를빌드해서만든 EXE를실행해 HTTP 요청을처리했던것처럼, IIS 서비스는 w3wp.exe 프로세스를실행해웹요청을처리한다. 단지차이점이있다면 IIS는 11.3 서비스응용프로그램 절에서배운서비스유형으로실행되므로컴퓨터에사용자가로그인하지않아도동작할수있다. 그림 12.55 와같은결과가나오기까지어떤처리가진행되는지짚어볼필요가있다. 우선웹브라우저 는사용자가입력한 URL 에서표 12.10 의규칙에따라요청정보를추출한다. 표 12.10: URL 의미 URL http:// localhost /mywebpage.html 의미 HTTP 통신임을나타내는접두사웹서버의도메인네임 (localhost는컴퓨터자신이대상임을지칭하는특별한문자열주소 ) 또는 IP 주소요청해야할웹자원 (Resource) 의경로 12. 프로젝트유형 95

이정보를바탕으로웹브라우저는다음과같은 GET 요청을 TCP 소켓을통해웹서버로전송하고 HTTP 규약에따라웹서버는 /mywebpage.html 이라는경로에위치한문서를웹브라우저로 TCP 소켓을통해응답하게된다. GET /mywebpage.html HTTP/1.1 Host: localhost 그런데웹서버는 mywebpage.html 파일을어떤근거로 c:\inetpub\wwwroot 폴더에서부터찾는걸까? 이를확인하려면 제어판 / 관리도구 에서 IIS( 인터넷정보서비스 ) 관리자 를실행해야한다. 그림 12.56과같은화면이나오면좌측상단의 연결 트리에서 사이트 하위의 Default Web Site 를마우스로선택하고, 우측하단의 고급설정 링크를누르면팝업설정창이나타나는데, 여기서 실제경로 에지정된값을확인할수있다. 그림 12.56: IIS 관리자에서사이트고급설정을통해경로를확인 일반적으로는 실제경로 대신 웹루트폴더 또는더간단하게 웹루트 라고부르기도한다. 그림 12.56에서볼수있듯이 IIS가기본으로지정한 %SystemDrive%\inetpub\wwwroot 경로는결국 C:\inetpub\wwwroot 가되기때문에 IIS 웹서버는 /mywebpage.html 파일을해당폴더에서찾아웹브라우저로 HTTP 응답헤더와함께파일의내용을함께실어전송한다. 96 3 부 _ 닷넷응용프로그램

HTTP/1.1 200 OK Content-Type: text/html Server: Microsoft-IIS/8.0 Date: Wed, 17 Jul 2013 10:42:09 GMT Content-Length: 263 <html> <head> <title> 웹페이지테스트 </title> </head>...[ 생략 ]... </html> 응답을받은웹브라우저는 HTTP 헤더내용으로부터 Content-Length 에지정된길이 ( 예제에서는 263) 를통해 HTTP 본문 (Body) 의크기를알게되고그것을해석해화면에보여준다. 물론원한다면웹루트폴더를변경해도된다. C:\temp 로바꾸고 mywebpage.html 파일을그곳으로복사해서서비스해도무방하다. 만약정적콘텐츠만담긴웹사이트를만들고싶다면이정도구성만으로도지금당장서비스를시작할수있다. 공용 IP가할당된컴퓨터에 IIS 서비스를설치하고웹루트폴더에파일을넣어두면전세계사용자들이방문해문서를열람할수있다. 문제는정적콘텐츠로는다양한웹서비스를만드는데한계가있다는점이다. 즉, 프로그램에의해자유롭게변경가능한동적콘텐츠 (Dynamic Content) 를생산할수있는능력이필요한데, 이러한요구사항을만족시키는과정에서본격적인 웹응용프로그램 의시대가열린다. 초기에는동적콘텐츠를생산하기위해그림 12.57에나온 CGI(Common Gateway Interface) 라는기술을사용했다. 웹서버는동적콘텐츠가필요할때마다내부적으로 EXE 프로세스를인자값과함께실행하고그결과를 HTML 텍스트로반환받는다. 그림 12.57: CGI 동작방식 표준입력을통해인자값전달 웹서버 (w3wp.exe) CGI 실행파일 ([...].exe) 표준출력을통해 HTML 텍스트반환 웹사이트의특성상 1 초에도수십건씩요청이들어올수있는데, 그때마다 EXE 파일을실행하면적 지않은부하 ( 오버헤드 : overhead) 가발생한다. 부하가발생하면자연스럽게응답속도가느려지고 12. 프로젝트유형 97

사용자의불편함이가중된다. 이런이유로 CGI는현재거의사용되지않고있다. 하지만코드에문제가있는경우웹서버프로세스 (w3wp.exe) 에영향을미치지않고 CGI로실행된 EXE 프로세스만종료되기때문에안정성이높다는장점은있다. 시간은흘러 CGI는마이크로소프트에서개발한 ASP(Active Server Pages) 기술에빠르게자리를내주게된다. ASP를이용하면 EXE 실행파일을만들필요없이스크립트 (Script) 언어를이용해동적콘텐츠를만들어낼수있으므로웹응용프로그램의제작속도가획기적으로향상된다. ASP로웹응용프로그램을만드는것이얼마나쉬운지직접시험해볼텐데, 이를위해서는기본설치된 IIS에그림 12.58에나와있는 ASP 와 ISAPI 확장 구성요소를추가로설치해야한다. 그림 12.58: ASP, ISAPI 확장구성요소설치 이두가지구성요소가설치된후부터 w3wp.exe 프로세스는확장자가 asp 인파일에대해해당파 일내부에포함된스크립트언어를처리할수있다. 테스트를위해다음과같은내용으로 mycalc.asp 파일을만들어 C:\inetpub\wwwroot 에저장한후 mycalc.asp <!DOCTYPE html> <html> <head> 98 3 부 _ 닷넷응용프로그램

</head> <title> 구구단 </title> <body> <% Dim number, i number = Request.QueryString("n") %> <b><% Response.Write(number) %> 단 </b><br /> <br /> <% For i = 1 to 9 %> <%=number%> * <%=i%> = <%=number * i%><br /> <% Next %> </body> </html> 코드에사용된언어는 Visual Basic 스크립트다. 지면관계상이언어를자세하게설명할수는없지만 ASP 예제는더이상나오지않으므로 C# 의 For 문과비교해서흐름만이해하고넘어가자. 또한이파일을비스타운영체제이상에서 C:\inetpub\wwwroot 폴더에저장하려면보안문제로인해저장이안된다. 그런경우에는 wwwroot 폴더에현재로그인한사용자계정으로쓰기권한을부여하거나메모장프로그램을관리자권한으로실행시켜서저장해야한다. 웹브라우저의주소표시줄에 http://localhost/mycalc.asp?n=5 를입력해서방문하면그림 12.59 처럼 5 단에해당하는구구단이출력된다. 12. 프로젝트유형 99

그림 12.59: asp 로구구단출력 자세히보면일반적인 HTML 태그가있는파일인데, VBScript 코드를넣는부분은 <%, %> 로둘러싸여있는것을알수있다. 이제웹브라우저의주소표시줄에입력된 URL에서물음표다음에나오는 n의값을다른숫자로대체해보자. 그럼 mycalc.asp 파일안에서 VBScript로작성한 Request. QueryString 메서드를이용해구한 n의값에그결과가반영되고 For 루프를돌아구구단결과가그에맞게바뀐다. EXE를만들어야하는 CGI보다는확장자만 asp로바뀐텍스트파일에스크립트언어를추가해서동적콘텐츠를구성하는편이확실히쉽다는것을알수있다. 조금더편의사항을추가해보자. 이예제에 <FORM /> 태그를추가하면사용자에게서입력값을받게할수도있다. MyCalc.asp <!DOCTYPE html> <html> <head> </head> <title> 구구단 </title> <body> <form action="./mycalc.asp"> 단 : <input type="text" name="n" /> <input type="submit" value=" 보기 " /><br /> </form><br /> <% Dim number, i 100 3 부 _ 닷넷응용프로그램

number = Request.QueryString("n") if number <> 0 then %> <b><% Response.Write(number) %> 단 </b><br /> <br /> <% For i = 1 to 9 %> <%=number%> * <%=i%> = <%=number * i%><br /> <% Next End if %> </body> </html> 변경사항을저장하고다시웹브라우저로방문하면그림 12.60 처럼사용자입력을스크립트에서받 아처리한다. 그림 12.60: <Form /> 입력값을 asp 스크립트로처리 이동작이바로여러분이사용하는웹사이트의핵심기능에해당한다. 이코드와함께 6.8 데이터베 이스 절에서배운데이터베이스처리를곁들이면사용자로그인, 게시판에글쓰기, 덧글달기등의기 능을제공하는 웹응용프로그램 이만들어지는것이다. 12. 프로젝트유형 101

ASP는내부언어로 VBScript 또는자바스크립트를사용하는데, 닷넷프레임워크가나오면서마이크로소프트는기존의 ASP 처리방식을근간으로닷넷언어를사용할수있는 ASP.NET을내놓는다. 기본설치된 IIS에 ASP.NET을활성화하려면그림 12.58에서 ASP.NET 으로시작하는항목을선택해서설치해야한다. 화면에서는 ASP.NET 3.5, ASP.NET 4.5 가있는데, IIS 관리자사용법에능숙하다면개별적으로선택해서설치해도되지만여기서는편의성을위해모두선택해서설치한다. 설치가완료되면 mycalc.asp 파일을 ASP.NET의확장자인 aspx에맞춰 mycalc.aspx로복사해서테스트해보자. 웹브라우저로방문해보면여전히기존의 ASP 스크립트가동작하는것을확인할수있는데, ASP.NET이기존의 ASP에대한하위호환성을제공하기때문이다. 물론언어를닷넷으로변경하는것도가능하다. 즉, aspx 파일에서 VBScript를걷어내고그자리에 C# 언어를사용할수있다. MyCalc.aspx <%@ Page language="c#" %> <!DOCTYPE html> <html> <head> </head> <title> 구구단 </title> <body> <form action="./mycalc.aspx"> 단 : <input type="text" name="n" /> <input type="submit" value=" 보기 " /><br /> </form><br /> <% int number, i; string txt = Request.QueryString["n"]; if (string.isnullorempty(txt) == true) return; number = Int32.Parse(txt); if (number!= 0) %> 102 3 부 _ 닷넷응용프로그램

<b><% Response.Write(number); %> 단 </b><br /> <br /> <% for (i = 1; i <= 9; i ++) %> <%=number%> * <%=i%> = <%=number * i%><br /> <% %> </body> </html> 닷넷언어는여러가지가있으므로 ASP.NET 웹페이지에서 C# 언어를사용하려면파일의첫부분에 Page 지시자 (Directive) 를통해 Language 속성으로 C# 을명시해야한다. 그이후의코드는전부기존의 VBScript 문법에서 C# 문법으로변경한것에불과하다. 이제다시웹브라우저를통해 mycalc. aspx 웹페이지를방문하면 mycalc.asp와완전히동일하게동작하는모습을확인할수있다. ASP와 ASP.NET에대한 IIS 웹서버측의처리과정이다르다는점을알아둘필요가있다. ASP의경우요청이오면스크립트엔진이매번 asp 파일을해석 (Interpret) 해서결과를출력하는반면, ASP. NET은 aspx 파일을요청한그순간하나의클래스파일로변환한후컴파일까지한다음에야처리하는구조다. 그런데어느것이더처리속도가빠를까? ASP는 asp에대한요청이들어오면매번스크립트언어를해석한다. 반면 ASP.NET은개별 aspx 파일의최초요청에대해서만 aspx 클래스 컴파일 로이어지는작업이필요하기때문에느릴수밖에없다. 하지만두번째요청부터는상황이역전된다. 여전히 ASP는스크립트언어의한계상같은요청이와도 asp 파일을해석해야하지만, ASP. NET은최초컴파일된결과를캐시로저장해두기때문에이후의요청에대해서는곧바로실행하는단계로넘어간다. 따라서전체적인웹응용프로그램의처리속도는 ASP.NET 측이월등하게빠르다. 12. 프로젝트유형 103

12.4.1 Form을이용한사용자입력처리 : GET/POST 웹브라우저에서웹서버로사용자입력을전달하는유형은크게 GET과 POST로나뉜다. 우선 GET 방식은 URL 자체에 Key=Value의쌍으로값을전달하는형식으로서, 여러개의값을전달할때는 & 기호를이용해붙인다. 1) http://localhost/test.aspx?name=anders 2) http://localhost/test.aspx?name=taylor&age=35 URL 에값을함께전달하기때문에웹브라우저의주소표시줄에직접입력하는경우도있지만, <FORM /> 태그로도 GET 방식의데이터를전송할수있다. 가령위의 2 번사례처럼 name 과 age 값 을 <FORM /> 을이용해전달하고싶다면다음과같이 HTML 태그를구성하면된다. FormGet.aspx <!DOCTYPE html> <html> <head> </head> <title>get 방식의 FORM</title> <body> <form method="get" action="formget.aspx"> 이름 : <input type="text" name="name" /><br /><br /> 나이 : <input type="text" name="age" /><br /><br /> <input type="submit" value=" 전송 " /><br /> </form><br /> </body> </html> 역시이파일을 C:\inetpub\wwwroot에저장하고웹브라우저를이용해 http://localhost/ FormGet.aspx로방문한후이름과나이에임의의값을입력하고 전송 버튼을눌러보자. 그럼그림 12.61에서볼수있듯이 <INPUT name= name />, <INPUT name= age /> 의값이 [name]=[value] 의쌍으로웹브라우저의주소표시줄에나타나는것을볼수있다. 104 3 부 _ 닷넷응용프로그램

그림 12.61: GET 방식을이용한 FORM 데이터전송 이상황을직접 TCP 소켓을열어통신한다고가정하면다음과같은패킷들이서로오가는것을확인 할수있다. 1) 최초사용자가 FormGet.aspx 파일을웹서버로호출했을때 웹브라우저 데이터전달방향 웹서버 GET /FormGet.aspx HTTP/1.1 Host: localhost 2) 웹서버의 FormGet.aspx 파일응답웹브라우저 데이터전달방향 웹서버 HTTP/1.1 200 OK Content-Type: text/html Server: Microsoft-IIS/8.0 Date: Wed, 17 Jul 2013 10:42:09 GMT Content-Length: 298 <html> <head> <title>get 방식의 FORM</title> </head>...[ 생략 ]... </html> 3) 사용자가웹브라우저에서 FORM 값을채우고 전송 버튼을눌렀을때 웹브라우저 데이터전달방향 웹서버 GET /FormGet.aspx?name=anders&age=35 HTTP/1.1 Host: localhost 12. 프로젝트유형 105

<FORM /> 내부의 <INPUT /> 데이터를이렇게 URL에붙여서전송하려면 FORM 태그의 method 속성에 GET 을설정해야하지만이는 FORM 태그의기본값이므로생략해도된다. 또한 action 속성을이용하면다른 URL로값을전달할수있다. 예를들어, action= Process.aspx 로설정하면웹브라우저는다음과같은 GET 요청으로폼데이터를전송한다. http://localhost/process.aspx?name=...&age=... 물론 action 값도생략할수있는데, 그러한경우기본적으로현재웹페이지 URL 로다시전송하게된 다. 따라서다음의세가지폼태그는완전히같은역할을한다. <form></form> <form method="get"></form> <form method="get" action="formget.aspx"></form> FORM 데이터를이처럼 GET 방식으로웹서버에전송했으면 ASP.NET 측에서는사용자가입력한이변수들의값을가져올수있어야한다. ASP.NET 웹페이지는기본적으로 System.Web. HttpRequest 타입의 Request 변수를포함하고있으며, 이것의 QueryString 컬렉션을이용해 GET 요청시전달된값을가져올수있다. string username = Request.QueryString["name"]; string userage = Request.QueryString["age"]; 해당변수의값이 QueryString 컬렉션에없으면 null 이반환된다는사실도알아두자. 이를종합해서 사용자가입력한이름과나이를화면에출력하는 FormGet.aspx 웹페이지를다음과같이만들수있 다. FormGet.aspx <%@ Page Language="C#" %> <!DOCTYPE html> <html> <% string username = Request.QueryString["name"]; string userage = Request.QueryString["age"]; %> <head> 106 3 부 _ 닷넷응용프로그램

</head> <title>get 방식의 FORM</title> <body> <form method="get" action="formget.aspx"> 이름 : <input type="text" name="name" /><br /><br /> 나이 : <input type="text" name="age" /><br /><br /> <input type="submit" value=" 전송 " /><br /> </form><br /> <% if (string.isnullorempty(username) == false && string.isnullorempty(userage) == false) %> 사용자가입력한정보 : <%=username%>, <%=userage %> <% %> </body> </html> HTTP 통신은웹브라우저측에서데이터를전송하기위해 GET 말고도 POST라는방식을하나더추가해뒀다. GET 방식은사용자가직접웹브라우저주소표시줄에입력할수있다는장점이있지만, 달리말하면보안에취약하다는의미이기도하다. 가령웹사이트에서로그인을시도하는웹페이지가다음과같이 GET 방식으로만들어졌다고가정해보자. LoginUser.aspx <%@ Page Language="C#" %> <!DOCTYPE html> <html> <head> </head> <title> 웹사이트로그인예제 </title> <body> 12. 프로젝트유형 107

<form method="get" action="userlogin.aspx"> 계정 : <input type="text" name="userid" /><br /><br /> 암호 : <input type="password" name="userpass" /><br /><br /> <input type="submit" value=" 로그인 " /><br /> </form><br /> </body> </html> 웹브라우저를통해 LoginUser.aspx 에방문하고계정, 암호를입력한후 로그인 버튼을누르면그 림 12.62 처럼사용자가입력한값이그대로보이는문제점이발생한다 ( 이순간옆에친구가있다면여 러분의계정정보를알수있다 ). 그림 12.62: 주소표시줄에보이는 FORM 전송값 이런문제를해결하려면 <FORM /> 의 method 속성값을 post 로바꾸면된다. <form method="post" > </form> 웹브라우저는 post 로설정된 Form 데이터를 GET 요청이아닌 POST 요청으로바꾸고 HTTP 헤더 다음의본문영역에 <INPUT /> 값을 Key=Value 쌍으로넣어서웹서버로보낸다. POST /UserLogin.aspx HTTP/1.1 Content-Type: application/x-www-form-urlencoded Content-Length: 25 Host: 192.168.0.85 userid=test&userpass=test 108 3 부 _ 닷넷응용프로그램

GET 요청에서는주소표시줄을통해전송됐던 userid= &userpass= 값이 HTTP 본문에담기도록바뀌었고, 그길이가 Content-Length에지정돼있다. 이처럼웹브라우저가내부적으로폼데이터를전송하기때문에 GET 요청시주소표시줄을통해민감한데이터가노출되는문제가사라진다. 아울러 ASP.NET 서버측에서는 GET 요청에대해 QueryString 컬렉션으로값을가져올수있었지만, POST로전송된데이터에대해서는 Form 컬렉션으로바꿔서구해야한다. <% string username = Request.Form["userId"]; string userage = Request.Form["userPass"]; %> GET과 POST 가운데어느것이좋다고단정할수는없다. GET은웹브라우저의주소표시줄에그결과가반영되므로해당주소만보관해둔다면이후에언제든지같은결과를얻기위해그페이지를방문할수있다는유연함이있다. 따라서각웹페이지에서요구되는특성에맞게적절한요청방식을사용하는것이중요하다. 12.4.2 파일업로드와보안 POST 요청에서또한가지주목할것은 Content-Type 헤더다. 이는 Form 태그가가진또다른속성인 enctype으로설정되는데, 기본값은 application/x-www-form-urlencoded 지만다음과같이명시해도무방하다. <form enctype="application/x-www-form-urlencoded" method="post" > </form> 대부분의경우 application/x-www-form-urlencoded 기본값을사용하지만, 딱한가지예외적으로웹브라우저에서 파일 을전송하는경우 enctype을 multipart/form-data 로바꿔야한다. 즉, 웹브라우저를통해파일을전송하는모든웹페이지는 POST 요청의 multipart/form-data 방식으로전송하는것이다. 다음은 POST 요청을이용한파일전송예제다. FileSend.aspx <%@ Page Language="C#" %> <!DOCTYPE html> <html> 12. 프로젝트유형 109

<% string temppath = @"C:\inetpub\temp"; if (this.request.files.count == 1) HttpPostedFile afile = this.request.files[0]; string filename = System.IO.Path.Combine(tempPath, afile.filename); afile.saveas(filename); %> <head> </head> <title> 파일업로드예제 </title> <body> <form method="post" action="filesend.aspx" enctype="multipart/form-data"> 첨부할파일 : <input type="file" name="myfile" /><br /><br /> <input type="submit" value=" 파일업로드 " /><br /> </form><br /> </body> </html> 사용자가첨부한파일을 <INPUT type= file /> 을통해웹서버로전송하면 ASP.NET에서는 Request 속성의 Files 컬렉션을이용해 HttpPostedFile 타입의인스턴스로다룰수있다. FileSend. aspx 예제에서는전송된파일데이터를 C:\inetpub\temp 폴더에저장하는데, 직접실습해보면다음과같은예외를만나는경우도있을것이다. Access to the path 'C:\inetpub\temp\test.txt' is denied. Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code. Exception Details: System.UnauthorizedAccessException: Access to the path 'C:\inetpub\temp\test. txt' is denied. 110 3 부 _ 닷넷응용프로그램

바로이전절에서언급한 11.3 서비스응용프로그램 절의내용을알고있어야만이오류의내용을이해할수있다. 원격지에서들어오는 HTTP 요청을처리하는웹서버는 서비스응용프로그램 의한사례다. 즉, 로그인한사용자가직접실행한프로그램이아니기때문에미리웹서버프로세스 (w3wp.exe) 에대한사용자계정이할당된다. 문제는윈도우서버는보안상의이유로기본적으로권한이축소된특별한계정으로 w3wp.exe 프로세스를실행한다는점이다. 바로그계정이 C:\ inetpub\temp 폴더에대해쓰기권한이없기때문에 FileSend.aspx 에서 Access to the path is denied 라는오류가발생하는것이다. 이오류를해결하는데는두가지방법이있다. 1. C:\inetpub\temp 폴더에 w3wp.exe 프로세스를실행한계정에대해 쓰기 권한을부여 2. w3wp.exe 프로세스의실행계정을 C:\inetpub\temp 폴더에쓰기권한을가진것으로교체 보안상의이유로인해보통첫번째방법이선호된다. 따라서업로드되는파일을저장하기위한용도로사용되는폴더는그림 12.63에서보는바와같이별도로윈도우탐색기를이용해 w3wp.exe의기본사용자계정을대표하는 IIS_IUSRS 를추가해 수정 (Modify) 권한을허용해야한다. 그림 12.63: IIS_IUSRS 계정으로쓰기권한설정 이렇게변경하고나면 FileSend.aspx의파일업로드가정상적으로수행되고, c:\inetpub\temp 폴더에사용자가선택한파일이저장되는것을확인할수있다. 파일업로드를구현할때한가지주의할점이있다면업로드되는파일의저장경로가웹응용프로그램의루트및그아래가돼서는안된다는것이다. 예를들어, 파일의저장경로를 C:\inetpub\wwwroot 로설정한다면악의적인목적을가진사용자가임의의코드를 aspx 웹페이지에담아웹서버측에전송할수있고, 따라서언제든웹브라우저로실행할수있는상태가된다. 설상가상으로이렇게만들어진웹응용프로그램의 w3wp.exe 실행권한이 Local SYSTEM 으로할당돼있다면해당웹서버가크래커에의해완전히장악되는것은시간문제에불과하다. 12. 프로젝트유형 111

사실웹서버에서 Access denied 가발생하는거의모든원인은 w3wp.exe를실행하는계정의약한권한때문이다. 이로인해발생하는설정이귀찮을수도있지만그렇다고해서 w3wp.exe를 Local SYSTEM 과같은막강한권한을가진계정으로실행하는것은최대한심사숙고해야한다. 본문에서예를든것처럼파일업로드폴더를웹루트폴더로지정하는것과같은실수를개발자는언제든할수있고, 이런실수로인한피해를최소화할수있는마지막보루가바로 w3wp.exe의실행권한이기때문이다. 12.4.3 상태관리콘솔, 윈폼, WPF와같은데스크톱용응용프로그램으로프로그래밍공부를처음시작하는경우일부프로그래머들이 ASP.NET 웹폼응용프로그램을만들면서겪게되는공통적인문제가하나있다. 이는대부분웹서버와웹브라우저가 HTTP를이용해통신한다는근본적인환경상의차이를이해하지못해서발생하는문제점이다. 데스크톱응용프로그램들은그림 12.64처럼한대의 PC에서 EXE가실행되는동안사용되는모든변수의상태가메모리에저장돼유지된다. 그림 12.64: 데스크톱응용프로그램의실행구조 사용자컴퓨터 myapp.exe 상태유지 RAM 반면웹응용프로그램은그림 12.65 에서보는바와같이클라이언트측의웹브라우저로부터전달된 요청이처리되는순간클래스가서버측메모리에올라오고 HTML 콘텐츠를생성해서반환한다음메 모리에서삭제된다. 그림 12.65: 웹응용프로그램의실행구조 사용자컴퓨터 서버컴퓨터 웹브라우저 HTTP 통신 웹서버 ASPX 클래스 RAM 112 3 부 _ 닷넷응용프로그램

이차이점을이해하기위해제곱근을구하는 sqrt.aspx 웹페이지를다음과같이만들어보자. sqrt.aspx <%@ Page Language="C#" %> <!DOCTYPE html> <html> <% string text = Request.QueryString["inputValue"]; double result = 0; if (string.isnullorempty(text) == false) int number = Int32.Parse(text); result = Math.Sqrt(number); %> <head> </head> <title> 제곱근구하는예제 </title> <body> <form method="get" action="sqrt.aspx"> 숫자 : <input type="text" name="inputvalue" /><br /><br /> <input type="submit" value=" 전송 " /><br /> </form><br /> <% if (result!= 0) %> 제곱근 : <%=result%> <% %> </body> </html> 12. 프로젝트유형 113

작성된예제페이지를웹브라우저로방문하면그림 12.66 처럼동작하는것을확인할수있다. 그림 12.66: 폼을 submit 한경우 그런데그림 12.66에서뭔가아쉬운부분이있지않을까? 전통적인윈도우폼응용프로그램을다뤄본개발자라면폼이전송된후에보여지는화면의 숫자 : 란에도당연히 5 가출력되리라생각할수있다. 실제로이프로그램이윈도우폼응용프로그램이었다면그렇게동작했을테지만, 웹응용프로그램에서는이것이불가능하다. 그이유를알고싶다면웹브라우저동작을자세하게살펴볼필요가있다. 우선 전송 버튼이눌린경우웹브라우저는웹서버로 TCP 연결을맺고 GET 요청을전송한다. 웹서버는해당 GET 요청에따라 aspx 웹페이지를처리하고그결과를 HTML 태그가담긴문자열로반환한다. 응답을받은웹브라우저는방금전까지화면에보여진내용을모두지우고, 새롭게받은 HTML 태그내용을기반으로화면을다시구성한다. 여기서중요한것은웹브라우저입장에서보면폼을전송하기전에보여준 HTML 내용과폼을전송한후에새롭게보여지게될 HTML 내용과는어떠한연관성도없다는점이다. 이처럼 HTTP 통신은기본적으로상태관리가되지않는 (Stateless) 방식이다. 만약상태관리를하고싶다면 (Stateful) HTTP 통신규약범위내에서제공되는수단을최대한활용해개발자가직접구현해야한다. 예를들어, sqrt.aspx의경우 FORM 처리가된후숫자값에사용자가입력한값을보여주려면다음과같은코드를추가해야한다. 114 3 부 _ 닷넷응용프로그램

sqrt.aspx [ 생략 ] <% string text = Request.QueryString["inputValue"]; [ 생략 ] %> [ 생략 ] <form method="get" action="sqrt.aspx"> 숫자 : <input type="text" name="inputvalue" value="<%=text%>" /><br /><br /> [ 생략 ] </html> 변경된 sqrt.aspx 페이지를웹브라우저로방문하면사용자가입력했던숫자값이그대로제곱근이구해진화면에서도입력상자에보이는것을확인할수있다. 웹응용프로그램을만드는일이어렵거나자잘한반복작업이많이필요한주요요인이바로 상태관리 에있다. HTTP 통신자체가상태를관리하지않는프로토콜인데, 개발자가상태관리를하는코드를일일이만들어야하는시점부터귀찮은작업이시작된다. 12.4.4 ASP.NET 웹폼응용프로그램지금까지설명한 aspx 웹페이지는 ASP.NET을 ASP처럼사용한방식이었다. 이것의장점은기존 ASP 웹페이지를 ASP.NET으로빠르게이전할수있다는것인데, 엄밀히말해서일반적인닷넷프로그래머는잘사용하지않는방식이다. 왜냐하면 ASP.NET은마치 윈도우폼 응용프로그램을다루듯이웹응용프로그램을만들수있는 웹폼 (web form) 을지원하기때문인데, 이방식을활용하면좀더빠르게웹사이트를제작할수있다. 웹폼개발의생산성향상이높은데에는비주얼스튜디오개발도구와잘통합돼있다는이유도한몫한다. 따라서이번절을실습하려면반드시비주얼스튜디오가설치돼있어야한다. 한가지문제가있다면여러분이지금까지실습한 Visual Studio Express 버전으로는웹폼개발을할수없다 12. 프로젝트유형 115

는것이다. 2.4 비주얼스튜디오개발환경 절을통해설치한 Visual Studio Express 2012는데스크톱용응용프로그램을개발하는용도로서정식명칭은 Visual Studio Express 2012 for Windows Desktop 이다. 이버전에는웹응용프로그램을작성하는기능이없기때문에실습을위해서는아래경로에서 Visual Studio Express 2012 for Web 버전을내려받아설치해야한다. Microsoft Visual Studio Express 2012 for Web ; http://www.microsoft.com/en-us/download/details.aspx?id=30669 Visual Studio Express가아닌 Professional 이상의경우기본적으로웹프로젝트지원기능이포함돼있으므로별도로웹개발용버전을설치하지않아도된다. 위의링크에서 다운로드 버튼을누르면 vns_full.exe 와 VS2012_WebExp_enu.iso 파일을선택할수있다. 가상 CD-ROM 응용프로그램을이용해 ISO 파일을마운트하는방법을알고있다면 VS2012_WebExp_enu.iso 파일을내려받아설치하고, 그렇지않다면 vns_full.exe 파일만내려받아설치하면된다. 일단설치프로그램이실행되면그과정은 2.4 비주얼스튜디오개발환경 절에설명한방법과동일하다. 설치가완료되면시작프로그램에 VS Express for Desktop 외에 VS Express for Web 이라는아이콘이하나더생긴다. 이프로그램을실행해이장의내용을실습하면된다. 그럼첫번째 ASP.NET 웹폼응용프로그램을만들어보자. VS Express for Web 프로그램을실행하고 파일 (FILE) / 새프로젝트 (New Project) 를선택하면그림 12.67과같은화면이나타나고좌측의 설치됨 (Installed) / 템플릿 (Templates) / Visual C# / 웹 (Web) 범주에들어가면보이는우측목록의 ASP.NET 빈웹응용프로그램 (Empty Web Application) 을선택한다. 116 3 부 _ 닷넷응용프로그램

그림 12.67: ASP.NET 웹폼프로젝트선택 기본생성된프로젝트를보면웹프로그램을위한어셈블리가참조됐고 Web.config 파일이추가된정도다. web.config은 5.2.3 응용프로그램구성파일 : app.config 절에서설명한 app.config과이름만다를뿐같은역할을한다. 이제웹폼을하나추가해보자. 솔루션탐색기에서프로젝트를마우스오른쪽버튼으로누르고 추가 (Add) / 새항목 (New Item) 을선택하면 웹폼 (Web Form) 항목이보이는데, 파일명을 Calc.aspx 로변경해서추가한다. 웹폼파일이생성되면비주얼스튜디오는그림 12.68처럼보일것이다. 그림 12.68: 웹폼추가 12. 프로젝트유형 117

설명할내용이많지만우선솔루션탐색기에새롭게포함된세파일을먼저보자. Calc.aspx 파일에 는 HTML 소스코드가포함돼있다. 이전에실습한 MyCalc.aspx 파일과 Calc.aspx 의역할은완전히 동일하다. 실제로 calc.aspx 상단에보면 Page 지시자가다음과같이설정된것을볼수있다. <%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Calc.aspx.cs" Inherits="WebApplication1.Calc" %> Language 값이 C# 으로기본설정돼있기때문에 Calc.aspx에포함된 HTML 소스에도 <%, %> 기호를이용해직접 C# 소스코드를추가할수있다. 생소한속성인 CodeBehind 값이 Calc.aspx.cs라고설정돼있고, 이는그림 12.69와같이솔루션탐색기에포함된 Calc.aspx 의하위항목에묶여있는 Calc.aspx.cs를가리킨다. 그림 12.69: 솔루션탐색기에도나오는 CodeBehind 파일 이파일을편집창에서확인하면예제 12.21 의내용을볼수있다. 예제 12.21: Calc.aspx.cs 파일의내용 using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.UI; using System.Web.UI.WebControls; namespace WebApplication1 public partial class Calc : System.Web.UI.Page 118 3 부 _ 닷넷응용프로그램

protected void Page_Load(object sender, EventArgs e) 현재만들어진기본상태그대로프로젝트를빌드하면 /bin 폴더아래에 WebApplication1.dll 어셈블리가만들어진다. 해당 DLL 파일에는웹프로젝트에포함된각종 *.aspx.cs 파일들이컴파일되어들어간다. 즉, Calc.aspx.cs 파일은컴파일되면 WebApplication1.dll로통합된다. Calc.aspx의 Page 지시자에 Inherits 값이 WebApplication1.Calc 를가리키고있다는점을주목하자. ASP.NET은이처럼 CodeBehind와 Inherits가지정된 aspx 웹페이지의요청이들어오면다음과같은순서로작업을처리한다. 1. Inherits로지정된타입을상속받아 aspx 자체를클래스로만들어생성한다. 즉, Calc.aspx.cs에정의됐던 Calc 타입을상속받는 Calc.aspx 클래스가별도로만들어진다. 2. aspx 클래스를컴파일한다. 3. 컴파일된 aspx 타입을생성하고웹폼의요청을전달해실행한다. 4. 웹폼이반환한 HTML 응답을웹브라우저로전송한다. Calc.aspx.cs에정의된 Calc 타입이 System.Web.UI.Page를상속받았다는사실도알아두자. Page 타입은웹페이지처리와관련된모든기반코드를담고있다. 보통웹폼개발자는 Page_Load 이벤트처리기에서각웹페이지요청시처음실행될코드를넣어둔다. 이는마치윈도우폼에서 Form 타입의 Load 이벤트를정의한것과유사한역할을한다. 이제다시그림 12.68로돌아가 Calc.aspx 편집창의하단영역에있는세개의탭을보자. 그림 12.68 에서는기본적으로 소스 탭이선택돼있는데, 이는편집창전체영역을 HTML 텍스트로보여주는역할을한다. 나누기 (Split), 디자인 (Design) 탭을각각눌러보면어떤역할을하는지금방알수있다. 말그대로나누기탭은편집영역을반으로나눠상단은 HTML 소스, 하단은해당 HTML 소스가웹브라우저에표시되는모습의디자인창을보여준다. 그리고 디자인 탭은편집영역전체를디자인모드로바꾼다. 간단하게웹폼을어떻게이용할수있는지실습해보자. 이전에만든 MyCalc.aspx 구구단과동일하게 Calc.aspx 웹폼을작성해볼텐데, 우선비주얼스튜디오의도구상자 (Toolbox) 로부터 TextBox 12. 프로젝트유형 119

와 Button 하나를추가하는것으로시작할수있다. 그림 12.70 과같이디자인창으로끌어다놓거나, 아니면 HTML 소스창의 <form > 과 </form> 사이에놓아도된다. 그림 12.70: 웹폼에 Button, TextBox 를추가 어느쪽에추가했든지변경된 calc.aspx 파일을저장하고나면 HTML 소스와디자인창의내용이일 치하는것을확인할수있다. 결과적으로 Calc.aspx 의 HTML 소스에추가된내용은다음과같다. <%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Calc.aspx.cs" Inherits="WebApplication1.Calc" %> <!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml"> <head runat="server"> <title></title> </head> <body> <form id="form1" runat="server"> 120 3 부 _ 닷넷응용프로그램

<asp:textbox ID="TextBox1" runat="server"></asp:textbox> <asp:button ID="Button1" runat="server" Text="Button" /> </form> </body> </html> 표준 HTML 태그외에 asp 접두사와함께 runat= server 옵션이붙은확장태그는오직 ASP.NET 에서만인식되는데, 이를가리켜 ASP.NET 웹폼컨트롤 이라한다. 여기서사용된 <asp:textbox />, <asp:button /> 의경우에는 System.Web.UI.WebControls 네임스페이스에정의된 TextBox, Button 컨트롤타입에각각대응된다. 아울러태그에포함된 ID의값이 TextBox1과 Button1로설정된것을보자. runat= server 옵션과함께설정되는 ID들은 Calc.aspx.designer.cs 파일에반영되므로해당파일을열어보면각 ID에대응하는변수가선언돼있음을확인할수있다. namespace WebApplication1 public partial class Calc protected global::system.web.ui.htmlcontrols.htmlform form1; protected global::system.web.ui.webcontrols.textbox TextBox1; protected global::system.web.ui.webcontrols.button Button1; TextBox1, Button1 외에 form1도포함돼있는데, 마찬가지로 aspx 파일에는 <form id= form1 runat= server /> 로정의돼있기때문에 designer.cs 파일에도함께존재한다. 기본적으로 Calc.aspx에추가된모든 <asp: /> 확장태그는 Calc.aspx.designer.cs 파일에반영된다고보면된다. designer.cs 파일내에정의된 Calc 타입에 partial 예약어가적용돼있다는것도눈여겨볼필요가있다. 따라서 designer.cs의내용은컴파일시에 Calc.aspx.cs에포함된 Calc 타입과합쳐진다. 이러한구조는윈도우폼응용프로그램에서다룬그림 12.5: Form1.Designer.cs의폼디자인과매우유사하다. 정리하자면 aspx 파일에추가된 <asp: /> 웹폼컨트롤은 designer.cs 파일에반영되고, 이파일의클래스는 aspx.cs에정의된클래스와부분 (partial) 클래스의관계에놓여있기때문에 Calc.aspx.cs의코드에서 aspx에정의된웹폼컨트롤의 ID를직접접근하는것이허용된다. 게다가당연히 HTML의 ID 값을바꾸면 Calc.aspx.designer.cs의소스코드에있는변수명도함께바뀐다. 12. 프로젝트유형 121

이뿐만아니라심지어이벤트처리기구조도윈도우폼과유사하게구현돼있다. 웹폼디자인화면에서 <asp:button /> 에해당하는버튼컨트롤을마우스로두번누르면 Calc.aspx.cs에는예제 12.22 와같이 Click 이벤트처리기코드가추가된다. 따라서사용자가웹브라우저에서버튼을누르면웹서버측으로요청이전달되고, Calc 타입이생성된후 Button1_Click 이벤트처리기가실행된다. 물론사용자가입력한 TextBox1의내용은 Calc.aspx.designer.cs에정의됐던그변수명 (TextBox1) 의 Text 속성값을이용해가져올수있다. 예제 12.22: 웹폼버튼의이벤트처리기 using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.UI; using System.Web.UI.WebControls; namespace WebApplication1 public partial class Calc : System.Web.UI.Page protected void Page_Load(object sender, EventArgs e) protected void Button1_Click(object sender, EventArgs e) int number = Int32.Parse(TextBox1.Text); 계속해서구구단의내용을 HTML 로출력하기위해도구상자에서 Literal 항목을웹폼에추가해보 자. <%@ Page Language="C#" AutoEventWireup="true" [ 생략 ] <body> <form id="form1" runat="server"> <asp:textbox ID="TextBox1" runat="server"></asp:textbox> <asp:button ID="Button1" runat="server" Text="Button" /> 122 3 부 _ 닷넷응용프로그램

<br /> <asp:literal ID="Literal1" runat="server"></asp:literal> </form> </body> </html> 마지막으로 Button 이눌렸을때 Literal1 영역에구구단의내용이출력되는코드를다음과같이작성 할수있다. using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Web; using System.Web.UI; using System.Web.UI.WebControls; namespace WebApplication1 public partial class Calc : System.Web.UI.Page protected void Page_Load(object sender, EventArgs e) protected void Button1_Click(object sender, EventArgs e) int number = Int32.Parse(TextBox1.Text); StringBuilder sb = new StringBuilder(); for (int i = 1; i < 10; i++) sb.appendformat("0 * 1 = 2<br />", number, i, number * i); Literal1.Text = sb.tostring(); // HTML 텍스트를 Literal1 영역에출력 12. 프로젝트유형 123

확인을위해편집창에 Calc.aspx를띄운상태에서 F5 키를눌러실행해보자. 그럼웹브라우저가실행되면서자동으로 http://localhost:[ 임의의포트번호 ]/Calc.aspx 주소로이동하게되고곧바로동작을테스트해볼수있다. 이번에는 ASP 방식으로만들었던또다른예제인 sqrt.aspx도 ASP.NET 웹폼으로만들어보자. 그림 12.68에서설명한것처럼 sqrtform.aspx 웹폼을추가하고, 아래의컨트롤을추가한다. 표 12.11: 제곱근을구하는웹폼의컨트롤속성 웹폼컨트롤 변경된속성 값 TextBox ID txtnumber Button ID btncalc Label ID lblresult Text ( 빈문자열 ) 이를반영한 aspx 파일은다음과같다. <%@ Page Language="C#" AutoEventWireup="true" CodeBehind="sqrtform.aspx.cs" Inherits="WebApplication1.sqrtform" %> <!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml"> <head runat="server"> <title> 제곱근구하는웹폼예제 </title> </head> <body> <form id="form1" runat="server"> <div> 숫자 : <asp:textbox ID="txtNumber" runat="server"></asp:textbox><br /><br /> <asp:button ID="btnCalc" runat="server" Text=" 전송 " /><br /><br /> <asp:label ID="lblResult" runat="server" Text=""></asp:Label> </div> </form> </body> </html> 124 3 부 _ 닷넷응용프로그램

비주얼스튜디오디자인창에서 <asp:button /> 영역을마우스로두번클릭한다. 그러면 sqrtform. aspx.cs 파일이열리면서 btncalc_click 이벤트처리기가추가되고, 여기에제곱근을구하는코드를 작성한다. using System; namespace WebApplication1 public partial class sqrtform : System.Web.UI.Page protected void Page_Load(object sender, EventArgs e) protected void btncalc_click(object sender, EventArgs e) int number = Int32.Parse(txtNumber.Text); lblresult.text = " 제곱근 : " + Math.Sqrt(number); 코드를실행해서결과를확인해보자. 이전에 상태관리 까지구현한 sqrt.aspx의마지막코드예제와완전히동일하게실행되는것을볼수있다. 이쯤에서기존의 ASP 방식으로만든 MyCalc.aspx / sqrt.aspx 웹페이지와 Calc.aspx / sqrtform. aspx 웹폼페이지의차이점을비교해보자. 어떤형식이든상관없이여러분은원하는동작이구현된웹페이지를만들수있었지만 ASP 방식의구현에는 HTML과 C# 소스코드가뒤섞여있는반면 ASP. NET 웹폼으로구현할때는 HTML과 C# 소스코드가분리됐다는특징이있다. 전자의경우를가리켜스파게티코드라고하는데, 임의의변경을손쉽게할수있다는장점은있지만웹응용프로그램의규모가커질수록프로그램을유지보수하기가어려워진다는것과디자이너와개발자의협업을거의불가능하게만들어버린다는단점이있다. 일반적으로 ASP.NET 응용프로그램이라고하면 웹폼 을이용한프로그램을의미하며, 실제로대부분의 ASP.NET 프로그래머들이 웹폼 을이용해웹사이트를만든다. 12. 프로젝트유형 125

12.4.5 배포및서비스비주얼스튜디오에서 F5를눌러실행한인터넷익스플로러의주소표시줄을보면 http:// localhost:[ 포트번호 ]/ aspx 와같은형식으로돼있다. 비주얼스튜디오는편의상현재컴퓨터에 IIS가설치돼있느냐와상관없이웹응용프로그램을실행할수있게 IIS의간편버전인 IISExpress. exe 프로세스를통해 ASP.NET 응용프로그램을호스팅한다. ASP.NET 측면에서봤을때 IIS와 IISExpress 간의차이점은거의없지만한가지주의해야할점이있다면 프로세스실행권한 이다르다는것이다. IIS는기본적으로축소된권한으로실행되지만, IISExpress는현재로그인한사용자계정의권한을따르므로보안과관련된작업을했을때 IISExpress 상에서는정상적으로동작하던것이 IIS에서동작시킬때는오류가발생할수있다. 이런차이를미리방지하려면비주얼스튜디오에서의웹프로젝트실행을 IISExpress가아닌 IIS 상에서동작하도록바꿔야한다. 그러자면당연히해당컴퓨터에는 IIS 서비스가미리설치돼있어야하고, 여러분이만든 ASP.NET 웹프로젝트가생성된폴더를그림 12.56에서설명한 IIS 웹사이트의 실제경로 로지정해야한다. 일반적으로프로젝트파일 (.csproj) 이있는폴더가기준이되는데, 이경로가 D:\temp\WebApplication1\WebApplication1 이라면그림 12.56의 실제경로 란에이값을그대로넣으면된다. 그런다음, 그림 12.71처럼비주얼스튜디오에서웹프로젝트의속성창을열고 웹 (Web) 범주에서 사용자지정웹서버사용 (Use Custom Web Server) 옵션을선택한후 서버 (Server) Url 을 http://localhost 로지정해야한다. 그림 12.71: IISExpress 에서 IIS 로호스팅환경을전환 126 3 부 _ 닷넷응용프로그램

이렇게변경하고 F5 를눌러실행해본다. 윈도우비스타이후의운영체제를사용하는독자라면로 컬관리자 (Administrator) 계정으로로그인하지않은경우 UAC( 사용자계정제어 : User Access Control) 로인해다음과같은오류메시지가나타날수있다. 웹서버에서디버깅을시작할수없습니다. 웹서버프로세스를디버깅할권한이없습니다. 웹서버와동일한사용자계정으로실행하거나관리자권한이있어야합니다. Unable to start debugging on the web server. You do not have permissions to debug the web server process. You need to either be running as the same user account as the web server, or have administrator privilege. 이는웹애플리케이션이수행될 w3wp.exe는비주얼스튜디오의실행프로세스인 devenv.exe와는다른윈도우세션 (Session) 에서실행되기때문에일반사용자계정권한으로는 F5를눌러디버깅상태로열수없기때문이다. 이문제를해결하려면비주얼스튜디오를 관리자권한 으로실행한다. 비주얼스튜디오가 관리자권한 으로실행됐다면 F5 키를눌러다시실행해보자. 그림 12.67과같은식으로프로젝트를생성했다면그림 12.72와같은오류가발생할것이다. 그림 12.72: HTTP 403.14 오류 이런오류가발생하는원인은 http://localhost/ 로지정된주소때문이다. 명시적으로 http:// localhost/calc.aspx 처럼 aspx 파일경로까지넣어준다면오류없이잘실행된다. 그런데여기서한가지의문이생길수있다. 예를들어, 다음 (Daum) 같은웹사이트를방문하게되는경우보통웹브라우저에 http://www.daum.net 이라고입력할뿐그다음의페이지경로까지지정하지는않는다. 그이유는 IIS에설정된 기본문서 (default document) 라는옵션때문이다. IIS 웹서버는페이지경 12. 프로젝트유형 127

로가지정되지않은경우자동으로루트폴더에서 default.htm, default.asp, index.htm, index. html, iisstart.htm, default.aspx의순서로파일이있는지검색하고그중에하나라도있다면먼저검색된순으로웹브라우저로전송하는기능이있다. 따라서일반적으로는 ASP.NET 웹프로젝트에 default.aspx 웹폼을추가하고사용자가가장먼저봐야할내용을그안에채워넣는것이관례다. 여기서는테스트를위해 default.aspx 웹폼을새롭게추가하고 HTML 내용을이번절에서실습한웹페이지로이동할수있게다음과같이작성한다. default.aspx <%@ Page Language="C#" AutoEventWireup="true" CodeBehind="default.aspx.cs" Inherits="WebApplication1._default" %> <!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml"> <head runat="server"> <title></title> </head> <body> <form id="form1" runat="server"> <div> <a href="calc.aspx">calc.aspx</a><br /> <a href="filesend.aspx">filesend.aspx</a><br /> <a href="formget.aspx">formget.aspx</a><br /> <a href="sqrt.aspx">sqrt.aspx</a><br /> <a href="sqrtform.aspx">sqrtform.aspx</a><br /> <a href="userlogin.aspx">userlogin.aspx</a><br /> </div> </form> </body> </html> default.aspx가마련됐으면다시비주얼스튜디오에서 F5 키를눌러실행해보자. 웹브라우저에서는 http://localhost 만주소표시줄에입력했지만이번에는아무런오류없이 default.aspx의내용이출력되는것을확인할수있다. 개발자컴퓨터에서정상적으로개발을마쳤으면이제실제로서비스할수있는웹서버에배포할차례다. 일단, 여러분의웹프로젝트를릴리즈 (Release) 모드로빌드한다. 128 3 부 _ 닷넷응용프로그램

디버그 / 릴리즈빌드의차이점은 '5.2.4 디버그빌드와릴리즈빌드 절에서설명한바있다. 다음으로개발자컴퓨터에있는웹프로젝트폴더를서비스가운영될웹서버의 IIS 웹사이트가지정한루트폴더에복사한다. 여기서유의해야할점은웹프로젝트에있는모든파일이복사될필요는없다는것이다. 예를들어, aspx.cs 파일들은모두빌드되어 /bin 폴더에 DLL로통합됐기때문에보안상웹서버측에복사하지않을것을권장한다. 파일복사를할때이런파일을일일이빼고복사하는것은귀찮은작업이기때문에보통 robocopy.exe와같은프로그램을이용해복사하는배치 (batch) 파일을만들어둔다. 예를들어, 웹서버의 IP가 192.168.0.85이고, 웹사이트의루트폴더가 wwwroot 라는이름으로공유된경우다음과같은배치파일을만들어명령행에서실행한다. robocopy D:\temp\WebApplication1\WebApplication1 \\192.168.0.85\wwwroot /S /XF *.cs *.csproj *.csproj.user 위의명령을실행하면 robocopy 프로그램은첫번째인자 (D:\temp\WebApplication1\ WebApplication1) 로전달한경로의내용을두번째인자 (\\192.168.0.85\wwwroot) 로지정된폴더로복사한다. 이때 /S 옵션이지정돼있으므로첫번째인자에포함된모든하위폴더도함께복사하지만, /XF 옵션다음에지정된파일들은복사에서제외한다. 즉, 확장자가.cs,.csproj,.csproj. user인파일은복사에서제외되어웹애플리케이션운영에꼭필요한파일만복사된다. 그런데이책을보는대부분의독자가웹서비스를운영할수있는전용서버를사용하는경우는거의없을것이다. 그런분들을위해집에서사용하는컴퓨터를이용해여러분이만든실습사이트를서비스하는방법을설명하겠다. 맨먼저할일은집에서쓰는컴퓨터에 IIS 웹서버를설치하고여러분이개발한웹사이트를배포하는것이다. 그런다음명령행에서 ipconfig을실행하거나 예제 6.32: 현재컴퓨터에할당된 IP 주소출력 코드를실행하면컴퓨터에설정된 IP 주소를구할수있다. 예를들어, 집컴퓨터의 IP 주소가 192.168.1.2라고가정했을때웹브라우저에서 http://192.168.1.2 를입력해서정상적으로웹응용프로그램이동작하는지확인한다. 이제부터가장중요한설정작업이시작된다. 컴퓨터에서사용중인 IP 주소가공용 IP인지, 사설 IP 인지확인해야한다. 6.7.4 System.Net.Dns 절에서이를구분하는방법을설명했으니다시한번확인한다. 컴퓨터가공용 IP로돼있다면, 지금당장친구에게전화를걸어웹브라우저를실행한후 12. 프로젝트유형 129

http://[ 공용IP]/ 를입력해보라고한다. 그럼여러분이작성한 default.aspx의내용이친구의웹브라우저에나타날것이다. 하지만대부분의경우 192.x.x.x 영역에해당하는사설 IP가할당돼있을것이므로아직친구에게말해서는안된다. 그런대로아쉽기는하지만집안에컴퓨터가두대이상있다면다른컴퓨터에서 http://[ 사설IP] 로웹브라우저에입력하면 default.aspx의내용이보일것이다. 사설 IP가사용된주요원인은그림 6.25에서설명한것처럼액세스포인트같은공유기장비가연결돼있기때문이다. 이러한경우 KT, LG 또는 SK텔레콤등의인터넷서비스제공자 (ISP) 측에서부여한공용 IP를공유기장비에서가져가버리고, 여러분의컴퓨터처럼공유기에연결된모든기기에는사설 IP가할당된다. 그래도방법은있으니걱정하지말자. 일반적인공유기에는관리자홈페이지가제공되고그화면을통해액세스포인트에할당된공용 IP를구할수있다. 예를들어, 국내에많이보급된 iptime 네트워크공유기는그림 12.73처럼 http://192.168.1.254라는주소를통해관리자설정홈페이지에접근할수있고첫번째화면에서공용 IP 주소를확인할수있다. ( 그림에서는 외부 IP 주소 로표현돼있고그값은 175.194.20.191로설정돼있다.) 그림 12.73: 공유기의관리자홈페이지를이용한공용 IP 주소확인

다른회사의공유기를사용중이라면공유기를구매했을때보관해둔사용설명서를펼쳐보면각공유기마다제공되는고유관리자페이지로들어가는방법이설명돼있을것이다. 설령사용설명서를잊어버렸더라도실망할필요는없다. 공유기를만든업체의홈페이지에들어가면설명서가제공될것이므로그곳에서내려받으면된다. 아직친구에게이주소를알려줘서는안된다. 왜냐하면 http://175.194.20.191로접속하더라도해당 IP 주소는공유기에할당돼있기때문에집컴퓨터까지 TCP 연결이되지않으므로서비스할수없기때문이다. 그럼어떻게해야할까? 역시이문제를공유기측에내장된기능으로해결할수있다. 공유기는외부에서들어오는요청을공유기에연결된내부컴퓨터로전달하는 NAT(Network Address Translation) 기능을제공한다. iptime의경우그림 12.74처럼좌측의트리에서 고급설정 / NAT/ 라우터관리 / 포트포워드설정 메뉴에서공용 IP에대해특정포트 (port) 로들어오는요청을내부 IP로전달하도록설정할수있다. 그림 12.74: 내부컴퓨터로포트포워드설정 그림 12.74 화면에서는공용 IP로 80포트를통해들어오는요청을공유기에연결된컴퓨터중에서 192.168.1.2 내부 IP 주소를가진컴퓨터로전달하도록설정한예를보여준다. 물론이설정은여러분의상황에맞게해야한다. 여기까지설정했으면이제당당하게친구에게 IP 주소를알려주고여러분이만든홈페이지를자랑해보자. 아쉬운점이있다면여러분의웹응용프로그램을서비스하는주소가 IP로돼있다는것이다. 게다가인터넷서비스제공자로부터부여받은공용 IP는언제회수되어또다른공용 IP가다시부여될지알수없는 유동 IP 에속한다. 공유기를밤에꺼두고그다음날다시켜면거의대부분은새로운공용 IP 가할당돼있을것이므로여러분의집컴퓨터를이용해홈페이지를개설하는것은아직은무리다. 이 12. 프로젝트유형 131

러한유동 IP 환경에서도홈페이지를변함없이운영하고싶다면반드시도메인이름을구매해야한다. 국내에서는가비아 (https://www.gabia.com/) 등의업체를통해도메인명을등록할수있으며, 종류에따라 2년에 38,000원 ~ 82,000원정도의비용이든다 ( 이후로갱신할때마다비용이든다 ). 필자가운영하는도메인 (sysnet.pe.kr) 도이런식으로구매해서 2년마다연장하며비용을지출하고있다. 도메인을구매했으면이제공용 IP와도메인을연결해야한다. 연결에대한설정은도메인을구매한업체측의홈페이지에서제공하므로그화면에들어가구매한도메인과여러분이가진공용 IP를함께설정하면되지만, 여전히같은문제가되풀이된다. 앞에서도언급했듯이인터넷서비스제공자로부터받은가정용 IP는변경될수있으므로도메인과연결해봤자지속적으로서비스를할수는없다. 바로이때필요한것이 Dynamic DNS 서비스다. 국내에서무료로 DDNS 서비스를제공하는곳은 디앤에스에버 (https://kr.dnsever.com/) 같은곳이있다. 이곳에간단하게회원가입을하고아래페이지에설명된대로설정하고나면여러분이구매한 DNS 명으로언제든지집에있는컴퓨터로연결하는것이가능하다. 다이나믹 DNS 사용법 ; http://kr.dnsever.com/index.html?selected_menu=aboutddns 다이나믹 DNS의원리는간단하다. 도메인명과연결될 IP 주소를서비스하는네임서버를 dnsever 업체측에서관리하게만든다음, 집컴퓨터에는 dnsever에서제공하는프로그램을설치하면된다. 그프로그램은컴퓨터의유동 IP가바뀔때마다 dnsever의네임서버에통보하고도메인명을최신 IP 주소로매핑해서서비스해준다. 정리이절에서배운내용이웹응용프로그래밍의전부는아니다. 쿠키 (Cookie), HTML5, 각종자바스크립트라이브러리, 웹폼과다른구조로웹사이트를만들수있는 ASP.NET MVC 방식등을비롯해배워야할것은무궁무진하게많다. 그렇지만그것들을모두배우고나서뭔가를만들어보려다가는자칫지루한시간을보내다가지칠수도있다. 혹시생각해본웹서비스가있다면지금당장시행착오를겪어가면서만들어가는것도분명의미가있다. 가령, 블로그정보를수집해양질의정보를제공하는웹사이트를만든다고가정해보자. 전세계의블로그주소를수집해그내용을가져오는웹로봇 (Robot) 을 11.3 서비스응용프로그램 절에서배운서비스형식의프로그램으로만들수있다. 그프로그램은 HttpWebRequest 타입을 132 3 부 _ 닷넷응용프로그램

이용해끊임없이블로그주소 (RSS) 를수집하고그안의글을내려받아지정된하드디스크영역에쌓게만든다. 그다음 웹사이트 를하나만들고로봇프로그램이쌓아둔데이터를화면에범주별로보여주는기능을만들수있다. 핵심기능이완료되면서서히서비스고도화작업을해나가면된다. 예를들어, 6.8 데이터베이스 절의코드를참고해회원정보를데이터베이스와연동할수있게확장하는것도가능하다. 집에서간단하게만든서비스가인기를끌어서비스를확장해야할시기가되면클라우드서비스를제공하는업체중에서하나를골라서비스를그곳으로이전할수도있다. 물론이쯤되면서비스를통해수익을고민해야하는단계까지온것이다. 하지만여러분이만든서비스가성공하지못했다고해서낙담할필요는없다. 그렇게서비스를운영하는전체적인실습과정자체가여러분의개발자인생에크나큰힘을실어줄것이기때문이다. 엄밀히말해서웹응용프로그래밍은 HTML 태그만을주고받는매우간단한유형의개발영역에속한다. 하지만쉽다고해서웹응용프로그래밍의중요성을평가절하해서는안된다. 인터넷의발달과함께웹은거대한생태계를구성하게됐고, 그규모는지금도폭발적으로성장하고있다. 기술기반이웹개발과는전혀다른임베디드 (Embedded) 영역에서도이제는웹과연동하려는노력이이뤄지고있다. 말그대로, 웹과연결되지않은기술은생존자체가위협받는시기가돼버린것이다. 앞으로어떤영역의개발자가될지는알수없으나, 한가지분명한것은웹을항상여러분의곁에두고응용할수있는여력은남겨두는것이좋다는것이다. 12.5 윈도우폰응용프로그램 아이폰의성공으로모바일폰은또하나의개발플랫폼으로주목받고있다. 윈도우운영체제를만든마이크로소프트에서도윈도우운영체제의모바일버전을개발했고, 이를기반으로노키아 (Nokia) 와같은제조업체에서윈도우폰을출시하고있다. 안드로이드 / 아이폰과비교했을때윈도우폰의시장점유율은상대적으로낮지만윈도우폰응용프로그램은나름대로장점을가지고있다. 쉬운개발환경 비주얼스튜디오와잘통합된윈도우폰개발환경은모바일응용프로그램의제작을다른모바일폰에비해상대적으 로쉽게만들어준다. 12. 프로젝트유형 133

XAML + C# WPF 응용프로그램을만들면서배웠던 XAML 기술로윈도우폰응용프로그램을만들수있기때문에기존의 WPF 개발자에게는진입장벽이낮다. 또한 C# 언어로개발할수있으므로기존의닷넷프레임워크에서배운기술의연장선에서응용프로그램을제작할수있다. 윈도우폰프로그래밍이실제로얼마나쉬운지한번체험해보자. 우선아래의경로에서현재사용중 인운영체제에해당하는윈도우폰 SDK 를내려받아설치한다. 윈도우비스타 /7 사용자 : Windows Phone SDK 7.1 ; http://www.microsoft.com/en-us/download/details.aspx?id=27570 Windows Phone SDK 7.1.1 Update ; http://www.microsoft.com/en-us/download/details.aspx?id=29233 윈도우 8 사용자 Windows Phone SDK 8.0 ; http://www.microsoft.com/en-us/download/details.aspx?id=35471 현재사용중인비주얼스튜디오가 Express 버전이라면 Windows Phone SDK 8.0 은새롭게 Visual Studio Express for Windows Phone 개발도구를설치한다. 그외에 Visual Studio Professional, Premium, Ultimate 버전이설치돼있는경우에는기존비주얼스튜디오에플러그인방식으로확장되기때문에이전과다름없이비주얼스튜디오를이용해개발할수있다. 설치과정은이전에 2.4 비주얼스튜디오개발환경 절에서설명한방법과동일하다. 설치를마치면새롭게설치된 Visual Studio Express for Windows Phone 을실행하고, 파일 (FILE) / 새프로젝트 (New Project) 메뉴를선택하면그림 12.75와같이다양한유형의윈도우폰을위한프로젝트템플릿을볼수있다. 134 3 부 _ 닷넷응용프로그램

그림 12.75: 윈도우폰용프로젝트템플릿 여기서는가장간단한유형으로실습할것이므로 Windows Phone App 을선택하고프로젝트이름을적절하게입력한후확인 (OK) 버튼을누른다. 그러면 (8.0 버전의 SDK를설치한경우 ) 대상이되는윈도우폰 OS 버전을물어볼수있는데, 어차피에뮬레이터에서만구동할것이므로아무거나선택해도된다 ( 여기서는 8.0을선택하고진행한다 ). 물론윈도우폰을사용중이라면에뮬레이터에서테스트한후윈도우폰으로직접배포하는것도가능하다. 프로젝트가생성되고나면그림 12.76 과같은프로젝트폴더구조가나타난다. 12. 프로젝트유형 135

그림 12.76: 솔루션탐색기 - 윈도우폰프로젝트 표 12.12: 윈도우폰프로젝트의기본항목설명 폴더파일설명 Properties AppManifest.xml 비주얼스튜디오에서자동으로관리하므로거의변경할필요가없다. WMAppManifest.xml 윈도우폰응용프로그램의전반적인설정을관리한다. 예를들어, 아이콘 을변경하거나지원하는해상도를설정할수있다. Assets 프로그램에사용되는자원을넣어둔다. 기본적으로는프로그램의아이콘 파일을포함한다. ( 프로젝트 ) App.xaml WPF 의 App.xaml 과유사 LocalizedStrings.cs MainPage.xaml XAML에서사용되는문자열들의다국어지원을위해기본포함된코드파일, 실제로변경되는파일은 /Resources/AppResources.resx이므로이파일자체는거의변경할필요가없다. WPF의 MainWindow.xaml과유사 WPF 프로그램을만드는것과비슷하기때문에 MainPage.xaml 파일을비주얼스튜디오에서여 는것으로시작하면된다. 그럼비주얼스튜디오는그림 12.77 과같이영역을둘로나눠왼쪽에서는 XAML 의디자인을보여주고, 오른쪽에서는 XAML 소스코드를편집할수있다. 136 3 부 _ 닷넷응용프로그램

그림 12.77: MainPage.xaml 편집및디자인화면 WPF 프로젝트에서편집했던것과동일하게이번에도어느쪽영역을편집하든변경사항을저장하면다른영역으로반영된다. MainPage.xaml의내용은주석을제외하면예제 12.23에서볼수있는것과같이간단하게구성돼있다. 예제 12.23: 윈도우폰앱기본예제 MainPage.xaml // ================== MainPage.xaml ============================ <phone:phoneapplicationpage x:class="phoneapp1.mainpage" [ 생략 ] FontFamily="StaticResource PhoneFontFamilyNormal" FontSize="StaticResource PhoneFontSizeNormal" Foreground="StaticResource PhoneForegroundBrush" SupportedOrientations="Portrait" Orientation="Portrait" shell:systemtray.isvisible="true"> <Grid x:name="layoutroot" Background="Transparent"> 12. 프로젝트유형 137

<Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <StackPanel x:name="titlepanel" Grid.Row="0" Margin="12,17,0,28"> <TextBlock Text="MY APPLICATION" Style="StaticResource PhoneTextNormalStyle" Margin="12,0"/> <TextBlock Text="page name" Margin="9,-7,0,0" Style="StaticResource PhoneTextTitle1Style"/> </StackPanel> </Grid> <Grid x:name="contentpanel" Grid.Row="1" Margin="12,0,12,0"> </Grid> </phone:phoneapplicationpage> 윈도우폰에서의 XAML은 WPF에서사용된 XAML과구조는같다. 예제 12.23의경우에도 WPF에서배운각종컨트롤을유사하게가져다배치할수있다. WPF와어느정도비슷한지알수있게예제 12.11의시계기능을그대로윈도우폰앱으로구현해보자. 우선 DataBinding을위해예제 12.24와같이 XAML의루트객체에 DataContext를설정하고, 시간을출력할 TextBlock 컨트롤을 Grid 컨트롤아래에추가한다. 예제 12.24: 윈도우폰앱만들기 - 시계 // ================== MainPage.xaml ============================ <phone:phoneapplicationpage x:class="phoneapp1.mainpage" [ 생략 ] DataContext="Binding RelativeSource=RelativeSource Self" > <Grid x:name="layoutroot" Background="Transparent"> [ 생략 ] <StackPanel x:name="titlepanel" Grid.Row="0" Margin="12,17,0,28"> <TextBlock Text="Timer" Style="StaticResource PhoneTextNormalStyle" Margin="12,0"/> <TextBlock Text="Current Time" Margin="9,-7,0,0" Style="StaticResource PhoneTextTitle1Style"/> </StackPanel> 138 3 부 _ 닷넷응용프로그램

<Grid x:name="contentpanel" Grid.Row="1" Margin="12,0,12,0"> <TextBlock Name="lblTime" Text="Binding Path=Time" /> </Grid> </Grid> </phone:phoneapplicationpage> 기존의예제 12.11과비교해서예제 12.24에서달라진것이있다면단지시간출력을위한 Label 컨트롤을 TextBlock으로바꾼것밖에없다. 아쉽게도윈도우폰의제한된자원으로인해 WPF와동등한수준의컨트롤이모두제공되지는않기때문에이런부분은고려해야한다. 시계기능을구현하기위한나머지작업으로 MainPage.xaml.cs에예제 12.11과동일한변경사항을적용하면된다. using System; using System.ComponentModel; using System.Windows.Threading; using Microsoft.Phone.Controls; namespace PhoneApp1 public partial class MainPage : PhoneApplicationPage, INotifyPropertyChanged DispatcherTimer _timer; string _time; public string Time get return _time; set _time = value; OnPropertyChanged("Time"); public virtual void OnPropertyChanged(string propertyname) if (PropertyChanged == null) return; 12. 프로젝트유형 139

PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); public MainPage() InitializeComponent(); _timer = new DispatcherTimer(); _timer.tick += _timer_tick; _timer.interval = new TimeSpan(0, 0, 1); // 1초마다발생 _timer.start(); void _timer_tick(object sender, EventArgs e) this.time = DateTime.Now.ToLongTimeString(); public event PropertyChangedEventHandler PropertyChanged; 이제 F5 키를눌러완성된윈도우폰시계앱을실행해보자. 잠시후그림 12.78처럼윈도우폰에뮬레이터가실행되고이어서우리가작성한시계앱이구동된다. 이처럼 WPF의개발경험을윈도우폰을위한앱에서도그대로활용할수있다. 단지 XAML의구현규모로봤을때 WPF가전체기능 (Full-set) 이면윈도우폰의 XAML은 WPF의부분집합으로보면된다. 12.5.1 페이지단위의응용프로그램구현 WPF에서는 Window 타입을상속받은 MainWindow가진입점이지만, 윈도우폰프로젝트에서는 PhoneApplicationPage를상속받은 MainPage 타입이진입점이다. 폰의특성상화면의크기와충전식배터리문제로인해윈도우폰에서는 Window 자원이배제되고, 한화면을꽉채우는페이지 (Page) 의개념이나온것이다.

윈도우폰앱하나는 1 개이상의페이지로구성되는데, 각페이지는 MainPage 와마찬가지로화면전 체를사용한다. 실습을위해새롭게예제프로젝트를생성하고, 그림 12.79 와같이새항목추가를선 택하고 Windows Phone Portrait Page 를 Second.xaml 이라는이름으로생성한다. 그림 12.79: 새로운페이지추가 앱이실행된후최초로보여질페이지는 /Properties/WMAppManifest.xml의 Navigation Page 에의해결정되는데, 기본값은 MainPage.xaml로설정돼있다. 따라서현재상태에서앱을실행하면 Second.xaml이아닌 MainPage.xaml의내용이화면에먼저보인다. 페이지간의이동을하고싶다면 NavigationService 타입의 Navigate 정적메서드를이용한다. 예제 12.25에서는 MainPage. xaml과 Second.xaml의화면에버튼을추가하고그버튼의이벤트처리기에서서로의페이지로전환하는방법을보여준다. 예제 12.25: 페이지전환 // ================== MainPage.xaml ============================ <phone:phoneapplicationpage x:class="phoneapp1.mainpage" [ 생략 ] DataContext="Binding RelativeSource=RelativeSource Self" > <Grid x:name="layoutroot" Background="Transparent"> [ 생략 ] 12. 프로젝트유형 141

<StackPanel x:name="titlepanel" Grid.Row="0" Margin="12,17,0,28"> <TextBlock Text="Timer" Style="StaticResource PhoneTextNormalStyle" Margin="12,0"/> <TextBlock Text="Current Time" Margin="9,-7,0,0" Style="StaticResource PhoneTextTitle1Style"/> </StackPanel> <Grid x:name="contentpanel" Grid.Row="1" Margin="12,0,12,0"> <Button Content="Second.xaml로이동 " Click="Button_Click" /> </Grid> </Grid> </phone:phoneapplicationpage> // ================== MainPage.xaml.cs =========================== using System; using System.Windows; using System.Windows.Navigation; using Microsoft.Phone.Controls; namespace PhoneApp1 public partial class MainPage : PhoneApplicationPage public MainPage() InitializeComponent(); private void Button_Click(object sender, RoutedEventArgs e) NavigationService.Navigate(new Uri("/Second.xaml", UriKind.Relative)); // ================== Second.xaml =========================== <phone:phoneapplicationpage x:class="phoneapp1.second" [ 생략 ] SupportedOrientations="Portrait" Orientation="Portrait" shell:systemtray.isvisible="true"> 142 3 부 _ 닷넷응용프로그램

<Grid x:name="layoutroot" Background="Transparent"> [ 생략 ] <Grid x:name="contentpanel" Grid.Row="1" Margin="12,0,12,0"> <Button Content="MainPage.xaml로이동 " Click="Button_Click" /> </Grid> </Grid> </phone:phoneapplicationpage> // ================== Second.xaml.cs =========================== using System; using System.Windows; using System.Windows.Navigation; using Microsoft.Phone.Controls; namespace PhoneApp1 public partial class Second : PhoneApplicationPage public Second() InitializeComponent(); private void Button_Click(object sender, RoutedEventArgs e) NavigationService.Navigate(new Uri("/MainPage.xaml", UriKind.Relative)); 프로그램을실행해서확인해보면버튼을누를때마다페이지가전체화면에걸쳐바뀌면서교체되는모습을확인할수있다. 페이지가모여서하나의모바일폰앱을구성한다는것은어찌보면 HTML 페이지로구성된웹애플리케이션과유사하다. 실제로 Navigate 메서드에전달되는문자열도일반적인 URL과유사한형식을띠고있어페이지간에정보를전달하기위한목적으로쿼리문자열을사용하는것도가능하다. 예제 12.26에서는예제 12.25의 MainPage.xaml에 TextBox를하나더추가하고사용자가입력한문자열을쿼리문자열을통해 Second.xaml에전달한다. 12. 프로젝트유형 143

예제 12.26: QueryString을이용한페이지간의정보전달 // ================== MainPage.xaml =========================== <phone:phoneapplicationpage x:class="phoneapp1.second" [ 생략 ] SupportedOrientations="Portrait" Orientation="Portrait" shell:systemtray.isvisible="true"> <Grid x:name="layoutroot" Background="Transparent"> [ 생략 ] <Grid x:name="contentpanel" Grid.Row="1" Margin="12,0,12,0"> <Grid.RowDefinitions> <RowDefinition Height="80" /> <RowDefinition Height="*" /> </Grid.RowDefinitions> <TextBox x:name="txtinput" Grid.Row="0" /> <Button Content="Second.xaml" Click="Button_Click" Grid.Row="1" /> </Grid> </Grid> </phone:phoneapplicationpage> // ================== MainPage.xaml.cs =========================== using System; using System.Windows; using System.Windows.Navigation; using Microsoft.Phone.Controls; namespace PhoneApp1 public partial class MainPage : PhoneApplicationPage public MainPage() InitializeComponent(); private void Button_Click(object sender, RoutedEventArgs e) NavigationService.Navigate(new Uri("/Second.xaml?p1=" + txtinput.text, UriKind.Relative)); 144 3 부 _ 닷넷응용프로그램

// ================== Second.xaml =========================== <phone:phoneapplicationpage x:class="phoneapp1.second" [ 생략 ] SupportedOrientations="Portrait" Orientation="Portrait" shell:systemtray.isvisible="true"> <Grid x:name="layoutroot" Background="Transparent"> [ 생략 ] <Grid x:name="contentpanel" Grid.Row="1" Margin="12,0,12,0"> <Grid.RowDefinitions> <RowDefinition Height="80" /> <RowDefinition Height="*" /> </Grid.RowDefinitions> <TextBlock x:name="lbltext" Grid.Row="0" /> <Button Content="MainPage.xaml로이동 " Click="Button_Click" Grid.Row="1" /> </Grid> </Grid> </phone:phoneapplicationpage> // ================== Second.xaml.cs =========================== using System; using System.Windows; using System.Windows.Navigation; using Microsoft.Phone.Controls; namespace PhoneApp1 public partial class Second : PhoneApplicationPage public Second() InitializeComponent(); 12. 프로젝트유형 145

protected override void OnNavigatedTo(NavigationEventArgs e) string inputvalue; NavigationContext.QueryString.TryGetValue("p1", out inputvalue); lbltext.text = inputvalue; base.onnavigatedto(e); private void Button_Click(object sender, RoutedEventArgs e) NavigationService.Navigate(new Uri("/MainPage.xaml", UriKind.Relative)); MainPage.xaml에추가된입력상자의값을 MainPage.xaml.cs의버튼클릭이벤트처리기에서마치웹사이트를방문하는것처럼쿼리문자열을전달하고있다. Second.xaml.cs에서쿼리문자열로전달된값을구하는방법도재미있다. 윈도우폰앱은페이지가로드될때 OnLoad에해당하는 OnNavigatedTo라는특별한메서드가호출된다. 따라서페이지단위로초기화해야하는코드가있다면 OnNavigatedTo 메서드에서할수있는데, 쿼리문자열을받아오는것도그에해당하는작업중하나가될수있다. NavigationContext 타입의 QueryString 정적속성을이용해전달된값을구하는데, 그방법은웹브라우저의 QueryString 규칙과동일하다. 부가적으로 OnNavigatedTo와반대되는 OnNavigatedFrom 메서드도있다는점을알아두자. 이메서드는페이지를떠나는순간에발생하며, 일반적으로는사용자가해당페이지내에서입력한값을저장하는코드를넣어둔다. 쿼리문자열을이용한방법은윈도우폰앱에서제공되는페이지간의정보전달방법중하나다. 사실쿼리문자열로정보를전달하는것은간단한유형의문자열에한해서만유용할뿐복잡한데이터를전달하기에는적합하지않다. 윈도우폰앱은웹사이트와는달리독자적인자신만의고유한프로세스 (EXE) 공간을가지기때문에변수를통해정보를전달할수도있다. 예를들어, App.xaml.cs에정의된 App 타입에정적변수를두고이를공유해서전달할수있다. 다음예제를보자. 146 3 부 _ 닷넷응용프로그램

예제 12.27: App 타입의공유변수를이용한페이지간의정보전달 // ================== App.xaml.cs =========================== namespace PhoneApp1 public partial class App : Application // 데이터공유를위한정적변수추가 static Dictionary<string, object> _datastore = new Dictionary<string, object>(); public static Dictionary<string, object> DataStore get return _datastore; // [ 생략 ] // ================== MainPage.xaml.cs =========================== using System; using System.Windows; using System.Windows.Navigation; using Microsoft.Phone.Controls; namespace PhoneApp1 public partial class MainPage : PhoneApplicationPage public MainPage() InitializeComponent(); // 이번예제에서는값설정을 OnNavigatedFrom 메서드에서설정 // 이메서드는페이지를벗어나기바로전에호출된다. protected override void OnNavigatedFrom(NavigationEventArgs e) App.DataStore["p1"] = txtinput.text; base.onnavigatedfrom(e); 12. 프로젝트유형 147

private void Button_Click(object sender, RoutedEventArgs e) // 여기서값을설정하는것도가능 // App.DataStore["p1"] = txtinput.text; NavigationService.Navigate(new Uri("/Second.xaml", UriKind.Relative)); // ================== Second.xaml.cs =========================== using System; using System.Windows; using System.Windows.Navigation; using Microsoft.Phone.Controls; namespace PhoneApp1 public partial class Second : PhoneApplicationPage public Second() InitializeComponent(); protected override void OnNavigatedTo(NavigationEventArgs e) lbltext.text = App.DataStore["p1"] as string; base.onnavigatedto(e); private void Button_Click(object sender, RoutedEventArgs e) NavigationService.Navigate(new Uri("/MainPage.xaml", UriKind.Relative)); 예제 12.27의기능은예제 12.26과완전히동일하다. 하지만같은기능을 QueryString이아닌 App 타입의정적변수를통해구현하는차이만있을뿐이다. QueryString과프로세스메모리의정적변수를이용한방법말고도디스크를이용하는방법도있다. 일반적인윈도우운영체제에서는하드디스크가있다면드라이브문자 ( 예 : C 드라이브 ) 를통해전체파 148 3 부 _ 닷넷응용프로그램

일시스템을접근할수있게허용하지만, 윈도우폰의경우해당앱만이접근할수있는제한적인파일시스템을제공한다. 이름하여 격리된저장소 (isolated storage) 라고하며, 각앱마다저장소가할당되어서로다른앱끼리는저장소공유가불가능하다. 격리된저장소를이용하는방법중하나는 System.IO.IsolatedStorage 네임스페이스에서제공되는 IsolatedStorageFileStream 타입을이용하는방법이다. 이를이용해예제 12.27을바꾸면예제 12.28과같다. 예제 12.28: 격리된저장소를이용한페이지간의정보전달 // ================== MainPage.xaml.cs =========================== using System; using System.Windows; using System.Windows.Navigation; using Microsoft.Phone.Controls; using System.IO.IsolatedStorage; using System.Text; namespace PhoneApp1 public partial class MainPage : PhoneApplicationPage public MainPage() InitializeComponent(); protected override void OnNavigatedFrom(NavigationEventArgs e) using (IsolatedStorageFileStream fs = new IsolatedStorageFileStream("p1.data", System.IO.FileMode.Create, IsolatedStorageFile.GetUserStoreForApplication())) byte [] buffer = Encoding.UTF8.GetBytes(txtInput.Text); fs.write(buffer, 0, buffer.length); fs.close(); base.onnavigatedfrom(e); 12. 프로젝트유형 149

private void Button_Click(object sender, RoutedEventArgs e) NavigationService.Navigate(new Uri("/Second.xaml?p1=" + txtinput.text, UriKind. Relative)); // ================== Second.xaml.cs =========================== using System; using System.Windows; using System.Windows.Navigation; using Microsoft.Phone.Controls; namespace PhoneApp1 public partial class Second : PhoneApplicationPage public Second() InitializeComponent(); protected override void OnNavigatedTo(NavigationEventArgs e) using (IsolatedStorageFile fs = IsolatedStorageFile.GetUserStoreForApplication()) bool exists = fs.fileexists("p1.data"); if (exists == true) var file = fs.openfile("p1.data", System.IO.FileMode.Open); byte [] buffer = new byte[file.length]; file.read(buffer, 0, buffer.length); lbltext.text = Encoding.UTF8.GetString(buffer, 0, buffer.length); base.onnavigatedto(e); 150 3 부 _ 닷넷응용프로그램

private void Button_Click(object sender, RoutedEventArgs e) NavigationService.Navigate(new Uri("/MainPage.xaml", UriKind.Relative)); 이전에다뤘던 6.5 파일 절의 System.IO.FileStream 사용법과비교해서생성자부분을제외하고는격리된저장소에파일을생성하고읽는작업은별반다르지않다. 격리된저장소를이용하면해당데이터가디스크에영구적으로저장되기때문에응용프로그램이종료된이후에도그값을재사용할수있다는특징이있다. 대신디스크를사용하기때문에많은데이터를다루는경우속도가느려질수있다는단점도있다. 그렇게되면앱은잠시동안반응하지않게되어사용자경험 (UX: User Experience) 이떨어진다. 이러한경우에는부가적으로비동기메서드를이용해데이터를저장 / 복구하는것도고려해볼수있다. 12.5.2 제한적인멀티태스킹윈도우폰환경의특징가운데빠뜨릴수없는것이바로 작은배터리용량 이다. 한손에들어가는폰의구조상배터리크기는작아질수밖에없고, 결국노트북과는비교도안되는저용량배터리를장착하게된다. 게다가항상켜져있는상태라서배터리소모가더빠르다. 이로써폰을위한운영체제는최대한배터리를절약하는방안을강구해야했고결정적으로 PC용운영체제의특징인 다중작업 (Multi-Tasking) 을배제하게된다. 즉, PC에서는게임을실행하다가도웹브라우저를띄우면게임이멈추지않은상태로웹브라우저가별도의윈도우영역을갖고실행됐지만윈도우폰에서는전체화면으로게임을실행하다가웹브라우저를실행하면이전에실행했던게임에더는 CPU 자원을할당하지않고오직현재순간에전체화면을점유하고있는앱에만 CPU 자원을할당한다. 예를들어, 사용자가윈도우폰의앱을다음과같은순서로실행한다고가정해보자. 1) " 웹브라우저앱 " 실행 2) " 홈버튼 " 클릭 3) " 홈화면 " 4) " 음악앱 " 실행 5) " 뒤로가기버튼 " 클릭 6) " 홈화면 " 12. 프로젝트유형 151

이전에도언급했듯이윈도우폰의앱은사용자에의한명시적인 종료 절차가없다. 1번에서 웹브라우저앱 을실행한사용자는 홈버튼 을클릭해 홈화면 으로이동한다. 이때 웹브라우저앱 은종료된것일까? 윈도우폰은이런경우앱을종료하지않는다. 왜냐하면사용자가 뒤로가기 버튼을이용해곧바로돌아올수있기때문이다. 그렇다고해서실행중인것도아니다. 왜냐하면웹브라우저앱에 CPU가더는할당되지않기때문이다. 이런앱의상태를 비활성화 (deactivation) 됐다고한다. 하지만 4, 5, 6번단계에서보다시피앱을실행하고 뒤로가기버튼 을이용해홈화면으로이동한경우는다르다. 이런경우는다시그앱으로들어가려면 음악앱 을명시적으로실행하는동작이수반되기때문에윈도우폰은 뒤로가기버튼 을이용해앱을빠져나가는경우는프로세스를종료해버린다. 그런데여기서한가지과정이더추가된다. 비활성화 상태에서여러다른앱을실행해메모리가부족해지면어떻게될까? 윈도우폰운영체제는이런상황이되면어쩔수없이비활성화상태의앱을강제로종료한다. 이렇게된앱을 Tombstone( 묘비가세워진상태 ) 됐다고한다. 그렇다면 tombstone 상태와 뒤로가기버튼 을이용해종료된상태가어떻게다른지의문이남는다. 이차이점이란앱을실행한과정의 이력 을토대로결정된다. 즉, 이력에없으면 종료 상태이고, 이력에있으면서종료된상태이면 tombstone 상태인것이다. 위의앱실행과정에서 1번의웹브라우저앱은아직이력에남아있으므로 6번과정에서다시한번뒤로가기를실행하면이전의웹브라우저앱이복원된다. 하지만 4번에서실행한음악앱은뒤로가기를통해완전히종료된상태이므로이력목록에남아있지않아다시음악앱아이콘을눌러실행하지않는한해당앱으로진행할수있는경로가없다. 그런데 1번과정에서실행한웹브라우저앱은 홈버튼 을누름으로써처음에는비활성화상태로되지만, 정확히어느시점에 tombstone 상태로빠질지는알수없다. 그것은운영체제에의해자원이부족하다고판단되는임의의시점이될수밖에없다. 앱개발자입장에서 비활성화 상태의앱이다시활성화되는것은아무런문제가없다. 하지만 tombstone 상태로빠진앱이다시활성화되는것은사정이다르다. 왜냐하면 tombstone의앱은사실상종료된것이기때문에메모리에보존했던데이터가모두제거된상태이므로사용자가 뒤로가기버튼 을통해 tombstone 상태의앱으로진입하면앱이완전히새롭게실행되는것과같다. tombstone을제대로이해해야만이력에남아있는앱이다시실행됐을때비정상적으로종료되는문제를막을수있다. 이문제가실제로어떻게나타날수있는지예제 12.27을통해직접체험해볼수있다. 예제 12.27을다음과같이실행하는경우를예로들어보자. 152 3 부 _ 닷넷응용프로그램

1) 예제 12.27 의앱을실행 (MainPage.xaml) 2) Second.xaml 로페이지전환 3) " 홈버튼 " 을클릭해서 " 홈화면 " 으로전환 이상태에서곧바로 뒤로가기버튼 을누르면윈도우폰은 Second.xaml이실행됐던상태의앱화면을그대로복구한다. 왜냐하면비활성화상태에있었기때문에앱프로세스가살아있었고, 따라서메모리에있던변수가그대로보존돼있었으므로앱전환이정상적으로이뤄진다. 하지만 tombstone 상태를재현하기위해부하가높은게임등을몇개실행한다음 뒤로가기버튼 을눌러 Second.xaml로돌아오면어떤일이발생할까? 그럼에도메모리가넉넉해서비활성화상태에머무르고있었다면아무런오류도발생하지않겠지만의도한대로 tombstone 상태로전환됐다면 Second.xaml.cs 파일의 OnNavigatedTo 코드에서다음과같은오류가발생한다. protected override void OnNavigatedTo(NavigationEventArgs e) lbltext.text = App.DataStore["p1"] as string; // 예외발생 base.onnavigatedto(e); 발생한예외 An exception of type 'System.Collections.Generic.KeyNotFoundException' occurred in mscorlib.ni.dll but was not handled in user code 이유는간단하다. 프로세스가종료되어 tombstone 상태이므로이전에앱이가졌던메모리상의모든데이터가제거된것이나다름없다. 그런데사용자가이력목록에만존재하는앱을 뒤로가기버튼 을통해진입했기때문에윈도우폰운영체제는해당앱을새롭게실행한후마지막으로보여졌던 Second.xaml 페이지를화면에보여주게된다. 결과적으로 OnNavigatedTo 메서드에서실행되는 App.DataStore에는더는 p1 키값을가진데이터가존재하지않기때문에예외가발생하는것이다. tombstone 상태를재현하기가쉽지않기때문에비주얼스튜디오에서는디버깅을쉽게할수있도록앱이비활성화상태로빠지면강제로 tombstone 상태로전환하는옵션을가지고있다. 윈도우폰프로젝트속성창을띄우고그림 12.80처럼 디버그 (Debug) 탭의 Tombstone upon deactivation while debugging 옵션을켜면된다. 12. 프로젝트유형 153

그림 12.80: tombstone 옵션 이렇게설정하고, F5 키를이용해디버깅상태로진입하면앱이실행되고, Second.xaml 페이지로이동한다음홈버튼을눌러비활성화상태로만들어보자. 옵션설정덕분에해당앱은곧바로 tombstone 상태로진입하게되고, 이상태에서 뒤로가기 버튼을눌러앱으로진입하면 Second. xaml.cs의 OnNavigatedTo 메서드에서예외가발생하는것을확인할수있다. 경험상안정화된윈도우폰앱을개발하려면반드시이옵션을켜고추가된페이지하나당모두 tombstone 테스트를해야만한다. 만약 tombstone 상태로빠진앱의상태를그대로다시진입하고싶다면어떻게해야할까? 답은간단하다. 메모리를사용해서는안되므로페이지상태를복원하기위해필요한데이터가있다면정적변수에보관하지말아야한다. 대신격리된저장소영역과같이프로세스종료이후에도보관 / 복원할수있는방법을택하면된다. 12.5.3 Launcher / Chooser 페이지간에데이터를연동하는방법과함께윈도우폰에서는 앱간에데이터를연동 하는방법도특별하다. 윈도우폰앱은전체화면을차지하는앱이또다른앱에의해비활성화되면 CPU 할당을전혀받을수없기때문에상호데이터를연동하는방법이모호해질수밖에없는것이다. 이런문제를해결하기위해윈도우폰앱은특정용도의앱과연동할수있는 Launcher와 Chooser라고하는라이브러리를제공한다. 간단한예를들어, 여러분이만든앱에서모바일폰의대표적인 단문메시지서비스 (SMS: Short Message Service) 를이용해야한다고가정해보자. 그러자면단문메시지를발송하는앱이실행돼야하는데, 윈도우폰앱에서는이를위해 SmsComposeTask 타입의 Launcher를제공한다. 예제 12.29에서는간단한사용법을보여준다. 154 3 부 _ 닷넷응용프로그램

예제 12.29: 모바일폰에서단문메시지를전송하는방법 // ================== MainPage.xaml.cs =========================== using System; using System.Windows; using System.Windows.Navigation; using Microsoft.Phone.Controls; namespace PhoneApp1 public partial class MainPage : PhoneApplicationPage public MainPage() InitializeComponent(); private void btnsendsms_click(object sender, RoutedEventArgs e) SmsComposeTask sms = new SmsComposeTask(); sms.body = "Hello World"; sms.show(); 이예제를실행해 SmsComposeTask 타입의 Show 메서드가호 출되면그림 12.81 처럼 Hello World 문자열이미리입력돼있는 SMS 앱이실행되는것을볼수있다. 그림 12.81: SMS 앱이 Launcher를통해활성화된모습 한가지주의할점은이상태는 SMS 앱이새롭게실행된것과전혀차이가없다는점이다. 따라서 SmsComposeTask를실행했던앱은비활성화상태에빠지고 SMS를발송하지않고 홈버튼 을눌러연속적인앱을실행하다보면 Tombstone 상태로진입할가능성도있다는점에유의해야한다. Launcher를통해원하는외부기능을실행했으면다시원래의앱으로돌아오기위해서는동일하게 뒤로가기버튼 을이용하면된다. Launcher의예를알아봤으니이제 Chooser를다뤄보자. 12. 프로젝트유형 155

Chooser 의한예로 CameraCaptureTask 타입을들수있는데, 이것을이용하면윈도우폰에서제 공되는카메라앱을실행할수있다. 예제 12.30: Camera를통해이미지를받아화면에출력 // ================== MainPage.xaml =========================== <phone:phoneapplicationpage x:class="phoneapp1.mainpage" [ 생략 ] SupportedOrientations="Portrait" Orientation="Portrait" shell:systemtray.isvisible="true"> <Grid x:name="layoutroot" Background="Transparent"> [ 생략 ] <Grid x:name="contentpanel" Grid.Row="1" Margin="12,0,12,0"> <Grid.RowDefinitions> <RowDefinition></RowDefinition> <RowDefinition></RowDefinition> </Grid.RowDefinitions> <Button Content="Capture Photo" Click="btnCapturePhoto_Click" /> <Image x:name="imgphoto" Grid.Row="1" /> </Grid> </Grid> </phone:phoneapplicationpage> // ================== MainPage.xaml.cs =========================== using System; using System.Windows; using System.Windows.Navigation; using Microsoft.Phone.Controls; namespace PhoneApp1 public partial class MainPage : PhoneApplicationPage public MainPage() InitializeComponent(); 156 3 부 _ 닷넷응용프로그램

private void btncapturephoto_click(object sender, RoutedEventArgs e) CameraCaptureTask camera = new CameraCaptureTask(); camera.completed += camera_completed; camera.show(); void camera_completed(object sender, PhotoResult e) if (e.taskresult == TaskResult.OK) BitmapImage photobitmap = new BitmapImage(); photobitmap.setsource(e.chosenphoto); imgphoto.source = photobitmap; CameraCaptureTask 타입은 Completed 이벤트를제공함으로써 Show 메서드가실행된이후카메라앱으로넘어갔을때찍힌사진데이터를넘겨받는작업을할수있다. 따라서 Completed 이벤트처리기에서는 PhotoResult 타입의 TaskResult 속성이 OK인경우, 사용자가카메라로촬영한사진데이터를 ChosenPhoto 속성으로넘겨받아처리할수있다. 예제 12.30에서는카메라앱이반환한이미지데이터를 XAML의 <Image /> 컨트롤을통해출력한다. Launcher와 Chooser는값의반환여부로구분할수있다. 예제를통해알수있듯이 Launcher에해당하는타입은다른앱을호출하는것으로끝나지만, Chooser 타입으로활성화된앱들은동작을완료한후호출자측에값을반환한다는뚜렷한차이가있다. 따라서 Chooser의경우 Launcher와비교해서 Completed 이벤트처리기를설정하는작업이하나더추가된다. 다음표에서는윈도우폰에서제공되는 Launcher와 Chooser의일부가정리돼있다. 12. 프로젝트유형 157

표 12.13: Launcher와 Chooser 종류 타입 설명 Launcher ConnectionSettingsTask 네트워크연결을변경할수있게설정앱을실행한다. EmailComposeTask 전자메일앱을실행한다. MediaPlayerLauncher 미디어플레이어앱을실행한다. PhoneCallTask 전화를걸수있는앱을실행한다. SearchTask 검색창이나오는앱을실행한다. WebBrowserTask 웹브라우저앱을실행한다. Chooser AddressChooserTask 연락처앱을실행하고선택된주소를반환한다. EmailAddressChooserTask 연락처앱을실행하고선택된전자메일주소를반환한다. PhoneNumberChooserTask 연락처앱을실행하고선택된전화번호를반환한다. PhotoChooserTask 앨범앱을실행하고선택된이미지를반환한다. SaveEmailAddressTask 연락처앱을실행하고새로운전자메일주소및그외항목을등록한다. 반환값을통해등록여부만을알수있다. SavePhoneNumberTask 연락처앱을실행하고새로운전화번호및그외항목을등록한다. 반환 값을통해등록여부만을알수있다. 정리국내시장에서차지하는윈도우폰의영향력은사실상미미한수준에불과하기때문에윈도우폰용앱을제작하는것은아직까지는그다지매력적이지않다. 그나마앱을제작하고싶다면반드시세계시장을바라보고시작하는것이좋다. 다만한가지긍정적인요소가있다면윈도우폰앱의기반기술이 XAML이라는점이다. 데스크톱응용프로그램인 WPF와윈도우 8의스토어앱 (Store App) 도 XAML을바탕으로하기때문에이중에서어느하나라도알고있다면다른두가지프로그램을만드는것도그리어렵지않다. 즉, 하나의기술이세가지서로다른응용프로그램분야 ( 데스크톱, 모바일폰, 태블릿 ) 에재사용될수있는것이다. 158 3 부 _ 닷넷응용프로그램

13 실습 파워포인트쇼제어프로그램 실습의모델이된응용프로그램은윈도우폰에서마이크로소프트오피스의파워포인트쇼를제어할 수있는프로그램으로서국내개발사인 SmartShare 에서만들었다. PRESENTER FOR WINDOWS PHONE ; http://www.ismartshare.net/portfolio/presenter/ 이프로그램을선택한이유는단순히학습을위한실습보다는실제로현실적인수준의응용프로그램이어떤기술과난이도로만들어지는가에대한감을잡는데도움될것이라생각해서였다. 윈도우폰을가지고있는독자가많지않을것이므로실습주제로적당하지않을수있으나우리가만들앱은에뮬레이터에서도충분히실습할수있으므로크게걱정하지않아도된다. 게다가실습을마치고나면안드로이드나아이폰용앱을개발해본경험이있는독자라면그리어렵지않게해당플랫폼을지원하는앱을만들수있을것이다. 이장의나머지부분에서는마치여러분이프로그램을처음으로만든다고가정하고설명을진행한다. 이를통해프로그램의개발프로세스를간접적으로체험하는데도움이되도록구성했다. 13. 실습 파워포인트쇼제어프로그램 159

13.1 앱기획 맨먼저할일은어떤앱을만들어야할지결정하는것이다. 간단한질문답변을통해이과정을완성 할수있다. 1. [ 질문 ] 어떤앱을만들것인가? [ 답변 ] PC를통해진행중인프레젠테이션쇼를모바일폰으로제어하는앱을만든다. 2. [ 질문 ] 어떤프레젠테이션응용프로그램을지원할것인가? [ 답변 ] 마이크로소프트의오피스프로그램중에파워포인트를지원한다. 또는무료프레젠테이션프로그램가운데프레지 (Prezi) 가있는데이것도지원하면좋겠다. 이이상으로더질문할필요는없다. 그이유는뭘까? 프레젠테이션쇼를제어하려면대상이되는프레젠테이션응용프로그램을제어할수있는방법을알아야한다. 그렇게하는방법이없다면이응용프로그램은애당초기획될의미가없는것이다. 따라서어떤앱이만들어져야할지결정되면다음단계로는정말그것이현실적으로타당한지신속히기술검토를해야한다. 13.2 기술검토 응용프로그램에대한기획이나왔다고해서모든앱이만들어질수있는것은아니다. 기술검토단계를거치지않으면나중에해당앱을만들수없다는판정이나왔을때심각한비용손실을입을수밖에없다. 따라서만들고자하는앱이기술적으로개발가능한지여부를앱개발초기에반드시확인해봐야한다. 물론응용프로그램의유형에따라난이도가쉬운경우라면기술검토단계를생략할수있다. 이같은결정은프로그램을만들리더급개발자가참여한가운데진행해야하며, 일단기술검토가필요하다고판단되면팀내최고의기술적인실력을갖춘팀원을뽑아서빠르게진행해야한다. 프레젠테이션쇼를제어하는앱은반드시기술검토가필요하다라고판단됐으므로이제파워포인트와프레지프로그램을대상으로쇼를제어하는방법이있는지알아내야한다. 우선파워포인트에대해알아보니외부에서접근할수있게확장모델을잘구현하고있음이밝혀졌다. PowerPoint Object Model ; http://msdn.microsoft.com/en-us/library/office/aa213568(v=office.11).aspx 160 3 부 _ 닷넷응용프로그램

게다가웹검색을통해예제도찾을수있다. How to use Automation to create and to show a PowerPoint 2002 presentation by using Visual C#.NET 2002 ; http://support.microsoft.com/kb/303718/en-us 자동화 (Automation) 라는 COM 기술을통해파워포인트를제어하는수단을제공하고있는데, 아직 은구체적으로알수없지만위의두문서를보니상당히세부적인부분까지제어할수있을것같다. 반면프레지는 Flash 를통해웹브라우저에서실행되는유형으로보이는데, 딱히외부에서제어하는 방법을찾을수는없었다. 기술검토로더시간을소비할수는없으므로일단프레지는포기하고오피 스파워포인트를대상으로앱을만들기로결정한다. 13.3 세부요구사항정의 대략적인기술검토가완료됐으면이제앞으로만들앱이갖춰야할기능을정의해야한다. 앱이도대체어떤기능을제공해야하는가에대한범위를정하는일은대단히중요하다. 최초로공개될앱이너무많은기능을제공한다면기능을구현하기위한개발기간이늘어날것이고자칫경쟁사가더일찍제품을출시해서타격을받을수있다. 반대로기능이별로없다면사용자들은해당앱이유용하지않다는선입견을받을수있고이후로개선되더라도첫인상으로결정된지배적인의견으로더이상의시장확대를기대하지못할수도있다. 따라서버전 1에서어떤기능을포함해야할지는신중하게결정해야한다. 쇼제어프로그램의요구사항을다음과같이간단하게정리했다. 표 13.1: 요구사항정리 요구사항 우선순위 1. 폰을이용해 PC 에서보여지는프레젠테이션의슬라이드를함께볼수있어야한다. 높음 2. 터치를이용해왼쪽으로밀면다음슬라이드로쇼가나타나고, 오른쪽으로밀면이전슬라이드가나타나야 한다. 높음 3. 슬라이드에적어놓은메모를함께봤으면좋겠다. 낮음 요구사항과함께우선순위를결정하는것도좋다. 또한필요하다면개별요구사항에대한기술검토를 할수도있다. 13. 실습 파워포인트쇼제어프로그램 161

13.4 응용프로그램의구조 요구사항이정리됐으므로이제본격적으로응용프로그램을구현할차례다. 하지만코드작성에앞서응용프로그램의전체적인구조 (architecture) 를구상하는것이좋다. 이를통해응용프로그램의동작구조를한눈에볼수있고아직개발되지않은상태에서도프로그램이어떤식으로동작하게될지를그려볼수있어문제에집중할수있다. 우선쇼제어를하려면두개의프로그램이있어야한다. 하나는 PC에서실행중인파워포인트를제어하는윈도우응용프로그램이고, 또다른하나는윈도우폰에서실행되어사용자가원격에서쇼를제어하는역할을한다. 이쯤에서솔루션이름과함께두응용프로그램의이름을정해보자. 표 13.2: 솔루션구조 솔루션프로젝트응용프로그램유형 OfficePresenter ShowController 윈도우폰응용프로그램 PPTShow WPF 응용프로그램 파워포인트프로그램을같은 PC 에서실행중인 PPTShow 가제어하고, PPTShow 는윈도우폰으로 부터사용자입력을받는 ShowController 의제어를받는다. 따라서동작구조는다음과같다. 그림 13.1: 응용프로그램간동작구조 PC 윈도우폰 PPTShow ShowController 파워포인트 이제다시질문을통해몇가지사항을결정해보자. 1. [ 질문 ] PC와윈도우폰간의통신은어떻게할것인가? [ 답변 ] PC 측의 PPTShow에서 TCP 서버를열고, 윈도우폰측의 ShowController에서접속하는방식이좋겠다. 2. [ 질문 ] TCP 서버 / 클라이언트를만들어직접소켓통신을하는것이정말좋을까? [ 답변 ] 아무래도소켓수준에서통신하는것은번거로우니간편하게 HTTP 통신을하는것이좋을것같다. 어차피많은데이터가오가는것은아니므로필요할때마다연결을맺고끊는 HTTP 통신이적합할듯하다. PPTShow 측에 HTTP 서버기능을넣고, ShowController 측에서는 WebClient 타입을이용해필요할때마다통신을한다. 162 3 부 _ 닷넷응용프로그램

3. [ 질문 ] 대기포트는몇번이좋을까? [ 답변 ] 큰의미는없으니그냥 5022 번으로하자. 여기까지의견이정해졌으면구조도의내용을좀더보강할수있다. 그림 13.2: 세부동작구조 PC 5022 번포트대기 윈도우폰 PPTShow HTTP 통신 ShowController 파워포인트 오피스자동화기술을이용해제어 구조가정해졌으면모든팀원들간에내용을공유하는것이좋다. 전체적인응용프로그램의구조를개발자가알고작업하는것과모르고작업하는것에는큰차이가있다. 모르고작업한다는것은협업이없다는것이고결국프로젝트후반에가서서로간의인터페이스가맞지않아잦은의견불일치로이어질수있다. 반면전체적인그림이머릿속에그려진개발자들은자신이맡은부분이결국어떤식으로상호연동될지판단할수있으므로협업을고려하면서작업을진행할수있다. 13.5 응용프로그램의동작시나리오및화면스케치 응용프로그램의동작구조가정해졌어도여전히코드를작성할수있는수준은아니다. 왜냐하면실제로응용프로그램이어떻게동작해야하는지에대해서는정해지지않았기때문에무턱대고코드먼저작성하면나중에다시작업해야하는부분들이발생할수있다. 따라서개략적인화면스케치와함께동작시나리오를작성해봄으로써응용프로그램의코드수준에서요구되는사항들을좀더자세하게이끌어낼수있다. 앞에서작성한 세부요구사항정의 를토대로실제로응용프로그램의화면동작까지정의하는과정을거치면된다. 이번에도질문답변을통해하나씩정의해보자. 1. [ 질문 ] PC 측의 PPTShow 가 HTTP 서버로동작하면클라이언트측에서접속해야할텐데, 포트는 5022 번으로결정했 지만 IP 주소는사용자상황에따라달라진다. 어떻게이주소를쉽게알수있을까? [ 답변 ] PPTShow 가실행되면 PC 측의 IP 정보를프로그램에출력하는방법으로해결하자. 13. 실습 파워포인트쇼제어프로그램 163

2. [ 질문 ] PPTShow는제어할파워포인트파일을어떻게선택할것인가? [ 답변 ] PPTShow 측에 Open 버튼을두고제어할 PPT 파일을직접선택하게하자. 3. [ 질문 ] 윈도우폰앱 (ShowController) 에서는 IP 주소를어떻게입력받는가? [ 답변 ] 그냥간단하게사용자가텍스트박스에입력하게하자. 4. [ 질문 ] 윈도우폰앱의페이지구성은어떻게할것인가? [ 답변 ] 첫화면에서는 PC측에연결하기위한 IP, 포트정보와 연결 버튼을둔다. PC 측에정상적으로연결되면페이지가바뀌고쇼를제어할수있는화면구성이나타난다. 5. [ 질문 ] 쇼를제어할수있는화면구성은어떻게할것인가? [ 답변 ] 화면을상하절반으로나누고상단에는현재쇼에나타난슬라이드화면이나타나고, 하단에는그슬라이드에포함된메모가보여지는것이좋겠다. 6. [ 질문 ] 쇼를넘기는터치작업은어떻게처리할것인가? [ 답변 ] 다행히윈도우폰용 XAML에는화면을터치로넘길수있게 Panorama 컨트롤이기본적으로제공되므로이를사용하면되겠다. 회의결과를텍스트형식으로정의하는것도좋지만이를바탕으로대략적인화면을그려볼필요가있다. 이를통해해당앱을기획한사람의의도와잘맞아떨어지는지확인할수있고, 그래야만나중에프로젝트가전혀다른방향으로흘러가는것을방지할수있다. 우선, PC 측에서파워포인트쇼를제어하는프로그램의외양을정의해보자. 그림 13.3: PPTShow 응용프로그램의외양스케치 Label 컨트롤... 컴퓨터 IP... Port 5022... 선택된 PPT 파일경로... Open Button 컨트롤 이것은회의결과를통해개발자가이해한응용프로그램의외양이다. 이를가지고다시앱기획자와 이야기를나눌수있다. 1. [ 질문 ] 컴퓨터에는 IP 주소가한개만있는가? [ 답변 ] 노트북의경우유무선어댑터가함께있으니 IP 는목록으로보여줄필요가있다. 이부분은 Label 컨트롤에서리 스트컨트롤로대체하겠다. 164 3 부 _ 닷넷응용프로그램

2. [ 질문 ] Open 버튼을누르면어떤동작이발생하는가? [ 답변 ] 파일선택창이뜨고쇼를진행할 PPT 파일을선택할수있다. 3. [ 질문 ] PPT 파일을선택 / 로드했는지알수있는상태를보여줄수는없을까? [ 답변 ] PPT 파일이선택된경우 선택된 PPT 파일경로 라벨컨트롤에해당파일의경로가출력되겠지만명시적인것을원한다면상태를알리기위한라벨컨트롤을하나더추가하겠다. 회의를마치고그결과를기존스케치에반영하면다음과같다. 그림 13.4: PPTShow 2 차스케치 목록컨트롤 Port 5022... 컴퓨터 IP 목록... Label 컨트롤... 선택된 PPT 파일경로... Open Not Loaded Button 컨트롤 당연히이결과를가지고다시기획팀과협의해야한다. 이번실습에서는여기서협의가완료된것으로가정하겠다. 다음으로윈도우폰에서실행될 ShowController 앱을스케치해보자. 회의에서결정한사항에따라이프로그램은 2개의페이지로구현하기로했다. 첫페이지에는 PPTShow에연결할수있는정보와 연결 버튼을두고, 두번째페이지는실제로쇼를제어할수있는페이지로구성한다. 그림 13.5: ShowController 페이지 1 스케치 IP Port... 컴퓨터 IP... 5022 Connect TextBox 컨트롤 TextBlock 컨트롤 Button 컨트롤

그림 13.6: ShowController 페이지 2 스케치 현재슬라이드이미지 Image 컨트롤 Panorama 컨트롤 슬라이드의메모내용 TextBlock 컨트롤 기획팀과의회의에서 페이지 1 은통과했지만, 페이지 2 에대해서는의문이제기됐다. 지난번회의에서기획팀은개발자로부터 Panorama 컨트롤 이라고하는말을듣긴했지만구체적으로이것이어떤것인지감이오지않아서질문하게된다 ( 독자여러분들도마찬가지일것이다 ). Panorama 컨트롤은윈도우폰응용프로그램에서만제공되며터치기반의인터페이스를제공한다. 그림 13.7에서보면 Panorama 컨트롤은윈도우폰의크기보다넓은콘텐츠를담는다. 현재보여지는콘텐츠에서왼쪽내용을보고싶으면터치를이용해화면을오른쪽으로밀어주면된다. 마찬가지로오른쪽내용을보고싶으면터치를통해화면을왼쪽으로밀어주는식으로보이는영역을조절할수있다. 그림 13.7: Panorama 컨트롤 따라서 ShowController 앱에서는로드된 PPT 파일의전체슬라이드를 Panorama 컨트롤에일렬로 넣어뒀다가터치를이용해화면을좌 / 우로밀면쇼로보여지는슬라이드도마찬가지로이전 / 이후슬라 이드로이동할수있게제어할수있다. 설명을다듣고난기획팀에서다음과같은질문이이어진다. 166 3 부 _ 닷넷응용프로그램

1. [ 질문 ] Panorama 컨트롤을쓰는경우, 터치동작을할때마다슬라이드를앞 / 뒤로하나씩만이동할수있는것같다. 맞는가? [ 답변 ] 맞다. 2. [ 질문 ] 내경험에비춰볼때 PPT 쇼를하다보면한참전에있는슬라이드로되돌아갈필요가있었다. Panorama 컨트롤만있는경우이작업을하려면밀어내는동작의터치를많이해야하는데이방식은다소불편한것같다. 개선할수없겠는가? [ 답변 ] 그렇다면페이지하단의영역을조금할당해서전체슬라이드목록을보여주는이미지리스트를제공하고해당이미지를선택하면곧바로관련된슬라이드로이동하게끔만들어보겠다. 3. [ 질문 ] 전체슬라이드목록을담고있는이미지리스트도터치를이용해좌 / 우로화면을밀어낼수있어야하겠다. 가능한가? [ 답변 ] 다행히윈도우폰의목록컨트롤은기본적으로터치인터페이스를지원하므로쉽게구현할수있다. 다시회의결과를반영해서 페이지 2 의스케치를보강해보자. 그림 13.8: ShowController 페이지 2 스케치의최종버전 현재슬라이드이미지 슬라이드의메모내용 Image 컨트롤 Panorama 컨트롤 TextBlock 컨트롤 전체슬라이드의이미지목록 ListBox 컨트롤 + Image 컨트롤 이화면역시최종적으로기획팀과의협의를거쳐확정해야한다. 응용프로그램을개략적으로스케치해보는단계는매우중요하다. 스케치단계를통해마치실제로서비스를사용하는상황을묘사하면서합의를이끌어낼수있기때문에코드가변경될가능성을그만큼줄일수있다. 이단계를거치지않으면프로젝트를진행할때최초의요구사항과맞지않는부분을발견할가능성이높아지고그로인해개발자가가장싫어하는코드재작업이발생할가능성이커진다. 현업에서프로젝트를진행하다보면이렇게까지협의를했는데도기본전제였던요구사항까지바뀌는사태가발생해코드를대대적으로변경해야하는경우도있다. 13. 실습 파워포인트쇼제어프로그램 167

13.6 응용프로그램개발 응용프로그램을스케치했으면이제프로그램을본격적으로개발하기시작한다. 순서로보면서 버역할을하는 PPTShow 를먼저개발하는것이좋다. 따라서 PPTShow 를마무리하고이어서 ShowController 앱개발을시작하겠다. 13.6.1 개발환경준비 OfficePresenter 응용프로그램은두개의응용프로그램 (WPF, 윈도우폰앱 ) 으로구성돼있는데, 문제는해당응용프로그램에대한프로젝트지원이비주얼스튜디오익스프레스버전에서는 for Windows Desktop 과 for Windows Phone 용으로나뉘어있다는점이다. 따라서 PPTShow WPF 응용프로그램은 for Windows Desktop 버전의비주얼스튜디오익스프레스로개발하고 ShowController 윈도우폰앱은 for Windows Phone 버전으로개발해야한다. 표 13.3: 비주얼스튜디오익스프레스의개발환경 솔루션 프로젝트 응용프로그램유형 비주얼스튜디오익스프레스 OfficePresenter ShowController 윈도우폰응용프로그램 for Windows Phone PPTShow WPF 응용프로그램 for Windows Desktop 반면상용버전의비주얼스튜디오는모든응용프로그램유형을지원하므로표 13.3처럼개발환경을나누지않아도된다. 편리하긴해도이책의독자들이익스프레스버전을주로사용한다는가정하에개발환경을나눠서진행하는쪽으로설명한다. 이와함께 PPTShow를개발하는 PC에는오피스프로그램도설치해야한다. 파워포인트쇼를제어하는것이기에당연한준비사항인데, 오피스프로그램의무료버전은없지만 60일동안한시적으로사용할수있는체험판을아래경로에서내려받아설치한다. Download Microsoft Office Professional Plus 2013 (60 일체험판 ) ; http://technet.microsoft.com/en-us/evalcenter/jj192782.aspx 168 3 부 _ 닷넷응용프로그램

13.6.2 PPTShow 개발첫단계로 PPTShow 라는이름의솔루션을만들고그안에 WPF 응용프로그램유형으로 PPTShow 프로젝트를추가한다. PPTShow의기능은크게세가지로나눌수있다. 기본적인 UI 구성 ShowController와통신하는 HTTP 서버역할 파워포인트쇼를제어하는역할 가장쉬운것부터차례대로구현을시작해보자. 13.6.2.1 UI 구성 그림 13.4 의스케치에따라 XAML 내에적절하게컨트롤을배치하자. 예제 13.1: PPTShow UI 디자인 // ======= MainWindow.xaml ======= <Window x:class="pptshow.mainwindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="PPTShow" Height="167" Width="352"> <Grid> <Grid.RowDefinitions> <RowDefinition></RowDefinition> <RowDefinition Height="35"></RowDefinition> </Grid.RowDefinitions> <Grid Margin="5"> <Grid.ColumnDefinitions> <ColumnDefinition></ColumnDefinition> <ColumnDefinition></ColumnDefinition> </Grid.ColumnDefinitions> <ListBox> </ListBox> <Grid Grid.Column="1"> <Grid.ColumnDefinitions> <ColumnDefinition></ColumnDefinition> <ColumnDefinition></ColumnDefinition> </Grid.ColumnDefinitions> 13. 실습 파워포인트쇼제어프로그램 169

<Label HorizontalAlignment="Right">Port</Label> <TextBox VerticalAlignment="Top" Height="25" VerticalContentAlignment="Center" Grid.Column="1" IsReadOnly="True" /> </Grid> </Grid> <Grid Margin="5" Grid.Row="1"> <Grid.ColumnDefinitions> <ColumnDefinition></ColumnDefinition> <ColumnDefinition Width="60"></ColumnDefinition> <ColumnDefinition Width="100"></ColumnDefinition> </Grid.ColumnDefinitions> <TextBox IsReadOnly="True" Text="Binding Path=FilePath" /> <Button Margin="5, 0, 0, 0" Grid.Column="1" Content="Open" /> <Label Margin="5, 0, 0, 0" Grid.Column="2"></Label> </Grid> </Grid> </Window> 이렇게디자인하고프로젝트를실행하면다음과같은화면을볼수있다. 그림 13.9: PPTShow 응용프로그램의 XAML 디자인 자, 그럼가장쉬운포트값을먼저채워볼텐데단순히 XAML 에직접입력하는방법도가능하지만 <TextBox VerticalAlignment="Top" Height="25" Text="5022" VerticalContentAlignment="Center" Grid.Column="1" IsReadOnly="True" /> 여기서는향후사용자가편집할것에대비해데이터바인딩을이용해처리한다. <TextBox VerticalAlignment="Top" Height="25" Text="Binding Path=Port" VerticalContentAlignment="Center" Grid.Column="1" IsReadOnly="True" /> 170 3 부 _ 닷넷응용프로그램

MainWindow 의생성자에서는 DataContext 를 this 로초기화하고데이터바인딩을위한 INotifyPropertyChanged 인터페이스와 Port 속성을구현한다. // ======= MainWindow.xaml.cs ======= using System.Windows; using System.Windows.Documents; using System.Net; using System.IO; using System.Threading; using Microsoft.Win32; using System.Windows.Threading; using System.ComponentModel; namespace PPTShow public partial class MainWindow : Window, INotifyPropertyChanged short _port; public short Port get return this._port; set if (this._port == value) return; this._port = value; OnPropertyChanged("Port"); public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged(string propertyname) if (PropertyChanged == null) return; PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); 13. 실습 파워포인트쇼제어프로그램 171

public MainWindow() InitializeComponent(); this.datacontext = this; this.port = 5022; 다음으로 IP 목록을채워넣는작업을처리한다. 물론이것도데이터바인딩을이용해처리한다. <ListBox ItemsSource="Binding Path=IPList"></ListBox> // ======= MainWindow.xaml.cs ======= using System.Windows; // [ 생략 ] using System.Collections.ObjectModel; using System.Net.Sockets; namespace PPTShow public partial class MainWindow : Window, INotifyPropertyChanged // [ 생략 ] ObservableCollection<string> _iplist; public ObservableCollection<string> IPList get return this._iplist; set if (this._iplist == value) return; this._iplist = value; OnPropertyChanged("IPList"); 172 3 부 _ 닷넷응용프로그램

public MainWindow() InitializeComponent(); this.datacontext = this; this.port = 5022; this.iplist = new ObservableCollection<string>(); PopulateIPList(this.IPList); private void PopulateIPList(ObservableCollection<string> list) IPAddress[] addrs = Dns.GetHostEntry(Dns.GetHostName()).AddressList; foreach (IPAddress ipaddr in addrs) if (ipaddr.addressfamily == AddressFamily.InterNetwork) list.add(ipaddr.tostring()); IP 주소를구하는방법은이전에소켓을다루면서설명한바있다. 위의코드는 IPList 컬렉션에값을 채울테고, XAML 에바인딩된영향으로자연스럽게 ListBox 요소에 IP 목록이출력된다. 마지막으로 Open 버튼과문서의로딩상태를보여주는 Label 컨트롤을처리해보자. <TextBox IsReadOnly="True" Text="Binding Path=FilePath" /> <Button Margin="5,0,0,0" Grid.Column="1" Click="btnOpenClicked" Content="Open" /> <Label Margin="5,0,0,0" Grid.Column="2" Content="Binding Path=Ready"></Label> PPT 파일을선택하기전에는 Ready 변수에 Not Loaded 를출력하고파일을선택한후에는 FilePath 바인딩변수에파일경로를넣고 Ready 변수의상태는 Loaded 메시지로바꾸는처리를 해준다. 13. 실습 파워포인트쇼제어프로그램 173

예제 13.2: PPT 파일선택을위한코드추가 // ======= MainWindow.xaml.cs ======= using System.Windows; // [ 생략 ] namespace PPTShow public partial class MainWindow : Window, INotifyPropertyChanged // [ 생략 ] public MainWindow() // [ 생략 ] this.ready = "Not Loaded"; // [ 생략 ] void btnopenclicked(object sender, RoutedEventArgs e) OpenFileDialog opendlg = new OpenFileDialog(); if (opendlg.showdialog() == true) string path = opendlg.filename; ProcessDocument(path); private void ProcessDocument(string path) if (File.Exists(path) == false) return; // 현재단계에서는이부분의코드는구현하지않는다. this.filepath = path; this.ready = "Loaded"; 174 3 부 _ 닷넷응용프로그램

string _filepath; public string FilePath get return this._filepath; set if (this._filepath == value) return; this._filepath = value; OnPropertyChanged("FilePath"); string _ready; public string Ready get return this._ready; set if (this._ready == value) return; this._ready = value; OnPropertyChanged("Ready"); 여기까지구현하고실행하면다음과같은화면을볼수있다. 13. 실습 파워포인트쇼제어프로그램 175

그림 13.10: PPTShow 실행화면 Open 버튼을누르고원하는 PPT 파일을선택하는것까지해보자. 그럼 PPT 파일경로가왼쪽텍 스트상자에출력되고, 상태메시지는 Loaded 로바뀌는것을확인할수있다. 기본적인 UI 구성은이정도로구현할수있다. 이어서 UI 에설정된 IP 와포트로동작하는 HTTP 서 버를구현해보자. 13.6.2.2 HTTP 서버구현 HTTP 서버는소켓을이용해구현할수있는데, 이미 6.7 네트워크통신 의 HTTP 통신 절에서서버를만드는방법을알아봤다. HTTP 통신 절에서작성한소스코드를보강해서직접 HTTP 서버기능을구현해도되지만작업시간을줄이기위해 C# 으로구현된 HTTP 서버를웹으로검색해찾아봤다. Simple HTTP Server in C# ; http://www.codeproject.com/articles/137979/simple-http-server-in-c 웹에는이처럼오픈소스로공개돼있는다양한소스코드를구할수있으므로이를적절히잘활용하는것도개발자의중요한능력중하나다. 위의소스코드를내려받아프로젝트에추가한다. 이와함께 HttpServer 클래스를상속받아다음과같이 MyHttpServer 클래스를정의한 MyHttpServer.cs 파일을프로젝트에추가한다. 176 3 부 _ 닷넷응용프로그램

// ======= MyHttpServer.cs ======= using System; using Bend.Util; using System.IO; namespace PPTShow public class MyHttpServer : HttpServer public MyHttpServer(int port) : base(port) // HttpServer 타입은 GET 요청이올때마다 // handlegetrequest 메서드를실행해준다. // 따라서이메서드에서특정 URL로요청이들어온것을감지할수있다. public override void handlegetrequest(httpprocessor p) string urltext = p.http_url; // URL에 '?' 문자이후로포함된문자열을제거한다. // 예를들어, "http://localhost:5022/test?dummy=123" 이라고요청이오면 // urltext 변수는 "http://localhost:5022/test" 문자열만포함한다. int pos = urltext.indexof("?"); if (pos!= -1) urltext = urltext.substring(0, pos); // 동작을검증할수있는테스트요청처리 if (urltext.endswith("/test") == true) p.writesuccess("text/html"); p.outputstream.write(datetime.now + ": Hello World!"); public override void handlepostrequest(httpprocessor p, StreamReader data) 13. 실습 파워포인트쇼제어프로그램 177

HTTP 웹서버는프로그램이시작될때대기를시작하면되므로 MyHttpServer 인스턴스를 MainWindow 생성자에서만들어도된다. 여기서주의할것은소켓대기는스레드를블로킹시키므로 별도의스레드를만들어 HTTP 요청을대기하게만들어야한다. // ======= MainWindow.xaml.cs ======= using System.Windows; // [ 생략 ] namespace PPTShow public partial class MainWindow : Window, INotifyPropertyChanged // [ 생략 ] MyHttpServer _listener; Thread _httpthread; public MainWindow() // [ 생략 ] SetupHttpServer(); // [ 생략 ] private void SetupHttpServer() _listener = new MyHttpServer(this.Port); _httpthread = new Thread(_listener.listen); _httpthread.isbackground = true; _httpthread.start(); 자원해제를명시하기위해 MainWindow 의 Closed 이벤트처리기를작성하고소켓을닫는것도권 장한다. 178 3 부 _ 닷넷응용프로그램

// ======= MainWindow.xaml.cs ======= using System.Windows; // [ 생략 ] namespace PPTShow public partial class MainWindow : Window, INotifyPropertyChanged // [ 생략 ] MyHttpServer _listener; Thread _httpthread; public MainWindow() // [ 생략 ] SetupHttpServer(); this.closed += new EventHandler(MainWindow_Closed); // [ 생략 ] void MainWindow_Closed(object sender, EventArgs e) ReleaseSocketResource(); private void ReleaseSocketResource() try if (_listener!= null) _listener.dispose(); catch _listener = null; try if (_httpthread!= null) 13. 실습 파워포인트쇼제어프로그램 179

_httpthread.abort(); catch _httpthread = null; 엄밀히말해서여기서의자원해제작업이반드시필요한것은아니다. 왜냐하면 MainWindow가닫히는경우프로세스가종료되므로해당프로세스에속한 Background 스레드와소켓자원은운영체제에의해모두강제로해제되기때문이다. PPTShow에 HTTP 서버기능을추가했으므로이제간단하게 PPTShow 프로그램을실행하고테스트해보자. MyHttpServer.cs의 handlegetrequest 메서드에추가한 /test 요청을웹브라우저를이용해테스트해보면다음과같은결과를볼수있다. 그림 13.11: http://localhost:5022/test 요청 / 응답테스트 이런식으로 HTTP 요청을정의해놓고해당요청에대응시켜파워포인트쇼를제어하는코드가실행 되게만들것이다. 이작업을위해우선어떻게파워포인트를제어할수있는지살펴보자. 13.6.2.3 파워포인트제어 (1) 오피스의파워포인트프로그램은그하나가거대한구성요소 (component) 로서외부에서제어할수있게끔구현돼있다. 이렇게제어가가능할수있는이유는 COM(Component Object Model) 이라는기술덕분인데, 닷넷프로그램에서는이를마치클래스다루듯이호출할수있는수단을제공한다. 바로 RCW(Runtime Callable Wrapper) 라는기술이다. 180 3 부 _ 닷넷응용프로그램

그림 13.12: RCW 의구조 닷넷응용프로그램 관리 (Managed) 영역 RCW 관리되지않는 (Unmanaged) 영역 COM 개체 닷넷응용프로그램과 COM 개체의호출을연결해주는 RCW는비주얼스튜디오에서생성해준다. 생성되는파일은 DLL 형식이며, 대개 Interop 이라는문자열이포함된다. 실제로비주얼스튜디오에서파워포인트에대한 RCW를생성해보자. 솔루션탐색기에서 PPTShow 프로젝트를대상으로마우스오른쪽버튼을눌러 참조추가 (Add Reference) 메뉴를선택하면 참조관리자 대화상자가나타난다. 그림 13.13에서보는것처럼좌측의 COM / 형식라이브러리 범주를선택한후나타나는오른쪽목록에서 Microsoft PowerPoint 15.0 Object Library 를선택한다. 그림 13.13: 파워포인트 COM 개체의 RCW 생성을위한참조추가 목록에 Microsoft PowerPoint 15.0 Object Library 가보이지않는다면해당 PC에오피스 2013이설치돼있지않기때문이다. 13. 실습 파워포인트쇼제어프로그램 181

확인 버튼을누르면비주얼스튜디오는 Microsoft.Office.Interop.PowerPoint.dll 파일을그순간 생성해서참조목록에추가한다. 그림 13.12 에서그려진항목들을현재생성된프로젝트에대응시켜 보면표 13.4 와같이정리할수있다. 표 13.4: RCW 호출관계 닷넷응용프로그램 RCW COM PPTShow Microsoft.Office.Interop.PowerPoint.dll 파워포인트프로그램 생성된 Interop을이용해파워포인트프로그램을제어하는클래스를정의할 PowerpointController. cs 파일을추가하고이제코드를조금씩붙여보자. 앞에서예제 13.2의 ProcessDocument 메서드가비어있었다는것을떠올려보자. 거기서 PPT 파일이선택되면우리는파워포인트프로그램을실행하고선택된 PPT 파일을로드하는것을구현해야한다. 따라서 PowerpointController 타입에는 Load 메서드를구현하고이를 ProcessDocument에서호출하는것부터코드작성을시작한다. // ======= PowerpointController.cs ======= using System; using System.Collections.Generic; using System.Linq; using System.Text; using Microsoft.Office.Interop.PowerPoint; namespace PPTShow public class PowerpointController Application _app; Presentation _current; public bool Load(string documentpath) try if (_app == null) // 파워포인트프로그램을실행하고 _app = new Application(); 182 3 부 _ 닷넷응용프로그램

// 실행된파워포인트프로그램을보이게한후 _app.visible = Microsoft.Office.Core.MsoTriState.msoTrue; // 파워포인트프로그램의 Presentations 객체를구해 var presentation = _app.presentations; // Presentations 객체에사용자가선택한 PPT 파일을로드한다. _current = presentation.open(documentpath); return true; catch return false; RCW DLL에서제공되는 Application 타입과 Presentation 타입을사용하면그호출이 COM으로전달된다. 결국닷넷프로그램에서 RCW 관련클래스를사용하는것으로파워포인트프로그램을제어할수있는것이다. 이렇게완성된 Load 메서드를이제 MainWindow의파일선택기능에연결하면된다. // ======= MainWindow.xaml.cs ======= using System.Windows; // [ 생략 ] namespace PPTShow public partial class MainWindow : Window, INotifyPropertyChanged // [ 생략 ] PowerpointController _pptcontroller; public MainWindow() // [ 생략 ] this._pptcontroller = new PowerpointController(); 13. 실습 파워포인트쇼제어프로그램 183

SetupHttpServer(); // [ 생략 ] // [ 생략 ] private void ProcessDocument(string path) if (File.Exists(path) == false) return; if (_pptcontroller.load(path) == false) return; this.filepath = path; this.ready = "Loaded"; 여기까지코드를완료하고빌드한후테스트를해보자. PPTShow에서 PPT 파일을선택하면이제파워포인트프로그램이실행되면서해당 PPT 파일이자동적으로로드되는것을확인할수있다. PPTShow에서 RCW를이용할수있게됐으므로이제 HTTP 서버를통해 PPTShow를제어할수있다. 간단한유형으로 PPT 쇼를시작하라는명령을내리는것부터구현해보자. 이를위해우선 PowerpointController 타입에 StartShow 메서드를구현한다. // ======= PowerpointController.cs ======= using System; // [ 생략 ] namespace PPTShow public class PowerpointController Application _app; Presentation _current; 184 3 부 _ 닷넷응용프로그램

// [ 생략 ] public void StartShow(int startslidenumber) object inslideshow = null; try // 현재파워포인트프로그램이 Show 상태인지를체크 inslideshow = _current.slideshowwindow; catch if (inslideshow == null) // 쇼상태가아니라면 // 쇼를시작한다. _current.slideshowsettings.run(); // 지정된슬라이드번호가파워포인트의슬라이드범위를넘지않게만들고, int start = Math.Min(_current.Slides.Count, startslidenumber); // 해당슬라이드로이동한다. // start 변수의값이 1이라면첫번째슬라이드가보여진다. _current.slideshowwindow.view.gotoslide(start, Microsoft.Office.Core.MsoTriState.msoTrue); 이제 MyHttpServer 타입에서 PowerpointController의 StartShow 메서드를호출해야하는데, 현재는그렇게할수없다. 왜냐하면 MyHttpServer 타입은 PowerpointController에대한참조가없기때문이다. 따라서 MyHttpServer 생성자에서 PowerpointController를전달받는식으로변경하고그것을이용해 /startshow 요청에서 StartShow 메서드를호출하게만든다. // ======= MyHttpServer.cs ======= using System; // [ 생략 ] 13. 실습 파워포인트쇼제어프로그램 185

namespace PPTShow public class MyHttpServer : HttpServer PowerpointController _document; public MyHttpServer(PowerpointController document, int port) : base(port) _document = document; public override void handlegetrequest(httpprocessor p) string urltext = p.http_url; if (urltext.endswith("/test") == true) // [ 생략 ] else if (urltext.contains("/startshow") == true) _document.startshow(1); p.writesuccess("text/html"); p.outputstream.write("ok"); // [ 생략 ] 당연히 MainWindow 의생성자는 PowerpointController 인스턴스를전달하도록코드를변경해야 한다. // ======= MainWindow.xaml.cs ======= using System.Windows; // [ 생략 ] namespace PPTShow public partial class MainWindow : Window, INotifyPropertyChanged 186 3 부 _ 닷넷응용프로그램

// [ 생략 ] private void SetupHttpServer() _listener = new MyHttpServer(this._pptController, this.port); _httpthread = new Thread(_listener.listen); _httpthread.isbackground = true; _httpthread.start(); 여기까지코드를작성하고이제다음과같은순서로테스트를진행해보자. 1. PPTShow 프로그램을실행한다. 2. Open 버튼을눌러 PPT 파일을선택한다. 그럼파워포인트가실행되고선택된문서가로드된다. 3. 웹브라우저를실행하고 http://localhost:5022/startshow 요청을보낸다. 그럼파워포인트프로그램은현재로드된 PPT 문서의쇼를시작한다. 지금단계에서그림 13.2의구조를다시한번들여다보자. 이쯤되면 OfficePresenter 프로그램의전체적인동작구조가그려져야한다. PPTShow는파워포인트를 RCW 타입을이용해제어하고윈도우폰에서실행되는 ShowController는 HTTP 요청을통해 PPTShow를제어하는것이다. 아직은제어동작이 startshow 하나에불과하지만나머지동작과정은 startshow를구현하는방식과유사하므로그다지어렵지않게구현해나갈수있다. 13.6.2.4 파워포인트제어 (2) startshow 제어외에어떤동작들이필요할까? 이를알아내기위해서는 12.5 응용프로그램의동작시나리오및화면스케치 절에서결정한사항을천천히다시읽어볼필요가있다. 윈도우폰앱에서실행되는 ShowController 프로그램은 PPT 파일에포함된모든슬라이드를작은그림파일로변환한목록이필요하다. 그리고각슬라이드에포함된메모내용도필요하다. 이동작을 /getsnapshot 요청으로정하자. 필자가계획한구현방법은다음과같다. 1. PPTShow 에서 PPT 파일을선택해서로드했을때슬라이드하나당이미지하나로대응시켜파일로저장한다. 2. PPT 파일에포함된모든슬라이드를열람하면서메모내용을읽어들인다. 13. 실습 파워포인트쇼제어프로그램 187

3. 추출된모든슬라이드의이미지와메모목록을 JSON 형식으로직렬화해서단일문자열로변환한다. 4. 윈도우폰앱에서 /getsnapshot 요청이오면 JSON 으로직렬화된문자열을그대로반환한다. 5. ( 윈도우폰앱은반환받은 JSON 문자열을다시역직렬화해서이미지와메모를구한다음사용한다.) 물론다른방식으로구현해도된다. 단지필자는위의방법으로구현해나가기로결정했을뿐이다. 여기서한가지더추가하면 JSON 직렬화 / 역직렬화를쉽게할수있게 PPT 파일과그안의개별슬라이드에해당하는클래스를만들것이다. 이를위해우선개별 PPT 슬라이드화면정보를담을 PPTPage 클래스를추가한다. // ======= PPTPage.cs ======= using System; namespace PPTShow public class PPTPage // 슬라이드를이미지로변경한다음문자열로직렬화한데이터 string _imageastext; public string ImageAsText get return _imageastext; set _imageastext = value; // 슬라이드에포함된메모내용 string _note; public string Note get return _note; set _note = value; 그리고 PPTPage 페이지가모여결국하나의 PPT 파일에담긴모든슬라이드정보를담게될 PPTDocument 클래스를추가한다. 188 3 부 _ 닷넷응용프로그램

// ======= PPTDocument.cs ======= using System.Collections.Generic; namespace PPTShow public class PPTDocument public PPTDocument() _list = new List<PPTPage>(); // PPT 슬라이드의모든내용을보관하는컬렉션 List<PPTPage> _list; public List<PPTPage> List get return _list; set _list = value; // PPT 파일에포함된슬라이드의수 public int Count get return _list.count; // PPTPage에포함된슬라이드이미지의가로크기 int _width; public int Width get return _width; set _width = value; // PPTPage에포함된슬라이드이미지의세로크기 int _height; public int Height get return _height; set _height = value; 13. 실습 파워포인트쇼제어프로그램 189

이제본격적으로 PPT 파일을로드하는시점에모든슬라이드의내용을추출해보자. PPT 파일을로 드하는단계에서처리할것이므로 PowerpointController.cs 파일만변경하면된다. 이때이미지조 작을할것이므로 System.Drawing 어셈블리참조를추가한다. System.Drawing 어셈블리는윈도우폼프로젝트인경우기본참조가되지만 WPF 프로젝트인경우에는그렇지않다. // ======= PowerpointController.cs ======= using System; // [ 생략 ] using System.IO; using System.Drawing; using System.Drawing.Imaging; using System.Web.Script.Serialization; using Microsoft.Office.Core; namespace PPTShow public class PowerpointController Application _app; Presentation _current; const int _slideimagewideth = 480; const int _slideimageheight = 358; // PPT 파일하나의모든슬라이드내용이이변수하나에문자열로직렬화된다. string _documentastext; public string DocumentAsText get return _documentastext; public bool Load(string documentpath) try // [ 생략 ] 190 3 부 _ 닷넷응용프로그램

// 임시로사용될폴더를하나만든다. string temppath = Path.Combine(Path.GetTempPath(),"ShowPresenter"); temppath = Path.Combine(tempPath, Guid.NewGuid().ToString()); Directory.CreateDirectory(tempPath); if (Directory.Exists(tempPath) == true) // Presentation 객체의 Export 메서드는 PPT 파일에포함된 // 슬라이드하나당이미지파일하나로만들어저장한다. // PPT에 3개의슬라이드가있는경우 " 슬라이드1.JPG", " 슬라이드2.JPG", // " 슬라이드3.JPG" 파일 3개가 temppath로지정된폴더에생성된다. _current.export(temppath, "JPG"); // temppath 폴더에저장된모든이미지를읽고, // Presentation으로부터해당슬라이드의메모를찾아내서 // PPTDocument에보관한다. PPTDocument doc = ReadAll(tempPath); // PPT 파일하나에대응되는 PPTDocument의내용을 // JSON 객체로직렬화한다. 따라서문자열하나로저장된다. JavaScriptSerializer json = new JavaScriptSerializer(); json.maxjsonlength = Int32.MaxValue; _documentastext = json.serialize(doc); // 임시로사용된폴더는더는필요하지않으므로삭제한다. Directory.Delete(tempPath, true); return true; catch return false; PPTDocument ReadAll(string temppath) int slidenumber = 0; // 슬라이드하나당이미지파일로저장된파일을열람한다. // 이때정렬함수를 CompareOnlyNumbers 로지정한것에유의하자. 13. 실습 파워포인트쇼제어프로그램 191

List<string> files = new List<string>(); files.addrange(directory.enumeratefiles(temppath)); files.sort(compareonlynumbers); PPTDocument document = new PPTDocument(); // Presentation 객체의 Slides 속성을통해 PPT 파일에포함된모든슬라이드를 // 열람할수있다. 열람하면서해당슬라이드에포함된메모를구한다. foreach (Slide slide in _current.slides) string note = string.empty; SlideRange nodepath = slide.notespage; foreach (Microsoft.Office.Interop.PowerPoint.Shape shape in nodepath.shapes) PpPlaceholderType type = PpPlaceholderType.ppPlaceholderObject; try type = shape.placeholderformat.type; catch if (type == PpPlaceholderType.ppPlaceholderBody) if (shape.hastextframe == MsoTriState.msoTrue) if (shape.textframe.hastext == MsoTriState.msoTrue) note += shape.textframe.textrange.text; // 메모와함께슬라이드이미지파일의내용을 // PPTPage 하나의객체에담는다. PPTPage page = new PPTPage(); page.note = note; page.imageastext = ConvertToImage(files[slideNumber], _slideimagewideth, _slideimageheight); 192 3 부 _ 닷넷응용프로그램

slidenumber++; document.list.add(page); document.width = _slideimagewideth; document.height = _slideimageheight; return document; private string ConvertToImage(string path, int width, int height) try // 이미지파일을 Image 객체로로드한다. using (Image img = Image.FromFile(path)) // 이미지파일을원본크기그대로윈도우폰에전송할필요는없다. // 폰화면은작기때문에그에맞는크기로변환해야 // 전송되는데이터의양을줄일수있다. // 따라서이미지크기를축소하는 GetThumbnailImage 메서드를이용한다. using (Image thumbnail = img.getthumbnailimage(width, height, delegate return false;, IntPtr.Zero)) MemoryStream ms = new MemoryStream(); thumbnail.save(ms, ImageFormat.Jpeg); ms.position = 0; // 바이트배열이아닌텍스트로반환한다. // Base64 인코딩은바이트를텍스트로바꿔준다. return Convert.ToBase64String(ms.GetBuffer(), 0, (int)ms.length); catch return string.empty; 13. 실습 파워포인트쇼제어프로그램 193

// 파일명중에서숫자만취해크기를비교한다. // 따라서파일명에포함된숫자로파일목록을정렬한다. private static int CompareOnlyNumbers(string x, string y) string filenamex = System.IO.Path.GetFileNameWithoutExtension(x); string filenamey = System.IO.Path.GetFileNameWithoutExtension(y); int xn = EraseChars(fileNameX); int yn = EraseChars(fileNameY); return xn.compareto(yn); // 문자열에서숫자만취해서정수로반환한다. private static int EraseChars(string x) StringBuilder sb = new StringBuilder(); foreach (char ch in x) if (Char.IsNumber(ch) == true) sb.append(ch); return Int32.Parse(sb.ToString()); 위코드의핵심은 _current.export 메서드다. 이메서드를호출하면파워포인트프로그램은현재로드된 PPT 파일에담긴모든슬라이드를각각대응되는이미지파일로저장한다. 이메서드가있다는사실을모르면 PPT의슬라이드를그림으로가져올수없기때문에 ShowController에서보여질 2번째페이지인그림 13.6의동작화면을구현할수없다. 코드가길지만나머지부분은 _current.export 로저장된이미지파일을슬라이드순서에맞게다시읽어 PPTDocument에넣는것이전부다. 일단이런식으로 _documentastext 문자열변수에 PPT 파일의모든내용을담았다면다음으로 HTTP 서버의코드에해당변수의값을가져갈수있는요청을추가하면된다. 194 3 부 _ 닷넷응용프로그램

// ======= MyHttpServer.cs ======= using System; // [ 생략 ] namespace PPTShow public class MyHttpServer : HttpServer // [ 생략 ] public override void handlegetrequest(httpprocessor p) string urltext = p.http_url; // [if 생략 ] else if (urltext.endswith("/getsnapshot") == true) p.writesuccess("application/json"); p.outputstream.write(_document.documentastext); // [ 생략 ] writesuccess의값을 application/json 으로바꾼것은 HTTP 통신의관례를따른것이다. DocumentAsText 변수에담긴내용이 JSON 직렬화를한내용이기때문에응답을받는측에이사실을확실히알리는역할을한다 (/startshow의응답에사용된것과같이 text/html 로반환해도무방하다 ). 이번에도테스트를위해웹브라우저를이용해방문해보자. 1. PPTShow 프로그램을실행한다. 2. Open 버튼을눌러 PPT 파일을선택한다. 그럼파워포인트가실행되고선택된문서가로드된다. 3. 웹브라우저를실행하고 http://localhost:5022/getsnapshot 요청을보낸다. 그럼 JSON 형식으로직렬화된문자열을반환받을수있다. 반환받은문자열을보면다음과같은식이다. 13. 실습 파워포인트쇼제어프로그램 195

"List":["ImageAsText":"/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRof Hh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQ...[ 생략 ]... KACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAP/9k=","Note":""],"Count":17," Width":480,"Height":358 중간에바이트가 Base64 인코딩방식으로변환되어문자열로변환된결과를확인할수있다. 물론이문자열을다시 Base64 형식으로디코딩하면원래의바이트배열로변환되고그것은곧 이미지파일의내용 이된다. getsnapshot 요청을통해이제윈도우폰앱에서 PPT 파일에포함된슬라이드를그림목록으로보여줄수있는기반이마련됐다. 하지만아직끝난것이아니다. 앱사용자가슬라이드이미지를선택하면 PC에서실행중인 PPTShow 프로그램에서는해당슬라이드를쇼에나타나게해야한다. 이기능을구현하기란매우쉽다. 다음과같이 PowerpointController 타입에쇼에나타날슬라이드번호를지정할수있는메서드를만들고 // ======= PowerpointController.cs ======= using System; // [ 생략 ] namespace PPTShow public class PowerpointController Application _app; Presentation _current; // [ 생략 ] public void SetCurrentSlide(int slidenumber) try _current.slideshowwindow.view.gotoslide(slidenumber, MsoTriState.msoTrue); catch 196 3 부 _ 닷넷응용프로그램

HTTP 통신으로이메서드를호출할수있는수단을만들어준다. // ======= MyHttpServer.cs ======= using System; // [ 생략 ] namespace PPTShow public class MyHttpServer : HttpServer // [ 생략 ] public override void handlegetrequest(httpprocessor p) string urltext = p.http_url; // [if 생략 ] else if (urltext.contains("/setslide/") == true) string txt = urltext.substring(urltext.lastindexof('/') + 1); int slide; if (Int32.TryParse(txt, out slide) == true) _document.setcurrentslide(slide); p.writesuccess("text/html"); p.outputstream.write("ok"); // [ 생략 ] 이동작을테스트해보자. 1. PPTShow 프로그램을실행한다. 2. Open 버튼을눌러 PPT 파일을선택한다. 그럼파워포인트가실행되고선택된문서가로드된다. 13. 실습 파워포인트쇼제어프로그램 197

3. 웹브라우저를실행하고 http://localhost:5022/startshow 요청을보낸다. 그럼 PPT 쇼가시작하고, 전체화면을차지한다 ( 웹브라우저가가려지므로 Alt + Tab 키를눌러가려진웹브라우저를나타나게만든다 ). 4. 화면에다시나타난웹브라우저를이용해 http://localhost:5022/setslide/2라고요청을보낸다. 그럼배경에진행중인 PPT 쇼는두번째슬라이드로바뀐다. 같은방식으로숫자만바꿔서 setslide 요청을다양하게테스트한다. 여기까지 PPTShow 의모든코드가완성됐다. 다음단계로넘어가기전에 PPTShow 프로그램에서 정의한 HTTP 요청을정리해보자. 표 13.5: PPTShow 에정의된 HTTP 요청 요청응답포맷설명 /getsnapshot "List":[ "ImageAsText":"...[base64]...", "Note":"...[memo]...", "ImageAsText":"...[base64]...", "Note":"...[memo]...", [PPT 슬라이드수만큼반복 ] ], "Width":480," Height":358 현재 PPT 의모든슬라이드에대한스냅 샷이미지와메모를반환 /setslide/[number] OK Show 중인 PPT 에서보여질슬라이드 를지정한다. /startshow OK PPT 쇼를시작한다. 이제정해진 HTTP 통신을이용해쇼를제어할수있는윈도우폰앱을만들차례다. 13.6.3 ShowController 개발 앞에서도설명했듯이비주얼스튜디오를유료버전이아닌익스프레스버전을사용하고있다면별도 로솔루션을만들어윈도우폰앱프로젝트를진행해야한다. 첫단계로 ShowController 라는이름의솔루션을만들고다시윈도우폰앱유형으로 ShowController 프로젝트를추가한다. ShowController 의기능은크게두가지로나눌수있다. 198 3 부 _ 닷넷응용프로그램

PPTShow에연결해 PPT 파일정보를가져오는 MainPage Panorama 컨트롤을이용해쇼를제어하는 PPTController 페이지 각화면은그림 13.5, 그림 13.6 에서이미정의했으므로그에따라 XAML 을구성해나가면서구현한 다. 13.6.3.1 연결페이지 MainPage.xaml 연결페이지의 UI 구성은간단하다. IP 주소와포트번호만입력받고연결버튼을누르기만하면 된다. // ======= MainPage.xaml ======= <phone:phoneapplicationpage x:class="showcontroller.mainpage" [ 생략 ] shell:systemtray.isvisible="true"> <Grid x:name="layoutroot" Background="Transparent"> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <StackPanel x:name="titlepanel" Grid.Row="0" Margin="12,17,0,28"> <TextBlock x:name="applicationtitle" Text="Show Controller" Style="StaticResource PhoneTextNormalStyle"/> </StackPanel> <Grid x:name="contentpanel" Grid.Row="1" Margin="12,0,12,0"> <Grid.RowDefinitions> <RowDefinition Height="80"></RowDefinition> <RowDefinition Height="80"></RowDefinition> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition></ColumnDefinition> <ColumnDefinition></ColumnDefinition> <ColumnDefinition></ColumnDefinition> </Grid.ColumnDefinitions> <TextBlock VerticalAlignment="Center">IP</TextBlock> <TextBox Grid.Column="1" Grid.ColumnSpan="2"></TextBox> 13. 실습 파워포인트쇼제어프로그램 199

<TextBlock VerticalAlignment="Center" Grid.Row="1">Port</TextBlock> <TextBox Grid.Row="1" Grid.Column="1"></TextBox> <Button Padding="0" Grid.Row="1" Grid.Column="2">Connect</Button> </Grid> </Grid> </phone:phoneapplicationpage> PPTShow 에서처럼 IP 주소와포트번호는데이터바인딩으로해결한다. // ======= MainPage.xaml ======= [ 생략 ] <TextBlock VerticalAlignment="Center">IP</TextBlock> <TextBox Text="Binding Path=IPSelected, Mode=TwoWay" Grid.Column="1" Grid.ColumnSpan="2"></TextBox> <TextBlock VerticalAlignment="Center" Grid.Row="1">Port</TextBlock> <TextBox Text="Binding Path=Port, Mode=TwoWay" Grid.Row="1" Grid.Column="1"></TextBox> [ 생략 ] 바인딩에사용된구문가운데 Mode=TwoWay 는전에설명한적이없는데, 표 13.6 에가능한값을정 리해놨으니참고하자. 표 13.6: 데이터바인딩의 Mode 값정리 Mode OneWay TwoWay OneTime 설명코드의변수값이바뀌면 XAML 요소에해당값이반영된다. 하지만 XAML 요소에서값이바뀌어도코드의변수에는반영되지않는다. 즉, 단방향으로만값이동기화된다. 코드의변수값이바뀌면 XAML 요소에도값이반영되고, XAML 요소에서도값이바뀌면코드의변수로반영된다. 즉, 양방향으로값이동기화된다. OneWay와성격은같지만단한번만동기화가발생한다. 한번코드의변수값이바뀌면 XAML 요소로동기화가되지만그이후로코드의변수값이바뀌는것은 XAML 요소로반영되지않는다. WPF 응용프로그램에서굳이바인딩의 Mode 값을지정하지않았던것은기본값이 TwoWay 였기 때문이다. 하지만윈도우폰에서는기본값이 OneWay 이기때문에양방향동기화를위해서는명시적 으로 TwoWay 로지정해야한다. 이제 XAML 요소에바인딩을한변수를 MainPage 타입에정의해준다. 200 3 부 _ 닷넷응용프로그램

// ======= MainPage.xaml.cs ======= using System.ComponentModel; using System.Windows; using Microsoft.Phone.Controls; namespace ShowController public partial class MainPage : PhoneApplicationPage, INotifyPropertyChanged string _ipselected; public string IPSelected get return this._ipselected; set if (this._ipselected == value) return; this._ipselected = value; OnPropertyChanged("IPSelected"); int _port; public int Port get return this._port; set if (this._port == value) return; this._port = value; OnPropertyChanged("Port"); 13. 실습 파워포인트쇼제어프로그램 201

public MainPage() InitializeComponent(); this.datacontext = this; this.port = 5022; // 초기값으로 5022 포트번호를미리지정 public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged(string propertyname) if (PropertyChanged == null) return; PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); 여기까지입력을마쳤으면실행해보자. 그럼에뮬레이터에서그림 13.14 와같이실행되는것을볼수 있다. 그림 13.14: MainPage 연결화면 202 3 부 _ 닷넷응용프로그램

기획에의하면이화면을통해 PPTShow 프로그램에서대기중인 HTTP 서버에 getsnapshot 요청 을보내 PPT 파일의내용을가져오는기능을구현해야한다. 따라서 Connect 버튼에이벤트처리기 를연결하고, // ======= MainPage.xaml ======= [ 생략 ] <Button Padding="0" Grid.Row="1" Grid.Column="2" Click="btnConnectClicked">Connect</Button> [ 생략 ] btnconnectclicked 메서드에서는 PPTShow 에서대기중인 HTTP 서버에 /getsnapshot 요청을 WebClient 객체를이용해보내고결과를받아처리한다. // ======= MainPage.xaml.cs ======= using System.ComponentModel; // [ 생략 ] using System; using System.Net; using Microsoft.Phone.Shell; namespace ShowController public partial class MainPage : PhoneApplicationPage, INotifyPropertyChanged // [ 생략 ] private void btnconnectclicked(object sender, RoutedEventArgs e) string url = string.format("http://0:1/getsnapshot", this.ipselected, this.port); Uri uri = new Uri(url); // 연결버튼이눌리면지정된 IP/Port 로 WebClient 객체를이용해 // HTTP 요청을보낸다. WebClient wc = new WebClient(); wc.headers[httprequestheader.ifmodifiedsince] = DateTime.UtcNow.ToString(); wc.downloadstringcompleted += new DownloadStringCompletedEventHandler(SnapshotCompleted); wc.downloadstringasync(uri); 13. 실습 파워포인트쇼제어프로그램 203

void SnapshotCompleted(object sender, DownloadStringCompletedEventArgs e) if (e.error!= null) MessageBox.Show(e.Error.ToString()); else try if (string.isnullorempty(e.result) == false) // 정상적으로 getsnapshot 결과를받은경우 string txt = e.result; // [ 생략 : txt 내용으로 PPT 슬라이드목록을구한다.] catch (Exception ex) throw ex; // [ 생략 ] 윈도우폰에서의 WebClient는동기방식의 DownloadString 메서드를제공하지않는다. 이것은마이크로소프트의정책적인결정에따른것인데, 폰의사용자반응성을떨어뜨리는동기화메서드의구현을일부러제공하지않기때문이다. 따라서 DownloadStringAsync 비동기메서드만사용할수있고통신이완료됐을때 DownloadStringCompleted 이벤트처리기를구현함으로써결과를받아처리할수있다. 성공적으로결과를반환받았다면 txt 문자열변수에는 PPTShow에서 JSON 포맷으로직렬화한 PPTDocument 객체의내용이들어있다. 따라서 txt의내용을다시 JSON으로역직렬화하면 PPTDocument의내용을그대로복원할수있다. 204 3 부 _ 닷넷응용프로그램

// ======= MainPage.xaml.cs ======= using System.ComponentModel; // [ 생략 ] using System.Text; using System.Runtime.Serialization.Json; using System.IO; namespace ShowController public partial class MainPage : PhoneApplicationPage, INotifyPropertyChanged // [ 생략 ] void SnapshotCompleted(object sender, DownloadStringCompletedEventArgs e) // [ 생략 ] string txt = e.result; MemoryStream ms = new MemoryStream(Encoding.UTF8.GetBytes(txt)); DataContractJsonSerializer serializer = new DataContractJsonSerializer(typeof(PPTDocument)); PPTDocument document = serializer.readobject(ms) as PPTDocument; // [ 생략 ] 그런데위의코드를컴파일하면 PPTDocument 타입이 ShowController 윈도우폰프로젝트에는정의돼있지않으므로오류가발생한다. 이를해결하려면 PPTShow에포함된 PPTPage.cs 파일과 PPTDocument.cs 파일을 ShowController 프로젝트가있는폴더로복사한후추가하면된다. 추가된 PPTDocument/PPTPage 타입은네임스페이스가 PPTShow로돼있으므로 using 문으로추가하는것도잊지말자. // ======= MainPage.xaml.cs ======= using System.ComponentModel; // [ 생략 ] using PPTShow; namespace ShowController public partial class MainPage : PhoneApplicationPage, INotifyPropertyChanged 13. 실습 파워포인트쇼제어프로그램 205

// [ 생략 ] 아직처리해야할단계가하나남아있는데, Connect 버튼이눌린경우기획단계에서구상했던그림 13.6의 UI가탑재된두번째페이지로넘어가는기능을구현해야한다. 추가돼야할페이지에 PPTController.xaml이라는이름을부여하고최종적으로 SnapshotCompleted에서는 PPTController.xaml로이동하는것으로마무리하자. 그런데여기서 getsnapshot으로구한 document 변수의내용을 PPTController.xaml로넘겨야하는데어떻게하는것이좋을까? 앞의 11.5 윈도우폰응용프로그램 / 페이지단위의응용프로그램구현 절에서페이지간데이터전송에대해다음과같은세가지방법이있음을설명했다. 1. QueryString 을이용한키 / 값전달 2. 전역정적변수를이용한상태공유 3. 격리된저장소를이용해상태공유 이가운데 PPTDocument 타입을쿼리문자열로전달하는방법은바람직하지않다. 그리고전역정적변수를이용하는경우 tombstone 문제가발생할수있기때문에남은선택으로는 3번방법이최선일것같다 ( 물론, tombstone을무시한다면 2번방법도좋은선택이될수있다 ). 아시다시피 3번방법은코드가다소길어진다는단점이있다. 그래서이번에는또다른 4번째방법을하나더소개하려고한다. 바로 PhoneApplicationService에서제공되는 State 컬렉션을이용하는것이다. // ======= MainPage.xaml.cs ======= using System.ComponentModel; // [ 생략 ] using PPTShow; namespace ShowController public partial class MainPage : PhoneApplicationPage, INotifyPropertyChanged // [ 생략 ] void SnapshotCompleted(object sender, DownloadStringCompletedEventArgs e) 206 3 부 _ 닷넷응용프로그램

// [ 생략 ] PPTDocument document = serializer.readobject(ms) as PPTDocument; PhoneApplicationService.Current.State["Document"] = document; Uri uri = new Uri(string.Format("/PPTController.xaml?ip=0&port=1", this.ipselected, this.port), UriKind.Relative); this.navigationservice.navigate(uri); // [ 생략 ] PhoneApplicationService.Current의 State 컬렉션은윈도우폰운영체제가자동으로관리해주는저장소다. 여기에저장된값은앱이비활성화될때자동으로디스크에저장되기때문에 격리저장소 와동일한영속성 (persistence) 을보장받을수있다. 이것으로 MainPage가하는역할은모두끝났다. 13.6.3.2 제어페이지 PPTController.xaml ShowController 프로젝트에 PPTController.xaml 이라는이름으로새페이지를하나추가한다. 그 리고다음과같이 Panorama 요소하나와 ListBox 를추가해 UI 를구성한다. // ======= PPTController.xaml ======= <phone:phoneapplicationpage x:class="showcontroller.pptcontroller" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:phone="clr-namespace:microsoft.phone.controls;assembly=microsoft.phone" xmlns:shell="clr-namespace:microsoft.phone.shell;assembly=microsoft.phone" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:controls="clr-namespace:microsoft.phone.controls;assembly=microsoft.phone.controls" FontFamily="StaticResource PhoneFontFamilyNormal" FontSize="StaticResource PhoneFontSizeNormal" Foreground="StaticResource PhoneForegroundBrush" 13. 실습 파워포인트쇼제어프로그램 207

SupportedOrientations="Portrait" Orientation="Portrait" mc:ignorable="d" d:designheight="768" d:designwidth="480" shell:systemtray.isvisible="true"> <Grid x:name="layoutroot" Background="Transparent"> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <StackPanel x:name="titlepanel" Grid.Row="0" Margin="12,17,0,28"> <TextBlock x:name="applicationtitle" Text="Show Controller" Style="StaticResource PhoneTextNormalStyle"/> </StackPanel> <Grid x:name="contentpanel" Grid.Row="1" Margin="12,0,12,0"> <Grid.RowDefinitions> <RowDefinition></RowDefinition> <RowDefinition Height="5"></RowDefinition> <RowDefinition Height="100"></RowDefinition> </Grid.RowDefinitions> <controls:panorama x:name="panorama"> </controls:panorama> <ListBox Grid.Row="2" x:name="imagelist" ScrollViewer.HorizontalScrollBarVisibility="Auto" ScrollViewer.VerticalScrollBarVisibility="Disabled"> <ListBox.ItemsPanel> <ItemsPanelTemplate> <StackPanel Orientation="Horizontal" /> </ItemsPanelTemplate> </ListBox.ItemsPanel> </ListBox> </Grid> </Grid> </phone:phoneapplicationpage> 208 3 부 _ 닷넷응용프로그램

PPTController 타입은페이지가로드될때자동으로실행되는 OnNavigatedTo 메서드를재정의해 초기화작업을시작한다. // ======= PPTController.xaml.cs ======= using System; using System.IO; using System.Net; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Navigation; using Microsoft.Phone; using Microsoft.Phone.Controls; using Microsoft.Phone.Shell; using PPTShow; namespace ShowController public partial class PPTController : PhoneApplicationPage string _baseurl; public PPTController() InitializeComponent(); protected override void OnNavigatedTo(NavigationEventArgs e) PPTDocument document = null; // MainPage에서저장해둔 PPTDocument 객체를받아온다. if (PhoneApplicationService.Current.State.ContainsKey("Document") == true) document = PhoneApplicationService.Current.State["Document"] as PPTDocument; if (document == null) 13. 실습 파워포인트쇼제어프로그램 209

return; // 쿼리문자열로전달된 IP주소와포트번호도함께받아온다. string ipaddress; string port; NavigationContext.QueryString.TryGetValue("ip", out ipaddress); NavigationContext.QueryString.TryGetValue("port", out port); if (string.isnullorempty(ipaddress) == true string.isnullorempty(port) == true) return; _baseurl = string.format("http://0:1", ipaddress, port); base.onnavigatedto(e); PhoneApplicationService.Current.State[ Document ] 컬렉션에담긴값은 PPTDocument 이므 로이를이용해곧바로 Panorama 컨트롤과하단의 ListBox 를초기화할수있다. 그중에서우선코 딩하기쉬운 ListBox 먼저채워보자. // ======= PPTController.xaml.cs ======= using System; // [ 생략 ] namespace ShowController public partial class PPTController : PhoneApplicationPage string _baseurl; // [ 생략 ] protected override void OnNavigatedTo(NavigationEventArgs e) 210 3 부 _ 닷넷응용프로그램

// [ 생략 ] LoadDocument(document); base.onnavigatedto(e); private void LoadDocument(PPTDocument document) foreach (var item in document.list) byte[] jpegcontents = Convert.FromBase64String(item.ImageAsText); Image img = new Image(); MemoryStream ms = new MemoryStream(jpegContents); WriteableBitmap bmp = PictureDecoder.DecodeJpeg(ms, 100, 100); img.source = bmp; Border border = new Border(); border.child = img; imagelist.items.add(border); PPTDocument의 List 속성에는 PPTPage 객체들이있고, PPTPage의 ImageAsText에는 PPT 슬라이드의이미지데이터가 Base64 형식으로인코딩된채로저장돼있다. 따라서이를 Base64 디코딩해서바이트배열로변환한다음 MemoryStream과 PictureDecoder 타입을이용해 WritableBitmap 타입으로변환할수있다. 일단 Bitmap 타입으로변환되면 Image 요소의 Source에지정해그림으로출력할수있다. 결국위의코드를실행하면 ListBox에들어갈항목하나당다음과같은 XAML이구성되는것과같다. <Border> <Border.Child> <Image Source="...WritableBitmap..." /> </Border.Child> </Boder> 13. 실습 파워포인트쇼제어프로그램 211

여기까지완성하고실행해보자. 1. PPTShow 에서 PPT 파일을선택한다. 2. ShowController 윈도우폰앱을실행하고연결버튼을누른다. 그럼다음과같이 PPTController 페이지하단에 PPT 슬라이드그림목록이출력되는것을볼수있 다. 그림 13.15: PPTController 페이지의 PPT 슬라이드그림목록 에뮬레이터에서테스트하는경우마우스를이용해이미지목록을좌 / 우로스크롤할수있고, 실제윈도우폰이라면터치동작을이용해이미지를좌 / 우로밀어낼수있다. 원래우리는이상태에서이미지로출력된슬라이드를선택하면 PPTShow에서진행중인쇼의슬라이드도변경하도록구현하는것이목표였다. 이를위해서는다음과같은선행과제가필요하다. 쇼의슬라이드를변경하기위해서는당연히 PPT 쇼가이미시작된상태여야한다. 하단의 ListBox에서현재선택된이미지가어떤것인지구분될필요가있다. 이를위해선택된슬라이드를구분하기쉽 게 Border 의선을노란색으로굵게표시한다. ListBox 내의 Image가선택되는이벤트처리기를작성한다. 그리고해당이벤트처리기내에서는현재선택된 Image 요소가몇번째슬라이드를표현하는것인지정보를조회할수있어야한다. 212 3 부 _ 닷넷응용프로그램

단계적으로하나씩구현해보자. 우선 PPTShow 에 ShowController 가연결됐으면파워포인트가쇼 를시작하게만드는것이바람직하다. 따라서 LoadDocument 의마지막단계에서 PPTShow 에쇼를 시작하라는 HTTP 요청을전송한다. // ======= PPTController.xaml.cs ======= using System; // [ 생략 ] namespace ShowController public partial class PPTController : PhoneApplicationPage // [ 생략 ] private void LoadDocument(PPTDocument document) foreach (var item in document.list) // [ 생략 ] StartShow(); void StartShow() string url = string.format("0/startshow", this._baseurl); CallUrl(url, null); void CallUrl(string url, DownloadStringCompletedEventHandler handler) WebClient wc = new WebClient(); wc.headers[httprequestheader.ifmodifiedsince] = DateTime.UtcNow.ToString(); if (handler!= null) wc.downloadstringcompleted += handler; 13. 실습 파워포인트쇼제어프로그램 213

wc.downloadstringasync(new Uri(url)); 코드는크게특별한것이없으므로이상태에서빌드한후테스트해보자. 이번에는연결버튼을누르자마자파워포인트의쇼가시작되는것을확인할수있다. 다음으로선택된슬라이드의 Border를구분하기쉽게노란색에굵기를 3으로만들게끔코드를추가한다. // ======= PPTController.xaml.cs ======= using System; // [ 생략 ] namespace ShowController public partial class PPTController : PhoneApplicationPage // [ 생략 ] Border oldborder = null; private void LoadDocument(PPTDocument document) int slideindex = 1; foreach (var item in document.list) // [ 생략 ] border.child = img; border.borderbrush = new SolidColorBrush(Colors.Yellow); if (slideindex == 1) // 처음연결한시점에는첫슬라이드를선택표시 border.borderthickness = new Thickness(3); oldborder = border; // [ 생략 ] 214 3 부 _ 닷넷응용프로그램

slideindex++; StartShow(); 처음 PPTShow에연결해서실행되는 LoadDocument 단계에서는무조건첫번째슬라이드가선택된것으로가정하고해당슬라이드의 Border에대해서만굵기를 3으로지정한다. 여기까지진행하고다시실행하면그림 13.16과같이첫번째슬라이드가선택됐음을쉽게파악할수있는외곽선을볼수있다. 그림 13.16: 첫번째슬라이드에지정된노란색외곽선 이제목록의이미지를선택한경우파워포인트의쇼에서해당이미지의슬라이드로교체되는기능을 구현할차례다. 이를위해 Image 요소에 Tap 이벤트를걸어주면사용자가하단의이미지영역을터 치하거나마우스로선택했을때실행되는코드를정의할수있다. // ======= PPTController.xaml.cs ======= using System; // [ 생략 ] 13. 실습 파워포인트쇼제어프로그램 215

namespace ShowController public partial class PPTController : PhoneApplicationPage string _baseurl; // [ 생략 ] private void LoadDocument(PPTDocument document) int slideindex = 1; foreach (var item in document.list) byte[] jpegcontents = Convert.FromBase64String(item.ImageAsText); Image img = new Image(); img.tap += img_tap; // [ 생략 ] StartShow(); void img_tap(object sender, GestureEventArgs e) // 선택된이미지를 PPT Show에서선택되도록 HTTP 요청처리 여기서문제가하나있다. 바로 ListBox 목록에서선택된이미지의슬라이드번호를 img_tap 이벤트처리기에서어떻게구하느냐다. 즉, 선택된객체의문맥 (context) 데이터가필요한상황인데, XAML 요소에서는이런경우사용자가임의의데이터를보관할수있게 Tag 속성을제공하므로이를이용해슬라이드번호를 Image 요소와연결할수있다. // ======= SlideItemData.cs ======= using System; namespace ShowController public class SlideItemData 216 3 부 _ 닷넷응용프로그램

int _slideindex; public int SlideIndex get return _slideindex; set _slideindex = value; // ======= PPTController.xaml.cs ======= using System; // [ 생략 ] namespace ShowController public partial class PPTController : PhoneApplicationPage string _baseurl; // [ 생략 ] private void LoadDocument(PPTDocument document) int slideindex = 1; foreach (var item in document.list) byte[] jpegcontents = Convert.FromBase64String(item.ImageAsText); Image img = new Image(); img.tap += img_tap; // 슬라이드번호를 Image 요소의 Tag 속성에연결 SlideItemData tagdata = new SlideItemData(); tagdata.slideindex = slideindex; img.tag = tagdata; // [ 생략 ] slideindex++; StartShow(); void img_tap(object sender, GestureEventArgs e) 13. 실습 파워포인트쇼제어프로그램 217

// 선택된이미지의슬라이드번호를 Tag 로부터구한다. SlideItemData tagdata = (sender as Image).Tag as SlideItemData; int slideindex = tagdata.slideindex; SetSlide(slideIndex); SetSelectedBorder(slideIndex); // 슬라이드번호로쇼에보여질슬라이드를결정 void SetSlide(int number) string url = string.format("0/setslide/1", this._baseurl, number); CallUrl(url, null); // ListBox에서선택된슬라이드에해당하는 Image만 Border를 3으로설정 void SetSelectedBorder(int slideindex) if (oldborder!= null) oldborder.borderthickness = new Thickness(0); Border border = imagelist.items[slideindex - 1] as Border; border.borderthickness = new Thickness(3); oldborder = border; imagelist.scrollintoview(border); // [ 생략 ] 여기까지구현한결과를테스트해보자. 이제는마우스또는터치를이용해하단의이미지를선택할수있고그때마다 PPTShow는파워포인트의쇼에서보여질슬라이드를변경한다. 마지막으로 Panorama에보여질슬라이드와메모목록을만들어보자. 우선, LoadDocument에서 ListBox의이미지목록을채웠던것과비슷하게 Panorama 컨트롤의내용도구성할수있다. 218 3 부 _ 닷넷응용프로그램

// ======= PPTController.xaml.cs ======= using System; // [ 생략 ] namespace ShowController public partial class PPTController : PhoneApplicationPage string _baseurl; private void LoadDocument(PPTDocument document) int slideindex = 1; foreach (var item in document.list) // [ 생략 : ListBox의 Image/Border 구성 ] // 파노라마컨트롤목록구성 PanoramaItem panitem = new PanoramaItem(); panitem.tag = tagdata; Grid grid = new Grid(); grid.rowdefinitions.add(new RowDefinition()); grid.rowdefinitions.add(new RowDefinition()); ms.position = 0; bmp = PictureDecoder.DecodeJpeg(ms, document.width, document.height); img = new Image(); img.source = bmp; Grid.SetRow(img, 0); TextBox txt = new TextBox(); txt.text = item.note; txt.isreadonly = true; Grid.SetRow(txt, 1); grid.children.add(img); grid.children.add(txt); panitem.content = grid; 13. 실습 파워포인트쇼제어프로그램 219

panorama.items.add(panitem); slideindex++; StartShow(); // [ 생략 ] 위의코드를실행하면 PanoramaItem 하나당다음과같은 XAML이구성되는것과같다. <Grid> <Grid.RowDefinitions> <RowDefinition /> <RowDefinition /> </Grid.RowDefinitions> <Image Source=" WritableBitmap " Grid.Row="0" /> <TextBox IsReadOnly="True" Grid.Row="1" Text=" item.note " /> </Grid> 여기까지코드를추가하고변경된사항을확인하기위해앱을실행해보자. 그림 13.17: Panorama 컨트롤의동작화면 220 3 부 _ 닷넷응용프로그램