Experiments in Building a Silverlight Photo Control ( Part 1 )

On web pages, you often see a control that displays a set of pictures with a list of the total number of pictures and a next/previous button to move between them.

MSN has one that looks like this;

image

and I kind of fancied using this as a means to play with a Silverlight control that did the same thing. What interests me about it is that my control clearly needs to;

  1. Display an image
  2. Display a Next and Previous button
  3. Display a “M of N” piece of text

but it doesn’t really need to know very much about how to display the image and so on. It can (hopefully) be nicely templated to avoid that kind of knowledge.

That’s what I wanted to play with.

Now, naturally, you could build a control like this so that it dynamically loads which images to display via some web service or maybe via a plug-in interface ( IProvidePictures? ) that you configure at the time of use but I wanted to keep it simple so I figure that ( in pseudo code ) I’d like to see the XAML for my control look something like;

<PhotoControl>

    <PhotoControl.Images>

        <PhotoImage Url=”http://wherever.com/1.jpg”/&gt;

        <PhotoImage Url=”http://wherever.com/2.jpg”/&gt;

    </PhotoControl.Images>

</PhotoControl>

or something along those lines anyway.

So, I got started! Firstly, I whipped out Powerpoint and sketched out how I thought my control might look using the ugliest colours that I could find;

image

I figure there are at least 5 distinct parts to this control ( and possibly more );

  1. The root element
  2. A previous element where a button might make sense
  3. A next element where a button might make sense
  4. A text element for the “Image 2 of 8” text
  5. An image element for the image itself

and I really don’t want to hardwire the look and feel of this thing, I just want to provide a default and leave the rest to be templated.

Now…this is the first time I’ve built a control that’s not a user control so I’m just feeling my way along but the first thing I did was to;

  1. Create myself a new Silverlight project in Visual Studio ( I called this PhotoControlProject )
  2. Allow VS to create a test project for me ( I called this PhotoControlProjectWeb )
  3. Create myself a new library project ( called ControlLibrary ) to put my control into and make sure that my Silverlight project references my library project (

The next thing that I did was to define my control and the various parts that make it up;

  [TemplatePart(Name = PhotoControl.PreviousButtonElement, Type = typeof(Button))]
  [TemplatePart(Name = PhotoControl.NextButtonElement, Type = typeof(Button))]
  [TemplatePart(Name = PhotoControl.ImageElement, Type = typeof(Image))]
  [TemplatePart(Name = PhotoControl.TextElement, Type = typeof(TextBlock))]
  [TemplatePart(Name = PhotoControl.LoadingElement, Type = typeof(FrameworkElement))]
  [TemplatePart(Name = PhotoControl.RootElement, Type = typeof(Panel))]
  public class PhotoControl : Control 
  {
    public PhotoControl()
    {
      DefaultStyleKey = typeof(PhotoControl);
    }

    public const string RootElement = "RootElement";
    public const string PreviousButtonElement = "PreviousButtonElement";
    public const string NextButtonElement = "NextButtonElement";
    public const string TextElement = "TextElement";
    public const string ImageElement = "ImageElement";
    public const string LoadingElement = "LoadingElement";
  }

I added in another element, a loading element that might be used to display some kind of “busy” status when the photo is loading pictures but, other than that, I’ve defined parts for the previous and next buttons, the image, the text and the root element. I’ve been a little arbitrary in deciding (for instance) that the root element has to be a Panel but it doesn’t seem so unreasonable to me.

Now, I need to make sure that I can gain access to these various items when my control is in use so I override the OnApplyTemplate method and define some member variables as in adding all these variables to my PhotoControl class;

    Button prevButton;
    Button nextButton;
    FrameworkElement rootElement;
    TextBlock textElement;
    Image imageElement;
    FrameworkElement loadingElement;

and then overriding OnApplyTemplate;

    public override void OnApplyTemplate()
    {
      base.OnApplyTemplate();

      prevButton = GetTemplateChild(PreviousButtonElement) as Button;
      nextButton = GetTemplateChild(NextButtonElement) as Button;
      rootElement = GetTemplateChild(RootElement) as FrameworkElement;
      textElement = GetTemplateChild(TextElement) as TextBlock;
      imageElement = GetTemplateChild(ImageElement) as Image;
      loadingElement = GetTemplateChild(LoadingElement) as FrameworkElement;
    }

note that I can end up with NULLs here if someone hasn’t (for instance) provided a TextElement as part of their template but I guess that’s ok.

Speaking of templates, I need to provide my own default one so I added a generic.xaml to my library project, set its build action;

image

and then added a bit of markup to represent the default view of my control. With a little help from Expression Blend 2.5 July Preview, I defined this as generic.xaml where the notable features are ( I guess ) that I’ve named the various parts to line up with the named parts that I defined in my code and that I’ve also restyled my buttons slightly and made sure that they have a disabled state and I’ve bound their IsEnabled value to properties called HasPreviousImage and HasNextImage.

<ResourceDictionary
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:vsm="clr-namespace:System.Windows;assembly=System.Windows"
  xmlns:local="clr-namespace:ControlLibrary">
  <ControlTemplate
    x:Key="PrevButtonTemplate"
    TargetType="Button">
    <Grid Height="60" Width="40">
      <vsm:VisualStateManager.VisualStateGroups>
        <vsm:VisualStateGroup
          x:Name="FocusStates">
          <vsm:VisualState
            x:Name="Unfocused" />
          <vsm:VisualState
            x:Name="Focused" />
        </vsm:VisualStateGroup>
        <vsm:VisualStateGroup
          x:Name="CommonStates">
          <vsm:VisualState
            x:Name="MouseOver" />
          <vsm:VisualState
            x:Name="Pressed" />
          <vsm:VisualState
            x:Name="Disabled">
            <Storyboard>
              <ColorAnimationUsingKeyFrames
                BeginTime="00:00:00"
                Duration="00:00:00.0010000"
                Storyboard.TargetName="path"
                Storyboard.TargetProperty="(Shape.Fill).(GradientBrush.GradientStops)[1].(GradientStop.Color)">
                <SplineColorKeyFrame
                  KeyTime="00:00:00"
                  Value="#4CFFFFFF" />
              </ColorAnimationUsingKeyFrames>
            </Storyboard>
          </vsm:VisualState>
          <vsm:VisualState
            x:Name="Normal" />
        </vsm:VisualStateGroup>
      </vsm:VisualStateManager.VisualStateGroups>
      <Path
        HorizontalAlignment="Stretch"
        Margin="6.65399980545044,4.03599977493286,16.55299949646,2.15899991989136"
        VerticalAlignment="Stretch"
        Stretch="Fill"
        Stroke="#FF000000"
        Data="M8.2305737,25.519167 L37.023209,-1.9249554 L36.216152,52.161915"
        x:Name="path">
        <Path.Fill>
          <LinearGradientBrush
            EndPoint="0.5,1"
            StartPoint="0.5,0">
            <GradientStop
              Color="#FF000000" />
            <GradientStop
              Color="#FFFFFFFF"
              Offset="1" />
          </LinearGradientBrush>
        </Path.Fill>
      </Path>
    </Grid>
  </ControlTemplate>
  <ControlTemplate
    x:Key="NextButtonTemplate"
    TargetType="Button">
    <Grid Height="60" Width="40">
      <vsm:VisualStateManager.VisualStateGroups>
        <vsm:VisualStateGroup
          x:Name="FocusStates">
          <vsm:VisualState
            x:Name="Unfocused" />
          <vsm:VisualState
            x:Name="Focused" />
        </vsm:VisualStateGroup>
        <vsm:VisualStateGroup
          x:Name="CommonStates">
          <vsm:VisualState
            x:Name="MouseOver">
            <Storyboard />
          </vsm:VisualState>
          <vsm:VisualState
            x:Name="Pressed">
            <Storyboard />
          </vsm:VisualState>
          <vsm:VisualState
            x:Name="Disabled">
            <Storyboard>
              <ColorAnimationUsingKeyFrames
                BeginTime="00:00:00"
                Duration="00:00:00.0010000"
                Storyboard.TargetName="path"
                Storyboard.TargetProperty="(Shape.Fill).(GradientBrush.GradientStops)[1].(GradientStop.Color)">
                <SplineColorKeyFrame
                  KeyTime="00:00:00"
                  Value="#4CFFFFFF" />
              </ColorAnimationUsingKeyFrames>
            </Storyboard>
          </vsm:VisualState>
          <vsm:VisualState
            x:Name="Normal">
            <Storyboard />
          </vsm:VisualState>
        </vsm:VisualStateGroup>
      </vsm:VisualStateManager.VisualStateGroups>
      <Path
        HorizontalAlignment="Stretch"
        Margin="6.65399980545044,4.03599977493286,16.55299949646,4"
        VerticalAlignment="Stretch"
        Stretch="Fill"
        Stroke="#FF000000"
        Data="M8.2305737,25.519167 L37.023209,-1.9249554 L36.216152,52.161915"
        RenderTransformOrigin="0.5,0.5"
        x:Name="path">
        <Path.RenderTransform>
          <TransformGroup>
            <ScaleTransform />
            <SkewTransform />
            <RotateTransform
              Angle="180" />
            <TranslateTransform />
          </TransformGroup>
        </Path.RenderTransform>
        <Path.Fill>
          <LinearGradientBrush
            EndPoint="0.5,1"
            StartPoint="0.5,0">
            <GradientStop
              Color="#FF000000" />
            <GradientStop
              Color="#FFFFFFFF"
              Offset="1" />
          </LinearGradientBrush>
        </Path.Fill>
      </Path>
    </Grid>
  </ControlTemplate>
  <Style
    TargetType="local:PhotoControl">
    <Setter
      Property="Template">
      <Setter.Value>
        <ControlTemplate
          TargetType="local:PhotoControl">
            <Grid
              x:Name="RootElement"
              Background="Black">
              <Border
                Margin="3"
                BorderBrush="Silver"
                CornerRadius="2"
                BorderThickness="2">
                <Grid>
                  <Grid.ColumnDefinitions>
                    <ColumnDefinition
                      Width="*" />
                    <ColumnDefinition
                      Width="8*" />
                    <ColumnDefinition
                      Width="*" />
                  </Grid.ColumnDefinitions>
                  <Grid.RowDefinitions>
                    <RowDefinition
                      Height="*" />
                    <RowDefinition
                      Height="Auto" />
                  </Grid.RowDefinitions>
                  <Rectangle
                    x:Name="LoadingElement"
                    Grid.ColumnSpan="3"
                    Opacity="0"
                    Fill="Gray" />
                  <Button
                    Grid.Column="0"
                    x:Name="PreviousButtonElement"
                    Content="P"
                    VerticalAlignment="Center"
                    Margin="5,0,5,0"
                    IsEnabled="{Binding HasPreviousImage}"
                    Template="{StaticResource PrevButtonTemplate}" />
                  <Border
                    Margin="3"
                    BorderBrush="Silver"
                    CornerRadius="2"
                    BorderThickness="2"
                    Grid.Column="1">
                    <Image
                      x:Name="ImageElement"
                      Margin="5" />
                  </Border>
                  <Button
                    Grid.Column="2"
                    x:Name="NextButtonElement"
                    Content="N"
                    VerticalAlignment="Center"
                    Margin="5,0,5,0"
                    IsEnabled="{Binding HasNextImage}"
                    Template="{StaticResource NextButtonTemplate}" />
                  <TextBlock
                    Grid.Row="1"
                    Grid.Column="1"
                    Foreground="White"
                    x:Name="TextElement"
                    Text="Not Set"
                    TextAlignment="Center"
                    VerticalAlignment="Center" />
                </Grid>
              </Border>
            </Grid>
        </ControlTemplate>
      </Setter.Value>
    </Setter>
  </Style>
</ResourceDictionary>

Ok, I now have a default look for my control and so if I now instantiate one of these from my Silverlight application’s page.xaml as in;

<UserControl x:Class="PhotoControlProject.Page"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:lib="clr-namespace:ControlLibrary;assembly=ControlLibrary"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" >
  <Grid x:Name="LayoutRoot"
        Background="White">
    <lib:PhotoControl>
    </lib:PhotoControl>
  </Grid>
</UserControl>

then I get the start of my UI;

image

Now, I need some images. I defined a little class;

 public class PhotoSource
  {
    public string Source { get; set; }
  }

Now, that string should really be a Uri but I can’t get that to work in XAML – not sure why but I added a property to my PhotoControl to contain a list of those PhotoSource classes and called it Photos;

    public List<PhotoSource> Photos 
    {
      get
      {
        return (photos);
      }
      set
      {
        photos = value;
      }
    }

and added a private member variable behind that;

List<PhotoSource> photos;

and initialised it in my constructor;

    public PhotoControl()
    {
      DefaultStyleKey = typeof(PhotoControl);
      photos = new List<PhotoSource>();
    }

so now I can write some XAML that looks like;

<UserControl x:Class="PhotoControlProject.Page"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:lib="clr-namespace:ControlLibrary;assembly=ControlLibrary"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" >
  <Grid x:Name="LayoutRoot"
        Background="White">
    <lib:PhotoControl>
      <lib:PhotoControl.Photos>
        <lib:PhotoSource
          Source="images/img1.jpg"/>
        <lib:PhotoSource
          Source="images/img2.jpg" />
        <lib:PhotoSource
          Source="images/img3.jpg" />
        <lib:PhotoSource
          Source="images/img4.jpg" />
        <lib:PhotoSource
          Source="images/img5.jpg" />
      </lib:PhotoControl.Photos>
    </lib:PhotoControl>
  </Grid>
</UserControl>

But my control still isn’t going to actually display those images just yet.

I want to asynchronously load the images and I want to try and make sure that if you’re looking at image N then I’ll already have set about loading image N+1 to avoid you having to wait when you click the “Next” button. I wrote this little class to help with this;

  public class ImageLoader : INotifyPropertyChanged
  {
    public static DependencyProperty HasPreviousProperty =
      DependencyProperty.Register("HasPrevious", typeof(bool), typeof(ImageLoader), null);

    public static DependencyProperty HasNextProperty =
      DependencyProperty.Register("HasNext", typeof(bool), typeof(ImageLoader), null);

    public ImageLoader(List<PhotoSource> photos) 
    {
      this.photos = photos;
      imageMap = new Dictionary<int, MemoryStream>();
    }
    public void Initialise()
    {
      LoadImages();
    }
    public void MoveToNextImage()
    {
      if (HasNextImage)
      {
        currentIndex++;
        LoadImages();
        FirePropertyChanged("HasNextImage");
        FirePropertyChanged("HasPreviousImage");
      }
    }
    public void MoveToPreviousImage()
    {
      if (HasPreviousImage)
      {
        currentIndex--;
        LoadImages();
        FirePropertyChanged("HasNextImage");
        FirePropertyChanged("HasPreviousImage");
      }
    }
    public int CurrentIndex
    {
      get
      {
        return (currentIndex);
      }
    }
    public bool HasNextImage
    {
      get
      {
        return (currentIndex < photos.Count - 1);
      }
    }
    public bool HasPreviousImage
    {
      get
      {
        return (currentIndex > 0);
      }
    }
    private MemoryStream CheckImageLoaded(int i)
    {
      MemoryStream ms = null;

      lock (imageMap)
      {
        if (imageMap.ContainsKey(i))
        {
          ms = imageMap[i];
        }
      }
      return (ms);
    }
    private void LoadImages()
    {
      int minIndex = Math.Max(0, currentIndex - 1);
      int maxIndex = Math.Min(photos.Count - 1, currentIndex + 1);

      for (int i = minIndex; i <= maxIndex; i++)
      {
        LoadImage(i);
      }
    }
    private void LoadImage(int i)
    {
      MemoryStream ms = CheckImageLoaded(i);

      if (ms == null)
      {
        WebClient client = new WebClient();

        client.OpenReadCompleted += (s, e) =>
          {
            MemoryStream readMs = new MemoryStream();
            e.Result.WriteTo(readMs);
            readMs.Seek(0, SeekOrigin.Begin);

            int index = (int)e.UserState;

            lock (imageMap)
            {
              imageMap[index] = readMs;
            }

            if (i == currentIndex)
            {
              FireImageAvailable(readMs);
            }
          };

        client.OpenReadAsync(new Uri(photos[i].Source, UriKind.RelativeOrAbsolute), i);
      }
      else if (i == currentIndex)
      {
        FireImageAvailable(ms);
      }
    }
    private void FireImageAvailable(Stream stream)
    {
      if (ImageStreamAvailable != null)
      {
        ImageStreamAvailable(this,
          new StreamEventArgs() { Stream = stream });
      }
    }
    private void FirePropertyChanged(string property)
    {
      if (PropertyChanged != null)
      {
        PropertyChanged(this,
          new PropertyChangedEventArgs(property));
      }
    }
    public event PropertyChangedEventHandler PropertyChanged;

    Dictionary<int, MemoryStream> imageMap;
    public event EventHandler<StreamEventArgs> ImageStreamAvailable;
    private int currentIndex;
    private List<PhotoSource> photos;
  }

So, what’s that class all about? You construct it with a List<PhotoSource> representing the images and then you call Initialise(). It will then go and try to load up to images 0,1 and it’ll fire an event when it loads image 0 because that’s the current image. The event relies on;

  public class StreamEventArgs : EventArgs
  {
    public Stream Stream { get; set; }
  }

Why am I passing a Stream here? It’s really my way of trying to avoid creating something like an Image on one thread and then passing it to another. So, I pass a Stream instead.

When the caller wants to move to the next image they can call MoveToNextImage which will cause image 2 to be proactively loaded but will spot that image 1 is already loaded and will fire the event straight away for image 1. It’s a similar situation for moving to previous images.

There’s some race conditions in the code I’ve written here but it’ll “mostly do” for what I want. I also wrote a couple of little extension methods so that I can write a stream to another stream. MemoryStream already has this (IIRC) but it’s not part of Stream itself as far as I know;

public static class StreamExtensions
{
  public static void WriteTo(this Stream inStream,
    Stream outStream)
  {
    byte[] buffer = new byte[4096];
    int bytesRead = 0;

    while ((bytesRead = inStream.Read(buffer, 0, buffer.Length)) > 0)
    {
      outStream.Write(buffer, 0, bytesRead);
    }
    outStream.Flush();

    inStream.ResetToBeginning();
  }
  public static void ResetToBeginning(this Stream stream)
  {
    if (stream.CanSeek)
    {
      stream.Seek(0, SeekOrigin.Begin);
    }
  }
}

Ok, with that code written I can now flesh out my PhotoControl class a little to use this new ImageLoader class;

namespace ControlLibrary
{
  public class PhotoSource
  {
    public string Source { get; set; }
  }

  [TemplatePart(Name = PhotoControl.PreviousButtonElement, Type = typeof(Button))]
  [TemplatePart(Name = PhotoControl.NextButtonElement, Type = typeof(Button))]
  [TemplatePart(Name = PhotoControl.ImageElement, Type = typeof(Image))]
  [TemplatePart(Name = PhotoControl.TextElement, Type = typeof(TextBlock))]
  [TemplatePart(Name = PhotoControl.LoadingElement, Type = typeof(FrameworkElement))]
  [TemplatePart(Name = PhotoControl.RootElement, Type = typeof(Panel))]
  public class PhotoControl : Control 
  {
    public PhotoControl()
    {
      DefaultStyleKey = typeof(PhotoControl);
      photos = new List<PhotoSource>();
      this.Loaded += OnLoaded;
    }

    void OnLoaded(object sender, RoutedEventArgs e)
    {
      imageLoader = new ImageLoader(photos);
      imageLoader.ImageStreamAvailable += OnImageAvailable;
      imageLoader.Initialise();
    }
    void OnImageAvailable(object sender, StreamEventArgs e)
    {
      Dispatcher.BeginInvoke(() =>
        {
          BitmapImage bi = new BitmapImage();
          bi.SetSource(e.Stream);
          imageElement.Source = bi;
        });
    }
    public List<PhotoSource> Photos 
    {
      get
      {
        return (photos);
      }
      set
      {
        photos = value;
      }
    }    
    public override void OnApplyTemplate()
    {
      base.OnApplyTemplate();

      prevButton = GetTemplateChild(PreviousButtonElement) as Button;
      nextButton = GetTemplateChild(NextButtonElement) as Button;
      rootElement = GetTemplateChild(RootElement) as FrameworkElement;
      textElement = GetTemplateChild(TextElement) as TextBlock;
      imageElement = GetTemplateChild(ImageElement) as Image;
      loadingElement = GetTemplateChild(LoadingElement) as FrameworkElement;

      prevButton.DataContext = imageLoader;
      nextButton.DataContext = imageLoader;

      SyncButtonHandlersAndBinding();
      FormatTextLabel();
    }
    private void SyncButtonHandlersAndBinding()
    {
      if (prevButton != null)
      {
        prevButton.Click += OnPrevClick;
      }
      if (nextButton != null)
      {
        nextButton.Click += OnNextClick;
      }
    }
    void OnPrevClick(object sender, RoutedEventArgs args)
    {
      if (imageLoader.HasPreviousImage)
      {
        imageLoader.MoveToPreviousImage();
        FormatTextLabel();
      }
    }
    void OnNextClick(object sender, RoutedEventArgs args)
    {
      if (imageLoader.HasNextImage)
      {
        imageLoader.MoveToNextImage();
        FormatTextLabel();
      }
    }
    void FormatTextLabel()
    {
      if (textElement != null)
      {
        textElement.Text = string.Format("{0} of {1}",
          photos.Count > 0 ? imageLoader.CurrentIndex + 1 : 0,
          photos.Count);
      }
    }
    Button prevButton;
    Button nextButton;
    FrameworkElement rootElement;
    TextBlock textElement;
    Image imageElement;
    FrameworkElement loadingElement;
    ImageLoader imageLoader;
    List<PhotoSource> photos;

    public const string RootElement = "RootElement";
    public const string PreviousButtonElement = "PreviousButtonElement";
    public const string NextButtonElement = "NextButtonElement";
    public const string TextElement = "TextElement";
    public const string ImageElement = "ImageElement";
    public const string LoadingElement = "LoadingElement";
  }
}

The additions here are that;

  1. We create an ImageLoader, pass it our List<PhotoSource> and call Initialise on it.
  2. We handle ImageLoader.ImageAvailable to put the image passed in the Stream into our imageElement.
  3. We sync up event handlers to the Next/Prev buttons and use those to “tell” the ImageLoader that we’ve moved.
  4. We use the ImageLoader as a DataContext for our Next/Prev buttons so that they can set their IsEnabled state based on the HasNextImage/HasPreviousImage values of ImageLoader.
  5. We format the textElement with a string “X of Y” if we’ve been given a text element.

So, now adding some image files to a sub-folder Images of my ClientBin folder of my test web project I can run up the code and get my UI;

image

and that all seems to be working “reasonably well”.

What I’d like to do next is to;

  1. Experiment with adding some visual states to the control.
  2. Experiment with templating the control to see if that works.
  3. Experiment with adding additional properties to the control – e.g. without replacing the whole template you might want to change fonts, alignments, stretch modes for the images, etc.
  4. Change my ImageLoader to keep less images in memory – at the moment if you visit 10 images it’ll cache them all but I’d like to just keep the current, next and previous images around and reload the others on demand as the user navigates “near” to them.

So, I’ll follow up in another post on some of these as this post is already way too long with lots of XAML and code.