Browse Source

#19962 Add AXAML Source Information to debug Builds (#20524)

* #19962 Add AXAML Source Information to debug Builds

* SimplifyXamlSourceInfo

* Add XamlSourceInfo for as many elements as possible

* Add tests to confirm XamlSourceInfo is set for all types

* Remove property only added for debugging during development

* update skipped test so it runs (even though it doesn't yet pass)

* Wrap XamlAstNewClrObjectNode instead of XamlAstObjectNode, run transformer late

* Remove unsupported value types from the More_Resources_Get_XamlSourceInfo_Set

* Fix Document property not being set in runtime parser

* Add a dedicated CreateSourceInfo parameter for RuntimeXamlLoaderConfiguration, instead of reusing DesignMode

* Inherit real XamlValueWithManipulationNode, move actual manipulation to a separate class

* Fix group transformers by unwrapping manipulation nodes first

* minor Resource related test change

* Update public API as agreed

* Add new failing tests for the dictionaries

* Fix randomly failing tests, that depend on the test order

* Fix assert

* Rename AvaloniaXamlResourceTransformer

* Emit XamlSourceInfo from AvaloniaXamlResourceTransformer

* Rename AvaloniaXamlIlResourceTransformer for consistency

* Cleanup comments

* Remove XamlSourceInfoValueWithManipulationNode, use standard XamlValueWithManipulationNode

* Add new RuntimeXamlLoaderDocument.Document property

* Use UriBuilder trick to support unix paths on windows

* Add private AttachedProperty for avalonia objects, instead of always using weak table

* Fix wrong UriBuilder usage and add more test assets

* Fix "Invalid URI" exception

---------

Co-authored-by: KimHenrik <kimhenrik@outlook.de>
Co-authored-by: Max Katz <maxkatz6@outlook.com>
pull/20557/merge
Matt Lacey 1 week ago
committed by GitHub
parent
commit
5361b98ce1
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 3
      packages/Avalonia/AvaloniaBuildTasks.targets
  2. 5
      packages/Avalonia/AvaloniaRules.Project.xml
  3. 4
      src/Avalonia.Build.Tasks/CompileAvaloniaXamlTask.cs
  4. 8
      src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs
  5. 7
      src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaXamlIlRuntimeCompiler.cs
  6. 15
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs
  7. 31
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlIncludeGroupTransformer.cs
  8. 69
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlAddSourceInfoTransformer.cs
  9. 191
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDeferredResourceTransformer.cs
  10. 343
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlResourceTransformer.cs
  11. 12
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
  12. 26
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlAstNewClrObjectHelper.cs
  13. 1
      src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj
  14. 151
      src/Markup/Avalonia.Markup.Xaml/Diagnostics/XamlSourceInfo.cs
  15. 6
      src/Markup/Avalonia.Markup.Xaml/RuntimeXamlLoaderConfiguration.cs
  16. 5
      src/Markup/Avalonia.Markup.Xaml/RuntimeXamlLoaderDocument.cs
  17. 10
      tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/ResourceIncludeTests.cs
  18. 9
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/MergeResourceIncludeTests.cs
  19. 4
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleIncludeTests.cs
  20. 583
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlSourceInfoTests.cs

3
packages/Avalonia/AvaloniaBuildTasks.targets

@ -134,6 +134,8 @@
<AvaloniaXamlIlVerifyIl Condition="'$(AvaloniaXamlIlVerifyIl)' == ''">false</AvaloniaXamlIlVerifyIl> <AvaloniaXamlIlVerifyIl Condition="'$(AvaloniaXamlIlVerifyIl)' == ''">false</AvaloniaXamlIlVerifyIl>
<AvaloniaXamlIlDebuggerLaunch Condition="'$(AvaloniaXamlIlDebuggerLaunch)' == ''">false</AvaloniaXamlIlDebuggerLaunch> <AvaloniaXamlIlDebuggerLaunch Condition="'$(AvaloniaXamlIlDebuggerLaunch)' == ''">false</AvaloniaXamlIlDebuggerLaunch>
<AvaloniaXamlVerboseExceptions Condition="'$(AvaloniaXamlVerboseExceptions)' == ''">false</AvaloniaXamlVerboseExceptions> <AvaloniaXamlVerboseExceptions Condition="'$(AvaloniaXamlVerboseExceptions)' == ''">false</AvaloniaXamlVerboseExceptions>
<AvaloniaXamlCreateSourceInfo Condition="'$(AvaloniaXamlCreateSourceInfo)' == '' AND '$(Configuration)' == 'Debug'">true</AvaloniaXamlCreateSourceInfo>
<AvaloniaXamlCreateSourceInfo Condition="'$(AvaloniaXamlCreateSourceInfo)' == '' AND '$(Configuration)' != 'Debug'">false</AvaloniaXamlCreateSourceInfo>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@ -162,6 +164,7 @@
DelaySign="$(DelaySign)" DelaySign="$(DelaySign)"
SkipXamlCompilation="$(_AvaloniaSkipXamlCompilation)" SkipXamlCompilation="$(_AvaloniaSkipXamlCompilation)"
DebuggerLaunch="$(AvaloniaXamlIlDebuggerLaunch)" DebuggerLaunch="$(AvaloniaXamlIlDebuggerLaunch)"
CreateSourceInfo="$(AvaloniaXamlCreateSourceInfo)"
DefaultCompileBindings="$(AvaloniaUseCompiledBindingsByDefault)" DefaultCompileBindings="$(AvaloniaUseCompiledBindingsByDefault)"
VerboseExceptions="$(AvaloniaXamlVerboseExceptions)" VerboseExceptions="$(AvaloniaXamlVerboseExceptions)"
AnalyzerConfigFiles="@(EditorConfigFiles)"/> AnalyzerConfigFiles="@(EditorConfigFiles)"/>

5
packages/Avalonia/AvaloniaRules.Project.xml

@ -31,6 +31,11 @@
Description="Allow debug XAML compilation" Description="Allow debug XAML compilation"
Category="Debug" /> Category="Debug" />
<BoolProperty Name="AvaloniaXamlCreateSourceInfo"
DisplayName="Generate XAML Source Info"
Description="When enabled, the XAML compiler embeds SourceInfo metadata (file path, line, and column) into generated code. This allows tools and debuggers to locate the original .axaml source position of a selected element at runtime or in the designer."
Category="Debug" />
<BoolProperty Name="AvaloniaXamlVerboseExceptions" <BoolProperty Name="AvaloniaXamlVerboseExceptions"
DisplayName="Report verbose internal exceptions with stack traces" DisplayName="Report verbose internal exceptions with stack traces"
Description="Also includes inner exceptions" Description="Also includes inner exceptions"

4
src/Avalonia.Build.Tasks/CompileAvaloniaXamlTask.cs

@ -32,7 +32,7 @@ namespace Avalonia.Build.Tasks
ProjectDirectory, VerifyIl, DefaultCompileBindings, outputImportance, ProjectDirectory, VerifyIl, DefaultCompileBindings, outputImportance,
new XamlCompilerDiagnosticsFilter(AnalyzerConfigFiles), new XamlCompilerDiagnosticsFilter(AnalyzerConfigFiles),
(SignAssembly && !DelaySign) ? AssemblyOriginatorKeyFile : null, (SignAssembly && !DelaySign) ? AssemblyOriginatorKeyFile : null,
SkipXamlCompilation, DebuggerLaunch, VerboseExceptions); SkipXamlCompilation, DebuggerLaunch, VerboseExceptions, CreateSourceInfo);
if (res.Success && !res.WrittenFile) if (res.Success && !res.WrittenFile)
{ {
@ -99,6 +99,8 @@ namespace Avalonia.Build.Tasks
public bool DebuggerLaunch { get; set; } public bool DebuggerLaunch { get; set; }
public bool CreateSourceInfo { get; set; }
public bool VerboseExceptions { get; set; } public bool VerboseExceptions { get; set; }
public ITaskItem[] AnalyzerConfigFiles { get; set; } public ITaskItem[] AnalyzerConfigFiles { get; set; }

8
src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs

@ -52,7 +52,7 @@ namespace Avalonia.Build.Tasks
string[] references, string projectDirectory, string[] references, string projectDirectory,
bool verifyIl, bool defaultCompileBindings, MessageImportance logImportance, bool verifyIl, bool defaultCompileBindings, MessageImportance logImportance,
XamlCompilerDiagnosticsFilter diagnosticsFilter, string strongNameKey, XamlCompilerDiagnosticsFilter diagnosticsFilter, string strongNameKey,
bool skipXamlCompilation, bool debuggerLaunch, bool verboseExceptions) bool skipXamlCompilation, bool debuggerLaunch, bool verboseExceptions, bool createSourceInfo)
{ {
try try
{ {
@ -67,7 +67,7 @@ namespace Avalonia.Build.Tasks
var compileRes = CompileCore( var compileRes = CompileCore(
engine, typeSystem, projectDirectory, verifyIl, engine, typeSystem, projectDirectory, verifyIl,
defaultCompileBindings, logImportance, diagnosticsFilter, defaultCompileBindings, logImportance, diagnosticsFilter,
debuggerLaunch, verboseExceptions); debuggerLaunch, verboseExceptions, createSourceInfo);
if (compileRes == null) if (compileRes == null)
return new CompileResult(true); return new CompileResult(true);
if (compileRes == false) if (compileRes == false)
@ -107,7 +107,8 @@ namespace Avalonia.Build.Tasks
MessageImportance logImportance, MessageImportance logImportance,
XamlCompilerDiagnosticsFilter diagnosticsFilter, XamlCompilerDiagnosticsFilter diagnosticsFilter,
bool debuggerLaunch, bool debuggerLaunch,
bool verboseExceptions) bool verboseExceptions,
bool createSourceInfo)
{ {
if (debuggerLaunch) if (debuggerLaunch)
{ {
@ -210,6 +211,7 @@ namespace Avalonia.Build.Tasks
{ {
EnableIlVerification = verifyIl, EnableIlVerification = verifyIl,
DefaultCompileBindings = defaultCompileBindings, DefaultCompileBindings = defaultCompileBindings,
CreateSourceInfo = createSourceInfo,
DynamicSetterContainerProvider = new DefaultXamlDynamicSetterContainerProvider(dynamicSettersBuilder) DynamicSetterContainerProvider = new DefaultXamlDynamicSetterContainerProvider(dynamicSettersBuilder)
}; };

7
src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaXamlIlRuntimeCompiler.cs

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
@ -344,7 +344,8 @@ namespace Avalonia.Markup.Xaml.XamlIl
{ {
EnableIlVerification = true, EnableIlVerification = true,
DefaultCompileBindings = configuration.UseCompiledBindingsByDefault, DefaultCompileBindings = configuration.UseCompiledBindingsByDefault,
IsDesignMode = configuration.DesignMode IsDesignMode = configuration.DesignMode,
CreateSourceInfo = configuration.CreateSourceInfo,
}; };
var parsedDocuments = new List<XamlDocumentResource>(); var parsedDocuments = new List<XamlDocumentResource>();
@ -363,7 +364,7 @@ namespace Avalonia.Markup.Xaml.XamlIl
} }
var parsed = compiler.Parse(xaml, overrideType); var parsed = compiler.Parse(xaml, overrideType);
parsed.Document = "runtimexaml:" + parsedDocuments.Count; parsed.Document = document.Document ?? ("runtimexaml" + parsedDocuments.Count);
compiler.Transform(parsed); compiler.Transform(parsed);
var xamlName = GetSafeUriIdentifier(document.BaseUri) var xamlName = GetSafeUriIdentifier(document.BaseUri)

15
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using Avalonia.Markup.Xaml.Loader.CompilerExtensions.Transformers;
using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.GroupTransformers; using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.GroupTransformers;
using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers; using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers;
using XamlX; using XamlX;
@ -20,6 +21,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
private readonly IXamlType _contextType = null!; private readonly IXamlType _contextType = null!;
private readonly AvaloniaXamlIlDesignPropertiesTransformer _designTransformer; private readonly AvaloniaXamlIlDesignPropertiesTransformer _designTransformer;
private readonly AvaloniaBindingExtensionTransformer _bindingTransformer; private readonly AvaloniaBindingExtensionTransformer _bindingTransformer;
private readonly AvaloniaXamlIlAddSourceInfoTransformer _addSourceInfoTransformer;
private readonly AvaloniaXamlResourceTransformer _resourceTransformer;
private AvaloniaXamlIlCompiler(TransformerConfiguration configuration, XamlLanguageEmitMappings<IXamlILEmitter, XamlILNodeEmitResult> emitMappings) private AvaloniaXamlIlCompiler(TransformerConfiguration configuration, XamlLanguageEmitMappings<IXamlILEmitter, XamlILNodeEmitResult> emitMappings)
: base(configuration, emitMappings, true) : base(configuration, emitMappings, true)
@ -47,6 +50,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
new AvaloniaXamlIlResolveClassesPropertiesTransformer(), new AvaloniaXamlIlResolveClassesPropertiesTransformer(),
new AvaloniaXamlIlTransformInstanceAttachedProperties(), new AvaloniaXamlIlTransformInstanceAttachedProperties(),
new AvaloniaXamlIlTransformSyntheticCompiledBindingMembers()); new AvaloniaXamlIlTransformSyntheticCompiledBindingMembers());
InsertAfter<PropertyReferenceResolver>( InsertAfter<PropertyReferenceResolver>(
new AvaloniaXamlIlAvaloniaPropertyResolver(), new AvaloniaXamlIlAvaloniaPropertyResolver(),
new AvaloniaXamlIlReorderClassesPropertiesTransformer(), new AvaloniaXamlIlReorderClassesPropertiesTransformer(),
@ -85,7 +90,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
); );
InsertBeforeMany(new [] { typeof(DeferredContentTransformer), typeof(AvaloniaXamlIlCompiledBindingsMetadataRemover) }, InsertBeforeMany(new [] { typeof(DeferredContentTransformer), typeof(AvaloniaXamlIlCompiledBindingsMetadataRemover) },
new AvaloniaXamlIlDeferredResourceTransformer()); _resourceTransformer = new AvaloniaXamlResourceTransformer());
InsertBefore<AvaloniaXamlIlTransformInstanceAttachedProperties>(new AvaloniaXamlIlTransformRoutedEvent()); InsertBefore<AvaloniaXamlIlTransformInstanceAttachedProperties>(new AvaloniaXamlIlTransformRoutedEvent());
@ -94,6 +99,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
Transformers.Add(new AvaloniaXamlIlEnsureResourceDictionaryCapacityTransformer()); Transformers.Add(new AvaloniaXamlIlEnsureResourceDictionaryCapacityTransformer());
Transformers.Add(new AvaloniaXamlIlRootObjectScope()); Transformers.Add(new AvaloniaXamlIlRootObjectScope());
Transformers.Add(_addSourceInfoTransformer = new AvaloniaXamlIlAddSourceInfoTransformer());
Emitters.Add(new AvaloniaNameScopeRegistrationXamlIlNodeEmitter()); Emitters.Add(new AvaloniaNameScopeRegistrationXamlIlNodeEmitter());
Emitters.Add(new AvaloniaXamlIlRootObjectScope.Emitter()); Emitters.Add(new AvaloniaXamlIlRootObjectScope.Emitter());
@ -122,6 +129,12 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
public const string PopulateName = "__AvaloniaXamlIlPopulate"; public const string PopulateName = "__AvaloniaXamlIlPopulate";
public const string BuildName = "__AvaloniaXamlIlBuild"; public const string BuildName = "__AvaloniaXamlIlBuild";
public bool CreateSourceInfo
{
get => _addSourceInfoTransformer.CreateSourceInfo || _resourceTransformer.CreateSourceInfo;
set => _addSourceInfoTransformer.CreateSourceInfo = _resourceTransformer.CreateSourceInfo = value;
}
public bool IsDesignMode public bool IsDesignMode
{ {
get => _designTransformer.IsDesignMode; get => _designTransformer.IsDesignMode;

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

@ -20,8 +20,12 @@ internal class AvaloniaXamlIncludeTransformer : IXamlAstGroupTransformer
{ {
public IXamlAstNode Transform(AstGroupTransformationContext context, IXamlAstNode node) public IXamlAstNode Transform(AstGroupTransformationContext context, IXamlAstNode node)
{ {
if (node is not XamlValueWithManipulationNode valueNode // Filter object initialization nodes like:
|| valueNode.Value is not XamlAstNewClrObjectNode objectNode // > XamlValueWithManipulationNode
// > > XamlAstNewClrObjectNode // StyleInclude or ResourceInclude, can be nested in another XamlValueWithManipulationNode
// > > XamlObjectInitializationNode
if (node is not XamlValueWithManipulationNode { Manipulation: XamlObjectInitializationNode initializationNode } valueNode
|| valueNode.UnwrapValue<XamlAstNewClrObjectNode>() is not { } objectNode
|| (objectNode.Type.GetClrType() != context.GetAvaloniaTypes().StyleInclude || (objectNode.Type.GetClrType() != context.GetAvaloniaTypes().StyleInclude
&& objectNode.Type.GetClrType() != context.GetAvaloniaTypes().ResourceInclude)) && objectNode.Type.GetClrType() != context.GetAvaloniaTypes().ResourceInclude))
{ {
@ -36,11 +40,6 @@ internal class AvaloniaXamlIncludeTransformer : IXamlAstGroupTransformer
throw new InvalidOperationException($"\"{nodeTypeName}\".Loaded property is expected to be defined"); throw new InvalidOperationException($"\"{nodeTypeName}\".Loaded property is expected to be defined");
} }
if (valueNode.Manipulation is not XamlObjectInitializationNode initializationNode)
{
throw new InvalidOperationException($"Invalid \"{nodeTypeName}\" node initialization.");
}
var additionalProperties = new List<IXamlAstManipulationNode>(); var additionalProperties = new List<IXamlAstManipulationNode>();
if (initializationNode.Manipulation is not XamlPropertyAssignmentNode { Property: { Name: "Source" } } sourceProperty) if (initializationNode.Manipulation is not XamlPropertyAssignmentNode { Property: { Name: "Source" } } sourceProperty)
{ {
@ -178,7 +177,23 @@ internal class AvaloniaXamlIncludeTransformer : IXamlAstGroupTransformer
node); node);
// We expect that AvaloniaXamlIlLanguageParseIntrinsics has already parsed the Uri and created node like: `new Uri(assetPath, uriKind)`. // 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 if (sourceProperty.Values.Count != 1)
{
OnInvalidSource(sourceProperty);
return (null, null);
}
// `new Uri` can be wrapped in manipulation node if source info or another manipulation was applied.
var sourceUriNodeWrapped = sourceProperty.Values.Single();
var sourceUriNode = sourceUriNodeWrapped switch
{
XamlAstNewClrObjectNode newObj => newObj,
XamlValueWithManipulationNode manipulation => manipulation.UnwrapValue<XamlAstNewClrObjectNode>(),
_ => null
};
// Validate Uri type and constant arguments.
if (sourceUriNode is null
|| sourceUriNode.Type.GetClrType() != context.GetAvaloniaTypes().Uri || 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 }) || sourceUriNode.Arguments.Skip(1).FirstOrDefault() is not XamlConstantNode { Constant: int uriKind })

69
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlAddSourceInfoTransformer.cs

@ -0,0 +1,69 @@
using System.Linq;
using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers;
using XamlX.Ast;
using XamlX.Emit;
using XamlX.IL;
using XamlX.Transform;
namespace Avalonia.Markup.Xaml.Loader.CompilerExtensions.Transformers
{
/// <summary>
/// An XAMLIL AST transformer that injects <see cref="Avalonia.Markup.Xaml.Diagnostics.XamlSourceInfo"/> metadata into the generated XAML code.
/// </summary>
/// <remarks>
/// This transformer wraps object creation nodes with a manipulation node that adds source information.
/// This source information includes line number, position, and document name, which can be useful for debugging and diagnostics.
/// Note: ResourceDictionary source info is handled separately in <see cref="AvaloniaXamlResourceTransformer"/>.
/// </remarks>
internal class AvaloniaXamlIlAddSourceInfoTransformer : IXamlAstTransformer
{
/// <summary>
/// Gets or sets a value indicating whether source information should be generated
/// and injected into the compiled XAML output.
/// </summary>
public bool CreateSourceInfo { get; set; }
public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node)
{
if (CreateSourceInfo
&& node is XamlAstNewClrObjectNode objNode
&& context.ParentNodes().FirstOrDefault() is not XamlValueWithManipulationNode { Manipulation: XamlSourceInfoValueManipulation }
&& !objNode.Type.GetClrType().IsValueType)
{
var avaloniaTypes = context.GetAvaloniaTypes();
return new XamlValueWithManipulationNode(
objNode, objNode,
new XamlSourceInfoValueManipulation(avaloniaTypes, objNode, context.Document));
}
return node;
}
private class XamlSourceInfoValueManipulation(
AvaloniaXamlIlWellKnownTypes avaloniaTypes,
XamlAstNewClrObjectNode objNode, string? document)
: XamlAstNode(objNode), IXamlAstManipulationNode, IXamlAstILEmitableNode
{
public XamlILNodeEmitResult Emit(XamlEmitContext<IXamlILEmitter, XamlILNodeEmitResult> context, IXamlILEmitter codeGen)
{
// Target object is already on stack.
// var info = new XamlSourceInfo(Line, Position, Document);
codeGen.Ldc_I4(Line);
codeGen.Ldc_I4(Position);
if (document is not null)
codeGen.Ldstr(document);
else
codeGen.Ldnull();
codeGen.Newobj(avaloniaTypes.XamlSourceInfoConstructor);
// Set the XamlSourceInfo property on the current object
// XamlSourceInfo.SetValue(@this, info);
codeGen.EmitCall(avaloniaTypes.XamlSourceInfoSetter);
return XamlILNodeEmitResult.Void(1);
}
}
}
}

191
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDeferredResourceTransformer.cs

@ -1,191 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Visitors;
using XamlX;
using XamlX.Ast;
using XamlX.Emit;
using XamlX.IL;
using XamlX.Transform;
using XamlX.TypeSystem;
namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
{
internal class AvaloniaXamlIlDeferredResourceTransformer : IXamlAstTransformer
{
public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node)
{
if (!(node is XamlPropertyAssignmentNode pa) || pa.Values.Count != 2)
return node;
var types = context.GetAvaloniaTypes();
if (pa.Property.DeclaringType == types.ResourceDictionary && pa.Property.Name == "Content"
&& ShouldBeDeferred(pa.Values[1]))
{
IXamlMethod addMethod = TryGetSharedValue(pa.Values[1], out var isShared) && !isShared
? types.ResourceDictionaryNotSharedDeferredAdd
: types.ResourceDictionaryDeferredAdd;
pa.Values[1] = new XamlDeferredContentNode(pa.Values[1], types.XamlIlTypes.Object, context.Configuration);
pa.PossibleSetters = new List<IXamlPropertySetter>
{
new XamlDirectCallPropertySetter(addMethod),
};
}
else if (pa.Property.Name == "Resources" && pa.Property.Getter?.ReturnType.Equals(types.IResourceDictionary) == true
&& ShouldBeDeferred(pa.Values[1]))
{
IXamlMethod addMethod = TryGetSharedValue(pa.Values[1], out var isShared) && !isShared
? types.ResourceDictionaryNotSharedDeferredAdd
: types.ResourceDictionaryDeferredAdd;
pa.Values[1] = new XamlDeferredContentNode(pa.Values[1], types.XamlIlTypes.Object, context.Configuration);
pa.PossibleSetters = new List<IXamlPropertySetter>
{
new AdderSetter(pa.Property.Getter, addMethod),
};
}
return node;
bool TryGetSharedValue(IXamlAstValueNode valueNode, out bool value)
{
value = default;
if (valueNode is XamlAstConstructableObjectNode co)
{
// Try find x:Share directive
if (co.Children.Find(d => d is XamlAstXmlDirective { Namespace: XamlNamespaces.Xaml2006, Name: "Shared" }) is XamlAstXmlDirective sharedDirective)
{
if (sharedDirective.Values.Count == 1 && sharedDirective.Values[0] is XamlAstTextNode text)
{
if (bool.TryParse(text.Text, out var parseValue))
{
// If the parser succeeds, remove the x:Share directive
co.Children.Remove(sharedDirective);
return true;
}
else
{
context.ReportTransformError("Invalid argument type for x:Shared directive.", node);
}
}
else
{
context.ReportTransformError("Invalid number of arguments for x:Shared directive.", node);
}
}
}
return false;
}
}
private static bool ShouldBeDeferred(IXamlAstValueNode node)
{
var clrType = node.Type.GetClrType();
// XAML compiler is currently strict about value types, allowing them to be created only through converters.
// At the moment it should be safe to not defer structs.
if (clrType.IsValueType)
{
return false;
}
// Never defer strings.
if (clrType.FullName == "System.String")
{
return false;
}
// Do not defer resources, if it has any x:Name registration, as it cannot be delayed.
// This visitor will count x:Name registrations, ignoring nested NestedScopeMetadataNode scopes.
// We set target scope level to 0, assuming that this resource node is a scope of itself.
var nameRegistrationsVisitor = new NameScopeRegistrationVisitor(
targetMetadataScopeLevel: 0);
node.Visit(nameRegistrationsVisitor);
if (nameRegistrationsVisitor.Count > 0)
{
return false;
}
return true;
}
class AdderSetter : IXamlILOptimizedEmitablePropertySetter, IEquatable<AdderSetter>
{
private readonly IXamlMethod _getter;
private readonly IXamlMethod _adder;
public AdderSetter(IXamlMethod getter, IXamlMethod adder)
{
_getter = getter;
_adder = adder;
TargetType = getter.DeclaringType;
Parameters = adder.ParametersWithThis().Skip(1).ToList();
bool allowNull = Parameters.Last().AcceptsNull();
BinderParameters = new PropertySetterBinderParameters
{
AllowMultiple = true,
AllowXNull = allowNull,
AllowRuntimeNull = allowNull,
AllowAttributeSyntax = false,
};
}
public IXamlType TargetType { get; }
public PropertySetterBinderParameters BinderParameters { get; }
public IReadOnlyList<IXamlType> Parameters { get; }
public IReadOnlyList<IXamlCustomAttribute> CustomAttributes => _adder.CustomAttributes;
public void Emit(IXamlILEmitter emitter)
{
var locals = new Stack<XamlLocalsPool.PooledLocal>();
// Save all "setter" parameters
for (var c = Parameters.Count - 1; c >= 0; c--)
{
var loc = emitter.LocalsPool.GetLocal(Parameters[c]);
locals.Push(loc);
emitter.Stloc(loc.Local);
}
emitter.EmitCall(_getter);
while (locals.Count>0)
using (var loc = locals.Pop())
emitter.Ldloc(loc.Local);
emitter.EmitCall(_adder, true);
}
public void EmitWithArguments(
XamlEmitContextWithLocals<IXamlILEmitter, XamlILNodeEmitResult> context,
IXamlILEmitter emitter,
IReadOnlyList<IXamlAstValueNode> arguments)
{
emitter.EmitCall(_getter);
for (var i = 0; i < arguments.Count; ++i)
context.Emit(arguments[i], emitter, Parameters[i]);
emitter.EmitCall(_adder, true);
}
public bool Equals(AdderSetter? other)
{
if (ReferenceEquals(null, other))
return false;
if (ReferenceEquals(this, other))
return true;
return _getter.Equals(other._getter) && _adder.Equals(other._adder);
}
public override bool Equals(object? obj)
=> Equals(obj as AdderSetter);
public override int GetHashCode()
=> (_getter.GetHashCode() * 397) ^ _adder.GetHashCode();
}
}
}

343
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlResourceTransformer.cs

@ -0,0 +1,343 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Visitors;
using XamlX;
using XamlX.Ast;
using XamlX.Emit;
using XamlX.IL;
using XamlX.Transform;
using XamlX.TypeSystem;
namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
{
/// <summary>
/// Transforms ResourceDictionary and IResourceDictionary property assignments
/// to use Add method calls with deferred content where applicable.
/// Additionally, handles x:Shared on assignments and injects XamlSourceInfo.
/// </summary>
internal class AvaloniaXamlResourceTransformer : IXamlAstTransformer
{
/// <summary>
/// Gets or sets a value indicating whether source information should be generated
/// and injected into the compiled XAML output.
/// </summary>
public bool CreateSourceInfo { get; set; } = true;
public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node)
{
if (!(node is XamlPropertyAssignmentNode pa) || pa.Values.Count != 2)
return node;
var types = context.GetAvaloniaTypes();
var document = context.Document;
if (pa.Property.DeclaringType == types.ResourceDictionary && pa.Property.Name == "Content")
{
var value = pa.Values[1];
(var adder, value) = ResolveAdderAndValue(value);
pa.Values[1] = value;
pa.PossibleSetters = new List<IXamlPropertySetter>
{
new AdderSetter(adder, CreateSourceInfo, types, value.Line, value.Position, document),
};
}
else if (pa.Property.Name == "Resources" && pa.Property.Getter?.ReturnType.Equals(types.IResourceDictionary) == true)
{
var value = pa.Values[1];
(var adder, value) = ResolveAdderAndValue(value);
pa.Values[1] = value;
pa.PossibleSetters = new List<IXamlPropertySetter>
{
new AdderSetter(pa.Property.Getter, adder, CreateSourceInfo, types, value.Line, value.Position, document),
};
}
return node;
(IXamlMethod adder, IXamlAstValueNode newValue) ResolveAdderAndValue(IXamlAstValueNode valueNode)
{
if (ShouldBeDeferred(valueNode))
{
var adder = TryGetSharedValue(valueNode, out var isShared) && !isShared
? types.ResourceDictionaryNotSharedDeferredAdd
: types.ResourceDictionaryDeferredAdd;
var deferredNode = new XamlDeferredContentNode(valueNode, types.XamlIlTypes.Object, context.Configuration);
return (adder, deferredNode);
}
else
{
var adder = XamlTransformHelpers.FindPossibleAdders(context, types.IResourceDictionary)
.FirstOrDefault() ?? throw new XamlTransformException("No suitable Add method found for IResourceDictionary.", node);
return (adder, valueNode);
}
}
bool TryGetSharedValue(IXamlAstValueNode valueNode, out bool value)
{
value = default;
if (valueNode is XamlAstConstructableObjectNode co)
{
// Try find x:Share directive
if (co.Children.Find(d => d is XamlAstXmlDirective { Namespace: XamlNamespaces.Xaml2006, Name: "Shared" }) is XamlAstXmlDirective sharedDirective)
{
if (sharedDirective.Values.Count == 1 && sharedDirective.Values[0] is XamlAstTextNode text)
{
if (bool.TryParse(text.Text, out var parseValue))
{
// If the parser succeeds, remove the x:Share directive
co.Children.Remove(sharedDirective);
return true;
}
else
{
context.ReportTransformError("Invalid argument type for x:Shared directive.", node);
}
}
else
{
context.ReportTransformError("Invalid number of arguments for x:Shared directive.", node);
}
}
}
return false;
}
}
private static bool ShouldBeDeferred(IXamlAstValueNode node)
{
var clrType = node.Type.GetClrType();
// XAML compiler is currently strict about value types, allowing them to be created only through converters.
// At the moment it should be safe to not defer structs.
if (clrType.IsValueType)
{
return false;
}
// Never defer strings.
if (clrType.FullName == "System.String")
{
return false;
}
// Do not defer resources, if it has any x:Name registration, as it cannot be delayed.
// This visitor will count x:Name registrations, ignoring nested NestedScopeMetadataNode scopes.
// We set target scope level to 0, assuming that this resource node is a scope of itself.
var nameRegistrationsVisitor = new NameScopeRegistrationVisitor(
targetMetadataScopeLevel: 0);
node.Visit(nameRegistrationsVisitor);
if (nameRegistrationsVisitor.Count > 0)
{
return false;
}
return true;
}
class AdderSetter : IXamlILOptimizedEmitablePropertySetter, IEquatable<AdderSetter>
{
private readonly IXamlMethod? _getter;
private readonly IXamlMethod _adder;
private readonly bool _emitSourceInfo;
private readonly AvaloniaXamlIlWellKnownTypes _avaloniaTypes;
private readonly string? _document;
private readonly int _line, _position;
/// <summary>
/// Creates an adder-only setter. Target is assumed to be already on the stack before emit.
/// For example:
/// var resourceDictionary = ...
/// resourceDictionary.Add(key, value);
/// resourceDictionary.Add(key2, value2);
/// </summary>
public AdderSetter(
IXamlMethod adder,
bool emitSourceInfo,
AvaloniaXamlIlWellKnownTypes avaloniaTypes,
int line, int position, string? document)
{
_adder = adder;
_emitSourceInfo = emitSourceInfo;
_avaloniaTypes = avaloniaTypes;
_line = line;
_position = position;
_document = document;
TargetType = adder.ThisOrFirstParameter();
Parameters = adder.ParametersWithThis().Skip(1).ToList();
bool allowNull = Parameters.Last().AcceptsNull();
BinderParameters = new PropertySetterBinderParameters
{
AllowMultiple = true,
AllowXNull = allowNull,
AllowRuntimeNull = allowNull
};
}
/// <summary>
/// Explicit target getter - target will be obtained by calling the getter first.
///
/// </summary>
public AdderSetter(
IXamlMethod getter, IXamlMethod adder,
bool emitSourceInfo,
AvaloniaXamlIlWellKnownTypes avaloniaTypes,
int line, int position, string? document)
: this(adder, emitSourceInfo, avaloniaTypes, line, position, document)
{
_getter = getter;
TargetType = getter.DeclaringType;
BinderParameters.AllowMultiple = false;
BinderParameters.AllowAttributeSyntax = false;
}
public IXamlType TargetType { get; }
public PropertySetterBinderParameters BinderParameters { get; }
public IReadOnlyList<IXamlType> Parameters { get; }
public IReadOnlyList<IXamlCustomAttribute> CustomAttributes => _adder.CustomAttributes;
/// <summary>
/// Emits the setter with arguments already on the stack.
/// </summary>
/// <remarks>
/// If _getter is null - assume target is already on the stack.
/// In this case, we can just call Emit. Unless _emitSourceInfo is true.
///
/// If _emitSourceInfo is true - we need to make sure that target and key are on the stack for XamlSourceInfo setting,
/// so we need to store parameters to locals first regardless.
/// </remarks>
public void Emit(IXamlILEmitter emitter)
{
using var keyLocal = emitter.LocalsPool.GetLocal(Parameters[0]);
if (_getter is not null || _emitSourceInfo)
{
var locals = new Stack<XamlLocalsPool.PooledLocal>();
// Save all "setter" parameters
for (var c = Parameters.Count - 1; c >= 0; c--)
{
var loc = emitter.LocalsPool.GetLocal(Parameters[c]);
locals.Push(loc);
emitter.Stloc(loc.Local);
if (c == 0 && _emitSourceInfo)
{
// Store the key argument for XamlSourceInfo later
emitter.Ldloc(loc.Local);
emitter.Stloc(keyLocal.Local);
}
}
if (_getter is not null)
{
emitter.EmitCall(_getter);
}
// Duplicate the target object on stack for setting XamlSourceInfo later
emitter.Dup();
while (locals.Count > 0)
using (var loc = locals.Pop())
emitter.Ldloc(loc.Local);
}
emitter.EmitCall(_adder, true);
if (_emitSourceInfo)
{
// Target is already on stack (dup)
// Load the key argument from local
emitter.Ldloc(keyLocal.Local);
EmitSetSourceInfo(emitter);
}
}
/// <summary>
/// Emits the setter with provided arguments that are not yet on the stack.
/// </summary>
/// <remarks>
/// If _getter is null - assume target is already on the stack.
/// If _emitSourceInfo is true - we need to make sure that target and key are on the stack for XamlSourceInfo setting.
/// </remarks>
public void EmitWithArguments(
XamlEmitContextWithLocals<IXamlILEmitter, XamlILNodeEmitResult> context,
IXamlILEmitter emitter,
IReadOnlyList<IXamlAstValueNode> arguments)
{
using var keyLocal = _emitSourceInfo ? emitter.LocalsPool.GetLocal(Parameters[0]) : null;
if (_getter is not null)
{
emitter.EmitCall(_getter);
}
if (_emitSourceInfo)
{
// Duplicate the target object on stack for setting XamlSourceInfo later
emitter.Dup();
}
for (var i = 0; i < arguments.Count; ++i)
{
context.Emit(arguments[i], emitter, Parameters[i]);
// Store the key argument for XamlSourceInfo later
if (i == 0 && _emitSourceInfo)
{
emitter.Stloc(keyLocal!.Local);
emitter.Ldloc(keyLocal.Local);
}
}
emitter.EmitCall(_adder, true);
if (_emitSourceInfo)
{
// Target is already on stack (dub)
// Load the key argument from local
emitter.Ldloc(keyLocal!.Local);
EmitSetSourceInfo(emitter);
}
}
private void EmitSetSourceInfo(IXamlILEmitter emitter)
{
// Assumes the target object and key are already on the stack
emitter.Ldc_I4(_line);
emitter.Ldc_I4(_position);
if (_document is not null)
emitter.Ldstr(_document);
else
emitter.Ldnull();
emitter.Newobj(_avaloniaTypes.XamlSourceInfoConstructor);
// Set the XamlSourceInfo property on the current object
// XamlSourceInfo.SetXamlSourceInfo(@this, key, info);
emitter.EmitCall(_avaloniaTypes.XamlSourceInfoDictionarySetter);
}
public bool Equals(AdderSetter? other)
{
if (ReferenceEquals(null, other))
return false;
if (ReferenceEquals(this, other))
return true;
return _getter?.Equals(other._getter) == true && _adder.Equals(other._adder);
}
public override bool Equals(object? obj)
=> Equals(obj as AdderSetter);
public override int GetHashCode()
=> (_getter, _adder).GetHashCode();
}
}
}

12
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs

@ -134,6 +134,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
public IXamlType ControlTemplate { get; } public IXamlType ControlTemplate { get; }
public IXamlType EventHandlerT { get; } public IXamlType EventHandlerT { get; }
public IXamlMethod GetClassProperty { get; } public IXamlMethod GetClassProperty { get; }
public IXamlConstructor XamlSourceInfoConstructor { get; }
public IXamlMethod XamlSourceInfoSetter { get; }
public IXamlMethod XamlSourceInfoDictionarySetter { get; }
sealed internal class InteractivityWellKnownTypes sealed internal class InteractivityWellKnownTypes
{ {
@ -343,6 +346,15 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
allowDowncast:false, allowDowncast:false,
cfg.WellKnownTypes.String cfg.WellKnownTypes.String
); );
var xamlSourceInfo = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.Diagnostics.XamlSourceInfo");
XamlSourceInfoConstructor = xamlSourceInfo.GetConstructor([
XamlIlTypes.Int32, XamlIlTypes.Int32, XamlIlTypes.String
]);
XamlSourceInfoSetter =
xamlSourceInfo.GetMethod("SetXamlSourceInfo", XamlIlTypes.Void, false, XamlIlTypes.Object, xamlSourceInfo);
XamlSourceInfoDictionarySetter =
xamlSourceInfo.GetMethod("SetXamlSourceInfo", XamlIlTypes.Void, false, IResourceDictionary, XamlIlTypes.Object, xamlSourceInfo);
} }
} }

