Overview
Having moved across to ASP.NET MVC3 within the last 6 months, one of the things that I found missing in the wealth of quality tutorials was how to use AJAX to pass a user-defined class. This tutorial aims to illustrate one way of doing this through the creation of an MVC3 AJAX-enabled search page to allow searching a catalogue of books.
You can download the completed C# project here.
Creating the Web Application
To start the tutorial, open Visual Studio 2010 and create a new project using the ASP.NET MVC 3 Web Application template. I named the application “Id.AjaxSearch.Example” but you can call it whatever you wish.

In the New ASP.NET MVC 3 Project dialog, select Intranet Application, select the Razor view engine, make sure ‘Use HTML5 semantic markup’ is checked and then click OK.

Adding the Model
The application uses the following simple model:

This is an Entity Framework 4.1 ‘Code-First’ model that uses SQL Server Compact 4.0 as its datastore. A detailed example of how to build a ‘Code-First’ model is detailed in the MVC Music Store tutorial – Part 4: Models and Data Access.
The ‘seed’ data to be added to the new model is found in the class ‘LibraryInitializer’ which is located in the Models folder. The code for this class is shown below.
protected override void Seed(LibraryEntities context)
{
Subject Antiquarian = new Subject() { Name = "Antiquarian, Rare & Collectable" };
Subject Biography = new Subject() { Name = "Biography" };
Subject Crime = new Subject() { Name = "Crime, Thrillers & Mystery" };
Subject History = new Subject() { Name = "History" };
Subject Horror = new Subject() { Name = "Horror" };
Subject Romance = new Subject() { Name = "Romance" };
Subject SciFiAndFantasy = new Subject() { Name = "Science Fiction & Fantasy" };
Subject Travel = new Subject() { Name = "Travel & Holiday" };
context.Subjects.Add(Antiquarian);
context.Subjects.Add(Biography);
context.Subjects.Add(Crime);
context.Subjects.Add(History);
context.Subjects.Add(Horror);
context.Subjects.Add(Romance);
context.Subjects.Add(SciFiAndFantasy);
context.Subjects.Add(Travel);
Format Hardcover = new Models.Format() { Name = "Hardcover" };
Format Paperback = new Models.Format() { Name = "Paperback" };
Format Kindle = new Models.Format() { Name = "Kindle Books" };
context.Formats.Add(Hardcover);
context.Formats.Add(Paperback);
context.Formats.Add(Kindle);
Author LKHamilton = new Author() { Name = "Laurell K. Hamilton" };
Author KCast = new Author() { Name = "Kristin Cast" };
Author PCCast = new Author() { Name = "P. C. Cast" };
Author RPike = new Author() { Name = "Richard Pike" };
Author CGibson = new Author() { Name = "Chris Gibson" };
Author RVincent = new Author() { Name = "Rachel Vincent" };
Author CHarris = new Author() { Name = "Charlaine Harris" };
Author JLake = new Author() { Name = "John Lake" };
Author MStyling = new Author() { Name = "Mark Styling" };
Author CWSmith = new Author() { Name = "Charles William Smith" };
Author CTomalin = new Author() { Name = "Claire Tomalin" };
Author ASugar = new Author() { Name = "Alan Sugar" };
Author TPratchett = new Author() { Name = "Sir Terry Pratchett" };
Author REFeist = new Author() { Name = "Raymond E. Feist" };
Author PIrvine = new Author() { Name = "Peter Irvine" };
Author DFriebe = new Author() { Name = "Daniel Friebe" };
Author KDoner = new Author() { Name = "Kim Doner" };
context.Authors.Add(LKHamilton);
context.Authors.Add(KCast);
context.Authors.Add(PCCast);
context.Authors.Add(RPike);
context.Authors.Add(CGibson);
context.Authors.Add(RVincent);
context.Authors.Add(CHarris);
context.Authors.Add(JLake);
context.Authors.Add(MStyling);
context.Authors.Add(CWSmith);
context.Authors.Add(CTomalin);
context.Authors.Add(ASugar);
context.Authors.Add(TPratchett);
context.Authors.Add(REFeist);
context.Authors.Add(PIrvine);
context.Authors.Add(DFriebe);
context.Authors.Add(KDoner);
context.Books.Add(new Book()
{
Title = "The Lightning Boys: True Tales from Pilots of the English Electric Lightning",
Price = 12.25M,
Isbn = "190811715X",
PublicationDate = new DateTime(2011, 7, 14),
Format = Hardcover,
Subject = History,
Authors = new List<Author>() { RPike }
});
context.Books.Add(new Book()
{
Title = "Awakened (House of Night)",
Price = 4.12M,
Isbn = "1905654855",
PublicationDate = new DateTime(2011, 10, 25),
Format = Paperback,
Subject = Horror,
Authors = new List<Author>() { KCast, PCCast }
});
context.Books.Add(new Book()
{
Title = "Destined: A House of Night Novel",
Price = 6.49M,
Isbn = "1905654871",
PublicationDate = new DateTime(2011, 10, 25),
Format = Hardcover,
Subject = Horror,
Authors = new List<Author>() { KCast, PCCast }
});
context.Books.Add(new Book()
{
Title = "Dragon's Oath: A House of Night Novella",
Price = 3.82M,
Isbn = "1907411186",
PublicationDate = new DateTime(2011, 7, 12),
Format = Paperback,
Subject = Horror,
Authors = new List<Author>() { KCast, PCCast }
});
context.Books.Add(new Book()
{
Title = "Dragon's Oath: A House of Night Novella",
Price = 3.56M,
Isbn = "1907410708",
PublicationDate = new DateTime(2010, 10, 26),
Format = Hardcover,
Subject = Horror,
Authors = new List<Author>() { PCCast, KDoner }
});
context.Books.Add(new Book()
{
Title = "Bullet (Anita Blake, Vampire Hunter)",
Price = 4.55M,
Isbn = "0755352580",
PublicationDate = new DateTime(2010, 11, 11),
Format = Paperback,
Subject = Horror,
Authors = new List<Author>() { LKHamilton }
});
context.Books.Add(new Book()
{
Title = "Incubus Dreams (Anita Blake Vampire Hunter 12)",
Price = 4.87M,
Isbn = "0755355407",
PublicationDate = new DateTime(2010, 3, 4),
Format = Paperback,
Subject = Horror,
Authors = new List<Author>() { LKHamilton }
});
context.Books.Add(new Book()
{
Title = "Vulcan's Hammer: V-Force Aircraft and Weapons Projects Since 1945",
Price = 20.80M,
Isbn = "1902109171",
PublicationDate = new DateTime(2011, 4, 30),
Format = Hardcover,
Subject = History,
Authors = new List<Author>() { CGibson }
});
context.Books.Add(new Book()
{
Title = "Deadlocked: A True Blood Novel",
Price = 14.24M,
Isbn = "0575096578",
PublicationDate = new DateTime(2012, 5, 17),
Format = Hardcover,
Subject = Horror,
Authors = new List<Author>() { CHarris }
});
context.Books.Add(new Book()
{
Title = "B-52 Stratofortress Units 1955-73",
Price = 12.99M,
Isbn = "1841766070",
PublicationDate = new DateTime(2004, 1, 16),
Format = Paperback,
Subject = History,
Authors = new List<Author>() { JLake, MStyling }
});
context.Books.Add(new Book()
{
Title = "Abstractions",
Price = 14517.21M,
Isbn = "B00085JQEI",
PublicationDate = new DateTime(1939, 1, 1),
Format = Hardcover,
Subject = Antiquarian,
Authors = new List<Author>() { CWSmith }
});
context.Books.Add(new Book()
{
Title = "The Way I See It: Rants, Revelations And Rules For Life",
Price = 8.00M,
Isbn = "0230760899",
PublicationDate = new DateTime(2011, 9, 29),
Format = Hardcover,
Subject = Biography,
Authors = new List<Author>() { ASugar }
});
context.Books.Add(new Book()
{
Title = "The Way I See It: Rants, Revelations And Rules For Life",
Price = 4.79M,
Isbn = "0230760899",
PublicationDate = new DateTime(2011, 9, 29),
Format = Kindle,
Subject = Biography,
Authors = new List<Author>() { ASugar }
});
context.Books.Add(new Book()
{
Title = "Charles Dickens: A Life",
Price = 13.50M,
Isbn = "0670917672",
PublicationDate = new DateTime(2011, 10, 6),
Format = Hardcover,
Subject = Biography,
Authors = new List<Author>() { CTomalin }
});
context.Books.Add(new Book()
{
Title = "What You See Is What You Get",
Price = 4.27M,
Isbn = "0330520474",
PublicationDate = new DateTime(2010, 9, 30),
Format = Kindle,
Subject = Biography,
Authors = new List<Author>() { ASugar }
});
context.Books.Add(new Book()
{
Title = "Snuff: Discworld Novel 39",
Price = 8.99M,
Isbn = "038561926X",
PublicationDate = new DateTime(2011, 10, 13),
Format = Hardcover,
Subject = SciFiAndFantasy,
Authors = new List<Author>() { TPratchett }
});
context.Books.Add(new Book()
{
Title = "I Shall Wear Midnight: Discworld Novel 38",
Price = 4.59M,
Isbn = "0552555592",
PublicationDate = new DateTime(2011, 6, 9),
Format = Paperback,
Subject = SciFiAndFantasy,
Authors = new List<Author>() { TPratchett }
});
context.Books.Add(new Book()
{
Title = "I Shall Wear Midnight: Discworld Novel 38",
Price = 3.99M,
Isbn = "0552555592",
PublicationDate = new DateTime(2011, 6, 9),
Format = Kindle,
Subject = SciFiAndFantasy,
Authors = new List<Author>() { TPratchett }
});
context.Books.Add(new Book()
{
Title = "Magician",
Price = 5.29M,
Isbn = "0586217835",
PublicationDate = new DateTime(2008, 9, 1),
Format = Paperback,
Subject = SciFiAndFantasy,
Authors = new List<Author>() { REFeist }
});
context.Books.Add(new Book()
{
Title = "Silverthorn",
Price = 6.74M,
Isbn = "0007229429",
PublicationDate = new DateTime(2008, 9, 1),
Format = Paperback,
Subject = SciFiAndFantasy,
Authors = new List<Author>() { REFeist }
});
context.Books.Add(new Book()
{
Title = "A Darkness at Sethanon",
Price = 5.29M,
Isbn = "0007229437",
PublicationDate = new DateTime(2008, 9, 1),
Format = Paperback,
Subject = SciFiAndFantasy,
Authors = new List<Author>() { REFeist }
});
context.Books.Add(new Book()
{
Title = "Scotland The Best",
Price = 9.49M,
Isbn = "0007442440",
PublicationDate = new DateTime(2011, 12, 8),
Format = Paperback,
Subject = Travel,
Authors = new List<Author>() { PIrvine }
});
context.Books.Add(new Book()
{
Title = "Mountain High: Europe's 50 Greatest Cycle Climbs",
Price = 10.00M,
Isbn = "0857386247",
PublicationDate = new DateTime(2011, 10, 27),
Format = Hardcover,
Subject = Travel,
Authors = new List<Author>() { DFriebe }
});
context.SaveChanges();
}
Adding the View Model
In Solution Explorer, right-click the Project and add a new folder called ViewModels. Right-click this new folder and add a new class called SearchCriteria.cs.

