Examining the Edit Methods and Edit View

This tutorial is the seventh in a series of a Visual Basic versions of the Introduction to ASP.NET MVC 5 tutorials published on the www.asp.net site. The original series, produced by Scott Guthrie (twitter @scottgu ), Scott Hanselman (twitter: @shanselman ), and Rick Anderson ( @RickAndMSFT ) was written using the C# language. My versions keep as close to the originals as possible, changing only the coding language. The narrative text is largely unchanged from the original and is used with permission from Microsoft.

This tutorial series will teach you the basics of building an ASP.NET MVC 5 Web application using Visual Studio 2013 and Visual Basic.  A Visual Studio Express For Web project with VB source code is available to accompany this series which you can download.

The tutorial series comprises 11 sections in total. They cover the basics of web development using the ASP.NET MVC framework and the Entity Framework for data access. They are intended to be followed sequentially as each section builds on the knowledge imparted in the previous sections. The navigation path through the series is as follows:

  1. Getting Started
  2. Adding a Controller
  3. Adding a View
  4. Adding a Model
  5. Creating a Connection String and Working with SQL Server LocalDB
  6. Accessing Your Model's Data from a Controller
  7. Examining the Edit Methods and Edit View
  8. Adding Search
  9. Adding a New Field
  10. Adding Validation
  11. Examining the Details and Delete Methods

7. Examining the Edit Methods and Edit View

In this section, you'll examine the generated Edit action methods and views for the movie controller. But first will take a short diversion to make the release date look better. Open the Models\Movie.vb file and add the highlighted lines shown below:

Imports System.ComponentModel.DataAnnotations
Imports System.Data.Entity

Namespace Models
    Public Class Movie
        Public Property ID As Integer
        Public Property Title As String
        <Display(Name:="Release Date")>
        <DataType(DataType.Date)>
        <DisplayFormat(DataFormatString:="{0:yyyy-MM-dd}", ApplyFormatInEditMode:=True)>
        Public Property ReleaseDate As DateTime
        Public Property Genre As String
        Public Property Price As Decimal
    End Class

    Public Class MovieDbContext
        Inherits DbContext

        Public Property Movies As DbSet(Of Movie)
    End Class

End Namespace

You can also make the date culture specific like this:

<DisplayFormat(DataFormatString:="{0:d}", ApplyFormatInEditMode:=True)>

The highlighted lines that appear within < > brackets are attributes from the DataAnnotations namespace. We'll cover Data Annotations in more detail in the next tutorial. The Display attribute specifies what to display for the name of a field (in this case "Release Date" instead of "ReleaseDate"). The DataType attribute specifies the type of the data, in this case it's a date, so the time information stored in the field is not displayed. The DisplayFormat attribute is needed for a bug in the Chrome browser that renders date formats incorrectly.

Run the application and browse to the Movies controller. Hold the mouse pointer over an Edit link to see the URL that it links to.

Edit Methods

 

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

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

Edit Methods

The Html object is a helper that's exposed using a property on the System.Web.Mvc.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 (In this case, the Edit action). 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:1804/Movies/Edit/4. The default route (established in App_Start\RouteConfig.vb) takes the URL pattern {controller}/{action}/{id}. Therefore, ASP.NET translates http://localhost:1804/Movies/Edit/4  into a request to the Edit action method of the Movies controller with the parameter ID equal to 4.  Examine the following code from the App_Start\RouteConfig.vb file. The MapRoute method is used to route HTTP requests to the correct controller and action method and supply the optional ID parameter.  The MapRoute method is also used by the HtmlHelpers such as ActionLink to generate URLs given the controller, action method and any route data.

Public Sub RegisterRoutes(ByVal routes As RouteCollection)
    
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}")

    routes.MapRoute(
            name:="Default",
            url:="{controller}/{action}/{id}",
            defaults:=New With {.controller = "Home", .action = "Index", .id = UrlParameter.Optional}
        )
End Sub

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

Edit Methods

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

' GET: /Movies/Edit/5
Function Edit(ByVal id As Integer?) As ActionResult
    If IsNothing(id) Then
        Return New HttpStatusCodeResult(HttpStatusCode.BadRequest)
    End If
    Dim movie As Movie = db.Movies.Find(id)
    If IsNothing(movie) Then
        Return HttpNotFound()
    End If
    Return View(movie)
End Function

