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

Silverlight Image Download Indicator using MVVM Pattern

0.00/5 (No votes)
5 Aug 2010 2  
Image download indicator for providing rich look in Silverlight application

Introduction

Microsoft Silverlight provides a BitmapImage class, which supports a set of features for Image expression including some of the useful events such as DownloadProgress, ImageFailed and ImageOpened events. These events can be effectively used to have a UI with nice look and feel by displaying download progress in case the images are downloaded from the web dynamically.

Probably, some folks might already have a solution for this scenario. But I hope many developers don't think in the same way. So, I have got something to share on this based on my experience. My approach involves simple SpinAnimation custom control implementation. Also there are chances that my solution may not be offering something of the expertise level, but at least it can help to discover the approach :).

This post explores a number of concepts; first, it involves creating a tiny custom SpinAnimation control with animation, next, implementing a View Model (To follow MVVM pattern) with necessary properties for indicating progress details to the View and finally, creating View to design the UI with Model property binding.

Prerequisites

  • .NET Framework 3.5 or higher
  • Windows XP, 2003, Vista, Windows7, VS2008

Custom Silverlight Control – SpinAnimationControl

As there are more articles on how to create a custom control in Silverlight, I am directly jumping on to the main code that is related to changing the visibility of the control based on download progress value. I know, you are asking now, why SpinAnimation control? :) Can't we change the visibility of the SpinAnimation control based on model property value? Yes, we can change, but I want to do some operation when the visibility of animation element gets changed. Like stop and start the animation when element visibility value gets changed. Both WPF and SL do not provide the Visibility property changed callback by default. But you could achieve using property metadata override logic in WPF. Unfortunately, Silverlight doesn't have a feature to override the property metadata like WPF does. So we can go ahead with generic solution which works both in SL and WPF. Let's start with SpinAnimation control constructor, which is pretty basic.

Setting Default Style Key for Custom Control

public class SpinAnimationControl : Control
{
        public SpinAnimationControl ()
        {
            this.DefaultStyleKey = typeof(SpinAnimationControl);
        }
}

Create a dependency property called Progress which receives the download progress value from application side. Note that I have mentioned property value changed callback OnProgressChanged in PropertyMetadata. In this callback, if the new value is less than 100, I am assuming that image is still not downloaded. If value is equal to 100, image gets downloaded without any issue. So, I can change the visibility of my SpinAnimation control based on the new value. And also, I can stop and start the animation after I change the visibility. Please refer the property value changed callback method for more details below.

public int Progress
{
      get { return (int)GetValue(ProgressProperty); }
      set { SetValue(ProgressProperty, value); }
}
 public static readonly DependencyProperty ProgressProperty = 
 DependencyProperty.Register("Progress", typeof(int), 
	typeof(SpinAnimationControl), new PropertyMetadata(0, OnProgressChanged));

Override OnApplyTemplate method for getting template child which has animation storyboard in resource section of root element. You can get template child using GetTemplateChild method. Grid is the root element in control template.

Grid mainGrid = null;
public override void OnApplyTemplate()
{
    base.OnApplyTemplate();
    mainGrid = GetTemplateChild("MainGrid") as Grid;
}

Property Value Changed Callback

private bool isAnimationStarted = false;
private static void OnProgressChanged(object sender, 
		DependencyPropertyChangedEventArgs args)
{
       if (sender is SpinAnimationControl)
       {
          (sender as SpinAnimationControl).OnProgressChanged(args);
       }
}

public void OnProgressChanged(DependencyPropertyChangedEventArgs args)
   {
         int newValue = (int)args.NewValue;
         Storyboard storyBrd = null;

           if (mainGrid != null)
           {
	    // Storyboard from grid resource.
            storyBrd = mainGrid.Resources["CircleAnimationStory"] as Storyboard;
           }

           if (newValue < 100)
           {
               this.Visibility = Visibility.Visible;
               if (!isAnimationStarted)
               {
                   if (storyBrd != null)
                   {
	               // Start the animation
                       storyBrd.Begin();
                       isAnimationStarted = true;
                   }
               }
           }
           else
           {
               this.Visibility = Visibility.Collapsed;
               if (isAnimationStarted)
               {
                   if (storyBrd != null)
                   {
	              // Stop the animation.
                       storyBrd.Stop();
                       isAnimationStarted = false;
                   }
               }
           }
    }