26
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlAstNewClrObjectHelper.cs

@ -0,0 +1,26 @@
using XamlX.Ast;
namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions;
internal static class XamlAstNewClrObjectHelper
{
/// <summary>
/// Tries to resolve the underlying value of a <see cref="XamlValueWithManipulationNode"/>,
/// unwrapping any nested <see cref="XamlValueWithManipulationNode"/> instances.
/// </summary>
public static TXamlAstValueNode? UnwrapValue<TXamlAstValueNode>(this XamlValueWithManipulationNode node)
where TXamlAstValueNode : class, IXamlAstValueNode
{
var current = node.Value;
while (current is XamlValueWithManipulationNode valueWithManipulation)
{
current = valueWithManipulation.Value;
if (current is TXamlAstValueNode typedValue)
{
return typedValue;
}
}
return current as TXamlAstValueNode;
}
}

1
src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj

@ -38,6 +38,7 @@
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="RuntimeXamlLoaderConfiguration.cs" /> <Compile Include="RuntimeXamlLoaderConfiguration.cs" />
<Compile Include="RuntimeXamlLoaderDocument.cs" /> <Compile Include="RuntimeXamlLoaderDocument.cs" />
<Compile Include="Diagnostics\XamlSourceInfo.cs" />
<Compile Include="Styling\MergeResourceInclude.cs" /> <Compile Include="Styling\MergeResourceInclude.cs" />
<Compile Include="Styling\ResourceInclude.cs" /> <Compile Include="Styling\ResourceInclude.cs" />
<Compile Include="Styling\StyleInclude.cs" /> <Compile Include="Styling\StyleInclude.cs" />

