Browse Source

Merge pull request #11265 from AvaloniaUI/resources-fix

ResourceDictionary xaml tests
pull/11283/head
Dan Walmsley 3 years ago
committed by GitHub
parent
commit
71fada936b
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 50
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlIncludeGroupTransformer.cs
  2. 10
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlThemeVariantProviderTransformer.cs
  3. 2
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
  4. 179
      tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/ResourceIncludeTests.cs
  5. 17
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs

50
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlIncludeGroupTransformer.cs

@ -36,13 +36,29 @@ internal class AvaloniaXamlIncludeTransformer : IXamlAstGroupTransformer
throw new InvalidOperationException($"\"{nodeTypeName}\".Loaded property is expected to be defined"); throw new InvalidOperationException($"\"{nodeTypeName}\".Loaded property is expected to be defined");
} }
if (valueNode.Manipulation is not XamlObjectInitializationNode if (valueNode.Manipulation is not XamlObjectInitializationNode initializationNode)
{
Manipulation: XamlPropertyAssignmentNode { Property: { Name: "Source" } } sourceProperty
})
{ {
throw new XamlDocumentParseException(context.CurrentDocument, throw new XamlDocumentParseException(context.CurrentDocument,
$"Source property must be set on the \"{nodeTypeName}\" node.", valueNode); $"Invalid \"{nodeTypeName}\" node initialization.", valueNode);
}
var additionalProperties = new List<IXamlAstManipulationNode>();
if (initializationNode.Manipulation is not XamlPropertyAssignmentNode { Property: { Name: "Source" } } sourceProperty)
{
if (initializationNode.Manipulation is XamlManipulationGroupNode manipulationGroup
&& manipulationGroup.Children.OfType<XamlPropertyAssignmentNode>()
.FirstOrDefault(p => p.Property.Name == "Source") is { } sourceProperty2)
{
sourceProperty = sourceProperty2;
// We need to copy some additional properties from ResourceInclude to ResourceDictionary except the Source one.
// If there is any missing properties, then XAML compiler will throw an error in the emitter code.
additionalProperties = manipulationGroup.Children.Where(c => c != sourceProperty2).ToList();
}
else
{
throw new XamlDocumentParseException(context.CurrentDocument,
$"Source property must be set on the \"{nodeTypeName}\" node.", valueNode);
}
} }
var (assetPathUri, sourceUriNode) = ResolveSourceFromXamlInclude(context, nodeTypeName, sourceProperty, false); var (assetPathUri, sourceUriNode) = ResolveSourceFromXamlInclude(context, nodeTypeName, sourceProperty, false);
@ -65,12 +81,12 @@ internal class AvaloniaXamlIncludeTransformer : IXamlAstGroupTransformer
{ {
if (targetDocument.BuildMethod is not null) if (targetDocument.BuildMethod is not null)
{ {
return FromMethod(context, targetDocument.BuildMethod, sourceUriNode, expectedLoadedType, node, assetPathUri, assembly); return FromMethod(context, targetDocument.BuildMethod, sourceUriNode, expectedLoadedType, node, assetPathUri, assembly, additionalProperties);
} }
if (targetDocument.ClassType is not null) if (targetDocument.ClassType is not null)
{ {
return FromType(context, targetDocument.ClassType, sourceUriNode, expectedLoadedType, node, assetPathUri, assembly); return FromType(context, targetDocument.ClassType, sourceUriNode, expectedLoadedType, node, assetPathUri, assembly, additionalProperties);
} }
return context.ParseError( return context.ParseError(
@ -95,11 +111,11 @@ internal class AvaloniaXamlIncludeTransformer : IXamlAstGroupTransformer
var buildMethod = avaResType.FindMethod(m => m.Name == relativeName); var buildMethod = avaResType.FindMethod(m => m.Name == relativeName);
if (buildMethod is not null) if (buildMethod is not null)
{ {
return FromMethod(context, buildMethod, sourceUriNode, expectedLoadedType, node, assetPathUri, assembly); return FromMethod(context, buildMethod, sourceUriNode, expectedLoadedType, node, assetPathUri, assembly, additionalProperties);
} }
else if (assetAssembly.FindType(fullTypeName) is { } type) else if (assetAssembly.FindType(fullTypeName) is { } type)
{ {
return FromType(context, type, sourceUriNode, expectedLoadedType, node, assetPathUri, assembly); return FromType(context, type, sourceUriNode, expectedLoadedType, node, assetPathUri, assembly, additionalProperties);
} }
return context.ParseError( return context.ParseError(
@ -108,7 +124,8 @@ internal class AvaloniaXamlIncludeTransformer : IXamlAstGroupTransformer
} }
private static IXamlAstNode FromType(AstTransformationContext context, IXamlType type, IXamlAstNode li, private static IXamlAstNode FromType(AstTransformationContext context, IXamlType type, IXamlAstNode li,
IXamlType expectedLoadedType, IXamlAstNode fallbackNode, string assetPathUri, string assembly) IXamlType expectedLoadedType, IXamlAstNode fallbackNode, string assetPathUri, string assembly,
IEnumerable<IXamlAstManipulationNode> manipulationNodes)
{ {
if (!expectedLoadedType.IsAssignableFrom(type)) if (!expectedLoadedType.IsAssignableFrom(type))
{ {
@ -116,15 +133,17 @@ internal class AvaloniaXamlIncludeTransformer : IXamlAstGroupTransformer
$"Resource \"{assetPathUri}\" is defined as \"{type}\" type in the \"{assembly}\" assembly, but expected \"{expectedLoadedType}\".", $"Resource \"{assetPathUri}\" is defined as \"{type}\" type in the \"{assembly}\" assembly, but expected \"{expectedLoadedType}\".",
li, fallbackNode); li, fallbackNode);
} }
IXamlAstNode newObjNode = new XamlAstObjectNode(li, new XamlAstClrTypeReference(li, type, false)); IXamlAstNode newObjNode = new XamlAstObjectNode(li, new XamlAstClrTypeReference(li, type, false));
((XamlAstObjectNode)newObjNode).Children.AddRange(manipulationNodes);
newObjNode = new AvaloniaXamlIlConstructorServiceProviderTransformer().Transform(context, newObjNode); newObjNode = new AvaloniaXamlIlConstructorServiceProviderTransformer().Transform(context, newObjNode);
newObjNode = new ConstructableObjectTransformer().Transform(context, newObjNode); newObjNode = new ConstructableObjectTransformer().Transform(context, newObjNode);
return new NewObjectTransformer().Transform(context, newObjNode); return new NewObjectTransformer().Transform(context, newObjNode);
} }
private static IXamlAstNode FromMethod(AstTransformationContext context, IXamlMethod method, IXamlAstNode li, private static IXamlAstNode FromMethod(AstTransformationContext context, IXamlMethod method, IXamlAstNode li,
IXamlType expectedLoadedType, IXamlAstNode fallbackNode, string assetPathUri, string assembly) IXamlType expectedLoadedType, IXamlAstNode fallbackNode, string assetPathUri, string assembly,
IEnumerable<IXamlAstManipulationNode> manipulationNodes)
{ {
if (!expectedLoadedType.IsAssignableFrom(method.ReturnType)) if (!expectedLoadedType.IsAssignableFrom(method.ReturnType))
{ {
@ -134,8 +153,11 @@ internal class AvaloniaXamlIncludeTransformer : IXamlAstGroupTransformer
} }
var sp = context.Configuration.TypeMappings.ServiceProvider; var sp = context.Configuration.TypeMappings.ServiceProvider;
return new XamlStaticOrTargetedReturnMethodCallNode(li, method,
new[] { new NewServiceProviderNode(sp, li) }); return new XamlValueWithManipulationNode(li,
new XamlStaticOrTargetedReturnMethodCallNode(li, method,
new[] { new NewServiceProviderNode(sp, li) }),
new XamlManipulationGroupNode(li, manipulationNodes));
} }
internal static (string?, IXamlAstNode?) ResolveSourceFromXamlInclude( internal static (string?, IXamlAstNode?) ResolveSourceFromXamlInclude(

10
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlThemeVariantProviderTransformer.cs

@ -10,7 +10,8 @@ internal class AvaloniaXamlIlThemeVariantProviderTransformer : IXamlAstTransform
{ {
public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node)
{ {
var type = context.GetAvaloniaTypes().IThemeVariantProvider; var avTypes = context.GetAvaloniaTypes();
var type = avTypes.IThemeVariantProvider;
if (!(node is XamlAstObjectNode on if (!(node is XamlAstObjectNode on
&& type.IsAssignableFrom(on.Type.GetClrType()))) && type.IsAssignableFrom(on.Type.GetClrType())))
return node; return node;
@ -21,6 +22,13 @@ internal class AvaloniaXamlIlThemeVariantProviderTransformer : IXamlAstTransform
if (keyDirective is null) if (keyDirective is null)
return node; return node;
var themeDictionariesColl = avTypes.IDictionaryT.MakeGenericType(avTypes.ThemeVariant, avTypes.IThemeVariantProvider);
if (context.ParentNodes().FirstOrDefault() is not XamlAstXamlPropertyValueNode propertyValueNode
|| !themeDictionariesColl.IsAssignableFrom(propertyValueNode.Property.GetClrProperty().Getter.ReturnType))
{
return node;
}
var keyProp = type.Properties.First(p => p.Name == "Key"); var keyProp = type.Properties.First(p => p.Name == "Key");
on.Children.Add(new XamlAstXamlPropertyValueNode(keyDirective, on.Children.Add(new XamlAstXamlPropertyValueNode(keyDirective,
new XamlAstClrProperty(keyDirective, keyProp, context.Configuration), new XamlAstClrProperty(keyDirective, keyProp, context.Configuration),

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

@ -65,6 +65,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
public IXamlType Int { get; } public IXamlType Int { get; }
public IXamlType Long { get; } public IXamlType Long { get; }
public IXamlType Uri { get; } public IXamlType Uri { get; }
public IXamlType IDictionaryT { get; }
public IXamlType FontFamily { get; } public IXamlType FontFamily { get; }
public IXamlConstructor FontFamilyConstructorUriName { get; } public IXamlConstructor FontFamilyConstructorUriName { get; }
public IXamlType Thickness { get; } public IXamlType Thickness { get; }
@ -194,6 +195,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
Int = cfg.TypeSystem.GetType("System.Int32"); Int = cfg.TypeSystem.GetType("System.Int32");
Long = cfg.TypeSystem.GetType("System.Int64"); Long = cfg.TypeSystem.GetType("System.Int64");
Uri = cfg.TypeSystem.GetType("System.Uri"); Uri = cfg.TypeSystem.GetType("System.Uri");
IDictionaryT = cfg.TypeSystem.GetType("System.Collections.Generic.IDictionary`2");
FontFamily = cfg.TypeSystem.GetType("Avalonia.Media.FontFamily"); FontFamily = cfg.TypeSystem.GetType("Avalonia.Media.FontFamily");
FontFamilyConstructorUriName = FontFamily.GetConstructor(new List<IXamlType> { Uri, XamlIlTypes.String }); FontFamilyConstructorUriName = FontFamily.GetConstructor(new List<IXamlType> { Uri, XamlIlTypes.String });
ThemeVariant = cfg.TypeSystem.GetType("Avalonia.Styling.ThemeVariant"); ThemeVariant = cfg.TypeSystem.GetType("Avalonia.Styling.ThemeVariant");

179
tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/ResourceIncludeTests.cs

@ -2,6 +2,8 @@
using System.Collections.Generic; using System.Collections.Generic;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Metadata;
using Avalonia.Styling;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using Xunit; using Xunit;
@ -9,87 +11,146 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
{ {
public class ResourceIncludeTests : XamlTestBase public class ResourceIncludeTests : XamlTestBase
{ {
public class StaticResourceExtensionTests : XamlTestBase [Fact]
public void ResourceInclude_Loads_ResourceDictionary()
{ {
[Fact] var documents = new[]
public void ResourceInclude_Loads_ResourceDictionary()
{ {
var documents = new[] new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Resource.xaml"), @"
{
new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Resource.xaml"), @"
<ResourceDictionary xmlns='https://github.com/avaloniaui' <ResourceDictionary xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'> xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<SolidColorBrush x:Key='brush'>#ff506070</SolidColorBrush> <SolidColorBrush x:Key='brush'>#ff506070</SolidColorBrush>
</ResourceDictionary>"), </ResourceDictionary>"),
new RuntimeXamlLoaderDocument(@" new RuntimeXamlLoaderDocument(@"
<UserControl xmlns='https://github.com/avaloniaui' <UserControl xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'> xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<UserControl.Resources> <UserControl.Resources>
<ResourceDictionary> <ResourceDictionary>
<ResourceDictionary.MergedDictionaries> <ResourceDictionary.MergedDictionaries>
<ResourceInclude Source='avares://Tests/Resource.xaml'/> <ResourceInclude Source='avares://Tests/Resource.xaml'/>
</ResourceDictionary.MergedDictionaries> </ResourceDictionary.MergedDictionaries>
</ResourceDictionary> </ResourceDictionary>
</UserControl.Resources> </UserControl.Resources>
<Border Name='border' Background='{StaticResource brush}'/> <Border Name='border' Background='{StaticResource brush}'/>
</UserControl>") </UserControl>")
}; };
using (StartWithResources()) using (StartWithResources())
{ {
var compiled = AvaloniaRuntimeXamlLoader.LoadGroup(documents); var compiled = AvaloniaRuntimeXamlLoader.LoadGroup(documents);
var userControl = Assert.IsType<UserControl>(compiled[1]); var userControl = Assert.IsType<UserControl>(compiled[1]);
var border = userControl.FindControl<Border>("border"); var border = userControl.FindControl<Border>("border");
var brush = (ISolidColorBrush)border.Background; var brush = (ISolidColorBrush)border.Background;
Assert.Equal(0xff506070, brush.Color.ToUInt32()); Assert.Equal(0xff506070, brush.Color.ToUInt32());
}
} }
}
[Fact] [Fact]
public void Missing_ResourceKey_In_ResourceInclude_Does_Not_Cause_StackOverflow() public void Missing_ResourceKey_In_ResourceInclude_Does_Not_Cause_StackOverflow()
{
var app = Application.Current;
var documents = new[]
{ {
var app = Application.Current; new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Resource.xaml"), @"
var documents = new[]
{
new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Resource.xaml"), @"
<ResourceDictionary xmlns='https://github.com/avaloniaui' <ResourceDictionary xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'> xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<StaticResource x:Key='brush' ResourceKey='missing' /> <StaticResource x:Key='brush' ResourceKey='missing' />
</ResourceDictionary>"), </ResourceDictionary>"),
new RuntimeXamlLoaderDocument(app, @" new RuntimeXamlLoaderDocument(app, @"
<Application xmlns='https://github.com/avaloniaui' <Application xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'> xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Application.Resources> <Application.Resources>
<ResourceDictionary> <ResourceDictionary>
<ResourceDictionary.MergedDictionaries> <ResourceDictionary.MergedDictionaries>
<ResourceInclude Source='avares://Tests/Resource.xaml'/> <ResourceInclude Source='avares://Tests/Resource.xaml'/>
</ResourceDictionary.MergedDictionaries> </ResourceDictionary.MergedDictionaries>
</ResourceDictionary> </ResourceDictionary>
</Application.Resources> </Application.Resources>
</Application>") </Application>")
}; };
using (StartWithResources()) using (StartWithResources())
{
try
{ {
try AvaloniaRuntimeXamlLoader.LoadGroup(documents);
{
AvaloniaRuntimeXamlLoader.LoadGroup(documents);
}
catch (KeyNotFoundException)
{
}
} }
catch (KeyNotFoundException)
{
}
}
}
[Fact]
public void ResourceInclude_Should_Be_Allowed_To_Have_Key_In_Custom_Container()
{
var app = Application.Current;
var documents = new[]
{
new RuntimeXamlLoaderDocument(new Uri("avares://Demo/en-us.axaml"), @"
<ResourceDictionary xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<x:String x:Key='OkButton'>OK</x:String>
</ResourceDictionary>"),
new RuntimeXamlLoaderDocument(app, @"
<ResourceDictionary xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'>
<ResourceDictionary.MergedDictionaries>
<local:LocaleCollection>
<ResourceInclude Source='avares://Demo/en-us.axaml' x:Key='English' />
</local:LocaleCollection>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>")
};
using (StartWithResources())
{
var groups = AvaloniaRuntimeXamlLoader.LoadGroup(documents);
var res = Assert.IsType<ResourceDictionary>(groups[1]);
Assert.True(res.TryGetResource("OkButton", null, out var val));
Assert.Equal("OK", val);
} }
}
private IDisposable StartWithResources(params (string, string)[] assets) private IDisposable StartWithResources(params (string, string)[] assets)
{
var assetLoader = new MockAssetLoader(assets);
var services = new TestServices(assetLoader: assetLoader);
return UnitTestApplication.Start(services);
}
}
// See https://github.com/AvaloniaUI/Avalonia/issues/11172
public class LocaleCollection : IResourceProvider
{
private readonly Dictionary<object, IResourceProvider> _langs = new();
public IResourceHost Owner { get; private set; }
public bool HasResources => true;
public event EventHandler OwnerChanged;
public void AddOwner(IResourceHost owner) => Owner = owner;
public void RemoveOwner(IResourceHost owner) => Owner = null;
public bool TryGetResource(object key, ThemeVariant theme, out object? value)
{
if (_langs.TryGetValue("English", out var res))
{ {
var assetLoader = new MockAssetLoader(assets); return res.TryGetResource(key, theme, out value);
var services = new TestServices(assetLoader: assetLoader);
return UnitTestApplication.Start(services);
} }
value = null;
return false;
} }
// Allow Avalonia to use this class as a collection, requires x:Key on the IResourceProvider
public void Add(object k, IResourceProvider v) => _langs.Add(k, v);
} }
} }

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

@ -385,6 +385,23 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
Assert.Equal(Colors.Blue, ((ISolidColorBrush)userControl.FindResource("brush")!).Color); Assert.Equal(Colors.Blue, ((ISolidColorBrush)userControl.FindResource("brush")!).Color);
} }
[Fact]
public void ResourceDictionary_Can_Be_Put_Inside_Of_ResourceDictionary()
{
using (StyledWindow())
{
var xaml = @"
<ResourceDictionary xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<ResourceDictionary x:Key='NotAThemeVariantKey' />
</ResourceDictionary>";
var resources = (ResourceDictionary)AvaloniaRuntimeXamlLoader.Load(xaml);
var nested = (ResourceDictionary)resources["NotAThemeVariantKey"];
Assert.NotNull(nested);
}
}
private IDisposable StyledWindow(params (string, string)[] assets) private IDisposable StyledWindow(params (string, string)[] assets)
{ {
var services = TestServices.StyledWindow.With( var services = TestServices.StyledWindow.With(

Loading…
Cancel
Save