locked
ImageSource.FromStream with caching RRS feed

  • Question

  • User64035 posted

    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

    Tuesday, August 12, 2014 3:48 PM

All replies

  • User14 posted

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

    Tuesday, August 12, 2014 4:52 PM
  • User64035 posted

    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)

    Tuesday, August 12, 2014 5:56 PM
  • User14 posted

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

    Tuesday, August 12, 2014 7:13 PM
  • User64035 posted

    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); }

    Tuesday, August 12, 2014 7:29 PM
  • User64035 posted

    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.

    Thursday, August 14, 2014 8:11 AM
  • User57638 posted

    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.

    Tuesday, November 11, 2014 8:54 PM
  • User65389 posted

    @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 :))

    Wednesday, March 18, 2015 12:20 PM
  • User40225 posted

    @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!

    Friday, November 29, 2019 5:19 PM
  • User371865 posted

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

    Thank you very much, You save my life.

    Tuesday, December 22, 2020 1:58 AM