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.The Edit link was generated by the
Html.ActionLink
method in the Views\Movies\Index.cshtml view:@Html.ActionLink("Edit", "Edit", new { id=item.ID })
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. 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 (established in App_Start\RouteConfig.cs) 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. Examine the following code from the
App_Start\RouteConfig.cs file.public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}", defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional } ); }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.Open the
Movies
controller. The two Edit
action
methods are shown below.// // 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); }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
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. The ID parameter
specifies a
default value of zero if the Edit
method is called without a
parameter. 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:@model MvcMovie.Models.Movie @{ ViewBag.Title = "Edit"; } <h2>Edit</h2> @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> @section Scripts { @Scripts.Render("~/bundles/jqueryval") }Notice how the view template has a
@model 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 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 HTML for the form element is shown below.
<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>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 theHttpPost
version of the
Edit
action method.[HttpPost] public ActionResult Edit(Movie movie) { if (ModelState.IsValid) { db.Entry(movie).State = EntityState.Modified; db.SaveChanges(); return RedirectToAction("Index"); } return View(movie); }The ASP.NET MVC model binder takes the posted form values and creates a
Movie
object that's passed as the movie
parameter. The
ModelState.IsValid
method verifies that 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 of
movie collection, including the changes just made.If the posted values aren't valid, they are redisplayed in the form. The
Html.ValidationMessageFor
helpers in the Edit.cshtml view
template take care of displaying appropriate error messages.
Note to support jQuery validation for non-English locales that use a comma (",") for a decimal point, you must include
globalize.js and your specific cultures/globalize.cultures.js
file(from
https://github.com/jquery/globalize ) and JavaScript to use
Globalize.parseFloat
. The following code shows the modifications to
the Views\Movies\Edit.cshtml file to work with the "fr-FR" culture:@section Scripts { @Scripts.Render("~/bundles/jqueryval") <script src="~/Scripts/globalize.js"></script> <script src="~/Scripts/globalize.culture.fr-FR.js"></script> <script> $.validator.methods.number = function (value, element) { return this.optional(element) || !isNaN(Globalize.parseFloat(value)); } $(document).ready(function () { Globalize.culture('fr-FR'); }); </script> <script> jQuery.extend(jQuery.validator.methods, { range: function (value, element, param) { //Use the Globalization plugin to parse the value var val = $.global.parseFloat(value); return this.optional(element) || ( val >= param[0] && val <= param[1]); } }); </script> }
The decimal field may require a comma, not a decimal point. As a temporary fix, 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>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.Adding a Search Method and Search View
In this section you'll add aSearchIndex
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 enter 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.Displaying the SearchIndex Form
Start by adding aSearchIndex
action method to the existing
MoviesController
class. The method will return a view that contains
an HTML form. Here's the code: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); }The first line of the
SearchIndex
method creates the following
LINQ query
to select the movies: var 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 (!String.IsNullOrEmpty(searchString)) { movies = movies.Where(s => s.Title.Contains(searchString)); }The
s => s.Title
code above is a
Lambda
Expression. Lambdas are used in method-based
LINQ queries
as arguments to standard query operator methods such as
the
Where method used in the above code. 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.When you click the Add button, the Views\Movies\SearchIndex.cshtml view template is created. Because you selected List in the Scaffold template list, Visual Studio automatically generated (scaffolded) some default markup 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:@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>Run the application and navigate to /Movies/SearchIndex. Append a query string such as
?searchString=ghost
to the URL. The filtered
movies are displayed.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 original
SearchIndex
method looks like this::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); }The modified
SearchIndex
method would look as follows: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); }You can now pass the search title as route data (a URL segment) instead of as a query string value.
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
: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); }Open the Views\Movies\SearchIndex.cshtml file, and just after
@Html.ActionLink("Create New", "Create")
, add the following:@using (Html.BeginForm()){ <p> Title: @Html.TextBox("SearchString")<br /> <input type="submit" value="Filter" /></p> }The following example shows a portion of the Views\Movies\SearchIndex.cshtml file with the added filtering markup.
@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") <br /> <input type="submit" value="Filter" /></p> } </p>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.
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.You could add the following
HttpPost SearchIndex
method. In that
case, the action invoker would match the HttpPost SearchIndex
method, and the HttpPost SearchIndex
method would run as shown in
the image below.[HttpPost] public string SearchIndex(FormCollection fc, string searchString) { return "<h3> From [HttpPost]SearchIndex: " + searchString + "</h3>"; }
However, even if you add this
HttpPost
version of the
SearchIndex
method, there's a limitation in how this has all been
implemented. Imagine that you want to bookmark a particular search or you want
to send a link to friends that they can click in order to see the same filtered
list of movies. Notice that the URL for the HTTP POST request is the same as the
URL for the GET request (localhost:xxxxx/Movies/SearchIndex) -- there's no
search information in the URL itself. Right now, the search string information
is sent to the server as a form field value. This means you can't capture that
search information to bookmark or send to friends in a URL.The solution is to use an overload of
BeginForm
that specifies
that the POST request should add the search information to the URL and that it
should be routed to the HttpGet version of the SearchIndex
method.
Replace the existing parameterless BeginForm
method with the
following: @using (Html.BeginForm("SearchIndex","Movies",FormMethod.Get))
Now when you submit a search, the URL contains a search query string. Searching will also go to the
HttpGet SearchIndex
action method,
even if you have a HttpPost SearchIndex
method.Adding Search by Genre
If you added theHttpPost
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 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)); } }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.
var GenreQry = from d in db.Movies orderby 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)) return View(movies); else { return View(movies.Where(x => x.Genre == movieGenre)); }
Adding Markup to the SearchIndex View to Support Search by Genre
Add anHtml.DropDownList
helper to the Views\Movies\SearchIndex.cshtml
file, just before the TextBox
helper. The completed markup is shown
below:<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>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
0 comments:
Post a Comment