Skip to content

Commit 8b4c228

Browse files
committed
feat(xamlparser): Adjust support for relative Uri parsing to enable contextual image loading
The change enables the support for XAML referencing library local images using a relative path
1 parent fb3a55e commit 8b4c228

File tree

9 files changed

+285
-26
lines changed

9 files changed

+285
-26
lines changed

src/SourceGenerators/Uno.UI.SourceGenerators/Content/Uno.UI.SourceGenerators.props

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@
8787
<CompilerVisibleProperty Include="UnoUISourceGeneratorDebuggerBreak" />
8888
<CompilerVisibleProperty Include="BuildingInsideVisualStudio" />
8989
<CompilerVisibleProperty Include="IsHotReloadHost" />
90+
<CompilerVisibleProperty Include="IsUnoHead" />
9091

9192
<CompilerVisibleProperty Include="UseWPF" />
9293
<CompilerVisibleProperty Include="IsUnoHead" />
@@ -111,6 +112,7 @@
111112

112113
<CompilerVisibleItemMetadata Include="AdditionalFiles" MetadataName="SourceItemGroup" />
113114
<CompilerVisibleItemMetadata Include="AdditionalFiles" MetadataName="Link" />
115+
<CompilerVisibleItemMetadata Include="AdditionalFiles" MetadataName="TargetPath" />
114116
<CompilerVisibleItemMetadata Include="AdditionalFiles" MetadataName="FullPath" />
115117
<CompilerVisibleItemMetadata Include="AdditionalFiles" MetadataName="Identity" />
116118
<CompilerVisibleItemMetadata Include="AdditionalFiles" MetadataName="XamlRuntime" />

src/SourceGenerators/Uno.UI.SourceGenerators/XamlGenerator/XamlCodeGeneration.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ internal partial class XamlCodeGeneration
5757
private readonly string _projectDirectory;
5858
private readonly string _projectFullPath;
5959
private readonly bool _xamlResourcesTrimming;
60+
private readonly bool _isUnoHead;
6061
private bool _shouldWriteErrorOnInvalidXaml;
6162
private readonly RoslynMetadataHelper _metadataHelper;
6263

@@ -176,6 +177,11 @@ public XamlCodeGeneration(GeneratorExecutionContext context)
176177
_xamlResourcesTrimming = xamlResourcesTrimming;
177178
}
178179

