WebClient 按顺序下载图像会导致 ImageList 出现问题

WebClient download images in sequence causes issue with ImageList

我花了很长时间进入 VB.NET 并通过 MySQL 令人惊讶地公平连接了一个程序并通过登录对话框验证 bcrypt 哈希。一切都那么美妙。

当用户继续 Form2 时,我们使用此代码来处理来自 MySQL 结果的列表:

Dim transferstable As New DataTable

sql = "SQL-SELECT-QUERY"

With cmd
    .Connection = con
    .CommandText = sql
End With
For Each row As DataRow In transferstable.Rows
    Using client As New WebClient()
        Dim Url = "domain.org/images/covers/" & row.Item("cover")
        client.DownloadFileAsync(New Uri(Url), "C:\App_Uploads\" & row.Item("cover"))
    End Using
Next
For Each row As DataRow In transferstable.Rows
    ' myImage barely loading image for each row, usually first 2 rows out of ~160 rows
    Dim myImage As System.Drawing.Image = Image.FromFile("C:\App_Uploads\" & row.Item("cover"))
    ListControl1.Add("somename", "item title", "description", "sideinfo", myImage, 1)
Next

所以第一个块工作正常,它将所有图像下载到 C:\App_Uploads

但是我们无法在不放弃/运行内存不足的情况下将这些图像正确传递给ListControl1.Add()

使用像 C:\test.png 这样的固定本地图像效果很好,并分配给从数据库中找到的列表中的每一行(所有 160 行),但是我们如何将离线(下载的)图像分配给结果?它现在已经耗费我们很多时间了。

我们走到这一步了!谢谢!

每行局部迭代图像的结果

每行迭代本地图像的结果

Dim myImage As System.Drawing.Image = Image.FromFile("C:\test.png")

更新---

我们从包装 WebClient() 中删除了 For Each 并且似乎方向正确,但是只有 1-2 个图像加载到视图中。

' Download image art locally            
Using client As New WebClient()
    Dim Url = "domain.com/images/covers/" & transferstable.Rows(0).Item(14)
    Await client.DownloadFileTaskAsync(New Uri(Url), "C:\App_Uploads\" & transferstable.Rows(0).Item(14))
End Using

For Each row As DataRow In transferstable.Rows
    Dim myImage As System.Drawing.Image = Image.FromFile("C:\App_Uploads\" & row.Item("cover"))
    ListControl1.Add("somename", "item title", "description", "sideinfo", myImage, 1)
Next

您正在使用 WebClient DownloadFile()DownloadFileAsync() 的事件驱动(不可等待)版本循环下载图像。 WebClient 对象在 Using 语句中声明。
假设 WebClient 实例可以在处理前终止下载,DownloadFileAsync() 方法 return 立即:您应该订阅 DownloadFileCompleted 以在文件准备好时接收通知.

第一个循环完成后,将启动另一个循环,从磁盘中检索图像。
再次假设所有文件都已实际下载(我没有测试过),你有一个竞争条件,因为你试图立即使用可能未完成或实际上不存在(并且可能永远不会存在)的文件.

在这种情况下,最好并行下载所有图像,而不将它们存储在磁盘上,除非严格要求;只有在确定下载完成后,才能在 UI.

中显示图像

使用静态 (Shared) HttpClient 对象的替代方法示例,该对象在公开 public 方法的帮助程序 class 中声明,DownloadImages(),它负责 returning 一个 ordered List(Of Image).
HttpClient 对象在此处声明为 Lazy(Of HttpClient)
有关详细信息,请参阅有关 Lazy<T> class 的文档。
(如果你不喜欢它,你可以在这里 change/remove Lazy<T> 实例化)。

列表中图像的顺序取决于下载 URL 传递给方法的顺序:图像按相同顺序 returned。
DownloadImages() 方法生成一个数字序列,与 URL 一起传递给私有 GetImage() 方法:

Dim tasks = urlList.Select(Function(url, idx) GetImage(idx, New Uri(url)))

添加顺序是因为等待 Task.WhenAll() 不能保证任务 return 按特定顺序编辑。
List(Of Task) 然后使用原始序列重新排序,以防顺序在您的用例中很重要。

注意:图片是并行下载的。如果您从同一个地址下载大量图像或下载频率非常高,您的 IP 地址可能最终会进入 黑名单

要使用 class,请创建一个新的 DownloadImagesHelper 对象和一个 Urls 集合(作为字符串)。
然后等待对 DownloadImages() 方法的调用。
当方法 returns 时,循环返回图像列表并将新项目添加到控件中。
例如:

Dim urls As New List(Of String) 
For Each row As DataRow In transferstable.Rows
    urls.Add($"domain.org/images/covers/{row.Item("cover")}") 
Next

Dim downloadHelper = New DownloadImagesHelper()
Dim images = Await downloadHelper.DownloadImages(urls)

For Each img As Image In images
   ListControl1.Add("somename", "item title", "description", "sideinfo", img, 1)
Next

class 实现 IDisposable。完成 DownloadImagesHelper 对象后调用其 Dispose() 方法。 Dispose 方法 尝试(不是立即)关闭现有连接并处理 HttpClient。

帮手class:

注意:DownloadImages() 不检查 returned 任务的状态:

Return tasks.OrderBy(Function(t) t.Result.Key).Select(Function(t) t.Result.Value)

您可以在不同的条件下验证是否最好添加此检查,如:

Return tasks.Where(Function(t) t.Status = TaskStatus.RanToCompletion).
             OrderBy(Function(t) t.Result.Key).
             Select(Function(t) t.Result.Value)

如果您不想 return 由于某些 HTTP 异常而无法下载图像的结果,请在 GetImage() 中将图像值设置为 Nothing(代码中描述)并在 DownloadImages():

中过滤结果
Return tasks.OrderBy(Function(t) t.Result.Key).
             Where(Function(t) t.Result.Value IsNot Nothing).
             Select(Function(t) t.Result.Value)

或者只是 return 空结果,稍后在调用这些方法的代码中过滤。


注意:HttpClientHandler 已初始化设置其 SslProtocols 属性。
这至少需要.Net Framework 4.8,否则它什么都不做(文档说.Net Framework 4.7.2+,不信:)
此外, 属性 被硬编码为 SslProtocols.Tls12 (它在那里提出问题)。您可以删除它或将其设置为 SslProtocols.Default,或者添加 SslProtocols.Tls11 以防默认系统设置(通常是 TLS12)不受您从中下载的一个或多个服务器的支持。
SslProtocols.Tls13,虽然包含,但目前不能使用,所以不要添加。
如果您使用的是较旧的 .Net 版本,则需要在创建任何连接之前 直接使用 ServicePointManager 设置 TLS 版本。例如:

ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12

编辑:
DownloadImages() 的 return 类型更改为 Task(Of List(Of Image)) 而不是 Task(Of IEnumerable(Of Image)),因为在本地调试和处理可能更简单。如果您需要延迟执行,请将其改回。

添加了一个可以设置为虚拟位图的 DummyImage 属性,以防 HTTP 请求产生异常(404 和类似)。

Imports System.Collections.Generic
Imports System.Drawing
Imports System.Linq
Imports System.Net
Imports System.Net.Http
Imports System.Security.Authentication
Imports System.Threading.Tasks

Public Class DownloadImagesHelper
    Implements IDisposable

    Private Shared ReadOnly client As New Lazy(Of HttpClient)(
        Function()
            Dim handler As New HttpClientHandler() With {
                .SslProtocols = SslProtocols.Tls12,
                .AllowAutoRedirect = True,
                .AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate
                .CookieContainer = New CookieContainer()
            }
            Dim client As New HttpClient(handler)
            client.DefaultRequestHeaders.Add("Cache-Control", "no-cache")
            client.DefaultRequestHeaders.Add("Accept-Encoding", "gzip, deflate")
            client.DefaultRequestHeaders.ConnectionClose = False
            Return client
        End Function)

    Public Sub New()
    End Sub

    Public Property DummyImage As Image = Nothing

    Public Async Function DownloadImages(urlList As IEnumerable(Of String)) As Task(Of List(Of Image))
        Dim tasks = urlList.Select(Function(url, idx) GetImage(idx, New Uri(url)))
        Await Task.WhenAll(tasks).ConfigureAwait(False)
        Return tasks.OrderBy(Function(t) t.Result.Key).Select(Function(t) t.Result.Value).ToList()
        ' Or, depending what you have decided to do in GetImage()
        ' only return results that have a non-null Image
        ' Return tasks.OrderBy(Function(t) t.Result.Key).Where(Function(t) t.Result.Value IsNot Nothing).Select(Function(t) t.Result.Value).ToList()
    End Function

    Private Async Function GetImage(pos As Integer, url As Uri) As Task(Of KeyValuePair(Of Integer, Image))
        Dim imageData As Byte() = Nothing
        Try
            imageData = Await client.Value.GetByteArrayAsync(url).ConfigureAwait(False)
            Return New KeyValuePair(Of Integer, Image)(
            pos, DirectCast(New ImageConverter().ConvertFrom(imageData), Image)
        )
        Catch hrEx As HttpRequestException
            ' Or return a null Image: Return New KeyValuePair(Of Integer, Image)(pos, Nothing)
            Return New KeyValuePair(Of Integer, Image)(pos, DummyImage)
        End Try
    End Function

    Public Sub Dispose() Implements IDisposable.Dispose
        Dispose(True)
        GC.SuppressFinalize(Me)
    End Sub
    Protected Overridable Sub Dispose(disposing As Boolean)
        client?.Value?.CancelPendingRequests()
        client?.Value?.Dispose()
    End Sub
End Class