Skip to content

Commit

Permalink
Enable partial hot reload on unsupported devices
Browse files Browse the repository at this point in the history
Fixes #17
  • Loading branch information
Kir-Antipov committed Dec 3, 2024
1 parent fa1598f commit 1f0e80d
Show file tree
Hide file tree
Showing 4 changed files with 69 additions and 29 deletions.
86 changes: 58 additions & 28 deletions src/HotAvalonia/AvaloniaAssetManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using Avalonia.Platform;
using HotAvalonia.Assets;
using HotAvalonia.DependencyInjection;
using HotAvalonia.Helpers;
using HotAvalonia.Reflection.Inject;

namespace HotAvalonia;
Expand Down Expand Up @@ -45,34 +46,13 @@ public AvaloniaAssetManager(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));

Type[] converterParameters = [typeof(ITypeDescriptorContext), typeof(CultureInfo), typeof(object)];
MethodInfo toIcon = typeof(IconTypeConverter).GetMethod(nameof(IconTypeConverter.ConvertFrom), converterParameters)!;
MethodInfo toBitmap = typeof(BitmapTypeConverter).GetMethod(nameof(BitmapTypeConverter.ConvertFrom), converterParameters)!;

_injections =
[
// Ideally, we would inject into something like
// `Avalonia.PropertyStore.EffectiveValue`1.SetLocalValueAndRaise`
// to catch all these scenarios, including custom ones, automatically.
// However, generic injections are quite flaky to say the least,
// so it's better to avoid them if we want predictable results.
//
// Perhaps we could add automatic detection of similar cases via reflection?
..new (Type Type, string Name, AvaloniaProperty Property)[]
{
(typeof(Window), nameof(Window.Icon), Window.IconProperty),
(typeof(Image), nameof(Image.Source), Image.SourceProperty),
(typeof(ImageBrush), nameof(ImageBrush.Source), ImageBrush.SourceProperty),
(typeof(CroppedBitmap), nameof(CroppedBitmap.Source), CroppedBitmap.SourceProperty),
(typeof(ImageDrawing), nameof(ImageDrawing.ImageSource), ImageDrawing.ImageSourceProperty),
}.Select(static x => CallbackInjector.Inject(
x.Type.GetProperty(x.Name).SetMethod,
([Caller] AvaloniaObject obj, object value) => TryBindDynamicAsset(obj, x.Property, value))
),

CallbackInjector.Inject(toIcon, TryLoadDynamicAsset<WindowIcon, IconTypeConverter>),
CallbackInjector.Inject(toBitmap, TryLoadDynamicAsset<Bitmap, BitmapTypeConverter>),
];
if (!TryInjectAssetCallbacks(out _injections))
{
LoggingHelper.Logger?.Log(
this,
"Failed to subscribe to asset loading events. Icons and images won't be reloaded upon file changes."
);
}
}

/// <summary>
Expand Down Expand Up @@ -147,4 +127,54 @@ private static void TryBindDynamicAsset(AvaloniaObject obj, AvaloniaProperty pro
if (value is IObservable<object> observableValue)
obj.Bind(property, observableValue);
}

/// <summary>
/// Attempts to inject asset-related callbacks for various Avalonia control properties.
/// </summary>
/// <param name="injections">
/// When this method returns, contains an array of <see cref="IInjection"/> instances
/// for each successful callback injection.
/// </param>
/// <returns>
/// <c>true</c> if the callback injections were successful;
/// otherwise, <c>false</c>.
/// </returns>
private static bool TryInjectAssetCallbacks(out IInjection[] injections)
{
if (!CallbackInjector.SupportsOptimizedMethods)
{
injections = [];
return false;
}

Type[] converterParameters = [typeof(ITypeDescriptorContext), typeof(CultureInfo), typeof(object)];
MethodInfo toIcon = typeof(IconTypeConverter).GetMethod(nameof(IconTypeConverter.ConvertFrom), converterParameters)!;
MethodInfo toBitmap = typeof(BitmapTypeConverter).GetMethod(nameof(BitmapTypeConverter.ConvertFrom), converterParameters)!;

injections =
[
// Ideally, we would inject into something like
// `Avalonia.PropertyStore.EffectiveValue`1.SetLocalValueAndRaise`
// to catch all these scenarios, including custom ones, automatically.
// However, generic injections are quite flaky to say the least,
// so it's better to avoid them if we want predictable results.
//
// Perhaps we could add automatic detection of similar cases via reflection?
..new (Type Type, string Name, AvaloniaProperty Property)[]
{
(typeof(Window), nameof(Window.Icon), Window.IconProperty),
(typeof(Image), nameof(Image.Source), Image.SourceProperty),
(typeof(ImageBrush), nameof(ImageBrush.Source), ImageBrush.SourceProperty),
(typeof(CroppedBitmap), nameof(CroppedBitmap.Source), CroppedBitmap.SourceProperty),
(typeof(ImageDrawing), nameof(ImageDrawing.ImageSource), ImageDrawing.ImageSourceProperty),
}.Select(static x => CallbackInjector.Inject(
x.Type.GetProperty(x.Name).SetMethod,
([Caller] AvaloniaObject obj, object value) => TryBindDynamicAsset(obj, x.Property, value))
),

CallbackInjector.Inject(toIcon, TryLoadDynamicAsset<WindowIcon, IconTypeConverter>),
CallbackInjector.Inject(toBitmap, TryLoadDynamicAsset<Bitmap, BitmapTypeConverter>),
];
return true;
}
}
2 changes: 1 addition & 1 deletion src/HotAvalonia/Helpers/AvaloniaControlHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ static AvaloniaControlHelper()

Type? sreMethodBuilder = xamlLoaderAssembly.GetType("XamlX.IL.SreTypeSystem+SreTypeBuilder+SreMethodBuilder");
ConstructorInfo? sreMethodBuilderCtor = sreMethodBuilder?.GetConstructors(InstanceMember).FirstOrDefault(x => x.GetParameters().Length > 1);
if (sreMethodBuilderCtor is not null)
if (sreMethodBuilderCtor is not null && CallbackInjector.SupportsOptimizedMethods)
s_sreMethodBuilderInjection = CallbackInjector.Inject(sreMethodBuilderCtor, OnNewSreMethodBuilder);
}

Expand Down
5 changes: 5 additions & 0 deletions src/HotAvalonia/Reflection/Inject/CallbackInjector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ internal static class CallbackInjector
/// </summary>
public static bool IsSupported => MethodInjector.IsSupported;

/// <summary>
/// Indicates whether callback injection is supported in optimized assemblies.
/// </summary>
public static bool SupportsOptimizedMethods => MethodInjector.SupportsOptimizedMethods;

/// <summary>
/// Throws an exception if callback injection is not supported in the current runtime environment.
/// </summary>
Expand Down
5 changes: 5 additions & 0 deletions src/HotAvalonia/Reflection/Inject/MethodInjector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ internal static class MethodInjector
/// </summary>
public static bool IsSupported => InjectionType is not InjectionType.None;

/// <summary>
/// Indicates whether method injection is supported in optimized assemblies.
/// </summary>
public static bool SupportsOptimizedMethods => InjectionType is InjectionType.Native;

/// <summary>
/// Injects a replacement method implementation for the specified source method.
/// </summary>
Expand Down

0 comments on commit 1f0e80d

Please sign in to comment.