login register Sysop! about ME  
qrcode
    최초 작성일 :    2012년 06월 19일
  최종 수정일 :    2012년 06월 19일
  작성자 :    songgun
  편집자 :    songgun (송 원석)
  읽음수 :    35,267

강좌 목록으로 돌아가기

필자의 잡담~

본문에서는 먼저 스캐폴딩(Scaffolding) 기능을 이용해서 새로운 컨트롤러와 뷰들을 추가해봅니다.

이렇게 생성된 코드들을 분석해보면서 강력한 형식의 뷰를 이용한 모델 데이터 접근 방식을 살펴보고, 그 과정에 사용되는 강력한 형식의 모델과 HttpPost/HttpGet 어트리뷰트, 몇 가지 도우미 메서드 등에 관해서 알아봅니다. 그리고, SQL 서버 LocalDB를 사용하는 방법도 간단하게 살펴봅니다.

마지막으로, 장르와 제목으로 영화 정보를 검색할 수 있는 기능을 추가해보면서, 실제 응용 프로그램 구축 작업시 고려해야 할 점들을 몇 가지 짚어봅니다.

본문은 ASP.net의 공식 MVC 관련 자습서인 Accessing Your Model's Data from a ControllerExamining the Edit Methods and Edit View를 편역한 글입니다. 마이크로소프트의 공식 번역 문서가 아니며 태오 사이트 MS 컬럼 번역팀에서 번역한 내용입니다. 그렇기에, 일부 오역이나 오타가 존재할 수 있는 점 미리 양해를 드립니다. 원문에 대한 모든 저작권은 마이크로소프트에 있으며, 컬럼 내용과 관련한 질의 문답 역시 원문 사이트에 문의하시는 것을 추천드립니다.

컨트롤러에서 모델 데이터에 접근하기
이번 강좌에서는 새로운 MoviesController 클래스를 생성하고, 뷰 템플릿을 이용해서 가져온 영화 데이터를 브라우저에 출력하는 코드를 작성해보도록 하겠습니다. 그러나, 다음 과정을 진행하기 전에 먼저 응용 프로그램부터 빌드하시기 바랍니다.

다른 컨트롤러를 만들 때처럼, Controllers 폴더를 마우스 오른쪽 버튼으로 클릭하고, Add > Controller를 선택해서 새로운 MoviesController 컨트롤러를 생성합니다. 방금 설명했던 것처럼, 응용 프로그램을 빌드해야만 본문의 지시와 동일한 옵션을 선택할 수 있습니다. 다음은 대화 상자에서 선택해야 할 옵션들입니다:
  • 컨트롤러 이름(Controller name): MoviesController.
  • 템플릿(Template): Controller with read/write actions and views, using Entity Framework.
  • 모델 클래스(Model class): Movie (MvcMovie.Models).
  • 데이터 컨텍스트 클래스(Data context class): MovieDBContext (MvcMovie.Models).
  • 뷰(Views): Razor (CSHTML). (기본값)

출처 : http://i1.asp.net/umbraco-beta-media/37697/WindowsLiveWriter_AccessingyourModelsDatafromaController_F59B_AddScaffoldedMovieController_thumb_2.png


대화 상자의 옵션을 모두 지정한 다음, Add를 클릭하면, Visual Studio Express가 다음의 폴더와 파일들을 생성해줍니다:
  • 프로젝트의 Controllers 폴더에 MoviesController.cs 파일이 생성됩니다.
  • 프로젝트의 Views 폴더에 Movies 폴더가 생성됩니다.
  • 새로운 Views\Movies 폴더에 Create.cshtml, Delete.cshtml, Details.cshtml, Edit.cshtml, Index.cshtml 파일이 생성됩니다.

출처 : http://i1.asp.net/umbraco-beta-media/37705/WindowsLiveWriter_AccessingyourModelsDatafromaController_F59B_NewMovieControllerScreenShot_thumb_2.png


바로 ASP.NET MVC 4가 여러분을 대신해서 자동으로 CRUD(Create, Read, Update, Delete)와 관련된 액션 메서드와 뷰들을 생성해주는 것입니다. (이런 방식으로 자동으로 만들어진 CRUD 액션 메서드와 뷰들을 다른 말로 스캐폴딩(Scaffolding)이라고도 부릅니다.) 결과적으로 별다른 노력 없이, 영화 정보를 생성, 조회, 수정, 삭제할 수 있는 완벽한 기능의 웹 응용 프로그램을 갖게 된 것입니다.

응용 프로그램을 다시 실행한 다음, 브라우저 주소 표시줄의 URL에 /Movies를 추가해서 Movies 컨트롤러로 이동해보십시요. 이 응용 프로그램은 기본 라우팅에 따라 동작하므로 (기본 라우팅은 Global.asax 파일에 정의되어 있습니다), 브라우저가 보낸 http://localhost:xxxxx/Movies에 대한 요청은 Movies 컨트롤러의 Index 액션 메서드로 라우트됩니다. 결국, http://localhost:xxxxx/Movies에 대한 요청과 http://localhost:xxxxx/Movies/Index에 대한 요청은 사실상 동일한 요청인 셈입니다. 현재는 데이터베이스에 저장된 영화 정보가 전혀 없으므로, 결과 페이지에는 다음과 같이 빈 영화 목록만 나타날 것입니다.

출처 : http://i1.asp.net/umbraco-beta-media/37703/WindowsLiveWriter_AccessingyourModelsDatafromaController_F59B_MovieLst1_thumb.png


영화 정보 생성하기
목록 페이지에서 Create New 링크를 클릭한 다음, 적당한 영화 정보를 입력하고 Create 버튼을 클릭합니다.

출처 : http://i1.asp.net/umbraco-beta-media/37696/WhenHarrySally.PNG


그러면, 폼에 입력된 영화 정보가 서버로 제출되고, 이 정보는 다시 데이터베이스에 저장됩니다. 그런 다음, 새로 생성된 영화 정보 목록을 확인할 수 있는 /Movies URL로 다시 재전송됩니다.

