One of the main design principles of Metro and Windows Store Apps is to make the UI as responsive as possible while maintaining the smooth transition effects in response to working threads.
For finished control implementation, please scroll to the bottom, unless you want to read a little more about the transitions and animations used for the control.
Following these two basic statements, we will try to solve a very common problem about using web resources in windows runtime. For this scenario, let’s assume we are working on an application that is supposed to visualize a certain dataset that is downloaded from a network source. In this dataset, let us again assume we have certain URIs that we want to use as a source to content images. The best solution for displaying a smooth layout while loading the images from this network source would be to set them to invisible until the image is loaded fully and then fade them in using a transition. (e.g. You can see a concrete example in the Bing app while it is opening the search results and loading the images)
This is not an uncommon problem and if we were dealing with WPF, we could easily deal with this using triggers.
<Style x:Key="StyleImageFadeIn" TargetType="{x:Type Image}"> <Setter Property="Opacity" Value="0" /> <Style.Triggers> <!--Sets visibility to Collapsed if Source is null - this will cause IsVisible to be false--> <Trigger Property="Source" Value="{x:Null}"> <Setter Property="Visibility" Value="Collapsed" /> </Trigger> <!--Fades-in the image when it becomes visible--> <Trigger Property="IsVisible" Value="True"> <Trigger.EnterActions> <BeginStoryboard> <BeginStoryboard.Storyboard> <Storyboard> <DoubleAnimation Storyboard.TargetProperty="Opacity" To="1" Duration="0:0:1"/> </Storyboard> </BeginStoryboard.Storyboard> </BeginStoryboard> </Trigger.EnterActions> </Trigger> </Style.Triggers> </Style>
Beautiful, so the image is collapsed when the Source is set to null. When it becomes visible, meaning when the source is set, we nicely fade it in.
BUT… Where are the triggers in WinRT and Windows Store Apps. The answer is: “They are gone”. However, we have the new, let’s say, “packaged” version of some commonly used animations such as “EntranceThemeTransition”, “RepositionThemeTransition” etc. These “packaged” transitions can be easily included in the transitions property of the framework elements. However, these are render transitions and it would not help us in our quest to load the image from the network source and fade it in, because in most cases the render transition would actually be played before the network resource is actually loaded.
We can go ahead and use the Image controls ImageOpened property and deal with the animation on the page’s implementation but we want a more compact solution that can easily be reused in our other projects.
So let’s try to implement a user control. First thing we do is to create a grid and add an image control in it. We also need to declare the fade-in animation and possibly fade-out animations for the image.
<UserControl x:Class="Samples.ImagePreview" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" x:Name="ControlRoot" d:DesignHeight="300" d:DesignWidth="400"> <Grid> <Image x:Name="Image" Source=" " Height="{Binding ElementName=ControlRoot, Path=ActualHeight}" Width="{Binding ElementName=ControlRoot, Path=ActualWidth}" /> <Grid.Resources> <Storyboard x:Name="ImageFadeOut"> <FadeOutThemeAnimation Storyboard.TargetName="Image" /> </Storyboard> <Storyboard x:Name="ImageFadeIn"> <FadeInThemeAnimation Storyboard.TargetName="Image" /> </Storyboard> </Grid.Resources> </Grid> </UserControl>
Notice that instead of using a standard DoubleAnimation on opacity we are using the FadeInThemeAnimation and the FadeOutThemeAnimation.
Next let’s declare a dependency property for the image source and another one for a placeholder (just in case).
public static readonly DependencyProperty SourceProperty = DependencyProperty.Register("Source", typeof(ImageSource), typeof(ImagePreview), new PropertyMetadata(default(ImageSource), SourceChanged)); public static readonly DependencyProperty PlaceHolderProperty = DependencyProperty.Register("PlaceHolder", typeof(ImageSource), typeof(ImagePreview), new PropertyMetadata(default(ImageSource))); public ImageSource Source { get { return (ImageSource)GetValue(SourceProperty); } set { SetValue(SourceProperty, value); } } public ImageSource PlaceHolder { get { return (ImageSource)GetValue(PlaceHolderProperty); } set { SetValue(PlaceHolderProperty, value); } }
So at first the PlaceHolder will be set as the source for the image control, and once the actual source is bound and loaded we can fadeout the placeholder and fade the actual image in. This process of fading out the placeholder and setting the source to the loaded image would look something like:
private void LoadImage(ImageSource source) { ImageFadeOut.Completed += (s, e) => { Image.Source = source; ImageFadeIn.Begin(); }; ImageFadeOut.Begin(); }
Once the Image control that is showing the placeholder, or another source as for that matter, is faded out, we can change the source and fade the control back in.
So only thing left to implement is a method to download the image from the network source when the bound Source is actually changed. (i.e. Notice in the dependency property declaration we already declared a callback for SourceChanged.)
private static void SourceChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs) { var control = (ImagePreview)dependencyObject; var newSource = (ImageSource) dependencyPropertyChangedEventArgs.NewValue; System.Diagnostics.Debug.WriteLine("Image source changed: {0}", ((BitmapImage)newSource).UriSource.AbsolutePath); if(newSource != null) { var image = (BitmapImage) newSource; // If the image is not a local resource or it was not cached if (image.UriSource.Scheme != "ms-appx" && image.UriSource.Scheme != "ms-resource" && (image.PixelHeight * image.PixelWidth == 0)) { // TODO: control.DownloadImageAsync(newSource); } else { control.LoadImage(newSource); } } }
This implemented method is doing the usual checks, if it is a local resource it is just loading the image directly with a fade-in, if it is a remote resource, it is calling the method DownloadImageAsync (which we haven’t implemented and will not have to), which, in short, “downloads” the image as a Random Access Stream, loads it into a BitmapImage and assigns it to the image control with LoadImage method.
However, if we go down this road, you will notice that every time we open the same page with our control on it, the images will be fading in and out. Normally, when an image is opened with an image control, (unless instructed otherwise) windows runtime creates a cached version for this image data. Under normal circumstances, we would want to make use of this image caching.
The easiest option would be to create a secondary image control in the UserControl grid which will be used to load the image for the first time, and then once it is loaded assign it to the actual element. Using this “staging” element we can make use of the image caching and have a reusable control.
Below is the completed sample.
Happy coding everyone…
Finished Control
XAML:
<Grid> <Image x:Name="Staging" Visibility="Collapsed"/> <Image x:Name="Image" Source="{Binding ElementName=ControlRoot, Path=PlaceHolder}" Height="{Binding ElementName=ControlRoot, Path=ActualHeight}" Width="{Binding ElementName=ControlRoot, Path=ActualWidth}" /> <Grid.Resources> <Storyboard x:Name="ImageFadeOut"> <FadeOutThemeAnimation Storyboard.TargetName="Image" /> </Storyboard> <Storyboard x:Name="ImageFadeIn"> <FadeInThemeAnimation Storyboard.TargetName="Image" /> </Storyboard> </Grid.Resources> </Grid>
And the C# implementation:
public sealed partial class ImagePreview : UserControl { public static readonly DependencyProperty SourceProperty = DependencyProperty.Register("Source", typeof(ImageSource), typeof(ImagePreview), new PropertyMetadata(default(ImageSource), SourceChanged)); public static readonly DependencyProperty PlaceHolderProperty = DependencyProperty.Register("PlaceHolder", typeof(ImageSource), typeof(ImagePreview), new PropertyMetadata(default(ImageSource))); public ImageSource Source { get { return (ImageSource)GetValue(SourceProperty); } set { SetValue(SourceProperty, value); } } public ImageSource PlaceHolder { get { return (ImageSource)GetValue(PlaceHolderProperty); } set { SetValue(PlaceHolderProperty, value); } } public ImagePreview() { this.InitializeComponent(); } private static void SourceChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs) { var control = (ImagePreview)dependencyObject; var newSource = (ImageSource) dependencyPropertyChangedEventArgs.NewValue; System.Diagnostics.Debug.WriteLine("Image source changed: {0}", ((BitmapImage)newSource).UriSource.AbsolutePath); if(newSource != null) { var image = (BitmapImage) newSource; // If the image is not a local resource or it was not cached if (image.UriSource.Scheme != "ms-appx" && image.UriSource.Scheme != "ms-resource" && (image.PixelHeight * image.PixelWidth == 0)) { image.ImageOpened += (sender, args) => control.LoadImage(image); control.Staging.Source = image; } else { control.LoadImage(newSource); } } } private void LoadImage(ImageSource source) { ImageFadeOut.Completed += (s, e) => { Image.Source = source; ImageFadeIn.Begin(); }; ImageFadeOut.Begin(); } }