Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Configurable Silverlight Image Rotator

0.00/5 (No votes)
23 Feb 2009 1  
Using Silverlight 2.0 and C#/VB.NET to build an image rotator that has a useful set of basic features and is easy to setup and deploy.

Introduction

Using Silverlight 2.0 and C#/VB.NET, we will build an image rotator that has a useful set of basic features and is easy to setup and deploy.

Features

  • Can link to local images deployed with an application, or to images hosted elsewhere on the Internet.
  • Image paths and attributes stored in an XML file for easy changes without the need to rebuild the application.
  • Auto play can be toggled on/off.
  • Duration that each image is shown is adjustable.
  • Image transition effects can be turned on/off.
  • Image number, last and next buttons can be hidden/displayed.
  • Can pause auto-play when the number button is selected, restarted when another button is selected.
  • Image border can be displayed/hidden.
  • Image border width and color/opacity can be changed.
  • All configuration changes can be made without recompiling the ImageRotator.

Using the code

The main emphasis I put on building this image viewer was to make it easy to implement on a site (HTML or ASP.NET) and yet contain a handful of useful features. I also wanted to make sure images could be changed, removed, and added without having to do a rebuild of the application or stop/start the web server. Since I abstracted a lot of the configuration complexities away, the amount of code got a little lengthy for what I would consider a simple application. For that reason, I will not go into the details of each method that makes up this control. Instead, I will give a brief overview of how it works, and then I will spend more time on the few items that caused me the most problems, along with hopefully good deployment instructions for those who don’t care how it works, but just want to use it.

Overview

This application is divided into two projects. The first is the website project (ImageRotator.Web) and the other is the Silverlight project (ImageRotator). The website project has a folder called ClientBin which the Silverlight application has access to at runtime. This is where we will store the actual images and the SlideShowImages.xml file.

ImageRotatorSolutionExplorer.jpg

The website also contains the two web pages which use the ImageRotator control. The first is ImageRotatorTestPage.aspx. This page contains not just the control, but also some controls to change the attributes of the ImageRotator and refresh the control so you can see the changes. Obviously, this is for demo purposes, and most likely, your page would not contain these other controls. The code-behind of this page also shows how you can load the required InitParams from the web.config's appSettings or from the web page control values on page refresh and pass them into the ImageRotator control. The second page containing the Silverlight control is ImageRotatorTestPage.html. This page does not contain all the controls to change InitParams and refresh the control at runtime. It does, however, show another example of how to load the InitParams using a param tag with the name of “initParams” and a coma delimited list of name/value pairs.

Please note that the ImageRotator requires that these parameters all be passed in. There are other methods of passing in InitParams that I haven’t shown such as passing through a URL string. I’ll leave these other methods for you to investigate.

Moving onto the second project, ImageRotator, we have the classes that make up the Silverlight control and gives it the functionality. I’ll give a brief description of each of these files.

  • SlideShowImage.cs: This class represents an image and contains the properties related to an individual image.
  • PictureAlbum.cs: This class contains a collection SlideShowImages and those properties common to all the images in the collection. This class also has the asynchronous call which loads the images from the SlideShowImages.xml file into the collection and raises the pictureAlbumLoadedHandler to notify listeners that the picture album has been loaded.
  • PictureAlbumEventArgs.cs: This class contains the support code for custom events which fire when the picture album has finished asynchronously loading the image collection to be used by the ImageRotator from the SlideShowImages.xml file contained on the website.
  • App.xaml.cs: When the application starts, the Application_Startup event is fired. This is where we verify all the parameters required by ImageRotator are being passed in. We are not doing any robust checking of the parameters, but simply verifying that the correct number is passed. Should any of the parameters not be correctly formatted, we will throw an exception later.
  • Page.xaml: Contains some of the markup for the ImageRotator control, the rest of which is created dynamically as the control is loaded. Here, however, you can see the image border which wraps the entire image. Also, the imageStackPanel which will have image controls dynamically added to it during runtime (1 per image). Finally, the navigationStackPanel can also be seen here which contains polygon (arrow) buttons, and the numberedItemsStackPanel which is a container for the numbered buttons which are also added at runtime.
  • <UserControl x:Class="ImageRotator_CSharp.Page"
      xmlns=http://schemas.microsoft.com/winfx/2006/xaml/presentation 
      xmlns:x=http://schemas.microsoft.com/winfx/2006/xaml 
      xmlns :local="clr-namespace:ImageRotator_CSharp" 
       x:Name="imageRotatorControl">
    
        <Border x:Name="imageBorder" Cursor="Hand">
            <Canvas x:Name="layoutRoot" Background="White" VerticalAlignment="Stretch" 
                HorizontalAlignment="Stretch" Margin="0">
                <StackPanel x:Name="imageStackPanel" />
                <Border x:Name="navigationBorder" Background="White" Opacity=".5" 
                    CornerRadius="10" BorderBrush="White" BorderThickness="0" 
                        Canvas.Left="10"
                    Canvas.Top="325">
                    <StackPanel x:Name="navigationStackPanel" Orientation="Horizontal"
                        Margin="2,1,2,1" > 
                        <Polygon x:Name="arrowLeft" 
                            MouseLeftButtonDown="LeftArrow_MouseLeftButtonDown"
                            MouseEnter="Arrow_MouseEnter" 
                            MouseLeave="Arrow_MouseLeave"
                            Points="0,6,10,0,10,12" 
                            Fill="Black" 
                            VerticalAlignment="Center" Margin="2,2,8,2" /> 
                        <StackPanel x:Name="numberedItemsStackPanel" 
                            Orientation="Horizontal" />
                        <Polygon x:Name="arrowRight"
                            MouseLeftButtonDown="RightArrow_MouseLeftButtonDown"
                             MouseEnter="Arrow_MouseEnter" 
                             MouseLeave="Arrow_MouseLeave" 
                             Points="0,0,10,6,0,12" 
                             Fill="Black" 
                             VerticalAlignment="Center" Margin="2" />
                    </StackPanel>
                </Border>
            </Canvas>
        </Border>
    
    </UserControl>
  • Page.xml.cs: This is where all the events, event handlers, and methods are contained that bring the ImageRotator and its functionality to life. Again, I won't cover all the methods here, but feel free to download and review this code yourself. I've tried to add good comments to everything; however, if you have any questions, feel free to contact me. I will cover those areas which caused the biggest hurdles during the development of this application.

Changing images without a rebuild

When I initially built the application, I placed the images in an image directory inside the ImageRotator Silverlight library. This worked fine except for one thing. When the application is called, this library is compiled into a .xap file and placed in the website's ClientBin folder. This includes everything in that project, even the images. Through a little research, however, I discovered the true magic of the website's ClientBin folder. The bottom line is, files can be stored in this directory and accessed at runtime by the Silverlight control. So, I created an images subdirectory, and placed a few of the images I wanted to display in this directory. I also placed the SlideShowImages.xml file here. This file contains the images you want as part of your deployment along with the properties of each.

<?xml version="1.0" encoding="utf-8"?>
<!--
ImageUri: Absolute or Relative path and name of image.
RedirectLink (optional): Uri to redirect to if image is clicked.
Order (optional): Image order expression.
-->

<PictureAlbum ImageWidth="480" ImageHeight="360"> 
    <SlideShowImage> 
        <ImageUri>../Images/a.jpg</ImageUri>      
        <RedirectLink>http://www.tasteofstepford.com</RedirectLink> 
        <Order>1</Order> 
        <DisplayImage>False</DisplayImage> 
    </SlideShowImage> 
    <SlideShowImage> 
        <ImageUri>../Images/b.jpg</ImageUri>
        <RedirectLink>http://www.tasteofstepford.com</RedirectLink> 
        <Order>2</Order> 
        <DisplayImage>True</DisplayImage> 
    </SlideShowImage> 
    <SlideShowImage> 
        <ImageUri>../Images/c.jpg</ImageUri>   
        <RedirectLink>http://www.tasteofstepford.com</RedirectLink> 
        <Order>3</Order> 
        <DisplayImage>True</DisplayImage> 
    </SlideShowImage> 
