Browse Source

Add vendored labs controls to fix asyncimage

pull/629/head
Ionite 7 months ago
parent
commit
38a104146f
No known key found for this signature in database
  1. 20
      StabilityMatrix.Avalonia/Controls/VendorLabs/AsyncImage/AsyncImageFailedEventArgs.cs
  2. 42
      StabilityMatrix.Avalonia/Controls/VendorLabs/AsyncImage/BetterAsyncImage.Events.cs
  3. 135
      StabilityMatrix.Avalonia/Controls/VendorLabs/AsyncImage/BetterAsyncImage.Properties.cs
  4. 248
      StabilityMatrix.Avalonia/Controls/VendorLabs/AsyncImage/BetterAsyncImage.cs
  5. 565
      StabilityMatrix.Avalonia/Controls/VendorLabs/Cache/CacheBase.cs
  6. 18
      StabilityMatrix.Avalonia/Controls/VendorLabs/Cache/CacheOptions.cs
  7. 36
      StabilityMatrix.Avalonia/Controls/VendorLabs/Cache/FileCache.cs
  8. 76
      StabilityMatrix.Avalonia/Controls/VendorLabs/Cache/ImageCache.cs
  9. 156
      StabilityMatrix.Avalonia/Controls/VendorLabs/Cache/InMemoryStorage.cs
  10. 49
      StabilityMatrix.Avalonia/Controls/VendorLabs/Cache/InMemoryStorageItem.cs
  11. 21
      StabilityMatrix.Avalonia/Controls/VendorLabs/LICENSE
  12. 3
      StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj.DotSettings

20
StabilityMatrix.Avalonia/Controls/VendorLabs/AsyncImage/AsyncImageFailedEventArgs.cs

@ -0,0 +1,20 @@
using System;
using Avalonia.Interactivity;
namespace StabilityMatrix.Avalonia.Controls.VendorLabs;
public partial class BetterAsyncImage
{
public class AsyncImageFailedEventArgs : RoutedEventArgs
{
internal AsyncImageFailedEventArgs(Exception? errorException = null, string errorMessage = "")
: base(FailedEvent)
{
ErrorException = errorException;
ErrorMessage = errorMessage;
}
public Exception? ErrorException { get; private set; }
public string ErrorMessage { get; private set; }
}
}

42
StabilityMatrix.Avalonia/Controls/VendorLabs/AsyncImage/BetterAsyncImage.Events.cs

@ -0,0 +1,42 @@
using System;
using Avalonia.Interactivity;
namespace StabilityMatrix.Avalonia.Controls.VendorLabs;
public partial class BetterAsyncImage
{
/// <summary>
/// Deines the <see cref="Opened"/> event
/// </summary>
public static readonly RoutedEvent<RoutedEventArgs> OpenedEvent = RoutedEvent.Register<
BetterAsyncImage,
RoutedEventArgs
>(nameof(Opened), RoutingStrategies.Bubble);
/// <summary>
/// Deines the <see cref="Failed"/> event
/// </summary>
public static readonly RoutedEvent<global::Avalonia.Labs.Controls.AsyncImage.AsyncImageFailedEventArgs> FailedEvent =
RoutedEvent.Register<
BetterAsyncImage,
global::Avalonia.Labs.Controls.AsyncImage.AsyncImageFailedEventArgs
>(nameof(Failed), RoutingStrategies.Bubble);
/// <summary>
/// Occurs when the image is successfully loaded.
/// </summary>
public event EventHandler<RoutedEventArgs>? Opened
{
add => AddHandler(OpenedEvent, value);
remove => RemoveHandler(OpenedEvent, value);
}
/// <summary>
/// Occurs when the image fails to load the uri provided.
/// </summary>
public event EventHandler<global::Avalonia.Labs.Controls.AsyncImage.AsyncImageFailedEventArgs>? Failed
{
add => AddHandler(FailedEvent, value);
remove => RemoveHandler(FailedEvent, value);
}
}

135
StabilityMatrix.Avalonia/Controls/VendorLabs/AsyncImage/BetterAsyncImage.Properties.cs

@ -0,0 +1,135 @@
using System;
using Avalonia;
using Avalonia.Animation;
using Avalonia.Labs.Controls;
using Avalonia.Media;
namespace StabilityMatrix.Avalonia.Controls.VendorLabs;
public partial class BetterAsyncImage
{
/// <summary>
/// Defines the <see cref="PlaceholderSource"/> property.
/// </summary>
public static readonly StyledProperty<IImage?> PlaceholderSourceProperty = AvaloniaProperty.Register<
BetterAsyncImage,
IImage?
>(nameof(PlaceholderSource));
/// <summary>
/// Defines the <see cref="Source"/> property.
/// </summary>
public static readonly StyledProperty<Uri?> SourceProperty = AvaloniaProperty.Register<
BetterAsyncImage,
Uri?
>(nameof(Source));
/// <summary>
/// Defines the <see cref="Stretch"/> property.
/// </summary>
public static readonly StyledProperty<Stretch> StretchProperty = AvaloniaProperty.Register<
BetterAsyncImage,
Stretch
>(nameof(Stretch), Stretch.Uniform);
/// <summary>
/// Defines the <see cref="PlaceholderStretch"/> property.
/// </summary>
public static readonly StyledProperty<Stretch> PlaceholderStretchProperty = AvaloniaProperty.Register<
BetterAsyncImage,
Stretch
>(nameof(PlaceholderStretch), Stretch.Uniform);
/// <summary>
/// Defines the <see cref="State"/> property.
/// </summary>
public static readonly DirectProperty<BetterAsyncImage, AsyncImageState> StateProperty =
AvaloniaProperty.RegisterDirect<BetterAsyncImage, AsyncImageState>(
nameof(State),
o => o.State,
(o, v) => o.State = v
);
/// <summary>
/// Defines the <see cref="ImageTransition"/> property.
/// </summary>
public static readonly StyledProperty<IPageTransition?> ImageTransitionProperty =
AvaloniaProperty.Register<BetterAsyncImage, IPageTransition?>(
nameof(ImageTransition),
new CrossFade(TimeSpan.FromSeconds(0.25))
);
/// <summary>
/// Defines the <see cref="IsCacheEnabled"/> property.
/// </summary>
public static readonly DirectProperty<BetterAsyncImage, bool> IsCacheEnabledProperty =
AvaloniaProperty.RegisterDirect<BetterAsyncImage, bool>(
nameof(IsCacheEnabled),
o => o.IsCacheEnabled,
(o, v) => o.IsCacheEnabled = v
);
private bool _isCacheEnabled;
/// <summary>
/// Gets or sets the placeholder image.
/// </summary>
public IImage? PlaceholderSource
{
get => GetValue(PlaceholderSourceProperty);
set => SetValue(PlaceholderSourceProperty, value);
}
/// <summary>
/// Gets or sets the uri pointing to the image resource
/// </summary>
public Uri? Source
{
get => GetValue(SourceProperty);
set => SetValue(SourceProperty, value);
}
/// <summary>
/// Gets or sets a value controlling how the image will be stretched.
/// </summary>
public Stretch Stretch
{
get { return GetValue(StretchProperty); }
set { SetValue(StretchProperty, value); }
}
/// <summary>
/// Gets or sets a value controlling how the placeholder will be stretched.
/// </summary>
public Stretch PlaceholderStretch
{
get { return GetValue(StretchProperty); }
set { SetValue(StretchProperty, value); }
}
/// <summary>
/// Gets the current loading state of the image.
/// </summary>
public AsyncImageState State
{
get => _state;
private set => SetAndRaise(StateProperty, ref _state, value);
}
/// <summary>
/// Gets or sets the transition to run when the image is loaded.
/// </summary>
public IPageTransition? ImageTransition
{
get => GetValue(ImageTransitionProperty);
set => SetValue(ImageTransitionProperty, value);
}
/// <summary>
/// Gets or sets whether to use cache for retrieved images
/// </summary>
public bool IsCacheEnabled
{
get => _isCacheEnabled;
set => SetAndRaise(IsCacheEnabledProperty, ref _isCacheEnabled, value);
}
}

248
StabilityMatrix.Avalonia/Controls/VendorLabs/AsyncImage/BetterAsyncImage.cs

@ -0,0 +1,248 @@
using System;
using System.IO;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
using Avalonia.Labs.Controls;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using Avalonia.Threading;
using StabilityMatrix.Avalonia.Controls.VendorLabs.Cache;
namespace StabilityMatrix.Avalonia.Controls.VendorLabs;
/// <summary>
/// An image control that asynchronously retrieves an image using a <see cref="Uri"/>.
/// </summary>
[TemplatePart("PART_Image", typeof(Image))]
[TemplatePart("PART_PlaceholderImage", typeof(Image))]
public partial class BetterAsyncImage : TemplatedControl
{
protected Image? ImagePart { get; private set; }
protected Image? PlaceholderPart { get; private set; }
private bool _isInitialized;
private CancellationTokenSource? _tokenSource;
private AsyncImageState _state;
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
ImagePart = e.NameScope.Get<Image>("PART_Image");
PlaceholderPart = e.NameScope.Get<Image>("PART_PlaceholderImage");
_tokenSource = new CancellationTokenSource();
_isInitialized = true;
if (Source != null)
{
SetSource(Source);
}
}
private async void SetSource(object? source)
{
if (!_isInitialized)
{
return;
}
_tokenSource?.Cancel();
_tokenSource = new CancellationTokenSource();
AttachSource(null);
if (source == null)
{
return;
}
State = AsyncImageState.Loading;
if (Source is IImage image)
{
AttachSource(image);
return;
}
if (Source == null)
{
return;
}
var uri = Source;
if (uri != null && uri.IsAbsoluteUri)
{
if (uri.Scheme == "http" || uri.Scheme == "https")
{
Bitmap? bitmap = null;
// Android doesn't allow network requests on the main thread, even though we are using async apis.
#if NET6_0_OR_GREATER
if (OperatingSystem.IsAndroid())
{
await Task.Run(async () =>
{
try
{
bitmap = await LoadImageAsync(uri, _tokenSource.Token);
}
catch (Exception ex)
{
await Dispatcher.UIThread.InvokeAsync(() =>
{
State = AsyncImageState.Failed;
RaiseEvent(new AsyncImageFailedEventArgs(ex));
});
}
});
}
else
#endif
{
try
{
bitmap = await LoadImageAsync(uri, _tokenSource.Token);
}
catch (Exception ex)
{
await Dispatcher.UIThread.InvokeAsync(() =>
{
State = AsyncImageState.Failed;
RaiseEvent(new AsyncImageFailedEventArgs(ex));
});
}
}
AttachSource(bitmap);
}
else if (uri.Scheme == "avares")
{
try
{
AttachSource(new Bitmap(AssetLoader.Open(uri)));
}
catch (Exception ex)
{
State = AsyncImageState.Failed;
RaiseEvent(new AsyncImageFailedEventArgs(ex));
}
}
else if (uri.Scheme == "file" && File.Exists(uri.LocalPath))
{
// Added error handling here for local files
try
{
AttachSource(new Bitmap(uri.LocalPath));
}
catch (Exception ex)
{
State = AsyncImageState.Failed;
RaiseEvent(new AsyncImageFailedEventArgs(ex));
}
}
else
{
RaiseEvent(
new AsyncImageFailedEventArgs(
new UriFormatException($"Uri has unsupported scheme. Uri:{source}")
)
);
}
}
else
{
RaiseEvent(
new AsyncImageFailedEventArgs(
new UriFormatException($"Relative paths aren't supported. Uri:{source}")
)
);
}
}
private void AttachSource(IImage? image)
{
if (ImagePart != null)
{
ImagePart.Source = image;
}
_tokenSource?.Cancel();
_tokenSource = new CancellationTokenSource();
if (image == null)
{
State = AsyncImageState.Unloaded;
ImageTransition?.Start(ImagePart, PlaceholderPart, true, _tokenSource.Token);
}
else if (image.Size != default)
{
State = AsyncImageState.Loaded;
ImageTransition?.Start(PlaceholderPart, ImagePart, true, _tokenSource.Token);
RaiseEvent(new RoutedEventArgs(OpenedEvent));
}
}
private async Task<Bitmap> LoadImageAsync(Uri? url, CancellationToken token)
{
if (await ProvideCachedResourceAsync(url, token) is { } bitmap)
{
return bitmap;
}
#if NET6_0_OR_GREATER
using var client = new HttpClient();
var stream = await client.GetStreamAsync(url, token).ConfigureAwait(false);
await using var memoryStream = new MemoryStream();
await stream.CopyToAsync(memoryStream, token).ConfigureAwait(false);
#elif NETSTANDARD2_0
using var client = new HttpClient();
var response = await client.GetAsync(url, token).ConfigureAwait(false);
var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
using var memoryStream = new MemoryStream();
await stream.CopyToAsync(memoryStream).ConfigureAwait(false);
#endif
memoryStream.Position = 0;
return new Bitmap(memoryStream);
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == SourceProperty)
{
SetSource(Source);
}
}
protected virtual async Task<Bitmap?> ProvideCachedResourceAsync(Uri? imageUri, CancellationToken token)
{
if (IsCacheEnabled && imageUri != null)
{
return await ImageCache
.Instance.GetFromCacheAsync(imageUri, cancellationToken: token)
.ConfigureAwait(false);
}
return null;
}
}

565
StabilityMatrix.Avalonia/Controls/VendorLabs/Cache/CacheBase.cs

@ -0,0 +1,565 @@
// Parts of this file was taken from Windows Community Toolkit CacheBase implementation
// https://github.com/CommunityToolkit/WindowsCommunityToolkit/blob/main/Microsoft.Toolkit.Uwp.UI/Cache/ImageCache.cs
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace StabilityMatrix.Avalonia.Controls.VendorLabs.Cache;
internal abstract class CacheBase<T>
{
private class ConcurrentRequest
{
public Task<T?>? Task { get; set; }
public bool EnsureCachedCopy { get; set; }
}
private readonly SemaphoreSlim _cacheFolderSemaphore = new SemaphoreSlim(1);
private string? _baseFolder = null;
private string? _cacheFolderName = null;
private string? _cacheFolder = null;
private InMemoryStorage<T>? _inMemoryFileStorage = null;
private ConcurrentDictionary<string, ConcurrentRequest> _concurrentTasks =
new ConcurrentDictionary<string, ConcurrentRequest>();
private HttpClient? _httpClient = null;
/// <summary>
/// Initializes a new instance of the <see cref="CacheBase{T}"/> class.
/// </summary>
protected CacheBase()
{
var options = CacheOptions.Default;
CacheDuration = options?.CacheDuration ?? TimeSpan.FromDays(1);
_baseFolder = options?.BaseCachePath ?? null;
_inMemoryFileStorage = new InMemoryStorage<T>();
RetryCount = 1;
}
/// <summary>
/// Gets or sets the life duration of every cache entry.
/// </summary>
public TimeSpan CacheDuration { get; set; }
/// <summary>
/// Gets or sets the number of retries trying to ensure the file is cached.
/// </summary>
public uint RetryCount { get; set; }
/// <summary>
/// Gets or sets max in-memory item storage count
/// </summary>
public int MaxMemoryCacheCount
{
get { return _inMemoryFileStorage?.MaxItemCount ?? 0; }
set
{
if (_inMemoryFileStorage != null)
_inMemoryFileStorage.MaxItemCount = value;
}
}
/// <summary>
/// Gets instance of <see cref="HttpClient"/>
/// </summary>
protected HttpClient HttpClient
{
get
{
if (_httpClient == null)
{
var messageHandler = new HttpClientHandler();
_httpClient = new HttpClient(messageHandler);
}
return _httpClient;
}
}
/// <summary>
/// Initializes FileCache and provides root folder and cache folder name
/// </summary>
/// <param name="folder">Folder that is used as root for cache</param>
/// <param name="folderName">Cache folder name</param>
/// <param name="httpMessageHandler">instance of <see cref="HttpMessageHandler"/></param>
/// <returns>awaitable task</returns>
public virtual async Task InitializeAsync(
string? folder = null,
string? folderName = null,
HttpMessageHandler? httpMessageHandler = null
)
{
_baseFolder = folder;
_cacheFolderName = folderName;
_cacheFolder = await GetCacheFolderAsync().ConfigureAwait(false);
if (httpMessageHandler != null)
{
_httpClient = new HttpClient(httpMessageHandler);
}
}
/// <summary>
/// Clears all files in the cache
/// </summary>
/// <returns>awaitable task</returns>
public async Task ClearAsync()
{
var folder = await GetCacheFolderAsync().ConfigureAwait(false);
var files = Directory.EnumerateFiles(folder!);
await InternalClearAsync(files.Select(x => x as string)).ConfigureAwait(false);
_inMemoryFileStorage?.Clear();
}
/// <summary>
/// Clears file if it has expired
/// </summary>
/// <param name="duration">timespan to compute whether file has expired or not</param>
/// <returns>awaitable task</returns>
public Task ClearAsync(TimeSpan duration)
{
return RemoveExpiredAsync(duration);
}
/// <summary>
/// Removes cached files that have expired
/// </summary>
/// <param name="duration">Optional timespan to compute whether file has expired or not. If no value is supplied, <see cref="CacheDuration"/> is used.</param>
/// <returns>awaitable task</returns>
public async Task RemoveExpiredAsync(TimeSpan? duration = null)
{
TimeSpan expiryDuration = duration ?? CacheDuration;
var folder = await GetCacheFolderAsync().ConfigureAwait(false);
var files = Directory.EnumerateFiles(folder!);
var filesToDelete = new List<string>();
foreach (var file in files)
{
if (file == null)
{
continue;
}
if (await IsFileOutOfDateAsync(file, expiryDuration, false).ConfigureAwait(false))
{
filesToDelete.Add(file);
}
}
await InternalClearAsync(filesToDelete).ConfigureAwait(false);
_inMemoryFileStorage?.Clear(expiryDuration);
}
/// <summary>
/// Removed items based on uri list passed
/// </summary>
/// <param name="uriForCachedItems">Enumerable uri list</param>
/// <returns>awaitable Task</returns>
public async Task RemoveAsync(IEnumerable<Uri> uriForCachedItems)
{
if (uriForCachedItems == null || !uriForCachedItems.Any())
{
return;
}
var folder = await GetCacheFolderAsync().ConfigureAwait(false);
var files = Directory.EnumerateFiles(folder!);
var filesToDelete = new List<string>();
var keys = new List<string>();
Dictionary<string, string> hashDictionary = new Dictionary<string, string>();
foreach (var file in files)
{
hashDictionary.Add(Path.GetFileName(file), file);
}
foreach (var uri in uriForCachedItems)
{
string fileName = GetCacheFileName(uri);
if (hashDictionary.TryGetValue(fileName, out var file))
{
filesToDelete.Add(file);
keys.Add(fileName);
}
}
await InternalClearAsync(filesToDelete).ConfigureAwait(false);
_inMemoryFileStorage?.Remove(keys);
}
/// <summary>
/// Assures that item represented by Uri is cached.
/// </summary>
/// <param name="uri">Uri of the item</param>
/// <param name="throwOnError">Indicates whether or not exception should be thrown if item cannot be cached</param>
/// <param name="storeToMemoryCache">Indicates if item should be loaded into the in-memory storage</param>
/// <param name="cancellationToken">instance of <see cref="CancellationToken"/></param>
/// <returns>Awaitable Task</returns>
public Task PreCacheAsync(
Uri uri,
bool throwOnError = false,
bool storeToMemoryCache = false,
CancellationToken cancellationToken = default(CancellationToken)
)
{
return GetItemAsync(uri, throwOnError, !storeToMemoryCache, cancellationToken);
}
/// <summary>
/// Retrieves item represented by Uri from the cache. If the item is not found in the cache, it will try to downloaded and saved before returning it to the caller.
/// </summary>
/// <param name="uri">Uri of the item.</param>
/// <param name="throwOnError">Indicates whether or not exception should be thrown if item cannot be found / downloaded.</param>
/// <param name="cancellationToken">instance of <see cref="CancellationToken"/></param>
/// <returns>an instance of Generic type</returns>
public Task<T?> GetFromCacheAsync(
Uri uri,
bool throwOnError = false,
CancellationToken cancellationToken = default(CancellationToken)
)
{
return GetItemAsync(uri, throwOnError, false, cancellationToken);
}
/// <summary>
/// Gets the string containing cached item for given Uri
/// </summary>
/// <param name="uri">Uri of the item.</param>
/// <returns>a string</returns>
public async Task<string> GetFileFromCacheAsync(Uri uri)
{
var folder = await GetCacheFolderAsync().ConfigureAwait(false);
return Path.Combine(folder!, GetCacheFileName(uri));
}
/// <summary>
/// Retrieves item represented by Uri from the in-memory cache if it exists and is not out of date. If item is not found or is out of date, default instance of the generic type is returned.
/// </summary>
/// <param name="uri">Uri of the item.</param>
/// <returns>an instance of Generic type</returns>
public T? GetFromMemoryCache(Uri uri)
{
T? instance = default(T);
string fileName = GetCacheFileName(uri);
if (_inMemoryFileStorage?.MaxItemCount > 0)
{
var msi = _inMemoryFileStorage?.GetItem(fileName, CacheDuration);
if (msi != null)
{
instance = msi.Item;
}
}
return instance;
}
/// <summary>
/// Cache specific hooks to process items from HTTP response
/// </summary>
/// <param name="stream">input stream</param>
/// <returns>awaitable task</returns>
protected abstract Task<T> ConvertFromAsync(Stream stream);
/// <summary>
/// Cache specific hooks to process items from HTTP response
/// </summary>
/// <param name="baseFile">storage file</param>
/// <returns>awaitable task</returns>
protected abstract Task<T> ConvertFromAsync(string baseFile);
/// <summary>
/// Override-able method that checks whether file is valid or not.
/// </summary>
/// <param name="file">storage file</param>
/// <param name="duration">cache duration</param>
/// <param name="treatNullFileAsOutOfDate">option to mark uninitialized file as expired</param>
/// <returns>bool indicate whether file has expired or not</returns>
protected virtual async Task<bool> IsFileOutOfDateAsync(
string file,
TimeSpan duration,
bool treatNullFileAsOutOfDate = true
)
{
if (file == null)
{
return treatNullFileAsOutOfDate;
}
var info = new FileInfo(file);
return info.Length == 0 || DateTime.Now.Subtract(info.LastWriteTime) > duration;
}
private static string GetCacheFileName(Uri uri)
{
return CreateHash64(uri.ToString()).ToString();
}
private static ulong CreateHash64(string str)
{
byte[] utf8 = System.Text.Encoding.UTF8.GetBytes(str);
ulong value = (ulong)utf8.Length;
for (int n = 0; n < utf8.Length; n++)
{
value += (ulong)utf8[n] << ((n * 5) % 56);
}
return value;
}
private async Task<T?> GetItemAsync(
Uri uri,
bool throwOnError,
bool preCacheOnly,
CancellationToken cancellationToken
)
{
T? instance = default(T);
string fileName = GetCacheFileName(uri);
_concurrentTasks.TryGetValue(fileName, out var request);
// if similar request exists check if it was preCacheOnly and validate that current request isn't preCacheOnly
if (request != null && request.EnsureCachedCopy && !preCacheOnly)
{
if (request.Task != null)
await request.Task.ConfigureAwait(false);
request = null;
}
if (request == null)
{
request = new ConcurrentRequest()
{
Task = GetFromCacheOrDownloadAsync(uri, fileName, preCacheOnly, cancellationToken),
EnsureCachedCopy = preCacheOnly
};
_concurrentTasks[fileName] = request;
}
try
{
if (request.Task != null)
instance = await request.Task.ConfigureAwait(false);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine(ex.Message);
if (throwOnError)
{
throw;
}
}
finally
{
_concurrentTasks.TryRemove(fileName, out _);
}
return instance;
}
private async Task<T?> GetFromCacheOrDownloadAsync(
Uri uri,
string fileName,
bool preCacheOnly,
CancellationToken cancellationToken
)
{
T? instance = default(T);
if (_inMemoryFileStorage?.MaxItemCount > 0)
{
var msi = _inMemoryFileStorage?.GetItem(fileName, CacheDuration);
if (msi != null)
{
instance = msi.Item;
}
}
if (instance != null)
{
return instance;
}
var folder = await GetCacheFolderAsync().ConfigureAwait(false);
var baseFile = Path.Combine(folder!, fileName);
bool downloadDataFile =
!File.Exists(baseFile)
|| await IsFileOutOfDateAsync(baseFile, CacheDuration).ConfigureAwait(false);
if (!File.Exists(baseFile))
{
File.Create(baseFile).Dispose();
}
if (downloadDataFile)
{
uint retries = 0;
try
{
while (retries < RetryCount)
{
try
{
instance = await DownloadFileAsync(uri, baseFile, preCacheOnly, cancellationToken)
.ConfigureAwait(false);
if (instance != null)
{
break;
}
}
catch (FileNotFoundException) { }
retries++;
}
}
catch (Exception ex)
{
File.Delete(baseFile);
throw; // re-throwing the exception changes the stack trace. just throw
}
}
if (EqualityComparer<T>.Default.Equals(instance, default(T)) && !preCacheOnly)
{
instance = await ConvertFromAsync(baseFile).ConfigureAwait(false);
if (_inMemoryFileStorage?.MaxItemCount > 0)
{
var properties = new FileInfo(baseFile);
var msi = new InMemoryStorageItem<T>(fileName, properties.LastWriteTime, instance);
_inMemoryFileStorage?.SetItem(msi);
}
}
return instance;
}
private async Task<T?> DownloadFileAsync(
Uri uri,
string baseFile,
bool preCacheOnly,
CancellationToken cancellationToken
)
{
T? instance = default(T);
using (MemoryStream ms = new MemoryStream())
{
using (var stream = await HttpClient.GetStreamAsync(uri))
{
stream.CopyTo(ms);
ms.Flush();
ms.Position = 0;
using (var fs = File.Open(baseFile, FileMode.OpenOrCreate, FileAccess.Write))
{
ms.CopyTo(fs);
fs.Flush();
ms.Position = 0;
}
}
// if its pre-cache we aren't looking to load items in memory
if (!preCacheOnly)
{
instance = await ConvertFromAsync(ms).ConfigureAwait(false);
}
}
return instance;
}
private async Task InternalClearAsync(IEnumerable<string?> files)
{
foreach (var file in files)
{
try
{
File.Delete(file!);
}
catch
{
// Just ignore errors for now}
}
}
}
/// <summary>
/// Initializes with default values if user has not initialized explicitly
/// </summary>
/// <returns>awaitable task</returns>
private async Task ForceInitialiseAsync()
{
if (_cacheFolder != null)
{
return;
}
await _cacheFolderSemaphore.WaitAsync().ConfigureAwait(false);
_inMemoryFileStorage = new InMemoryStorage<T>();
if (_baseFolder == null)
{
_baseFolder = Path.GetTempPath();
}
if (string.IsNullOrWhiteSpace(_cacheFolderName))
{
_cacheFolderName = GetType().Name;
}
try
{
_cacheFolder = Path.Combine(_baseFolder, _cacheFolderName);
Directory.CreateDirectory(_cacheFolder);
}
finally
{
_cacheFolderSemaphore.Release();
}
}
private async Task<string?> GetCacheFolderAsync()
{
if (_cacheFolder == null)
{
await ForceInitialiseAsync().ConfigureAwait(false);
}
return _cacheFolder;
}
}

18
StabilityMatrix.Avalonia/Controls/VendorLabs/Cache/CacheOptions.cs

@ -0,0 +1,18 @@
using System;
namespace StabilityMatrix.Avalonia.Controls.VendorLabs.Cache;
public class CacheOptions
{
private static CacheOptions? _cacheOptions;
public static CacheOptions Default => _cacheOptions ??= new CacheOptions();
public static void SetDefault(CacheOptions defaultCacheOptions)
{
_cacheOptions = defaultCacheOptions;
}
public string? BaseCachePath { get; set; }
public TimeSpan? CacheDuration { get; set; }
}

36
StabilityMatrix.Avalonia/Controls/VendorLabs/Cache/FileCache.cs

@ -0,0 +1,36 @@
using System.IO;
using System.Threading.Tasks;
namespace StabilityMatrix.Avalonia.Controls.VendorLabs.Cache;
/// <summary>
/// Provides methods and tools to cache files in a folder
/// </summary>
internal class FileCache : CacheBase<string>
{
/// <summary>
/// Private singleton field.
/// </summary>
private static FileCache? _instance;
/// <summary>
/// Gets public singleton property.
/// </summary>
public static FileCache Instance => _instance ?? (_instance = new FileCache());
protected override Task<string> ConvertFromAsync(Stream stream)
{
// nothing to do in this instance;
return Task.FromResult<string>("");
}
/// <summary>
/// Returns a cached path
/// </summary>
/// <param name="baseFile">storage file</param>
/// <returns>awaitable task</returns>
protected override Task<string> ConvertFromAsync(string baseFile)
{
return Task.FromResult(baseFile);
}
}

76
StabilityMatrix.Avalonia/Controls/VendorLabs/Cache/ImageCache.cs

@ -0,0 +1,76 @@
using System;
using System.IO;
using System.Threading.Tasks;
using Avalonia.Media.Imaging;
namespace StabilityMatrix.Avalonia.Controls.VendorLabs.Cache;
/// <summary>
/// Provides methods and tools to cache images in a folder
/// </summary>
internal class ImageCache : CacheBase<Bitmap>
{
/// <summary>
/// Private singleton field.
/// </summary>
[ThreadStatic]
private static ImageCache? _instance;
/// <summary>
/// Gets public singleton property.
/// </summary>
public static ImageCache Instance => _instance ?? (_instance = new ImageCache());
/// <summary>
/// Creates a bitmap from a stream
/// </summary>
/// <param name="stream">input stream</param>
/// <returns>awaitable task</returns>
protected override async Task<Bitmap> ConvertFromAsync(Stream stream)
{
if (stream.Length == 0)
{
throw new FileNotFoundException();
}
return new Bitmap(stream);
}
/// <summary>
/// Creates a bitmap from a cached file
/// </summary>
/// <param name="baseFile">file</param>
/// <returns>awaitable task</returns>
protected override async Task<Bitmap> ConvertFromAsync(string baseFile)
{
using (var stream = File.OpenRead(baseFile))
{
return await ConvertFromAsync(stream).ConfigureAwait(false);
}
}
/// <summary>
/// Checks whether file is valid or not.
/// </summary>
/// <param name="file">file</param>
/// <param name="duration">cache duration</param>
/// <param name="treatNullFileAsOutOfDate">option to mark uninitialized file as expired</param>
/// <returns>bool indicate whether file has expired or not</returns>
protected override async Task<bool> IsFileOutOfDateAsync(
string file,
TimeSpan duration,
bool treatNullFileAsOutOfDate = true
)
{
if (file == null)
{
return treatNullFileAsOutOfDate;
}
var fileInfo = new FileInfo(file);
return fileInfo.Length == 0
|| DateTime.Now.Subtract(File.GetLastAccessTime(file)) > duration
|| DateTime.Now.Subtract(File.GetLastWriteTime(file)) > duration;
}
}

156
StabilityMatrix.Avalonia/Controls/VendorLabs/Cache/InMemoryStorage.cs

@ -0,0 +1,156 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Labs.Controls.Cache;
namespace StabilityMatrix.Avalonia.Controls.VendorLabs.Cache;
/// <summary>
/// Generic in-memory storage of items
/// </summary>
/// <typeparam name="T">T defines the type of item stored</typeparam>
public class InMemoryStorage<T>
{
private int _maxItemCount;
private ConcurrentDictionary<string, InMemoryStorageItem<T>> _inMemoryStorage =
new ConcurrentDictionary<string, InMemoryStorageItem<T>>();
private object _settingMaxItemCountLocker = new object();
/// <summary>
/// Gets or sets the maximum count of Items that can be stored in this InMemoryStorage instance.
/// </summary>
public int MaxItemCount
{
get { return _maxItemCount; }
set
{
if (_maxItemCount == value)
{
return;
}
_maxItemCount = value;
lock (_settingMaxItemCountLocker)
{
EnsureStorageBounds(value);
}
}
}
/// <summary>
/// Clears all items stored in memory
/// </summary>
public void Clear()
{
_inMemoryStorage.Clear();
}
/// <summary>
/// Clears items stored in memory based on duration passed
/// </summary>
/// <param name="duration">TimeSpan to identify expired items</param>
public void Clear(TimeSpan duration)
{
var expirationDate = DateTime.Now.Subtract(duration);
var itemsToRemove = _inMemoryStorage
.Where(kvp => kvp.Value.LastUpdated <= expirationDate)
.Select(kvp => kvp.Key);
if (itemsToRemove.Any())
{
Remove(itemsToRemove);
}
}
/// <summary>
/// Remove items based on provided keys
/// </summary>
/// <param name="keys">identified of the in-memory storage item</param>
public void Remove(IEnumerable<string> keys)
{
foreach (var key in keys)
{
if (string.IsNullOrWhiteSpace(key))
{
continue;
}
_inMemoryStorage.TryRemove(key, out _);
}
}
/// <summary>
/// Add new item to in-memory storage
/// </summary>
/// <param name="item">item to be stored</param>
public void SetItem(InMemoryStorageItem<T> item)
{
if (MaxItemCount == 0)
{
return;
}
_inMemoryStorage[item.Id] = item;
// ensure max limit is maintained. trim older entries first
if (_inMemoryStorage.Count > MaxItemCount)
{
var itemsToRemove = _inMemoryStorage
.OrderBy(kvp => kvp.Value.Created)
.Take(_inMemoryStorage.Count - MaxItemCount)
.Select(kvp => kvp.Key);
Remove(itemsToRemove);
}
}
/// <summary>
/// Get item from in-memory storage as long as it has not ex
/// </summary>
/// <param name="id">id of the in-memory storage item</param>
/// <param name="duration">timespan denoting expiration</param>
/// <returns>Valid item if not out of date or return null if out of date or item does not exist</returns>
public InMemoryStorageItem<T>? GetItem(string id, TimeSpan duration)
{
if (!_inMemoryStorage.TryGetValue(id, out var tempItem))
{
return null;
}
var expirationDate = DateTime.Now.Subtract(duration);
if (tempItem.LastUpdated > expirationDate)
{
return tempItem;
}
_inMemoryStorage.TryRemove(id, out _);
return null;
}
private void EnsureStorageBounds(int maxCount)
{
if (_inMemoryStorage.Count == 0)
{
return;
}
if (maxCount == 0)
{
_inMemoryStorage.Clear();
return;
}
if (_inMemoryStorage.Count > maxCount)
{
Remove(_inMemoryStorage.Keys.Take(_inMemoryStorage.Count - maxCount));
}
}
}

49
StabilityMatrix.Avalonia/Controls/VendorLabs/Cache/InMemoryStorageItem.cs

@ -0,0 +1,49 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
namespace StabilityMatrix.Avalonia.Controls.VendorLabs.Cache;
/// <summary>
/// Generic InMemoryStorageItem holds items for InMemoryStorage.
/// </summary>
/// <typeparam name="T">Type is set by consuming cache</typeparam>
public class InMemoryStorageItem<T>
{
/// <summary>
/// Gets the item identifier
/// </summary>
public string Id { get; private set; }
/// <summary>
/// Gets the item created timestamp.
/// </summary>
public DateTime Created { get; private set; }
/// <summary>
/// Gets the item last updated timestamp.
/// </summary>
public DateTime LastUpdated { get; private set; }
/// <summary>
/// Gets the item being stored.
/// </summary>
public T Item { get; private set; }
/// <summary>
/// Initializes a new instance of the <see cref="InMemoryStorageItem{T}"/> class.
/// Constructor for InMemoryStorageItem
/// </summary>
/// <param name="id">uniquely identifies the item</param>
/// <param name="lastUpdated">last updated timestamp</param>
/// <param name="item">the item being stored</param>
public InMemoryStorageItem(string id, DateTime lastUpdated, T item)
{
Id = id;
LastUpdated = lastUpdated;
Item = item;
Created = DateTime.Now;
}
}

21
StabilityMatrix.Avalonia/Controls/VendorLabs/LICENSE

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 AvaloniaUI
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

3
StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj.DotSettings

@ -2,4 +2,5 @@
<s:String x:Key="/Default/CodeEditing/Localization/Localizable/@EntryValue">Yes</s:String>
<s:String x:Key="/Default/CodeEditing/Localization/LocalizableInspector/@EntryValue">Pessimistic</s:String>
<s:String x:Key="/Default/CodeInspection/Daemon/ConfigureAwaitAnalysisMode/@EntryValue">UI</s:String>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=controls_005Cpropertygrid/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=controls_005Cpropertygrid/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=controls_005Cvendorlabs_005Casyncimage/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

Loading…
Cancel
Save