From 3b5a2ecb01d7e26b455f327959f473d51881eebb Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Fri, 20 Jul 2018 13:22:13 -0500 Subject: [PATCH 1/8] Move binding syntax sugar parsing into the regular binding path parsing instead of being XAML-only. --- .../MarkupExtensions/BindingExtension.cs | 167 +----------------- src/Markup/Avalonia.Markup/Data/Binding.cs | 96 +++++----- .../Markup/Parsers/ArgumentListParser.cs | 10 +- .../Parsers/ExpressionObserverBuilder.cs | 12 +- .../Markup/Parsers/ExpressionParser.cs | 137 ++++++++++++-- .../Markup/Parsers/Nodes/ElementNameNode.cs | 39 ++++ .../Markup/Parsers/Nodes/FindAncestorNode.cs | 54 ++++++ .../Markup/Parsers/Nodes/SelfNode.cs | 12 ++ 8 files changed, 293 insertions(+), 234 deletions(-) create mode 100644 src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs create mode 100644 src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs create mode 100644 src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/SelfNode.cs diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs index 789f7675be..08ba26573f 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs +++ b/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(); + object anchor = context.GetFirstAmbientValue(); // 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. diff --git a/src/Markup/Avalonia.Markup/Data/Binding.cs b/src/Markup/Avalonia.Markup/Data/Binding.cs index e5f7ea1742..c62a232c7e 100644 --- a/src/Markup/Avalonia.Markup/Data/Binding.cs +++ b/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(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(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(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(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(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; } diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs index ae48657c01..d63c4a46ac 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs @@ -10,7 +10,7 @@ namespace Avalonia.Markup.Parsers { internal static class ArgumentListParser { - public static IList Parse(Reader r, char open, char close) + public static IList 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(); diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs index ddbe252fc0..ff1666930f 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs +++ b/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 typeResolver = null) + internal static (ExpressionNode Node, SourceMode Mode) Parse(string expression, bool enableValidation = false, Func 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(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(rootGetter != null); return new ExpressionObserver( - () => rootGetter(), - Parse(expression, enableDataValidation, typeResolver), + rootGetter, + Parse(expression, enableDataValidation, typeResolver).Node, update, description ?? expression); } diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs index 95bb421777..059be3cbe7 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs +++ b/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(); 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 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 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 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 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, diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs new file mode 100644 index 0000000000..bd70c1b222 --- /dev/null +++ b/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; + } + } +} diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs new file mode 100644 index 0000000000..6a5f5a5751 --- /dev/null +++ b/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; + } + } +} diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/SelfNode.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/SelfNode.cs new file mode 100644 index 0000000000..88163eda44 --- /dev/null +++ b/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"; + } +} From 16e274d29ae88961d26650dc10abde8d3e967313 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 20 Jul 2018 10:37:55 +0200 Subject: [PATCH 2/8] Added some unit tests for #1764. --- .../Xaml/BindingTests.cs | 29 ++++++++++++++++++- .../Xaml/StyleTests.cs | 28 +++++++++++++++++- 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs index 568c6482f5..f327e9ccf2 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs +++ b/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 = @" + + + + + +"; + 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)); + } + } } -} \ No newline at end of file +} diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs index 2c7e850fee..4517aa6aa1 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs @@ -146,5 +146,31 @@ 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 = @" + + + + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var textBlock = (TextBlock)window.Content; + + window.ApplyTemplate(); + + Assert.Equal(Dock.Right, DockPanel.GetDock(textBlock)); + } + } } -} \ No newline at end of file +} From ffcaa545bb64db5489de3cb135e4f831afb07a8f Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 20 Jul 2018 10:45:07 +0200 Subject: [PATCH 3/8] Added PropertyParser. So we don't need to use a regex to parse property strings. --- .../Avalonia.Markup.Xaml.csproj | 1 + .../Parsers/PropertyParser.cs | 84 +++++++ .../Markup/Parsers/IdentifierParser.cs | 2 +- .../Avalonia.Markup/Markup/Parsers/Reader.cs | 2 +- .../Parsers/PropertyParserTests.cs | 225 ++++++++++++++++++ 5 files changed, 312 insertions(+), 2 deletions(-) create mode 100644 src/Markup/Avalonia.Markup.Xaml/Parsers/PropertyParser.cs create mode 100644 tests/Avalonia.Markup.Xaml.UnitTests/Parsers/PropertyParserTests.cs diff --git a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj index cdc22f4102..8c843a4b49 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj +++ b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj @@ -33,6 +33,7 @@ + diff --git a/src/Markup/Avalonia.Markup.Xaml/Parsers/PropertyParser.cs b/src/Markup/Avalonia.Markup.Xaml/Parsers/PropertyParser.cs new file mode 100644 index 0000000000..ce82ffe0a1 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/Parsers/PropertyParser.cs @@ -0,0 +1,84 @@ +using System; +using Avalonia.Data.Core; +using Avalonia.Markup.Parsers; + +namespace Avalonia.Markup.Xaml.Parsers +{ + internal class PropertyParser + { + public (string ns, string owner, string name) Parse(Reader 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); + } + } +} diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/IdentifierParser.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/IdentifierParser.cs index f86f2db321..9431dab45e 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/IdentifierParser.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/IdentifierParser.cs @@ -6,7 +6,7 @@ using System.Text; namespace Avalonia.Markup.Parsers { - internal static class IdentifierParser + public static class IdentifierParser { public static string Parse(Reader r) { diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/Reader.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/Reader.cs index 9355bc9aa3..4a3d6aa277 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/Reader.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/Reader.cs @@ -5,7 +5,7 @@ using System; namespace Avalonia.Markup.Parsers { - internal class Reader + public class Reader { private readonly string _s; private int _i; diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Parsers/PropertyParserTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Parsers/PropertyParserTests.cs new file mode 100644 index 0000000000..a05485f55b --- /dev/null +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Parsers/PropertyParserTests.cs @@ -0,0 +1,225 @@ +using System; +using Avalonia.Data.Core; +using Avalonia.Markup.Parsers; +using Avalonia.Markup.Xaml.Parsers; +using Xunit; + +namespace Avalonia.Markup.Xaml.UnitTests.Parsers +{ + public class PropertyParserTests + { + [Fact] + public void Parses_Name() + { + var target = new PropertyParser(); + var reader = new Reader("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 Reader("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 Reader("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 Reader("(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 Reader("(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 Reader(""); + + var ex = Assert.Throws(() => 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 Reader(" "); + + var ex = Assert.Throws(() => 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 Reader(" Foo"); + + var ex = Assert.Throws(() => 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 Reader("Foo "); + + var ex = Assert.Throws(() => 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 Reader("123"); + + var ex = Assert.Throws(() => 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 Reader("Foo%"); + + var ex = Assert.Throws(() => 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 Reader("Foo.123"); + + var ex = Assert.Throws(() => 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 Reader("Foo. Bar"); + + var ex = Assert.Throws(() => 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 Reader("Foo.Bar.Baz"); + + var ex = Assert.Throws(() => 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 Reader("foo:bar:Baz"); + + var ex = Assert.Throws(() => 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 Reader("(Foo)"); + + var ex = Assert.Throws(() => 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 Reader("(foo:Bar)"); + + var ex = Assert.Throws(() => 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 Reader("(Foo.Bar"); + + var ex = Assert.Throws(() => 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 Reader("Foo.Bar)"); + + var ex = Assert.Throws(() => target.Parse(reader)); + Assert.Equal(7, ex.Column); + Assert.Equal("Unexpected ')'.", ex.Message); + } + } +} From 0b796adc53f923208b41991b6806141492dac8a2 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 20 Jul 2018 10:57:43 +0200 Subject: [PATCH 4/8] Fix a few issues with AvaloniaPropertyTypeConverter - Don't use regex to parse property strings, use `PropertyParser` - Handle XAML namespaces on attached properties Fixes #1764 --- .../AvaloniaPropertyTypeConverter.cs | 37 ++++++------------- 1 file changed, 11 insertions(+), 26 deletions(-) diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs index 63b7811dbc..6cdf0452d0 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs @@ -4,19 +4,16 @@ using System; using System.ComponentModel; using System.Globalization; -using System.Text.RegularExpressions; +using Avalonia.Markup.Parsers; +using Avalonia.Markup.Xaml.Parsers; using Avalonia.Markup.Xaml.Templates; using Avalonia.Styling; -using Portable.Xaml; 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,8 +21,10 @@ 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) ?? + var parser = new PropertyParser(); + var reader = new Reader((string)value); + var (ns, owner, propertyName) = parser.Parse(reader); + var ownerType = TryResolveOwnerByName(context, ns, owner) ?? context.GetFirstAmbientValue()?.TargetType ?? context.GetFirstAmbientValue + + +"; + var loader = new AvaloniaXamlLoader(); + var ex = Assert.Throws(() => loader.Load(xaml)); + + Assert.Equal( + "Property 'Button.IsDefault' is not registered on 'Avalonia.Controls.TextBlock'.", + ex.InnerException.Message); + } + } } } From e0be7353a88e37957173a2fd19df643b206eb4c2 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Fri, 20 Jul 2018 14:02:43 -0500 Subject: [PATCH 6/8] Fix tests that broke from the API change. --- .../Parsers/ExpressionNodeBuilderTests.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionNodeBuilderTests.cs b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionNodeBuilderTests.cs index 212b16965c..2d687ff4f7 100644 --- a/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionNodeBuilderTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionNodeBuilderTests.cs @@ -162,8 +162,9 @@ namespace Avalonia.Markup.UnitTests.Parsers Assert.Equal(e.Arguments.ToArray(), args); } - private List ToList(ExpressionNode node) + private List ToList((ExpressionNode node, SourceMode mode) parsed) { + var (node, _) = parsed; var result = new List(); while (node != null) From 62526bef3599681e08d9b6a2ed51d8b9316f0165 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 20 Jul 2018 21:38:14 +0200 Subject: [PATCH 7/8] Don't disallow setting unregistered properties. The changes made to the animation system in #1768 currently needs to be able to set any property on any object in order for animations on transforms to work. --- .../Converters/AvaloniaPropertyTypeConverter.cs | 9 ++++++++- tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs index 2470778685..0588e82901 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs @@ -5,6 +5,7 @@ using System; using System.ComponentModel; using System.Globalization; using Avalonia.Controls; +using Avalonia.Logging; using Avalonia.Markup.Parsers; using Avalonia.Markup.Xaml.Parsers; using Avalonia.Markup.Xaml.Templates; @@ -42,7 +43,13 @@ namespace Avalonia.Markup.Xaml.Converters !property.IsAttached && !registry.IsRegistered(targetType, property)) { - throw new XamlLoadException($"Property '{effectiveOwner.Name}.{propertyName}' is not registered on '{targetType}'."); + Logger.Warning( + LogArea.Property, + this, + "Property '{Owner}.{Name}' is not registered on '{Type}'.", + effectiveOwner, + propertyName, + targetType); } return property; diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs index 1d2f90d383..beaf7477d0 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs @@ -174,7 +174,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml } } - [Fact] + [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)) From dc6f14e77550bbf898d48a93f92ac4f1b1fafb8c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 20 Jul 2018 23:04:28 +0200 Subject: [PATCH 8/8] Reader -> CharacterReader And moved `CharacterReader` and `IdentifierParser` into Avalonia.Utilities. --- .../Utilities/CharacterReader.cs} | 6 +-- .../Utilities}/IdentifierParser.cs | 4 +- .../AvaloniaPropertyTypeConverter.cs | 3 +- .../Parsers/PropertyParser.cs | 3 +- .../Markup/Parsers/ArgumentListParser.cs | 3 +- .../Parsers/ExpressionObserverBuilder.cs | 3 +- .../Markup/Parsers/ExpressionParser.cs | 23 +++++------ .../Parsers/PropertyParserTests.cs | 39 ++++++++++--------- 8 files changed, 45 insertions(+), 39 deletions(-) rename src/{Markup/Avalonia.Markup/Markup/Parsers/Reader.cs => Avalonia.Base/Utilities/CharacterReader.cs} (89%) rename src/{Markup/Avalonia.Markup/Markup/Parsers => Avalonia.Base/Utilities}/IdentifierParser.cs (94%) diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/Reader.cs b/src/Avalonia.Base/Utilities/CharacterReader.cs similarity index 89% rename from src/Markup/Avalonia.Markup/Markup/Parsers/Reader.cs rename to src/Avalonia.Base/Utilities/CharacterReader.cs index 4a3d6aa277..0910d5b969 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/Reader.cs +++ b/src/Avalonia.Base/Utilities/CharacterReader.cs @@ -3,14 +3,14 @@ using System; -namespace Avalonia.Markup.Parsers +namespace Avalonia.Utilities { - public class Reader + public class CharacterReader { private readonly string _s; private int _i; - public Reader(string s) + public CharacterReader(string s) { _s = s; } diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/IdentifierParser.cs b/src/Avalonia.Base/Utilities/IdentifierParser.cs similarity index 94% rename from src/Markup/Avalonia.Markup/Markup/Parsers/IdentifierParser.cs rename to src/Avalonia.Base/Utilities/IdentifierParser.cs index 9431dab45e..14b8affbdd 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/IdentifierParser.cs +++ b/src/Avalonia.Base/Utilities/IdentifierParser.cs @@ -4,11 +4,11 @@ using System.Globalization; using System.Text; -namespace Avalonia.Markup.Parsers +namespace Avalonia.Utilities { public static class IdentifierParser { - public static string Parse(Reader r) + public static string Parse(CharacterReader r) { if (IsValidIdentifierStart(r.Peek)) { diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs index 0588e82901..627a646bcf 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs @@ -10,6 +10,7 @@ using Avalonia.Markup.Parsers; using Avalonia.Markup.Xaml.Parsers; using Avalonia.Markup.Xaml.Templates; using Avalonia.Styling; +using Avalonia.Utilities; using Portable.Xaml.ComponentModel; namespace Avalonia.Markup.Xaml.Converters @@ -25,7 +26,7 @@ namespace Avalonia.Markup.Xaml.Converters { var registry = AvaloniaPropertyRegistry.Instance; var parser = new PropertyParser(); - var reader = new Reader((string)value); + var reader = new CharacterReader((string)value); var (ns, owner, propertyName) = parser.Parse(reader); var ownerType = TryResolveOwnerByName(context, ns, owner); var targetType = context.GetFirstAmbientValue()?.TargetType ?? diff --git a/src/Markup/Avalonia.Markup.Xaml/Parsers/PropertyParser.cs b/src/Markup/Avalonia.Markup.Xaml/Parsers/PropertyParser.cs index ce82ffe0a1..702758efae 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Parsers/PropertyParser.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Parsers/PropertyParser.cs @@ -1,12 +1,13 @@ 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(Reader r) + public (string ns, string owner, string name) Parse(CharacterReader r) { if (r.End) { diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs index ae48657c01..89ef5dcabc 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs +++ b/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 Parse(Reader r, char open, char close) + public static IList Parse(CharacterReader r, char open, char close) { if (r.Peek == open) { diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs index ddbe252fc0..6cfb86634b 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs +++ b/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; @@ -15,7 +16,7 @@ namespace Avalonia.Markup.Parsers return new EmptyExpressionNode(); } - var reader = new Reader(expression); + var reader = new CharacterReader(expression); var parser = new ExpressionParser(enableValidation, typeResolver); var node = parser.Parse(reader); diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs index 95bb421777..682be572da 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs @@ -3,6 +3,7 @@ using Avalonia.Data.Core; using Avalonia.Markup.Parsers.Nodes; +using Avalonia.Utilities; using System; using System.Collections.Generic; using System.Linq; @@ -20,7 +21,7 @@ namespace Avalonia.Markup.Parsers _enableValidation = enableValidation; } - public ExpressionNode Parse(Reader r) + public ExpressionNode Parse(CharacterReader r) { var nodes = new List(); var state = State.Start; @@ -64,7 +65,7 @@ namespace Avalonia.Markup.Parsers return nodes.FirstOrDefault(); } - private State ParseStart(Reader r, IList nodes) + private State ParseStart(CharacterReader r, IList nodes) { if (ParseNot(r)) { @@ -93,7 +94,7 @@ namespace Avalonia.Markup.Parsers return State.End; } - private static State ParseAfterMember(Reader r, IList nodes) + private static State ParseAfterMember(CharacterReader r, IList nodes) { if (ParseMemberAccessor(r)) { @@ -112,7 +113,7 @@ namespace Avalonia.Markup.Parsers return State.End; } - private State ParseBeforeMember(Reader r, IList nodes) + private State ParseBeforeMember(CharacterReader r, IList nodes) { if (ParseOpenBrace(r)) { @@ -132,7 +133,7 @@ namespace Avalonia.Markup.Parsers } } - private State ParseAttachedProperty(Reader r, List nodes) + private State ParseAttachedProperty(CharacterReader r, List nodes) { string ns = string.Empty; string owner; @@ -171,7 +172,7 @@ namespace Avalonia.Markup.Parsers return State.AfterMember; } - private State ParseIndexer(Reader r, List nodes) + private State ParseIndexer(CharacterReader r, List nodes) { var args = ArgumentListParser.Parse(r, '[', ']'); @@ -184,27 +185,27 @@ namespace Avalonia.Markup.Parsers return State.AfterMember; } - private static bool ParseNot(Reader r) + 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('^'); } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Parsers/PropertyParserTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Parsers/PropertyParserTests.cs index a05485f55b..cae6449722 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Parsers/PropertyParserTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Parsers/PropertyParserTests.cs @@ -2,6 +2,7 @@ using Avalonia.Data.Core; using Avalonia.Markup.Parsers; using Avalonia.Markup.Xaml.Parsers; +using Avalonia.Utilities; using Xunit; namespace Avalonia.Markup.Xaml.UnitTests.Parsers @@ -12,7 +13,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Parses_Name() { var target = new PropertyParser(); - var reader = new Reader("Foo"); + var reader = new CharacterReader("Foo"); var (ns, owner, name) = target.Parse(reader); Assert.Null(ns); @@ -24,7 +25,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Parses_Owner_And_Name() { var target = new PropertyParser(); - var reader = new Reader("Foo.Bar"); + var reader = new CharacterReader("Foo.Bar"); var (ns, owner, name) = target.Parse(reader); Assert.Null(ns); @@ -36,7 +37,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Parses_Namespace_Owner_And_Name() { var target = new PropertyParser(); - var reader = new Reader("foo:Bar.Baz"); + var reader = new CharacterReader("foo:Bar.Baz"); var (ns, owner, name) = target.Parse(reader); Assert.Equal("foo", ns); @@ -48,7 +49,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Parses_Owner_And_Name_With_Parentheses() { var target = new PropertyParser(); - var reader = new Reader("(Foo.Bar)"); + var reader = new CharacterReader("(Foo.Bar)"); var (ns, owner, name) = target.Parse(reader); Assert.Null(ns); @@ -60,7 +61,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Parses_Namespace_Owner_And_Name_With_Parentheses() { var target = new PropertyParser(); - var reader = new Reader("(foo:Bar.Baz)"); + var reader = new CharacterReader("(foo:Bar.Baz)"); var (ns, owner, name) = target.Parse(reader); Assert.Equal("foo", ns); @@ -72,7 +73,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Fails_With_Empty_String() { var target = new PropertyParser(); - var reader = new Reader(""); + var reader = new CharacterReader(""); var ex = Assert.Throws(() => target.Parse(reader)); Assert.Equal(0, ex.Column); @@ -83,7 +84,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Fails_With_Only_Whitespace() { var target = new PropertyParser(); - var reader = new Reader(" "); + var reader = new CharacterReader(" "); var ex = Assert.Throws(() => target.Parse(reader)); Assert.Equal(0, ex.Column); @@ -94,7 +95,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Fails_With_Leading_Whitespace() { var target = new PropertyParser(); - var reader = new Reader(" Foo"); + var reader = new CharacterReader(" Foo"); var ex = Assert.Throws(() => target.Parse(reader)); Assert.Equal(0, ex.Column); @@ -105,7 +106,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Fails_With_Trailing_Whitespace() { var target = new PropertyParser(); - var reader = new Reader("Foo "); + var reader = new CharacterReader("Foo "); var ex = Assert.Throws(() => target.Parse(reader)); Assert.Equal(3, ex.Column); @@ -116,7 +117,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Fails_With_Invalid_Property_Name() { var target = new PropertyParser(); - var reader = new Reader("123"); + var reader = new CharacterReader("123"); var ex = Assert.Throws(() => target.Parse(reader)); Assert.Equal(0, ex.Column); @@ -127,7 +128,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Fails_With_Trailing_Junk() { var target = new PropertyParser(); - var reader = new Reader("Foo%"); + var reader = new CharacterReader("Foo%"); var ex = Assert.Throws(() => target.Parse(reader)); Assert.Equal(3, ex.Column); @@ -138,7 +139,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Fails_With_Invalid_Property_Name_After_Owner() { var target = new PropertyParser(); - var reader = new Reader("Foo.123"); + var reader = new CharacterReader("Foo.123"); var ex = Assert.Throws(() => target.Parse(reader)); Assert.Equal(4, ex.Column); @@ -149,7 +150,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Fails_With_Whitespace_Between_Owner_And_Name() { var target = new PropertyParser(); - var reader = new Reader("Foo. Bar"); + var reader = new CharacterReader("Foo. Bar"); var ex = Assert.Throws(() => target.Parse(reader)); Assert.Equal(4, ex.Column); @@ -160,7 +161,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Fails_With_Too_Many_Segments() { var target = new PropertyParser(); - var reader = new Reader("Foo.Bar.Baz"); + var reader = new CharacterReader("Foo.Bar.Baz"); var ex = Assert.Throws(() => target.Parse(reader)); Assert.Equal(8, ex.Column); @@ -171,7 +172,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Fails_With_Too_Many_Namespaces() { var target = new PropertyParser(); - var reader = new Reader("foo:bar:Baz"); + var reader = new CharacterReader("foo:bar:Baz"); var ex = Assert.Throws(() => target.Parse(reader)); Assert.Equal(8, ex.Column); @@ -182,7 +183,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Fails_With_Parens_But_No_Owner() { var target = new PropertyParser(); - var reader = new Reader("(Foo)"); + var reader = new CharacterReader("(Foo)"); var ex = Assert.Throws(() => target.Parse(reader)); Assert.Equal(1, ex.Column); @@ -193,7 +194,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Fails_With_Parens_And_Namespace_But_No_Owner() { var target = new PropertyParser(); - var reader = new Reader("(foo:Bar)"); + var reader = new CharacterReader("(foo:Bar)"); var ex = Assert.Throws(() => target.Parse(reader)); Assert.Equal(1, ex.Column); @@ -204,7 +205,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Fails_With_Missing_Close_Parens() { var target = new PropertyParser(); - var reader = new Reader("(Foo.Bar"); + var reader = new CharacterReader("(Foo.Bar"); var ex = Assert.Throws(() => target.Parse(reader)); Assert.Equal(8, ex.Column); @@ -215,7 +216,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Fails_With_Unexpected_Close_Parens() { var target = new PropertyParser(); - var reader = new Reader("Foo.Bar)"); + var reader = new CharacterReader("Foo.Bar)"); var ex = Assert.Throws(() => target.Parse(reader)); Assert.Equal(7, ex.Column);