151
src/Markup/Avalonia.Markup.Xaml/Diagnostics/XamlSourceInfo.cs

@ -0,0 +1,151 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Avalonia.Controls;
namespace Avalonia.Markup.Xaml.Diagnostics
{
/// <summary>
/// Represents source location information for an element within a XAML or code file.
/// </summary>
// ReSharper disable once ClassNeverInstantiated.Global //This class is instantiated through the XAML compiler.
public record XamlSourceInfo
{
private static readonly AttachedProperty<XamlSourceInfo?> s_xamlSourceInfo =
AvaloniaProperty.RegisterAttached<AvaloniaObject, XamlSourceInfo?>(
"XamlSourceInfo", typeof(XamlSourceInfo));
private static readonly ConditionalWeakTable<object, XamlSourceInfo?> s_sourceInfo = [];
private static readonly ConditionalWeakTable<IResourceDictionary, Dictionary<object, XamlSourceInfo?>> s_keyedSourceInfo = [];
/// <summary>
/// Gets the full path of the source file containing the element, or <c>null</c> if unavailable.
/// </summary>
public Uri? SourceUri { get; }
/// <summary>
/// Gets the 1-based line number in the source file where the element is defined.
/// </summary>
public int LineNumber { get; }
/// <summary>
/// Gets the 1-based column number in the source file where the element is defined.
/// </summary>
public int LinePosition { get; }
/// <summary>
/// Initializes a new instance of the <see cref="XamlSourceInfo"/> class
/// with a specified line, column, and file path.
/// </summary>
/// <param name="line">The line number of the source element.</param>
/// <param name="column">The column number of the source element.</param>
/// <param name="filePath">The full path of the source file.</param>
public XamlSourceInfo(int line, int column, string? filePath)
{
LineNumber = line;
LinePosition = column;
SourceUri = filePath is not null ? new UriBuilder("file", "") { Path = filePath }.Uri : null;
}
/// <summary>
/// Associates XAML source information with the specified object for debugging or diagnostic purposes.
/// </summary>
/// <remarks>This method is typically used to enable enhanced debugging or diagnostics by tracking
/// the origin of XAML elements at runtime. If the same object is passed multiple times, the most recent source
/// information will overwrite any previous value.</remarks>
/// <param name="obj">The object to associate with the XAML source information. Cannot be null.</param>
/// <param name="info">The XAML source information to associate with the object, or null to remove any existing association.</param>
public static void SetXamlSourceInfo(object obj, XamlSourceInfo? info)
{
if (obj is null)
throw new ArgumentNullException(nameof(obj));
if (obj is AvaloniaObject avaloniaObject)
{
avaloniaObject.SetValue(s_xamlSourceInfo, info);
}
else
{
s_sourceInfo.AddOrUpdate(obj, info);
}
}
/// <summary>
/// Associates XAML source information with the specified key in the given resource dictionary.
/// </summary>
/// <param name="dictionary"> The resource dictionary to associate with the XAML source information.</param>
/// <param name="key">The key associated with the source info.</param>
/// <param name="info">The XAML source information to associate with the object, or null to remove any existing association.</param>
public static void SetXamlSourceInfo(IResourceDictionary dictionary, object key, XamlSourceInfo? info)
{
if (dictionary is null)
throw new ArgumentNullException(nameof(dictionary));
var dict = s_keyedSourceInfo.GetOrCreateValue(dictionary);
if (info == null)
{
_ = dict.Remove(key);
}
else
{
dict[key] = info;
}
}
/// <summary>
/// Retrieves the XAML source information associated with the specified object, if available.
/// </summary>
/// <param name="obj">The object for which to obtain XAML source information. Cannot be null.</param>
/// <returns>A <see cref="XamlSourceInfo"/> instance containing the XAML source information for the specified object, or
/// <see langword="null"/> if no source information is available.</returns>
public static XamlSourceInfo? GetXamlSourceInfo(object obj)
{
if (obj is null)
throw new ArgumentNullException(nameof(obj));
if (obj is AvaloniaObject avaloniaObject)
{
return avaloniaObject.GetValue(s_xamlSourceInfo);
}
else
{
s_sourceInfo.TryGetValue(obj, out var info);
return info;
}
}
/// <summary>
/// Retrieves the XAML source information associated with the specified key in the given resource dictionary, if available.
/// </summary>
/// <param name="dictionary"> The resource dictionary associated with the XAML source information.</param>
/// <param name="key">The key associated with the source info.</param>
/// <returns>A <see cref="XamlSourceInfo"/> instance containing the XAML source information for the specified key, or
/// <see langword="null"/> if no source information is available.</returns>
public static XamlSourceInfo? GetXamlSourceInfo(IResourceDictionary dictionary, object key)
{
if (dictionary is null)
throw new ArgumentNullException(nameof(dictionary));
if (s_keyedSourceInfo.TryGetValue(dictionary, out var dict)
&& dict.TryGetValue(key, out var info))
{
return info;
}
return null;
}
/// <summary>
/// Returns a string that represents the current <see cref="XamlSourceInfo"/>.
/// </summary>
/// <returns>
/// A formatted string in the form <c>"FilePath:Line,Column"</c>,
/// or <c>"(unknown):Line,Column"</c> if the file path is not set.
/// </returns>
public override string ToString()
{
var filePath = SourceUri?.LocalPath ?? "(unknown)";
return $"{filePath}:{LineNumber},{LinePosition}";
}
}
}

