Skip to content

Commit 23e687a

Browse files
committed
feat: 引入可供自定义主页使用的异步网络 ImageSource
1 parent bf6fa71 commit 23e687a

File tree

1 file changed

+227
-0
lines changed

1 file changed

+227
-0
lines changed

Plain Craft Launcher 2/Modules/Base/ModBase.vb

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
Imports System.ComponentModel
12
Imports System.Globalization
23
Imports System.IO.Compression
34
Imports System.Reflection
45
Imports System.Runtime.CompilerServices
56
Imports System.Security.Cryptography
67
Imports System.Security.Principal
78
Imports System.Text.RegularExpressions
9+
Imports System.Threading.Tasks
810
Imports System.Xaml
911
Imports Newtonsoft.Json
1012

@@ -3208,4 +3210,229 @@ Public Class InverseBooleanConverter
32083210
End Function
32093211
End Class
32103212

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+
32113438
#End Region

0 commit comments

Comments
 (0)