This class represents the search criteria provided by the user when searching the books catalogue. A new folder was created to separate the domain (database) model from the view model.
Build the application so that the user model will be available to the scaffolding wizard in the next step.
Creating the Default View
The next step is to add an action method and view to allow the user to search.
Delete the existing Views\Home\Index.cshtml file. You will create a new Index file to display the search fields.
In the HomeController class, replace the contents of the class with the following code:
Models.LibraryEntities libraryDB = new Models.LibraryEntities();
public ActionResult Index()
{
var formats = libraryDB.Formats.OrderBy(f => f.Name).ToList();
ViewBag.Format = new SelectList(formats, "FormatId", "Name");
var subjects = libraryDB.Subjects.OrderBy(s => s.Name).ToList();
ViewBag.Subject = new SelectList(subjects, "SubjectId", "Name");
return View();
}
Right-click inside the Index method and then click Add View.

Select the Create a strongly-typed view option. For Model class, select SearchCriteria. (If you don’t see SearchCriteria in the Model class box, you need to build the project.) Make sure that the View engine is set to Razor. Set Scaffold template to Details and then click Add.
Replace the contents of the newly created Views\Home\Index.cshtml file with the following:
@model Id.AjaxSearch.Example.ViewModels.SearchCriteria
@{
ViewBag.Title = "Ajax Search Example :: Search Books";
}
<h2>
Search Books</h2>
@using (Html.BeginForm())
{
<fieldset id="search">
<legend>Search Criteria</legend>
<div>
<div class="searchOption">
@Html.LabelFor(model => model.Title)
@Html.EditorFor(model => model.Title)
</div>
<div class="searchOption">
@Html.LabelFor(model => model.FormatId)
@Html.DropDownList("Format", "All Formats")
</div>
</div>
<div>
<div class="searchOption">
@Html.LabelFor(model => model.Author)
@Html.EditorFor(model => model.Author)
</div>
<div class="searchOption">
@Html.LabelFor(model => model.SubjectId)
@Html.DropDownList("Subject", "All Subjects")
</div>
<div class="searchOption">
<input type="hidden" id="StartIndex" value="1" />
<input type="button" id="Search" title="Search" value="Search" /> <img src="@Url.Content("~/Content/themes/base/images/busy.gif")" alt="Please wait..." title="Please wait..." id="waitImage" height="22" width="22" style="display: none;" />
</div>
</div>
</fieldset>
}
<div id="searchResults">
<!-- placeHolder for search results -->
</div>
Add the following to the bottom of Content\Site.css:
#search
{
padding-top: 8px;
width: auto;
font-size: 0.85em;
}
#search label
{
width: 75px;
display: inline-block;
}
.searchOption
{
margin: 4px 4px 2px 4px;
display: inline;
}
#searchResults, #searchResults table
{
width: 100%;
}
Creating the Search Results View
The next step is to add a Search action method and a partial view to display the search results.
Add the following Search method to the home controller:
[HttpPost]
public ActionResult Search(ViewModels.SearchCriteria criteria)
{
string authorCriteria = string.Empty;
string titleCriteria = string.Empty;
if (!string.IsNullOrWhiteSpace(criteria.Author)) authorCriteria = criteria.Author.ToLower().Trim();
if (!string.IsNullOrWhiteSpace(criteria.Title)) titleCriteria = criteria.Title.ToLower().Trim();
IQueryable<Models.Book> results = libraryDB.Books;
if (criteria.FormatId != 0)
results = results.Where(b => b.Format.FormatId == criteria.FormatId);
if (criteria.SubjectId != 0)
results = results.Where(b => b.Subject.SubjectId == criteria.SubjectId);
if(titleCriteria != string.Empty)
results = results.Where(b => b.Title.ToLower().Contains(titleCriteria));
if (authorCriteria != string.Empty)
{
results = (from book in results
from author in book.Authors
where author.Name.ToLower().Contains(authorCriteria)
select book).Distinct();
}
results = results.Include(b => b.Authors).OrderBy(b => b.Title);
return PartialView("SearchResults", results.ToList());
}
Right-click inside the Search method and then select Add View.

