user3813075
user3813075

Reputation: 17

Images in Background Workers in WPF Application

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.

Upvotes: 0

Views: 238

Answers (1)

Clemens
Clemens

Reputation: 128077

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

Upvotes: 2

Related Questions