0

I have the following code snippet that I use to create a List to add to a scrollviewer as a binding in a WPF application:

private void LoadThumbs(object sender, DoWorkEventArgs e)
{
        //ClearScreen();
        int max = (int)e.Argument;
        int current = 0;



        foreach (string filename in filenames)
        {
            Image thumbnail = new Image();
            Uri image_path = new Uri(filename);
            BitmapImage image = new BitmapImage(image_path);

            Thickness thumb_margin = thumbnail.Margin;
            thumb_margin.Bottom = 2.5;
            thumb_margin.Top = 2.5;
            thumb_margin.Left = 2.5;
            thumb_margin.Right = 2.5;

            thumbnail.Margin = thumb_margin;
            thumbnail.Width = 100;

            image.DecodePixelWidth = 200;

            thumbnail.Source = image;
            thumbnail.Tag = filename;

            thumbnail.MouseDown += image_Click;
            thumbnail.MouseEnter += hand_Over;
            thumbnail.MouseLeave += normal_Out;

            images.Add(thumbnail);

            thumbnail = null;

    }
}

This worked fine until I added a BackgroundWorker to process this. Now, when execution gets to

Image thumbnail = new Image();

I get the following exception:

System.InvalidOperationException: 'The calling thread must be STA, because many UI components require this.'

Two questions:

(1) How can I process this code to allow the background worker to work on Image, or, (2) is there a better way to do what I am doing to allow for the BackgroundWorker to work?

I have zero experience working in a multi-threaded environment. I want it to work this way because the largest record I process has 180 images and creates about a 10-15 second hang.

  • 1
    [BGW threads are MTA](https://stackoverflow.com/questions/4685237/how-can-i-make-a-background-worker-thread-set-to-single-thread-apartment). – Yurii Feb 20 '18 at 18:32
  • A windows form project has a Program.cs with following : [STAThread] static void Main(). I just put it in a console application above main and it usually works. – jdweng Feb 20 '18 at 18:36
  • This isn't a windows form application. As title says, it's a WPF application. Edit: My bad, didn't add to title, just opening sentence. – user3813075 Feb 20 '18 at 18:39
  • @user3813075 The very same applies to WPF as well. The main entry point is still labeled as `STAThread`. Have a look at the link posted by @Yuriy to see why that's a problem here. – Alejandro Feb 20 '18 at 18:44
  • Do not create Image elements in a background worker, or not at all in code behind. See here for what to do: https://stackoverflow.com/a/18351268/1136211 – Clemens Feb 20 '18 at 18:47
  • @Alejandro I did look at the post, but it references a WinForms app. Trying something similar in the WPF app, I get an error when using this.Invoke that Window does not have method invoke. Not sure where I would set the [StatThread] attribute - I checked in App.xaml.cs and it already has STATThreadAttribute on the Main function. – user3813075 Feb 20 '18 at 18:53
  • I have run into very strange decoder issues when background-loading images on a separate dispatcher (which is, in theory, the 'right' way to do it). I suspect those issues were related to a caching scheme or some deferred-loading mechanism used under the covers. I found it easier to simply put the image location (string or URI) in the relevant model class, then present it with `Binding.IsAsync`, e.g.: ``. If that works for you, just do that. – Mike Strobel Feb 20 '18 at 18:57
  • @Clemens I'm not sure how that would work when the images I am loading are based on a user selecting a record then loading the images associated with that record. I'm not wanting to load all at once, and the paths are unknown until the record is selected. Also, the images are resized and eventhandlers added to treat them as thumbnails for the DocumentViewer. So, currently, I have images load from a List of file names, build an image, and add that image to my List. Once that is complete, I bind to the Itemscontrol. So, how do you not load in code behind in this scenario? – user3813075 Feb 20 '18 at 19:11
  • Take a look at the answer here. The LoadImagesAsync method accepts a collection of file names as parameter. – Clemens Feb 20 '18 at 19:12

1 Answers1

2

Do not create Image elements in code behind. Instead, use an ItemControl with an appropriate ItemTemplate:

<ScrollViewer>
    <ItemsControl ItemsSource="{Binding Images}">
        <ItemsControl.ItemTemplate>
            <DataTemplate>
                <Image Source="{Binding}"/>
            </DataTemplate>
        </ItemsControl.ItemTemplate>
    </ItemsControl>
</ScrollViewer>

Bind it to a view model like shown below, which is capable of asynchronously loading the image files. It is important that the BitmapImages are frozen to make them cross-thread accessible.

public class ViewModel
{
    public ObservableCollection<ImageSource> Images { get; }
        = new ObservableCollection<ImageSource>();

    public async Task LoadImagesAsync(IEnumerable<string> filenames)
    {
        foreach (var filename in filenames)
        {
            Images.Add(await Task.Run(() => LoadImage(filename)));
        }
    }

    public ImageSource LoadImage(string filename)
    {
        var bitmap = new BitmapImage();
        bitmap.BeginInit();
        bitmap.CacheOption = BitmapCacheOption.OnLoad;
        bitmap.DecodePixelWidth = 200;
        bitmap.UriSource = new Uri(filename);
        bitmap.EndInit();
        bitmap.Freeze();
        return bitmap;
    }
}

which is initialized like this:

private ViewModel viewModel = new ViewModel();

public MainWindow()
{
    InitializeComponent();
    DataContext = viewModel;
}

private async void Button_Click(object sender, RoutedEventArgs e)
{
    ...
    await viewModel.LoadImagesAsync(..., "*.jpg"));
}

An alternative view model method could load the BitmapImages directly from a FileStream instead of an Uri:

public ImageSource LoadImage(string filename)
{
    using (var stream = new FileStream(filename, FileMode.Open, FileAccess.Read))
    {
        var bitmap = new BitmapImage();
        bitmap.BeginInit();
        bitmap.DecodePixelWidth = 200;
        bitmap.CacheOption = BitmapCacheOption.OnLoad;
        bitmap.StreamSource = stream;
        bitmap.EndInit();
        bitmap.Freeze();
        return bitmap;
    }
}
Clemens
  • 123,504
  • 12
  • 155
  • 268
  • This looks good, thank you. The only issue I see with this is the images are created and loaded at application start up. The issue with that is user searches for a record, selects a match from a new window, those results are passed to main window and images associated with that record are loaded. – user3813075 Feb 23 '18 at 15:04
  • You can call the view model's LoadImagesAsync method whenever you want. Even multiple times. Probably add `Images.Clear()` then. – Clemens Feb 23 '18 at 16:49
  • Thank you for all the help. I just have one more question, and yes, it sounds possibly ignorant, but how do I call the LoadImagesAsync method (in my case I have my search function returning the filepaths to the images - I need to call it after I have my list of files)? I see it's attached to the Loaded event, but like I said - no clue on async methods, and pretty much the same on anonymous. I'm still learning. – user3813075 Feb 23 '18 at 19:09
  • Instead of creating a local variable, have a class member like `private ViewModel viewModel = new ViewModel();` (as shown in the edited answer). Then you can call `await viewModel.LoadImagesAsync(...)` whenever you like, e.g. from a Button's Click event hander. – Clemens Feb 23 '18 at 19:18