출처 : http://i1.asp.net/umbraco-beta-media/37701/WindowsLiveWriter_AccessingyourModelsDatafromaController_F59B_IndexWhenHarryMet_thumb_2.png


테스트를 위해서 영화 정보를 몇 개 정도 더 생성해보십시요. 그리고, Edit, Details, Delete 링크를 사용해서 모든 기능들을 테스트해보시기 바랍니다.


생성된 코드 살펴보기
이제 브라우저를 닫고 Controllers\MoviesController.cs 파일을 열어서, 만들어져 있는 Index 메서드의 코드를 살펴보겠습니다. 이 컨트롤러의 Index 메서드 부분의 코드는 다음과 같습니다.
public class MoviesController : Controller 
{
    private MovieDBContext db = new MovieDBContext(); 

    //
    // GET: /Movies/ 

    public ActionResult Index() 
    {
        return View(db.Movies.ToList());
    }
이 코드의 다음 라인은, 이전 강좌에서 설명했던 영화 데이터베이스 컨텍스트의 인스턴스를 생성합니다. 이 영화 데이터베이스 컨텍스트를 사용해서 영화 정보를 질의하고, 수정하고, 삭제할 수 있습니다.
private MovieDBContext db = new MovieDBContext();
브라우저에서 Movies 컨트롤러를 요청하면 이 Index 메서드가 영화 데이터베이스의 Movies 테이블에 존재하는 모든 항목을 가져온 다음, 그 결과를 Index 뷰에 전달합니다.


강력한 형식 모델과 @model 키워드
이전 강좌에서는 ViewBag 개체를 통해서 컨트롤러에서 뷰 템플릿으로 데이터나 개체를 전달하는 방법을 살펴봤습니다. ViewBag은 동적 개체로, 뷰에 정보를 전달할 때 편리한 후기 바인드(Late-Bound) 기능을 제공해줍니다.

그러나, ASP.NET MVC는 뷰 템플릿에 강력한 형식의 데이터나 개체를 전달할 수 있는 방법도 제공해줍니다. 이런 강력한 형식 기반의 접근방식은, 작성된 코드에 대한 보다 훌륭한 컴파일 시점 검사를 수행할 수 있게 해주고, Visual Studio Express 편집기에서 풍부한 인텔리센스의 이점를 제공해줍니다. Visual Studio Express 스캐폴딩 메커니즘은 MoviesController 클래스와 뷰 템플릿의 메서드 및 뷰를 생성할 때, 바로 이 접근방식을 사용합니다.

그러면 이번에는 Controllers\MoviesController.cs 파일에 만들어진 Details 메서드를 분석해보도록 하겠습니다. 다음 코드는 MoviesController 클래스의 Details 메서드 부분입니다. Movie 모델의 인스턴스가 Details 뷰로 전달되는 것을 확인할 수 있습니다.
public ActionResult Details(int id = 0)
{
    Movie movie = db.Movies.Find(id);
    if (movie == null)
    {
        return HttpNotFound();
    }
    return View(movie);
}
뷰 템플릿 파일의 상단에 @model 구문을 작성하면, 컨트롤러에서 뷰로 전달되기를 바라는 개체의 형식을 지정할 수 있습니다. Visual Studio Express는 MoviesController 클래스를 생성할 때, Details.cshtml 파일의 상단에 자동으로 다음과 같은 @model 구문을 추가해줍니다:
@model MvcMovie.Models.Movie
이렇게 뷰 상단에 @model 지시자를 작성하면, 강력한 형식인 Model 개체를 통해서 컨트롤러로부터 전달된 영화 정보에 접근할 수 있습니다. 가령, Details.cshtml 뷰 템플릿의 코드에서는 강력한 형식의 Model 개체를 이용해서, 각각의 영화 정보 필드를 DisplayNameFor 도우미 메서드와 DisplayFor 도우미 메서드에 전달합니다. 뿐만 아니라, Create 메서드와 Edit 메서드, 그리고 관련 뷰 템플릿들도 영화 모델 개체를 전달하고 전달받습니다.

계속해서 Index.cshtml 뷰 템플릿과 MoviesController.cs 파일의 Index 메서드를 다시 살펴보도록 하겠습니다. Index 액션 메서드에서 View 도우미 메서드를 호출할 때, List 개체를 생성하는 방식을 주의 깊게 살펴보시기 바랍니다. 생성된 Movies 목록은 컨트롤러에서 뷰로 전달됩니다:
public ActionResult Index()
{
    return View(db.Movies.ToList());
}
Visual Studio Express는 MoviesController 클래스를 생성할 때, Index.cshtml 파일의 상단에 자동으로 다음과 같은 @model 구문을 추가해줍니다:
@model IEnumerable<MvcMovie.Models.Movie>
이렇게 뷰 상단에 @model 지시자를 작성하면, 강력한 형식인 Model 개체를 통해서 컨트롤러로부터 전달된 영화 정보 목록에 접근할 수 있습니다. 가령, Index.cshtml 템플릿의 코드에서는 foreach 구문을 이용해서 강력한 형식인 Model 개체의 영화 정보 목록에 대해 루프문을 수행합니다:
@foreach (var item in Model) {
    <tr>
        <td>
            @Html.DisplayFor(modelItem => item.Title)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.ReleaseDate)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.Genre)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.Price)
        </td>
        <th>
            @Html.DisplayFor(modelItem => item.Rating)
        </th>
        <td>
            @Html.ActionLink("Edit", "Edit", new { id=item.ID }) |
            @Html.ActionLink("Details", "Details", new { id=item.ID }) |
            @Html.ActionLink("Delete", "Delete", new { id=item.ID }) 
        </td>
    </tr>
}
이 코드의 Model 개체는 강력한 형식이므로 (IEnumerable<Movie> 개체), 루프문 내부의 item 개체는 Movie 형식입니다. 결과적으로, 다른 여러 가지 장점들과 함께, 컴파일 시점 검사와 코드 편집기 상에서의 완벽한 인텔리센스를 지원 받을 수 있습니다.

출처 : http://i1.asp.net/umbraco-beta-media/37702/WindowsLiveWriter_AccessingyourModelsDatafromaController_F59B_ModelIntellisene_thumb_2.png


SQL 서버 LocalDB를 사용해서 작업하기
Entity Framework Code First는 제공된 데이터베이스 연결 문자열에 지정된 Movies 데이터베이스가 존재하지 않으면, 자동으로 해당 데이터베이스를 생성해줍니다. App_Data 폴더를 살펴보면 생성된 데이터베이스를 확인할 수 있습니다. 만약, Movies.sdf 파일이 보이지 않는다면, Solution Explorer의 툴바에서 Show All Files 버튼을 클릭한 다음, Refresh 버튼을 클릭하고, App_Data 폴더를 확장해보십시요.


출처 : http://i1.asp.net/umbraco-beta-media/37706/WindowsLiveWriter_AccessingyourModelsDatafromaController_F59B_SDF_in_SolnExp_thumb_2.png


출처 : http://i1.asp.net/umbraco-beta-media/37693/p5_App_Data_MoviesMDF.PNG


그런 다음, Movies.mdf 파일을 마우스로 더블 클릭해서 DATABASE EXPLORER를 엽니다. 그리고, Tables 폴더를 확장해서 데이터베이스에 생성된 테이블들을 확인해봅니다.

출처 : http://i1.asp.net/umbraco-beta-media/37699/WindowsLiveWriter_AccessingyourModelsDatafromaController_F59B_DB_explorer_thumb_2.png


모두 두 개의 테이블이 존재하는 것을 볼 수 있는데, Movie 엔티티 모음이 저장되는 테이블과 EdmMetadata 테이블이 바로 그것입니다. EdmMetadata 테이블은 모델과 데이터베이스 간에 동기화가 필요한지 여부를 파악하기 위한 용도로 Entity Framework가 사용하는 테이블입니다.

두 테이블 중에서, Movies 테이블을 마우스 오른쪽 버튼으로 클릭한 다음, Show Table Data를 선택하면 여러분이 생성한 데이터를 확인할 수 있습니다.

출처 : http://i1.asp.net/umbraco-beta-media/37694/p5_showTableData.PNG


그리고, 다시 Movies 테이블을 마우스 오른쪽 버튼으로 클릭한 다음, Open Table Definition을 선택하면 Entity Framework Code First가 자동으로 생성해준 테이블 구조를 살펴볼 수 있습니다.

출처 : http://i1.asp.net/umbraco-beta-media/37704/WindowsLiveWriter_AccessingyourModelsDatafromaController_F59B_MoviesTable_thumb_2.png


출처 : http://i1.asp.net/umbraco-beta-media/37707/WindowsLiveWriter_AccessingyourModelsDatafromaController_F59B_TableSchemaSM_4278d6c7-0e1f-4f76-813d-3bb33f36e74c.png


Movies 테이블의 스키마와 여러분이 이전 강좌에서 작성한 Movie 클래스가 서로 어떻게 맵핑되는지를 주의 깊게 살펴보시기 바랍니다. Entity Framework Code First는 Movie 클래스를 기반으로 이 스키마를 자동으로 생성해줍니다.

작업을 모두 마쳤으면, Movies.mdf 파일을 마우스 오른쪽 버튼으로 클릭한 다음, Close Connection을 클릭하여 연결을 닫습니다. (연결을 닫지 않으면, 다음 번에 프로젝트를 실행할 때 오류가 발생할 수도 있습니다.)

출처 : http://i1.asp.net/umbraco-beta-media/37698/WindowsLiveWriter_AccessingyourModelsDatafromaController_F59B_CloseConnection_thumb_2.png


이제 데이터베이스와 그 데이터베이스로부터 가져온 데이터를 출력해주는 간단한 목록 페이지가 만들어졌습니다. 그러면 계속해서 나머지 스캐폴딩 코드들을 살펴보고, 데이터베이스에서 영화를 검색할 수 있는 기능을 제공해주는 SearchIndex 메서드와 SearchIndex 뷰를 작성해보도록 하겠습니다.


Edit 메서드 및 Edit 뷰 살펴보기
이번에는 MoviesController 클래스에 생성된 나머지 액션 메서드와 뷰들을 살펴보고, 사용자 정의 검색 페이지도 추가해보도록 하겠습니다.

다시 응용 프로그램을 실행시킨 다음, 브라우저 주소 표시줄의 URL에 /Movies를 추가해서 Movies 컨트롤러로 이동합니다. 그리고, 마우스 포인터를 Edit 링크 위에 올려 놓은 상태 그대로, 링크가 가리키는 URL을 살펴보십시요.

출처 : http://i1.asp.net/umbraco-beta-media/37712/Windows-Live-Writer_Examining-the-Edit-Methods-and-Edit-View_E344_EditLink_sm_10958d0e-da03-46f0-9656-5937da4aca7c.png


Edit 링크는 Html.ActionLink 메서드를 통해서 Views\Movies\Index.cshtml 뷰에 만들어진 링크입니다:
@Html.ActionLink("Edit", "Edit", new { id=item.ID })

출처 : http://i1.asp.net/umbraco-beta-media/37714/Windows-Live-Writer_Examining-the-Edit-Methods-and-Edit-View_E344_Html.ActionLink_thumb.png


여기에서 사용된 Html 개체는 System.Web.Mvc.WebViewPage 기본 클래스의 속성을 통해서 노출된 도우미(Helper)입니다. 도우미에서 제공되는 ActionLink 메서드를 사용하면 손쉽게 컨트롤러의 액션 메서드를 링크하는 HTML 하이퍼링크를 동적으로 생성할 수 있습니다. ActionLink 메서드의 첫 번째 매개변수에는 렌더 될 링크의 텍스트(<a>Edit Me</a>)를 지정합니다. 두 번째 매개변수에는 호출될 액션 메서드의 이름을 지정합니다. 그리고, 마지막 매개변수에는 라우트 데이터로 사용될 (이 예제의 경우, ID 값인 4) 익명 개체를 지정합니다.

이번 절의 첫 번째 그림에 나타난 링크의 URL은 http://localhost:xxxxx/Movies/Edit/4 인 것을 확인할 수 있습니다. Global.asax.cs 파일에 지정되어 있는 기본 라우트는 {controller}/{action}/{id} 구조의 URL 패턴을 따르므로, ASP.NET은 http://localhost:xxxxx/Movies/Edit/4 에 대한 요청을 Movies 컨트롤러의 Edit 액션 메서드에 대해 ID 매개변수 값이 4인 요청으로 변환해줍니다.

쿼리스트링을 통해서도 액션 메서드 매개변수를 전달할 수 있습니다. 즉, http://localhost:xxxxx/Movies/Edit?ID=4 형태의 URL도 Movies 컨트롤러의 Edit 액션 메서드에 ID 매개변수 값 4를 전달합니다.

출처 : http://i1.asp.net/umbraco-beta-media/37713/Windows-Live-Writer_Examining-the-Edit-Methods-and-Edit-View_E344_EditQueryString_thumb.png


계속해서 다시 Movies 컨트롤러를 열어 보십시요. 그러면, 다음과 같은 두 개의 Edit 액션 메서드를 확인할 수 있을 것입니다.

//
// GET: /Movies/Edit/5

public ActionResult Edit(int id = 0)
{
    Movie movie = db.Movies.Find(id);
    if (movie == null)
    {
        return HttpNotFound();
    }
    return View(movie);
}

//
// POST: /Movies/Edit/5

[HttpPost]
public ActionResult Edit(Movie movie)
{
    if (ModelState.IsValid)
    {
        db.Entry(movie).State = EntityState.Modified;
        db.SaveChanges();
        return RedirectToAction("Index");
    }
    return View(movie);
}
두 번째 Edit 액션 메서드 앞에 HttpPost 어트리뷰트가 지정되어 있는 것을 주의해서 살펴보시기 바랍니다. 이 어트리뷰트를 지정하면, 이 Edit 메서드의 오버로드는 오직 POST 요청인 경우에만 호출됩니다. 물론, 명확하게 구분하기 위해서 첫 번째 Edit 액션 메서드에 HttpGet 액션 메서드를 지정해도 무방하겠지만, 이는 기본값이므로 굳이 그럴 필요는 없습니다. (다만, 본문에서는 설명 상의 편의를 위해, 암시적으로 HttpGet 어트리뷰트가 지정되었다고 간주하고 이 첫 번째 액션 메서드를 HttpGet 메서드라고 부르도록 하겠습니다.)

HttpGet Edit 메서드는 영화 정보의 ID를 매개변수로 받아서, Entity Framework의 Find 메서드를 이용해서 영화 정보를 찾은 다음, 검색된 영화 정보를 Edit 뷰로 반환합니다. Edit 메서드는 매개변수 없이 호출되는 경우, ID 매개변수에 기본값인 0 이 지정됩니다. 그리고, 지정된 ID에 해당하는 영화가 존재하지 않는 경우에는, HttpNotFound가 반환됩니다. 스캐폴딩 시스템은 Edit 뷰를 생성할 때, Movie 클래스를 분석해서 각각의 속성들에 대한 <label> 요소와 <input> 요소들을 렌더하는 코드를 생성해줍니다. 다음 예제는 그렇게 생성된 Edit 뷰를 보여주고 있습니다.
@model MvcMovie.Models.Movie

@{
    ViewBag.Title = "Edit";
}

<h2>Edit</h2>

<script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>

@using (Html.BeginForm()) {
    @Html.ValidationSummary(true)
    <fieldset>
        <legend>Movie</legend>

        @Html.HiddenFor(model => model.ID)

        <div class="editor-label">
            @Html.LabelFor(model => model.Title)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.Title)
            @Html.ValidationMessageFor(model => model.Title)
        </div>

        <div class="editor-label">
            @Html.LabelFor(model => model.ReleaseDate)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.ReleaseDate)
            @Html.ValidationMessageFor(model => model.ReleaseDate)
        </div>

        <div class="editor-label">
            @Html.LabelFor(model => model.Genre)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.Genre)
            @Html.ValidationMessageFor(model => model.Genre)
        </div>

        <div class="editor-label">
            @Html.LabelFor(model => model.Price)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.Price)
            @Html.ValidationMessageFor(model => model.Price)
        </div>

        <p>
            <input type="submit" value="Save" />
        </p>
    </fieldset>
}

<div>
    @Html.ActionLink("Back to List", "Index")
</div>
뷰 템플릿 파일의 가장 상단에 작성되어 있는 @model MvcMovie.Models.Movie 구문에 주목하시기 바랍니다. 이 구문은 뷰가 이 뷰 템플릿의 모델 형식으로 Movie 형식을 기대하고 있다는 것을 나타냅니다.

그리고, HTML 마크업을 구성하는 스캐폴딩 코드에 몇 가지 도우미 메서드가 사용되고 있는 것을 볼 수 있습니다. 가령, Html.LabelFor 도우미를 사용해서 필드들의 이름을 ("Title", "ReleaseDate", "Genre", "Price"), 그리고 Html.EditorFor 도우미를 사용해서 HTML <input> 요소들을 렌더합니다. Html.ValidationMessageFor 도우미는 각 속성들에 대한 유효성 검사 메시지를 출력해줍니다.

