Photo Gallery – jQuery Integration
In the last post, I released a version of the Photo Gallery that utilized ASP.NET and home brew JavaScript. For more information on the Photo Gallery check out the roadmap and previous posts. During this post, I will be replacing much of the home brew JavaScript and utilize jQuery functions.
Getting Started
For those of you who have not heard of jQuery…WAKE UP! The jQuery library is a popular JavaScript resource that simplifies HTML document traversing, event handling, animating, and AJAX interactions. The jQuery library is extensible via plug-ins. This blog entry will not cover any plug-ins.
You can download jQuery from here. At this time, the current release is version 1.3.2. To help get started with jQuery, the jQuery team provides getting started tutorials here. I found the “How jQuery Works” and the “Getting Started with jQuery” very informative. In addition, they are hands-on tutorials that encourage you to build as you go.
In addition, there are many other resources on the web for jQuery. A quick search will generally yield helpful results. For off-line resources I recommend the following two books:
- Pro JavaScript Techniques by John Resig – Although this book does not provide details on jQuery, it provides a solid foundation of JavaScript programming and is a great pre-requisite for jQuery. John Resig is the creator and lead developer of jQuery and you will find the concepts discussed in this book used heavily in jQuery.
- jQuery in Action by Bear Bibeault and Yehuda Katz – This book covers the gamut of jQuery topics. It covers getting started, selectors, events, animations, creating your own plug-ins, popular plug-ins, and AJAX.
I will not cover the basics of jQuery here. I will provide the details of how I used jQuery in the Photo Gallery application. Recall that previously I had written some home-brew JavaScript to handle element selection, showing / hiding elements, and a simple fade in /out animation. This code works, but jQuery provides all the same functionality (and a ton more) with the benefit of a team of developers ensuring it is cross-browser compatible and bug free. As you will see, the code that was previously released in the “~/scrips/jsUtils.js” file will be completely replaced with jQuery calls and is will no longer be needed.
Updating the Main Image Animation
The purpose of the main image is to show a larger scale version of the selected bitmap. Previously, I wired up my JavaScript to fade the current image out, update the ‘src’ attribute on the HTML image element, wait for the load event to be fired, and the fade the new image in. The relevant home brew code is shown here:
// Show the new image function showNewImage( imageUrl, imageName ) { // Get the image element. var imageId = '<%= this._image.ClientID %>'; var image = rlc.getById(imageId); // Fade out the current image. rlc.fadeOut(image, 500, 30, function(){ fadeInNew( image, imageUrl, imageName ); }); return false; } function fadeInNew(image, imageUrl, imageName) { // Set the new image source image.src = imageUrl; image.alt = imageName; image.onload = function() { rlc.fadeIn(image, 500, 30, 100); }; return false; }
One of the powerful features of jQuery is the ability to select elements. There are a number of selector options. Because I am using a Master Page, my element IDs are mangled by the ASP.NET rendering engine. In the above code, I used the ASP.NET macro to provide the actual element ID for the image. This was necessary because my home brew JavaScript library provided only one selector ‘getById’ to get an element by ID. There are many more selectors provided in jQuery. The following code shows the jQuery replacement for the above home brew.
// Do this as early as possible $(document).ready(function(){ // Hook up an onload handler to always // fade in asynchronously load images. $(".image").load(function(){ $(this).fadeIn("slow"); }); }); // Show the new image function showNewImage( imageUrl, imageName ) { $(".image").fadeOut("slow", function(){ $(this).attr("src", imageUrl).attr("alt", imageName); }); return false; }
Notice how I use the $(“.image”) function to select the element based upon the class name. This simplifies the code by removing the ASP.NET macro. The $(document).ready function provides the ability to have JavaScript executed at the earliest possible time in the page load cycle. I have taken the opportunity to hook up a handler to the ‘load’ event of the image that slowly fades in the image whenever the image source changes. This handler is connected up only once. Architecturally, this is an improvement over the previous design.
Updating the Thumbnail Animation
The photo gallery provides a thumb nail strip that functionally serves the purpose of allowing the user to select an image to view at a higher resolution. A number of aesthetic features have been added to enhance the usability of the application. Initially, all the thumbnails fade in to an opacity of 67% (33% faded out) with a white border. Moving the mouse over an image increases the opacity to 100% (0% faded out). Moving the mouse off of the image returns the opacity to 67%. Clicking on a thumbnail sets the opacity to 100%, changes the border color to black and triggers the main image to update. The following images show the effects as starting from mouse not over (left), mouse over (center), and selected (right).
There is a slow transition between each state that is supplied by the JavaScript fade features.
The code below shows the home-brew JavaScript for connecting up the initial fade in to the opacity of 67%.
<script type='text/javascript' language='javascript'> // Fade in the element as soon as possible. var elemId = '<%= this._fade.ClientID %>'; var elem = rlc.getById( elemId ); if(elem != null) { rlc.fadeIn( elem, 1000, 30, 67 ); } </script>
This code is placed in a global script block below the element being faded in. This ensures the element exists when the script is executed and triggers the script as soon as possible. The code below show the equivalent code using jQuery.
<script type='text/javascript' language='javascript'> // Fade in the element as soon as possible. var elemId = '<%= this._imageFrame.ClientID %>'; $("#" + elemId).fadeIn("slow").fadeTo("slow", 0.67); </script>
Since there will be a number of thumbnails in the page at the same time, the selection must be done using the element ID. Again, since ASP.NET mangles the IDs when you use a Master Page, I have embedded the ASP.NET macro to provide the actual client ID of the element. The jQuery selection is done using an element ID with the following notation: $(“#” + elemID).
A outstanding feature of jQuery is the ability to chain functions together. This can be seen in the jQuery version of the code, where the function to select the element has been chained with the function to fade in the element to 100% opacity, which has been chained to a function to fade the element to 67% opacity. This chaining leads to very compact and powerful lines of code.
The home brew JavaScript to handle the mouse over / mouse out events is shown below:
// Hook up mouse event handlers. elem.onmouseover = function(){ rlc.setOpacity( this, 100 ); } elem.onmouseout = function() { // Am I the selected frame? var fadeOut = true; var sel = rlc.hasClass("imageFrameSelected"); if( sel.length > 0 ) { var imageFrame = sel[0]; for(var i=0;i<imageFrame.childNodes.length;i++) { if(imageFrame.childNodes[i].id==this.id) { fadeOut = false; break; } } } if(fadeOut == true) { // I must not be the selected frame... rlc.setOpacity( this, 67 ); } }
This code connects up handlers for the ‘mouseover’ and ‘mouseout’ events. The ‘mouseout’ event is complicated by some logic to prevent thumbnails that are selected from being faded back down to 67% opaque. The following is the jQuery replacement code:
// Hook up mouse event handlers. $("#" + elemId).hover( function(){ $(this).fadeTo("fast", 1.0); }, function(){ $(this).not(".imageFrameSelected").fadeTo("fast", 0.67); } );
Notice, jQuery exposes a ‘hover’ function that takes two callbacks. Internally, it connects these callbacks to the ‘mouseover’ and ‘mouseout’ events. Since these two events are many times connected up at the same time, jQuery provides the ‘hover’ function as a convenience helper. Also notice the power of the jQuery selectors and chaining in the ‘mouseout’ callback. Let’s walk through that one:
- $(this) – Selects the element that is being wired up.
- .not(“.imageFrameSelected”) – Removes all elements that have the class of ‘imageFrameSelected’. This prevents the next command from operating on the selected image thumbnail.
- .fadeTo(“fast”, 0.67) – Fades the image at the ‘fast’ pace from an opacity of 100% to 67%.
The home brew JavaScript for the selection of a thumbnail is shown below:
elem.onclick = function() { var linkId = '<%= this._link.ClientID %>'; var link = rlc.getById(linkId); var href = link.href; if(href!=null && href.indexOf('doPostBack')!=-1) { // This is a thumbnail of an image. var sel = rlc.hasClass("imageFrameSelected"); if( sel.length > 0 ) { // There was a previously selected image. // Reset its class. rlc.setClass( sel[0], "imageFrame"); // Reset the fade on the previously selected // child node. for(var i=0;i<sel[0].childNodes.length;i++) { var node = sel[0].childNodes[i]; if(node.id != undefined) { if(sel[0].childNodes[i].id.indexOf('_fade')!=-1) { rlc.setOpacity(sel[0].childNodes[i], 67); } } } } // Set the opacity of the selected node. rlc.setOpacity( this, 100 ); // Set me as the selected thumbnail. var imageFrameId = '<%= this._imageFrame.ClientID %>'; var frame = rlc.getById(imageFrameId); rlc.setClass( frame, "imageFrameSelected"); // Gather information to call the // global 'showNewImage' method. var imageId = '<%= this._image.ClientID %>'; var image = rlc.getById(imageId); var thumbUrl = image.src; var temp = new Array(); temp = thumbUrl.split('&'); var imageUrl = temp[0]; for(var i=1;i<temp.length;i++) { if(temp[i]!='t=1') { imageUrl += '&' + temp[i]; } } var imageName = image.alt; showNewImage(imageUrl, imageName); // Return false to short-circuit the normal // post back. return false; } else { // This is the thumbnail of a directory. // Returning true allows the normal postback // to occur. return true; } }
This code wires up a handler for the ‘click’ event. The first ‘if’ block in this code determines if the thumbnail being click is an image or a folder. If an image is selected some logic is executed and the function returns false preventing a post back from occurring in the browser. All the necessary behavior is handled on the client side. In the case of a folder being clicked, no logic is performed and the function returns true. This ensures a post back occurs and server side logic is used to update the UI. In the jQuery version, all post backs have been removed and all navigation is handled on the client side using AJAX (more below).
In the above code, the determination of whether an image or folder is being clicked hinges upon format of the ‘href’ attribute for the link that was clicked. Reviewing that code now, this seems fragile and should have been done differently. If an image was clicked the above code then handles deselecting the previously selected thumbnail, setting the opacity of the newly selected thumbnail to 100%, setting the newly selected thumbnail as the selected one, and calling the ‘showNewImage’ (see above) JavaScript function to update the main image.
The following is the jQuery replacement for the ‘click’ event handler:
$("#" + elemId).click(function(){ if($(this).attr('class')=="folderFrame") { // This is the thumbnail of a directory. var relativePath = $(this).find('a').attr('href'); loadThumbs(relativePath); return false; } else { // If there was a previously selected element // then deselect it and fade it out a bit $(".imageFrameSelected").removeClass("imageFrameSelected").fadeTo("fast", 0.67); // Fade the new picture in all the way and set // it as selected. $(this).fadeTo("fast", 1.0).addClass("imageFrameSelected"); // Gather information to call the // global 'showNewImage' method. var jqElem = $(this).find('img'); var thumbUrl = jqElem.attr('src'); var temp = new Array(); temp = thumbUrl.split('&'); var imageUrl = temp[0]; for(var i=1;i<temp.length;i++) { if(temp[i]!='t=1') { imageUrl += '&' + temp[i]; } } var imageName = jqElem.attr('alt'); showNewImage(imageUrl, imageName); return false; } });
I took the opportunity to improve the code that determines if an image or a folder has been clicked. In the improved version, I leverage jQuery to inspect the class attribute and look for a ‘folderFrame’ value. If this is found, then a folder was clicked. That leads to a call to the ‘loadThumbs’ (more info below) JavaScript function and returning false (no post back). Notice again how the jQuery selectors and chaining allow for more compact and readable code.
Using AJAX to Dynamically Update the UI
The previous release of the Photo Gallery utilized a full page post back when ever you changed folders. In other words, browsing up or down the folder tree by clicking on the “folder” thumbnail caused a post back and the SERVER updated the page to have the thumbnails reflect the newly selected folder. I wanted to remove this post back to provide a better user experience.
The concept is that we want the JavaScript code to asynchronously (i.e. use AJAX) fetch the thumbnail HTML from the server and update the UI. The JavaScript for implementing this is shown here:
<script language="javascript" type="text/javascript"> // Do this as early as possible $(document).ready(function(){ loadThumbs(""); }); // Request a new set of thumbnails function loadThumbs( relativePath ) { PageMethods.GetThumbHtml( relativePath, OnSucceedThumbs, OnFailed); } // Parse the returned html and update the UI function OnSucceedThumbs(html) { // Really have two html chunks seperated by a '|' var temp = new Array(); temp = html.split('|'); $("#_divOuterThumbs").fadeOut("slow", function(){ $(this).html(temp[0]).fadeIn("slow"); }); $("#_divPath").html(temp[1]); } </script>
This code calls the ‘loadThumbs’ JavaScript function when the page is first loaded. This function calls the ‘GetThumbHtml’ page method passing in the current relative path. Calls to page methods happen asynchronously, so we need to hook up success and failure callbacks. The ‘GetThumbHtml’ page method, is executed on the server and renders the HTML for two page elements: the thumbnails and the current path. This HTML is returned in a single call and is separated by the ‘|’ character. The ‘OnSucceedThumbs’ callback, parses the returned HTML and sets the inner html of the elements. Once again, jQuery’s selectors and chaining are used to simplify the code.
The ‘GetThumbHtml’ page method is shown below:
[WebMethod(EnableSession = true)] public static string GetThumbHtml(string relativePath) { // Create the full path. // string rootDirectory = ConfigData.RootGalleryDirectory; string fullPath = Path.Combine(rootDirectory, relativePath); fullPath = Path.GetFullPath(fullPath); // Create a page object to facilitate the rendering. // Page page = new Page(); // Create a place holder to hold the controls // PlaceHolder placeHolderThumbs = new PlaceHolder(); page.Controls.Add(placeHolderThumbs); // Get all the object contained in this folder. // List<GalleryObject> allItems = GalleryManager.GetAllObjects(fullPath, CurrentUser); // Add each item to the page. // foreach (GalleryObject item in allItems) { // Create a user control for this item. // try { MyImageFrame imageControl = page.LoadControl("ImageFrame.ascx") as MyImageFrame; imageControl.SetImage(item); placeHolderThumbs.Controls.Add(imageControl); } catch (Exception ex) { } } StringBuilder sb = new StringBuilder(); StringWriter tw = new StringWriter(sb); HtmlTextWriter hw = new HtmlTextWriter(tw); placeHolderThumbs.RenderControl(hw); return sb.ToString() + "|" + GetPathHtml(relativePath); } [WebMethod(EnableSession = true)] public static string GetPathHtml(string relativePath) { // Create the full path. // string rootDirectory = ConfigData.RootGalleryDirectory; string fullPath = Path.Combine(rootDirectory, relativePath); fullPath = Path.GetFullPath(fullPath); // Create a page object to facilitate the rendering. // Page page = new Page(); // Create a place holder to hold the controls // PlaceHolder placeHolderCurrentPath = new PlaceHolder(); page.Controls.Add(placeHolderCurrentPath); // Strip off the root directory. // List<string> allFolders = new List<string>(); allFolders.Add("root"); if (!string.IsNullOrEmpty(relativePath)) { string[] folders = relativePath.Replace('/','').Split(Path.DirectorySeparatorChar); allFolders.AddRange(folders); } string currentFolder = string.Empty; for (int i = 0; i < allFolders.Count; i++) { if (string.IsNullOrEmpty(allFolders[i])) { continue; } if (i == 0 & allFolders.Count != 1) { // Add the link to the root folder. // HtmlAnchor link = new HtmlAnchor(); link.InnerHtml = allFolders[i]; link.Attributes.Add("onclick", "loadThumbs('');return false;"); link.HRef = "javascript::void();"; placeHolderCurrentPath.Controls.Add(link); } else if (i < allFolders.Count - 1) { // Add the link to the folder. // currentFolder = Path.Combine(currentFolder, allFolders[i]); HtmlAnchor link = new HtmlAnchor(); link.InnerHtml = allFolders[i]; link.Attributes.Add("onclick", "loadThumbs('" + currentFolder.Replace('', '/') + "');return false;"); link.HRef = "javascript::void();"; placeHolderCurrentPath.Controls.Add(link); } else { // Don't link to myself...I'm already there. // Label label = new Label(); label.Text = allFolders[i]; placeHolderCurrentPath.Controls.Add(label); } Label seperator = new Label(); seperator.Text = ""; placeHolderCurrentPath.Controls.Add(seperator); } StringBuilder sb = new StringBuilder(); StringWriter tw = new StringWriter(sb); HtmlTextWriter hw = new HtmlTextWriter(tw); placeHolderCurrentPath.RenderControl(hw); return sb.ToString(); }
On the server, there are two static methods decorated with the ‘WebMethod’ attribute that are used to generate the new HTML. Each method dynamically builds page elements using the ASP.NET server side controls. Once the elements are built up the HtmlTextWriter class is used to render the elements out to a string. This string data is then returned back to the web-browser to be used by the JavaScript as described previously.
Summary
Adding jQuery to the Photo Gallery project was extremely simple. I was able to replace most of the home brew JavaScript with jQuery calls. The selectors and chaining features of jQuery provide an easy to use and really concise framework.
The Visual Studio 2008 project can be found here. You will need to place some photos in the “Photo” folder. A live version of this application can be found here.