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...

Monday, 17 May 2010

Computer virtualization with VirtualBox: Guest OS copying and extending

Leave a Comment
Virtualization is one of the greatest technologies and there are numerous practical aspects of it together with cool factors which provides a huge advantage for businesses. The main benefit is a money-saving aspect related to a minimised hardware cost, systems' replication, disaster recovery, software testing. Since computer virtualization allows a number of operating systems to be used on a single machine that facilitate the beneficial computer resource utilisation.
Virtualization allows easy creation of base computing environment, software / network testing, application deployment, etc. Virtualization can help tremendously especially if it is free, lightweight and easy to use. This is where Sun VirtualBox comes to play.
VirtualBox is a great desktop virtualization solution which has a number of advantages such as free to use (for personal and evaluation purpose), open source, less resource usage in host, performance benefits, snapshots, save and resume time of virtual machine (VM). ( more here )
It is very flexible and there is a good article "Virtualisation for Developers with VirtualBox" (can be found here) which provides a great inside on how to create, configure and fast start virtual machine (VM) using VirtualBox.
Once a basic Guest OS configuration created it might be copied into a safe place in order to be reused which would save a valuable time creating a new VM. However, managing VM cloning/copying and Hard drive expansion are not so straightforward as they are used in VMware Workstation or Virtual PC.
A number of posts on the web explain how to handle these in one or the other way. The article here describes both clone and copy ways.
I would like to put it in more details though. Below is how we can copy VM:
- Find your VM file, which is located in your user folder within .VirtualBox/HardDisks folder (by default) with .vdi extension.

 
- Copy your .vdi file and rename.
- Open Windows command line from VirtualBox installation folder and issue the following command: VBoxManage internalcommands setvdiuuid "Path_to_copied_vdi". The setvdiuuid command will create a new UUID and assign it the copied disk.- Copy your .vdi file and rename.

 
- Create a new VM and attach the new copied disk.

 
Now new VM is ready for use.

 
Working with VM and adding software might quickly lead to a full hard drive situation. There would be a need to increase the hard disk size in such cases. Below is how we can do that:
- Shut down the Guest OS.
- Create second .vdi hard drive image with a bigger size by going to the File > Virtual Media Manager and click New button to start Create New Virtual Disk wizard.

 
- Click on the Settings button for your Guest OS, and choose the Storage category. Click on Add Attachment under Storage tree and add the created .vdi file as a hard disk. It should be set as IDE Primary Slave on the Attribute panel.

 
- Download and install a free edition of HDClone here
- Click on the Settings button for your VM, and choose the Storage category. Mount hdclone.iso (located in the bootimages folder of the HDClone's installation) in CD/DVD Device.

 
- Ensure the boot order is set to CD-ROM first and start the VM.

 
- Follow HDClone wizard and select Drive- - Once a complete copy is created, quit HDClone, unmount the hdclone.iso in VirtualBox, set new hard drive as master and start VM.
- The old hard drive can be deleted or reformatted.
That's it. Enjoy it!
Read More...

Sunday, 28 February 2010

An issue with HttpCookie using HasKeys property and encoded single value string

Leave a Comment
In this post I would like to share my experiance with HttpCookie object used in some e-commerce application.
A web application containts the cookie implementation, which allows customers to persists thier contact details on the client's side using cookies. That code was implemented by third-party web-developers using a well-known HttpCookie class from the "System.Web" assembly. The provided functionality seemed working as expected untill some customers have sent us complains about unability to persist thier contact details for the future shopping needs. The problem was quite hard to identify. In many cases the coookie was persisted without any problem. Eventually, after digging into HttpCookie code implementation, we have found that the HasKeys property was responsible for all these troubles. The HttpCookie object, in our code, is created and stored with single value which is encoded using Base64 scheme as a standard for transmitting data on the Web. Below is the sample code to demonstrate the case:

First we create the cookie and store it on the customer's machine.

// Build a string from the Form values
            string valuesToPersist = "FirstNameVal=" + TextBoxFirstName.Text + "&SecondNameVal=" + TextBoxSecondName.Text;
 
            // Encode the plain text using Base64 scheme
            byte[] dBytes = Encoding.UTF8.GetBytes(valuesToPersist);
            string strEncoded = Convert.ToBase64String(dBytes);
 
            // Create sample single-value cookie
            HttpCookie sampleCookie = new HttpCookie("CookieExample");
            sampleCookie.Value = strEncoded;
            sampleCookie.Expires = DateTime.Now.AddDays(7);
            Response.Cookies.Add(sampleCookie);

Then on the customers's return the cookie should be retrieved and its value dislayed on the page.

// Retrieve available cookies
            HttpCookie cookie = Request.Cookies.Get("CookieExample");
 
            // Check whether a cookie has subkeys and populate the form with retrieved values
            if (cookie.HasKeys)
            {
                string storedValue = cookie.Value;
                // Decript the value (decription ommited here)  and split into the array
                string[] cookieValues = cookie.Value.Split('&');
 
                // Populate Form with 
                TextBoxFirstName.Text = cookieValues[0];
                TextBoxSecondName.Text = cookieValues[1];
            }


I know that the property HasKeys is supposed to be used when the cookie stores multipe values (read more on this on MSDN).
If we look at the implementation of HttpCookie, then we can see that HasKeys property allows us to check and single value as well. So the above statement is correct but it is producing our problem.

If we look at the implementation code of the HttpCookie in Reflector, for this statement, then we can see that cookie object checks its Values collection which is of type NaveValueCollection:

               public bool HasKeys
                {
                    get
                    {
                        return this.Values.HasKeys();
                    }
                }


And to follow the implementation of the Values member, we can see that it populates the multiValue collection from the stringValue field, which holds our encoded string:

public NameValueCollection Values
                {
                    get
                    {
                        if (this._multiValue == null)
                        {
                            this._multiValue = new HttpValueCollection();
                            if (this._stringValue != null)
                            {
                                if ((this._stringValue.IndexOf('&') >= 0) || (this._stringValue.IndexOf('=') >= 0))
                                {
                                    this._multiValue.FillFromString(this._stringValue);
                                }
                                else
                                {
                                    this._multiValue.Add(null, this._stringValue);
                                }
                                this._stringValue = null;
                            }
                        }
                        this._changed = true;
                        return this._multiValue;
                    }
                }


Now it becomes obvious that the _multiValue collection populated from the _stringValue (single value) if our encoded string contains eigher ‘&’ or “=”. Then the HasKeys value set to true. Otherwise it returns false. Does our encoded string contain any “&” or “=”? Yes, the “=” sometimes presented at the end of the string. And that’s why the cookies are persisted ok sometimes. This is where some explanation about Base64 encoding scheme is required.

Base64 encoding scheme encodes arbitrary binary data as a string composed from a set of 64 characters (see the Base-64 Alphabet image).
The character "=" is used for the padding. This character is the main problem in our cookies issue.

The Base64 encoding schema is used on the Web because of efficiency. It only uses 64 characters. Thus any of the characters can be represented with just 6 bits. (for more explanation on this here:)

The Base64 refers to a specific MIME content transfer encoding(more details here). The transformation takes place as a sequence of octets to get a format that can be expressed in short lines of 6-bit characters, as required by transfer protocols such as SMTP.
So input data used in the sequence of octets which represented as 3x8 (24bits) buffer that can be used 6-bits at a time (4 chars). The first byte is placed in the most significant eight bits of the 24-bit buffer, the next in the middle eight, and the third in the least significant eight bits. The remaining buffer bits (if any left) will be zero. The process is repeated on the remaining data until fewer than four octets remain. If three octets remain, they are processed normally. If fewer than three octets (24 bits) are remaining to encode, the input data is right-padded with zero bits to form an integral multiple of six bits. if two octets of the 24-bit buffer are padded-zeros, two "=" characters are appended to the output; if one octet of the 24-bit buffer is filled with padded-zeros, one "=" character is appended. This guarantees that the decoder will correctly reconstructed data and encoded output length is a multiple of 4 bytes.

The output, in some cases will be like one below with padded chars at the end:
string strEncoded = "FZpZVzcz1zZXJnZXklNDByeWFuYWlyLmNvbQ==";

Or like that:
string strEncoded = "c1ZpZXdfQ29udGFjdElucHU0MHJ5YW5haXIuY29t";                

So, in order to make our code works in all cases we need to remove the check for subkeys and use cookies.value field.

That is it. I hope you found my article useful and it saved you some of debugging time.
Read More...