다시 응용 프로그램을 실행시킨 다음, /Movies URL로 이동하고, Edit 링크를 클릭해서 브라우저에서 페이지의 소스를 살펴봅니다. 그러면, 다음과 비슷한 모습의 폼 요소 HTML을 확인할 수 있을 것입니다.
<form action="/Movies/Edit/4" method="post">
    <fieldset>
        <legend>Movie</legend>

        <input data-val="true" data-val-number="The field ID must be a number." data-val-required="The ID field is required." id="ID" name="ID" type="hidden" value="4" />

        <div class="editor-label">
            <label for="Title">Title</label>
        </div>
        <div class="editor-field">
            <input class="text-box single-line" id="Title" name="Title" type="text" value="Rio Bravo" />
            <span class="field-validation-valid" data-valmsg-for="Title" data-valmsg-replace="true"></span>
        </div>

        <div class="editor-label">
            <label for="ReleaseDate">ReleaseDate</label>
        </div>
        <div class="editor-field">
            <input class="text-box single-line" data-val="true" data-val-date="The field ReleaseDate must be a date." data-val-required="The ReleaseDate field is required." id="ReleaseDate" name="ReleaseDate" type="text" value="4/15/1959 12:00:00 AM" />
            <span class="field-validation-valid" data-valmsg-for="ReleaseDate" data-valmsg-replace="true"></span>
        </div>

        <div class="editor-label">
            <label for="Genre">Genre</label>
        </div>
        <div class="editor-field">
            <input class="text-box single-line" id="Genre" name="Genre" type="text" value="Western" />
            <span class="field-validation-valid" data-valmsg-for="Genre" data-valmsg-replace="true"></span>
        </div>

        <div class="editor-label">
            <label for="Price">Price</label>
        </div>
        <div class="editor-field">
            <input class="text-box single-line" data-val="true" data-val-number="The field Price must be a number." data-val-required="The Price field is required." id="Price" name="Price" type="text" value="2.99" />
            <span class="field-validation-valid" data-valmsg-for="Price" data-valmsg-replace="true"></span>
        </div>

        <p>
            <input type="submit" value="Save" />
        </p>
    </fieldset>
</form>
이 소스의 <input> 요소들은 /Movies/Edit URL로 게시되도록 action 어트리뷰트에 설정된 HTML <form> 요소 내부에 존재합니다. 그리고, 당연한 얘기지만 Edit 버튼이 클릭되면 폼 데이터가 서버로 전송됩니다.


POST 요청 처리하기
다음 목록은 Edit 액션 메서드의 HttpPost 버전을 보여주고 있습니다.
[HttpPost]
public ActionResult Edit(Movie movie)
{
    if (ModelState.IsValid)
    {
        db.Entry(movie).State = EntityState.Modified;
        db.SaveChanges();
        return RedirectToAction("Index");
    }
    return View(movie);
}
ASP.NET MVC 모델 바인더가 전송된 폼의 값들을 받아서 Movie 개체를 생성한 다음, 생성된 개체를 movie 매개변수에 전달해줍니다. 그러면, ModelState.IsValid 메서드를 호출해서 제출된 폼의 데이터가 Movie 개체 수정에 적합한 유효한 데이터인지 검사합니다. 만약 제출된 데이터가 유효하다면, db(MovieDBContext 클래스의 인스턴스)의 Movies 컬랙션에 저장되었다가, MovieDBContextSaveChanges 메서드가 호출되는 시점에 데이터베이스에 저장됩니다. 그런 다음, 다시 사용자를 MoviesController 클래스의 Index 액션 메서드로 재전송하여 방금 변경한 데이터를 비롯한 영화 컬랙션을 보여줍니다.

그러나, 전송된 폼의 데이터가 유효하지 않은 경우에는 Edit 뷰의 폼이 다시 출력됩니다. 그러면, Edit.cshtml 뷰 템플릿에 작성되어 있는 Html.ValidationMessageFor 도우미가 적절한 오류 메시지를 출력해줍니다.

출처 : http://i1.asp.net/umbraco-beta-media/37709/Windows-Live-Writer_Examining-the-Edit-Methods-and-Edit-View_E344_abcNotValid_thumb.png


로케일 관련 참고사항   만약, 여러분이 영어 문화권 이외의 문화권에서 주로 작업한다면, Supporting ASP.NET MVC Validation with Non-English Locales를 참고하시기 바랍니다. 가령, "Price" 필드에 소숫점 대신, 콤마를 사용해야 할 수도 있습니다. 임시로 이 문제를 피해가려면, 프로젝트 루트의 web.config 파일에 globalization 요소를 추가하면 됩니다. 다음 코드는 문화권(Culture)을 미국-영어로 설정한 globalization 요소를 보여줍니다.
<system.web>
  <globalization culture ="en-US" />
  <!--명확성을 위해 다른 요소들은 제거했습니다.-->
</system.web>
다른 HttpGet 메서드들도 모두 HttpPost 버전의 Edit 액션 메서드와 비슷한 패턴을 갖고 있습니다. 다시 말해서, Movie 개체를 가져온 다음 (Index 메서드의 경우는 개체들의 목록을), 뷰로 이 모델을 전달하는 식입니다. 다만, Create 메서드의 경우는 빈 Movie 개체를 Create 뷰로 전달합니다. 그리고, 데이터를 생성하거나, 수정하거나, 삭제하는 등의 모든 데이터 변경 작업은 각 메서드의 HttpPost 버전에서 수행됩니다. HTTP GET 메서드에서 데이터를 변경하면 ASP.NET MVC Tip #46 ? Don’t use Delete Links because they create Security Holes 블로그 포스트에서 설명하고 있는 것처럼 보안상의 위험 요소가 만들어지게 됩니다. 더불어, HTTP의 모범 사례와, GET 요청은 응용 프로그램의 상태를 변경해서는 안된다는 REST 패턴 아키텍처를 위배하는 일이기도 합니다. 간단하게 정리하자면, GET 작업은 부작용을 발생시키지 않고 영속화 데이터를 변경하지 않는 안전한 작업만을 수행해야 합니다.


Search 메서드 및 Search 뷰 추가하기
그러면 이번에는, 사용자들이 장르와 제목으로 영화를 검색할 수 있도록 SearchIndex 액션 메서드를 추가해보겠습니다. 이 메서드는 /Movies/SearchIndex URL을 통해서 접근할 수 있을 것입니다. 사용자가 이 URL을 요청하면 검색할 영화를 입력할 수 있는 input 요소가 포함된 HTML 폼이 나타나게 됩니다. 이 폼에 검색할 영화 정보를 입력하고 전송하면, 액션 메서드가 사용자가 입력한 검색값을 받아서 이를 이용하여 데이터베이스를 검색하게 됩니다.