SpinAnimation Control Template – generic.xaml

Here is the template of SpinAnimation control. I put some ellipses with storyboard for animating download progress indicator. Please refer to generic.xaml for the complete template code.

<ControlTemplate TargetType="local:SpinAnimationControl">
<Grid x:Name="SpinGrid" Height="{TemplateBinding Height}" 
	VerticalAlignment="Center" HorizontalAlignment="Center" 
Width="{TemplateBinding Width}">
<Grid.Resources>
<Storyboard x:Name="RetrievalCircleAnimation" RepeatBehavior="Forever" SpeedRatio="4">
<!-- Animation for ellipse -->
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="ellipse" 
Storyboard.TargetProperty=
    "(UIElement.RenderTransform).(TransformGroup.Children)[0].(ScaleTransform.ScaleX)">
<EasingDoubleKeyFrame KeyTime="00:00:00" Value="1"/>
<EasingDoubleKeyFrame KeyTime="00:00:01.5000000" Value="0.5"/>
<EasingDoubleKeyFrame KeyTime="00:00:03.5000000" Value="0.5"/>
<EasingDoubleKeyFrame KeyTime="00:00:04" Value="1"/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
<!- More codes --> 
</Grid.Resources>
<Ellipse x:Name="ellipse" Height="12" Width="12" HorizontalAlignment="Center" 
VerticalAlignment="Top" RenderTransformOrigin="0.5,0.5" 
				Fill="{StaticResource ballbrush}">
<Ellipse.RenderTransform>
<TransformGroup>
<ScaleTransform ScaleX="0.5" ScaleY="0.5"/>
<SkewTransform/>
<RotateTransform/>
<TranslateTransform/>
</TransformGroup>
</Ellipse.RenderTransform>
</Ellipse>
<!- More codes -->
</Grid>
</ControlTemplate>

SpinAnimation Control Snap

Creating ViewModel and View to Present the Image with Download Indicator

If you are a SL or WPF developer, I believe that you prefer the MVVM approach only, nevertheless I always prefer MVVM since it is the only powerful pattern for SL or WPF apps till now. That is the reason I decided to implement image Uri and Progress notification from View Model.

ImageAppViewModel Class

ImageAppViewModel class has a property called ImageList with the type of ObservableCollection. This is the property that we are going to bind as an ItemsSource of Listbox control in View.

