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");
}
if (valueNode.Manipulation is not XamlObjectInitializationNode
{
Manipulation: XamlPropertyAssignmentNode { Property: { Name: "Source" } } sourceProperty
})
if (valueNode.Manipulation is not XamlObjectInitializationNode initializationNode)
{
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);
@ -65,12 +81,12 @@ internal class AvaloniaXamlIncludeTransformer : IXamlAstGroupTransformer
{
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)
{
return FromType(context, targetDocument.ClassType, sourceUriNode, expectedLoadedType, node, assetPathUri, assembly);
return FromType(context, targetDocument.ClassType, sourceUriNode, expectedLoadedType, node, assetPathUri, assembly, additionalProperties);
}
return context.ParseError(
@ -95,11 +111,11 @@ internal class AvaloniaXamlIncludeTransformer : IXamlAstGroupTransformer
var buildMethod = avaResType.FindMethod(m => m.Name == relativeName);
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)
{
return FromType(context, type, sourceUriNode, expectedLoadedType, node, assetPathUri, assembly);
return FromType(context, type, sourceUriNode, expectedLoadedType, node, assetPathUri, assembly, additionalProperties);
}
return context.ParseError(
@ -108,7 +124,8 @@ internal class AvaloniaXamlIncludeTransformer : IXamlAstGroupTransformer
}
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))
{
@ -116,15 +133,17 @@ internal class AvaloniaXamlIncludeTransformer : IXamlAstGroupTransformer
$"Resource \"{assetPathUri}\" is defined as \"{type}\" type in the \"{assembly}\" assembly, but expected \"{expectedLoadedType}\".",
li, fallbackNode);
}
IXamlAstNode newObjNode = new XamlAstObjectNode(li, new XamlAstClrTypeReference(li, type, false));
((XamlAstObjectNode)newObjNode).Children.AddRange(manipulationNodes);
newObjNode = new AvaloniaXamlIlConstructorServiceProviderTransformer().Transform(context, newObjNode);
newObjNode = new ConstructableObjectTransformer().Transform(context, newObjNode);
return new NewObjectTransformer().Transform(context, newObjNode);
}
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))
{
@ -134,8 +153,11 @@ internal class AvaloniaXamlIncludeTransformer : IXamlAstGroupTransformer
}
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(

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)
{
var type = context.GetAvaloniaTypes().IThemeVariantProvider;
var avTypes = context.GetAvaloniaTypes();
var type = avTypes.IThemeVariantProvider;
if (!(node is XamlAstObjectNode on
&& type.IsAssignableFrom(on.Type.GetClrType())))
return node;
@ -21,6 +22,13 @@ internal class AvaloniaXamlIlThemeVariantProviderTransformer : IXamlAstTransform
if (keyDirective is null)
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");
on.Children.Add(new XamlAstXamlPropertyValueNode(keyDirective,
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 Long { get; }
public IXamlType Uri { get; }
public IXamlType IDictionaryT { get; }
public IXamlType FontFamily { get; }
public IXamlConstructor FontFamilyConstructorUriName { get; }
public IXamlType Thickness { get; }
@ -194,6 +195,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
Int = cfg.TypeSystem.GetType("System.Int32");
Long = cfg.TypeSystem.GetType("System.Int64");
Uri = cfg.TypeSystem.GetType("System.Uri");
IDictionaryT = cfg.TypeSystem.GetType("System.Collections.Generic.IDictionary`2");
FontFamily = cfg.TypeSystem.GetType("Avalonia.Media.FontFamily");
FontFamilyConstructorUriName = FontFamily.GetConstructor(new List<IXamlType> { Uri, XamlIlTypes.String });
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 Avalonia.Controls;
using Avalonia.Media;
using Avalonia.Metadata;
using Avalonia.Styling;
using Avalonia.UnitTests;
using Xunit;
@ -9,87 +11,146 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
{
public class ResourceIncludeTests : XamlTestBase
{
public class StaticResourceExtensionTests : XamlTestBase
[Fact]
public void ResourceInclude_Loads_ResourceDictionary()
{
[Fact]
public void ResourceInclude_Loads_ResourceDictionary()
var documents = new[]
{
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'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<SolidColorBrush x:Key='brush'>#ff506070</SolidColorBrush>
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<SolidColorBrush x:Key='brush'>#ff506070</SolidColorBrush>
</ResourceDictionary>"),
new RuntimeXamlLoaderDocument(@"
new RuntimeXamlLoaderDocument(@"
<UserControl xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceInclude Source='avares://Tests/Resource.xaml'/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<Border Name='border' Background='{StaticResource brush}'/>
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceInclude Source='avares://Tests/Resource.xaml'/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<Border Name='border' Background='{StaticResource brush}'/>
</UserControl>")
};
};
using (StartWithResources())
{
var compiled = AvaloniaRuntimeXamlLoader.LoadGroup(documents);
var userControl = Assert.IsType<UserControl>(compiled[1]);
var border = userControl.FindControl<Border>("border");
using (StartWithResources())
{
var compiled = AvaloniaRuntimeXamlLoader.LoadGroup(documents);
var userControl = Assert.IsType<UserControl>(compiled[1]);
var border = userControl.FindControl<Border>("border");
var brush = (ISolidColorBrush)border.Background;
Assert.Equal(0xff506070, brush.Color.ToUInt32());
}
var brush = (ISolidColorBrush)border.Background;
Assert.Equal(0xff506070, brush.Color.ToUInt32());
}
}
[Fact]
public void Missing_ResourceKey_In_ResourceInclude_Does_Not_Cause_StackOverflow()
[Fact]
public void Missing_ResourceKey_In_ResourceInclude_Does_Not_Cause_StackOverflow()
{
var app = Application.Current;
var documents = new[]
{
var app = Application.Current;
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'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<StaticResource x:Key='brush' ResourceKey='missing' />
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<StaticResource x:Key='brush' ResourceKey='missing' />
</ResourceDictionary>"),
new RuntimeXamlLoaderDocument(app, @"
new RuntimeXamlLoaderDocument(app, @"
<Application xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceInclude Source='avares://Tests/Resource.xaml'/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceInclude Source='avares://Tests/Resource.xaml'/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>")
};
};
using (StartWithResources())
using (StartWithResources())
{
try
{
try
{
AvaloniaRuntimeXamlLoader.LoadGroup(documents);
}
catch (KeyNotFoundException)
{
}
AvaloniaRuntimeXamlLoader.LoadGroup(documents);
}
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);
var services = new TestServices(assetLoader: assetLoader);
return UnitTestApplication.Start(services);
return res.TryGetResource(key, theme, out value);
}
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);
}
[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)
{
var services = TestServices.StyledWindow.With(

Loading…
Cancel
Save