ImageSource.FromStream with caching

MAZMAZ CHMember ✭✭
edited August 2014 in Xamarin.Forms

My webservice return a custom JSON (with base64-encoded image byte data) instead of a normal byte content of image type.

Since just ImageSource.FromUrl has caching, I tried to implement it myself (using ImageDownloader from https://github.com/xamarin/prebuilt-apps/blob/5691957f41af865704eaae153f5fa9b88998a88c/EmployeeDirectory/EmployeeDirectory/Utilities/ImageDownloader.cs as basis)

I have a Command responsible to load my Image from the web or from the filesystem (when cached):

private ImageSource _image = null;
public ImageSource MainImageSource
{
    get { return _image; }
    set
    {
        _image = value;
        OnPropertyChanged("MainImageSource");
    }
}
// more code
public Command LoadImage
{
    get
    {
        return new Command(async (id) =>
        {
            var stream = await _imageDownloader.GetDraftImageAsync();
            MainImageSource = ImageSource.FromStream(() => stream);
        });
    }
}

The ImageDownloader uses the IsolatedStorage to save the image to the filesystem on the different platforms.

This is perfectly working, until I navigate away and come back to the view (Page)... When I navigate away, my streams (I've more since its a listview) are being disposed by Xamarin, when I come back, it tries to read from a disposed Stream and raises an ObjectDisposedException.

The question is, how can I avoid this? I was thinking like, if the stream is closed it will call again the LoadImage command, but how can I tell from the _image (ImageSource) field if has being disposed?

Or why can't just give the cache file path as ImageSource? If I use ImageSource.FromFile("pathToIsolatedStorageFile") is not working because FromFile expect a file which is Resource bundled to the application instead of being dynamically created.

Thanks a lot for any inputs

Answers

  • CraigDunnCraigDunn USXamarin Team Xamurai

    Is the exception being thrown from this code, or from your _imageDownloader?

  • MAZMAZ CHMember ✭✭

    Hi Craig, neither, the exception is generated from a Xamarin.Forms component (StreamImagesourceHandler), here the full stacktrace:

    [System.ObjectDisposedException] = {System.ObjectDisposedException: Cannot access a closed Stream.
       at System.IO.__Error.StreamIsClosed()
       at System.IO.MemoryStream.Read(Byte[] buffer, Int32 offset, Int32 count)
       at System.Windows.Media.Imaging.BitmapSource.SetSourceInternal(Stream streamSource)
       at System.Windows.Media.Imaging.BitmapImage.SetSourceInternal(Stream streamSource)
       at System.Windows.Media.Imaging.BitmapSource.SetSource(Stream streamSource)
       at Xamarin.Forms.Platform.WinPhone.StreamImagesourceHandler.<LoadImageAsync>d__0.MoveNext()
    --- End of stack trace from previous location where exception was thrown ---
       at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
       at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
       at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
       at Xamarin.Forms.Platform.WinPhone.ImageRenderer.<SetSource>d__0.MoveNext()
    --- End of stack trace from previous location where exception was thrown ---
       at System.Runtime.CompilerServices.AsyncMethodBuilderCore.<ThrowAsync>b__3(Object state)
    
  • CraigDunnCraigDunn USXamarin Team Xamurai

    Okay - but that stream is coming from your code right, in _imageDownloader.GetDraftImageAsync? It's impossible to guess what the problem might be just from the information you've provided.

    Other users have found they needed stream.Position = 0; to read properly, but yours could be a different issue (given the exception specifically mentions being disposed).

  • MAZMAZ CHMember ✭✭
    edited August 2014

    Thanks Craig, actually I was the one proposing the stream.Position = 0 solution :)

    The stream is created in the GetDraftImageAsync (but NOT disposed). Xamarin will dispose all streams associated with the current page when you navigate to another page (i.e. PushPageAsync) which is correct to save memory.
    The problem is, that once you come back to a previous page and this is displayed, Xamarin will try to load the closed stream generating the exception.

    I'm not telling this is wrong or my code should work, just how am I suppose to work with this behavior and the ImageSource.LoadStream method.

    The only way I can see is:

    • On page (re)appearing reload all streams manually
    • On ImageSource check if the associated stream is already been closed (how?) and if so reload the stream from cache/internet.
      Example
    public ImageSource MainImageSource
    {
        get {  
            if(!_image.Stream.IsDisposed()){
                return _image; 
            }
            else {
                LoadImage.Execute(null);
                return ImageSource.LoadFile("imageloading.png");
            }
        }
        set
        {
            _image = value;
            OnPropertyChanged("MainImageSource");
        }
    }
    
    • Add a pseudo ImageSource.LoadFromIsolatedStorage(String filepath) extension so that the framework will handle image open/close directly
    • Other?

    I hereby add the code responsible for the stream creation

    private async Task<Stream> GetImage(String url)
    {
        var filename = Uri.EscapeDataString(url);
        if (HasLocallyCachedCopy(url))
        {
            var cachedStream = OpenStorage(filename, FileMode.Open);
            // Stream will be disposed by ImageSource 
            return cachedStream;
        }
        else
        {
            Debug.WriteLine("Image NOT cached for " + url);
            using (var d = await _http.GetDraftImageStream(url))
            {
                using (var o = OpenStorage(filename, FileMode.Create))
                {
                    d.CopyTo(o);
                }
            }
            var newStream = OpenStorage(filename, FileMode.Open);
            // Stream will be disposed by ImageSource 
            return newStream;
        }
    }
    protected virtual Stream OpenStorage(string fileName, FileMode mode)
    {
        return _store.OpenFile(Path.Combine("ImageCache", fileName), mode);
    }
    
  • OnnoJOnnoJ NLUniversity ✭✭

    I'm doing something similar in one of my apps and I'm using ImageSource.FromFile() instead of ImageSource.FromStream which is working very well.

  • FredyWengerFredyWenger CHInsider ✭✭✭✭✭

    @CraigDunn (and at the other readers of this thread):

    Your positing :

    Other users have found they needed stream.Position = 0; to read properly, but yours could be a different issue (given the exception specifically mentions being disposed).

    has helped me :smile:

    In my app the user can select an image from MediaPicker.
    Then, I have to resize the image, before I send it to the webserver (to store it on a SQL-Server).
    In WP, I had a crash:

    System.Runtime.InteropServices.COMException" in Microsoft.Phone.ni.dll.
      _message=The component cannot be found. (Exception from HRESULT: 0x88982F50)
    

    According to the meaningful error-message, I first thought, that something is missing in the WP-Runtime.
    I then have find this thread and added the stream.Position = 0;
    This is needed for WP (else the MemoryStream will be empty and then also the Byte-Array after the copy):

     Stream stream = Bild.Source; 
     using (MemoryStream ms = new MemoryStream())
     {
         stream.Position = 0; // needed for WP (in iOS and Android it also works without it)!!
         stream.CopyTo(ms);  // was empty without stream.Position = 0;
         imageData = ms.ToArray(); 
       }
    

    So thanks (and Like :))

  • GautamJainGautamJain INMember ✭✭✭

    @MAZ said:
    So, following the "If you want something done, do it yourself" quote I came up with the following solution.

    Instead of returning a Stream I return the image byte array from the GetDraftImageAsyncBytes() method.

    Instead of loading a "pre-created" stream, in the ImageSource lambda I create a new one each time:

    var bytes = await _imageDownloader.GetDraftImageAsyncBytes();
    MainImageSource = ImageSource.FromStream(() => new MemoryStream(bytes));
    

    The bytes will stay in memory, and the stream will be disposed each time but also recreated when Xamarin call the lambda function during rendering.

    I suppose this has memory implications, so I'm open for suggestion.

    Thanks for sharing :) This worked great!

Sign In or Register to comment.