Photo Gallery – Silverlight Film Strip
I have done 10 posts in the past building up a photo gallery. The following summarizes some of those past blogs:
- Roadmap & Requirements – A description of the requirements for the project. Most important was that I want this project series to be fruitful in the knowledge that I am learning, but also produce a photo gallery that my family can use.
- ASP.NET & HTML – Design of the photo gallery layout & initial presentation layer implementation, data access / business logic.
- JavaScript – The JavaScript development was done twice. The initial development was done writing all the JavaScript which created a homebrew JavasScript library. The second time used jQuery to implement the JavaScript features. I then spent some time using jQuery to add an enhanced the film strip, slide show player, and AJAX service layer to remove all full page post-backs.
You can find the working demonstration of this version of the photo gallery here.
If you read the roadmap, I always intended on developing a Silverlight version of the photo gallery. This development will take a couple of blogs. The intent is to provide access to both the Silverlight and HTML / JavaScript version of the photo gallery. Some in my family will not have Silverlight installed and I want to be able to default to a version they can use.
A solid data access / business logic layer has already been developed and will be used by the Silverlight version of the photo gallery. All the time that we spent creating a service layer to feed AJAX calls will pay-off because those same calls will feed data to our new Silverlight presentation layer.
One of the advantageous of working with Silverlight is you don’t have to worry so much about cross-browser differences. If the browser supports the Silverlight plug-in, then the plug-in will host the application and rendering will be the exactly how you designed it!
Here is a mock-up of the design created using Balsamiq Mockups For Desktop. This is a great tool for creating fast designs and very in-expensive!
To get started make sure you have the latest version of Visual Studio 2008 and have installed the Silverlight 2 developer toolkit. Then open the solution and add a Silverlight Application. The wizard will provide you with options to create a test page in your web application.
Because of some limitations on URLs in Silverlight, I recommend moving your “xap” application to the root folder of your web app. To do this right click on your web app and select properties. Then choose “Silverlight Applications” in the selection list on the left. Remove the current Silverlight project. Then add a new one. Select your Silverlight project, and be sure to remove any text in the “Destination folder” option. This will make it easier for your Silverlight application to access the current resources (images for example) that are already in the web application.
The film strip along the left-hand edge of the application is what we will develop in this blog. This design draws inspiration from a blog by Michiel Post. To create a vertically scrolling area in XAML we use the following code:
<ScrollViewer x:Name="Scroll1" HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Auto" Height="550" > <StackPanel x:Name="ImageStrip" HorizontalAlignment="Stretch" VerticalAlignment="Top" Orientation="Vertical"> </StackPanel> </ScrollViewer>
The ScrollViewer object is setup for vertical scrolling. The StackPanel child will be dynamically loaded with folder / image thumbnails. The StackPanel has been configured to stack children vertically. If you want to have your film strip scroll horizontally, then switch the scrollbar visibility of the ScrollViewer and the orientation of the StackPanel. To dynamically load thumbnails, we will need to leverage the services that we previously created.
In previous articles, we developed a service layer which exposed services through a “ServiceLayer.asmx” web service. To use this service layer in our right-click the Silverlight project and select the “Add Service Reference…” option. The wizard should be able to discover the service and create a proxy for consuming the services. Here’s a picture of the proxy I added in my project:
This proxy is used in the application just like any WCF service. Here is the code that instantiates an instance of the proxy:
private ServiceLayerProxy.ServiceLayerSoapClient _proxy = null; private void CreateProxy() { // Create the proxy. // BasicHttpBinding bind = new BasicHttpBinding(); EndpointAddress endpoint = new EndpointAddress("http://localhost:57836/Web/ServiceLayer.asmx"); _proxy = new PhotoGallery1.ServiceLayerProxy.ServiceLayerSoapClient(bind, endpoint); // Connect up service proxy events. // _proxy.GetThumbListCompleted += new EventHandler<PhotoGallery1.ServiceLayerProxy.GetThumbListCompletedEventArgs>(_proxy_GetThumbListCompleted); _proxy.GetImageInfoCompleted += new EventHandler<PhotoGallery1.ServiceLayerProxy.GetImageInfoCompletedEventArgs>(_proxy_GetImageInfoCompleted); }
Before deploying this application, we will obviously want to move the end point URI (either dynamically generate it or put it in a config file). Remember WCF services are defined by ‘ABC’: Address, Binding, Contract. The Address & Binding are being set programmatically above. The Contract was defined by the ServiceLayer.asmx WSDL.
Now we have enough structure to dynamically load the thumbnails into the Silverlight film strip. Here is the page load code that starts this off:
public Page() { InitializeComponent(); // Create the service proxy. // CreateProxy(); // Fetch the image list. // FetchImgList(string.Empty); }
Pretty simple. We create the service proxy and then we fetch the image list. Here is the code to fetch the image list:
private void FetchImgList(string relativePath) { _proxy.GetThumbListAsync(relativePath); }
Again, super simple. We use our proxy to call a “GetThumbList” service. All the calls are done asynchronously. That is the reason for the additional code when we create the proxy that binds callback to the the asynchronous complete events. Here is the code for the service call and the callback for the “GetThumbList” is shown here:
[WebMethod(EnableSession = true)] public string GetThumbList(string relativePath) { // Create the full path. // string rootDirectory = ConfigData.RootGalleryDirectory; string fullPath = Path.Combine(rootDirectory, relativePath); fullPath = Path.GetFullPath(fullPath); // Get all the object contained in this folder. // List<GalleryObject> allItems = GalleryManager.GetAllObjects(fullPath, CurrentUser); // Add each item to the page. // StringBuilder imgList = new StringBuilder(); foreach (GalleryObject item in allItems) { if (imgList.Length > 0) { imgList.Append("|"); } string relImgPath = item.FullPath.Replace(ConfigData.RootGalleryDirectory, ""); if (item.GalleryObjectType == GalleryObject.GalleryObjectTypeOptions.Directory) { imgList.Append("d:"); relImgPath += "/folderthumb"; } else { imgList.Append("i:"); } imgList.Append(relImgPath); } return imgList.ToString(); }
void _proxy_GetThumbListCompleted(object sender, PhotoGallery1.ServiceLayerProxy.GetThumbListCompletedEventArgs e) { // Got a new thumbnail list // string[] imageData = e.Result.Split('|'); List<ImageInfo> imgs = new List<ImageInfo>(); foreach (string imgInfo in imageData) { string[] data = imgInfo.Split(':'); ImageInfo img = new ImageInfo(); if (data[0] == "d") { img.ImageType = ImageInfo.ImageTypeOptions.Folder; } else { img.ImageType = ImageInfo.ImageTypeOptions.Image; } img.RelativePath = data[1].Replace("\", "/"); imgs.Add(img); } LoadImageStrip(imgs); }
The purpose of this code is to package the information about the thumbnails that need to be loaded into the film strip. The service call leverages the data access / business logic layer that has already been developed. The job of he call back is to parse the content returned by the service call and create a list of ImageInfo objects. There are two types of thumbnails that will be displayed in the film strip: Folder and Image. The Folder type is simply a folder of images and an Image type is a real image. Whenever a Folder thumbnail is selected it causes the film strip to load a fresh set of thumbnails (one for each Folder / Image that reside in it). Whenever an Image type is selected the main image is updated with a big version of the thumbnail.
After a collection of ImageInfo objects has been created a call to LoadImageStrip does the work of creating thumbnail object to be added to the StackPanel that we created earlier.
private void LoadImageStrip(List<ImageInfo> imgs) { // Clear the current images. // ImageStrip.Children.Clear(); // Add each image. // bool foundImage = false; foreach (ImageInfo img in imgs) { // Create a URL that can be used to dynamically // fetch the image via the image handler. // string relPath = "Image.ashx?rp=" + img.RelativePath + "&t=1"; string absPath = GetAbsoluteUrl(relPath); if (img.ImageType == ImageInfo.ImageTypeOptions.Folder) { // This is a folder of images. // ImageFolder folder = new ImageFolder(absPath); folder.RelativePath = img.RelativePath.Replace("folderthumb", ""); folder.FolderSelected += new EventHandler(ShowFolder); ImageStrip.Children.Add(folder); } else { // This is an image. // Thumbnail thumb = new Thumbnail(absPath); thumb.RelativePath = img.RelativePath; thumb.ImageSelected += new EventHandler(ShowImage); ImageStrip.Children.Add(thumb); if (!foundImage) { thumb.Select(); foundImage = true; } } } }
The above code iterates through the ImageInfo objects, creates a thumbnail object (either a ImageFolder or a Thumbnail) and adds it to the ImageStrip StackPanel. Again, we are reusing an image handler (Image.ashx) to serve up the images. Each object has a selected event that is being wired up at this time. There is also a bit of additional code to select the first image that is encountered. The real UI magic happens in the XAML for the ImageFolder and Thumbnail objects.
<UserControl x:Class="PhotoGallery1.Thumbnail" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" > <Grid x:Name="LayoutRoot" Background="Transparent"> <Grid.Resources> <Storyboard x:Name="GrowAnim" FillBehavior="HoldEnd"> <DoubleAnimation x:Name="GrowAnimW" Storyboard.TargetProperty="Width" Storyboard.TargetName="Frame1" To="175" Duration="00:00:00.2" /> <DoubleAnimation x:Name="GrowAnimH" Storyboard.TargetProperty="Height" Storyboard.TargetName="Frame1" To="125" Duration="00:00:00.2" /> </Storyboard> <Storyboard x:Name="ShrinkAnim" FillBehavior="HoldEnd"> <DoubleAnimation x:Name="ShrinkAnimW" Storyboard.TargetProperty="Width" Storyboard.TargetName="Frame1" To="140" Duration="00:00:00.2"/> <DoubleAnimation x:Name="ShrinkAnimH" Storyboard.TargetProperty="Height" Storyboard.TargetName="Frame1" To="110" Duration="00:00:00.2"/> </Storyboard> </Grid.Resources> <Border x:Name="Frame1" CornerRadius="5" BorderBrush="Black" BorderThickness="1" Background="White" Width="140" Height="110" > <Image x:Name="Image1" Stretch="Fill" Margin="5" Source="" MouseEnter="Image1_MouseEnter" MouseLeave="Image1_MouseLeave" MouseLeftButtonDown="Image1_MouseLeftButtonDown" /> </Border> </Grid> </UserControl>
The above XAML defines the Thumbnail user control. Animation resources are embedded in the XAML to define a grow / shrink animation that occurs for mouse enter / mouse leave events. The UI is simple an Image inside a Border element. Got to love the ease with which you can get rounded corners. The code behind is shown here:
public partial class Thumbnail : UserControl { public string ImageSource { get; set; } public string RelativePath { get; set; } public bool IsSelected { get; set; } public event EventHandler ImageSelected; public Thumbnail() { InitializeComponent(); } public Thumbnail(string imageSource) { InitializeComponent(); Uri uri = new Uri(imageSource, UriKind.RelativeOrAbsolute); ImageSource img = new System.Windows.Media.Imaging.BitmapImage(uri); Image1.SetValue(Image.SourceProperty, img); ImageSource = imageSource; IsSelected = false; } public void Select() { IsSelected = true; GrowAnim.Begin(); if (ImageSelected != null) { ImageSelected(this, new EventArgs()); } } public void DeSelect() { IsSelected = false; ShrinkAnim.Begin(); } private void Image1_MouseEnter(object sender, MouseEventArgs e) { GrowAnim.Begin(); } private void Image1_MouseLeave(object sender, MouseEventArgs e) { if (!IsSelected) { ShrinkAnim.Begin(); } } private void Image1_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { Select(); } }
The ImageFolder object is pretty much the same with the exception of a few UI differences to make an image folder look different than an image. Here are two screen shots of the Silverlight film strip in action.
The image on the left shows two ImageFolder objects. The one on the bottom has the mouse over it (can’t see the mouse in the screen capture). After selecting the 2008 ImageFolder object, the thumbnails shown in the right hand image are displayed. The right hand image shows one thumbnail that is selected.
That was very easy! We are really benefiting from our previous efforts to separate the presentation layer from the business logic / data access layer. The service layer wrapper is being used directly by the Silverlight application. Because of this separation and the reliance on services we are easily able to pull off the ASP.NET / JavaScript presentation layer and replace it with Silverlight. Next time we will develop the main image and the toolbar functionality that we previously had.