SearchIndex 폼 출력하기
먼저 기존의 MoviesController 클래스에 SearchIndex 액션 메서드부터 추가하겠습니다. 이 메서드는 HTML 폼을 담고 있는 뷰를 반환하게 될 것입니다. 다음은 SearchIndex 액션 메서드의 코드입니다:
public ActionResult SearchIndex(string searchString)
{
    var movies = from m in db.Movies
select m;

    if (!String.IsNullOrEmpty(searchString))
    { 
        movies = movies.Where(s => s.Title.Contains(searchString));
    }

    return View(movies);
}
SearchIndex 메서드의 첫 번째 코드 라인은 영화 정보들을 가져오는 다음과 같은 LINQ 질의를 생성합니다:
var movies = from m in db.Movies 
             select m;
그러나, 아직 이 시점에는 질의가 정의만 되었을 뿐, 데이터 저장소에 대해 실제로 수행된 것은 아니라는 점에 주의하십시요.

계속해서 searchString 매개변수에 검색할 문자열이 담겨 있다면, 다음 코드와 같이 검색 문자열 값으로 필터링되도록 movies 질의가 변경됩니다:
if (!String.IsNullOrEmpty(searchString))
{ 
    movies = movies.Where(s => s.Title.Contains(searchString));
}
이 코드에 사용된 s => s.TitleTitle.Contains(searchString) 구문은 람다식(Lambda Expression)입니다. 메서드 기반의 LINQ 질의에서 이 코드에 사용된 Where 메서드 등의 표준 질의 연산자 메서드의 매개변수로 람다식이 사용됩니다. LINQ 질의는 해당 질의가 정의된 시점이나, WhereOrderBy 등의 메서드를 호출해서 변경하는 바로 그 시점에는 질의가 실행되지 않습니다. 그대신, 질의의 실제 값이 루프문 등을 통해서 구체적으로 접근되거나, ToList 등의 메서드가 호출될 때까지 표현식의 평가가 미뤄지고 질의의 실행이 지연됩니다. 가령, SearchIndex 예제의 경우, 질의가 실행되는 곳은 SearchIndex 뷰입니다. 지연된 질의 실행(Deferred Query Execution)에 관한 보다 자세한 정보는 Query Execution을 참고하시기 바랍니다.

이제 폼을 사용자에게 제공하기 위한 SearchIndex 뷰를 작성해보겠습니다. SearchIndex 메서드를 마우스 오른쪽 버튼으로 클릭한 다음, Add View를 클릭합니다. Add View 대화 상자가 나타나면, 뷰 템플릿의 모델 클래스로 Movie 개체의 목록를 전달하고자 한다는 것을 지정합니다. 그리고, Scaffold template 목록에서 List를 선택한 다음, Add를 클릭합니다.

출처 : http://i1.asp.net/umbraco-beta-media/37710/Windows-Live-Writer_Examining-the-Edit-Methods-and-Edit-View_E344_AddSearchView_thumb.png


그러면, Views\Movies\SearchIndex.cshtml 뷰 템플릿이 생성되는데, Scaffold template 목록에서 List를 선택했으므로 자동으로 Visual Studio Express가 기본적인 마크업을 뷰에 생성해줍니다. 이 스캐폴딩은 HTML 폼의 형태로 만들어지게 되며, Movie 클래스를 분석한 결과에 따라 각각의 속성들에 대한 <label> 요소들을 렌더해주는 코드가 만들어집니다. 다음 목록은 생성된 SearchIndex 뷰를 보여주고 있습니다:
@model IEnumerable<MvcMovie.Models.Movie>

@{
    ViewBag.Title = "SearchIndex";
}

<h2>SearchIndex</h2>

<p>
     @Html.ActionLink("Create New", "Create") 
</p>
<table>
    <tr>
        <th>
            Title
        </th>
        <th> 
            ReleaseDate 
        </th>
        <th> 
            Genre
        </th>
        <th> 
            Price
        </th>
        <th></th>
    </tr> 

@foreach (var item in Model) {
    <tr>
        <td>
            @Html.DisplayFor(modelItem => item.Title) 
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.ReleaseDate)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.Genre) 
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.Price)
        </td>
        <td>
            @Html.ActionLink("Edit", "Edit", new { id=item.ID }) |
            @Html.ActionLink("Details", "Details", new { id=item.ID}) |
            @Html.ActionLink("Delete", "Delete", new { id=item.ID })
        </td>
    </tr>
}

</table>
다시 응용 프로그램을 실행시킨 다음, /Movies/SearchIndex로 이동합니다. 그리고, URL의 쿼리스트링에 ?searchString=ghost와 같은 문자열을 추가합니다. 그러면, 필터링 된 목록이 출력될 것입니다.

출처 : http://i1.asp.net/umbraco-beta-media/37719/Windows-Live-Writer_Examining-the-Edit-Methods-and-Edit-View_E344_SearchQryStr_thumb.png


매개변수의 이름을 id로 변경해서 SearchIndex 메서드의 시그니처를 변경하면, Global.asax 파일에 정의된 기본 라우트 설정의 {id} 플레이스홀더와 이 id 매개변수가 일치하게 됩니다.
{controller}/{action}/{id}
본래의 SearchIndex 메서드는 다음과 같았습니다:
public ActionResult SearchIndex(string searchString)
{
    var movies = from m in db.Movies
 select m;

    if (!String.IsNullOrEmpty(searchString))
    {
        movies = movies.Where(s => s.Title.Contains(searchString));
    }

    return View(movies);
}
그리고, 변경된 SearchIndex 메서드는 다음과 비슷할 것입니다:
public ActionResult SearchIndex(string id)
{
    string searchString = id;
    var movies = from m in db.Movies
 select m;

    if (!String.IsNullOrEmpty(searchString))
    {
        movies = movies.Where(s => s.Title.Contains(searchString));
    }

    return View(movies);
}
메서드를 이렇게 변경하고 나면 검색 타이틀을 쿼리스트링 대신 라우트 데이터(URL의 일부)로 전달할 수 있습니다.

