Intro to XNA Part 3: More Sound and adding Text
I want to know who is looking at my websites. Don't get me wrong -- I'm not paranoid, I'm just curious (and maybe a little vain). After I have undergone all of the pain and effort of building a web page, I want to know that someone is actually looking at it.
Yes, yes -- I know about website statistics programs. But that is not what I want. I want to know who is visiting a web page right now in real time. Furthermore, I want to share this information with anyone else who is visiting the web page.
This month, we build a custom ASP.NET 2.0 control that displays a list of everyone who is currently looking at a particular web page (see Figure 1). The list rendered by the control is updated, in real time, using Javascript and Ajax. People pop in and out of the list as they visit and leave the web page. We create a control named the WhoIsLooking control.
Figure 1 - The WhoIsLooking control
The WhoIsLooking control is a complicated control. To get it to work, I had to take advantage of a number of advanced control building techniques. Therefore, this post is divided into several parts.
In the first part, I describe how you create custom controls for the ASP.NET 2.0 framework. After getting the basics out of the way, I explain how you can embed Javascript and Cascading Style Sheets in a custom control. Then, we explore the very trendy topic of Ajax. Finally, I discuss how you can grab information about the users viewing a web page.
If you want to play with the control right now, you can go ahead and click the download link at the top of this article to download all of the control source code (There is both a C# and VB.NET version of the source code). The download package also includes a test ASP.NET website that you can use to try out the control. When testing the control, open up multiple types of browsers and point them to the same page (If you open up multiple instances of Microsoft Internet Explorer, you'll see only one instance of the browser listed by the WhoIsLooking control since all instances of the same browser share the same set of cookies).
All the ASP.NET 2.0 controls that you find in the Visual Studio toolbox, such as the GridView, TextBox, and TreeView controls, were created as custom controls. If you ever get bored of the standard controls, you always have the option of building new ones.
An ASP.NET control -- like just about everything else in the .NET framework -- is a class. Typically, a custom control is a class that derives from the WebControl class. Creating a custom control generally involves completing the following three steps:
(1) Add a new class to your website's App_Code folder
(2) Modify the class so that it derives from the base WebControl class
(3) Override the base WebControl class's RenderContents() method
One of my favorite new features of ASP.NET 2.0 is the magic App_Code folder. If you add the source code for a class to this special folder, the class gets compiled automatically. Since ASP.NET controls are just a type of class, this means that you can create a new ASP.NET control simply by adding the source for the control to the App_Code folder.
When you create a custom control, you typically derive the control from the base WebControl class (found in the System.Web.UI.WebControls namespace) and override the base class's RenderContents() method.
At its most basic level, a control is simply a class that renders content to a Web browser. In the RenderContents() method, you output the content that you want to send to the browser.
For example, Listing 1 contains the simplest control that I can imagine -- a Hello World control. This control renders the text “Hello World”.
Listing 1 -- HelloWorld.cs
using System; using System.Web.UI; using System.Web.UI.WebControls; namespace myControls { public class HelloWorld : WebControl { protected override void RenderContents(HtmlTextWriter writer) { writer.Write("Hello World"); } } }
Listing 1 -- HelloWorld.vb
Imports System Imports System.Web.UI Imports System.Web.UI.WebControls Namespace myControls Public Class HelloWorld Inherits WebControl Protected Overrides Sub RenderContents(ByVal writer As HtmlTextWriter) writer.Write("Hello World") End Sub End Class End Namespace
After you create a new custom control, you can add it to a page in one of three ways. The first option is to register the control in a particular page. For example, you can register the HelloWorld control in a page by adding the following directive to the page:
<%@ Register TagPrefix="custom" Namespace="myControls" %>
The value of the TagPrefix attribute can be anything you want. The value of the Namespace attribute must match the namespace that you declared in the source code for the custom control.
After you register to a control for a page, you can declare the control with a tag that looks like this:
<custom:HelloWorld
Another option is to register the control in your website's configuration file. This is a new option in ASP.NET 2.0. If you want to register a control so that it can be used by all pages in an application, then you can add the following <pages> element to your Web.Config file:
<pages>
<controls>
<add tagPrefix="custom" namespace="myControls"/>
</controls>
</pages>
I tend to use this second option the most often when incorporating custom controls into my applications.
Finally, you can add a custom control to the Visual Studio (or Visual Web Developer) Toolbox. If you add a control to the Toolbox, then you can add the control to a page simply by dragging the control from the Toolbox onto the page.
Unfortunately, you cannot take advantage of this last option when creating custom controls in the App_Code folder. You can add a custom control to the Toolbox only when the control has been compiled into a separate assembly. When we create the WhoIsLooking control later in this article, we will create the control as a separate assembly.
You add a control to the Toolbox by right-clicking the Toolbox window and selecting the menu option Choose Items. Selecting this menu item will open a dialog box that enables you to browse to an assembly (Click the Browse button). After you select the assembly that contains the custom control, the control will appear in your Toolbox.
Our control is going to do something much more interesting than merely render some HTML to the browser. Our control is going to inject Javascript and CSS into its containing page.
There is one main class in the ASP.NET Framework included for working with Javascript: the ClientScriptManager class. This class is exposed by the Page.ClientScript property. The ClientScriptManager class has several useful methods for working with Javascript including:
The RegisterClientScriptBlock and RegisterStartupScript methods enable you to add inline Javascript scripts to a page. If you have a little snippet of Javascript that you need to add to a page, then these are great methods to use. However, if you need to work with a more complex Javascript script, as we will need to do, then you should use either the RegisterClientScriptInclude() or GetWebResourceUrl() method.
The RegisterClientScriptInclude() method adds a tag that looks like this to your page:
<script type="text/Javascript" src="SomeScript.js"></script>
The method simply adds a reference to an external Javascript file (you supply the method with the path to the Javascript file that you want to include).
The disadvantage of using the RegisterClientScriptInclude() method is that it requires you to distribute a separate Javascript file (or files) with your custom control. In order for people to use your control, they need to add the necessary Javascript files to their projects in the right locations.
A better alternative is to take advantage of the GetWebResourceUrl() method. You can embed a Javascript file (or any other type of resource) directly into your custom control. Using the GetWebResourceUrl() method, you can get the URL that you need to retrieve the embedded Javascript file from your control. In other words, by taking advantage of the GetWebResourceUrl() method, you can distribute a single assembly (.dll file) that contains both your custom control and Javascript files.
The tricky part is embedding the resource (the Javascript file) in the assembly. Unfortunately, you cannot embed a resource into an assembly when you create a control in the App_Code folder. You need to create the control in a separate project. For this reason, I created the custom WhoIsLooking control in a separate Class Library project.
You cannot create a Class Library project with Visual Web Developer. If you want to create a Class Library project, you either need to use the full version of Visual Studio 2005 or you need to download either Microsoft Visual C# 2005 Express Edition or Microsoft Visual Basic 2005 Express Edition from the Microsoft website (The download that accompanies this article includes both a Visual C# Express and Visual VB.NET Express Class Library project that contains the source code for the WhoIsLooking control).
Note: When creating custom controls in a Class Library project, you need to add a reference to the System.Web.dll assembly. Select Project, Add Reference and select the System.Web assembly from beneath the .NET tab.
Within Visual C# Express or Visual VB.NET Express, you can embed any file as a resource into an assembly by selecting the file in the Solution Explorer window, opening the Properties window, and changing the Build Action property for the file to the value Embedded Resource.
Next, you need to add a WebResource attribute to the AssemblyInfo file for each embedded resource. When working with Microsoft Visual Basic 2005 Express Edition, select the menu option Project, Show All Files and expand the My Project folder in the Solution Explorer window to view the AssemblyInfo file. When working with Microsoft C# 2005 Express Edition, expand the Properties folder to view the AssemblyInfo file. You need to add the following attribute to embed an image resource named MyImage.gif:
VB.NET
<Assembly: WebResource("ProjectNamespace.MyImage.gif", "image/gif")>
C#
[Assembly: WebResource("ProjectNamespace.MyImage.gif", "image/gif")]
By default, the ProjectNamespace will be the same as your project name. In the case of the WhoIsLooking project, the project namespace is WhoIsLooking. You can view the default namespace for your project by right-clicking on the name of your project in the Solution Explorer window and selecting Properties.
The WhoIsLooking control adds a reference to Javascript file in its OnPreRender() method. The OnPreRender() method includes the following two lines of code:
C#
// Add Javascript include
string scriptUrl = Page.ClientScript.GetWebResourceUrl( this.GetType(),
"WhoIsLooking.WhoIsLooking.js");
Page.ClientScript.RegisterClientScriptInclude("WhoIsLooking", scriptUrl);
VB.NET
' Add Javascript include
Dim scriptUrl As String = Page.ClientScript.GetWebResourceUrl(Me.GetType(), "WhoIsLooking.WhoIsLooking.js")
Page.ClientScript.RegisterClientScriptInclude("WhoIsLooking", scriptUrl)
You can use the same technique that we used to embed Javascript into our custom control to embed a Cascading Style Sheet. The WhoIsLooking control takes advantage of a Cascading Style Sheet to format the appearance of the control.
If you select the WhoIsLooking.css file in the Solution Explorer window and view the properties for the file, then you will see that the Build Action for the file is set to the value Embedded Resource.
Furthermore, the WhoIsLooking project includes the following attribute in its AssemblyInfo file:
VB.NET
<Assembly: WebResource("WhoIsLooking.WhoIsLooking.css", "text/css")>
C#
[Assembly: WebResource("WhoIsLooking.WhoIsLooking.css", "text/css")]
Finally, the WhoIsLooking control includes the following lines of code in its OnPreRender() method:
VB.NET
' Add style sheet to parent page
Dim cssUrl As String = Page.ClientScript.GetWebResourceUrl(Me.GetType(), "WhoIsLooking.WhoIsLooking.css")
Dim cssLink As New HtmlLink()
cssLink.Href = cssUrl
cssLink.Attributes.Add("rel", "stylesheet")
cssLink.Attributes.Add("type", "text/css")
Page.Header.Controls.Add(cssLink)
' Add class name
Me.CssClass = "WhoIsLooking"
C#
// Add style sheet to parent page
string cssUrl = Page.ClientScript.GetWebResourceUrl(this.GetType(),
"WhoIsLooking.WhoIsLooking.css");
HtmlLink cssLink = new HtmlLink();
cssLink.Href = cssUrl;
cssLink.Attributes.Add("rel", "stylesheet");
cssLink.Attributes.Add("type", "text/css");
this.Page.Header.Controls.Add(cssLink);
// Add class name
this.CssClass = "WhoIsLooking";
This code grabs a reference to the external Cascading Style Sheet file by taking advantage of the GetWebResourceUrl() method. Next, an HtmlLink control is created and added to the header for the page. This control renders the following HTML:
<link href="/TestSite/WebResource.axd?d=Zyww-wIBakY8NHKUtjeDQZr5yvS_5HEj1E2zyc2CVX9Hyb34lNTSxDhkIp1IsJrd0&t=504910944000000000" rel="stylesheet" type="text/css" />
Notice that the link refers to a strange file named WebResource.axd. Resources are exposed to the client-side through an HTTP handler named WebResource.axd. The link contains a long key that uniquely identifies the resource to retrieve.
Finally, the code associates the WhoIsLooking control with the WhoIsLooking Cascading Style Sheet class. Any style rules for the WhoIsLooking class are applied to the WhoIsLooking control automatically.
Using an external Cascading Style Sheet to format a control is great way to style a control when you need to provide a lot of default formatting. You can use the handy Style Sheet editor included in Visual Studio to build up a fancy Style Sheet and then embed it in your control.
The WhoIsLooking control takes advantage of Ajax to display who is visiting a particular web page in real time. The control polls the server (by default, every 5 seconds) to retrieve a list of the people currently visiting the page.
Ajax stands for Asynchronous Javascript and XML. Ajax enables you to send information back and forth between a web browser and a web server without posting the page back to the web server. Communication happens behind the scenes.
The WhoIsLooking control takes advantage of the Ajax support built into the ASP.NET 2.0 Framework. In the ASP.NET 2.0 Framework, Ajax is called client callbacks. I'm going to continue to refer to the technology as Ajax instead of client callbacks because no one refers to the technology as client callbacks.
Implementing Ajax in a custom control requires you to complete three steps:
(1) Use the GetCallbackEventReference() method to initiate an Ajax call from the browser to the web server
(2) Implement the ICallBackEventHandler interface to respond to the Ajax call on the server-side. This interface has two methods that you must implement: the RaiseCallbackEvent() and GetCallbackResult methods.
(3) Create a Javascript function that does something with the result returned by the server
The WhoIsLooking control initiates an Ajax call every 5 seconds. The control includes the following code in its OnPreRender() method:
C#
// Add polling call
string callback = Page.ClientScript.GetCallbackEventReference
(
this,
null,
"WhoIsLooking.UpdateDisplay",
String.Format("'{0}'", this.ClientID),
"WhoIsLooking.CallbackError",
true);
string startupScript = String.Format("setInterval( \"{0}\", {1} );", callback, _PollingInterval * 1000);
Page.ClientScript.RegisterStartupScript(this.GetType(), "WhoIsLooking", startupScript, true);
VB.NET
' Add polling call
Dim callback As String = Page.ClientScript.GetCallbackEventReference( _
Me, _
Nothing, _
"WhoIsLooking.UpdateDisplay", _
String.Format("'{0}'", Me.ClientID), _
"WhoIsLooking.CallbackError", _
True)
Dim startupScript As String = String.Format("setInterval( ""{0}"", {1} );", callback, _PollingInterval * 1000)
Page.ClientScript.RegisterStartupScript(Me.GetType(), "WhoIsLooking", startupScript, True)
In the first line of code, the GetCallbackEventReference() method is used to get the snippet of Javascript necessary for initiating the Ajax call. Next, the Javascript window.setInterval method is used to initiate the Ajax call whenever a particular interval of time passes (the default polling interval is 5 seconds). Finally, the RegisterStartupScript() method is used to add the Javascript to the page.
The WhoIsLooking control implements the ICallbackEventHandler interface. The control includes both a RaiseCallbackEvent() and GetCallbackResult() method. The RaiseCallbackEvent() method adds the current user to the list of users viewing the page. The method also removes users who have not viewed the page for more than 10 seconds (twice the polling interval).
The GetCallbackResult() method returns a list of users viewing the page back to the browser. The following information is returned for each user:
This method returns a string that represents a JSON array of users. JSON is the standard format for serializing information in an Ajax call (to learn more about JSON, visit JSON.org). For example, if there were two users visiting the page, the JSON string would look something like this:
[{userId:"fooglm45cjcycw55qi4yluvk",userName:"SUPEREXPERT\\Steve", duration:"0 minute(s)", browser: "IE 7.0", remoteHost: "superexpert.com", platform: "WinXP"},{userId:"1kqatn55sxc4vi55ummxghil",userName:"SUPEREXPERT\\Bill", duration:"0 minute(s)", browser: "Firefox 1.5.0.11", remoteHost: "superexpert.com", platform: "WinXP"}]
You create a JSON array by creating a comma-separated list of values enclosed in brackets. You create a JSON object by create a comma-separated list of name/value pairs enclosed in curly braces. That's pretty much it for the JSON standard (it's much leaner and easier to use than XML).
Finally, the WhoIsLooking control uses a Javascript function named UpdateDisplay() to display the list of users in the browser. This function creates a <div> tag for each user. When you hover your mouse over a <div> tag, details for the user popup (see Figure 2).
Figure 2 - Viewing the WhoIsLooking control
Most of the information that the WhoIsLooking control retrieves for each user is retrieved with the help of the HttpBrowserCapabilities class (this class is exposed by the Request.Browser property). The HttpBrowserCapabilities class contains a wealth of information about the browser being used to visit the page. There are far too many properties to list here, but here is a random selection of some of the more interesting ones:
• AOL - Returns true if the client is using an AOL browser
• Browser - Returns the type of browser (IE, Firefox, Opera)
• ClrVersion - Returns the version of the .NET Framework installed on the client
• EcmaScriptVersion - Returns the version of Javascript installed on the client
• SupportsCallback - Returns a value indicating whether the browser supports client callbacks (Ajax)
• W3CDomVersion - Returns the version of the DOM (Document Object Model) supported by the browser
There is one property that the WhoIsLooking control displays that it does not get with the help of the HttpBrowserCapabilities class. The control displays the remote domain name associated with the user requesting the page. The remote domain name is retrieved by performing a reverse DNS lookup. In the .NET Framework 2.0, performing a reverse DNS lookup is ridiculously easy. The WhoIsLooking control uses the following code:
C#
// Get remote IP address
_remoteIP = context.Request.UserHostAddress;
// Get host name
_remoteHost = Dns.GetHostEntry(_remoteIP).HostName;
VB.NET
' Get remote IP address
_remoteIP = context.Request.UserHostAddress
' Get host name
_remoteHost = Dns.GetHostEntry(_remoteIP).HostName
I think the WhoIsLooking control is super cool. The control takes advantage of Javascript and Ajax to display a list of everyone who is looking at a particular Web page in real time. Please feel free to add the control to your websites, I'll come and visit.
After 2 days of scavenging the web for info on the topic of RegisterClientScriptResource and RegisterClientScriptInclude I am doing the rounds and sharing what I discovered with the people that posted on the same topic:
RegisterClientScriptInclude will not work unless your page has a <form runat='server"> somewhere.
Hope this helps all the people having problems with these new APIs,
/cheer
Man...this was a nice article! You just helped me solve a STUPID problem I was having!
I tried adding the css in headers as
this.Page.Header.Controls.Add(cssLink);
But i get an error saying :
System.Web.HttpException(0x80004005): The Controls collection cannot be modified because the control contains code blocks (i.e. <% ... %>)
Out of curiosity, after looking at the source, how does locking the thread make the generic dictionary an application level object? (it seems to be the same object throughout all user's request)
The control doesn't render the css at design time.
Great, simple, easy instructions.
Thanks!
When using the GetWebResourceUrl method in combination with creating an HtmlLink and adding it to the page header, how do you prevent duplicates when they have placed more than one of your controls on the page?
Thanks
Great article Stephen! It provides just the right amount of detail for me to really get started with custom controls.
Great archive. I like books writed by Stephen.
Great article. You clearly explained the why's and how's of some important pieces required to build server controls.
Thanks!
@Al: Stepping back and thinking about it, I think you could but would require you to do stuff with the global.asax file for your web app and then from there keep an up to date tracking.
I haven't tried to do this however yet. Seems do able.
Is it possible to use control to watch who is looking other pages, not page with control? How to pass page into whoislooking page?
Man you made my day, great article indeed.
Thanks!!
Nice article..
I will tried it out with my requirements today..
Many Thanks
very very nice and handy article....