Even though the ASP.NET Framework is a server-side programming framework, there is nothing to prevent you from taking advantage of JavaScript—a client-side programming language—in your custom controls. In fact, you can do many exciting things by integrating JavaScript into your custom controls.
If you want to create rich and interactive user interfaces then you have no choice but to use JavaScript. For example, by taking advantage of JavaScript, you can create controls that display floating windows, rich text boxes, and drag-and-drop interfaces. In other words, by taking advantage of JavaScript, you can create the same type of experience that users have come to expect from working with traditional desktop applications.
In this chapter, you learn how to integrate JavaScript into your custom controls. First, you learn how to take advantage of the methods and properties of the ClientScriptManager
class. This class exposes the main application programming interface for working with client-side scripts. You also learn how to detect the features of different browsers by using the HttpBrowserCapabilities
class.
Next, we get our hands dirty by building several controls that use client-side JavaScript. In the second part of this chapter, we build the following controls:
NewWindowLink
—. This control renders a button that opens a new browser window. By setting properties of the control, you can configure the position and size of the new browser window.
WebWindow
—. This control renders a virtual browser window by rendering a floating <div>
tag. The WebWindow
control enables you to display multiple windows in a single page.
ClientTabs
—. This control enables you to divide the content displayed in a page into multiple tabs. Only the contents of a single tab are displayed at one time. When you switch tabs, the page is not posted back to the server.
In the final section of this chapter, we discuss my favorite topic in the universe: AJAX. By taking advantage of AJAX, a custom control can communicate with the web server without posting the page that contains the control back to the web server.
In the final section of this chapter, we build two AJAX controls:
ServerTimeButton
—. This control renders a button. When you click the button, the current time is retrieved from the server and displayed in a browser alert dialog box.
ComboBox
—. This control displays a drop-down list of matching records from a database as you type.
One reason that many programmers avoid using JavaScript is the issue of browser compatibility. Client-side programming is a mess because different browsers implement JavaScript and the Browser Object Model in different ways. However, by taking advantage of feature detection, you can write JavaScript that works the same way across all modern browsers. All the controls discussed in this chapter are compatible with Internet Explorer 6, Firefox 1, and Opera 8.
The ClientScriptManager
class contains the main application programming interface for working with JavaScript. You’ll make heavy use of this class whenever you add JavaScript to your custom controls.
The ClientScriptManager
class supports the following methods for adding JavaScript to a page:
RegisterArrayDeclaration
—. Enables you to add a JavaScript array to a page.
RegisterClientScriptBlock
—. Enables you to add a JavaScript script after the page’s opening server-side <form>
tag.
RegisterClientScriptInclude
—. Enables you to add a JavaScript include
after the page’s opening server-side <form>
tag.
RegisterClientScriptResource
—. Enables you to add JavaScript in a page that has been compiled into an assembly.
RegisterExpandoAttribute
—. Enables you to add script for adding an expando attribute to an element of the page.
RegisterHiddenField
—. Enables you to add a hidden form field after the page’s opening server-side <form>
tag.
RegisterOnSubmitStatement
—. Enables you to add JavaScript that executes immediately before a page is posted back to the server.
RegisterStartupScript
—. Enables you to add a JavaScript script before the page’s closing server-side <form>
tag.
Notice that there are two methods for rendering a JavaScript script in the body of a page: RegisterClientScriptBlock()
and RegisterStartupScript()
. The only difference between these methods is the location where they render the JavaScript. The location of a JavaScript script in a page matters because you cannot refer to an HTML element in JavaScript unless the script is located after the element. If you use the RegisterStartupScript()
method, then you know that all the HTML elements in the body of the server-side <form>
tag have been created.
All the methods listed here were designed so that you can safely call them more than once. Because you might have multiple instances of the same control in the same page, you don’t want to add duplicate instances of the same script to a page. For example, if you call the RegisterClientScriptInclude()
method more than once, then only one JavaScript include is added to the page.
You can detect whether or not a script has already been registered in a page by using one of the following methods:
IsClientScriptBlockRegistered
—. Returns true
when a script has already been registered with the RegisterClientScriptBlock()
method.
IsClientScriptIncludeRegistered
—. Returns true
when a JavaScript include
has already been registered with the RegisterClientScriptInclude()
method.
IsOnSubmitStatementRegistered
—. Returns true
when a script has already been registered with the RegisterOnSubmitStatement()
method.
IsStartupScriptRegistered
—. Returns true
when a script has already been registered with the RegisterStartupScript()
method.
After you have entered the messy universe of JavaScript, you must handle the frustrating incompatibilities between different web browsers. For example, you don’t want to call the showModalDialog()
or addEventListener()
method on a browser that doesn’t support it. You can detect browser capabilities either on the client side or the server side.
On the client side, you can perform feature detection in your JavaScript scripts to check whether particular methods are supported by a browser. For example, Internet Explorer and Firefox use different methods for adding an event handler. Internet Explorer uses the attachEvent()
method and Firefox uses the (more standards-compliant) addEventListener()
method.
The following script correctly adds a load event handler in the case of both browsers:
if (window.addEventListener) window.addEventListener('load', doSomething, false); else window.attachEvent('onload', doSomething);
When you request a page that contains this script with Internet Explorer, calling window.addEventListener
returns a value equivalent to false
and the window.attachEvent()
method is used. When you request a page that contains this script with Firefox or Opera, on the other hand, the window.addEventListener()
method is called.
On the server side, you can use the properties of the HttpBrowserCapabilities
class to detect the features of the browser being used to request a page. This class has a huge number of properties (too many to list here). However, here are some of the more useful properties that you can detect:
ActiveXControls
—. Returns true
when a browser supports ActiveXControls.
AOL
—. Returns true
when a browser is an American Online browser.
Browser
—. Returns the type of browser (for example, IE, Firefox, Opera).
ClrVersion
—. Returns the latest version of the .NET Framework installed on the browser.
Cookies
—. Returns true
when a browser supports cookies.
EcmaScriptVersion
—. Returns the version of JavaScript supported by the browser.
MajorVersion
—. Returns the major version of the browser as an Integer.
MinorVersion
—. Returns the minor version of the browser as a couble.
MinorVersionString
—. Returns the minor version of the browser as a string.
MSDomVersion
—. Returns the version of the Microsoft Document Object Model supported by the browser.
Platform
—. Returns the platform of the client (for example, WinXP).
SupportsCallback
—. Returns true
when a browser supports AJAX.
SupportsCSS
—. Returns true
when a browser supports Cascading Style Sheets.
Version
—. Returns the full version of the browser.
W3CDomVersion
—. Returns the W3C Document Object Model version supported by the browser (for example, 1.0).
The HttpBrowserCapabilities
object is exposed through the Request
object. You use Request.Browser
to get a reference to the HttpBrowserCapabilities
object. For example, you can use the following code to execute a subroutine only when the requesting browser is Internet Explorer version 5.0 or greater:
If Request.Browser.Browser = "IE" And Request.Browser.MajorVersion >= 5 Then doSomething() End If
Behind the scenes, the HttpBrowserCapabilities
object uses the User-Agent header sent by a browser to determine the browser’s capabilities. A database of browser capabilities is stored in a set of XML files located in the following folder:
WINDOWSMicrosoft.NETFramework[version]CONFIGBrowsers
The information reported back by these properties is only as accurate as the information stored in these XML files.
In this section, you learn how to build three custom JavaScript controls. You start with a simple sample of a custom control that renders JavaScript. We build a NewWindowLink
control that enables you to open a new browser window when you click a button.
Next, we build a WebWindow
control that enables you to render a virtual browser window. This control enables you to simulate multiple windows in a single web form page.
Finally, we create a ClientTabs
control. This control enables you to switch between different tabs of content on the client without a postback to the server.
Being a good web developer is hard. To be a good web developer, you need to know HTML, Cascading Style Sheets, SQL, XML, and JavaScript. You also need to know about the different implementations of these technologies in the case of different browsers and different operating systems.
One of the main jobs of a framework, such as the ASP.NET Framework, is to shield you from all the underlying technologies on which the framework is built. If you integrate JavaScript into a custom control, then you can learn the JavaScript for five minutes and then never need to worry about it again.
For example, in this section, we build a NewWindowLink
control that opens a new browser window (see Figure 32.1). The JavaScript required to open a new window is quite simple. However, the advantage of creating a control that opens a new window is that you don’t need to remember the JavaScript in the future.
The code for the NewWindowLink
control is contained in Listing 32.1.
Example 32.1. NewWindowLink.vb
Imports System Imports System.Web Imports System.Web.UI Imports System.Web.UI.WebControls Namespace myControls Public Class NewWindowLink Inherits WebControl Private _text As String = "Click Here!" Private _navigateUrl As String Private _target As String = "_blank" Private _windowWidth As Integer = 400 Private _windowHeight As Integer = 300 Private _windowLeft As Integer = 100 Private _windowTop As Integer = 100 Private _fullScreen As Boolean = False Private _resizable As Boolean = True Public Property Text() As String Get Return _text End Get Set(ByVal Value As String) _text = value End Set End Property Public Property NavigateUrl() As String Get Return _navigateUrl End Get Set(ByVal Value As String) _navigateUrl = value End Set End Property Public Property Target() As String Get Return _target End Get Set(ByVal Value As String) _target = value End Set End Property Public Property WindowWidth() As Integer Get Return _windowWidth End Get Set(ByVal Value As Integer) _windowWidth = value End Set End Property Public Property WindowHeight() As Integer Get Return _windowHeight End Get Set(ByVal Value As Integer) _windowHeight = value End Set End Property Public Property WindowLeft() As Integer Get Return _windowLeft End Get Set(ByVal Value As Integer) _windowLeft = value End Set End Property Public Property WindowTop() As Integer Get Return _windowTop End Get Set(ByVal Value As Integer) _windowTop = value End Set End Property Public Property FullScreen() As Boolean Get Return _fullScreen End Get Set(ByVal Value As Boolean) _fullScreen = value End Set End Property Public Property Resizable() As Boolean Get Return _resizable End Get Set(ByVal Value As Boolean) _resizable = value End Set End Property Protected Overrides Sub AddAttributesToRender(ByVal writer As HtmlTextWriter) Dim fullscreenValue As String = "no" If (_fullScreen) Then fullscreenValue = "yes" End If Dim resizableValue As String = "no" If (_resizable) Then resizableValue = "yes" End If Dim features As String = "width={0},height={1},left={2},top={3},fullscreen={4}, resizable={5},status=no,toolbar=no,menubar=no,location=no" Dim featuresValue As String = String.Format(features, _windowWidth, _windowHeight, _windowLeft, _ windowTop, fullscreenValue, resizableValue) Dim script As String = String.Format("window.open('{0}','{1}','{2}'),return false;", Page.ResolveUrl(_navigateUrl), _target, featuresValue) writer.AddAttribute(HtmlTextWriterAttribute.Onclick, script) writer.AddAttribute(HtmlTextWriterAttribute.Href, Page.ResolveUrl(_navigateUrl)) MyBase.AddAttributesToRender(writer) End Sub Protected Overrides Sub RenderContents(ByVal writer As HtmlTextWriter) writer.Write(_text) End Sub Protected Overrides ReadOnly Property TagKey() As HtmlTextWriterTag Get Return HtmlTextWriterTag.A End Get End Property End Class End Namespace
The majority of the code in Listing 32.1 is devoted to declaring a set of properties. The NewWindowLink
control includes properties for the new window’s position and size. It also includes properties you can set to open a window that is resizable or full screen.
The JavaScript for opening the window is contained in the AddAttributesToRender()
method. This method adds a client-side OnClick
handler to the link rendered by the control. When you click the link, the window.open()
method is called on the client.
The page in Listing 32.2 illustrates how you can use the NewWindowLink
control in an ASP.NET page. The page contains two instances of the NewWindowLink
control. The first instance opens a normal window. The control opens the page specified by its NavigateUrl
property in a new window.
The second instance of the NewWindowLink
control opens a full-screen window. A full-screen window is supported only with Internet Explorer. (You get a normal new window when the control is used with other browsers such as Firefox.) After you open a full-screen window, you can close it by selecting Alt+F4.
Example 32.2. ShowNewWindowLink.aspx
<%@ Page Language="VB" %> <%@ Register TagPrefix="custom" Namespace="myControls" %> <!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 id="Head1" runat="server"> <title>Show NewWindowLink</title> </head> <body> <form id="form1" runat="server"> <div> <custom:NewWindowLink id="NewWindowLink1" Text="Open Window" NavigateUrl="~/Default.aspx" Runat="server" /> <br /><br /> <custom:NewWindowLink id="NewWindowLink2" Text="Open Full Screen Window" NavigateUrl="~/Default.aspx" FullScreen="true" Runat="server" /> </div> </form> </body> </html>
There are several disadvantages that result from using separate browser windows in a web application. First, communicating information between multiple browser windows is difficult. When you update one window, you must reload the other windows to reflect the new information. Furthermore, new browser windows can be blocked by pop-up blockers.
A better alternative is to create virtual windows by creating floating <div>
tags. Unlike true browser windows, you don’t run into problems communicating information between virtual windows. Furthermore, a virtual window will never be blocked by a pop-up blocker.
By taking advantage of a little JavaScript, you can even drag and drop a virtual window. In other words, you can position a virtual window at different locations on the screen.
The code for the virtual window control, the WebWindow
control, is contained in Listing 32.3.
Example 32.3. WebWindow.vb
Imports System Imports System.Web.UI Imports System.Web.UI.WebControls Imports System.ComponentModel Namespace myControls <ParseChildren(False)> _ Public Class WebWindow Inherits WebControl Implements IPostBackEventHandler Private _windowTitleText As String = "Untitled" Public Event Closed As EventHandler Public Property WindowTitleText() As String Get Return _windowTitleText End Get Set(ByVal Value As String) _windowTitleText = value End Set End Property Protected Overrides Sub OnPreRender(ByVal e As EventArgs) If Not Page.ClientScript.IsClientScriptIncludeRegistered("WebWindow") Then Page.ClientScript.RegisterClientScriptInclude("WebWindow", Page.ResolveClientUrl("~/ClientScripts/WebWindow.js")) End If Dim startupScript As String = String.Format("WebWindow.init('{0}'),", Me.ClientID) Page.ClientScript.RegisterStartupScript(Me.GetType(), Me.ClientID, startupScript, True) End Sub Protected Overrides Sub RenderContents(ByVal writer As HtmlTextWriter) RenderTitleBar(writer) writer.AddAttribute(HtmlTextWriterAttribute.Class, "webWindowBody") writer.RenderBeginTag(HtmlTextWriterTag.Div) Me.RenderChildren(writer) writer.RenderEndTag() End Sub Private Sub RenderTitleBar(ByVal writer As HtmlTextWriter) writer.AddAttribute(HtmlTextWriterAttribute.Class, "webWindowTitleBar") writer.AddAttribute("onmousedown", "WebWindow.mouseDown(event)") writer.AddStyleAttribute(HtmlTextWriterStyle.TextAlign, "right") writer.RenderBeginTag(HtmlTextWriterTag.Div) writer.AddAttribute(HtmlTextWriterAttribute.Class, "webWindowTitleText") writer.RenderBeginTag(HtmlTextWriterTag.Span) writer.Write(_windowTitleText) writer.RenderEndTag() RenderCloseButton(writer) writer.RenderEndTag() End Sub Private Sub RenderCloseButton(ByVal writer As HtmlTextWriter) Dim eventRef As String = Page.ClientScript.GetPostBackEventReference(Me, String.Empty) writer.AddAttribute(HtmlTextWriterAttribute.Class, "webWindowClose") writer.AddAttribute(HtmlTextWriterAttribute.Onclick, eventRef) writer.RenderBeginTag(HtmlTextWriterTag.Span) writer.Write("X") writer.RenderEndTag() End Sub Protected Overrides Sub AddAttributesToRender(ByVal writer As HtmlTextWriter) writer.AddStyleAttribute(HtmlTextWriterStyle.Position, "absolute") writer.AddAttribute(HtmlTextWriterAttribute.Class, "webWindow") MyBase.AddAttributesToRender(writer) End Sub Protected Overrides ReadOnly Property TagKey() As HtmlTextWriterTag Get Return HtmlTextWriterTag.Div End Get End Property Public Sub RaisePostBackEvent(ByVal eventArgument As String) Implements IPostBackEventHandler.RaisePostBackEvent RaiseEvent Closed(Me, EventArgs.Empty) End Sub End Class End Namespace
The RenderContents()
method in Listing 32.3 is responsible for rendering the virtual window. This method renders a title bar that contains a close button. It also renders a <div>
tag that contains the window’s contents.
All the magic happens in an external JavaScript file. A JavaScript include
is rendered by the WebWindow
control in its OnPreRender()
method. The control assumes that a JavaScript file named WebWindow.js
is located in a subfolder named ClientScripts.
The contents of the WebWindow.js
file are contained in Listing 32.4.
Example 32.4. ClientScriptsWebWindow.js
var WebWindow = new Object(); WebWindow.init = function(winId) { eval("place=" + this.getCookie(winId + "_place")); place = place || {left:20, top:20}; var win = document.getElementById(winId); win.style.left = place.left; win.style.top = place.top; } WebWindow.mouseDown = function(e) { var e = e || window.event; var src = e.target || e.srcElement; var win = src.offsetParent; var startWinX = win.offsetLeft; var startWinY = win.offsetTop; var startMouseX = e.clientX; var startMouseY = e.clientY; var move = function(e) { var e = e || window.event; win.style.left = (startWinX - (startMouseX - e.clientX)) + 'px'; win.style.top = (startWinY - (startMouseY - e.clientY)) + 'px'; if (document.all) { e.cancelBubble = true; e.returnValue = false; } if (e.preventDefault) { e.preventDefault(); } } var up = function(e) { document.onmousemove = null; document.onmouseup = null; WebWindow.setCookie(win.id + "_place", "{left:'" + win.style.left + "', top:'" + win.style.top + "'}"); } document.onmousemove = move; document.onmouseup = up; } WebWindow.setCookie = function(name, value) { var expires = new Date(); expires.setTime( expires.getTime() + 365 * 24 * 60 * 60 * 1000 ); document.cookie = name + "=" + escape(value) + ";expires=" + expires.toGMTString(); } WebWindow.getCookie = function(name) { var aCookie = document.cookie.split("; "); for (var i=0; i < aCookie.length; i++) { var aCrumb = aCookie[i].split("="); if (name == aCrumb[0]) return unescape(aCrumb[1]); } return null; }
The JavaScript file in Listing 32.4 contains the methods that enable you to drag the WebWindow
around the page. When you click your mouse on the WebWindow
’s title bar, the WebWindow.mouseDown()
method executes and records your current mouse position. As you move your mouse, the move()
method executes and updates the WebWindow's
position on the screen by modifying the left
and top
style properties of the WebWindow
’s containing div
element.
Whenever you move a WebWindow
, its new position is recorded in a browser cookie. The next time that you open the page, the WebWindow
appears in the same location.
Be careful about where you declare the WebWindow
control in a page. Do not declare the control inside of another <div>
tag or you might encounter difficulties when you attempt to drag the control outside of its containing <div>
tag. I’ve tested the WebWindow
control with Internet Explorer 6.0, Firefox 1.0, and Opera 8.0.
The page in Listing 32.5 illustrates how you can use the WebWindow
control in an ASP.NET page. When the page first opens, the WebWindow
is not visible by default. When you click the Open Window link, the WebWindow
is displayed (see Figure 32.2). Finally, when you click the WebWindow
’s Close button, the WebWindow1_Closed()
event handler executes and the WebWindow
is hidden.
Example 32.5. ShowWebWindow.aspx
<%@ Page Language="VB" %> <%@ Register TagPrefix="custom" Namespace="myControls" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <script runat="server"> protected sub lnkOpenWindow_Click(sender As object, e As EventArgs) WebWindow1.Visible = true End Sub protected sub WebWindow1_Closed(sender As object, e As EventArgs) WebWindow1.Visible = false End Sub </script> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <style type="text/css"> .webWindow { width:400px; height:400px; border:Outset; background-color:white; } .webWindowBody { padding:10px; } .webWindowTitleBar { font:14px Verdana,Sans-Serif; padding-left:10px; background-color:Blue; color:white; cursor:move; } .webWindowTitleText { float:left; } .webWindowClose { background-color:Red; cursor:pointer; } </style> <title>Show WebWindow</title> </head> <body> <form id="form1" runat="server"> <div> <asp:LinkButton id="lnkOpenWindow" Text="Open Window" OnClick="lnkOpenWindow_Click" Runat="server" /> </div> <custom:WebWindow id="WebWindow1" WindowTitleText="The WebWindow Title" Visible="false" OnClosed="WebWindow1_Closed" Runat="server"> Here is some content </custom:WebWindow> </form> </body> </html>
In Chapter 4, “Using the Rich Controls,” you learned how to use the Menu
control with the MultiView
control to display tabbed content in a page. The drawback of using these controls is that they require you to post the page containing the controls back to the server each and every time you click a tab.
In this section, you learn how to create a client-side tab control named the ClientTabs
control (see Figure 32.3). This control renders the contents of all the tabs to the browser. However, it hides the contents of all the tabs except for the selected tab. When you select a new tab, JavaScript is executed on the browser to hide the contents of all the tabs except the newly selected tab.
Unfortunately, the entire code for the ClientTabs
control is too long to include in the body of this book. However, the complete source code (in both Visual Basic .NET and C#) is included on the CD that accompanies this book. Listing 32.6 contains a partial listing of the ClientTabs
control.
Example 32.6. ClientTabs.vb
(Partial Listing)
Namespace myControls <ParseChildren(False)> _ Public Class ClientTabs Inherits WebControl Implements IPostBackDataHandler ''' <summary> ''' Render tabs and tab content ''' </summary> Protected Overrides Sub RenderContents(ByVal writer As HtmlTextWriter) RenderTabs(writer) RenderTabContent(writer) End Sub ''' <summary> ''' Render the tab strip ''' </summary> Private Sub RenderTabs(ByVal writer As HtmlTextWriter) writer.AddAttribute(HtmlTextWriterAttribute.Class, "tabs") writer.RenderBeginTag(HtmlTextWriterTag.Table) writer.RenderBeginTag(HtmlTextWriterTag.Tbody) writer.AddAttribute(HtmlTextWriterAttribute.Id, TabContainerID) writer.RenderBeginTag(HtmlTextWriterTag.Tr) Dim index As Integer For index = 0 To Controls.Count - 1 Step index + 1 Dim currentTab As Tab = CType(Controls(index), Tab) Dim script As String = String.Format("ClientTabs.selectTab('{0}','{1}')", Me.ClientID, currentTab.ClientID) writer.AddAttribute(HtmlTextWriterAttribute.Onclick, script) If index = SelectedIndex Then writer.AddAttribute(HtmlTextWriterAttribute.Class, "tab selectedTab") Else writer.AddAttribute(HtmlTextWriterAttribute.Class, "tab unselectedTab") End If writer.AddAttribute(HtmlTextWriterAttribute.Id, currentTab.ClientID + "_tab") writer.RenderBeginTag(HtmlTextWriterTag.Td) writer.Write(currentTab.Text) writer.RenderEndTag() Next writer.RenderEndTag() writer.RenderEndTag() writer.RenderEndTag() End Sub ''' <summary> ''' Render the tab contents ''' </summary> Private Sub RenderTabContent(ByVal writer As HtmlTextWriter) writer.AddAttribute(HtmlTextWriterAttribute.Id, TabContentContainerID) writer.AddAttribute(HtmlTextWriterAttribute.Class, "tabContent") writer.RenderBeginTag(HtmlTextWriterTag.Div) Dim index As Integer For index = 0 To Controls.Count - 1 Step index + 1 Dim currentTab As Tab = CType(Controls(index), Tab) If index <> SelectedIndex Then currentTab.Style.Add("display", "none") End If currentTab.RenderControl(writer) Next writer.RenderEndTag() End Sub End Class End Namespace
Listing 32.6 contains the code for rendering the tabs. When the ClientTab
control renders its contents, it iterates through its child controls in two passes. First, it renders the tab links. Next, it renders the body of each tab. Any tab that is not selected is hidden by the CSS display:none
property.
One special feature of the ClientTabs
control needs to be explained. Imagine that each of the tabs contains a separate form with a separate button. When you click the button and submit the form to the server, you want the same tab to be displayed again when the page is reloaded. In other words, to function properly, the ClientTabs
control needs to retain state across postbacks.
The ClientTabs
control registers a hidden form field in its OnPreRender()
method. When you select a new tab, the ID of the selected tab is assigned to the hidden form field. When the page is posted back to the server, the value of the hidden form field is used to determine the currently selected tab. That way, when the ClientTab
control renders its tabs again, the tab selected on the client remains selected when the control is rendered on the server.
The ClientTabs
control uses a Cascading Style Sheet file to format the appearance of the tabs. This Cascading Style Sheet file is added to the page containing the ClientTabs control by the ClientTabs control’s OnPreRender() method. The contents of the Cascading Style Sheet file are contained in Listing 32.7.
Example 32.7. ClientScriptsClientTabs.css
.tab { padding:2px 25px; border:solid 1px black; cursor:pointer; } .unselectedTab { background-color:#eeeeee; } .selectedTab { background-color:White; border-bottom:solid 1px white; } .tabs { position:relative; top:3px; left:15px; } .tabContent { border:Solid 1px black; background-color:White; padding:10px; }
Finally, the ClientTabs
control executes a client-side JavaScript function named ClientTabs.selectTab()
when a user selects a new tab. The JavaScript library is added to the page in the ClientTabs
control’s OnPreRender()
method. The ClientTabs
control assumes that there is a JavaScript file named ClientTabs.js
that is located in the ClientScripts folder. The contents of this JavaScript file are contained in Listing 32.8.
Example 32.8. ClientScriptsClientTabs.js
var ClientTabs = new Object(); ClientTabs.selectTab = function(controlId, tabId) { // Get previous value var hiddenField = document.getElementById(controlId + '_hidden'), var prevTabId = hiddenField.value; // Hide previous tab document.getElementById(prevTabId + '_tab').className = 'tab unselectedTab'; document.getElementById(prevTabId).style.display = 'none'; // Show new tab document.getElementById(tabId).style.display = 'block'; document.getElementById(tabId + '_tab').className = 'tab selectedTab'; // Update hidden value hiddenField.value = tabId; }
The page in Listing 32.9 illustrates how you can use the ClientTabs
control within an ASP.NET page. The page in Listing 32.9 contains a ClientTabs
control that contains three Tab
controls. The final Tab
control includes a simple form with a Button
control. Notice that after you submit the page to the server, the correct tab is selected when the tabs are rendered back to the client.
Example 32.9. ShowClientTabs.aspx
<%@ Page Language="VB" %> <%@ Register TagPrefix="custom" Namespace="myControls" %> <!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 id="Head1" runat="server"> <title>Show ClientTabs</title> </head> <body> <form id="form1" runat="server"> <div> <custom:ClientTabs id="ClientTabs1" Runat="server"> <custom:Tab ID="Tab1" Text="First Tab" runat="server"> Contents of the first tab </custom:Tab> <custom:Tab ID="Tab2" Text="Second Tab" runat="server"> Contents of the second tab </custom:Tab> <custom:Tab ID="Tab3" Text="Third Tab" runat="server"> Contents of the third tab <br /><br /> <asp:Label id="lblUserName" Text="User Name:" AssociatedControlID="txtUserName" Runat="server" /> <asp:TextBox id="txtUserName" Runat="server" /> <asp:Button id="btnSubmit" Text="Submit" Runat="server" /> </custom:Tab> </custom:ClientTabs> </div> </form> </body> </html>
AJAX (Asynchronous JavaScript and XML) is the future of the web. By taking advantage of AJAX, you can avoid performing a postback each and every time you perform an action in an ASP.NET page. A control that uses AJAX can communicate directly with a web server by performing a “sneaky postback.”
Three of the standard ASP.NET controls use AJAX: the TreeView
, GridView
, and DetailsView
controls. When you expand a node in a TreeView
, the child nodes of the expanded node can be retrieved from the web server without a postback. When you sort or page rows in a GridView
control, the rows can be retrieved from the server without a postback. Finally, when you page through records in a DetailsView
control, you do not need to post the page back to the server.
Microsoft has developed a new framework (code named Atlas) that enables you to build rich AJAX applications on top of the ASP.NET Framework. You can learn more about Atlas by visiting http://atlas.asp.net.
In this section, you learn how to take advantage of AJAX when building custom controls. We’ll start simple. First, we create a ServerTimeButton
control that retrieves the current time from the server and displays it in the browser without requiring a postback. Next, we’ll create a more practical control. We re-create Google Suggest by creating an AJAX-enabled ComboBox
control.
To implement AJAX in a custom control, you must perform the following steps:
Render the JavaScript that initiates the AJAX call.
Create the methods on the server that reply to the AJAX call.
Create the JavaScript on the browser that displays the result from the server.
You initiate an AJAX call from the browser in order to execute methods on the server. The server returns a result that can be used on the client.
You create the JavaScript that initiates the AJAX call by calling the Page.ClientScripts.GetCallbackEventReference()
method. This method returns a string that represents a JavaScript function call that looks like this:
WebForm_DoCallback('myControl',null,showResult,null,showError,false)
The GetCallbackEventReference()
method is overloaded. Here are the parameters for one of the overloaded versions of this method:
control
—. The control that initiates the AJAX call.
argument
—. The argument that is sent to the web server in the AJAX call.
clientCallback
—. The name of the JavaScript function that executes after a result is returned from the web server.
context
—. The argument that is passed back to the clientCallback()
and clientErrorCallback()
methods after the AJAX call completes.
clientErrorCallback
—. The name of the JavaScript function that executes when an error on the server results from an AJAX call.
useAsync
—. When true, the AJAX call is performed asynchronously.
Next, you need to implement the methods on the server that respond to the AJAX call. To do this, you need to implement the ICallbackEventHandler
interface.
AJAX, in Microsoft terminology, is called client callbacks. The name AJAX was invented in a Blog post by Jesse James Garrett.
The ICallbackEventHandler
interface has two methods that you must implement:
RaiseCallbackEvent
—. This method is called first on the server when an AJAX call is performed.
GetCallbackResult
—. This method returns the result of the AJAX call to the client.
Finally, you must implement a JavaScript function on the client that is called when the results are returned from the server.
We start by creating a really simple control that uses AJAX. The ServerTimeButton
control retrieves the current time from the server and displays it in the web browser (see Figure 32.4). The ServerTimeButton
control is contained in Listing 32.10.
Example 32.10. ServerTimeButton.vb
Imports System Imports System.Web.UI Imports System.Web.UI.WebControls Namespace myControls Public Class ServerTimeButton Inherits WebControl Implements ICallbackEventHandler ''' <summary> ''' Add the onclick attribute that initiates the AJAX call ''' </summary> Protected Overrides Sub AddAttributesToRender(ByVal writer As HtmlTextWriter) Dim eRef As String = Page.ClientScript.GetCallbackEventReference( _ Me, _ Nothing, _ "showResult", _ Nothing, _ "showError", _ False) writer.AddAttribute(HtmlTextWriterAttribute.Onclick, eRef) MyBase.AddAttributesToRender(writer) End Sub ''' <summary> ''' Add the Javascript file that process the AJAX call results ''' to the page ''' </summary> Protected Overrides Sub OnPreRender(ByVal e As EventArgs) Page.ClientScript.RegisterClientScriptInclude("serverTimeButton", Page.ResolveClientUrl("~/ClientScripts/ServerTimeButton.js")) End Sub Protected Overrides Sub RenderContents(ByVal writer As HtmlTextWriter) writer.Write("Show Server Time") End Sub Protected Overrides ReadOnly Property TagKey() As HtmlTextWriterTag Get Return HtmlTextWriterTag.Button End Get End Property ''' <summary> ''' Called on server by AJAX ''' </summary> ''' <param name="eventArgument"></param> Public Sub RaiseCallbackEvent(ByVal eventArgument As String) Implements ICallbackEventHandler.RaiseCallbackEvent End Sub ''' <summary> ''' Returns result back to AJAX call ''' </summary> ''' <returns></returns> Public Function GetCallbackResult() As String Implements ICallbackEventHandler.GetCallbackResult 'throw new Exception("Server Exception"); Return DateTime.Now.ToString() End Function End Class End Namespace
The ServerTimeButton
control renders an HTML button. Clicking the button initiates the AJAX call to the server. The script for initiating the AJAX call is generated in the AddAttributesToRender()
method by a call to the Page.ClientScript.GetCallbackEventReference()
method.
Notice that the ServerTimeButton
control implements the ICallbackEventHandler
interface. This interface includes two methods named RaiseCallbackEvent
and GetCallbackResult()
. In Listing 32.10, the RaiseCallbackEvent()
does nothing. The GetCallbackResult()
method returns the current time as a string.
Finally, the ServerTimeButton
control uses a JavaScript file named ServerTimeButton.js
. This file is registered in the control’s OnPreRender()
method. The contents of the ServerTimeButton.js
file are contained in Listing 32.11.
The JavaScript file in Listing 32.11 includes two functions. The first function, named showResult()
, displays the result of the AJAX call. This method simply displays the server time in a JavaScript alert box. The showError()
function displays any error message returned by the server as a result of the AJAX call.
The page in Listing 32.12 uses the ServerTimeButton
control.
Example 32.12. ShowServerTimeButton.aspx
<%@ Page Language="VB" %> <%@ Register TagPrefix="custom" Namespace="myControls" %> <!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 id="Head1" runat="server"> <title>Show Server Time</title> </head> <body> <form id="form1" runat="server"> <div> Initial Time: <%= DateTime.Now.ToString() %> <br /><br /> <custom:ServerTimeButton id="lnkServerTime" Runat="Server" /> </div> </form> </body> </html>
If you open the page in Listing 32.12 and click the button, the time is retrieved from the server and displayed in the browser. At no time is the page posted back to the server. The server time is retrieved through an AJAX call.
The application that caused all the excitement over AJAX was Google Suggest (http://www.google.com/webhp?complete=1). Google Suggest is an enhanced version of Google Search. As you type a search phrase, Google Suggest automatically displays a list of matching entries in a drop-down list (see Figure 32.5).
The amazing thing about Google Suggest is that every time you enter a new letter into the search box, an AJAX call is performed to retrieve a list of matching results from the server. The fact that the AJAX calls can be performed in real time blew everyone’s mind.
In this section, we create a ComboBox
control that mimics the functionality of Google Suggest. Each time you enter a new letter into the combo box, a new AJAX call is performed against the web server to retrieve matching entries (see Figure 32.6).
The ComboBox
control is contained in Listing 32.13.
Example 32.13. ComboBox.vb
Imports System Imports System.Collections.Generic Imports System.Data Imports System.Configuration Imports System.Web Imports System.Web.UI Imports System.Web.UI.WebControls Namespace myControls Public Class ComboBox Inherits CompositeControl Implements ICallbackEventHandler Private _comboTextBox As TextBox Private _dataKeyName As String = String.Empty Private _selectCommand As String = String.Empty Private _connectionString As String = String.Empty Private _clientArgument As String ''' <summary> ''' Name of the database field used for the lookup ''' </summary> Public Property DataKeyName() As String Get Return _dataKeyName End Get Set(ByVal Value As String) _dataKeyName = value End Set End Property ''' <summary> ''' SQL Select command issued against database ''' </summary> Public Property SelectCommand() As String Get Return _selectCommand End Get Set(ByVal Value As String) _selectCommand = value End Set End Property ''' <summary> ''' Connection String for database ''' </summary> Public Property ConnectionString() As String Get Return _connectionString End Get Set(ByVal Value As String) _connectionString = value End Set End Property Private ReadOnly Property ComboSelectId() As String Get Return Me.ClientID + "_select" End Get End Property Public Property Text() As String Get EnsureChildControls() Return _comboTextBox.Text End Get Set(ByVal Value As String) EnsureChildControls() _comboTextBox.Text = value End Set End Property Public Property Columns() As Integer Get EnsureChildControls() Return _comboTextBox.Columns End Get Set(ByVal Value As Integer) EnsureChildControls() _comboTextBox.Columns = value End Set End Property Protected Overrides Sub OnPreRender(ByVal e As EventArgs) ' Make sure all the properties are set If _dataKeyName = String.Empty Then Throw New Exception("DataKeyName cannot be empty") End If If _connectionString = String.Empty Then Throw New Exception("ConnectionString cannot be empty") End If If _selectCommand = String.Empty Then Throw New Exception("SelectCommand cannot be empty") End If ' Register Include File If Not Page.ClientScript.IsClientScriptIncludeRegistered("ComboBox") Then Page.ClientScript.RegisterClientScriptInclude("ComboBox", Page.ResolveUrl("~/ClientScripts/ComboBox.js")) End If End Sub Protected Overrides Sub CreateChildControls() ' Create the TextBox _comboTextBox = New TextBox() _comboTextBox.Attributes("autocomplete") = "off" Controls.Add(_comboTextBox) End Sub Protected Overrides Sub RenderContents(ByVal writer As HtmlTextWriter) ' Define the callback Dim callBackRef As String = Page.ClientScript.GetCallbackEventReference( _ Me, _ "this.value", _ "comboBox_ClientCallback", _ String.Format("'{0}'", ComboSelectId), _ "comboBox_ErrorCallback", _ True) ' Render the text box _comboTextBox.Attributes.Add("onkeyup", callBackRef) _comboTextBox.Attributes.Add("onblur", "comboBox_Blur(this)") _comboTextBox.RenderControl(writer) writer.WriteBreak() ' Render the drop-down writer.AddAttribute(HtmlTextWriterAttribute.Id, ComboSelectId) writer.AddStyleAttribute(HtmlTextWriterStyle.Display, "none") writer.AddStyleAttribute(HtmlTextWriterStyle.Position, "absolute") writer.RenderBeginTag(HtmlTextWriterTag.Select) writer.RenderEndTag() End Sub Protected Overrides ReadOnly Property TagKey() As HtmlTextWriterTag Get Return HtmlTextWriterTag.Div End Get End Property Public Sub RaiseCallbackEvent(ByVal clientArgument As String) Implements ICallbackEventHandler.RaiseCallbackEvent _clientArgument = clientArgument End Sub Public Function GetCallbackResult() As String Implements ICallbackEventHandler.GetCallbackResult ' If no text, then return nothing If _clientArgument.Trim() = String.Empty Then Return "[]" End If ' Otherwise, get the matching rows Dim src As SqlDataSource = New SqlDataSource(_connectionString, _selectCommand) src.SelectParameters.Add(_dataKeyName, _clientArgument) Dim dvw As DataView Try dvw = CType(src.Select(DataSourceSelectArguments.Empty), DataView) Catch Return "[]" End Try ' Return matching rows in a JavaScript array Dim rows As New List(Of String)() Dim row As DataRowView For Each row In dvw rows.Add(String.Format("'{0}'", row(0).ToString().Replace("'", "'"))) Next Return "[" + String.Join(",", rows.ToArray()) + "]" End Function End Class End Namespace
The control in Listing 32.13 renders a text box and a list box. As you type letters into the text box, a list of matching database records is displayed in the list box.
The ComboBox
uses the JavaScript functions contained in Listing 32.14.
Example 32.14. ClientScriptsComboBox.js
// Display rows from database in the SELECT tag function comboBox_ClientCallback(result, context) { // convert rows into an array var rows; eval( 'rows=' + result ); // Get the Select element var comboSelect = document.getElementById( context ); // Add the options comboSelect.options.length = 0; for (var i=0;i<rows.length;i++) { var newOption = document.createElement("OPTION"); newOption.text= rows[i]; newOption.value= rows[i]; if (document.all) comboSelect.add(newOption); else comboSelect.add(newOption, null); } // If results, show the SELECT, otherwise hide it if (comboSelect.options.length > 0) { comboSelect.size = comboSelect.options.length + 1; comboSelect.selectedIndex = 0; comboSelect.style.display='block'; } else comboSelect.style.display='none'; } // When leaving comboBox, get selected value from SELECT function comboBox_Blur(src) { var container = src.parentNode; var comboSelect = container.getElementsByTagName('select')[0]; if ( comboSelect.style.display != 'none' && comboSelect.selectedIndex != -1) src.value = comboSelect.value; comboSelect.style.display = 'none'; } // If server error, just show it function comboBox_ErrorCallback(result) { alert( result ); }
The JavaScript library in Listing 32.14 contains three functions. The first function, comboBox_ClientCallback()
, displays the results of a database lookup after each callback. This function updates the list of matching entries displayed by the list box.
The comboBox_Blur()
function updates the TextBox
with the item in the ListBox
that matches the text entered into the TextBox
. Finally, the comboBox_ErrorCallback()
method displays any errors returned from the server.
The page in Listing 32.15 illustrates how you can use the ComboBox
control. When you enter text into the combo box, a list of matching movie titles is displayed.
Example 32.15. ShowComboBox.aspx
<%@ Page Language="VB" %> <%@ Register TagPrefix="custom" Namespace="myControls" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <script runat="server"> Protected Sub btnSubmit_Click(ByVal sender As Object, ByVal e As EventArgs) lblResult.Text = ComboBox1.Text End Sub </script> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <title>Show ComboBox</title> </head> <body> <form id="form1" runat="server"> <div> <custom:ComboBox id="ComboBox1" ConnectionString='<%$ ConnectionStrings:Movies %>' DataKeyName="Title" SelectCommand="SELECT Title FROM Movies WHERE Title LIKE @Title+'%' ORDER BY Title" Style="float:left" Runat="Server" /> <asp:Button id="btnSubmit" Text="Submit" Runat="server" OnClick="btnSubmit_Click" /> <hr /> <asp:Label id="lblResult" Runat="server" /> </div> </form> </body> </html>
In Listing 32.15, the ConnectionString
, DataKeyName
, and SelectCommand
properties are set. The ConnectionString
property represents a connection to a database. The DataKeyName
property represents a primary key column from a database table. Finally, the SelectCommand
uses a SQL SELECT
command to retrieve a list of matching records. Notice that the SELECT
command uses a LIKE
operator to retrieve all records that begin with the text entered into the combo box.
In this chapter, you learned how to build custom controls that take advantage of client-side JavaScript. In the first section, you learned how to use the ClientScriptManager
class to add JavaScript scripts to a page. You also learned how to detect browser capabilities with the HttpBrowserCapabilities
object.
Next, we built three JavaScript controls. You learned how to create a NewWindowLink
control that renders a button that opens a new browser window. We also created a virtual window control named the WebWindow
control. Finally, we created a ClientTabs
control that renders a client-side tab strip.
The final section of this chapter explored the exciting topic of AJAX. You learned how to initiate an AJAX call to the server and implement the ICallbackEventHandler
interface. We also created a ComboBox
that uses AJAX to retrieve matching database records from the web server with each key press.