thumbmunkeys
thumbmunkeys

Reputation: 20764

Image Source and Caching

I use the following code to show images from a webserver:

   <Image Source="{Binding Url}" />

The image gets automatically downloaded, and I assume there is also some caching based on the Url.

My problem is, that when the app is offline, the assumably cached images are not shown.

Is there any way to change the caching behavior, so that images are also loaded when there is no network available? Pointers to documentation regarding the caching would be very helpful as well.

Upvotes: 13

Views: 19538

Answers (5)

Daniel Luberda
Daniel Luberda

Reputation: 7454

You could also use FFImageLoading (https://github.com/molinch/FFImageLoading/)

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:MvxCachedImage Name="image"
    VerticalAlignment="Stretch" 
    HorizontalAlignment="Stretch"
    LoadingPlaceholder="loading.png"
    ErrorPlaceholder="error.png"
    RetryCount="3"
    RetryDelay="250"
    DownsampleHeight="300"
    ImagePath="http://lorempixel.com/output/city-q-c-600-600-5.jpg">
</ff: MvxCachedImage >

Sample projects here: https://github.com/molinch/FFImageLoading/tree/master/samples/

Upvotes: 1

My solution: (save image from web to local storage and bind saved image to page)

XAML

<ListView ItemsSource="{Binding Items}">
<ListView.ItemTemplate>
    <DataTemplate>
        <!--Some code removed-->
        <Image Source="{Binding Img_Thumb.Result}" />
    </DataTemplate>
</ListView.ItemTemplate>

DataModel

public class DataModel_ListOfEvents
{
    public DataModel_ListOfEvents(String img_thumb)
    {
        this.Img_Thumb = new NotifyTaskCompletion<string>(JsonCached.ImageFromCache2(img_thumb));
    }
    public NotifyTaskCompletion<string> Img_Thumb { get; private set; }
}

public sealed class SampleData_ListOfEvents
{
    private static SampleData_ListOfEvents _sampleDataSource = new SampleData_ListOfEvents();

    private ObservableCollection<DataModel_ListOfEvents> _items = new ObservableCollection<DataModel_ListOfEvents>();
    public ObservableCollection<DataModel_ListOfEvents> Items { get { return this._items; } }
}

Magic

public class JsonCached
{
    public static async Task<string> ImageFromCache2(string path)
    {
        int ru = path.IndexOf(".ru") + 4;// TODO: .com .net .org
        string new_path = path.Substring(ru).Replace("/", "\\");

        StorageFolder localFolder = ApplicationData.Current.LocalFolder;
        try
        {
            Stream p = await localFolder.OpenStreamForReadAsync(new_path);
            p.Dispose();
            System.Diagnostics.Debug.WriteLine("From cache");
            return localFolder.Path + "\\" + new_path;
        }
        catch (FileNotFoundException)
        {

        }
        catch (Exception e)
        {
            System.Diagnostics.Debug.WriteLine("{0}", e.Message);
        }

        StorageFile storageFile = await localFolder.CreateFileAsync(new_path, CreationCollisionOption.OpenIfExists);

        Uri Website = new Uri(path);
        HttpClient http = new HttpClient();
        // TODO: Check connection. Return message on fail.
        System.Diagnostics.Debug.WriteLine("Downloading started");
        byte[] image_from_web_as_bytes = await http.GetByteArrayAsync(Website);

        MakeFolders(localFolder, path.Substring(ru));

        Stream outputStream = await storageFile.OpenStreamForWriteAsync();
        outputStream.Write(image_from_web_as_bytes, 0, image_from_web_as_bytes.Length);
        outputStream.Position = 0;

        System.Diagnostics.Debug.WriteLine("Write file done {0}", outputStream.Length);

        outputStream.Dispose();
        return localFolder.Path + "\\" + new_path;
    }

    private static async void MakeFolders(StorageFolder localFolder, string path)
    {
        //pics/thumbnail/050/197/50197442.jpg
        int slash = path.IndexOf("/");
        if (slash <= 0) // -1 Not found
            return;

        string new_path = path.Substring(0, slash);
        StorageFolder opened_folder = await localFolder.CreateFolderAsync(new_path, CreationCollisionOption.OpenIfExists);
        string very_new_path = path.Remove(0, new_path.Length + 1);
        MakeFolders(opened_folder, very_new_path);
    }
}

NotifyTaskCompletion

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.ComponentModel;

namespace App2.NotifyTask
{
    public sealed class NotifyTaskCompletion<TResult> : INotifyPropertyChanged
    {
        public NotifyTaskCompletion(Task<TResult> task)
        {
            Task = task;
            if (!task.IsCompleted)
            {
                var _ = WatchTaskAsync(task);
            }
        }
        private async Task WatchTaskAsync(Task task)
        {
            try
            {
                await task;
            }
            catch
            {
            }
            var propertyChanged = PropertyChanged;
            if (propertyChanged == null)
                return;
            propertyChanged(this, new PropertyChangedEventArgs("Status"));
            propertyChanged(this, new PropertyChangedEventArgs("IsCompleted"));
            propertyChanged(this, new PropertyChangedEventArgs("IsNotCompleted"));
            if (task.IsCanceled)
            {
                propertyChanged(this, new PropertyChangedEventArgs("IsCanceled"));
            }
            else if (task.IsFaulted)
            {
                propertyChanged(this, new PropertyChangedEventArgs("IsFaulted"));
                propertyChanged(this, new PropertyChangedEventArgs("Exception"));
                propertyChanged(this,
                  new PropertyChangedEventArgs("InnerException"));
                propertyChanged(this, new PropertyChangedEventArgs("ErrorMessage"));
            }
            else
            {
                propertyChanged(this,
                  new PropertyChangedEventArgs("IsSuccessfullyCompleted"));
                propertyChanged(this, new PropertyChangedEventArgs("Result"));
            }
        }
        public Task<TResult> Task { get; private set; }
        public TResult Result { get { return (Task.Status == TaskStatus.RanToCompletion) ? Task.Result : default(TResult); } }
        public TaskStatus Status { get { return Task.Status; } }
        public bool IsCompleted { get { return Task.IsCompleted; } }
        public bool IsNotCompleted { get { return !Task.IsCompleted; } }
        public bool IsSuccessfullyCompleted { get { return Task.Status == TaskStatus.RanToCompletion; } }
        public bool IsCanceled { get { return Task.IsCanceled; } }
        public bool IsFaulted { get { return Task.IsFaulted; } }
        public AggregateException Exception { get { return Task.Exception; } }
        public Exception InnerException { get { return (Exception == null) ? null : Exception.InnerException; } }
        public string ErrorMessage { get { return (InnerException == null) ? null : InnerException.Message; } }
        public event PropertyChangedEventHandler PropertyChanged;
    }
}

Upvotes: 1

Artem Zinnatullin
Artem Zinnatullin

Reputation: 4447

I have got a solution for you. It is JetImageLoader, I created it for application, where we need to load, cache and show big amount of logos, icons and so on.

It can be used as binding converter, so you should not even change your code! Just update your XAMLs!

Please, check out samples in repository, you'll love it ;)

Features:

  • Caching on disk
  • Caching in memory
  • Fully asynchronous
  • Available as binding converter or programmatically from your code
  • Fully open source, fork and improve it!

Here is the example:

<Image Source="{Binding ImageUrl, Converter={StaticResource MyAppJetImageLoaderConverter}}"/>

Upvotes: 5

Neil Turner
Neil Turner

Reputation: 2727

BitmapImage automatically caches remote images by default. It's best used in conjunction with CreateOptions="BackgroundCreation" for the best performance.

<Image Height="100" Width="100" Margin="12,0,9,0">
  <Image.Source>
    <BitmapImage UriSource="{Binding ImgURL}" CreateOptions="BackgroundCreation"/>
  </Image.Source>
</Image>

This MSDN blog post, old but still relevant, lists and explains all the CreationOptions and that caching is automatic in most modes.

I use these options to display many news items with images and it works well. I can load the list of articles, exit the app and turn Flight Mode to On, then start a new instance of the app and the images still load up.

Manual Approach

If you'd like to control the caching yourself and cache HTTPS resources then there are few good examples...

Upvotes: 25

Stefan Wexel
Stefan Wexel

Reputation: 1154

I don't think there is a build in way to do it, but you could save the images in IsolatedStorage and use a Converter that checks the internet availability and either returns the online or offline url.

A quick search yielded this which might be exactly what you are looking for (it's compatible to Windows Phone 7 and might not be the best solution for Windows Phone 8)

Upvotes: 1

Related Questions