Simple Search Panel in MVC 4

Introduction

This article provides a demonstration of implementing a search panel in an MVC project using little more than a bit of jQuery along with a couple of freely available plugins.

Demo-Project-with-Collapsible-Search-Panel.jpg
Figure 1 - Demo Project with Collapsible Search Panel

What you will need

In order to build this project you will need a copy of jQuery (or you can use a CDN) and the jQuery Cookie plugin. In order to run the demonstration you will also need a copy of the zip code database. The good news is that all of these things are included in the project. You will need to restore the zip code database from a backup to your local instance of SQL Server; check the download for a copy of the database entitled, "PostalCodes.bak". Of course you will also need a copy of Visual Studio or it won't be loads of fun looking at the files in the attachment.
 
Getting Ready

Start out by restoring the database to your local SQL Server instance; if you don't have one then you can download SQL Express or SQL Server Developer Edition free from Microsoft. Then unzip the project and open it up in Visual Studio 2012. You can then go into the web configuration file, locate the connection strings and update them to point to your local copy of the zip code database. At this point you should be able to run the project.  If you would rather locate and download your own zip code or any other database to use instead of the demo database then feel free to swap it out for something more useful to you.
 
Demo Project

The project consists of a single MVC 4 application; aside from the usual boilerplate, the project contains a home controller along with a single Index view associated with that controller.  It also contains a style sheet, the jQuery cookie plugin, and a couple of images to support a button with a hover effect.  Not much to it all.  There is an Entity Framework model (.edmx) that points to the database and a single accessor method to fetch the postal codes for display in a grid. Since the grid is a WebGrid, the project also imports the System.Web.Helper library as well.  You can find the model and accessor class in the DAL folder in the project.

As far as filtering the data goes, everything that needs to be done happens entirely in the Index controller action. Whilst this is certainly not the most expeditious way to handle a large fetch, I think you will find that dealing with 42,000+ records, it is not what one would think of as terribly slow either. I would rather consider this a very quick and dirty way to set up a filtering mechanization without much bother.

The next figure (Figure 2) contains a look at the Solution Explorer for the project. The controller and view folders are open to show the home controller and index view that make up the working part of the demo application.

Solution-Explorer.jpg
Figure 2 - Solution Explorer
 
Home Controller

With the project open in Visual Studio 2012, open up the Home Controller and have a look at the one and only controller action it contains:  Index. In looking it over, you will see the first thing it does is call the accessor's method used to fetch the postal codes.  If no filters were passed to the controller, then the results are not filtered at all. If any filters are passed in then they will be applied to the result set.  Not overly efficient but not terribly slow either. You could write instead accessor methods that take each of the filter arguments as optional and return only the filtered results to the associated view, but this is quick and dirty; feel free to modify it or do it entirely with AJAX if you prefer.

So, if the method receives any filter arguments passed in then you can see the that code looks to see if the filter contains any content and if so, the controller applies that filter to the list of postal codes to limit what is passed on to the view. I mixed it up so that most use the expression as the basis for a StartsWith filter, with the latitude and longitude that made less sense than using Contains so I applied Contains to the latitude and longitude filters. I suppose you could provide the user an option to make it contains or starts with if you were so inclined; I wasn't.

  1. using System;  
  2. using System.Collections.Generic;  
  3. using System.Linq;  
  4. using System.Web;  
  5. using System.Web.Mvc;  
  6. using SearchPanelDemo.DAL;  
  7. using SearchPanelDemo.Models;  
  8. using System.Web.Helpers;  
  9. using System.Web.WebPages;  
  10. namespace SearchPanelDemo.Controllers  
  11. {  
  12.     public class HomeController : Controller  
  13.     {  
  14.         //  
  15.         // GET: /Home/  
  16.         public ActionResult Index(string zipfilter, string cityfilter, string statefilter,  
  17.         string latitudefilter, string longitudefilter)  
  18.         {  
  19.             ViewBag.Title = "US Postal Codes";  
  20.             List<PostalCodeModel> viewModel = Accessor.FetchPostalCodes().ToList();  
  21.             if (!string.IsNullOrWhiteSpace(zipfilter))  
  22.                 viewModel = viewModel.Where(p => p.ZipCode.StartsWith(zipfilter)).ToList();  
  23.             if (!string.IsNullOrWhiteSpace(cityfilter))  
  24.                 viewModel = viewModel.Where(p => p.City.ToUpper().StartsWith(cityfilter.ToUpper())).ToList();  
  25.             if (!string.IsNullOrWhiteSpace(statefilter))  
  26.                 viewModel = viewModel.Where(p => p.State.ToUpper().StartsWith(statefilter.ToUpper())).ToList();  
  27.             if (!string.IsNullOrWhiteSpace(latitudefilter))  
  28.                 viewModel = viewModel.Where(p => p.Latitude.Contains(latitudefilter)).ToList();  
  29.             if (!string.IsNullOrWhiteSpace(longitudefilter))  
  30.                 viewModel = viewModel.Where(p => p.Longitude.Contains(longitudefilter)).ToList();  
  31.             return View(viewModel);  
  32.         }  
  33.     }  
  34. } 

