Browse Source
* #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
committed by
GitHub
20 changed files with 1272 additions and 215 deletions
@ -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); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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}"; |
|||
} |
|||
} |
|||
} |
|||
@ -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…
Reference in new issue