180+
if (bool.TryParse(context.GetMSBuildPropertyValue("IsUnoHead"), out var isUnoHead))
181+
{
182+
_isUnoHead = isUnoHead;
183+
}
184+
179185
if (bool.TryParse(context.GetMSBuildPropertyValue("UnoForceHotReloadCodeGen"), out var isHotReloadEnabled))
180186
{
181187
_isHotReloadEnabled = isHotReloadEnabled;
@@ -277,7 +283,7 @@ public List<KeyValuePair<string, string>> Generate(GenerationRunInfo generationR
277283
TryGenerateUnoResourcesKeyAttribute(resourceKeys);
278284

279285
var filesFull = new XamlFileParser(_excludeXamlNamespaces, _includeXamlNamespaces, _metadataHelper)
280-
.ParseFiles(_xamlSourceFiles, _generatorContext.CancellationToken);
286+
.ParseFiles(_xamlSourceFiles, _projectDirectory, _generatorContext.CancellationToken);
281287

282288
var xamlTypeToXamlTypeBaseMap = new ConcurrentDictionary<INamedTypeSymbol, XamlRedirection.XamlType>();
283289
Parallel.ForEach(filesFull, file =>
@@ -336,6 +342,7 @@ public List<KeyValuePair<string, string>> Generate(GenerationRunInfo generationR
336342
isWasm: _isWasm,
337343
isDebug: _isDebug,
338344
isHotReloadEnabled: _isHotReloadEnabled,
345+
isUnoHead: _isUnoHead,
339346
isDesignTimeBuild: _isDesignTimeBuild,
340347
skipUserControlsInVisualTree: _skipUserControlsInVisualTree,
341348
shouldAnnotateGeneratedXaml: _shouldAnnotateGeneratedXaml,

src/SourceGenerators/Uno.UI.SourceGenerators/XamlGenerator/XamlConstants.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ public static class Types
113113
public const string KeyTime = Namespaces.MediaAnimation + ".KeyTime";
114114
public const string Duration = BaseXamlNamespace + ".Duration";
115115
public const string FontFamily = Namespaces.Media + ".FontFamily";
116+
public const string ImageSource = Namespaces.Media + ".ImageSource";
116117

117118
// Controls
118119
public const string NativePage = Namespaces.Controls + ".NativePage";
@@ -123,6 +124,7 @@ public static class Types
123124
public const string Control = Namespaces.Controls + ".Control";
124125
public const string Panel = Namespaces.Controls + ".Panel";
125126
public const string Button = Namespaces.Controls + ".Button";
127+
public const string Image = Namespaces.Controls + ".Image";
126128
public const string TextBox = Namespaces.Controls + ".TextBox";
127129
public const string ColumnDefinition = Namespaces.Controls + ".ColumnDefinition";
128130
public const string RowDefinition = Namespaces.Controls + ".RowDefinition";

src/SourceGenerators/Uno.UI.SourceGenerators/XamlGenerator/XamlFileDefinition.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,12 @@ namespace Uno.UI.SourceGenerators.XamlGenerator
1313
{
1414
internal class XamlFileDefinition : IEquatable<XamlFileDefinition>
1515
{
16-
public XamlFileDefinition(string file)
16+
public XamlFileDefinition(string file, string targetFilePath)
1717
{
1818
Namespaces = new List<NamespaceDeclaration>();
1919
Objects = new List<XamlObjectDefinition>();
2020
FilePath = file;
21+
TargetFilePath = targetFilePath;
2122

2223
UniqueID = SanitizedFileName + "_" + HashBuilder.Build(FilePath);
2324
}
@@ -32,6 +33,11 @@ public XamlFileDefinition(string file)
3233

3334
public string FilePath { get; private set; }
3435

36+
/// <summary>
37+
/// Provides the path to the file using an actual target path in the project
38+
/// </summary>
39+
public string TargetFilePath { get; }
40+
3541
/// <summary>
3642
/// Unique and human-readable file ID, used to name generated file.
3743
/// </summary>

src/SourceGenerators/Uno.UI.SourceGenerators/XamlGenerator/XamlFileGenerator.cs

Lines changed: 93 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ internal partial class XamlFileGenerator
7979
private readonly Dictionary<string, string[]> _uiAutomationMappings;
8080
private readonly string _defaultLanguage;
8181
private readonly bool _isDebug;
82+
private readonly bool _isHotReloadEnabled;
83+
private readonly bool _isUnoHead;
8284
private readonly bool _isDesignTimeBuild;
8385
private readonly string _relativePath;
8486

@@ -150,6 +152,8 @@ internal partial class XamlFileGenerator
150152
private readonly INamedTypeSymbol _dependencyObjectSymbol;
151153
private readonly INamedTypeSymbol _markupExtensionSymbol;
152154
private readonly INamedTypeSymbol _brushSymbol;
155+
private readonly INamedTypeSymbol _imageSourceSymbol;
156+
private readonly INamedTypeSymbol _imageSymbol;
153157
private readonly INamedTypeSymbol _dependencyObjectParseSymbol;
154158
private readonly INamedTypeSymbol? _androidContentContextSymbol; // Android.Content.Context
155159
private readonly INamedTypeSymbol? _androidViewSymbol; // Android.Views.View
@@ -207,8 +211,6 @@ internal partial class XamlFileGenerator
207211
/// </summary>
208212
private string SingletonClassName => $"ResourceDictionarySingleton__{_fileDefinition.UniqueID}";
209213

210-
private readonly bool _isHotReloadEnabled;
211-
212214
private const string DictionaryProviderInterfaceName = "global::Uno.UI.IXamlResourceDictionaryProvider";
213215

214216
static XamlFileGenerator()
@@ -237,6 +239,7 @@ public XamlFileGenerator(
237239
bool isDebug,
238240
bool isHotReloadEnabled,
239241
bool isDesignTimeBuild,
242+
bool isUnoHead,
240243
bool skipUserControlsInVisualTree,
241244
bool shouldAnnotateGeneratedXaml,
242245
bool isUnoAssembly,
@@ -261,6 +264,7 @@ public XamlFileGenerator(
261264
_defaultLanguage = defaultLanguage.HasValue() ? defaultLanguage : "en-US";
262265
_isDebug = isDebug;
263266
_isHotReloadEnabled = isHotReloadEnabled;
267+
_isUnoHead = isUnoHead;
264268
_isDesignTimeBuild = isDesignTimeBuild;
265269
_skipUserControlsInVisualTree = skipUserControlsInVisualTree;
266270
_shouldAnnotateGeneratedXaml = shouldAnnotateGeneratedXaml;
@@ -281,6 +285,8 @@ public XamlFileGenerator(
281285
_contentPresenterSymbol = (INamedTypeSymbol)_metadataHelper.GetTypeByFullName(XamlConstants.Types.ContentPresenter);
282286
_frameworkElementSymbol = (INamedTypeSymbol)_metadataHelper.GetTypeByFullName(XamlConstants.Types.FrameworkElement);
283287
_uiElementSymbol = (INamedTypeSymbol)_metadataHelper.GetTypeByFullName(XamlConstants.Types.UIElement);
288+
_imageSourceSymbol = (INamedTypeSymbol)_metadataHelper.GetTypeByFullName(XamlConstants.Types.ImageSource);
289+
_imageSymbol = (INamedTypeSymbol)_metadataHelper.GetTypeByFullName(XamlConstants.Types.Image);
284290
_dependencyObjectSymbol = (INamedTypeSymbol)_metadataHelper.GetTypeByFullName(XamlConstants.Types.DependencyObject);
285291
_markupExtensionSymbol = (INamedTypeSymbol)_metadataHelper.GetTypeByFullName(XamlConstants.Types.MarkupExtension);
286292
_brushSymbol = (INamedTypeSymbol)_metadataHelper.GetTypeByFullName(XamlConstants.Types.Brush);
@@ -463,6 +469,8 @@ private string InnerGenerateFile()
463469

464470
using (writer.BlockInvariant("partial class {0} : {1}", _xClassName.ClassName, controlBaseType.ToDisplayString()))
465471
{
472+
BuildBaseUri(writer);
473+
466474
using (Scope(_xClassName.Namespace, _xClassName.ClassName))
467475
{
468476
var componentBuilder = new IndentedStringBuilder();
@@ -501,6 +509,19 @@ private string InnerGenerateFile()
501509
return writer.ToString();
502510
}
503511

512+
/// <summary>
513+
/// Builds the BaseUri strings constants to be set to all FrameworkElement instances
514+
/// </summary>
515+
private void BuildBaseUri(IIndentedStringBuilder writer)
516+
{
517+
var assembly = _isUnoHead ? "" : _generatorContext.Compilation.AssemblyName + "/";
518+
519+
writer.AppendLineIndented("[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]");
520+
writer.AppendLineInvariantIndented($"private const string __baseUri_prefix_{_fileUniqueId} = \"ms-appx:///{assembly}\";");
521+
writer.AppendLineIndented("[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]");
522+
writer.AppendLineInvariantIndented($"private const string __baseUri_{_fileUniqueId} = \"ms-appx:///{assembly}{_fileDefinition.TargetFilePath}\";");
523+
}
524+
504525
private void BuildInitializeComponent(IndentedStringBuilder writer, XamlObjectDefinition topLevelControl, INamedTypeSymbol controlBaseType, bool isDirectUserControlChild)
505526
{
506527
writer.AppendLineIndented("global::Windows.UI.Xaml.NameScope __nameScope = new global::Windows.UI.Xaml.NameScope();");
@@ -989,6 +1010,8 @@ private void BuildChildSubclasses(IIndentedStringBuilder writer, bool isTopLevel
9891010

9901011
using (writer.BlockInvariant($"{classAccessibility} class {className}"))
9911012
{
1013+
BuildBaseUri(writer);
1014+
9921015
using (ResourceOwnerScope())
9931016
{
9941017
writer.AppendLineIndented("global::Windows.UI.Xaml.NameScope __nameScope = new global::Windows.UI.Xaml.NameScope();");
@@ -1272,6 +1295,7 @@ private void BuildTopLevelResourceDictionary(IIndentedStringBuilder writer, Xaml
12721295
AnalyzerSuppressionsGenerator.Generate(writer, _analyzerSuppressions);
12731296
using (writer.BlockInvariant("public sealed partial class GlobalStaticResources"))
12741297
{
1298+
BuildBaseUri(writer);
12751299

12761300
IDisposable WrapSingleton()
12771301
{
@@ -1653,6 +1677,8 @@ private void BuildResourceDictionaryBackingClass(IIndentedStringBuilder writer,
16531677
AnalyzerSuppressionsGenerator.Generate(writer, _analyzerSuppressions);
16541678
using (writer.BlockInvariant("public sealed partial class {0} : {1}", className.ClassName, GetGlobalizedTypeName(controlBaseType.ToDisplayString())))
16551679
{
1680+
BuildBaseUri(writer);
1681+
16561682
using (Scope(className.Namespace, className.ClassName!))
16571683
{
16581684
using (writer.BlockInvariant("public void InitializeComponent()"))
@@ -3630,9 +3656,23 @@ private void BuildExtendedProperties(IIndentedStringBuilder outerwriter, XamlObj
36303656
writer.AppendLineIndented($");");
36313657
}
36323658

3633-
if (_isDebug && IsFrameworkElement(objectDefinition.Type))
3659+
if (IsFrameworkElement(objectDefinition.Type))
36343660
{
3635-
writer.AppendLineIndented($"global::Uno.UI.FrameworkElementHelper.SetBaseUri({closureName}, \"file:///{_fileDefinition.FilePath.Replace("\\", "/")}\");");
3661+
if (_isDebug)
3662+
{
3663+
writer.AppendLineIndented(
3664+
$"global::Uno.UI.FrameworkElementHelper.SetBaseUri(" +
3665+
$"{closureName}, " +
3666+
$"__baseUri_{_fileUniqueId}, " +
3667+
$"\"file:///{_fileDefinition.FilePath.Replace("\\", "/")}\", " +
3668+
$"{objectDefinition.LineNumber}, " +
3669+
$"{objectDefinition.LinePosition}" +
3670+
$");");
3671+
}
3672+
else
3673+
{
3674+
writer.AppendLineIndented($"global::Uno.UI.FrameworkElementHelper.SetBaseUri({closureName}, __baseUri_{_fileUniqueId});");
3675+
}
36363676
}
36373677

36383678
if (_isUiAutomationMappingEnabled)
@@ -4893,15 +4933,7 @@ string Inner()
48934933

48944934
case "System.Uri":
48954935
var uriValue = GetMemberValue();
4896-
4897-
if (uriValue.StartsWith("/", StringComparison.Ordinal))
4898-
{
4899-
return "new System.Uri(\"ms-appx://" + uriValue + "\")";
4900-
}
4901-
else
4902-
{
4903-
return "new System.Uri(\"" + uriValue + "\", global::System.UriKind.RelativeOrAbsolute)";
4904-
}
4936+
return $"new System.Uri({RewriteUri(uriValue)}, global::System.UriKind.RelativeOrAbsolute)";
49054937

49064938
case "System.Type":
49074939
return $"typeof({GetGlobalizedTypeName(GetType(GetMemberValue()).ToDisplayString())})";
@@ -4965,9 +4997,7 @@ string Inner()
49654997
return "Windows.Media.Core.MediaSource.CreateFromUri(new Uri(\"" + memberValue + "\"))";
49664998

49674999
case "Windows.UI.Xaml.Media.ImageSource":
4968-
// We have an implicit conversion from string to ImageSource.
4969-
//
4970-
return $"\"{memberValue}\"";
5000+
return RewriteUri(memberValue);
49715001
}
49725002

49735003
var isEnum = propertyType.TypeKind == TypeKind.Enum;
@@ -5041,9 +5071,55 @@ string Inner()
50415071

50425072
static string? SplitAndJoin(string? value)
50435073
=> value == null ? null : splitRegex.Replace(value, ", ");
5074+
5075+
string RewriteUri(string? rawValue)
5076+
{
5077+
if (rawValue is not null
5078+
&& Uri.TryCreate(rawValue, UriKind.RelativeOrAbsolute, out var parsedUri)
5079+
&& !parsedUri.IsAbsoluteUri)
5080+
{
5081+
var declaringType = FindFirstConcreteAncestorType(owner?.Owner);
5082+
5083+
if (
5084+
declaringType.Is(_imageSourceSymbol)
5085+
|| declaringType.Is(_imageSymbol)
5086+
)
5087+
{
5088+
var uriBase = rawValue.StartsWith("/", StringComparison.Ordinal)
5089+
? "\"ms-appx:///\""
5090+
: $"__baseUri_prefix_{_fileUniqueId}";
5091+
5092+
return $"{uriBase} + \"{rawValue.TrimStart('/')}\"";
5093+
}
5094+
else
5095+
{
5096+
// Breaking change, support for ms-resource:// for non framework owners (https://github.com/unoplatform/uno/issues/8339)
5097+
}
5098+
}
5099+
5100+
return $"\"{rawValue}\"";
5101+
}
50445102
}
50455103
}
50465104

5105+
/// <summary>
5106+
/// Finds the first ancestor type that is resolving to a known type (excluding _unknownContent members)
5107+
/// </summary>
5108+
private INamedTypeSymbol? FindFirstConcreteAncestorType(XamlObjectDefinition? objectDefinition)
5109+
{
5110+
while (objectDefinition is not null)
5111+
{
5112+
if (FindType(objectDefinition.Type) is { } type)
5113+
{
5114+
return type;
5115+
}
5116+
5117+
objectDefinition = objectDefinition.Owner;
5118+
}
5119+
5120+
return null;
5121+
}
5122+
50475123
private string? BuildLocalizedResourceValue(INamedTypeSymbol? owner, string memberName, string objectUid)
50485124
{
50495125
// see: https://docs.microsoft.com/en-us/windows/uwp/app-resources/localize-strings-ui-manifest
@@ -5159,7 +5235,7 @@ string Inner()
51595235

51605236
if (propertyType != null)
51615237
{
5162-
var s = BuildLiteralValue(propertyType, memberValue, owner, member.Member.Name, objectUid);
5238+
var s = BuildLiteralValue(propertyType, memberValue, owner ?? member, member.Member.Name, objectUid);
51635239

51645240
s += $"/* {propertyType}/{originalType}, {memberValue}, {member?.Member?.DeclaringType?.Name}/{member?.Member?.Name} */";
51655241

src/SourceGenerators/Uno.UI.SourceGenerators/XamlGenerator/XamlFileParser.cs

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,22 +52,35 @@ public XamlFileParser(string excludeXamlNamespaces, string includeXamlNamespaces
5252
_metadataHelper = roslynMetadataHelper;
5353
}
5454

55-
public XamlFileDefinition[] ParseFiles(Uno.Roslyn.MSBuildItem[] xamlSourceFiles, CancellationToken cancellationToken)
55+
public XamlFileDefinition[] ParseFiles(Uno.Roslyn.MSBuildItem[] xamlSourceFiles, string projectDirectory, CancellationToken cancellationToken)
5656
{
5757
return xamlSourceFiles
5858
.AsParallel()
5959
.WithCancellation(cancellationToken)
60-
.Select(f => ParseFile(f.File, cancellationToken))
60+
.Select(f => InnerParseFile(f, cancellationToken))
6161
.Where(f => f != null)
6262
.ToArray()!;
63+
64+
XamlFileDefinition? InnerParseFile(MSBuildItem fileItem, CancellationToken cancellationToken)
65+
{
66+
// Generate an actual TargetPath to be used with BaseUri, so that
67+
// it maps to the actual path in the app package.
68+
var targetFilePath = fileItem.GetMetadataValue("TargetPath") is { Length: > 0 } targetPath
69+
? targetPath
70+
: fileItem.GetMetadataValue("Link") is { Length: > 0 } link
71+
? link
72+
: fileItem.GetMetadataValue("Identity").Replace(projectDirectory, "");
73+
74+
return ParseFile(fileItem.File, targetFilePath.Replace("\\", "/"), cancellationToken);
75+
}
6376
}
6477

6578
private static void ScavengeCache()
6679
{
6780
_cachedFiles.Remove(kvp => DateTimeOffset.Now - kvp.Value.LastTimeUsed > _cacheEntryLifetime);
6881
}
6982

70-
private XamlFileDefinition? ParseFile(AdditionalText file, CancellationToken cancellationToken)
83+
private XamlFileDefinition? ParseFile(AdditionalText file, string targetFilePath, CancellationToken cancellationToken)
7184
{
7285
try
7386
{
@@ -107,7 +120,7 @@ private static void ScavengeCache()
107120
{
108121
cancellationToken.ThrowIfCancellationRequested();
109122

110-
var xamlFileDefinition = Visit(reader, file.Path);
123+
var xamlFileDefinition = Visit(reader, file.Path, targetFilePath);
111124
if (!disableCaching)
112125
{
113126
_cachedFiles[cachedFileKey] = new CachedFile(DateTimeOffset.Now, xamlFileDefinition);
@@ -391,11 +404,11 @@ private static StringBuilder ReplaceFirst(string targetString, string oldValue,
391404
return (included, excluded, disableCaching);
392405
}
393406

394-
private XamlFileDefinition Visit(XamlXmlReader reader, string file)
407+
private XamlFileDefinition Visit(XamlXmlReader reader, string file, string targetFilePath)
395408
{
396409
WriteState(reader);
397410

398-
var xamlFile = new XamlFileDefinition(file);
411+
var xamlFile = new XamlFileDefinition(file, targetFilePath);
399412

400413
do
401414
{

0 commit comments

Comments
 (0)