Select the Create a strongly-typed view option. Name the view SearchResults, verify that the Model class box contains Book. Make sure that the View engine is set to Razor. Set Scaffold template to List. Select the Create as a partial view option and then click Add.
Replace the contents of the newly created Views\Home\SearchResults.cshtml file with the following:
@model IEnumerable<Id.AjaxSearch.Example.Models.Book>
<div id="searchResults">
<table>
<tr>
<th>
Title
</th>
<th>
Authors
</th>
<th>
Price
</th>
<th>
Isbn
</th>
<th>
Publication Date
</th>
<th>
Format
</th>
<th>
Subject
</th>
</tr>
@foreach (var item in Model)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.Title)
</td>
<td>
<ul>
@foreach (var author in item.Authors)
{
<li>
@Html.DisplayFor(modelItem => author.Name)
</li>
}
</ul>
</td>
<td>
@Html.DisplayFor(modelItem => item.Price)
</td>
<td>
@Html.DisplayFor(modelItem => item.Isbn)
</td>
<td>
@Html.DisplayFor(modelItem => item.PublicationDate)
</td>
<td>
@Html.DisplayFor(modelItem => item.Format.Name)
</td>
<td>
@Html.DisplayFor(modelItem => item.Subject.Name)
</td>
</tr>
}
</table>
</div>
Notice that the partial view contains the same Div element that is present in the Index view that you created previously. Why this is there is discussed in the next section.
Creating the AJAX search method
Add the following to the bottom of Views\Home\Index.cshtml
<script type="text/javascript">
$(function () {
$("#waitImage").ajaxStart(function () {
$(this).show();
}).ajaxStop(function () {
$(this).hide();
});
$("#Search").click(function () {
var searchParameters = GetSearchParameters();
var jsonData = JSON.stringify(searchParameters, null, 2);
$.ajax({
url: '@Url.Content("~/Home/Search/")',
type: 'POST',
data: jsonData,
datatype: 'json',
contentType: 'application/json; charset=utf-8',
success: function (data) {
$('#searchResults').replaceWith(data);
},
error: function (request, status, err) {
alert(status);
alert(err);
}
});
return false;
});
function GetSearchParameters() {
var title = $("#Title").val();
var formatId = $("#Format").val();
var author = $("#Author").val();
var subjectId = $("#Subject").val();
if (formatId == undefined) formatId = 0;
if (subjectId == undefined) subjectId = 0;
return { Title: title,
Author: author,
FormatId: formatId,
SubjectId: subjectId
};
}
});
</script>
Performing the AJAX request is handle via jQuery. The jQuery library has a full suite of AJAX capabilities that are cross-browser.
The first step is to provide a JSON version of the SearchCriteria object that the Search method expects. This is done by constructing a JavaScript version of that object. This is achieved through the following code:
function GetSearchParameters() {
var title = $("#Title").val();
var formatId = $("#Format").val();
var author = $("#Author").val();
var subjectId = $("#Subject").val();
if (formatId == undefined) formatId = 0;
if (subjectId == undefined) subjectId = 0;
return { Title: title,
Author: author,
FormatId: formatId,
SubjectId: subjectId
};
}
Using the JSON.stringify method this is then converted into a JSON string.
Finally the Ajax call is made to the server. Note that the url parameter is set by invoking a Razor method. This allows complete portability of you application. If the method is successful, the call-back function defined as the success parameter is invoked. This function replaces the element (and all its contents) defined with the results from the Search method. This is why it is important to wrap the search results in the same element that was used as place holder in the Index.cshtml view. If you didn’t return the element you replaced then you would not be able to find it to replace it with the results from further searches.
You now have a simple but functional AJAX enabled search page.
References