Wednesday, August 4, 2010

Silverlight Image download indicator with MVVM Pattern

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 solution for this scenario. But I hope many developers don’t think in the same way. So, 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 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.

SpinAnimationControl - Custom Silverlight control

As there are more articles on how to create 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? J 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 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);

}

}

Creating 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 call back, if new value is less then 100, I am assuming that image is still not downloaded. If value is equal to 100, image gets downloaded witout 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 spinGrid = null;

public override void OnApplyTemplate()

{

base.OnApplyTemplate();

spinGrid = GetTemplateChild("SpinGrid") 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 storyBoard = null;

if (spinGrid != null)

{

// Storyboard from grid resource.

storyBoard = spinGrid.Resources["RetrievalCircleAnimation"] as Storyboard;

}

if (newValue < 100)

{

this.Visibility = Visibility.Visible;

if (!isAnimationStarted)

{

if (storyBoard != null)

{

// Start the animation

storyBoard.Begin();

isAnimationStarted = true;

}

}

}

else

{

this.Visibility = Visibility.Collapsed;

if (isAnimationStarted)

{

if (storyBoard != null)

{

// Stop the animation.

storyBoard.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 the generic.xaml for 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 MVVM approach only, nevertheless I always prefer MVVM since it is the only powerful pattern for SL or WPF apps till now. So that is the reason I decided that implementing image Uri and Progress notification from View Model.

ImageAppViewModel class

ImageAppViewModel class has 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()

{

ImagesList = new ObservableCollection<ImageExt>();

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 property for image source in view model?. Typically we will send Image uri as a string type. See the below

Uri="http://www.pdssb.com.my/images/stories/gallery/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 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

// Updating Progress property value based on DownloadProgressEventArgs value and un registering the event once its // 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 gets downloaded. And also 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();

}

}

Run the demo application and see the output. Here is some snaps.

Download initiated



Download completed


Download complete source code from here.

Enjoy!!!!.


1 comment: