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
First off, I want to start by saying I don't know a whole lot about Reporting Services.
We recently had an issue where a staff member told us that they hadn't been receiving a certain report. When I logged onto Reporting Services and tested the connection to the data source I was getting "Unspecified Error". Not too helpful. I then tried to find the last successful run. I could not find this in the Reporting Services interface. However, there is a view named ExecutionLog2 that had what I wanted.
SELECT TOP 100 elog.status, elog.TimeStart, elog.*
FROM [dbo].[ExecutionLog2] elog
WHERE elog.reportPath = 'MyReportPath'
AND elog.UserName = 'MyReportUser'
ORDER BY elog.timestart DESC
We actually ended up restarting the Reporting Services service and the "Unspecified Error" went away. I just thought I'd post this in case it helps anyone else.
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);" />
The FreeTextTable function in SQL Server is very useful when you need to search a column for words, phrases, etc. FreeTextTable will return a table that you can join on. It also returns the Rank of the matches so you can order the most relevant matches at the top.
In the example below I am searching the Keywords column of the MyTable table using the @Keywords passed into the stored procedure. I am then ordering them by Rank so the highest matches show up first.
CREATE PROCEDURE [dbo].[FreeTextSample]
@Keywords [varchar](1000) = ''
WITH EXECUTE AS CALLER
AS
BEGIN
SET NOCOUNT ON;
SELECT myTable.MyPrimaryKeyID,
myTable.Title,
myTable.Description
FROM MyTable myTable(nolock)
INNER JOIN FreeTextTable(dbo.MyTable, Keywords, @Keywords) AS searchTable
ON myTable.MyPrimaryKeyID = searchTable.[KEY]
ORDER By searchTable.[RANK] DESC
END
GO
One thing to note is that LINQ does not contain an equivalent keyword for FreeTextTable. So what I did was to create a new method on my DataContext that returns the results of the stored procedure. See my post here for further details on how to do this.
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>