Skip to content

Commit

Permalink
Remove direct file system calls from HotAvalonia
Browse files Browse the repository at this point in the history
Use `IFileSystem` instead.
This way we can finally implement #10.
  • Loading branch information
Kira-NT committed Jan 7, 2025
1 parent 9238d7e commit 5b1938b
Show file tree
Hide file tree
Showing 20 changed files with 1,233 additions and 585 deletions.
25 changes: 0 additions & 25 deletions src/HotAvalonia/Assets/AssetInfo.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
using System.Reflection;
using HotAvalonia.Helpers;
using Paths = System.IO.Path;

namespace HotAvalonia.Assets;

Expand Down Expand Up @@ -43,27 +41,4 @@ public AssetInfo(Uri uri, Assembly assembly, Uri project, string path)
Project = project;
Path = path;
}

/// <summary>
/// Initializes a new instance of the <see cref="AssetInfo"/> class.
/// </summary>
/// <param name="uri">The URI of the asset.</param>
/// <param name="assembly">The assembly associated with the asset.</param>
/// <param name="project">The path of the project root containing the asset.</param>
/// <param name="path">
/// The path of the asset. If <c>null</c>, the path is resolved automatically
/// from the <paramref name="project"/> and <paramref name="uri"/>.
/// </param>
public AssetInfo(Uri uri, Assembly assembly, string project, string? path = null)
{
project = Paths.GetFullPath(project);
char projectEnd = project.Length > 0 ? project[project.Length - 1] : Paths.DirectorySeparatorChar;
if (projectEnd != Paths.DirectorySeparatorChar && projectEnd != Paths.AltDirectorySeparatorChar)
project += Paths.DirectorySeparatorChar;

Uri = uri;
Assembly = assembly;
Project = new(project);
Path = path ?? UriHelper.ResolvePathFromUri(project, uri);
}
}
16 changes: 9 additions & 7 deletions src/HotAvalonia/Assets/DynamicAsset.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,12 @@ static DynamicAsset()
/// Initializes a new instance of the <see cref="DynamicAsset{TAsset}"/> class using a URI.
/// </summary>
/// <param name="uri">The URI of the asset.</param>
private DynamicAsset(Uri uri)
/// <param name="fileSystem">The file system where the asset can be found.</param>
private DynamicAsset(Uri uri, IFileSystem fileSystem)
{
_uri = uri;
_asset = s_create(this, null);
_fileObserver = new(uri.LocalPath, Refresh);
_fileObserver = new(fileSystem, uri.LocalPath, Refresh);
}

/// <summary>
Expand All @@ -90,11 +91,12 @@ private DynamicAsset(Uri uri)
/// <param name="stream">The stream containing the asset data.</param>
/// <param name="uri">The URI of the asset.</param>
/// <param name="fileName">The file name of the asset.</param>
private DynamicAsset(Stream stream, Uri uri, string fileName)
/// <param name="fileSystem">The file system where the asset can be found.</param>
private DynamicAsset(Stream stream, Uri uri, string fileName, IFileSystem fileSystem)
{
_uri = uri;
_asset = s_create(this, stream);
_fileObserver = new(fileName, Refresh);
_fileObserver = new(fileSystem, fileName, Refresh);
}

