locked
How do you create a BitmapImage on a background thread? RRS feed

  • Question

  • I need to create quite a few BitmapImages (~100 - 10000). Once created, they will never be written to.

    In WPF I could create the BitmapImage on any thread, and, as long as I froze it, I could use it later on the UI thread.

    My current Windows Store App code:

            public static async Task<BitmapImage> CreateBitmapImageAsync(StorageFile storageFile)
            {
                var fileStream = await storageFile.OpenReadAsync();

                BitmapImage bitmapImage = null;
                await App.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
                {
                    bitmapImage = new BitmapImage();
                    bitmapImage.SetSource(fileStream);
                });

                return bitmapImage;
            }

    Because these are read only objects, isn't it theoretically safe to leave the UI thread out of their creation? That is to say, is there a way to create BitmapImages solely on background threads so that the UI thread is left alone to do more important things?

    Saturday, November 9, 2013 5:11 AM

Answers

All replies

  • Set the BitmapImage with the SetSourceAsync method rather than with SetSource. That will set the source asynchronously without blocking.
    Saturday, November 9, 2013 8:17 AM
    Moderator
  • Thanks for the quick response, Rob, and on a weekend too! This did lead me to a workable answer.

    If you have any way to pass feedback along to the folks that write the BitmapImage API, would you forward the bit below?

    ===

    Loading images for a virtualized view seems difficult with the current BitmapImage API.

    Think of a map view. It's made up of readonly tiled BitmapImages. Creating a BitmapImage unavoidably involves some pretty high cost operations: network/disk IO, and decoding. To make your map view appear fast, you have to be deliberate about the order in which you create your tiles. Stuff on screen is more important than stuff just off-screen, and stuff just off-screen is more important than stuff far off-screen. Loading of tiles is prioritized. If the user moves the view, the load order needs to be redetermined.

    The need to reprioritize BitmapImage creation means BitmapImage.SetSourceAsync is not viable on its own. If you have a prioritized list of tiles to load and you call SetSourceAsync on all of them, then all those IO requests queue up at the disk/nework and must be serviced before new load requests. Now imagine the user moves the view- all tiles have new load priorities... but you have to wait for that previous batch of IO to complete before you can load anything new.

    One approach to this would be to put loading of images into TPL Tasks and create a specialized TaskScheduler that ensures tasks are executed in priority order. This would let you respond to re-prioritization; thereby, allowing you to ensure that the tiles that are on-screen are always loaded first.

    The prioritized TaskScheduler would only work if it waited for each task to complete before starting a new one. The TaskScheduler gaurds system level resources (CPU, IO etc), so it would be great if the whole app used it... which means it would be good if the TaskScheduler could blindly treat all tasks the same way... which means it shouldn’t need to know about BitmapImage.ImageOpened. This means there needs to be a way to create a BitmapImage that appears synchronous.

    In WPF this was not super easy, but at least it was possible in a function that fit on the screen:

           public static BitmapImage BitmapImage(Uri path, Rotation rotation, int? desiredWidth = null)

           {

               MiscUtil.MainThreadDiskAccessWarning();


               var bitmapImage = new BitmapImage();


               try

               {

                   bitmapImage.BeginInit();


                   if (desiredWidth != null)

                       bitmapImage.DecodePixelWidth = (int)desiredWidth;


                   bitmapImage.CacheOption = BitmapCacheOption.OnLoad;

                   bitmapImage.UriSource = path;

                   bitmapImage.Rotation = rotation;

                   bitmapImage.EndInit();

               }

               catch (ArgumentException)

               {

                   try

                   {

                       bitmapImage = new BitmapImage();

                       bitmapImage.BeginInit();


                       // Possibly a corrupt color profile. Try again ignoring the color profile.

                       bitmapImage.CreateOptions = BitmapCreateOptions.IgnoreColorProfile;


                       if (desiredWidth != null)

                           bitmapImage.DecodePixelWidth = (int)desiredWidth;


                       bitmapImage.CacheOption = BitmapCacheOption.OnLoad;

                       bitmapImage.UriSource = path;

                       bitmapImage.Rotation = rotation;

                       bitmapImage.EndInit();


                       Trace.TraceWarning("Image with corrupted color profile: " + path);

                   }

                   catch (Exception e)

                   {

                       Trace.TraceError("Exception in AsyncLoadedImage.BitmapImage(): " + e);

                       throw;

                   }

               }


               bitmapImage.Freeze();


               return bitmapImage;

           }


    In WindowsStore, it seems to require its own class:

       public class BitmapImageBuilder

       {

           private BitmapImage _bitmapImage;

           private readonly ManualResetEvent _manualResetEvent = new ManualResetEvent(false);

           protected CancellationToken? CancelationToken;


           public async Task<BitmapImage> CreateBitmapImageAsync(

               StorageFile storageFile,

               CancellationToken? cancellationToken,

               int? desiredWidth = null)

           {

               CancelationToken = cancellationToken;


               if (IsCanceled())

                   return null;


               var fileStream = await storageFile.OpenReadAsync();


               if (IsCanceled())

                   return null;


               return await CreateBitmapImageAsync(fileStream, desiredWidth);

           }


           private async Task<BitmapImage> CreateBitmapImageAsync(

               IRandomAccessStream stream,

               int? desiredWidth = null)

           {

               if (IsCanceled())

                   return null;


               App.Dispatcher.RunAsync(

                   CoreDispatcherPriority.Normal,

                   () => CreateImage(stream, desiredWidth));


               _manualResetEvent.WaitOne();


               if (IsCanceled())

                   return null;


               return _bitmapImage;

           }


           private bool IsCanceled()

           {

               return CancelationToken != null &&

                      ((CancellationToken)CancelationToken).IsCancellationRequested;

           }


           private async Task CreateImage(

               IRandomAccessStream fileStream,

               int? desiredWidth)

           {

               _bitmapImage = new BitmapImage();

               _bitmapImage.ImageOpened += OnImageOpened;

               _bitmapImage.ImageFailed += OnImageFailed;


               if (desiredWidth != null)

                   _bitmapImage.DecodePixelWidth = (int)desiredWidth;


               if (IsCanceled())

               {

                   _manualResetEvent.Set();

                   return;

               }


               await _bitmapImage.SetSourceAsync(fileStream);

           }


           private void OnImageOpened(object sender, RoutedEventArgs e)

           {

               _bitmapImage.ImageOpened -= OnImageOpened;

               _bitmapImage.ImageOpened -= OnImageFailed;

               _manualResetEvent.Set();

           }

           private void OnImageFailed(object sender, RoutedEventArgs e)

           {

               _bitmapImage.ImageOpened -= OnImageOpened;

               _bitmapImage.ImageOpened -= OnImageFailed;

               _bitmapImage = null;

               _manualResetEvent.Set();

           }

       }


    PS - Do let me know if you see an easier way


    • Edited by Tristan2 Thursday, November 14, 2013 6:47 PM Formatting
    Thursday, November 14, 2013 6:44 PM