Click here to Skip to main content
Click here to Skip to main content

Overlib Custom Controls using WebResource Files

, 15 Apr 2007
Rate this:
Please Sign up or sign in to vote.
Article demonstrates how to embed the Overlib JavaScript library using custom controls and additionally how to provide access to the library with a Web Resource file, thereby eliminating the need to copy the scripts into the directory of your website.

Introduction

This article describes an encapsulation of an existing JavaScript popup library using ASP.NET custom controls with the script being provided to the page directly from an assembly using a WebResource (.axd) stream.

Some background on the script library will be given, along with how to use the WebResource technique to eliminate having to manually deploy the script on your website or to dynamically modify the web.config and site structure at design time to accomplish the same end.

Lastly some examples of extending standard ASP.NET controls such as the button, link button and image will be demonstrated, illustrating how easy it is to use the scripts, in conjunction with a script manager control which is placed on the page at design time.

The demonstration code uses VS 2005 SP1 to take advantage of Web Applications, and the sample code is written with C#, so make sure you have both.

Background

While working on a client project, I had decided to provide extra information about a grid item using an icon, with the text embedded as a tooltip. This worked fine, however ToolTips disappear quickly. The client wanted time to read the text, particularly as there might be a few paragraphs. There are quite a few alternatives to show extra information including:

  • JavaScript popup window using the window.open(...) method
  • Use a select command on Gridview, requiring a postback
  • Have an information panel next to the grid, requiring a command handler (already present)
  • Div popup like some of the clever AJAX popups, or those that show ads on many commerical web-sites

I have tried all of the methods in the past with success, but settled on the div-popup because it eliminated the postback and could show the information right away. The trouble was writing the JavaScript to do it and ending up with a polished result. After searching and finding some pretty cool techniques, the Overlib JavaScript library by Erik Bosrup et al. seemed to offer a lot of flexibility, different types of windows and was a project still being managed.

The Overlib library can be used without custom controls by linking the source in the HEAD, adding a reusable DIV in the body and adding event handlers like ONMOUSEOVER, ONMOUSEOUT to call the script. And there is nothing wrong with that technique. But as a professional developer, I have an aversion to JavaScript and to pages where manual calls to it are made. Besides, having learnt to write custom controls the hard way, I know how much easier it is to drag-an-drop controls on the page to automatically to do it. In short, I want to forget about the JavaScript itself and get back to writing code the .NET way.

Overlib offers a lot of options to control the windows and there has been a solution posted as a pluggable C# control based library in C# 1.1 which works in 2.0 as-is. It is easy to see that a lot of hard work has gone into it as it can be configured to accept revisions to the JavaScript, and because popups can be configured and added dynamically.

This is all fine when your page can be built that way, but I wanted to use the Overlib to extend the already existing controls that participate in binding like the asp:image, asp:linkbutton, asp:button and also my own control suite that I make available and extend with each new client project. And I wanted to simplify the controls I extended so that I can just drag-and-drop from the toolbox controls that were already preconfigured to use Overlib and to never configure them again.

The article is divided into three parts:

  • Part 1: Demonstration Files for Overlib
  • Part 2: Encapsulation with WebResource.axd, and
  • Part 3: A Custom Overlib Library.

Part 1 demonstrates the use of Overlib without custom controls, and relies on manual connection of attributes, addition of a script reference to every page that uses the library and a script folder. This will be useful for comparison with the later parts.

Part 2 shows how to encapsulate the Overlib JavaScript library in an assembly, how this is used to eliminate the need for a script folder, and how to extend some ASP.NET controls to use the Overlib library automatically.

Finally, part 3 shows how to implement a simple library of controls so that they can be drag-and-dropped directly to the page without having to remember all of the Overlib settings, with no code-behind required.

Part 1: Demonstration Files for Overlib

In order to appreciate what the code does, I need to show you what the code later in this article is going to replace. Begin by referring to the OverlibDemo project attached. There are two key files for this part

  1. DemoWithScriptFolder
  2. OverlibDemo.Web

DemoWithScriptFolder is a Web application that has a single default page that shows how to use Overlib as-is. The scripts are in a folder called scripts/overlib421 and are in their own folder to keep version 4.21 separate from future releases. No changes to the scripts have been made and they are as I extracted them.

If you run the demo after setting DemoWithScriptFolder as the StartUp project, you will see that the Overlib popups appear over the first row of HTML controls as well as the second row of ASP.NET controls. The HTML controls are configured exactly as in the reference code for Overlib but I have attached to the ASP.NET controls using helper methods from the class library OverlibDemo.Web.

The page works because of the script in the head and the div directly after the body tag in the following html snippet which you must manually add to all pages where the Overlib library is to be used.

<head runat="server">
    <title>Untitled Page</title>
    <script type="text/javascript"
    src="http://www.mydomain.com/scripts/overlib421/overlib.js">
    <!-- overLIB (c) Erik Bosrup -->
    </script>
</head>
<body>
    <div id="overDiv" style="position:absolute;
            visibility:hidden; z-index:1000;"></div>
    <form id="form2" runat="server">
    <div>

...

using System;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.HtmlControls;

using OverlibDemo.Web;

namespace DemoWithScriptFolder
{
    public partial class Default : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            if (!IsPostBack)
            {
                AttachControlAttributes();
            }
        }

        private void AttachControlAttributes()
        {
            // Attach attributes directly

            TextBox1.Attributes.Add("onmouseover",
            "return overlib('This is an ordinary  popup.');");
            TextBox1.Attributes.Add("onmouseout", "return nd();");

            // Attach attributes using a helper class

            Helper.AttachOverlibAttributes(LinkButton1, AttributeAttach.OnMouseOver,
                    "'This is what we call a sticky, since I stick around
            (it goes away if you move the mouse OVER and then OFF
            the overLIB popup--or mouseover another overLIB).',
            STICKY, MOUSEOFF");
            Helper.AttachOverlibAttributes(Label1, AttributeAttach.OnClick,
                "'This is a sticky with a caption.
            And it is centered under the mouse!', STICKY, CAPTION,
            'Sticky!', CENTER");
        }
    }
}    

Because the attributes need only be attached once, a call is made in the first page_load. The onmouseover and onmouseout attributes are added for the TextBox control in a manual way to show what the Helper class does. It would be possible this way to have helper class methods to produce predefined popup types but I have left that for the interested reader. There will be more on this in the next demonstration.

Part 2: Encapsulation with WebResource.axd

If you are happy to use the script folder method, then this section need not apply to you. However, I think this is a valuable method of accessing scripts and other resources in general.

The aim is to produce the following HTML code snippet, with each script representing a cached copy of each of the Overlib scripts. If you don't like so many scripts for just one implementation, then consider concatenating the scripts into one script.

<script src="http://www.mydomain.com/WebResource.axd?d=
RA_RSv4e2a3_RCfGm3LhncSOtTvZxzDlgRvJ9DhK21R2FRY1x3xUfiRbjPuNJs0Tr6AoMVhJBSZnxk
NsyizEStKD-NGRQVn8MVZPOt-U4K-HtAl2XxxXO591bJekyiuI0&t=633118836619375000"
type="text/javascript"></script>

<script src="http://www.mydomain.com/WebResource.axd?d=
RA_RSv4e2a3_RCfGm3LhncSOtTvZxzDlgRvJ9DhK21R2FRY1x3xUfiRbjPuNJs0Tr6AoMVhJBSZnxk
NsyizEStKD-NGRQVn8MVZPOt-U4K97UyrpPXHxsyoT-BPfJ8hD0&t=633118836619375000"
type="text/javascript"></script>

<script src="http://www.mydomain.com/WebResource.axd?d=
RA_RSv4e2a3_RCfGm3LhncSOtTvZxzDlgRvJ9DhK21R2FRY1x3xUfiRbjPuNJs0Tr6AoMVhJBSZnxk
NsyizEStKD-NGRQVn8MVZPOt-U4K_5EclWnboZocXmXtsHPZzEXGg9-deQZ_JuBvTyRc6ubw2&t=633118836619375000"
type="text/javascript"></script>

