Browse Source

Move binding syntax sugar parsing into the regular binding path parsing instead of being XAML-only.

pull/1772/head
Jeremy Koritzinsky 8 years ago
parent
commit
3b5a2ecb01
  1. 167
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs
  2. 96
      src/Markup/Avalonia.Markup/Data/Binding.cs
  3. 10
      src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs
  4. 12
      src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs
  5. 137
      src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs
  6. 39
      src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs
  7. 54
      src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs
  8. 12
      src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/SelfNode.cs

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.

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

@ -105,34 +105,51 @@ 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, Path, enableDataValidation);
observer = CreateTemplatedParentObserver(target, node);
}
else if (RelativeSource.Mode == RelativeSourceMode.FindAncestor)
{
@ -144,8 +161,7 @@ namespace Avalonia.Data
observer = CreateFindAncestorObserver(
(target as IStyledElement) ?? (anchor as IStyledElement),
RelativeSource,
Path,
enableDataValidation);
node);
}
else
{
@ -176,10 +192,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);
@ -195,48 +210,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);
@ -260,36 +268,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;
}

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

@ -10,7 +10,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(Reader r, char open, char close, char delimiter = ',')
{
if (r.Peek == open)
{
@ -21,7 +21,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 +35,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 +43,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();

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

@ -8,11 +8,11 @@ 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);
@ -36,7 +36,7 @@ namespace Avalonia.Markup.Parsers
{
return new ExpressionObserver(
root,
Parse(expression, enableDataValidation, typeResolver),
Parse(expression, enableDataValidation, typeResolver).Node,
description ?? expression);
}
@ -50,7 +50,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 +66,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);
}

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

@ -9,6 +9,12 @@ using System.Linq;
namespace Avalonia.Markup.Parsers
{
internal enum SourceMode
{
Data,
Control
}
internal class ExpressionParser
{
private readonly bool _enableValidation;
@ -20,10 +26,11 @@ namespace Avalonia.Markup.Parsers
_enableValidation = enableValidation;
}
public ExpressionNode Parse(Reader r)
public (ExpressionNode Node, SourceMode Mode) Parse(Reader r)
{
var nodes = new List<ExpressionNode>();
var state = State.Start;
var mode = SourceMode.Data;
while (!r.End && state != State.End)
{
@ -48,6 +55,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,7 +78,7 @@ 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)
@ -71,6 +88,14 @@ namespace Avalonia.Markup.Parsers
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;
@ -134,19 +159,7 @@ namespace Avalonia.Markup.Parsers
private State ParseAttachedProperty(Reader 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('.'))
{
@ -184,6 +197,88 @@ namespace Avalonia.Markup.Parsers
return State.AfterMember;
}
private State ParseElementName(Reader 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(Reader 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 Reader(args[0]));
ancestorType = _typeResolver(typeName.ns, typeName.typeName);
}
}
else
{
var typeName = ParseTypeName(new Reader(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(Reader 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(Reader r)
{
return !r.End && r.TakeIf('!');
@ -209,9 +304,21 @@ namespace Avalonia.Markup.Parsers
return !r.End && r.TakeIf('^');
}
private static bool ParseDollarSign(Reader r)
{
return !r.End && r.TakeIf('$');
}
private static bool ParseSharp(Reader 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";
}
}
Loading…
Cancel
Save