Archive for category Javascript

Executing long running process from a web site

I recently had to build a report which accepts one xls excel file, churns through a bunch the data looking stuff up on other systems and performing calculations, then spits out another xls excel sheet the other end. The whole process takes about 15 to 20 minutes to run and at present is a simple command line application which accepts a couple of parameters (input and output filenames) and I fire up manually- obviously a situation which is no good moving forward.

All our internal business systems are web forms/ mvc running from local IIS boxes so ideally I would like to add a screen where the user can submit their xls file, hit go, and then get the result back 15/ 20 minutes later- but I don’t want to tie up one of the threads in the aspnet worker thread pool for the entire duration. Some Googling revealed the best approach appears to be to write a windows service which hosts the long running process then get the web app to call across to that to kick off the task.

Implementation

This is to be a standard Windows Service which will run my long running process, hosting a WCF service which the website can use to call into it with. If you;ve not done this before, you can follow the steps on the MSDN ; msdn.microsoft.com/en-us/library/ms733069.aspx.

Create your WCF Service which will respond to requests from the website. In my instance I wanted to be able to start the operation, passing a byte[] containing the xls file, request status updates, then finally request the resulting xls file, again as a byte[]. For the sake of this example, the WCF service is the one which is doing all the work with regards spawning the worker thread and tracking updates from the actual worker class, and the worker class is a type called “Calculator”.

    using System.ServiceModel;
    [ServiceContract(Namespace = "http://Moneybarn.VIVS.FleetRevaluation")]
    public interface IMyWCFService
    {
        [OperationContract]
        string Start(byte[] excel_file, string user_name);

        [OperationContract]
        Progress GetProgress();

        [OperationContract]
        byte[] GetResult();
    }

    using System.Threading;
    public class MyWCFService: IMyWCFService
    {
        private static string CurrentUserInstance;
        private static Calculator MyCalculatorInstance;
        private static Progress LastUpdate;
        private static Thread WorkerThread;
        private static bool IsComplete;
        private static string LastError;
        private static byte[] FinishedFile;

        public string Start(byte[] excel_file, string user_name)
        {
            if (WorkerThread == null)
            {
                CurrentUserInstance = user_name;
                MyCalculatorInstance = new Calculator(excel_file);

                MyCalculatorInstance.OnProgress += new OnProgressEventHandler(MyCalculatorInstance_OnProgress);
                MyCalculatorInstance.OnComplete += new OnCompleteEventHandler(MyCalculatorInstance_OnComplete);
                MyCalculatorInstance.OnError += new OnErrorEventHandler(MyCalculatorInstance_OnError);

                WorkerThread = new Thread(new ThreadStart(MyCalculatorInstance.Start));
                WorkerThread.Start();

                return null; // no news is good news!
            }
            else
            {
                return "Instance already running for user " + CurrentUserInstance;
            }
        }

        public Progress GetProgress()
        {
            return LastUpdate;
        }

        public string GetError()
        {
            return LastError;
        }

        public byte[] GetResult()
        {
            if (IsComplete)
            {
                byte[] buff = FinishedFile;

                MyCalculatorInstance = null;
                WorkerThread = null;
                CurrentUserInstance = string.Empty;
                LastUpdate = null;
                FinishedFile = null;
                IsComplete = false;

                return buff;
            }
            else
                return null;
        }

        private void MyCalculatorInstance_OnError(string error)
        {
            LastError = error;
        }

        private void MyCalculatorInstance_OnComplete(byte[] the_file)
        {
            FinishedFile = the_file;
            IsComplete = true;
        }

        private void MyCalculatorInstance_OnProgress(int stage, float percent)
        {
            LastUpdate = new Progress() { 
                Stage = stage, 
                Percent = (int)(percent * 100) 
            };
        }
    }

Add a new class called ProjectInstaller- this will handle registering your service with Windows

    using System.ComponentModel;
    using System.Configuration.Install;
    using System.ServiceProcess;

    [RunInstaller(true)]
    public class ProjectInstaller : Installer
    {
        private ServiceProcessInstaller process;
        private ServiceInstaller service;

        public ProjectInstaller()
        {
            process = new ServiceProcessInstaller();
            process.Account = ServiceAccount.LocalSystem;
            service = new ServiceInstaller();
            service.ServiceName = "MyWindowsService";  // change this!
            Installers.Add(process);
            Installers.Add(service);
        }
    }

Add the actual service body; add a new “Windows Service” file to the project. This is the meat of the service, and contains no real logic- it’s only job is to fire up the ServiceHost for your WCF service.

    using System.ServiceModel;
    using System.ServiceProcess;

    public class MyWindowsService : ServiceBase
    {
        public ServiceHost serviceHost = null;

        public MyWindowsService()
        {
            // Name the Windows Service
            ServiceName = "MyWindowsService";
        }

        public static void Main()
        {
            ServiceBase.Run(new MyWindowsService());
        }

        // Start the Windows service.
        protected override void OnStart(string[] args)
        {
            if (serviceHost != null)
            {
                serviceHost.Close();
            }

            // The type of your WCF service
            serviceHost = new ServiceHost(typeof(MyWCFService));

            serviceHost.Open();
        }

        protected override void OnStop()
        {
            if (serviceHost != null)
            {
                serviceHost.Close();
                serviceHost = null;
            }
        }
    }

Finally there is a little bit of config to pop into the app.config for the WCF endpoints- I’ve created a custom binding here to allow me to receive larger files over the WCF call than the default limits allow (in this case, up to 10 meg);

  <system.serviceModel>
    <bindings>
      <wsHttpBinding>
        <binding name="bigWSHttpBinding" 
                 maxBufferPoolSize="10485760" maxReceivedMessageSize="10485760"> <!-- 10 MB limit -->
          <readerQuotas maxDepth="10485760" maxStringContentLength="10485760" maxArrayLength="10485760" maxBytesPerRead="10485760" />
        </binding>
      </wsHttpBinding>
    </bindings>
    <services>
      <service behaviorConfiguration="MyWCFServiceBehavior"
        name="Demo.MyWCFService">
        <endpoint address="" binding="wsHttpBinding" bindingConfiguration="bigWSHttpBinding"
          contract="Demo.IMyWCFService" />
        <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" />
        <host>
          <baseAddresses>
            <add baseAddress="http://localhost:8000/MyWCFService/service" />
          </baseAddresses>
        </host>
      </service>
    </services>
    <behaviors>
      <serviceBehaviors>
        <behavior name="MyWCFServiceBehavior">
          <serviceMetadata httpGetEnabled="true"/>
          <serviceDebug includeExceptionDetailInFaults="true"/>
        </behavior>
      </serviceBehaviors>
    </behaviors>
  </system.serviceModel>

I also created a couple of little batch files which I mark as “Copy to output directory” for installing and removing the service;

rem install.bat
c:\windows\microsoft.net\Framework\v4.0.30319\InstallUtil MyWindowsService.exe
net start MyWindowsService
rem remove.bat
net stop MyWindowsService
c:\windows\microsoft.net\Framework\v4.0.30319\InstallUtil MyWindowsService.exe /u

That’s the Windows service part built. You can now build, and jump to the bin output folder and run the install.bat file from the command line- with any luck (more likly with a little debugging!) you will be up and running. The second part is even easier;

Jump over to your web app, add a service reference to the url of the base address from your app config above then it’s just a matter of exposing those web service methods so that you can call them from your page with an ajax call- so in MVC I created a controller with Start, GetProgress & GetResult methods which returned JsonResults called straight from the client with jQuery.

2 Comments

My 8 Week Game Entry!

For the last 8 weeks I’ve been beavering away on a game to enter into the 8 week game competition! The last one (back in 2010) I won! (with Manic Spaceman, a Manic Miner clone, set in.. you guessed it… space)! So the pressure is on this time round with more competitors, some of which have experience in the games industry.

You can try out “The Distant Future (The Year 2000)” Here!

As last time, I had the choice of either XNA windows game, or a browser based html5/javascript/canvas effort- I went for the latter. I decided to make a game in the vein of the original classic Syndicate from the Amiga made by bullfrog. The competition states you have to do all the coding yourself, but other assets are up to you, which is great because I suck at trying to draw little people and animate them; so I managed to get the original sprites from Syndicate, along with some sound effects- for the levels I decided to use real cities, suing imagery from Bing Maps (which, to my surprise, were actually much better than the Google maps photography). I built a very basic level editor using jQuery, knockout and EaselJS allowing me to define walkable and non-walkable regions, and also define where buildings were so my game could make the levels pseudo-3d, where your characters would appear to walk behind buildings and trees etc and produce JSON data which could be dropped into the main game. (You can have a play with that here, there’s not a great deal you can do with it to be honest, but it was a great help during development).