</PictureAlbum>

Passing configuration/setup values from the website into the Silverlight application

Step #1: The first step is actually passing the name/value pairs from the website. This can be accomplished in several different ways. The way I passed them from my ImageRotatorTestPage.aspx page is through the code-behind. I built a string of name value pairs (get the initial load values from web.config and refresh the values from the page controls). I then set the InitParams string into the ImageRotator instance's InitParamaters property.

protected void refresh_Click(object sender, EventArgs e)
{
    string browserWindowOptions = "resizable=1|scrollbars=1|menubar=1|status=1|"
    browserWindowOptions += "toolbar=1|titlebar=1|width=900|height=725|left=5|top=5";

    string initParams = "autoPlay=" + autoPlay.Checked.ToString();
    initParams += ",autoPlayInterval=" + autoPlayInterval.Text;
    initParams += ",numberedNavigation=" + numberedNavigation.Checked.ToString();
    initParams += ",arrowNavigation=" + arrowNavigation.Checked.ToString();
    initParams += ",stopStartAutoPlay=" + stopStartAutoPlay.Checked.ToString();
    initParams += ",animation=" + animation.Checked.ToString();
    initParams += ",border=" + border.Checked.ToString();
    initParams += ",borderThickness=" + borderThickness.SelectedValue;
    initParams += ",argb=" + a.Text + "|" + r.Text + 
                  "|" + g.Text + "|" + b.Text;
    initParams += ",imageRotatorDiv=" + imageRotatorDiv;
    initParams += ",browserWindowOptions=" + browserWindowOptions;
    imageRotator.InitParameters = initParams;
}

Note that two of InitParams' name/value pairs contain a pipe delimited list. I had initially used commas to delimit, but this messed up the delimiters of initParams. To get around this, I encoded the commas. However, I found this hard to read and maintain. In the end, I decided to use a pipe as a delimiter. Since the ImageRotatorTestPage.html has no code-behind, I had to find another method for passing InitParams from it. As it turns out, this can be accomplished with the param tag having its name attribute set to initParams and its value attribute set to a comma delimited list of attribute=value pairs.

<object data="data:application/x-silverlight-2," 
        type="application/x-silverlight-2" width="100%" height="100%">
    <param name="source" value="ClientBin/ImageRotator_CSharp.xap"/>
    <param name="onerror" value="onSilverlightError" />
    <param name="background" value="white" />
    <param name="minRuntimeVersion" value="2.0.31005.0" />
    <param name="autoUpgrade" value="true" />

    <!-- Begin: initParams for ImageRotator -->
    <param name="initParams" 
        value="autoPlay=true,
        autoPlayInterval=7,
        numberedNavigation=true,
        arrowNavigation=true,
        stopStartAutoPlay=true,
        animation=true,
        border=true,
        borderThickness=2,
        imageRotatorDiv=silverlightControlHost,
        browserWindowOptions=resizable=1|scrollbars=1|menubar=1|
            status=1|toolbar=1|titlebar=1|width=1000|height=725|left=5|top=5,
            argb=255|0|0|0" /> 
    <!-- End: initParams for ImageRotator -->

    <a href=http://go.microsoft.com/fwlink/?LinkID=124807 style="text-decoration: none;">
        <img src=http://go.microsoft.com/fwlink/?LinkId=108181 
          alt="Get Silverlight" style="border-style: none"/>
    </a>
</object>

Step #2: The parameters must be passed into the Silverlight page (Page.xaml). This is not done directly, and it is not setup to pass InitParams by default. They must be passed into the application class (App.xaml.cs) first, which then passes them on to whichever user control you want (Page.xaml.cs, in my case). More specifically, every time the Silverlight application is loaded (initially, or by page refresh), the App.xaml.cs default constructor (App()) is called. This wires up the Application_Startup event which is then called. By default, this method simply calls the default constructor inside Page.xaml.cs. What we need it to do is to call an overloaded constructor (which we will need to create) that takes in the InitParams as an argument. Here's a look at the updated Application_Startup method calling the overloaded Page class constructor.

private void Application_Startup(object sender, StartupEventArgs e)
{
    if (e.InitParams.Count == 11)
        this.RootVisual = new Page(e.InitParams);
    else
    {
        string exc = "Must pass the required InitParams " + 
                     "to the Page.xaml.cs constructor"
        throw new Exception(exc);
    }
}

Step #3: Here is a look at the overloaded constructor which accepts the InitParams and places the values into some private member variables for later use.

public Page(IDictionary<string, > parameters)
{
    InitializeComponent();

     // retrieve the initParams, convert back any ASCII characters 
     foreach (var item in parameters)
     {
         switch (item.Key)
         {
             case "autoPlay":
                 enableAutoPlay = Convert.ToBoolean(item.Value);
                 break;
             case "autoPlayInterval":
                 autoPlayIntervalSeconds = Convert.ToInt32(item.Value);
                 break;
             case "numberedNavigation":
                 displayNumberedButtons = Convert.ToBoolean(item.Value);
                 break;
             case "arrowNavigation":
                 displayArrows = Convert.ToBoolean(item.Value);
                 break;
             case "stopStartAutoPlay":
                 buttonsStopStartAutoPlay = Convert.ToBoolean(item.Value);
                 break;
             case "animation":
                 enableAnimations = Convert.ToBoolean(item.Value);
                 break;
             case "border":
                 displayImageBorder = Convert.ToBoolean(item.Value);
                 break;
             case "borderThickness":
                 imageBorderThickness = Convert.ToInt32(item.Value);
                 break;
             case "argb":
                 argbBorderColor = item.Value.ToString().Replace("|", ",");
                 break;
             case "imageRotatorDiv":
                 imageRotatorDiv = item.Value.ToString();
                 break;
             case "browserWindowOptions":
                 browserWindowOptions = item.Value.ToString().Replace("|", ",");
                 break;
        }
    }
    LoadPictureAlbum();
}

Asynchronously load the XML file from the website into the instance of the PictureAlbum class and notify the custom event listener when completed

There are two important things to note here. First, we want to asynchronously go to the website and get the SlideShowImages.xml file. This XML file contains a <SlideShowImage> element for each picture. Each SlideShowImage element contains several elements which identify attributes of that image. I chose to use elements for these instead of attributes because I want this application to be easy to use for non-developers as well, and I feel elements are easier to read than attributes for them (and me for that matter). We need this in order to load up the PictureAlbum class which contains a collection SlideShowImages used to drive the ImageRotator. We also need to let the ImageRotator know this process is done so that it can then use the SlideShowImages instance to dynamically create the images and buttons on the control itself.

This was probably were I spent the most of my time. My first thought was to use a wait-handle of some sort to block the calling thread from my Page.xaml.cs class until the XML file was loaded, then signal that it had completed so the image controls could be created. Depending on how I created a new thread, and using wait-handles, I came across different problems. Either the wait-handle would not appear to block, or it would get to the blocking control and spin forever without ever reaching my callback method to signal. Trust me when I say, I tried everything. Then, I did a little research (which I should have done much earlier), and discovered the problem. There is no issue with using WaitHandles or CallBackMethods or ManualResetEvent's with Silverlight. However, you cannot block the main UI thread in a Silverlight application. My alternative approach, which worked with little effort, was to create a custom event that gets fired when I'm done loading the XML file.

First, in the PictureAlbum class, I created a delegate for my asynchronous call, and a custom event to be raised when it was completed.

public delegate void PictureAlbumLoaded(object sender, PictureAlbumLoadedEventArgs a);
public event PictureAlbumLoaded pictureAlbumLoaded;

