Monday, 7 June 2010

Building multilingual Asp.Net MVC 2 Photo gallery using jQuery, XSLT, and XML

Leave a Comment
I built the demo gallery a while time ago. Now I would like to share my implementation design and the source code. I am not going to cover the code implementation in full details here in order to keep the article relatively short. Please refer to the source code (link provided below) for design details.

The demo photo gallery solution developed as ASP.NET MVC 2 areas application that contains a single project with default main entry point to the Web application and photos area.



The application structure includes Area folder with Photos subfolder that contains the area-specific child folders (for more information on creating an ASP.NET MVC Areas Application using a Single Project refer to MSDN resources).

The demo gallery implementation is mainly based on a third-party jQuery plugin. So the photos area has only one area-enabled controller PhotosController with one action method Index. I am not going to cover the basics of development with Asp.Net MVC 2 as so many resources available on the web. The index action would return a JavaScript coded view for a photo gallery plugin.

So many amazing photo galleries are available now with huge popularity of jQuery and its beauty for web development. I have decided to choose Galleriffic. Galleriffic is a jQuery plugin that provides a rich, post-back free experience optimized to handle high volumes of photos while conserving bandwidth. It is cool, easy to integrate into a code and it is built with a number of quite useful features.

The gallery is initialized by calling the galleriffic initialization function on the thumbnails container, and passing in settings parameters.
jQuery(document).ready(function($) {
    var gallery = $('#thumbs').galleriffic({
        delay:                     3000 // in milliseconds
        numThumbs:                 20 // The number of thumbnails to show page
        preloadAhead:              40 // Set to -1 to preload all images
        enableTopPager:            false,
        enableBottomPager:         true,
        maxPagesToShow:            7  // The maximum number of pages to display in either the top or bottom pager
        imageContainerSel:         '', // The CSS selector for the element within which the main slideshow image should be rendered
        controlsContainerSel:      '', // The CSS selector for the element within which the slideshow controls should be rendered
        captionContainerSel:       '', // The CSS selector for the element within which the captions should be rendered
        loadingContainerSel:       '', // The CSS selector for the element within which should be shown when an image is loading
        renderSSControls:          true, // Specifies whether the slideshow's Play and Pause links should be rendered
        renderNavControls:         true, // Specifies whether the slideshow's Next and Previous links should be rendered
        playLinkText:              'Play',
        pauseLinkText:             'Pause',
        prevLinkText:              'Previous',
        nextLinkText:              'Next',
        nextPageLinkText:          'Next ›',
        prevPageLinkText:          '‹ Prev',
        enableHistory:             false, // Specifies whether the url's hash and the browser's history cache should update when 

the current slideshow image changes
        enableKeyboardNavigation:  true, // Specifies whether keyboard navigation is enabled
        autoStart:                 false, // Specifies whether the slideshow should be playing or paused when the page first loads
        syncTransitions:           false, // Specifies whether the out and in transitions occur simultaneously or distinctly
        defaultTransitionDuration: 1000 // If using the default transitions, specifies the duration of the transitions
        onSlideChange:             undefined, // accepts a delegate like such: function(prevIndex, nextIndex) { ... }
        onTransitionOut:           undefined, // accepts a delegate like such: function(slide, caption, isSync, callback) { ... }
        onTransitionIn:            undefined, // accepts a delegate like such: function(slide, caption, isSync) { ... }
        onPageTransitionOut:       undefined, // accepts a delegate like such: function(callback) { ... }
        onPageTransitionIn:        undefined, // accepts a delegate like such: function() { ... }
        onImageAdded:              undefined, // accepts a delegate like such: function(imageData, $li) { ... }
        onImageRemoved:            undefined  // accepts a delegate like such: function(imageData, $li) { ... }
    });
});

To make the photo gallery multilingual the navigation settings should be replaced with translated strings in the above script. This is where XSLT and XML would help us greatly to render the localised versions of galleriffic jQuery. I have added a custom ViewEngine that renders XML using XSLT implemented follow a technique described in Pro Asp.Net MVC Framework by Steven Sanderson. It writes view templates as XSLT transformations and uses them to render XML documents.
public ActionResult Index(string albumId)
    {
      return View(GetXml(albumId));
    }

    private XDocument GetXml(string albumId)
    {
      // Load the main content document based on the current culture's thread
      var currentCulture = Thread.CurrentThread.CurrentCulture.Name;
      var xmlMainDoc = XDocument.Load(Server.MapPath(string.Format("~/Areas/Photos/Cultures/{0}/PhotoGalleryTile.xml", 

currentCulture)));
    
      // Load the albums document based on the current culture's thread
      var xmlAlbumsDoc = XElement.Load(Server.MapPath(string.Format("~/Areas/Photos/Cultures/{0}/ImagesRepository.xml", 

currentCulture)));

      // Retrieve the list of image nodes from the specific album
      var images = from image in xmlAlbumsDoc.XPathSelectElements(string.Format("album[@id='{0}']/image", albumId)) select image;
      var xImages = new XElement("images", new XAttribute("albumId", albumId), images);

      // Construct the Albums xml which will be used for album navigation urls
      var xAlbums = new XElement("albums",
                                 xmlAlbumsDoc.Elements("album").Select(
                                   album => new {album, albId = (string) album.Attribute("id")}).Select(
                                   @t => new XElement("album",
                                                      new XAttribute("id", @t.albId),
                                                      new XAttribute("title", (string) @t.album.Attribute("title")),
                                                      (@t.albId == albumId ? new XAttribute("selected", "true") : null)
                                           ))
        );

      // Append the images and album xml to the main content document
      xmlMainDoc.Element("root").Add(xImages);
      xmlMainDoc.Element("root").Add(xAlbums);
      
      return xmlMainDoc;
    }


The gallery view calls three xslt templates which are responsible for rendering gallery content.
View is rendered by loading and parsing the culture-specific xml documents.

For each culture we create two xml files that will contain the localised text and place them into a folder with a culture’s code name. Below is English version of the gallery control:



The next file provides the list of images with appropriate description:



The Area’s Contents folder is where we put all images grouped by albums.



The idea to switch between cultures is to include the culture code in URL. Thus our localised web application will have the following URL schema: /{culture}/{area}, e.g. http://localhost/en-IE/photos

We create the language switch menu in a Photos.Master file by adding the specific menu item for each language we are going to support:
<%= Html.ActionLink("EN", "Index", "PhotosHome", new { area = "photos", culture = "en-IE" }, null)%>
  
   <%= Html.ActionLink("RU", "Index", "PhotosHome", new { area = "photos", culture = "ru-RU" }, null)%>

The routing engine needs to parse an url to create the right controller. For this purpose we create the LocalisedControllerBase abstract class which overrides Execute method. The Execute method gets the culture from RouteData and sets the CurrentCulture as well as CurrentUICulture thread.
protected override void Execute(System.Web.Routing.RequestContext requestContext)
    {
      string currentCulture = requestContext.RouteData.Values["culture"].ToString();
      Thread.CurrentThread.CurrentCulture = System.Globalization.CultureInfo.GetCultureInfoByIetfLanguageTag(currentCulture);
      Thread.CurrentThread.CurrentUICulture = System.Globalization.CultureInfo.GetCultureInfoByIetfLanguageTag(currentCulture);

      base.Execute(requestContext);
    }


PhotosHomeController inherits the LocalisedControllerBase and will use the current culture info to load the correct localised resources.

Basically, this is all about implementation of the Photo Gallery. Project repository can be found here.
Read More...