Home Notes Web development

Development
Deleting a project from Team Foundation Service E-mail
Friday, 22 March 2013 16:10

So I just started working with Microsoft's Team Foundation Service.

Problem

For the life of me I couldn't figure out how to delete a project from the Administration panel of my VisualStudio.com account, the homepage of which looks like:

tfs-screenshot

 

Solution

For whatever reason, any projects have to be deleted via the command line, through a utility called TfsDeleteProject, detailed here.

 

The command to issue is:

TfsDeleteProject /collection:https://pthesis.visualstudio.com/defaultcollection "Contacts Git"

  • https://pthesis.visualstudio.com/defaultcollection - URL of your TFS collection account (circled in the screenshot above)
  • "Contacts Git" - name of the project you want to delete.  (I've already deleted this particular project from my account which is why it's not showing up in the screenshot.)

Below is what the whole transaction looks like in the DOS Command Prompt (cmd.exe).  Note the path to the TfsDeleteProject executable in the prompt, which in my case is c:\Program Files\Microsoft Visual Studio 11.0\Common7\IDE but could be different for you.

tfsdeleteproject-dos-prompt

 
Trouble installing curb gem for Ruby E-mail
Wednesday, 06 March 2013 01:00

If you're having trouble getting the curb gem to install in your Ruby on Rails project, you might not have the required libcurl package installed (website).

In Linux you can get the package from your distribution's package manager.  In openSUSE, at the command line run:

sudo zypper install libcurl-devel

From there you should be able to run bundle install or gem install curb as usual.

 
Google Maps not loading in IE8 or IE7? E-mail
Monday, 19 September 2011 07:08

I came across the following implementation of a Google Maps embedded map in a Rails application.  The map loaded fine in every popular browser except Internet Explorer 8 and 7.  After some time I figured out what it was:

 

Original code

this.drawMap = function() {
 
    if($('#map[data-map-js-url]').length > 0) {
 
        $.get($('#map').attr('data-map-js-url'), function(data){
 
        script = $(data).text();    //line to change
 
        eval(script);
 
        });
 
    }
 
}

 

Modified line that loads map in IE8 and IE7

script = $(data).html();

 

If I were to just guess at the cause, I would say that newer browser are more forgiving in what they let eval parse, whereas IE8 and IE7 require proper HTML.

 
OpenCart 1.5.0.x: creating a simple new layout E-mail
Sunday, 26 June 2011 21:45

Introduction

If you don't already know, OpenCart is an open-source e-commerce solution written in PHP that's designed around the MVC paradigm.  I've only worked with it briefly, but if you're familiar with other MVC platforms (like Ruby on Rails or CodeIgniter for PHP) then you can find your way around fairly easily.

 

One thing I would say about OpenCart is that it doesn't have the greatest documentation in the world.  Since it is MVC-based you can make up for that loss from your own experience, but still, it's less than ideal.  The forums seem pretty active, but they're still no substitute for thorough documentation.

 

In this tutorial I want to demonstrate how to add a custom catalog (non-admin) view to the many default ones OpenCart comes with.  These include (but are not limited to):

  • home
  • category
  • product
  • manufacturer

Task and assumptions

The view I want to add is a simple one.  It displays all the categories with some number of products from each category all on one page.  That's a view that OpenCart doesn't come with by default.  In OpenCart parlance what we're creating is a new layout.

 

This really only makes sense with a flat category structure.  If you have nested categories it would get unwieldy pretty quickly to display all the categories and their respective products.  So I will assume that categories are only one level deep with no children.

 

I will be demonstrating on a default installation of OpenCart 1.5.0.  Before you start, make a copy of the default theme, under /catalog/view/theme/default.  I'll call it default-copy.

File structure