public ImageAppViewModel()
{
//Took some random images link from web for demonstration purpose only.
ImagesList = new ObservableCollection();
ImagesList.Add(new ImageExt() { 
    Uri = "http://www.pdssb.com.my/images/stories/gallery/car.jpg" });
ImagesList.Add(new ImageExt() { 
    Uri = "http://joshua.maruskadesign.com/blog/uploaded_images/Car-02-723748.png" });
ImagesList.Add(new ImageExt() { Uri = "http://www.carbodydesign.com/archive/2009/02/
13-automotive-designers-show-program/Autodesk-Concept-Car-rendering-lg.jpg" });
ImagesList.Add(new ImageExt() { 
    Uri = "http://www2.hiren.info/desktopwallpapers/other/car-silver-87c.jpg" });
ImagesList.Add(new ImageExt() { 
    Uri = "http://brianhansford.com/wp-content/uploads/2010/02/red-sports-car.jpg" });
}
private ObservableCollection<ImageExt> m_ImagesList;
public ObservableCollection<ImageExt> ImagesList
{
  get
   {
     return m_ImagesList;
   }
  set
   {
     m_ImagesList = value;
     OnPropertyChanged("ImagesList");
   }
}

ImageExt Class

ImageExt class has property called Uri, ImageSource(Type of BitmapImage) and Progress property. Why two properties for image source in view model? Typically we will send Image uri as a string type. See the example as mentioned below.

Uri= “http://brianhansford.com/wp-content/uploads/2010/02/red-sports-car.jpg" 

But we need to construct the BitmapImage object using this uri and register the DownloadProgress event in property getter. In download progress event handler, update the Progress property of ImageExt class which we are using for indicating how much percentage of bytes has been downloaded for a particular image. Now bind only ImageSource property in ItemTemplate of ListBox in View.

public class ImageExt : INotifyPropertyChanged
{
        private int m_Progress;
        public int Progress
        {
            get
            {
                return m_Progress;
            }
            set
            {
                m_Progress = value;
                OnPropertyChanged("Progress");
            }
        }

        private string m_Uri;
        public string Uri
        {
            get
            {
                return m_Uri;
            }
            set
            {
                m_Uri = value;
                OnPropertyChanged("Uri");
            }
        }

        BitmapImage image = null;
        public BitmapImage ImageSource
        {
            get
            {
                image = new BitmapImage(new Uri(Uri, UriKind.Absolute));
                image.DownloadProgress += 
		new EventHandler<DownloadProgressEventArgs>(image_DownloadProgress);
                return image;
            }
        }
}

DownloadProgress Event Handler

Update Progress property value based on DownloadProgressEventArgs value and un registering the event once it gets downloaded.

void image_DownloadProgress(object sender, DownloadProgressEventArgs e)
{
            Progress = e.Progress;

            if (e.Progress == 100)
            {
                image.DownloadProgress -= 
		new EventHandler<DownloadProgressEventArgs>(image_DownloadProgress);
            }
}

ImageAppView and ImageAppView.xaml

Now, create the ImageAppView.xaml and put the simple list box control with ViewModel property binding. See the below code snippet, ListBox ItemsSource property bound with ImageList property which we declared in ViewModel.

<ListBox ItemsSource="{Binding ImageList}" 
	Height="110" BorderBrush="Blue" BorderThickness="2">
</ListBox>

Set the ItemsPanelTemplate with StackPanel to display images in horizontal view.

<ListBox.ItemsPanel>
    <ItemsPanelTemplate>
         <StackPanel Orientation="Horizontal"></StackPanel>
   </ItemsPanelTemplate>
</ListBox.ItemsPanel>

Set the ItemTemplate to host our SpinAnimation and Image control to display the actual image once it get downloaded. I put some textboxes to show the download percentage details as well.

<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<Image Name="icon" Width="100" Height="100" 
	Source ="{Binding ImageSource}" Stretch="Uniform"  
VerticalAlignment="Center" HorizontalAlignment="Center" />
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" 
HorizontalAlignment="Center" Visibility="{Binding  Path=Visibility, 
	ElementName=spinAnimationControl}">
<TextBlock FontFamily="Cambria" FontSize="10" Text="{Binding Progress}" 
FontWeight="Bold" Foreground="Black" TextAlignment="Center" 
TextWrapping="Wrap"  LineStackingStrategy="BlockLineHeight" 
	LineHeight="9"  VerticalAlignment="Center" HorizontalAlignment="Stretch" 
Margin="1"/>
<TextBlock Text="%" FontFamily="Cambria" FontSize="10" 
FontWeight="Bold" Foreground="Black" TextAlignment="Center" 
	TextWrapping="Wrap"  LineStackingStrategy="BlockLineHeight" 
LineHeight="9"  VerticalAlignment="Center" HorizontalAlignment="Stretch" Margin="1"/>
</StackPanel>
<control:SpinAnimationControl Name="spinAnimationControl" 
	Width="50" Height="50"  VerticalAlignment="Center" 
HorizontalAlignment="Stretch" Progress="{Binding Progress}" Margin="1"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>

Finally, assign the ViewModel in DataContext of View.

public partial class MainPage : UserControl
    {
        public MainPage()
        {
            InitializeComponent();
            this.DataContext = new ImageAppViewModel();
        }
    }

Demo Application Snaps

Download initiated with percentage details

Download completed for all the images

History

  • 5th August, 2010: Initial post

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