13

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.

thumbmunkeys
  • 20,606
  • 8
  • 62
  • 110

5 Answers5

25

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...

Neil Turner
  • 2,712
  • 2
  • 18
  • 37
  • thank you, that blog post is great! I realized that http urls are cached, while https are not for whatever reasons. Do you have an idea why? – thumbmunkeys Jun 24 '13 at 16:00
  • 1
    that's possibly a legacy Silverlight reason - ie. don't cache HTTPS content in the browser. If you want to cache HTTPS resource, you'll have to write that yourself - I can add a couple of links to some good examples if you like? – Neil Turner Jun 24 '13 at 16:23
  • that silverlight argument sounds very reasonable, and regarding the examples, yes i like :) – thumbmunkeys Jun 24 '13 at 21:09
  • 1
    The links make this answer perfect :) Here are some additions: https://github.com/artem-zinnatullin/jet-image-loader – Alex Sorokoletov Apr 07 '14 at 06:18
5

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}}"/>
Artem Zinnatullin
  • 4,305
  • 1
  • 29
  • 43
  • What do you mean with "non-leaking"? Memory leaks? If so, memory cache based on `WeakRefDictionary`, so `GC` can collect objects when he wants, no memory leaks :) You can write me on GitHub if you got questions – Artem Zinnatullin Nov 02 '13 at 22:20
  • Yep, but what about nulling unused images? There's famous leak in WP. I made this workaround some time ago: https://simca.codeplex.com/. More details about leak: http://stackoverflow.com/questions/9549980/memory-leak-changing-images-in-windows-phone-7 http://stackoverflow.com/questions/17571825/how-to-free-memory-hold-by-images-in-data-template-in-windows-phone-8-app?rq=1 http://stackoverflow.com/questions/13816569/windows-phone-listbox-with-images-out-of-memory?lq=1 http://stackoverflow.com/questions/18127027/memory-consumption-of-bitmapimage-image-control-in-windows-phone-8 – Vitalii Vasylenko Nov 03 '13 at 17:00
  • Hm, I analyzed my `wp8` app where we got huge amount of big and small images loading via `JetImageLoader` with memory profiler and I saw that after closing page with images GC cleaned memory to amount ~= memory amount what was before opening this page. May be this bug was fixed in `wp8`? – Artem Zinnatullin Nov 03 '13 at 21:09
  • Anyway, thank you for that issue, I'll try to check it in nearest time – Artem Zinnatullin Nov 03 '13 at 21:10
  • I believe, its wp7 issue. I didnt test wp8 :) – Vitalii Vasylenko Nov 03 '13 at 21:30
1

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)

Stefan Wexel
  • 1,154
  • 1
  • 8
  • 14
1

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/

Community
  • 1
  • 1
Daniel Luberda
  • 7,374
  • 1
  • 32
  • 40
  • the line `CacheDuration="30"` has the same effect than doing`CacheDuration = TimeSpan.FromDays(30)` in C#, right? – Enrique Zavaleta Apr 27 '17 at 00:17
  • Hi Thanks for excellent sample. I am using binding for insert image in to the ffimageloading in android. If I set cache duration to specific time for that ffimage, is it working fine? – Deepak Dec 21 '17 at 11:40
  • FFImage is now depreciated, you should use `MvxCachedImageView`. I'll update my answer. It works on all platforms. You should set `CacheDuration `property which is of `Timestamp` type. – Daniel Luberda Dec 21 '17 at 14:03
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;
    }
}