출처 : http://i1.asp.net/umbraco-beta-media/37720/Windows-Live-Writer_Examining-the-Edit-Methods-and-Edit-View_E344_SearchRouteData_thumb.png


그렇지만, 그 어떤 개발자도 영화를 검색할 때마다 사용자들이 이런식으로 매번 URL을 수정하기를 바라지는 않을 것입니다. 따라서, 이번에는 사용자들이 영화를 필터링 할 때 편리하게 사용할 수 있도록 UI를 추가해보도록 하겠습니다. 라우트-바운드 ID 매개변수 전달 방식을 살펴보기 위해서 방금 전에 변경했던 SearchIndex 메서드의 시그니처를 다시 원래대로 되돌려서, 본래대로 searchString 문자열 매개변수를 받도록 변경합니다:
public ActionResult SearchIndex(string searchString)
{
    var movies = from m in db.Movies
 select m;

    if (!String.IsNullOrEmpty(searchString))
    {
        movies = movies.Where(s => s.Title.Contains(searchString));
    }

    return View(movies);
}
그리고, Views\Movies\SearchIndex.cshtml 파일을 열어서, @Html.ActionLink("Create New", "Create") 라인 바로 뒤에 다음과 같은 코드를 추가합니다:
@using (Html.BeginForm()){
    <p>Title: @Html.TextBox("SearchString")
    <input type="submit" value="Filter" /></p>
}
다음은 필터링 관련 마크업이 추가된 Views\Movies\SearchIndex.cshtml 파일의 일부를 보여주고 있습니다.
@model IEnumerable<MvcMovie.Models.Movie>

@{
    ViewBag.Title = "SearchIndex";
}

<h2>SearchIndex</h2>

<p>
    @Html.ActionLink("Create New", "Create") 
    
    @using (Html.BeginForm()){
        <p>Title: @Html.TextBox("SearchString")
        <input type="submit" value="Filter" /></p>
    }
</p>
이 코드에서 Html.BeginForm 도우미는 열린 <form> 태그를 생성하고, 사용자가 Filter 버튼을 클릭해서 폼을 제출하면, 폼을 현재 페이지 자신으로 전송되도록 만듭니다.

이제 다시 응용 프로그램을 시작한 다음, 영화 정보를 검색해봅니다.

출처 : http://i1.asp.net/umbraco-beta-media/37720/Windows-Live-Writer_Examining-the-Edit-Methods-and-Edit-View_E344_SearchRouteData_thumb.png


역주   위의 그림은 잘못된 캡춰입니다. 원문의 편집상 실수로 보입니다. 일단, 지금 막 추가한 텍스트 박스와 Filter 버튼이 보이지 않는 점만 감안해봐도 쉽게 짐작하실 수 있으실 것입니다. 그리고, 주소 표시줄의 URL도 본문의 맥락과 맞지 않습니다. 여러분이 한 번 직접 예제를 실행하여 올바른 화면을 확인해보시기 바랍니다.

SearchIndex 메서드의 경우 HttpPost 오버로드는 필요가 없습니다. 그 이유는, 이 메서드는 데이터를 필터링하기만 할 뿐, 어떠한 응용 프로그램의 상태도 변경하지는 않기 때문입니다.

물론, 다음과 같은 HttpPost SearchIndex 메서드를 추가한다고 해도 문제는 되지 않습니다. 그러면, 액션 호출자(Action Invoker)가 HttpPost SearchIndex 메서드를 인식해서 실행한 다음, 아래 그림과 같은 결과를 보여주게 될 것입니다.
[HttpPost]
public string SearchIndex(FormCollection fc, string searchString)
{
    return "<h3> From [HttpPost]SearchIndex: " + searchString + "</h3>";
}

출처 : http://i1.asp.net/umbraco-beta-media/37718/Windows-Live-Writer_Examining-the-Edit-Methods-and-Edit-View_E344_SearchPostGhost_thumb.png


그러나, 비록 이런 SearchIndex 메서드의 HttpPost 버전을 추가해서 실제 코드를 모두 구현했다고 하더라도 여기에는 한계가 존재할 수 밖에 없습니다. 가령, 특정 검색 결과를 북마크 해두거나, 친구에게 링크를 보내서 그 친구가 이 링크를 클릭했을 때, 필터링 된 동일한 영화 목록이 나타나기를 원한다고 상상해보십시요. 그런데, 주목해야 할 점은 HTTP POST 요청의 URL이 GET 요청의 URL(localhost:xxxxx/Movies/SearchIndex)과 동일하고, 이 URL에는 검색에 관한 정보가 전혀 포함되어 있지 않다는 것입니다. 그 이유는 검색 문자열 정보가 서버에 폼 필드 값의 형태로 전달되기 때문입니다. 결과적으로, 여러분은 북마크나 친구에게 보낼 URL에 결코 검색 정보를 담을 수가 없는 셈입니다.

이 문제를 해결할 수 있는 방법은 BeginForm 메서드의 오버로드를 사용해서, URL에 검색 정보가 추가되어 SearchIndex 메서드의 HttpGet 버전으로 라우트되도록 만드는 것입니다. 기존의 매개변수가 지정되지 않은 BeginForm 메서드를 다음 코드와 같이 변경합니다:
@using(Html.BeginForm("SearchIndex", "Movies", FormMethod.Get))

출처 : http://i1.asp.net/umbraco-beta-media/37711/Windows-Live-Writer_Examining-the-Edit-Methods-and-Edit-View_E344_BeginFormPost_SM_thumb.png


이제 검색을 수행해보면 URL에 검색 쿼리스트링이 포함됩니다. 그리고, HttpPost SearchIndex 메서드가 존재하더라도 HttpGet SearchIndex 액션 메서드가 실행됩니다.

