Introduction
This article demonstrates a photo gallery template, similar to the Windows XP filmstrip folder view with Slideshow features and "on-fly" thumbnail image generation during run-time. As an option there is a demonstration of how to use the basic XMLHttpRequest
to get the required data from a server through an HttpHandler
. A demo is located here.
Background
There are many photo galleries available throughout the web, but to my surprise, I wasn't able to find what I needed exactly, for a small project. Most of the photo albums show a thumbnail gallery similar to the "Icon" view, which usually takes a lot of screen space when you have a number of images. Anyway, my client had asked to show images as a thumbnail "strip" with scrolling through them and having large images with some details below, and it gave me a reason to start developing this. And in my opinion, this type of a gallery suits well, for example, to show a catalog of items, etc.
The main idea here is to show small images from the gallery in one row with scrolling back and forth, and put one large image below, something similar to the WinXP folder “Filmview”, or similar to a catalog view. When a user clicks on a small image, he gets a large image and some details about the product, item or photo, that is pulled from a database or a data file. (It is a different story to link data with images.)
Using the code
The page has the following controls: a Datalist
with HyperLink
items to show the folders list and navigate between galleries. It binds an array of folders that have photos/images. The name for the main photo folder is set in the Web.config file:
<add key="photofolder" value="photo"/>
When the page is loaded, the Datalist
is filled with subdirectory names from the main photo folder, and each DataItem
has links to the proper folders. A subfolder name is added to TargetUrl
as a parameter. For example, a URL to switch our page to the desired folder looks like PhotoGallery.aspx?G="foldername". If there is no parameter, the page will load the first gallery and the first image from it, using the GetPhotos()
method.
So, we have our folder list from the main photo directory. What is next? We want to show only portions, for instance, 10 images, and indicate that there is more to show. I used a Repeater
control for the reason that it is a simple list control, and I only need to have images listed within. The main challenge was to create “pages” for the Repeater
control, but searching the Internet I found an article by Harrison Enholm. The main idea is to bind my array of addresses of images to the PagedDataSource
first, set PageSize
, PageNumber
etc., and then, bind the Repeater
to it. Simple, but effective.
Now, we can bind the data and use the ViewState to keep track of pages, etc. If there is only one page, the navigation control could be hidden or disabled, depending on client preference. I put some navigation control here like "Go to Page: #", "Go to last/first page" etc.
The GetPhotos()
is the main method to get the desired images from a directory, based on the page parameters.
This method will iterate through the chosen directory (depending on the parameter ?G=""), and using the System.IO.Directory.GetFiles
method, we will get a string
array of image names. Having the names, we can use them as the source for image controls, link address for the large image etc. So then, we will use this array as the datasource for our Repeater
control. At the same time, we will fill the DropDownList dlPages
with page numbers, which we will use to jump to any page directly. This option is good when you have more than 10 pages, for example.
Within the Repeater
, each item is a <img>
with a src
attribute:
<a><img id=iPhoto runat=server
src='<%# "ThumbGen.ashx?path="+Container.DataItem%>'
title='<%# Container.DataItem %>'
width="69" height="90" style="cursor:hand"
class=photoover>
</a>
private void GetPhotos()
{
string photodir =
System.Configuration.ConfigurationSettings.AppSettings["photofolder"];
string curGallery="";
if (Request["G"]!=null)
{
curGallery=Request["G"];
}
else
{
curGallery=
System.IO.Directory.GetDirectories(Server.MapPath(photodir))[0];
string parent=(string) ViewState["parent"];
curGallery=curGallery.Substring(parent.Length);
}
try
{
string[] photos = System.IO.Directory.GetFiles(
Server.MapPath(photodir+"/"+curGallery),"*.jpg");
int i=System.IO.Directory.GetParent(
Server.MapPath(photodir)).FullName.Length;
for (int ix=0; ix<photos.Length; ix++)
{
photos[ix]=photos[ix].Substring(i+1);
}
int num=photos.Length;
lblPage.InnerText=num.ToString();
if (num>9) imgbNext.Visible=true;
PagedDataSource objPds = new PagedDataSource();
objPds.DataSource = photos;
objPds.AllowPaging = true;
objPds.PageSize = 10;
objPds.CurrentPageIndex = CurrentPage;
ViewState["Pages"]=objPds.PageCount;
if (!Page.IsPostBack)
{
dlPages.Items.Clear();
for (int ip=1;ip<=objPds.PageCount;ip++)
{
dlPages.Items.Add(ip.ToString());
}
}
else
{
}
lblPage.InnerText=" Page "+(CurrentPage+1).ToString() +
" of "+objPds.PageCount.ToString();
imgbPrev.Enabled = !objPds.IsFirstPage;
imgbNext.Enabled = !objPds.IsLastPage;
repPhotoG.DataSource=objPds;
repPhotoG.DataBind();
}
catch (Exception ex)
{
}
}
Initially, I created separate thumbnail images with some distinctive names and then parsed file names to select only thumbnails, but not large images from the directory, and bound the list with my Repeater
template. However, logically, it seems better to generate them on the fly, to make life easer for the user. So, I used System.Drawing
classes and the method getThumbnail
to get small images. Now the image src
attribute looks like “src=<% ''ThumbGen.ashx?path="+Container.DataItem %>
”. ThumbGen.ashx is an HttpHandler
, handling the creation of images on the fly. It is a fairly simple handler, its task is to create a small image and return a byte stream back using the image path as the input parameter. Some details are shown below:
public class ThumbGen : IHttpHandler
{
public void ProcessRequest (HttpContext context)
{
if (context.Request["path"]!=null)
{
string img=(string)context.Request["path"];
string path1=context.Server.MapPath(context.Request.ApplicationPath);
path1=context.Server.MapPath(img);
img=path1;
System.Drawing.Image.GetThumbnailImageAbort myCallback =
new System.Drawing.Image.GetThumbnailImageAbort(ThumbnailCallback);
Bitmap bitmap = new Bitmap(img);
ImageFormat imgformat=bitmap.RawFormat;
System.Drawing.Image imgOut=
bitmap.GetThumbnailImage(70, 90, myCallback, IntPtr.Zero);
bitmap.Dispose();
context.Response.ContentType="image/jpeg";
imgOut.Save(context.Response.OutputStream, ImageFormat.Jpeg);
imgOut.Dispose();
}
I left commented code, that I used as well, so you could try different ways to generate thumbnails. I used a standard method available in the .NET framework. Another approach creates a white background first and then loads the bitmap on it. At the same time I use hard coded thumbnail image sizes, but again it could be passed as optional parameters to the handler or be calculated based on the image orientation (landscape or portrait). It is possible, for example, to add more parameters, like size, width or height for the thumbnail image etc. In my case, landscape images will look distorted. Finally each small image will be generated "on-the-fly" and returned back to the page.
In addition, there are some Microsoft filters for thumbnails and large photos. All thumbnail photos are shown in gray and becomes colored on mouse hover. Large images have a fade effect when they are changed. To avoid "flickering", the page transition effect is applied to the whole page, but it be might more appropriate to apply the transition to the thumbnail only. (Place to experiment.)
After we have got the thumbnail gallery, we still need to decide how to show large images. For this I am using the ItemDataBound
event. Here we can find images within the Repeater
, and then using string manipulation, we can convert absolute paths to relative (for example, instead of c:\\foldername\filename just /<virtualfolder>/<filename>) and add a JavaScript function OnClick
for each image to show the large image. Relative image path names are passed to the JavaScript function:
img.Attributes["onclick"]="showImg('"+IMGpath(img.Src)+"')";
Here is the JavaScript function:
function showImg(imgName) {
imgOn = ("" + imgName);
document.imgLarge.src = imgOn;
hdr.innerHTML=imgName; document.imgLarge.filters[0].Apply();
document.imgLarge.filters[0].Play();
}
So, when the user clicks on a small image, the large image imgLarge
's src
attribute will be changed, and the full size photo will be shown.
At the same time, I decided to check the size of the first image and use this info to change the size of my large image to have a proportional picture. Initially, I had all my images in all the folders the same size, that was good just for me, but in real life, images could be of different sizes and orientations. In my case they should be the same size within one folder only. May be it is not the best way so far, somehow it is better to resize each image on the client side before assigning it for a large one.
For this purpose (to find the size of images), I used a Bitmap
created from the first photo to get the width- to-height ratio and change the size of the large image proportionally.
private void repPhotoG_ItemDataBound(object sender,
System.Web.UI.WebControls.RepeaterItemEventArgs e)
{
System.Web.UI.HtmlControls.HtmlImage img=
(System.Web.UI.HtmlControls.HtmlImage)e.Item.FindControl("iPhoto");
img.Src=img.Src.Replace('\\', '/');
img.Attributes["onclick"]="showImg('"+IMGpath(img.Src)+"')";
img.Attributes["onmouseover"]="this.className='thumb'";
img.Attributes["title"]=img.Src;
img.Attributes["onmouseout"]="this.className='photoover'";
if (e.Item.ItemIndex==0)
{
imgLarge.ImageUrl=IMGpath(img.Src);
string path=Server.MapPath(imgLarge.ImageUrl);
Bitmap newimg = new Bitmap(path);
double ratio= (double)newimg.Width/(double)newimg.Height;
lblName.InnerText="size:"+newimg.Width.ToString()
+"-"+newimg.Height.ToString();
lblName.InnerText=lblName.InnerText+"Resolution:"+
newimg.VerticalResolution.ToString();
imgLarge.Style["Width"]=(imgLarge.Width.Value*ratio).ToString();
newimg.Dispose();
}
}
That's it. To use this gallery in real life just specify the main folder name, put PhotGallery.aspx on the web server, copy the bin folder, and create a subfolder with images, and don't forget to correct the Web.config file (add config keys and the HttpHandler
section). The application will take care about others.
Updates (October 24,2005):
A slideshow feature is added. Again, perhaps there are a few different ways to create a slideshow. My intention was not to use any postback and do everything on client side. The idea behind slideshows is to have a list of images on the client side and then dynamically change our image.src
property based on the current choice.
For this purpose, I put a hidden HtmlListBox
on the page to keep a list of all image info within one folder. Pretty much simple. At the time when we are binding our Repeater
on the server side, we bind the ListBox
with image paths. When the page is returned to the client we will have the full list of images that we will play in the slideshow. After that we only need to add some scripting to control playback/stop and slideshow appearance. There is a set of different Java functions responsible for the slideshow. All the slideshow functionality is based on JavaScript/DHTML (file gallery.js).
How it works: clicking on Slideshow-Launch will activate the slideshow and will show the "slide control box" on the top of the image. (Previous/Next/Pause/ Full Size/Stop) For "Fullsize" functionality, we are using DTHML so we can change the layout and the position of elements. So when the user will click on the "FullSize" button, he will get an image on the middle (the position will be calculated using the client screen size).
To stop the slide show, for simplicity sake, I just used a regular ASP.NET button "postback", which will refresh a page and reload the gallery again. Otherwise we need to have a function opposite to "Full-Size" and return everything back. Yet another option could be a separate "full screen" window.
function SlideShow()
{
document.imgLarge.filters[0].Duration=2.5;
document.imgLarge.filters[0].Apply();
document.getElementById('hdr').innerText=
document.getElementById("listImg").options[i].text;
document.imgLarge.src=document.getElementById("listImg").options[i].text;
document.imgLarge.filters[0].Play();
if (i>=((document.getElementById("listImg").length)-1))
{
i=0;
}
else
{
i=i+1;
}
}
"AJAX" way to create a slideshow:
Just to explore the AJAX technology the new option is added to HttmlHandler
. If we will use the parameter "Dir'" (will look "ThumbGen.asxh?Dir=___"
), which means the directory name where we want to run the "slideshow", the handler will return to the response stream XML document with all image names. We will use this to make a remote callback from the client side through an XtmlHTTP
request and get the required list of images. The result we have is an array, but please note that we will convert the return stream to get an XML presentation of the data. This will give you more options to work with documents on client side, especially when you have data from a database. For example if we have a DataSet
, we can use the method .GetXML()
to get the XML format. Otherwise we simply make a call to ThumbGen.ashx with the required parameters and in return will get a "list" with photo names. Then we fill Arrays
on the client side. That's it, we have a source for the slideshow and will use the same script as before to run it.
I tried this way just to see for myself how we can use the XmlHtml
object and HtmlHandler
. In a similar way we can request data from a database. On a page you could also find a hidden dropdown listbox, just for example, and try how we can fill it with data using a request. I am not using this for any other purpose. Tip: If we will not specify for example the width of an image, it will be taken proportional to the height automatically.
I believe there is a lot of room for improvement, but this just one template to build a gallery and a different example of Javascripts/DHTML, and the remote callback technology.
function ProcessResponse()
{
if(req.readyState == 4)
{
if(req.status == 200)
{
var p=req.responseXML.documentElement;
var ddlTest = document.getElementById("test");
for (var count = ddlTest.options.length-1;count >-1; count--)
{
ddlTest.options[count] = null;
}
var photos = p.getElementsByTagName('photos');
var text;
var listItem;
var l=photos.length;
for (var count = 0; count < photos.length; count++)
{
text = (photos[count].textContent ||
photos[count].innerText || photos[count].text);
listItem = new Option(text, text, false, false);
ddlTest.options[ddlTest.length] = listItem;
}
}
else
{
alert("Error retrieving data!" );
}
for (i = 0; i<l; i++)
{
text2 = (photos[i].textContent ||
photos[i].innerText || photos[i].text);
Album[i]= new Image();
Album[i].src = text2;
}
slide=0;
AjaxSlideShow();
SlideMenu();
}
function AjaxSlideShow()
{
StopSlide();
if (slide>=Album.length)
{
slide=0;
}
document.imgLarge.filters[0].Duration=2.5;
document.imgLarge.filters[0].Apply();
document.imgLarge.src=Album[slide].src;
document.imgLarge.filters[0].Play();
var tm=document.getElementById("slidetime").value*1000
oInterval=window.setTimeout("AjaxSlideShow()",tm);
slide++;
}
Points of Interest
I am thinking of extending the handler by adding some methods to get images from a database and tie the images with a data file or a database. I am still looking to improve thumbnail loading to avoid "flickering" and get a more smooth appearance. I also left a link to the "Slideshow" function: to play all the files from the directory - that is to be implemented. Also I noticed that filters are not working in the first place when a page is just loaded and starts working after the next click. Also I want to try converting this to a User Control with some external properties. There is always a way to add something, to improve. That's why I like programming, it is endless.
History
- August 2005 - Basic project.
- October 2005 - Rewritten for usability.
- October 24, 2005- Slideshow feature added.