<script src="http://www.mydomain.com/WebResource.axd?d=
RA_RSv4e2a3_RCfGm3LhncSOtTvZxzDlgRvJ9DhK21R2FRY1x3xUfiRbjPuNJs0Tr6AoMVhJBSZnxk
NsyizEStKD-NGRQVn8MVZPOt-U4K9IAQrv9eBK5iELJ9z_XugH0&t=633118836619375000"
type="text/javascript"></script>

<script src="http://www.mydomain.com/WebResource.axd?d=
RA_RSv4e2a3_RCfGm3LhncSOtTvZxzDlgRvJ9DhK21R2FRY1x3xUfiRbjPuNJs0Tr6AoMVhJBSZnxk
NsyizEStKD-NGRQVn8MVZPOt-U4K9lJFNkVYCC9HejPdW3brQN0&t=633118836619375000"
type="text/javascript"></script>

<script src="http://www.mydomain.com/WebResource.axd?d=
RA_RSv4e2a3_RCfGm3LhncSOtTvZxzDlgRvJ9DhK21R2FRY1x3xUfiRbjPuNJs0Tr6AoMVhJBSZnxk
NsyizEStKD-NGRQVn8MVZPOt-U4K9FDnGy2T1Ue2LUFVEy0A4x0&t=633118836619375000"
type="text/javascript"></script>

<script src="http://www.mydomain.com/WebResource.axd?d=
RA_RSv4e2a3_RCfGm3LhncSOtTvZxzDlgRvJ9DhK21R2FRY1x3xUfiRbjPuNJs0Tr6AoMVhJBSZnxk
NsyizEStKD-NGRQVn8MVZPOt-U4K-v-UsQI3RAmi58dICS1iLs0&t=633118836619375000"
type="text/javascript"></script>

<script src="http://www.mydomain.com/WebResource.axd?d=
RA_RSv4e2a3_RCfGm3LhncSOtTvZxzDlgRvJ9DhK21R2FRY1x3xUfiRbjPuNJs0Tr6AoMVhJBSZnxk
NsyizEStKD-NGRQVn8MVZPOt-U4K8tmjuGT_2t2VScXJFR9FJVXbaFiN5ojD_lJWbSwEpChw2&t=633118836619375000"
type="text/javascript"></script>

<script src="http://www.mydomain.com/WebResource.axd?d=
RA_RSv4e2a3_RCfGm3LhncSOtTvZxzDlgRvJ9DhK21R2FRY1x3xUfiRbjPuNJs0Tr6AoMVhJBSZnxk
NsyizEStKD-NGRQVn8MVZPOt-U4K8YbNQeEYKqJM9qqdnsXXFs0&t=633118836619375000"
type="text/javascript"></script>

<script src="http://www.mydomain.com/WebResource.axd?d=
RA_RSv4e2a3_RCfGm3LhncSOtTvZxzDlgRvJ9DhK21R2FRY1x3xUfiRbjPuNJs0Tr6AoMVhJBSZnxk
NsyizEStKD-NGRQVn8MVZPOt-U4K-B4jTGkO2pU5MA9ug0_qfX0&t=633118836619375000"
type="text/javascript"></script>

<script src="http://www.mydomain.com/WebResource.axd?d=
RA_RSv4e2a3_RCfGm3LhncSOtTvZxzDlgRvJ9DhK21R2FRY1x3xUfiRbjPuNJs0Tr6AoMVhJBSZnxk
NsyizEStKD-NGRQVn8MVZPOt-U4K8Q92ExUFMgFmDdkB-bhsRa0&t=633118836619375000"
type="text/javascript"></script> 

Each WebResource.axd is simply a keyed reference to a unique resource, in this case a script file. For more information on WebResource files refer to Praveen Yerneni's article on MSDN.

To begin the demonstration, there are two project files in the OverlibDemo that you should refer to :

  1. DemoWithScriptEmbedding
  2. OverlibDemo.Web.UI.WebControls

The first is the Web Application that has the demonstration pages for Part 2 and Part 3, and the second file is a class library that cleanly encapsulates the Overlib scripts. Set the default StartUp project to DemoWithScriptEmbedding and take a look at the Part 2 link to make sure it all works.

The technique described here is to embed the scripts as part of the class library assembly, and write custom controls that automate the registration of script files. The first step is to add the following line to the AssemblyInfo.cs file