The game’s main mechanic, from a technical standpoint, is taking the flat 2d Bing Map and making it into a 3d city your guys can walk around. This is achieved by taking the buildings as defined in the level editor, then snipping those pieces of the map out and placing them onto a separate layer, then it’s just a matter of figuring out what order to draw everything in- should each little guy be in front of or behind each building- I think it’s quite effective at giving the illusion of depth, though could do with a little refining, given more time!

As a base for the game, unlike my last 8 week game where everything was written from scratch, I used the CreateJS set of libraries to underpin everything. This is a great set of open source libraries which covers a whole bunch of stuff but the parts I used were EaselJS for the canvas itself, SoundJS to handle html5 audio, and PreloadJS which handles the preloading of various assets. This made it a bit more of a level playing field with the XNA guys who have a lot of this great stuff built into the framework. I found create js to be a great library, abstracting out lots of repetitive bits of code which are boring to write, but while still letting you manipulate the canvas directly when you need to. (EaselJS in particular has some great bitmap caching features in there too which let you dynamically build up textures as you go which I put to good use with the blood splatters and bullet holes!)

Getting the guys to find their way round the city proved quite complex- I used Dijkstra’s algorithm for this which used my array of non-walkable regions built in the level editor as the node graph and calculates the shortest region. I actually ported some code from C to JavaScript for this- the original being written by Darel Finley and had to make some adjustments to allow it to read from my data structures. This also proved to be quite an expensive calculation which would freeze the browser for a moment so I employed Web Workers, which are new to HTML5 and effectively bring threading to JavaScript for the first time, which solved this issue.

Everything else was pretty straight forward- the interface is all built with HTML5/ CSS/ jQuery and utilises a class I wrote called layout manager which reshuffles all the little grey boxes to fit together when they all change size. I also reused my Mini Map class to generate the level previews you see on the briefing screen.

The competition is judged by voting on the Facebook event page which is open to the general public next week- 11th Feb 2013 onwards. You can check out the competition details and the other entries on the Facebook page found at www.facebook.com/events/165946213543847

No Comments

Threading in JavaScript with Web Workers

I’m about 5 weeks into the second 8 week game competition now building my, as yet unnamed, (original) Syndicate clone.

My levels consist of real images from Bing Maps (which I was surprised to find had nicer imagery when compared with Google, despite Google maps generally being my go-to maps site), which I then outline the buildings on using a level editor I built for the task, then save a big ol JSON array. This is then used in game to tell the little guys where they are and aren’t allowed to walk, amongst other things. The windy streets of my first level (Brighton, on the south coast of the UK) make for some difficult navigation- so I had to employ an A* search algorithm which would let my little guys find their way around these defined polygons which represent the footprints of the buildings. This all worked fine, however I noticed a the browser would lock up for a second or so while it ran through checking all the polygons and building paths for all four of your guys. “Wouldn’t it be great if I could do this in another thread” I thought…

Web Workers to the rescue!

Web workers are new in HTML5 and let you fire up another piece of javascript code in another thread, totally separate from the UI (so it will stop the freezeing I was seeing when the Javascript had to think really hard). You only have one mechanism (correct me if I’m wrong here) to send messages between your “Worker” thread and back to the parent application; the postMessage() method and onmessage callback function.

On the application side you create your new worker like this;


// setup the worker- giving it the url for the js file with the code to execute
var worker = new Worker('mysource.js'); 

// define a call back to handle messages we get from the worker
worker.onmessage = function(msg) {  
  alert(msg);
}

// send a message to the worker
worker.postMessage('shaw');  

Then on the worker side you have the same methods at your disposal;

// this exists in mysource.js
// setup a call back to handle messages we receive from the application
self.onmessage = function(msg) {  
  // post a message back to the application
  self.postMessage('hello ' + msg);  
}

In case you’ve not figured it out, in the above example you should expect to get an alert box saying “hello shaw”.

It’s also work pointing out that “self” is the global scope inside one of these web workers, as you won’t have access to any of the usual browser window or document objects.

So this is all pretty straight forward, but I still had a problem with my route finding example; I’d written a class which has a constructor accepting the polygon array and then a method which works out the route and returns it– how does that work in this model?? How do you call methods and constructors when the only access you have to your thread is a simple postMessage method?

