Thursday, March 29, 2007

Asynchronous Data templates, data providers and Data Binding

DataBinding, templates and providers and really cool things in WPF, but have you tried to load something really heavy? How time it's take to load 20 images? A couple of seconds? And what about if each image is 3000X3000 px? Huh, your HMI thread will be locked until all those images will be downloaded and displayed. In this post, I'll explain how to build asynchronous XMLDataProvider, that load RSS from Flickr, asynchronous DataTemplate, that loaded image by image without locking your application and asynchronous Binding, that provides an ability to separate images and not to load all of them, if they not visible. So, let's start.

First of all we have to retrieve Flickr RSS feed. So, we'll create XmlDataProvider for this

<XmlDataProvider  
                 Source="http://api.flickr.com/services/feeds/photos_public.gne?tags=wallpaper&amp;format=rss_200" 
                 XPath="/rss/channel/item" x:Key="src"/>

This the first and really easy part. Nice, now we'll create data template and the host for it



<DataTemplate x:Key="img">
      <Image Source="{Binding XPath=media:content/@url}" Stretch="Uniform"/>
</DataTemplate>
 


<ListBox Grid.Row="1" ItemsSource="{Binding Source={StaticResource src}}"
            ItemTemplate="{StaticResource img}"/>

Fine, run it to see nothing. Why? because of XmlNamespace media, found in RSS 2.0. We should tell XmlDataProvider what's the namespace, so we have to add Namespace manager to point to http://search.yahoo.com/mrss/ URI, that actually describes media namespace. To do it, all you have to do it to add XmlNamespaceMapping to your resources and point XmlDataProvider to see it.


 



<XmlNamespaceMappingCollection x:Key="map">
      <XmlNamespaceMapping Uri="http://search.yahoo.com/mrss/" Prefix="media"/>
</XmlNamespaceMappingCollection>
 
<XmlDataProvider XmlNamespaceManager="{StaticResource map}" 
                     Source="http://api.flickr.com/services/feeds/photos_public.gne?tags=wallpaper&amp;format=rss_200" 
                     XPath="/rss/channel/item" x:Key="src"/>

So far so good, but now, when you'll run your application, it become frozen for 15 minutes, until all images will be loaded, then it force your CPU to 100% for 2 minutes to render all those images. Hm, well, you have really fast internet and your computer has Four Quad Core CPU. In this case, your application freeze for only three minutes. Really bad, isn't it?


So what to do? Let try first of all convert our data source to asynchronous. In order to do it, we'll set IsAsynchronous property to"True". Additional fix we'll add is to force the data provider to work, even before, we'll need it. Set IsInitialLoadEnabled="True". Compile.


Now it's better. We just saved the interface from freezing for 10 seconds (while RSS loaded). Still bad, really bad! The actual problem is not in DataProvider, but in all those urls and large images in it. Let make our DataBinding to work asynchronous. Let's make it load images one by one.


We'll add another property to the binding expression of our hosting control. And now it'll looks like this


 



<ListBox ItemsSource="{Binding IsAsync=True, Source={StaticResource src}}"
             ItemTemplate="{StaticResource img}"/>

Very well. Now our UI is released and all images loaded one-by-one without holding STAThread, which actually run it.


The next challenge is to force an application not to load all those images, which are not visible and see how many images already downloaded. For this, we'll add eventhandler Loaded event of image in the template. Add read only Dependency Property to your window and bind it to textblock to see.



int loadedTotalCounter = 0;
        void onImgLoaded(object sender, RoutedEventArgs e)
        {

SetValue(ImagesTotalPropertyKey, ++loadedTotalCounter);
        }


public static readonly DependencyPropertyKey ImagesTotalPropertyKey = DependencyProperty.RegisterReadOnly("ImagesTotal",

    typeof(int),

    typeof(Window1),

    new PropertyMetadata(0));

 

public static readonly DependencyProperty ImagesTotalProperty = ImagesTotalPropertyKey.DependencyProperty;

 

public int ImagesTotal

{

    get { return (int)GetValue(ImagesTotalProperty); }

}

Wow, if we'll put our ListView into grid, all invisible items will not being loaded, until we'll reveal them. Nice feature. Really nice, but why our counter of loaded images grows, even if the images are invisible?


The reason is simple, ImageSource is asynchronous property, so when we even got the source, this does not means, that the image is really loaded, so how to figure the actually number of loaded images. Let's dive deeper into ImageSource.


The object behind the ImageSource is BitmapFrame and it's (as we already tell) asynchronous object, so it has Download-related events such as DownloadCompleted, DownloadProgress and DownloadFailed. Let's put another readonly DP to our window, to cound actually downloaded frames


 



public static readonly DependencyPropertyKey ImagesLoadedPropertyKey = DependencyProperty.RegisterReadOnly("ImagesLoaded",
            typeof(int),
            typeof(Window1),
            new PropertyMetadata(0));
 
        public static readonly DependencyProperty ImagesLoadedProperty = ImagesLoadedPropertyKey.DependencyProperty;
 
        public int ImagesLoaded
        {
            get { return (int)GetValue(ImagesLoadedProperty); }
        }
 
        public static readonly DependencyPropertyKey ImagesTotalPropertyKey = DependencyProperty.RegisterReadOnly("ImagesTotal",
            typeof(int),
            typeof(Window1),
            new PropertyMetadata(0));

Now, let's get actual source and subscribe it to counter


 


void onDownloadCompleted(object sender, EventArgs e)
        {
            SetValue(ImagesLoadedPropertyKey, ++loadedCounter);
        }
 
 
 
        int loadedCounter = 0;
        int loadedTotalCounter = 0;
        void onImgLoaded(object sender, RoutedEventArgs e)
        {
            Image img = sender as Image;
            if (img != null)
            {
                BitmapFrame bimage = img.Source as BitmapFrame;
                if (bimage != null)
                {
                    bimage.DownloadCompleted += new EventHandler(onDownloadCompleted);
                }
            }
            SetValue(ImagesTotalPropertyKey, ++loadedTotalCounter);

        }

That's works great, but so some reason, DownloadCompleted event not always being fired. Recently, when the framework "skips" it, the image appears too fast. Is it bug? Not really. The real source for image is not network, but local cache, so not always BitmapFrame downloaded. And if it is not, why to fire event. Add handler for this, set break point and see.


 


void onDownloadCompleted(object sender, EventArgs e)
        {
            SetValue(ImagesLoadedPropertyKey, ++loadedCounter);
        }
 
 
 
        int loadedCounter = 0;
        int loadedTotalCounter = 0;
        void onImgLoaded(object sender, RoutedEventArgs e)
        {
            Image img = sender as Image;
            if (img != null)
            {
                BitmapFrame bimage = img.Source as BitmapFrame;
                if (bimage != null)
                {
                    //why this?
                    //Actually, the images are loaded from cache, so we have to check if it's downloading before, we'll want it to complete an action :)
                    if (bimage.IsDownloading)
                    {
                        bimage.DownloadCompleted += new EventHandler(onDownloadCompleted);
                    }
                    else
                    {
                        SetValue(ImagesLoadedPropertyKey, ++loadedCounter);
                    }
                }
            }
            SetValue(ImagesTotalPropertyKey, ++loadedTotalCounter);

        }

Dunno. Now we have everything working asynchronous way and our UI is free for action. Does WPF architecture is really genius to think about all those cases?


Source code for this article

No comments: