Click here to Skip to main content
13,189,884 members (50,930 online)
Click here to Skip to main content
Add your own
alternative version


51 bookmarked
Posted 13 Jun 2009

ASP.NET AJAX Testing Made Easy using Visual Studio 2008 Web Test

, 11 Jun 2011
Rate this:
Please Sign up or sign in to vote.
A collection of ExtractionRules, ValidationRules, and Request Plugin that makes ASP.NET and AJAX website testing painless. No need to record tests, write parameterized tests using server-side control names, handle UpdatePanels, simulate clicks on buttons - all from Web Test.


Visual Studio 2008 comes with rich Web Testing support, but it’s not rich enough to test highly dynamic AJAX websites where the page content is generated dynamically from database and the same page output changes very frequently based on some external data source e.g. RSS feed. Although you can use the Web Test Record feature to record some browser actions by running a real browser and then play it back. But if the page that you are testing changes everytime you visit the page, then your recorded tests no longer work as expected. The problem with recorded Web Test is that it stores the generated ASP.NET Control ID, Form field names inside the test. If the page is no longer producing the same ASP.NET Control ID or same Form fields, then the recorded test no longer works. A very simple example is in VS Web Test, you can say “click the button with ID ctrl00_UpdatePanel003_SubmitButton002”, but you cannot say “click the 2nd Submit button inside the third UpdatePanel”. Another key limitation is in Web Tests, you cannot address Controls using the Server side Control ID like “SubmitButton”. You have to always use the generated Client ID which is something weird like “ctrl_00_SomeControl001_SubmitButton”. Moreover, if you are making AJAX calls where certain call returns some JSON or updates some UpdatePanel and then based on the server returned response, you want to make further AJAX calls or post the refreshed UpdatePanel, then recorded tests don’t work properly. You *do* have the option to write the tests hand coded and write code to handle such scenario but it’s pretty difficult to write hand coded tests when you are using UpdatePanels because you have to keep track of the page viewstates, form hidden variables, etc. across async post backs. So, I have built a library that makes it significantly easier to test dynamic AJAX websites and UpdatePanel rich web pages. There are several ExtractionRule and ValidationRule available in the library which makes testing Cookies, Response Headers, JSON output, discovering all UpdatePanel in a page, finding controls in the response body, finding controls inside some UpdatePanel all very easy.

What are the Capabilities of this Web Test Framework

First, let me give you an example of what can be tested using this library. My open source project Dropthings produces a Web 2.0 Start Page where the page is composed of widgets.


Each widget is composed of two UpdatePanels. There’s a header area in each widget which is one UpdatePanel and the body area is another UpdatePanel. Each widget is rendered from database using the unique ID of the widget row, which is an INT IDENTITY. Every page has unique widgets, with unique ASP.NET Control ID. As a result, there’s no way you can record a test and play it back because none of the ASP.NET Control IDs are ever the same for the same page on different visits. This is where my library comes to the rescue.

See the web test I did:


This test simulates an anonymous user's first visit experience and some activity on the page. When anonymous user visits Dropthings for the first time, two pages are created with some default widgets. You can also add new widgets on the page, you can drag & drop widgets, you can delete a widget.

This Web Test simulates the following behaviors:

  • Visit the homepage
  • Show the widget list which is an UpdatePanel. It checks if the UpdatePanel contains the BBC World widget.
  • Then it clicks on the “Edit” link of the “How to of the day” widget which brings up some options dynamically inside an UpdatePanel. Then it tries to change the Dropdown value inside the UpdatePanel to 10.
  • Adds a new widget from the Widget List. Ensures that the UpdatePanel postback successfully renders the new widget.
  • Deletes the newly added widget and ensures the widget is gone.
  • Logs user out.

Let’s build the Web Test step by step. First you add a Web Test plug-in:


This ASPNETWebTestPlugin is supplied with the library. It does a lot of work for you. I will explain it later. For now, keep in mind that it will handle all the post back, viewstate, UpdatePanel issues for you.

Then you add a request to visit some page. Here I am visiting the Default.aspx.


In case you are new to Visual Studio Web Tests, the {{Config.TestParameters.ServerURL}} is a configuration key where the Url of the website is set, e.g. http://localhost:8000. The configuration file is an XML file defined as:


