diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs index 953a7e9a15..15197d39c5 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs @@ -25,6 +25,7 @@ namespace Avalonia.Markup.Parsers Traversal, TypeName, Property, + AttachedProperty, Template, End, } @@ -74,6 +75,9 @@ namespace Avalonia.Markup.Parsers case State.Name: (state, syntax) = ParseName(ref r); break; + case State.AttachedProperty: + (state, syntax) = ParseAttachedProperty(ref r); + break; } if (syntax != null) { @@ -270,11 +274,15 @@ namespace Avalonia.Markup.Parsers return (State.CanHaveType, ParseType(ref r, new OfTypeSyntax())); } - private static (State, ISyntax) ParseProperty(ref CharacterReader r) + private static (State, ISyntax?) ParseProperty(ref CharacterReader r) { var property = r.ParseIdentifier(); - if (!r.TakeIf('=')) + if (r.TakeIf('(')) + { + return (State.AttachedProperty, default); + } + else if (!r.TakeIf('=')) { throw new ExpressionParseException(r.Position, $"Expected '=', got '{r.Peek}'"); } @@ -286,6 +294,42 @@ namespace Avalonia.Markup.Parsers return (State.CanHaveType, new PropertySyntax { Property = property.ToString(), Value = value.ToString() }); } + private static (State, ISyntax) ParseAttachedProperty(ref CharacterReader r) + { + var syntax = ParseType(ref r, new AttachedPropertySyntax()); + if (!r.TakeIf('.')) + { + throw new ExpressionParseException(r.Position, $"Expected '.', got '{r.Peek}'"); + } + var property = r.ParseIdentifier(); + if (property.IsEmpty) + { + throw new ExpressionParseException(r.Position, $"Expected Attached Property Name, got '{r.Peek}'"); + } + syntax.Property = property.ToString(); + + if (!r.TakeIf(')')) + { + throw new ExpressionParseException(r.Position, $"Expected ')', got '{r.Peek}'"); + } + + if (!r.TakeIf('=')) + { + throw new ExpressionParseException(r.Position, $"Expected '=', got '{r.Peek}'"); + } + + var value = r.TakeUntil(']'); + + syntax.Value = value.ToString(); + + r.Take(); + + var state = r.End + ? State.End + : State.Middle; + return (state, syntax); + } + private static TSyntax ParseType(ref CharacterReader r, TSyntax syntax) where TSyntax : ITypeSyntax { @@ -461,6 +505,26 @@ namespace Avalonia.Markup.Parsers } } + public class AttachedPropertySyntax : ISyntax, ITypeSyntax + { + public string Xmlns { get; set; } = string.Empty; + + public string TypeName { get; set; } = string.Empty; + + public string Property { get; set; } = string.Empty; + + public string Value { get; set; } = string.Empty; + + public override bool Equals(object obj) + { + return obj is AttachedPropertySyntax syntax + && syntax.Xmlns == Xmlns + && syntax.TypeName == syntax.TypeName + && syntax.Property == syntax.Property + && syntax.Value == syntax.Value; + } + } + public class IsSyntax : ISyntax, ITypeSyntax { public string TypeName { get; set; } = string.Empty; diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs index 11fb287d46..23bbdb762d 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Globalization; using Avalonia.Styling; using Avalonia.Utilities; +using System.Linq; namespace Avalonia.Markup.Parsers { @@ -75,23 +76,67 @@ namespace Avalonia.Markup.Parsers throw new InvalidOperationException($"Cannot find '{property.Property}' on '{type}"); } + { + object typedValue; + + if (TypeUtilities.TryConvert( + targetProperty.PropertyType, + property.Value, + CultureInfo.InvariantCulture, + out typedValue)) + { + result = result.PropertyEquals(targetProperty, typedValue); + } + else + { + throw new InvalidOperationException( + $"Could not convert '{property.Value}' to '{targetProperty.PropertyType}"); + } + } + break; + } + case SelectorGrammar.AttachedPropertySyntax attachedProperty: + var targetType = result?.TargetType; + + if (targetType == null) + { + throw new InvalidOperationException("Attached Property selectors must be applied to a type."); + } + + var attachedPropertyOwnerType = Resolve(attachedProperty.Xmlns, attachedProperty.TypeName); + + if (attachedPropertyOwnerType is null) + { + throw new InvalidOperationException($"Cannot find '{attachedProperty.Xmlns}:{attachedProperty.TypeName}"); + } + + var targetAttachedProperty = AvaloniaPropertyRegistry.Instance.GetRegisteredAttached(targetType) + .FirstOrDefault(ap => ap.OwnerType == attachedPropertyOwnerType && ap.Name == attachedProperty.Property); + + if (targetAttachedProperty == null) + { + throw new InvalidOperationException($"Cannot find '{attachedProperty.Property}' on '{attachedPropertyOwnerType}"); + } + + { object typedValue; if (TypeUtilities.TryConvert( - targetProperty.PropertyType, - property.Value, + targetAttachedProperty.PropertyType, + attachedProperty.Value, CultureInfo.InvariantCulture, out typedValue)) { - result = result.PropertyEquals(targetProperty, typedValue); + result = result.PropertyEquals(targetAttachedProperty, typedValue); } else { throw new InvalidOperationException( - $"Could not convert '{property.Value}' to '{targetProperty.PropertyType}"); + $"Could not convert '{attachedProperty.Value}' to '{targetAttachedProperty.PropertyType}"); } - break; } + + break; case SelectorGrammar.ChildSyntax child: result = result.Child(); break;