Browse Source

Merge pull request #9571 from AvaloniaUI/xamlinclude-fix

Xamlinclude fix
pull/9587/head
Max Katz 4 years ago
committed by GitHub
parent
commit
976172b137
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs
  2. 105
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlIncludeGroupTransformer.cs
  3. 2
      src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaUriTypeConverter.cs
  4. 3
      src/Markup/Avalonia.Markup.Xaml/Styling/ResourceInclude.cs
  5. 3
      src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs
  6. 88
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleIncludeTests.cs

2
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs

@ -274,7 +274,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
{
var uriText = text.Trim();
var kind = ((!uriText?.StartsWith("/") == true) ? UriKind.Absolute : UriKind.Relative);
var kind = ((!uriText?.StartsWith("/") == true) ? UriKind.RelativeOrAbsolute : UriKind.Relative);
if (string.IsNullOrWhiteSpace(uriText) || !Uri.TryCreate(uriText, kind, out var _))
{

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

@ -28,6 +28,13 @@ internal class AvaloniaXamlIncludeTransformer : IXamlAstGroupTransformer
}
var nodeTypeName = objectNode.Type.GetClrType().Name;
var expectedLoadedType = objectNode.Type.GetClrType().GetAllProperties()
.FirstOrDefault(p => p.Name == "Loaded")?.PropertyType;
if (expectedLoadedType is null)
{
throw new InvalidOperationException($"\"{nodeTypeName}\".Loaded property is expected to be defined");
}
if (valueNode.Manipulation is not XamlObjectInitializationNode
{
Manipulation: XamlPropertyAssignmentNode { Property: { Name: "Source" } } sourceProperty
@ -36,91 +43,109 @@ internal class AvaloniaXamlIncludeTransformer : IXamlAstGroupTransformer
return context.ParseError($"Source property must be set on the \"{nodeTypeName}\" node.", node);
}
// We expect that AvaloniaXamlIlLanguageParseIntrinsics has already parsed the Uri and created node like: `new Uri(assetPath, uriKind)`.
if (sourceProperty.Values.OfType<XamlAstNewClrObjectNode>().FirstOrDefault() is not { } sourceUriNode
|| sourceUriNode.Type.GetClrType() != context.GetAvaloniaTypes().Uri
|| sourceUriNode.Arguments.FirstOrDefault() is not XamlConstantNode { Constant: string originalAssetPath })
|| sourceUriNode.Arguments.FirstOrDefault() is not XamlConstantNode { Constant: string originalAssetPath }
|| sourceUriNode.Arguments.Skip(1).FirstOrDefault() is not XamlConstantNode { Constant: int uriKind })
{
// TODO: make it a compiler warning
// Source value can be set with markup extension instead of the Uri object node, we don't support it here yet.
return node;
}
if (originalAssetPath.StartsWith("/"))
var uriPath = new Uri(originalAssetPath, (UriKind)uriKind);
if (!uriPath.IsAbsoluteUri)
{
var baseUrl = context.CurrentDocument.Uri ?? throw new InvalidOperationException("CurrentDocument URI is null.");
originalAssetPath = baseUrl.Substring(0, baseUrl.LastIndexOf('/')) + originalAssetPath;
uriPath = new Uri(new Uri(baseUrl, UriKind.Absolute), uriPath);
}
else if (!originalAssetPath.StartsWith("avares://"))
else if (!uriPath.Scheme.Equals("avares", StringComparison.CurrentCultureIgnoreCase))
{
return context.ParseError(
$"Avalonia supports only \"avares://\" sources or relative sources starting with \"/\" on the \"{nodeTypeName}\" node.",
node);
$"\"{nodeTypeName}.Source\" supports only \"avares://\" absolute or relative uri.",
sourceUriNode, node);
}
originalAssetPath = Uri.UnescapeDataString(new Uri(originalAssetPath).AbsoluteUri);
var assetPath = originalAssetPath.Replace("avares://", "");
var assetPathUri = Uri.UnescapeDataString(uriPath.AbsoluteUri);
var assetPath = assetPathUri.Replace("avares://", "");
var assemblyNameSeparator = assetPath.IndexOf('/');
var assembly = assetPath.Substring(0, assemblyNameSeparator);
var fullTypeName = Path.GetFileNameWithoutExtension(assetPath.Replace('/', '.'));
if (context.Documents.FirstOrDefault(d => string.Equals(d.Uri, originalAssetPath, StringComparison.InvariantCultureIgnoreCase)) is {} targetDocument)
// Search file in the current assembly among other XAML resources.
if (context.Documents.FirstOrDefault(d => string.Equals(d.Uri, assetPathUri, StringComparison.InvariantCultureIgnoreCase)) is {} targetDocument)
{
if (targetDocument.ClassType is not null)
if (targetDocument.BuildMethod is not null)
{
return FromType(context, targetDocument.ClassType, node);
return FromMethod(context, targetDocument.BuildMethod, sourceUriNode, expectedLoadedType, node, assetPathUri, assembly);
}
if (targetDocument.BuildMethod is null)
if (targetDocument.ClassType is not null)
{
return context.ParseError($"\"{originalAssetPath}\" cannot be instantiated.", node);
return FromType(context, targetDocument.ClassType, sourceUriNode, expectedLoadedType, node, assetPathUri, assembly);
}
return FromMethod(context, targetDocument.BuildMethod, node);
return context.ParseError(
$"Unable to resolve XAML resource \"{assetPathUri}\" in the current assembly.",
sourceUriNode, node);
}
// If resource wasn't found in the current assembly, search in the others.
if (context.Configuration.TypeSystem.FindAssembly(assembly) is not { } assetAssembly)
{
return context.ParseError($"Assembly \"{assembly}\" was not found from the \"{originalAssetPath}\" source.", node);
return context.ParseError($"Assembly \"{assembly}\" was not found from the \"{assetPathUri}\" source.", sourceUriNode, node);
}
if (assetAssembly.FindType(fullTypeName) is { } type
&& type.FindMethod(m => m.Name == "!XamlIlPopulate") is not null)
var avaResType = assetAssembly.FindType("CompiledAvaloniaXaml.!AvaloniaResources");
if (avaResType is null)
{
return FromType(context, type, node);
return context.ParseError(
$"Unable to resolve \"!AvaloniaResources\" type on \"{assembly}\" assembly.", sourceUriNode, node);
}
else
{
var avaResType = assetAssembly.FindType("CompiledAvaloniaXaml.!AvaloniaResources");
if (avaResType is null)
{
return context.ParseError(
$"Unable to resolve \"!AvaloniaResources\" type on \"{assembly}\" assembly.", node);
}
var relativeName = "Build:" + assetPath.Substring(assemblyNameSeparator);
var buildMethod = avaResType.FindMethod(m => m.Name == relativeName);
if (buildMethod is null)
{
return context.ParseError(
$"Unable to resolve build method \"{relativeName}\" resource on the \"{assembly}\" assembly.",
node);
}
return FromMethod(context, buildMethod, node);
var relativeName = "Build:" + assetPath.Substring(assemblyNameSeparator);
var buildMethod = avaResType.FindMethod(m => m.Name == relativeName);
if (buildMethod is not null)
{
return FromMethod(context, buildMethod, sourceUriNode, expectedLoadedType, node, assetPathUri, assembly);
}
else if (assetAssembly.FindType(fullTypeName) is { } type)
{
return FromType(context, type, sourceUriNode, expectedLoadedType, node, assetPathUri, assembly);
}
}
private static IXamlAstNode FromType(AstTransformationContext context, IXamlType type, IXamlLineInfo li)
return context.ParseError(
$"Unable to resolve XAML resource \"{assetPathUri}\" in the \"{assembly}\" assembly.",
sourceUriNode, node);
}
private static IXamlAstNode FromType(AstTransformationContext context, IXamlType type, IXamlAstNode li,
IXamlType expectedLoadedType, IXamlAstNode fallbackNode, string assetPathUri, string assembly)
{
if (!expectedLoadedType.IsAssignableFrom(type))
{
return context.ParseError(
$"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));
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, IXamlLineInfo li)
private static IXamlAstNode FromMethod(AstTransformationContext context, IXamlMethod method, IXamlAstNode li,
IXamlType expectedLoadedType, IXamlAstNode fallbackNode, string assetPathUri, string assembly)
{
if (!expectedLoadedType.IsAssignableFrom(method.ReturnType))
{
return context.ParseError(
$"Resource \"{assetPathUri}\" is defined as \"{method.ReturnType}\" type in the \"{assembly}\" assembly, but expected \"{expectedLoadedType}\".",
li, fallbackNode);
}
var sp = context.Configuration.TypeMappings.ServiceProvider;
return new XamlStaticOrTargetedReturnMethodCallNode(li, method,
new[] { new NewServiceProviderNode(sp, li) });

2
src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaUriTypeConverter.cs

@ -17,7 +17,7 @@ namespace Avalonia.Markup.Xaml.Converters
if (s == null)
return null;
//On Unix Uri tries to interpret paths starting with "/" as file Uris
var kind = s.StartsWith("/") ? UriKind.Relative : UriKind.Absolute;
var kind = s.StartsWith("/") ? UriKind.Relative : UriKind.RelativeOrAbsolute;
if (!Uri.TryCreate(s, kind, out var res))
throw new ArgumentException("Unable to parse URI: " + s);
return res;

3
src/Markup/Avalonia.Markup.Xaml/Styling/ResourceInclude.cs

@ -42,7 +42,8 @@ namespace Avalonia.Markup.Xaml.Styling
if (_loaded == null)
{
_isLoading = true;
_loaded = (IResourceDictionary)AvaloniaXamlLoader.Load(Source, _baseUri);
var source = Source ?? throw new InvalidOperationException("ResourceInclude.Source must be set.");
_loaded = (IResourceDictionary)AvaloniaXamlLoader.Load(source, _baseUri);
_isLoading = false;
}

3
src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs

@ -51,7 +51,8 @@ namespace Avalonia.Markup.Xaml.Styling
if (_loaded == null)
{
_isLoading = true;
var loaded = (IStyle)AvaloniaXamlLoader.Load(Source, _baseUri);
var source = Source ?? throw new InvalidOperationException("StyleInclude.Source must be set.");
var loaded = (IStyle)AvaloniaXamlLoader.Load(source, _baseUri);
_loaded = new[] { loaded };
_isLoading = false;
}

88
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleIncludeTests.cs

@ -79,7 +79,35 @@ public class StyleIncludeTests
}
[Fact]
public void Relative_StyleInclude_Is_Resolved_With_Two_Files()
public void Relative_Back_StyleInclude_Is_Resolved_With_Two_Files()
{
var documents = new[]
{
new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Subfolder/Style.xaml"), @"
<Style xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Style.Resources>
<Color x:Key='Red'>Red</Color>
</Style.Resources>
</Style>"),
new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Subfolder/Folder/Root.xaml"), @"
<ContentControl xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<ContentControl.Resources>
<StyleInclude x:Key='Include' Source='../Style.xaml'/>
</ContentControl.Resources>
</ContentControl>")
};
var objects = AvaloniaRuntimeXamlLoader.LoadGroup(documents);
var style = Assert.IsType<Style>(objects[0]);
var contentControl = Assert.IsType<ContentControl>(objects[1]);
Assert.IsType<Style>(contentControl.Resources["Include"]);
}
[Fact]
public void Relative_Root_StyleInclude_Is_Resolved_With_Two_Files()
{
var documents = new[]
{
@ -94,7 +122,63 @@ public class StyleIncludeTests
<ContentControl xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<ContentControl.Resources>
<StyleInclude x:Key='Include' Source='/../Style.xaml'/>
<StyleInclude x:Key='Include' Source='/Style.xaml'/>
</ContentControl.Resources>
</ContentControl>")
};
var objects = AvaloniaRuntimeXamlLoader.LoadGroup(documents);
var style = Assert.IsType<Style>(objects[0]);
var contentControl = Assert.IsType<ContentControl>(objects[1]);
Assert.IsType<Style>(contentControl.Resources["Include"]);
}
[Fact]
public void Relative_StyleInclude_Is_Resolved_With_Two_Files()
{
var documents = new[]
{
new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Folder/Style.xaml"), @"
<Style xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Style.Resources>
<Color x:Key='Red'>Red</Color>
</Style.Resources>
</Style>"),
new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Folder/Root.xaml"), @"
<ContentControl xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<ContentControl.Resources>
<StyleInclude x:Key='Include' Source='Style.xaml'/>
</ContentControl.Resources>
</ContentControl>")
};
var objects = AvaloniaRuntimeXamlLoader.LoadGroup(documents);
var style = Assert.IsType<Style>(objects[0]);
var contentControl = Assert.IsType<ContentControl>(objects[1]);
Assert.IsType<Style>(contentControl.Resources["Include"]);
}
[Fact]
public void Relative_Dot_Syntax__StyleInclude_Is_Resolved_With_Two_Files()
{
var documents = new[]
{
new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Folder/Style.xaml"), @"
<Style xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Style.Resources>
<Color x:Key='Red'>Red</Color>
</Style.Resources>
</Style>"),
new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Folder/Root.xaml"), @"
<ContentControl xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<ContentControl.Resources>
<StyleInclude x:Key='Include' Source='./Style.xaml'/>
</ContentControl.Resources>
</ContentControl>")
};

Loading…
Cancel
Save