Асинхронная загрузка веб-изображения

Я столкнулся с проблемой отображения изображения из Интернета в моем приложении WPF:

Я использовал BitmapImage для этой задачи. Моя первая попытка состояла в том, чтобы выполнить его в потоке пользовательского интерфейса, но я быстро понял, что это не так, поскольку приложение перестало отвечать до тех пор, пока изображение не было полностью загружено. Моя вторая попытка состояла в том, чтобы использовать BackgroudWorker:

var worker = new BackgroundWorker();
worker.DoWork += worker_LoadImage;
worker.RunWorkerCompleted+=worker_RunWorkerCompleted;
worker.RunWorkerAsync(someURI);

и рабочие функции:

    private void worker_LoadImage(object sender, DoWorkEventArgs e)
    {
        var image = new BitmapImage(); 
        image.BeginInit();
        image.CacheOption = BitmapCacheOption.OnDemand;
        image.UriSource = e.Argument as Uri;
        image.DownloadFailed += new EventHandler<ExceptionEventArgs>(image_DownloadFailed);
        image.EndInit();
        e.Result = image;
    }

    void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
        //if I understand correctly, this code runs in the UI thread so the 
        //access to the component image1 is valid.
        image1.Source = e.Result as BitmapImage;
    }

после этого я все еще получил InvalidOperationException: «Вызывающий поток не может получить доступ к этому объекту, потому что им владеет другой поток». Я немного изучил и обнаружил, что, поскольку BitmapImage равно Freezable, мне нужно вызвать Freeze перед доступом к объекту из другого потока. Итак, я попытался заменить последнюю строку в worker_LoadImage на:

        image.Freeze();
        e.Result = image;

Но затем я получил исключение, что мое изображение не может быть заморожено, я обнаружил, что это, вероятно, потому, что изображение не было загружено, когда я пытался вызвать Freeze(). Поэтому я добавил следующий код для создания образа:

        image.DownloadCompleted += image_DownloadCompleted;

где:

 void image_DownloadCompleted(object sender, EventArgs e)
 {
     BitmapImage img = (BitmapImage)sender;
     img.Freeze();
 }

Итак, теперь мы подошли к главному вопросу: Как заставить фоновый рабочий процесс ждать, пока изображение не будет полностью загружено и не будет запущено событие?

Я пробовал много вещей: зацикливание, пока isDownloading изображения истинно, Thread.Sleep, Thread.Yield, семафоры, дескрипторы ожидания событий и многое другое. Я не знаю, как загрузка изображения на самом деле работает за кулисами, но что происходит, когда я пробую один из описанных выше методов, так это то, что загрузка изображения никогда не заканчивается (isDownloading зависает на True)

Есть ли лучший и более простой способ выполнить довольно простую задачу, которую я пытаюсь выполнить?

Некоторые вещи, на которые стоит обратить внимание:

  1. этот ответ действительно работает, но только один раз: когда я пытаюсь загрузить другое изображение, он говорит, что диспетчер закрыт. Даже прочитав немного о Dispatchers, я не очень понимаю, как OP достиг этого или можно ли расширить решение для более чем одного изображения.

  2. Когда я помещаю окно сообщения до того, как рабочий выходит из своей функции DoWork, я нажимаю «ОК», и появляется изображение, которое означает, что загрузка продолжалась, пока окно сообщения было открыто и завершено до того, как я нажал «ОК».


person Block    schedule 16.12.2014    source источник
comment
BitmapImage уже загружает изображение асинхронно. Все, что вам нужно сделать, это image1.Source = new BitmapImage(someURI);. Чтобы выполнить загрузку всего изображения в отдельном потоке, см. этот ответ.   -  person Clemens    schedule 17.12.2014
comment
Работает как шарм! огромное спасибо.   -  person Block    schedule 17.12.2014


Ответы (1)


Учитывая, что вы используете возможность растрового изображения для асинхронной загрузки изображения, во-первых, вам не нужен BackgroundWorker. Вместо создания BGW для запуска асинхронной операции и ожидания ее завершения просто используйте асинхронную операцию напрямую.

Все, что вам нужно сделать, это обновить пользовательский интерфейс из вашего обработчика image_DownloadCompleted (после замораживания изображения), и вам больше не нужен BGW:

private void FetchImage(Uri uri)
{
    var context = SynchronizationContext.Current;
    var image = new BitmapImage();
    image.BeginInit();
    image.CacheOption = BitmapCacheOption.OnDemand;
    image.UriSource = uri;
    image.DownloadFailed += image_DownloadFailed;
    image.DownloadCompleted += (s, args) =>
    {
        image.Freeze();
        context.Post(_ => image1.Source = image, null);
    };
    image.EndInit();
}
person Servy    schedule 16.12.2014
comment
Я чувствую себя таким глупым.. Спасибо! Я вернулся и проверил: первый BitmapImage блокируется и заставляет весь экран зависать на пару секунд, но другие изображения действительно загружаются плавно и асинхронно. Есть ли причина, по которой такое могло произойти? есть ли способ предотвратить это? - person Block; 17.12.2014
comment
@Block Где он блокирует? - person Servy; 17.12.2014
comment
Я не уверен на 100%, так как я использую окна сообщений, чтобы увидеть, где зависание, но я думаю, что это происходит в: image.EndInit(); линия - person Block; 17.12.2014
comment
@Block Я не уверен, что могло бы вызвать это, но вы могли бы обернуть все после var context ... в вызов Task.Run и запустить код создания задачи в другом потоке, если он по какой-то причине блокирует пользовательский интерфейс. Это немного хак, но если проблема в этом, то все будет хорошо. - person Servy; 17.12.2014
comment
Я не уверен, что полностью понимаю, что вы имеете в виду (извините, я совсем новичок в этом). Я пытался поместить все после объявления контекста в Task.Run, но, очевидно, это не работает, поскольку новый поток и объявление изображения внутри него уничтожается до того, как загрузка может быть завершена. Не могли бы вы объяснить, что вы имеете в виду? - person Block; 17.12.2014
comment
@Block Они не уничтожены. Они выходят за рамки, но не будут собраны, поскольку созданный поток является корневым, даже если он недоступен из потока пользовательского интерфейса. - person Servy; 17.12.2014
comment
Тогда почему событие DownloadCompleted никогда не запускается таким образом? - person Block; 17.12.2014