Introduction
ASP.NET AJAX applications are very dynamic and rich, creating a new generation of web applications. This article illustrates some of the tips
and tricks to creating rich features in an application.
Setting Up the Project
The first step in setting up the project is to add a new project, which will be the web site (notice I started out with a blank solution).
This article uses the web application project template; the web site template can be used by selecting Add > New Web Site option instead.
I tend to prefer the web application project model.
The next step is to setup the web site by giving it a name of WebSiteStarterKit. Notice I'm using the ASP.NET Web Application option.
Since this is a .NET 3.5 framework project, the project template sets up some of the AJAX features and .NET 3.5 components. Take a look at
the web.config file to see these new settings if you are unfamiliar with them.
Let's start by adding an AJAX master page. To create the master page, select the AJAX master page option, and give it a name of Site.Master.
This template definition is a typical master page format, plus a reference to the ScriptManager. Take a look at the following script definition.
<%@ Master Language="C#" AutoEventWireup="true" CodeBehind="Site.master.cs" Inherits="WebSiteStarterKit.Site" %>
<html xmlns="http://www.w3.org/1999/xhtml" >
<head id="Head1" runat="server">
<title>Untitled Page</title>
<asp:ContentPlaceHolder ID="headPlaceholder" runat="server">
</asp:ContentPlaceHolder>
</head>
<body>
<form id="form1" runat="server">
<div>
<asp:ScriptManager ID="ScriptManager1" runat="server" />
<asp:ContentPlaceHolder ID="bodyPlaceholder" runat="server">
</asp:ContentPlaceHolder>
</div>
</form>
</body>
</html>
This is the base template of the initial master page; however, to incorporate a wide array of features, I may create multiple master pages.
Multiple master pages create a personalized approach, and is expandable.
Alright, it's time to build in some features.
Modal Popups
Let's start with the first feature of modal popups through the ModalPopupExtender. This extender pops up a panel whenever the target control
is clicked. The target is usually a button, but in this case the target is the login status control (which under the hood is a button).
When the login status is clicked, it pops up a panel to login (for logging in only).
<asp:LoginStatus ID="lgsLogin" runat="server" CssClass="SiteLayoutMenuBarItem" />
<asp:Panel ID="pnlLogin" runat="server" CssClass="ModalPopupPanelBackground">
Please login to the application.<br /><br />
<asp:Login ID="lgnLogin" runat="server" />
<br /> <hr /> <br />
Use the form below to email your password to you.<br /><br />
<asp:PasswordRecovery ID="prLogin" runat="server" />
<asp:Button ID="btnCancel" runat="server" Text="Cancel" />
</asp:Panel>
<ajax:ModalPopupExtender ID="mpeLogin" runat="server" TargetControlID="lgsLogin"
PopupControlID="pnlLogin" CancelControlID="btnCancel" BackgroundCssClass="ModalPopupBackground" />
The ModalPopupExtender targets the LoginStatus control, the first element, through its TargetControlID. It knows to popup a Panel, the
second element, because the PopupControlID is set to its ID. Now, instead of the default click action, the modal popup appears when the
LoginStatus is clicked, and allows the user to login through a modal interface.
The modal interface allows the user to login or recover their password. The action can be cancelled using the cancel button. The
ModalPopupExtender can target an OK and Cancel button for the modal window, which will close the window when clicked.
Let's take a look at what this looks like in the application. When the button is clicked, the following screen appears.
Notice the gray background; this background covers the entire screen, preventing the user from making any modifications to the background
information. The user has to focus their attention solely on the login/password recovery process at hand.
Selecting a Theme
The next process discussed is selecting a theme for the site. Some web sites allow you to choose the theme of the site, which changes the
color style of the site to a different color. This is a nice feature to have for a site, so we'll implement that here.
The .NET 2.0 framework incorporated the idea of themes into a site, which allows the site to be personalized with both client-side CSS and
server-side Skins, a new way to apply styles using the .NET server control conventions. If you haven't seen a server-side skin, let's take
a look at the following skin for the Login control.
<asp:Login runat="server" BackColor="#EFF3FB" BorderColor="#B5C7DE" BorderPadding="4"
BorderStyle="Solid" BorderWidth="1px" ForeColor="#333333">
<TextBoxStyle />
<LoginButtonStyle BackColor="White" BorderColor="#507CD1" BorderStyle="Solid"
BorderWidth="1px" ForeColor="#284E98" />
<InstructionTextStyle Font-Italic="True" ForeColor="Black" />
<TitleTextStyle BackColor="#507CD1" Font-Bold="True" ForeColor="White" />
</asp:Login>
A skin can assign a value to any property that has the Themeable attribute set, which isn't always known. The way to know whether this
works or not is to set the value in the skin and run the application. If you can't assign a property value using the skin, the compiler
will let you know.
A personalized skin for a specific situation can be assigned by adding the SkinID attribute. If a skin has a SkinID specified, any control
that has a matching SkinID (2.0 also added the SkinID property at the control level) will apply that skin. This skin overrides the default
skin applied.
CSS styles work the same way, except they set client-side attribute values instead. For instance, the following defines two CSS classes.
hr
{
border:solid 1px navy;
width:1px;
}
.ModalPopupBackground
{
background-color: #C0C0C0;
}
.ModalPopupPanelBackground
{
background-color: #FFFFCC;
color: #000080;
border: solid 1px #000080;
}
The period denotes a CSS class that's applied through the HTML class property, whereas the first style works with the HR tag simply by
matching the name.
All of these styles makes up a theme, which this example will allow the user to change. The interface for changing the themes appears below.
<asp:DropDownList ID="ddlTheme" runat="server" CssClass="SiteLayoutMenuBarDropDown"
DataTextField="FriendlyName" DataValueField="Name"
onchange="themeDropDown_change"></asp:DropDownList>
<asp:LinkButton ID="lnkThemeChange" runat="server" CausesValidation="false"
CssClass="SiteLayoutMenuBarItem">Change Theme</asp:LinkButton>
Clicking on the "change theme" button posts back to the server to show the new theme. There is a two-step process for changing the theme
of a site dynamically. The first challenge is accessing the value of the selected theme and storing it for the post back. The second
option is loading the stored value and changing the theme.
The challenge with this is that changing themes at runtime has to happen during PreInit, which can be a challenge. Before getting to
this, let's look at the approach used.
Searching
Most applications have some sort of search capability built into a site. This makes it convenient to quickly access related content that
is being sought for. Sometimes it can be helpful to see what has been searched for in the past, along with how many instances of that
search item has been found.
The following markup exists in the master page, a perfect place to put a search textbox.
<asp:TextBox ID="txtSearch" runat="server" />
<ajax:TextBoxWatermarkExtender ID="extSearch" runat="server" TargetControlID="txtSearch"
WatermarkText="Type to Search" />
<ajax:AutoCompleteExtender ID="extSearch2" runat="server" TargetControlID="txtSearch"
ServicePath="~/Services/SearchService.asmx" ServiceMethod="GetTopSearchPhrases"
MinimumPrefixLength="1" CompletionListItemCssClass="AutoCompleteItemStyle"
CompletionListHighlightedItemCssClass="AutoCompleteSelectedItemStyle"
OnClientItemSelected="autoComplete_itemSelected" />
<asp:LinkButton ID="lnkSearch" runat="server" CssClass="SiteLayoutMenuBarItem"
OnClick="lnkSearch_Click">Search</asp:LinkButton>
There's quite the number of .NET controls for implementing a search. Outside of the textbox that retains search criteria and a
linkbutton that triggers the search, the two extenders help add on additional functionality without requiring a custom control.
The TextBoxWatermarkExtender helps by providing a helpful message to the user identifying the purpose of the search textbox. The
second extender, the AutoCompleteExtender, is a powerful extender that queries the database, looking for existing search results
and returning the list.
The AutoCompleteExtender works by using a web service and web method to get the items that match the text that the user has entered.
The web service has to conform to a specific signature for this to work, which I'll discuss in a moment.
As the user clicks the submit button, whatever is searched for is logged to the database. There are several schemes that can be
implemented to track what the user searches for, and create a popularity ranking. The first question you have to ask is shall whatever
the user enters be logged, or only searches that have results? Should low to high volume results be differentiated?
In this example, whatever the user enters is stored in the database, but more ideally any search text that has actual results should
be stored. This could be done whenever loading the search page, or search results.
Another question is what is the search querying? Is the search using a full-text index in the database, index server, or some other
mechanism? This is important too, though I do not have a solution implemented in this example; I'm only focused on storing the user's query.
Let's start with the web service definition. As I mentioned before, a web service used by the AutoCompleteExtender needs to conform
to a specific signature, shown below.
[WebMethod]
public string[] GetTopSearchPhrases(string prefixText, int count)
{
SearchManager manager = SearchManager.GetManager();
SearchPhraseCollection phraseCollection = manager.GetTopSearchPhrases(prefixText, count);
return (from p in phraseCollection
select string.Format("{0} ({1} occurrence(s))", p.Phrase, p.ResultCount.ToString())).ToArray();
}
The method takes two to three parameters, depending on the configuration. In this example, the method takes the prefixText, which is
the characters the user has entered, and the count, the total number of items to limit the search by. The latter value is specified
in the body of the AutoCompleteExtender; it's configurable by changing the CompletionSetCount property value.
If the UseContextKey property of that extender is set to true, a third parameter, contextKey, can be added as well. Notice that an
array of strings is returned to the caller. This is also required. Notice the specialized output, which will include items in the
format of "Toasters (12 occurrence(s))".
As the user enters keys, the user is prompted with a selection of items as shown in the screenshot below.
These entries can be selected using the mouse, or by clicking the up and down arrow. Selecting an entry places the item in the textbox.
These entries are stored in the database using the following code in the search button click event (next to the textbox, but not shown above).
protected void lnkSearch_Click(object sender, EventArgs e)
{
if (string.IsNullOrEmpty(this.txtSearch.Text))
return;
string text = this.txtSearch.Text.Trim();
if (string.IsNullOrEmpty(text))
return;
//Updates the search phrase count.
SearchManager manager = SearchManager.GetManager();
manager.UpdateSearchPhraseCount(text);
}
The SearchManager component makes a call to the data layer to perform the update. If the searched text already exists, then the number
of occurrences is incremented by one; otherwise, a new entry is created. There is only one problem not yet discussed. Upon selecting
an item in the auto complete popup, the text "(X occurrences)" would also be copied to the textbox. This is not very useful, and needs
to be stripped out.
The solution to this is to attach to the itemSelected client event. Notice in the AutoCompleteExtender markup above that the
OnClientItemSelected references the name of an event handler. This event handler performs the work of stripping out the
"(X occurrences)" text from the selected entry. It does this with the following JavaScript.
function autoComplete_itemSelected(sender, e)
{
var text = e.get_text();
if (text.indexOf('(') > 0)
{
text = text.substring(0, text.indexOf('('));
if (text != null && text.length > 0)
text = text.trimEnd();
}
var targetElement = sender.get_element();
targetElement.value = text;
}
The event argument in this event is AutoCompleteItemEventArgs, which has an item, text, and value property. I'm using the text
property getter, parsing it to look for the first parenthesis. If found, it is stripped out, and assigned to the target element,
accessible using the element property getter. Remember that extenders target another property and do not emit its own interface,
so an extender's element property references the extended control.
The issue that most people will have with the solution above is that there isn't any intellisense to find all this out, and there
isn't any MSDN documentation. I had to manually dig through the AJAX control toolkit source code to find all of this information.
I'd highly recommend looking at the source code whenever using these extenders.
Conclusion
With ASP.NET AJAX, it's easy to include great features into an application related to the membership framework, searching, theming,
and other areas of the application as well. I plan to continue the series to show other facets of web site development.