Creating a Site Search Widget

JavaScript EditorFreeware JavaScript Editor     Ajax Tutorials 



Main Page

Previous Page
Next Page

Creating a Site Search Widget

Search functionality is an integral part of any web site; it enables your viewers to find the data they desire quickly and easily. However, conventional search mechanisms suffer from the same problems as the rest of the Web: they require a page refresh and possibly losing data when the search is performed.

Within the past year, many Ajax solutions have cropped up with one standing out above the rest: LiveSearch. LiveSearch (http://blog.bitflux.ch/wiki/LiveSearch), developed by the people at BitFlux (www.bitflux.ch), is a search-as-you-type solution to emulate the Apple Spotlight feature in OSX Tiger.

LiveSearch presents a new take on web site searches, but it also has its critics. For one, it offers a different approach to achieving the desired results. The average user is used to entering his or her search criteria and pressing a Search button. LiveSearch, on the other hand, uses the onkeypress DOM event and returns the results as you type. This method may be more efficient in terms of getting your results, but it is unexpected by the user, which can cause confusion.

This next widget is an Ajax solution for searching a site that uses a SQL database as a data store. It will feature a user interface that users are already accustomed to: a search box, a submit button, and a <div/> element that displays the search results (see Figure 8-5).

Image from book
Figure 8-5

The Server-Side Component

You will use the .NET Framework and C# language to interface with an MSSQL database to perform the search query. The returned results will be in JSON, thanks to the JSON C# library mentioned in Chapter 7 (www.crockford.com/JSON/cs/). The code in the following sections will use a SQL query to search a database containing the posts on a web log (blog).

The Database Information

The database table for this specific blog is relatively simple. It contains four columns: id, title, date, and posting. The following SQL statement creates this table, called BlogPosts:

CREATE TABLE BlogPosts (
    id int IDENTITY (1, 1) NOT NULL,
    date datetime NOT NULL,
    title text NOT NULL,
    posting text NOT NULL
)

When this query runs, it creates the table, but it is empty. The code download for this example, located at www.wrox.com, contains a SQL file that will add records to the table.

There are primarily two pieces of desired information needed for the search results: id (used in the URL to the blog post) and title. With this knowledge, you can create the search query:

SELECT TOP 10
id, title
FROM BlogPosts
WHERE posting LIKE '%SEARCHSTRING%' OR
title LIKE '%SEARCHSTRING%'
ORDER BY date DESC

This query selects the id and title columns from the BlogPosts table where the contents of posting and title contain an instance of the search string. The results returned are ordered by descending date, and only ten records are returned.

Before delving into the code, however, the returned data structure needs discussing.

The Returned Data Structure

The only two pieces of information retrieved are the blog post ID and its title, and a result of a search could contain more than one blog post. At this point, you need a result set of objects that each contains title and id properties:

[
    {
        title : "Title 1",
        id    : "1"
    },
    {
        title : "Title 2",
        id    : "2"

    },
    {
        title : "Title 3",
        id    : "3"
    }
]

This code illustrates what the data returned from the server looks like. The structure is an array containing several objects, each with title and id properties. With the result set mapped out, you can now approach the SiteSearch class.

The SiteSearch Class

This widget uses a simple class called SiteSearch to connect to the database, perform the search query, and return the results as a JSON string. This class has one private field _conString, the connection string to the MSSQL database, and accepts a string argument containing the connection string:

private string _conString;

public SiteSearch(string connectionString)
{
    _conString = connectionString;
}

As you can see, the constructor is extremely simple. The only operation performed is assigning _conString the value of the connectionString argument. As you've probably already guessed, the class's sole method, Search(), is the meat and potatoes of the class.

The Search() method accepts one argument: the search string. This search string is used in a String.Format() method to format the search query:

public string Search (string searchString)
{
    string query = String.Format("SELECT TOP 10 id, title FROM BlogPosts
        WHERE posting LIKE '{0}%' OR title LIKE '{0}%' ORDER BY date DESC",
        searchString);

The variable query now holds the completed query string used later in the method.

The JSON C# library provides the ability to build JSON strings dynamically. Two classes are used to create these strings: JSONArray and JSONObject. As you might have guessed, JSONArray creates an array, whereas JSONObject creates an object. Referring back to the returned data structure, remember the client code expects an array of objects. Therefore, the next step is to instantiate a JSONArray object:

Nii.JSON.JSONArray jsa = new Nii.JSON.JSONArray();

For the moment, this is all the JSON code used, and you won't see it again until further in the database operation.

Next, create a database connection and a SqlCommand:

using (SqlConnection conn = new SqlConnection(_conString))
{
    SqlCommand command = new SqlCommand(query, conn);
    conn.Open();

This code creates the database connection using the connection string stored in _conString. A new SqlCommand is then created using the search query and database connection. This essentially prepares the query to run against the database, which the next few lines illustrate:

using (SqlDataReader reader = command.ExecuteReader())
{


}

This code creates a SqlDataReader returned by the ExecuteReader() method. This data reader provides the ability to read the result from the executed database query. Before any data is retrieved from the data reader, you should first check if the result contains any rows. You can do this using the HasRows property:

using (SqlDataReader dataReader = command.ExecuteReader())
{
    try {
        if (reader.HasRows)
    {
            int i = 0;
            while (reader.Read())
            {

            }
        }
    catch {}
}

This code uses the HasRows property to determine if the data reader contains any rows. If so, the processing of the contained data can begin.

The while loop is used to read the result set through the Read() method of the SqlDataReader class. The Read() method returns a Boolean value; when the end of the result set is reached, Read() returns false and the loop exits. Inside the loop, you create a JSONObject object and populate it with its properties and their values with the put() method. The put() method of the JSONObject class accepts two arguments: a string containing the property name and that property's value. To access the database column's value, pass the column name as the index to dataReader:

using (SqlDataReader dataReader = command.ExecuteReader())
{
    try {
        if (reader.HasRows)
        {

            int i = 0;
            while (reader.Read())
            {
                JSONObject jso = new JSONObject();
                jso.put("title",reader["title"]);
                jso.put("id",reader["id"]);

                jsa.put(i++,jso);
            }
        }
    catch {}
}

When the JSONObject is populated, you add it to the JSONArray object created earlier. The JSONArray class exposes a put() method as well, but the arguments are a bit different. The first is the index of the existing element of the array, while the second is the JSONObject object.

When the loop exits, the JSONArray is completed and ready for returning. But what if the query returned no results? The client-side code handles this functionality. In the event that no rows are found from the query, an empty array is returned. The client-side code will check the length of the result set, and perform the necessary operation.

At this point, the JSON string construction is complete. To retrieve the string representation of the JSONArray, use the ToString() method:

return jsa.ToString();

This last line of the Search() method returns the JSON string that can be written into the page.

Building the Search Page

With the working class completed, all that remains on the server-side is the search page. This page will accept an argument in the query string called search, which contains the search term. An example query string could look like the following:

http://yoursite.com/search.aspx?search=ajax

To provide this functionality, check for the existence of this parameter by using the Response.QueryString collection:

Response.CacheControl = "no-cache";
Response.ContentType = "text/plain; charset=utf-8";

if (Request.QueryString["search"] != null)
{

}

The first two lines should be familiar to you by now. The first line sets the CacheControl header to no-cache so that the browser will not cache the data, while the second sets the MIME ContentType to text/plain with UTF-8 encoding.

Note

As you learned in Chapter 7, plain text with Unicode encoding is the desired content-type for JSON strings.

After the headers are set, the existence of the search parameter in the query string is checked. Inside the if code block, a SiteSearch object is instantiated:

if (Request.QueryString["search"] != null)
{
    string searchTerm = Request.QueryString["search"];
    string conStr = "uid=sa;pwd=pass;data source=localhost;initial
        catalog=BlogPosts";

    SiteSearch siteSearch = new SiteSearch(con);
}

The first new line creates the searchTerm variable, a string storing the value of the search parameter in the query string, followed by the database connection string.

Connecting to an MSSQL database in C# is quite different from connecting to a MySQL database with PHP, demonstrated in Chapter 7. In PHP, the user name, password, and database host name were passed to the mysql_connect() function. In C# (and other .NET languages, for that matter), this information is contained in what is called a connection string. As seen in the previous example, this connection string contains name/value pairs separated by semicolons. The names in the preceding sample mean the following:

  • uid: The user name for the particular database. In the previous example, sa is a built-in user meaning Super-Admin.

  • pwd: The password to connect to the database.

  • data source: The host name or IP address of the database server.

  • initial catalog: The database name your queries are run against.

Of course, the information in the connection string should reflect the login information of your own MSSQL database; therefore, feel free to change the information wherever needed. Changing this information requires you to recompile the web application; however, a widget should be ready to use out of the box (or ZIP file), so a different approach is desirable.

The web.config file of an ASP.NET application contains configuration settings available for accessing within the application. This file's purpose is twofold:

  1. It provides a central location for all application settings.

  2. It provides a secure means of storing settings that contain sensitive data (such as database information), as the web server will not serve .config files.

Inside the web.config file, you'll notice that it is nothing more than an XML file with the document root being <configuration/>. To add your own custom settings, add an element called <appSettings/> as the first child of <configuration/>:

<configuration>
    <appSettings>


    </appSettings>

To add a setting, use the <add/> element, which has key and value attributes:

<appSettings>
    <add key="connectionStr" value="uid=sa;pwd=pass;data source=localhost;
            initial catalog=BlogPosts"/>
</appSettings>

With the connection string now a part of the application settings, the if block can be changed to the following:

if (Request.QueryString["search"] != null)
{
    string searchTerm = Request.QueryString["search"];
    string conStr = ConfigurationSettings.AppSettings["connectionStr"];

    SiteSearch siteSearch = new SiteSearch(conStr);
}

This code is slightly different from the code earlier. Instead of hard coding the database information, the information is pulled from the application settings with the ConfigurationSettings.AppSettings collection. Pass the value from the key attribute in the web.config file to the AppSettings collection, and the value is retrieved.

Important

The ConfigurationSettings object is from the System.Configuration namespace, so you need to add System.Configuration to the using statements at the beginning of the class file.

With the SiteSearch object instantiated, the final steps inside the if block is to perform the search and output the results to the page:

if (Request.QueryString["search"] != null)
{
    string searchTerm = Request.QueryString["search"];
    string conStr = ConfigurationSettings.AppSettings["connectionStr"];

    SiteSearch siteSearch = new SiteSearch(conStr);

    string json = siteSearch.Search(searchTerm);
    Response.Write(json);
}

The new lines in this code call the Search() method to perform the search. The resulting JSON string is returned and stored in the json variable, which is then written to the page via the Response.Write() method.

Note

You could add an else block to handle the case when the search parameter in the query string does not exist; however, the client-side code will handle form validation making it unnecessary to do so.

With the .aspx file complete, the final code looks like this:

Response.CacheControl = "no-cache";
Response.ContentType = "text/plain; charset=utf-8";

if (Request.QueryString["search"] != null)
{
    string searchTerm = Request.QueryString["search"];
    string conStr = ConfigurationSettings.AppSettings["connectionStr"];

    SiteSearch siteSearch = new SiteSearch(conStr);

    string json = siteSearch.Search(searchTerm);
    Response.Write(json);
}

The Client-Side Component

Client functionality is overly important, especially for a widget such as this. Before using the search capabilities of your site, the user already made the assumption of how it works. Therefore, it is important to follow a couple of guidelines:

  • The user will enter text to search and press either Enter or the Submit button. The search-as-you-type feature of LiveSearch is revolutionary, but it goes against what the user is already accustomed to. Near instantaneous results without a page refresh is enough new functionality.

  • The user expects to be told when no results are found. If you'll remember from the SiteSearch class, an empty JSON array is returned when no results are found; therefore, this responsibility is passed to the client code.

These guidelines may seem like a no-brainer, but it is important to consider the user's experience. What's hip and cool isn't necessarily always the right thing to do.

The User Interface

The first step in any client-side component is to build the user interface with HTML. For this widget, you will use four elements contained within a <div/> element:

<div class=" ajaxSiteSearchContainer">
    <form class=" ajaxSiteSearchForm">
        <input class=" ajaxSiteSearchTextBox" />
        <input type=" submit" value=" Go" class=" ajaxSiteSearchButton" />
    </form>
    <div class=" ajaxSiteSearchResultPane">
        <a class=" ajaxSiteSearchLink" href=" http://yoursite.com">Result Text</a>
    </div>
</div>

Every element contains a class attribute. This ensures that the widget is easily customizable with CSS, and you can tailor it to fit in almost every web site.

Of course, you will not add this HTML directly into the HTML code of your web site; JavaScript dynamically creates the HTML and appends it to the desired HTML element.

The AjaxSiteSearch Class

The AjaxSiteSearch class encapsulates everything needed to display the user interface, make requests to the server, and display the server's response, aside from the CSS information and other dependencies. The class's constructor accepts one argument, an HTML element to append the search user interface:

function AjaxSiteSearch(oElement) {

}

The first step is to write the HTML elements that make up the user interface. This is done, naturally, with DOM methods:

var oThis = this;
this.result = null;

this.widgetContainer = document.createElement("div");
this.form = document.createElement("form");
this.textBox = document.createElement("input");
this.submitButton = document.createElement("input");
this.resultPane = document.createElement("div");

this.widgetContainer.className = "ajaxSiteSearchContainer";
this.form.className = "ajaxSiteSearchForm";
this.textBox.className = "ajaxSiteSearchTextBox";
this.submitButton.className = "ajaxSiteSearchButton";
this.resultPane.className = "ajaxSiteSearchResultPane";

this.submitButton.type = "submit";
this.submitButton.value = "Go";

The first line of this code creates the oThis variable, a pointer to the object. This comes in handy later in the constructor. The following lines create the needed HTML elements and assign their class names, and the Submit button's type and value attributes are set to submit and Go, respectively.

When the user clicks the Submit button or presses the Enter key when the form has focus, the form's onsubmit event fires. The following event handler will start the search process:

this.form.onsubmit = function () {
    oThis.clearResults();

    return false;
};

This is where the object pointer's use is required. Inside the onsubmit event handler, the scope changes; so, this references the <form/> element instead of the AjaxSiteSearch object. Because the event handler makes calls to the AjaxSiteSearch object, an external variable referencing the object is required, and that is what oThis does.

The first line of the onsubmit event handler calls the object's clearResults() method. This method, covered later, removes any links in the results list from a prior search. This ensures that only the results from the current search request are displayed. In the last line, the value of false is returned. This overrides the form's default behavior, which is to submit the form.

Also during the onsubmit event, the form is validated. If the text box does not contain any text, the user is notified that no text is entered:

this.form.onsubmit = function () {
    oThis.clearResults();

    if (oThis.textBox.value != "") {
        oThis.search();
    } else {
        alert("Please enter a search term");
    }

    return false;
};

If text is entered, however, the object's search() method is called. This method, also covered shortly, is responsible for retrieving the search term and making the XMLHttp request to the server.

With the elements created and the onsubmit event handler written, all that remains in the constructor is appending the elements to the document:

this.form.appendChild(this.textBox);
this.form.appendChild(this.submitButton);
this.widgetContainer.appendChild(this.form);
this.widgetContainer.appendChild(this.resultPane);

var oToAppend = (oElement)?oElement:document.body;
oToAppend.appendChild(this.widgetContainer);

Because this widget appends itself to the given HTML element, it is a good idea to create an AjaxSiteSearch object during the page's onload event. Otherwise, the desired destination element could not exist, thus throwing an error.

Clearing the Results

The clearResults() method is a simple method to remove all child nodes in the results <div/> element:

AjaxSiteSearch.prototype.clearResults = function () {
    while (this.resultPane.hasChildNodes()) {
        this.resultPane.removeChild(this.resultPane.firstChild);
    }
};

This method utilizes the hasChildNodes() method, a method exposed by a node in the DOM. As long as the results <div/> contains children, the first child is removed. It is a simple method, but it gets the job done.

Making the XMLHttp Request

As stated before, the search() method makes the request to the server to perform the search:

AjaxSiteSearch.prototype.search = function () {
    var oThis = this;
    var sUrl = encodeURI("search.aspx?search=" + this.textBox.value);

    var oReq = zXmlHttp.createRequest();
    oReq.onreadystatechange = function () {
        if (oReq.readyState == 4) {
            if (oReq.status == 200) {
                oThis.handleResponse(oReq.responseText);
            }
        }
    };

    oReq.open("GET", sUrl, true);
    oReq.send();
};

The familiar first line creates a pointer to the object used inside of the XMLHttp object's onreadystate-change event handler. The second line encodes the search URL and search term with the encodeURI() function. Doing this replaces certain characters with their appropriate escape sequence (for example: white space is turned into %20).

The remainder of the code performs the XMLHttp request. On a successful request, the responseText is passed to the handleResponse() method.

Processing the Information

The handleResponse() method takes the server's response (the JSON string), decodes it, and displays the results as links in the results <div/> element. This method accepts one argument, a JSON string:

AjaxSiteSearch.prototype.handleResponse = function (sJson) {
    this.result = JSON.parse(sJson);
};

This code takes the sJson argument and passes it to the JSON.parse() method to convert the string into JavaScript.

Now that the information can be used programmatically, the code begins to go through a decision-making process. Remember, the result from the server is an array of objects; therefore, you can use the length property to determine if the search returned any results:

AjaxSiteSearch.prototype.handleResponse = function (sJson) {
    this.result = JSON.parse(sJson);

    if (this.result.length > 0) {
        //Results go here
    } else {
        alert("Your search returned no results.");
    }
};

Naturally, if any results are present, you want to display that information. If not, the user is notified through an alert box that no results were found from their search.

Displaying the results is as simple as creating <a/> elements:

AjaxSiteSearch.prototype.handleResponse = function (sJson) {
    this.result = JSON.parse(sJson);

    if (this.result.length > 0) {
        var oFragment = document.createDocumentFragment();
        for (var i = 0; i < this.result.length; i++) {
            var linkResult = document.createElement("a");
            linkResult.href = "http://yoursite.com/?postid=" + this.result[i].id;
            linkResult.innerHTML = this.result[i].title;
            linkResult.className = "ajaxSiteSearchLink";

            oFragment.appendChild(linkResult);
        }

        this.resultPane.appendChild(oFragment);
    } else {
        alert("Your search returned no results.");
    }
};

The first new line of code creates a document fragment to append the <a/> elements to. The next block of code, a for loop, generates the links. Notice the assignment of the href property of the link. In order for this to work on your site, you must change the href value to reflect your own web site.

When the link creation is complete, it is added to the document fragment, which is appended to the results <div/> element. These links remain visible until a new search is performed, which will clear the results pane and populate it with new results.

Customizing the Site Search Widget

To make the Site Search widget conform to your page's look and feel, it was designed to be fully customizable. Every element in the widget has a corresponding CSS classification, making customization a snap.

The outermost <div/> element, the widget's container, has the CSS class ajaxSiteSearchContainer. You can give your search widget its overall look with this element; you can also set a global style, since all elements will inherit many of its style properties:

div.ajaxSiteSearchContainer
{
    background-color: #fdfed4;
    border: 1px solid #7F9DB9;
    font: 12px arial;
    padding: 5px;
    width: 225px;
}

The first two lines set the background color of the element and its border, respectively. These two properties give the visual idea that everything within the border and background color belongs to the widget. This can be helpful to the user, especially when text seems to appear from the nether. The next line sets the font-size and -family for the widget. This setting is inherited by the results <div/> element and the links it contains. The 5-pixel padding is mainly for visual purposes; otherwise, everything could look scrunched together. Last, the width is applied to the widget. This confines the widget into a specified space, and text will wrap accordingly.

The <form/> element also possesses the ability for styling. The given class name for this element is ajaxSiteSearchForm:

form.ajaxSiteSearchForm {}

This example does not apply any style to the element, but the ability exists to do so. By applying padding or a border (or any other style for that matter), you can give the visual impression of separating the form from the results.

The <form/> contains two child elements, the text box and the Submit button, both of which are <input/> elements:

input.ajaxSiteSearchTextBox
{
    border: 1px solid #7F9DB9;
    padding: 2px;
}

input.ajaxSiteSearchButton
{
    background-color: #7F9DB9;
    border: 0px;
    color: white;
    font-weight: bold;
    margin-left: 3px;
    padding: 1px;
}

The text box's CSS (class ajaxSiteSearchTextBox) gives the box a solid, colored border 1 pixel in width and pads the contents by 2 pixels. The button, whose class name is ajaxSiteSearchButton, has a background color and no border but is padded on all sides by one pixel. The text inside the button is white and bold. It is scooted 3 pixels to the right by setting its left margin to 3 pixels.

The results <div/> in this example does not have any styling. Instead, it inherits the font-size and -family from its parent:

div.ajaxSiteSearchResultPane {}

The final elements in this widget are the links:

a.ajaxSiteSearchLink
{
    color: #316ac5;
    display: block;
    padding: 2px;
}

a:hover.ajaxSiteSearchLink
{
    color: #9b1a1a;
}

In the example CSS, only two states are styled: a normal link and a link when the mouse hovers over it. In the former (default) state, the links are treated as block-level elements, meaning that each link starts on its own line. They have a bluish color and contain two pixels of padding. When the user mouses over a link, the hover state is activated. The only style change made is the color, which turns the text color from bluish to reddish.

These style properties are for example only; the real fun with widgets is making them your own and fitting them into your own page. Feel free to bend these elements to your will.

Implementing the Site Search Widget

Much like the weather widget, your options of implementation are twofold:

  1. You can add the class to an already existing ASP.NET-enabled web site. Doing so would require a recompilation of your code. Doing so, however, would require you to recompile your code and modify the search.aspx page to fit your namespace.

  2. You can use the code contained in the downloadable examples as its own free-standing mini application. You can follow the steps outlined at the end of Chapter 5 on how to do this.

Just like the weather widget, the choice of implementation is yours; however, the remainder of the code assumes you chose the latter option. On the client page, you need to reference all files needed to use this widget. The AjaxSiteSearch class depends on the zXml and JSON libraries to function properly:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
        "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns=" http://www.w3.org/1999/xhtml" >
<head>
    <title>Ajax SiteSearch</title>
    <link rel=" stylesheet" type=" text/css" href=" css/ajaxsitesearch.css" />
    <script type=" text/javascript" src=" js/json.js"></script>
    <script type=" text/javascript" src=" js/zxml.js"></script>
    <script type=" text/javascript" src=" js/ajaxsitesearch.js"></script>
</head>
<body>

</body>
</html>

To instantiate an AjaxSiteSearch object, use the new keyword:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
        "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns=" http://www.w3.org/1999/xhtml" >
<head>
    <title>Ajax SiteSearch</title>
    <link rel=" stylesheet" type=" text/css" href=" css/ajaxsitesearch.css" />
    <script type=" text/javascript" src=" js/json.js"></script>
    <script type=" text/javascript" src=" js/zxml.js"></script>
    <script type=" text/javascript" src=" js/ajaxsitesearch.js"></script>
    <script type=" text/javascript">
    function init() {
        var oSiteSearch = new AjaxSiteSearch();
    }

    onload = init;
    </script>
</head>
<body>

</body>
</html>

When creating the AjaxSiteSearch object, pass the desired element you want the widget to be appended to. If no HTMLElement is passed to the constructor, it is appended to the document.body. Creating an object automatically generates the required HTML elements, so there is nothing left to do to implement the widget.


Previous Page
Next Page




JavaScript EditorAjax Editor     Ajax Validator


©