/// <summary>
Expand Down Expand Up @@ -122,7 +124,7 @@ public static TAsset Create(Uri uri, Uri? baseUri, AvaloniaProjectLocator projec
uri = new(baseUri, uri);

if (uri.IsAbsoluteUri && uri.IsFile)
return new DynamicAsset<TAsset>(uri).Asset;
return new DynamicAsset<TAsset>(uri, projectLocator.FileSystem).Asset;

(Stream stream, Assembly assembly) = AssetLoader.OpenAndGetAssembly(uri);
if (!uri.IsAbsoluteUri || uri.Scheme != UriHelper.AvaloniaResourceScheme)
Expand All @@ -131,8 +133,8 @@ public static TAsset Create(Uri uri, Uri? baseUri, AvaloniaProjectLocator projec
if (!projectLocator.TryGetDirectoryName(assembly, out string? rootPath))
return s_fromStream(stream);

string fileName = UriHelper.ResolvePathFromUri(rootPath, uri);
return new DynamicAsset<TAsset>(stream, uri, fileName).Asset;
string fileName = projectLocator.FileSystem.ResolvePathFromUri(rootPath, uri);
return new DynamicAsset<TAsset>(stream, uri, fileName, projectLocator.FileSystem).Asset;
}

/// <summary>
Expand Down
35 changes: 26 additions & 9 deletions src/HotAvalonia/Assets/DynamicAssetLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ internal class DynamicAssetLoader
/// <summary>
/// The file system accessor.
/// </summary>
protected readonly CachingFileSystemAccessor _fileSystem;
protected readonly IFileSystem _fileSystem;

/// <summary>
/// Initializes a new instance of the <see cref="DynamicAssetLoader"/> class.
Expand All @@ -45,7 +45,7 @@ protected DynamicAssetLoader(IAssetLoader fallbackAssetLoader, AvaloniaProjectLo
{
_assetLoader = fallbackAssetLoader ?? throw new ArgumentNullException(nameof(fallbackAssetLoader));
_projectLocator = projectLocator ?? throw new ArgumentNullException(nameof(projectLocator));
_fileSystem = new();
_fileSystem = new CachingFileSystem(projectLocator.FileSystem);
}

/// <inheritdoc cref="Create(IAssetLoader, AvaloniaProjectLocator)"/>
Expand Down Expand Up @@ -76,7 +76,7 @@ public bool Exists(Uri uri, Uri? baseUri = null)
return true;

if (TryGetAssetInfo(uri, baseUri, out AssetInfo? asset))
return File.Exists(asset.Path);
return _fileSystem.FileExists(asset.Path);

return false;
}
Expand All @@ -86,13 +86,13 @@ public IEnumerable<Uri> GetAssets(Uri uri, Uri? baseUri)
{
IEnumerable<Uri> assets = _assetLoader.GetAssets(uri, baseUri);

if (TryGetAssetInfo(uri, baseUri, out AssetInfo? asset) && Directory.Exists(asset.Path))
if (TryGetAssetInfo(uri, baseUri, out AssetInfo? asset) && _fileSystem.DirectoryExists(asset.Path))
{
Uri assemblyUri = new UriBuilder(asset.Uri.Scheme, asset.Uri.Host).Uri;
Uri projectUri = asset.Project;
IEnumerable<Uri> fileAssets = Directory
IEnumerable<Uri> fileAssets = _fileSystem
.EnumerateFiles(asset.Path, "*", SearchOption.AllDirectories)
.Select(x => projectUri.MakeRelativeUri(new(Path.GetFullPath(x))))
.Select(x => projectUri.MakeRelativeUri(new(x)))
.Select(x => new Uri(assemblyUri, x));

assets = assets.Concat(fileAssets).Distinct();
Expand All @@ -108,10 +108,10 @@ public Stream Open(Uri uri, Uri? baseUri = null)
/// <inheritdoc cref="IAssetLoader.OpenAndGetAssembly(Uri, Uri?)"/>
public (Stream stream, Assembly assembly) OpenAndGetAssembly(Uri uri, Uri? baseUri = null)
{
if (!TryGetAssetInfo(uri, baseUri, out AssetInfo? asset) || !_fileSystem.Exists(asset.Path))
if (!TryGetAssetInfo(uri, baseUri, out AssetInfo? asset) || !_fileSystem.FileExists(asset.Path))
return _assetLoader.OpenAndGetAssembly(uri, baseUri);

return (_fileSystem.Open(asset.Path), asset.Assembly);
return (_fileSystem.OpenRead(asset.Path), asset.Assembly);
}

/// <summary>
Expand Down Expand Up @@ -144,9 +144,26 @@ private bool TryGetAssetInfo(Uri uri, Uri? baseUri, [NotNullWhen(true)] out Asse
if (!_projectLocator.TryGetDirectoryName(assembly, out string? rootPath))
return false;

assetInfo = new(absoluteUri, assembly, rootPath);
assetInfo = ResolveAssetInfo(absoluteUri, assembly, rootPath);
return true;
}

/// <summary>
/// Resolves an asset from the given URI.
/// </summary>
/// <param name="uri">The URI of the asset.</param>
/// <param name="assembly">The assembly associated with the asset.</param>
/// <param name="project">The path of the project root containing the asset.</param>
/// <returns>A resolved <see cref="AssetInfo"/> instance.</returns>
private AssetInfo ResolveAssetInfo(Uri uri, Assembly assembly, string project)
{
project = _fileSystem.GetFullPath(project);
char projectEnd = project.Length > 0 ? project[project.Length - 1] : _fileSystem.DirectorySeparatorChar;
if (projectEnd != _fileSystem.DirectorySeparatorChar && projectEnd != _fileSystem.AltDirectorySeparatorChar)
project += _fileSystem.DirectorySeparatorChar;

return new(uri, assembly, new(project), _fileSystem.ResolvePathFromUri(project, uri));
}
}

/// <summary>
Expand Down
42 changes: 7 additions & 35 deletions src/HotAvalonia/AvaloniaControlManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,6 @@ public sealed class AvaloniaControlManager : IDisposable
/// </summary>
private readonly WeakSet<object> _controls;

/// <summary>
/// The name of the XAML file associated with the control.
/// </summary>
private string _fileName;

/// <summary>
/// The dynamically compiled populate method associated with the control.
/// </summary>
Expand All @@ -41,14 +36,9 @@ public sealed class AvaloniaControlManager : IDisposable
/// Initializes a new instance of the <see cref="AvaloniaControlManager"/> class.
/// </summary>
/// <param name="controlInfo">The Avalonia control information.</param>
/// <param name="fileName">The name of the XAML file.</param>
public AvaloniaControlManager(AvaloniaControlInfo controlInfo, string fileName)
public AvaloniaControlManager(AvaloniaControlInfo controlInfo)
{
_ = controlInfo ?? throw new ArgumentNullException(nameof(controlInfo));
_ = fileName ?? throw new ArgumentNullException(nameof(fileName));

_controlInfo = controlInfo;
_fileName = fileName;
_controlInfo = controlInfo ?? throw new ArgumentNullException(nameof(controlInfo));
_controls = new();

if (!TryInjectPopulateCallback(controlInfo, OnPopulate, out _populateInjection))
Expand All @@ -60,38 +50,20 @@ public AvaloniaControlManager(AvaloniaControlInfo controlInfo, string fileName)
/// </summary>
public AvaloniaControlInfo Control => _controlInfo;

/// <summary>
/// The name of the XAML file associated with the control.
/// </summary>
public string FileName
{
get => _fileName;
set => _fileName = value ?? throw new ArgumentNullException(nameof(value));
}

/// <inheritdoc/>
public void Dispose()
=> _populateInjection?.Dispose();

/// <summary>
/// Reloads the controls associated with this manager asynchronously.
/// </summary>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
public async Task ReloadAsync(CancellationToken cancellationToken = default)
{
if (!File.Exists(_fileName))
return;

string xaml = await FileHelper.ReadAllTextAsync(_fileName, cancellationToken: cancellationToken).ConfigureAwait(true);
await Dispatcher.UIThread.InvokeAsync(() => ReloadAsync(xaml, cancellationToken), DispatcherPriority.Render);
}

/// <summary>
/// Reloads the controls associated with this manager on the UI thread.
/// </summary>
/// <param name="xaml">The XAML markup to reload the control from.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
private async Task ReloadAsync(string xaml, CancellationToken cancellationToken)
public Task ReloadAsync(string xaml, CancellationToken cancellationToken = default)
=> Dispatcher.UIThread.InvokeAsync(() => UnsafeReloadAsync(xaml, cancellationToken), DispatcherPriority.Render);

/// <inheritdoc cref="ReloadAsync(string, CancellationToken)"/>
private async Task UnsafeReloadAsync(string xaml, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();

Expand Down
Loading

0 comments on commit 5b1938b

Please sign in to comment.