1

I'm developing an app for UWP.

I need to load a folder that contains around 700 small pictures. This is the method I use to load the pictures into memory:

    private async Task<ObservableCollection<ImageSource>> LoadPicturesAsync()
    {
        var pictureList = new ObservableCollection<ImageSource> { };
        pictureFiles.ForEach(async file =>
        {
            var img = new BitmapImage();
            pictureList.Add(img);
            var stream = await file.OpenReadAsync();
            await img.SetSourceAsync(stream);
        });

        return pictureList;
    }

When this method gets called by the constructor of my view model, the view seems to be blocked (unresponsive) for about 6 seconds.

This is strange because all IO operations are done asynchronously, and the only thing running in UI thread is creating of BitmapImage objects in a foreach loop. This is so fast it shouldn't block the UI thread.

My question is: Why did the UI thread block for 6 seconds knowing that I run all IO operations asynchronously? And how to fix this so UI thread is not blocked?

This is how I call that method:

    private async Task Init()
    {
        PictureList = await LoadPicturesAsync();
    }

    //constructor
    public MainVewModel(){
        Init();
    }
disklosr
  • 1,536
  • 17
  • 26
  • How/where do you call Init()? Where is your view model construcor called? Have you tried to create only new collection in constructor: `PictureList = new ObservableCollection();` and then fill the collection asynchronously outside constructor? (`... await img.SetSourceAsync(stream); PictureList.Add(img);` - changed order) – Romasz Mar 12 '16 at 16:39
  • What type is `pictureFiles` and where does it come from? – Gediminas Masaitis Mar 12 '16 at 16:41
  • @Romasz see my updated question. – disklosr Mar 12 '16 at 17:01
  • @GediminasMasaitis it's a list of StorageFile objects – disklosr Mar 12 '16 at 17:02
  • @Romasz cant see any updates? – Rohit Mar 12 '16 at 17:02
  • @Romasz you should now. Sorry. – disklosr Mar 12 '16 at 17:04
  • @disklosr Try like this: in your viewmodel constructor create only the collecition: `PictureList = new ObservableCollection(); ` then in for example *Loaded* event of your page, fill the list with items, without assigning PictureList again, just fire the foreach and move `pictureList.Add(img);` at the and. – Romasz Mar 12 '16 at 17:48
  • @Romasz Problem with this approach is if I assign it before adding observable collection will fire 3 events each time I add, so there's going to be 3x700 events firing which may also slow the UI. But I will try your suggestion to see how it behaves. – disklosr Mar 12 '16 at 18:21
  • @disklosr When dealing with such an amount of images/data - think of using *IncrementalLoading*. IMO this will be the best option. Apart from that - what events you have on your mind? – Romasz Mar 12 '16 at 18:23
  • `List.ForEach` does not support aync, you need to use a traditional `foreach`, it is not going to wait for the `await` to finish before starting the next loop because it is treating it as a `async void` function. – Scott Chamberlain Mar 12 '16 at 19:59

1 Answers1

0

Why is this happening?

Because you are trying to run so many threads concurrently since you are using the ForEach method of the list and passing an async action into it. Replace the ForEach with a for-each and you'll be fine.

private async Task<ObservableCollection<ImageSource>> LoadPicturesAsync() {        
    foreach (var file in pictureFiles) {
       var stream = await file.OpenReadAsync();
       var image = new BitmapImage(); 
       await image.SetSourceAsync(stream);
       pictureList.Add(image );
    }
}       

private async Task Init() {
    PictureList = await LoadPicturesAsync();
}

After all, I see you call the Init method in your view-model:

//constructor
public MainVewModel(){
    Init();
}

Since Init returns a Task, you need be aware that the PictureList property/field may not be set when construction finishes, so if you attempt to access it immediately after instantiate, you may face a NullReferenceException.

MainViewModel viewModel = new MainViewModel();
var pics = viewModel.PictureList;
var count = pics.Count; // High chance of NullReferenceException

In order to avoid that, you may consider defining an static CreateAsync method for you view-model. More information can be found here.

Community
  • 1
  • 1
Mehrzad Chehraz
  • 5,092
  • 2
  • 17
  • 28
  • My init method is running in ui thread, but it returns instantly and rest of work (reading files to streams) is asynchronous so it shouldn't block UI right? Creating 700 BitmapSource objects is fast and doesn't block the UI. It's the loading of the files that introduces the issue. – disklosr Mar 12 '16 at 18:23
  • The rest of work runs on ui but it's fast enough not to block it for 6 seconds. It's just creating empty objects and adding them to list. Reading from file and loading images DOES NOT run on UI thread. – disklosr Mar 12 '16 at 18:30
  • Also, regarding your answer, creating BitmapImage objects is already running on UI thread in my version of code. The separation between creating objects in UI and loading them in background is what my code is already doing. – disklosr Mar 12 '16 at 18:34