0

this is an extension to my previous question how should a metro app cache images for tombstoning (and should it)?

The solutions that I have been finding are to use HttpClient, but this results in an unnecessary second hit per file on the web server.

Is there anyway to save the existing image/stream from a Image/ImageSource (using the ImageOpened event to ensure it is available) ?

Solution must work with current RP API as this is an area where large changes have been made from CP.

Community
  • 1
  • 1
rob
  • 8,134
  • 8
  • 58
  • 68

3 Answers3

2

First of all please note, that without doing anything custom (i.e. using a URI as an Image.Source), there's already some kind of cache implemented as if you have a Fiddler running next to your app, you'll see that requests are issued once and not each time an item is displayed.

This being said, if you want some kind of persistent cache, you could create a Converter which checks if the desired image is in the App's temporary directory and downloads it if it's not the case.

Here's a way to do it:

public sealed class UriToCachedImageConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, string language)
    {
        Uri uri = value as Uri;
        if (uri == null)
            return null;

        // Don't let user know those are images, yet keep a unique and consistent name.
        string fileName = Path.GetFileName(uri.AbsolutePath);
        StorageFile file = null;
        try
        {
            Task<StorageFile> task = ApplicationData.Current.TemporaryFolder.GetFileAsync(fileName).AsTask<StorageFile>();
            task.Wait();
            file = task.Result;
        }
        catch (AggregateException ex)
        {
            if (ex.InnerException is FileNotFoundException)
            {
                // http://social.msdn.microsoft.com/Forums/en-US/winappswithcsharp/thread/1eb71a80-c59c-4146-aeb6-fefd69f4b4bb/
            }
            else
            {
                throw;
            }
        }

        if (file == null)
        {
            // File isn't present in cache => download it.

            // This creates a file in the app's INetCache
            // but we can't retrieve its path or know its name for sure
            // hence we're not using this file as our cache file.
            Task<StorageFile> streamTask = StorageFile.CreateStreamedFileFromUriAsync(fileName, uri, RandomAccessStreamReference.CreateFromUri(uri)).AsTask<StorageFile>();
            streamTask.Wait();
            StorageFile storageFile = streamTask.Result;

            // Copy file to our temporary directory (can't move as we don't have write access on the initial file).
            Task<StorageFile> copyTask = storageFile.CopyAsync(ApplicationData.Current.TemporaryFolder, fileName, NameCollisionOption.ReplaceExisting).AsTask<StorageFile>();
            copyTask.Wait();
            file = copyTask.Result;
        }

        Task<IRandomAccessStreamWithContentType> openTask = file.OpenReadAsync().AsTask<IRandomAccessStreamWithContentType>();
        openTask.Wait();
        IRandomAccessStreamWithContentType randomAccessStream = openTask.Result;

        BitmapImage bitmapImage = new BitmapImage();
        bitmapImage.SetSource(randomAccessStream);
        return bitmapImage;
    }

    public object ConvertBack(object value, Type targetType, object parameter, string language)
    {
        throw new NotImplementedException();
    }
}

Note: This code snippet also illustrates how to call async methods from a synchronous method ;)

canderso
  • 662
  • 1
  • 7
  • 19
  • Thank you for posting this! For me, CreateStreamdFileFromUriAsync retrieves the file, but I get the exception "The system cannot find the file specified. (Exception from HRESULT: 0x80070002)" when I try to do CopyAsync from that streamed file to the storagefile to actually cache it. Am I missing a step here? – SelAromDotNet May 06 '13 at 23:27
0

I can not see a way of doing what I require from the ImageOpened event but instead I have found a working model.

instead of binding the image uri to the Image Source and letting metro download the image, instead bind to the Tag of the Image and then after the data binding is complete loop through each Image in xaml and download the image as a RandomAccessStream, saving the stream and setting the Image Source to a BitmapImage created from the stream.

RandomAccessStreamReference rasf = RandomAccessStreamReference.CreateFromUri(new Uri(MyImage.Tag as string));
BitmapImage bitmapImage = new BitmapImage();
var ras = await rasf.OpenReadAsync();
bitmapImage.SetSource(ras);
MyImage.Source = bitmapImage;

I also investigated automating the process by putting the code in a converter and a view model property, but due to the async call neither of these was possible.

rob
  • 8,134
  • 8
  • 58
  • 68
0

You could also use FFImageLoading (https://github.com/molinch/FFImageLoading/) or see its sources to see how it's implemented (https://github.com/molinch/FFImageLoading/tree/master/FFImageLoading.Windows)

Features

  • Xamarin.iOS (min iOS 7), Xamarin.Android (min Android 4), Xamarin.Forms and Windows (WinRT, UWP) support
  • Configurable disk and memory caching
  • Deduplication of similar download/load requests
  • Error and loading placeholders support
  • Images can be automatically downsampled to specified size (less memory usage)
  • WebP support
  • Image loading Fade-In animations support
  • Can retry image downloads (RetryCount, RetryDelay)
  • On Android transparency is disabled by default (configurable). Saves 50% of memory
  • Transformations support
    • BlurredTransformation
    • CircleTransformation, RoundedTransformation, CornersTransformation
    • ColorSpaceTransformation, GrayscaleTransformation, SepiaTransformation
    • FlipTransformation
    • Supports custom transformations (native platform ITransformation implementations)

It's just as simple as:

<ff:FFImage Name="image"
    VerticalAlignment="Stretch" 
    HorizontalAlignment="Stretch"
    LoadingPlaceholder="loading.png"
    ErrorPlaceholder="error.png"
    CacheDuration="30"
    RetryCount="3"
    RetryDelay="250"
    DownsampleHeight="300"
    Source="http://lorempixel.com/output/city-q-c-600-600-5.jpg">
</ff:FFImage>

Sample project here: https://github.com/molinch/FFImageLoading/tree/master/samples/Simple.WinPhone.Sample

Community
  • 1
  • 1
Daniel Luberda
  • 7,374
  • 1
  • 32
  • 40