Browse Source

Merge branch 'master' into awaitablerun-animations

pull/1795/head
Jumar Macato 8 years ago
committed by GitHub
parent
commit
140d28e7c3
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 52
      src/Avalonia.Base/AvaloniaPropertyRegistry.cs
  2. 6
      src/Avalonia.Base/Utilities/CharacterReader.cs
  3. 6
      src/Avalonia.Base/Utilities/IdentifierParser.cs
  4. 1
      src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj
  5. 69
      src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs
  6. 167
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs
  7. 85
      src/Markup/Avalonia.Markup.Xaml/Parsers/PropertyParser.cs
  8. 97
      src/Markup/Avalonia.Markup/Data/Binding.cs
  9. 11
      src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs
  10. 15
      src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs
  11. 158
      src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs
  12. 39
      src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs
  13. 54
      src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs
  14. 12
      src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/SelfNode.cs
  15. 11
      tests/Avalonia.Base.UnitTests/AvaloniaPropertyRegistryTests.cs
  16. 3
      tests/Avalonia.Markup.UnitTests/Parsers/ExpressionNodeBuilderTests.cs
  17. 36
      tests/Avalonia.Markup.Xaml.UnitTests/Converters/AvaloniaPropertyConverterTest.cs
  18. 226
      tests/Avalonia.Markup.Xaml.UnitTests/Parsers/PropertyParserTests.cs
  19. 29
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs
  20. 54
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs

52
src/Avalonia.Base/AvaloniaPropertyRegistry.cs

@ -106,7 +106,7 @@ namespace Avalonia
}
/// <summary>
/// Finds a registered non-attached property on a type by name.
/// Finds a registered property on a type by name.
/// </summary>
/// <param name="type">The type.</param>
/// <param name="name">The property name.</param>
@ -130,7 +130,7 @@ namespace Avalonia
}
/// <summary>
/// Finds a registered non-attached property on a type by name.
/// Finds a registered property on an object by name.
/// </summary>
/// <param name="o">The object.</param>
/// <param name="name">The property name.</param>
@ -148,52 +148,6 @@ namespace Avalonia
return FindRegistered(o.GetType(), name);
}
/// <summary>
/// Finds a registered attached property on a type by name.
/// </summary>
/// <param name="type">The type.</param>
/// <param name="ownerType">The owner type.</param>
/// <param name="name">The property name.</param>
/// <returns>
/// The registered property or null if no matching property found.
/// </returns>
/// <exception cref="InvalidOperationException">
/// The property name contains a '.'.
/// </exception>
public AvaloniaProperty FindRegisteredAttached(Type type, Type ownerType, string name)
{
Contract.Requires<ArgumentNullException>(type != null);
Contract.Requires<ArgumentNullException>(ownerType != null);
Contract.Requires<ArgumentNullException>(name != null);
if (name.Contains('.'))
{
throw new InvalidOperationException("Attached properties not supported.");
}
return GetRegisteredAttached(type).FirstOrDefault(x => x.Name == name);
}
/// <summary>
/// Finds a registered non-attached property on a type by name.
/// </summary>
/// <param name="o">The object.</param>
/// <param name="ownerType">The owner type.</param>
/// <param name="name">The property name.</param>
/// <returns>
/// The registered property or null if no matching property found.
/// </returns>
/// <exception cref="InvalidOperationException">
/// The property name contains a '.'.
/// </exception>
public AvaloniaProperty FindRegisteredAttached(AvaloniaObject o, Type ownerType, string name)
{
Contract.Requires<ArgumentNullException>(o != null);
Contract.Requires<ArgumentNullException>(name != null);
return FindRegisteredAttached(o.GetType(), ownerType, name);
}
/// <summary>
/// Checks whether a <see cref="AvaloniaProperty"/> is registered on a type.
/// </summary>
@ -287,4 +241,4 @@ namespace Avalonia
_attachedCache.Clear();
}
}
}
}

6
src/Markup/Avalonia.Markup/Markup/Parsers/Reader.cs → src/Avalonia.Base/Utilities/CharacterReader.cs

