Experiments in Building a Silverlight Photo Control ( Part 2 )

Following on from this post. I wanted to make sure that I only ever had a maximum of [Previous, Current, Next] images loaded in my control whilst still pro-actively trying to load an image as a user “heads towards it”.

That is, if the user is on image N then I want to try and make sure I’ve already loaded N-1, N, N+1 as a way of making sure I’ve got their “next” image already.

How to do that? Previously, I just kept a Dictionary<int,MemoryStream> around where the int is the index of the image and the MemoryStream is the bytes of the image and this Dictionary would just grow and grow.

There’s only one place in my code where I add to that Dictionary so ( rather than going off and building some LRU Dictionary class ) I just made sure that if my dictionary has grown a little large then I just trim it.

Whilst there, I decided that using a Lambda statement for the completion of WebClient.OpenReadCompleted was a bit ugly so refactored that into a separate function as well. I also noticed that I’d added 2 DependencyProperty’s to my ImageLoader class and I’ve no idea why so I took those away. The code for my ImageLoader class now looks like.

  public class ImageLoader : INotifyPropertyChanged
  {
    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 += OnReadImageCompleted;

        client.OpenReadAsync(new Uri(photos[i].Source, UriKind.RelativeOrAbsolute), i);
      }
      else if (i == currentIndex)
      {
        FireImageAvailable(ms);
      }
    }
    void OnReadImageCompleted(object sender, OpenReadCompletedEventArgs e)
    {
      int index = (int)e.UserState;

      MemoryStream readMs = new MemoryStream();
      e.Result.WriteTo(readMs);
      e.Result.Close();
      readMs.Seek(0, SeekOrigin.Begin);

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

        // New bit, try and trim the dictionary 
        if (imageMap.Keys.Count > 3)
        {
          int minKey = imageMap.Keys.Min();
          int maxKey = imageMap.Keys.Max();

          int minDistance = Math.Abs(minKey - index);
          int maxDistance = Math.Abs(maxKey - index);

          int evictKey = minDistance > maxDistance ? minKey : maxKey;

          imageMap.Remove(evictKey);
        }
      }
      if (index == currentIndex)
      {
        FireImageAvailable(readMs);
      }
    }
    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, “onwards and upwards” – next stop needs to be introducing visual states I think and the transitions between them.