Browse Source

Merge pull request #6992 from workgroupengineering/features/Issue_6984

feat(Style): Allow to using attached properties in attribute selector
repro/minimal-repro-stackoverflow-onewaytosource-binding
Max Katz 4 years ago
committed by Dan Walmsley
parent
commit
763a3e6a29
  1. 5
      src/Avalonia.Styling/Styling/PropertyEqualsSelector.cs
  2. 70
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs
  3. 2
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
  4. 68
      src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs
  5. 55
      src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs
  6. 36
      tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs
  7. 49
      tests/Avalonia.Markup.UnitTests/Parsers/SelectorParserTests.cs
  8. 67
      tests/Avalonia.Styling.UnitTests/SelectorTests_PropertyEquals.cs

5
src/Avalonia.Styling/Styling/PropertyEqualsSelector.cs

@ -51,11 +51,16 @@ namespace Avalonia.Styling
if (_property.IsAttached)
{
builder.Append('(');
builder.Append(_property.OwnerType.Name);
builder.Append('.');
}
builder.Append(_property.Name);
if (_property.IsAttached)
{
builder.Append(')');
}
builder.Append('=');
builder.Append(_value ?? string.Empty);
builder.Append(']');

70
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs

@ -40,6 +40,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
var selectorType = pn.Property.GetClrProperty().Getter.ReturnType;
var initialNode = new XamlIlSelectorInitialNode(node, selectorType);
var avaloniaAttachedPropertyT = context.GetAvaloniaTypes().AvaloniaAttachedPropertyT;
XamlIlSelectorNode Create(IEnumerable<SelectorGrammar.ISyntax> syntax,
Func<string, string, XamlAstClrTypeReference> typeResolver)
{
@ -85,6 +86,47 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
result = new XamlIlPropertyEqualsSelector(result, targetProperty, typedValue);
break;
}
case SelectorGrammar.AttachedPropertySyntax attachedProperty:
{
var targetType = result?.TargetType;
if (targetType == null)
{
throw new XamlParseException("Attached Property selectors must be applied to a type.",node);
}
var attachedPropertyOwnerType = typeResolver(attachedProperty.Xmlns, attachedProperty.TypeName).Type;
if (attachedPropertyOwnerType is null)
{
throw new XamlParseException($"Cannot find '{attachedProperty.Xmlns}:{attachedProperty.TypeName}",node);
}
var attachedPropertyName = attachedProperty.Property + "Property";
var targetPropertyField = attachedPropertyOwnerType.GetAllFields()
.FirstOrDefault(f => f.IsStatic
&& f.IsPublic
&& f.Name == attachedPropertyName
&& f.FieldType.GenericTypeDefinition == avaloniaAttachedPropertyT
);
if (targetPropertyField is null)
{
throw new XamlParseException($"Cannot find '{attachedProperty.Property}' on '{attachedPropertyOwnerType.GetFqn()}", node);
}
var targetPropertyType = XamlIlAvaloniaPropertyHelper
.GetAvaloniaPropertyType(targetPropertyField, context.GetAvaloniaTypes(), node);
if (!XamlTransformHelpers.TryGetCorrectlyTypedValue(context,
new XamlAstTextNode(node, attachedProperty.Value, context.Configuration.WellKnownTypes.String),
targetPropertyType, out var typedValue))
throw new XamlParseException(
$"Cannot convert '{attachedProperty.Value}' to '{targetPropertyType.GetFqn()}",
node);
result = new XamlIlAttacchedPropertyEqualsSelector(result, targetPropertyField, typedValue);
break;
}
case SelectorGrammar.ChildSyntax child:
result = new XamlIlCombinatorSelector(result, XamlIlCombinatorSelector.SelectorType.Child);
break;
@ -338,6 +380,34 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
}
}
class XamlIlAttacchedPropertyEqualsSelector : XamlIlSelectorNode
{
public XamlIlAttacchedPropertyEqualsSelector(XamlIlSelectorNode previous,
IXamlField propertyFiled,
IXamlAstValueNode value)
: base(previous)
{
PropertyFiled = propertyFiled;
Value = value;
}
public IXamlField PropertyFiled { get; set; }
public IXamlAstValueNode Value { get; set; }
public override IXamlType TargetType => Previous?.TargetType;
protected override void DoEmit(XamlEmitContext<IXamlILEmitter, XamlILNodeEmitResult> context, IXamlILEmitter codeGen)
{
codeGen.Ldsfld(PropertyFiled);
context.Emit(Value, codeGen, context.Configuration.WellKnownTypes.Object);
EmitCall(context, codeGen,
m => m.Name == "PropertyEquals"
&& m.Parameters.Count == 3
&& m.Parameters[1].FullName == "Avalonia.AvaloniaProperty"
&& m.Parameters[2].Equals(context.Configuration.WellKnownTypes.Object));
}
}
class XamlIlOrSelectorNode : XamlIlSelectorNode
{
List<XamlIlSelectorNode> _selectors = new List<XamlIlSelectorNode>();

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

@ -15,6 +15,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
public IXamlType AvaloniaObjectExtensions { get; }
public IXamlType AvaloniaProperty { get; }
public IXamlType AvaloniaPropertyT { get; }
public IXamlType AvaloniaAttachedPropertyT { get; }
public IXamlType IBinding { get; }
public IXamlMethod AvaloniaObjectBindMethod { get; }
public IXamlMethod AvaloniaObjectSetValueMethod { get; }
@ -90,6 +91,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
AvaloniaObjectExtensions = cfg.TypeSystem.GetType("Avalonia.AvaloniaObjectExtensions");
AvaloniaProperty = cfg.TypeSystem.GetType("Avalonia.AvaloniaProperty");
AvaloniaPropertyT = cfg.TypeSystem.GetType("Avalonia.AvaloniaProperty`1");
AvaloniaAttachedPropertyT = cfg.TypeSystem.GetType("Avalonia.AttachedProperty`1");
BindingPriority = cfg.TypeSystem.GetType("Avalonia.Data.BindingPriority");
IBinding = cfg.TypeSystem.GetType("Avalonia.Data.IBinding");
IDisposable = cfg.TypeSystem.GetType("System.IDisposable");

68
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<TSyntax>(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 == TypeName
&& syntax.Property == Property
&& syntax.Value == Value;
}
}
public class IsSyntax : ISyntax, ITypeSyntax
{
public string TypeName { get; set; } = string.Empty;

55
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;

36
tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs

@ -197,6 +197,42 @@ namespace Avalonia.Markup.UnitTests.Parsers
result);
}
[Fact]
public void OfType_AttachedProperty()
{
var result = SelectorGrammar.Parse("Button[(Grid.Column)=1]");
Assert.Equal(
new SelectorGrammar.ISyntax[]
{
new SelectorGrammar.OfTypeSyntax { TypeName = "Button" },
new SelectorGrammar.AttachedPropertySyntax {
Xmlns = string.Empty,
TypeName="Grid",
Property = "Column",
Value = "1" },
},
result);
}
[Fact]
public void OfType_AttachedProperty_WithNamespace()
{
var result = SelectorGrammar.Parse("Button[(x|Grid.Column)=1]");
Assert.Equal(
new SelectorGrammar.ISyntax[]
{
new SelectorGrammar.OfTypeSyntax { TypeName = "Button" },
new SelectorGrammar.AttachedPropertySyntax {
Xmlns = "x",
TypeName="Grid",
Property = "Column",
Value = "1" },
},
result);
}
[Fact]
public void Not_OfType()
{

49
tests/Avalonia.Markup.UnitTests/Parsers/SelectorParserTests.cs

@ -7,6 +7,25 @@ namespace Avalonia.Markup.UnitTests.Parsers
{
public class SelectorParserTests
{
static SelectorParserTests()
{
//Ensure the attached properties are registered before run tests
System.Runtime.CompilerServices.RuntimeHelpers.RunClassConstructor(typeof(Grid).TypeHandle);
System.Runtime.CompilerServices.RuntimeHelpers.RunClassConstructor(typeof(Auth).TypeHandle);
}
class Auth
{
public readonly static AttachedProperty<string> NameProperty =
AvaloniaProperty.RegisterAttached<Auth, AvaloniaObject, string>("Name");
public static string GetName(AvaloniaObject avaloniaObject) =>
avaloniaObject.GetValue(NameProperty);
public static void SetName(AvaloniaObject avaloniaObject, string value) =>
avaloniaObject.SetValue(NameProperty, value);
}
[Fact]
public void Parses_Boolean_Property_Selector()
{
@ -14,6 +33,36 @@ namespace Avalonia.Markup.UnitTests.Parsers
var result = target.Parse("TextBlock[IsPointerOver=True]");
}
[Fact]
public void Parses_AttacchedProperty_Selector_With_Namespace()
{
var target = new SelectorParser((ns, type) =>
{
return (ns, type) switch
{
("", nameof(TextBlock)) => typeof(TextBlock),
("l",nameof(Auth)) => typeof(Auth),
_ => null
};
});
var result = target.Parse("TextBlock[(l|Auth.Name)=Admin]");
}
[Fact]
public void Parses_AttacchedProperty_Selector()
{
var target = new SelectorParser((ns, type) =>
{
return (ns, type) switch
{
("", nameof(TextBlock)) => typeof(TextBlock),
("", nameof(Grid)) => typeof(Grid),
_ => null
};
});
var result = target.Parse("TextBlock[(Grid.Column)=1]");
}
[Fact]
public void Parses_Comma_Separated_Selectors()
{

67
tests/Avalonia.Styling.UnitTests/SelectorTests_PropertyEquals.cs

@ -8,6 +8,73 @@ namespace Avalonia.Styling.UnitTests
{
public class SelectorTests_PropertyEquals
{
static SelectorTests_PropertyEquals()
{
//Ensure the attached properties are registered before run tests
System.Runtime.CompilerServices.RuntimeHelpers.RunClassConstructor(typeof(Grid).TypeHandle);
System.Runtime.CompilerServices.RuntimeHelpers.RunClassConstructor(typeof(Auth).TypeHandle);
}
class Auth
{
public readonly static AttachedProperty<string> NameProperty =
AvaloniaProperty.RegisterAttached<Auth, AvaloniaObject, string>("Name");
public static string GetName(AvaloniaObject avaloniaObject) =>
avaloniaObject.GetValue(NameProperty);
public static void SetName(AvaloniaObject avaloniaObject, string value) =>
avaloniaObject.SetValue(NameProperty, value);
}
[Fact]
public async Task PropertyEquals_Attached_Property_Matching_Value()
{
var target = new Markup.Parsers.SelectorParser((ns, type) =>
{
return (ns, type) switch
{
("", nameof(TextBlock)) => typeof(TextBlock),
("", nameof(Grid)) => typeof(Grid),
_ => null
};
}).Parse("TextBlock[(Grid.Column)=1]");
var control = new TextBlock();
var activator = target.Match(control).Activator.ToObservable();
Assert.False(await activator.Take(1));
Grid.SetColumn(control, 1);
Assert.True(await activator.Take(1));
Grid.SetColumn(control, 0);
Assert.False(await activator.Take(1));
}
[Fact]
public async Task PropertyEquals_Attached_Property_With_Namespace_Matching_Value()
{
var target = new Markup.Parsers.SelectorParser((ns, type) =>
{
return (ns, type) switch
{
("", nameof(TextBlock)) => typeof(TextBlock),
("l", nameof(Auth)) => typeof(Auth),
_ => null
};
}).Parse("TextBlock[(l|Auth.Name)=Admin]");
var control = new TextBlock();
var activator = target.Match(control).Activator.ToObservable();
Assert.False(await activator.Take(1));
Auth.SetName(control, "Admin");
Assert.True(await activator.Take(1));
Auth.SetName(control, null);
Assert.False(await activator.Take(1));
}
[Fact]
public async Task PropertyEquals_Matches_When_Property_Has_Matching_Value()
{

Loading…
Cancel
Save