I think I may have written something similar to this before but I got asked about loading images from the web and so I thought I’d write something down.
It’s a pretty common scenario that you have an image control and you want that image control to display an image from the web which can both take time and can also fail to load.
While the image is loading from the web, you may well want to display some kind of “loading” content and when the image fails to load from the web you may well want to display some kind of “failed” content and it might be nice to smoothly transition between the “loading” content and the image when it asynchronously shows up ready to be displayed.
I’ve tended to approach this kind of problem by trying to write a user control although it might be more correct to go the whole distance and build a templated control – let’s see…
Defining a User Control
In the first instance, here’s a (fairly) quick UserControl which displays an Image overlayed with 2 ContentControls where I can display “Loading…” style content or “Failed to Load” style content while the image is loading and if it fails to load respectively;
<UserControl x:Class="App30.ImageLoader" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:App30" xmlns:cmn="using:App30.Common" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="400" x:Name="parent"> <UserControl.Resources> <!-- Put this here but I'd put it in App.xaml really --> <cmn:BooleanToVisibilityConverter x:Name="converter" /> </UserControl.Resources> <Grid> <Image x:Name="imgDisplay" Source="{Binding ElementName=parent,Path=Source}" ImageFailed="OnImageFailed" ImageOpened="OnImageOpened"/> <ContentControl Visibility="{Binding ElementName=parent,Path=IsLoading,Converter={StaticResource converter}}" Content="{Binding ElementName=parent,Path=LoadingContent}" VerticalContentAlignment="Stretch" HorizontalContentAlignment="Stretch"> </ContentControl> <ContentControl Visibility="{Binding ElementName=parent,Path=IsFailed,Converter={StaticResource converter}}" Content="{Binding ElementName=parent,Path=FailedContent}" VerticalContentAlignment="Stretch" HorizontalContentAlignment="Stretch"> </ContentControl> </Grid> </UserControl>
You’d notice that (line 17, line 22, line 27) the controls inside of this control are picking up properties from the user control itself via binding. That is – the Image.Source property is bound to my ImageLoader.Source property and the code that defines these properties is as below;
using System; using System.Collections.Generic; using System.IO; using System.Linq; using Windows.Foundation; using Windows.Foundation.Collections; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Controls.Primitives; using Windows.UI.Xaml.Data; using Windows.UI.Xaml.Input; using Windows.UI.Xaml.Media; using Windows.UI.Xaml.Navigation; namespace App30 { public sealed partial class ImageLoader : UserControl { public static DependencyProperty IsLoadedProperty = DependencyProperty.Register("IsLoaded", typeof(bool), typeof(ImageLoader), new PropertyMetadata(false)); public static DependencyProperty IsLoadingProperty = DependencyProperty.Register("IsLoading", typeof(bool), typeof(ImageLoader), new PropertyMetadata(false)); public static DependencyProperty IsFailedProperty = DependencyProperty.Register("IsFailed", typeof(bool), typeof(ImageLoader), new PropertyMetadata(false)); public static DependencyProperty LoadingContentProperty = DependencyProperty.Register("LoadingContent", typeof(object), typeof(ImageLoader), null); public static DependencyProperty FailedContentProperty = DependencyProperty.Register("FailedContent", typeof(object), typeof(ImageLoader), null); public static DependencyProperty SourceProperty = DependencyProperty.Register("Source", typeof(ImageSource), typeof(ImageLoader), new PropertyMetadata(null, OnSourceChanged)); public ImageLoader() { this.InitializeComponent(); } public bool IsLoading { get { return ((bool)base.GetValue(IsLoadingProperty)); } set { base.SetValue(IsLoadingProperty, value); } } public bool IsLoaded { get { return ((bool)base.GetValue(IsLoadedProperty)); } set { base.SetValue(IsLoadedProperty, value); } } public bool IsFailed { get { return ((bool)base.GetValue(IsFailedProperty)); } set { base.SetValue(IsFailedProperty, value); } } public object LoadingContent { get { return (base.GetValue(LoadingContentProperty)); } set { base.SetValue(LoadingContentProperty, value); } } public object FailedContent { get { return (base.GetValue(FailedContentProperty)); } set { base.SetValue(FailedContentProperty, value); } } public ImageSource Source { get { return ((ImageSource)base.GetValue(SourceProperty)); } set { base.SetValue(SourceProperty, value); } } static void OnSourceChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args) { ImageLoader loader = (ImageLoader)sender; loader.IsFailed = false; loader.IsLoaded = false; loader.IsLoading = true; } void OnImageFailed(object sender, ExceptionRoutedEventArgs e) { this.IsLoading = false; this.IsFailed = true; } void OnImageOpened(object sender, RoutedEventArgs e) { this.IsLoading = false; this.IsLoaded = true; } } }
Using the Control
In terms of making use of the control, I can go and author a piece of XAML like this one for instance;
<local:ImageLoader Source="http://www.hdwallpapers.in/walls/mupe_bay_england-HD.jpg"> <local:ImageLoader.LoadingContent> <Grid Background="Red"> <TextBlock VerticalAlignment="Center" HorizontalAlignment="Center" Text="Loading..." FontSize="24" /> </Grid> </local:ImageLoader.LoadingContent> <local:ImageLoader.FailedContent> <Grid Background="Green"> <TextBlock VerticalAlignment="Center" HorizontalAlignment="Center" Text="Failed to Load" FontSize="24" /> </Grid> </local:ImageLoader.FailedContent> </local:ImageLoader>
and so when I run the Windows 8 app that I’ve built this into I see;
and then…
and if I take this a little further and perhaps have a few images;
<GridView> <GridView.Items> <x:String>http://www.hdwallpapers.in/wallpapers/autumn_season-1280x800.jpg</x:String> <x:String>http://www.hdwallpapers.in/wallpapers/tree_beach_side-1280x800.jpg</x:String> <x:String>http://www.hdwallpapers.in/wallpapers/beautiful_scenery-1280x800.jpg</x:String> </GridView.Items> <GridView.ItemTemplate> <DataTemplate> <local:ImageLoader Source="{Binding}" Width="250" Height="250"> <local:ImageLoader.LoadingContent> <StackPanel VerticalAlignment="Center" HorizontalAlignment="Center"> <ProgressRing IsActive="True" Width="48" Height="48" Foreground="White"/> <TextBlock Text="Loading..." /> </StackPanel> </local:ImageLoader.LoadingContent> </local:ImageLoader> </DataTemplate> </GridView.ItemTemplate> </GridView>
Then I get a loading display (if I duplicate the images to add a few more URLs into that previous XAML snippet);
Now, what might be nice is if I could have an animation that moves from the “Loading…” content to the “Image” content but I think that might be where things go a little off the rails here
Making Use of Visual States
Ideally, I’d like to be able to have the user of my control be able to specify what animation they wanted to have happen when the “Loading…” content changes to the requested image.
The user might want a fade or a swipe or something like that and it’d be “nice” to make that a configurable element of the control.
The way I’ve done things here with a UserControl, I’m not sure whether that’s possible. I have an Image element “inside” of my UserControl which the user’s custom animation would need to target in order to make a transition from the “Loading…” content to the image display.
However…the user of my control has no real way of targeting the image hidden away inside my control.
To make that happen, I think I’d need to have made a templated control with a default template that contained;
- An Image
- 2 Content Presenters (for loading/failed)
- Visual States – Loading, Loaded, Failed
- Animations that transition the elements of the template through the visual states.
If I’d done that I also wouldn’t have had to bind Visibility values to various boolean flags because those Visibility values would have been set by the transition from one visual state to another.
But I didn’t go down this route. Even so, it’d be relatively easy to bake a particular transition into the control itself and lean on the Visual State Manager. Here’s some modified XAML for the control;
<UserControl x:Class="App30.ImageLoader" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:App30" xmlns:cmn="using:App30.Common" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="400" x:Name="parent"> <UserControl.Resources> <!-- Put this here but I'd put it in App.xaml really --> <cmn:BooleanToVisibilityConverter x:Name="converter" /> </UserControl.Resources> <Grid> <VisualStateManager.VisualStateGroups> <VisualStateGroup x:Name="Default"> <VisualState x:Name="Loading"> <Storyboard> <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)" Storyboard.TargetName="loadingDisplay"> <DiscreteObjectKeyFrame KeyTime="0"> <DiscreteObjectKeyFrame.Value> <Visibility>Visible</Visibility> </DiscreteObjectKeyFrame.Value> </DiscreteObjectKeyFrame> </ObjectAnimationUsingKeyFrames> </Storyboard> </VisualState> <VisualState x:Name="Loaded"> <Storyboard> <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)" Storyboard.TargetName="imgDisplay"> <DiscreteObjectKeyFrame KeyTime="0"> <DiscreteObjectKeyFrame.Value> <Visibility>Visible</Visibility> </DiscreteObjectKeyFrame.Value> </DiscreteObjectKeyFrame> </ObjectAnimationUsingKeyFrames> </Storyboard> </VisualState> <VisualState x:Name="Failed"> <Storyboard> <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)" Storyboard.TargetName="failedDisplay"> <DiscreteObjectKeyFrame KeyTime="0"> <DiscreteObjectKeyFrame.Value> <Visibility>Visible</Visibility> </DiscreteObjectKeyFrame.Value> </DiscreteObjectKeyFrame> </ObjectAnimationUsingKeyFrames> </Storyboard> </VisualState> </VisualStateGroup> </VisualStateManager.VisualStateGroups> <Image x:Name="imgDisplay" Source="{Binding ElementName=parent,Path=Source}" ImageFailed="OnImageFailed" ImageOpened="OnImageOpened" Visibility="Collapsed" /> <ContentControl x:Name="loadingDisplay" Content="{Binding ElementName=parent,Path=LoadingContent}" VerticalContentAlignment="Stretch" HorizontalContentAlignment="Stretch" FontFamily="Global User Interface" Visibility="Collapsed"/> <ContentControl x:Name="failedDisplay" Content="{Binding ElementName=parent,Path=FailedContent}" VerticalContentAlignment="Stretch" HorizontalContentAlignment="Stretch" Visibility="Collapsed"/> </Grid> </UserControl>
and it’s worth saying that those Storyboards were created by Blend (as anyone who’s manually created Storyboards will spot because they never look like Blend generated ones) and then the code for my control becomes much less;
using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Media; namespace App30 { public sealed partial class ImageLoader : UserControl { public static DependencyProperty LoadingContentProperty = DependencyProperty.Register("LoadingContent", typeof(object), typeof(ImageLoader), null); public static DependencyProperty FailedContentProperty = DependencyProperty.Register("FailedContent", typeof(object), typeof(ImageLoader), null); public static DependencyProperty SourceProperty = DependencyProperty.Register("Source", typeof(ImageSource), typeof(ImageLoader), new PropertyMetadata(null, OnSourceChanged)); public ImageLoader() { this.InitializeComponent(); } public object LoadingContent { get { return (base.GetValue(LoadingContentProperty)); } set { base.SetValue(LoadingContentProperty, value); } } public object FailedContent { get { return (base.GetValue(FailedContentProperty)); } set { base.SetValue(FailedContentProperty, value); } } public ImageSource Source { get { return ((ImageSource)base.GetValue(SourceProperty)); } set { base.SetValue(SourceProperty, value); } } static void OnSourceChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args) { ImageLoader loader = (ImageLoader)sender; VisualStateManager.GoToState(loader, "Loading", true); } void OnImageFailed(object sender, ExceptionRoutedEventArgs e) { VisualStateManager.GoToState(this, "Failed", true); } void OnImageOpened(object sender, RoutedEventArgs e) { VisualStateManager.GoToState(this, "Loaded", true); } } }
But the animations in those StoryBoards are simply toggling Visibility values on controls instantaneously rather than doing a nice, smooth transition.
Making Use of Built-In Animations
One of the guidelines around Windows 8 user interface is to try and make use of the same animations that that platform itself uses and these have been encoded into the XAML frameworks (and the JavaScript/CSS animations that are done by the WinJS JavaScript framework as well).
In moving from my simple “on/off” toggle animations that I had in the previous snippet of XAML it’d be smart to lean on those animations in the framework (see the docs for full details).
I hunted down FadeInThemeAnimation and FadeOutThemeAnimation and tried them out – here’s the XAML definition for that control where the sharp-eyed might notice that my animation of Visibility has changed to be an animation of Opacity;
<UserControl x:Class="App30.ImageLoader" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:App30" xmlns:cmn="using:App30.Common" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="400" x:Name="parent"> <Grid> <VisualStateManager.VisualStateGroups> <VisualStateGroup x:Name="Default"> <VisualStateGroup.Transitions> <VisualTransition From="Failed"> <Storyboard> <FadeOutThemeAnimation TargetName="failedDisplay" /> </Storyboard> </VisualTransition> <VisualTransition From="Loading"> <Storyboard> <FadeOutThemeAnimation TargetName="loadingDisplay"/> </Storyboard> </VisualTransition> <VisualTransition From="Displaying"> <Storyboard> <FadeOutThemeAnimation TargetName="imgDisplay"/> </Storyboard> </VisualTransition> </VisualStateGroup.Transitions> <VisualState x:Name="Loading"> <Storyboard> <DoubleAnimation Storyboard.TargetName="loadingDisplay" Storyboard.TargetProperty="Opacity" To="1.0" /> </Storyboard> </VisualState> <VisualState x:Name="Displaying"> <Storyboard> <DoubleAnimation Storyboard.TargetName="imgDisplay" Storyboard.TargetProperty="Opacity" To="1.0" /> </Storyboard> </VisualState> <VisualState x:Name="Failed"> <Storyboard> <DoubleAnimation Storyboard.TargetName="failedDisplay" Storyboard.TargetProperty="Opacity" To="1.0" /> </Storyboard> </VisualState> </VisualStateGroup> </VisualStateManager.VisualStateGroups> <Image x:Name="imgDisplay" Source="{Binding ElementName=parent,Path=Source}" ImageFailed="OnImageFailed" ImageOpened="OnImageOpened" Opacity="0"/> <ContentControl x:Name="loadingDisplay" Content="{Binding ElementName=parent,Path=LoadingContent}" VerticalContentAlignment="Stretch" HorizontalContentAlignment="Stretch" Opacity="0"/> <ContentControl x:Name="failedDisplay" Content="{Binding ElementName=parent,Path=FailedContent}" VerticalContentAlignment="Stretch" HorizontalContentAlignment="Stretch" Opacity="0"/> </Grid> </UserControl>
and the code that goes along side it;
using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Media; namespace App30 { public sealed partial class ImageLoader : UserControl { public static DependencyProperty LoadingContentProperty = DependencyProperty.Register("LoadingContent", typeof(object), typeof(ImageLoader), null); public static DependencyProperty FailedContentProperty = DependencyProperty.Register("FailedContent", typeof(object), typeof(ImageLoader), null); public static DependencyProperty SourceProperty = DependencyProperty.Register("Source", typeof(ImageSource), typeof(ImageLoader), new PropertyMetadata(null, OnSourceChanged)); public ImageLoader() { this.InitializeComponent(); } public object LoadingContent { get { return (base.GetValue(LoadingContentProperty)); } set { base.SetValue(LoadingContentProperty, value); } } public object FailedContent { get { return (base.GetValue(FailedContentProperty)); } set { base.SetValue(FailedContentProperty, value); } } public ImageSource Source { get { return ((ImageSource)base.GetValue(SourceProperty)); } set { base.SetValue(SourceProperty, value); } } static void OnSourceChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args) { ImageLoader loader = (ImageLoader)sender; VisualStateManager.GoToState(loader, "Loading", true); } void OnImageFailed(object sender, ExceptionRoutedEventArgs e) { VisualStateManager.GoToState(this, "Failed", true); } void OnImageOpened(object sender, RoutedEventArgs e) { VisualStateManager.GoToState(this, "Displaying", true); } } }
Using that Control
If I go back to my example of using a GridView to exercise this control then I can tweak the UI a little;
<GridView ItemsSource="{Binding}"> <GridView.ItemTemplate> <DataTemplate> <local:ImageLoader Source="{Binding}" Width="250" Height="250"> <local:ImageLoader.LoadingContent> <StackPanel VerticalAlignment="Center" HorizontalAlignment="Center"> <ProgressRing IsActive="True" Width="48" Height="48" Foreground="White"/> <TextBlock Text="Loading..." /> </StackPanel> </local:ImageLoader.LoadingContent> <local:ImageLoader.FailedContent> <TextBlock Text="Failed" VerticalAlignment="Center" HorizontalAlignment="Center" /> </local:ImageLoader.FailedContent> </local:ImageLoader> </DataTemplate> </GridView.ItemTemplate> </GridView>
and then just have some code that sets up a mixture of URLs that will either succeed/fail in loading and then mess around with them a little on a timer;
ObservableCollection<Uri> uris = new ObservableCollection<Uri>() { new Uri("http://windows7themes.net/wp-content/gallery/windows-7-beaches-theme/4-beaches-wallpaper.jpg"), new Uri("http://badurl.com/picture.jpg"), new Uri("http://3.bp.blogspot.com/-SWKJEF0vxro/TWBgqX3FaGI/AAAAAAAACz8/8hWE0u4WX-8/s1600/The-best-top-desktop-beach-wallpapers-hd-beach-wallpaper-35.jpg"), new Uri("http://badurl.com/picture.jpg"), new Uri("http://1.bp.blogspot.com/-kcl3OMZMKNM/TWBgpMl6sTI/AAAAAAAACz4/_uPYJxozJd4/s1600/The-best-top-desktop-beach-wallpapers-hd-beach-wallpaper-34.jpeg"), new Uri("http://badurl.com/picture.jpg"), new Uri("http://www.keralaheritages.com/images/beach.jpg"), new Uri("http://badurl.com/picture.jpg"), new Uri("http://3.bp.blogspot.com/-JSwv_cKSiF4/TWBgWjMEsxI/AAAAAAAACy4/It1i-bIxLuc/s1600/The-best-top-desktop-beach-wallpapers-hd-beach-wallpaper-18.jpg"), new Uri("http://badurl.com/picture.jpg") }; this.DataContext = uris; DispatcherTimer t = new DispatcherTimer(); t.Interval = TimeSpan.FromSeconds(5); t.Tick += (a, b) => { for (int i = 0; i < uris.Count - 1; i += 2) { Uri temp = uris[i]; uris[i] = uris[i + 1]; uris[i + 1] = temp; } }; t.Start();
and that seems to work quite nicely in terms of toggling the images between the states although once images start being cached the “loading…” state becomes a little too quick to notice;
Next Step?
That was a bit of fun but I think the next step would be to write a proper, templated control which allowed the user of the control to use a default template or specify their own template including the animations that are involved in moving from one visual state within the control to another.
There’s a very good example of this over on Tim’s blog site if you want to look how that works out in the Windows 8 world.