6
src/Markup/Avalonia.Markup.Xaml/RuntimeXamlLoaderConfiguration.cs

@ -22,6 +22,12 @@ public class RuntimeXamlLoaderConfiguration
/// </summary> /// </summary>
public bool DesignMode { get; set; } = false; public bool DesignMode { get; set; } = false;
/// <summary>
/// When enabled, the XAML compiler embeds SourceInfo metadata (file path, line, and column) into generated code.
/// Default is 'false'.
/// </summary>
public bool CreateSourceInfo { get; set; } = false;
/// <summary> /// <summary>
/// XAML diagnostics handler. /// XAML diagnostics handler.
/// </summary> /// </summary>

5
src/Markup/Avalonia.Markup.Xaml/RuntimeXamlLoaderDocument.cs

@ -58,6 +58,11 @@ public class RuntimeXamlLoaderDocument
/// </summary> /// </summary>
public Uri? BaseUri { get; set; } public Uri? BaseUri { get; set; }
/// <summary>
/// Path to the XAML document being loaded.
/// </summary>
public string? Document { get; set; }
/// <summary> /// <summary>
/// The optional instance into which the XAML should be loaded. /// The optional instance into which the XAML should be loaded.
/// </summary> /// </summary>

10
tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/ResourceIncludeTests.cs

