I wanted to document an easy way to add auto complete functionality to a search box in ASP.NET MVC using the jQuery UI Autocomplete widget. I've tried to make the code simple while still being left with a workable example. This solution is a simplified version of this functionality I saw while watching the free ASP.NET MVC 3 video series from K. Scott Allen of Pluralsight.
Layout
First, below is the layout file we are using for this example. Nothing special here. Only thing to really note is the two optional sections: styles and scripts. These are used on the View to add specific CSS and JavaScript needed for the example.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>ASP.NET MVC Example</title>
@Styles.Render("~/Content/css")
@RenderSection("styles", required: false)
</head>
<body>
@RenderBody()
@Scripts.Render("~/bundles/jquery")
@RenderSection("scripts", required: false)
</body>
</html>
View
Now let's talk about the View. The jQuery UI AutoComplete widget is being setup in the scripts @section by calling the jQuery UI autocomplete method and passing the properties I want to use. I am setting the source to the SearchAutoCompleter action method of my SearchExample Controller (see the next section). I am also setting minLength to 2. This means the call to the action method will not happen unless the user enters at least 2 characters.
<h2>Autocomplete Enabled Search</h2>
@using (Html.BeginForm())
{
<input type="text" name="searchQuery" id="autocomplete" />
<input type="submit" value="Search" />
}
@section scripts
{
<script type="text/javascript" src="~/Scripts/jquery-ui-1.8.24.min.js"></script>
<script type="text/javascript" src="~/Scripts/jquery.unobtrusive-ajax.min.js"></script>
<script type="text/javascript">
$(document).ready(function () {
$("#autocomplete").autocomplete({
source: "@Url.Action("SearchAutoCompleter","SearchExample")",
minLength: 2
});
});
</script>
}
Controller
Below is an example of what you would have in your controller's action method. I am showing a LINQ query that could be accessing a LINQ to SQL or Entity Framework repository, etc. Here I am just getting 10 distinct items, but you can do whatever logic you need to do. Notice that I am creating an anonymous type with one property named "label". This is what the jQuery UI AutoComplete widget is expecting. Note that label is case sensitive here. Finally, the collection is returned as Json for the AutoComplete widget to process.
public class SearchExampleController : Controller
{
public JsonResult SearchAutoCompleter(string term)
{
var myObjects = (from o in myRepository.MyClass
where o.ColumnToSearchOn.Contains(term)
select new{
label = o.ColumnToSearchOn
}).Distinct().Take(10);
return Json(myObjects, JsonRequestBehavior.AllowGet);
}
}
Resources
I really like storing Session State out-of-process in a SQL Server database. However, I use custom code that switches the connection string based on the current environment my code is running in (test, stage, production, etc.). It was a little tricky to get this setup properly. But I am pretty happy with the following solution.
namespace MyApplication.Utility
{
public class SessionConnStringResolver : System.Web.IPartitionResolver
{
public void Initialize() { }
// Return the correct ASP.NET Session DB for the current running environment
public String ResolvePartition(Object key)
{
// Custom code to get connection string based on current environment
return MyConfigClass.GetConfigSettings.SessionDBConnString;
}
}
}
If you are using LINQ to SQL you should also set Serialization Mode to Unidirectional on the Object Relational Designer as discussed in this link. This allows you to serialize/deserialize your LINQ classes when saving to the database.
For a better explanation of the Partition Resolver feature of SQL Server Session State see the following links. Above is just the steps I followed to set everything up.
The jQuery UI Dialog does not seem to play nicely with the ASP.NET AJAX UpdatePanel. After doing a partial page postback using an UpdatePanel the jQuery UI Dialog div would no longer function. Thanks to Stack Overflow and some blog posts I discovered that you need to add some code to hook into the ASP.NET AJAX JavaScript pageLoaded event and this will reinitialize the jQuery UI Dialog div every time. We also need to append the div to the form element so that any ASP.NET buttons on the div will post back to our page. In the following JavaScript snippet my jQuery modal dialog div has an ID of "myDiv". Also, in this example we are setting up the dialog to be modal and autoOpen equal to false.
<script type="text/javascript">
Sys.WebForms.PageRequestManager.getInstance().add_pageLoaded(function (evt, args) {
var myDiv = $("#myDiv").dialog({ autoOpen: false, modal: true, open: function (type, data) {
$(this).parent().appendTo("form");
}
});
});
</script>
Recently I was working a project that needed a very complicated Advanced Search. I had to dynamically build the Where clause. The tricky part for this project was that the Where statement would be using both SQL ORs and ANDs. Using LINQ, the prior way I was building dynamic Where statements was just using ANDs. Basically I was just adding AND statements if they needed to be included in the query. However, this time I needed to include a clause where the user could pick one or more of an item from a list. Thankfully, I discovered the PredicateBuilder class (part of the LINQKit library), which allows you to dynamically build a Where statement with both ANDs and ORs.
I have put together a very simple and crude example to summarize what I did. Please excuse the simplicity of the code below. Usually I would write the code much cleaner, but I wanted to keep it simple for readability.
So let's say you have an advanced restaurant search form. The user must select one or more cuisine types and then can optionally choose the city, state, country where the restaurant is located. The form might look like this:
The query I was building in my project was more complicated than this, but you get an idea of how complicated these kinds of queries can get. Here's some example SQL of what LINQ might need to generate.
SELECT *
FROM Restaurant
WHERE City='Dallas'
AND State='TX'
AND(Mexican=1 OR German=1 OR Italian=1)
With the PredicateBuilder class this is totally doable. Following is the button click event for the Search button where you can get an idea of how to use PredicateBuilder. First, you build the inner OR statement and then you can add that to your full Where clause at the end. This is just a quick example, so go read the documentation for a better understanding of how PredicateBuilder works.
using LinqKit;
protected void btnSearch_Click(object sender, EventArgs e)
{
// Create Inner Where Statements
var innerWhere = PredicateBuilder.False<Restaurant>();
if (cbMexican.Checked)
innerWhere = innerWhere.Or(q => q.Mexican);
if (cbItalian.Checked)
innerWhere = innerWhere.Or(q => q.Italian);
if (cbChinese.Checked)
innerWhere = innerWhere.Or(q => q.Chinese);
if (cbGerman.Checked)
innerWhere = innerWhere.Or(q => q.German);
if (cbAmerican.Checked)
innerWhere = innerWhere.Or(q => q.American);
// Build Full Where Statement
var outerWhere = PredicateBuilder.True<Restaurant>();
if (!string.IsNullOrEmpty(ddlCountry.SelectedValue))
outerWhere = outerWhere.And(q => q.Country == ddlCountry.SelectedValue);
if (!string.IsNullOrEmpty(ddlState.SelectedValue))
outerWhere = outerWhere.And(q => q.State == ddlState.SelectedValue);
if (!string.IsNullOrEmpty(txtCity.Text.Trim()))
outerWhere = outerWhere.And(q => q.City == txtCity.Text.Trim());
// Add the inner OR query to the Where statement
outerWhere = outerWhere.And(innerWhere);
var query = from r in myDC.Restaurants.Where(outerWhere)
select r;
// Bind query to ListView, GridView, etc.
}
One of the first things I like to setup when I am creating a new site is a customized error page and error logging. To log all unhandled errors on your site you can add some code to the Application_Error event of your Global.asax file. What I do is to grab the exception and then I use a static method of a helper class I have in my class library. I pass the name of the web application where the error occurred and the exception itself. In the helper method I can then extract the details I need from the exception and log it to a database table.
Global.asax.cs:
public const string APP_NAME = "Name of My App";
void Application_Error(object sender, EventArgs e)
{
Exception ex = Server.GetLastError().GetBaseException();
SiteErrorLog.LogError(APP_NAME, ex);
}
Next, in the web.config you can setup your site so the user sees a nice, customized error page instead of the default ASP.NET error page. Under the <system.web> node, add the following:
<customErrors mode="RemoteOnly" defaultRedirect="ErrorPage.aspx"></customErrors>
Now when your site is hit from a remote client (RemoteOnly) and an unhandled error occurs they will be redirected to the page you created (ErrorPage.aspx). This is much nicer for the user of your site.
After an AJAX partial postback you may need to return to the top of your ASPX page to display an error message, etc. Here is one way that I have done it. You can add the JavaScript function below to your ASPX page and then call the method when needed in your code-behind by using the ScriptManager.RegisterClientScriptBlock method.
ASP.NET C# Code-behind:
ScriptManager.RegisterClientScriptBlock(this, Page.GetType(),
"ToTheTop", "ToTopOfPage();", true);
JavaScript:
<script type="text/javascript">
function ToTopOfPage(sender, args) {
setTimeout("window.scrollTo(0, 0)", 0);
}
</script>
You can also just JavaScript to scroll to the top of the page by using the OnClientClick property of your button. But this will cause this behavior to occur every time the button is clicked and not just when you want it to happen. For example:
<asp:Button id="bntTest" runat="server"
Text="Test" OnClick="btn_Test"
OnClientClick="javascript:window.scrollTo(0,0);" />
In the following table the page executing the code is at the following physical location on my machine:
C:\Users\me\Documents\Visual Studio 2010\Projects\TestStuff\TestStuff\Nested\Paths.aspx
Property/Method and Description | Result |
Request.ApplicationPath
Gets the root virtual path.
|
/TestStuff |
Request.CurrentExecutionFilePath
Gets the virtual path to the current file. Same as Request.Path and Request.FilePath.
|
/TestStuff/Nested/Paths.aspx |
Request.CurrentExecutionFilePathExtension
Gets the file extension of the current file.
|
.aspx |
Request.FilePath
Gets the virtual path to the current file. Same as Request.CurrentExecutionFilePath and Request.Path.
|
/TestStuff/Nested/Paths.aspx |
Request.MapPath("")
Gets the full physical path to the current directory. Seems to be the same as Server.MapPath("").
|
C:\Users\me\Documents\Visual Studio 2010\Projects\TestStuff\TestStuff\Nested |
Request.Path
Gets the virtual path to the current file. Same as Request.CurrentExecutionFilePath and Request.FilePath.
|
/TestStuff/Nested/Paths.aspx |
Request.PhysicalApplicationPath
Gets the physical path to the root directory. Seems to be the same as AppDomain.CurrentDomain.BaseDirectory.
|
C:\Users\me\Documents\Visual Studio 2010\Projects\TestStuff\TestStuff\ |
Request.PhysicalPath
Gets the full physical path to the current file.
|
C:\Users\me\Documents\Visual Studio 2010\Projects\TestStuff\TestStuff\Nested\Paths.aspx |
AppDomain.CurrentDomain.BaseDirectory
Gets the physical path to the root directory. Seems to be the same as Request.PhysicalApplicationPath.
|
C:\Users\me\Documents\Visual Studio 2010\Projects\TestStuff\TestStuff\ |
Server.MapPath("")
Gets the full physical path to the current directory. Seems to be the same as Request.MapPath("").
|
C:\Users\me\Documents\Visual Studio 2010\Projects\TestStuff\TestStuff\Nested |
Also, if you are joining a directory to a file/path I recommend using the Path.Combine method found in the System.IO namespace. Here is an example:
using System.IO;
string fileName = "MyFile.pdf";
string fullPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory,
fileName);
Following is an easy way to hide the modal popup dialog extender from the AJAX control toolkit using JavaScript. The key is to set a BehaviorID on the ModalPopupExtender. Then you can use this ID to call the hide() method via Javascript like this:
function HideModal() {
$find('modalPopupBehavior').hide();
}
In the full example below I am closing the modal popup dialog box when the user clicks on a link.
Full ASPX example:
<%@ Register Assembly="AjaxControlToolkit" Namespace="AjaxControlToolkit" TagPrefix="ajaxtk" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
<title>My Page</title>
<link href="css/Test.css" rel="stylesheet" type="text/css" />
</head>
<body>
<form id="aspNetForm" runat="server">
<asp:ScriptManager ID="scriptMan" runat="server" />
<script type="text/javascript" language="javascript">
function HideModal() {
$find('modalPopupBehavior').hide();
}
</script>
<asp:UpdatePanel ID="upnlMain" runat="server">
<ContentTemplate>
<div>
<ajaxtk:ModalPopupExtender ID="modalPopup" runat="server"
BehaviorID="modalPopupBehavior"
TargetControlID="btnPopup" PopupControlID="pnlPopup">
</ajaxtk:ModalPopupExtender>
<asp:Button ID="btnPopup" runat="server" Text="Show Popup" />
<asp:Panel ID="pnlPopup" runat="server" CssClass="modalPopup"
style="width: 715px; display: none;">
<div>
<p>
<a href="http://www.jonathanjungman.com/blog/"
target="_blank" onclick="HideModal()">Hide Modal</a>
</p>
</div>
</asp:Panel>
</div>
</ContentTemplate>
</asp:UpdatePanel>
</form>
</body>
</html>
I ran into an issue the other day where my UpdateProgress control (an animated GIF) would display behind any div that was using the DropShadowExtender control from the AJAX Control Toolkit. In order to fix this issue I set the CSS class on the div of the UpateProgress to "postion: fixed" and the "z-index" to an arbitrarily high number.
This is before the CSS change. Notice the animated GIF just peeking out from the end of the drop shadow at the bottom right of this image.
And here is what it looks like after the CSS change. The animated GIF now displays properly in front of the div using the drop shadow.
CSS:
div.divProgress { z-index: 1001; position: fixed; }
ASPX:
<%@ Register Assembly="AjaxControlToolkit" Namespace="AjaxControlToolkit" TagPrefix="ajaxtk" %>
<ajaxtk:DropShadowExtender id="dse" runat="server" TargetControlID="divPanel"
TrackPosition="true" />
<div id="divPanel" runat="server" class="Box">
<div class="BoxHeader">This Is My Header</div>
<div>
<p>Text here. More Text. Whoa. Etc...</p>
<p>
<asp:Button ID="btnDoSomething" runat="server" Text="Do Something"
onclick="btnDoSomething_Click" />
</p>
</div>
</div>
<ajaxtk:AlwaysVisibleControlExtender ID="AlwaysVisibleControlExtender" runat="server"
TargetControlID="updateProgress" HorizontalSide="Center" VerticalSide="Middle" />
<asp:UpdateProgress ID="updateProgress" runat="server" DynamicLayout="true" >
<ProgressTemplate>
<div class="divProgress" id="divProgress" runat="server" align="center">
<img src="~/images/red_rotation.gif" id="imgUpdateProgress"
runat="server" alt="Please Wait..." />
</div>
</ProgressTemplate>
</asp:UpdateProgress>
I got this error in Visual Studio when I was running one of our projects on a new development machine where I had just recently installed a fresh copy of Visual Studio 2008. After installing the Microsoft .NET SDK this error went away. Hopefully this post helps someone else out there.