|
| 1 | +Imports System.ComponentModel |
1 | 2 | Imports System.Globalization |
2 | 3 | Imports System.IO.Compression |
3 | 4 | Imports System.Reflection |
4 | 5 | Imports System.Runtime.CompilerServices |
5 | 6 | Imports System.Security.Cryptography |
6 | 7 | Imports System.Security.Principal |
7 | 8 | Imports System.Text.RegularExpressions |
| 9 | +Imports System.Threading.Tasks |
8 | 10 | Imports System.Xaml |
9 | 11 | Imports Newtonsoft.Json |
10 | 12 |
|
@@ -3208,4 +3210,229 @@ Public Class InverseBooleanConverter |
3208 | 3210 | End Function |
3209 | 3211 | End Class |
3210 | 3212 |
|
| 3213 | +''' <summary> |
| 3214 | +''' 异步加载的网络图片源,需传入 Url,最终内容使用 MyBitmap 解析。<br/> |
| 3215 | +''' Source - 源 Url,必须指定。<br/> |
| 3216 | +''' FallbackSource - 备用 Url;在设计理念上,返回的内容应当与主 Url 相同。<br/> |
| 3217 | +''' LoadingSource - 加载时显示的图片,合法值为 空 / 可被 MyBitmap 解析的字符串 / ImageSource。<br/> |
| 3218 | +''' EnableCache - 是否启用缓存(默认启用),不启用的话每次都会联网获取图片。<br/> |
| 3219 | +''' FileCacheExpiredTime - 缓存到期时间,默认为七天,遵循 TimeSpan 的格式解析。<br/> |
| 3220 | +''' Result - 不在 xaml 中使用,用于存储输出的 ImageSource。<br/><br/> |
| 3221 | +''' 在 xaml 中引用的语法为:Source="{local:AsyncImageSource https://example.com/example.png}",<br/> |
| 3222 | +''' 最终效果相当于将 Source 属性绑定到了一个动态改变的值上。 |
| 3223 | +''' </summary> |
| 3224 | +Public Class AsyncImageSource |
| 3225 | + Inherits Markup.MarkupExtension |
| 3226 | + Implements INotifyPropertyChanged |
| 3227 | + |
| 3228 | + ''' <summary> |
| 3229 | + ''' 工具类,接受同样的标识符时始终返回同一个对象,除非该对象已被回收。 |
| 3230 | + ''' </summary> |
| 3231 | + Private Class InstanceProvider(Of T As Class) |
| 3232 | + Private ReadOnly _InstanceSupplier As Func(Of T) |
| 3233 | + Private ReadOnly _ExistingInstances As New Concurrent.ConcurrentDictionary(Of Object, WeakReference(Of T)) |
| 3234 | + Private ReadOnly _CleanupTimer As New Timer(AddressOf CleanupGoneInstances, Nothing, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(1)) |
| 3235 | + |
| 3236 | + Public Sub New(InstanceSupplier As Func(Of T)) |
| 3237 | + If InstanceSupplier Is Nothing Then Throw New ArgumentNullException("InstanceSupplier") |
| 3238 | + _InstanceSupplier = InstanceSupplier |
| 3239 | + End Sub |
| 3240 | + |
| 3241 | + Public Function GetFrom(Key As Object) As T |
| 3242 | + If Key Is Nothing Then Throw New ArgumentNullException("Key") |
| 3243 | + GetFrom = Nothing |
| 3244 | + While True |
| 3245 | + Dim Wr As WeakReference(Of T) = Nothing |
| 3246 | + If _ExistingInstances.TryGetValue(Key, Wr) Then |
| 3247 | + If Wr.TryGetTarget(GetFrom) Then |
| 3248 | + Exit While |
| 3249 | + Else |
| 3250 | + GetFrom = _InstanceSupplier.Invoke() |
| 3251 | + If _ExistingInstances.TryUpdate(Key, New WeakReference(Of T)(GetFrom), Wr) Then |
| 3252 | + Exit While |
| 3253 | + End If |
| 3254 | + End If |
| 3255 | + Else |
| 3256 | + GetFrom = _InstanceSupplier.Invoke() |
| 3257 | + If _ExistingInstances.TryAdd(Key, New WeakReference(Of T)(GetFrom)) Then |
| 3258 | + Exit While |
| 3259 | + End If |
| 3260 | + End If |
| 3261 | + End While |
| 3262 | + If GetFrom Is Nothing Then Throw New Exception("获取实例意外失败。") |
| 3263 | + End Function |
| 3264 | + |
| 3265 | + Private Sub CleanupGoneInstances() |
| 3266 | + Try |
| 3267 | + _ExistingInstances _ |
| 3268 | + .Where(Function(e) Not e.Value.TryGetTarget(Nothing)) _ |
| 3269 | + .Select(Function(e) e.Key) _ |
| 3270 | + .ToList() _ |
| 3271 | + .ForEach(AddressOf AttemptRemoveGoneInstance) |
| 3272 | + Catch ex As Exception |
| 3273 | + Log(ex, $"清理失效 {GetType(T).Name} 实例意外失败") |
| 3274 | + End Try |
| 3275 | + End Sub |
| 3276 | + |
| 3277 | + Private Function AttemptRemoveGoneInstance(Key As Object) As Boolean |
| 3278 | + Dim Wr As WeakReference(Of T) = Nothing |
| 3279 | + If Not _ExistingInstances.TryGetValue(Key, Wr) Then Return False |
| 3280 | + If Wr.TryGetTarget(Nothing) Then Return False |
| 3281 | + Return CType(_ExistingInstances, ICollection(Of KeyValuePair(Of Object, WeakReference(Of T)))) _ |
| 3282 | + .Remove(New KeyValuePair(Of Object, WeakReference(Of T))(Key, Wr)) |
| 3283 | + End Function |
| 3284 | + End Class |
| 3285 | + |
| 3286 | + Private Shared ReadOnly _FileCacheDirectory As String = $"{PathTemp}MyImage\" |
| 3287 | + Private Shared ReadOnly _SemaphoreProvider As New InstanceProvider(Of SemaphoreSlim)(Function() New SemaphoreSlim(1, 1)) |
| 3288 | + Private Shared ReadOnly _LoadingSourceRealDefault As ImageSource = New MyBitmap("pack://application:,,,/images/Icons/NoIcon.png") |
| 3289 | + |
| 3290 | + Private _Source As String |
| 3291 | + Private _TempDownloadingPath As String |
| 3292 | + Private _FileCacheExpiredTimeReal As TimeSpan = TimeSpan.FromDays(7) |
| 3293 | + Private _LoadingSourceReal As ImageSource = _LoadingSourceRealDefault |
| 3294 | + Private _Result As ImageSource |
| 3295 | + |
| 3296 | + Public Property Source As String |
| 3297 | + Get |
| 3298 | + Return _Source |
| 3299 | + End Get |
| 3300 | + Set(value As String) |
| 3301 | + If value Is Nothing Then Throw New InvalidOperationException("AsyncImageSource.Source 不可设置为 null。") |
| 3302 | + If _Source IsNot Nothing Then Throw New InvalidOperationException("AsyncImageSource.Source 不可重复设置。") |
| 3303 | + _TempDownloadingPath = $"{_FileCacheDirectory}_{GetHash(value)}.png" |
| 3304 | + _Source = value |
| 3305 | + End Set |
| 3306 | + End Property |
| 3307 | + |
| 3308 | + Public Property FallbackSource As String |
| 3309 | + |
| 3310 | + Public WriteOnly Property LoadingSource As Object |
| 3311 | + Set(value As Object) |
| 3312 | + If value Is Nothing Then |
| 3313 | + _LoadingSourceReal = Nothing |
| 3314 | + ElseIf TypeOf value Is String Then |
| 3315 | + If CType(value, String).Length = 0 Then |
| 3316 | + _LoadingSourceReal = Nothing |
| 3317 | + Else |
| 3318 | + _LoadingSourceReal = New MyBitmap(CType(value, String)) |
| 3319 | + End If |
| 3320 | + Else |
| 3321 | + _LoadingSourceReal = CType(value, ImageSource) |
| 3322 | + End If |
| 3323 | + End Set |
| 3324 | + End Property |
| 3325 | + |
| 3326 | + Public Property EnableCache As Boolean = True |
| 3327 | + |
| 3328 | + Public WriteOnly Property FileCacheExpiredTime As Object |
| 3329 | + Set(value As Object) |
| 3330 | + Static Converter As New TimeSpanConverter |
| 3331 | + _FileCacheExpiredTimeReal = Converter.ConvertFrom(value) |
| 3332 | + End Set |
| 3333 | + End Property |
| 3334 | + |
| 3335 | + Public Property Result As ImageSource |
| 3336 | + Get |
| 3337 | + Return _Result |
| 3338 | + End Get |
| 3339 | + Private Set(value As ImageSource) |
| 3340 | + _Result = value |
| 3341 | + RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs("Result")) |
| 3342 | + End Set |
| 3343 | + End Property |
| 3344 | + |
| 3345 | + Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged |
| 3346 | + |
| 3347 | + Public Sub New() |
| 3348 | + End Sub |
| 3349 | + |
| 3350 | + Public Sub New(Source As String) |
| 3351 | + Me.Source = Source |
| 3352 | + End Sub |
| 3353 | + |
| 3354 | + Public Overrides Function ProvideValue(serviceProvider As IServiceProvider) As Object |
| 3355 | + If Source Is Nothing Then Throw New InvalidOperationException("AsyncImageSource.Source 未被设置。") |
| 3356 | + StartLoad() |
| 3357 | + Return New Binding("Result") With {.Source = Me}.ProvideValue(serviceProvider) |
| 3358 | + End Function |
| 3359 | + |
| 3360 | + Public Sub StartLoad() |
| 3361 | + Windows.Application.Current.Dispatcher.InvokeAsync(AddressOf LoadAsync) |
| 3362 | + End Sub |
| 3363 | + |
| 3364 | + Private Async Function LoadAsync() As Task |
| 3365 | + '需运行在 UI 线程上 |
| 3366 | + Try |
| 3367 | + Result = _LoadingSourceReal '加载中占位符 |
| 3368 | + Dim LoadSemaphore = Await Task.Run(Function() _SemaphoreProvider.GetFrom(_TempDownloadingPath)) |
| 3369 | + Await LoadSemaphore.WaitAsync() '保证使用同样文件缓存路径的实例串行加载 |
| 3370 | + Try |
| 3371 | + '尝试使用缓存 |
| 3372 | + Dim ResultFromCache As ImageSource |
| 3373 | + ResultFromCache = Await Task.Run(AddressOf TryLoadCache) |
| 3374 | + If ResultFromCache IsNot Nothing Then |
| 3375 | + Result = ResultFromCache |
| 3376 | + Exit Function |
| 3377 | + End If |
| 3378 | + '缓存无效 |
| 3379 | + Await Task.Run(AddressOf DownloadImage) '从网络下载图片 |
| 3380 | + Result = Await Task.Run(Function() New MyBitmap(_TempDownloadingPath)) '加载图片 |
| 3381 | + Finally |
| 3382 | + LoadSemaphore.Release() |
| 3383 | + End Try |
| 3384 | + Catch ex As Exception |
| 3385 | + Log(ex, $"异步网络图片加载失败(图片源:{Source},备用源:{If(FallbackSource, "无")})", LogLevel.Hint) |
| 3386 | + Result = Nothing |
| 3387 | + End Try |
| 3388 | + End Function |
| 3389 | + |
| 3390 | + ''' <summary> |
| 3391 | + ''' 从缓存获取 ImageSource,缓存未启用/不存在/过期/损坏或运行失败返回 Nothing,不会抛出异常。 |
| 3392 | + ''' </summary> |
| 3393 | + Private Function TryLoadCache() As MyBitmap |
| 3394 | + Try |
| 3395 | + If Not EnableCache Then Return Nothing '未启用缓存 |
| 3396 | + '判断缓存是否有效 |
| 3397 | + Dim CacheAvailable As Boolean |
| 3398 | + With New FileInfo(_TempDownloadingPath) |
| 3399 | + CacheAvailable = .Exists AndAlso (Date.Now - .LastWriteTime < _FileCacheExpiredTimeReal) |
| 3400 | + End With |
| 3401 | + If CacheAvailable Then |
| 3402 | + '缓存有效 |
| 3403 | + Try |
| 3404 | + Return New MyBitmap(_TempDownloadingPath) |
| 3405 | + Catch |
| 3406 | + 'MyBitmap 从文件解析失败 |
| 3407 | + File.Delete(_TempDownloadingPath) |
| 3408 | + End Try |
| 3409 | + End If |
| 3410 | + Catch ex As Exception |
| 3411 | + Log(ex, $"读取网络图片缓存(缓存位置 {_TempDownloadingPath},源 {Source})时预期之外的异常") |
| 3412 | + End Try |
| 3413 | + Return Nothing |
| 3414 | + End Function |
| 3415 | + |
| 3416 | + ''' <summary> |
| 3417 | + ''' 下载图片至本地缓存文件,失败后若指定了 FallbackSource 会再尝试,再失败后抛出异常。 |
| 3418 | + ''' </summary> |
| 3419 | + Private Sub DownloadImage() |
| 3420 | + Dim TargetUrl As String = Source, Retried As Boolean = False |
| 3421 | + Try |
| 3422 | +DownloadRetry: |
| 3423 | + Using Client As New WebClient() |
| 3424 | + Client.DownloadFile(TargetUrl, _TempDownloadingPath) |
| 3425 | + End Using |
| 3426 | + Catch ex As Exception When (Not Retried) AndAlso (FallbackSource IsNot Nothing) |
| 3427 | + Log(ex, $"下载图片可重试地失败({Source})", LogLevel.Developer) |
| 3428 | + TargetUrl = FallbackSource |
| 3429 | + Retried = True |
| 3430 | + GoTo DownloadRetry |
| 3431 | + Catch ex As Exception |
| 3432 | + Throw New Exception("下载图片失败。", ex) |
| 3433 | + End Try |
| 3434 | + End Sub |
| 3435 | + |
| 3436 | +End Class |
| 3437 | + |
3211 | 3438 | #End Region |
0 commit comments