@ -12,8 +12,10 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
{ {
public class ResourceIncludeTests : XamlTestBase public class ResourceIncludeTests : XamlTestBase
{ {
[Fact] [Theory]
public void ResourceInclude_Loads_ResourceDictionary() [InlineData(false)]
[InlineData(true)]
public void ResourceInclude_Loads_ResourceDictionary(bool createSourceInfo)
{ {
var documents = new[] var documents = new[]
{ {
@ -37,9 +39,11 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
</UserControl>") </UserControl>")
}; };
var config = new RuntimeXamlLoaderConfiguration { CreateSourceInfo = createSourceInfo };
using (StartWithResources()) using (StartWithResources())
{ {
var compiled = AvaloniaRuntimeXamlLoader.LoadGroup(documents); var compiled = AvaloniaRuntimeXamlLoader.LoadGroup(documents, config);
var userControl = Assert.IsType<UserControl>(compiled[1]); var userControl = Assert.IsType<UserControl>(compiled[1]);
var border = userControl.GetControl<Border>("border"); var border = userControl.GetControl<Border>("border");

9
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/MergeResourceIncludeTests.cs

@ -18,8 +18,10 @@ public class MergeResourceIncludeTests : XamlTestBase
RuntimeHelpers.RunClassConstructor(typeof(RelativeSource).TypeHandle); RuntimeHelpers.RunClassConstructor(typeof(RelativeSource).TypeHandle);
} }
[Fact] [Theory]
public void MergeResourceInclude_Works_With_Single_Resource() [InlineData(false)]
[InlineData(true)]
public void MergeResourceInclude_Works_With_Single_Resource(bool createSourceInfo)
{ {
var documents = new[] var documents = new[]
{ {
@ -42,7 +44,8 @@ public class MergeResourceIncludeTests : XamlTestBase
</UserControl>") </UserControl>")
}; };
var objects = AvaloniaRuntimeXamlLoader.LoadGroup(documents); var config = new RuntimeXamlLoaderConfiguration { CreateSourceInfo = createSourceInfo };
var objects = AvaloniaRuntimeXamlLoader.LoadGroup(documents, config);
var contentControl = Assert.IsType<UserControl>(objects[1]); var contentControl = Assert.IsType<UserControl>(objects[1]);
var resources = Assert.IsType<ResourceDictionary>(contentControl.Resources); var resources = Assert.IsType<ResourceDictionary>(contentControl.Resources);

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

@ -265,6 +265,8 @@ public class StyleIncludeTests : XamlTestBase
[Fact] [Fact]
public void StyleInclude_Should_Be_Replaced_With_Direct_Call() public void StyleInclude_Should_Be_Replaced_With_Direct_Call()
{ {
using var _ = UnitTestApplication.Start(TestServices.StyledWindow);
var control = (ContentControl)AvaloniaRuntimeXamlLoader.Load(@" var control = (ContentControl)AvaloniaRuntimeXamlLoader.Load(@"
<ContentControl xmlns='https://github.com/avaloniaui' <ContentControl xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml' xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
@ -281,6 +283,8 @@ public class StyleIncludeTests : XamlTestBase
[Fact] [Fact]
public void Style_Inside_Resources_Should_Produce_Warning() public void Style_Inside_Resources_Should_Produce_Warning()
{ {
using var _ = UnitTestApplication.Start(TestServices.StyledWindow);
var diagnostics = new List<RuntimeXamlDiagnostic>(); var diagnostics = new List<RuntimeXamlDiagnostic>();
var control = (ContentControl)AvaloniaRuntimeXamlLoader.Load(new RuntimeXamlLoaderDocument(@" var control = (ContentControl)AvaloniaRuntimeXamlLoader.Load(new RuntimeXamlLoaderDocument(@"
<ContentControl xmlns='https://github.com/avaloniaui' <ContentControl xmlns='https://github.com/avaloniaui'

583
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlSourceInfoTests.cs

@ -0,0 +1,583 @@
using System;
using System.Linq;
using Avalonia.Animation;
using Avalonia.Controls;
using Avalonia.Controls.Shapes;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Input.GestureRecognizers;
using Avalonia.Markup.Xaml.Diagnostics;
using Avalonia.Markup.Xaml.Templates;
using Avalonia.Media;
using Avalonia.Styling;
using Xunit;
namespace Avalonia.Markup.Xaml.UnitTests.Xaml
{
public class XamlSourceInfoTests : XamlTestBase
{
private static readonly RuntimeXamlLoaderConfiguration s_configuration = new RuntimeXamlLoaderConfiguration
{
CreateSourceInfo = true
};
[Theory]
[InlineData(@"C:\TestFolder\TestFile.xaml")] // Windows-style path
[InlineData("/TestFolder/TestFile.xaml")] // Unix-style path
public void Root_UserControl_With_BaseUri_Gets_XamlSourceInfo_SourceUri_Set(string document)
{
var xamlDocument = new RuntimeXamlLoaderDocument(
"""
<UserControl xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
</UserControl>
""")
{
Document = document
};
var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xamlDocument, s_configuration);
var sourceInfo = XamlSourceInfo.GetXamlSourceInfo(userControl);
Assert.NotNull(sourceInfo);
Assert.Equal("file", sourceInfo.SourceUri!.Scheme);
Assert.True(sourceInfo.SourceUri!.IsAbsoluteUri);
Assert.Equal(new UriBuilder("file", "") {Path = document}.Uri, sourceInfo.SourceUri);
}
[Fact]
public void Root_UserControl_Gets_XamlSourceInfo_Set()
{
var xaml = new RuntimeXamlLoaderDocument(@"
<UserControl xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
</UserControl>");
var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml, s_configuration);
var sourceInfo = XamlSourceInfo.GetXamlSourceInfo(userControl);
Assert.NotNull(sourceInfo);
}
[Fact]
public void Nested_Controls_All_Get_XamlSourceInfo_Set()
{
var xaml = new RuntimeXamlLoaderDocument(@"
<UserControl xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<StackPanel>
<Button />
<TextBlock />
</StackPanel>
</UserControl>");
var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml, s_configuration);
var stackPanel = (StackPanel)userControl.Content!;
var button = (Button)stackPanel.Children[0];
var textblock = (TextBlock)stackPanel.Children[1];
var userControlSourceInfo = XamlSourceInfo.GetXamlSourceInfo(userControl);
Assert.NotNull(userControlSourceInfo);
var stackPanelSourceInfo = XamlSourceInfo.GetXamlSourceInfo(stackPanel);
Assert.NotNull(stackPanelSourceInfo);
var buttonSourceInfo = XamlSourceInfo.GetXamlSourceInfo(button);
Assert.NotNull(buttonSourceInfo);
var textblockSourceInfo = XamlSourceInfo.GetXamlSourceInfo(textblock);
Assert.NotNull(textblockSourceInfo);
}
[Fact]
public void Property_Elements_Get_XamlSourceInfo_Set()
{
var xaml = new RuntimeXamlLoaderDocument(@"
<UserControl xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Rectangle Fill=""Blue"" Width=""63"" Height=""41"">
<Rectangle.OpacityMask>
<LinearGradientBrush StartPoint=""0%,0%"" EndPoint=""100%,100%"">
<LinearGradientBrush.GradientStops>
<GradientStop Offset=""0"" Color=""Black""/>
<GradientStop Offset=""1"" Color=""Transparent""/>
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Rectangle.OpacityMask>
</Rectangle>
</UserControl>");
var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml, s_configuration);
var rect = (Rectangle)userControl.Content!;
var gradient = (LinearGradientBrush)rect.OpacityMask!;
var stopOne = (GradientStop)gradient.GradientStops.First();
var stopTwo = (GradientStop)gradient.GradientStops.Last();
var rectSourceInfo = XamlSourceInfo.GetXamlSourceInfo(rect);
Assert.NotNull(rectSourceInfo);
var gradientSourceInfo = XamlSourceInfo.GetXamlSourceInfo(gradient);
Assert.NotNull(gradientSourceInfo);
var stopOneSourceInfo = XamlSourceInfo.GetXamlSourceInfo(stopOne);
Assert.NotNull(stopOneSourceInfo);
var stopTwoSourceInfo = XamlSourceInfo.GetXamlSourceInfo(stopTwo);
Assert.NotNull(stopTwoSourceInfo);
}
[Fact]
public void Shapes_Get_XamlSourceInfo_Set()
{
var xaml = new RuntimeXamlLoaderDocument(@"
<UserControl xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Canvas Name=""TheCanvas"" Background=""Yellow"" Width=""300"" Height=""400"">
<Ellipse Fill=""Green"" Width=""58"" Height=""58"" Canvas.Left=""88"" Canvas.Top=""100""/>
<Path Fill=""Orange"" Canvas.Left=""30"" Canvas.Top=""250""/>
<Path Fill=""OrangeRed"" Canvas.Left=""180"" Canvas.Top=""250"">
<Path.Data>
<PathGeometry>
<PathFigure StartPoint=""0,0"" IsClosed=""True"">
<QuadraticBezierSegment Point1=""50,0"" Point2=""50,-50"" />
<QuadraticBezierSegment Point1=""100,-50"" Point2=""100,0"" />
<LineSegment Point=""50,0"" />
<LineSegment Point=""50,50"" />
</PathFigure>
</PathGeometry>
</Path.Data>
</Path>
<Line StartPoint=""120,185"" EndPoint=""30,115"" Stroke=""Red"" StrokeThickness=""2""/>
<Polygon Points=""75,0 120,120 0,45 150,45 30,120"" Stroke=""DarkBlue"" StrokeThickness=""1"" Fill=""Violet"" Canvas.Left=""150"" Canvas.Top=""31""/>
</Canvas>
</UserControl>");
var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml, s_configuration);
var canvas = (Canvas)userControl.Content!;
var ellipse = (Ellipse)canvas.Children[0];
var path1 = (Path)canvas.Children[1];
var path2 = (Path)canvas.Children[2];
var geometry = (PathGeometry)path2.Data!;
var figure = (PathFigure)geometry.Figures![0];
var segment1 = figure.Segments![0];
var segment2 = figure.Segments![1];
var segment3 = figure.Segments![2];
var segment4 = figure.Segments![3];
var line = (Line)canvas.Children[3];
var polygon = (Polygon)canvas.Children[4];
var canvasSourceInfo = XamlSourceInfo.GetXamlSourceInfo(canvas);
Assert.NotNull(canvasSourceInfo);
var ellipseSourceInfo = XamlSourceInfo.GetXamlSourceInfo(ellipse);
Assert.NotNull(ellipseSourceInfo);
var path1SourceInfo = XamlSourceInfo.GetXamlSourceInfo(path1);
Assert.NotNull(path1SourceInfo);
var path2SourceInfo = XamlSourceInfo.GetXamlSourceInfo(path2);
Assert.NotNull(path2SourceInfo);
var geometrySourceInfo = XamlSourceInfo.GetXamlSourceInfo(geometry);
Assert.NotNull(geometrySourceInfo);
var figureSourceInfo = XamlSourceInfo.GetXamlSourceInfo(figure);
Assert.NotNull(figureSourceInfo);
var segment1SourceInfo = XamlSourceInfo.GetXamlSourceInfo(segment1);
Assert.NotNull(segment1SourceInfo);
var segment2SourceInfo = XamlSourceInfo.GetXamlSourceInfo(segment2);
Assert.NotNull(segment2SourceInfo);
var segment3SourceInfo = XamlSourceInfo.GetXamlSourceInfo(segment3);
Assert.NotNull(segment3SourceInfo);
var segment4SourceInfo = XamlSourceInfo.GetXamlSourceInfo(segment4);
Assert.NotNull(segment4SourceInfo);
var lineSourceInfo = XamlSourceInfo.GetXamlSourceInfo(line);
Assert.NotNull(lineSourceInfo);
var polygonSourceInfo = XamlSourceInfo.GetXamlSourceInfo(polygon);
Assert.NotNull(polygonSourceInfo);
}
[Fact]
public void Styles_Get_XamlSourceInfo_Set()
{
var xaml = new RuntimeXamlLoaderDocument(@"
<UserControl xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<UserControl.Styles>
<Style Selector=""Button"">
<Setter Property=""Margin"" Value=""5"" />
</Style>
<ContainerQuery Name=""container""
Query=""max-width:400"">
<Style Selector=""Button"">
<Setter Property=""Background""
Value=""Red""/>
</Style>
</ContainerQuery>
</UserControl.Styles>
</UserControl>");
var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml, s_configuration);
var style = (Style)userControl.Styles[0];
var query = (ContainerQuery)userControl.Styles[1];
var styleSourceInfo = XamlSourceInfo.GetXamlSourceInfo(style);
Assert.NotNull(styleSourceInfo);
var querySourceInfo = XamlSourceInfo.GetXamlSourceInfo(query);
Assert.NotNull(querySourceInfo);
}
[Fact]
public void Animations_Get_XamlSourceInfo_Set()
{
var xaml = new RuntimeXamlLoaderDocument(@"
<UserControl xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<UserControl.Styles>
<Style Selector=""Rectangle.red"">
<Setter Property=""Fill"" Value=""Red""/>
<Style.Animations>
<Animation Duration=""0:0:3"">
<KeyFrame Cue=""0%"">
<Setter Property=""Opacity"" Value=""0.0""/>
</KeyFrame>
<KeyFrame Cue=""100%"">
<Setter Property=""Opacity"" Value=""1.0""/>
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
</UserControl.Styles>
</UserControl>");
var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml, s_configuration);
var style = (Style)userControl.Styles[0];
var animation = (Animation.Animation)style.Animations[0];
var frame1 = animation.Children[0];
var frame2 = animation.Children[1];
var styleSourceInfo = XamlSourceInfo.GetXamlSourceInfo(style);
Assert.NotNull(styleSourceInfo);
var animationSourceInfo = XamlSourceInfo.GetXamlSourceInfo(animation);
Assert.NotNull(animationSourceInfo);
var frameOneSourceInfo = XamlSourceInfo.GetXamlSourceInfo(frame1);
Assert.NotNull(frameOneSourceInfo);
var frameTwoSourceInfo = XamlSourceInfo.GetXamlSourceInfo(frame2);
Assert.NotNull(frameTwoSourceInfo);
}
[Fact]
public void DataTemplates_And_Deferred_Contents_Get_XamlSourceInfo_Set()
{
var xaml = new RuntimeXamlLoaderDocument(@"
<UserControl xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='using:Avalonia.Markup.Xaml.UnitTests.Xaml'>
<UserControl.DataTemplates>
<DataTemplate DataType=""local:SourceInfoTestViewModel"">
<Border Background=""Red"" CornerRadius=""8"">
<TextBox Text=""{Binding Name}""/>
</Border>
</DataTemplate>
</UserControl.DataTemplates>
</UserControl>");
var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml, s_configuration);
var datatemplate = (DataTemplate)userControl.DataTemplates[0];
Border border;
// The template and it's content is deferred as not used (yet)
if (datatemplate.Content is IDeferredContent deferredContent)
{
var templateResult = (ITemplateResult)deferredContent.Build(null)!;
border = (Border)templateResult.Result!;
}
else
{
border = (Border)datatemplate.Content!;
}
var textBox = (TextBox)border!.Child!;
var datatemplateSourceInfo = XamlSourceInfo.GetXamlSourceInfo(datatemplate);
Assert.NotNull(datatemplateSourceInfo);
var borderSourceInfo = XamlSourceInfo.GetXamlSourceInfo(border);
Assert.NotNull(borderSourceInfo);
var textBoxSourceInfo = XamlSourceInfo.GetXamlSourceInfo(textBox);
Assert.NotNull(textBoxSourceInfo);
}
[Fact]
public void Resources_Get_XamlSourceInfo_Set()
{
var xaml = new RuntimeXamlLoaderDocument(@"
<UserControl xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key='Light'>
<SolidColorBrush x:Key='BackgroundBrush' Color='White'/>
<SolidColorBrush x:Key='ForegroundBrush' Color='Black'/>
</ResourceDictionary>
<ResourceDictionary x:Key='Dark'>
<SolidColorBrush x:Key='BackgroundBrush' Color='Black'/>
<SolidColorBrush x:Key='ForegroundBrush' Color='White'/>
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
<SolidColorBrush x:Key=""Background"" Color=""Yellow"" />
<SolidColorBrush x:Key='OtherBrush'>Black</SolidColorBrush>
</ResourceDictionary>
</UserControl.Resources>
</UserControl>");
var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml, s_configuration);
var backgroundBrush = userControl.Resources["Background"];
var lightDictionary = (ResourceDictionary)userControl.Resources.ThemeDictionaries[ThemeVariant.Light];
var darkDictionary = (ResourceDictionary)userControl.Resources.ThemeDictionaries[ThemeVariant.Dark];
var lightForeground = lightDictionary["ForegroundBrush"];
var darkBackground = lightDictionary["BackgroundBrush"];
var otherBrush = userControl.Resources["OtherBrush"];
var backgroundBrushSourceInfo = XamlSourceInfo.GetXamlSourceInfo(backgroundBrush!);
Assert.NotNull(backgroundBrushSourceInfo);
var lightDictionarySourceInfo = XamlSourceInfo.GetXamlSourceInfo(lightDictionary!);
Assert.NotNull(lightDictionarySourceInfo);
var darkDictionarySourceInfo = XamlSourceInfo.GetXamlSourceInfo(darkDictionary!);
Assert.NotNull(darkDictionarySourceInfo);
var lightForegroundSourceInfo = XamlSourceInfo.GetXamlSourceInfo(lightForeground!);
Assert.NotNull(lightForegroundSourceInfo);
var darkBackgroundSourceInfo = XamlSourceInfo.GetXamlSourceInfo(darkBackground!);
Assert.NotNull(darkBackgroundSourceInfo);
var otherBrushSourceInfo = XamlSourceInfo.GetXamlSourceInfo(otherBrush!);
Assert.NotNull(otherBrushSourceInfo);
}
[Fact]
public void ResourceDictionary_Value_Types_Do_Not_Set_XamlSourceInfo()
{
var xaml = new RuntimeXamlLoaderDocument(@"
<UserControl xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<UserControl.Resources>
<x:String x:Key='text'>foobar</x:String>
<x:Double x:Key=""A_Double"">123.3</x:Double>
<x:Int16 x:Key=""An_Int16"">123</x:Int16>
<x:Int32 x:Key=""An_Int32"">37434323</x:Int32>
<Thickness x:Key=""PreferredPadding"">10,20,10,0</Thickness>
</UserControl.Resources>
</UserControl>");
var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml, s_configuration);
var foobarString = userControl.Resources["text"];
var aDouble = userControl.Resources["A_Double"];
var anInt16 = userControl.Resources["An_Int16"];
var anInt32 = userControl.Resources["An_Int32"];
var padding = userControl.Resources["PreferredPadding"];
// Value types shouldn't get source info
var foobarStringSourceInfo = XamlSourceInfo.GetXamlSourceInfo(foobarString!);
Assert.Null(foobarStringSourceInfo);
var aDoubleSourceInfo = XamlSourceInfo.GetXamlSourceInfo(aDouble!);
Assert.Null(aDoubleSourceInfo);
var anInt16SourceInfo = XamlSourceInfo.GetXamlSourceInfo(anInt16!);
Assert.Null(anInt16SourceInfo);
var anInt32SourceInfo = XamlSourceInfo.GetXamlSourceInfo(anInt32!);
Assert.Null(anInt32SourceInfo);
var paddingSourceInfo = XamlSourceInfo.GetXamlSourceInfo(padding!);
Assert.Null(paddingSourceInfo);
}
[Fact]
public void ResourceDictionary_Set_Resource_Source_Info()
{
var xaml = new RuntimeXamlLoaderDocument(@"
<UserControl xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<UserControl.Resources>
<x:String x:Key='text'>foobar</x:String>
<x:Double x:Key=""A_Double"">123.3</x:Double>
<x:Int16 x:Key=""An_Int16"">123</x:Int16>
<x:Int32 x:Key=""An_Int32"">37434323</x:Int32>
<Thickness x:Key=""PreferredPadding"">10,20,10,0</Thickness>
<x:Uri x:Key='homepage'>http://avaloniaui.net</x:Uri>
<SolidColorBrush x:Key='MyBrush' Color='Red'/>
</UserControl.Resources>
</UserControl>");
var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml, s_configuration);
var resources = userControl.Resources;
var foobarStringSourceInfo = XamlSourceInfo.GetXamlSourceInfo(resources, "text");
Assert.NotNull(foobarStringSourceInfo);
var aDoubleSourceInfo = XamlSourceInfo.GetXamlSourceInfo(resources, "A_Double");
Assert.NotNull(aDoubleSourceInfo);
var anInt16SourceInfo = XamlSourceInfo.GetXamlSourceInfo(resources, "An_Int16");
Assert.NotNull(anInt16SourceInfo);
var anInt32SourceInfo = XamlSourceInfo.GetXamlSourceInfo(resources, "An_Int32");
Assert.NotNull(anInt32SourceInfo);
var paddingSourceInfo = XamlSourceInfo.GetXamlSourceInfo(resources, "PreferredPadding");
Assert.NotNull(paddingSourceInfo);
var homepageSourceInfo = XamlSourceInfo.GetXamlSourceInfo(resources, "homepage");
Assert.NotNull(homepageSourceInfo);
var myBrushSourceInfo = XamlSourceInfo.GetXamlSourceInfo(resources, "MyBrush");
Assert.NotNull(myBrushSourceInfo);
}
[Fact]
public void ResourceDictionary_Set_Resource_Source_Info_With_Nested_Dictionaries()
{
var xaml = new RuntimeXamlLoaderDocument(@"
<UserControl xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<UserControl.Resources>
<ResourceDictionary>
<x:String x:Key='text'>foobar</x:String>
<x:Double x:Key=""A_Double"">123.3</x:Double>
<x:Int16 x:Key=""An_Int16"">123</x:Int16>
<x:Int32 x:Key=""An_Int32"">37434323</x:Int32>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary>
<Thickness x:Key=""PreferredPadding"">10,20,10,0</Thickness>
<x:Uri x:Key='homepage'>http://avaloniaui.net</x:Uri>
</ResourceDictionary>
</ResourceDictionary.MergedDictionaries>
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key='Light'>
<SolidColorBrush x:Key='MyBrush' Color='Red'/>
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
</ResourceDictionary>
</UserControl.Resources>
</UserControl>");
var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml, s_configuration);
var resources = userControl.Resources;
var innerResources = (IResourceDictionary)resources.MergedDictionaries[0];
var themeResources = (IResourceDictionary)resources.ThemeDictionaries[ThemeVariant.Light];
// Outer define source info
var foobarStringSourceInfo = XamlSourceInfo.GetXamlSourceInfo(resources, "text");
Assert.NotNull(foobarStringSourceInfo);
var aDoubleSourceInfo = XamlSourceInfo.GetXamlSourceInfo(resources, "A_Double");
Assert.NotNull(aDoubleSourceInfo);
var anInt16SourceInfo = XamlSourceInfo.GetXamlSourceInfo(resources, "An_Int16");
Assert.NotNull(anInt16SourceInfo);
var anInt32SourceInfo = XamlSourceInfo.GetXamlSourceInfo(resources, "An_Int32");
Assert.NotNull(anInt32SourceInfo);
// Outer one should not have source info for inner resources
var paddingSourceInfo = XamlSourceInfo.GetXamlSourceInfo(resources, "PreferredPadding");
Assert.Null(paddingSourceInfo);
var homepageSourceInfo = XamlSourceInfo.GetXamlSourceInfo(resources, "homepage");
Assert.Null(homepageSourceInfo);
var myBrushSourceInfo = XamlSourceInfo.GetXamlSourceInfo(resources, "MyBrush");
Assert.Null(myBrushSourceInfo);
// Inner defined source info
homepageSourceInfo = XamlSourceInfo.GetXamlSourceInfo(innerResources, "homepage");
Assert.NotNull(homepageSourceInfo);
myBrushSourceInfo = XamlSourceInfo.GetXamlSourceInfo(themeResources, "MyBrush");
Assert.NotNull(myBrushSourceInfo);
// Non-value types should have source info themselves
var homepage = XamlSourceInfo.GetXamlSourceInfo(innerResources["homepage"]!);
Assert.NotNull(homepage);
var myBrush = XamlSourceInfo.GetXamlSourceInfo(themeResources["MyBrush"]!);
Assert.NotNull(myBrush);
}
[Fact]
public void Gestures_Get_XamlSourceInfo_Set()
{
var xaml = new RuntimeXamlLoaderDocument(@"
<UserControl xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<UserControl.GestureRecognizers>
<ScrollGestureRecognizer CanHorizontallyScroll=""True""
CanVerticallyScroll=""True""/>
<PullGestureRecognizer PullDirection=""TopToBottom""/>
</UserControl.GestureRecognizers>
</UserControl>");
var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml, s_configuration);
var scroll = (ScrollGestureRecognizer)userControl.GestureRecognizers.First();
var pull = (PullGestureRecognizer)userControl.GestureRecognizers.Last();
var scrollSourceInfo = XamlSourceInfo.GetXamlSourceInfo(scroll);
Assert.NotNull(scrollSourceInfo);
var pullSourceInfo = XamlSourceInfo.GetXamlSourceInfo(pull);
Assert.NotNull(pullSourceInfo);
}
[Fact]
public void Transitions_Get_XamlSourceInfo_Set()
{
var xaml = new RuntimeXamlLoaderDocument(@"
<UserControl xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<UserControl.Transitions>
<Transitions>
<DoubleTransition Property=""Width"" Duration=""0:0:1.5""/>
<DoubleTransition Property=""Height"" Duration=""0:0:1.5""/>
</Transitions>
</UserControl.Transitions>
</UserControl>");
var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml, s_configuration);
var width = (DoubleTransition)userControl.Transitions!.First();
var height = (DoubleTransition)userControl.Transitions!.Last();
var widthSourceInfo = XamlSourceInfo.GetXamlSourceInfo(width);
Assert.NotNull(widthSourceInfo);
var heightSourceInfo = XamlSourceInfo.GetXamlSourceInfo(height);
Assert.NotNull(heightSourceInfo);
}
}
public class SourceInfoTestViewModel
{
public string? Name { get; set; }
}
}
Loading…
Cancel
Save