committed by
GitHub
27 changed files with 506 additions and 189 deletions
@ -0,0 +1,113 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers; |
||||
|
using XamlX.Ast; |
||||
|
using XamlX.IL.Emitters; |
||||
|
|
||||
|
namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.GroupTransformers; |
||||
|
#nullable enable |
||||
|
|
||||
|
internal class XamlMergeResourceGroupTransformer : IXamlAstGroupTransformer |
||||
|
{ |
||||
|
public IXamlAstNode Transform(AstGroupTransformationContext context, IXamlAstNode node) |
||||
|
{ |
||||
|
var resourceDictionaryType = context.GetAvaloniaTypes().ResourceDictionary; |
||||
|
if (node is not XamlObjectInitializationNode resourceDictionaryNode |
||||
|
|| resourceDictionaryNode.Type != resourceDictionaryType |
||||
|
|| resourceDictionaryNode.Manipulation is not XamlManipulationGroupNode resourceDictionaryManipulation) |
||||
|
{ |
||||
|
return node; |
||||
|
} |
||||
|
|
||||
|
var mergeResourceIncludeType = context.GetAvaloniaTypes().MergeResourceInclude; |
||||
|
var mergeSourceNodes = new List<XamlPropertyAssignmentNode>(); |
||||
|
var hasAnyNonMergedResource = false; |
||||
|
foreach (var manipulationNode in resourceDictionaryManipulation.Children.ToArray()) |
||||
|
{ |
||||
|
void ProcessXamlPropertyAssignmentNode(XamlManipulationGroupNode parent, XamlPropertyAssignmentNode assignmentNode) |
||||
|
{ |
||||
|
if (assignmentNode.Property.Name == "MergedDictionaries" |
||||
|
&& assignmentNode.Values.FirstOrDefault() is XamlValueWithManipulationNode valueNode) |
||||
|
{ |
||||
|
if (valueNode.Type.GetClrType() == mergeResourceIncludeType) |
||||
|
{ |
||||
|
if (valueNode.Manipulation is XamlObjectInitializationNode objectInitialization |
||||
|
&& objectInitialization.Manipulation is XamlPropertyAssignmentNode sourceAssignmentNode) |
||||
|
{ |
||||
|
parent.Children.Remove(assignmentNode); |
||||
|
mergeSourceNodes.Add(sourceAssignmentNode); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
throw new XamlDocumentParseException(context.CurrentDocument, |
||||
|
"Invalid MergeResourceInclude node found. Make sure that Source property is set.", |
||||
|
valueNode); |
||||
|
} |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
hasAnyNonMergedResource = true; |
||||
|
} |
||||
|
|
||||
|
if (hasAnyNonMergedResource && mergeSourceNodes.Any()) |
||||
|
{ |
||||
|
throw new XamlDocumentParseException(context.CurrentDocument, |
||||
|
"Mix of MergeResourceInclude and other dictionaries inside of the ResourceDictionary.MergedDictionaries is not allowed", |
||||
|
valueNode); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (manipulationNode is XamlPropertyAssignmentNode singleValueAssignment) |
||||
|
{ |
||||
|
ProcessXamlPropertyAssignmentNode(resourceDictionaryManipulation, singleValueAssignment); |
||||
|
} |
||||
|
else if (manipulationNode is XamlManipulationGroupNode groupNodeValues) |
||||
|
{ |
||||
|
foreach (var groupNodeValue in groupNodeValues.Children.OfType<XamlPropertyAssignmentNode>().ToArray()) |
||||
|
{ |
||||
|
ProcessXamlPropertyAssignmentNode(groupNodeValues, groupNodeValue); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
var manipulationGroup = new XamlManipulationGroupNode(node, new List<IXamlAstManipulationNode>()); |
||||
|
foreach (var sourceNode in mergeSourceNodes) |
||||
|
{ |
||||
|
var (originalAssetPath, propertyNode) = |
||||
|
AvaloniaXamlIncludeTransformer.ResolveSourceFromXamlInclude(context, "MergeResourceInclude", sourceNode, true); |
||||
|
if (originalAssetPath is null) |
||||
|
{ |
||||
|
return node; |
||||
|
} |
||||
|
|
||||
|
var targetDocument = context.Documents.FirstOrDefault(d => |
||||
|
string.Equals(d.Uri, originalAssetPath, StringComparison.InvariantCultureIgnoreCase)) |
||||
|
?.XamlDocument.Root as XamlValueWithManipulationNode; |
||||
|
if (targetDocument is null) |
||||
|
{ |
||||
|
return context.ParseError( |
||||
|
$"Node MergeResourceInclude is unable to resolve \"{originalAssetPath}\" path.", propertyNode, node); |
||||
|
} |
||||
|
|
||||
|
var singleRootObject = ((XamlManipulationGroupNode)targetDocument.Manipulation) |
||||
|
.Children.OfType<XamlObjectInitializationNode>().Single(); |
||||
|
if (singleRootObject.Type != resourceDictionaryType) |
||||
|
{ |
||||
|
return context.ParseError( |
||||
|
$"MergeResourceInclude can only include another ResourceDictionary", propertyNode, node); |
||||
|
} |
||||
|
|
||||
|
manipulationGroup.Children.Add(singleRootObject.Manipulation); |
||||
|
} |
||||
|
|
||||
|
if (manipulationGroup.Children.Any()) |
||||
|
{ |
||||
|
// MergedDictionaries are read first, so we need ot inject our merged values in the beginning.
|
||||
|
resourceDictionaryManipulation.Children.Insert(0, manipulationGroup); |
||||
|
} |
||||
|
|
||||
|
return node; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,26 @@ |
|||||
|
using System; |
||||
|
using System.Diagnostics.CodeAnalysis; |
||||
|
using Avalonia.Controls; |
||||
|
|
||||
|
#nullable enable |
||||
|
|
||||
|
namespace Avalonia.Markup.Xaml.Styling; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Loads a resource dictionary from a specified URL.
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// If used from the XAML code, it is merged into the parent dictionary in the compile time.
|
||||
|
/// When used in runtime, this type behaves like <see cref="ResourceInclude"/>.
|
||||
|
/// </remarks>
|
||||
|
[RequiresUnreferencedCode(TrimmingMessages.StyleResourceIncludeRequiresUnreferenceCodeMessage)] |
||||
|
public class MergeResourceInclude : ResourceInclude |
||||
|
{ |
||||
|
public MergeResourceInclude(Uri? baseUri) : base(baseUri) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
public MergeResourceInclude(IServiceProvider serviceProvider) : base(serviceProvider) |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,124 @@ |
|||||
|
using System; |
||||
|
using System.Xml; |
||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Media; |
||||
|
using Avalonia.Styling; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Avalonia.Markup.Xaml.UnitTests.Xaml; |
||||
|
|
||||
|
public class MergeResourceIncludeTests |
||||
|
{ |
||||
|
[Fact] |
||||
|
public void MergeResourceInclude_Works_With_Single_Resource() |
||||
|
{ |
||||
|
var documents = new[] |
||||
|
{ |
||||
|
new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Resources.xaml"), @"
|
||||
|
<ResourceDictionary xmlns='https://github.com/avaloniaui'
|
||||
|
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
|
||||
|
<SolidColorBrush x:Key='brush2'>Red</SolidColorBrush> |
||||
|
</ResourceDictionary>"),
|
||||
|
new RuntimeXamlLoaderDocument(@"
|
||||
|
<UserControl xmlns='https://github.com/avaloniaui'
|
||||
|
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
|
||||
|
<UserControl.Resources> |
||||
|
<ResourceDictionary> |
||||
|
<SolidColorBrush x:Key='brush1'>Blue</SolidColorBrush> |
||||
|
<ResourceDictionary.MergedDictionaries> |
||||
|
<MergeResourceInclude Source='avares://Tests/Resources.xaml'/>
|
||||
|
</ResourceDictionary.MergedDictionaries> |
||||
|
</ResourceDictionary> |
||||
|
</UserControl.Resources> |
||||
|
</UserControl>")
|
||||
|
}; |
||||
|
|
||||
|
var objects = AvaloniaRuntimeXamlLoader.LoadGroup(documents); |
||||
|
var contentControl = Assert.IsType<UserControl>(objects[1]); |
||||
|
|
||||
|
var resources = Assert.IsType<ResourceDictionary>(contentControl.Resources); |
||||
|
Assert.Empty(resources.MergedDictionaries); |
||||
|
|
||||
|
var initialResource = (ISolidColorBrush)resources["brush1"]!; |
||||
|
Assert.Equal(Colors.Blue, initialResource.Color); |
||||
|
|
||||
|
var mergedResource = (ISolidColorBrush)resources["brush2"]!; |
||||
|
Assert.Equal(Colors.Red, mergedResource.Color); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Mixing_MergeResourceInclude_And_ResourceInclude_Is_Not_Allowed() |
||||
|
{ |
||||
|
var documents = new[] |
||||
|
{ |
||||
|
new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Resources1.xaml"), @"
|
||||
|
<ResourceDictionary xmlns='https://github.com/avaloniaui'
|
||||
|
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
|
||||
|
<SolidColorBrush x:Key='brush1'>Red</SolidColorBrush> |
||||
|
</ResourceDictionary>"),
|
||||
|
new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Resources2.xaml"), @"
|
||||
|
<ResourceDictionary xmlns='https://github.com/avaloniaui'
|
||||
|
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
|
||||
|
<SolidColorBrush x:Key='brush2'>Blue</SolidColorBrush> |
||||
|
</ResourceDictionary>"),
|
||||
|
new RuntimeXamlLoaderDocument(@"
|
||||
|
<ResourceDictionary xmlns='https://github.com/avaloniaui'
|
||||
|
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
|
||||
|
<ResourceDictionary.MergedDictionaries> |
||||
|
<MergeResourceInclude Source='avares://Tests/Resources1.xaml'/>
|
||||
|
<ResourceInclude Source='avares://Tests/Resources2.xaml'/>
|
||||
|
</ResourceDictionary.MergedDictionaries> |
||||
|
</ResourceDictionary>")
|
||||
|
}; |
||||
|
|
||||
|
Assert.ThrowsAny<XmlException>(() => AvaloniaRuntimeXamlLoader.LoadGroup(documents)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void MergeResourceInclude_Works_With_Multiple_Resources() |
||||
|
{ |
||||
|
var documents = new[] |
||||
|
{ |
||||
|
new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Resources1.xaml"), @"
|
||||
|
<ResourceDictionary xmlns='https://github.com/avaloniaui'
|
||||
|
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
|
||||
|
<SolidColorBrush x:Key='brush1'>Red</SolidColorBrush> |
||||
|
<SolidColorBrush x:Key='brush2'>Blue</SolidColorBrush> |
||||
|
</ResourceDictionary>"),
|
||||
|
new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Resources2.xaml"), @"
|
||||
|
<ResourceDictionary xmlns='https://github.com/avaloniaui'
|
||||
|
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
|
||||
|
<SolidColorBrush x:Key='brush4'>Yellow</SolidColorBrush> |
||||
|
<ResourceDictionary.MergedDictionaries> |
||||
|
<MergeResourceInclude Source='avares://Tests/Resources1_2.xaml'/>
|
||||
|
</ResourceDictionary.MergedDictionaries> |
||||
|
</ResourceDictionary>"),
|
||||
|
new RuntimeXamlLoaderDocument(@"
|
||||
|
<ResourceDictionary xmlns='https://github.com/avaloniaui'
|
||||
|
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
|
||||
|
<ResourceDictionary.MergedDictionaries> |
||||
|
<MergeResourceInclude Source='avares://Tests/Resources1.xaml'/>
|
||||
|
<MergeResourceInclude Source='avares://Tests/Resources2.xaml'/>
|
||||
|
</ResourceDictionary.MergedDictionaries> |
||||
|
<SolidColorBrush x:Key='brush5'>Black</SolidColorBrush> |
||||
|
<SolidColorBrush x:Key='brush6'>White</SolidColorBrush> |
||||
|
</ResourceDictionary>"),
|
||||
|
new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Resources1_2.xaml"), @"
|
||||
|
<ResourceDictionary xmlns='https://github.com/avaloniaui'
|
||||
|
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
|
||||
|
<SolidColorBrush x:Key='brush3'>Green</SolidColorBrush> |
||||
|
</ResourceDictionary>"),
|
||||
|
}; |
||||
|
|
||||
|
var objects = AvaloniaRuntimeXamlLoader.LoadGroup(documents); |
||||
|
var resources = Assert.IsType<ResourceDictionary>(objects[2]); |
||||
|
Assert.Empty(resources.MergedDictionaries); |
||||
|
|
||||
|
Assert.Equal(Colors.Red, ((ISolidColorBrush)resources["brush1"]!).Color); |
||||
|
Assert.Equal(Colors.Blue, ((ISolidColorBrush)resources["brush2"]!).Color); |
||||
|
Assert.Equal(Colors.Green, ((ISolidColorBrush)resources["brush3"]!).Color); |
||||
|
Assert.Equal(Colors.Yellow, ((ISolidColorBrush)resources["brush4"]!).Color); |
||||
|
Assert.Equal(Colors.Black, ((ISolidColorBrush)resources["brush5"]!).Color); |
||||
|
Assert.Equal(Colors.White, ((ISolidColorBrush)resources["brush6"]!).Color); |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue