파트 6: Edit 메서드와 Edit 뷰 살펴보기

등록일시: 2012-06-19 11:48,  수정일시: 2015-05-23 20:29
조회수: 5,120
이 문서는 ASP.NET MVC 기술을 널리 알리고자 하는 개인적인 취지로 제공되는 번역문서입니다. 이 문서에 대한 모든 저작권은 마이크로소프트에 있으며 요청이 있을 경우 언제라도 게시가 중단될 수 있습니다. 번역 내용에 오역이 존재할 수 있고 주석은 번역자 개인의 의견일 뿐이며 마이크로소프트는 이에 관한 어떠한 보장도 하지 않습니다. 번역이 완료된 이후에도 대상 제품 및 기술이 개선되거나 변경됨에 따라 원문의 내용도 변경되거나 보완되었을 수 있으므로 주의하시기 바랍니다.

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

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

EditLink_sm

Edit 링크는 Html.ActionLink 메서드를 통해서 Views\Movies\Index.cshtml 뷰에서 만들어진 링크입니다:

@Html.ActionLink("Edit", "Edit", new { id=item.ID })

Html.ActionLink

여기에서 사용된 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를 전달합니다.

EditQueryString

계속해서 다시 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> 요소 내부에 존재합니다. 그리고, 당연한 얘기지만 Save 버튼이 클릭되면 폼 데이터가 서버로 전송됩니다.

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 도우미가 적절한 오류 메시지를 출력해줍니다.

abcNotValid

로케일 관련 참고사항: 만약, 여러분이 영어 문화권 이외의 문화권에서 주로 작업한다면, 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를 클릭합니다.

AddSearchView

그러면, 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와 같은 문자열을 추가합니다. 그러면, 필터링 된 목록이 출력될 것입니다.

SearchQryStr

매개변수의 이름을 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의 일부)로 전달할 수 있습니다.

SearchRouteData

그렇지만, 그 어떤 개발자도 영화를 검색할 때마다 사용자들이 이런식으로 매번 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")<br />
    <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 버튼을 클릭해서 폼을 제출하면, 폼을 현재 페이지 자신으로 전송되도록 만듭니다.

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

역주 위의 그림은 잘못된 캡춰입니다. 원문의 편집상 실수로 보입니다. 일단, 지금 막 추가한 텍스트 박스와 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>";
}

SearchPostGhost

그러나, 비록 이런 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))

BeginFormPost_SM

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

SearchIndexWithGetURL

장르 검색 추가하기

먼저, 앞에서 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)를 추가하는 방법을 살펴보도록 하겠습니다.