Click here to Skip to main content
15,946,342 members
Articles / Web Development / HTML5

HTML5 WebWorkers Experiment

Rate me:
Please Sign up or sign in to vote.
4.95/5 (59 votes)
13 Jul 2011CPOL13 min read 176.5K   1.6K   108   44
Using HTML5 WebWorkers and a custom jQuery plug-in to create a Flickr image wall.

Table of Contents

Introduction

What is this article all about? Well, it is about a couple of different things that I have never done before, namely writing my own jQuery plug-in and working with HTML5 WebWorkers.

For those that have never heard of jQuery, it's essentially a JavaScript library. Here is what the jQuery web site says about jQuery:

"jQuery is a fast and concise JavaScript library that simplifies HTML document traversing, event handling, animating, and AJAX interactions for rapid web development. jQuery is designed to change the way that you write JavaScript."

-- http://jQuery.com/ up on date 13/07/2011

Creating your own jQuery plug-in is a well documented thing, which you can read about on the jQuery site, but I will be going through this in some detail in the sections below.

HTML5 WebWorkers are essentially threads that are created via JavaScript; if you are a .NET kind of chap, think System.Threading.Tasks.Task. They simple carry out a unit of work in a dedicated thread. We will see more on this later.

So how are these two things used together within this article? Clearly, I needed some use case. So here was the scenario that I came up with, which is what this article does.

I wanted to create a jQuery plug-in that can be applied to a single element, where the jQuery plug-in would accept an array of search terms. For each of the search terms, a new HTML5 WebWorker is spawned that will do an AJAX Flickr search for images that match that search term. When the HTML5 WebWorker completes, the HTML5 WebWorker calls back into the jQuery plug-in, at which point an image wall is created.

In a nutshell, that is what this articles code does. We will be going through this in some detail in the sections below.

Authoring a jQuery Plug-in

As I stated in the introduction, I wanted to create my own jQuery plug-in. As I also stated, this is a well documented process which is available at the jQuery website link: http://docs.jquery.com/Plugins/Authoring.

My jQuery plug-in does not implement all the suggestions at the jQuery website. Here is a list of the features it does implement:

  • Context
  • Maintains Chainability
  • Defaults and Options

To see what these mean, please consult the jQuery website link: http://docs.jquery.com/Plugins/Authoring.

Web Workers: The Basic Idea

HTML5 WebWorkers are reasonably new and I guess they are not yet in widespread usage. So what is the basic idea behind them? Well, speaking plainly, a WebWorker is a unit of work that will be carried out on a new thread. Yes, that's right, the ability to create new threads in the browser.

Communication Between Host and WebWorker

Communication between the hosting code (the code that creates the WebWorker) and the actual WebWorker is achieved using a PostMessage like API and a couple of event handlers that the WebWorker exposes.

Here is what some typical code may look like within the hosting code, notice how we create a new WebWorker and hook up its two events:

  1. onMessage, is hooked to the workerResultReceiver function
  2. onError, is hooked to the workerErrorReceiver function

And the worker is started using the PostMessage API, which is how all message to/from the worker are done.

JavaScript
//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// code in WebWorkwer hosting code
//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
var worker = new Worker("scripts/FlickrWorkerSearch.js");
worker.onmessage = workerResultReceiver;
worker.onerror = workerErrorReceiver;
worker.postMessage({ 'cmd': 'start', 'msg': settings.searchWords[i] });

function workerResultReceiver(e) {
    //so something with the workers data
    var result = e.Data;
}

function workerErrorReceiver(e) {
    console.log("there was a problem with the WebWorker within " + e);
}

And this is what some typical WebWorker would look like:

JavaScript
//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// code in WebWorkwer JavaScript file
//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

self.addEventListener('message', function (e) {
    var data = e.data;

    switch (data.cmd) {
        case 'start':
            postMessage("The worker says hello");
            break;
        case 'stop':
            self.close(); // Terminates the worker.
            break;
        default:
            self.postMessage('Unknown command: ' + data.msg);
    };
}, false);

This simple example simply sends a string back to the host saying "The worker says hello".

Unit of Work

A unit of work as far as a WebWorker is concerned is a single JavaScript file that is passed to the WebWorker when it is constructed, something like this:

JavaScript
var worker = new Worker("scripts/FlickrWorkerSearch.js");

We will see what a typical worker JavaScript file looks like later within this article.

Importing Other Scripts

You can import other JavaScript files in your main WebWorker by using a line similar to the one shown below:

JavaScript
ImportScripts("scripts/SomeJavaScriptFile.js");

Beware though, the JavaScript files you import must not touch the DOM in any way (so that means they probably can't use jQuery internally either, as jQuery uses window (which is in the DOM)).

Areas of Caution

This section outlines some areas where you must be careful when working with WebWorkers.

No DOM Access

WebWorkers can not see the DOM, and as such will not be able to import any script that accesses the DOM, such as jQuery. Which is a shame as jQuery has extra non-DOM related stuff in it that would be nice to use in a WebWorker, but rules are rules I suppose.

I initially thought I could circumvent this rule by using the DOM in the WebWorker host and passing in some object into the WebWorker via a message; alas, this approach failed too, as the object could not be cloned.

The general rule of thumb is that the messages to/from a WebWorker must be primitive types such as array/string and other simple types.

No Thread Locking Primitives

OK, so we have the ability to run threads, but unfortunately, we have no threading objects to ensure the communication of these threads, and ensure that there is thread safe access to shared data structures. Seems quite a big oversight. What it does mean is that your WebWorker and its related message handler within the host must pretty much be self contained and not access any shared data structures. I fell into this trap with the code within this article where I was populating a common array, until I realised that this was probably not that smart, and I then moved the array to be scoped locally to the WebWorker, and then it was all OK.

How the Demo App Works

These subsections shall outline how the demo app works.

How to Run the Demo

The demo is a VS2010 solution for convince, but do not worry if you do not have VS2010, just open Windows Explorer and locate the file "Index.html" and right click and open with Firefox; yes, that's right, I need you to use a specific browser (more on this later).

Image 1

Browser Support

WebWorker(s) are supported by the following browsers:

BrowserWebWorker supporting option
Chromev3 or above
Firefoxv3.5 or above
IEv9 or above
Opera10.6 or above
Safari4 or above

As can be seen from the table above, there is quite good support for WebWorker(s). Although for this article I am not really concerned with cross browser compatibility and have not spent any time worrying about that at all, yes, you can moan about that if you like, but it will not change anything.

My reasons for this are as follows:

  • I wanted to write about WebWorker(s), not how to get things to work across browsers
  • I simply do not have enough hours free to care about this
  • I feel this article demonstrates the key concepts behind using WebWorker(s) just fine, which is what I was going for

As such, the only browser that I know works for sure is the one that I use most often on the rare occasions I do try and do some web coding, which is Firefox (v3.5 or above). The reason that some of the other browsers do not work is that the free Image Gallery jQuery plug-in that I used does not like different size images coming back to it. Firefox seems to cope with this just fine, but Chrome does not. Go figure. As I say, my focus on this article was the WebWorker(s) not modifying someone else's Image Gallery jQuery plug-in, so sorry, but it is the way it is.

HTML Part

See JavaScript file: Index.html

Most of the HTML was taken from the freely available Image Gallery jQuery plug-in that this article uses. The only thing that I changed was to include my additional JavaScript files, and to create dynamic content for the DIV class="container" element via the use of my own custom jQuery plug-in which we discuss next.

HTML
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" 
       "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head>
    <title>Experimenting With HTML5 WebWorkers</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <meta name="description" content="Simple Jquery/Html5 
             WebWorkers demo to build image wall based on WebWorker Flickr search" />
    <meta name="keywords" content="jquery, html5, full screen, webworker, flickr" />
    <link rel="stylesheet" href="css/style.css" 
            type="text/css" media="screen" />
    <script type="text/javascript" src="scripts/jquery-1.6.2.min.js"></script>
    <script type="text/javascript" src="scripts/jquery.easing.1.3.js"></script>
    <script type="text/javascript" src="scripts/FlickrWall.js"></script>
    <script type="text/javascript" src="scripts/FullPageImageGallery.js"></script>
</head>
<body>
    
    <div id="webWorkAvailability" style="display :none;" >
        <p>WEB WORKERS ARE NOT AVAILABLE</p>
    </div>

    <div id="fp_gallery" class="fp_gallery">
        <img src="images/1.jpg" alt="" 
                 class="fp_preview" style="display: none;" />
        <div class="fp_overlay"></div>
        <div id="fp_loading" class="fp_loading">
        </div>
        <div id="fp_next" class="fp_next">
        </div>
        <div id="fp_prev" class="fp_prev">
        </div>
        <div id="outer_container">
            <div id="thumbScroller">
                <div class="container">
                </div>
            </div>
        </div>
        <div id="fp_thumbtoggle" 
             class="fp_thumbtoggle">View Thumbs</div>
    </div>
    <div>
    </div>
</body>
</html>

Basically, this section gets dynamically updated by the use of the custom jQuery plug-in which we discuss next.

HTML
<div class="container">
</div>

As I say, most of this HTML was taken from the freely available Image Gallery jQuery plug-in that this article uses.

Custom jQuery Plug-in Part

See JavaScript file: FlickrWall.js

The jQuery plug-in that I wrote does a pretty simple job. It is applied to a specific element, where the jQuery plug-in would accept an array of search terms. For each of the search terms, a new WebWorker is spawned that will do an AJAX Flickr search for images that match that search term. For each WebWorker spawned, my custom jQuery plug-in will hook up to both the onMessage()/onError events from the newly spawned WebWorker. When the WebWorker completes, the WebWorker calls back into the jQuery plug-in, at which point an image wall is created.

JavaScript
//------------------------------------------------------------------------
//
// This is a simple jQuery plugin that can be applied to a single element, 
// where the jQuery plugin would accept an array of search terms. 
// For each of the search terms a new Html5 WebWorker is spawned that will 
// do a Ajax flickr search for images that match that search term. 
// When the Html5 WebWorker completes, the Html5 WebWorker calls back into the  
// jQuery plugin, at which point an image wall is created.
//
//------------------------------------------------------------------------
(function ($) {

    $.fn.FlickrImageWall = function (options) {

        var wwsAreOk = false;
        var workersCompleted = 0;
        var src = "";
        var workerArray = new Array();
        var imagesSoFar = 0;
        var maxImages = 15;

        //Check for WebWorker availability
        if (Supports_web_workers()) {
            $(".webWorkAvailability").hide();
            wwsAreOk = true;
        }        //Assume these setting values, unless new values are
        //supplied by the caller of this plugin
        var settings = {
            'searchWords': ['dog', 'cat', 'shark']
        };


        //The is the call back from the WebWorker that is called
        //when the worker sends a message back to this hosting jQuery plugin
        //via the PostMessage API
        function workerResultReceiver(e) {

            //Each worker must have its only local data,
            //cant modified unsafe global fields, as they are not thread safe.
            var workerImages = new Array();
            var jsonData = $.parseJSON(e.data);
            var src;

            for (var i = 0; i < 5; i++) {
                src = "http://farm" + jsonData.photos.photo[i].farm + 
                      ".static.flickr.com/" + jsonData.photos.photo[i].server + 
                      "/" + jsonData.photos.photo[i].id + "_" + 
                      jsonData.photos.photo[i].secret + "_b.jpg";
                workerImages.push(src);
            }
            PopulateWall(workerImages);

            //check to see if all the web workers have completed yet, and if stop all
            //workers by sending a new stop message

            //Pretty sure the access to imagesSoFar is not thread safe, but had no choice
            imagesSoFar = imagesSoFar + 1;
            if (imagesSoFar == workerArray.length) {
                for (var j = 0; j < workerArray.length; j++) {
                    workerArray[j].postMessage({ 'cmd': 'stop', 'msg': null });
                }
            }
        }

        //The is the call back from the WebWorker that is called
        //when the worker sends a error message back
        function workerErrorReceiver(e) {
            console.log("there was a problem with the WebWorker " + e);
        }

        //The jQuery meat, this is what will be run against the selected
        //element set that this jQuery plugin is applied to
        return this.each(function () {
            if (options) {
                $.extend(settings, options);
            }            //allows chaining of jQuery plugins
            var $this = $(this);

            //if webworkers are supported
            if (wwsAreOk) {
                //for each keyword, need to start a new web worker off that will search Flickr
                for (i = 0; i < settings.searchWords.length; i++) {
                    var worker = new Worker("scripts/FlickrWorkerSearch.js");
                    worker.onmessage = workerResultReceiver;
                    worker.onerror = workerErrorReceiver;
                    worker.postMessage({ 'cmd': 'start', 'msg': settings.searchWords[i] });
                    workerArray.push(worker);
                }
            }
        });

        //populate the wall by building up dynamic content based on the Ajax fetch Flickr
        //data. Finally make a call to the ImageGallery
        //jQuery plugin, via the CreateWall() function
        function PopulateWall(images) {

            var fullcontent = "";
            for (var j = 0; j < 5; j++) {
                var imagName = images[j];
                var fullstring = "<div class=\"content\"><div><a" + 
                    " href=\"#\"><img src=\"" + 
                    imagName + "\" alt=\"" + imagName + 
                    "\" class=\"thumb\" /></a></div></div>";
                fullcontent = fullcontent + fullstring;
            }

            $(".container").append(fullcontent);
            CreateWall();
        }

        //checks for WebWorker support
        function Supports_web_workers() {
            return !!window.Worker;
        }
    };
})(jQuery);
$(document).ready(function () {

    //hook up FlickrImageWall jQuery plugin to the single
    //element that will receive the dynamic images being 
    //added to it
    $(".container").FlickrImageWall({ 'searchWords': ['bikini', 'tatoo','water']
    });
});

I should point out that the call to CreateWall() calls into a third party jQuery plug-in which is discussed within this article at Image Gallery Part.

The WebWorker Part

See JavaScript file: FlickrWorkerSearch.js

As you now know, WebWorker(s) must be created using a standard JavaScript. You now also know that a WebWorker can not touch the DOM. And I have already stated that my problem domain dictated that each WebWorker would do an AJAX request against the Flickr API. How is this done? Well, we just use a manual AJAX call.

Here is the full code for the WebWorker JavaScript file "FlickrWorkerSearch.js" which is used within my custom Query plug-in "FlickrWall.js" when creating the new WebWorker(s) to do the searching.

JavaScript
//Does the Ajax flickr search based on a given url and then
//either posts the Ajax response or null using the PostMessage API
function GetData(url) {
    try {
        var xhr = new XMLHttpRequest();
        xhr.open('GET', url, false);
        xhr.setRequestHeader("Content-Type", 
            "application/x-www-form-urlencoded");
        xhr.onreadystatechange = function () {
            if (xhr.readyState == 4) {
                if (xhr.status == 200) {
                    postMessage(xhr.responseText);
                }
            }
        };
        xhr.send(null);
    } catch (e) {
        postMessage(null);
    }
}

//Adds a listener to the message event
//This is the main message pump for the PostMessage API for the WebWorker,
//this deals with all the different message types that the worker/host can use
//to communicate
self.addEventListener('message', function (e) {
    var data = e.data;

    switch (data.cmd) {
        case 'start':
            var url = "http://api.flickr.com/services/rest/?method=flickr.photos.search" + 
                        "&api_key=FLICKR_API_KEY&tags=" + data.msg +
                        "&safe_search=1&per_page=20&format=json&nojsoncallback=1";
            GetData(url);
            break;
        case 'stop':
            self.close(); // Terminates the worker.
            break;
        default:
            self.postMessage('Unknown command: ' + data.msg);
    };
}, false);

Please Note

I have been good enough to leave in my Flickr API key to give you a fully working demo, but please do not copy it or change the code to slam Flickr with requests, or generally do anything that would cause Flickr to cancel my developer key. Basically, please play nice.

Image Gallery Part

See JavaScript file: FullPageImageGallery.js and jquery.easing.1.3.js

This is perhaps the prettiest part of my demo, and alas I can take no credit for this, this is simply one of many free jQuery image library plug-ins which are available on the internet.

The original source is freely available: http://www.ajaxshake.com/plugin/EN/781/0a2daf19/jQuery-full-page-image-gallery-full-gallery.html where this jQuery plug-in (see "FullPageImageGallery.js") also makes use of a jQuery easing plug-in, which is the "jquery.easing.1.3.js" file.

I did have certain timing issues, due to WebWorker(s) essentially being a new thread of execution, which the designer of the image gallery obviously did not think about. This led me to re-factor the image library code to allow it to be called on receipt of a WebWorker posted message inside my own jQuery plug-in. These changes were small and for the large part are not relevant to this article. I did however have to come up with a way to dynamically manipulate the DOM for the window element that was the target for the image library jQuery plug-in. That is not really that relevant to the scope of this article though, rest assured I had to do some DOM manipulation in order to get a dynamic wall using this jQuery plug-in.

I have to say though that the reason I picked this particular image library jQuery plug-in is that it is really, really cool.

But Wait Browser Weirdness

There is however one issue with this free jQuery plug-in, in that it kind of assumes all the images will be of a certain size and that they shall be square (which is obviously something I can not garmented as the images are coming from Flickr). Firefox seems to cope with this just fine but other browsers (such as Chrome) do not. As the main focus on this article was the WebWorker(s) not modifying someone else's Image Gallery jQuery plug-in, sorry but it is the way it is.

Anyway, apart from the browser issues, once it is populated with images that come back from the WebWorker(s) AJAX requests, it will use the dynamically created content as shown below, where the user may scroll left to right using the mouse, and then click on an image (click the image below for a bigger image).

Image 2

Once an image is clicked on, it will expand to full screen at which point the user may scroll through full screen images using the left/right buttons, or return to the thumbnails again. This is shown below (click the image below for a bigger image):

Image 3

All in all, I was very happy with this free jQuery plug-in.

To be honest, I think Microsoft could learn a lot from the jQuery/community based contributions. Imagine if people could just extend WPF/Silverlight the way you could jQuery and put that stuff out there. I'll give you an example of this image library using an animation jQuery library. So let us look at that some more. I wrote a jQuery plug-in that called a freely available jQuery image library plug-in which in turn depends on yet another freely available jQuery animation plug-in.

If we were in Microsoft land (as I often am), we would have to search the internet searching for Silverlight/WPF controls on CodePlex or wherever and we would probably find something that 10-50 people had downloaded, or we would simply have to wait to see what Microsoft had installed for us with .NET(x).

In contrast, when you search for something like an image library for jQuery, there are literally hundreds to choose from. It is just so much more community based, as it's not so closed to extension, that much is obvious.

However, with anything that open, you will end up with things that look good but end up being untested, rough, and plain unusable; however, if you are willing to take a hunt while on the internet, there are some gems out there.

Me, personally, I like the internet, so I am willing to hunt out the gems.

Conclusion

That's it for now. I know I am not known for my web based community contributions, and to be honest, I do not see that changing, but it is always worth while making sure you know how to do things in popular technologies. I do see that HTML5 will become very popular, but in my opinion, there are some really dumb things in it that need to be fixed such as the things I have found within this article.

You know such as there now being support for extra threading (yes, via WebWorker(s)), but having no native JavaScript mechanism for doing thread safe stuff is pretty weird, you know things like lock(..), and all of the other locking primitives we have available inside .NET/Java or most statically typed languages, heck even most languages for that matter.

I do get the idea of there not being any access allowed to the DOM from the WebWorker, well, I kind of do. It would have been better to allow it, but it has to be with thread affinity, where some sort of thread affinity object is available to the WebWorker. Windows has used this model like forever, Java is surprisingly tolerant of thread affinity. I don't care which approach is taken, but the blanket rule of "NO DOM" JavaScript interaction in a WebWorker is pretty restrictive; you know jQuery has lots of good stuff in it, I know it's primarily a DOM manipulation API, but there is loads of other very useful stuff on offer, such as the ability to do AJAX calls, parse JSON, all sorts of stuff really.

Anyway, enough ranting, hope you enjoyed the article.

License

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


Written By
Software Developer (Senior)
United Kingdom United Kingdom
I currently hold the following qualifications (amongst others, I also studied Music Technology and Electronics, for my sins)

- MSc (Passed with distinctions), in Information Technology for E-Commerce
- BSc Hons (1st class) in Computer Science & Artificial Intelligence

Both of these at Sussex University UK.

Award(s)

I am lucky enough to have won a few awards for Zany Crazy code articles over the years

  • Microsoft C# MVP 2016
  • Codeproject MVP 2016
  • Microsoft C# MVP 2015
  • Codeproject MVP 2015
  • Microsoft C# MVP 2014
  • Codeproject MVP 2014
  • Microsoft C# MVP 2013
  • Codeproject MVP 2013
  • Microsoft C# MVP 2012
  • Codeproject MVP 2012
  • Microsoft C# MVP 2011
  • Codeproject MVP 2011
  • Microsoft C# MVP 2010
  • Codeproject MVP 2010
  • Microsoft C# MVP 2009
  • Codeproject MVP 2009
  • Microsoft C# MVP 2008
  • Codeproject MVP 2008
  • And numerous codeproject awards which you can see over at my blog

Comments and Discussions

 
GeneralMy vote of 5 Pin
BadassAlien6-Sep-13 13:22
professionalBadassAlien6-Sep-13 13:22 
GeneralMy vote of 5 Pin
Sudhakar Shinde13-Mar-13 5:53
Sudhakar Shinde13-Mar-13 5:53 
QuestionHTML5 Pin
harin42031-Jan-13 22:07
harin42031-Jan-13 22:07 
GeneralMy vote of 5 Pin
Ahmed Ibrahim Assaf24-Dec-12 21:46
professionalAhmed Ibrahim Assaf24-Dec-12 21:46 
GeneralRe: My vote of 5 Pin
Sacha Barber24-Dec-12 22:28
Sacha Barber24-Dec-12 22:28 
GeneralDevoted to your Article Pin
Anurag Sarkar23-May-12 5:06
Anurag Sarkar23-May-12 5:06 
GeneralRe: Devoted to your Article Pin
Sacha Barber23-May-12 5:09
Sacha Barber23-May-12 5:09 
GeneralMy vote of 5 Pin
Florian Rappl30-Jan-12 7:36
professionalFlorian Rappl30-Jan-12 7:36 
Generalmy vote of 5 Pin
Uday P.Singh2-Jan-12 18:53
Uday P.Singh2-Jan-12 18:53 
GeneralRe: my vote of 5 Pin
Sacha Barber2-Jan-12 19:36
Sacha Barber2-Jan-12 19:36 
GeneralNice Article, My Vote 5 Pin
Shakeel Iqbal29-Nov-11 1:18
Shakeel Iqbal29-Nov-11 1:18 
GeneralRe: Nice Article, My Vote 5 Pin
Sacha Barber29-Nov-11 3:05
Sacha Barber29-Nov-11 3:05 
QuestionWeb worker support in IE9 Pin
Secrets9-Nov-11 7:41
Secrets9-Nov-11 7:41 
GeneralMy vote of 5 Pin
Saraf Talukder31-Aug-11 2:24
Saraf Talukder31-Aug-11 2:24 
GeneralMy vote of 5 Pin
Dr. Song Li24-Aug-11 16:43
Dr. Song Li24-Aug-11 16:43 
GeneralMy vote of 5 Pin
Abhijit Jana10-Aug-11 21:58
professionalAbhijit Jana10-Aug-11 21:58 
GeneralRe: My vote of 5 Pin
Sacha Barber12-Aug-11 0:23
Sacha Barber12-Aug-11 0:23 
QuestionThread locking primitives Pin
Daniel Vaughan18-Jul-11 21:52
Daniel Vaughan18-Jul-11 21:52 
AnswerRe: Thread locking primitives Pin
Sacha Barber18-Jul-11 23:12
Sacha Barber18-Jul-11 23:12 
GeneralRe: Thread locking primitives Pin
Daniel Vaughan19-Jul-11 0:08
Daniel Vaughan19-Jul-11 0:08 
GeneralRe: Thread locking primitives Pin
Sacha Barber19-Jul-11 2:15
Sacha Barber19-Jul-11 2:15 
GeneralMy vote of 5 Pin
Pablo Aliskevicius18-Jul-11 20:52
Pablo Aliskevicius18-Jul-11 20:52 
GeneralRe: My vote of 5 Pin
Sacha Barber18-Jul-11 23:05
Sacha Barber18-Jul-11 23:05 
QuestionI should give you a three Pin
Gregory Gadow18-Jul-11 19:07
Gregory Gadow18-Jul-11 19:07 
AnswerRe: I should give you a three Pin
Sacha Barber18-Jul-11 20:03
Sacha Barber18-Jul-11 20:03 

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.