Once I hit the page, I ensure the following things:

  • A persistent anonymous cookie is generated using the CookieValidationRule. It checks if the .DBANON cookie is generated by the ASP.NET AnonymousIdentficationProvider. It also ensures there’s no authenticated cookie produced like the .DBAUTH12 cookie. These are ASP.NET Membership cookies. Here I am testing both positive and negative scenarios. Positive scenario is what is expected – the anonymous cookie to be generated. Negative scenario is what is not expected – some authenticated cookie for some user should not be there. Here’s how the first Rule is configured in the Properties window:


  • Ensure there’s no cache header produced which will mistakenly cache the page on browser. We do not want the page to be cached anywhere. The CacheHeaderValidation class does this check.


  • Then I am using a couple of FindText validation rules to ensure the generated page has the default widgets by looking for some texts like “How to of the day”, “All rights reserved”, etc. A good practice is to ensure some texts from header, body and footer area is tested to ensure the whole page content is at least delivered properly.

Now I am going to click on a link that will open an UpdatePanel and check for certain content inside the UpdatePanel. The following test clicks on the “Add Stuff” link that you see on Dropthings which shows a widget list inside an UpdatePanel dynamically rendered from server.


The exciting thing here is the AsyncPostbackRequestPlugin. This request plug-in is specifically made to handle asynchronous postback made inside UpdatePanel. It has only two properties that you need to define, the name of the Control that needs to be clicked or posted back and the UpdatePanel which contains the Control.


Here’s the amazing part – the name of the Control is the server-side name, the name you used in the .aspx or .ascx. You don’t need to specify the Client ID which can be in weird form like ctrl00_MasterPage_ContentHandler001_ShowAddContentPanel. Similarly you can address the UpdatePanel using the server-side name of the UpdatePanel. The prefix $UPDATEPANEL is used to identify UpdatePanel, followed by the server-side name and then the index number or the nth UpdatePanel number. Here I am using “.1” because I want to hit the first UpdatePanel which has server-side ID OnPageMenuUpdatePanel.

This step clicks the control named “ShowAddContentPanel” inside the UpdatePanel named “OnPageMenuUpdatePanel”.

Now we are going to do some complicated form post. We want to click some link that will produce some new controls inside an UpdatePanel. Then we will modify those controls, click some button which will save the controls and refresh the UpdatePanel with new value.


If you do not have Dropthings open in front of you, go open it. Then click on the “edit” link on the “How to of the Day” widget. You will notice it shows a Dropdown list that defines how many items from the RSS feed to show. By default, it’s set to 5. I am going to change it to 10 and save the setting and see if the widget now shows 10 feed links.


First is to click the “edit” link which refreshes the UpdatePanel to show the Feed Count dropdown list.

Here’s the first AsyncPostbackRequestPlugin properties:


Since the “How to of the day” widget is the first widget on the page, I am using “.1” everywhere. If you want to test the second widget, change it to “.2

Now the second request sets the value of the dropdown to 10 and then clicks the “Close edit” link to apply the changes. Once clicked, it will refresh the feed links and show 10 links. The way I verify whether there are really 10 links generated on the UpdatePanel is by using the FindText ValidationRule:


Here I am checking if the FeedLink_ctl09_FeedLink link is there. If it’s there, then 10 links have been produced. Key here is to check for the ctl09.

The rest of the steps in the Web Test follow the same principle. They click something, expect some output, use the output to post something back to the server again and then check if the post was successful or not.

Code Walkthrough

Time to walk you through all the classes that work behind-the-scenes to bring you the simplified Web Test framework. First is the ASPNETWebTestPlugin.

