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)
{
storyBrd = mainGrid.Resources["CircleAnimationStory"] as Storyboard;
}
if (newValue < 100)
{
this.Visibility = Visibility.Visible;
if (!isAnimationStarted)
{
if (storyBrd != null)
{
storyBrd.Begin();
isAnimationStarted = true;
}
}
}
else
{
this.Visibility = Visibility.Collapsed;
if (isAnimationStarted)
{
if (storyBrd != null)
{
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">
-->
<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()
{
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:
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