Browse Source

Merge pull request #11190 from AvaloniaUI/stable-AssetLoader

Introduce static AssetLoader
pull/11194/head
Nikita Tsukanov 3 years ago
committed by GitHub
parent
commit
c4da74a798
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 295
      src/Avalonia.Base/Platform/AssetLoader.cs
  2. 255
      src/Avalonia.Base/Platform/StandardAssetLoader.cs
  3. 2
      src/Avalonia.Base/Platform/StandardRuntimePlatformServices.cs
  4. 23
      tests/Avalonia.Base.UnitTests/AssetLoaderTests.cs
  5. 2
      tests/Avalonia.Benchmarks/Styling/ResourceBenchmarks.cs
  6. 2
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleIncludeTests.cs
  7. 2
      tests/Avalonia.RenderTests/TestBase.cs
  8. 8
      tests/Avalonia.UnitTests/TestServices.cs

295
src/Avalonia.Base/Platform/AssetLoader.cs

@ -1,281 +1,48 @@
using System;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Reflection;
#if !BUILDTASK
using Avalonia.Platform.Internal;
using Avalonia.Utilities;
#endif
namespace Avalonia.Platform
{
/// <summary>
/// Loads assets compiled into the application binary.
/// </summary>
public class AssetLoader
namespace Avalonia.Platform;
#if !BUILDTASK
: IAssetLoader
/// <inheritdoc cref="IAssetLoader"/>
#endif
{
public static class AssetLoader
{
#if !BUILDTASK
private static IAssemblyDescriptorResolver s_assemblyDescriptorResolver = new AssemblyDescriptorResolver();
private AssemblyDescriptor? _defaultResmAssembly;
/// <remarks>
/// Introduced for tests.
/// </remarks>
internal static void SetAssemblyDescriptorResolver(IAssemblyDescriptorResolver resolver) =>
s_assemblyDescriptorResolver = resolver;
/// <summary>
/// Initializes a new instance of the <see cref="AssetLoader"/> class.
/// </summary>
/// <param name="assembly">
/// The default assembly from which to load resm: assets for which no assembly is specified.
/// </param>
public AssetLoader(Assembly? assembly = null)
{
if (assembly == null)
assembly = Assembly.GetEntryAssembly();
if (assembly != null)
_defaultResmAssembly = new AssemblyDescriptor(assembly);
}
/// <summary>
/// Sets the default assembly from which to load assets for which no assembly is specified.
/// </summary>
/// <param name="assembly">The default assembly.</param>
public void SetDefaultAssembly(Assembly assembly)
{
_defaultResmAssembly = new AssemblyDescriptor(assembly);
}
/// <summary>
/// Checks if an asset with the specified URI exists.
/// </summary>
/// <param name="uri">The URI.</param>
/// <param name="baseUri">
/// A base URI to use if <paramref name="uri"/> is relative.
/// </param>
/// <returns>True if the asset could be found; otherwise false.</returns>
public bool Exists(Uri uri, Uri? baseUri = null)
{
return TryGetAsset(uri, baseUri, out _);
}
/// <summary>
/// Opens the asset with the requested URI.
/// </summary>
/// <param name="uri">The URI.</param>
/// <param name="baseUri">
/// A base URI to use if <paramref name="uri"/> is relative.
/// </param>
/// <returns>A stream containing the asset contents.</returns>
/// <exception cref="FileNotFoundException">
/// The asset could not be found.
/// </exception>
public Stream Open(Uri uri, Uri? baseUri = null) => OpenAndGetAssembly(uri, baseUri).Item1;
/// <summary>
/// Opens the asset with the requested URI and returns the asset stream and the
/// assembly containing the asset.
/// </summary>
/// <param name="uri">The URI.</param>
/// <param name="baseUri">
/// A base URI to use if <paramref name="uri"/> is relative.
/// </param>
/// <returns>
/// The stream containing the resource contents together with the assembly.
/// </returns>
/// <exception cref="FileNotFoundException">
/// The asset could not be found.
/// </exception>
public (Stream stream, Assembly assembly) OpenAndGetAssembly(Uri uri, Uri? baseUri = null)
{
if (TryGetAsset(uri, baseUri, out var assetDescriptor))
{
return (assetDescriptor.GetStream(), assetDescriptor.Assembly);
}
throw new FileNotFoundException($"The resource {uri} could not be found.");
}
public Assembly? GetAssembly(Uri uri, Uri? baseUri)
{
if (!uri.IsAbsoluteUri && baseUri != null)
{
uri = new Uri(baseUri, uri);
}
if (TryGetAssembly(uri, out var assemblyDescriptor))
{
return assemblyDescriptor.Assembly;
}
return null;
}
/// <summary>
/// Gets all assets of a folder and subfolders that match specified uri.
/// </summary>
/// <param name="uri">The URI.</param>
/// <param name="baseUri">Base URI that is used if <paramref name="uri"/> is relative.</param>
/// <returns>All matching assets as a tuple of the absolute path to the asset and the assembly containing the asset</returns>
public IEnumerable<Uri> GetAssets(Uri uri, Uri? baseUri)
{
if (uri.IsAbsoluteResm())
{
if (!TryGetAssembly(uri, out var assembly))
{
assembly = _defaultResmAssembly;
}
return assembly?.Resources?
.Where(x => x.Key.Contains(uri.GetUnescapeAbsolutePath()))
.Select(x => new Uri($"resm:{x.Key}?assembly={assembly.Name}")) ??
Enumerable.Empty<Uri>();
}
uri = uri.EnsureAbsolute(baseUri);
if (uri.IsAvares())
{
if (!TryGetResAsmAndPath(uri, out var assembly, out var path))
{
return Enumerable.Empty<Uri>();
}
private static IAssetLoader GetAssetLoader() => AvaloniaLocator.Current.GetRequiredService<IAssetLoader>();
if (assembly?.AvaloniaResources == null)
{
return Enumerable.Empty<Uri>();
}
/// <inheritdoc cref="IAssetLoader.SetDefaultAssembly"/>
public static void SetDefaultAssembly(Assembly assembly) => GetAssetLoader().SetDefaultAssembly(assembly);
if (path.Length > 0 && path[path.Length - 1] != '/')
{
path += '/';
}
/// <inheritdoc cref="IAssetLoader.Exists"/>
public static bool Exists(Uri uri, Uri? baseUri = null) => GetAssetLoader().Exists(uri, baseUri);
return assembly.AvaloniaResources
.Where(r => r.Key.StartsWith(path, StringComparison.Ordinal))
.Select(x => new Uri($"avares://{assembly.Name}{x.Key}"));
}
/// <inheritdoc cref="IAssetLoader.Open"/>
public static Stream Open(Uri uri, Uri? baseUri = null) => GetAssetLoader().Open(uri, baseUri);
return Enumerable.Empty<Uri>();
}
/// <inheritdoc cref="IAssetLoader.OpenAndGetAssembly"/>
public static (Stream stream, Assembly assembly) OpenAndGetAssembly(Uri uri, Uri? baseUri = null)
=> GetAssetLoader().OpenAndGetAssembly(uri, baseUri);
private bool TryGetAsset(Uri uri, Uri? baseUri, [NotNullWhen(true)] out IAssetDescriptor? assetDescriptor)
{
assetDescriptor = null;
/// <inheritdoc cref="IAssetLoader.GetAssembly"/>
public static Assembly? GetAssembly(Uri uri, Uri? baseUri = null)
=> GetAssetLoader().GetAssembly(uri, baseUri);
if (uri.IsAbsoluteResm())
{
if (!TryGetAssembly(uri, out var assembly) && !TryGetAssembly(baseUri, out assembly))
{
assembly = _defaultResmAssembly;
}
if (assembly?.Resources != null)
{
var resourceKey = uri.AbsolutePath;
if (assembly.Resources.TryGetValue(resourceKey, out assetDescriptor))
{
return true;
}
}
}
uri = uri.EnsureAbsolute(baseUri);
if (uri.IsAvares())
{
if (TryGetResAsmAndPath(uri, out var assembly, out var path))
{
if (assembly.AvaloniaResources == null)
{
return false;
}
if (assembly.AvaloniaResources.TryGetValue(path, out assetDescriptor))
{
return true;
}
}
}
return false;
}
private static bool TryGetResAsmAndPath(Uri uri, [NotNullWhen(true)] out IAssemblyDescriptor? assembly, out string path)
{
path = uri.GetUnescapeAbsolutePath();
if (TryLoadAssembly(uri.Authority, out assembly))
{
return true;
}
return false;
}
private static bool TryGetAssembly(Uri? uri, [NotNullWhen(true)] out IAssemblyDescriptor? assembly)
{
assembly = null;
if (uri != null)
{
if (!uri.IsAbsoluteUri)
{
return false;
}
if (uri.IsAvares() && TryGetResAsmAndPath(uri, out assembly, out _))
{
return true;
}
if (uri.IsResm())
{
var assemblyName = uri.GetAssemblyNameFromQuery();
if (assemblyName.Length > 0 && TryLoadAssembly(assemblyName, out assembly))
{
return true;
}
}
}
return false;
}
private static bool TryLoadAssembly(string assemblyName, [NotNullWhen(true)] out IAssemblyDescriptor? assembly)
{
assembly = null;
try
{
assembly = s_assemblyDescriptorResolver.GetAssembly(assemblyName);
return true;
}
catch (Exception) { }
return false;
}
/// <inheritdoc cref="IAssetLoader.GetAssets"/>
public static IEnumerable<Uri> GetAssets(Uri uri, Uri? baseUri)
=> GetAssetLoader().GetAssets(uri, baseUri);
#endif
public static void RegisterResUriParsers()
{
if (!UriParser.IsKnownScheme("avares"))
UriParser.Register(new GenericUriParser(
GenericUriParserOptions.GenericAuthority |
GenericUriParserOptions.NoUserInfo |
GenericUriParserOptions.NoPort |
GenericUriParserOptions.NoQuery |
GenericUriParserOptions.NoFragment), "avares", -1);
}
internal static void RegisterResUriParsers()
{
if (!UriParser.IsKnownScheme("avares"))
UriParser.Register(new GenericUriParser(
GenericUriParserOptions.GenericAuthority |
GenericUriParserOptions.NoUserInfo |
GenericUriParserOptions.NoPort |
GenericUriParserOptions.NoQuery |
GenericUriParserOptions.NoFragment), "avares", -1);
}
}

255
src/Avalonia.Base/Platform/StandardAssetLoader.cs

@ -0,0 +1,255 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Reflection;
using Avalonia.Platform.Internal;
using Avalonia.Utilities;
namespace Avalonia.Platform;
/// <summary>
/// Loads assets compiled into the application binary.
/// </summary>
internal class StandardAssetLoader : IAssetLoader
{
private readonly IAssemblyDescriptorResolver _assemblyDescriptorResolver;
private AssemblyDescriptor? _defaultResmAssembly;
public StandardAssetLoader(IAssemblyDescriptorResolver resolver, Assembly? assembly = null)
{
if (assembly == null)
assembly = Assembly.GetEntryAssembly();
if (assembly != null)
_defaultResmAssembly = new AssemblyDescriptor(assembly);
_assemblyDescriptorResolver = resolver;
}
public StandardAssetLoader(Assembly? assembly = null) : this(new AssemblyDescriptorResolver(), assembly)
{
}
/// <summary>
/// Sets the default assembly from which to load assets for which no assembly is specified.
/// </summary>
/// <param name="assembly">The default assembly.</param>
public void SetDefaultAssembly(Assembly assembly)
{
_defaultResmAssembly = new AssemblyDescriptor(assembly);
}
/// <summary>
/// Checks if an asset with the specified URI exists.
/// </summary>
/// <param name="uri">The URI.</param>
/// <param name="baseUri">
/// A base URI to use if <paramref name="uri"/> is relative.
/// </param>
/// <returns>True if the asset could be found; otherwise false.</returns>
public bool Exists(Uri uri, Uri? baseUri = null)
{
return TryGetAsset(uri, baseUri, out _);
}
/// <summary>
/// Opens the asset with the requested URI.
/// </summary>
/// <param name="uri">The URI.</param>
/// <param name="baseUri">
/// A base URI to use if <paramref name="uri"/> is relative.
/// </param>
/// <returns>A stream containing the asset contents.</returns>
/// <exception cref="FileNotFoundException">
/// The asset could not be found.
/// </exception>
public Stream Open(Uri uri, Uri? baseUri = null) => OpenAndGetAssembly(uri, baseUri).Item1;
/// <summary>
/// Opens the asset with the requested URI and returns the asset stream and the
/// assembly containing the asset.
/// </summary>
/// <param name="uri">The URI.</param>
/// <param name="baseUri">
/// A base URI to use if <paramref name="uri"/> is relative.
/// </param>
/// <returns>
/// The stream containing the resource contents together with the assembly.
/// </returns>
/// <exception cref="FileNotFoundException">
/// The asset could not be found.
/// </exception>
public (Stream stream, Assembly assembly) OpenAndGetAssembly(Uri uri, Uri? baseUri = null)
{
if (TryGetAsset(uri, baseUri, out var assetDescriptor))
{
return (assetDescriptor.GetStream(), assetDescriptor.Assembly);
}
throw new FileNotFoundException($"The resource {uri} could not be found.");
}
public Assembly? GetAssembly(Uri uri, Uri? baseUri)
{
if (!uri.IsAbsoluteUri && baseUri != null)
{
uri = new Uri(baseUri, uri);
}
if (TryGetAssembly(uri, out var assemblyDescriptor))
{
return assemblyDescriptor.Assembly;
}
return null;
}
/// <summary>
/// Gets all assets of a folder and subfolders that match specified uri.
/// </summary>
/// <param name="uri">The URI.</param>
/// <param name="baseUri">Base URI that is used if <paramref name="uri"/> is relative.</param>
/// <returns>All matching assets as a tuple of the absolute path to the asset and the assembly containing the asset</returns>
public IEnumerable<Uri> GetAssets(Uri uri, Uri? baseUri)
{
if (uri.IsAbsoluteResm())
{
if (!TryGetAssembly(uri, out var assembly))
{
assembly = _defaultResmAssembly;
}
return assembly?.Resources?
.Where(x => x.Key.Contains(uri.GetUnescapeAbsolutePath()))
.Select(x => new Uri($"resm:{x.Key}?assembly={assembly.Name}")) ??
Enumerable.Empty<Uri>();
}
uri = uri.EnsureAbsolute(baseUri);
if (uri.IsAvares())
{
if (!TryGetResAsmAndPath(uri, out var assembly, out var path))
{
return Enumerable.Empty<Uri>();
}
if (assembly?.AvaloniaResources == null)
{
return Enumerable.Empty<Uri>();
}
if (path.Length > 0 && path[path.Length - 1] != '/')
{
path += '/';
}
return assembly.AvaloniaResources
.Where(r => r.Key.StartsWith(path, StringComparison.Ordinal))
.Select(x => new Uri($"avares://{assembly.Name}{x.Key}"));
}
return Enumerable.Empty<Uri>();
}
private bool TryGetAsset(Uri uri, Uri? baseUri, [NotNullWhen(true)] out IAssetDescriptor? assetDescriptor)
{
assetDescriptor = null;
if (uri.IsAbsoluteResm())
{
if (!TryGetAssembly(uri, out var assembly) && !TryGetAssembly(baseUri, out assembly))
{
assembly = _defaultResmAssembly;
}
if (assembly?.Resources != null)
{
var resourceKey = uri.AbsolutePath;
if (assembly.Resources.TryGetValue(resourceKey, out assetDescriptor))
{
return true;
}
}
}
uri = uri.EnsureAbsolute(baseUri);
if (uri.IsAvares())
{
if (TryGetResAsmAndPath(uri, out var assembly, out var path))
{
if (assembly.AvaloniaResources == null)
{
return false;
}
if (assembly.AvaloniaResources.TryGetValue(path, out assetDescriptor))
{
return true;
}
}
}
return false;
}
private bool TryGetResAsmAndPath(Uri uri, [NotNullWhen(true)] out IAssemblyDescriptor? assembly, out string path)
{
path = uri.GetUnescapeAbsolutePath();
if (TryLoadAssembly(uri.Authority, out assembly))
{
return true;
}
return false;
}
private bool TryGetAssembly(Uri? uri, [NotNullWhen(true)] out IAssemblyDescriptor? assembly)
{
assembly = null;
if (uri != null)
{
if (!uri.IsAbsoluteUri)
{
return false;
}
if (uri.IsAvares() && TryGetResAsmAndPath(uri, out assembly, out _))
{
return true;
}
if (uri.IsResm())
{
var assemblyName = uri.GetAssemblyNameFromQuery();
if (assemblyName.Length > 0 && TryLoadAssembly(assemblyName, out assembly))
{
return true;
}
}
}
return false;
}
private bool TryLoadAssembly(string assemblyName, [NotNullWhen(true)] out IAssemblyDescriptor? assembly)
{
assembly = null;
try
{
assembly = _assemblyDescriptorResolver.GetAssembly(assemblyName);
return true;
}
catch (Exception) { }
return false;
}
}

2
src/Avalonia.Base/Platform/StandardRuntimePlatformServices.cs

@ -14,7 +14,7 @@ namespace Avalonia.Platform
AssetLoader.RegisterResUriParsers();
AvaloniaLocator.CurrentMutable
.Bind<IRuntimePlatform>().ToConstant(standardPlatform)
.Bind<IAssetLoader>().ToConstant(new AssetLoader(assembly))
.Bind<IAssetLoader>().ToConstant(new StandardAssetLoader(assembly))
.Bind<IDynamicLibraryLoader>().ToConstant(
#if NET6_0_OR_GREATER
new Net6Loader()

23
tests/Avalonia.Base.UnitTests/AssetLoaderTests.cs

@ -7,8 +7,10 @@ using Xunit;
namespace Avalonia.Base.UnitTests;
public class AssetLoaderTests : IDisposable
public class AssetLoaderTests
{
private IAssemblyDescriptorResolver _resolver;
public class MockAssembly : Assembly { }
private const string AssemblyNameWithWhitespace = "Awesome Library";
@ -17,22 +19,20 @@ public class AssetLoaderTests : IDisposable
public AssetLoaderTests()
{
var resolver = Mock.Of<IAssemblyDescriptorResolver>();
_resolver = Mock.Of<IAssemblyDescriptorResolver>();
var descriptor = CreateAssemblyDescriptor(AssemblyNameWithWhitespace);
Mock.Get(resolver).Setup(x => x.GetAssembly(AssemblyNameWithWhitespace)).Returns(descriptor);
Mock.Get(_resolver).Setup(x => x.GetAssembly(AssemblyNameWithWhitespace)).Returns(descriptor);
descriptor = CreateAssemblyDescriptor(AssemblyNameWithNonAscii);
Mock.Get(resolver).Setup(x => x.GetAssembly(AssemblyNameWithNonAscii)).Returns(descriptor);
AssetLoader.SetAssemblyDescriptorResolver(resolver);
Mock.Get(_resolver).Setup(x => x.GetAssembly(AssemblyNameWithNonAscii)).Returns(descriptor);
}
[Fact]
public void AssemblyName_With_Whitespace_Should_Load_Resm()
{
var uri = new Uri($"resm:Avalonia.Base.UnitTests.Assets.something?assembly={AssemblyNameWithWhitespace}");
var loader = new AssetLoader();
var loader = new StandardAssetLoader(_resolver);
var assemblyActual = loader.GetAssembly(uri, null);
@ -43,7 +43,7 @@ public class AssetLoaderTests : IDisposable
public void AssemblyName_With_Non_ASCII_Should_Load_Avares()
{
var uri = new Uri($"avares://{AssemblyNameWithNonAscii}/Assets/something");
var loader = new AssetLoader();
var loader = new StandardAssetLoader(_resolver);
var assemblyActual = loader.GetAssembly(uri, null);
@ -54,7 +54,7 @@ public class AssetLoaderTests : IDisposable
public void Invalid_AssemblyName_Should_Yield_Empty_Enumerable()
{
var uri = new Uri($"avares://InvalidAssembly");
var loader = new AssetLoader();
var loader = new StandardAssetLoader(_resolver);
var assemblyActual = loader.GetAssets(uri, null);
@ -71,9 +71,4 @@ public class AssetLoaderTests : IDisposable
Mock.Get(descriptor).Setup(x => x.Assembly).Returns(assembly);
return descriptor;
}
public void Dispose()
{
AssetLoader.SetAssemblyDescriptorResolver(new AssemblyDescriptorResolver());
}
}

2
tests/Avalonia.Benchmarks/Styling/ResourceBenchmarks.cs

@ -17,7 +17,7 @@ namespace Avalonia.Benchmarks.Styling
private static IDisposable CreateApp()
{
var services = new TestServices(
assetLoader: new AssetLoader(),
assetLoader: new StandardAssetLoader(),
globalClock: new MockGlobalClock(),
platform: new AppBuilder().RuntimePlatform,
renderInterface: new MockPlatformRenderInterface(),

2
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleIncludeTests.cs

@ -282,7 +282,7 @@ public class StyleIncludeTests
public void StyleInclude_From_CodeBehind_Resolves_Compiled()
{
using var locatorScope = AvaloniaLocator.EnterScope();
AvaloniaLocator.CurrentMutable.BindToSelf<IAssetLoader>(new AssetLoader(GetType().Assembly));
AvaloniaLocator.CurrentMutable.BindToSelf<IAssetLoader>(new StandardAssetLoader(GetType().Assembly));
var sp = new TestServiceProvider();
var styleInclude = new StyleInclude(sp)

2
tests/Avalonia.RenderTests/TestBase.cs

@ -45,7 +45,7 @@ namespace Avalonia.Direct2D1.RenderTests
private static readonly TestDispatcherImpl threadingInterface =
new TestDispatcherImpl();
private static readonly IAssetLoader assetLoader = new AssetLoader();
private static readonly IAssetLoader assetLoader = new StandardAssetLoader();
static TestBase()
{

8
tests/Avalonia.UnitTests/TestServices.cs

@ -20,7 +20,7 @@ namespace Avalonia.UnitTests
public class TestServices
{
public static readonly TestServices StyledWindow = new TestServices(
assetLoader: new AssetLoader(),
assetLoader: new StandardAssetLoader(),
platform: new StandardRuntimePlatform(),
renderInterface: new MockPlatformRenderInterface(),
standardCursorFactory: Mock.Of<ICursorFactory>(),
@ -31,7 +31,7 @@ namespace Avalonia.UnitTests
windowingPlatform: new MockWindowingPlatform());
public static readonly TestServices MockPlatformRenderInterface = new TestServices(
assetLoader: new AssetLoader(),
assetLoader: new StandardAssetLoader(),
renderInterface: new MockPlatformRenderInterface(),
fontManagerImpl: new MockFontManagerImpl(),
textShaperImpl: new MockTextShaperImpl());
@ -50,13 +50,13 @@ namespace Avalonia.UnitTests
keyboardDevice: () => new KeyboardDevice(),
keyboardNavigation: new KeyboardNavigationHandler(),
inputManager: new InputManager(),
assetLoader: new AssetLoader(),
assetLoader: new StandardAssetLoader(),
renderInterface: new MockPlatformRenderInterface(),
fontManagerImpl: new MockFontManagerImpl(),
textShaperImpl: new MockTextShaperImpl());
public static readonly TestServices TextServices = new TestServices(
assetLoader: new AssetLoader(),
assetLoader: new StandardAssetLoader(),
renderInterface: new MockPlatformRenderInterface(),
fontManagerImpl: new HarfBuzzFontManagerImpl(),
textShaperImpl: new HarfBuzzTextShaperImpl());

Loading…
Cancel
Save