public class ASPNETWebTestPlugin : WebTestPlugin
    #region Fields

    public const string STEP_NO_KEY = "$WEBTEST.StepNo";

    private const string TEMPORARY_STORE_EXTRACTION_RULE_KEY = "$TEMP.ExtractionRules";

    #endregion Fields

    #region Methods

    public override void PostRequest(object sender, PostRequestEventArgs e)
        base.PostRequest(sender, e);

        int stepNo = (int)e.WebTest.Context[STEP_NO_KEY];
        e.WebTest.Context[STEP_NO_KEY] = stepNo + 1;

        // When cookies are issues domain wide like, 
        // it does not get added to the Cookie container properly so that the 
        // cookie is sent out on visit
        foreach (Cookie cookie in e.Response.Cookies)
            if (cookie.Domain.StartsWith("."))
                CookieContainer container = e.WebTest.Context.CookieContainer;
                cookie.Domain = cookie.Domain.TrimStart('.');

    public override void PreRequest(object sender, PreRequestEventArgs e)
        base.PreRequest(sender, e);
        e.Request.ExtractValues += new EventHandler<ExtractionEventArgs>(

        if (!e.WebTest.Context.ContainsKey(STEP_NO_KEY))
            e.WebTest.Context[STEP_NO_KEY] = 1;

    void Request_ExtractValues(object sender, ExtractionEventArgs e)
        RuleHelper.WhenAspNetResponse(e.Response, () =>
                    () =>
                        // Extract all hidden fields so that they can be used in next 
                        // async/sync postback Hidden fields like __EVENTVALIDATION, 
                        // __VIEWSTATE changes after every async/sync postback. So, 
                        // these fields need to be kept up-to-date to make subsequent 
                        // async/sync postback
                        var extractionRule = new ExtractHiddenFields();
                        extractionRule.Required = true;
                        extractionRule.HtmlDecode = true;
                        extractionRule.ContextParameterName = "1";
                        extractionRule.Extract(sender, e);

                // Extract all INPUT/SELECT elements so that they can be posted in 
                // (async)postback
                    () => new ExtractFormElements().Extract(sender, e));

                // Extract all __doPostBack(...) so that the ID of the control can be 
                // used to make async postbacks
                    () => new ExtractPostbackNames().Extract(sender, e));

                // Extract all updatepanels so that during async postback, the 
                // updatepanel name can be derived
                    () => new ExtractUpdatePanels().Extract(sender, e));

The PreRequest method introduces a handy StepNo in the Context which makes it easy to identify at which step certain request failed. You can then map it with the Web Test requests and see exactly which step failed. This is handy when you have several same type of requests but can’t easily figure out which one failed. For example, you are hitting default.aspx on many places and one failed. From the step count, you can see which one failed:


Then on the Request_ExtractValues, it extracts the following items:

  • Extracts all hidden fields e.g. __VIEWSTATE.
  • Extracts all form elements like INPUT and SELECT tags and their selected value.
  • Finds all __doPostBack calls in buttons, links that can result in a postback or async-postback
  • Finds all UpdatePanel names.

First step is to extract the hidden fields. It uses Microsoft.VisualStudio.TestTools.WebTesting.Rules.ExtractHiddenFields to extract the hidden fields. Nothing special here. This class creates entries like:


The next step is to use my own class ExtractFormElements to extract all input, select tags and find their value.

