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

人气:1,052 发布:2022-10-16 标签: .net vb.net winforms controls webclient

问题描述

我花了很长时间进入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()方法立即返回:您应该订阅DownloadFileCompleted,以便在文件准备就绪时收到通知。

第一个循环完成后,将启动另一个循环,以从磁盘检索图像。 再次假设所有文件都已实际下载(我尚未对其进行测试),则存在争用情况,因为您正在尝试立即使用可能未完成或实际不存在(也可能永远不会存在)的文件。

在这种情况下,除非严格要求,否则最好并行下载所有图像,而不将它们存储在磁盘上;只有在确定下载完成后,才在用户界面中显示图像。

替代方法的示例,该方法使用在公开公共方法的帮助器类中声明的静态(Shared)HttpClient对象,DownloadImages()负责返回有序List(Of Image)。 HttpClient对象在这里声明为Lazy(Of HttpClient)。 有关详细信息,请参阅有关Lazy<T>类的文档。 (如果您不喜欢,您可以在此处更改/删除Lazy<T>实例化)。

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

Dim tasks = urlList.Select(Function(url, idx) GetImage(idx, New Uri(url)))
添加该序列是因为AWATINGTask.WhenAll()不保证任务按特定顺序返回。 然后使用原始顺序对List(Of Task)重新排序,以防该顺序在您的用例中非常重要。

注意:镜像是并行下载的。如果您从同一地址或频繁下载大量图像,您的IP地址可能会被列入黑名单。

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

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

该类实现IDisposable。当您完成DownloadImagesHelper对象时,调用它的Dispose()方法。Dispose方法尝试(不是立即)关闭现有连接并释放HttpClient。

帮助器类:

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

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)

如果您不想在某个HTTP异常导致无法下载图像时返回结果,请在GetImage()(代码中描述)中将Image值设置为Nothing,并在DownloadImages()

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

或者只返回空结果并稍后在调用这些方法的代码中进行筛选。

注意:HttpClientHandler是通过设置其SslProtocols属性进行初始化的。 这至少需要.Net Framework4.8,否则它什么也做不了(文档上说.Net Framework4.7.2+,不相信:) 此外,该属性被硬编码为SslProtocols.Tls12(它在那里是为了表示问题)。您可以将其删除或设置为SslProtocols.Default,或者添加,例如SslProtocols.Tls11,以防您下载的一个或多个服务器不支持默认系统设置(通常为TLS12)。 SslProtocols.Tls13虽然已包含,但当前无法使用,请不要添加。 如果您使用的是较旧的.Net版本,则需要在创建任何连接之前,直接使用ServicePointManager设置TLS版本。例如:

ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12

编辑: 将DownloadImages()的返回类型更改为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

549