Adding Search

This tutorial is the eighth 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

8. Adding a Search Method and Search View

In this section you will add search capability to the Index action method that lets you search movies by genre or name.

Updating the Index Form

Start by updating the Index action method to the existing MoviesController class. Here's the code:

Function Index(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(movie) movie.Title.Contains(searchString))
    End If
    Return View(movies)
End Function

The first line of the Index 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 database.

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(movie) movie.Title.Contains(searchString))
End If

The Function(movie) 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 Select,  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 in Visual Basic code or the ToList  method is called. In the Search sample, the query is executed in  the Index.vbhtml view when the For Each loop is executed. For more information about deferred query execution,  see Query  ExecutionNote: The Contains  method is run on the database when you use LINQ to Entities, not the Visual Basic code above. On the database, Contains maps to SQL LIKE,  which is case insensitive.

Now you can update the Index view that will display the form  to the user.

Run the application and navigate to /Movies/Index. Append a query string such as ?searchString=ar (or something that contains part of the title of at least one of the movies you created) to the URL. The filtered  movies are displayed.

Adding search

 

If you change the signature of the Index method to have a  parameter named id, the id parameter will match the {id} placeholder for the default routes set in the App_Start\RouteConfig.vb  file.

{controller}/{action}/{id}

The original Index method looks like this:

Function Index(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(movie) movie.Title.Contains(searchString))
    End If
    Return View(movies)
End Function

The modified Index method would look as follows:

Function Index(ByVal id As String) As ActionResult
    Dim searchString = id
    Dim movies = From m In db.Movies Select m
    If Not String.IsNullOrEmpty(searchString) Then
        movies = movies.Where(Function(movie) movie.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.

Adding Search

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 Index method to test how to pass  the route-bound ID parameter, change it back so that your Index  method takes a string parameter named searchString:
Function Index(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(movie) movie.Title.Contains(searchString))
    End If
    Return View(movies)
End Function

Open the Views\Movies\Index.vbhtml file, and just after @Html.ActionLink("Create New", "Create"), add the form markup highlighted below:

@ModelType IEnumerable(Of MvcMovie.Models.Movie)
@Code
    ViewData("Title") = "Index"
End Code

<h2>Index</h2>

<p>
    @Html.ActionLink("Create New", "Create")
    
    @Using Html.BeginForm("Index", "Movies", FormMethod.Get)
        @<p>Title: @Html.TextBox("SearchString") <br />
         <input type="submit" value="Filter" /></p>
    End Using
</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.

Visual Studio 2013 has a nice improvement when displaying and editing View files. When you run the application with a view file open, Visual Studio 2013 invokes the correct controller action method to display the view.

Adding Search

With the Index view open in Visual Studio (as shown in the image above), tap Ctr F5 or F5 to run the application and then try searching for a movie.

Adding search

There's no HttpPost overload of the Index  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 Index method. In that  case, the action invoker would match the HttpPost Index  method, and the HttpPost Index method would run as shown in  the image below.

<HttpPost> _
Function Index(ByVal fc As FormCollection, ByVal searchString As String)
    Return "<h3> From &lt;HttpPost&gt;Index: " & searchString & "</h3>"
End Function

Adding search

However, even if you add this HttpPost version of the Index 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/Index) - 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 Index method.  Replace the existing parameterless BeginForm method with the following markup:

@Using Html.BeginForm("Index", "Movies", FormMethod.Get)

Adding Search

 

Now when you submit a search, the URL contains a search query string.  Searching will also go to the HttpGet Index action method,  even if you have a HttpPost Index method.

Adding Search

 

Adding Search by Genre

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

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

Function Index(ByVal movieGenre As String, ByVal searchString As String)
    Dim genreList As New List(Of String)
    Dim genreQuery = From m In db.Movies
                     Order By m.Genre
                     Select m.Genre
                     
    genreList.AddRange(genreQuery.Distinct)
    
    ViewBag.movieGenre = New SelectList(genreList)
    Dim movies = From m In db.Movies
                 Select m


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

    If Not String.IsNullOrEmpty(movieGenre) Then
        movies = movies.Where(Function(m) m.Genre.Equals(movieGenre))
    End If

    Return View(movies)
End Function

This version of the Index 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 genreQuery = From m In db.Movies
                     Order By m.Genre
                     Select m.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, Science Fiction  would be added twice in our sample). The code then stores the list of genres in  the ViewBag.movieGenre object. Storing category data (such a movie genre's) as a SelectList object in a ViewBag, then accessing the category data in a dropdown list box is a typical approach for MVC applications.

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 Not String.IsNullOrEmpty(movieGenre) Then
    movies = movies.Where(Function(m) m.Genre.Equals(movieGenre))
End If

As stated previously, the query is not run on the data base until the movie list is iterated over (which happens in the View, after the Index action method returns).

Adding Markup to the Index View to Support Search by Genre

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

@ModelType IEnumerable(Of MvcMovie.Models.Movie)
@Code
ViewData("Title") = "Index"
End Code

<h2>Index</h2>

<p>
    @Html.ActionLink("Create New", "Create")
    @Using Html.BeginForm("Index", "Movies", FormMethod.Get)
         @<p>Genre: @Html.DropDownList("movieGenre", "All")
          Title: @Html.TextBox("SearchString") <br />
         <input type="submit" value="Filter" /></p>
    End Using
</p>
<table class="table">

In the following code:

Genre: @Html.DropDownList("movieGenre", "All")

The parameter "movieGenre" provides the key for the DropDownList helper to find an IEnumerable<SelectListItem> in the  ViewBag. The ViewBag was  populated in the action  method:

Function Index(ByVal movieGenre As String, ByVal searchString As String)
    Dim genreList As New List(Of String)
    Dim genreQuery = From m In db.Movies
                     Order By m.Genre
                     Select m.Genre
                     
    genreList.AddRange(genreQuery.Distinct)
    
    ViewBag.movieGenre = New SelectList(genreList)
    Dim movies = From m In db.Movies
                 Select m


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

    If Not String.IsNullOrEmpty(movieGenre) Then
        movies = movies.Where(Function(m) m.Genre.Equals(movieGenre))
    End If

    Return View(movies)
End Function

The parameter "All" provides the item in the list to be preselected. Had we used the following code:

Genre: @Html.DropDownList("movieGenre", "Science Fiction")

And we had a movie with a "Science Fiction" genre in our database, "Science Fiction" would be  preselected in the dropdown list. Because we don't have a movie genre "All",  there is no "All" in the SelectList, so when we post back without making a slection, the movieGenre query string value is empty.

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

Adding Search

 

In this section 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.