@ -3,14 +3,14 @@
using System;
namespace Avalonia.Markup.Parsers
namespace Avalonia.Utilities
{
internal class Reader
public class CharacterReader
{
private readonly string _s;
private int _i;
public Reader(string s)
public CharacterReader(string s)
{
_s = s;
}

6
src/Markup/Avalonia.Markup/Markup/Parsers/IdentifierParser.cs → src/Avalonia.Base/Utilities/IdentifierParser.cs

@ -4,11 +4,11 @@
using System.Globalization;
using System.Text;
namespace Avalonia.Markup.Parsers
namespace Avalonia.Utilities
{
internal static class IdentifierParser
public static class IdentifierParser
{
public static string Parse(Reader r)
public static string Parse(CharacterReader r)
{
if (IsValidIdentifierStart(r.Peek))
{

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

@ -33,6 +33,7 @@
<Compile Include="MarkupExtensions\ResourceInclude.cs" />
<Compile Include="MarkupExtensions\StaticResourceExtension.cs" />
<Compile Include="MarkupExtensions\StyleIncludeExtension.cs" />
<Compile Include="Parsers\PropertyParser.cs" />
<Compile Include="PortableXaml\AvaloniaXamlContext.cs" />
<Compile Include="PortableXaml\AttributeExtensions.cs" />
<Compile Include="PortableXaml\AvaloniaMemberAttributeProvider.cs" />

69
src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs

@ -4,19 +4,19 @@
using System;
using System.ComponentModel;
using System.Globalization;
using System.Text.RegularExpressions;
using Avalonia.Controls;
using Avalonia.Logging;
using Avalonia.Markup.Parsers;
using Avalonia.Markup.Xaml.Parsers;
using Avalonia.Markup.Xaml.Templates;
using Avalonia.Styling;
using Portable.Xaml;
using Avalonia.Utilities;
using Portable.Xaml.ComponentModel;
using Portable.Xaml.Markup;
namespace Avalonia.Markup.Xaml.Converters
{
public class AvaloniaPropertyTypeConverter : TypeConverter
{
private static readonly Regex regex = new Regex(@"^\(?(\w*)\.(\w*)\)?|(.*)$");
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
return sourceType == typeof(string);
@ -24,39 +24,48 @@ namespace Avalonia.Markup.Xaml.Converters
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
var (owner, propertyName) = ParseProperty((string)value);
var ownerType = TryResolveOwnerByName(context, owner) ??
context.GetFirstAmbientValue<ControlTemplate>()?.TargetType ??
context.GetFirstAmbientValue<Style>()?.Selector?.TargetType;
var registry = AvaloniaPropertyRegistry.Instance;
var parser = new PropertyParser();
var reader = new CharacterReader((string)value);
var (ns, owner, propertyName) = parser.Parse(reader);
var ownerType = TryResolveOwnerByName(context, ns, owner);
var targetType = context.GetFirstAmbientValue<ControlTemplate>()?.TargetType ??
context.GetFirstAmbientValue<Style>()?.Selector?.TargetType ??
typeof(Control);
var effectiveOwner = ownerType ?? targetType;
var property = registry.FindRegistered(effectiveOwner, propertyName);
if (ownerType == null)
if (property == null)
{
throw new XamlLoadException(
$"Could not determine the owner type for property '{propertyName}'. " +
"Please fully qualify the property name or specify a target type on " +
"the containing template.");
throw new XamlLoadException($"Could not find property '{effectiveOwner.Name}.{propertyName}'.");
}
var property = AvaloniaPropertyRegistry.Instance.FindRegistered(ownerType, propertyName);
if (property == null)
if (effectiveOwner != targetType &&
!property.IsAttached &&
!registry.IsRegistered(targetType, property))
{
throw new XamlLoadException($"Could not find AvaloniaProperty '{ownerType.Name}.{propertyName}'.");
Logger.Warning(
LogArea.Property,
this,
"Property '{Owner}.{Name}' is not registered on '{Type}'.",
effectiveOwner,
propertyName,
targetType);
}
return property;
}
private Type TryResolveOwnerByName(ITypeDescriptorContext context, string owner)
private Type TryResolveOwnerByName(ITypeDescriptorContext context, string ns, string owner)
{
if (owner != null)
{
var resolver = context.GetService<IXamlTypeResolver>();
var result = resolver.Resolve(owner);
var result = context.ResolveType(ns, owner);
if (result == null)
{
throw new XamlLoadException($"Could not find type '{owner}'.");
var name = string.IsNullOrEmpty(ns) ? owner : $"{ns}:{owner}";
throw new XamlLoadException($"Could not find type '{name}'.");
}
return result;
@ -64,19 +73,5 @@ namespace Avalonia.Markup.Xaml.Converters
return null;
}
private (string owner, string property) ParseProperty(string s)
{
var result = regex.Match(s);
if (result.Groups[1].Success)
{
return (result.Groups[1].Value, result.Groups[2].Value);
}
else
{
return (null, result.Groups[3].Value);
}
}
}
}
}

167
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs

@ -32,185 +32,28 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
{
var descriptorContext = (ITypeDescriptorContext)serviceProvider;
var pathInfo = ParsePath(Path, descriptorContext);
ValidateState(pathInfo);
return new Binding
{
TypeResolver = descriptorContext.ResolveType,
Converter = Converter,
ConverterParameter = ConverterParameter,
ElementName = pathInfo.ElementName ?? ElementName,
ElementName = ElementName,
FallbackValue = FallbackValue,
Mode = Mode,
Path = pathInfo.Path,
Path = Path,
Priority = Priority,
Source = Source,
RelativeSource = pathInfo.RelativeSource ?? RelativeSource,
RelativeSource = RelativeSource,
DefaultAnchor = new WeakReference(GetDefaultAnchor((ITypeDescriptorContext)serviceProvider))
};
}
private class PathInfo
{
public string Path { get; set; }
public string ElementName { get; set; }
public RelativeSource RelativeSource { get; set; }
}
private void ValidateState(PathInfo pathInfo)
{
if (pathInfo.ElementName != null && ElementName != null)
{
throw new InvalidOperationException(
"ElementName property cannot be set when an #elementName path is provided.");
}
if (pathInfo.RelativeSource != null && RelativeSource != null)
{
throw new InvalidOperationException(
"ElementName property cannot be set when a $self or $parent path is provided.");
}
if ((pathInfo.ElementName != null || ElementName != null) &&
(pathInfo.RelativeSource != null || RelativeSource != null))
{
throw new InvalidOperationException(
"ElementName property cannot be set with a RelativeSource.");
}
}
private static PathInfo ParsePath(string path, ITypeDescriptorContext context)
{
var result = new PathInfo();
if (string.IsNullOrWhiteSpace(path) || path == ".")
{
result.Path = string.Empty;
return result;
}
else if (path.StartsWith("!"))
{
int pathStart = 0;
for (; pathStart < path.Length && path[pathStart] == '!'; ++pathStart);
result.Path = path.Substring(0, pathStart);
path = path.Substring(pathStart);
}
if (path.StartsWith("#"))
{
var dot = path.IndexOf('.');
if (dot != -1)
{
result.Path += path.Substring(dot + 1);
result.ElementName = path.Substring(1, dot - 1);
}
else
{
result.Path += string.Empty;
result.ElementName = path.Substring(1);
}
}
else if (path.StartsWith("$"))
{
var relativeSource = new RelativeSource
{
Tree = TreeType.Logical
};
result.RelativeSource = relativeSource;
var dot = path.IndexOf('.');
string relativeSourceMode;
if (dot != -1)
{
result.Path += path.Substring(dot + 1);
relativeSourceMode = path.Substring(1, dot - 1);
}
else
{
result.Path += string.Empty;
relativeSourceMode = path.Substring(1);
}
if (relativeSourceMode == "self")
{
relativeSource.Mode = RelativeSourceMode.Self;
}
else if (relativeSourceMode == "parent")
{
relativeSource.Mode = RelativeSourceMode.FindAncestor;
relativeSource.AncestorLevel = 1;
}
else if (relativeSourceMode.StartsWith("parent["))
{
relativeSource.Mode = RelativeSourceMode.FindAncestor;
var parentConfigStart = relativeSourceMode.IndexOf('[');
if (!relativeSourceMode.EndsWith("]"))
{
throw new InvalidOperationException("Invalid RelativeSource binding syntax. Expected matching ']' for '['.");
}
var parentConfigParams = relativeSourceMode.Substring(parentConfigStart + 1).TrimEnd(']').Split(';');
if (parentConfigParams.Length > 2 || parentConfigParams.Length == 0)
{
throw new InvalidOperationException("Expected either 1 or 2 parameters for RelativeSource binding syntax");
}
else if (parentConfigParams.Length == 1)
{
if (int.TryParse(parentConfigParams[0], out int level))
{
relativeSource.AncestorType = null;
relativeSource.AncestorLevel = level + 1;
}
else
{
relativeSource.AncestorType = LookupAncestorType(parentConfigParams[0], context);
}
}
else
{
relativeSource.AncestorType = LookupAncestorType(parentConfigParams[0], context);
relativeSource.AncestorLevel = int.Parse(parentConfigParams[1]) + 1;
}
}
else
{
throw new InvalidOperationException($"Invalid RelativeSource binding syntax: {relativeSourceMode}");
}
}
else
{
result.Path += path;
}
return result;
}
private static Type LookupAncestorType(string ancestorTypeName, ITypeDescriptorContext context)
{
var parts = ancestorTypeName.Split(':');
if (parts.Length == 0 || parts.Length > 2)
{
throw new InvalidOperationException("Invalid type name");
}
if (parts.Length == 1)
{
return context.ResolveType(string.Empty, parts[0]);
}
else
{
return context.ResolveType(parts[0], parts[1]);
}
}
private static object GetDefaultAnchor(ITypeDescriptorContext context)
{
object anchor = null;
// The target is not a control, so we need to find an anchor that will let us look
// If the target is not a control, so we need to find an anchor that will let us look
// up named controls and style resources. First look for the closest IControl in
// the context.
anchor = context.GetFirstAmbientValue<IControl>();
object anchor = context.GetFirstAmbientValue<IControl>();
// If a control was not found, then try to find the highest-level style as the XAML
// file could be a XAML file containing only styles.

85
src/Markup/Avalonia.Markup.Xaml/Parsers/PropertyParser.cs

@ -0,0 +1,85 @@
using System;
using Avalonia.Data.Core;
using Avalonia.Markup.Parsers;
using Avalonia.Utilities;
namespace Avalonia.Markup.Xaml.Parsers
{
internal class PropertyParser
{
public (string ns, string owner, string name) Parse(CharacterReader r)
{
if (r.End)
{
throw new ExpressionParseException(0, "Expected property name.");
}
var openParens = r.TakeIf('(');
bool closeParens = false;
string ns = null;
string owner = null;
string name = null;
do
{
var token = IdentifierParser.Parse(r);
if (token == null)
{
if (r.End)
{
break;
}
else
{
if (openParens && !r.End && (closeParens = r.TakeIf(')')))
{
break;
}
else if (openParens)
{
throw new ExpressionParseException(r.Position, $"Expected ')'.");
}
throw new ExpressionParseException(r.Position, $"Unexpected '{r.Peek}'.");
}
}
else if (!r.End && r.TakeIf(':'))
{
ns = ns == null ?
token :
throw new ExpressionParseException(r.Position, "Unexpected ':'.");
}
else if (!r.End && r.TakeIf('.'))
{
owner = owner == null ?
token :
throw new ExpressionParseException(r.Position, "Unexpected '.'.");
}
else
{
name = token;
}
} while (!r.End);
if (name == null)
{
throw new ExpressionParseException(0, "Expected property name.");
}
else if (openParens && owner == null)
{
throw new ExpressionParseException(1, "Expected property owner.");
}
else if (openParens && !closeParens)
{
throw new ExpressionParseException(r.Position, "Expected ')'.");
}
else if (!r.End)
{
throw new ExpressionParseException(r.Position, "Expected end of expression.");
}
return (ns, owner, name);
}
}
}

97
src/Markup/Avalonia.Markup/Data/Binding.cs

@ -105,37 +105,53 @@ namespace Avalonia.Data
ExpressionObserver observer;
var (node, mode) = ExpressionObserverBuilder.Parse(Path, enableDataValidation, TypeResolver);
if (ElementName != null)
{
observer = CreateElementObserver(
(target as IStyledElement) ?? (anchor as IStyledElement),
ElementName,
Path,
enableDataValidation);
node);
}
else if (Source != null)
{
observer = CreateSourceObserver(Source, Path, enableDataValidation);
observer = CreateSourceObserver(Source, node);
}
else if (RelativeSource == null || RelativeSource.Mode == RelativeSourceMode.DataContext)
else if (RelativeSource == null)
{
if (mode == SourceMode.Data)
{
observer = CreateDataContextObserver(
target,
node,
targetProperty == StyledElement.DataContextProperty,
anchor);
}
else
{
observer = new ExpressionObserver(
(target as IStyledElement) ?? (anchor as IStyledElement),
node);
}
}
else if (RelativeSource.Mode == RelativeSourceMode.DataContext)
{
observer = CreateDataContextObserver(
target,
Path,
node,
targetProperty == StyledElement.DataContextProperty,
anchor,
enableDataValidation);
anchor);
}
else if (RelativeSource.Mode == RelativeSourceMode.Self)
{
observer = CreateSourceObserver(target, Path, enableDataValidation);
observer = CreateSourceObserver(target, node);
}
else if (RelativeSource.Mode == RelativeSourceMode.TemplatedParent)
{
observer = CreateTemplatedParentObserver(
(target as IStyledElement) ?? (anchor as IStyledElement),
Path,
enableDataValidation);
node);
}
else if (RelativeSource.Mode == RelativeSourceMode.FindAncestor)
{
@ -147,8 +163,7 @@ namespace Avalonia.Data
observer = CreateFindAncestorObserver(
(target as IStyledElement) ?? (anchor as IStyledElement),
RelativeSource,
Path,
enableDataValidation);
node);
}
else
{
@ -179,10 +194,9 @@ namespace Avalonia.Data
private ExpressionObserver CreateDataContextObserver(
IAvaloniaObject target,
string path,
ExpressionNode node,
bool targetIsDataContext,
object anchor,
bool enableDataValidation)
object anchor)
{
Contract.Requires<ArgumentNullException>(target != null);
@ -198,48 +212,41 @@ namespace Avalonia.Data
if (!targetIsDataContext)
{
var result = ExpressionObserverBuilder.Build(
var result = new ExpressionObserver(
() => target.GetValue(StyledElement.DataContextProperty),
path,
node,
new UpdateSignal(target, StyledElement.DataContextProperty),
enableDataValidation,
typeResolver: TypeResolver);
null);
return result;
}
else
{
return ExpressionObserverBuilder.Build(
return new ExpressionObserver(
GetParentDataContext(target),
path,
enableDataValidation,
typeResolver: TypeResolver);
node,
null);
}
}
private ExpressionObserver CreateElementObserver(
IStyledElement target,
string elementName,
string path,
bool enableDataValidation)
ExpressionNode node)
{
Contract.Requires<ArgumentNullException>(target != null);
var description = $"#{elementName}.{path}";
var result = ExpressionObserverBuilder.Build(
var result = new ExpressionObserver(
ControlLocator.Track(target, elementName),
path,
enableDataValidation,
description,
typeResolver: TypeResolver);
node,
null);
return result;
}
private ExpressionObserver CreateFindAncestorObserver(
IStyledElement target,
RelativeSource relativeSource,
string path,
bool enableDataValidation)
ExpressionNode node)
{
Contract.Requires<ArgumentNullException>(target != null);
@ -263,36 +270,32 @@ namespace Avalonia.Data
throw new InvalidOperationException("Invalid tree to traverse.");
}
return ExpressionObserverBuilder.Build(
return new ExpressionObserver(
controlLocator,
path,
enableDataValidation,
typeResolver: TypeResolver);
node,
null);
}
private ExpressionObserver CreateSourceObserver(
object source,
string path,
bool enableDataValidation)
ExpressionNode node)
{
Contract.Requires<ArgumentNullException>(source != null);
return ExpressionObserverBuilder.Build(source, path, enableDataValidation, typeResolver: TypeResolver);
return new ExpressionObserver(source, node);
}
private ExpressionObserver CreateTemplatedParentObserver(
IAvaloniaObject target,
string path,
bool enableDataValidation)
ExpressionNode node)
{
Contract.Requires<ArgumentNullException>(target != null);
var result = ExpressionObserverBuilder.Build(
var result = new ExpressionObserver(
() => target.GetValue(StyledElement.TemplatedParentProperty),
path,
node,
new UpdateSignal(target, StyledElement.TemplatedParentProperty),
enableDataValidation,
typeResolver: TypeResolver);
null);
return result;
}