Calling methods from ‘the other side’
postMessage() lets you send JSON as well as just plain old strings, which makes it quite a bit more useful. I used this to build a basic messaging system between the app and the thread- I would pass something like this;

worker.postMessage({ method: 'Init', args : [my_array, null, 5]})

then in the worker I would have a call back which looked like this;

self.onmessage = function(msg) {
    // the if catches the 'Init' special case- this instanciates a new RouteFinder
    if (msg.method == 'Init') 
    {
        // create a new instance of our route finder class, using it's constructor and passing in args
        self.MyRouteFinder = RouteFinder.apply( null, msg.args );
    }
    else  // the else catches all the other method calls, whatever they may be
    {  
        // find the method from the prototype chain, and apply it against our globally scoped object, again passing the args
        // the result of this method call is passed straight back to the application
        self.postMessage(RouteFinder.prototype[msg.method].call(self.MyRouteFinder, msg.args));
    }
}

This worked well for me though there are lots of (often better!) variations of this pattern on-line.

What if I don’t want to put my worker code in another file?

Also not a problem as detailed in this article on html5rocks.com. It basically involves grabbing your code from it’s own script tab on the same page, then smashing it into a Blob, then passing this blob into the construtor of your web worker.

Wait a sec.. how do I debug these workers?

You may have fired up some code in Chrome and noticed it hasn’t worked, so gone to the dev tools (F12) then flicked around in the scripts tab and… wait a minute… the web worker code doesn’t show up in the scripts panel! Well don’t worry- if you’re up to date with your copy of Chrome go down to the bottom right hand corner of the scripts panel where you’ll see one of the collapsably panels is called “Workers” and lists the code executing in each of your workers- just click one to launch another debug window which you can set break points on in the usual way.

No Comments

Excellent free online book on Javascript Design Patterns

Excellent free online book on Javascript Design Patterns by Addy Osmani called “Learning JavaScript Design Patterns” – An essential read for any one doing any kind of serious Javascript development (and even if you’re not!).

No Comments

Why you should use var in your for loop control variables

Had a conversation recently about JavaScript and the importance of var for scoping your variables- even in for-loop control variables; This little gotcha still seem’s to get a few devs so thought I would post a fiddle…

http://jsfiddle.net/shawson/gH4KC/5/

1 Comment

Highcharts- Highlight the last clicked bar

This is handy, only really if you have charts which, on click, have drill down data which pops up, perhaps in a table underneath or something- I basically just wanted to highlight the last clicked column in a column chart;

Check it out on jsFiddle : http://jsfiddle.net/shawson/CkkbF/8/

2 Comments

Recursive jQuery Drop Down Menu’s

Following on from my previous post about drop downs, I recently expanded the code to allow for sub menus- as many levels deep as you would like.

The Code:

$(document).ready(function () { // Shawsons' Teeny-Tiny Recursive Drop Downs!
    $('.drop-down-menu>li>ul').hide().mouseleave(function () {
        $('.highlighted', this).removeClass('highlighted');
        $(this).hide();
    });
    $('.drop-down-menu li>a').mouseenter(function () {
        var menu_root = $(this).parent().parent();
        $('ul', menu_root).hide();
        $('.highlighted', menu_root).removeClass('highlighted');
        $('>ul', $(this).addClass('highlighted').parent()).show();
    }).mouseleave(function (o) {
        if ($(this).parent().has($(o.relatedTarget)).length < 1) {
            $('ul', $(this).parent()).hide();
        }
    });
});

The Markup:

<ul class="drop-down-menu">
        <li>Drop Down Menu - Demo Title : </li>
        <li><a href="">Menu 1</a>
            <ul>
                <li><a href="a.htm">a</a></li>
                <li><a href="b.htm">b</a></li>
                <li><a href="c.htm">c &raquo;</a>
                    <ul>
                        <li><a href="c-a.htm">c-a</a></li>
                        <li><a href="c-b.htm">c-b &raquo;</a>
                          <ul>
                              <li><a href="c-b-a.htm">c-b-a</a></li>
                              <li><a href="c-b-b.htm">c-b-b &raquo;</a>
                                  <ul>
                                      <li><a href="c-b-b-a.htm">c-b-b-a</a></li>
                                  </ul>
                              </li>
                          </ul>
                        </li>
                        <li><a href="c-c.htm">c-c</a></li>
                    </ul>
                </li>
            </ul>
        </li>
        <li><a href="">Menu 2</a>
            <ul>
                <li><a href="d.htm">d</a></li>
                <li><a href="e.htm">e</a></li>
                <li><a href="f.htm">f</a></li>
            </ul>
        </li>
    </ul>

