diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlIncludeGroupTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlIncludeGroupTransformer.cs index afc7b569a8..b6a4b599fa 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlIncludeGroupTransformer.cs +++ b/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(); + if (initializationNode.Manipulation is not XamlPropertyAssignmentNode { Property: { Name: "Source" } } sourceProperty) + { + if (initializationNode.Manipulation is XamlManipulationGroupNode manipulationGroup + && manipulationGroup.Children.OfType() + .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 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 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( diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlThemeVariantProviderTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlThemeVariantProviderTransformer.cs index 05df8be1b6..256a7472f3 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlThemeVariantProviderTransformer.cs +++ b/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), diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs index b5c0c7734d..c5c3cdd123 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs +++ b/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 { Uri, XamlIlTypes.String }); ThemeVariant = cfg.TypeSystem.GetType("Avalonia.Styling.ThemeVariant"); diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/ResourceIncludeTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/ResourceIncludeTests.cs index a6b930962e..9a008503f5 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/ResourceIncludeTests.cs +++ b/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"), @" - #ff506070 + xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'> +#ff506070 "), - new RuntimeXamlLoaderDocument(@" + new RuntimeXamlLoaderDocument(@" - - - - - - - - - + xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'> + + + + + + + + + ") - }; + }; - using (StartWithResources()) - { - var compiled = AvaloniaRuntimeXamlLoader.LoadGroup(documents); - var userControl = Assert.IsType(compiled[1]); - var border = userControl.FindControl("border"); + using (StartWithResources()) + { + var compiled = AvaloniaRuntimeXamlLoader.LoadGroup(documents); + var userControl = Assert.IsType(compiled[1]); + var border = userControl.FindControl("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"), @" - + xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'> + "), - new RuntimeXamlLoaderDocument(app, @" + new RuntimeXamlLoaderDocument(app, @" - - - - - - - + xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'> + + + + + + + ") - }; + }; - 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"), @" + + OK +"), + new RuntimeXamlLoaderDocument(app, @" + + + + + + +") + }; + + using (StartWithResources()) + { + var groups = AvaloniaRuntimeXamlLoader.LoadGroup(documents); + var res = Assert.IsType(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 _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); } } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs index e73ef456af..b1b7a74dd3 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs +++ b/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 = @" + + +"; + 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(