// Adding the tag prefix tag means that when a control is added to a page,
// the tag prefix will be meaningful
[assembly: TagPrefix("OverlibDemo.Web.UI.WebControls", "OL")]  

You can, of course, change the prefix to whatever you want, and it just makes it easier to identify your controls in a complex page. Note that if you make a typo in the assembly name, you will get cc1, cc2 etc. as the default tag prefix when you add the controls to a page.

This is what the assembly reference looks at the top of an aspx page (I chose the WebControls namespace to follow Microsoft's conventions):

<%@ Register Assembly="OverlibDemo.Web.UI.WebControls"
Namespace="OverlibDemo.Web.UI.WebControls" TagPrefix="OL" %>    

Referring to the class library, there is a folder Overlib421 with all of the Overlib scripts. Because these are to be embedded it is VERY IMPORTANT to alter the Build Action for each script to Embedded Resource. Failure to do this will result in the script links appearing on the page, but references to the JavaScript causing errors. The build action is set by single-clicking each JavaScript, and editing the Build Action property which is the first property in the Advanced section.

To make the WebResource work and be available on the page, I drop a single custom control on the page called OverLibPageControl (to be discussed shortly) which performs all of the script registration. However a number of support classes are required to make this control work. The first class is a ScriptManager class which registers the scripts as follows.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Text;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

[assembly: WebResource("OverlibDemo.Web.UI.WebControls.OverLib421.overlib.js",
        "application/x-javascript", PerformSubstitution = false)]
[assembly: WebResource("OverlibDemo.Web.UI.WebControls.OverLib421.overlib_anchor.js",
        "application/x-javascript", PerformSubstitution = false)]
[assembly: WebResource("OverlibDemo.Web.UI.WebControls.OverLib421.overlib_crossframe.js",
        "application/x-javascript", PerformSubstitution = false)]
[assembly: WebResource("OverlibDemo.Web.UI.WebControls.OverLib421.overlib_cssstyle.js",
        "application/x-javascript", PerformSubstitution = false)]
[assembly: WebResource("OverlibDemo.Web.UI.WebControls.OverLib421.overlib_debug.js",
        "application/x-javascript", PerformSubstitution = false)]
[assembly: WebResource("OverlibDemo.Web.UI.WebControls.OverLib421.overlib_exclusive.js",
        "application/x-javascript", PerformSubstitution = false)]
[assembly: WebResource("OverlibDemo.Web.UI.WebControls.OverLib421.overlib_followscroll.js",
        "application/x-javascript", PerformSubstitution = false)]
[assembly: WebResource("OverlibDemo.Web.UI.WebControls.OverLib421.overlib_hideform.js",
        "application/x-javascript", PerformSubstitution = false)]
[assembly: WebResource("OverlibDemo.Web.UI.WebControls.OverLib421.overlib_setonoff.js",
        "application/x-javascript", PerformSubstitution = false)]
[assembly: WebResource("OverlibDemo.Web.UI.WebControls.OverLib421.overlib_shadow.js",
        "application/x-javascript", PerformSubstitution = false)]

namespace OverlibDemo.Web.UI.WebControls.OverLib421
{
    public class ScriptManager
    {
        private static string scriptNamespace = "OverlibDemo.Web.UI.WebControls.OverLib421";

        private static string[] scriptNames = new string[] {   "overlib",
                                                        "overlib_anchor",
                                                        "overlib_centerpopup",
                                                        "overlib_crossframe",
                                                        "overlib_cssstyle",
                                                        "overlib_debug",
                                                        "overlib_exclusive",
                                                        "overlib_followscroll",
                                                        "overlib_hideform",
                                                        "overlib_setononff",
                                                        "overlib_shadow" };
        private static string scriptFmtFullName = "{0}.{1}.js";

        public static void RenderOverlibScripts(Page page)
        {
            ClientScriptManager cs = page.ClientScript;

            Type rstype = typeof(ScriptManager);

            foreach (string scriptName in scriptNames)
            {
                string script = string.Format(scriptFmtFullName,
                        scriptNamespace, scriptName);

                cs.RegisterClientScriptResource(rstype, script);
            }
        }
    }
}    

The first block of assembly meta-data marks the scripts as is available from the assembly for streaming with firstly their full namespace name, then their mime-type. It is very important to get the namespace correct as I found this quite tricky the first time round. Importantly, note that the OverLib421 folder is included as part of the namespace.

Another noteworthy part of the class is the RenderOverlibScripts method which registers the scripts on the page by iterating over the array of script names. The ScriptManager will be called from other controls which rely on the script library, which is why the Script Manager itself is not a custom control.

The next required class is ScriptPageControl which will be a base class to simplify the design of the final page control, OverLibPageControl.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Text;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace OverlibDemo.Web.UI.WebControls.OverLib421
{
    [ToolboxData("<{0}:ScriptPageControl runat="server">")]
    public class ScriptPageControl : WebControl, IRegisterOverLib
    {
        protected override void OnPreRender(EventArgs e)
        {
            base.OnPreRender(e);

            RegisterScripts(this.Page);
        }

        #region IRegisterOverLib Members

        public void RegisterScripts(Page page)
        {
            ScriptManager.RenderOverlibScripts(page);
        }

        #endregion
    }
}    

ScriptPageControl is a custom control, which simply calls the script manager to register the Overlib scripts, once it has completed the OnPreRender method. The control that you as a developer might drop on the page is OverLibPageControl, as below:

using System.Web.UI;

namespace OverlibDemo.Web.UI.WebControls
{
    [ToolboxData("<{0}:OverLibPageControl runat="server">")]
    public class OverLibPageControl : OverLib421.ScriptPageControl
    {
        protected override void RenderContents(HtmlTextWriter writer)
        {
            if (DesignMode)
            {
                writer.Write("" + this.ClientID + "");
            }
        }
    }
}    

OverLibPageControl is in a slightly different namespace, and has RenderContents overridden to make the control visible on the Design Surface in VS2005.

There are two ASP.NET controls that have been extended to use the scripts, these being ImageExt and LinkButtonExt. Refer to the class files for more information. As far as their presence on the aspx page is concerned, all that is required to make a popup appear when mousing over is to add text to the PopupText attribute.

<OL:ImageExt ID="ImageExt1" runat="server"
    PopupText="'This is a sticky with a caption.
    And it is centered under the mouse!', STICKY, CAPTION, 'Sticky!', CENTER" />
<OL:LinkButtonExt ID="LinkButtonExt1" runat="server" Text="here"
    PopupText="'This is what we call a sticky, since I stick around
    (it goes away if you move the mouse OVER and then OFF the overLIB popup--
    or mouseover another overLIB).', STICKY, MOUSEOFF"/>    

Both of these ASP.NET controls are extended by making use of AddAttributesToRender and preventing empty popups appearing. The advantage of this encapsulation is that they can be drag-and-dropped straight onto the page which will keep your code clean.

protected override void AddAttributesToRender(HtmlTextWriter writer)
{
    if (PopupText.Trim() != string.Empty)
    {
        writer.AddAttribute("onmouseover", "return overlib(" + PopupText + ");");
        writer.AddAttribute("onmouseout", "return nd();");
    }
    base.AddAttributesToRender(writer);
}

Part 3: A Custom Overlib Library

This section builds on the classes in Part 2 by building three purpose-built popup controls that you can alter and extend as required. Open the Part 3 link from the previous demonstration, and then take a look at the OverlibCustomControls.aspx page. There are no code-behind methods on the page, and three independent custom controls on the design surface. There is no OverlibPageControl required as these controls register the scripts themselves. If you also require access to the JavaScripts, then they are automatically available as long as at least one of the three controls is present. Deliberately adding an OverlibPageControl won't cause any problems, it just isn't necessary. Also note that the special overlib div isn't present on the page. If you require more control of the Z-Index of the div, then embed it as part of the Script Manager, otherwise overlib adds it automatically. The three controls created for this demo are:

  1. OrdinaryPopup
  2. StickyCaption
  3. StickyPopup

You will see that they are very similar and rely on a single base class called OverLib421Popup, parts of which are shown below. The class has two key properties: Text and PopupText. The Text is the link text, while PopupText is the text that is shown in the popup. Because of the encapsulation and re-use, each of the three controls adds the overlib formatting arguments so that unlike in the previous demonstration, the PopupText doesn't contain any formatting information.

/// <span class="code-SummaryComment"><summary></span>
/// A method that the inheriting class must implement.
/// The inheriting control is responsible for creating the string
/// that is passed to javascript function overlib()
/// <span class="code-SummaryComment"></summary></span>
/// <span class="code-SummaryComment"><returns></returns></span>
protected virtual string OverlibParameterString()
{
    throw new Exception(ExOverlibParameterStringMustBeImplementedByInheritingControl);
}

protected virtual string OverlibActivationAttribute()
{
    return StrOnMouseOver;
}

protected override void RenderContents(HtmlTextWriter output)
{

    output.WriteBeginTag("a");
    output.WriteAttribute("href", "javascript:void(0);");

    if (PopupText.Trim() != string.Empty)
    {
        output.WriteAttribute(OverlibActivationAttribute(),
        string.Format(FmtOverlibParam, OverlibParameterString()));
        output.WriteAttribute("onmouseout", StrOverlibNd);
    }

    output.Write('>');
    output.Write(Text);
    output.WriteEndTag("a");
}         

The important methods are shown above. OverlibParameterString and OverlibActivationAttribute are both virtual methods returning strings. OverlibParameterString will be called by an inheriting class and will return a formatted JavaScript overlib argument string, while OverlibActivationAttribute is typically either 'onclick' or 'onmouseover'.

For an ordinary popup, the control OrdinaryPopup inherits the popup control, and since there are no formatting arguments the PopupText is returned as the only argument for overlib().

using System.Web.UI;

namespace OverlibDemo.Web.UI.WebControls
{
    [ToolboxData("<{0}:OrdinaryPopup runat="server">")]

    public class OrdinaryPopup : OverLib421.OverLib421Popup
    {
        const string FmtOrdinaryParam = "'{0}'";

        protected override string OverlibParameterString()
        {
            return string.Format(FmtOrdinaryParam, PopupText);
        }
    }
}  

The sticky caption is encapsulated with StickyCaption as below:

using System.ComponentModel;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace OverlibDemo.Web.UI.WebControls
{
    [ToolboxData("<{0}:StickyCaption runat="server">")]

    public class StickyCaption : OverLib421.OverLib421Popup
    {
        const string FmtStickyCaptionParam = "'{0}', STICKY, CAPTION,
                            'Sticky!', CENTER";

        protected override string OverlibParameterString()
        {
            return string.Format(FmtStickyCaptionParam, PopupText);
        }

        protected override string OverlibActivationAttribute()
        {
            return StrOnClick;
        }
    }
}   

The Caption is activated by a mouse click so the activation attribute is overridden. There is a change to the formatting also. The implementation of StickyPopup is similar, so please refer to the class for implementation details.

Summary

There is a fair amount of detail here, though I hope that with referring to the demonstration code and the text it will make sense. I believe the overlib scripts are a great resource and are simple to implement and encapsulate. With some effort, the WebResource.axd approach, though new for me, will ease deployment of your own class libraries.

Note that whether you stay with script folders or use WebResource files, the beginnings of your own class library can be made to work with either.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Zoggs
Web Developer
Australia Australia
No Biography provided

Comments and Discussions

 
GeneralI didnt work for me. PinmemberDeepthiP28-Jun-07 20:19 
GeneralRe: I didnt work for me. PinmemberZoggs16-Jul-07 18:42 
GeneralGood work mate! PinmemberAdam Tibi17-Apr-07 2:23 
GeneralRe: Good work mate! PinmemberZoggs22-Apr-07 13:20 
Generali find a bug PinmemberDatabinder15-Apr-07 16:29 
GeneralRe: i find a bug PinmemberZoggs22-Apr-07 13:18 
GeneralRe: i find a bug PinmemberDatabinder22-Apr-07 18:04 
GeneralRe: i find a bug PinmemberZoggs22-Apr-07 19:31 
GeneralRe: i find a bug PinmemberDatabinder22-Apr-07 20:20 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web02 | 2.8.140827.1 | Last Updated 15 Apr 2007
Article Copyright 2007 by Zoggs
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid