Examining the Edit Methods and Edit View (VB)

by Rick Anderson

This tutorial will teach you the basics of building an ASP.NET MVC Web application using Microsoft Visual Web Developer 2010 Express Service Pack 1, which is a free version of Microsoft Visual Studio. Before you start, make sure you've installed the prerequisites listed below. You can install all of them by clicking the following link: Web Platform Installer. Alternatively, you can individually install the prerequisites using the following links:

If you're using Visual Studio 2010 instead of Visual Web Developer 2010, install the prerequisites by clicking the following link: Visual Studio 2010 prerequisites.

A Visual Web Developer project with VB.NET source code is available to accompany this topic. Download the VB.NET version. If you prefer C#, switch to the C# version of this tutorial.

In this section, you'll examine the generated action methods and views for the movie controller. Then you'll add a custom search page.

Run the application and browse to the Movies controller by appending /Movies to the URL in the address bar of your browser. Hold the mouse pointer over an Edit link to see the URL that it links to.

Screenshot shows the MVC Move App with the Edit link for one of the movies selected.

The Edit link was generated by the Html.ActionLink method in the Views\Movies\Index.vbhtml view:

@Html.ActionLink("Edit", "Edit", New With {.id = currentItem.ID}) |

Screenshot shows the Html.ActionLink in a code editor.

The Html object is a helper that's exposed using a property on the WebViewPage base class. The ActionLink method of the helper makes it easy to dynamically generate HTML hyperlinks that link to action methods on controllers. The first argument to the ActionLink method is the link text to render (for example, <a>Edit Me</a>). The second argument is the name of the action method to invoke. The final argument is an anonymous object that generates the route data (in this case, the ID of 4).

The generated link shown in the previous image is http://localhost:xxxxx/Movies/Edit/4. The default route takes the URL pattern {controller}/{action}/{id}. Therefore, ASP.NET translates http://localhost:xxxxx/Movies/Edit/4 into a request to the Edit action method of the Movies controller with the parameter ID equal to 4.

You can also pass action method parameters using a query string. For example, the URL http://localhost:xxxxx/Movies/Edit?ID=4 also passes the parameter ID of 4 to the Edit action method of the Movies controller.

EditQueryString

Open the Movies controller. The two Edit action methods are shown below.

'
' GET: /Movies/Edit/5

Function Edit(id As Integer) As ViewResult
    Dim movie As Movie = db.Movies.Find(id)
    Return View(movie)
End Function

'
' POST: /Movies/Edit/5

<HttpPost()>
Function Edit(movie As Movie) As ActionResult
    If ModelState.IsValid Then
        db.Entry(movie).State = EntityState.Modified
        db.SaveChanges()
        Return RedirectToAction("Index")
    End If

    Return View(movie)
End Function

Notice the second Edit action method is preceded by the HttpPost attribute. This attribute specifies that overload of the Edit method can be invoked only for POST requests. You could apply the HttpGet attribute to the first edit method, but that's not necessary because it's the default. (We'll refer to action methods that are implicitly assigned the HttpGet attribute as HttpGet methods.)

The HttpGet Edit method takes the movie ID parameter, looks up the movie using the Entity Framework Find method, and returns the selected movie to the Edit view. When the scaffolding system created the Edit view, it examined the Movie class and created code to render <label> and <input> elements for each property of the class. The following example shows the Edit view that was generated:

@ModelType MvcMovie.Movie

@Code
    ViewData("Title") = "Edit"
End Code

<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(Function(model) model.ID)

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

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

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

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

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

<div>
    @Html.ActionLink("Back to List", "Index")
</div>

Notice how the view template has a @ModelType MvcMovie.Models.Movie statement at the top of the file — this specifies that the view expects the model for the view template to be of type Movie.

The scaffolded code uses several helper methods to streamline the HTML markup. The Html.LabelFor helper displays the name of the field ("Title", "ReleaseDate", "Genre", or "Price"). The Html.EditorFor helper displays an HTML <input> element. The Html.ValidationMessageFor helper displays any validation messages associated with that property.

Run the application and navigate to the /Movies URL. Click an Edit link. In the browser, view the source for the page. The HTML in the page looks like the following example. (The menu markup was excluded for clarity.)

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>Edit</title>
    <link href="/Content/Site.css" rel="stylesheet" type="text/css" />
    <script src="/Scripts/jquery-1.5.1.min.js" type="text/javascript"></script>
    <script src="/Scripts/modernizr-1.7.min.js" type="text/javascript"></script>
</head>
<body>
    <div class="page">
        <header>
            <div id="title">
                <h1>MVC Movie App</h1>
            </div>
           ...
        </header>
        <section id="main">

<h2>Edit</h2>

<script src="/Scripts/jquery.validate.min.js" type="text/javascript"></script>
<script src="/Scripts/jquery.validate.unobtrusive.min.js" type="text/javascript"></script>

<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-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="9.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>
<div>
    <a href="/Movies">Back to List</a>
</div>

        </section>
        <footer>
        </footer>
    </div>
</body>
</html>

The <input> elements are in an HTML <form> element whose action attribute is set to post to the /Movies/Edit URL. The form data will be posted to the server when the Edit button is clicked.

Processing the POST Request

The following listing shows the HttpPost version of the Edit action method.

'
' POST: /Movies/Edit/5

<HttpPost()>
Function Edit(movie As Movie) As ActionResult
    If ModelState.IsValid Then
        db.Entry(movie).State = EntityState.Modified
        db.SaveChanges()
        Return RedirectToAction("Index")
    End If

    Return View(movie)
End Function

The ASP.NET framework model binder takes the posted form values and creates a Movie object that's passed as the movie parameter. The ModelState.IsValid check in the code verifies that the data submitted in the form can be used to modify a Movie object. If the data is valid, the code saves the movie data to the Movies collection of the MovieDBContext instance. The code then saves the new movie data to the database by calling the SaveChanges method of MovieDBContext, which persists changes to the database. After saving the data, the code redirects the user to the Index action method of the MoviesController class, which causes the updated movie to be displayed in the listing of movies.

If the posted values aren't valid, they are redisplayed in the form. The Html.ValidationMessageFor helpers in the Edit.vbhtml view template take care of displaying appropriate error messages.

abcNotValid

Note about locales If you normally work with a locale other than English, see Supporting ASP.NET MVC 3 Validation with Non-English Locales.

Making the Edit Method More Robust

The HttpGet Edit method generated by the scaffolding system doesn't check that the ID that's passed to it is valid. If a user removes the ID segment from the URL (http://localhost:xxxxx/Movies/Edit), the following error is displayed:

Null_ID

A user could also pass an ID that doesn't exist in the database, such as http://localhost:xxxxx/Movies/Edit/1234. You can make two changes to the HttpGet Edit action method to address this limitation. First, change the ID parameter to have a default value of zero when an ID isn't explicitly passed. You can also check that the Find method actually found a movie before returning the movie object to the view template. The updated Edit method is shown below.

Public Function Edit(Optional ByVal id As Integer = 0) As ActionResult
    Dim movie As Movie = db.Movies.Find(id)
    If movie Is Nothing Then
        Return HttpNotFound()
    End If
    Return View(movie)
End Function

If no movie is found, the HttpNotFound method is called.

All the HttpGet methods follow a similar pattern. They get a movie object (or list of objects, in the case of Index), and pass the model to the view. The Create method passes an empty movie object to the Create view. All the methods that create, edit, delete, or otherwise modify data do so in the HttpPost overload of the method. Modifying data in an HTTP GET method is a security risk, as described in the blog post entry ASP.NET MVC Tip #46 – Don't use Delete Links because they create Security Holes. Modifying data in a GET method also violates HTTP best practices and the architectural REST pattern, which specifies that GET requests should not change the state of your application. In other words, performing a GET operation should be a safe operation that has no side effects.

Adding a Search Method and Search View

In this section you'll add a SearchIndex action method that lets you search movies by genre or name. This will be available using the /Movies/SearchIndex URL. The request will display an HTML form that contains input elements that a user can fill in in order to search for a movie. When a user submits the form, the action method will get the search values posted by the user and use the values to search the database.

SearchIndx_SM

Displaying the SearchIndex Form

Start by adding a SearchIndex action method to the existing MoviesController class. The method will return a view that contains an HTML form. Here's the code:

Public Function SearchIndex(ByVal searchString As String) As ActionResult
    Dim movies = From m In db.Movies
                 Select m 

    If Not String.IsNullOrEmpty(searchString) Then 
        movies = movies.Where(Function(s) s.Title.Contains(searchString)) 
    End If
    Return View(movies) 
End Function

The first line of the SearchIndex method creates the following LINQ query to select the movies:

Dim movies = From m In db.Movies    Select m

The query is defined at this point, but hasn't yet been run against the data store.

If the searchString parameter contains a string, the movies query is modified to filter on the value of the search string, using the following code:

If Not String.IsNullOrEmpty(searchString) Then
movies = movies.Where(Function(s) s.Title.Contains(searchString))
End If

LINQ queries are not executed when they are defined or when they are modified by calling a method such as Where or OrderBy. Instead, query execution is deferred, which means that the evaluation of an expression is delayed until its realized value is actually iterated over or the ToList method is called. In the SearchIndex sample, the query is executed in the SearchIndex view. For more information about deferred query execution, see Query Execution.

Now you can implement the SearchIndex view that will display the form to the user. Right-click inside the SearchIndex method and then click Add View. In the Add View dialog box, specify that you're going to pass a Movie object to the view template as its model class. In the Scaffold template list, choose List, then click Add.

AddSearchView

When you click the Add button, the Views\Movies\SearchIndex.vbhtml view template is created. Because you selected List in the Scaffold template list, Visual Web Developer automatically generated (scaffolded) some default content in the view. The scaffolding created an HTML form. It examined the Movie class and created code to render <label> elements for each property of the class. The listing below shows the Create view that was generated:

@ModelType IEnumerable(Of MvcMovie.Movie)

@Code
    ViewData("Title") = "SearchIndex"
End Code

<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>

@For Each item In Model
    Dim currentItem = item
    @<tr>
        <td>
            @Html.DisplayFor(Function(modelItem) currentItem.Title)
        </td>
        <td>
            @Html.DisplayFor(Function(modelItem) currentItem.ReleaseDate)
        </td>
        <td>
            @Html.DisplayFor(Function(modelItem) currentItem.Genre)
        </td>
        <td>
            @Html.DisplayFor(Function(modelItem) currentItem.Price)
        </td>
        <td>
            @Html.ActionLink("Edit", "Edit", New With {.id = currentItem.ID}) |
            @Html.ActionLink("Details", "Details", New With {.id = currentItem.ID}) |
            @Html.ActionLink("Delete", "Delete", New With {.id = currentItem.ID})
        </td>
    </tr>
Next

</table>

Run the application and navigate to /Movies/SearchIndex. Append a query string such as ?searchString=ghost to the URL. The filtered movies are displayed.

SearchQryStr

If you change the signature of the SearchIndex method to have a parameter named id, the id parameter will match the {id} placeholder for the default routes set in the Global.asax file.

{controller}/{action}/{id}

The modified SearchIndex method would look as follows:

Public Function SearchIndex(ByVal id As String) As ActionResult
Dim searchString As String = id
Dim movies = From m In db.Movies
             Select m

If Not String.IsNullOrEmpty(searchString) Then
    movies = movies.Where(Function(s) s.Title.Contains(searchString))
End If

Return View(movies)
End Function

You can now pass the search title as route data (a URL segment) instead of as a query string value.

SearchRouteData

However, you can't expect users to modify the URL every time they want to search for a movie. So now you you'll add UI to help them filter movies. If you changed the signature of the SearchIndex method to test how to pass the route-bound ID parameter, change it back so that your SearchIndex method takes a string parameter named searchString:

Open the Views\Movies\SearchIndex.vbhtml file, and just after @Html.ActionLink("Create New", "Create"), add the following:

@Code
    ViewData("Title") = "SearchIndex"
    Using (Html.BeginForm())
         @<p> Title: @Html.TextBox("SearchString") 
         <input type="submit" value="Filter" /></p>
        End Using
End Code

The Html.BeginForm helper creates an opening <form> tag. The Html.BeginForm helper causes the form to post to itself when the user submits the form by clicking the Filter button.

Run the application and try searching for a movie.

SearchIndxIE9_title

There's no HttpPost overload of the SearchIndex method. You don't need it, because the method isn't changing the state of the application, just filtering data. If you added the following HttpPost SearchIndex method, the action invoker would match the HttpPost SearchIndex method, and the HttpPost SearchIndex method would run as shown in the image below.

<HttpPost()>
 Public Function SearchIndex(ByVal fc As FormCollection, ByVal searchString As String) As String
     Return "<h3> From [HttpPost]SearchIndex: " & searchString & "</h3>"
 End Function

SearchPostGhost

Adding Search by Genre

If you added the HttpPost version of the SearchIndex method, delete it now.

Next, you'll add a feature to let users search for movies by genre. Replace the SearchIndex method with the following code:

Public Function SearchIndex(ByVal movieGenre As String, ByVal searchString As String) As ActionResult
    Dim GenreLst = New List(Of String)()

    Dim GenreQry = From d In db.Movies
                   Order By d.Genre
                   Select d.Genre
    GenreLst.AddRange(GenreQry.Distinct())
    ViewBag.movieGenre = New SelectList(GenreLst)

    Dim movies = From m In db.Movies
                 Select m

    If Not String.IsNullOrEmpty(searchString) Then
        movies = movies.Where(Function(s) s.Title.Contains(searchString))
    End If

    If String.IsNullOrEmpty(movieGenre) Then
        Return View(movies)
    Else
        Return View(movies.Where(Function(x) x.Genre = movieGenre))
    End If

End Function

This version of the SearchIndex method takes an additional parameter, namely movieGenre. The first few lines of code create a List object to hold movie genres from the database.

The following code is a LINQ query that retrieves all the genres from the database.

Dim GenreQry = From d In db.Movies
                   Order By d.Genre
                   Select d.Genre

The code uses the AddRange method of the generic List collection to add all the distinct genres to the list. (Without the Distinct modifier, duplicate genres would be added — for example, comedy would be added twice in our sample). The code then stores the list of genres in the ViewBag object.

The following code shows how to check the movieGenre parameter. If it's not empty the code further constrains the movies query to limit the selected movies to the specified genre.

If String.IsNullOrEmpty(movieGenre) Then
        Return View(movies)
    Else
        Return View(movies.Where(Function(x) x.Genre = movieGenre))
    End If

Adding Markup to the SearchIndex View to Support Search by Genre

Add an Html.DropDownList helper to the Views\Movies\SearchIndex.vbhtml file, just before the TextBox helper. The completed markup is shown below:

<p>
    @Html.ActionLink("Create New", "Create")
    @Code
    ViewData("Title") = "SearchIndex"
    Using (Html.BeginForm())
         @<p> Genre: @Html.DropDownList("movieGenre", "All")
         Title: @Html.TextBox("SearchString") 
         <input type="submit" value="Filter" /></p>
        End Using
End Code
</p>

Run the application and browse to /Movies/SearchIndex. Try a search by genre, by movie name, and by both criteria.

In this section you examined the CRUD action methods and views generated by the framework. You created a search action method and view that let users search by movie title and genre. In the next section, you'll look at how to add a property to the Movie model and how to add an initializer that will automatically create a test database.