같은프로그램, 새로운기술 WPF, 왜배워야되죠? WPF() 는.NET 으로쓰인프로그램의사용자인터페이스를만들기위한기술입니다. WPF 프로그램은일반적으로윈도우데스크톱의사용자인터페이스를보여줍니다. WPF는윈도우소프트웨어개발중에서하나의인기있는기술이기도하죠. 그리고 WPF 의친

Similar documents
PowerPoint Template

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

View Licenses and Services (customer)

아이콘의 정의 본 사용자 설명서에서는 다음 아이콘을 사용합니다. 참고 참고는 발생할 수 있는 상황에 대처하는 방법을 알려 주거나 다른 기능과 함께 작동하는 방법에 대한 요령을 제공합니다. 상표 Brother 로고는 Brother Industries, Ltd.의 등록 상

1

ISP and CodeVisionAVR C Compiler.hwp

Microsoft Word - windows server 2003 수동설치_non pro support_.doc

쓰리 핸드(삼침) 요일 및 2405 요일 시간, 및 요일 설정 1. 용두를 2의 위치로 당기고 반시계방향으로 돌려 전날로 를 설정합니다. 2. 용두를 시계방향으로 돌려 전날로 요일을 설정합니다. 3. 용두를 3의 위치로 당기고 오늘 와 요일이 표시될 때까지 시계방향으로

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

Windows 8에서 BioStar 1 설치하기

System Recovery 사용자 매뉴얼

목차 1. 시스템요구사항 암호및힌트설정 ( 윈도우 ) JetFlash Vault 시작하기 ( 윈도우 ) JetFlash Vault 옵션 ( 윈도우 )... 9 JetFlash Vault 설정... 9 JetFlash Vault

IRISCard Anywhere 5

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

Office 365 사용자 가이드

Oracle VM VirtualBox 설치 VirtualBox에서 가상머신 설치 가상머신에 Ubuntu 설치

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

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

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

Studuino소프트웨어 설치

1

윈도 모바일 6.1을 OS로 사용하는 스마트폰(옴니아2 등)에서의 Tcl/Tk의 사용

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

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

메뉴얼41페이지-2

Visual Basic Visual Basic 소개

2_안드로이드UI

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

PowerPoint 프레젠테이션

Visual Basic 반복문

SIGIL 완벽입문

차례보기 Easy Setting Box 소개 03 Easy Setting Box 란 03 Easy Setting Box 주요기능 04 사용요구사항 Easy Setting Box 설치 / 제거하기 05 Easy Setting Box 설치하기 08 Easy Setting

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

Microsoft Word - Armjtag_문서1.doc

사용설명서를 읽기 전에 ios용 아이디스 모바일은 네트워크 연결을 통해 ios 플랫폼 기반의 모바일 기기(iOS 버전 6.0 이상의 ipod Touch, iphone 또는 ipad)에서 장치(DVR, 네트워크 비디오 서버 및 네트워크 카메라)에 접속하여 원격으로 영상을

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

캘크 시작하기

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

사용설명서를 읽기 전에 안드로이드(Android)용 아이디스 모바일은 네트워크 연결을 통해 안드로이드 플랫폼 기반의 모바일 기기에서 장치 (DVR, NVR, 네트워크 비디오 서버, 네트워크 카메라) 에 접속하여 원격으로 영상을 감시할 수 있는 프로그램입니다. 장치의 사

MF Driver Installation Guide

Windows Server 2012

MVVM 패턴의 이해

Javascript

NTD36HD Manual

Microsoft Word - ntasFrameBuilderInstallGuide2.5.doc

소프트웨어공학 Tutorial #2: StarUML Eun Man Choi

MF5900 Series MF Driver Installation Guide

SBR-100S User Manual

PowerPoint 프레젠테이션

PathEye 공식 블로그 다운로드 받으세요!! 지속적으로 업그래이드 됩니다. 여러분의 의견을 주시면 개발에 반영하겠 습니다.

PowerPoint Presentation

tiawPlot ac 사용방법

Endpoint Protector - Active Directory Deployment Guide

7. 설치가 끝나면 오픈오피스를 실행합니다. 오픈오피스 설치 이 설명서는 여러분이 윈도우에 대한 기본적인 지식을 가지고 있다고 가정합니다. 따라서 일반적인 윈도우 시스템의 관리에 대해서는 언급하지 않습니다. 여기에서 설명하는 단계별 절차에 따라 윈도우 시스템에 오픈오피

PowerPoint Template

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

쉽게 풀어쓴 C 프로그래밍

Install stm32cubemx and st-link utility

윈도우시스템프로그래밍

<4D F736F F F696E74202D20C1A632C0E520C7C1B7CEB1D7B7A5B0B3B9DFB0FAC1A4>

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

슬라이드 1

Microsoft Word - src.doc

01장

Javascript

JAVA 플랫폼 개발 환경 구축 및 활용

4장기본프로그래밍2

PowerPoint Presentation

untitled

<322EBCF8C8AF28BFACBDC0B9AEC1A6292E687770>

인쇄하기, 내보내기, 이메일로 문서 보내기

Microsoft PowerPoint - e pptx

C스토어 사용자 매뉴얼

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

설치 순서 Windows 98 SE/Me/2000/XP 1 PC를 켜고 Windows를 시작합니다. 아직 컴퓨터에 프린터를 연결하지 마십시오. 2 PC에 P-S100 CD-ROM(프 린터 드라이버)을 삽입합니다. 3 설치 프로그램을 시작합니다. q CD-ROM의 PS1

Microsoft PowerPoint - gnu-w06-python_[실습]_day13-turtle-shape

vRealize Automation용 VMware Remote Console - VMware

MF3010 MF Driver Installation Guide

Title Here

6. 설치가시작되는동안 USB 드라이버가자동으로로드됩니다. USB 드라이버가성공적으로로드되면 Setup is starting( 설치가시작되는중 )... 화면이표시됩니다. 7. 화면지침에따라 Windows 7 설치를완료합니다. 방법 2: 수정된 Windows 7 ISO

Programming hwp

소규모 비즈니스를 위한 플레이북 여기서 다룰 내용은 다음과 같습니다. 1. YouTube 소개 2. YouTube에서 비즈니스를 위한 채널 만들기 3. 눈길을 끄는 동영상 만들기 4. 고객의 액션 유도하기 5. 비즈니스에 중요한 잠재고객에게 더 많이 도달하기

게임 기획서 표준양식 연구보고서

윈도우시스템프로그래밍

Microsoft PowerPoint _사용자매뉴얼.ppt

<4F B8A620C0CCBFEBC7D120C8B8B7CE20C0DBBCBAC0C720B1E2C3CA2E687770>

<4D F736F F F696E74202D20B5A5C0CCC5CDBAA3C0CCBDBA5F3130C1D6C2F75F32C2F7BDC32E >

Microsoft PowerPoint - CSharp-10-예외처리

e-비즈니스 전략 수립

슬라이드 1

미쓰리 파워포인트

슬라이드 제목 없음

Visual Studio online Limited preview 간략하게살펴보기

JAVA PROGRAMMING 실습 08.다형성

1. 자바프로그램기초 및개발환경 2 장 & 3 장. 자바개발도구 충남대학교 컴퓨터공학과

오버라이딩 (Overriding)

<41736D6C6F D20B9AEBCADBEE7BDC42E687770>

EEAP - Proposal Template

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

Transcription:

좋은소식이있어요! 당신의요청이승인되어서컴퓨터를 Windows 2003 으로업그레이드해준대요. 헤드퍼스트 C# 의일부프로젝트들은윈도우 8 이필요한스토어앱으로되어있습니다. 부록 ii 에서 WPF 를이용하여스토어앱으로만든프로젝트를데스크톱앱으로만들어봅시다. PDF 헤드퍼스트 C#, WPF 사용자가이드 수지는 1 년반정도가지나서야윈도우업그레이드를할수있게되었습니다. 정말놀라운소식이군요! 윈도우 8 이없다고요? 괜찮습니다. 헤드퍼스트 C# 의 3 판에서일부프로젝트를하기위해서는윈도우 8 과 Visual Studio 2013 이필요합니다. 하지만, 아직까지회사나집에서최신버전의윈도우를사용하는사람들이많지않습니다. 그래서 WPF() 가있는거죠. WPF 는조금오래된윈도우에서실행되는 Visual Studio 2008 과 2010 에서도동작합니다. WPF 에는 C# 의핵심기술이포함되어있습니다. 만약여러분이윈도우 8 을사용하고있더라도, WPF 를설치하고경험해보는것은큰도움이될것입니다. 이부록에서 WPF 를이용하여책에있는윈도우스토어앱프로젝트를데스크톱앱으로만들어봅시다. 새로운장입니다 1

같은프로그램, 새로운기술 WPF, 왜배워야되죠? WPF() 는.NET 으로쓰인프로그램의사용자인터페이스를만들기위한기술입니다. WPF 프로그램은일반적으로윈도우데스크톱의사용자인터페이스를보여줍니다. WPF는윈도우소프트웨어개발중에서하나의인기있는기술이기도하죠. 그리고 WPF 의친숙한환경과 C# 과.NET 의전문개발자들을위해서마이크로소프트개발팀에서많은노력을기울였습니다. 비주얼스튜디오에서 WPF 프로젝트만들기 비주얼스튜디오에서새 WPF 응용프로그램을생성하는것은데스크톱응용프로그램을생성하는것과비슷합니다. Visual Studio Express 2013 일경우, Visual Studio 2013 Express for Desktop 에디션을사용하세요 (for Windows 에디션은 WPF 프로젝트를생성하지않습니다 ). 여러분은또한 Visual Studio 2013 Professional, Premium 혹은 Ultimate 에디션을사용할수있습니다. 새프로젝트를생성할때, 파일 (File) 메뉴 > 새프로젝트 (New Project) > Visual C# 을선택하고, (WPF 응용프로그램 ) 을선택해주세요. WPF 프로그램은 XAML(Extensible Application Markup Language) 을이용해서사용자인터페이스를만듭니다. 이책에서윈도우스토어앱에관한내용을접한독자들에게는정말좋은소식이죠. 이책의대부분의윈도우스토어앱프로젝트들은약간의 XAML 코드수정혹은수정없이 WPF 로만들수있습니다. 앱바와페이지내비게이션같은것들은윈도우스토어앱과다릅니다. 부록에서는 WPF 에서대체가능한것들을보여줄거예요. 윈도우 8 과 Visual Studio 2013 을쓰고있는데, WPF 를해야하나요? C# 개발자들은 WPF 를다룰수있어야합니다 거의모든프로그래밍언어는수많은환경과운영체제에서사용할수있어야합니다. C# 도예외는아니죠. 여러분이만약 C# 전문개발자가되고싶다면, 다양한기술을접하고현장에서적용할수있어야합니다. 그리고 WPF는특히 C# 개발자들에게중요한기술이죠. 회사에서 WPF를이용하여많은프로그램을만들고있기때문입니다. 앞으로도계속해서그럴거고요. 만약여러분의목표가 C# 전문가라면, WPF는이력서의리스트에작성되어야할겁니다. WPF를배우는것은또한윈도우 8을사용하고헤드퍼스트 C# 에있는모든코드를작성한독자들에게좋은취미거리가될것입니다. 개발언어를학습하는데가장효과적인방법은같은문제를다양한방법으로접근하는거죠. 이부록에서는 WPF를이용하여책에있는프로젝트를만들기위한방향을제시해줍니다. WPF와윈도우 8로만든프로젝트를비교해서살펴보는것은여러분에게가치있는관점을제공해줍니다. 그리고이것은좋은개발자가되기위한하나의과정이기도합니다. 또한 Visual Studio 2010, Visual C# 2010 Express, Visual Studio 2008 에서 C# WPF 응용프로그램을 생성할수있습니다. Visual Studio 2008 혹은 2010 Express 에디션을사용한다면, 프로젝트파일은처 음에임시폴더에생성됩니다. 그리고저장 (Save) 혹은모두저장 (Save All) 을누를때까지는새프로젝 트창에서작성한위치에저장되지않습니다. WPF 는인터넷익스플로러나다른브라우저안에서실행하는 XAML 브라우저응용프로그램을만들수있습니다. 이부록에서는다루지않지만, 자세한사항은아래의사이트를참고하세요. http://msdn.microsoft.com/ko-kr/library/aa970060.aspx 마이크로소프트는 XAML 을사용한또다른기술이있습니다. 이를실버라이트 (Silverlight) 라고하는데, 자세한사항은아래의사이트를참고하세요. http://www.microsoft.com/korea/silverlight/ 아래의사이트에서프로젝트코드를내려받을수있습니다. http://www.hanbit.co.kr/exam/2165 2 Appendix ii 지금여기예요 4 3

시작해봅시다 부록에대한설명 이부록은 WPF 프로젝트를위해서헤드퍼스트 C# 3 판을대체하는내용을담고있습니다. 부록 을각장별로나눠서무엇을해야하는지에대한전반적인개요와어떤페이지가바뀌었고, 책의 어떤부분을읽어야하는지에대한가이드를제시합니다. Chapter 비주얼스튜디오가최신버전이아니라도, 프로젝트를만들수있습니다. 하지만조금힘들수있습니다. 마이크로소프트팀은 XAML 을편집하는기능에서특히 Visual Studio 2013 의사용자인터페이스 를향상시키는데많은노력을기울였습니다. 그리고헤드퍼스트 C# 에서한가지중요한것은개 발도구인비주얼스튜디오 IDE 를경험하고학습하는겁니다. 가능하다면, 비주얼스튜디오의최 신버전의사용을권장합니다. 하지만, 모든독자들이 Visual Studio 2013 을설치할수없다는걸잘알고있습니다 ( 예를들어, 회 사컴퓨터를사용하는독자들이새로운소프트웨어를설치하려면, 관리자권한이필요한경우도 있습니다 ). 이전버전의비주얼스튜디오로프로젝트들을수행하기힘들더라도, 여러분이이책을 잘활용했으면좋겠습니다. 우리가할수있는한많은지침을제공하기위해서최선을다했습니다. 그러나비주얼스튜디오의최신버전을사용하는독자대부분에게해를끼치지않도록학습의균 형을유지하고있습니다. 여러분이 Visual Studio 2010 혹은그이전버전의사용으로인해어려운점이있다면, 책에나오 는메뉴옵션들을여러분의화면에서찾을수없는경우입니다. 이경우에서는 XAML 과 C# 코드 를직접손으로입력하는것을권장합니다. 비주얼스튜디오에코드를복사및붙여넣기해도좋습 니다. XAML 코드가정확하다면, IDE 에서제공하는기능들을쉽게파악할수있습니다. 부록에서는다운로드할수있는책의모든소스코드를제공합니다. 메뉴옵션이없어서코드를복사 및붙여넣기를해야할경우에사용하면좋습니다. 소스코드의자세한내용은이책의웹사이트 (http://www.hanbit.co.kr/exam/2165) 를참고하세요. 여러분은또한코드를직접 http://hfcsharp.codeplex.com/ 에서다운로드할수있습니다. 부록에서는반드시 WPF 폴더에서코드를내려받아야합니다. 만약 WPF 프로젝트에서윈도우 스토어코드를사용하려고하면오류를만나게될거예요. 이부록에서는이책혹은 PDF 에서대체된부분의페이지를찾을수있습니다. 페이지번호를잘 참고해주세요. 만약여러분이킨들 (Kindle) 이나또다른 ebook 을사용한다면, 페이지번호가맞지 않을수도있습니다. 대신에해당챕터의소제목을찾아보세요. 예를들어이부록의 2 장에서종이 책의 72, 73 페이지를대체할 기초부터만들기 라는소제목이있습니다. 그러면 ebook 에서 2 장 목차에서해당하는소제목을찾을수있습니다 ( 연습문제인 83 쪽과연습문제정답인 85 쪽은목차에 나오지않습니다. 연습문제는각장마다있습니다 ). 이렇게부록의 PDF 를쉽게활용할수있습니다. 1 WPF 에서 Save The Humans 게임을만들수있습니다. 책에서대체된페이지는 56 페이지에서 91 페이지입니다. 게임을만들며, IDE 를온몸으로느껴봅시다 이책의첫번째프로젝트는재미있는게임을만드는과정을안내합니 다. 이프로젝트의목표는독자들이비주얼스튜디오 IDE 를사용하면 서사용자인터페이스의생성과 C# 코드를작성하는데익숙해지는것 입니다. 이책의 55 페이지까지읽고난후에, 이부록의 1 장을읽으면됩니다. 이부록은책의 56 페이지에서 91 페이지를대체합니다. WPF 버전의 Save the Humans 을다만들었다면, 2 장으로넘어가면됩니다. 이번장에서나타나는화면은 Visual Studio 2013 for Windows Desktop 입니다. ( 부록을번역하는시점에서 ) 비주얼스튜디오의최신버전이죠. 만약, 여러분이 Visual Studio 2010 을사용한다면, IDE 의창과메뉴옵션이다를거예요. 올바른메뉴옵션을찾을수있도록우리가도와줄겁니다. 여러분이 C# 의중요한개념을학습하는데집중할수있도록이리저리페이지를넘기는것을최소화하는데많은노력을기울였습니다. 1 장에서 55 쪽까지읽고, 부록을읽으면됩니다. 2 장에서는부록에서다섯페이지만읽고, 종이책을읽으면됩니다. 이책의 3 장에서 9 장까지는이전버전의윈도우에서구축할수있는데스크톱응용프로그램에집중하고있습니다. 4 Appendix ii 지금여기예요 4 5

처음부터시작하기 빈응용프로그램으로시작하기 모든앱은새프로젝트로시작합니다. 파일 (File) 메뉴에서새프로젝트 (New Project) 를선택하세요. 창에서 Visual C# > Windows 를누른뒤 WPF 응용프로 그램을선택하세요. 이름에 Save the Humans 을입력하고확인버튼을누르시 면됩니다. 1 파일이름이.cs 로끝나지않는다면여러분은실수로 JavaScript, Visual Basic 혹은 Visual C++ 로프로젝트를생성했을것입니다. 이창을닫고다시 Visual C# 을선택해서다시실행하세요. 프로젝트이름인 Save the Humans 를사용하고싶으면이전에실수로만들어진프로젝트를지우시면됩니다. 프로젝트가생성되면디자이너창이보입니다. 솔루션탐색기 (Solution Explorer) 에있는 MainPage.xaml 을 더블 - 클릭합니다. 디자이너의왼쪽코너에있는줌드롭 - 다운을찾아모두맞춤 (Fit All) 을선택해서화면을 축소합니다. 현재위치 XAML 메인윈도우와컨테이너 Main window Grid Canvas WPF UI 컨트롤 Rectangle StackPanel ProgressBar Target timer 틱 (Tick) 이벤트핸들러 C# 코드 Start button Enemy timer 틱 (Tick) 이벤트핸들러 클릭이벤트핸들러 methods StartGame() Ellipse Rectangle AddEnemy() AnimateEnemy() EndTheGame() 디자이너아랫부분은 XAML 코드입니다. 디자이너의 빈 페이지는빈게아닙니다 (XAML 그리드코드부분포함되어있습니다 ). 그리드는 HTML 페이지혹은워드문서의테이블과 비슷합니다. XAML 을이용하여창을다양한크기로늘리거나줄여볼거예요. 그리드를위한 XAML 코드는 IDE 가생성해줍니다. 이부분을잘살펴보세요. 행과열을추가할거예요. 디자이너는작업하고있는미리보기페이지를제공합니다. 빈페이지의기본검은색배경화면으로되어있군요. 이렇게컨트롤을포함하는그리드태그를열고닫는거군요. 행과열, 컨트롤을그리드에추가할때태그를열고닫는사이에코드를추가하면됩니다. Visual Studio 2012 와 2013 에서만이버튼들이보입니다. 이전버전에서는안보일거예요. 그리드라인표시를위해세개의버튼을사용합니다. 스넵핑을켜면각각컨트롤의위치를맞출수있습니다. 그리드라인에스넵핑을켜면그리드의격자선에맞게정렬할수있습니다. 이프로젝트는책의 1 장과거의비슷합니다. 이프로젝트는 1 부터 3 까지의과정이있습니다. 다음페이지로넘겨주세요. 56 Appendix ii 책에서 56~92 페이지를대체하는내용을부록에담았습니다. 부록에있는다른프로젝트들도마찬 가지로책을대체하는정보를제공합니다. 책을이한페이지, 한페이지다대체하지는않지만, 프로 젝트를만드는데필요한모든정보를여러분에게제공합니다. 지금여기예요 4 57

한걸음, 한걸음 2 오렌지색삼각형과선이나타날때그리드테두리를조정하면됩니다. 그리고여기를클릭해서그리드안에아래쪽행을만드세요. 빈페이지의템플릿에제목행을포함한 2 개의행과 3 개의열을가진그리드를 만들어봅시다. 여기에플레이영역이포함된중간부분에큰공간이있습니다. 테두리에서선과삼각형이보일때행과열을조정해봅시다. 행과열을추가하기위해서삼각형표시를보려면테두리와가까운창안쪽을클릭해야할거예요. WPF 앱의어떤해상도에서라도크기에맞게창이조절되어보여야합니다. 다음몇몇페이지에서는강력한도구인비주얼스튜디오 IDE 가제공하는많은기능을살펴보며학습할것입니다. 이책을통해 IDE 를사용하며 C# 을탐험하게됩니다. 이것은정말여러분의뇌를자극하는효과적인방법입니다. 3 창의위쪽테두리를이전페이지에서했던것처럼해봅시다. 이번에는왼편과오른편에각각선을넣어서두열 을만들어봅시다. 행의높이와열의너비는신경쓰지마세요. 여러분이어디에클릭하느냐에따라크기가다릅 니다. 아래에서고칠거예요. 행의높이와열의너비가다르다고걱정하지마세요. 다음페이지에서고칠거예요. 작업이완료되면, XAML 윈도우창으로가서이전페이지와같은간격의그리드를만들어봅시다. 열너비와행 높이가창의상단과측면에있는숫자와일치합니다. 앱은그리드의열과행을이용하여자동으로레이아웃을윈도우창의크기에맞게조정합니다. 행이추가되면선은파란색으로변하고, 테두리에서행높이의숫자가보입니다. 각행의높이는 * 에있는숫자가됩니다. 여기에서는숫자에신경쓰지않아도됩니다. 바보같은질문이란없습니다 이것이 3 번과정의왼쪽열의너비입니다. 이너비는디자이너에서보이는너비와일치합니다. IDE 에서이 XAML 코드를생성해줍니다. Q : 그리드에많은행과열이있는데, 회색선은뭐죠? A: 회색선은비주얼스튜디오가여러분의컨트롤을페이지에맞게잘배치할수있도록도와주는역할을합니다. 이회색선을버튼으로켜고끌수있습니다. 비주얼스튜디오밖에서앱을실행할때는디자이너에서이선들을볼수없습니다. 그리고여러분이클릭을해서새로운행을추가할때, XAML 코드가실제로변경됩니다. 컴파일하고실행할때추가된내용이적용될것입니다. Q : 잠시만요. 전 C# 을배우고싶은데, 왜 XAML 을배우는데시간을투자해야하나요? A: 왜냐하면 WPF 앱을만들때, 대부분 XAML 로디자인된사용자인터페이스로시작합니다. 또한비주얼스튜디오는사용자인터페이스를쉽게만들어주는멋진 XAML 에디터가있습니다. 이책의전반에걸쳐다양한유형의프로그램 (XAML 을사용하는윈도우스토어앱과데스크톱앱과 XAML 을사용하지않는콘솔응용프로그램 ) 을만들면서 C# 을조금더깊이이해할수있도록도와줍니다. 이제그리드의행과열을추가했습니다. XAML 그리드는다른컨트롤을담을수있는컨테이너컨트롤입니다. 그리드는셀로정의된행과열로구성되어있습니다. 각각의셀은버튼, 텍스트, 도형을나타내는다른 XAML 컨트롤을담을수있습니다. 그리드는창을쉽게꾸며줍니다. 화면의크기에맞춰행과열을다시조정할수있죠. 인간들이뭔가준비하고있어. 상황이별로좋지못한것같군. 58 Appendix ii 지금여기예요 4 59

크기설정하기 그리드설정하기 프로그램은다양한크기의화면에서작동될수있어야합니다. 그리드는이런일 을유연하게해주는좋은도구입니다. 그리드는행과열을특정 Pixel 의높이에 맞게설정할수있습니다. 그리고 Star(*) 를이용해서, 화면의크기나위치에상 관없이서로다른페이지의적정크기를유지할수있습니다. 1 왼쪽열의너비를설정합니다. 드롭 - 다운메뉴가나타날때까지첫번째열위의번호를가리키세 요. 픽셀을 Pixel 을선택해서 Star(*) 를자물쇠표시로변경합니다. 그리고숫자를클릭한다음 140 을입력하세요. 열의숫자는아래와 같아야합니다 창의테두리에서 120* 과 19* 같은숫자가보이지않는다면, 디자이너에서창의바깥쪽을클릭해보세요. 3 실수로가운데열의너비를 Pixel 로바꿨을경우, 다시 1* 로바꿀수있습니다. 가운데열과행을기본크기로만듭시다. 가운데열의너비가 로되어있어야합니다. 그렇지않은경우중간에있는 열의숫자를클릭해서 1 을입력하세요. 드롭 - 다운을사용하지마세요 (Star 로 놔두세요 ). 아래와같은그림을볼수있습니다. 그런다음 IDE 가자동으로크 기를조정하지않았는지, 다른열을한번확인해보세요. 만약다른열의크기 가변경되었다면과정 1, 2 에서설정했던너비로바꿔줍니다. XAML 과 C# 은대소문자를구분합니다! 예제코드와대문자와소문자가일치하는지확인하세요. 1* 을입력할때, IDE 는기본너비에맞게열의크기를설정합니다. 다른열도그렇게적용될것입니다. 만약그렇다면 160 픽셀로다시바꿔주세요. 2 오른쪽열과아래쪽행에도위와같이합니다. 오른쪽열에서 160, 아래쪽행에서 150 을입력하고, Pixel 을 선택하세요. 행과열을 Pixel 로설정하면너비와높이가고정되도록설정합니다. Star(*) 로설정하게되면그리드의나머지부분에맞게행과열이확대되거나축소될수있습니다. 디자이너에서너비와높이속성을 XAML 로알맞게변경해보세요. 만약너비와높이의속성을제거한다면, 그것은 1* 로설정하는것과같습니다. 열을 Pixel 로변경할때, 너비에비례한픽셀에서실제너비의픽셀로바뀝니다. 앱을꾸미는데능숙하지않아도괜찮아요. 앞으로앱을디자인하는것에대 해서많이이야기할거예요. 지금 부터는게임을만드는과정을살펴봅니다. 이책의마 지막부분에서여러분은모든것을정확하게이해할 수있을거예요. XAML 코드를봅시다. 그리드의코드부분으로갑니다. 그리고 XAML 윈도우창에서코드가잘만들어졌는지확인해봅시다. 구버전 IDE의디자이너에서열의크기를바꿀순있지만, 우리가실습했던것처럼 60 Appendix ii 잘되지않습니다. 만약구버전의 IDE를사용한다면, 행과열을만든후 XAML 에서행과열을수동으로입력하는것을추천합니다. 지금여기예요 4 61 4 잠시후, 그리드에컨트롤을추가할것입니다. 행과열을정의한후여기에컨트롤을배치할거예요. XAML 에서그리드의열이정의되는부분입니다. 3 개의열과 2 개의행을추가해서, 3 개의 ColumnDefinition 태그와 2 개의 RowDefinition 태그가있습니다. 행과열의드롭 - 다운을설정해서너비와높이의속성을변경했었죠. Visual Studio 2010 을쓴다면 IDE 가다르게보입니다. 열크기부분에마우스를올려놓으면, Pixel 과 Star 를선택하는이런상자가보입니다. 꼭대기에있는 <Grid> 태그는그리드전체를의미합니다. 그아래에그리드의일부들이있죠. 디자이너를이용해서아래행의높이를 150 으로설정했었죠.

컨트롤을추가해봅시다 그리드에컨트롤추가하기 버튼과텍스트, 사진, 프로그래스바, 슬라이더, 드롭-다운과메뉴를여러분의앱에어떻게채워넣을까요? 이것들을컨트롤 (control) 이라부르죠. 이제컨트롤을앱에추가할때가왔습니다. 그리드의행과열로이루어진셀안에컨트롤을추가해볼까요? 1 도구상자의 Common WPF Controls (Common WPF 컨트롤 ) 을펼친후제일왼쪽아래의셀로끌어놓습니다. IDE 에서도구상자가안보이면, 보기 (View) 메뉴로가서도구상자를열수있습니다. 자동으로숨겨지지않도록고정핀을사용하세요. 3 다음도구상자에있는 ALl WPF Controls(All WPF 컨트롤 ) 을펼칩니다. 맨아래의중앙셀에를끌어놓습니다. 그리고을맨아래오른쪽셀에놓습니다 ( 이미배치한 TextBlock 밑에놓으면됩니다 ). 그리고를중앙에놓습니다. 이제여러분의창에이렇게컨트롤이배치되어있어야합니다 ( 아래그림과다르다고요? 걱정하지마세요. 같이수정해봅시다 ). Canvas 컨트롤을추가할때빈상자처럼보일거예요. 조금있다가고쳐봅시다. 디자이너아래로가서 IDE 가생성한 XAML 태그를봅니다. 여러분이 컨트롤을끌어놓은위치에따라여백숫자가다르게보이고, 속성의 순서가다를수도있습니다. 도구상자에서핀을눌렀을때, 아래의탭에서도구상자를선택할수있습니다. 첫번째과정에서추가한버튼이여기있어요. 두번째과정에서추가한 TextBlock 컨트롤입니다. 같은셀에 ContentControl 도추가했었죠. XAML 버튼코드는여는태그와같이여기서부터시작합니다. 이것이속성입니다. 속성은이름과 =, 값으로이루어져있습니다 ( 이름 = 값 ). 여러분이추가한 ProgressBar 입니다. 이게뭘까요? ContentControl 입니다! 2 Drag a 를그리드의맨오른쪽아래의셀에끌어놓습니다. XAML 코드는아래와같이보일거예 요. 컨트롤이어느행과열에배치되어있는지어떻게알수있을까요? 도구상자에있는포인터를클릭하고 TextBlock 를선택해서움직여보세요. IDE 에있는 XAML 코드의여백속성이변경되는것을확인해보세요. XAML 을읽기쉽게하려고들여쓰기를했어요. 여러분도직접해보세요! 만약도구상자가보이지않는다면, IDE 의위쪽왼쪽모퉁이에있는도구상자의글씨를클릭하세요. 만약여기에도없다면보기 (View) 메뉴에서도구상자 (Toolbox) 를클릭하면, 도구상자를볼수있습니다. 중앙에있는 Canvas 컨트롤을선택합니다 ( 선택이안되면도구상자의포인터를눌러서 선택해보세요 ). XAML 창에는아래와같이보입니다. Canvas 컨트롤에대한 XAML 태그입니다. 이것은 <Canvas 로시작하고 /> 로끝납니다. 그사이에 Grid.Column= 1 (Canvas 를가운데열에배치 ) 과 Height= 100 속성이있습 니다. XAML 창에그리드를클릭해서다른컨트롤을선택해보세요. 도구상자에서컨트롤을끌어놓을때, IDE 는자동으로여러분이끌어놓은위치의 XAML 코드를생성합니다. 62 Appendix ii 지금여기예요 4 63 4... 이버튼을눌러보세요. 문서개요 (Document Outline) 창이나타납니다. 어떻게사용하는지아시나요? 나중에배워봅시다.

속성값을넣어봅시다 속성값으로컨트롤이변하는모습을봅시다 비주얼스튜디오 IDE 는컨트롤을미세하게조절할수있습니다. IDE 의속성창은컨트롤의외형과 동작을변경할수있습니다. 1 Button 컨트롤의텍스트를바꿔봅시다. 여러분이그리드로끌어놓은 Button 컨트롤을오른쪽클릭한뒤, 메뉴에서텍스트편집 (Edit Text) 을 선택합니다. Start! 로변경한후 XAML 의 Button 태그를확인해보세요. 텍스트를편집할때 Esc 키를눌러편집을끝내면됩니다. 컨트롤들도이렇게하시면됩니다. 이전내용을편집 (Edit) > 실행취소 (Undo 혹은 Ctrl-z) 로돌릴수있습니다. 실행취소를몇번해보면취소한만큼의이전내용으로돌아갑니다. 여러분이컨트롤을잘못선택했을경우편집메뉴에있는몇항목들이비활성화됩니다. 또한 Esc 키를눌러컨트롤의선택을해제할수있습니다. 만약 StackPanel 과 Grid 와같은컨테이너안에컨트롤이선택되어있다면, Esc 키로컨테이너를선택할수있습니다. Esc 키를여러번쳐야하는경우도있죠. 3 창의크기와제목을바꿔봅시다. XAML 메인윈도우와컨테이너 아무컨트롤을선택한다음, XAML 편집기에서 <Window> 태그가보일때까지 Esc 키를누릅니다. Main window Grid Canvas WPF UI 컨트롤 Rectangle StackPanel Ellipse ProgressBar Rectangle Target timer 틱 (Tick) 이벤트핸들러 C# 코드 Start button 클릭이벤트핸들러 지금여기에있어요! Enemy timer 틱 (Tick) 이벤트핸들러 methods StartGame() AddEnemy() AnimateEnemy() EndTheGame() 속성창의이름칸에 startbutton 을입력합니다. Button 컨트롤에텍스트를입력할때, XAML 의 Content 속성이업데이트됩니다. 2... 속성창에서 Button 을수정합니다. IDE 에서 Button 을선택하고, 아래의오른쪽코너에속성창으로갑니 다. 컨트롤의이름을 startbutton 으로변경하고, 셀의중앙에컨트롤 을배치해봅시다. Button 의속성이맞는지확인하려면, 마우스오른쪽 을클릭해서소스보기 (View Source) 를선택하면됩니다. XAML 창에서 <Button> 태그를확인할수있습니다. 이작은네모난상자는속성이설정되었는지알려줍니다. 색칠된상자는속성이변경되었다는뜻이고, 빈상자는기본값으로설정되어있다는것을의미합니다. XAML 편집기에서 <Window> 를클릭합니다. <Window> 태그에 Height 와 Width 속성이있습 니다. 창에서이속성들을변경했을때잘반영되는지살펴봅시다. Width 와 Height 를각각 1000 과 700 으로설정해줍니다. 창은바로입력한크기를반영하죠. 디자 이너에서전체창을보기위해서확대 / 축소드롭 - 다운에서 모두맞춤 (Fit all) 옵션을선택합니 다. 다른행과열의픽셀이유지되면서, 크기가창에맞게바뀐것을볼수있습니다. 속성창의 공용 (Common) 섹션을확장해서, Title 속성에 Save The Humans 를입력합니다. 창의제목이 바뀐걸볼수있습니다. TextBlock 과 ContentControl 은그리드의오른쪽아래의셀에있습니다. 공용과레이아웃섹션을확장합니다. 버튼은 Width 와 Height 를자동 (Auto) 로설정해줍니다. 오른쪽메뉴를이용해서텍스트편집을눌러 Button 의텍스트를변경했을경우, IDE 는이속성의내용을업데이트합니다. HorizontalAlignment 와 VerticalAlignment 속성의, 버튼으로컨트롤을셀의중앙에배치하세요. 페이지에 Button 을끌어놓을때 IDE 는 Margin( 여백 ) 속성을이용해서셀의정확한위치에놓습니다. 작은네모상자를클릭하고, 메뉴에서다시설정 (Reset) 을선택하면 Margin 이 0 값으로재설정됩니다. 속성들이조금다르게정렬되어있어도괜찮아요! 구버전의 IDE 에서는아이콘대신 Center 가있습니다. XAML 창으로돌아가서 XAML 코드가업데이트되었는지확인해보세요. StackPanel 을사용해서 TextBlock 와 ContentControl 을묶어봅시다. 셀에서 TextBlock 을위로배치하고 ContentControl 을아래로배치합니다. Shift 키로두컨트 롤을선택한후마우스오른쪽버튼을클릭한후, 팝업메뉴에서그룹으로묶기 (Group Into) > StackPanel 을선택합니다. 이제페이지에새로운 StackPanel 컨트롤이추가되었습니다. 두컨 트롤을클릭해서 StackPanel 을선택할수있습니다. StackPanel 은 Grid, Canvas 와많은부분이비슷합니다. 공통점은다른컨트롤을묶어주죠 ( 이 것을컨테이너라고부릅니다 ). 컨테이너는페이지에서보이지않습니다. 여기에서는셀에서윗 부분의 TextBlock 와아랫부분의 ControlPanel 을묶은후 StackPanel 을만들면서 Grid 의셀을 대부분채워줍니다. StackPanel 을선택한후, 마우스오른쪽버튼을눌러레이아웃 > 모두다 시설정을클릭하면속성들이빠르게다시설정됩니다 ( 셀의수평과수직의레이아웃이 Stretch 로정렬됩니다 ). 그리고 TextBlock 과 ContentControl 에오른쪽버튼을클릭해서레이아웃을모 두다시설정해줍니다. ContentControl 을설정할때, 수평과수직정렬을이용해컨트롤을가 64 Appendix ii 지금여기예요 4 65 운데로배치합니다. 4 5 TextBlock 을수정해서텍스트와글꼴크기를바꿔봅시다. 마우스오른쪽버튼을눌러텍스트편집을이용해서 TextBlock 를 Avoid These 로변경합니 다 ( 텍스트입력이끝나면 Esc 키를눌러편집을끝냅니다 ). 속성창의텍스트섹션을확장해 서글꼴크기에 18px 를입력합니다. 이렇게설정하면텍스트가두줄로되는경우가있습니 다. 그렇다면, TextBlock 컨트롤을옆으로조금늘려줍니다. 마우스를이동하면 StackPanel 주변에상자가보일거에요. StackPanel 에있는 TextBlock 과 ContentConrol 에서각각오른쪽버튼을클릭해서레이아웃을다시설정해주세요.

게임이하고싶군요 컨트롤로게임동작하게하기 컨트롤은제목과캡션과같은보이는것들만있는게아닙니다. 컨트롤은게임이동작할수있도록하는중심역할을합니다. 컨트롤을추가해서플레이어들이게임을할수있도록해봅시 다. 여러분이다음에만들것은다음과같습니다. 게임배경에그라데이션을넣을거에요. 그리고아래쪽행에서뭔가해봅시다. 구버전의비주얼스튜디오에서색상을편집하는사용자인터페이스가달라도, 색상을편집할수있습니다. 문서개요창에서원시적인방법으로편집할수있지만, Visual Studio 2010 에서는템플릿을시각적으로생성하는게쉽지않습니다. 구버전의 IDE 에서템플릿을생성하는가장쉬운방법은헤드퍼스트랩에서소스코드를내려받아서 <Windows.Resources> 에서 </Windows. Resources> 태그까지복사합니다. 그리고이것을 <Grid> 시작태그위에붙여넣습니다. 웹사이트의 WPF 폴더에서내려받아야합니다. 그리고 ContentControl 을선택한뒤, 속성창에서 Template 속성을 EnemyTemplate 로설정합니다. 적군은악날한외계인같이생겼습니다. 이부록에서 44, 45 페이지를참고하세요. ProgressBar 를이만큼늘릴거에요. 그리고템플릿으로적을이렇게만들어봅시다. 3 적템플릿을생성해봅시다. 게임화면에서같은모양의많은적군들이움직여야합니다. 다행히도, XAML 에서많은컨트롤을같은적군모양으로만들 어주는템플릿 (Template) 을제공해줍니다. 다음, 문서개요 (Document Outline) 창에서 [StackPanel] 을펼친후, [ContentControl] 에서마우스오른쪽클릭합니다. 그리고템플릿편집 (Edit Template) > 빈항목만들기 (Create Empty ) 메뉴를 선택합니다. EnemyTemplate 으로이름을입력하고확인을누르면, XAML 창에템플릿이추가됩니다. 움직이는적들은여기서보이지않습니다. 적군이표시되도록컨트롤을추가하고크기를설정하기전까지디자이너창에는어떤템플릿도표시되지않습니다. 뭔가잘못됐다고걱정하지마세요. 취소 (Undo) 하고다시하면됩니다. 템플릿이선택되지않을경우, 문서개요창에서그리드를선택하세요. IDE 의오른쪽부분에서문서개요를클릭해서열수있습니다. 1 2 ProgressBar 를수정해봅시다. 그리드의중앙아랫부분의셀에서 ProgressBar 를마우스오른쪽클릭합니다. 레이아웃 > 모두다시설 정을선택합니다. 그리고속성창의레이아웃섹션으로가서 Height 에 20 을입력합니다. XAML 창에 서레이아웃과관련된속성을찾아서 Height 가추가되었는지확인해봅시다. Canvas 컨트롤을게임플레이영역으로바꿔봅시다. 중앙에 Canvas 컨트롤놓은거기억하시죠? 그런데지금찾기가힘드네요. 도구상자에 서 Canvas Control 을놓을때, 잘보이지않습니다. 그러나, 찾기쉬운방법이있습니다. XAML 창에서툴팁박스에서문서개요 (Document Outline) 라고쓰인 다. 그리고버튼을클릭해서 Canvas 컨트롤을선택합니다. Canvas 컨트롤을선택했으면, 속성창의이름칸에 playarea 를입력합니다. 왼쪽의탭을클릭해서그라데이션의시작색을선택하세요. 그리고오른쪽탭으로그라데이션의끝나는색을선택하세요.. 버튼을클릭합니 Canvas 컨트롤에이름을붙인후, 문서개요창을닫으세요. 그리고 속성창에있는, 버튼 (Stretch) 을눌러수평과수직으로정 렬하고, Margin( 여백 ) 을다시설정합니다. 그리고 Width 와 Height 에있는 이름이변경되면문서개요창에 [Canvas] 대신 playarea 라고뜹니다. 버튼 (Auto) 을각각누릅니다. 그리고 Column 에 0 과 ColumnSpan(Column 의오른쪽 ) 에 3 을입력합니다. 마지막으로, 속성창의브러시 (Brush) 섹션에서 Canvas 에그라데이 션을주기위해 버튼 ( 그라데이션브러시 ) 을클릭합니다. 편집기 아랫부분의왼쪽과오른쪽탭을클릭해서그라데이션의시작색과 끝나는색을선택하고색상을클릭하세요. 보기 (View) > 다른창 (Other Windows) > 문서개요 (Document Outline) 에서볼수있습니다. 문서개요를이용해 StackPanel, TextBlock, Grid 컨트롤을수정해봅시다. 문서개요로돌아옵니다 ( 문서개요창의윗부분에이보이면, 를클릭해서 Window 로돌아옵니다 ). 컨트롤을선택해서, 수평과수직이중앙으로정렬되어있는지, 여백이설정되었는지확인합니다. 마찬가지 로 TextBlock 도확인합니다. 그리고속성창에서색상편집기를통해서 Foreground 속성을흰색으로설정해줍니다. 마지막으로 Grid 를선택합니다. 속성창의브러시 (Brush) 섹션을열어서, 단색브러시를선택한후, Background 를검정색으로만들어주세요. 66 Appendix ii 지금여기예요 4 67 4 5 현재새로만든템플릿이선택되어있습니다. 그런데템플릿이보이지않네요. 다음과정에서템플릿을보이게합시다. 만 약컨트롤템플릿의바깥부분을클릭했다면, 여러분은문서개요 (Document Outline) 를열어서다시선택할수있습니다. ContentControl 을오른쪽클릭해서템플릿편집 (Edit Template) > 현재항목편집 (Edit Current) 를선택하면됩니다. 적템플릿을편집해봅시다. 빨간색원을템플릿에추가해봅시다. 도구상자에있는를더블클릭해서원을추가하세요. Ellipse 의 Height 와 Width 의속성을 100 으로설정하세요. 그러면 셀에서원이보일거에요. Margin, HorizontalAlignment, VerticalAlignment 속성의옆에 작은네모상자를눌러, 다시설정을선택합니다. 속성창의브러시섹션으로가서단색브러시 (Solid-color brush) 를선택합니다. 긴색상바안의표식을윗부분으로끌어서, 원형에빨간색을칠합 니다. 그런다음정사각형의색상선택부분을클릭해서오른쪽위 의코너로마우스를끌어놓습니다. ContentControl 의 XAML 코드는이제이렇게보일겁니다. 디자이너에서원형이보일때까지아무데나클릭하지마세요. 템플릿이선택되는것을유지하려고합니다. 오른쪽위의모서리를클릭해서, 이색상을선택하세요. XAML 윈도우창을스크롤해서 EnemyTemplate 가정의된것을확인해보세요. AppName 리소스가밑줄과같아야합니다. 이부분을클릭하면색상편집기가나옵니다. TextBlock 을흰색으로바꿔주세요. 이제페이지를꾸미는작업이거의끝났습니다! 마지막단계를향해페이지를넘겨주세요.

여러분이만든창을점검해봅시다 6 Canvas 에사람을추가해봅시다. 디자이너에서사람을추가하는과정을똑같이따라했을때, XAML 코드는다음과같습니다. 7 사람을추가하기위한두가지옵션이있습니다. 첫번째는다음세단계를따라하면됩니다. 두번째는조금더빠른방법인데 XAML 코드에 4 줄만추가하면됩니다. 여러분이선택하세요! Ellipse 컨트롤을 Canvas 에추가하기위해서셀중앙의 Canvas 컨트롤을선택한후, 도구상자의모든 XAML 컨트롤섹션에서 Ellipse 를더블 - 클릭합니다. Canvas 컨트롤을다시선택해서 Rectangle 을더블클릭합니다. Rectangle 은 Ellipse 의바로위에추가될것입니다. Rectangle 을 Ellipse 아래로끌어놓습니다. Shift 키를누른채 Ellipse 를클릭한후, 두컨트롤모두선택되게합니다. 선택한부분에서오른쪽버튼을눌러그룹으로묶기 (Group Into) > StackPanel 을선택합니다. Ellipse 를선택해서단색브러시속성을이용해서흰색으로바꿉니다. 그리고 Width 와 Height 속성을 10 으로합니다. 다음 Rectangle 를선택해서, 역시흰색으로바꾼후 Width 를 10, Height 를 25 로합니다. 문서개요창에서 playarea 에있는 StackPanel 을선택하세요 ( 속성창에서형식이 StackPanel 인지확인하세요 ). 여백을다시설정한후, Width와 Height 속성에있는버튼 ( 자동으로설정 ) 을클릭하세요. 그리고이름칸에 human 을 입력합니다. 여기에생성된 XAML 코드가있습니다. Ellipse 와 Rectangle 의 Stroke 속성이검은색으로설정되어있을겁니다 ( 만약보이지않는다면, 한번추가해보세요. 무 슨일이일어날까요?) 문서개요창으로돌아와새로운컨트롤이어떻게나타나는지확인해봅시다. 만약 human 이 playarea 아래에있지않다면, human 을마우스로끌어서 위의그림처럼만들어주세요. Game Over 텍스트를추가합니다. 게임이끝났을때, 게임이끝났다는메시지를표시해야합니다. TextBlock 컨트롤 을추가해서글꼴을설정하고이름을입력해봅시다. Canvas 를선택하고도구상자의 TextBlock 을끌어다놓습니다. 속성창의 TextBlock 의이름칸에 gameovertext 를입력합니다. 속성창의텍스트섹션을펼친후글꼴을 Arial Black, 글꼴크기를 100px 로하고, Bold 와기울임꼴을선택합니다. TextBlock 을끌어서, Canvas 의중앙에놓습니다. 그리고, 텍스트에 Game Over 를입력합니다. 만약 XAML 창에이코드를입력한다면 </Canvas> 태그바로위에코드를입력하세요. 이것은 Canvas 안에사람이있다는것을의미합니다. 과정 2 에서 Canvas 컨트롤의이름을 playarea 로해서, 문서개요창에이렇게보입니다. 컨트롤에마우스를올려보세요. Canvas 에컨트롤을끌어놓을때위와왼쪽의속성값은끌어놓은위치값으로설정됩니다. 만약위와왼쪽의속성값을바꾸고싶으면컨트롤을움직이면됩니다. 8 9 사람을끌어넣을타깃포탈을추가해봅시다. Canvas 에마지막컨트롤을추가합니다. 플레이어가사람을포탈에끌어넣을겁니다 (Canvas 가어디에있어도 상관없습니다 ). Canvas 컨트롤을선택해서 Rectangle 컨트롤을위에놓습니다. 속성창의브러시섹션에서 그라데이션을줍니다. 그리고 Height 와 Width 속성을 50 으로합니다. Rectangle 을 45 도로회전해서다이아몬드모양으로바꿔봅시다. 속성창의변형섹션을열어서 을눌러 Angle 에 45 를입력합니다. 마지막으로, 속성창의이름칸에 target 을입력합니다. 몇가지사항을점검해봅시다. 문서개요창을열어서 human 의 StackPanel, gameovertext 의 TextBlock, target 의 Rectangle 이 PlayArea 의 Canvas([Grid] 아래에있음 ) 아래에있는지확인해봅시다. 그리고 playarea 의 Canvas 컨트롤을선택해서, Height 와 Width 가자동 (Auto) 으로설 정되어있는지확인해봅니다. 이모든사항은찾기힘든버그를발생시킵니다. 여러분의 문서개요창은왼쪽그림과같아야합니다. 마지막으로, 속성창의이름칸에 target 을입력합니다. 축하합니다. 메인윈도우를완성했습니다! 버튼을클릭하여 그림없네요 회전버튼 human 과 gameovertext, target 컨트롤이 playarea 컨트롤아래에있는지, playarea 를펼쳐서확인했습니다. 여기에서컨트롤의순서는상관없습니다 ( 컨트롤들을위아래로움직여보세요. 대신위의그림처럼들여쓰기가맞아야합니다 ). 컨테이너컨트롤안에컨트롤의포함관계를문서개요창을통해알수있습니다. 68 Appendix ii 지금여기예요 4 69

컨트롤 지금까지사용자인터페이스를만들어보면서컨트롤이무슨역할을하는지에대한감각을키웠습니다. 컨트롤의속성이무엇을 하는지, IDE 의속성창에어디에있는지찾아서연결해보세요. Content XAML 속성 IDE 의속성창어디에서찾을수있나요? 속성창꼭대기 무엇을하죠? 컨트롤의크기를알수있습니다. 게임만들준비가됐습니다 이제코딩을할수있습니다. 여러분은창의기초가되는그리드를설정하고, 컨트롤을추가했습니다. 현재위치 XAML 메인윈도우와컨테이너 WPF UI 컨트롤 C# 코드 Height Rotation Fill x:name 힌트 : 속성을찾기위해윈도우속성창에있는검색속성을이용할수있습니다. 검색한속성중의일부는모든컨트롤의속성에존재하지않습니다. 컨트롤의각도를설정할수있어요. C# 코드로특정컨트롤을조작하기위해사용합니다. 컨트롤의색상 컨트롤안에보이는텍스트를바꿀때사용합니다. 해답은 79 페이지에있습니다. Main window Grid 첫번째단계에서프로젝트를생성하고그리드를설정했습니다. Canvas Rectangle StackPanel Ellipse ProgressBar Rectangle 그리고창에컨트롤을추가했습니다. 다음단계에서는이들을조작하기위한코드를작성할거에요. Target timer 틱 (Tick) 이벤트핸들러 Start button 클릭이벤트핸들러 Enemy timer 틱 (Tick) 이벤트핸들러 methods StartGame() AddEnemy() AnimateEnemy() EndTheGame() 비주얼스튜디오는창을꾸미기위한유용한도구를제공해줍니다. XAML 코드를자동으로생성해주죠. 이걸로뭔가하기엔부족합니다. 이제여러분이나서야할차례입니다. 70 Appendix ii 지금여기예요 4 71

다음단계로 다음에해야할것 이제부터재미있는부분입니다. 게임이동작하는코드를추가하기위한세가지단계가있습니다. 첫번째적군을움직이게한다음플레이어와상호동작할수있도록만듭니다. 마지막으로게임을조금다듬어서멋지게만들어봅시다. 먼저, 적을움직여봅시다. 첫번째할일은시작버튼을눌렀을때적들이계속화면을가로질러움직이게하는 C# 코드를작성하는겁니다. 메서드추가하기 이제코딩할때가왔습니다. 먼저메서드 (method) 를추가해봅시다. IDE가생성해준코드로쉽게시작할수있습니다. 페이지를편집할때, 컨트롤을더블-클릭하면 IDE는코드를자동으로생성해줍니다. 페이지디자이너에서 Start 버튼을더블-클릭합니다. 여러분은플레이어가 Start 버튼을클릭할때, 게임을시작하는코드를추가할것입니다. 다음코드를팝업창에서볼수있습니다. 게임이동작하려면프로그래스바가카운트다운되어야합니다. 사람이움직여서적에부딪치거나정해진시간이다되었을때게임을종료해야합니다. 그다음, 게임을해봅시다. 많은프로그래머들이코드를조금씩점진적으로작성하며프로젝트를진행합니다. 다음단계로넘어가기전에요구사항을확인합니다. 즉, 이프로그램의나머지부분도이런방식으로진행합니다. 여러분은 Canvas 컨트롤에움직이는적을 AddEnemy() 의메서드에추가하는코드를작성할겁니다. 페이지에움직이는적들을채우는것으로시작해봅시다. 게임의나머지부분을만드는데도움이될겁니다. 필요한메서드를만들어봅시다 중괄호안을클릭해서괄호 () 와세미콜론 ; 을포함하는문장을입력해주세요. Button 컨트롤을더블 - 클릭할때, IDE 는이메서드를생성합니다. 플레이어가 Start! 를클릭할때게임을시작합니다. 그냥텍스트를입력했는데빨간색물결모양의선이왜나올까요? 이것은 IDE 가뭔가잘못되었다는것을알려주는것 입니다. 물결모양의선을클릭하면, 오류를고치는데도움을주는작은파란색상자가나타납니다. 작은파란색작은상자위에서팝업되는 수있습니다. 한번클릭해보세요. 빨간색물결모양의선은 IDE 가여기에문제가있다는것을알려줍니다. 작은파란색상자는 IDE 가이문제를해결할수있는방법이있다는것을의미합니다. 이아이콘을클릭합니다. 그러면메서드스텁을생성하라는상자를볼 또한 IDE 는이것을 XAML 에추가합니다. 한번찾아보세요. 2 장에서이것이무엇인지알아봅니다. 템플릿을사용해서빨간색원을만듭니다. 템플릿을수정해서나쁜외계인처럼만들어봅시다. 마지막으로, 적을외계인모습으로만들어볼까요? 바보같은질문이란없습니다 Q : 메서드가뭐예요? Q : IDE가메서드를생성해주나요? A: 메서드는코드블록의이름입니다. 2장에서메서 A: 네 현재까지는요. 메서드는프로그램의한부분드에대해서조금더이야기해봅시다. 을만드는기본단위입니다. 여러분은코딩을하면서많은메서드를작성하고, 메서드를사용할겁니다. 72 Appendix ii 지금여기예요 4 73

지적이고센스있게 메서드에코드채워넣기 이제프로그램이뭔가해야할차례입니다. IDE에서메서드스텁을생성해서 Start 버튼의메서드를만들었습니다. 메서드에코드를입력해봅시다. 여기에있는 C# 코드와정확하게일치해야합니다. 오타로실수하기쉽습니다. C# 코 드를입력할때, 대소문자가정확 하게일치해야합니다. 괄호와콤마, 세미콜론을잘 4 AddEnemy() 메서드를채우기전에, 파일의위쪽에한줄의코드를추가합니다. public sealed partial Main- Window : Window 로시작하는줄을찾아서중괄호 이후에내용을입력합니다. 1 IDE 가생성한메서드스텁의내용을지웁니다. 확인해보세요. 하나라도빠지면프로그램은동작하 지않습니다. 이것을필드 (Field) 라고부릅니다. 4 장에서조금더자세히배워봅니다. 2 이것을선택해서지우세요. 12 장에서예외에대한것을배울겁니다. 코드를추가합니다. Content 를메서드몸체에입력합니다. IDE 는키워드를제안하는인텔리센스창을띄울 것입니다. 리스트에서 ContentControl 을선택하세요. 5 이라인은적컨트롤객체에 Children ( 자식 ) 이라고불리는컬렉션을추가합니다. 8 장에서컬렉션에 대해배워봅니다. 아래내용을 AddEnemy 메서드에입력해봅시다. 빨간색물결의밑줄이보입 니다. 여기에메서드스텁을추가하면 AnimateEnemy() 에있는밑줄은사라 집니다. PlayArea 에물결모양의밑줄이보인다고요? XAML 편집기로돌아가서 Canvas 컨트롤의이름이 playarea 로되어있는지확인해보세요. XAML 과 C# 의코드위치를바꾸고싶다면윈도우위의탭을이용하세요. 6 작은파란색상자를이용하여버튼을누르면 AnimateEnemy() 메서드스텁이생성됩니다. AddEnemy() 에서했던것처럼말이죠. 이번에는 enemy, p1, p2, p3이라고불리는 4개의매개변수 (parameter) 가추가됩니다. 메서드의위쪽에있는 3개의매개변수를수정해봅시다. p1을 from으로, p2를 to, p3을 propertytoanimate로수정합니다. 그리고 int 유형을 double로수정합니다. 3 마지막으로첫번째줄에서이코드를추가하세요. new 를입력할때다른인텔리센스창이나타납니다. 2 장에서메서드와 매개변수에대해배워봅니다. 이라인은새로운 ContentControl 객체를생성합니다. 3 장에서객체와 new 키워드를, 4 장에서 enemy 와같은참조변수를배웁니다. IDE에서메서드스텁을 int 유형으로생성할겁니다. 이것을 double 로바꿔주세요. 4장에서유형에대해배워봅니다. 다음페이지에서프로그램을실행해봅시다. 74 Appendix ii 지금여기예요 4 75

OK, 이정도면꽤훌륭해요! 메서드를끝내고, 프로그램을실행해봅시다 여러분의프로그램은실행할준비가거의다되었습니다. 이제남은부분은 AnimateEnemy() 메서드를마무리하는겁니다. 아직안된다고좌절하지마세요. 아마 프로그래밍을할때콤마나괄호같은것이빠져있을수도있어요. 빠져있는부분 이나오타를조심해야합니다! 1 C# 에서.NET 라이브러리의코드를사용할수있는이러한구문들은 2 장에서배워봅니다. 2 파일의윗부분에 using 문을추가해주세요. 파일맨위쪽으로이동합니다. IDE 는 using 으로시작하는몇개의라인들을 생성해줍니다. 아래의리스트밑에서한라인을추가해주세요. 적이움직이는애니메이션코드를추가해봅시다. 이줄을조금더쉽게작성할수있습니다. 인텔리센스창을이용해서코드를입력해봅시다. 마지막부분에세미콜론입력하는것을잊지마세요. 이전페이지에서 AnimateEnemy() 메서드를만들었습니다. 이제코드를추가할차례입 니다. 적들이화면을가로지르며움직이게만들어봅시다. 이 using 문은.NET 프레임워크에서적들이움직이는애니메이션코드를사용할수있게해줍니다. 아직물결모양의빨간색선이보인다구요? IDE 는그문제가뭔지알고있습니다. 아직물결모양의빨간색밑줄이보인다면, 그 것은코드의일부분이잘못입력되었다는것 을의미합니다. 이책은수많은사람의테스트 를거쳤습니다. 잘못된부분이없는지꼼꼼하 게확인했습니다. 그러므로프로그램이돌아가 게하려면이책과똑같이입력하시면됩니다. 4 장에서객체 이니셜라이저에대해배워봅니다. 4 5 프로그램을실행해봅시다. IDE 에서버튼을찾아프로그램을실행해봅시다. 이제프로그램이실행됩니다! 프로그램을실행할때, 메인윈도우가뜹니다. 그리고 Start! 버튼을몇번클릭해보세요. 클릭할때마다원 모양의도형이캔버스를가로지르며움직일겁니다. IDE 에서창들을다시설정하고싶을때, 창 (Window) 메뉴 > 창레이아웃다시설정 (Reset Window Layout) 을하면됩니다. 이버튼을누르면프로그램이시작합니다. 훌륭하네요! 우리가약속했던것처럼오래걸리진않았죠? 조금만더작업을해서, 게임을만들어봅시다. 애니메이션에대해서는 16 장에서배울거예요. 이코드는적이 playarea 를가로지르며움직이게해줍니다. 만약여러분이숫자 4, 6 을수정한다면, 적들을느리거나빠르게만들수있습니다. 만약에적들이움직이지않거나플레이영역을벗어난다면, 코드를다시확인해보세요. 단어나괄호가빠져있을수도있습니다. 3 코드를살펴봅시다. 오류가있으면안됩니다. 오류목록창이비어있어야하죠. 오류목록이보인다면, 그오류를더블-클릭하세요. IDE는문제를추적하기위해여러분의커서가오류가발생한부분으로이동합니다. 오류목록창을볼수없다면메뉴에서보기 (View) > 오류목록 (Error List) 을선택하세요. 2 장에서오류창과코드 디버깅에대해배워봅니다. 6 프로그램을멈춰봅시다. Alt + Tab 을눌러 IDE 로돌아와서도구모음 (Toolbar) 에있는버튼의위치에버튼으로바뀐모두중단 (break), 디버깅중지 (stop), 다시시작 (restart) 을확인하세요. 디버깅중지를눌러프로그램을멈춰봅시다. 76 Appendix ii 지금여기예요 4 77

여기는어디고, 뭘해야하죠? 지금까지한일 축하합니다! 여러분은프로그램이돌아갈수있게만들었습니다. 아직은게임을완성하지않았지만, 이것은시작에불과합니다. 잠시뒤를돌아보고, 무엇을했는지살펴볼까요? 현재위치 70 페이지의 누가무엇을하나요? 의연습문제해답이 여기있습니다. 앞으로, 여러분에게십자퍼즐과 연습문제에대한답을제공합니다. 정답 XAML 메인윈도우와컨테이너 WPF UI 컨트롤 C# 코드 지금까지사용자인터페이스를만들어보면서컨트롤이무슨역할을하는지에대한감각을키웠습니다. 컨트롤의속성이무엇을 하는지, IDE 의속성창에어디에있는지찾아서연결해보세요. Main window Rectangle ProgressBar Target timer 틱 (Tick) 이벤트핸들러 Enemy timer 틱 (Tick) 이벤트핸들러 Content XAML 속성 IDE 의속성창어디에서찾을수있나요? 속성창꼭대기 무엇을하죠? 컨트롤의크기를알수있습니다. Grid Canvas StackPanel Start button 클릭이벤트핸들러 methods StartGame() Height 컨트롤의각도를설정할수있어요. Ellipse Rectangle AddEnemy() AnimateEnemy() EndTheGame() Rotation C# 코드로특정컨트롤을조작하기위해사용합니다. 사용자인터페이스를만들며순탄하게시작했었죠. 이단계에서 C# 코드를작성하며동작하는게임을만듭니다. Fill 컨트롤의색상 그러나앱이실제로동작할수있게하는 C# 코드가필요했습니다. x:name 컨트롤안에보이는텍스트를바꿀때사용합니다. 비주얼스튜디오는일부코드를자동으로생성해줍니다. 자동으로생성한코드만으로아무것도할수가없죠. 앱을만들기전에뭘해야할지생각하고, 코드를작성해야합니다. Canvas 컨트롤의이름에 playarea 로입력한걸기억나나요? XAML 의 x:name 속성에있습니다. 이것은 Canvas 에서 C# 코드를쉽게작성할수있도록도와줍니다. 78 Appendix ii 지금여기예요 4 79

틱틱틱 타이머추가하기 게임을동작하는하나의중요한요소를추가해봅시다. 이게임은적들이계속늘어나야합니 다. 그리고플레이어가사람을목표지점으로이동시키는중에프로그래스바가천천히채워져 야합니다. 타이머 (timers) 로두문제를해결해봅시다. 1 2 윗부분에 C# 코드를몇라인더추가해봅시다. 몇페이지전에추가했던 using 문을하나더추가해야합니다. 그러고나서여러분이추가한 Random 라인으로올라갑니다. 아래 3 줄을추가해주세요. 타어머에대한메서드를추가합니다. IDE 가생성한이코드를찾아보세요. DispatcherTimers 를사용하기위한 using 문입니다. 여러분이작성한 MainWindow. Xaml.cs 파일에는 MainWindow 클래스코드가포함되어있습니다. 클래스에대해서는 3 장에서배워봅니다. Random 아래 3 줄을추가하세요. 이것을필드라합니다. 4 장에서배워봅시다. 틱 틱 틱 3 MainWindow() 메서드를마무리해봅시다. 두번째타이머를위한 Tick 이벤트핸들러를추가해봅시다. 두줄의코드를추 가해야합니다. 여러분이작성한 MainWindow() 메서드와 IDE 가생성한 2 개의 메서드가있습니다. 메서드를작성할때, 괄호 () 를붙입니다. 게임을다만들었을때, 여기에있는숫자를바꿔봅시다. 어떻게게임이바뀔까요? 여러분이 Tab 을눌러이벤트핸들러를추가할때마다 IDE 는여기에코드를생성합니다. 여러분은타이머의 Tick 마다실행되는코드로수정해야합니다. 브레인파워 지금바로시작버튼을눌러움직이는적을플레이영역에추가해봅시다. 시작버튼을눌러적을추가하는대신에적을자동으로생성하려면무엇이필요할까요? 커서를맨마지막줄의세미콜론바로뒤에놓은뒤, 엔터키를두번칩니다. 그리고 enemytimer. 를입력합니다 ( 점을포함합니다 ). 점을입력하는순간, 인텔리센스창 이나타날겁니다. 여기서 Tick 을선택하고아래와같이입력하세요. += 를입력하면 아래의팝업상자가뜹니다. Tab 을누르면 IDE 에서다른상자가나타날겁니다. (<TAB> 키를눌러삽입합니다.) <TAB> 키를눌러이클래스에 enemytimer_tick 처리기를생성합니다. Tab 을한번더누르면, IDE 는아래의코드를생성해줍니다. IDE 는이벤트핸들러에대한메서드를생성해줍니다. 15 장에서이벤트핸들러에대해배워봅니다. 타이머의 tick 은시간간격마다메서드를호출해줍니다. 첫번째타이머는몇초마다적들을추가합니다. 두번째타이머는시간이만료되면게임을종료합니다. 4 progressbar 를입력할때계속해서대문자 P로바뀌나요? IDE가코드에소문자 p의 progressbar 가없어서이와비슷한컨트롤유형을찾아주기때문입니다. EndTheGame() 메서드를추가합시다. targettimer_tick() 메서드로가서, IDE 가생성해준코드를지우고, 아래의코드를추가합니다. EndTheGame() 을입력하고, 전에했던것처럼메서드스텁을생성해주세요. 왜 progressbar 에오류가날까요? 여러분이컨트롤을사용할때, 이름이없거나오타가발생할경우어떻게 되는지보여주기위해서일부러이렇게코드를입력했습니다. XAML 코드창 (IDE 의다른탭에있는 ) 으로가 서, 아래줄에추가한 ProgressBar 컨트롤을찾아보세요. 그리고이름을 progressbar 로수정해주세요. 다음, 코드창으로돌아가서 EndTheGame() 메서드스텁을생성합니다. 조금전에했던 AddEnemy() 를생 성한것처럼말이죠. 여기에메서드코드가있습니다. gameovertext 가오류로뜬다면, TextBlock 의이름속성이 gameovertext 로되어있지않다는것을의미합니다. 지금돌아가서바로수정하세요. XAML 코드가있는디자이너탭이닫혀있다면, 솔루션탐색기창에있는 MainWindow.xaml 을더블 - 클릭하세요. 이메서드는타이머를멈추고, 시작버튼을다시보이게합니다. 그리고게임을종료하며, GAME OVER 텍스트를 playarea 에보여줍니다. 80 Appendix ii 지금여기예요 4 81

이제얼마남지않았습니다 시작버튼동작하게하기 시작버튼을누르면캔버스에원이나오게만든걸기억하나요? 이제진짜게임이시작되도록고쳐봅시다. 1 시작버튼이동작하도록만들어봅시다. 이미추가한시작버튼을누르면적이생성되는코드를찾습니다. 다음과같이수정하세요. 코드구울준비 이책에서는여러분이작성할많은코드를제공합니다. 이책의마지막에서이코드가무엇을하게되 는지알게될겁니다. 지금처럼아무생각없 이그냥코드를입력할수있습니다. 진행상황을볼까요? 게임이거의완성되어갑니다. 게임을실행해서어떻게동작하는지살펴봅시다. Start! 버튼을누를때, 버튼이사라지고, 적군들이나타나며, 프로그래스바가채워지기시작합니다. 플레이영역은움직이는적들로천천히채워지기시작합니다. 경고! 우리의스파이가인간들이방어막을구축하고있다는보고를접수했다. 이라인은시작버튼으로적을 playarea 캔버스에생성하는대신게임을시작하게합니다. 현재여러분이해야할일은코드를정확하게입력하고, 지시에정확히따르는겁니다. 이렇게코딩을하면서 IDE에익숙해질수있습니다. 만약진행이잘되지않는다면, MainWindow. 2 StartGame() 메서드를추가합니다. 메서드스텁을생성해서 StartGame() 메서드를만드세요. IDE 가추가한메 서드스텁에다음과같이코드를입력하세요. 15 장에서 IsHitTestVisible 에대해배워봅니다. xaml과 MainWindow.Xaml.cs 파일을내려받으세요. 혹은각각의메서드에 XAML과 C# 의코드를복사 / 붙여넣기하세요. 아래의 URL에있습니다. http://www.hanbit.co.kr/exam/2165 한가지더! 부록에있는프로젝트를내려받을때, WPF 폴더에서받았는지꼭확인해야합니 다. 윈도우스토어앱은 WPF 프로젝트에서실 행되지않습니다. 포탈 (target) 의 Rectangle 과사람 (human) 의 StackPanel 컨트롤의이름설정을했습니다. 컨트롤의이름을맞게설정했는지확인해보세요. 3 EnemyTimer 에적을추가합니다. IDE 가생성한 enemytimer_tick() 메서드를찾아서다음과같이수정하세요. 코딩에익숙해지면, 빠진괄호나세미콜론을찾는데능숙해집니다. 프로그래스바가다찼을때, 게임이끝나고 Game Over 텍스트가표시됩니다. 브레인파워 이해할수없는오류들이오류목록창에보입니까? 하나의점이나세미콜론이잘못되었을경우, 하나의오류가두개, 세개, 네개혹은그이상의오류를발생시킵니다. 모든오타를찾는데시간을낭비하지마세요! 아래의웹페이지로가면이프로그램의코드를쉽게찾아서복사 / 붙여넣기를할수있습니다. http://www.hanbit.co.kr/exam/2165 타깃타이머가천천히채워지면서, 적군들은 2 초마다생성됩니다. 만약타이머가동작하지않는다면, MainWindow() 메서드에서추가한모든코드를확인해보세요. 게임을완성하기위해서어떤일들이남았을까요? 나머지해야할일들은다음페이지에있습니다. 82 Appendix ii 지금여기예요 4 83

이벤트 플레이어가컨트롤을조정하는코드를작성해봅시다 플레이어가사람을포탈로끌어야합니다. 사람이포탈에닿을때, 적절한반응을해 야합니다. 이러한코드를작성해봅시다. 1 XAML 디자이너로가서, 문서개요창의 human 을선택합니다 ( 기억하시죠? Circle 과 Rectangle 로이루어진 StackPanel 입니다 ). 그리고속성창에서 보입니다. MouseDown 행을찾아빈상자에더블 - 클릭하세요. 버튼을누르면이벤트핸들러가 코드를작성하기전에앱을멈추고, IDE 로전환하세요. 4 장에서속성 창에있는이벤트헨들러를배워봅니다. 3 이벤트핸들러를바르게추가했는지확인하세요! 이전엔사람컨트롤에 MouseDown 이벤트핸들러를추가했습니다. 여기에서는타깃에 MouseEnter 이벤트핸들러를추가해봅시다. 문서개요창에서이름이 target 으로된 Rectangle 을선택합니다. 속성창의 선택한요 소의이벤트처리기 (event handlers view) 에서 MouseEnter 이벤트핸들러를추가합니 다. 여기에메서드코드가있습니다. 속성창에서이벤트핸들러가보인다면, 빈이벤트핸들러상자에더블 - 클릭하세요. IDE 는메서드스텁을생성해줍니다. 이상자를더블 - 클릭하세요. 이제 XAML 창에서 IDE 가 StackPanel 을위해무엇을추가했는지확인해보세요. 메서드스텁이추가됐습니다. XAML 창에서 human_mousedown 을오른쪽클릭합니다. 정의로이동 을선택해 C# 코드로바로넘어갑니다. 4 이벤트핸들러와속성을보여주는속성창에서컨트롤이름을바꿀수있습니다. 이번에는 playarea Canvas 컨트롤에 2 개의이벤트핸들러를추가합니다. 문서개요에서 [Grid] 를선택한후, 이 름을 grid 로설정합니다. 그리고 Canvas 의 MouseMove 와 MouseLeave 이벤트핸들러를처리하는메서드를추 가합니다. 괄호가정말많네요! 실수하지않게조심하세요. 2 그리고코드를입력하세요. 속성창에서속성과이벤트핸들러가있는이버튼을이용하면됩니다. 두개의수직바는논리연산을의미합니다. 2 장에서배워봅시다. 숫자 3 보다작거나크게해서, 감도를높이거나낮출수있습니다. 디자이너에서 StackPanel 을다시클릭한다면, 속성창에서새로운이벤트핸들러메서드의이름이보입니다. 더많은이벤트핸들러를같은방법으로추가할수있습니다. 이벤트핸들러에코드를똑바로입력했는지확인하세요. 두내용이바뀌면안됩니다. 84 Appendix ii 지금여기예요 4 85

모두구할수없어요 사람과적이부딪히면게임이끝납니다 플레이어가사람을적으로끌어놓으면, 게임이종료하는코드를추가해봅시다. AddEnemy() 메서드의마지막부분에서한줄의코드를추가합니다. 인텔리센스창을이용해서 enemy.mouseenter 를입력합니다. 이제게임을할수있습니다 게임을실행해보세요. 이제거의끝났습니다. 시작버튼을누를때, 플레이영역에서동그란적이나타납니다. 그리고사람과포탈이놓여있습니다. 프로그래스바가가득차기전에사람을포탈로옮겨야합니다. 처음엔쉽지만, 시간이지날수록적이늘어나서게임이어려워집니다. enemy 를입력하고점 (.) 을누르면인텔리센스창이뜹니다. 그리고 Enter 를입력해보세요. Enter 를포함하는목록으로건너뜁니다. 목록에서 MouseEnter 를선택하세요 ( 잘못선택해도괜찮아요. 점 (.) 까지다시지우면됩니다. 다시점을입력해서 인텔리센스창을띄우세요 ). AddEnemy() 메서드의마지막줄에커서를두고, 엔터키를친후코드를입력하세요. MouseEventHandler UIElement.MouseEnter 마우스포인터가이요소의경계에들어올때발생합니다. 다음, 전에했던것처럼이벤트핸들러를추가합니다. += 를입력하고 Tab 키를누르세요. (<TAB> 키를눌러삽입합니다.) 사람을안전한곳으로대피시켜주세요! 시간이가기전에사람을피신시켜주세요. 외계인들은이리저리움직이며사람을순찰합니다. 사람과적이부딪히는순간게임은끝납니다. 외계인을피해인간을잘움직인다면, 적으로부터일시적으로안전합니다. human 에적용된 IsHitTestVisible 속성을살펴보세요. IsHitTestVisible 속성이 true 일때, 사람은 MouseEnter 이벤트를차단합니다. 사람 (human) 의 StackPanel 컨트롤이포탈에도착했을때, 사람과포탈이새로운위치로이동하기때문이죠. 다시 Tab 키를눌러이벤트핸들러를위한스텁을생성합니다. <TAB> 키를눌러이클래스에 enemy_mouseenter 처리기를생성합니다. 15 장에서이벤트핸들러가 어떻게동작하는지배워봅니다. 너무빨리끌면사람을잃을수있어요! 이제새로운메서드가생성되었습니다. 다음코드를입력하세요. 86 Appendix ii 지금여기예요 4 87

휘파람부는외계인 적을외계인처럼 빨간색원은뭔가이상합니다. 템플릿을사용해서빨간색원을조금더멋지게수정해봅시다. 1 문서개요에서 ContentControl 오른쪽클릭 > 템플릿편집 > 현재항목편집을선택합니다. XAML 창에서템플릿을볼수있습니다. Ellipse 태그에서 width 를 75로 Fill 을 Gray 로설정합니다. 그리고 Stroke= Black 을추가해서바깥테두리선을만듭니다. 그리고수평과수직으로다시정렬합니다. 코드는다음과같습니다 ( 작업을하면서실수로추가된속성들을지워도됩니다 ). 속성대신이벤트가보인다고요? 속성창에있는토글버 튼을이용해서여러분이 선택한컨트롤의속성과이벤트를볼 수있습니다. 스패너와 번개모양의아이콘을 클릭해보세요. 여기에새롭게만든적템플릿의 XAML 코드가있습니다. 2 도구상자에서이미만든타원위에또다른 Ellipse 를추가합니다. Fill= black, width=25, height=35 로수 정합니다. 그리고정렬과여백설정을다음과같이합니다. 마우스나방향키를이용해 Ellipse 를눈으로이동시켜눈알을만들수도있습니다. 기존에있는 Ellipse 를복사 / 붙여넣기해서눈위에놓고, 조금만수정하면됩니다. 3 속성창에서변형섹션의 ( 기울이기 ) 를선택하고다음과같이합니다. 4 이미만든타원위에 Ellipse 를하나더추가합니다. Fill= black, width=25, height=35 로수정합니다. 그리 고정렬과여백설정을다음과같이합니다. XAML 코드를참고해서사람과포탈, 플레이영역과외계인을창의적으로바꿔보세요. 그리고기울이기를추가합니다. 이제여러분의적은인간을잡아먹는외계인처럼생겼네요. 한가지더해야할것은 그냥여러분이만든게임을즐기세요! 그리고복습하는것을잊지마세요. 여러분은임무를훌륭하게완수했습니다. 굿잡! 88 Appendix ii 지금여기예요 4 89

변수값의추이를지켜보려면디버거를사용하세요 디버거는여러분이만든프로그램이동작하는방식을이해하는최상의도구입니다. 이전 페이지에있는코드가실제로어떻게움직이는지확인하는데디버거를사용할수있죠. 디버깅해봅시다! Chapter 2 1 2 새 WPF 응용프로그램프로젝트를생성합니다. 페이지에다 TextBlock 을하나끌어다놓고, 이름을 output 으로입력합니다. 그리고 Button 을추가한뒤더블-클릭을해서, Button_Click() 메서드를추가하세요. IDE는자동으로코드편집창에있는메서드로갑니다. 이전페이지있는모든코드를메서드안에작성하세요. 코드첫번째줄에중단점 (breakpoint) 을추가하세요. 코드첫번째줄 (int number = 15;) 에서오른쪽버튼을클릭하고중단점메뉴에서중단점삽입 (Insert Breakpoint) 을선택하세요 ( 디버그 (Debug) 메뉴 > 중단점설정 / 해제 (Toggle Breakpoint) 를선택하거나 F9키를입력해도됩니다 ). 2 장의첫부분에나오는몇몇프로젝트들은 XAML 과윈도우스토어앱을사용합니다. 이것을 WPF 프로젝트로바꿔봅시다. WPF 프로젝트와함께코딩을시작해봅시다. 두번째장은 C# 코드를작성하는것부터시작합니다. 그리고몇몇의 예제가윈도우스토어앱으로만들어졌습니다. 우리는아래의방법을추천합니다. 책 112쪽까지 2장의주요부분을읽어주세요. 부록에서는책 113쪽의내용을대체합니다. 그리고책 114, 115, 116 쪽을읽어주세요. 프로그램을만드는 117, 118쪽의내용을부록에서대체합니다. 그리고프로젝트의나머지부분은이책을참고하면됩니다. 그리고책 126쪽까지읽어주세요. 다음 127쪽의연습문제와 129쪽의해답이부록에있습니다. 연습문제를다풀고나서, 9 장과두번째실습전까지는윈도우 8 혹은 윈도우스토어앱프로젝트가아닙니다. 10 장이되기전까지는책을읽 어주세요. /* * 디자이너에서버튼을더블 - 클릭하세요. * 빈 Button_Click() 메서드가생성됩니다. */ // 여기가중단점입니다. 코드상에중단점을설정하면해당줄은빨간색으로변하고코드편집기가장자리에빨간점이생깁니다. IDE 내부에서디버깅할때, 프로그램은중단점에이르자마자실행을중단하고모든변수의값을검사하고변경할수있게합니다. 주석 ( 두개이상의슬래시다음이나 /* 와 */ 사이에오는내용 ) 은 IDE 에서녹색으로표시됩니다. 이마크사이에무엇을입력해야할지는고민할필요가전혀없어요. 컴파일러는항상주석을무시한답니다. 새 WPF 프로젝트를생성하는것은 IDE 에서빈페이지하나와새프로젝트를만들라고얘기해주는것입니다. 여러분은프로젝트이름을 UseTheDebugger 와같은이름으로지을수도있습니다 ( 이페이지에서의제목과맞추기위해서 ). 많은프로그램을이책으로만든후, 필요할때다시사용할수있습니다. 책 114 쪽으로가서계속읽어주세요! 112 Appendix ii 지금여기예요 4 113

일부러페이지를비웠습니다 이프로젝트에맞는이름을지으세요. 여러분이나중에다시볼수도있으니까요. 기초부터만들기 운동화그림은코드실습을의미합니다. 모든프로그램에서실제로일을하는것은선언문입니다. 지금까지어떻게선언문들이페이지와함께동작 하는지살펴봤습니다. 이번실습에서는코드의내용을모두이해할수있습니다. Windows > WPF 응용프로 그램을생성해봅시다. 메인윈도우를열어서, 3 개의행과 2 개의열을그리드에추가해봅시다. 그리고 4 개의 Button 컨트롤과 TextBlock 컨트롤을셀에추가해봅시다. 페이지에는 3 개의행과 2 개의열이있습니다. 각각의행의높이는 1* 로합니다. 아무런속성없이 <RowDefinition/> 으로만되어있습니다. 열에서도높이를설정한것과같은방법으로해주세요. 윈도우의각행에는 4 개의 Button 컨트롤이있습니다. Content 속성에서텍스트를 Show a message, if/else, Another conditional test, A loop 로입력하세요. 윈도우를만들어봅시다 부록에서두페이지모드를유지하기위해서일부러페이지를비웠습니다. 연습문제와연습문제정답이서로마주하는면에나오면안되기때문이죠. 그리고두페이지모드의짝수쪽에는오른쪽위에, 홀수쪽에는왼쪽위에작은캡션이있습니다. 각버튼은셀에서가운데에있습니다. Grid. Row 와 Grid.Column 속성 ( 기본값 0) 에서행과열의위치를설정하세요. TextBlock 컨트롤은여기서실제로보이지않습니다. 텍스트가없어서보이지않죠. 맨아래행의가운데에각열에서 ColumnSpan 이 2 로설정되어있습니다. 아랫부분의 Text 컨트롤의이름을 mylabel 로합니다. x:name 속성을이용해서버튼의이름을 button1, button2, button3, button4 로수정하세요. 이름을바꾼후각버튼을더블 - 클릭해서이벤트핸들러메서드를추가하세요. 116 Appendix ii 지금여기예요 4 117

연습문제정답 여기에연습문제정답이있습니다. 여러분이한것과비슷하나요? 줄바꿈이나속성이조금다르다고요? 그래도괜찮아요! 여기에행과열의정의 (definition) 가있습니다. 3 개의행과 2 개의열이 있네요. IDE 를사용하지않고 XAML 을손으로코드를작성하는프로그래머도있습니다. 여러분도 IDE 를사용하지않고코드를작성할수있나요? 여기에새응용프로그램에서 IDE 가생성한 <Window> 와 <Grid> 태그가있습니다. 디자이너에서각버튼을더블 - 클릭했을때, IDE 는버튼의이름에 _Click 이따라오는메서드를생성합니다. 이책에는이런연습문제들이많이있답니다. 답은몇페이지뒤에있어요. 혹시중간에막히게되거든너무부담갖지말고막히는부분만참고해도괜찮아요. 이창을만드세요. 그리드에두개의행과두개의열 이있습니다. 창의높이는 150 픽 셀이고너비는 450 픽셀입니다. 창 제목을 Fun with if/else statements 로해주세요. if/else 문연습을해봅시다. 아래의프로그램을만들어보세요. IDE 에서두개의행을만들고하나의행높이에 1* 을설정하면, 한행이없어진것처럼보입니다. 작게축소되었기때문이죠. 다른행을 1* 로설정하면, 축소된행이보일거예요. Button 과 CheckBox 를추가합니다. 이책을보면서수많은애플리케이션을만들게될텐데, 각각의애플리케이션의이름은서로다르게붙여야합니다. 애플리케이션이름은 PracticeUsingIfElse 같은식으로장번호의같은폴더에관리하면편리합니다. 도구상자에서 Button 컨트롤아래에 CheckBox 컨트롤이 있습니다. Button 의이름을 changetext 로, CheckBox 의이 름을 enablecheckbox 로설정하세요. 두컨트롤의텍스트를 바꾸기위해, 오른쪽을클릭하고텍스트편집 (Edit Text) 메 뉴를선택해서바꿔주세요 ( 텍스트편집을끝내려면 Esc 를누 르면됩니다 ). 각컨트롤에서오른쪽을클릭하고, 레이아웃 > 모두다시설정을선택한후, 속성창에서 VerticalAlignment 와 HorizontalAlignment 를 Center 로설정하세요. TextBlock 을추가합니다. 이전프로젝트에서창아래에 Text- Block 을추가한것과같습니다. 이름을 labeltochange 로하고 Grid.Row 속 성을 1 로설정해주세요. Checkbox 가체크되지않은상태에서버튼을클릭하면, 이런메시지가뜹니다. Checkbox 가체크되었는지확인하기위한조건테스트는다음과같습니다. enablecheckbox.ischecked == true 이버튼은두번째열과두번째행에있습니다. 그리고이들의속성은 1 로설정되어있습니다. 테스트가 true 가아니면, 프로그램은두개의선언문을실행해야합니다. 힌트 : 이것을 else 블록에넣으면돼요. labeltochange.text = "Text changing is disabled"; labeltochange.horizontalalignment = HorizontalAlignment.Center; 버튼에서 HorizontalAlignment 나 VerticalAlignment 속성을지워보세요. 이속성이설정되어있지않으면, 버튼이수평혹은수직으로셀전체를채웁니다. 브레인파워 왜왼쪽열과위쪽행은숫자 1 이아니라 0 일까요? 그리고왼쪽위의셀 ( Show a message ) 에서는 Grid.Row 와 Grid.Column 속성이설정되어있지않을까요? Checkbox가체크된상태에서 Button을클릭하면, TextBlock이왼쪽에서가보이고, 오른쪽에서가보입니다. 레이블의 Text 속성이현재 Right 면, 텍스트를 Left 로바꾸고 HorizontalAlignment 속성을 HorizontalAlignment. Left 로바꿔야합니다. 그렇지않다면텍스트를 Right 로바꾸고 HorizontalAlignment 속성을 HorizontalAlignment. Right 로바꿔야합니다. 여러분이 Checkbox 가체크된상태에서버튼을클릭했을때, 레이블이앞뒤로움직여야합니다. 118 Appendix ii 지금여기예요 4 127

그리드에대한 XAML 코드입니다. <Grid> <Grid.RowDefinitions> <RowDefinition/> <RowDefinition/> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition/> <ColumnDefinition/> </Grid.ColumnDefinitions> if/else 문연습을해봅시다. 아래의프로그램을만들어보세요. 가독성을위해들여쓰기를했습니다. 디자이너에서이름을설정하기전에 Button 을더블 - 클릭했다면, changetext_click() 대신, Button_Click_1() 이라는이벤트핸들러메서드가생성됩니다. 책의다음부분에서는 XAML을사용하지않습니다. 2장의나머지부분에서는윈도우 8을요구하지않습니다. 윈도우 2003 을사용하거나 Visual Studio 2010 으로실습할수있죠. 그리고 3장부터 9장까지는윈도우폼응용프로그램 ( 혹은윈폼 ) 프로젝트를사용합니다. 이프로젝트는데스크톱앱을만들기위해서스토어앱보다오래된기술을사용합니다. 여러분이 IDE를통해서 C# 과 XAML 을배우고, 경험한것처럼잠시동안책으로돌아가서윈폼에대해배워봅시다. 책 131 쪽을살펴보세요. C# 의기본개념을익히기위해서윈폼으로전환하는이유에대해서설명합니다. WPF 도마찬가지입니다. 윈폼프로젝트를통해 C# 의핵심개념을빠르게익힐수있습니다. 그리고 WPF 를학습하는지름길이기도하죠. <Button x:name="changetext" Content="Change the label if checked" HorizontalAlignment="Center" VerticalAlignment="Center" Click="changeText_Click"/> 10 장이되기전까지는윈도우 8 과 WPF 가필요없단말이죠? 왜최신기술을사용하지않는거죠? </Grid> <CheckBox x:name="enablecheckbox" Content="Enable label changing" HorizontalAlignment="Center" VerticalAlignment="Center" IsChecked="true" Grid.Column="1"/> <TextBlock x:name="labeltochange" Grid.Row="1" TextWrapping="Wrap" Text="Press the button to set my text" HorizontalAlignment="Center" VerticalAlignment="Center" Grid.ColumnSpan="2"/> 이벤트핸들러메서드에대한 C# 코드입니다. private void changetext_click(object sender, RoutedEventArgs e) if (enablecheckbox.ischecked == true) if (labeltochange.text == "Right") labeltochange.text = "Left"; labeltochange.horizontalalignment = HorizontalAlignment.Left; else labeltochange.text = "Right"; labeltochange.horizontalalignment = HorizontalAlignment.Right; else labeltochange.text = "Text changing is disabled"; labeltochange.horizontalalignment = HorizontalAlignment.Center; 때론오래된기술이좋은학습도구로사용됩니다. WPF 는데스크톱앱을만드는좋은도구입니다. 하지만, 조금더간단한기술이 C# 의 개념을쉽게익히는데도움을줍니다. 그리고윈폼을사용하는또다른이유가있습니 다. 여러가지기술을익히면서공통점과차이점을비교하며, 기술에대한통찰력을얻 을수있습니다. 책 132 쪽부터윈폼앱을만들며, C# 의기초를다져봅시다. 그리고 10 장에서다시 XAML 로돌아왔을때, 여러분이학습한기초를기반으로조금더쉽게 WPF 에접근할수있습니다. 일부몇몇장은 Visual Studio 2008 에서지원되지않는.NET 4.0 에있는 C# 기능들을사용합니다. Visual Studio 2008 을사용하는경우, 이책의 3 장끝에도달했을때, 몇가지문제 가발생할수있습니다. 2008 의.NET 프레임워크의최신버전은 3.5 입니다. 책에있 는예제들은.NET 4.0 의 C# 기능들을사용합니다. 3 장에서객체이니셜라이저, 8 장에서컬렉션이니셜 라이저와공변성 (covariance) 을배웁니다. Visual Studio 2008 에서는이기능들이아직추가되지않았 기때문에, 책에있는예제코드들은컴파일되지않습니다. 2008 보다더새로운비주얼스튜디오버전 을설치할수없다면, 대부분의예제는실행할수있지만, 위에서언급한 C# 의기능들을사용할수없 습니다. 129 Appendix ii

일부러페이지를비웠습니다 Chapter 10 부록에서두페이지모드를유지하기위해서일부러페이지를비웠습니다. 연습문제와연습문제정답이서로마주하는면에나오면안되기때문이죠. 그리고두페이지모드의짝수쪽에는오른쪽위에, 홀수쪽에는왼쪽위에작은캡션이있습니다. 이번장에서는책에있는예제들을 WPF 로재설계하여개발해봅시다. 윈폼앱을 WPF 앱으로포팅할수있습니다. 3~9 장의연습문제와실습을통해많은양의코드를작성했습니다. 이번장에서는책에서본일부코드를사용해서 WPF 를익혀봅니다. 10 장에서여러분이참고해야할사항입니다. 책의 10 장처음부터 541 쪽까지읽어주세요. 직접해봅시 다!, 연필을깎으며 와같은문제들이있습니다. 이부록은책의 542-549 쪽을대체합니다. 책 550 쪽은윈도우스토어프로젝트만해당합니다. WPF 에 는해당되진않지만, 읽어도됩니다. 그리고, 책의 553-555 쪽을대체합니다. 마지막으로책 558, 559 쪽을읽고난후, 부록으로돌아와서 나머지부분 (560-577 쪽 ) 을읽으면됩니다. 122 Appendix ii 지금여기예요 4 123

XAML 탐험하기 윈도우스토어앱은 XAML 로 UI 객체를만듭니다 직접해봅시다! 3 몇몇의 labeltochange 속성을조사식창에추가해봅시다. WPF 응용프로그램의사용자인터페이스를구축하기위해 XAML 을사용하면객체그래프를만들수있습 니다. 윈폼과같이 IDE의조사식창을볼수있습니다. 2장의 기초부터만들기 에서연습문제로한 Program2 를열어봅시다. 그리고 MainWindow.xaml.cs 를열고생성자에서 InitializeComponent() 를호출하는곳에중단점을설정해서앱의 UI 객체를탐험해봅시다. 1 디버깅을시작해봅시다. F10 키눌러프로시저단위로실행해봅시다. 디버그 (Debug) > 창 (Windows) > 조 사식 (Watch) > 조사식 1(Watch 1) 을누르세요. 조사식창에서 this 를살펴봅시다. 2 페이지에정의된 XAML 코드를살펴봅시다. <Grid Background="StaticResource ApplicationPageBackgroundThemeBrush"> <Grid.RowDefinitions> <RowDefinition/> <RowDefinition/> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition/> <ColumnDefinition/> </Grid.ColumnDefinitions> <Button x:name="changetext" Content="Change the label if checked" HorizontalAlignment="Center" Click="changeText_Click"/> <CheckBox x:name="enablecheckbox" Content="Enable label changing" HorizontalAlignment="Center" IsChecked="true" Grid.Column="1"/> <TextBlock x:name="labeltochange" Grid.Row="1" TextWrapping="Wrap" Text="Press the button to set my text" HorizontalAlignment="Center" VerticalAlignment="Center" Grid.ColumnSpan="2"/> </Grid> labeltochange 는 TextBlock 의인스턴스입니다. 윈도우의컨트롤을정의하는 XAML 은필드와 UI 컨트롤에대한참조를포함하는속성을가진 Window 객체에설정되어있습니다. 앱은 XAML 에있는기본속성들을자동으로설정해줍니다. <TextBlock x:name="labeltochange" Grid.Row="1" TextWrapping="Wrap" Text="Press the button to set my text" HorizontalAlignment="Center" VerticalAlignment="Center" Grid.ColumnSpan="2"/> 그러나조사식창에서 labeltochange.grid 혹은 labeltochange.columnspan 을추가해봅시다. 이컨트롤은 Windows.UI.Controls.TextBlock 객체이고, 이객체는이속성들을가지고있지않습니다. 이 XAML 속성은 무엇일까요? 클래스를보기위해 Window 위에마우스를올려보세요. 4 프로그램을중단하고 MainWindow.xaml.cs 파일을열어클래스가선언된곳에서 MainWindow 를찾아보세요. 이클 래스는 Window 의서브클래스입니다. IDE 에서 Window 라적힌곳에마우스를놓고클래스의전체이름을보세요. 이제프로그램을다시시작해봅시다. InitializeComponent() 를호출하는부분에중단점을설정하고 F10 키를눌러프로시 저단위로실행해봅시다. 조사식창으로돌아와서 this > base > base 를확장하며상속계층구조를살펴봅시다. 슈퍼클래스를보기위해서이들을확장해주세요. XAML 이생성한객체를잠깐동안살펴봅시다. 이객체들은책의뒤에서자세히살펴볼겁니다. 여기에서는앱뒤에숨어 있는얼마나많은객체들이있는지만알고넘어갑시다. 창과대화상자의수명을생성, 구성, 표시및관리하는기능을제공합니다. Content 를확장하고 [System.Windows.Controls.Grid] 노드를살펴보세요. 542 Appendix ii 지금여기예요 4 543

새롭게단장하기 고피시폼을윈도우 WPF 응용프로그램으로새롭게단장해봅시다 8 장에서만든고피시 (Go Fish!) 게임을 WPF 응용프로그램으로만들어봅시다. Visual Studio 2013 for Desktop 을열 어새로운 WPF 응용프로그램을생성합니다 ( Save the Humans 를생성했던것처럼요 ). 다음에나오는몇페이지들을 XAML 로새롭게디자인해서, 윈폼보다더유연한메인윈도우를만들어볼겁니다. 폼의윈도우데스크톱컨트롤을사용 하는대신 WPF XAML 컨트롤을사용해봅시다. 직접해봅시다! 이것은 <TextBox/> 가될거예요. 이것은 <Button/> 이될거고요. TextBox 와 Button 컨트롤을그룹으로묶기위해수평 StackPanel 을사용합니다. 그래서이둘은그리드의같은셀안에있군요. 앱메인윈도우는아래와같습니다. 게임에서코드의대부분은동일하게유지되지만, UI 코드가바뀝니다. 이것은 <ListBox/> 가될거예요. 창의크기에따라확장하거나축소되는그리드의행과열들은컨트롤을포함하고있습니다. 창크기에맞 추기위해서게임화면을늘리거나줄일수있습니다. 이것은 <ScrollViewer/> 가될거예요. 창크기에상관없이게임이실행됩니다. 이것도 <ScrollViewer/> 가될거예요. 이것은도구상자에있는또다른컨트롤입니다. 텍스트가창범위를벗어난다면수직 / 수평방향의스크롤바를추가해서텍스트를보여줍니다. 이것은 <Button/> 이될거고요. 544 Appendix ii 지금여기예요 4 545

유연한윈도우 창레이아웃과컨트롤 XAML 과 WPF 앱은하나의공통점이있습니다. 둘모두창레이아웃에서컨트롤에의존하고있습니다. 고피시창에는 2개의 Button 과카드를보여주기위한 ListBox, 사용자의이름을입력받기위한 TextBox, 네개의 TextBlock 레이블, 두개의 Scroll- Viewer 컨트롤이있습니다. 게임진행상황 (Game progress) 과북 (Books) 의컨트롤은흰배경입니다. 창의각레이블은 ( Your name, Game progress 등 ) TextBlock 입니다. Margin 속성에서위쪽여백 을 10 으로설정해주세요. <TextBlock Text="Game progress" Grid.Row="2" Margin="0,10,0,0"/> 3 4 1 창크기가커질경우 ScrollViewer 는여분의수직공간을채우기위해크기를늘려야합니다. 텍스트가너무커질경우에스크롤막대를표시해야하겠죠? ScrollViewer 는다양한북을표시할만큼충분한공간이있어야하고, 필요한경우에는스크롤막대를표시해야합니다. 2 5 창크기가커질경우 ListBox 또한여분의수직공간을채우기위해크기를늘려야합니다. ScrollViewer 컨트롤은게임진행상황 (Game Progress) 을표시해줍니다. 텍스트가길어질경우스크롤바가나타납니다. 3 <ScrollViewer Grid.Row="3" FontSize="24" Background="White" Foreground="Black" /> 북을표시하기위한또다른 TextBlock 과 ScrollViewer 컨트롤이있네요. ScrollViewer 컨트롤의 기본수평및수직정렬은 Stretch( 늘림 ) 로되어있습니다. 정말유용한컨트롤이죠. ScrollViewer 컨트롤이다양한화면크기에맞게확장할수있도록, 행과열을설정해봅시다. 메인윈도우에대한 XAML 은 <Window> 태그로시작합니다. Title 속성은창의제목을 Go Fish! 로, Height 와 Width 속성은 창크기를설정해주죠 ( 디자이너에서창크기를바꿀때마다, 속성이변하는것을볼수있습니다 ). 그리고 Background 속성을 회색인 Gary 로설정해주세요. XAML 에반영된 <Window> 의시작태그입니다. 이프로젝트의이름을 GoFish 로지었죠. 만약다른이름을입력했다면, x:class 속성에여러분이입력한이름이보일겁니다. <Window> 태그에있는이속성들로윈도우의제목과높이및너비를설정합니다. <Window x:class="go_fish.mainwindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Go Fish!" Height="500" Width="525" Background="Gray"> <Grid Margin="10" > StackPanel 을이용해서플레이어이름을입력할 TextBox 와시작 Button 을한셀에놓습니다. 6 4 <TextBlock Text="Books" Margin="0,10,0,0" Grid.Row="4"/> <ScrollViewer FontSize="24" Background="White" Foreground="Black" Grid.Row="5" Grid.RowSpan="2" /> 컨트롤사이의조그만공간을위해서열의여백을 40 픽셀로둡니다. ListBox 와 Button 의컨트롤 은세번째열로배치해야겠군요. ListBox 의행은 2 행과 6 행사이에걸쳐있어서, Grid.Row= 1 과 Grid.RowSpan= 5 로설정했습니다. 창이커질때 ListBox 도같이커집니다. 5 <TextBlock Text="Your hand" Grid.Row="0" Grid.Column="2" /> <ListBox x:name="cards" Background="White" FontSize="24" Height="Auto" Margin="0,0,0,10" Grid.Row="1" Grid.RowSpan="5" Grid.Column="2"/> 행과열의인덱스는 0 으로시작해서세번째열에있는컨트롤은 Grid.Column= 2 입니다. 1 <TextBlock Text="Your Name" /> <StackPanel Orientation="Horizontal" Grid.Row="1"> <TextBox x:name="playername" FontSize="24" Width="150" /> 2 <Button x:name="startbutton" Margin="5,0" Content="Start the game!"/> Margin 속성에서두개의숫자가있는경우수평 ( 왼쪽 / 오른쪽 ) 과수직 ( 위 / 아래 ) 여백을지정합니다. 여기에서는수평여백을 5, 수직여백을 5 로 다음페이지에서그리드를마무리해봅시다. 546 Appendix ii 지금여기예요 4 547 설정했습니다. 그리고 5,0,0,0 를입력해서왼쪽의여백을 5, 오른쪽의여백을 0 으로설정할수도있습니다. Ask for a card Button 의 HorizontalAlignment 와 VerticalAlignment 는 Stretch 로설정했습니다. 그래서버튼이셀안에채워져있군요. 그리고 ListBox 컨트롤아래에 20 픽셀의작은틈이있습니다. 6 <Button x:name="askforacard" Content="Ask for a card" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Grid.Row="6" Grid.Column="2"/>

늘렸다가줄였다가 창크기에맞게행과열이조절됩니다 그리드는창레이아웃을위한매우효과적인도구입니다. 다양한장치의페이지를디자인하는데유용하죠. * 로끝나는높이와 너비는항상자동으로각각다른화면에맞게조정합니다. 고피시페이지는 3 개의열이있습니다. 첫번째와세번째의너비는 5* 와 2* 입니다. 그리드가늘어나거나줄어들어도항상 5:2 의비율을유지하죠. 두번째열은첫번째와세번째의분리를유지하기 위해 40 픽셀로고정된폭을갖습니다. 페이지안에있는컨트롤을포함해서행과열을배치하는방법이아래에있습니다. <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto" MinHeight="150"/> <RowDefinition Height="Auto"/> <TextBlock/> <StackPanel Grid.Row="1"> <TextBlock/> <Button/> <TextBlock Grid.Row="2"/> <ScrollViewer Grid.Row="3"/> <TextBlock Grid.Row="4"/> <ColumnDefinition Width="5*"/> Row= 1 은두번째행을의미합니다. 행번호가 0 부터시작하기때문이죠. 이행의기본높이는 1* 로 ScrollViewer 는기본수직 / 수평정렬로 Stretch 로설정되어있습니다. 페이지의크기에따라행이늘어나거나줄어듭니다. <ScrollViewer Grid.Row="5" Grid.RowSpan="2"> ScrollViewer 가 150 보다더작아지지않도록여섯번째행 (XAML 에서 0 부터번호를매기기때문에행번호는 5 입니다 ) 의최소높이를 150 으로설정했습니다. <ColumnDefinition Width="40"/> <ColumnDefinition Width="2*"/> <TextBlock Grid.Column= "1"/> <ListBox Grid.Column="1" Grid.RowSpan="5"/> ListBox 는네번째행을포함해서 5 개의행을차지하고있네요. 이는페이지의전체오른쪽측면을채우기위해 ListBox 를늘렸습니다. <Button Grid.Row="6" Grid.Column="2" /> 창레이아웃을작업하기위한행과열의정의부분입니다. <Grid.ColumnDefinitions> <ColumnDefinition Width="5*"/> <ColumnDefinition Width="40"/> <ColumnDefinition Width="2*"/> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto" MinHeight="150" /> <RowDefinition Height="Auto"/> </Grid.RowDefinitions> 그리드에서컨트롤위혹은아래에행과열의정의를추가할수있습니다. 여기에선컨트롤아래에정의를추가했습니다. </Grid> </Window> 마지막으로 WPF 앱의레이아웃을마무리하는그리드와윈도우의닫는태그를추가합니다. 첫번째열은항상세번째열보다 2.5 배넓네요 (5:2 의비율 ). 그리고그사이 40 픽셀의열이있습니다. 데이터를표시하는 ScrollViewer 와 ListBox 컨트롤의 HorizontalAlignment 는 Stretch 로설정되어서열을채워줍니다. 네번째행의기본높이는 1* 입니다. 다른행에서사용하지않는공간을채우기위해서행이늘어나거나줄어들수있죠. ListBox 와첫번째 ScrollViwer 가이행에걸쳐있어서, 늘어나거나줄어들수있습니다. 대부분모든행의높이는 Auto 로되어있습니다. 어떤하나의행이늘어나거나줄어들면, 그행에속한컨트롤또한늘어나거나줄어듭니다. XAML 에서의행과열번호는 0 부터시작합니다. 이버튼의행은 6, 열은 2 입니다. 그리고수직 / 수평정렬은 Stretch 로설정되어셀의공간을모두차지하죠. 이행의높이 (RowDefinitionHeight) 는자동 (Auto) 으로되어있어서내용의길이에따라높이가결정됩니다 ( 버튼의여백포함 ). 548 Appendix ii 지금여기예요 4 549

비슷해보이는프로그램 XAML 을이용하여윈폼프로그램을 WPF 응용프로그램으로다시만들어봅시다. 새 WPF 응용프로그램을생성하고, 각창의그리드를수정하고컨트롤을추가합니다. 앱이 실행될필요는없습니다. 그냥 XAML 을생성한뒤, 아래그림에맞게만들어주세요. 이창을디자인하기위해서 StackPanel 을사용합니다. 높이를 300, 너비를 525로설정합니다. 그리고 ResizeMode 속성을 NoResize 로합니다. 2개의 <Border> 컨트롤을사용해서, 하나는위쪽 StackPanel 컨트롤의테두리를만들고, 다른하나는 ScrollViewer 컨트롤의테두리를만들어줍니다. <StackPanel Margin= 5 > 이것은 <ComboBox> 입니다. 그리고항목은 <ComboBoxItem/> 태그와항목의이름을설정하는 Content 속성으로설정되어있죠. 오른쪽으로정렬된이버튼의폰트크기는 18 픽셀이고, 오른쪽여백은 20 픽셀입니다. <TextBlock/> <StackPanel Orientation= Horizontal > <StackPanel> <TextBlock/> <ComboBox> <ComboBoxItem/> <ComboBoxItem/>... 4 more... </ComboBox> <StackPanel> <TextBlock/> <TextBox/> <Button/> Border 컨트롤을이용하여 ScrollViewer 컨트롤주위에테두리를만들어주세요. 속성창이나인텔리센스창을보면, ScrollViewer 컨트롤에는 BorderBrush 와 BorderThickness 속성이있습니다. 이속성들은 실제로아무것도하지않기때문에, 조금잘못된것이죠. ScrollViewer 는 ContentControl 의서브클래스입니다. ContentControl 의 속성들을상속받지만, 실제로는아무것도하지않습니다. ScrollViewer 컨트롤에쉽게테두리를만드는방법이있습니다. 다른컨트롤도마찬가지로요. Border 컨트롤을사용해보세요. 여기에 Breakfast for Lumberjacks 창에서사용할수있는 XAML 코드가있습니다. BorderThickness 와 BorderBrush 속성을이용해서테두리의굵기와색을설정합니다. 또한테두리주위의배경과같은다른시각적인요소들을설정할수있습니다. Border 컨트롤은하나의다른컨트롤을포함할수있습니다. 만약 2 개이상의컨트롤을넣으려면 StackPanel, Grid, Canvas 컨트롤혹은다른컨테이너를사용하세요. 이 ScrollViwer 에텍스트를추가하기위해서 Content 속성을사용합니다. 줄바꿈은 를추가하면됩니다. BorderThickness 를 2 픽셀로하고, BorderBrush 를이용해서흰색테두리를만들어주세요. 높이는 250 입니다. 이폼을디자인하기위해서그리드를사용합니다. 7개의행에높이를 Auto 로설정하여, 내용을맞게확장합니다. 그리고한행의높이를기본 (1* 과같음 ) 으로설정하면, 행이그리드에맞춰확대됩니다. StackPanel 을이용하여같은행에여러컨트롤을놓습니다. 각 TextBlock 컨트롤은아래에 5픽셀의여백이있습니다. 그리고아래의두 TextBlock 컨트롤은위쪽으로각 10픽셀의여백이있습니다. <Window> 태그안의속성을이용하세요. 이것은 ListBox 입니다. ComboBox 에서사용한 <ComboBoxItem/> 태그와같이 <ListBoxItem/> 태그를이용합니다. VerticalAlignment 를 Stretch 로설정합니다. 행이늘어나거나줄어들때, ListBox 도같이늘어나거나줄어듭니다. 창의 ResizeMode 를 CanResizeWithGrip 으로설정하면, 이렇게창크기조정의그립을표시해줍니다. 보통페이지에있는컨트롤과관련해클래스에메서드나속성을채워넣지만, 그냥컨트롤에더미데이터를추가해서바로이화면처럼보이도록만들어주세요. 552 Appendix ii 지금여기예요 4 553 <Button/> <TextBlock/> <ScrollViewer/> <Grid Grid.Row= 1 Margin= 5 > <TextBlock/> <TextBox/> <TextBlock/> <ListBox VerticalAlignment= Stretch > <ListBoxitem/> <ListBoxitem/>... 4 more... </ListBox> <TextBlock> <StackPanel Orientation= Horizontal > <TextBox/> <ComboBox>... 4 items... </ComboBox> <Button/> <ScrollViewer/> <StackPanel Orientation= Horizontal > <Button/> <Button/> ComboBox 컨트롤의 SelectedIndex 속성을 0 으로설정해서첫번째항목이보이게해주세요. <Window> 속성을이용하여창의처음과최소크기를설정해보세요. 그리고창의크기를조정해서바르게동작하는지확인해보세요 (Height= 400 MinHeight= 350 Width= 525, MinWidth= 300 ). 이행은기본높이 (1*) 입니다. 나머지행의높이는 Auto 죠. 그래서이행은창의크기가바뀔때마다, 늘어나거나줄어듭니다.

XAML 을이용하여윈폼프로그램을 WPF 응용프로그램으로다시만들어봅시다. 새 WPF 응용프로그램을생성하고, 각창의그리드를수정하고컨트롤을추가합니다. 앱이 실행될필요는없습니다. 그냥 XAML 을생성한뒤, 아래그림에맞게만들어주세요. <Window x:class="beehivemanagementsystem.mainwindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Beehive Management System" Height="300" Width="525" ResizeMode="NoResize"> 우리가준여백입니다. 숫자하나 (5) 만있다면, 위쪽, 왼쪽, 아래, 오른쪽의여백이같은값으로설정됩니다. <StackPanel Margin="5"> <TextBlock Text="Worker Bee Assignments" Margin="0,0,0,5" /> <Border BorderThickness="1" BorderBrush="Black"> <StackPanel Orientation="Horizontal" Margin="5"> <StackPanel Margin="0,0,10,0"> <TextBlock Text="Job"/> <ComboBox SelectedIndex="0" > <ComboBoxItem Content="Baby bee tutoring"/> <ComboBoxItem Content="Egg care"/> <ComboBoxItem Content="Hive maintenance"/> <ComboBoxItem Content="Honey Menufacturing"/> <ComboBoxItem Content="Nectar collector"/> <ComboBoxItem Content="Sting patrol"/> </ComboBox> <StackPanel> <TextBlock Text="Shifts" /> <TextBox/> <Button Content="Assign this job to a bee" VerticalAlignment="Bottom" Margin="10,0,0,0" /> </Border> <Button Content="Work the next shift" Margin="0,20,20,0" FontSize="18" HorizontalAlignment="Right" /> <TextBlock Text="Shift report" Margin="0,10,0,5"/> <Border BorderBrush="Black" BorderThickness="1" Height="100"> <ScrollViewer Content=" Report for shift #20 Worker #1 will be done with 'Nectar collector' after this shift Worker #2 finished the job Worker #2 is not working Worker #3 is doing 'Sting patrol' for 3 more shifts Worker #4 is doing 'Baby bee tutoring' for 6 more shifts "/> </Border> </Window> XAML 코드가연습문제정답과다르다고요? XAML 에서매우유사한 ( 또한동일한 ) 창을표시하는여러가지방법이있습니다. 그리고 XAML 은태그순서처리에유연합니다. 같은윈도우객체그래프에있다면, 태그를다른순서로배치할수있습니다. Border 컨트롤은 ScrollViewer 컨트롤주위에테두리를그려줍니다. 근무시간보고서를채우는데사용되는빈데이터입니다. Content 속성은줄바꿈문자를무시합니다. 대신 을사용하면됩니다. <Window x:class="breakfastforlumberjacks.mainwindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Breakfast for Lumberjacks" Width="525" Height="400" MinWidth="300" MinHeight="350" ResizeMode="CanResizeWithGrip" > <Grid Grid.Row="1" Margin="5"> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition /> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> </Grid.RowDefinitions> </Grid> </Window> <TextBlock Text="Lumberjack name" Margin="0,0,0,5" /> <TextBox Grid.Row="1"/> <TextBlock Grid.Row="2" Text="Breakfast line" Margin="0,10,0,5" /> <ListBox Grid.Row="3" VerticalAlignment="Stretch"> <ListBoxItem Content="1. Ed"/> <ListBoxItem Content="2. Billy"/> <ListBoxItem Content="3. Jones"/> <ListBoxItem Content="4. Fred"/> <ListBoxItem Content="5. Johansen"/> <ListBoxItem Content="6. Bobby, Jr."/> </ListBox> 여기에윈도우속성이설정되어있습니다. 처음창의크기는 525 x 400 이고, 최소창크기는 300 x 350 입니다. <TextBlock Grid.Row="4" Text="Feed a lumberjack" Margin="0,10,0,5" /> <StackPanel Grid.Row="5" Orientation="Horizontal"> <TextBox Text="2" Margin="0,0,10,0" Width="30"/> <ComboBox SelectedIndex="0" Margin="0,0,10,0"> <ComboBoxItem Content="Crispy"/> <ComboBoxItem Content="Soggy"/> <ComboBoxItem Content="Browned"/> <ComboBoxItem Content="Banana"/> </ComboBox> <Button Content="Add flapjacks" /> 크기가재조정되는것을막기위해서 ResizeMode 속성을 NoResize 로설정할수있습니다. CanMinimize 는최소화만할수있고, CanResizeWithGrip 은창의오른쪽아래의코너에크기를조절할수있는그립을표시해줍니다. 연습문제의일환으로폼이마치동작되는것처럼보이는외형을만들기위해서더미항목을추가했습니다. 여기에있는 ListBox 와같은컨트롤을클래스의속성들과어떻게바인딩하는지배울거예요. <Border BorderThickness="1" BorderBrush="Gray" Grid.Row="6" Margin="0,5,0,0"> <ScrollViewer Content="Ed has 7 flapjacks" BorderThickness="2" BorderBrush="White" MinHeight="50"/> </Border> 더미내용입니다. <StackPanel Grid.Row="7" Orientation="Horizontal" Margin="0,10,0,0"> <Button Content="Add Lumberjack" Margin="0,0,10,0" /> <Button Content="Next Lumberjack" /> 554 Appendix ii 지금여기예요 4 555

슬라피조, WPF 를만나다 데이터바인딩으로슬라피조의향상된메뉴판을만들어봅시다 4 장에서나온슬라피조를기억하시나요? 슬라피조는여러분이 XAML 에능숙하다고알고있습니다. 그는샌드위치메뉴를위한 WPF 앱을만들고싶어합니다. 우리가만들려고하는페이지입니다. ListView 와 TextBlock 의내부에있는 <Run> 태그를채우기위해단방향데이터바인딩을사용합니다. 그리고실제바인딩을수행하기위해서 <Run> 태그를이용하여, TextBox 에양방향바인딩을사용합니다. <StackPanel Grid.Row="1" Margin="120,0"> <StackPanel Orientation="Horizontal"> <StackPanel> <TextBlock/> <TextBox Text="Binding NumberOfItems, Mode=TwoWay"/> <Button/> Window 객체는데이터컨텍스트를사용하기위해 MenuMaker 의인스턴스를생성합니다. Window 객체의생성자는 StackPanel 의 DataContext 속성을 MenuMaker 의인스턴스로설정합니다. 이제바인딩은 XAML 에서실행됩니다. Window 객체 StackPanel 객체 StackPanel 객체 MenuItem Meat Condiment Bread override ToString() MenuMaker 객체 MenuItem 은간단한데이터객체입니다. ListView 에있는텍스트를설정하기위해 ToSTring() 메서드를오버라이딩합니다. ObservableCollection 메뉴항목의수를설정하기위해 TextBox 는양방향바인딩을사용합니다. StackPanel 객체 MenuItem 객체 이것은 TextBox 가 x:name 속성이필요없다는것을의미합니다. MenuMaker 객체에 NumberOfItems 의속성이바인딩되기때문에, 그것을참조하기위해어떤 C# 코드에서도작성할필요가없는거죠. MenuItem 객체 MenuItem 객체 MenuItem MenuItem 객체객체 TextBox 의양방향바인딩은맨처음 NumberOfItems 속성값으로채워지고, 유저가 TextBox 값을편집할때마다그속성이업데이트됩니다. 바인딩할속성이있는객체가필요합니다. Windows 객체는 3개의 public 속성을가진 MenuMaker 클래스의인스턴스가있습니다. int형의 NumberOfItems 와 ObservableCollection 유형의 Menu, DateTime 유형인 GeneratedDate 가있죠. ListView 객체 TextBlock 객체 <ListView ItemsSource="Binding Menu"/> <TextBlock> <Run/> <Run Text="Binding GeneratedDate"/> </TextBlock> MenuMaker NumberOfItems Menu GeneratedDate UpdateMenu() TextBox object ListView 와 TextBlock 객체또한 MenuMaker 객체의속성으로바인딩됩니다. TextBlock 객체 ListView 객체 Menu GeneratedDate Button 객체 이제코딩할차례입니다. 페이지를넘겨코드를보기전에, 지금까지읽은것을바탕으로새롭고향상된슬라피조앱을만들수있나요? TextBox 객체 TextBlock 객체 버튼은 MenuMaker 를갱신하라고알려줍니다. NumberOfItems 버튼은 MenuMaker 의 UpdateMenu() 메서드를호출하고, ObservableCollection 에새로운 MenuItem 을추가하여메뉴를갱신합니다. ListView 는 ObservableCollection 이변경될때마다자동으로갱신됩니다. 560 Appendix ii 지금여기예요 4 561

슬라피조 2: 구불구불한감자튀김의전설 1 2 다른프로젝트에서했던것처럼솔루션탐색기에서프로젝트이름에마우스오른쪽버튼을클릭하고, 새클래스를추가하면됩니다. 윈도우에있는이러한속성들을보여주기위해서데이터바인딩을사용할거예요. 또한 NumberOfItems 를갱신하기위해양방향바인딩을사용할겁니다. 먼저, 프로젝트를생성합니다. MenuMaker 클래스를추가합니다. 4 장에서부터여기까지먼길을왔습니다. 속성으로항목수를설정하여잘캡슐화된클래스를만들어봅시다. 생성자에 MenuItem 의 ObserbableCollection 을생성합니다. 이 Menu 는 UpdateMenu() 메서드가호출될때마다갱신됩니다. 이메 서드는현재 Menu 에대한타임스탬프와 GeneratedDated 라불리는 DateTime 속성을갱신합니다. 프로젝트에 Menu- Maker 클래스를추가해봅시다. using System.Collections.ObjectModel; class MenuMaker private Random random = new Random(); private List<String> meats = new List<String>() "Roast beef", "Salami", "Turkey", "Ham", "Pastrami" ; private List<String> condiments = new List<String>() "yellow mustard", "brown mustard", "honey mustard", "mayo", "relish", "french dressing" ; private List<String> breads = new List<String>() "rye", "white", "wheat", "pumpernickel", "italian bread", "a roll" ; public ObservableCollection<MenuItem> Menu get; private set; public DateTime GeneratedDate get; set; public int NumberOfItems get; set; public MenuMaker() Menu = new ObservableCollection<MenuItem>(); NumberOfItems = 10; UpdateMenu(); private MenuItem CreateMenuItem() string randommeat = meats[random.next(meats.count)]; string randomcondiment = condiments[random.next(condiments.count)]; string randombread = breads[random.next(breads.count)]; return new MenuItem(randomMeat, randomcondiment, randombread); public void UpdateMenu() Menu.Clear(); for (int i = 0; i < NumberOfItems; i++) Menu.Add(CreateMenuItem()); GeneratedDate = DateTime.Now; NumberOfItems 가음수로설정되면무슨일이일어날까요? 이 using 라인이필요할거예요. ObservableCollection<T> 가이네임스페이스에있기때문이죠. 날짜에관한건 DateTime 을사용하세요. 직접해보세요! 새로운 WPF 응용프로그램을생성합니다. 기본창크기로유지하고, 제목을 Welcome to Sloppy Joe s 로합니다. 새롭게추가된 CreateMenuItem() 메서드는문자열이아닌 MenuItem 객체를반환합니다. 원하는경우쉽게항목이표시되는방식을바꿀수있습니다. 어떻게이반복문이동작하는지자세히살펴보세요. 새로운 MenuItem 컬렉션을생성하지않습니다. 새로운항목을추가하여현재상태를갱신합니다. 날짜를저장하기위해서 DateTime 유형을사용했습니다. DateTime 은날짜와시간을생성하고수정하는데도사용됩니다. DateTime 에서현재시간을반환해주는 static 속성인 Now 가있습니다. 또한 DateTime 은 AddSeconds() 메서드처럼초단위, 밀리초단위, 일별로추가하거나변환할수있습니다. 그리고 Hour 와 DayOfWeek 와같은속성으로날짜를분리할수있습니다. MenuItem 클래스를추가합니다. class MenuItem public string Meat get; set; public string Condiment get; set; public string Bread get; set; public MenuItem(string meat, string condiment, string bread) Meat = meat; Condiment = condiment; Bread = bread; 데이터를저장하기위해문자열대신클래스를이용하는경우조금더유연한프로그램을만들수있다는것을 살펴봤습니다. 메뉴항목을추가하는간단한클래스가아래에있네요. 여러분이좋아하는메뉴도추가해보세요. 562 Appendix ii 지금여기예요 4 563 3 4 public override string ToString() return Meat + " with " + Condiment + " on " + Bread; XAML 윈도우를만들어봅시다. 여기에스크린샷이있네요. StackPanel 을이용해서아래처럼만들수있나요? TextBox 의너비는 100 입니다. 아래쪽의 TextBlock 은 BodyTextBlockStyle 의스타 일과두개의 <Run> 태그가있습니다 ( 두번째부분은날짜를표시합니다 ). XAML 코드를보기전에스크린샷만으로이창을만들수있나요? 항목을구성하는이세문자열은생성자에서설정합니다. 그리고읽기전용의자동속성입니다. ToString() 메서드를오버라이드했네요. MenuItem 자체에서문자열을보여줍니다. 이번에는더미데이터를넣지마세요. 데이터바인딩을사용할겁니다. 이것은 ListView 컨트롤입니다. ListBox 컨트롤과많이닮았죠. 사실이것은 ListBox 와같은베이스클래스로부터상속을받습니다. 그래서동일한항목의선택이있죠. 하지만 ListView 컨트롤은더많은유연성을제공합니다. 각항목에대한데이터템플릿을정의함으로써, 항목을다양한방법으로표시할수있습니다. 잠시후데이터템플릿에대해서조금더자세히배워봅시다.

바운드그리고결정 5 ListView 컨트롤입니다. ListBox 로바꿔서창이어떻게바뀌는지살펴보세요. 6 객체이름과데이터바인딩을 XAML 에추가합니다. MainWindow.xaml 에추가할것들이여기에있군요. XAML 에서시작태그 <Grid> 와닫는태그 </Grid> 를 StackPanel 로바꿔주세요. 그리고버튼의이름을 newmenu 로합니다. ListView, TextBlock, TextBox 에서데 이터바인딩을사용합니다. 데이터바인딩을사용하는컨트롤의이름을설정할필요가없습니다 ( 심지어는버튼 에도이름을설정할필요가없죠. IDE 에서버튼을더블 - 클릭했을때, newmenu_click 이라는이름의이벤트핸 들러를자동으로추가해줍니다 ). <StackPanel Margin="5" x:name="pagelayoutstackpanel"> <StackPanel Orientation="Horizontal" Margin="0,0,0,10"> <StackPanel Margin="0,0,10,0"> <TextBlock Text="Number of items" Margin="0,0,0,5" /> <TextBox Width="100" HorizontalAlignment="Left" Text="Binding NumberOfItems, Mode=TwoWay" /> <Button x:name="newmenu" VerticalAlignment="Bottom" Click="newMenu_Click" Content="Make a new menu"/> <ListView ItemsSource="Binding Menu" Margin="0,0,20,0" /> <TextBlock> <Run Text="This menu was generated on " /> <Run Text="Binding GeneratedDate"/> </TextBlock> MainWindow.xaml.cs 에윈도우의코드 - 비하인드를추가합니다. 윈도우생성자는메뉴컬렉션과 MenuMaker 의인스턴스를생성하고, 데이터바인딩을사용하기위한컨트롤 의데이터컨텍스트를설정해줍니다. menumaker 라는 MenuMaker 필드가있습니다. MenuMaker menumaker = new MenuMaker(); public MainPage() this.initializecomponent(); pagelayoutstackpanel.datacontext = menumaker; MainWindow.xaml.cs 의메인윈도우클래스는 MenuMaker 필드를가져옵니다. 이것은바인딩된컨트롤을모두포함하는 StackPanel 의데이터컨텍스트로사용되죠. TextBox 에서항목수를설정하고얻기위해서 (get/ set), 양방향데이터바인딩을사용합니다. 이 <Run> 태그가편리한걸보여주네요. 텍스트가바인딩되는부분에서만하나의 TextBlock 을설정할수있습니다. 이제프로그램을실행해봅시다. TextBox 의값을 3 으로설정하고, 아래의 3 가지항목이있는 메뉴를생성해보세요. 바인딩이얼마나유연한지한번살펴봅시다. TextBox 에 xyz 혹은데이터를입력하지않고 엔터키를쳐보세요. 아무일도일어나지않습니다. TextBox 가문자열처리를똑똑하게하네 요. 바인딩경로가 NumberOfItems 인지알고있습니다. 이이름의속성을가진데이터컨텍 스트를찾아서, 문자열을속성의유형에맞게변환해줍니다. 텍스트속성은 NumberOfItems 에바인딩되어있습니다. 그리고데이터컨텍스트에 NumberOfItems 속성을가지고있군요! "3" 인문자열이속성의유형에잘맞을까요? 제가잘처리해보겠습니다. TextBox 객체 아래생성되는날짜를자세히살펴보세요. 메뉴와같이갱신되지않습니다. 아직뭔가해야할일이남았군요. StackPanel 의바깥쪽에서데이터컨텍스트를선언해야합니다. 그래야모든컨트롤이포함된데이터컨텍스 트를전달할수있기때문이죠. 마지막으로 Click 이벤트핸들러메서드스텁을생성하기위해서, 버튼을더블 - 클릭합니다. 메뉴를갱신하는 코드는다음과같습니다. private void newmenu_click(object sender, RoutedEventArgs e) menumaker.updatemenu(); XAML과 C# 코드를동시에갱신할수있도록이벤트핸들러의이름을바꿀수있는쉬운방법이 있습니다. 부록의 남은것들 #8 에가서, IDE 의리펙터링도구에대해서자세한내용을살펴보세요. TextBox 객체 흠 데이터컨텍스트가 NumberOfItems 는 int 형이라고말해주네요. 그리고문자열 "xyz" 를어떻게 int 형으로바꾸는지는저도잘모르겠습니다. 아무것도할수없을것같네요. 564 Appendix ii 지금여기예요 4 565

컨텍스트에데이터넣기 정적리소스로 XAML 의객체를선언해봅시다 XAML 로윈도우를만들때, StackPanel, Grid, TextBlock, Button 과같은객체들과객체그래프를만듭니다. <TextBox> 태그를 XAML 에추가하게되면, 윈도우객체는 TextBox 의인스턴스에대한참조를가진 TextBox 필드를갖게됩니다. 그리고 x:name 속성을이용해서 TextBox 에이름을부여해주죠. 코드-비하인드의 C# 코드가 TextBox 에접근하기위해이이름을사용할수있습니다. XAML 에정적리소스 (Static Resource) 를추가함으로써클래스의인스턴스를생성하고, 윈도우의필드에저장할수있습니다. 그리고데이터바인딩은정적리소스와잘맞습니다. 특히디자이너에서말이죠. 슬라피조의프로그램으로돌아가서 Menu- Maker 를정적리소스로바꿔봅시다. 3 XAML 에정적리소스를추가하고데이터컨텍스트를설정하세요. <Window.Resources> 태그를 XAML 위쪽에추가합니다. 그러면 </Window.Resources> 닫는태그가추가되죠. 그사이 에 <local: 을입력하면인텔리센스창이뜹니다. 클래스생성자의매개변수가없을때만정적리소스를추가할수있습니다. 생성자에매개변수가있다면, XAML 페이지에어떤인수를생성자에넘기죠? 1 2 코드 - 비하인드에서 MenuMaker 필드를삭제합니다. XAML 에서 MenuMaker 클래스와 XAML 컨텍스트데이터를설정하는 C# 코드를지웁니다. MenuMaker menumaker = new MenuMaker(); public MainWindow() this.initializecomponent(); pagelayoutstackpanel.datacontext = menumaker; 프로젝트의네임스페이스를 XAML 에추가합니다. 윈도우의 XAML 코드상단을보세요. xmlns 속성을가진윈도우의시작태그를볼수있습니다. 이속 성들은네임스페이스로정의되어있죠. XAML 로윈도우에정적리소스를추가할때, 이를 FindResource() 메서드로접근할수있습니다. 4 이창은우리가사용하는네임스페이스의모든클래스들을보여줍니다. MenuMaker 를선택하고, 리소스키의 x:key 에 menumaker 를입력합니다. <local:menumaker x:key="menumaker"/> 이제윈도우는 menumaker 라불리는정적 MenuMaker 리소스를갖고있습니다. StackPanel 과 StackPanel 의모든자식에대한 DataContext 를설정합니다. 바깥쪽의 StackPanel 에서 DataContext 속성을설정합니다. <StackPanel Margin="5" DataContext="StaticResource ResourceKey=menuMaker"> 마지막으로, 버튼의 Click 이벤트핸들러를수정합니다. 정적리소스를찾아서, 메뉴를갱신해줍니다. private void newmenu_click(object sender, RoutedEventArgs e) MenuMaker menumaker = FindResource("menuMaker") as MenuMaker; menumaker.updatemenu(); 새로운 xmlns 속성을추가해봅시다. 프로그램은여전히이전처럼동작합니다. 그리고 XAML 에데이터컨텍스트를추가했을때, IDE 에서무슨일이일어났는지 알아챘나요? 데이터컨텍스트가추가되는순간, IDE 는 MenuMaker 의인스턴스를생성하고, 바인딩된모든컨트롤을채우 기위해속성을사용합니다. 프로그램을실행하기전에도이미메뉴가생성되어디자이너에서바로볼수있네요. 놀랍군요! 프로그램을실행하기전에도메뉴는디자이너에서바로볼수있습니다. 이것은 XML 의네임스페이스속성입니다. xmlns: 로되어있고, 뒤에식별자가있습니다. 여기선 local 을사용하네요. 566 인텔리센스창에서항목을선택하면, 아래와같이추가됩니다. 네임스페이스의값이 using: 으로시작할때, 프로젝트의네임스페이스중하나를참조합니다. 또한표준 XAML 네임스페이스를참조하기위해서 http:// 로시작하는네임스페이스가있습니다. xmlns:local="using:sloppyjoechapter10" 프로젝트의네임스페이스에서객체를생성하기위한식별자입니다. 앱의이름은 SloppyJoeChapter10 입니다. 그래서네임스페이스가이렇게생성되었습니다. 앱에해당하는네임스페이스를찾아보세요. 그곳에 MenuMaker 가있습니다. 뭔가이상하군요. 항목수를입력했는데날짜가생성되지않네요. 뭐가잘못된거죠? 지금여기예요 4 567

리스트의분위기를바꿔봅시다 데이터템플릿으로객체를표현하기 리스트에서항목을표시할때, ObservableCollection 에있는객체에바인딩되어있는 ListView 에대한 ListViewItem, ListBoxItem 혹은 ComboBoxItem 컨트롤의내용을보여줍니다. 그리고슬라피조의메뉴목록의각 ListViewItem 객체는 Menu 컬렉션에있는 MenuItem 객체에바인딩됩니다. ListViewItem 객체는기본적으로 MenuMaker 객체의 ToString() 메서드를호출합니다. 또다른방법으로데이터바인딩을사용하는데이터템플릿 (data template) 을이용 하여, 바인딩된객체의속성으로부터데이터를보여줄수있습니다. 이것은기본데이터템플릿입니다. ListViewItem 을표시하는데사용됩니다. <DataTemplate> 을바꿔주세요. ListView 의나머지부분들은그대로남겨두세요. <ListView> 태그를수정하여기본데이터템플릿을추가합니다. 기본 Biding 를이용하여항목의 ToString() 메서드를호출합니다. <ListView ItemsSource="Binding Menu" Margin="0,0,20,0"> <ListView.ItemTemplate> <DataTemplate> <TextBlock Text="Binding"/> </DataTemplate> </ListView.ItemTemplate> </ListView> 데이터템플릿을변경해서메뉴에색깔을입혀봅시다. 경로없이 Binding 을추가하면바인딩된객체의 ToString() 메서드를호출합니다. 각 Run 태그를바인딩할수있습니다. 각태그의색깔과글씨체, 다른속성들도바꿀수있죠. 이럴수가! 데이터템플릿은여러분이원하는어떤컨트롤들도포함할수있습니다. ListView 태그를그대로남겨두고, /> 대신 > 로바꾼뒤, 아래에닫는 </ListView> 태그를추가할수있습니다. 그리고 ListView. ItemTemplate 태그를추가해서데이터템플릿을사용합니다. <DataTemplate> <TextBlock> <Run Text="Binding Meat" Foreground="Blue"/><Run Text=" on "/> <Run Text="Binding Bread" FontWeight="Light"/><Run Text=" with "/> <Run Text="Binding Condiment" Foreground="Red" FontWeight="ExtraBold"/> </TextBlock> </DataTemplate> <DataTemplate> <StackPanel Orientation="Horizontal"> <StackPanel> <TextBlock Text="Binding Bread"/> <TextBlock Text="Binding Bread"/> <TextBlock Text="Binding Bread"/> DataTemplate 객체의 Content 속성은하나의객체에서만사용할수있습니다. 그래서데이터템플릿을다수의컨트롤에적용시키고싶다면, StackPanel 과같은컨테이너를사용하면됩니다. <Ellipse Fill="DarkSlateBlue" Height="Auto" Width="10" Margin="10,0"/> <Button Content="Binding Condiment" FontFamily="Segoe Script"/> </DataTemplate> 바보같은질문이란없습니다 Q : 이제창레이아웃을 StackPanel 이나 Grid 를사용할수있습니다. XAML 정적리소스나코드 - 비하인드에서필드를사용할수도있죠. 컨트롤의속성을설정하거나데이터바인딩도요. 같은일을처리하는방법이왜이렇게많은거죠? A: 앱을만드는데 C# 과 XAML 은굉장히유연한도구입니다. 이유연함은다양한많은장치들과디스플레이에서매우세부적인창을디자인할수있게해주죠. 이것은창의목적맞게사용될수있도록여러분에게매우큰도구상자를제공합니다. 이러한다양한옵션에혼동되지마세요. 다양한옵션을살펴보고, 목적에맞게잘선택하면됩니다. Q : 어떻게돌아가는지이해가잘안되네요. <Window.Resources> 에태그를추가하면어떤일이일어나는거죠? A: 태그를추가할때, Window 객체가갱신되고, 정적리소스를추가합니다. 이경우에는 MenuMaker 의인스턴스를생성하고, Window 객체리소스를추가합니다. Window 객체는 Resources 라불리는딕셔너리를갖고있습니다. 만약이태그를추가한뒤, 디버거를이용해서 Window 객체를살펴본다면, MenuMaker 의인스턴스를포함하는 Resources 를찾을수있습니다. 리소스를선언할때, x:key 를사용하여리소스에키를할당합니다. 이것은윈도우의정적리소스에서 MenuMake 객체를찾기위해서, 이키의사용을허용한것이죠. FindResource() 메서드에서이미살펴봤습니다. Q : Key 를이용하여 MenuMaker 리소스키를설정했습니다. 또, x:name 을이용하여컨트롤에이름을붙였습니다. 뭐가다른거죠? MenuMaker 객체를찾기위해서왜 FindResource() 메서드를이용한거죠? 그리고키대신이름으로찾으면안되나요? A: WPF 창에서컨트롤을추가할때, XAML 에의해서생성된 Window 객체에해당필드가추가됩니다. x:name 속성을사용하면, 코드에이름을붙여줍니다. 만약이름을설정하지않으면, 컨트롤객체는 Window 객체그래프의일부로써여전히생성됩니다. 이름을설정한다면, XAML 객체의해당컨트롤의참조필드에이름을붙여줍니다. 버튼의이벤트핸들러에중단점을놓고, 조사식창에서 newmenu 를추가해서확인할수있습니다. System. Windows.Controls 를참조합니다. 그리고 Content 속성이있는 Button 객체는 "Make a new menu" 로설정됩니다. 리소스는다르게처리됩니다. Window 객체의딕셔너리에추가되죠. FindResource() 메서드는 x:key 에서설정한키를사용합니다. 똑같이중단점을놓고, 조사식창에 this.resources[ menumaker ] 를추가해보세요. 이번에는 MenuMaker 객체의참조를보게될겁니다. 리소스딕셔너리에서해당참조를키로찾기때문이죠. 정적리소스 (static resource) 는이름이조금잘못됐습니다. 각인스턴스마다정적리소스들이생성되기때문이죠. 또그렇다고해서 static 필드는아닙니다. Q : 바인딩경로가꼭문자열속성이어야합니까? A: 아니요. 모든유형의속성으로바인딩할수있습니다. 만약속성의유형과경로의원본이서로변환될수있으면바인딩이동작합니다. 그렇지않다면바인딩이무시되죠. 그리고모든컨트롤의속성들이텍스트로만되어있지는않습니다. EnableMyObject 라불리는데이터컨텍스트에서 bool 값을얻었다고가정해봅시다. 아래의 IsEnabled 처럼어떤 Boolean 속성으로든이것을바인딩할수있습니다. EnableMyObject 속성값에따라컨트롤을활성화하거나 (enable), 비활성화 (disable) 상태로만들어줍니다. IsEnabled="Binding EnableMyObject" 물론, 텍스트속성으로바인딩해서그냥 True 혹은 False 를출력할수있습니다 ( 이것에대해서생각해봤다면, 잘이해한겁니다 ). Q : 정적리소스를추가하고, XAML 에서데이터컨텍스트를설정했을때, C# 에서설정했을때와는달리왜 IDE 는데이터를폼에표시한걸까요? A: IDE 는창을렌더링할때필요한객체를생성하는데모든정보들을알고있기때문이죠. 여러분이 XAML 코드에 MenuMaker 리소스를추가하자마자, IDE 가 MenuMaker 의인스턴스를생성합니다. 하지만생성자안에서 new 문으로이렇게할수없을겁니다. 왜냐하면많은다른선언문들이생성자에있을수있고, 이선언문들이실행되어야하기때문이죠. 프로그램이실행될때 IDE 는단지코드 - 비하인드의 C# 코드만실행합니다. 그러나만약페이지에정적리소스를추가한다면, IDE 는 TextBlock, StackPanel 등페이지에있는다른컨트롤의인스턴스를생성하는것처럼인스턴스를생성합니다. 이는디자이너에나타낼컨트롤의속성들을설정하죠. 그래서데이터컨텍스트와바인딩경로를설정했을때, 이들또한메뉴의항목에반영되어, IDE 의디자이너에나타난거죠. 창이맨처음불려질때, 윈도우의정적리소스들이인스턴스화됩니다. 그리고이정적리소스들은객체에의해언제든지사용될수있습니다. 568 Appendix ii 지금여기예요 4 569

체체체체인지 INotifyPropertyChanged 는바인딩된객체를갱신해줍니다 MenuMaker 클래스가메뉴를수정할때, 바인딩된 ListView 가갱신됩니다. 하지만 MenuMaker 는동시에 GeneratedDate 속성을갱신하지못했죠. 바인딩된 TextBlock 컨트롤이왜이것을갱신하지못했을까요? 그이유는 ObservableCollection 이바뀔때마다이벤트를발생시켜바인딩된컨트롤에게데이터가변했다고알려주기때문입니다. 이는 Button 컨트롤에서클릭했을때, Click 이벤트를발생시키거나 Timer 가특정시간이경과되었을때, Tick 이벤트를발 생시킨것과같습니다. 즉, ObservableCollection 에서항목이추가되거나삭제될때, 이벤트가발생합니다. 데이터객체는타깃속성과바인딩된컨트롤에게데이터가변했는지알려줄수있습니다. PropertyChagned 라는단일 이벤트가포함된 INotifyPropertyChagned 인터페이스를구현해주기만하면됩니다. 속성이변경될때마다이벤트를발 생시켜바인딩된컨트롤을자동으로갱신해줍니다. 데이터객체는 PropertyChanged 이벤트를발생시킵니다. 바인딩된컨트롤에게속성이변했다고알려줍니다. ~PropertyChanged 이벤트 GeneratedDate 속성이바뀔때, 알림을받아서 MenuMaker 를수정해봅시다 INotifyPropertyChanged 는 System.ComponentModel 네임스페이스에있어서, MenuMaker 클래스파일의위쪽에 using 문을추가합니다. using System.ComponentModel; MenuMaker 클래스에 INotifyPropertyChanged 를구현하기위해서, IDE 를이용해자 동으로인터페이스를구현합니다. 7, 8 장에서본것과조금다를수있습니다. 어떠한메서드나속성이추가되지않기때문 이죠. 대신이벤트를추가합니다. public event PropertyChangedEventHandler PropertyChanged; 이렇게이벤트가발생하는것은이번이처음이네요. 1 장에서부터이벤트핸들러 메서드를사용했었죠. 하지 만이벤트를발생시킨건이번이처음이네요. 15 장에서이벤트의동작방식에대해서배울거 예요. 지금은그냥인터페이스에이벤트가포 함될수있다는것만알아두세요. OnPropertyChanged() 메서드는다른객체로이벤트를발 생시킬때, 표준 C# 패턴을따릅니다. 데이터컨텍스트 그리고 PropertyChanged 이벤트를발생시킬 OnPropertyChanged() 메서드를추가합니다. 데이터객체 바인딩 컨트롤객체 private void OnPropertyChanged(string propertyname) PropertyChangedEventHandler propertychangedevent = PropertyChanged; if (propertychangedevent!= null) 이것은이벤트를발생시키는표준.NET 패턴입니다. propertychangedevent(this, new PropertyChangedEventArgs(propertyName)); 소스속성 타깃속성 컨트롤이이벤트를수신하고바인딩된소스속성으로부터데이터를읽어타깃속성을새로고칩니다. 이제바인딩된컨트롤에게속성이변했는지알려주기위해서속성의이름을인수로한 OnPropertyChanged() 를호출합니다. 그 리고매번메뉴를갱신할때마다 GeneratedDate 로바인딩된 TextBlock 을갱신하기위해서, 마지막에한줄의 UpdateMenu() 를추가합니다. 컬렉션은데이터객체와거의비슷한방식으로작동합니다. ObservableCollection<T> 객체는실제로 INotifyPropertyChanged를구현하지않습니다. 대신, 밀접하게연관된 INotifyCollectionChanged라는인터페이스를구현합니다. 이인터페이스는 PropertyChanged 이벤트대신 CollectionChanged 이벤트를발생시킵니다. 컨트롤은이이벤트가언제수신되었는지알고있습니다. 왜냐하면 ObservableCollection은 INotifyCollectionChanged 인터페이스를구현하기때문이죠. ListView의데이터컨텍스트를 INotifyCollectionChanged 객체로설정하여이러한이벤트에게응답하게됩니다. public void UpdateMenu() Menu.Clear(); for (int i = 0; i < NumberOfItems; i++) Menu.Add(CreateMenuItem()); GeneratedDate = DateTime.Now; OnPropertyChanged("GeneratedDate"); 이제메뉴를생성할때, 날짜가바뀌는군요. INotifyPropertyChanged 를구현하는것을잊지마세요. 컨트롤이이인터페이스를구현해야만 데이터바인딩이작동합니다. 만약클 래스선언부에서 : INotifyPropertyChanged 를빼먹는다면, 바인딩된컨트롤은갱신되지않습니다. 데이터객체가 PropertyChanged 이벤트를발생시킬때도요. 570 Appendix ii 지금여기예요 4 571

고피시게임과 XAML 1 2 3 4 고피시게임을 WPF 응용프로그램으로포팅해봅시다. 데이터바인딩을추가하려면이장의앞부분에서처럼 XAML 을수정해야합니다. 그리고 8 장에있는고피시게임의모든클래스와열거형을복사하고 ( 혹은저희웹 사이트에서내려받으세요 ), Player 와 Game 클래스를수정해봅시다. 기존의클래스파일을추가하고, 앱에맞게네임스페이스를고쳐봅시다. 8 장의고피시게임코드를프로젝트에추가해봅시다 (Values.cs, Suits.cs, Card.cs, Deck.cs, Comparer_by- Suit.cs, CardComparer_byValue.cs, Game.cs, Player.cs). 솔루션탐색기에서기존의항목을사용할순있지 만, 새로운프로젝트에맞게각네임스페이스를바꿔야합니다 ( 이책의여러프로젝트에서해온것처럼요 ). 프로젝트를빌드해보세요. Game.cs 와 Player.cs 에에러가이렇게보일거예요. 윈폼클래스와객체의모든참조를지우고, Game 클래스에 using 라인을추가합니다. 여기서윈폼을사용하진않습니다. Game.cs 와 Player.cs 에 Using System.Windows.Forms; 를지워주세요. 그 리고 TextBox 라고적힌건모조리지워주세요. INotifyPropertyChaged 와 ObservableCollection<T> 를사용하 기위해서 Game 클래스를손봐야합니다. Game.cs 의위쪽에아래 using 라인을추가합니다. using System.ComponentModel; using System.Collections.ObjectModel; 정적리소스로사용될 Game 인스턴스를추가하고, 데이터컨텍스트를설정합니다. XAML 을수정하여정적리소스로사용될 Game 인스턴스를추가합니다. 이것을앞에서만든고피시페이 지를포함하는그리드를위한데이터컨텍스트로사용합니다. 정적리소스의 XAML 코드는 <local:game x:name= game /> 입니다. 그리고새로운생성자가필요한데, 매개변수가없는생성자를가진리소스만포함될 수있죠. public Game() PlayerName = "Ed"; Hand = new ObservableCollection<string>(); ResetGame(); 데이터바인딩을위해서 Game 클래스에 public 속성을추가합니다. 윈도우의컨트롤속성을바인딩할속성이아래에있습니다. public bool GameInProgress get; private set; public bool GameNotStarted get return!gameinprogress; public string PlayerName get; set; public ObservableCollection<string> Hand get; private set; public string Books get return DescribeBooks(); public string GameProgress get; private set; XAML 위에꼭 <Window. Resources> 태그를추가해야해요. 그리고 566, 567 쪽에서했던것처럼 xmlns:local 도꼭필요합니다! public void StartGame() ClearProgress(); GameInProgress = true; OnPropertyChanged("GameInProgress"); OnPropertyChanged("GameNotStarted"); Random random = new Random(); players = new List<Player>(); 5 이렇게각각두개씩필요하군요. 6 7 바인딩으로 TextBox, ListBox, Button 을활성 / 비활성 (enable/disable) 상태를설정합니다. 게임을시작하지않았을때만 Your Name 의 TextBox 와 Start the Game 의 Button 을사용할수있도록해봅 시다. 그리고게임을시작할때만 Your hand 의 ListBox 와 Ask for a card Button 을사용할수있도록합니다. Game 클래스에 GameInProgress 속성을추가할거예요. 어떻게돌아가는지살펴보고, 속성을아래와같은추가 하여 TextBox 와 ListBox, 두개의 Button 에바인딩해봅시다. IsEnabled="Binding GameInProgress" IsEnabled="Binding GameInProgress" players.add(new Player(PlayerName, random, this)); players.add(new Player("Bob", random, this)); players.add(new Player("Joe", random, this)); Deal(); players[0].sorthand(); Hand.Clear(); foreach (String cardname in GetPlayerCardNames()) Hand.Add(cardName); if (!GameInProgress) AddProgress(DescribePlayerHands()); OnPropertyChanged("Books"); IsEnabled="Binding GameNotStarted" IsEnabled="Binding GameNotStarted" 게임의진행상황을 Game 클래스에알려주도록 Play 클래스를수정합니다. 윈폼버전인 Player 클래스의생성자는매개변수를 TextBox 로했습니다. 이를 Game 클래스의참조로바꿔주 고, private 필드에담아주세요 ( 어떻게이새로운생성자가플레이어를추가할때사용되는지아래의 Start- Game() 메서드를살펴보세요 ). 그리고 TextBox 참조를사용하는줄을찾아서 Game 객체의 AddProgress() 메 서드를호출하는걸로바꿔주세요. Game 클래스를수정합니다. bool 대신 void 를반환하도록 PlayOneRound() 메서드를수정합니다. 그리고게임의진행상황을알리기위해서, TextBox 대신 AddProgress() 메서드를추가합니다. 만약플레이어가이겼을때는게임을리셋하고, 처음으로돌 아가게합니다. 패배했다면 Hand 컬렉션을초기화한후, 플레이어에게카드를나눠줍니다. 4 개의메서드를추가 / 수정해야합니다. 이메서드들이어떻게동작하는지살펴보세요. public void ClearProgress() GameProgress = String.Empty; OnPropertyChanged("GameProgress"); public void AddProgress(string progress) GameProgress = progress + Environment.NewLine + GameProgress; OnPropertyChanged("GameProgress"); 그리고속성을갱신하기위해서 INotifyPropertyChanged 인터페이스를구현하고, MenuMaker 클래스와같은 OnPropertyChanged() 메서드를추가합니다. public void ResetGame() GameInProgress = false; OnPropertyChanged("GameInProgress"); OnPropertyChanged("GameNotStarted"); books = new Dictionary<Values, Player>(); stock = new Deck(); Hand.Clear(); 572 Appendix ii 지금여기예요 4 573

연습문제정답 Player 클래스가변경된부분입니다. Game game; public MainWindow() 여기에모든코드-비하인드가있군요. InitializeComponent(); game = this.findresource("game") as Game; private void startbutton_click(object sender, RoutedEventArgs e) game.startgame(); private void askforacard_click(object sender, RoutedEventArgs e) if (cards.selectedindex >= 0) game.playoneround(cards.selectedindex); private void cards_mousedoubleclick(object sender, MouseButtonEventArgs e) if (cards.selectedindex >= 0) game.playoneround(cards.selectedindex); class Player private string name; public string Name get return name; private Random random; private Deck cards; private Game game; public Player(String name, Random random, Game game) this.name = name; this.random = random; this.game = game; this.cards = new Deck(new Card[] ); game.addprogress(name + " has just joined the game"); public Deck DoYouHaveAny(Values value) Deck cardsihave = cards.pulloutvalues(value); game.addprogress(name + " has " + cardsihave.count + " " + Card.Plural(value)); return cardsihave; public void AskForACard(List<Player> players, int myindex, Deck stock, Values value) game.addprogress(name + " asks if anyone has a " + value); int totalcardsgiven = 0; for (int i = 0; i < players.count; i++) if (i!= myindex) Player player = players[i]; Deck CardsGiven = player.doyouhaveany(value); totalcardsgiven += CardsGiven.Count; while (CardsGiven.Count > 0) cards.add(cardsgiven.deal()); if (totalcardsgiven == 0) game.addprogress(name + " must draw from the stock."); cards.add(stock.deal()); XAML 에서변경된부분입니다. <Grid Margin="10" DataContext="StaticResource ResourceKey=game"> <TextBlock Text="Your Name" /> <StackPanel Orientation="Horizontal" Grid.Row="1"> <TextBox x:name="playername" FontSize="12" Width="150" Text="Binding PlayerName, Mode=TwoWay" IsEnabled="Binding GameNotStarted" /> TextBox 는 PlayerName 에양방향바인딩되어있군요. <Button x:name="startbutton" Margin="5,0" IsEnabled="Binding GameNotStarted" Content="Start the game!" Click="startButton_Click"/> <TextBlock Text="Game progress" Grid.Row="2" Margin="0,10,0,0"/> <ScrollViewer Grid.Row="3" FontSize="12" Background="White" Foreground="Black" Content="Binding GameProgress" /> <TextBlock Text="Books" Margin="0,10,0,0" Grid.Row="4"/> <ScrollViewer FontSize="12" Background="White" Foreground="Black" Grid.Row="5" Grid.RowSpan="2" Content="Binding Books" /> <TextBlock Text="Your hand" Grid.Row="0" Grid.Column="2" /> <ListBox x:name="cards" Background="White" FontSize="12" Height="Auto" Margin="0,0,0,10" Grid.Row="1" Grid.RowSpan="5" Grid.Column="2" ItemsSource="Binding Hand" IsEnabled="Binding GameInProgress" MouseDoubleClick="cards_MouseDoubleClick" /> <Button x:name="askforacard" Content="Ask for a card" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Grid.Row="6" Grid.Column="2" Click="askForACard_Click" IsEnabled="Binding GameInProgress" /> <Grid.ColumnDefinitions> <ColumnDefinition Width="5*"/> <ColumnDefinition Width="40"/> <ColumnDefinition Width="2*"/> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto" MinHeight="150" /> <RowDefinition Height="Auto"/> </Grid.RowDefinitions> </Grid> IsEnabled 속성은부울속성으로컨트롤을활성화하거나비활성화합니다. 이속성을부울속성으로바인딩합니다. 그래서속성에따라컨트롤을활성 / 비활성 (on/off) 상태로만들어줍니다. 그리드의데이터컨텍스트는 Game 클래스입니다. 모든바인딩은이클래스의속성이기때문이죠. 여기에시작버튼의 Click 이벤트핸들러가있군요. 게임진행과북의 ScrollViewer 는 GameProgress 와 Books 속성에바인딩되었군요. // 나머진기존 Player 클래스와같습니다 574 Appendix ii 지금여기예요 4 575

연습문제정답 이속성들은 XAML 의데이터바인딩에사용되는군요. 이메서드들은게임진행데이터바인딩을동작하게합니다. 새로운게임진행의라인은윗부분에추가됩니다. 이전에있던라인은 ScrollViewer 아래에있습니다. 문제에서제공한 StartGame() 메서드입니다. 이메서드는게임진행을초기화하고, 플레이어를생성합니다. 카드를돌리고, 상황과북을업데이트합니다. Game 클래스가변경된부분입니다. 문제에서설명한코드가포함되어있습니다. using System.ComponentModel; using System.Collections.ObjectModel; class Game : INotifyPropertyChanged private List<Player> players; private Dictionary<Values, Player> books; private Deck stock; public bool GameInProgress get; private set; public bool GameNotStarted get return!gameinprogress; public string PlayerName get; set; public ObservableCollection<string> Hand get; private set; public string Books get return DescribeBooks(); public string GameProgress get; private set; public Game() PlayerName = "Ed"; Hand = new ObservableCollection<string>(); ResetGame(); public void AddProgress(string progress) GameProgress = progress + Environment.NewLine + GameProgress; OnPropertyChanged("GameProgress"); public void ClearProgress() GameProgress = String.Empty; OnPropertyChanged("GameProgress"); INotifyPropertyChanged 와 ObservableCollection 를위한라인입니다. public void StartGame() ClearProgress(); GameInProgress = true; OnPropertyChanged("GameInProgress"); OnPropertyChanged("GameNotStarted"); Random random = new Random(); players = new List<Player>(); players.add(new Player(PlayerName, random, this)); players.add(new Player("Bob", random, this)); players.add(new Player("Joe", random, this)); Deal(); players[0].sorthand(); Hand.Clear(); foreach (String cardname in GetPlayerCardNames()) Hand.Add(cardName); if (!GameInProgress) AddProgress(DescribePlayerHands()); OnPropertyChanged("Books"); 새로운 Game 클래스의생성자군요. 게임이리셋될때, 단하나의컬렉션을생성하고, 초기화합니다. 만약새컬렉션객체가생성되면, 이전객체에대한참조를잃고, 업데이트가중단됩니다. 지금까지이책에서만든모든프로그램은 XAML 을사용한 WPF 응용프로그램과같이적용하거나다시만들수있습니다. 만드는방법은다양하죠. 특히 XAML 을사용한다면요! 연습문제에서이러한많은코드의문제를주는이유이기도하죠. public void PlayOneRound(int selectedplayercard) Values cardtoaskfor = players[0].peek(selectedplayercard).value; for (int i = 0; i < players.count; i++) if (i == 0) players[0].askforacard(players, 0, stock, cardtoaskfor); else players[i].askforacard(players, i, stock); if (PullOutBooks(players[i])) AddProgress(players[i].Name + " drew a new hand"); int card = 1; while (card <= 5 && stock.count > 0) players[i].takecard(stock.deal()); card++; OnPropertyChanged("Books"); players[0].sorthand(); if (stock.count == 0) AddProgress("The stock is out of cards. Game over!"); AddProgress("The winner is... " + GetWinnerName()); ResetGame(); return; Hand.Clear(); foreach (String cardname in GetPlayerCardNames()) Hand.Add(cardName); if (!GameInProgress) AddProgress(DescribePlayerHands()); public void ResetGame() GameInProgress = false; OnPropertyChanged("GameInProgress"); OnPropertyChanged("GameNotStarted"); books = new Dictionary<Values, Player>(); stock = new Deck(); Hand.Clear(); public event PropertyChangedEventHandler PropertyChanged; private void OnPropertyChanged(string propertyname) PropertyChangedEventHandler propertychangedevent = PropertyChanged; if (propertychangedevent!= null) propertychangedevent(this, new PropertyChangedEventArgs(propertyName)); 폼이진행상황을업데이트하기위해서, Boolean 값을반환했던부분이네요. 지금은그냥 AddProgress() 메서드를호출하고, 데이터바인딩을통해진행상황을업데이트해줍니다. // 나머진기존 Game 클래스와같습니다. 북이바뀌면, 폼은이를알아차려서 ScrollViewer 를갱신합니다. PlayOneRound() 메서드가수정된부분이군요. 게임이끝났을때, 진행상황을업데이트해줍니다. 그렇지않은경우, 플레이어의카드와북을업데이트해주는군요. 문제에서본 ResetGame() 메서드네요. books, stock, hand 를초기화해줍니다. 이전에본표준 PropertyChanged 이벤트패턴입니다. 576 Appendix ii 지금여기예요 4 577

Chapter 11 네임스페이스에클래스가존재하지않는다는 XAML 오류를만났나요? 모든 C# 코드가컴파일되었는지확인하고, 모든컨트롤의이벤트핸들러가코드 - 비하인드에선언되었는지확인해보세요. 심지어 MyWpfApplication 네임스페이스에 MyDataClass 라불리는클래스를확실히선언했다고하더라도 정적리소스를선언하지않을때, 이와같은오류를볼수있습니다. 코드 - 비하인드혹은 XAML 컨트롤의이벤트핸들러가없을경우자주발생하는오류입니다. 약간의오해의소지가있을 수있습니다. 왜냐하면다른곳에서코드오류가발생했는데, 정적리소스가선언된태그에오류가발생했다고알려주기 때문이죠. 여러분이이오류를재현할수있습니다. MyWpfApplication 이라는새 WPF 프로젝트를생성하고, MyDataClass 라는데이 터클래스를추가한뒤, <Window.Resources> 에정적리소스로이클래스를추가합니다. 그리고창에버튼을추가해주세 요. 그러고나서버튼의이벤트핸들러를 XAML 에추가하기위해서, Button 태그에 Click= Button_Click 을추가합니다. 절대 로코드 - 비하인드에 Button_Click() 메서드를추가하지마세요. 코드를다시빌드했을때, 위와같은오류를볼수있습니다. 코드 - 비하인드에 Button_Click() 메서드를추가하면, 오류가사라집니다. 윈도우스토어는비동기프로그래밍을고려해서만들었습니다. WPF 에서도할수있지만, 비동기프로그래밍에대한도구를많이제공하지않습니다. 책 580, 581 쪽을읽어주세요 (9 장에서본익숙한파일클래스가다어디로갔죠?) 글쎄요, WPF 앱에서는문제될게없습니다. 여러분이이미사용하고있는파일클래스와직렬화 를계속사용할수있습니다. 하지만 WPF 응용프로그램은윈도우스토어에대한.NET 프레임워크와함께제공되는새로운비동기파일및다이얼로그클래스를사용할수없습 니다. 책에서윈도우스토어앱에대해서많이다루지만, 부록의 WPF 에서도핵심내용을배울수있습니다. 이부록에서는 async 와 await 키워드, 데이터컨트렉트직렬화를사용해서두개의 WPF 프로젝트를진행합니다. 11 장에서는아래의방법을추천합니다. 솔루션탐색기에서프로젝트를마우스오른쪽클릭하여, " 프로젝트언로드 "(Unload Project) 를선택하고, 다시 " 프로젝트다시로드 "(Reload Project) 를선택했을때, 오류가조금더명확해지는경우가있습니다. 여러분에게더많은도움이되는다른오류메시지가표시될수도있습니다. 이부록은 582, 583쪽을대체합니다. 584-589 쪽은윈도우스토어앱입니다. 넘겨주세요. 책 590, 591쪽에서데이터컨트랙트직렬화에대한내용을읽어주세요. 592-594 쪽은윈도우스토어앱입니다. 넘겨주세요. 책 595 쪽을읽어주세요. 그리고이부록에서책 596-600 쪽 직접해보세요! 를 대체하는프로젝트를진행합니다. 이책의나머지부분은브라이언의변명관리에대한윈도우스토어앱을다룹니 다. 이프로젝트의목표는윈도우스토어앱의 Windows.Storage 네임스페이스 에있는파일도구를배웁니다. 이클래스들은윈도우스토어앱에한정되어있기 때문에, WPF 에서이프로젝트를대체할수없습니다. 578 Appendix ii 지금여기예요 4 579

절기다리게하지마세요 반응적인 await 윈폼프로그램에서 MessageBox.Show() 를호출했을때무슨일이일어났죠? 모든것이중단되고, 프로그램은대 화상자가사라질때까지잠시멈춰져있었습니다. 이프로그램은별로반응적이지못하네요. WPF 앱은항상반응 적이어야합니다. 심지어사용자가피드백을기다리는동안에도말이죠. 하지만, 대화상자를기다리거나모든파일 을읽고쓰는일은시간이오래걸립니다. 어떤메서드가수행될때, 다른나머지프로그램들이이메서드의수행이 끝날때까지기다리는것을블로킹 (Blocking) 이라고합니다. 이것이반응적이지못한프로그램의가장큰이유이기 도하죠. 윈도우스토어앱은블록상태에있는동안반응적이지못한것을방지하기위해서 await 연산자 (operator) 와 async 제한자 (modifier) 를사용합니다. WPF 로하나의작업을정의해서어떻게블록상태가되는지그리고이를 어떻게비동기적으로처리하는지예제를통해살펴봅시다. await 연산자는이코드에서수행하고있는메서드를잠시멈춥니다. 그리고 LongTaskAsync () 메서드수행이끝 날때까지기다립니다. 이메서드는사용자가하나의명령을내릴때까지블록상태가됩니다. 그사이나머지프로 그램은다른이벤트와반응을유지합니다. LongTaskAsync() 메서드가반환되자마자이를호출한메서드는블록된 메서드를깨워줍니다 ( 그사이수행된다른모든이벤트가끝날때까지기다릴수도있습니다 ). 만약메서드가 await 연산자를사용한다면, async 제한자를반드시선언해야합니다. private async void countbutton_click(object sender, RoutedEventArgs e) 비동기적으로호출될수있다는것을나타내기위해서 async 제한자로메서드를선언합니다. private async Task LongTaskAsync() await Task.Delay(5000); //... 어떤코드... await LongTaskAsync(); //... 어떤코드...: 메서드가 async 로선언되었을때, 이메서드를호출하는몇가지옵션이있습니다. 평소와같은방법으로이메서 드를호출한다면, await 문이호출되자마자앱의블로킹상태를막은뒤, 그결과값이반환됩니다. Task 클래스는 System. Therading.Tasks 네임스페이스에있습니다. 그리고 Delay() 메서드는지정한밀리초단위로메서드수행을블록상태로만듭니다. 이메서드는 2 장에서본 Thread.Sleep() 메서드와매우비슷합니다. 하지만 await 연산자를통해비동기적으로호출될수있도록, async 제한자와함께선언되어야합니다. 새로운 WPF 응용프로그램을생성하고다음과같이메인윈도우 XAML 을추가해주세요. 어떻게코드가동작하는 지볼수있습니다. <Window x:class="wpfandasync.mainwindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="WPF and async" Height="150" Width="200" ResizeMode="CanResizeWithGrip"> <Grid> <StackPanel> <CheckBox x:name="useawaitasync" IsChecked="True" Content="Use await/async" Margin="5"/> <Button x:name="countbutton" Content="Start counting" HorizontalAlignment="Left" Click="countButton_Click" Margin="5"/> <TextBlock x:name="progress" HorizontalAlignment="Left" Margin="5" /> </Grid> </Window> 코드 - 비하인드입니다. using System.Threading; using System.Windows.Threading; public partial class MainWindow : Window DispatcherTimer timer = new DispatcherTimer(); public MainWindow() InitializeComponent(); timer.tick += timer_tick; timer.interval = TimeSpan.FromSeconds(.1); int i = 0; void timer_tick(object sender, EventArgs e) progress.text = (i++).tostring(); private async void countbutton_click(object sender, RoutedEventArgs e) countbutton.isenabled = false; timer.start(); if (useawaitasync.ischecked == true) await LongTaskAsync(); else LongTask(); countbutton.isenabled = true; 582 Appendix ii 응상태에빠지게됩니다. 지금여기예요 4 583 private void LongTask() Thread.Sleep(5000); timer.stop(); private async Task LongTaskAsync() await Task.Delay(5000); timer.stop(); 프로젝트이름을 WpfAndAsync 로지었습니다. 만약프로젝트를다른이름으로설정했다면, 여러분은다음부분을프로젝트의이름에맞게고쳐야합니다. x:class= WpfAndAsync.MainWindow 버튼의이벤트핸들러는체크박스의 IsChecked 속성을사용합니다. 박스가체크되었다면, 이벤트핸들러는비동기메서드인 await LongTaskAsync() 를호출합니다. 이메서드는 await 키워드와함께호출되어서, 이벤트핸들러를잠시멈추고, 프로그램의나머지부분을계속실행할수있게해줍니다. 창의속성을바꾸거나, 출력창에텍스트를보여주는다른버튼을추가해보세요. 타이머가틱을하는동안이버튼을사용합니다. 체크박스가체크되어있지않으면, IsChecked 는 false 가됩니다. 그리고이벤트핸들러는 LongTask() 를호출해서블록상태에빠지게되어, 전체프로그램이무반응상태가됩니다. 추가한다른버튼들도마찬가지로이벤트에아무런반응이없습니다. 체크박스가체크된상태에서버튼을클릭하세요. 숫자가증가하면서, 폼이반응적이게됩니다. 버튼이비활성화되 면서, 폼을움직이거나크기를조정할수있죠. 만약체크박스가체크되지않은상태에서버튼을누르면, 폼은무반

사람이여기저기에있네요 Guy 객체를파일로보내주세요 데이터컨트랙트직렬화실습프로젝트를해봅시다. 새 WPF 응용프로그램을생성하고, 책 595 쪽 과같이데이터컨트랙트가있는두클래스를추가합니다 ( 두클래스모두 using System.Runtime. Serialization 이필요합니다 ). Card 클래스를위한 Suites 와 Values 열거형을추가해주세요. 그리 고아래와같은창을만들면됩니다. 1 코딩을시작하기전에솔루션탐색기 > 참조 ( 마우스오른쪽클릭 ) > 참조추가메뉴를선택합니다. 프레임워크를선택한후 System.Runtime.Serialization 을찾아서체크하세요. 그리고확인을클릭합니다. 이것은 WPF 응용프로그램이 System.Runtime.Serialization 네임스페이스를사용하겠다는것을의미합니다. 과정 2 에서 XAML 을추가할때, <local:guymanager> 의오류를지우기위해서빈 GuyManager 클래스를추가할수있습니다. 과정 3 에서 GuyManager 클래스의내용을채웁니다. 직접해봅시다! 2 윈도우의 XAML 코드입니다. <Window x:class="guyserializer.mainwindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:guyserializer" Title="Guy Serializer" Height="275" Width="525" ResizeMode="NoResize"> <Window.Resources> <local:guymanager x:key="guymanager"/> </Window.Resources> <Grid DataContext="StaticResource guymanager" Margin="5"> <Grid.ColumnDefinitions> <ColumnDefinition/> <ColumnDefinition/> <ColumnDefinition/> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="4*"/> <RowDefinition Height="3*"/> </Grid.RowDefinitions> 윈도우는두개의행과세개의열이있습니다. <StackPanel> <Button x:name="writejoe" Content="Write Joe" HorizontalAlignment="Left" Click="WriteJoe_Click"/> <TextBlock Text="Binding Joe" Margin="0,0,10,20" TextWrapping="Wrap"/> <StackPanel Grid.Column="1"> <Button x:name="writebob" Content="Write Bob" HorizontalAlignment="Left" Click="WriteBob_Click"/> <TextBlock Text="Binding Bob" Margin="0,0,0,20" TextWrapping="Wrap"/> <StackPanel Grid.Column="2" Margin="10,0,0,0"> <Button x:name="writeed" Content="Write Ed" HorizontalAlignment="Left" Click="WriteEd_Click"/> <TextBlock Text="Binding Ed" Margin="0,0,0,20" TextWrapping="Wrap"/> <StackPanel Grid.Row="1" Grid.ColumnSpan="2" Margin="0,0,20,0"> <TextBlock>Last filename written</textblock> <TextBox Text="Binding GuyFile, Mode=TwoWay" TextWrapping="Wrap" Height="60" Margin="0,0,0,20"/> <StackPanel Grid.Row="1" Grid.Column="2" Margin="10,0,0,0"> <Button x:name="readnewguy" Content="Read a new Guy" HorizontalAlignment="Left" Click="ReadNewGuy_Click" /> <StackPanel> <TextBlock Text="New guy:"/> <TextBlock TextWrapping="Wrap" Text="Binding NewGuy"/> 이프로젝트의이름은 GuySerializer 입니다. 프로젝트에다른네임스페이스를사용한다면, 이줄을여러분의네임스페이스에맞게바꿔주세요. 그리드의데이터컨택스트는 GuyManager 의정적리소스입니다. 위쪽행에있는각각의열은 TextBlock 과 Button 으로이루어진 StackPanel 이있습니다. 이 TextBlock 은 GuyManager 의 Ed 속성에바인딩되어있습니다. 맨아래쪽행의첫번째셀은두열을차지하고있네요. 몇몇의컨트롤이속성에바인딩되었군요. 왜 TextBox 에 Path( 경로 ) 가설정되었을까요? 아직안끝났어요. 페이지를넘겨주세요! </Grid> </Window> 596 Appendix ii 지금여기예요 4 597

관심사분리를생각해봅시다 3 GuyManager 클래스를추가합니다. using System.ComponentModel; using System.IO; using System.Runtime.Serialization; class GuyManager : INotifyPropertyChanged private Guy joe = new Guy("Joe", 37, 176.22M); public Guy Joe get return joe; private Guy bob = new Guy("Bob", 45, 4.68M); public Guy Bob get return bob; private Guy ed = new Guy("Ed", 43, 37.51M); public Guy Ed get return ed; public Guy NewGuy get; set; public string GuyFile get; set; public void ReadGuy() if (String.IsNullOrEmpty(GuyFile)) return; 이프로그램에서는 get 접근자만으로읽기전용의속성으로바인딩된 TextBox 를사용합니다. private set 접근자와 public get 접근자를가진속성으로바인딩을한다면, 오류가발생합니다. 이코드와같이백킹필드를사용하면아무런문제가없습니다. 3 개의읽기전용 Guy 속성이 있습니다. private 백킹필드군요. XAML 에서 TextBlock 은이들을바인딩하고있습니다. 네번째 TextBlock 은이 Guy 속성에바인딩되어있습니다. 이속성은 ReadGuy() 메서드에의해설정됩니다. 파일이이미존재한다면, 지워진후, 파일스트림을이용해파일을다시만듭니다. 그리고데이터컨트랙트직렬화를사용합니다. 4 MainWindow.xaml.cs 의코드 - 비하인드입니다. public partial class MainWindow : Window GuyManager guymanager; public MainWindow() InitializeComponent(); public void WriteGuy(Guy guytowrite) GuyFile = Path.GetFullPath(guyToWrite.Name + ".xml"); if (File.Exists(GuyFile)) File.Delete(GuyFile); using (Stream outputstream = File.OpenWrite(GuyFile)) DataContractSerializer serializer = new DataContractSerializer(typeof(Guy)); serializer.writeobject(outputstream, guytowrite); OnPropertyChanged("GuyFile"); public event PropertyChangedEventHandler PropertyChanged; private void OnPropertyChanged(string propertyname) PropertyChangedEventHandler propertychangedevent = PropertyChanged; if (propertychangedevent!= null) propertychangedevent(this, new PropertyChangedEventArgs(propertyName)); guymanager = FindResource("guyManager") as GuyManager; 파일을쓸전체경로를얻기위해서, Path 클래스 (System.IO 네임스페이스 ) 의 GetFullPath() 메서드를사용합니다. 이전장에서본코드와똑같네요. INotifyPropertyChanged 를구현하고 PropertyChanged 이벤트를발생시킵니다. using (Stream inputstream = File.OpenRead(GuyFile)) private void WriteJoe_Click(object sender, RoutedEventArgs e) guymanager.writeguy(guymanager.joe); DataContractSerializer serializer = new DataContractSerializer(typeof(Guy)); private void WriteBob_Click(object sender, RoutedEventArgs e) NewGuy = serializer.readobject(inputstream) as Guy; guymanager.writeguy(guymanager.bob); OnPropertyChanged("NewGuy"); private void WriteEd_Click(object sender, RoutedEventArgs e) guymanager.writeguy(guymanager.ed); private void ReadNewGuy_Click(object sender, RoutedEventArgs e) guymanager.readguy(); 598 Appendix ii 지금여기예요 4 599 ReadGuy() 메서드는스트림을열고, 읽기위해서 System.IO 메서드를사용하는것과비슷합니다. XML 파일의데이터를직렬화하기위해서, BinaryFormatter 를사용하는대신 DataContractSerializer 를사용합니다.

직렬화하기 Guy Serializer 의시범운행 Guy Serializer 로데이터컨트랙트직렬화를시험해보세요. 각 Guy 객체를프로젝트폴더 > bin > Debug 폴더에파일로씁니다. 그리고작성된사람의정보를읽기위해서 ReadGuy 버튼을클릭합니다. 파일을읽기위해서 TextBox 에있는경로를사용하죠. 다른사람의정보를읽기위해서경로를바꿔봅시다. 그리고존재하지않는파일을읽어봅시다. 무슨일이일어나나요? 전에만든텍스트편집기를열어봅니다. 파일을열고저장하는파일피커의옵션을 XML 파일에추가했기때문에사람 (Guy) 파일을편집할수있습니다. 한사람의파일을열어보고, 수정및저장을해봅시다. 그리고다시편집한파일을 Guy Serializer 에서읽어봅시다. XML 이유효하지않다면어떤일이일어날까요? 카드무늬나숫자가변했다면, 유효한 enum 값과일치할까요? [DataMember(Name= )] 의이름을추가하거나제거해보세요. XML에무슨일이일어나나요? 컨트랙트를수정했을때, 이전에저장된 XML 파일을불러오나요? 프로그램이올바르게동작하도록 XML 파일을고쳐봅시다. Card 데이터컨트랙트의네임스페이스를바꿔보세요. XML에무슨일이일어나나요? Q : 가끔 XAML 이나코드를수정할때, IDE 디자이너는다시빌드하라는메시지를줍니다. 왜그렇죠? A: IDE 의 XAML 디자이너는정말로똑똑합니다. XAML 코드를수정할때, 수정된페이지를실시간으로볼수있게해줍니다. XAML 이정적리소스를사용할때, Window 클래스에대한객체참조를추가하는것은이미알고있습니다. 이객체가디자이너에서보이기위해서는초기화될필요가있습니다. 그리고정적자원에사용되는클래스를수정할경우, 클래스를다시빌드할때비로소디자이너가업데이트됩니다. IDE 가여러분의프로젝트를다시빌드하라고요청하는것은당연한일입니다. 정적리소스를인스턴스화할필요가있는메모리에서컴파일된코드가실제로없다면말이죠. IDE 에서이렇게동작하는과정을한번살펴봅시다. Guy Serializer 를열고, Guy.ToString() 메서드에서반환값을다른단어로수정합니다. 디자이너는아직수정되지않은결과값을보여줍니다. 이제메뉴에서솔루션다시빌드를선택합니다. 코드가다시빌드되자마자디자이너스스로업데이트합니다. 다른코드도바꿔보세요. 잠깐, 다시빌드는아직하지마세요. 대신 Guy 객체에바인딩된또다른 TextBlock 을추가해보세요. IDE 가다시빌드될때까지, 객체의이전버전을사용하고있을거예요. 바보같은질문이란없습니다 Q : 네임스페이스가아직도헷갈리네요. 프로그램의네임스페이스와 XML 파일의네임스페이스는뭐가다른거죠? A: 네임스페이스가왜필요한지다시한번생각해봅시다. C#, XML 파일, 윈도우파일시스템, 웹페이지등모두다른네이밍시스템을사용합니다 ( 보통연관되어있습니다 ). 각클래스, XML 문서, 파일혹은웹페이지에대한자신의유일한이름이있죠. 그래서왜이러한네임스페이스가중요한걸까요? 9 장에서브라이언이변명폴더를알아내기위해서 KnownFolders 클래스를만들었습니다. 이제.NET 프레임워크가이미 KnownFolders 클래스가있는지살펴보세요..NET 의 KnownFolders 클래스는 Windows.Storage 네임스페이스에있어서걱정할필요가없습니다. 같은클래스이름이라도나란히행복하게있군요. 정말명확 (disambiguation) 하군요. 데이터컨트랙트또한차이를보여야합니다. 이책에서몇개의다른 Guy 클래스를봤습니다. 만약 2 개의다른컨트랙트를서로다른 Guy 로직렬화하고싶다면어떻게해야할까요? 이둘을명확히구분하기위해서서로다른네임스페이스에두면됩니다. 그리고여러분은클래스와컨트랙트를혼동할필요가없습니다. 네임스페이스는클래스에대한것들과분리되는게맞는거죠. Chapter 12 9 장에서브라이언의변명관리프로그램을만들었습니다. 그런데, 몇몇의버그가있네요. 이번장에서이들을고쳐봅시다. WPF 의예외처리는윈폼과윈도우스토어앱과같은방식으로동작합니다. 12 장의부록은 XAML 코드가없습니다. 왜냐하면 WPF 응용프로그램, 윈폼프로그램, 윈도우스토어앱, 심지어콘솔응용프로그램에서든지상관없이헤드퍼스트 C# 에서는 같은예외처리에대해서학습하는내용이같기때문이죠. 12 장에서참고해야할사항입니다. 책에서 619쪽까지읽어주세요. 연필을깎으며 문제도풀어주세요. 이부록은 620, 621쪽을대체합니다. 책 622, 623쪽을읽어주세요. 부록 624-634 쪽을읽어주세요. 책 635쪽을건너뜁니다. 그리고책 12장의나머지부분을읽어주세요. 13장은책을읽으면됩니다. 한가지더있습니다. WPF 응용프로그램에서윈폼과같은 OpenFileDialog 와 SaveFileDialog 클래스를사용할수있습니다. 조금더자세한정보와코드예제는아래에있습니다. http://msdn.microsoft.com/ko-kr/library/aa969773.aspx 이번장을마치고, 책 13 장으로바로가서읽으면됩니다. 13 장은윈도우 8 스토어앱에의존하지않습니다. 600 Appendix ii 지금여기예요 4 601

아무도예상치못한 브라이언의코드도예상치못한방식으로동작했군요 브라이언은변명관리프로그램을만들당시에사용자가빈디렉토리에서 Random Excuse 버튼을클릭하려고한다는점은전혀예상치못했습니다. 1 이예제는 9 장에서만들었던변명관리프로그램입니다. 부록과코드가맞지않다면, 아래의사이트에서코드를내려받으세요. http://www.hanbit.co.kr/exam/2165 노트북에설치한브라이언의변명관리프로그램은디렉토리가비어있는상태에서 Random 버 튼을클릭했을때문제가발생했습니다. 먼저코드를살펴보고어떤점이잘못됐는지찾아봅시다. IDE 외부에서프로그램실행시띄워진처리되지않은예외창이여기에나와있습니다. 4 무슨일이일어났죠? 빈폴더를선택했을때, Directory.GetFiles() 에서빈배열을반환합니다. 그래서 filenames. Length는 0이고, Random.Next() 에 0을전달하면항상 0을반환합니다. 빈배열에 0번째요소를접근해보세요. 그러면인덱스가배열의범위를벗어났다 (Index was outside the bounds of the array) 는메시지와함께 System.IndexOutOfRangeException 예외가발생합니다. 문제파악을했으니, 한번고쳐봅시다. 임의의변명 (Random excuse) 을불러오기전에, 선택한폴더에서변명파일이있는지확인하면됩니다. 2 여기서부터시작해볼까요? 내용을보니어떤값이어떤범위에들어가지못한다고하는 군요. 아래의표시된코드줄에중단점을설정하고, 디버거를실행해봅시다. public async void OpenRandomExcuseAsync() string[] filenames = Directory.GetFiles(folder, "*.excuse"); OpenFile(fileNames[random.Next(fileNames.Length)]); 직접해봅시다! private void randomexcuse_click(object sender, EventArgs e) if (Directory.GetFiles(selectedFolder).Length == 0) MessageBox.Show("There are no excuse files in the selected folder."); else if (CheckChanged()) currentexcuse = new Excuse(random, selectedfolder); UpdateForm(false); 위코드에대해서어떻게생각하세요? 이코드를폼에넣는게좋을까요? 아니면 Excuse 클래스내부에캡슐화를하는것이더좋을까요? 오, 알겠어요. 예외가항상나쁜것만은아니군요. 예외는버그를식별하기도하지만, 대부분은제가예상한것과다르게일어나는상황이무엇인지알려주는군요. Excuse 객체를생성하기전에폴더에변명파일이있는지검사해서예외가발생되는것을방지하고, 도움이되는메시지를보여주고있습니다. 3 문제를추적하기위해서조사식창을이용해봅시다. 이름에 files.length 를추가하세요. 0을반환하는것같군요. 그리고 random.next(filenames.length) 를추가해보면, 역시 0을반환합니다. 이름에 filenames[random.next(filenames.length)] 를추가합니다. 조사식창의값열에서과정 1에서본오류 ( 배열의인덱스범위가넘어감 : Out of bounds array index) 를볼수있습니다. 맞습니다. 예외는예상치못한동작을하는코드가어디에있는지찾아낼수있는매우유용한도구입니다. 많은프로그래머들이예외를처음접하게되면당황하지만, 실제로는매우유용하며좋은점이많습니다. 예외는여러분의코드가언제예상치못한상황에대응하게될것인지를알수있게도움이되는단서를제공해줍니다. 또한여러분의프로그램이처리해야만하는새로운시나리오에대한정보를알려주며, 이를처리할수있는기회를제공합니다. 조사식창에서메서드를호출하고, 반환된값으로배열의인수 ( 인덱서 ) 를사용할수있습니다. 여기서예외가발생한다면, 조사식창에서그예외를이렇게볼수있죠. 620 Appendix ii 지금여기예요 4 621

조사식탐정가 디버거를사용해서변명관리프로그램의문제를살펴봅시다 3 프로그램이예외를던질때까지, 한단계씩코드를실행하세요. 조사식창은아주편리한기능을제공합니다. 예외를재현하기위해서조사식창을사용해봅시다. 예외를발생시키기 디버거를사용해서변명관리프로그램에서마주쳤던문제를좀더자세히살펴 보죠. 지난몇장에서도디버거를사용했습니다. 이번에는변명관리프로그램의 문제를자세히들여다보기위해서디버깅을단계별로진행해봅시다. 디버깅해봅시다! 위해서프로시저단위실행 (F10) 을두번선택합니다. 그러고나서 filenames.length 를선택한후오른쪽클릭을한후, ( 조사식추가 ) 를선택해서조사식을추가합니다. 그리고 random.next(filenames.length) 와 filenames[random.next(filenames.length)] 도마찬가지로조사식창에추가합니다. 1 Random 버튼의이벤트핸들러에중단점을추가합니다. 어디에서문제가발생하는지는알고있습니다. 바로빈폴더를선택한후에 Random Excuse 버튼을클릭할때예외가발생합니다. 따라서이버튼의코드-비하인드창을열고메서드첫번째줄을클릭한후, 디버그 (Debug) 메뉴 > 중단점설정 / 해체 (Toggle Breakpoint) 를선택하거나 F9키를누른후, 프로그램을실행하세요. 빈폴더를선택하고 Random 버튼을클릭해서프로그램이중단점에서멈추도록합니다. 조사식창은유용한기능이많습니다. 조사식창에표시되는변수와필드의값을바꿀수있습니다. 심지어메서드를실행하거나새로운객체를생성할수있습니다. 값옆에표시되는이아이콘은메서드를다시실행해서, 값을다시계산할수있습니다. 4 Exception 객체를조사식창에추가합니다. 디버깅은프로그램에서범죄현장조사를수행하는것과비슷합니다. 문제의원인이무엇인지를알아야비로소그원인 에대해서조금더세부적으로살펴볼수있습니다. 그래서단서를따라범인을추적하는 CSI 키트, 즉디버거가필요하 죠. 그리도또하나의다른팁이있습니다. 조사식창에서 $exception 을추가해보세요. Exception 객체가발생한내용을 2 Excuse 생성자에서한단계씩코드를실행합니다. 문제를재현해야하지만, 문제를해결하는코드를이미추가했습니다. 다음과정을따라해서문제를재현해봅시다. currentexcuse = new Excuse(random, selectedfolder); 오른쪽클릭한후, 다음문설정 (Ctrl-Shift-F10) 을선택한다음, 한단계씩코드실행 (F11) 을선택해서생성자로들어갑니다. 예외를피하기위해서추가한해결방안의코드를건너뛸수있도록디버거를사용했습니다. 그러면 Excuse 생성자가다시예외를던질수있습니다. 여러분에게보여줍니다. 예외가발생했을때, 예외가발생한곳으로돌아가서디버거로예외를재현해보세요. Exception 객체를이용해서문제의원인을파악할수있습니다. 624 Appendix ii 지금여기예요 4 625

쉬어가는시간 바보같은질문이란없습니다 어휴, 코드에여전히문제가있네요 브라이언은가벼운마음으로변명관리프로그램을사용하다가변명관리프로그램으로만들어지지않은 XML 파일로가득차있는폴더를잘못선택했습니다. 이렇게했을경우어떤일이일어나는지살펴봅시다. 제발 또문제가있다고요? Q : 중단점을어디에설정해야되죠? A: 정말좋은질문입니다. 사실이질문에대한정답은없습니다. 코드에서예외가발생할때예외를발생시킨문장부터살펴보는것이좋습니다. 하지만보통은실제로문제가발생한장소는예외발생훨씬이전이며, 발생한예외는단지그부산물일뿐입니다. 예를들어, 0 으로나누는에러를발생시킨문장은실제로사용된적이없는 10 개의문장이전에생성된값으로나눗셈을하게됩니다. 결국상황에따라다르므로중단점을어디에삽입해야하는지에대한정답은없지만, 여러분의코드가어떤식으로수행되는지알고있다면적당한지점이어디인지알수있을것입니다. Q : 조사식창에서메서드를실행할수있나요? A: 예, 프로그램에서이상없이동작한문장은조사식창에서도문제없이동작합니다. 프로그램을실행하고, 중단한후다음내용을조사식창에추가해보세요. Q : 잠깐만요! 조사식창에서뭔가실행할때, 실행하고있는프로그램의실행방식을바꿀수없다는건가요? A: 예! 영구적인영향을주지않습니다. 하지만, 결과에는영향을끼칠수있죠. 디버거내부의필드에마우스를올리면, 프로그램의동작을변경할수있습니다. 속성의 get 접근자에실행할메서드가있다면, 그속성에마우스를올릴경우이메서드가실행됩니다. 그리고메서드가어떤값을설정한다면, 프로그램을다시실행할경우그값이남아있습니다. 이러한경우, 디버거내부에서예측할수없는결과를일으킬수있습니다. 예측할수없고무작위적인결과를하이젠버그 (heisenbug) 라부릅니다 ( 상자속에갇힌고양이와하이젠베르크란물리학자이름에서따온거예요 ). 1 2 브라이언이고민하고있는문제를재현할수있습니다. 변명파일로직렬화되지않은아무파일을선택해서.excuse 확장자를붙여주세요. IDE에서변명관리프로그램을실행한후, 변명을열어보세요. 예외가발생합니다. 메시지를보고, 중단 (Break) 버튼을눌러무엇이문제인지살펴봅시다. System.Threading.Thread.Sleep(2000) 이메서드는프로그램을 2 초동안멈추게합니다. 어떤일이일어날까요? 이메서드가실행되는동안 IDE 가 2 초동안잠시멈추고, 기다리는커서를표시합니다. Sleep() 은반환값이없으므로, 조사식창은 식을계산했지만값이없습니다 라는메시지를표시하고, 반환값이없음을알려줄것입니다. 뿐만아니라코드를작성하는데도움을주는작은팝업창이나타나는데, 현재메모리에있는해당객체에사용가능한속성과메서드의목록을보여주므로아주유용합니다. IDE 에서프로그램을실행할때처리되지않은예외는마치중단점을설정한것처럼프로그램을중지시킵니다. 3 지역창을열어서조사식창에 $exception 을입력한후, + 버튼을눌러확장합니다. 무엇이잘못되었는지알아내기위해서, 이들의멤버를자세히살펴봅시다. 왜프로그램은예외를발생시킬까요? 변명 (.excuse) 형식이아닌 XML 파일을선택했을때, 프로그램이멈추는게맞는걸까요? 이문제를해결할수있는방법이있나요? 626 Appendix ii 지금여기예요 4 627

예측할수없는사용자의입력 try 와 catch 로예외를처리합시다 강력한, 튼튼한 (robust), 형용사 정반대의상황을극복하거나버틸수 있는능력. 타코마다리붕괴사고후에도시건설 팀은이를대체할좀더튼튼한다리설 계를검토하고있다. 파일에직렬화된객체가정확하지않을경우 BinaryFormatter 클래스또한 SerializationException 을발생시킵니다. DataContractSerializer 보다더신경쓸게많죠. 잠깐만요. 물론잘못된파일을읽으려고하니프로그램이충돌했겠죠. 항상사용자들은모든것을망쳐놓는다니까요. 이런상황에서무엇을해야하죠? 실제로여러분이할수있는것이있습니다. 사용자들이항상문제가되는것은사실입니다. 세상살이가다그런거죠. 그렇다고여러분이할수있는것이아무것도없지는않습니다. 잘못된데 이터를입력하거나다른예상치못한상황들을처리하는프로그램을부르 는이름이있는데, 바로튼튼한 (robust) 프로그램이라고합니다. C# 은여 러분의프로그램을좀더튼튼하게만들수있게도움을주는실제로강력 한예외처리도구를제공하고있습니다. 비록사용자들이무엇을하는지 제어할수없지만, 사용자들이방해할때프로그램이충돌하지않게할수 는있습니다. 잘못된직렬화파일이있다면, 시리얼라이저는예외를발생시킵니다. 변경관리프로그램에서 SerializationException 을발생시 키는것은쉽습니다. 그냥직렬화된 Excuse 객체를가지고있지않은파일을 열면되죠. 파일로부터객체를역직렬화하려고시도할때 DataContractSerializer 는당연히읽으려고하는클래스와일치하는직렬화된객체를가지고있는 파일을읽고있다고생각합니다. 만약파일에다른내용이있다면 ReadObject() 메서드는 SerializationException 을발생시킵니다. C# 에서는 이코드를한번실행해보고 (try) 예외가발생하면, 이예외를처리하는 (catch), 다른코드를수행한 다 라고말할수있습니다. 여기서시도하는부분이바로 try 블록이며, 예외를처리하는부분이바로 catch 블 록입니다. catch 블록에서는프로그램을중단하는대신, 친절한에러메시지를보여줄수있습니다. public async Task ReadExcuseAsync() try this.excusepath = excusepath; BinaryFormatter formatter = new BinaryFormatter(); Excuse tempexcuse; 여기가 try 블록입니다. try 를사용해서예외처리를시작합니다. using (Stream input = File.OpenRead(excusePath)) tempexcuse = (Excuse)formatter.Deserialize(input); Description = tempexcuse.description; Results = tempexcuse.results; LastUsed = tempexcuse.lastused; catch (SerializationException) MessageBox.Show("Unable to read " + excusepath); Excuse.cs 파일위쪽에아래의 using 문이필요합니다. using System.Runtime.Serialization; using System.Windows.Forms; LastUsed = DateTime.Now; 위의코드는간단한예외처리의예입니다. 프로그램을중지하고예외메시지를보여준 후나머지부분을계속수행하는군요. 이문장이없다면무슨일이일어날까요? 이것을왜 catch 블록에포함시키는지이유를알아채셨나요? try 블록내부에예외를발생시킬만한코드를넣습니다. 만약예외가발생하지않는다면다른때와마찬가지로정상적으로실행되며, catch 블록에있는문장들은무시됩니다. 하지만예외를발생시키면, try 블록에있는나머지문장들은실행되지않습니다. catch 블록은바로다음에예외를처리하는블록이나온다는것을의미합니다. 브레인파워 메서드전체가이 try 블록으로되어있어서, 코드를쉽게알아볼수있을거예요. 예외가발생할때프로그램은즉시 catch 블록을수행합니다. 예외를발생시켜여러분의코드가자동으로 catch 블록으로넘어간다면예외가발생하기전에작업했던객체나데이터에는무슨일이일어날까요? 628 Appendix ii 지금여기예요 4 629

위험한비즈니스 위험한메서드호출 바보같은질문이란없습니다 사용자는예측불가능합니다. 그들은모든종류의왜곡된데이터를프로그 램에입력해서여러분이절대로예상하지못한방식으로프로그램을사용 합니다. 여기까지는그나마다행입니다. 왜냐하면잘짜인예외처리를사 용하면예상치못한입력사항을처리할수있기때문입니다. 1 사용자가예상치못한내용을 입력했다고칩시다. 2 이메서드는런타임시에동작 하지않을것같은뭔가위험한 것을합니다. 런타임 은 프로그램이실행되는동안 을나타냅니다. 일부사람들은예외를 런타임오류 라고부르곤하죠. 3 여러분이호출하는메서드가 위험하다는사실을알아야합니다. 예외를발생시키지않는덜위험한방법을찾을수만있다면, 이보다더좋은방법은없습니다! 하지만어느정도위험은피할수없으므로이렇게처리해야합니다. 4 위험한상황으로인한오작동을 처리할수있는코드를작성합니다. 사용자 작성한클래스 사용자 public class Data public void Process(Input i) if (i.isbad()) explode(); 사용자입력 å ß ıïô œ ˆ øƒ Ω ÔÒÎ åß ÒÅ åƒ ß å ß ƒå ß ƒ å ß ƒ ƒ å ç ƒ ˆ å ƒ 입력내용 메서드로흘러가는데이터 public void Process(Input i) if (i.isbad()) explode(); 이부분을클릭하면어떻게되는지궁금한데 와, 이프로그램은정말쓸만한데! 프로그램이조금더강력해졌습니다. public class Data public void Process(Input i) if (i.isbad()) explode(); 작성한클래스 잘못된데이터가입력되면 Process() 메서드가폭발하는군. public class Data public void Process(Input i) if (i.isbad()) explode(); 작성한클래스 Q : 그렇다면 try 와 catch 는언제사용하나요? A: 위험성이있거나예외가발생할소지가있는코드를작성할때사용하세요. 문제는과연어떤코드가위험하고, 어떤코드가더안전한가하는것입니다. 여러분은이미사용자가입력한내용을기반으로처리하는코드가위험해진다는것을봤습니다. 사용자들은잘못된파일이나숫자대신문자를, 날짜대신이름을입력하거나, 여러분이상상할수있는모든곳을클릭해댑니다. 좋은프로그램일수록모든입력사항에대해조용히, 예측가능한방법으로처리합니다. 사용자에게처리한결과를보여주지는않겠지만, 문제를발견하고해결책을제시할수는있어야합니다. Q : 어떻게하면미리알지못하는문제에관한해결책을프로그램이제시할수있죠? A: 그런일을바로 catch 블록이합니다. catch 블록은 try 블록에서예외가발생할때만수행되며, 여러분은사용자에게뭔가잘못된것이있고이를올바로고쳐야한다는사실을확인시켜줄수있는기회를가지게된거죠. 만약변명관리프로그램에잘못된데이터가입력될때충돌하기만한다면이프로그램은별로쓸만하지않겠죠. 또한입력사항을읽어폼에쓰레기데이터를보여준다면이또한유용하지않고오히려더나쁜프로그램이라고얘기할것입니다. 하지만파일을읽을수없다는사실을사용자에게알려 주는에러메시지를보여준다면사용자는무엇이잘못되었고그문제를고칠수있는지에대한단서를얻게될것입니다. Q : 디버거는예외를해결하는데만사용되나요? A: 아닙니다. 디버거는실제로작성한코드를검사하는데사용할수있는유용한도구의하나일뿐입니다. 코드를단계별로실행해서특정변수나필드의값을검사하는데유용한도구죠. 여러분이복잡한메서드를작성할때그것이제대로작동되고있는지확인할때사용할수있습니다. 하지만 디버거 란이름에서알수있듯이, 이것의주용도는버그를추적해서제거하는것입니다. 가끔씩그러한버그들이예외가되죠. 앞으로여러분은꽤오랜시간동안예상하지못한결과를산출하는코드처럼다양한종류의문제점을찾아내는데디버거를사용할것같군요. Q : 조사식창에대해아직도잘모르겠어요. 이것은무엇을위한것이죠? A: 프로그램을디버깅할때여러분은임의의필드나변수들값이어떻게변하고있는지알고싶어할것입니다. 바로이때조사식창이등장합니다. 일부변수들을조사식에추가하면, 조사식창은코드가단계별로실행될때마다해당변수의값을갱신합니다. 조사식은모든문장에서정확히무슨일이일어나고있는지감시할수있게하며, 이는문제점을추적할때정말유용하게사용할수있습니다. 또한조사식창에여러분이원하는문장을입력할수있죠. 심지어는메서드를호출해주고, 그내용을계산해서결과를보여줍니다. 만약문장내에서어떤필드나변수를갱신한다면조사식창에서도적용되어프로그램이실행되는동안값이변경됩니다. 이런점은예외나다른버그들을재현하는데있어아주유용한도구로사용할수있죠. 조사식창에서만든변경사항은메모리에있는데이터에영향을줄뿐이며, 프로그램이실행되는동안에만지속됩니다. 프로그램을재시작하면변경했던모든값들은원래상태로남아있게될거예요. catch 블록은 try 블록의코드에서예외가발생할때만수행됩니다. 이를이용해서여러분은사용자에게문제점을수정하기위한정보를제공할수있습니다. 만약의사태에대비해야하니까요. public class Data public void Process(Input i) try if (i.isbad()) explode(); catch HandleIt(); 예외처리를하는클래스사용자 630 Appendix ii 지금여기예요 4 631

리듬에몸을맡기세요 디버거를사용해서 try/catch 흐름을따라가보죠 예외를처리하는중요한부분이바로 try 블록에있는문장에서예외가발생 할때인데, 이때블록에있는나머지코드들은중단됩니다. 일명단락 (shortcircuited) 된다고표현하죠. 그리고프로그램은즉각 catch 블록의첫번째라인으로이동합니다. 하지만항상이렇게동작하는것은아닙니다. 디버깅해봅시다! 3 프로시저단위실행을눌러다음문장을수행합니다. 디버거가 Deserialize() 문을 실행하자마자예외가발생합니다. 그리고메서드의예외가발생한곳부터 try 블록이끝나는 지점까지흐름이일단락되고 (short-circuits), 바로 catch 블록으로이동합니다. 1 몇페이지전에작성한변명관리프로그램의 OpenFile() 메서드로갑니다. 그러고 나서 try 블록에서시작되는 에중단점을설정합니다. 2 디버깅을시작해서유효하지않은변명파일을열어봅시다 ( 확장자가.xml 이어야합니다 ). 디버거가중단점에서멈출때, 프로시저단위실행 ( 혹은 F10키 ) 을다섯번클릭합니다. 그러면 Excuse 객체를역직렬화하는 ReadObject() 메서드로가게됩니다. 디버거는아래와같이보일겁니다. 디버거는노란색블록으로 " 다음문장 " 을표시하고, 블록의나머지부분은회색으로표시하는데, 이는곧실행될부분을의미합니다. try 블록이시작하는중괄호에중단점을설정하세요. 4 계속 (Continue) 버튼 (F5) 을클릭해서프로그램을다시실행하면노란색의 다음문장 이 표시된부분에서시작합니다. 이경우엔 catch 블록이되겠죠. 아무일이일어나지않은 것처럼대화상자를표시합니다. 이렇게예외처리를하는군요. 스트림으로부터 Excuse 객체를읽기위해서, 노란색막대의 다음문장 까지프로시저단위실행을합니다. 여기유용한팁이있군요. C# 프로그래머면접시생성자내부에서발생한예외를어떻게처리할것인지묻는경우가많죠. 예외가발생할것같은코드는생성자밖에서작성하세요. void형이아니더라도지금까지생성자는값을반환하지않는다는것을봤습니다. 생성자는실제로아무것도반환하지않기때문이죠. 생성자의목적은객체를초기화하는것이며, 이로인해생성자내부에서예외를처리할경우문제가발생될수있습니다. 생성자내부에서예외가발생할때클래스를초기화하려는문장에서객체의인스턴스를생성하지못합니다. 632 Appendix ii 지금여기예요 4 633

뒷정리를잘합시다 언제나실행되는코드를작성하려면 finally 문을사용하세요 알림 : 부록 12 장을마치고, 책 13 장으로바로가면됩니다. 13 장은윈도우 8 스토어앱에의존하지않습니다. 프로그램이예외를발생시킬때몇가지일이일어나는데, 발생된예외가처리되지않는다면프로그램은 실행을중지하고충돌하게되죠. 예외가처리된다면여러분의코드는 catch 블록으로이동합니다. 하지 만 try 블록에있는나머지코드들은어떻게될까요? 여러분이스트림을닫거나중요한리소스들을해제 해야한다면어떨까요? 예외가발생하거나프로그램의상태가엉망이되더라도이러한코드들은수행될 필요가있습니다. 이런경우 try 와 catch 블록다음에오는 finally 블록이유용합니다. 예외가발생하건 말건 finally 블록은항상수행됩니다. NewExcuse() 메서드가호출되면, 이는 Excuse 객체를다시설정합니다. 만약 PropertyChanged 이벤트가실행되지않는다면, 페이지는 CurrentExcuse 속성을읽지않습니다. finally 블록은예외가발생하든하지않든지상관없이 PropertyChanged 이벤트가실행되도록해줍니다. private void OpenFile(string excusepath) try this.excusepath = excusepath; BinaryFormatter formatter = new BinaryFormatter(); Excuse tempexcuse; using (Stream input = File.OpenRead(excusePath)) tempexcuse = (Excuse)formatter.Deserialize(input); Description = tempexcuse.description; Results = tempexcuse.results; LastUsed = tempexcuse.lastused; catch (SerializationException) finally MessageBox.Show("Unable to read " + excusepath); LastUsed = DateTime.Now; // 여기에있는모든코드는뭐든지실행됩니다. Chapter 14 LINQ 는모든종류의 C# 프로그램에서동작합니다. 이책 14 장의주요부분을읽을때, 다른장들과는다르게구성되었다는것을볼수있습 니다. 복잡한 LINQ 쿼리를설명하기위해서, 작은콘솔앱을통해서예제를살펴봅니다. 이번장의전반에걸쳐서모든쿼리를단일사용자인터페이스로결합하는윈도우스토 어앱예제를볼수있습니다. 이부록의다음몇페이지에걸쳐서이와같은쿼리를실행 하는 WPF 응용프로그램을만드는법을배워봅니다. 14 장의부록을아래와같이활용하 면좋습니다. 책 701쪽까지읽어주세요. 14 장에서는많은 LINQ 쿼리가있습니다. 책에는이러한쿼리들을모두윈도어스토어앱에서사용합니다. 대신이부록에서는 WPF 응용프로그램을만들겁니다. 709 쪽까지윈도우스토어앱을만드는과정이있지만, 읽어주세요. 특히익 명유형 (anonymous type) 이나오는부분을넘어가면안됩니다. 이것은 Comic, ComicQuery, ComicQueryManager 클래스가어떻게동작하는지이해하는데 도움이됩니다. 710, 711 쪽은 LINQ 쿼리의기능에대해설명합니다. 712, 713 쪽은윈도우스토어 앱에관한것이기때문에넘어가도됩니다. 714-724 쪽을읽어주세요. 723쪽에있는연습문제는하지마세요. 항상 SerializationException 같은특정한예외를잡아내세요. 일반적으로 catch문다음에특정종류의예외를명시해서이예외를잡아냅니다. C# 에서도 catch (Exception) 형태로코드를작성하는데예외유형을생략하고 catch만표기해도됩니다. 이런경우에는, 발생한예외의유형에상관없이모든예외를잡아내죠. 하지만이렇게사용하는것은좋은습관이아닙니다. 가능한한항상특정예외타입을명시하도록하세요 나머지부분은윈도우스토어앱에관련된겁니다. 넘어가주세요. 이부분을부 록의 724-727 쪽에서대체합니다. 634 Appendix ii 지금여기예요 4 635

WPF 만화쿼리응용프로그램을만들어봅시다 책 14장을읽으면서, LINQ 쿼리를실행하는윈도우스토어앱을만드는과정을살펴봤습니다. 관심사분리의원칙을따랐기때문에, 데이터를관리하고쿼리를실행하는코드와사용자인터페이스를생성하는코드를따로분리했습니다. 이것은비주얼스튜디오의분할앱템플릿으로또다른앱을구축할때, 동일한데이터및쿼리를관리하는클래스들을쉽게재사용할수있게해줍니다. 이와같은관심사분리를활용해서, 같은데이터와쿼리클래스를이용하여 WPF 응용프로그램을만들어봅시다. 직접해봅시다! 2 ComicQueryManager.cs 에서두가지를수정해봅시다. ComicQueryManager.cs 에서두가지의작은변화가있습니다. WPF 응용프로그램은 Windows.UI 네임스페이스를사용하지 않습니다. 이네임스페이스는윈도우스토어앱의.NET 프레임워크에만해당하기때문이죠. 그래서 using 문의 "Windows.UI" 를 "System.Windows" 로바꿔줍니다. using System.Collections.ObjectModel; using System.Windows.Media.Imaging; WPF 응용프로그램에서이미지를불러오는방식은윈도우스토어앱과조금다릅니다. ComicQueryManager 의 CreateImage- FromAssets() 메서드를아래와같이바꿔줍니다. 1 새로운 WPF 응용프로그램을생성하고, 만화책앱에서기존클래스들과이미지들을추가합니다. 이프로젝트를시작하기전에, 14 장의 JimmysComics 앱의코드를내려받아야합니다. 여기 (http://www.hanbit.co.kr/exam/2165) 에소스코드의링크가있습니다. 소스코드를내려받았다면, JimmysComics 라는 WPF 응용프로그램을생성합니다. 그러고나서솔루션탐색기 의프로젝트이름에마우스오른쪽을클릭한후, 기존항목추가를선택해서책에서만든윈도우스토어앱의항 목을추가합니다 ( 책의웹사이트에서내려받을수있습니다 ). Purchase.cs ComicQuery.cs PriceRange.cs. Comic.cs ComicQueryManager.cs Assets 폴더의 bluegray_250x250.jpg, bluegray_250x250.jpg, captain_ amazing_250x250.jpg, captain_amazing_zoom_250x250.jpg 파일들을 WPF 응용프로그램의루트레벨에추가해주세요. XAML 과 C# 파일에나란히놓아주세요. 위사항이반영된솔루션탐색기창입니다. 프로젝트의이름을다르게했다면, 여러분이추가한 C# 파일의네임스페이스를여러분의프로젝트이름에맞게변경해주세요. 솔루션탐색기의각이미지파일을클릭합니다. 그리고속성 창의빌드작업 (Build Action) 에서내용 (Content) 을, 출력 디렉터리로복사 (Copy to Output Directory) 에서항상복사 (Copy always) 를선택합니다. 이작업을각.jpg 파일을선 택해서해주세요. 3 private static BitmapImage CreateImageFromAssets(string imagefilename) try Uri uri = new Uri(imageFilename, UriKind.RelativeOrAbsolute); return new BitmapImage(uri); catch (System.IO.IOException) return new BitmapImage(); 메인윈도우의코드 - 비하인드를추가합니다. MainWindow.xaml.cs 에대한코드 - 비하인드입니다. public partial class MainWindow : Window ComicQueryManager comicquerymanager; public MainWindow() InitializeComponent(); comicquerymanager = FindResource("comicQueryManager") as ComicQueryManager; comicquerymanager.updatequeryresults(comicquerymanager.availablequeries[0]); private void ListView_SelectionChanged(object sender, SelectionChangedEventArgs e) if (e.addeditems.count >= 1 && e.addeditems[0] is ComicQuery) comicquerymanager.currentqueryresults.clear(); comicquerymanager.updatequeryresults(e.addeditems[0] as ComicQuery); 프로젝트의최상위레벨폴더에.jpg 파일을복사했습니다. 새 CreateImageFromAssets() 메서드는이파일들을불러옵니다. ListView 컨트롤은항목이선택 / 해제될때마다 SelectionChanged 이벤트를발생시킵니다. 선택된항목은 e.addeditems 컬렉션에서찾을수있습니다. 724 Appendix ii 지금여기예요 4 725

4 ListView 의 SelectionMode 는한번에하나의쿼리만선택할수있는 Single 로설정됩니다. 메인윈도우의 XAML 을추가합니다. 메인윈도우의 XAML 코드입니다. 만약다른프로젝트의이름을사용했다면 JimmysComics 를여러분이만든프로젝트의네임 스페이스로바꿔주세요. <Window x:class="jimmyscomics.mainwindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:jimmyscomics" Title="Jimmy's Comics" Height="350" Width="525"> <Window.Resources> <local:comicquerymanager x:key="comicquerymanager"/> </Window.Resources> <Grid DataContext="StaticResource ResourceKey=comicQueryManager"> <Grid.ColumnDefinitions> <ColumnDefinition Width="2*"/> <ColumnDefinition Width="3*"/> 왼쪽에있는 ListView 는각쿼리에대한정보를표시하는항목템플릿 (ItemTemplate) 이있습니다. </Grid.ColumnDefinitions> <ListView SelectionMode="Single" ItemsSource="Binding AvailableQueries" SelectionChanged="ListView_SelectionChanged"> <ListView.ItemTemplate> <DataTemplate> <Grid Height="55" Margin="6"> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <Border Width="55" Height="55"> <Image Source="Binding Image" Stretch="UniformToFill"/> </Border> <StackPanel Grid.Column="1" VerticalAlignment="Top" Margin="10,0,0,0"> <TextBlock Text="Binding Title" TextWrapping="NoWrap"/> <TextBlock Text="Binding Subtitle" TextWrapping="NoWrap"/> <TextBlock Text="Binding Description" TextWrapping="NoWrap"/> </Grid> </DataTemplate> </ListView.ItemTemplate> </ListView> <ListView Grid.Column="1" SelectionMode="Single" ItemsSource="Binding CurrentQueryResults"> <ListView.ItemTemplate> <DataTemplate> <StackPanel Orientation="Horizontal"> <Image Source="Binding Image" Margin="0,0,20,0" Stretch="UniformToFill" Width="25" Height="25" VerticalAlignment="Top" HorizontalAlignment="Right"/> <StackPanel> <TextBlock Text="Binding Title" /> </DataTemplate> </ListView.ItemTemplate> </ListView> </Grid> </Window> 오른쪽에있는 ListView 는쿼리결과의개별항목을보여주는항목템플릿 (ItemTemplate) 이있습니다. 앱을실행하면쿼리는왼쪽에표시됩니다. 그리고왼쪽에서선택한항목의쿼리결과는오른쪽에표시됩니다. 만화책정보를반환하는쿼리는추가적인정보 ( 가격, 개요, 표지이미지 ) 가있습니다. 여러분은각만화책이추가적인정보를표시하는쿼리를작성할수있나요? 책 733, 734 쪽의 XAML 코드를보면도움이됩니다. 726 Appendix ii 지금여기예요 4 727

일부러페이지를비웠습니다 Chapter 15 부록에서두페이지모드를유지하기위해서일부러페이지를비웠습니다. 연습문제와연습문제정답이서로마주하는면에나오면안되기때문이죠. 그리고두페이지모드의짝수쪽에는오른쪽위에, 홀수쪽에는왼쪽위에작은캡션이있습니다. 이번장의책에서윈도우스토어앱에대한몇몇페이지가있습니다. 아무튼이페이지들을읽어야해요! 이벤트는모든앱에유용합니다. 그리고 XAML 을이해하는것도중요합니다. 이책의전반에걸쳐서사용되는이벤트는단순하고직관적일수있습니다. 여러 분의예상과는달리더깊은이벤트의세계가있습니다. 이번장을통해서이벤트 를자세히이해해봅시다. 이번장에서참고할부분입니다 책 755쪽까지읽어주세요. 이부록은책의연습문제 756, 757쪽과정답 758, 759쪽을대체합니다. 책 760-763 쪽을읽어주세요. 책 764-767 쪽은윈도우스토어앱에대한내용이지만, 이페이지를읽 는것을추천합니다. 이것은윈도우스토어앱뿐만아니라, 윈도우 8 의 기초적인특징을파악할수있습니다. 부록은책 768-773 쪽을대체합니다. 책나머지부분을읽어주세요. 그리고 784 쪽의윗부분과 786, 787 쪽은 넘어가주세요. 728 Appendix ii 지금여기예요 4 729

모두합치기 2 지금까지배운내용을실습할때가되었습니다. Ball 과 Pitcher 클래스를완성하고, Fan 클래스를추가해서 1 2 야구게임시뮬레이터의기본버전이제대로동작하는지확인하세요. Pitcher 클래스를완성합니다. 아래에 Pitcher 클래스에대한코드가나와있습니다. CatchBall() 과 CoverFirstBase() 메서드를추가하세요. 이두메서드는포수가공을잡거나 1 루로공을송구한경우내용을출력해야합니다. 그리고 PitcherSays 라 불리는 public ObservableCollection<string> 컬렉션에해당문자열을추가합니다. class Pitcher public Pitcher(Ball ball) ball.ballinplay += new EventHandler(ball_BallInPlay); void ball_ballinplay(object sender, EventArgs e) if (e is BallEventArgs) BallEventArgs balleventargs = e as BallEventArgs; if ((balleventargs.distance < 95) && (balleventargs.trajectory < 60)) CatchBall(); else CoverFirstBase(); Fan 클래스를작성합니다. Fan 클래스를생성합니다. 이클래스또한자신의생성자에서 BallInPlay 이벤트를구독해야합니다. 이객체의이벤트핸들러는거리가 400 피트보다크고궤도가 30 도보다큰지 ( 홈런일경우 ) 검사해서이 조건에맞으면글러브로공을잡으려고시도합니다. 조건에맞지않을경우팬은울상을짓는군요. 팬의 비명과고함을 ObservableCollection<string> FanSays 에추가하기만하면됩니다. 정확히어떤내용을출력해야하는지는다음페이지에있는결과를참조하세요. PitcherSays 의 ObservableCollection 에문자열을추가하려면두개의메서드가필요합니다. Pitcher 객체? Fan 객체 3 4 아주간단한시뮬레이터를만듭니다. 아직시뮬레이터를만들지않았다면, 새로운 WPF 응용프로그램을생성합니다. 그리고 BaseballSimulator 클래스를추 가합니다. 그러고나서윈도우에정적리소스를추가합니다. using System.Collections.ObjectModel; class BaseballSimulator private Ball ball = new Ball(); private Pitcher pitcher; private Fan fan; 공 1: 각도 : 거리 : public ObservableCollection<string> FanSays get return fan.fansays; public ObservableCollection<string> PitcherSays get return pitcher.pitchersays; public int Trajectory get; set; public int Distance get; set; public BaseballSimulator() pitcher = new Pitcher(ball); fan = new Fan(ball); public void PlayBall() BallEventArgs balleventargs = new BallEventArgs(Trajectory, Distance); ball.onballinplay(balleventargs); 메인윈도우를만듭니다. 오른쪽에있는화면을보고똑같은 XAML 을만들수있나요? 두개의 TextBox 컨트 롤은정적리소스인 BaseballSimulator 의 Trajectory( 각도 ) 와 Distance( 거리 ) 속성 에바인딩되어있습니다. 그리고 Pitcher 와 Fan Says 는두 ObservableCollections 에 바인딩된 ListView 컨트롤입니다. 세개의공이성공적으로날아갔다고가정 하고여러분이만든시뮬레이터가아래와 같은결과를출력하는지확인하세요. 아래 와같은결과를얻으려면어떤값을사용해 야하는지도하단에적어보세요. 공 2: 각도 : 거리 : 이버튼의이벤트클릭핸들러를꼭추가해야됩니다. 공 3: 각도 : 거리 : 756 Appendix ii 지금여기예요 4 757

연습문제정답 이벤트핸들러들은오직전달된데이터를읽기만하므로읽기전용자동속성들은이벤트인수에서실제로 잘동작합니다. Ball 과 BallEventArgs 클래스와새 Fan 클래스에추가할내용입니다. class Ball public event EventHandler BallInPlay; public void OnBallInPlay(BallEventArgs e) EventHandler ballinplay = BallInPlay; if (ballinplay!= null) ballinplay(this, e); class BallEventArgs : EventArgs public int Trajectory get; private set; public int Distance get; private set; public BallEventArgs(int trajectory, int distance) this.trajectory = trajectory; this.distance = distance; Fan 객체의 BallInPlay 이벤트핸들러는공의높이와날아간거리를검사하는군요. using System.Collections.ObjectModel; class Fan public ObservableCollection<string> FanSays = new ObservableCollection<string>(); private int pitchnumber = 0; public Fan(Ball ball) ball.ballinplay += new EventHandler(ball_BallInPlay); void ball_ballinplay(object sender, EventArgs e) pitchnumber++; if (e is BallEventArgs) BallEventArgs balleventargs = e as BallEventArgs; if (balleventargs.distance > 400 && balleventargs.trajectory > 30) FanSays.Add("Pitch #" + pitchnumber + ": Home run! I'm going for the ball!"); else FanSays.Add("Pitch #" + pitchnumber + ": Woo-hoo! Yeah!"); 윈도우에대한코드 - 비하인드입니다. public partial class MainWindow : Window BaseballSimulator baseballsimulator; public MainWindow() InitializeComponent(); OnBallInPlay() 메서드가 BallInPlay 이벤트만발생시키고있는데, null 이아닌지검사해야합니다. 만약그렇게하지않으면예외가발생합니다. baseballsimulator = FindResource("baseballSimulator") as BaseballSimulator; private void Button_Click(object sender, RoutedEventArgs e) baseballsimulator.playball(); Fan 객체의생성자는자신의이벤트핸들러를 BallInPlay 이벤트와연결하고있습니다. 여기에윈도우에대한 XAML 코드가있군요. 여기에 <local:baseballsimulator x:name="baseballsimulator"/> 가필요합니다. <Window.Resources> <local:baseballsimulator x:key="baseballsimulator"/> </Window.Resources> <Grid Margin="5" DataContext="StaticResource ResourceKey=baseballSimulator"> <Grid.ColumnDefinitions> <ColumnDefinition Width="200" /> <ColumnDefinition/> </Grid.ColumnDefinitions> <StackPanel Margin="0,0,10,0"> <TextBlock Text="Trajectory" Margin="0,0,0,5"/> <TextBox Text="Binding Trajectory, Mode=TwoWay" Margin="0,0,0,5"/> <TextBlock Text="Distance" Margin="0,0,0,5"/> <TextBox Text="Binding Distance, Mode=TwoWay" Margin="0,0,0,5"/> <Button Content="Play ball!" Click="Button_Click"/> <StackPanel Grid.Column="1"> <TextBlock Text="Pitcher says" Margin="0,0,0,5"/> <ListView ItemsSource="Binding PitcherSays" Height="125"/> <TextBlock Text="Fan says" Margin="0,0,0,5"/> <ListView ItemsSource="Binding FanSays" Height="125"/> </Grid> 공 1: 각도 : 거리 : 75 105 공 2: 각도 : 거리 : 48 80 공 3: 각도 : 거리 : <Window> 태그에 xmlns:local 속성을꼭추가하세요. 그리고여기에 Pitcher 클래스가있군요 ( 맨위에 using System.Collections.ObjectModel; 을선언해야합니다 ). class Pitcher public ObservableCollection<string> PitcherSays = new ObservableCollection<string>(); private int pitchnumber = 0; public Pitcher(Ball ball) ball.ballinplay += ball_ballinplay; void ball_ballinplay(object sender, EventArgs e) pitchnumber++; if (e is BallEventArgs) BallEventArgs balleventargs = e as BallEventArgs; if ((balleventargs.distance < 95) && (balleventargs.trajectory < 60)) CatchBall(); else CoverFirstBase(); private void CatchBall() PitcherSays.Add("Pitch #" + pitchnumber + ": I caught the ball"); private void CoverFirstBase() PitcherSays.Add("Pitch #" + pitchnumber + ": I covered first base"); 조금전에 Pitcher 의 BallInPlay 이벤트핸들러를봤었죠. 낮게오는공을검사합니다. 40 435 결과를얻기위해사용한값들이여기나와있군요. 여러분의값이여기있는것과약간다를수도있습니다. 758 Appendix ii 지금여기예요 4 759

버블버블 XAML 컨트롤은라우트된이벤트를사용합니다 책 766 쪽으로돌아가서여러분이 override 를입력했을때뜨는인텔리센스창의팝업을살펴봅니다. 그렇죠, 윈도우 스토어앱에대한겁니다. 하지만원리는 WPF 에서도똑같이적용됩니다. DoubleTapped 이벤트의두번째인수는 DoubleTappedRouteEventArgs 유형을갖고있고, GotFocus 이벤트는 RoutedEventArgs 유형을갖고있습니다. 두 이벤트인수유형의이름이다른것들과조금구별됩니다. 그이유는 DoubleTapped 와 GotFocus 는라우트된이벤트 (routed event) 기때문이죠. 이들은다른두가지를제외하고는보통의이벤트와같습니다. 컨트롤객체가라우트된이 벤트에응답할때, 먼저평소와같이이벤트핸들러메서드를호출합니다. 그리고다른작업을수행합니다. 만약이벤트 가처리되지않은경우, 라우트된이벤트를컨트롤의컨테이너에보내줍니다. 이컨테이너는이벤트를발생시키죠. 그리 고나서이벤트가처리되지않는다면, 라우트된이벤트를상위컨테이너에게또보내줍니다. 이벤트가처리되거나루트 (root) 혹은최상위컨테이너로도달할때까지, 이벤트는버블링 (bubbling) 을유지합니다. 아래에전형적인라우트된이 벤트핸들러메서드의시그네처가있습니다. private void EventHandler(object sender, RoutedEventArgs e) RoutedEventArgs 객체는 Handled 속성이있습니다. 이벤트핸들러는이벤트를처리하는것을알리기위해서이속성 을사용하죠. 이속성을 true 로설정하면이벤트가버블링되지않습니다. 라우트된이벤트와표준이벤트에서, sender 매개변수는항상이벤트핸들러를호출하는객체의참조를포함하고있 습니다. 그래서만약이벤트가컨트롤에서 Grid 와같은컨테이너로버블링된다면, Grid 는이벤트핸들러를호출할때 sender 는 Grid 컨트롤의참조가됩니다. 하지만여러분이어떤컨트롤에서본이벤트를발생시키는지찾아야한다면무 엇을해야할까요? RoutedEventArgs 객체는컨트롤이처음발생한이벤트의참조를포함하는 OriginalSource 라불리 는속성이있습니다. 만약 OriginalSource 와 sender 가같은객체를가리킨다면이벤트핸들러를호출한컨트롤은이벤 트가발생되고버블링이시작된같은컨트롤이라는것을의미합니다. 마우스와포인터에서어느한요소를 " 볼수 " 있는것은 IsHitTestVisible 이결정합니다 일반적으로페이지에있는모든요소는특정영역의범위를벗어나지않는한, 포인 터나마우스에의해서 선택 될수있습니다. 요소들이보여야하고 (Visibility 속성 으로바꿀수있죠 ), Background 혹은 Fill 속성이 null 이아니어야합니다 ( 투명도 인 Transparent 는괜찮습니다 ). 그리고활성화되어있고 (IsEnabled 속성 ), height 와 width 가 0 보다커야합니다. 만약이모든것들을만족한다면 IsHitTestVisible 속성은 True 를반환하고, 포인터나마우스이벤트에응답하게됩니다. 이속성은특히이벤트를마우스에 보이지않는 (invisible) 상태로만들때유용합니 다. 만약 IsHitTestVisible 이 False 로설정되어있다면, 어떤포인터의탭혹은마우스 클릭은컨트롤을통해서바로전달됩니다. 만약보이지않는컨트롤아래에다른컨트롤 이있다면, 그컨트롤이대신이벤트를얻게되는거죠. 라우트된이벤트개요에대해서자세히살펴보려면 http://msdn.microsoft.com/ko-kr/library/windows/apps/hh758286.aspx 다른컨트롤을포함하는컨트롤의구조를객체트리라부릅니다. 라우트된이벤트는자식으로부터부모까지 ( 상위루트에갈때까지 ) 트리를버블링합니다. 라우트된이벤트는객체트리를 라우트된이벤트를살펴봅시다 다음그림은라우트된이벤트를실험하는데사용하는 WPF 응용프로그램입니다. Border 를포함하는 StackPanel 이있습니다. 이것은 Grid 와그안에사각형의 Rrectangle 과원의 Ellipse 가있죠. 그림을한 번살펴보세요. Rectangle 이 Ellipse 위에있는걸어떻게볼수있죠? 만약두개의컨트롤이같은셀에 놓여있다면, 각두컨트롤의위에또다른컨트롤을놓을수있습니다. 그리고두컨트롤모두같은부모 를가지죠. Grid 의부모인 Border, Border 의부모인 StackPanel 을가집니다. Rectangle 혹은 Ellipse 에서라우트된이벤트는부모에서객체트리의루트까지버 블링됩니다. 체크상태를전환할수있는 CheckBox 컨트롤입니다. Content 속성은컨트롤에대한레이블을설정합니다. IsChecked 속성은 Nullable<bool> 입니다. 왜냐하면체크가된상태와안된상태그리고또다른불확정상태를가질수있기때문이죠. <Grid Margin="5"> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/> <ColumnDefinition/> </Grid.ColumnDefinitions> <StackPanel x:name="panel" MouseDown="StackPanel_MouseDown"> <Border BorderThickness="10" BorderBrush="Blue" Width="155" x:name="border" Margin="20" MouseDown="Border_MouseDown"> <Grid x:name="grid" MouseDown="Grid_MouseDown"> <Ellipse Fill="Red" Width="100" Height="100" MouseDown="Ellipse_MouseDown"/> < Rectangle Fill="Gray" Width="50" Height="50" MouseDown="Rectangle_MouseDown" x:name="grayrectangle"/> 버블링합니다. </Grid> </Border> <ListBox BorderThickness="1" Width="250" Height="140" x:name="output" Margin="0,0,20,0"/> <StackPanel Grid.Column="1"> <CheckBox Content="Border sets handled" x:name="bordersetshandled"/> <CheckBox Content="Grid sets handled" x:name="gridsetshandled" /> <CheckBox Content="Ellipse sets handled" x:name="ellipsesetshandled"/> <CheckBox Content="Rectangle sets handled" x:name="rectanglesetshandled"/> <Button Content="Update Rectangle IsHitTestVisible" Click="UpdateHitTestButton" Margin="0,20,20,0"/> <CheckBox IsChecked="True" Content="New IsHitTestVisible value" x:name="newhittestvisiblevalue" /> </Grid> IsChecked 의기본값은 False 입니다. 이 CheckBox 는 True 로설정되어있습니다. 기본적으로컨트롤의 IsHiTestVisible 속성은항상참이기때문이죠. 페이지를넘겨앱을완성해봅시다 768 Appendix ii 지금여기예요 4 769

객체트리올라가기 ListBox 에출력결과를표시하기위해서 ObservableCollection 이필요합니다. outputitems 필드를만들고, 윈도우생성자에 ListBox.ItemSource 속성을집어넣습니다. 그리고 ObservableCollection<T> 를위 해서 using System.Collections.ObjectModel; 문장을추가하는것도잊지마세요. public partial class MainWindow : Window ObservableCollection<string> outputitems = new ObservableCollection<string>(); LayoutAwarePage 객체 메인윈도우의객체그래프입니다. MainWindow 클래스는객체트리의루트입니다. 새로운 WPF 응용프로그램을생성할때 MainWindow.xaml 과 MainWindow.xaml.cs 파일은 Window 클래스를확장하는객체를생성합니다. public MainWindow() this.initializecomponent(); output.itemssource = outputitems; 여기에코드 - 비하인드가있습니다. 각컨트롤의 MouseDown 이벤트핸들러는출력결과를초기화합니다. 그러고나서출력결 과에문자열을추가합니다. 만약 handled 를체크상태로전환한다면, 이벤트를처리하기위해서 e.handled 를사용합니다. Grid 객체 XAML 에서여러분이추가한그리드입니다. 다 른컨트롤들을포함하고있죠. private void Ellipse_MouseDown(object sender, MouseButtonEventArgs e) if (sender == e.originalsource) outputitems.clear(); outputitems.add("the ellipse was pressed"); if (ellipsesetshandled.ischecked == true) e.handled = true; private void Rectangle_MouseDown(object sender, MouseButtonEventArgs e) if (sender == e.originalsource) outputitems.clear(); outputitems.add("the rectangle was pressed"); if (rectanglesetshandled.ischecked == true) e.handled = true; ToggleSwitch StackPanel 객체 Button 객체 StackPanel 객체 여기에 Border, Grid, Ellipse, Rectangle 을포함 하는 StackPanel 이있군요. private void Grid_MouseDown(object sender, MouseButtonEventArgs e) if (sender == e.originalsource) outputitems.clear(); outputitems.add("the grid was pressed"); if (gridsetshandled.ischecked == true) e.handled = true; private void Border_MouseDown(object sender, MouseButtonEventArgs e) if (sender == e.originalsource) outputitems.clear(); outputitems.add("the border was pressed"); if (bordersetshandled.ischecked == true) e.handled = true; private void StackPanel_MouseDown(object sender, MouseButtonEventArgs e) if (sender == e.originalsource) outputitems.clear(); outputitems.add("the panel was pressed"); private void UpdateHitTestButton(object sender, RoutedEventArgs e) grayrectangle.ishittestvisible = (bool)newhittestvisiblevalue.ischecked; ToggleSwitch 객체 그리드는라우트된 MouseDown 이벤트를수신합니다. 하지만이벤트를발생시키진않습니다. IsHiTestVisible 속성이 False 로되어있다면, 그리드는 Background 혹은 Fill 속성이없습니다. 만약 XAML 을수정하여 Background 속성을추가한다면, IsHitTestVisible 속성이 true 로설정됩니다 ( 심지어 Transparent 속성을설정해도상관없습니다 ). 그리고포인터를눌렀을때, 결과를출력하게하죠. Ellipse 객체 Border 객체 Grid 객체 Rectangle 객체 버튼의 Click 이벤트는토글을전환하는 IsChecked 속성을이용해서 Ractangle 컨트롤의 IsHitTestVisible 을 true/false 로설정합니다. 페이지를넘겨서지금까지만든앱으로라우트된이벤트를살펴봅시다. 770 Appendix ii 지금여기예요 4 771

머릿속에버블링이바로전달됩니다 앱을실행하고회색사각형을탭하거나눌러봅시다. 오른쪽화면과같은출력결과를볼수있습니다. Rectangle 컨트롤의 PointerPressed 이벤트핸들러인 Rectangle_MouseDown() 의첫번째줄에중단점을놓고, 디버깅하면무슨일어나는지알수있습니다. Grid sets handled 를체크하고, 회색사각형을클릭하거나탭해봅시다. 출력결과가이렇게나와야합니다. 회색사각형을다시한번클릭해보세요. 이번에는중단점이실행될겁니다. 프로시저단 위실행 (F10) 을이용해서코드한줄씩실행해보세요. 먼저 if 문에서 ListBox 에바인딩된 ObservableCollection 의 outputitems 를초기화합니다. sender 와 e.originalsource 가같은 Rectangle 컨트롤을참조하기때문에이코드가실행되죠. 그래서이벤트가발생된컨트롤 ( 클릭혹은탭할경우 ) 에대한이벤 트핸들러메서드의 sender == e.originalsource 는 true 입니다. 메서드의끝에도달할때, 프로시저단위실행을유지하세요. 이벤트는객체트리를통해버블링합니다. 먼저 Rectangle 의이벤 트핸들러에버블링되고난후, Grid 와 Border, Panel 의이벤트핸들러순으로버블링합니다. 그리고마침내 Window 의일부 이벤트핸들러메서드를실행합니다 ( 여러분의코드를벗어났고, 라우트된이벤트의일부가아니기때문에항상실행됩니다 ). 그리고컨트롤중에서이벤트의원본출처가없기때문에, 이벤트핸들러의 senders 는 e.originalsource 와일치하지않습니 다. 그래서이들중아무것도출력결과를초기화하지않습니다. IsHitTestVisible 을체크하지않은상태로두고, Update 버튼을눌러서회색사각형을클릭하거나탭해봅시다. 출력결과가이렇게나와야합니다. 잠깐만요! Rectangle 을눌렀는데 Ellipse 컨트롤의 MouseDown 이벤트핸들 러가발생했습니다. 무슨일이일어난걸까요? 버튼을눌렀을때, Click 이벤트핸들러는 Rectangle 컨트롤의 IsHitTest- Visible 속성을 false 로갱신합니다. 이것은포인터를누르거나클릭혹은 다른포인터이벤트에서컨트롤을보이지않게합니다 (invisible). 그래서 Rectangle 을클릭했을때, 이이벤트는 Rectangle 아래에있는 Ellipse 가페 이지아래의최상위컨트롤에전달됩니다. 페이지의 Ellipse 는 IsHiTestVisible 속성이 true 로설정되고, Color 혹은 Transparent 가설정된 Background 속성을가집니다. 이경우에는 Ellipse 컨트롤을발견하고, 컨트롤의 MouseDown 이벤트를발생시 킵니다. 왜리스트박스에출력결과가두줄밖에 나오지않을까요? 프로시저단위실행을 통해무슨일이일어나는지살펴봅시다. 이번에는여러분이 gridsetshandled 를체크했기때문에, gridsethandled. IsChecked 가 true 로설정돼있습니다. 그래서 Grid 의이벤트핸들러의마지막줄 에서 e.ishandled 가 true 로설정됩니다. 라우트된이벤트핸들러가버블링을시 작할때, 이벤트는버블링을중단합니다. Grid 의이벤트핸들러의수행을끝내자 마자, 앱은이벤트가처리된걸보고서는 Border 혹은 Panel 의이벤트핸들 러메서드를호출하지않습니다. 대신코드외부에추가한 Window 의이벤 트핸들러로건너뜁니다. 앱을이용해서라우트된이벤트를실험해봅시다. 아래에해야할일들이있습니다. 회색사각형과빨간색타원을클릭하고이벤트가어떻게버블링되는지출력 결과를살펴봅시다. 이벤트핸들러가 e.handled 를 true 로설정하도록, 위에서부터각각의토글 을전환해봅시다. 그리고버블링이중단된결과를살펴봅시다. 모든이벤트핸들러메서드에중단점을설정해서디버깅해봅시다. Ellipse 의이벤트핸들러메서드에중단점을놓고, Rectangle 의 IsHitTest- Visible 속성을체크 / 해제해봅시다. 아래의토글로전환하고, Update 버 튼을누릅니다. 프로시저단위실행을통해서언제 IsHitTestVisible 이 false 로되는지살펴봅시다. 프로그램을멈추고, Background 속성을 Grid 에추가해서포인터가도달하 는것을볼수있게해봅시다. 라우트된이벤트는먼저컨트롤에서발생된이벤트의이벤트핸들러를발생시킵니다. 그리고컨트롤의계층구조에따라최상위컨트롤에도달하거나이벤트핸들러가 e.handled 를 true 로설정한곳에도달할때까지버블링합니다. 772 Appendix ii 지금여기예요 4 773

일부러페이지를비웠습니다 Chapter 16 부록에서두페이지모드를유지하기위해서일부러페이지를비웠습니다. 연습문제와연습문제정답이서로마주하는면에나오면안되기때문이죠. 그리고두페이지모드의짝수쪽에는오른쪽위에, 홀수쪽에는왼쪽위에작은캡션이있습니다. MVVM(Model-View-ViewModel) 패턴을사용하여응용프로그램을만들때, 앞으로여러분의코드를더쉽게관리할수있습니다. 숙련된개발자는디자인패턴을사용합니다. 이번장에서는효과적인 WPF 응용프로그램을만들기위해서 MVVM(Model- View-ViewModel) 디자인패턴에대해알아보겠습니다. 디자인패턴이무엇인지 그리고애니메이션을위해서 XAML 컨트롤을어떻게사용하는지배워봅니다. 16 장에서참고할사항입니다. 책 793쪽까지읽어주세요. 이부록은책 794-801 쪽을대체합니다. 책 802-808 쪽을읽어주세요. 책 806쪽에서스톱워치프로젝트를시작하세요. 계속읽다가 809, 812, 814-817, 825-831 쪽이나오면이부분은부록을참고해주세요. 책 832쪽을읽습니다. 이부록은책 833-851 쪽을대체합니다. 850쪽에실습 #3에대한정보가있습니다. 774 Appendix ii 지금여기예요 4 775

패턴적용하기 MVVM 패턴으로농구선발명단앱을만들어봅시다 새윈도우스토어앱을생성하고, 이름은농구선발명단인 BacketballRoster 로합니다 ( 이코드에서 BasketballRoster 네임스페이스를사용하기때문이죠. 여러분의코드와다음에나오는몇페이지의코드가같아야합니다 ). 1 프로젝트에모델, 뷰, 뷰모델폴더를생성합니다. 프로젝트의솔루션탐색기에서마우스오른쪽버튼을클릭하고, 추가 > 새폴더를선택합니다. 직접해봅시다! 2 Player 클래스를추가해서모델을만들어봅시다. 모델폴더를오른쪽클릭해서 Player 클래스를추가합니다. 폴더에클래스를추가하면, IDE 는 끝으로폴더이름의네임스페이스를업데이트합니다. 여기에 Player 클래스가있습니다. namespace BasketballRoster.Model class Player public string Name get; private set; public int Number get; private set; public bool Starter get; private set; 폴더에클래스파일을추가할때, IDE 는폴더이름으로된네임스페이스를추가합니다. Player Name: string Number: int Starter: bool 모델 솔루션탐색기에서프로젝트에새폴더를추가할때, IDE 는폴더이름을기반으로한새로운네임스페이스를생성합니다. 이것은추가 > 클래스메뉴옵션에서생성한클래스들의네임스페이스가됩니다. 만약 Model 폴더에클래스를추가한다면, IDE 는클래스파일의꼭대기에있는네임스페이스라인에서 BasketballRoster. Model 을추가합니다. 3 public Player(string name, int number, bool starter) Name = name; Number = number; Starter = starter; 왜클래스들은서로다른것들에만관심을보일까요? 비슷해보이기도한데말이죠. 마지막으로 Roster 클래스를 Model 에추가합니다. 다음으로 Model 폴더에 Roster 클래스를추가합니다. 아래에코드가있군요. namespace BasketballRoster.Model class Roster public string TeamName get; private set; 이두클래스는선수가각명부에있는데이터를파악하는데만관심이있기때문에클래스가간단합니다. 모델에있는클래스는데이터를보여주는데관심이없죠. 그저데이터를관리할뿐입니다. private readonly List<Player> _players = new List<Player>(); public IEnumerable<Player> Players get return new List<Player>(_players); Roster TeamName: string Players: IEnumerable<string> Model, View, ViewModel 폴더를추가합니다. 솔루션탐색기는오른쪽사진과같이보입니다. _ 는 private 필드를의미합니다. public Roster(string teamname, IEnumerable<Player> players) TeamName = teamname; _players.addrange(players); Model 폴더는이렇게생겼네요. 이폴더에앱의클래스와컨트롤, 윈도우를담을거예요. _players 필드의이름에언더스코어 (_) 를추가했습니다. 언더스코어를 private 필드앞에추가하는것은일반적인네이밍컨벤션입니다. 이번장에서이러한규칙이사용되는것을볼수있습니다. 뷰는다음페이지에 794 Appendix ii 지금여기예요 4 795

컨트롤을통제하라 4 5 View 폴더에메인윈도우를추가합니다. View 폴더에오른쪽클릭을한후, 새 LeagueWindow.xaml 을추가합니다. View 폴더에 XAML 윈도우인 LeagueWindow.xaml 이있습니다. 이것은부록전반에서본 MainWindow.xaml 윈도우와같은동작을하죠. 그리고여전히 XAML 로정의된그래프의 Window 객체입니다. 다른점이있다면 MainWindow 를대신 LeagueWindow 로부르는겁니다. 메인윈도우를지우고, 이것을새윈도우로교체합니다. 솔루션탐색기에서 MainWindow.xaml 파일을선택하고, 마우스오른쪽클릭 > 삭제를선택합니다. 한번프로젝트를 빌드해서실행해보세요. 프로그램이시작될때, 아래와같은예외를얻게됩니다. 글쎄요, MainWindow.xaml 을지웠으니예외가나오는건이해가되네요. WPF 응용프로그램이시작될때, App. xaml 파일의 <Application> 태그에있는 StartupUri 속성에서지정한윈도우를보여줍니다. App.xaml 파일을열고 StartupUri 속성을수정합니다. 입력하는도중인텔리센스창에서여러분이추가한윈도우를 볼수있습니다. <Application x:class="basketballroster.app" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" StartupUri="View/LeagueWindow.xaml"> 위와같이수정하고난뒤, 프로그램을다시빌드해서실행해보세요. 이제프로그램이실행되고, 새롭게추가된창이 보일겁니다. 뷰 사용자정의컨트롤 잠시농구선수명단프로그램을살펴보세요. 각팀은같은세트의컨트롤로구성되어있습니다. TextBlock, ListView, 또다른 TextBlock, ListView 모든컨트롤은 Border 내부의 StackPanel 컨트롤안에있습니다. 같은컨트롤들의두세트를정말로페이지에추가해야할까요? 만약세번째 팀과네번째팀을추가해야한다면, 중복적으로일어나는일에대해서생각해봐야합니다. 그리고 여기에사용자정의컨트롤 (User Control) 이있습니다. 사용자정의컨트롤은자신만의컨트롤을만 들기위해서사용되는클래스입니다. 보통페이지를만들때처럼사용자정의컨트롤을만들기위해 서 XAML 과코드 - 비하인드를이용하죠. BasketballRoster 프로젝트에사용자정의컨트롤을추가 796 Appendix ii 페이지에맞게채워줍니다. 지금여기예요 4 797 해봅시다. 1 2 3 페이지를넘기기전에책 790 쪽에서윈도우스토어앱의스크린샷을보고, 새 RosterControl.xaml 에서무엇을해야할지한번생각해보세요. 파란색 <Border> 내부에있는컨트롤을 <StackPanel> 에담아야합니다. View 폴더에새로운사용자정의컨트롤을추가합니다. 사용자정의컨트롤 (WPF) 을선택하고, 이름을 RosterControl.xaml 로합니다. 새로운사용자정의컨트롤의코드 - 비하인드를살펴봅시다. RosterControl.xaml.cs 를열면, 여러분의새컨트롤은 UserControl 베이스클래스를확 장합니다. 사용자정의컨트롤로정의된모든비하인드 - 코드는아래의코드와같습니다. 새로운사용자정의컨트롤의 XAML 을살펴봅시다. IDE 는빈 <Grid> 의사용자정의컨트롤을추가합니다. Border 컨트롤에서둥근모서리를만들려면어떤속성을사용해야할까요? 선수에대한데이터를표시하는두개의 ListView 컨트롤이있습니다. 또 한 DataTemplate 이포함된 <UserControl.Resources> 섹션이필요합니다. 이템플릿의이름을 PlayerItemTemplate 으로합니다. ListView 항목을선발, 벤치플레이어를의미하는 Starters 와 Bench 속 성에바인딩합니다. 그리고위쪽에있는 TextBlock 컨트롤을팀이름의 TeamName 속성과바인딩해줍니다. Border 컨트롤은 <Grid> 안에있습니다. 그리고 <Grid> 에는한라인의 Height= Auto 속성이있는데, 이것은하단부분의 ListView 컨트롤을 낚시하는법배우기 UserControl 은여러분이지정한컨트롤을캡슐화하는방법을제공하는베이스클래스입니다. 그리고컨트롤의동작을정의하는로직을만들수있게해주죠. 점점이책의끝을향해가고있습니다. 우리는여러분이현실세계에서직면하게될문제들을고민하고해결하는습관을유도하고있습니다. 좋은프로그래머는직면할문제에대해서많은추측을합니다. 이책에서도 UserControl 이어떻게동작하는지에관한충분한정보를제공하고있습니다. 아직 UserControl 의바인딩설정을하지않아서, 디자이너에는데이터가보이지않습니다. 여러분이코드를보기위해페이지를넘기기전에여러분이할수있는만큼 XAML 코드를작성해보세요.

모델 - 뷰 - 뷰모델 4 RosterControl 의 XAML 코드를완성해봅시다. View 폴더에추가한 RosterControl 의사용자정의컨트롤코드가아래에있습니다. 데이터컨텍스트없이속성에 어떻게바인딩하는지눈치채셨나요? 윈도우의두컨트롤은서로다른데이터를보여주기때문에페이지는각컨 트롤에게다른데이터컨텍스트를설정해야합니다. <UserControl x:class="basketballroster.view.rostercontrol" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" mc:ignorable="d" d:designheight="450" d:designwidth="300"> <UserControl.Resources> <DataTemplate x:key="playeritemtemplate"> <TextBlock> <Run Text="Binding Name, Mode=OneWay"/> <Run Text=" #"/> <Run Text="Binding Number, Mode=OneWay"/> </TextBlock> </DataTemplate> </UserControl.Resources> <Grid> 두 ListView 컨트롤은정적리소스로정의된같은템플릿을사용하고있네요. <Grid.RowDefinitions> <RowDefinition Height="Auto"/> </Grid.RowDefinitions> <Border BorderThickness="2" BorderBrush="Blue" CornerRadius="6" Background="Black"> <StackPanel Margin="20"> <TextBlock Foreground="White" FontFamily="Segoe" FontSize="20px" FontWeight="Bold" Text="Binding TeamName" /> <TextBlock Foreground="White" FontFamily="Segoe" FontSize="16px" Text="Starting Players" Margin="0,5,0,0"/> <ListView Background="Black" Foreground="White" Margin="0,5,0,0" ItemTemplate="StaticResource PlayerItemTemplate" ItemsSource="Binding Starters" /> <TextBlock Foreground="White" FontFamily="Segoe" FontSize="16px" Text="Bench Players" Margin="0,5,0,0"/> <ListView Background="Black" Foreground="White" ItemsSource="Binding Bench" </Border> 여러분은이미컨트롤은 Height 및 Width 속성에따라크기가변경되는것을알고있죠. 이속성들을수정할때, 숫자를조정해서디자이너창에표시되는컨트롤의크기를변경해도됩니다. ListView 항목에대한데이터템플릿을정적리소스에놓습니다. 그러고나서 <ListView.ItemTemplate> 섹션대신, ListView 태그의 ItemTemplate 속성에정적리소스를설정합니다. ItemTemplate="StaticResource PlayerItemTemplate" CornerRadius 속성을사용하면 Border 에둥근모서리효과를줄수있습니다. ItemTemplate="StaticResource PlayerItemTemplate" Margin="0,5,0,0"/> BasketballRoster 앱에서모델의데이터와뷰의바인딩을살펴봅시다. 그리고이둘을 연결하는 " 배관 " 역할을하는뷰모델을만들어봅시다. LeaguePage.xaml 에컨트롤을추가합니다. 먼저새네임스페이스를인식하도록아래의 xmlns 속성을페이지에추가합니다. xmlns:view="clr-namespace:basketballroster.view" xmlns:viewmodel="clr-namespace:basketballroster.viewmodel" <Window.Resources> <viewmodel:leagueviewmodel x:key="leagueviewmodel"/> </Window.Resources> PlayerViewModel Name: string Number: int RosterViewModel TeamName: string Starters: ObservableCollection <PlayerViewModel> Bench: ObservableCollection <PlayerViewModel> constructor: RosterViewModel(Model.Roster) private UpdateRosters() LeagueViewModel JimmysTeam: RosterViewModel BriansTeam: RosterViewModel private GetBomberPlayers(): Model.Roster private GetAmazinPlayers(): Model.Roster </Grid> IDE가 XAML 디자이너에에러메시지를띄운다면, ViewModel 네임스페이스에 </UserControl> LeagueViewModel 이존재하지않을겁니다. 하지만여러분이정확하게클래스를추가했다면, 798 Appendix ii BasketballRoster 프로젝트에마우스오른쪽버튼을클릭한후, 프로젝트언로드 (Unload Project) 를선택합니다. 다시오른쪽버튼을클릭한후프로젝트다시로드 (Reload Project) 를 지금여기예요 4 799 선택해서프로젝트를다시불러옵니다. 물론 C# 코드파일에어떤오류도없어야합니다. 1 2 3 LINQ 쿼리에대한힌트를얻으려면책 792 쪽을 살펴보세요. 그러고나서 LeagueViewModel 인스턴스를정적리소스에추가합니다. 이제두 RosterControl 이있는 StackPanel 을추가합니다. 뷰모델 <StackPanel Orientation="Horizontal" Margin="5" VerticalAlignment="Center" HorizontalAlignment="Center" DataContext="StaticResource ResourceKey=LeagueViewModel" > <view:rostercontrol Width="200" DataContext="Binding JimmysTeam" Margin="0,0,20,0" /> <view:rostercontrol Width="200" DataContext="Binding BriansTeam" /> 뷰모델클래스를추가합니다. ViewModel 폴더에아래의세클래스를생성해주세요. 뷰모델클래스를동작하게합니다. PlayerViewModel 클래스는두속성이있는간단한데이터객체입니다. 정확한위치에폴더에클래스와윈도우를생성해야합니다. 그렇지않으면, 네임스페이스는솔루션탐색기에있는코드와일치하지않습니다. LeagueViewModel 클래스는페이지에더미데이터를생성하기위한두 private 메서드가있습니다. 이것은 RosterViewModel 생성자에전달받을각팀에대한 Model.Roster 객체를생성합니다. RosterViewModel 클래스의생성자의매개변수는 Model.Roster 객체입니다. 이생성자는 TeamName 속성 을설정한다음, private UpdateRosters() 메서드를호출합니다. 이메서드는선발과벤치플레이어를추출하 기위해서 LINQ 쿼리를사용하고, Starters 와 Bench 속성을업데이트합니다. 모델의네임스페이스에서객체 를사용하려면클래스의상단에 "using Model;" 을추가합니다.

연습문제정답 LeagueViewModel 클래스는 RosterControl 을데이터컨텍스트로사용할수있는 RosterViewModel 을노출합니다. 그리고이필드는 RosterViewModel 을사용할수있도록모델객체를생성합니다. 이 private 메서드는 Player 객체의새로운리스트를생성하는더미데이터를만들어냅니다. 데이터를저장하기위해뷰의클래스들을이용할수있습니다. 이메서드는 PlayerViewModel 객체가아닌 Player 객체를반환하기때문이죠. BasketballRoster 앱의뷰모델에는세클래스가있습니다 (LeagueViewModel, PlayerViewModel, RosterViewModel 클래스 ). 이들은모두 ViewModel 폴더에있습니다. namespace BasketballRoster.ViewModel using Model; using System.Collections.ObjectModel; class LeagueViewModel public RosterViewModel BriansTeam get; private set; public RosterViewModel JimmysTeam get; private set; public LeagueViewModel() Roster briansroster = new Roster("The Bombers", GetBomberPlayers()); BriansTeam = new RosterViewModel(briansRoster); Roster jimmysroster= new Roster("The Amazins", GetAmazinPlayers()); JimmysTeam = new RosterViewModel(jimmysRoster); private IEnumerable<Player> GetBomberPlayers() List<Player> bomberplayers = new List<Player>() new Player("Brian", 31, true), new Player("Lloyd", 23, true), new Player("Kathleen",6, true), new Player("Mike", 0, true), new Player("Joe", 42, true), new Player("Herb",32, false), new Player("Fingers",8, false), ; return bomberplayers; private IEnumerable<Player> GetAmazinPlayers() List<Player> amazinplayers = new List<Player>() new Player("Jimmy",42, true), new Player("Henry",11, true), new Player("Bob",4, true), new Player("Lucinda", 18, true), new Player("Kim", 16, true), new Player("Bertha", 23, false), new Player("Ed",21, false), ; return amazinplayers; namespace BasketballRoster.ViewModel class PlayerViewModel public string Name get; private set; public int Number get; private set; public PlayerViewModel(string name, int number) Name = name; Number = number; 만약 "using Model; 이없다면, 여러분은 Roster 를입력하는대신 Model.Roster 를입력해야합니다. 더미데이터는일반적으로뷰모델에있습니다. 왜냐하면 MVVM 응용프로그램의상태는 ViewModel 객체안에캡슐화된 Model 클래스의인스턴스로관리하기때문이죠. PlayerViewModel 클래스는간단한데이터객체입니다. 데이터템플릿을바인딩하는속성이있습니다. namespace BasketballRoster.ViewModel using Model; using System.Collections.ObjectModel; using System.ComponentModel; class RosterViewModel : INotifyPropertyChanged public ObservableCollection<PlayerViewModel> Starters get; private set; public ObservableCollection<PlayerViewModel> Bench get; private set; private Roster _roster; private string _teamname; public string TeamName get return _teamname; set _teamname = value; public RosterViewModel(Roster roster) _roster = roster; Starters = new ObservableCollection<PlayerViewModel>(); Bench = new ObservableCollection<PlayerViewModel>(); TeamName = _roster.teamname; UpdateRosters(); private void UpdateRosters() var startingplayers = from player in _roster.players where player.starter select player; foreach (Player player in startingplayers) Starters.Add(new PlayerViewModel(player.Name, player.number)); var benchplayers = from player in _roster.players where player.starter == false select player; TeamName 속성이변할때마다, RosterViewModel 은 PropertyChanged 이벤트를발생시켜서, 바인딩된객체들을갱신합니다. foreach (Player player in benchplayers) Bench.Add(new PlayerViewModel(player.Name, player.number)); 보통 MVVM 앱에서는 ViewModel 의클래스들만 INotifyPropertyChanged 인터페이스를구현합니다. 왜냐하면이들만이 XAML 컨트롤에바인딩된객체이기때문이죠. ViewModel 내부에있는캡슐화된 Roster 객체에서앱의상태를저장합니다. 클래스의나머지부분은 View 를바인딩할수있는속성으로 Model 데이터를바꿉니다. LINQ 쿼리는모든선발플레이어들을찾아서 ObservableCollection 속성의 Starters 에추가합니다. 여기에벤치플레이어들을찾는비슷한 LINQ 쿼리가있네요. 일반적인 MVVM 앱은 ViewModel 클래스에서만 INotifyPropertyChanged 를구현합니다. 왜냐하면 ViewModel 은 XAML 컨트롤을바인딩하는객체들만포함하기때문이죠. 그러나연습문제에서는생성자에서바인딩된속성을갱신하지않기때문에, INotifyPropertyChanged 를구현하지않았습니다. 브라이언과지미가그들의팀이름을변경하기위해서프로젝트를수정하고싶다면, TeamName 의 set 접근자에서 PropertyChanged 이벤트를발생시켜야합니다. 800 Appendix ii 지금여기예요 4 801

스톱워치뷰만들기 스톱워치컨트롤을위한 XAML 코드가있습니다. View 폴더에 BasicStopwatch.xaml 이라는 WPF 사용자정의컨트롤을추가하고, 아래의코드를입력하세요. 사용자정의컨트롤에는경과시간과랩타임을표시하기위한 TextBlock 컨트롤과시작, 중지, 리셋, 랩타임버튼이있습니다. 뷰 책 810, 811 쪽을참고할때, 고쳐야할부분이하나있습니다. 810 쪽에서아래의 using 문을찾아주세요. using Windows.UI.Xaml; 그리고아래와같이바꿔주세요. using System.Windows.Threading; Windows.UI.Xaml 네임스페이스는윈도우스토어에대한.NET 프레임워크의일부입니다. 이걸 WPF 응용프로그램에서는사용할수없죠. 대신 System.Windows.Threading 네임스페이스가필요합니다. 이부분을제외하고코드가똑같습니다. 그리고여기에서 Model- View-ViewModel 의좋은예를보여주고있습니다. 하나의 using 문을제외하고, ViewModel 과 Model 에대해서똑같은 C# 코드를사용하기때문에, 윈도우스토어앱의스톱워치앱클래스들을 WPF 에서쉽게재사용할수있습니다. <UserControl x:class="stopwatch.view.basicstopwatch" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" mc:ignorable="d" d:designheight="300" d:designwidth="300" xmlns:viewmodel="clr-namespace:stopwatch.viewmodel"> <UserControl.Resources> <viewmodel:stopwatchviewmodel x:key="viewmodel"/> </UserControl.Resources> <Grid DataContext="StaticResource ResourceKey=viewModel"> <StackPanel> <TextBlock> <Run>Elapsed time: </Run> <Run Text="Binding Hours, Mode=OneWay"/> <Run>:</Run> <Run Text="Binding Minutes, Mode=OneWay"/> <Run>:</Run> <Run Text="Binding Seconds, Mode=OneWay"/> </TextBlock> <TextBlock> <Run>Lap time: </Run> <Run Text="Binding LapHours, Mode=OneWay"/> <Run>:</Run> <Run Text="Binding LapMinutes, Mode=OneWay"/> <Run>:</Run> <Run Text="Binding LapSeconds, Mode=OneWay"/> </TextBlock> <StackPanel Orientation="Horizontal"> </Grid> </UserControl> <Button Click="StartButton_Click" Margin="0,0,5,0">Start</Button> <Button Click="StopButton_Click" Margin="0,0,5,0">Stop</Button> <Button Click="ResetButton_Click" Margin="0,0,5,0">Reset</Button> <Button Click="LapButton_Click">Lap</Button> 힌트 : 지속적으로 Model 을확인하고, 속성을업데이트하기위해서 DispatcherTimer 를사용합니다. 이사용자정의컨트롤은정적리소스로써 ViewModel 의인스턴스를저장합니다. 그리고인스턴스를데이터컨텍스트로사용하죠. 데이터컨텍스트를설정하기위해서컨테이너가필요없습니다. 인스턴스에서자신의상태를계속저장하고있습니다. 이 TextBlock 컨트롤은경과시간을반환해주는 ViewModel 의속성에바인딩되어있습니다. 이 TextBlock 컨트롤은랩타임을노출하는속성에바인딩되어있습니다. 네임스페이스를추가하기위해서이 xaml 속성이필요합니다. 프로젝트의이름이 Stopwatch 이기때문에, 뷰모델의네임스페이스는 Stopwatch. ViewModel 입니다. ViewModel 은이값들을최신으로유지하기위해서 PropertyChanged 이벤트를발생시켜야합니다. 컴파일을위해서 ViewModel 네임스페이스의 StopwatchViewModel 클래스와컨트롤에 Click 이벤트핸들러를추가해야합니다. 책 810, 811 쪽에 ViewModel 의코드가있습니다. 책을보기전에 View 와 Model 의코드로부터얼마만큼 ViewModel 코드를작성할수있나요? BasicStopwatch 컨트롤을메인윈도우에추가해보세요. 그리고코드를작성한뒤, 810, 811 쪽과비교해보세요. 808 Appendix ii 정말주의해야할부분은 IDE 가필연적으로잘못됐다는것을가정하지말아야한다는겁니다. 때로는한 XAML 페이지의오류 ( 깨진 xamls 속성과같은 ) 가디자이너를중단시킬수있기때문이죠. 지금여기예요 4 809

틱틱틱 스톱워치앱완성하기 몇가지해결해야할부분이있습니다. 여러분의스톱워치사용자정의컨트롤에는이벤트핸들러가없어서이를추가 해야합니다. 그리고메인윈도우에컨트롤을추가해야합니다. 1 먼저, BasicStopwatch.xaml.cs 에서아래의이벤트핸들러를코드 - 비하인드에추가합니다. ViewModel.StopwatchViewModel viewmodel; public BasicStopwatch() InitializeComponent(); viewmodel = FindResource("viewModel") as ViewModel.StopwatchViewModel; private void StartButton_Click(object sender, RoutedEventArgs e) viewmodel.start(); private void StopButton_Click(object sender, RoutedEventArgs e) viewmodel.stop(); private void ResetButton_Click(object sender, RoutedEventArgs e) viewmodel.reset(); private void LapButton_Click(object sender, RoutedEventArgs e) viewmodel.lap(); View 의버튼은단지 ViewModel 의메서드를호출합니다. 이것은 View 의깔끔하고일반적인패턴이죠. 부록에서두페이지모드를유지하기위해서일부러페이지를비웠습니다. 연습문제와연습문제정답이서로마주하는면에나오면안되기때문이죠. 그리고두페이지모드의짝수쪽에는오른쪽위에, 홀수쪽에는왼쪽위에작은캡션이있습니다. 2 MainWindow.xaml 에대한 XAML 코드입니다. <Window x:class="stopwatch.mainwindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="150" Width="250" xmlns:view="clr-namespace:stopwatch.view"> <Grid> <view:basicstopwatch Margin="5"/> </Grid> </Window> 모든행위는사용자정의컨트롤에있습니다. 그래서메인윈도우에대한코드 - 비하인드가없습니다. 이제앱을실행해봅시다. Start, Stop, Reset, Lap 버튼을눌러스톱워치가 작동하는지확인해봅시다. 812 Appendix ii

뷰모델의유용한도구 변환기는자동으로바인딩값을변환해줍니다 디지털시계는보통 분 앞의숫자에 0 이있습니다. 우리의스톱워치도마찬가지로두숫자의 분 을보여줍니 다. 그리고 초 역시도두숫자를표시하고, 백분의일초에가깝게시간이돌아갑니다. 적절한형식의문자열 값을표시하기위해서뷰모델을수정할수있습니다. 하지만이것은같은데이터를다른형식으로표시할때마 다계속다른속성을추가해야하는것을의미하죠. 그래서값변환기 (Value converter) 는매우편리합니다. 값 변환기는 XAML 바인딩을이용한객체입니다. 데이터가컨트롤에전달되기전에수정해주죠. IValueConverter 인터페이스 (System.Windows.Data 네임스페이스에있는 ) 를구현함으로써값변환기를만들수있습니 다. 이제값변환기를스톱워치에추가해봅시다. 1 2 using System.Windows.Data; class TimeNumberFormatConverter : IValueConverter public object Convert(object value, Type targettype, 변환기는어떻게 decimal 을 int 유형으로변환시키는지알고있습니다. int 값의경우, 선택적으로매개변수에전달할수있습니다. ViewModel 폴더에 TimeNumberFormatConverter 클래스를추가합니다. using System.Windows.Data; 를클래스의맨위에추가한뒤, IValueConverter 인터페이스를구현합니다. IDE 를이용하여자동으로인터페이스를구현합니다. 이것은두메서드스텁을추가합니다 (Convert(), ConvertBack() 메서드 ). Convert() 메서드를구현합니다. Convert() 메서드는몇몇의매개변수가있습니다. 그중에서두개를사용해봅시다. value 매개변수는바인딩을 통해전달되는원본값이고, parameter 매개변수는 XAML 에서매개변수를지정할수있습니다. object parameter, System.Globalization.CultureInfo culture) if (value is decimal) return ((decimal)value).tostring("00.00"); else if (value is int) if (parameter == null) return ((int)value).tostring("d1"); else return ((int)value).tostring(parameter.tostring()); return value; ConvertBack() 메서드는양방향바인딩을사용합니다. 이프로젝트에서이메서드를사용하진않습니다. 그래서이메서드스텁을아래와같이남겨둡니다. 뷰모델 변환기는뷰모델을만드는데유용한도구입니다. public object ConvertBack(object value, Type targettype, object parameter, System.Globalization.CultureInfo culture) throw new NotImplementedException(); BasicStopwatch.xaml 의정적리소스에변환기를추가합니다. ViewModel 객체를다음과같이추가합니다. <UserControl.Resources> <viewmodel:stopwatchviewmodel x:key="viewmodel"/> <viewmodel:timenumberformatconverter x:key="timenumberformatconverter"/> </UserControl.Resources> <TextBlock> <Run>Elapsed time: </Run> <Run Text="Binding Hours, Mode=OneWay, Converter=StaticResource timenumberformatconverter"/> <Run>:</Run> <Run Text="Binding Minutes, Mode=OneWay, Converter=StaticResource timenumberformatconverter, ConverterParameter=d2"/> <Run>:</Run> <Run Text="Binding Seconds, Mode=OneWay, </TextBlock> <TextBlock> Converter=StaticResource timenumberformatconverter"/> <Run>Lap time: </Run> <Run Text="Binding LapHours, Mode=OneWay, Converter=StaticResource timenumberformatconverter"/> <Run>:</Run> <Run Text="Binding LapMinutes, Mode=OneWay, Converter=StaticResource timenumberformatconverter, ConverterParameter=d2"/> <Run>:</Run> <Run Text="Binding LapSeconds, Mode=OneWay, </TextBlock> Converter=StaticResource timenumberformatconverter"/> 숫자가올바른형식으로나타납니다. 코드에서아직구현되지않았다는 NotImplementedException 예외를남기는좋은예입니다. 프로젝트에서이메서드가절대로실행되지않습니다. 하지만, 만약이메서드가실행된다면, 사용자는 814 Appendix ii 이것을보지못한채조용히실패를처리하는게좋을까요? 아니면문제를추적할수있도록예외를던지는것이좋을까요? 이중어느것이더좋다고생각하나요? 반드시하나의정답이있는것은아닙니다. 지금여기예요 4 815 3 4 값변환기를사용하기위해 XAML 코드를수정합니다. 각 <Run> 태그에있는 Converter= 를추가해서 Binding 마크업을수정합니다. 이제스톱워치는 TextBlock 컨트롤에값을전달하기 전에변환기를통해값을실행합니다. 그리고창에서 이라인을추가한후, 디자이너에서프로젝트가실행되지않는다면솔루션다시빌드를해야할수도있습니다. 드물지만프로젝트를언로드하고다시로드해야할수도있습니다. 매개변수가없다면, 닫는괄호 () 를추가하는것을잊지마세요. 뷰 ConverterParameter 를이용하여매개변수를변환기에전달해줍니다.

다른유형으로변환하기 변환기는다양한유형으로작업할수있습니다 TextBlock 과 TextBox 컨트롤은텍스트를취급해서, 문자열혹은숫자를 Text 속성에바인딩합니다. 하지만다양한속 성들이존재하고, 마찬가지로그속성들도바인딩할수있습니다. 만약뷰모델에부울속성이있다면, 그것은모든 true/ false 속성에바인딩될수있습니다. 심지어열거형을이용하여속성을바인딩할수도있습니다 (IsVisible 속성은값변환 기를쓸수있는 Visibility 열거형을사용합니다 ). 부울속성과 Visibility 바인딩을추가하여스톱워치를바꿔봅시다. 여기에유용한두컨버터가있군요. 바인딩된속성이 false 일경우, 컨트롤이활성화되도록 IsEnabled 같은부울속성을바인딩하고싶을때가있습니다. 우리는 BooleanNotConverter 라불리는새로운변환기를추가할겁니다.! 연산자는대상부울속성을반전시킵니다. IsEnabled="Binding Running, Converter=StaticResource notconverter" 여러분은종종데이터컨텍스트에서부울속성을기준으로컨트롤을표시하거나숨기고싶어합니다. 여러분은컨트롤의 Visibility 속성을 Visibility 유형의대상속성으로바인딩할수있습니다 (Visibility.Collapsed 와같이값을반환하는것을의미합니다 ). BooleanVisibilityConverter 라불리는변환기를추가하여컨트롤의 Visibility 속성을대상부울속성에바인딩하여, 컨트롤을표시하거나숨겨봅시다. 2 3 부울값을반전시키는변환기를추가합니다. 아래에 true 를 false 혹은그반대로반전시켜주는값변환기가있습니다. 컨트롤에 IsEnabled 속성처럼부울속성을사용할수있습니다. using System.Windows.Data; class BooleanNotConverter : IValueConverter public object Convert(object value, Type targettype, object parameter, System.Globalization.CultureInfo culture) if ((value is bool) && ((bool)value) == false) return true; else return false; public object ConvertBack(object value, Type targettype, object parameter, System.Globalization.CultureInfo culture) throw new NotImplementedException(); 부울속성을 Visibility 열거형으로바꾸는변환기를추가합니다. 여러분은 Visibility 속성을 Visible 혹은 Collapsed 로설정함으로써컨트롤을보이거나보이지않게만드는방법을이미 알고있습니다. Visible 와 Collapsed 의값은 System.Windows 네임스페이스의 Visibility 의열거형에있습니다. 아래에 부울값을 Visibility 값으로바꿔주는변환기가있네요. 뷰모델 1 Visibility="Binding Running, Converter=StaticResource visibilityconverter" 뷰모델의 Tick 이벤트핸들러를수정합니다. Running 속성의값이변했을때 PropertyChanged 이벤트를발생시키기위해서 DispatcherTimer 의 Tick 이벤 트핸들러를수정합니다. int _lasthours; int _lastminutes; decimal _lastseconds; bool _lastrunning; void TimerTick(object sender, object e) if (_lastrunning!= Running) _lastrunning = Running; OnPropertyChanged("Running"); if (_lasthours!= Hours) _lasthours = Hours; OnPropertyChanged("Hours"); if (_lastminutes!= Minutes) _lastminutes = Minutes; OnPropertyChanged("Minutes"); if (_lastseconds!= Seconds) _lastseconds = Seconds; OnPropertyChanged("Seconds"); Running 을추가하여타이머를체크합니다. 모델이대신이벤트를발생시키는것이더좋을까요? 뷰모델 using System.Windows; using System.Windows.Data; class BooleanVisibilityConverter : IValueConverter public object Convert(object value, Type targettype, object parameter, System.Globalization.CultureInfo culture) if ((value is bool) && ((bool)value) == true) return Visibility.Visible; else return Visibility.Collapsed; public object ConvertBack(object value, Type targettype, object parameter, System.Globalization.CultureInfo culture) throw new NotImplementedException(); 변환기를사용하기위해서스톱워치컨트롤을수정합니다. BasicStopwatch.xaml 을수정하여정적리소스의변환기인스턴스를추가합니다. <StackPanel Orientation="Horizontal"> <Button IsEnabled="Binding Running, Converter=StaticResource notconverter" Click="StartButton_Click" Margin="0,0,5,0">Start</Button> <Button IsEnabled="Binding Running" Click="StopButton_Click" Margin="0,0,5,0">Stop</Button> <Button Click="ResetButton_Click" Margin="0,0,5,0">Reset</Button> <Button IsEnabled="Binding Running" Click="LapButton_Click">Lap</Button> <TextBlock Text="Stopwatch is running" Visibility="Binding Running, Converter=StaticResource visibilityconverter"/> 816 Appendix ii 지금여기예요 4 817 4 <viewmodel:booleanvisibilityconverter x:key="visibilityconverter"/> <viewmodel:booleannotconverter x:key="notconverter"/> 이제컨트롤의 IsEnabled 와 Visibility 속성을뷰모델의 Running 속성에바인딩할수있습니다. 이것은스톱워치가실행될때, TextBlock 컨트롤을보이게해줍니다. 뷰 스톱워치가실행되지않은경우에만, Start 버튼을활성화합니다.

일부러페이지를비웠습니다 부록에서두페이지모드를유지하기위해서일부러페이지를비웠습니다. 연습문제와연습문제정답이서로마주하는면에나오면안되기때문이죠. 그리고두페이지모드의짝수쪽에는오른쪽위에, 홀수쪽에는왼쪽위에작은캡션이있습니다. 같은뷰모델을이용하여아날로그스톱워치를만들어봅시다 MVVM 패턴은뷰모델에서뷰를, 모델에서뷰모델을분리 (decouple) 합니다. MVVM 패턴은이중하나를변 경해야할경우매우유용합니다. 역할을분리하는디커플링 (decoupling) 덕분에 산탄총수술 효과없이쉽 게수정할수있고, 다른계층에서도파문이일어나지않습니다. 그래서스톱워치프로그램의뷰모델에서뷰의 디커플링을잘했을까요? 이것을확인하는한가지방법이있습니다. 뷰모델에있는클래스의변경없이전체 로새로운뷰를만들어봅시다. C# 코드에서변경해야할단한가지는숫자로된분과초를아날로그형태로 바꾸는뷰모델의새변환기입니다. 1 using System.Windows.Data; class AngleConverter : IValueConverter public object Convert(object value, Type targettype, object parameter, System.Globalization.CultureInfo culture) double parsedvalue; if ((value!= null) && double.tryparse(value.tostring(), out parsedvalue) 디지털시간을아날로그로바꾸는변환기를추가합니다. ViewModel 폴더에 AngleConverter 클래스를추가해주세요. 동그란시계의침을위해사용됩니다. && (parameter!= null)) switch (parameter.tostring()) case "Hours": return parsedvalue * 30; case "Minutes": case "Seconds": return parsedvalue * 6; return 0; 시간의범위는 0~11 이고, 이를각도로변환하려면 30 을곱해줍니다 (360/12 = 30). 분과초의범위는 0~59 이고, 이를각도로변환하면 6 을곱해줍니다 (360/60=6). public object ConvertBack(object value, Type targettype, object parameter, System.Globalization.CultureInfo culture) throw new NotImplementedException(); 14 장에서지미의만화책에 데이터클래스만들어서어떻게사용했고, 클래스를재사용해서아무런변화없이분할앱을생성한거기억하시죠? 이것과같은발상입니다. 직접해봅시다! 뷰모델 2 새 UserControl 을추가합니다. View 폴더에 AnalogStopwatch 라불리는새로운사용자정의컨트롤을추가합니다. 그리고 ViewModel 의네임스페이스에 <UserControl> 태그를추가합니다. 너비와높이도바꿔주세요. d:designheight="300" d:designwidth="400" xmlns:viewmodel="using:stopwatch.viewmodel"> 그리고뷰모델과두변환기, 스타일을사용자정의컨트롤의정적리소스에추가해주세요. 뷰 <UserControl.Resources> <viewmodel:stopwatchviewmodel x:key="viewmodel"/> <viewmodel:booleannotconverter x:key="notconverter"/> <viewmodel:angleconverter x:key="angleconverter"/> </UserControl.Resources> 지금여기예요 4 825

여러분의컨트롤로바꿔주세요 열너비를설정하는것은그어떤컨테이너에들어가더라도이것을채우기위해, 확장을유지합니다. 분침이군요 랩타임을나타내는두개의노란색침입니다. 3 그리드에동그란시계판과침을추가합니다. <Grid> 태그를수정하여동그란스톱워치판과, 4 개의사각형으로침을추가해주세요. <Grid x:name="basegrid" DataContext="StaticResource ResourceKey=viewModel"> <Grid.ColumnDefinitions> <ColumnDefinition Width="400"/> </Grid.ColumnDefinitions> <Ellipse Width="300" Height="300" Stroke="Black" StrokeThickness="2"> <Ellipse.Fill> <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0"> <LinearGradientBrush.RelativeTransform> <CompositeTransform CenterY="0.5" CenterX="0.5" Rotation="45"/> </LinearGradientBrush.RelativeTransform> <GradientStop Color="#FFB03F3F"/> <GradientStop Color="#FFE4CECE" Offset="1"/> </LinearGradientBrush> </Ellipse.Fill> </Ellipse> <Rectangle RenderTransformOrigin="0.5,0.5" Width="2" Height="150" Fill="Black"> <Rectangle.RenderTransform> <TransformGroup> <TranslateTransform Y="-60"/> <RotateTransform Angle="Binding Seconds, Converter=StaticResource ResourceKey=angleConverter, ConverterParameter=Seconds"/> </TransformGroup> </Rectangle.RenderTransform> </Rectangle> <Rectangle RenderTransformOrigin="0.5,0.5" Width="4" Height="100" Fill="Black"> <Rectangle.RenderTransform> <TransformGroup> <TranslateTransform Y="-50"/> <RotateTransform Angle="Binding Minutes, Converter=StaticResource ResourceKey=angleConverter, ConverterParameter=Minutes"/> </TransformGroup> </Rectangle.RenderTransform> </Rectangle> <Rectangle RenderTransformOrigin="0.5,0.5" Width="1" Height="150" Fill="Yellow"> <Rectangle.RenderTransform> <TransformGroup> <TranslateTransform Y="-60"/> <RotateTransform Angle="Binding LapSeconds, Converter=StaticResource ResourceKey=angleConverter, ConverterParameter=Seconds"/> </TransformGroup> </Rectangle.RenderTransform> </Rectangle> <Rectangle RenderTransformOrigin="0.5,0.5" Width="2" Height="100" Fill="Yellow"> <Rectangle.RenderTransform> <TransformGroup> <TranslateTransform Y="-50"/> <RotateTransform Angle="Binding LapMinutes, Converter=StaticResource ResourceKey=angleConverter, ConverterParameter=Minutes"/> </TransformGroup> </Rectangle.RenderTransform> </Rectangle> <Ellipse Width="10" Height="10" Fill="Black"/> </Grid> 침이겹치는것을가리기위해서, 중간에다른원을그려줍니다. 이것은 Grid 의하단에있기때문에마지막으로그려서, 상단으로오게합니다. 뷰 이것은스톱워치의동그란판입니다. 검은윤곽선과회색그라데이션배경으로되어있습니다. 여기에두번째침이있네요. 길고가는사각형이고, 숫자를각도로변환해줍니다. 모든컨트롤은하나의 RenderTransform 섹션을가지고있습니다. TransformGroup 태그는같은컨트롤에변환을여러번적용할수있습니다. 스톱워치판은 Save the Humans 에사용한배경화면처럼회색그라데이션브러시로채워져있습니다. 각침은두번변환됩니다. 판의중심으로부터시작합니다. 첫번째변환은회전하는위치로가기위해서, 침을들어올립니다. 모든컨트롤은표시될방법을바꿀수있는하나의 RenderTransform 요소를가질수있습니다. 이것은회전, 오프셋이동, 크기를확장하거나축소하는것등을포함하죠. Save the Humans 에서외계인처럼보이려고적군의타원모양을바꾸기위해서 변형 (Transform) 을사용했습니다. <TranslateTransform Y="-60"/> <RotateTransform Angle="Binding Seconds, Converter=StaticResource ResourceKey=angleConverter, ConverterParameter=Seconds"/> 두번째변환은올바른각도로회전하는겁니다. 회전의 Angle 속성은뷰모델의분과초에바인딩되어있고, 각도변환기를이용해서숫자값을각도로변환해줍니다. 초침이추가되자마자스톱워치가틱 (tick) 을시작합니다. 디자이너에서컨트롤을그리기위해서, 정적리소스인뷰모델의인스턴스가생성되기때문이죠. 디자이너가갱신을하지않을수도있습니다. 이경우디자이너창을다시전환해서재시작할수있습니다. 826 Appendix ii 지금여기예요 4 827

리소스추가하기 4 스톱워치에버튼을추가합니다. 5 두스톱워치를보여주기위해서메인윈도우를수정합니다. ViewModel 이동일하기때문에버튼도같은방식으로동작합니다. BasicStopwatch.xaml 에서사용했던버튼을 Analog- AnalogStopwatch 컨트롤을추가하기위해서 MainWindow.xaml 을수정합니다. Stopwatch.xaml 에추가합니다. <StackPanel Orientation="Horizontal" VerticalAlignment="Bottom"> <Button IsEnabled="Binding Running, Converter=StaticResource notconverter" Click="StartButton_Click" Margin="0,0,5,0">Start</Button> <Button IsEnabled="Binding Running" Click="StopButton_Click" Margin="0,0,5,0">Stop</Button> <Button Click="ResetButton_Click" Margin="0,0,5,0">Reset</Button> <Button IsEnabled="Binding Running" Click="LapButton_Click">Lap</Button> AnalogStopwatch.xaml.cs 에대한코드-비하인드입니다. ViewModel.StopwatchViewModel viewmodel; public AnalogStopwatch() InitializeComponent(); <Window x:class="stopwatch.mainwindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Two Stopwatches" Height="450" Width="400" ResizeMode="NoResize" xmlns:view="clr-namespace:stopwatch.view"> <Grid> <StackPanel> <view:basicstopwatch Margin="5"/> <view:analogstopwatch Margin="5"/> </Grid> </Window> 앱을실행해봅시다. 창에두개의스톱워치컨트롤이있군요. viewmodel = FindResource("viewModel") as ViewModel.StopwatchViewModel; private void StartButton_Click(object sender, RoutedEventArgs e) viewmodel.start(); private void StopButton_Click(object sender, RoutedEventArgs e) viewmodel.stop(); private void ResetButton_Click(object sender, RoutedEventArgs e) viewmodel.reset(); private void LapButton_Click(object sender, RoutedEventArgs e) viewmodel.lap(); 각스톱워치는자신의시간을유지합니다. 각자정적리소스로서뷰모델의분리된인스턴스를가지기때문이죠. _stopwatchmodel 필드를 static 으로만들어서뷰모델을바꿔보세요. 이것이스톱워치앱의어떤행동을변화시킬까요? 무슨일이일어나고있는지맞혀보세요. 828 Appendix ii 지금여기예요 4 829

결국, 모든게코드네요. UI 컨트롤도인스턴스화할수있습니다 XAML 코드가 System.Windows 네임스페이스의클래스를인스턴스화하는것을이미알고있습니다. 10장에서조사식창을통해살펴봤습니다. 그러나코드내부에서컨트롤을만들려고하면무엇을해야할까요? 글쎄요, 컨트롤은단지객체이기때문에컨트롤을생성할수있고, 다른어느객체와마찬가지로컨트롤을다룰수있습니다. 아날로그스톱워치의코드-비하인드를수정하여, 시계판에눈금을추가해봅시다. 게임을위해서우리가필요한모든것을만들어줘서고마워요. 이제우리는권위있는객체마을의트로피를위해경쟁할수있게됐습니다. public sealed partial class AnalogStopwatch : UserControl public AnalogStopwatch() InitializeComponent(); <Rectangle> 태그와같은 Rectangle 객체의인스턴스를생성합니다. viewmodel = FindResource("viewModel") as ViewModel.StopwatchViewModel; AddMarkings(); private void AddMarkings() 눈금을추가하는메서드는생성자에서호출합니다. 이문장은 % 나머지연산자를이용하여시간의눈금을분의눈금보다두껍게만듭니다. i % 30 은 30 으로나누어떨어질경우에만 0 을반환합니다. for (int i = 0; i < 360; i += 3) Rectangle rectangle = new Rectangle(); rectangle.width = (i % 30 == 0)? 3 : 1; rectangle.height = 15; rectangle.fill = new SolidColorBrush(Colors.Black); rectangle.rendertransformorigin = new Point(0.5, 0.5); 스톱워치에눈금을추가했으니, 심판이조금더공정한신호를보낼수있습니다. TransformGroup transforms = new TransformGroup(); transforms.children.add(new TranslateTransform() Y = -140 ); transforms.children.add(new RotateTransform() Angle = i ); rectangle.rendertransform = transforms; basegrid.children.add(rectangle); //... 버튼이벤트핸들러는똑같음 Grid, StackPanel, Canvas 와같은컨트롤은다른모든컨트롤의참조를담을수있는 Children 컬렉션이있습니다. Add() 메서드로그리드에컨트롤을담을수있고, Clear() 메서드를호출하여모든컨트롤을지울수있습니다. TransformGroup 도같은방식으로변형을추가합니다. 시침과분침의 XAML 코드로돌아가보세요. 이코드는 Angle 속성을바인딩하는대신, 값을설정하는것은제외하고정확히같은변형을설정합니다. 11 장에서도마찬가지로 C# 코드에서 Binding 객체를이용하여데이터바인딩을설정했습니다. 시침과분침의 Rectangle 컨트롤을생성하기위해서 XAML 코드를어떻게지우고, 같은일을하기위해서 XAML 코드를 C# 코드로어떻게교체하는지아시겠죠? 어떤팀이컨퍼런스를지배하고객체마을트로피를차지할까요? 아무도모릅니다. 우리가아는것이라고는조, 밥, 에드중하나라는겁니다. 830 Appendix ii 지금여기예요 4 831

다음프로젝트를위해서여기 (http://www.hanbit.co.kr/exam/2165p) 에서벌이미지를내려받습니다. 지미의만화책앱에서했던것처럼, 최상위폴더의프로젝트에이미지를추가해야됩니다. 또한솔루션탐색기에서각이미지파일을선택합니다. 그리고속성창의빌드작업 (Build Action) 에서내용 (Content) 을출력디렉터리로복사 (Copy to Output Directory) 에서항상복사 (Copy always) 를선택합니다. 이작업을지미의만화책앱에서했던것처럼각이미지파일을선택해서해주세요. Bee animation 1.png, Bee animation 2.png, Bee animation 3.png, Bee animation 4.png 파일에적용해주세요. 이미지애니메이션을위한사용자정의컨트롤만들기 모든프레임별로애니메이션코드를캡슐화해봅시다. View 폴더에 AnimatedImage 라는 WPF 사용자정 의컨트롤을추가합니다. 이 XAML 코드는매우작습니다. 모든핵심은코드 - 비하인드에있죠. XAML 의 <UserControl> 안의내용은이게전부입니다. <Grid> <Image x:name="image" Stretch="Fill"/> </Grid> 코드 - 비하인드차례입니다. 오버로드된생성자를살펴보면, StartAnimation() 메서드를호출합니다. 이메서드는 Image 컨트롤 의 Source 속성을애니메이션화하기위해서스토리보드와키프레임애니메이션객체를생성합니다. using System.Windows.Media.Animation; using System.Windows.Media.Imaging; public partial class AnimatedImage : UserControl public AnimatedImage() InitializeComponent(); public AnimatedImage(IEnumerable<string> imagenames, TimeSpan interval) : this() 만약 XAML을이용하여컨트롤의인스턴스를생성한다면, 모든컨트롤은 StartAnimation(imageNames, interval); 반드시매개변수가없는생성자가있어야합니다. 생성자를오버로드할순 있지만, 컨트롤을생성하기위해서코드를쓰는경우에서만유용합니다. public void StartAnimation(IEnumerable<string> imagenames, TimeSpan interval) Storyboard storyboard = new Storyboard(); ObjectAnimationUsingKeyFrames animation = new ObjectAnimationUsingKeyFrames(); Storyboard.SetTarget(animation, image); Storyboard.SetTargetProperty(animation, new PropertyPath(Image.SourceProperty)); TimeSpan currentinterval = TimeSpan.FromMilliseconds(0); foreach (string imagename in imagenames) ObjectKeyFrame keyframe = new DiscreteObjectKeyFrame(); keyframe.value = CreateImageFromAssets(imageName); keyframe.keytime = currentinterval; animation.keyframes.add(keyframe); currentinterval = currentinterval.add(interval); Media.Imaging 네임스페이스에비트맵이미지의 BitmapImage 클래스가있습니다. 스토리보드와다른애니메이션클래스는 Media.Animation 네임스페이스에있습니다. Storyboard 클래스의 static SetTarget() 과 SetTargetProperty() 메서드는애니메이션화된 이미지 (image) 의대상객체와 원본 (Source) 을바꿀속성을설정합니다. storyboard.repeatbehavior = RepeatBehavior.Forever; storyboard.autoreverse = true; storyboard.children.add(animation); storyboard.begin(); private static BitmapImage CreateImageFromAssets(string imagefilename) try Uri uri = new Uri(imageFilename, UriKind.RelativeOrAbsolute); return new BitmapImage(uri); catch (System.IO.IOException) return new BitmapImage(); 14 장에서본같은 메서드군요. Storyboard 객체가생성되고애니메이션이 Children 컬렉션에추가되고됐으면, 애니메이션을호출하기위해서 Begin() 메서드를호출합니다. 지금여기예요 4 833

날개를얻은벌 벌이날아다닙니다 시험비행을위해서 AnimatedImage 컨트롤을사용해봅시다. 직접해봅시다! 3 윈도우의코드 - 비하인드를추가합니다. Storyboard 와 DoubleAnimation 을포함하는네임스페이스의 using 문을추가합니다. 1 2 View 폴더에서 MainPage.xaml 을삭제하고새기본페이지로교체합니다. View 폴더에 FlyingBees.xaml 이라는윈도우를추가합니다. 프로젝트의 MainWindow.xaml 파일은삭제해주세요. 그 런다음 App.xaml 의 <Application> 태그에서 StartupUri 속성을수정합니다. StartupUri="View\FlyingBees.xaml" 벌은 Canvas 컨트롤주위를맴돕니다. 아래에윈도우에대한코드가있습니다 ( 다른프로젝트이름을사용했다면, AnimatedBee 네임스페이스를바꿔주세 요 ). 이코드에서 FlyingBees.xaml 에서 Canvas 컨트롤을사용합니다. Canvas 컨트롤은컨테이너기때문에 Grid 혹 은 StackPanel 과같은다른컨트롤을담을수있습니다. Canvas 의특징은 Canvas.Left 와 Canvas.Top 속성을이용하 여컨트롤의좌표를설정합니다. 1 장의 Save the Humans 에서플레이영역을만들기위해서 Canvas 를사용했습니다. FlyingBees.xaml 에서도 Canvas 를추가해봅시다. <Window x:class="animatedbee.view.flyingbees" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:view="clr-namespace:animatedbee.view" Title="Flying Bees" Height="600" Width="600"> <Grid> <Canvas Background="SkyBlue"> <view:animatedimage Canvas.Left="55" Canvas.Top="40" x:name="firstbee" Width="50" Height="50"/> <view:animatedimage Canvas.Left="80" Canvas.Top="260" x:name="secondbee" Width="200" Height="200"/> <view:animatedimage Canvas.Left="230" Canvas.Top="100" </Canvas> </Grid> </Window> AnimatedImage 컨트롤은 CreateFrameImage() 메서드가호출될때까지보이지않습니다. Canvas 의컨트롤의윤곽선만표시하죠. 문서개요를이용하여선택할수있습니다. Canvas.Left 와 Canvas.Top 속성이변하는지보려면컨트롤을캔버스주위로이동해보세요. x:name="thirdbee" Width="300" Height="125"/> 이번장의앞에서본 <Storyboard> 태그와 <DoubleAnimation> 태그를쓰는대신, 코드에서 Storyboard 객체와 DoubleAnimation 객체를생성하고, 속성을설정할수있습니다. using System.Windows.Media.Animation; 벌애니메이션을구동하기위해서 FlyingBees.xaml.cs 의생성자를수정해봅시다. 또한 Canvas.Left 속성을움직이는 DoubleAnimation 객체를생성해봅시다. 그리고이번장앞에서본 XAML 코드에서스토리보드와 <DoubleAnimation> 태그 의애니메이션을생성하는코드를비교해보세요. public FlyingBees() this.initializecomponent(); List<string> imagenames = new List<string>(); imagenames.add("bee animation 1.png"); imagenames.add("bee animation 2.png"); imagenames.add("bee animation 3.png"); imagenames.add("bee animation 4.png"); firstbee.startanimation(imagenames, TimeSpan.FromMilliseconds(50)); secondbee.startanimation(imagenames, TimeSpan.FromMilliseconds(10)); thirdbee.startanimation(imagenames, TimeSpan.FromMilliseconds(100)); Storyboard storyboard = new Storyboard(); DoubleAnimation animation = new DoubleAnimation(); Storyboard.SetTarget(animation, firstbee); Storyboard.SetTargetProperty(animation, new PropertyPath(Canvas.LeftProperty)); animation.from = 50; animation.to = 450; animation.duration = TimeSpan.FromSeconds(3); animation.repeatbehavior = RepeatBehavior.Forever; animation.autoreverse = true; storyboard.children.add(animation); storyboard.begin(); 프로그램을실행합니다. 이제세마리의벌이날개로날아다니는것 을볼수있습니다. 벌들은다른시간간격을갖고있어서, 서로다 른속도로날아다닙니다. 프레임이바뀌기전에서로다른시간을기 다리기때문이죠. 맨위의벌은 Canvas 를가지고있습니다. Left 속 성은 50 에서 450 으로움직이다가다시반대로돌아옵니다. 이속성 은벌들이페이지주변을돌아다니게합니다. DoubleAnimation 객 체에설정된속성을자세히살펴보고, 앞서사용한 XAML 속성과비 교해보세요. CreateFrameImages() 메서드는이미지이름의순서와프레임이변하는속도를설정하는 TimeSpan 객체를취합니다. 애니메이션이끝난후 Storyboard 객체는가비지컬렉션으로갑니다. 이과정을보기위해서조사식창에서개체 ID 만들기를누르고, 애니메이션이끝난후, 버튼을클릭하세요. 뭔가이상한데 뭐가잘못된걸까요? 834 Appendix ii 지금여기예요 4 835

MVVM 패턴 뭔가이상하군요. Model 이나 ViewModel 폴더가없고, View 폴더에는더미데이터만있군요. 이것은 MVVM 이아니에요! 더많은벌을추가하고싶다면, View 폴더에더많은컨트롤을추가한뒤, 개별적으로초기화작업을해줘야합니다. 만약서로다른종류나크기의 벌을만들고싶다거나애니메이션을다르게주고싶다면어떻게해야될까 요? 데이터에최적화된모델이있다면, 이작업이조금더쉬워질겁니다. 이프로젝트에 MVVM 패턴을어떻게적용할까요? MODEL??? 뷰 뷰모델 ITemsPanelTemplate 을이용하여캔버스에컨트롤을바인딩해봅시다 항목들을 ListView, GridView 혹은 ListBox 로바인딩하는 ItemSource 속성을사용했을때, 어떤항목을바인딩하는 지는중요하지않았습니다. 왜냐하면 ItemSource 속성은항상같은방식으로동작했기때문이죠. 만약정확히같은행 동을하는세개의클래스를만들려고한다면, 그행동을베이스클래스에넣고, 세클래스를확장하면될까요? 마이크 로소프트팀은항목을선택하는컨트롤을만들었을때, 정확히같은동작을하도록만들었습니다. ListView, GridView, ListBox 모두는 Selector 클래스를확장합니다. 이클래스는컬렉션의항목을표시하는 ItemsControl 클래스의서브클래 스입니다. 1 2 3 항목의레이아웃을조절하는패널의템플릿을설정하기위해서 ItemPanel 속성을사용합니다. 먼저, FlyingBees.xaml 에서 ViewModel 네임스페이스를추가합니다. xmlns:viewmodel="clr-namespace:animatedbee.viewmodel" 다음, BeeViewModel 이라는빈클래스를 ViewModel 폴더에추가하고, 이클래스의인스턴스를 FlyingBees.xaml 의정적리소스에추가합니다. <viewmodel:beeviewmodel x:key="viewmodel"/> FlyingBees.xaml.cs 를편집해봅시다. FlyingBees 컨트롤의 FlyingBees() 생성자에서추가했던 코드만지웁니다. 원래있던코드를지우면안돼요. ItemControl 에대한 XAML 코드입니다. FlyingBees.xaml 파일을열어서여러분이추가한 <Canvas> 태그를지우고, ItemControl 로바꿔줍니다. 다른프로젝트이름을사용했다면, AnimatedBee 네임스페이스를바꿔주세요. 정적리소스의뷰모델을데이터컨텍스트로사용합니다. 그리고 ItemSource 를 Sprites 속성에바인딩합니다. 누워서떡먹기죠. 컨트롤의 ObservableCollection 컬렉션을추가하고, Canvas 객체의 Children 속성을컬렉션에바인딩합니다. 어려울게뭐가있죠? 그렇게하면안됩니다. 데이터바인딩은컨테이너컨트롤의 Children 속성과동작할수없습니다. 데이터바인딩은 XAML 코드에있는속성들과연결되어동작하도록만들어 졌습니다. Canvas 객체는 public Children 속성을가지지만, XAML 코드 (Children="Binding...") 으로바인딩을설정한다면, 컴파일이되지않습니다. 하지만여러분은 ItemsSource 속성을이용하여 ListView 와 GridView 컨트 롤을바인딩하면서, 어떻게객체의컬렉션을 XAML 컨트롤에바인딩하는지 알고있습니다. Canvas 객체에자식 ( 하위 ) 컨트롤을추가하기위해서데이터 바인딩을활용할수있습니다. 패널을이렇게설정할수있지만, 여러분이원하는대로해도상관없습니다. Canvas 는하늘색배경이군요. <ItemsControl DataContext="StaticResource viewmodel" ItemsSource="Binding Path=Sprites" > <ItemsControl.ItemsPanel> <ItemsPanelTemplate> <Canvas Background="SkyBlue" /> </ItemsPanelTemplate> </ItemsControl.ItemsPanel> </ItemsControl> ItemControl 이생성될때, 컨트롤의모든항목이있는 Panel 을만들고, ItemsPanelTemplate 컨트롤템플릿을사용합니다. ItemsPanelTemplate 을설정하기위해서 ItemPanel 속성을사용합니다. ItemsPanelTemplate 은단일의 Panel 컨트롤과 Panel 클래스를확장한 Grid 와 Canvas 를포함하고있습니다. ItemsSource 에바인딩되어있는어떤항목이든지 Panel 의 Children 에추가될수있습니다. 836 Appendix ii 지금여기예요 4 837

꼭알아야할기본내용 4 View 폴더에새 BeeHelper 클래스를생성합니다. 뷰모델 의벌관리를돕기위해서, static 메서드만가진 static 클 래스입니다. 이펙토리메서드는 Bee 컨트롤을생성합니다. 모든게사용자인터페이스에관한내용이라서뷰에속합니다. using System.Windows; using System.Windows.Controls; using System.Windows.Media.Animation; static class BeeHelper public static AnimatedImage BeeFactory( double width, double height, TimeSpan flapinterval) 838 Appendix ii List<string> imagenames = new List<string>(); imagenames.add("bee animation 1.png"); imagenames.add("bee animation 2.png"); imagenames.add("bee animation 3.png"); imagenames.add("bee animation 4.png"); AnimatedImage bee = new AnimatedImage(imageNames, flapinterval); bee.width = width; 재사용할일이많은작은블록의코드를메서드 ( 혹은 static 메서드 ) 에 bee.height = height; 넣을때, 때때로이것을헬퍼메서드 (Helper method) 라고부릅니다. return bee; 이름끝이 Helper 인 static 클래스안에헬퍼메서드를놓는다면, 가독성이조금더좋아집니다. public static void SetBeeLocation(AnimatedImage bee, double x, double y) Canvas.SetLeft(bee, x); Canvas.SetTop(bee, y); public static void MakeBeeMove(AnimatedImage bee, double fromx, double tox, double y) Canvas.SetTop(bee, y); Storyboard storyboard = new Storyboard(); DoubleAnimation animation = new DoubleAnimation(); Storyboard.SetTarget(animation, bee); Storyboard.SetTargetProperty(animation, new PropertyPath(Canvas.LeftProperty)); animation.from = fromx; animation.to = tox; animation.duration = TimeSpan.FromSeconds(3); animation.repeatbehavior = RepeatBehavior.Forever; animation.autoreverse = true; storyboard.children.add(animation); storyboard.begin(); 팩토리메서드패턴 MVVM 은많은디자인패턴중하나입니다. 가장평범하고유용한패턴중하나는팩토리메서드패턴 (Factory method pattern) 입니다. 객체를생성하는 펙토리 ( 공장 ) 메서드가있죠. 팩토리메서드는대부분 static 이고, 메서드이름끝에 Factory 가붙습니다. 그래야무슨일을하는지잘알수있으니까요. 윈도우생성자에있는것과같은코드입니다. static 헬퍼메서드안에있군요. 마지막실습에유용한정보입니다. 스프라이트 (Sprite) 는 2D 이미지나게임혹은애니메이션에서사용하는용어입니다. 6 5 using View; using System.Collections.ObjectModel; using System.Collections.Specialized; 모든 XAML 컨트롤은 System.Windows 네임스페이스의 UIElement 베이스클래스로부터상속받습니다. ViewModel 에서 UI 와관련된코드를위해서 using 문을쓰는대신클래스의몸체에 System.Windows. UIElement 네임스페이스를명시적으로사용합니다. 대부분의클래스가스프라이트를확장하는 abstract 클래스인 UIElement 클래스를사용합니다. 어떤프로젝트들은 FrameworkElement 와같은서브클래스가좀더적합할수도있습니다. 왜냐하면 Width, Height, Opacity, HorizontalAlignment 등과같은많은속성들이정의되어있기때문이죠. ViewModel 폴더에추가한빈 BeeViewModel 클래스입니다. UI 와 관련된코드를뷰에이동함으로써, 뷰모델에있는코드가간단해지 고, 벌과관련된로직에관해서만관리할수있습니다. class BeeViewModel private readonly ObservableCollection<Windows.UI.Xaml.UIElement> _sprites = new ObservableCollection<Windows.UI.Xaml.UIElement>(); public INotifyCollectionChanged Sprites get return _sprites; public BeeViewModel() AnimatedImage firstbee = BeeHelper.BeeFactory(50, 50, TimeSpan.FromMilliseconds(50)); _sprites.add(firstbee); AnimatedImage secondbee = BeeHelper.BeeFactory(200, 200, TimeSpan.FromMilliseconds(10)); _sprites.add(secondbee); AnimatedImage thirdbee = BeeHelper.BeeFactory(300, 125, TimeSpan.FromMilliseconds(100)); _sprites.add(thirdbee); BeeHelper.MakeBeeMove(firstBee, 50, 450, 40); BeeHelper.SetBeeLocation(secondBee, 80, 260); BeeHelper.SetBeeLocation(thirdBee, 230, 100); 앱을실행해봅시다. 이전과똑같이실행되어야 하지만, 이제각계층에서일을분배합니다. UI 와관련된코드는뷰에서, 벌이이동하는코드 는뷰모델에있습니다. readonly 키워드 ItemsControl 의 ItemSource 속성에바인딩되어있는 _sprites 필드에 AnimatedImage 컨트롤을추가했을때, 컨트롤에서 ITemsPanelTemplate 을바탕으로생성된항목패널을추가합니다. Sprites 속성을캡슐화하기위해서두가지의과정이필요합니다. 나중에덮어쓰이지않기위해서, 백킹필드를 readonly 로선언합니다. 그리고다른클래스에서는볼수있으나, 수정하지못하도록 INotifyCollectionChanged 속성으로노출합니다. ObservableCollection 에컨트롤을추가한뒤, 속성을변경하고애니메이션을추가하고있습니다. 왜작동할까요? 캡슐화를하는가장중요한이유는실수로다른클래스의데이터를덮어쓰는것을방지하는겁니다. 원래데이터에뭔가덮어쓰는것을막을순없을까요? 필드에 readonly 키워드가있다면, 선언이나생성자에서만쓸수있습니다. 지금여기예요 4 839

긴연습문제 과정 4 에있는코드는과정 9 의 PlayAreaSize 속성을만들때까지컴파일이되지않습니다. 속성스텁을생성하여컴파일되게할수있습니다. 1 2 3 여러분이만들어야할앱입니다. 이책의마지막연습문제입니다. 별과벌을움직이는프로그램을만들어봅시다. 여기에는엄청나게많은 코드가있습니다. 하지만여러분이어떻게작업하느냐에따라코드의양이다를수있습니다. 그리고이긴 연습문제를끝낸다면, 비디오게임을만드는데필요한도구를얻게됩니다 ( 실습 3 에서무엇을할까요?). 어두운파란색배경의캔버스주위에벌들이날아다닙니다. 그뒤에별들이나타났다가사라집니다. 이것을보여주기위해서 벌과별, 페이지를담는뷰를만듭니다. 그리고벌이움직이고별이반짝일때, 상태를파악하고이벤트를발생시키는데필요 한모델과뷰와모델을연결하는뷰모델을만들어봅시다. 별은나타났다사라집니다. 벌은하늘주변을아무데나 ( 랜덤하게 ) 날아다닙니다. 만약캔버스크기가변한다면, 벌은캔버스의새로운위치에서납니다. 만약캔버스실행영역의크기가변한다면, 별은즉시이동하고, 벌은천천히새로운위치에서날게됩니다. 프로그램을실행하고디버깅을할때창크기를조정하면서테스트할수있습니다. 별은빠르게움직입니다! 새로운윈도우스토어앱프로젝트를생성합니다. 새프로젝트의이름을 ( 별이빛나는 ) 총총한밤을뜻하는 StarryNight 으로합니다. 다음, Model 과 View, ViewModel 폴더를 추가합니다. 그리고 ViewModel 폴더에빈 BeeStarViewModel 클래스를추가합니다. View 폴더에기본페이지를생성합니다. MainWindow.xaml 을지워주세요. 그러고나서 View 폴더에새윈도우인 BeesOnAStarryNight.xaml 파일을추가합니다. BeesOnAStarryNight.xaml 의최상위레벨에있는태그에서네임스페이스를추가합니다 ( 프로젝트이름인 StarryNight 과 같아야합니다 ). xmlns:viewmodel="clr-namespace:starrynight.viewmodel" 정적리소스에 ViewModel 을추가하고, 윈도우이름을바꿉니다. <Window.Resources> <viewmodel:beestarviewmodel x:key="viewmodel"/> </Window.Resources> Canvas 컨트롤의배경이 Blue 인것과 SizeChanged 이벤트핸들러를제외하고, 전프로젝트의 FlyingBees.xaml 과똑같은윈도우에대한 XAML 입니다. <Canvas Background="Blue" SizeChanged="SizeChangedHandler" /> 응용프로그램을시작할때, 새창을열기위해서 App.xaml 의 <application> 태그를수정합니다. StartupUri="View\BeesOnAStarryNight.xaml" 컨트롤의크기가변할때, SizeChanged 이벤트가발생합니다. 새로운크기에대한정보는 EventArgs 에있습니다. 윈도우와앱에대한코드 - 비하인드를추가합니다. View 폴더의 BeesOnAStarryNight.xaml.cs 에 SizeChanged 이벤트핸들러를추가합니다. 비주얼스튜디오는도형을그리는데도와주는환상적인도구가있습니다. C# 프로젝트에서복사 / 붙여넣기할수있는 http://msdn.microsoft.com/ko-kr/library/windows/apps/xaml/hh465055.aspx XAML 도형을생성하기위해서 Blend for Visual Studio 2013을사용합니다. 자세한내용은여기에있습니다. 840 Appendix ii http://msdn.microsoft.com/ko-kr/library/vstudio/jj171012(v=vs.120).aspx 지금여기예요 4 841 4 5 6 ViewModel.BeeStarViewModel viewmodel; public BeesOnAStarryNight() InitializeComponent(); viewmodel = FindResource("viewModel") as ViewModel.BeeStarViewModel; private void SizeChangedHandler(object sender, SizeChangedEventArgs e) viewmodel.playareasize = new Size(e.NewSize.Width, e.newsize.height); View 폴더에 AnimatedImage 라는사용자정의컨트롤을추가합니다. View 폴더로돌아가서 AnimatedImage 사용자정의컨트롤을추가합니다. 이컨트롤은이번장앞에서본컨트롤과같습 니다. 프로젝트에애니메이션프레임을위한이미지파일을추가합니다. 그리고각파일을선택한뒤, 속성창의빌드작업 에서내용을선택하고, 출력디렉터리로복사에서항상복사를선택해주세요. View 폴더에 StarControl 이라는사용자정의컨트롤을추가합니다. 이컨트롤은별을그립니다. 두개의스토리보드에서하나는별이나타나고, 또하나는별이사라집니다. 스토리보드를 작동시키기위해서 FadeIn() 과 FadeOut() 메서드를코드 - 비하인드에추가해주세요. Polygon 컨트롤은다각형을그리기위해서포인트집합을사용합니다. UserControl 은 Ploygon 컨트롤로별을그리고있네요. <UserControl // IDE가생성하는일반적인 XAML 코드는아무런문제가없습니다. // 이사용자정의컨트롤을위해서별도의네임스페이스가필요하지않습니다. > <UserControl.Resources> <Storyboard x:name="fadeinstoryboard"> <DoubleAnimation From="0" To="1" Storyboard.TargetName="starPolygon" Storyboard.TargetProperty="Opacity" Duration="0:0:1.5" /> </Storyboard> <Storyboard x:name="fadeoutstoryboard"> <DoubleAnimation From="1" To="0" Storyboard.TargetName="starPolygon" Storyboard.TargetProperty="Opacity" Duration="0:0:1.5" /> </Storyboard> </UserControl.Resources> <Grid> <Polygon Points="0,75 75,0 100,100 0,25 150,25" Fill="Snow" Stroke="Black" x:name="starpolygon"/> </Grid> </UserControl> 타원, 사각형, 다각형을뛰어넘는셰이프 ( 모양 ) 에대해서조금더자세히알고싶다면, 코드 - 비하인드에서스토리보드를실행시키는 public FadeIn() 과 FadeOut() 메서드를추가합니다. 별이나타났다가사라지게하죠. 뷰 starplygon 은별을그립니다. 다른모양으로그려도상관없습니다.

오나의별이여! 7 View 폴더에 BeeStarHelper 클래스를추가합니다. ( 계속 ) 핼퍼클래스입니다. 몇가지친숙한것도있고, 새로운것도보이네요. View 폴더에 using System.Windows; 클래스를추가합니다. using System.Windows.Controls; using System.Windows.Media.Animation; using System.Windows.Shapes; static class BeeStarHelper public static AnimatedImage BeeFactory(double width, double height, TimeSpan flapinterval) List<string> imagenames = new List<string>(); imagenames.add("bee animation 1.png"); imagenames.add("bee animation 2.png"); imagenames.add("bee animation 3.png"); imagenames.add("bee animation 4.png"); AnimatedImage bee = new AnimatedImage(imageNames, flapinterval); bee.width = width; bee.height = height; Canvas 에는컨트롤의 x축의위치를설정하고얻어오는, SetLeft() 와 GetLeft() return bee; 메서드가있습니다. SetTop() 와 GetTop() 메서드는 y축을맡고있죠. 컨트롤이 Canvas 에추가되고난후에 x, y축을담당하는메서드들이실행됩니다. public static void SetCanvasLocation(UIElement control, double x, double y) Canvas.SetLeft(control, x); Canvas.SetTop(control, y); public static void MoveElementOnCanvas(UIElement uielement, double tox, double toy) double fromx = Canvas.GetLeft(uiElement); double fromy = Canvas.GetTop(uiElement); Storyboard storyboard = new Storyboard(); DoubleAnimation animationx = CreateDoubleAnimation(uiElement, fromx, tox, new PropertyPath(Canvas.LeftProperty)); DoubleAnimation animationy = CreateDoubleAnimation(uiElement, fromy, toy, new PropertyPath(Canvas.TopProperty)); storyboard.children.add(animationx); storyboard.children.add(animationy); storyboard.begin(); public static DoubleAnimation CreateDoubleAnimation(UIElement uielement, double from, double to, PropertyPath propertytoanimate) DoubleAnimation animation = new DoubleAnimation(); Storyboard.SetTarget(animation, uielement); Storyboard.SetTargetProperty(animation, propertytoanimate); animation.from = from; animation.to = to; animation.duration = TimeSpan.FromSeconds(3); return animation; public static void SendToBack(StarControl newstar) Canvas.SetZIndex(newStar, -1000); ZIndex 는컨트롤이패널에놓인순서를의미합니다. 높은 ZIndex 의컨트롤은낮은 ZInxex 의위에그려집니다. 뷰 3 초의 DoubleAnimation 을생성하는 CreateDoubleAnimation() 헬퍼메서드를추가합니다. 이메서드에서 Canvas.Left 와 Canvas.Top 속성을이동시켜서, 현재위치에서새로운위치로이동합니다. Bee, Star, EventArgs 클래스를 Model 폴더에추가합니다. 모델은벌의크기와위치, 별의위치를파악해서이벤트를발생시킵니다. 그래서뷰모델은벌이나별이언제바뀌는지알고있죠. 842 Appendix ii 지금여기예요 4 843 8 using System.Windows; class Bee public Point Location get; set; public Size Size get; set; public Rect Position get return new Rect(Location, Size); public double Width get return Position.Width; public double Height get return Position.Height; public Bee(Point location, Size size) Location = location; Size = size; using System.Windows; class BeeMovedEventArgs : EventArgs public Bee BeeThatMoved get; private set; public double X get; private set; public double Y get; private set; public BeeMovedEventArgs(Bee beethatmoved, double x, double y) BeeThatMoved = beethatmoved; X = x; Y = y; using System.Windows; class StarChangedEventArgs : EventArgs public Star StarThatChanged get; private set; public bool Removed get; private set; 모델은 EventArgs 를사용하는이벤트를발생시켜서, 뷰모델에게언제변하는지알려줍니다. public StarChangedEventArgs(Star starthatchanged, bool removed) StarThatChanged = starthatchanged; Removed = removed; Polygon 컨트롤의 Points 속성은 Point 구조체의컬렉션입니다. Point, Size, Rect 구조체 using System.Windows; class Star public Point Location get; set; 모델 public Star(Point location) Location = location; Star 클래스에서부울값을가진회전속성인 Rotating 을추가해서, 몇개의별이주위를천천히회전하도록만들어보세요. Rect 구조체에는몇개의오버로드된생성자가있습니다. 그리고너비와높이, 크기, 위치 (Point 구조체혹은각 X, Y 의두좌표 ) 를쉽게추출해주는메서드가있습니다. System.Windows 네임스페이스는몇가지매우유용한구조체가있습니다. Point 구조체는 X 와 Y 의 double 속성을이용해서좌표세트를저장합니다. Size 구조체도역시 Width 와 Height 의 double 속성을가집니다. 또한특별한빈 Empty 값도있습니다. Rect 구조체는직사각형의왼쪽상단과오른쪽하단모서리의두좌표를저장합니다. 다른 Rect 구조체와그외의높이와너비, 교차지점을찾는많은유용한메서드가있습니다.

윙윙 ~ using System.Windows; class BeeStarModel public static readonly Size StarSize = new Size(150, 100); private readonly Dictionary<Bee, Point> _bees = new Dictionary<Bee, Point>(); private readonly Dictionary<Star, Point> _stars = new Dictionary<Star, Point>(); private Random _random = new Random(); public BeeStarModel() _playareasize = Size.Empty; public void Update() MoveOneBee(); AddOrRemoveAStar(); private static bool RectsOverlap(Rect r1, Rect r2) r1.intersect(r2); if (r1.width > 0 r1.height > 0) return true; return false; ( 계속 ) 9 BeeStarModel 클래스를 Model 폴더에추가합니다. private 필드와몇가지유용한메서드가있군요. 여러분이 BeeStarModel 클래스를마무리해주세요. 상수구조체값을생성하기위해서 readonly 를사용합니다. 뷰모델은타이머를이용해서 Update() 메서드를주기적으로호출합니다. PlayAreaSize 는속성입니다. public Size PlayAreaSize // CreateBees() 와 CreateStarts() 메서드를호출하는 set 접근자가있는백킹필드를추가해주세요. private void CreateBees() // 만약실행영역이비어있다면 return; 합니다. 만약벌이있다면, 움직이게합니다. // 그렇지않다면, 5 에서 15 사이크기 (50 에서 150 픽셀 ) 의벌을생성하고, // _bees 컬렉션에추가합니다. 그리고 BeeMoved 이벤트를발생시킵니다. private void CreateStars() // 만약실행영역이비어있다면 return; 합니다. 만약별이있다면, // 각별들의위치를새로운위치로설정하고, StarChanged 이벤트를발생시킵니다. // 그렇지않다면 5 에서 10 사이로 CreateAStar() 메서드를호출합니다. private void CreateAStar() // 겹치지않는위치를찾아서, 새로운 Star 객체를 _stars 컬렉션에추가합니다. // 그리고 StarChanged 이벤트를발생시킵니다. private Point FindNonOverlappingPoint(Size size) // 다른벌과별들이겹치지않게하기위해서사각형의왼쪽상단의모서리를찾습니다. // 임의로몇개의 Rect 구조체를만든다음, LINQ 쿼리를이용해서겹치는 // 벌이나별을찾습니다 (RectsOverlap() 메서드를이용하세요 ) private void MoveOneBee(Bee bee = null) // 만약벌이없다면, return; 합니다. 만약 bee 매개변수가 null 이면, 임의의벌을선택합니다. // 그렇지않으면 bee 인자값을이용해서겹치지않는위치를찾아서벌의위치를갱신합니다. // 그리고 _bee 컬렉션을업데이트한다음, OnBeeMoved 이벤트를발생시킵니다. private void AddOrRemoveAStar() // 동전을뒤집는확률로 ( _random.next(2) == 0 ) 으로 CreateAStar() 메서드를이용해서별을생성하거나제거하세요. // 그리고 OnStarChanged 이벤트를발생시킵니다. 항상별이 5 개보다작으면하나를생성하고, // 20 개보다많으면하나를제거하세요. _stars.keys.tolist()[_random.next(_stars.count)] 는임의의별을찾아줍니다. // BeeMoved 와 StarChanged 이벤트와이들을호출하는메서드를추가합니다. // 이벤트는 BeeMovedEventArgs 와 StarChangedEventArgs 클래스를사용합니다. Size.Empty 는값이없는예약된 ( 설정된 ) Size 값입니다. 실행영역의크기가바뀔때, 벌과별을생성하는데서만사용됩니다. 이메서드는두 Rect 구조체가있습니다. 그리고 Rect.Intersect() 메서드를이용해서두구조체가겹칠때 true 를반환합니다. 만약메서드가겹치지않는위치를찾으려고임의로 1000 번을시도했다면, 실행 영역에아마공간이없다고판단하기때문에, 아무위치나반환해줍니다. 모델 10 using View; using Model; using System.Collections.ObjectModel; using System.Collections.Specialized; using System.Windows; using DispatcherTimer = Windows.UI.Xaml.DispatcherTimer; using UIElement = Windows.UI.Xaml.UIElement; class BeeStarViewModel private readonly ObservableCollection<UIElement> _sprites = new ObservableCollection<UIElement>(); public INotifyCollectionChanged Sprites get return _sprites; BeeStarViewModel 클래스를 ViewModel 폴더에추가합니다. 메서드에있는주석을채워주세요. Model 이어떻게동작하고, View 가무엇을하는지자세히살펴 봐야합니다. 그리고유용한헬퍼메서드를잘활용하세요. private readonly Dictionary<Star, StarControl> _stars = new Dictionary<Star, StarControl>(); private readonly List<StarControl> _fadedstars = new List<StarControl>(); private BeeStarModel _model = new BeeStarModel(); private readonly Dictionary<Bee, AnimatedImage> _bees = new Dictionary<Bee, AnimatedImage>(); private DispatcherTimer _timer = new DispatcherTimer(); public Size PlayAreaSize /* get 및 set 접근자는 _model.playareasize 를설정하고반환합니다. */ public BeeStarViewModel() // BeeStarModel 의 BeeMove 와 StarChanged 이벤트에이벤트핸들러를연결하고, // 2초마다타이머가틱 (tick) 합니다. void timer_tick(object sender, object e) // 매번타이머가틱할때, _fadedstars 컬렉션에있는모든 StarControl 참조를찾아서 // 각 _sprites 를제거한뒤, 스스로업데이트할수있도록알려주기위해서 // BeeViewModel 의 Update() 메서드를호출합니다. void BeeMovedHandler(object sender, BeeMovedEventArgs e) // _bees 딕셔너리는 Model의 Bee 객체와 View의 AnimatedImage 컨트롤과매핑됩니다. // 벌이움직일때, BeeViewModel 은 BeeMoved 이벤트를발생시켜서 // 벌이움직이는것을듣고있는누군가에게벌의새로운위치를알려줍니다. 만약 _bees 딕셔너리가 // 이미벌의 AnimatedImage 컨트롤을포함하지않는다면, 새것을하나만들어서 // 마캔버스위치를설정하고, _bee와 _sprites 모두업데이트해줍니다. // 만약 _bee 딕셔너리가컨트롤을이미포함하고있다면, 해당 AnimatedImage 컨트롤을찾은다음, // 애니메이션과함께캔버스의새로운위치로이동시켜줍니다. void StarChangedHandler(object sender, StarChangedEventArgs e) // _stars 딕셔너리는 _bee 딕셔너리와같은방식으로동작합니다. Star 객체와 StarControl 컨트롤이 // 매핑되는것을제외하고말이죠. EventArgs 는 (Location 속성을가진 ) Star 객체의참조를포함하고있습니다. // 그리고만약별이제거될때 boolean 값으로알려줍니다. 별이사라지는과정은 _stars로부터별을지우고, // _fadedstars 에별을추가합니다. 그리고 FadeOut() 메서드를호출합니다 (_sprites 에서별을삭제한다음 // Update() 메서드가호출됩니다. 이것은타이머의틱간격설정이 // StarControl 의사라지는애니메이션시간보다커야하는이유입니다. // // 만약별이사라지지않았다면, _starts 에서별을포함하는지확인해봅니다. 사라지지않았다면, // StarControl 참조를얻고, 그렇지않다면별이나타나는새 StarControl 을생성하고, // _sprites 에추가해서다시별을보냅니다. 그래야벌이별앞에서날수있습니다. // 그리고나서 StartControl 의캔버스위치를설정합니다. 뷰모델 DistpatcherTimer 와 UIElement 는 ViewModel 에서사용하는 System.Windows 네임스페이스에서만존재하는클래스입니다. using 키워드는다른네임스페이스로부터한멤버를선언하기위해서 = 을사용할수있습니다. 새로운 Canvas 위치를설정할때, 별이 Canvas 에있더라도컨트롤은업데이트되어있습니다. 이것은실행영역의크기가바뀔때, 별들이스스로그주변을움직이는방법입니다. 844 Appendix ii 지금여기예요 4 845

긴연습문제정답 여기에벌들이있다면, 벌은각각움직입니다. MoveOneBee() 는각벌들이겹치지않게하기위해서새위치를찾고, BeeMove 이벤트를발생시킵니다. using System.Windows; class BeeStarModel public static readonly Size StarSize = new Size(150, 100); private readonly Dictionary<Bee, Point> _bees = new Dictionary<Bee, Point>(); private readonly Dictionary<Star, Point> _stars = new Dictionary<Star, Point>(); private Random _random = new Random(); public BeeStarModel() _playareasize = Size.Empty; public void Update() MoveOneBee(); AddOrRemoveAStar(); private static bool RectsOverlap(Rect r1, Rect r2) r1.intersect(r2); if (r1.width > 0 r1.height > 0) return true; return false; private Size _playareasize; public Size PlayAreaSize get return _playareasize; set _playareasize = value; CreateBees(); CreateStars(); private void CreateBees() if (PlayAreaSize == Size.Empty) return; 메서드가채워진 BeeStarModel 클래스입니다. 이들은문제에서미리주어진거죠. PlayAreaSize 속성이언제변하든지간에, Model 은백킹필드인 _playareasize 를갱신한다음 CreateBees() 와 CreateStarts() 메서드를호출합니다. 이것은 ViewModel 이 Model 에게크기가변할때마다스스로크기를변경하라고알려줍니다. if (_bees.count() > 0) List<Bee> allbees = _bees.keys.tolist(); foreach (Bee bee in allbees) MoveOneBee(bee); else int beecount = _random.next(5, 10); for (int i = 0; i < beecount; i++) int s = _random.next(50, 100); Size beesize = new Size(s, s); Point newlocation = FindNonOverlappingPoint(beeSize); Bee newbee = new Bee(newLocation, beesize); _bees[newbee] = new Point(newLocation.X, newlocation.y); OnBeeMoved(newBee, newlocation.x, newlocation.y); 아직모델에벌이하나도없다면, Bee 객체를생성하고, 위치를설정해줍니다. 언제라도벌이추가되거나바뀔때, BeeMoved 이벤트를발생시켜야합니다. private void CreateStars() if (PlayAreaSize == Size.Empty) return; if (_stars.count > 0) foreach (Star star in _stars.keys) star.location = FindNonOverlappingPoint(StarSize); OnStarChanged(star, false); else int starcount = _random.next(5, 10); for (int i = 0; i < starcount; i++) CreateAStar(); private void CreateAStar() Point newlocation = FindNonOverlappingPoint(StarSize); Star newstar = new Star(newLocation); _stars[newstar] = new Point(newLocation.X, newlocation.y); OnStarChanged(newStar, false); private Point FindNonOverlappingPoint(Size size) Rect newrect = new Rect(); bool nooverlap = false; int count = 0; while (!nooverlap) newrect = new Rect(_random.Next((int)PlayAreaSize.Width - 150), _random.next((int)playareasize.height - 150), size.width, size.height); var overlappingbees = from bee in _bees.keys where RectsOverlap(bee.Position, newrect) select bee; var overlappingstars = from star in _stars.keys where RectsOverlap( 별이있다면, PlayArea 에서별이있던위치에서새로운위치로설정하고, StarChanged 이벤트를발생시킵니다. 해당이벤트를처리하고, 컨트롤을움직이는것은뷰모델의일입니다. 임의의 Rect 구조체를만들어서, 겹쳐져있는지확인합니다. 오른쪽에 250 픽셀의간격과아래쪽에 150 픽셀의간격을줘서별과벌이실행영역을벗어나지않게합니다. LINQ 쿼리는 RectsOverlap() 메서드를호출해서새로운 Rect 와겹치는벌혹은별을찾아줍니다. 만약둘중하나에서반환되는숫자값이있으면, 새로운 Rect 는뭔가와겹쳐져있습니다. new Rect(star.Location.X, star.location.y, StarSize.Width, StarSize.Height), newrect) select star; if ((overlappingbees.count() + overlappingstars.count() == 0) (count++ > 1000)) nooverlap = true; return new Point(newRect.X, newrect.y); private void MoveOneBee(Bee bee = null) if (_bees.keys.count() == 0) return; if (bee == null) int beecount = _stars.count; List<Bee> bees = _bees.keys.tolist(); bee = bees[_random.next(bees.count)]; bee.location = FindNonOverlappingPoint(bee.Size); _bees[bee] = bee.location; OnBeeMoved(bee, bee.location.x, bee.location.y); 1000 번동안반복됐다면, 아마도 실행영역에겹치지않는곳이없을겁니다. 그래서무한대의반복문을멈춰야하죠. 846 Appendix ii 지금여기예요 4 847

긴연습문제정답 private void AddOrRemoveAStar() if (((_random.next(2) == 0) (_stars.count <= 5)) && (_stars.count < 20 )) CreateAStar(); else Star startoremove = _stars.keys.tolist()[_random.next(_stars.count)]; _stars.remove(startoremove); OnStarChanged(starToRemove, true); BeeStarModel 클래스의마지막멤버들입니다. public event EventHandler<BeeMovedEventArgs> BeeMoved; private void OnBeeMoved(Bee beethatmoved, double x, double y) EventHandler<BeeMovedEventArgs> beemoved = BeeMoved; if (beemoved!= null) beemoved(this, new BeeMovedEventArgs(beeThatMoved, x, y)); public event EventHandler<StarChangedEventArgs> StarChanged; private void OnStarChanged(Star starthatchanged, bool removed) EventHandler<StarChangedEventArgs> starchanged = StarChanged; if (starchanged!= null) starchanged(this, new StarChangedEventArgs(starThatChanged, removed)); 메서드가채워진 BeeStarViewModel 클래스입니다. using View; using Model; using System.Collections.ObjectModel; using System.Collections.Specialized; using System.Windows; using DispatcherTimer = System.Windows.Threading.DispatcherTimer; using UIElement = System.Windows.UIElement; class BeeStarViewModel private readonly ObservableCollection<UIElement> _sprites = new ObservableCollection<UIElement>(); public INotifyCollectionChanged Sprites get return _sprites; private readonly Dictionary<Star, StarControl> _stars = new Dictionary<Star, StarControl>(); private readonly List<StarControl> _fadedstars = new List<StarControl>(); private BeeStarModel _model = new BeeStarModel(); private readonly Dictionary<Bee, AnimatedImage> _bees = new Dictionary<Bee, AnimatedImage>(); private DispatcherTimer _timer = new DispatcherTimer(); 동전을던져서 0 이나 1 을무작위로선택하는것처럼되어있지만, 만약별이 5 개보다적으면별을추가하고, 20 개보다많다면별을제거합니다. Update() 메서드가호출될때마다, 별을추가하거나삭제해야됩니다. CreateAStar() 메서드는별들을생성합니다. 만약하나의별이삭제되면, _starts 에서그별을제거하고, StarCahgned 이벤트를발생시켜줍니다. 평범한이벤트핸들러와이벤트를발생시키는메서드입니다. 문제에서주어진코드군요. public Size PlayAreaSize get return _model.playareasize; set _model.playareasize = value; public BeeStarViewModel() _model.beemoved += BeeMovedHandler; _model.starchanged += StarChangedHandler; _timer.interval = TimeSpan.FromSeconds(2); _timer.tick += timer_tick; _timer.start(); void timer_tick(object sender, object e) foreach (StarControl starcontrol in _fadedstars) _sprites.remove(starcontrol); _model.update(); void BeeMovedHandler(object sender, BeeMovedEventArgs e) if (!_bees.containskey(e.beethatmoved)) AnimatedImage beecontrol = BeeStarHelper.BeeFactory( e.beethatmoved.width, e.beethatmoved.height, TimeSpan.FromMilliseconds(20)); BeeStarHelper.SetCanvasLocation(beeControl, e.x, e.y); _bees[e.beethatmoved] = beecontrol; _sprites.add(beecontrol); else AnimatedImage beecontrol = _bees[e.beethatmoved]; BeeStarHelper.MoveElementOnCanvas(beeControl, e.x, e.y); void StarChangedHandler(object sender, StarChangedEventArgs e) if (e.removed) StarControl starcontrol = _stars[e.starthatchanged]; _stars.remove(e.starthatchanged); _fadedstars.add(starcontrol); starcontrol.fadeout(); else StarControl newstar; if (_stars.containskey(e.starthatchanged)) newstar = _stars[e.starthatchanged]; else newstar = new StarControl(); _stars[e.starthatchanged] = newstar; newstar.fadein(); BeeStarHelper.SendToBack(newStar); _sprites.add(newstar); BeeStarHelper.SetCanvasLocation( _fadedstars 컬렉션은현재사라지고, 제거된컨트롤들을포함하고있습니다. 그다음 ViewModel 의 Update() 메서드가호출되죠. newstar, e.starthatchanged.location.x, e.starthatchanged.location.y); 별이추가될때 FadeIn() 메서드를호출해야합니다. 별이이미거기에있다면, 실행영역의크기가변했기때문에별이새위치로이동합니다. 어느쪽이든 Canvas 의새위치로이동하면됩니다. 848 Appendix ii 지금여기예요 4 849

긴연습문제정답 using System.Windows.Media.Animation; public partial class StarControl : UserControl public StarControl() InitializeComponent(); public void FadeIn() Storyboard fadeinstoryboard = FindResource("fadeInStoryboard") as Storyboard; fadeinstoryboard.begin(); public void FadeOut() Storyboard fadeoutstoryboard = FindResource("fadeOutStoryboard") as Storyboard; fadeoutstoryboard.begin(); ViewModel 의 PlayAreaSize 속성은 Model 의속성에전달되지만, Model 의 PlayAreaSize 의 set 접근자는 BeeMoved 와 StartChanged 이벤트를발생시키는메서드를호출합니다. 그래서화면의해상도가변경될때일어나는일은 관심사분리가잘되어있을때, 설계는자연스럽게결합도가느슨해지는경향이있습니다. StarControl 의코드 - 비하인드에대한메서드입니다. 실습 3 을위한모든도구를익혔습니다. 인베이더게임을만들어 봅시다! 마지막실습을위해서흥미로운프로젝트를아껴뒀습니다. 이책의마 지막실습에서고전게임인인베이더게임을만듭니다. 이게임은윈도 우스토어앱을위한실습이지만, 이책을다읽고잘이해했다면인베 이더게임의 WPF 버전을만들수있습니다. WPF 에서도마지막실습을 위한필요한도구를지원합니다. 다만, 플레이어가우주선을움직이는 방법이다릅니다. 윈도우스토어앱은터치및마우스입력을처리하는 제스처이벤트가있지만, WPF 에서는이러한이벤트를지원하지않습 니다. 대신 KeyUp 과 KeyDown 이벤트를사용하면됩니다. 그리고 4 장 에서키게임에관련된예제를진행했습니다. 인베이더게임도이와같 은방법으로키를조작합니다. 다음과같습니다. 1) Canvas 는 SizeChanged 이벤트를발생시킵니다. 2) 이이벤트는 ViewModel 의 PlayAreaSize 속성을갱신합니다. 3) 이속성은 Model 의속성을갱신합니다. 4) 이속성은벌과별을업데이트하는메서드를호출합니다. 5) 이메서드는 BeeMoved 와 StarChanged 이벤트를발생시킵니다. 6) 이이벤트는 ViewModel 의이벤트핸들러를작동시킵니다. 7) 이이벤트핸들러는 Sprites 컬렉션을업데이트합니다. 8) 이컬렉션은 Canvas 에있는컨트롤을갱신시킵니다. 이예제는작업을조정하는단일의중앙객체가없기때문에결합도가낮습니다. 각각의객체는다른객체들이어떻게작동하는지에대해명확하게알필요가없기때문에, 소프트웨어구축이매우안정적입니다. 이벤트를발생하고, 처리하고, 메서드를호출하고, 속성을호출하는등의작은일들만알고있으면됩니다. 축하합니다 ( 아직끝난건아니에요 )! 마지막연습문제를다풀었나요? 모든게어떻게동작하는지이해하셨죠? 그렇다면, 축하드립니다. 여러분은전체에서많은부분의 C# 을배웠고, 여러분이예상한것보다는아마더빨리마쳤을겁니 다. 프로그래밍의세계는여러분을기다립니다. 마지막실습을하기전에, 아직해야할몇가지일들이있습니다. 만약여러분이배운것들을복습 한다면, 머릿속에그내용이오래남게될겁니다. 프로그래밍에서마법이란없습니다. 코드를이해할수있고실행되도록만들어졌기때문에, 모든프로그램이동작하는거죠. Save the Humans 를마지막으로다시한번살펴봅시다. 책에서우리가요구한것들을다했다면, 여러분은 Save The Humans 를두 번만들었습니다. 1 장과 10 장을시작하기전에만들었었죠. 심지어두번째 로만들때도마법처럼느껴지는부분이있습니다. 하지만, 프로그래밍에서 마법이란없습니다. 여러분이만든코드를다시한번살펴보면, 얼마나많이 이해하고있는지에대해서깜짝놀라게됩니다. 긍정적으로여러분의두뇌 에배운것들을익히기만하면됩니다. 프로그래머가디자인패턴과객체지향프로그래밍원리를잘사용한다면, 코드가더이해하기쉬워집니다. 잠시휴식을취합시다. 낮잠을자면더좋고요. 여러분의두뇌는많은정보를스폰지처럼빨아들였습니 다. 가끔은새로운정보를흡수하기위해서휴면상태로 들어가서낮잠을자는것도좋은방법입니다. 수많은신 경과학의연구에서뇌의정보흡수가충분한숙면을취한 후현저하게개선되었다는것을보여줍니다. 여러분의 두뇌를충분히휴식시켜주세요. 여러분의친구한테배운것을말해보세요. 인간은사회적동물입니다. 그리고더나은지혜 를사람 ( 사회구성원 ) 들로부터배웠습니다. 그리 고이것도친구들에게 말하는것 도소셜네트 워킹을의미합니다. 그리고친구들에게말하면서 뭔가더배울수있습니다. 친구들한테배운것들 을자랑해보세요. 인간은우리를잊고 있었어. 경비가허술하니까, 지금이공격할때군. 850 Appendix ii 지금여기예요 4 851