11
src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs

@ -2,6 +2,7 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Data.Core;
using Avalonia.Utilities;
using System;
using System.Collections.Generic;
using System.Text;
@ -10,7 +11,7 @@ namespace Avalonia.Markup.Parsers
{
internal static class ArgumentListParser
{
public static IList<string> Parse(Reader r, char open, char close)
public static IList<string> Parse(CharacterReader r, char open, char close, char delimiter = ',')
{
if (r.Peek == open)
{
@ -21,7 +22,7 @@ namespace Avalonia.Markup.Parsers
while (!r.End)
{
var builder = new StringBuilder();
while (!r.End && r.Peek != ',' && r.Peek != close && !char.IsWhiteSpace(r.Peek))
while (!r.End && r.Peek != delimiter && r.Peek != close && !char.IsWhiteSpace(r.Peek))
{
builder.Append(r.Take());
}
@ -35,7 +36,7 @@ namespace Avalonia.Markup.Parsers
if (r.End)
{
throw new ExpressionParseException(r.Position, "Expected ','.");
throw new ExpressionParseException(r.Position, $"Expected '{delimiter}'.");
}
else if (r.TakeIf(close))
{
@ -43,9 +44,9 @@ namespace Avalonia.Markup.Parsers
}
else
{
if (r.Take() != ',')
if (r.Take() != delimiter)
{
throw new ExpressionParseException(r.Position, "Expected ','.");
throw new ExpressionParseException(r.Position, $"Expected '{delimiter}'.");
}
r.SkipWhitespace();

15
src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs

@ -1,4 +1,5 @@
using Avalonia.Data.Core;
using Avalonia.Utilities;
using System;
using System.Collections.Generic;
using System.Reactive;
@ -8,14 +9,14 @@ namespace Avalonia.Markup.Parsers
{
public static class ExpressionObserverBuilder
{
internal static ExpressionNode Parse(string expression, bool enableValidation = false, Func<string, string, Type> typeResolver = null)
internal static (ExpressionNode Node, SourceMode Mode) Parse(string expression, bool enableValidation = false, Func<string, string, Type> typeResolver = null)
{
if (string.IsNullOrWhiteSpace(expression))
{
return new EmptyExpressionNode();
return (new EmptyExpressionNode(), default);
}
var reader = new Reader(expression);
var reader = new CharacterReader(expression);
var parser = new ExpressionParser(enableValidation, typeResolver);
var node = parser.Parse(reader);
@ -36,7 +37,7 @@ namespace Avalonia.Markup.Parsers
{
return new ExpressionObserver(
root,
Parse(expression, enableDataValidation, typeResolver),
Parse(expression, enableDataValidation, typeResolver).Node,
description ?? expression);
}
@ -50,7 +51,7 @@ namespace Avalonia.Markup.Parsers
Contract.Requires<ArgumentNullException>(rootObservable != null);
return new ExpressionObserver(
rootObservable,
Parse(expression, enableDataValidation, typeResolver),
Parse(expression, enableDataValidation, typeResolver).Node,
description ?? expression);
}
@ -66,8 +67,8 @@ namespace Avalonia.Markup.Parsers
Contract.Requires<ArgumentNullException>(rootGetter != null);
return new ExpressionObserver(
() => rootGetter(),
Parse(expression, enableDataValidation, typeResolver),
rootGetter,
Parse(expression, enableDataValidation, typeResolver).Node,
update,
description ?? expression);
}

158
src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs

@ -3,12 +3,19 @@
using Avalonia.Data.Core;
using Avalonia.Markup.Parsers.Nodes;
using Avalonia.Utilities;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Avalonia.Markup.Parsers
{
internal enum SourceMode
{
Data,
Control
}
internal class ExpressionParser
{
private readonly bool _enableValidation;
@ -20,10 +27,11 @@ namespace Avalonia.Markup.Parsers
_enableValidation = enableValidation;
}
public ExpressionNode Parse(Reader r)
public (ExpressionNode Node, SourceMode Mode) Parse(CharacterReader r)
{
var nodes = new List<ExpressionNode>();
var state = State.Start;
var mode = SourceMode.Data;
while (!r.End && state != State.End)
{
@ -48,6 +56,16 @@ namespace Avalonia.Markup.Parsers
case State.Indexer:
state = ParseIndexer(r, nodes);
break;
case State.ElementName:
state = ParseElementName(r, nodes);
mode = SourceMode.Control;
break;
case State.RelativeSource:
state = ParseRelativeSource(r, nodes);
mode = SourceMode.Control;
break;
}
}
@ -61,16 +79,24 @@ namespace Avalonia.Markup.Parsers
nodes[n].Next = nodes[n + 1];
}
return nodes.FirstOrDefault();
return (nodes.FirstOrDefault(), mode);
}
private State ParseStart(Reader r, IList<ExpressionNode> nodes)
private State ParseStart(CharacterReader r, IList<ExpressionNode> nodes)
{
if (ParseNot(r))
{
nodes.Add(new LogicalNotNode());
return State.Start;
}
else if (ParseSharp(r))
{
return State.ElementName;
}
else if (ParseDollarSign(r))
{
return State.RelativeSource;
}
else if (ParseOpenBrace(r))
{
return State.AttachedProperty;
@ -93,7 +119,7 @@ namespace Avalonia.Markup.Parsers
return State.End;
}
private static State ParseAfterMember(Reader r, IList<ExpressionNode> nodes)
private static State ParseAfterMember(CharacterReader r, IList<ExpressionNode> nodes)
{
if (ParseMemberAccessor(r))
{
@ -112,7 +138,7 @@ namespace Avalonia.Markup.Parsers
return State.End;
}
private State ParseBeforeMember(Reader r, IList<ExpressionNode> nodes)
private State ParseBeforeMember(CharacterReader r, IList<ExpressionNode> nodes)
{
if (ParseOpenBrace(r))
{
@ -132,21 +158,9 @@ namespace Avalonia.Markup.Parsers
}
}
private State ParseAttachedProperty(Reader r, List<ExpressionNode> nodes)
private State ParseAttachedProperty(CharacterReader r, List<ExpressionNode> nodes)
{
string ns = string.Empty;
string owner;
var ownerOrNamespace = IdentifierParser.Parse(r);
if (r.TakeIf(':'))
{
ns = ownerOrNamespace;
owner = IdentifierParser.Parse(r);
}
else
{
owner = ownerOrNamespace;
}
var (ns, owner) = ParseTypeName(r);
if (r.End || !r.TakeIf('.'))
{
@ -171,7 +185,7 @@ namespace Avalonia.Markup.Parsers
return State.AfterMember;
}
private State ParseIndexer(Reader r, List<ExpressionNode> nodes)
private State ParseIndexer(CharacterReader r, List<ExpressionNode> nodes)
{
var args = ArgumentListParser.Parse(r, '[', ']');
@ -184,34 +198,128 @@ namespace Avalonia.Markup.Parsers
return State.AfterMember;
}
private static bool ParseNot(Reader r)
private State ParseElementName(CharacterReader r, List<ExpressionNode> nodes)
{
var name = IdentifierParser.Parse(r);
if (name == null)
{
throw new ExpressionParseException(r.Position, "Element name expected after '#'.");
}
nodes.Add(new ElementNameNode(name));
return State.AfterMember;
}
private State ParseRelativeSource(CharacterReader r, List<ExpressionNode> nodes)
{
var mode = IdentifierParser.Parse(r);
if (mode == "self")
{
nodes.Add(new SelfNode());
}
else if (mode == "parent")
{
Type ancestorType = null;
var ancestorLevel = 0;
if (PeekOpenBracket(r))
{
var args = ArgumentListParser.Parse(r, '[', ']', ';');
if (args.Count > 2 || args.Count == 0)
{
throw new ExpressionParseException(r.Position, "Too many arguments in RelativeSource syntax sugar");
}
else if (args.Count == 1)
{
if (int.TryParse(args[0], out int level))
{
ancestorType = null;
ancestorLevel = level;
}
else
{
var typeName = ParseTypeName(new CharacterReader(args[0]));
ancestorType = _typeResolver(typeName.ns, typeName.typeName);
}
}
else
{
var typeName = ParseTypeName(new CharacterReader(args[0]));
ancestorType = _typeResolver(typeName.ns, typeName.typeName);
ancestorLevel = int.Parse(args[1]);
}
}
nodes.Add(new FindAncestorNode(ancestorType, ancestorLevel));
}
else
{
throw new ExpressionParseException(r.Position, "Unknown RelativeSource mode.");
}
return State.AfterMember;
}
private static (string ns, string typeName) ParseTypeName(CharacterReader r)
{
string ns, typeName;
ns = string.Empty;
var typeNameOrNamespace = IdentifierParser.Parse(r);
if (!r.End && r.TakeIf(':'))
{
ns = typeNameOrNamespace;
typeName = IdentifierParser.Parse(r);
}
else
{
typeName = typeNameOrNamespace;
}
return (ns, typeName);
}
private static bool ParseNot(CharacterReader r)
{
return !r.End && r.TakeIf('!');
}
private static bool ParseMemberAccessor(Reader r)
private static bool ParseMemberAccessor(CharacterReader r)
{
return !r.End && r.TakeIf('.');
}
private static bool ParseOpenBrace(Reader r)
private static bool ParseOpenBrace(CharacterReader r)
{
return !r.End && r.TakeIf('(');
}
private static bool PeekOpenBracket(Reader r)
private static bool PeekOpenBracket(CharacterReader r)
{
return !r.End && r.Peek == '[';
}
private static bool ParseStreamOperator(Reader r)
private static bool ParseStreamOperator(CharacterReader r)
{
return !r.End && r.TakeIf('^');
}
private static bool ParseDollarSign(CharacterReader r)
{
return !r.End && r.TakeIf('$');
}
private static bool ParseSharp(CharacterReader r)
{
return !r.End && r.TakeIf('#');
}
private enum State
{
Start,
RelativeSource,
ElementName,
AfterMember,
BeforeMember,
AttachedProperty,

39
src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs

@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
using System.Text;
using Avalonia.Data.Core;
using Avalonia.LogicalTree;
namespace Avalonia.Markup.Parsers.Nodes
{
internal class ElementNameNode : ExpressionNode
{
private readonly string _name;
private IDisposable _subscription;
public ElementNameNode(string name)
{
_name = name;
}
public override string Description => $"#{_name}";
protected override void StartListeningCore(WeakReference reference)
{
if (reference.Target is ILogical logical)
{
_subscription = ControlLocator.Track(logical, _name).Subscribe(ValueChanged);
}
else
{
_subscription = null;
}
}
protected override void StopListeningCore()
{
_subscription?.Dispose();
_subscription = null;
}
}
}

54
src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs

@ -0,0 +1,54 @@
using System;
using System.Collections.Generic;
using System.Text;
using Avalonia.Data.Core;
using Avalonia.LogicalTree;
namespace Avalonia.Markup.Parsers.Nodes
{
internal class FindAncestorNode : ExpressionNode
{
private readonly int _level;
private readonly Type _ancestorType;
private IDisposable _subscription;
public FindAncestorNode(Type ancestorType, int level)
{
_level = level;
_ancestorType = ancestorType;
}
public override string Description
{
get
{
if (_ancestorType == null)
{
return $"$parent[{_level}]";
}
else
{
return $"$parent[{_ancestorType.Name}, {_level}]";
}
}
}
protected override void StartListeningCore(WeakReference reference)
{
if (reference.Target is ILogical logical)
{
_subscription = ControlLocator.Track(logical, _level, _ancestorType).Subscribe(ValueChanged);
}
else
{
_subscription = null;
}
}
protected override void StopListeningCore()
{
_subscription?.Dispose();
_subscription = null;
}
}
}

12
src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/SelfNode.cs

@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Text;
using Avalonia.Data.Core;
namespace Avalonia.Markup.Parsers.Nodes
{
internal class SelfNode : ExpressionNode
{
public override string Description => "$self";
}
}

11
tests/Avalonia.Base.UnitTests/AvaloniaPropertyRegistryTests.cs

@ -99,17 +99,6 @@ namespace Avalonia.Base.UnitTests
Assert.Null(result);
}
[Fact]
public void FindRegisteredAttached_Finds_Property()
{
var result = AvaloniaPropertyRegistry.Instance.FindRegisteredAttached(
typeof(Class1),
typeof(AttachedOwner),
"Attached");
Assert.Equal(AttachedOwner.AttachedProperty, result);
}
private class Class1 : AvaloniaObject
{
public static readonly StyledProperty<string> FooProperty =

3
tests/Avalonia.Markup.UnitTests/Parsers/ExpressionNodeBuilderTests.cs

@ -162,8 +162,9 @@ namespace Avalonia.Markup.UnitTests.Parsers
Assert.Equal(e.Arguments.ToArray(), args);
}
private List<ExpressionNode> ToList(ExpressionNode node)
private List<ExpressionNode> ToList((ExpressionNode node, SourceMode mode) parsed)
{
var (node, _) = parsed;
var result = new List<ExpressionNode>();
while (node != null)

36
tests/Avalonia.Markup.Xaml.UnitTests/Converters/AvaloniaPropertyConverterTest.cs

@ -10,6 +10,7 @@ using Xunit;
using System.ComponentModel;
using Portable.Xaml;
using Portable.Xaml.Markup;
using Avalonia.Controls;
namespace Avalonia.Markup.Xaml.UnitTests.Converters
{
@ -26,7 +27,8 @@ namespace Avalonia.Markup.Xaml.UnitTests.Converters
public void ConvertFrom_Finds_Fully_Qualified_Property()
{
var target = new AvaloniaPropertyTypeConverter();
var context = CreateContext();
var style = new Style(x => x.OfType<Class1>());
var context = CreateContext(style);
var result = target.ConvertFrom(context, null, "Class1.Foo");
Assert.Equal(Class1.FooProperty, result);
@ -47,7 +49,8 @@ namespace Avalonia.Markup.Xaml.UnitTests.Converters
public void ConvertFrom_Finds_Attached_Property()
{
var target = new AvaloniaPropertyTypeConverter();
var context = CreateContext();
var style = new Style(x => x.OfType<Class1>());
var context = CreateContext(style);
var result = target.ConvertFrom(context, null, "AttachedOwner.Attached");
Assert.Equal(AttachedOwner.AttachedProperty, result);
@ -57,12 +60,37 @@ namespace Avalonia.Markup.Xaml.UnitTests.Converters
public void ConvertFrom_Finds_Attached_Property_With_Parentheses()
{
var target = new AvaloniaPropertyTypeConverter();
var context = CreateContext();
var style = new Style(x => x.OfType<Class1>());
var context = CreateContext(style);
var result = target.ConvertFrom(context, null, "(AttachedOwner.Attached)");
Assert.Equal(AttachedOwner.AttachedProperty, result);
}
[Fact]
public void ConvertFrom_Throws_For_Nonexistent_Property()
{
var target = new AvaloniaPropertyTypeConverter();
var style = new Style(x => x.OfType<Class1>());
var context = CreateContext(style);
var ex = Assert.Throws<XamlLoadException>(() => target.ConvertFrom(context, null, "Nonexistent"));
Assert.Equal("Could not find property 'Class1.Nonexistent'.", ex.Message);
}
[Fact]
public void ConvertFrom_Throws_For_Nonexistent_Attached_Property()
{
var target = new AvaloniaPropertyTypeConverter();
var style = new Style(x => x.OfType<Class1>());
var context = CreateContext(style);
var ex = Assert.Throws<XamlLoadException>(() => target.ConvertFrom(context, null, "AttachedOwner.NonExistent"));
Assert.Equal("Could not find property 'AttachedOwner.NonExistent'.", ex.Message);
}
private ITypeDescriptorContext CreateContext(Style style = null)
{
var tdMock = new Mock<ITypeDescriptorContext>();
@ -126,4 +154,4 @@ namespace Avalonia.Markup.Xaml.UnitTests.Converters
AvaloniaProperty.RegisterAttached<AttachedOwner, Class1, string>("Attached");
}
}
}
}

226
tests/Avalonia.Markup.Xaml.UnitTests/Parsers/PropertyParserTests.cs

@ -0,0 +1,226 @@
using System;
using Avalonia.Data.Core;
using Avalonia.Markup.Parsers;
using Avalonia.Markup.Xaml.Parsers;
using Avalonia.Utilities;
using Xunit;
namespace Avalonia.Markup.Xaml.UnitTests.Parsers
{
public class PropertyParserTests
{
[Fact]
public void Parses_Name()
{
var target = new PropertyParser();
var reader = new CharacterReader("Foo");
var (ns, owner, name) = target.Parse(reader);
Assert.Null(ns);
Assert.Null(owner);
Assert.Equal("Foo", name);
}
[Fact]
public void Parses_Owner_And_Name()
{
var target = new PropertyParser();
var reader = new CharacterReader("Foo.Bar");
var (ns, owner, name) = target.Parse(reader);
Assert.Null(ns);
Assert.Equal("Foo", owner);
Assert.Equal("Bar", name);
}
[Fact]
public void Parses_Namespace_Owner_And_Name()
{
var target = new PropertyParser();
var reader = new CharacterReader("foo:Bar.Baz");
var (ns, owner, name) = target.Parse(reader);
Assert.Equal("foo", ns);
Assert.Equal("Bar", owner);
Assert.Equal("Baz", name);
}
[Fact]
public void Parses_Owner_And_Name_With_Parentheses()
{
var target = new PropertyParser();
var reader = new CharacterReader("(Foo.Bar)");
var (ns, owner, name) = target.Parse(reader);
Assert.Null(ns);
Assert.Equal("Foo", owner);
Assert.Equal("Bar", name);
}
[Fact]
public void Parses_Namespace_Owner_And_Name_With_Parentheses()
{
var target = new PropertyParser();
var reader = new CharacterReader("(foo:Bar.Baz)");
var (ns, owner, name) = target.Parse(reader);
Assert.Equal("foo", ns);
Assert.Equal("Bar", owner);
Assert.Equal("Baz", name);
}
[Fact]
public void Fails_With_Empty_String()
{
var target = new PropertyParser();
var reader = new CharacterReader("");
var ex = Assert.Throws<ExpressionParseException>(() => target.Parse(reader));
Assert.Equal(0, ex.Column);
Assert.Equal("Expected property name.", ex.Message);
}
[Fact]
public void Fails_With_Only_Whitespace()
{
var target = new PropertyParser();
var reader = new CharacterReader(" ");
var ex = Assert.Throws<ExpressionParseException>(() => target.Parse(reader));
Assert.Equal(0, ex.Column);
Assert.Equal("Unexpected ' '.", ex.Message);
}
[Fact]
public void Fails_With_Leading_Whitespace()
{
var target = new PropertyParser();
var reader = new CharacterReader(" Foo");
var ex = Assert.Throws<ExpressionParseException>(() => target.Parse(reader));
Assert.Equal(0, ex.Column);
Assert.Equal("Unexpected ' '.", ex.Message);
}
[Fact]
public void Fails_With_Trailing_Whitespace()
{
var target = new PropertyParser();
var reader = new CharacterReader("Foo ");
var ex = Assert.Throws<ExpressionParseException>(() => target.Parse(reader));
Assert.Equal(3, ex.Column);
Assert.Equal("Unexpected ' '.", ex.Message);
}
[Fact]
public void Fails_With_Invalid_Property_Name()
{
var target = new PropertyParser();
var reader = new CharacterReader("123");
var ex = Assert.Throws<ExpressionParseException>(() => target.Parse(reader));
Assert.Equal(0, ex.Column);
Assert.Equal("Unexpected '1'.", ex.Message);
}
[Fact]
public void Fails_With_Trailing_Junk()
{
var target = new PropertyParser();
var reader = new CharacterReader("Foo%");
var ex = Assert.Throws<ExpressionParseException>(() => target.Parse(reader));
Assert.Equal(3, ex.Column);
Assert.Equal("Unexpected '%'.", ex.Message);
}
[Fact]
public void Fails_With_Invalid_Property_Name_After_Owner()
{
var target = new PropertyParser();
var reader = new CharacterReader("Foo.123");
var ex = Assert.Throws<ExpressionParseException>(() => target.Parse(reader));
Assert.Equal(4, ex.Column);
Assert.Equal("Unexpected '1'.", ex.Message);
}
[Fact]
public void Fails_With_Whitespace_Between_Owner_And_Name()
{
var target = new PropertyParser();
var reader = new CharacterReader("Foo. Bar");
var ex = Assert.Throws<ExpressionParseException>(() => target.Parse(reader));
Assert.Equal(4, ex.Column);
Assert.Equal("Unexpected ' '.", ex.Message);
}
[Fact]
public void Fails_With_Too_Many_Segments()
{
var target = new PropertyParser();
var reader = new CharacterReader("Foo.Bar.Baz");
var ex = Assert.Throws<ExpressionParseException>(() => target.Parse(reader));
Assert.Equal(8, ex.Column);
Assert.Equal("Unexpected '.'.", ex.Message);
}
[Fact]
public void Fails_With_Too_Many_Namespaces()
{
var target = new PropertyParser();
var reader = new CharacterReader("foo:bar:Baz");
var ex = Assert.Throws<ExpressionParseException>(() => target.Parse(reader));
Assert.Equal(8, ex.Column);
Assert.Equal("Unexpected ':'.", ex.Message);
}
[Fact]
public void Fails_With_Parens_But_No_Owner()
{
var target = new PropertyParser();
var reader = new CharacterReader("(Foo)");
var ex = Assert.Throws<ExpressionParseException>(() => target.Parse(reader));
Assert.Equal(1, ex.Column);
Assert.Equal("Expected property owner.", ex.Message);
}
[Fact]
public void Fails_With_Parens_And_Namespace_But_No_Owner()
{
var target = new PropertyParser();
var reader = new CharacterReader("(foo:Bar)");
var ex = Assert.Throws<ExpressionParseException>(() => target.Parse(reader));
Assert.Equal(1, ex.Column);
Assert.Equal("Expected property owner.", ex.Message);
}
[Fact]
public void Fails_With_Missing_Close_Parens()
{
var target = new PropertyParser();
var reader = new CharacterReader("(Foo.Bar");
var ex = Assert.Throws<ExpressionParseException>(() => target.Parse(reader));
Assert.Equal(8, ex.Column);
Assert.Equal("Expected ')'.", ex.Message);
}
[Fact]
public void Fails_With_Unexpected_Close_Parens()
{
var target = new PropertyParser();
var reader = new CharacterReader("Foo.Bar)");
var ex = Assert.Throws<ExpressionParseException>(() => target.Parse(reader));
Assert.Equal(7, ex.Column);
Assert.Equal("Unexpected ')'.", ex.Message);
}
}
}

29
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs

@ -281,5 +281,32 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
Assert.Equal(5.6, AttachedPropertyOwner.GetDouble(textBlock));
}
}
[Fact]
public void Binding_To_Attached_Property_In_Style_Works()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Xaml;assembly=Avalonia.Markup.Xaml.UnitTests'>
<Window.Styles>
<Style Selector='TextBlock'>
<Setter Property='local:TestControl.Double' Value='{Binding}'/>
</Style>
</Window.Styles>
<TextBlock/>
</Window>";
var loader = new AvaloniaXamlLoader();
var window = (Window)loader.Load(xaml);
var textBlock = (TextBlock)window.Content;
window.DataContext = 5.6;
window.ApplyTemplate();
Assert.Equal(5.6, AttachedPropertyOwner.GetDouble(textBlock));
}
}
}
}
}

54
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs

@ -7,6 +7,7 @@ using Avalonia.Markup.Xaml.Styling;
using Avalonia.Media;
using Avalonia.Styling;
using Avalonia.UnitTests;
using Portable.Xaml;
using Xunit;
namespace Avalonia.Markup.Xaml.UnitTests.Xaml
@ -146,5 +147,56 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
Assert.NotNull(target.FocusAdorner);
}
}
[Fact]
public void Setter_Can_Set_Attached_Property()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Xaml;assembly=Avalonia.Markup.Xaml.UnitTests'>
<Window.Styles>
<Style Selector='TextBlock'>
<Setter Property='DockPanel.Dock' Value='Right'/>
</Style>
</Window.Styles>
<TextBlock/>
</Window>";
var loader = new AvaloniaXamlLoader();
var window = (Window)loader.Load(xaml);
var textBlock = (TextBlock)window.Content;
window.ApplyTemplate();
Assert.Equal(Dock.Right, DockPanel.GetDock(textBlock));
}
}
[Fact(Skip = "The animation system currently needs to be able to set any property on any object")]
public void Disallows_Setting_Non_Registered_Property()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Xaml;assembly=Avalonia.Markup.Xaml.UnitTests'>
<Window.Styles>
<Style Selector='TextBlock'>
<Setter Property='Button.IsDefault' Value='True'/>
</Style>
</Window.Styles>
<TextBlock/>
</Window>";
var loader = new AvaloniaXamlLoader();
var ex = Assert.Throws<XamlObjectWriterException>(() => loader.Load(xaml));
Assert.Equal(
"Property 'Button.IsDefault' is not registered on 'Avalonia.Controls.TextBlock'.",
ex.InnerException.Message);
}
}
}
}
}

Loading…
Cancel
Save