Some CSS:


.drop-down-menu { display:block; }
.drop-down-menu li { float:left; padding: 8px; }
.drop-down-menu>li>a { padding:8px; }
.drop-down-menu li ul  
{
    position:absolute; 
    background-color:#fff; 
    border: 1px solid #999;
    border-radius: 0px 7px 7px 7px; -moz-border-radius:0px 7px 7px 7px; -webkit-border-radius:0px 7px 7px 7px;
    padding:5px; 
    width: 50px;
}
.drop-down-menu li ul a {
   display:block;
   width:50px;
}
.drop-down-menu li ul li ul 
{
    margin-left:50px;
    margin-top: -20px;
}
.drop-down-menu li ul li { float:none; }
.drop-down-menu li ul li a { color:#000; }
.drop-down-menu li ul a.highlighted { background-color:#F68833; }

Demo :

 

2 Comments

Adding cross browser consistent keyboard short-cuts to your website

I wanted to employ keyboard short cuts to a frequently used internal web app used within the company- Pinch o’ the proverbial piss I thought- I’ll just use access keys! I’ve used them before on front facing sites for accessibility- but when I actually came to document these keys for the users I realised that support across different browsers is a mess;

IE Alt + {key}then hit return
Firefox Alt + Shift + {key}
Chrome Alt + {key} (the most sensible implementation, in my opinion)

IE also seems to refuse to aknowlege the links unless they are currently visible, and as the options I was short cutting existed in drop down menus this was no good, as they existing in bulleted lists which are “display:none” until the user mouses-over the menu bar (using my tiny drop down menu code)!

So to get around this I wrote some jQuery which standardises the short cuts- note this is no good for normal accessibility uses as it’s a new key combo which none of the browsers use, so without documentation no one will know- but for my use on an internal system it’s perfect.

I took the decision to make all the short cuts Ctrl+Alt+{key}, and wanted to keep using the accesskey attribute on my links rather than introduce another mechanism for dictating which keys triggered what- I also wanted to ensure if the link had a javascript onclick handler, instead of navigating away, we respect the handler and simply trigger that. Here’s the code;

$(document).keydown(function(e) {
	if (e.ctrlKey && e.altKey && e.which >= 65 && e.which <= 122) {
		e.preventDefault();
		
		var key = String.fromCharCode(e.which);
		var the_link = $("a[accesskey=" + key.toLowerCase() + "]");
		
		if (typeof(the_link) != 'undefined') {
			// does this link have a javascript assigned click handler, or do we simply send the user to the links href value?
			var events = $.data( $(the_link).get(0), 'events' );
			if (typeof(events) != 'undefined') {
				if (typeof(events.click) != 'undefined') {
					$(the_link).click();
					return;
				}
			}
			window.location = $(the_link).attr('href');
		}
	}
});

1 Comment

Quick & Tiny jQuery Drop Down Menus

Note: this has been superseded by the recursive drop down menu script.

The Code:

$(document).ready(function () { // Shawsons' Teeny-Tiny Drop Downs!
    $('.drop-down-menu li ul').hide().mouseleave(function () {
        $(this).hide();
    });
    $('.drop-down-menu>li>a').mouseenter(function () {
        $('ul', $(this).parent().parent()).hide();
        $('ul', $(this).parent()).show();
    }).mouseleave(function (o) {
        if ($(this).parent().has($(o.relatedTarget)).length < 1) {
            $('ul', $(this).parent()).hide();
        }
    });
});

The Markup:

<ul class="drop-down-menu">
        <li>Available Actions : </li>
        <li><a href="">Menu 1</a>
            <ul>
                <li><a href="a.htm">a</a></li>
                <li><a href="b.htm">b</a></li>
                <li><a href="c.htm">c</a></li>
            </ul>
        </li>
        <li><a href="">Menu 2</a>
            <ul>
                <li><a href="d.htm">d</a></li>
                <li><a href="e.htm">e</a></li>
                <li><a href="f.htm">f</a></li>
            </ul>
        </li>
    </ul>

Some CSS:

.drop-down-menu { display:block; }
.drop-down-menu li { float:left; padding: 8px; }
.drop-down-menu li a { padding:8px; }
.drop-down-menu li ul  
{
    position:absolute; 
    background-color:#fff; 
    border: 1px solid #999;
    border-radius: 0px 7px 7px 7px; -moz-border-radius:0px 7px 7px 7px; -webkit-border-radius:0px 7px 7px 7px;
    padding:5px; 
}
.drop-down-menu li ul li { float:none; }
.drop-down-menu li ul li a { color:#000; }
.drop-down-menu li ul li:hover { background-color:#F68833; }

2 Comments

Grouped pagination links function in JavaScript

I just created a JavaScript port of an awesome bit of pagination code I first used years and years ago while working in Classic ASP, originally written by Dave Child who I was working with at the time. The original can be found on this site here.

It’s an awesome script when you have 100′s of pages of data (which probably suggests you need to implement some good filtering and search features to save your poor users..) and presents the links like so..

« Prev 1 2 3 … 7 [8] 9 … 208 209 210 Next »

function BuildGroupedPagination(current_page, total_pages, base_url, seperator) 
{ 
	var url = base_url; 
	var strPages = ""; 
	var intMaxPages = 0;
	var intMinPages = 0;
	var intPaginI = 0;

    if (typeof(seperator) == 'undefined')
    {
        seperator = " ";
    }

	if (total_pages > 10) 
	{ 
		if (total_pages > 3) 
		{ 
			intMaxPages = 3;
		}
		else 
		{
			intMaxPages = total_pages;
		} 

		for (intPaginI = 1; intPaginI <= intMaxPages; intPaginI++) 
		{

			if (intPaginI == current_page) 
			{ 
				strPages += "<strong>[" + intPaginI + "]</strong>";
			}
			else 
			{
				strPages += "<a href=\"" + url + intPaginI + "\">" + intPaginI + "</a>";
			} 

			if (intPaginI < intMaxPages) 
			{ 
				strPages += seperator;
			} 
		} 

		if (total_pages > 3) 
		{ 
			if ((current_page > 1) && (current_page < total_pages)) 
			{ 
				if (current_page > 5) 
				{ 
					strPages += " ... ";
				}
				else 
				{
					strPages += seperator;
				} 

				if (current_page > 4) 
				{ 
					intMinPages = current_page;
				}
				else 
				{
					intMinPages = 5;
				} 

				if (current_page < total_pages - 4) 
				{ 
					intMaxPages = current_page;
				}
				else  
				{
					intMaxPages = total_pages - 4;
				} 

				for (intPaginI = intMinPages - 1 ; intPaginI <= intMaxPages + 1; intPaginI++) 
				{
					if (intPaginI == current_page) 
					{ 
						strPages += "<strong>[" + intPaginI + "]</strong>";
					}
					else 
					{
						strPages += "<a href=\"" + url + intPaginI + "\">" + intPaginI + "</a>";
					}

					if (intPaginI < intMaxPages + 1) 
					{ 
						strPages += seperator;
					}
				}

				if (current_page < total_pages - 4) 
				{ 
					strPages += " ... ";
				}
				else 
				{
					strPages += seperator;
				} 
			}
			else 
			{
				strPages += " ... ";
			} 

			for (intPaginI = total_pages - 2; intPaginI <= total_pages; intPaginI++) {
				if (intPaginI == current_page) { 
					strPages += "<strong>[" + intPaginI + "]</strong>";
				}
				else {
					strPages += "<a href=\"" + url + intPaginI + "\">" + intPaginI + "</a>";
				} 

				if (intPaginI < total_pages) { 
					strPages += seperator;
				} 
			} 
		}
	}
	else 
	{
		for (intPaginI = 1; intPaginI <= total_pages; intPaginI++) 
		{
			if (intPaginI == current_page) 
			{ 
				strPages += "<strong>" + intPaginI + "</strong>"; 
			}
			else 
			{
				strPages += "<a href=\"" + url + intPaginI + "\">" + intPaginI + "</a>";
			} 

			if (intPaginI < total_pages) 
			{ 
				strPages += seperator;
			}
		}
	}
	return strPages;
}

6 Comments