Index View

The index view is a razor view that contains the markup and script necessary to format the view and manage the search panel. Since we are going back to the controller to filter the results and we don't want the search panel closing each time we do, I used the jQuery cookie plugin to keep track of whether the panel should be opened or closed and then set the panel accordingly. As with all things, you could use something other than the cookie to persist the opened/closed state of the panel if you prefer.

I also used the cookie for maintaining the position of the cursor for the same reason. When you select a filter TextBox or tab into one, each time the view is returned it will set the cursor into the last left text box. This prevents the cursor from returning to the start of the tab order each time the view returns.

The sum total of the view is contained in the code below; have a look below to see how the search panel is configured and then look to the script blocks at the end of it to see how jQuery was used to control the search panel visibility and the tab order.

 

  1. @using System.Web.Helpers;  
  2. @model List<SearchPanelDemo.Models.PostalCodeModel>  
  3. @{  
  4.     var grid = new WebGrid(source: Model, defaultSort: "PropertyName", rowsPerPage: 20);  
  5. }  
  6. <div id="mainCont">  
  7.     <div id="ContainerBox">  
  8.         <div class="Title">US Postal Codes  
  9.                 <div style="float:right;padding-right:30px">  
  10.                     <input id="searchPanelLink" type="image" src="../Content/images/search.png"  
  11.                     title="Click to open/close search panel"/>   
  12.                 </div>  
  13.         </div>  
  14.         <div id="searchParameters" style="width:778px;background:#FFFFFF;padding:10px; border:1px solid #000000">  
  15.             <h3>Search Parameters</h3><br />  
  16.             <form method="get">  
  17.                 <div>  
  18.                     <div style="padding-bottom:5px">  
  19.                         Postal Code:  @Html.TextBox("zipfilter"nullnew {autocomplete="off", tabindex = 1,   
  20.                                          maxlength="5", onchange="$('form').submit()"})  
  21.                         City:  @Html.TextBox("cityfilter"nullnew {autocomplete="off", tabindex = 2,   
  22.                                          onchange="$('form').submit()"})  
  23.                         State:  @Html.TextBox("statefilter"nullnew {autocomplete="off", tabindex = 3,   
  24.                                          maxlength="2", onchange="$('form').submit()"})  
  25.                     </div>  
  26.                     <div>  
  27.                         Latitude:  @Html.TextBox("latitudefilter"nullnew {autocomplete="off", tabindex = 4,   
  28.                                          maxlength="10", style="width:100px", onchange="$('form').submit()"})  
  29.                         Longitude:  @Html.TextBox("longitudefilter"nullnew {autocomplete="off", tabindex = 5,   
  30.                                          maxlength="10", style="width:100px", onchange="$('form').submit()"})  
  31.                         <input id="searchSubmit" type="submit" value="Search" tabindex="7" style="margin-left:230px"/>  
  32.                         <input id="searchReset" type="submit" value="Clear" tabindex="8" style="margin-left:90px"/>  
  33.                     </div>  
  34.                 </div>  
  35.             </form>  
  36.         </div>  
  37.         @grid.GetHtml(columns: grid.Columns(  
  38.                 grid.Column("ZipCode""Postal Code", canSort: true, style: "text-align-center"),  
  39.                 grid.Column("City""City", canSort: true, style: "text-align-center"),  
  40.                 grid.Column("State""State", canSort: true, style: "text-align-center"),  
  41.                 grid.Column("Latitude""Latitude", canSort: true, style: "text-align-center"),  
  42.                 grid.Column("Longitude""Longitude", canSort: true, style: "text-align-center")  
  43.             ))  
  44.     </div>  
  45. </div>  
  46. <script>  
  47.     $(document).ready(function () {  
  48.         $('thead > tr > th > a[href*="[email protected]"]').parent().append("@(grid.SortDirection == SortDirection.Ascending ? " ▲" : " ▼")");  
  49.         if ($.cookie('searchPanelVis') == null) {  
  50.             $.cookie('searchPanelVis''hidden'); // initial load  
  51.             $('#searchParameters').hide();  
  52.         }  
  53.         else {  
  54.             var toggleStatus = $.cookie('searchPanelVis'); // restore hide/show  
  55.             if (toggleStatus == 'hidden') {  
  56.                 $('#searchParameters').hide();  
  57.             }  
  58.             else {  
  59.                 $('#searchParameters').show();  
  60.             }  
  61.         }  
  62.         // toggle the search panel on/off  
  63.         $('#searchPanelLink').click(function () {  
  64.             $('#searchParameters').slideToggle('slow'function(){  
  65.                 if ($('#searchParameters').is(':hidden')) {  
  66.                     $.cookie('searchPanelVis''hidden');  
  67.                 }  
  68.                 else {  
  69.                     $.cookie('searchPanelVis''visible');  
  70.                 }  
  71.             })  
  72.         });  
  73.         // hover effect for the search panel icon  
  74.         $("#searchPanelLink").mouseenter(function () {  
  75.             $("#searchPanelLink").attr('src''../Content/images/search_hover.png');  
  76.         });  
  77.         $("#searchPanelLink").mouseleave(function () {  
  78.             $("#searchPanelLink").attr('src''../Content/images/search.png');  
  79.         });  
  80.         // clear search parameters and reload the grid with all records  
  81.         $('#searchReset').click(function () {  
  82.             $('#zipfilter').val('');  
  83.             $('#cityfilter').val('');  
  84.             $('#statefilter').val('');  
  85.             $('#latitudefilter').val('');  
  86.             $('#longitudefilter').val('');  
  87.         });  
  88.         $(':text').focus(function () {  
  89.             var nameValue = $(this).attr('name');  
  90.             $.cookie('lastFocus', nameValue);  
  91.         });  
  92.         if ($.cookie('lastFocus') != null) {  
  93.             var focused = $.cookie('lastFocus');  
  94.             switch (focused) {  
  95.                 case 'zipfilter':  
  96.                     $('#zipfilter').select().focus();  
  97.                     break;  
  98.                 case 'cityfilter':  
  99.                     $('#cityfilter').select().focus();  
  100.                     break;  
  101.                 case 'statefilter':  
  102.                     $('#statefilter').select().focus();  
  103.                     break;  
  104.                 case 'latitudefilter':  
  105.                     $('#latitudefilter').select().focus();  
  106.                     break;  
  107.                 case 'longitudefilter':  
  108.                     $('#longitudefilter').select().focus();  
  109.                     break;  
  110.                 default:  
  111.                     break;  
  112.             }  
  113.         }  
  114.     });  
  115. </script> 

 

Conclusion

The rest of the project just supports the demo or is pure boilerplate. Anyway, have a look and give it a shot; it is a very quick and easy way to implement a search panel and simple filtering on a quick project. If performance is more of an issue then you may wish to handle the search and filtering functionality with AJAX. 


Similar Articles