The place to start is by examining the OpenCart file structure to see where we want to add our modifications.  (There are always multiple ways of accomplishing the same task, and this is just one way of doing it.  Frankly I haven't explored any others.)

Here's the default OpenCart file structure:

/opencart

--- /admin

--- /catalog

--- /download

--- image

--- /system

- .htaccess.txt

- config.php

- index.php

- php.ini

 

It's fairly self-explanatory.  Since we're only adding a new front-end (catalog) view, the catalog folder is the only we're concerned with.  That folder looks like:

/catalog

--- /controller

--- /language

--- /model

--- /view

 

OpenCart follows an MVC+L design, the L represented by the language folder above.  Again, since we're only adding a new layout, we won't be doing anything with the model or language files, so we're only concerned with the controller and view folders.

There are many folders inside /catalog/controller:

/controller

--- /account

--- /affiliate

--- /checkout

--- /common

--- /error

--- /feed

--- /information

--- /module

--- /payment

--- /product

--- /total

 

Most of these are fairly obvious as to what they pertain to.  For instance, account contains controller files relevant to customer accounts, checkout contains controller files relevant to the shopping checkout process, and so on.

 

We want the controller files that deal with the products on the site.  We want to display categories and products, both of which are handled by files in the product folder.  The product folder contains:

/product

- category.php

- compare.php

- manufacturer.php

- product.php

- search.php

- special.php

 

It may or may not be obvious what these files represent (depending on whether you've worked with MVC-based applications before).  Loosely put, each of these files represents a product-related resource users of your site can request.  So:

  • category.php lets users view products for any given category
  • manufacturer.php lets users view products by manufacturer (brand)
  • product.php lets users view a single product of interest
  • search.php lets users view the products relevant to their search term

We're out to add a new layout (a "type" of view) to the ones already available, so we want to add a new controller file here.  I've called it categories.php but you can call it something else if you so desire.  Once we're done, categories.php will let users view all the categories of products, with some number of products from each category, like:

  1. Category One
    • Product One
    • Product Two
    • Product Three
  2. Category Two
    • Product Four
    • Product Five
    • Product Six
  3. Category Three
    • Product Seven
    • Product Eight
    • Product Nine

Now the view...

The categories.php controller will retrieve whatever data it requires from the relevant models (which I won't go into) and send them to the appropriate view.  View files are held inside /catalog/view, which looks like:

/view

--- /javascript

--- /theme

------ /default

------ /default-copy

--------- /image

--------- /stylesheet

--------- /template

------------ /account

------------ /affiliate

------------ /checkout

------------ /common

------------ /error

------------ /information

------------ /mail

------------ /module

------------ /payment

------------ /product

------------ /total

 

Except for the mail folder, there is a 1-to-1 correspondence between the folders under /catalog/controller and /catalog/view/theme/default-copy/template.  It makes intuitive sense that since we added a new controller under /controller/products, we'll add a corresponding view file under /template/products.

 

The /template/products folder contains the following template files (with the tpl file extension):

/template/products

- category.tpl

- compare.tpl

- manufacturer_info.tpl

- manufacturer_list.tpl

- product.tpl

- review.tpl

- search.tpl

- special.tpl

 

Similar to how we added a categories.php controller, we'll add a new view file here (call it categories.tpl), containing the HTML markup and embedded PHP to display the data categories.php is sending it.

We're adding a new controller file and new view/template file for the new view we want to implement:

  • We're calling the new controller categories.php and adding it to /catalog/controller/products
  • The new view is called categories.tpl and gets added to /catalog/view/theme/default-copy/template/products

Let's move on to the actual contents of the new files.

categories.php controller

What we want to display is most similar to what category.php currently does, which is display some number of products for one given category.  That seems like a good starting point toward our objective, to display some products from all categories.

 

For reference, this is the default category.php that comes with the default installation of OpenCart.

Let's look at categories.php, which starts out as a copy of category.php which we modify.  I will do this in three steps:

  1. Irrelevant code blocks commented out
  2. Irrelevant code blocks deleted
  3. Relevant code modified and new code added

In the code below, I've commented out blocks of code for the following features, which we don't need for our new view:

  • Product sorting, ordering, and pagination
  • Breadcrumbs - getting the category title and link, since we're not viewing any single category
  • Getting child categories - we're working with a flat category structure
  • Category information - name, description, keywords, etc.
  • Check for whether a category exists - again, we're not viewing any single category, so no sense in keeping this

It's much easier working with a smaller file with less code, so I've removed all the commented code from the previous step, which is what's shown below.

Now we can look at the full code with modifications and additions.

 

Lines of note are the following:

Lines 25-31

Here we specify the number of products we want displayed for each category under limit, which is 3 in this case.  We've commented out the sort and order lines because they don't apply in this layout.  start will be 0 since there is no pagination in this layout, which is fine for our purpose.

 

Lines 68-79

We're adding an element to the $product_data array, which is a product from the category we're currently iterating over.  The properties we don't require for this layout have been commented out.

 

Lines 82-87

We store the current category as an array element to $this->data['categories'].  The products for this category (from $product_data above) are stored as $this->data['categories']['products'].

categories.tpl view

Similar to how we modeled categories.php from category.php, we'll model categories.tpl from category.tpl.

 

Here's the default category.tpl view template:

And here's categories.tpl, which derives from it but is a lot shorter, because it:

  1. Displays less information about the products than the single category view
  2. Removes the logic to handle child/nested categories
  3. Shows the products by default (and only) in grid view, and removes the option for switching between list and grid view

I think the code below is pretty straight-forward and self-explanatory.  It iterates through all the categories and for each category:

  1. Displays the category title
  2. Displays a few products from that category
  3. Adds a "view more" link to go to the category page

One last thing

Since we've added a new layout to our OpenCart project, we should add it to the database, which keeps a list of layouts, so we can specify modules that we want to display in our layout.

There are two places to add an entry.

  1. The first is in the table called layout.  Notice the last entry in the screenshot below, which represents the new layout.

    OpenCart layout database table

     

  2. You also need an entry in layout_route, as shown below (the last entry): 

    OpenCart layout_route database table

Now we can do the following in our OpenCart admin to add a module to the new layout we've created (last entry below):

OpenCart admin module page

Summary

To create a new layout in an OpenCart 1.5.0.x project that shows a list of all categories and some number of products from each category, follow these steps:

  1. Add a new controller file to /catalog/controller.  I called the file categories.php.
  2. Copy the contents of category.php which comes with the default theme in OpenCart to the new file (or just copy the file and rename it), and modify it to look like the following:

  3. Copy the default folder under /catalog/view/theme into a new folder.  I called it default-copy.
  4. Add a new view/template file under /default-copy/template/product called categories.tpl which looks like the following:

  5. Add an entry for the new layout to the layout and layout_route tables in the OpenCart database so you can add modules to it from within the OpenCart administration panel.

Alternative

I haven't tried it, but another way to accomplish this same task would be to add a new action to the category.php controller, instead of adding a new controller like I did.  Whereas everything is contained in the index action by default, we are free to add a new action with alternative code and corresponding new view.

 
JavaScript: small details E-mail
Saturday, 11 June 2011 21:29

I was thinking of alternative ways to define a custom class in JavaScript, and came up with two implementations.  I would like to share what I came up with, to demonstrate the flexibility of JavaScript as well as to highlight a couple different ways of accomplishing the same purpose.

Exhibits

Introduction

In what I've called Design 1 and Design 2, there is a different architecture of the Scanner class.  Both implementations return the same result, with the same structure (shown below).  Also in Design 2 I made several small improvements over Design 1 in the code that serves the same purpose between the two designs but is implemented differently in each.

 

The Scanner class:

  1. Has a single point of entry to start a scanning process, which
  2. Scans a web page (the DOM) for categories of posts, and
  3. For each category, it scans and retrieves the following properties of each post and saves it to a Post object:
    • Title
    • Image
    • URL
    • Rating
  4. Several objects, one for each category scanned, are returned with the properties:
    • Name of category
    • Array of Post objects
  5. There is a special category called "Recent edits", which is scanned separately
    • The posts in this category have another property, "Last edited", which is stored in a wrapper along with  the Post

The returned objects look like:

Scanner class return object

Scanner class code

Here is the actual code for the alternative class designs.  You may want to copy the code elsewhere to view the two versions side by side.

Discussion

Architecture

I'll admit that the design difference between the two models is slight, but it's an important one.  It can be summed up in one statement: In Design 1, the methods perform at the class-level, whereas in Design 2, all methods are declared on the prototype and perform on instances of the Scanner class.

 

Despite the difference being a small one, for clarity's sake I decided to map out the two designs in mind maps, shown below.  (Unfortunately you'll need to manually expand the nodes to see them.)  I asked around to see which is better, and for the scope of this small and simplified problem the answer is "it doesn't really matter."  However, if the problem were a larger, more realistic one, Design 2 seems more suitable.

 

Design 1

 

 

Design 2

 

Optimizations

This is the more interesting part, for me at least.  It's easy to forget about the small optimizations you can make in your code when you're just trying to get it to work in the first place.

 

I'm going to step through the various sections of the code in both designs, and highlight what's better in Design 2.

Design 1

//Start Scanning process
Scanner.startScanning = function() {
  Scanner.getCategories();
   $(categories).each(function() {
     Scanner.getPostsByCategory(this);
   });
   Scanner.getRecentPosts();
}

Design 2

Scanner.fn.startScanning = function() {
   var categories = this.getCategories();
   var context = this;
   for (var i = 0, j = categories.length; i < j; i++) {
     this.getPostsWithCategory($(categories[i]));
  }
  this.getRecentPosts();
 
};

  • $.each is an expensive process, so Design 2 uses a simple for loop instead

Design 1

//Return jQuery object of all post categories
Scanner.getCategories = function() {
  categories = $(// some jQuery selector);
}

Design 2

Scanner.fn.getCategories = function() {
   return $(// some jQuery selector);
}

  • Declaring a variable without the var keyword (like categories above in Design 1) makes it global, which is never a good idea unless you specifically need a global variable
  • While both designs do basically the same thing here, Design 2 adheres to functional programming principles, in that it doesn't have any side effects

Design 1

Scanner.getPostsByCategory = function(category) {
  var scanned = new Scanner();
  scanned.category = Scanner.getCategoryName(category);
  $(category).find(// some jQuery selector).each(function() {
    var post = Scanner.createPost(this);
    scanned.posts.push(post);
  });
  return scanned;
}

Design 2

Scanner.fn.getPostsWithCategory = function(jSingleCategory) {
  var context = this;
  context.category = context.getCategoryName(jSingleCategory);
  jSingleCategory.find(// some jQuery selector).each(function() {
    var post = context.createPost(this);
    context.posts.push(post);
  });
}

  • jQuery ($) lookups are expensive as well, so instead of using $(category) in multiple places like in Design 1, in Design 2 we instead pass $(categories)[i] above in startScanning() so that it gets cached, and can be quickly reused as jSingleCategory

Design 1

//Gather post data - title, URL, image, rating
Scanner.createPost = function(post) {
  var title = Scanner.getTitle(post);
  var image = Scanner.getImage(post);
  var url = Scanner.getUrl(post);
  var rating = Scanner.getRating(post);
  var post = new Post(title, image, url, rating);
  return post;
}
 
 
 
/*********************
Post class
*********************/
 
//Post constructor
var Post = function(title, image, url, rating) {
  this.title = title;
  if (image != null) {
    this.image = image;
  }
  if (url != null) {
    this.url = url;
  }
  if (rating != null) {
    this.rating = rating;
  }
}

Design 2

Scanner.fn.createPost = function(post) {
  var postObj = {};
  var jPost = $(post);
  postObj.title = this.getTitle(jPost);
  postObj.image = this.getImage(jPost);
  postObj.url = this.getUrl(jPost);
  postObj.rating = this.getRating(jPost);
  return post;
}

  • Since the Post object is doing nothing except storing a Post, it is overkill to declare a class for it.  As Design 2 does above, it's enough to just create a Post object literal in the Scanner class.

 
My explanation of REST (Representational State Transfer) E-mail
Sunday, 17 April 2011 12:01

There is a lot of information on the web about REST.  But I find much of it unclear, especially for someone without a background in how HTTP works and web application development.  I would like to provide my own (slightly verbose) definition of REST and also link to a handful of useful resources around the web on the subject.

 

My explanation

Background (source)

  1. First of all, REST is not a standard or any kind, it is a style of architecting web applications that aims to benefit from pre-existing features of the HTTP protocol, the protocol that essentially powers the Web.
  2. Under REST, "things" on the web that users are interested in and interact with are resources.  So on a blog site a particular blog post is a resource; a listing of all blog posts is also a resource.  On a shopping site any particular item is a resource, a listing by category is a resource, and so on.
  3. Users interact with resources in representations.  Again, for a blog site, viewing a blog post in a web browser is an HTML representation of that resource; viewing the blog post in an RSS reader is an XML representation of the same resource.
  4. Each interaction places the requesting client in a state, and each new interaction between the client (web browser, RSS reader, etc.) and server is a transfer that updates the state of the client.

What does it mean in practice?

An important thing to know:

  • Resources are represented by nouns.  That is, under a REST-ful application design, your URLs contain the identifier (ID, name, etc.) of the resource you are interacting with, and not some verb describing how you are interacting with that resource.

Let's say a resource is requested at a URL: http://www.somesite.com/blog/posts/a_blog_post

On its own the URL is ambiguous.  You may assume that a user is trying to view the blog post, but under REST, the same URL is used when you want to perform other operations on that resource, like edit or delete it.  So how does the application know what you are trying to do with the resource?  This is where HTTP comes in.

HTTP verbiage

HTTP has built into it four "verbs" that describe the type of interaction you are trying to perform.  If you're familiar with the common CRUD operations, this is how they map to those HTTP verbs:

 

CRUD Operation HTTP operation
Create POST
Read GET
Edit/Update PUT
Delete DELETE

 

So, here's the lesson: With every request sent to a server, two pieces of data are being transmitted:

  1. A noun - the identifier of the resource you are interested in
  2. A verb - an HTTP operation describing how you want to interact with that resource

The beauty of REST is that these descriptors being used are already present in the technologies that power the World Wide Web, so you're not inventing anything new, but simply taking advantage of something that's already there but for some reason has been overlooked.

Consequences

  1. Instead of a URL like http://www.somesite.com/blog/posts/?id=1234&action=edit you can have something like http://www.somesite.com/blog/posts/1234, and the application knows you are trying to edit (instead of view or delete, for example) because the HTTP header that gets sent along with the request tells it so.
  2. Additionally, that same URL can grab an XML (or JSON, or some other) version of the same blog post.  What I didn't mention above is that a third piece of data is being transmitted with your request: the format you want the response in.

Result

One clean and simple URL with many applications, thanks to protocols already built into the system.

The Art of Rails

One of the best explanations of REST is provided in Chapter 6 of the book The Art of Rails by Edward Benson.  Here are some worthy excerpts.

The Art of Rails

On representations

When I drive to work in the morning, I am using the physical representation of that resource. If I look at a photo of my car, I am looking at a graphical representation of that resource. When my transmission blew, the auto repair shop used their computers to pull up a repair-history representation of that resource. These are three different physical representations of the same conceptual entity, the resource.

On REST

Resource-oriented programming ... uses URLs to represent the resources contained within the system — the nouns — and makes the specification of actions to be performed on those resources secondary.

From the REST perspective, web applications are programs that observe and modify the states of those resources.

RESTful development suggests that developers return to using HTTP commands to specify the CRUD operation to be performed and to standardize on these commands as the common set of operations available for all resources contained within a web application.

The mandate that all RESTful resources be accessible by the same standard set of operations simplifies both API development and API use. Your web application API becomes the set of resource endpoints, and HTTP-based REST operations become the method calls into that API.

Other references

 
Ruby on Rails 3: Simple voting system with AJAX (with jQuery) E-mail
Tuesday, 12 April 2011 15:28

Introduction

You have a site based around a model - it could be blog posts, photos, videos, comments on any of those other models - and you want to add a simple voting feature where visitors can vote on a blog post, photo, or whatever else it might be.

 

There are plugins available for Rails to implement voting, thumbs_up being the most popular one for Rails 3, but it's not hard to create your own simple voting mechanism.

 

The one I'm going to demonstrate here will have the following features:

  • Item being voted on: item, item serving as the vote: vote (simple enough?)
  • Only one type of voting - the plugins allow for "for" and "against" votes, but I didn't need that in my project.  It wouldn't be too hard to modify this method to accommodate this.
  • Time-limited voting - a user can vote for any one "item" once every 24 hours.  For this to work your site needs a registration system, or some method that uniquely identifies users, so you can track who has voted on what and when.
  • Items will be sortable by votes
  • AJAX response - when a user votes the vote will be saved asynchronously and a response will be returned via AJAX.  I'll be using jQuery.
  • Administrators will not be creating, modifiying, or deleting votes manually

Requirements

In addition to what comes bundled with Rails, you will need:

You can now link to both these files with:

<%= javascript_include_tag 'http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.5.1.js', 'rails' %>

This code can go in your application.html.erb layout file so it's included everywhere in your application, or you can place it only in the files that need it, that will have something to do with the voting functionality you're adding.

Code generation

(I'm showing the final code here, but I'll break it down further on in the next step of the tutorial.)

At the command prompt execute:

rails g scaffold vote item_id:integer user_id:integer

If you haven't used Rails scaffolding before, this will generate the model, controller, views to execute CRUD operations, and related test files.  You won't need most of the functions and methods the scaffolding will generate, but it's quicker to delete what you don't need than to create what you need from scratch.

Create a column in your items table to store a votes_count integer value

rails g migration add_votes_to_items votes_count:integer

This will run both the scaffold-generated migration for vote and the one you just generated for item

rake db:migrate

 
 
class Item < ActiveRecord::Base
 
    has_many :votes
 
 

Delete all files under views/votes, we won't be needing them, and trim your model and controller to look like the following:

vote.rb

 
 
class Vote < ActiveRecord::Base
  belongs_to :item, :counter_cache => true
 
# rest of your code here
end
 
 

votes_controller.rb

Copy this code to a text file or somewhere else where you can reference it as you read the explanation that follows.

In the views/votes folder that you just emptied, create a create.js.erb file.  This is the response that will be returned by the votes' controller's create action after a vote has been successfully created.

 
 
$(document).ready(function(){
 
    alert('This is a pathetically simple AJAX response!');
 
});
 
 

All you need is a simple button that allows a user to vote for an item in, for example, the items index and item show views.  The form should have hidden fields for the two values you need to store,  item_id and user_id.  Notice the :remote => true in the form_for tag.

 
 
<%= form_for(@vote, :remote => true) do |f| %>
<%= f.hidden_field "item_id", :value => item.id %>
<%= f.hidden_field "user_id", :value => @user.id %>
<%= image_submit_tag "buttons/vote-for-item.png" %>
<% end %>
 
 

You also need to add a line to the corresponding actions in items_controller.rb.  If you don't add this you'll get an error when you go to pull up your views.

 
 
def index
 
    @vote = Vote.new(params[:vote])
 
    #rest of your code here
 
end
 
 

 
 
def show
 
    @vote = Vote.new(params[:vote])
 
    #rest of your code here
 
 

Explanation

I'll break this down by file:

 
 
class Vote < ActiveRecord::Base
  belongs_to :item, :counter_cache => true
 
# rest of your code here
end
 
 

  • belongs_to you should already know by now.  It creates a one-to-many relationship between item and vote (with has_many :votes in item.rb)
  • counter_cache is a great feature of Rails that keeps track of the "count" of votes an item has in the additional votes_count column we added to the items table.  So if later you want to sort items by votes, you can do something like:

 
 
@items = Item.order("votes_count DESC")
 
 

Lines 18-24, 34-36

These lines set up the check for whether the current user (line 19) has voted for the current item (line 18) within the last 24 hours (line 24).  If not, the vote is saved, otherwise the user is alerted (line 35) that they cannot vote again just yet.

Line 27

This line is required for the AJAX response.  This tells the controller that this action can return a JavaScript response in addition to the HTML response that's there by default.  The create.js.erb will now be rendered because we've told the controller to respond to JS requests and we've named the view the same as the controller action.

The key here is the :remote => true, which enables the AJAX functionality of this simple form.

And that's it!  Your AJAX'ed voting system is ready!

 
Springsiteam.com site details E-mail
Sunday, 27 February 2011 17:57

It's difficult to showcase the smaller details that aren't readily visible on a website, so I wanted to outline some of the finer points of the latest Rails project I've worked on, Springsiteam.com.  The details I want to point out focus on coding and server-side efficiency.

API key

The IDX feed requires an auto-generated API key to consume.  The easy thing to do would be to get a new key with every request sent to the server, but the efficient thing to do would be to send the request, and only if an unsuccessful response is received back, request a new API key, which is how I did it using Rails' in-built error- and exception-handling.

Pagination

Springsiteam.com pagination

 

The IDX feed the site retrieves the property listings from features server-side pagination.  Since I use will_paginate for pagination in all my Rails projects, the best solution would be to use will_paginate to navigate the server-side pagination, which is what I did with the following simple code:

@listings = WillPaginate::Collection.create(page, 10, @count) do |pager|
    pager.replace(response['listing'].to_a)
end

Previous/Next navigation

Springsiteam.com

 

The previous/next navigation on individual listing pages was a challenge, because thanks to the server-side pagination, only ten entries were retrieved at a time.  It was easy enough to link to previous/next listings when they weren't the first or last of those 10 records, but for those edge cases something else had to be done.  It would have been inefficient to send a new request to the server with every listing displayed, to get the adjacent listings, so what I ended up doing was:

  • Link to the adjacent listings as long as the current listing wasn't an edge case
  • If the current entry is the first on the page and the current page is not the first page, request the previous ten records from the server and link to the last of those records
  • Similarly, if the current entry is the last entry on the page and the current page is not the last page, request the next ten records from the server and link to the first of those records

Considering that most users don't navigate using previous/next buttons for more than a few entries, this saves a lot of unnecessary communication (traffic) between client and server.

 
«StartPrev12345678NextEnd»

Page 1 of 8