
Imagine how to build an uncomplicated online photo gallery. In simple terms, these are two separate pages: a list of all photos and viewing of a single photo. When moving from one page to another user has to wait for a full page reload. Interactivity is lost.
Another approach: use AJAX. All page navigation logic is moved to javascript. When you first access the gallery, the page is loaded completely, with subsequent actions of the user, only the necessary part of the page is updated.
This approach has disadvantages:
- Complex JavaScript logic.
- Browser navigation back / forward does not work.
- Individual photos do not have their own URL for a direct link.
The last two drawbacks come down to the first by making the JavaScript code even more complicated. In the article I will show how to develop a simple photo gallery application using the
Page-View pattern . The main advantage of the approach is a highly scalable
object-oriented JavaScript code.
')
Visually, the gallery will look like this:
Page-View Pattern
So, what is the main idea. We introduce two abstractions.
Page - a whole page that requires a full reload in case of an update. In our application, it is one - the whole photo gallery.
View is a separate page view. In the case of the gallery there are two of them: a list and a separate photo. Thus, one page corresponds to several presentations, however at any given time only one of them is active. Below is a class diagram of Page and View hierarchies:
Let's try to figure out what is shown here.
Let's start with how to use it. Below is a fragment of the html-code of a single page in our application:
<!-- ListView -->
< div id ="list_container" style ="display:none;" >
</ div >
<!-- ImageView -->
< div id ="image_container" style ="display:none;" >
< p >< a id ="back_link" href ="javascript:;" > < < Back to list </ a ></ p >
< p id ="img_place" ></ p >
</ div >
< script language ="javascript" >
$( document ).ready( function ()
{
var p = new Gallery.Page();
p.Init();
});
</ script >
* This source code was highlighted with Source Code Highlighter .
In the code above, the markup is set for two views:
ListView and
ImageView . Then an instance of the
Gallery.Page class, representing the gallery page, is initialized.
$ (document) .ready - a
jQuery library construct that allows you to perform a given function after loading the entire contents of the document, in other words, when the entire document object model (DOM) of the document is ready for use. The jQuery library will be used repeatedly in the future. Her calls are easily recognized by the '$' symbol.
Consider now the description of the class
PageBase .
// * abstract class PageBase *
//
// Represents an entire page.
function PageBase()
{
}
// Array of page's views.
PageBase.prototype._views = {};
PageBase.prototype._AddView = function (view)
{
this ._views[view.GetTypeName()] = view;
}
* This source code was highlighted with Source Code Highlighter .
The
_views field is an associative array, the key of which is the name of the view, the value is an instance of a specific
ViewBase class. This array is filled when initializing
PageBase subclasses using the
_AddView function.
// Performs page initialization.
PageBase.prototype.Init = function ()
{
var t = this ;
$.history.init( function (hash) { t._PageLoad(hash); });
}
PageBase.prototype._currentViewName = {};
// Handles page load event.
PageBase.prototype._PageLoad = function (hash)
{
var params = PageBase._ParseHash(hash);
if (! this ._views[ params .view])
{
// first view is the default one
for ( var viewName in this ._views)
{
PageBase.Goto(viewName, params );
return ;
}
}
// checks if the passed view has changed
if ( this ._currentViewName && params .view !== this ._currentViewName)
{
this ._views[ this ._currentViewName].Hide();
}
this ._currentViewName = params .view;
this ._views[ params .view].Show( params );
}
* This source code was highlighted with Source Code Highlighter .
The
Init method initializes the
History for jQuery plugin. This plugin allows you to rewrite url pages depending on the current View and its parameters, as well as work correctly with back / forward navigation.
The
_PageLoad private method selects the appropriate mapping for the url parameters and displays it, hiding the previous View if necessary.
PageBase.Goto = function (viewName, params )
{
params .view = viewName;
var hash = PageBase._MakeHash( params );
$.history.load(hash);
}
* This source code was highlighted with Source Code Highlighter .
Goto is a static function that performs a transition to a given View. I pay attention that parameters with which all View work is an associative array. While in the url parameters (hash) are written as a string. To serialize and deserialize the parameters, the methods are
PageBase._MakeHash and
PageBase._ParseHash , respectively.
Let's turn to a specific implementation of the
PageBase class, that is, to
Gallery.Page :
// * namespace Gallery *
var Gallery = Gallery || {};
// * class Page *
//
// Represents an entire gallery page.
Gallery.Page = function () // extends PageBase
{
PageBase.call( this );
}
OO.Extends(Gallery.Page, PageBase);
// * public methods *
Gallery.Page.prototype.Init = function ()
{
this ._AddView( new Gallery.ListView());
this ._AddView( new Gallery.ImageView());
Gallery.Page.superclass.Init.call( this );
}
* This source code was highlighted with Source Code Highlighter .
The
OO.Extends function is a JavaScript inheritance pattern. The implementation is borrowed from the
Pro JavaScript Design Patterns book (authored by Yahoo and Google engineers). This is the best book on
OOP on JavaScript from those that I know. Highly recommend.
It's time to deal with the hierarchy classes View:
//* abstract class ViewBase *
//
// Represents a single view on a page.
function ViewBase()
{
}
// * private/protected fields *
ViewBase.prototype._params = null ;
ViewBase.prototype._container = null ;
// * public methods *
ViewBase.prototype.GetViewName = function ()
{
throw "ViewBase.GetViewName is not implemented." ;
}
ViewBase.prototype.Show = function ( params )
{
if (! this ._params) // lazy initialization
{
this ._Init();
this ._params = {};
}
if (!ViewBase._CompareParams( params , this ._params))
{
this ._params = params ;
this ._Refresh();
}
this ._ShowImpl();
}
ViewBase.prototype.Hide = function ()
{
this ._container.hide();
}
// * private/protected methods *
ViewBase.prototype._ShowImpl = function ()
{
this ._container.show();
}
// View initialization. Container must be specified here (in subclasses).
ViewBase.prototype._Init = function ()
{
throw "ViewBase._Init is not implemented." ;
}
ViewBase.prototype._Refresh = function ()
{
throw "ViewBase._Refresh is not implemented." ;
}
* This source code was highlighted with Source Code Highlighter .
The
_container field is a jQuery object representing the DOM element of the container defined in the html page markup. Initialization of the field should occur in the function
Init subclasses.
By checking the parameters for equality using the
ViewBase._CompareParams function, the caching mechanism is implemented: the view is not updated if the parameters have not changed since last time.
Throwing exceptions is nothing more than abstract class methods. Thus, all that remains to be done in subclasses of
ViewBase is to define implementations of the methods
GetViewName ,
_Init ,
_Refresh . Here is how it is done in the
ListView class:
// * namespace Gallery *
var Gallery = Gallery || {};
// * class ListView *
//
// Represents view on a page.
Gallery.ListView = function () // extends ViewBase
{
ViewBase.call( this );
}
OO.Extends(Gallery.ListView, ViewBase);
// * public methods *
Gallery.ListView.prototype.GetTypeName = function ()
{
return "list" ;
}
// * private/protected methods *
Gallery.ListView.prototype._Init = function ()
{
this ._container = $( "#list_container" );
}
Gallery.ListView.prototype._Refresh = function ()
{
var t = this ;
$.ajax(
{
url: "/Home/List" ,
dataType: "json" ,
success: function (data) { t._ListLoaded(data); }
});
}
Gallery.ListView.prototype._ListLoaded = function (data)
{
this ._container.empty();
for ( var i = 0; i < data.Images.length; i++)
{
this ._container.append( "<p><a href='javascript:;'>" + data.Images[i] + "</a></p>" );
}
$( "a" , this ._container).click( function () { PageBase.Goto( "image" , { img: $( this ).text() }); });
}
* This source code was highlighted with Source Code Highlighter .
The
_Refresh method to update the page data accesses the web service located at url / Home / List. Note that thanks to the caching mechanism mentioned above, the query is executed only when the list is opened for the first time.
Below is the implementation code for a web service in
ASP.NET MVC :
[AcceptVerbs(HttpVerbs.Get)]
public JsonResult List ()
{
var images = new List < string >();
foreach ( var file in Directory .GetFiles(HostingEnvironment.ApplicationPhysicalPath
+ "/Content/Images/" ))
{
images.Add(Path.GetFileName(file));
}
return Json( new {Images = images});
}
* This source code was highlighted with Source Code Highlighter .
The complete application code (VS 2008, ASP.NET MVC) can be downloaded here .
JS-scripts can also be downloaded separately .Summarizing
Thanks to the Page-View pattern, the problems indicated at the beginning of the article were solved. Indeed, despite the fact that the code was rather cumbersome, most of it is concentrated in basic abstractions (PageBase and ViewBase). Thus, adding new pages (Page) and views (View) becomes a trivial task.
Another positive result of the pattern. Note that the application's server logic is concentrated in the
List method. The page containing the markup is static. This allows you to develop highly scalable web applications. The dynamic behavior of the server side is limited to processing ajax requests. The rest of the content, including the html markup and js code, is static and, therefore, can be easily distributed across different servers in the cluster.
PS Many thanks to my colleague Dmitry Yegorov, who is the author
of most of the ideas described here :)