Next, I created a method (LoadPictureAlbumXMLFile) which gets called by the page class during its construction. This method creates an instance of WebClient which we will use to get the XML file across the wire via its DownloadStringAsync method. We will also wire up to the WebClient instance's DownloadStringCompleted event. This method takes a callback method as a parameter which I have given it (XMLFileLoaded). Once the WebClient has completed its asynchronous call to get the file, it will fire its DownloadStringCompleted event. In our case, this will cause XMLFileLoaded to be called. This method takes the XML file which has been returned as a string, and parses it into an XDocument. We then use a little LINQ to XML to get the image details out and into the PictureAlbum's collection of images. Finally, the pictureAlbumLoaded event is fired to notify any listeners of the success or failure of this method call.

public void LoadPictureAlbumXMLFile()
{
    WebClient xmlClient = new WebClient();
    xmlClient.DownloadStringCompleted += 
        new DownloadStringCompletedEventHandler(XMLFileLoaded);
    xmlClient.DownloadStringAsync(new Uri("SlideShowImages.xml", 
        UriKind.RelativeOrAbsolute));
}

private void XMLFileLoaded(object sender, DownloadStringCompletedEventArgs e)
{
    if (e.Error == null)
    {
        Int32 buttonNumber = 0;
        slideShowImages = new List<SlideShowImage>();
        PictureAlbumLoadedEventArgs pictureAlbumLoadedCompletedSuccessfully = null;

        try
        {
            xdoc = XDocument.Parse(e.Result);

            
            imageWidth = Convert.ToDouble(xdoc.Element("PictureAlbum")
                .Attribute("ImageWidth").Value);

            imageHeight = Convert.ToDouble(xdoc.Element("PictureAlbum")
                .Attribute("ImageHeight").Value);

               foreach (XElement item in xdoc.Elements("PictureAlbum")
                .Elements("SlideShowImage"))
               {
                   if (item.Element("DisplayImage").Value.ToLower() == "true")
                   {
                       SlideShowImage slideShowImage = new SlideShowImage();

                       if (item.Element("ImageUri").Value.Contains("http"))
                           slideShowImage.ImageUri = 
                               new Uri(item.Element("ImageUri").Value, UriKind.Absolute);
                       else
                       {
                           slideShowImage.ImageUri = 
                               new Uri(item.Element("ImageUri").Value, UriKind.Relative);
                       }

                       slideShowImage.RedirectLink = item.Element("RedirectLink").Value;
                       slideShowImage.Order = Convert.ToInt32(item.Element("Order").Value);
                       slideShowImage.ButtonNumber = buttonNumber++;
                       slideShowImages.Add(slideShowImage);
                   }
            }

            pictureAlbumLoadedCompletedSuccessfully = new PictureAlbumLoadedEventArgs(true);
        }
        catch
        {
            pictureAlbumLoadedCompletedSuccessfully = 
                new PictureAlbumLoadedEventArgs(false);
        }
        finally
        {
            // Fire off pictureAlbumLoadedHandler so the listeners can respond.
            pictureAlbumLoaded(this, pictureAlbumLoadedCompletedSuccessfully); 
        }                
    }
}

The method that calls all of this (LoadPictureAlbum) is in the Page class, and it also has some asynchronous logic. It first wires up a listener to the PictureAlbumLoaded event, passing in the PictureAlbumLoaded method as the callback. It then calls LoadPictureAlbumXMLFile, which, as we saw previously, will raise the PictureAlbumLoaded listener. Once this listener is called, PictureAlbumLoaded is called. It first checks the PictureAlbumLoadedEventArgs to verify the load was successful, and if not, throws an exception. If the load was successful, it continues on to dynamically generate an Image control for each image in the collection.

private void LoadPictureAlbum()
{
    pictureAlbum.pictureAlbumLoaded += 
        new PictureAlbum.PictureAlbumLoaded(PictureAlbumLoaded);
            
    pictureAlbum.LoadPictureAlbumXMLFile();
}