[DisplayName("Extract Form Elements")]
public class ExtractFormElements : ExtractionRule
    #region Fields

    public const string FORM_ELEMENT_KEYS = "$FORM_ELEMENTS";
    public const string INPUT_PREFIX = "$INPUT.";
    public const string SELECT_PREFIX = "$SELECT.";
    public const string VALUE_SUFFIX = ".VALUE";

    private static Regex _FindInputTags = new Regex(
        + @"(?<value>([^""]*))""",
        | RegexOptions.Multiline
        | RegexOptions.IgnorePatternWhitespace
        | RegexOptions.Compiled
    private static Regex _FindSelectTags = new Regex(
        + @"*[^>]*selected=""[^""]*""\s*[^>]*value=""(?<value>([^""]*))""",
        | RegexOptions.Singleline
        | RegexOptions.IgnorePatternWhitespace
        | RegexOptions.Compiled

    #endregion Fields

    #region Methods

    public override void Extract(object sender, ExtractionEventArgs e)
        string body = e.Response.BodyString;

        List<string> formElements = new List<string>();
        var processMatches = new Action<MatchCollection, string>((matches, prefix) =>
                foreach (Match match in matches)
                    string name = match.Groups["name"].Value;
                    string value = match.Groups["value"].Value;

                    string lastPartOfName = name.Substring(name.LastIndexOf('$') + 1);
                    string keyName = RuleHelper.PlaceUniqueItem(e.WebTest.Context, 
                         prefix + lastPartOfName, name);
                    e.WebTest.Context[keyName + VALUE_SUFFIX] = value;

                    // Create a name value pair in context as it is using 
                    // the form element's name
                    e.WebTest.Context[name] = value;

        processMatches(_FindInputTags.Matches(body), INPUT_PREFIX);
        processMatches(_FindSelectTags.Matches(body), SELECT_PREFIX);

        e.WebTest.Context[FORM_ELEMENT_KEYS] = formElements.ToArray();

    #endregion Methods

This results in the following Context entries which you can use in the web test:


These are all INPUT buttons that have the same server-side name but ASP.NET generated unique ID for them. So, you can get both the unique ID and the value using the server-side name that you have used in your code.

Next step is to extract all the postback links, buttons, dropdown changes, etc. Anything that calls the ASP.NET’s __doPostback function to postback to server. We need to know this in order to get the name of the control that needs to be posted back. This helps us simulate clicks on links, buttons.

[DisplayName("Extract PostBack Names")]
public class ExtractPostbackNames : ExtractionRule
    #region Fields

    private static Regex _FindPostbackNames = new Regex(@"__doPostBack\('(.*?)'", 
        RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.IgnoreCase
        | RegexOptions.CultureInvariant);

    #endregion Fields

    #region Methods

    /// <summary>
    /// Find all javascript:__doPostback(...) type declarations which indicates
    /// all controls that support postback. It finds all such controls that support
    /// postback and then stores the full client ID of the control in Context
    /// using the last part of the ID as key in Context. For example:
    /// $POSTBACK.1.AddNewWidget = WidgetUpdatePanel001$ctl_002$AddNewWidget
    /// This way you can find a particular controls full client ID when you know only the
    /// server ID of the control.
    /// </summary>
    /// <param name="bodyHtml">Body HTML</param>
    /// <param name="context">WebTest Context</param>
    public static void ExtractPostBackNames(string bodyHtml, WebTestContext context)
        RuleHelper.NotAlreadyDone(context, "$POSTBACK.EXTRACTED", () =>
                var matches = _FindPostbackNames.Matches(bodyHtml);
                foreach (Match match in matches)
                    string fullID = match.Groups[1].Value;
                    string lastPartOfID = fullID.Substring(fullID.LastIndexOf('$') + 1);
                    string contextKeyName = "$POSTBACK." + lastPartOfID;

                    RuleHelper.PlaceUniqueItem(context, contextKeyName, fullID);

    public override void Extract(object sender, ExtractionEventArgs e)
        string bodyHtml = e.Response.BodyString;

        ExtractPostBackNames(bodyHtml, e.WebTest.Context);

    #endregion Methods

Finally, the most complicated ones, finding all the UpdatePanel on the page and the controls that belong to the UpdatePanel. We need to know the UpdatePanel before simulating click on any control because we need to send the UpdatePanel name to the server so that it knows which updatepanel is posted back and needs to be refreshed.

public class ExtractUpdatePanels : ExtractionRule
  #region Fields

  public const string UPDATE_PANEL_DECLARATION = 
  public const string UPDATE_PANEL_KEY = "$UPDATEPANEL";
  public const string UPDATE_PANEL_POS_KEY = ".$POS";
  public const string UPDATE_PANEL_PREFIX = UPDATE_PANEL_KEY + ".";

  private static Regex _FindUpdatePanelRegex = new Regex(
    | RegexOptions.Multiline
    | RegexOptions.IgnorePatternWhitespace
    | RegexOptions.Compiled

  #endregion Fields

  #region Methods

  public static void ExtractUpdatePanelNamesFromHtml(string body, WebTestContext context)
    RuleHelper.NotAlreadyDone(context, UPDATEPANEL_EXTRACTED_KEY, () =>
        // Do not extract update panel names twice
        int pos = body.IndexOf(UPDATE_PANEL_DECLARATION);
        if (pos > 0)
          // found declaration of all update panels on the page
          pos += UPDATE_PANEL_DECLARATION.Length;
          int endPos = body.IndexOf(']', pos);
          string updatePanelNamesDelimited = body.Substring(pos, endPos - pos);
          string[] updatePanelNames = updatePanelNamesDelimited.Split(',');
          int updatePanelCounter = 1;
          foreach (string updatePanelName in updatePanelNames)
            // Create a unique key in the context using the UpdatePanel's Last 
            // part of the ID which is usually the ID specified in aspx page
            string updatePanelFullId = 
            string updatePanelIdLastPart = 
		updatePanelFullId.Substring(updatePanelFullId.LastIndexOf('$') + 1);
            string contextKeyName = UPDATE_PANEL_PREFIX + updatePanelIdLastPart;
            string keyName = 
		RuleHelper.PlaceUniqueItem(context, contextKeyName, updatePanelFullId);

            // Store all update panels as $UPDATEPANEL.1, $UPDATEPANEL.2, ...
            context[UPDATE_PANEL_PREFIX + updatePanelCounter] = updatePanelFullId;

            // Find the position of the UpdatePanel
            string updatePanelDivId = updatePanelFullId.Replace('$', '_');
            // Look for a div with id having the updatepanel ID, 
            // e.g. <div id="UserTabPage_TabUpdatePanel">
            string lookingFor = "<div id=\"" + updatePanelDivId + "\"";
            int updatePanelDivIdPos = body.IndexOf(lookingFor);
            context[UPDATE_PANEL_PREFIX + updatePanelCounter + 
			UPDATE_PANEL_POS_KEY] = updatePanelDivIdPos;
            context[keyName + UPDATE_PANEL_POS_KEY] = updatePanelDivIdPos;


          context[UPDATE_PANEL_COUNT_KEY] = updatePanelCounter;

  public override void Extract(object sender, ExtractionEventArgs e)
    string body = e.Response.BodyString;
    if (e.Response.ContentType.Contains("text/html"))
      ExtractUpdatePanelNamesFromHtml(body, e.WebTest.Context);
    else // if (e.Response.ContentType.Contains("text/plain"))
      ExtractUpdatePanelNamesFromAsyncPostback(body, e.WebTest.Context);

  private void ExtractUpdatePanelNamesFromAsyncPostback
			(string body, WebTestContext context)
    RuleHelper.NotAlreadyDone(context, "$UPDATEPANEL.EXTRACTED", () =>
        int newUpdatePanelsAdded = 0;
        foreach (Match match in _FindUpdatePanelRegex.Matches(body))
          string updatePanelDivID = match.Groups["name"].Value;
          string updatePanelFullId = updatePanelDivID.Replace('_', '$');
          string updatePanelIdLastPart = 
		updatePanelFullId.Substring(updatePanelFullId.LastIndexOf('$') + 1);

          string contextKeyName = UPDATE_PANEL_PREFIX + updatePanelIdLastPart;
          string keyName = RuleHelper.PlaceUniqueItem
			(context, contextKeyName, updatePanelFullId);

          int countOfKeys = context.Count;
          RuleHelper.PlaceUniqueItem(context, UPDATE_PANEL_KEY, updatePanelFullId);
          if (context.Count > countOfKeys)

        context[UPDATE_PANEL_COUNT_KEY] = 
		((int)context[UPDATE_PANEL_COUNT_KEY]) + newUpdatePanelsAdded;

  #endregion Methods

This is a complex class. It finds the position of each UpdatePanel div from the HTML output and the text output from async-postback. Regular postback output is HTML and easy to parse. But output from an async-postback is in text format and in a special format that needs different parsing logic.

This results in following entries in Context:


This helps you identify UpdatePanel from server-side name. For example, you can find the first WidgetBodyUpdatePanel using $UPDATEPANEL.WidgetBodyUpdatePanel.1.

That’s all about the ASPNETWebTestPlugin. This plugin intercepts every request and response and prepares the Context with useful entries which you can use to prepare your test steps.

Next important plugin is the request specific plugin – AsyncPostbackRequestPlugin. You already know how to use it, now see how it does its job:

public class AsyncPostbackRequestPlugin : WebTestRequestPlugin
    #region Properties

    public string ControlName
        get; set;

    public string UpdatePanelName
        get; set;

    #endregion Properties

    #region Methods

    public override void PostRequest(object sender, PostRequestEventArgs e)
        base.PostRequest(sender, e);

    public override void PreRequest(object sender, PreRequestEventArgs e)
        base.PreRequest(sender, e);

        e.Request.Headers.Add("x-microsoftajax", "Delta=true");
        FormPostHttpBody formBody = e.Request.Body as FormPostHttpBody;
        if (null == formBody)
            formBody = (e.Request.Body = new FormPostHttpBody()) as FormPostHttpBody;
            e.Request.Method = "POST";

        string controlName = RuleHelper.ResolveContextValue(e.WebTest.Context, 
        string updatePanelName = RuleHelper.ResolveContextValue(e.WebTest.Context, 

        // Post all input hidden fields
        string[] hiddenFieldKeyNames = e.WebTest.Context["$HIDDEN1"] as string[];
        if (null != hiddenFieldKeyNames)
            foreach (string hiddenFieldKeyName in hiddenFieldKeyNames)
                     e.WebTest.Context["$HIDDEN1." + hiddenFieldKeyName] as string);

            RuleHelper.SetParameter(formBody.FormPostParameters, "ScriptManager1", 
                 updatePanelName + "|" + controlName, true);
            RuleHelper.SetParameter(formBody.FormPostParameters, "__EVENTTARGET", 
                  controlName, true);
            RuleHelper.SetParameter(formBody.FormPostParameters, "__ASYNCPOST", 
                  "true", true);

    #endregion Methods

First it sets the proper request headers for async postback. Then it collects all the hidden fields and stores in the FormPostParameters collection. Then it adds the three key entries that identifies which UpdatePanel to use, which control is being posted back and identify the postback as async postback.

There you have it, a super convenient way to test AJAX websites that is reusable, works even if the control IDs are auto generated or moved somewhere else, works over async postback, allows you to write tests with very little plumbing.

There are couple of other handy extraction rules and request plugins that you can use. Check out the code documentation to find out how they work.


Source Code

The source code of this project is available in the Dropthings codebase. Check out the Dropthings.Test project.


This Web Test library will greatly simplify your automated Web Test. The benefit of using automated Web Test is that, you don't have to use some browser based recording tools which can only playback using a browser. Web Tests are fully executable without a browser and you can make Web Tests part of your Load Test. As a result, you write Web Test once, and then you can perform Load Tests using the same Web Tests. This is the single most important reason I use Web Tests instead of browser based tools like Selenium because I can do Load Test using the same Web Tests. But without this library, it's a super pain to write useful Web Tests. So, hope this library helps you write great Web Tests.

DotNetKicks Image


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


About the Author

You may also be interested in...


Comments and Discussions

QuestionNeed to Select an AJAX drop down control item givin the text of the item, is this possible? Pin
ggauvin7-Jan-10 3:47
memberggauvin7-Jan-10 3:47 
AnswerRe: Need to Select an AJAX drop down control item givin the text of the item, is this possible? Pin
Omar Al Zabir7-Jan-10 3:55
mvpOmar Al Zabir7-Jan-10 3:55 
GeneralRe: Need to Select an AJAX drop down control item givin the text of the item, is this possible? Pin
ggauvin7-Jan-10 4:45
memberggauvin7-Jan-10 4:45 
QuestionQuestions about WebTest using Visual Studio Test Edition Pin
cpp_prgmer15-Nov-09 13:49
membercpp_prgmer15-Nov-09 13:49 
Hi Omar,

Your article is indeed very informative but I have some specific WebTest issues using Visual Studio Test Edition. For example I have recorded a web test but while I play it back I find some sub-requests failing, and I am unable to trace the cause (I've tried a few standard workarounds suggested in msdn and a few other places, but couldn't find a solution)...
Could you suggest some forums or some good books\article to get a good grip on this subject?

I am focussed on issues concerning Web Load Test using Visual Studio Test Edition and there do not seem to be enough forums or even common Testing websites discussing the same... Any help is appreciated.
AnswerRe: Questions about WebTest using Visual Studio Test Edition Pin
Omar Al Zabir16-Nov-09 1:20
mvpOmar Al Zabir16-Nov-09 1:20 
GeneralWhere is Visual Studio 2008 Web Test Pin
Tatworth27-Jul-09 3:56
memberTatworth27-Jul-09 3:56 
GeneralRe: Where is Visual Studio 2008 Web Test Pin
Dalsoft1-Sep-09 2:27
memberDalsoft1-Sep-09 2:27 
GeneralSuperb work Pin
Moim Hossain13-Jun-09 6:56
memberMoim Hossain13-Jun-09 6:56 

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

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

Permalink | Advertise | Privacy | Terms of Use | Mobile
Web01 | 2.8.171016.2 | Last Updated 11 Jun 2011
Article Copyright 2009 by Omar Al Zabir
Everything else Copyright © CodeProject, 1999-2017
Layout: fixed | fluid