Browse Source

Merge pull request #8422 from AvaloniaUI/feature/lazy-load-resources

Lazy load resources
pull/8680/head
Max Katz 4 years ago
committed by GitHub
parent
commit
3d77c13c65
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 56
      src/Avalonia.Base/Controls/ResourceDictionary.cs
  2. 5
      src/Avalonia.Base/Controls/ResourceNodeExtensions.cs
  3. 8
      src/Avalonia.Base/Controls/Templates/ITemplateResult.cs
  4. 3
      src/Avalonia.Base/Controls/Templates/TemplateResult.cs
  5. 4
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs
  6. 90
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDeferredResourceTransformer.cs
  7. 9
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
  8. 207
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs

56
src/Avalonia.Base/Controls/ResourceDictionary.cs

@ -3,6 +3,7 @@ using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Collections;
using Avalonia.Controls.Templates;
namespace Avalonia.Controls
{
@ -29,7 +30,11 @@ namespace Avalonia.Controls
public object? this[object key]
{
get => _inner?[key];
get
{
TryGetValue(key, out var value);
return value;
}
set
{
Inner[key] = value;
@ -119,6 +124,12 @@ namespace Avalonia.Controls
Owner?.NotifyHostedResourcesChanged(ResourcesChangedEventArgs.Empty);
}
public void AddDeferred(object key, Func<IServiceProvider?, object?> factory)
{
Inner.Add(key, new DeferredItem(factory));
Owner?.NotifyHostedResourcesChanged(ResourcesChangedEventArgs.Empty);
}
public void Clear()
{
if (_inner?.Count > 0)
@ -143,10 +154,8 @@ namespace Avalonia.Controls
public bool TryGetResource(object key, out object? value)
{
if (_inner is not null && _inner.TryGetValue(key, out value))
{
if (TryGetValue(key, out value))
return true;
}
if (_mergedDictionaries != null)
{
@ -165,12 +174,28 @@ namespace Avalonia.Controls
public bool TryGetValue(object key, out object? value)
{
if (_inner is not null)
return _inner.TryGetValue(key, out value);
if (_inner is not null && _inner.TryGetValue(key, out value))
{
if (value is DeferredItem deffered)
{
_inner[key] = value = deffered.Factory(null) switch
{
ITemplateResult t => t.Result,
object v => v,
_ => null,
};
}
return true;
}
value = null;
return false;
}
public IEnumerator<KeyValuePair<object, object?>> GetEnumerator()
{
return _inner?.GetEnumerator() ?? Enumerable.Empty<KeyValuePair<object, object?>>().GetEnumerator();
}
void ICollection<KeyValuePair<object, object?>>.Add(KeyValuePair<object, object?> item)
{
@ -198,12 +223,17 @@ namespace Avalonia.Controls
return false;
}
public IEnumerator<KeyValuePair<object, object?>> GetEnumerator()
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
internal bool ContainsDeferredKey(object key)
{
return _inner?.GetEnumerator() ?? Enumerable.Empty<KeyValuePair<object, object?>>().GetEnumerator();
}
if (_inner is not null && _inner.TryGetValue(key, out var result))
{
return result is DeferredItem;
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
return false;
}
void IResourceProvider.AddOwner(IResourceHost owner)
{
@ -258,5 +288,11 @@ namespace Avalonia.Controls
}
}
}
private class DeferredItem
{
public DeferredItem(Func<IServiceProvider?, object?> factory) => Factory = factory;
public Func<IServiceProvider?, object?> Factory { get; }
}
}
}

5
src/Avalonia.Base/Controls/ResourceNodeExtensions.cs

@ -132,6 +132,11 @@ namespace Avalonia.Controls
{
_target.OwnerChanged += OwnerChanged;
_owner = _target.Owner;
if (_owner is object)
{
_owner.ResourcesChanged += ResourcesChanged;
}
}
protected override void Deinitialize()

8
src/Avalonia.Base/Controls/Templates/ITemplateResult.cs

@ -0,0 +1,8 @@
namespace Avalonia.Controls.Templates
{
public interface ITemplateResult
{
public object? Result { get; }
public INameScope NameScope { get; }
}
}

3
src/Avalonia.Controls/Templates/TemplateResult.cs → src/Avalonia.Base/Controls/Templates/TemplateResult.cs

@ -1,9 +1,10 @@
namespace Avalonia.Controls.Templates
{
public class TemplateResult<T>
public class TemplateResult<T> : ITemplateResult
{
public T Result { get; }
public INameScope NameScope { get; }
object? ITemplateResult.Result => Result;
public TemplateResult(T result, INameScope nameScope)
{

4
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs

@ -60,6 +60,10 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
InsertAfter<TypeReferenceResolver>(
new XDataTypeTransformer());
InsertBefore<DeferredContentTransformer>(
new AvaloniaXamlIlDeferredResourceTransformer()
);
// After everything else
InsertBefore<NewObjectTransformer>(
new AddNameScopeRegistration(),

90
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDeferredResourceTransformer.cs

@ -0,0 +1,90 @@
using System.Collections.Generic;
using System.Linq;
using XamlX.Ast;
using XamlX.Emit;
using XamlX.IL;
using XamlX.Transform;
using XamlX.TypeSystem;
namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
{
internal class AvaloniaXamlIlDeferredResourceTransformer : IXamlAstTransformer
{
public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node)
{
if (!(node is XamlPropertyAssignmentNode pa) || pa.Values.Count != 2)
return node;
if (!ShouldBeDeferred(pa.Values[1]))
return node;
var types = context.GetAvaloniaTypes();
if (pa.Property.DeclaringType == types.ResourceDictionary && pa.Property.Name == "Content")
{
pa.Values[1] = new XamlDeferredContentNode(pa.Values[1], types.XamlIlTypes.Object, context.Configuration);
pa.PossibleSetters = new List<IXamlPropertySetter>
{
new XamlDirectCallPropertySetter(types.ResourceDictionaryDeferredAdd),
};
}
else if (pa.Property.Name == "Resources" && pa.Property.Getter.ReturnType.Equals(types.IResourceDictionary))
{
pa.Values[1] = new XamlDeferredContentNode(pa.Values[1], types.XamlIlTypes.Object, context.Configuration);
pa.PossibleSetters = new List<IXamlPropertySetter>
{
new AdderSetter(pa.Property.Getter, types.ResourceDictionaryDeferredAdd),
};
}
return node;
}
private static bool ShouldBeDeferred(IXamlAstValueNode node)
{
// XAML compiler is currently strict about value types, allowing them to be created only through converters.
// At the moment it should be safe to not defer structs.
return !node.Type.GetClrType().IsValueType;
}
class AdderSetter : IXamlPropertySetter, IXamlEmitablePropertySetter<IXamlILEmitter>
{
private readonly IXamlMethod _getter;
private readonly IXamlMethod _adder;
public AdderSetter(IXamlMethod getter, IXamlMethod adder)
{
_getter = getter;
_adder = adder;
TargetType = getter.DeclaringType;
Parameters = adder.ParametersWithThis().Skip(1).ToList();
}
public IXamlType TargetType { get; }
public PropertySetterBinderParameters BinderParameters { get; } = new PropertySetterBinderParameters
{
AllowMultiple = true
};
public IReadOnlyList<IXamlType> Parameters { get; }
public void Emit(IXamlILEmitter emitter)
{
var locals = new Stack<XamlLocalsPool.PooledLocal>();
// Save all "setter" parameters
for (var c = Parameters.Count - 1; c >= 0; c--)
{
var loc = emitter.LocalsPool.GetLocal(Parameters[c]);
locals.Push(loc);
emitter.Stloc(loc.Local);
}
emitter.EmitCall(_getter);
while (locals.Count > 0)
using (var loc = locals.Pop())
emitter.Ldloc(loc.Local);
emitter.EmitCall(_adder, true);
}
}
}
}

9
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs

@ -98,6 +98,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
public IXamlType TextDecorations { get; }
public IXamlType TextTrimming { get; }
public IXamlType ISetter { get; }
public IXamlType IResourceDictionary { get; }
public IXamlType ResourceDictionary { get; }
public IXamlMethod ResourceDictionaryDeferredAdd { get; }
public AvaloniaXamlIlWellKnownTypes(TransformerConfiguration cfg)
{
@ -218,6 +221,12 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
TextDecorations = cfg.TypeSystem.GetType("Avalonia.Media.TextDecorations");
TextTrimming = cfg.TypeSystem.GetType("Avalonia.Media.TextTrimming");
ISetter = cfg.TypeSystem.GetType("Avalonia.Styling.ISetter");
IResourceDictionary = cfg.TypeSystem.GetType("Avalonia.Controls.IResourceDictionary");
ResourceDictionary = cfg.TypeSystem.GetType("Avalonia.Controls.ResourceDictionary");
ResourceDictionaryDeferredAdd = ResourceDictionary.FindMethod("AddDeferred", XamlIlTypes.Void, true, XamlIlTypes.Object,
cfg.TypeSystem.GetType("System.Func`2").MakeGenericType(
cfg.TypeSystem.GetType("System.IServiceProvider"),
XamlIlTypes.Object));
}
}

207
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs

@ -3,6 +3,7 @@ using Avalonia.Controls;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Templates;
using Avalonia.Media;
using Avalonia.Media.Immutable;
using Avalonia.Styling;
using Avalonia.UnitTests;
using Xunit;
@ -66,6 +67,212 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
}
}
[Fact]
public void Item_Is_Added_To_ResourceDictionary_As_Deferred()
{
using (StyledWindow())
{
var xaml = @"
<ResourceDictionary xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<SolidColorBrush x:Key='Red' Color='Red' />
</ResourceDictionary>";
var resources = (ResourceDictionary)AvaloniaRuntimeXamlLoader.Load(xaml);
Assert.True(resources.ContainsDeferredKey("Red"));
}
}
[Fact]
public void Item_Added_To_ResourceDictionary_Is_UnDeferred_On_Read()
{
using (StyledWindow())
{
var xaml = @"
<ResourceDictionary xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<SolidColorBrush x:Key='Red' Color='Red' />
</ResourceDictionary>";
var resources = (ResourceDictionary)AvaloniaRuntimeXamlLoader.Load(xaml);
Assert.True(resources.ContainsDeferredKey("Red"));
Assert.IsType<SolidColorBrush>(resources["Red"]);
Assert.False(resources.ContainsDeferredKey("Red"));
}
}
[Fact]
public void Item_Is_Added_To_Window_Resources_As_Deferred()
{
using (StyledWindow())
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Window.Resources>
<SolidColorBrush x:Key='Red' Color='Red' />
</Window.Resources>
</Window>";
var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
var resources = (ResourceDictionary)window.Resources;
Assert.True(resources.ContainsDeferredKey("Red"));
}
}
[Fact]
public void Item_Is_Added_To_Window_MergedDictionaries_As_Deferred()
{
using (StyledWindow())
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary>
<SolidColorBrush x:Key='Red' Color='Red' />
</ResourceDictionary>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
</Window>";
var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
var resources = (ResourceDictionary)window.Resources.MergedDictionaries[0];
Assert.True(resources.ContainsDeferredKey("Red"));
}
}
[Fact]
public void Item_Is_Added_To_Style_Resources_As_Deferred()
{
using (StyledWindow())
{
var xaml = @"
<Style xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Style.Resources>
<SolidColorBrush x:Key='Red' Color='Red' />
</Style.Resources>
</Style>";
var style = (Style)AvaloniaRuntimeXamlLoader.Load(xaml);
var resources = (ResourceDictionary)style.Resources;
Assert.True(resources.ContainsDeferredKey("Red"));
}
}
[Fact]
public void Item_Is_Added_To_Styles_Resources_As_Deferred()
{
using (StyledWindow())
{
var xaml = @"
<Styles xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Styles.Resources>
<SolidColorBrush x:Key='Red' Color='Red' />
</Styles.Resources>
</Styles>";
var style = (Styles)AvaloniaRuntimeXamlLoader.Load(xaml);
var resources = (ResourceDictionary)style.Resources;
Assert.True(resources.ContainsDeferredKey("Red"));
}
}
[Fact]
public void Item_Can_Be_StaticReferenced_As_Deferred()
{
using (StyledWindow())
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Window.Resources>
<SolidColorBrush x:Key='Red' Color='Red' />
</Window.Resources>
<Button>
<Button.Resources>
<StaticResource x:Key='Red2' ResourceKey='Red' />
</Button.Resources>
</Button>
</Window>";
var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
var windowResources = (ResourceDictionary)window.Resources;
var buttonResources = (ResourceDictionary)((Button)window.Content!).Resources;
Assert.True(windowResources.ContainsDeferredKey("Red"));
Assert.True(buttonResources.ContainsDeferredKey("Red2"));
}
}
[Fact]
public void Item_StaticReferenced_Is_UnDeferred_On_Read()
{
using (StyledWindow())
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Window.Resources>
<SolidColorBrush x:Key='Red' Color='Red' />
</Window.Resources>
<Button>
<Button.Resources>
<StaticResource x:Key='Red2' ResourceKey='Red' />
</Button.Resources>
</Button>
</Window>";
var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
var windowResources = (ResourceDictionary)window.Resources;
var buttonResources = (ResourceDictionary)((Button)window.Content!).Resources;
Assert.IsType<SolidColorBrush>(buttonResources["Red2"]);
Assert.False(windowResources.ContainsDeferredKey("Red"));
Assert.False(buttonResources.ContainsDeferredKey("Red2"));
}
}
[Fact]
public void Value_Type_With_Parse_Converter_Should_Not_Be_Deferred()
{
using (StyledWindow())
{
var xaml = @"
<ResourceDictionary xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Color x:Key='Red'>Red</Color>
</ResourceDictionary>";
var resources = (ResourceDictionary)AvaloniaRuntimeXamlLoader.Load(xaml);
Assert.False(resources.ContainsDeferredKey("Red"));
Assert.IsType<Color>(resources["Red"]);
}
}
[Fact]
public void Value_Type_With_Ctor_Converter_Should_Not_Be_Deferred()
{
using (StyledWindow())
{
var xaml = @"
<ResourceDictionary xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Thickness x:Key='Margin'>1 1 1 1</Thickness>
</ResourceDictionary>";
var resources = (ResourceDictionary)AvaloniaRuntimeXamlLoader.Load(xaml);
Assert.False(resources.ContainsDeferredKey("Margin"));
Assert.IsType<Thickness>(resources["Margin"]);
}
}
private IDisposable StyledWindow(params (string, string)[] assets)
{
var services = TestServices.StyledWindow.With(

Loading…
Cancel
Save