private void PictureAlbumLoaded(object sender, 
             PictureAlbumLoadedEventArgs args)
{
    try
    {
        if (args.PictureAlbumLoadedSuccessfully)
        {
            imageWidth = pictureAlbum.ImageWidth;
            imageHeight = pictureAlbum.ImageHeight;

            pictures = pictureAlbum.SlideShowImages;
            imageCount = pictures.Count;

            // create first image 
            AddNewImage(0, Visibility.Visible);

            imagesToLoadCount = imageCount - 1;

            // create rest of images asynchronously 
            for (int i = 1; (i < pictures.Count); i++)
                ThreadPool.QueueUserWorkItem(ProcessSlideShowImage, i.ToString());
        }
        else
        {
            string exc = "The SlideShowImages.xml file failed to load the "
            exc += "PictureAlbum class successfully."
            throw new Exception(exc);
        }
    }
    finally
    {
        pictureAlbum.pictureAlbumLoaded -= 
            new PictureAlbum.PictureAlbumLoaded(PictureAlbumLoaded);
    }
}

Fading out an old image and fading in a new image

This one wasn't too bad, but it did take a little trial and error. Basically, I created a method with animation (DoubleAnimation) to fade out the current Image control. Inside that method, I wired up the storyboard's Completed event. When the storyboard animation completes, the image will have faded, and the event will be raised. This event is tied to a callback method which then swaps the old Image control with the new one and then calls FadeIn. FadeIn is another animation (DoubleAnimation) which, as its name implies, fades in the new Image control.

private void FadeOut()
{
    DoubleAnimation fadeOutDoubleAnimation = new DoubleAnimation();

    fadeOutDoubleAnimation.From = 1;
    fadeOutDoubleAnimation.To = 0;
    fadeOutDoubleAnimation.Duration = new Duration(TimeSpan.FromSeconds(1));

    fadeOutStoryboard = new Storyboard();
    fadeOutStoryboard.Children.Add(fadeOutDoubleAnimation);
    fadeOutStoryboard.Completed += new EventHandler(FadeOutStoryboard_Completed);

    Storyboard.SetTarget(fadeOutDoubleAnimation, 
        ((Image)imageStackPanel.Children.ElementAt(lastImageIndex)));

    Storyboard.SetTargetProperty(fadeOutDoubleAnimation, 
        new PropertyPath(ListBox.OpacityProperty));

    layoutRoot.Resources.Add("fadeOutStoryboard", fadeOutStoryboard);

    fadeOutStoryboard.Begin();

    layoutRoot.Resources.Remove("fadeOutStoryboard");
}


private void FadeOutStoryboard_Completed(object sender, EventArgs e)
{
    ((Image)imageStackPanel.Children.ElementAt(lastImageIndex)).Visibility = 
        Visibility.Collapsed;

    ((Image)imageStackPanel.Children.ElementAt(nextImageIndex)).Visibility = 
        Visibility.Visible;

    lastImageIndex = nextImageIndex;

    FadeIn();

    if ((enableAutoPlay || buttonsStopStartAutoPlay) && leavePaused == false)
        autoPlayTimer.Start();
}

private void FadeIn()
{
    DoubleAnimation fadeInDoubleAnimation = new DoubleAnimation();

    fadeInDoubleAnimation.From = 0;
    fadeInDoubleAnimation.To = 1;
    fadeInDoubleAnimation.Duration = new Duration(TimeSpan.FromSeconds(1));

    fadeInStoryboard = new Storyboard();
    fadeInStoryboard.Children.Add(fadeInDoubleAnimation);

    Storyboard.SetTarget(fadeInDoubleAnimation, 
       ((Image)imageStackPanel.Children.ElementAt(nextImageIndex)));

    Storyboard.SetTargetProperty(fadeInDoubleAnimation, 
               new PropertyPath(ListBox.OpacityProperty));

    layoutRoot.Resources.Add("fadeInStoryboard", fadeInStoryboard);

    fadeInStoryboard.Begin();

    layoutRoot.Resources.Remove("fadeInStoryboard");
}

History

  • February 23, 2009 - Added source code for VB.NET.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here