출처 : http://i1.asp.net/umbraco-beta-media/37716/Windows-Live-Writer_Examining-the-Edit-Methods-and-Edit-View_E344_SearchIndexWithGetURL_thumb.png



장르 검색 추가하기
먼저, 앞에서 SearchIndex 메서드의 HttpPost 버전을 추가했었다면 제거합니다.

이번에는 사용자들이 장르로 영화 정보를 검색할 수 있는 기능을 추가해 볼 것입니다. SearchIndex 메서드를 다음의 코드로 대체합니다:
public ActionResult SearchIndex(string movieGenre, string searchString)
{
    var GenreLst = new List<string>();

    var GenreQry = from d in db.Movies
   orderby d.Genre
   select d.Genre;
    GenreLst.AddRange(GenreQry.Distinct());
    ViewBag.movieGenre = new SelectList(GenreLst);

    var movies = from m in db.Movies
 select m;

    if (!String.IsNullOrEmpty(searchString))
    {
        movies = movies.Where(s => s.Title.Contains(searchString));
    }

    if (string.IsNullOrEmpty(movieGenre))
        return View(movies);
    else
    {
        return View(movies.Where(x => x.Genre == movieGenre));
    }
}
이 버전의 SearchIndex 메서드는 movieGenre라는 이름의 추가적인 매개변수를 받습니다. 코드의 처음 몇 라인은 데이터베이스로부터 가져온 장르들을 저장할 List 개체를 생성하는 코드입니다.

그리고, 다음 코드는 데이터베이스로부터 모든 장르를 가져오는 LINQ 질의입니다.
var GenreQry = from d in db.Movies
               orderby d.Genre
               select d.Genre;
제네릭 List 컬랙션의 AddRange 메서드를 이용해서 유일한 모든 장르들을 목록에 추가합니다. (Distinct 수정자를 지정하지 않으면 중복된 장르들이 추가될 것입니다. 가령, 이 예제의 경우 코메디 장르가 반복됩니다.) 그리고, ViewBag 개체에 이 장르들의 목록을 저장합니다.

다음 코드는 movieGenre 매개변수를 점검하는 방법을 보여주고 있습니다. 이 매개변수가 비어있지 않으면, 지정된 장르로 검색된 영화 정보를 제한하는 제약조건을 다시 한 번 추가합니다.
if (string.IsNullOrEmpty(movieGenre))
    return View(movies);
else
{
    return View(movies.Where(x => x.Genre == movieGenre));
}

SearchIndex 뷰에 장르 검색 지원을 위한 마크업 추가하기
마지막으로 Views\Movies\SearchIndex.cshtml 파일을 열고, 다음 코드와 같이 TextBox 도우미 라인 직전에 Html.DropDownList 도우미 라인을 추가합니다. 작업이 마무리 된 완전한 마크업은 다음과 같습니다:
<p> 
    @Html.ActionLink("Create New", "Create") 
    @using (Html.BeginForm("SearchIndex","Movies",FormMethod.Get)) {
        <p>Genre: @Html.DropDownList("movieGenre", "All")
        Title: @Html.TextBox("SearchString")
        <input type="submit" value="Filter" /></p>
    }
</p>
응용 프로그램을 실행하고, /Movies/SearchIndex로 이동한 다음, 장르 검색, 영화 제목 검색, 그리고, 두 가지 모두를 이용한 검색을 테스트 해보십시요.

지금까지 프레임워크가 자동으로 생성해준 CRUD 액션 메서드와 뷰들을 살펴봤습니다. 그리고, 사용자들에게 영화 제목과 장르로 검색할 수 있는 기능을 제공해주기 위한 검색 액션 메서드와 뷰를 작성해봤습니다. 다음 단계에서는, Movie 모델에 속성을 추가하는 방법과 자동으로 테스트 데이터베이스를 생성하는 이니셜라이저(Initializer)를 추가하는 방법을 살펴보도록 하겠습니다.


다음 강좌에서 계속 이어집니다...


역자 송원석은 현재 프리랜서 개발자로 활동하고 있습니다. 2006년부터 3년간 마이크로소프트 IIS 부문 MVP를 수상했으며, 개인적으로 지난 6, 7년간 꾸준히 IIS.net의 문서들을 번역하여 소개해오고 있습니다.

프로 ASP.NET MVC 3 프레임워크"와 “프로 ASP.NET MVC 프레임워크”의 공역에 참여했던 경험을 계기 삼아, 본격적으로 ASP.net의 문서 번역 작업을 시작하게 되었습니다.

두 딸아이의 아버지며, 공을 사용하는 모든 운동을 싫어하고, 맥주와 막걸리, 그리고 진도 홍주를 사랑하는 배가 나온 평범한 남자입니다.


authored by

  sequel2
  2012-06-19(13:19)
소중한 강좌 유용하게 잘 보고 있습니다. 다음 강좌도 기대하겠습니다. ^^
  songgun
  2012-06-20(08:52)
캐릭 이미지
감사합니다. ^_^

IIS 7.x/8.0 시리즈도 많이 사랑해주세용~ (*^_^*)

  sisyphus2020
  2013-08-02(15:17)
캐릭 이미지
잘 보고 갑니다. ^^
  kitchu
  2013-10-10(11:17)
캐릭 이미지
강좌 잘 봤습니다! 마지막 검색에서 무심코 Movies로 이동했다가 에러나서 당황했네요


예제처럼 SearchIndex()에만 카테고리 검색해서 ViewBag에 넣어주는 구문 넣어주면
Index()로 이동할때 에러가 나더군요. index.cshtml에서 쓸 아이템목록이 없다고..^^

강좌를 볼땐 코드만 복붙하는게 아니라 코드의 이해와 함께해야한다는 것을 깨달았습니다!


 
 
.NET과 Java 동영상 기반의 교육사이트

로딩 중입니다...

서버 프레임워크 지원 : NeoDEEX
based on ASP.NET 3.5
Creative Commons License
{5}
{2} 읽음   :{3} ({4})