'POST: /Movies/Edit/5
'To protect from overposting attacks, please enable the specific properties you want to bind to, for 
'more details see http://go.microsoft.com/fwlink/?LinkId=317598.
<HttpPost()>
<ValidateAntiForgeryToken()>
Function Edit(<Bind(Include := "ID,Title,ReleaseDate,Genre,Price")> ByVal 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 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 Bind attribute is another important security mechanism that keeps hackers from over-posting data to your model. You should only include properties in the bind attribute that you want to change. You can read about overposting and the bind attribute in Rick Anderson's overposting security note. In the simple model used in this tutorial, we will be binding all the data in the model. The ValidateAntiForgeryToken attribute is used to prevent forgery of a request and is paired up with @Html.AntiForgeryToken() in the edit view file (Views\Movies\Edit.vbhtml), a portion of which is shown below:

@ModelType MvcMovie.Models.Movie
@Code
    ViewData("Title") = "Edit"
End Code

<h2>Edit</h2>

@Using (Html.BeginForm())
    @Html.AntiForgeryToken()
    
    @<div class="form-horizontal">
        <h4>Movie</h4>
        <hr />
        @Html.ValidationSummary(true)
        @Html.HiddenFor(Function(model) model.ID)

        <div class="form-group">
            @Html.LabelFor(Function(model) model.Title, New With { .class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(Function(model) model.Title)
                @Html.ValidationMessageFor(Function(model) model.Title)
            </div>
        </div>

@Html.AntiForgeryToken()  generates a hidden form anti-forgery token that must match in the Edit method of the Movies controller. You can read more about Cross-site request forgery (also known as XSRF or CSRF) in Rick Andersion's tutorial XSRF/CSRF Prevention in MVC.

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.  If a movie cannot be found,  HttpNotFound is returned. 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 by the visual studio scaffolding system:

@ModelType MvcMovie.Models.Movie
@Code
    ViewData("Title") = "Edit"
End Code

<h2>Edit</h2>

@Using (Html.BeginForm())
    @Html.AntiForgeryToken()
    
    @<div class="form-horizontal">
        <h4>Movie</h4>
        <hr />
        @Html.ValidationSummary(true)
        @Html.HiddenFor(Function(model) model.ID)

        <div class="form-group">
            @Html.LabelFor(Function(model) model.Title, New With { .class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(Function(model) model.Title)
                @Html.ValidationMessageFor(Function(model) model.Title)
            </div>
        </div>

        <div class="form-group">
            @Html.LabelFor(Function(model) model.ReleaseDate, New With { .class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(Function(model) model.ReleaseDate)
                @Html.ValidationMessageFor(Function(model) model.ReleaseDate)
            </div>
        </div>

        <div class="form-group">
            @Html.LabelFor(Function(model) model.Genre, New With { .class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(Function(model) model.Genre)
                @Html.ValidationMessageFor(Function(model) model.Genre)
            </div>
        </div>

        <div class="form-group">
            @Html.LabelFor(Function(model) model.Price, New With { .class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(Function(model) model.Price)
                @Html.ValidationMessageFor(Function(model) model.Price)
            </div>
        </div>

        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Save" class="btn btn-default" />
            </div>
        </div>
    </div>
End Using

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

@Section Scripts 
    @Scripts.Render("~/bundles/jqueryval")
End Section

The @ModelType MvcMovie.Models.Movie statement at the top of the file 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 renders 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 generated HTML for the form element is shown below.

<form action="/Movies/Edit/4" method="post">
    <input name="__RequestVerificationToken" type="hidden" value="2woDzftMzbwwv1BFDqGL4CRGJKc-Y72RCnnTZmKoBPBP_MNhM4XVu5dl6p9CV7S4yqv3yhtbqFzyJKLd3gqRRYNnfyP6Zy3DxtOXKf1p3ok1" />    
    <div class="form-horizontal">
        <h4>Movie</h4>
        <hr />

        <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="form-group">
            <label class="control-label col-md-2" for="Title">Title</label>
            <div class="col-md-10">
                <input class="text-box single-line" id="Title" name="Title" type="text" value="Up" />
                <span class="field-validation-valid" data-valmsg-for="Title" data-valmsg-replace="true"></span>
            </div>
        </div>

        <div class="form-group">
            <label class="control-label col-md-2" for="ReleaseDate">Release Date</label>
            <div class="col-md-10">
                <input class="text-box single-line" data-val="true" data-val-date="The field Release Date must be a date." data-val-required="The Release Date field is required." id="ReleaseDate" name="ReleaseDate" type="date" value="2009-10-09" />
                <span class="field-validation-valid" data-valmsg-for="ReleaseDate" data-valmsg-replace="true"></span>
            </div>
        </div>

        <div class="form-group">
            <label class="control-label col-md-2" for="Genre">Genre</label>
            <div class="col-md-10">
                <input class="text-box single-line" id="Genre" name="Genre" type="text" value="Animation" />
                <span class="field-validation-valid" data-valmsg-for="Genre" data-valmsg-replace="true"></span>
            </div>
        </div>

        <div class="form-group">
            <label class="control-label col-md-2" for="Price">Price</label>
            <div class="col-md-10">
                <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="6.99" />
                <span class="field-validation-valid" data-valmsg-for="Price" data-valmsg-replace="true"></span>
            </div>
        </div>

        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Save" class="btn btn-default" />
            </div>
        </div>
    </div>
</form>

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 Save button is clicked. The second line shows the hidden XSRF token generated by the @Html.AntiForgeryToken() method call.

Processing the POST Request

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

<HttpPost()>
<ValidateAntiForgeryToken()>
Function Edit(<Bind(Include := "ID,Title,ReleaseDate,Genre,Price")> ByVal 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 ValidateAntiForgeryToken attribute validates the  XSRF token generated by the @Html.AntiForgeryToken() call in the view.

The ASP.NET MVC model binder takes the posted form values and creates a Movie object from the values passed as the movie parameter. The ModelState.IsValid property is set to True if the data submitted in the form can be used to modify (edit or update) a Movie object. If the data is valid, the movie data is saved to the Movies collection of the db (MovieDbContext instance). The  new movie data is saved to the database by calling the SaveChanges method of MovieDbContext. After saving the data, the code redirects the user to the Index action method of the MoviesController class, which displays the movie collection, including the changes just made.

As soon as the client side validation determines the values of a field are not valid, an error message is displayed. If you disable JavaScript, you won't have client side validation but the server will detect the posted values are not valid, and the form values will be redisplayed with error messages. Later in the tutorial we examine validation in more detail.

 The Html.ValidationMessageFor helpers in the Edit.vbhtml view template take care of displaying appropriate error messages.

Edit Methods

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 and doesn't modify your persisted data.

If you are using a US-English computer, you can skip this section and go to the next tutorial. You can download the Globalize version of this tutorial here. For an excellent two part tutorial on Internationalization, see Nadeem's ASP.NET MVC 5 Internationalization.

 

Note to support jQuery validation for non-English locales that use a comma (",") for a decimal point, and non US-English date formats, you must include globalize.js and your specificcultures/globalize.cultures.js file(from https://github.com/jquery/globalize ) and JavaScript to useGlobalize.parseFloat. You can get the jQuery non-English validation from NuGet. (Don't install Globalize if you are using a English locale.)

  1. From the Tools menu click Library Package Manager, and then click Manage NuGet Packages for Solution.

    Edit Methods

  2. On the left pane, select Online. (See the image below.)
  3. In the Search Installed packages input box, enter Globalize.

    Edit Methods

  4. Click Install. The Scripts\jquery.globalize\globalize.js file will be added to your project. The Scripts\jquery.globalize\cultures\ folder will contain many culture JavaScript files. Note, it can take five minutes to install this package.

The following code shows the modifications to the Views\Movies\Edit.vbhtml file:

@Section Scripts 
    @Scripts.Render("~/bundles/jqueryval")

<script src="~/Scripts/globalize/globalize.js"></script>
    <script src="~/Scripts/globalize/cultures/globalize.culture.@(System.Threading.Thread.CurrentThread.CurrentCulture.Name).js"></script>
    <script>
    $.validator.methods.number = function (value, element) {
        return this.optional(element) ||
            !isNaN(Globalize.parseFloat(value));
    }
    $(document).ready(function () {
        Globalize.culture('@(System.Threading.Thread.CurrentThread.CurrentCulture.Name)');
    });
    </script>
    <script>
        jQuery.extend(jQuery.validator.methods, {
            range: function (value, element, param) {
                //Use the Globalization plugin to parse the value
                var val = Globalize.parseFloat(value);
                return this.optional(element) || (
                    val >= param[0] && val <= param[1]);
            }
        });
        $.validator.methods.date = function (value, element) {
            return this.optional(element) ||
                Globalize.parseDate(value) ||
                Globalize.parseDate(value, "yyyy-MM-dd");
        }
    </script>
    }
End Section

To avoid repeating this code in every Edit view, you can move it to the layout file.  To optimize the script download, see my tutorial Bundling and Minification.

For more information see ASP.NET MVC 3 Internationalization and ASP.NET MVC 3 Internationalization - Part 2 (NerdDinner).

As a temporary fix, if you can't get validation working in your locale, you can force your computer to use US English or you can disable JavaScript in your browser. To force your computer to use US English, you can add the globalization element to the projects root web.config file. The following code shows the globalization element with the culture set to United States English.

<system.web>
    <globalization culture ="en-US" />
    <!--elements removed for clarity-->
</system.web>

In the next tutorial, we'll implement search functionality.