Browse Source

Merge branch 'master' into RelativeSource-ExpressionNodes

pull/1772/head
Jeremy Koritzinsky 8 years ago
committed by GitHub
parent
commit
15a844c99d
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. 85
      src/Markup/Avalonia.Markup.Xaml/Parsers/PropertyParser.cs
  7. 3
      src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs
  8. 3
      src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs
  9. 39
      src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs
  10. 11
      tests/Avalonia.Base.UnitTests/AvaloniaPropertyRegistryTests.cs
  11. 36
      tests/Avalonia.Markup.Xaml.UnitTests/Converters/AvaloniaPropertyConverterTest.cs
  12. 226
      tests/Avalonia.Markup.Xaml.UnitTests/Parsers/PropertyParserTests.cs
  13. 29
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs
  14. 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);
}
}
}
}
}

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);
}
}
}

3
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, char delimiter = ',')
public static IList<string> Parse(CharacterReader r, char open, char close, char delimiter = ',')
{
if (r.Peek == open)
{

3
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(), default);
}
var reader = new Reader(expression);
var reader = new CharacterReader(expression);
var parser = new ExpressionParser(enableValidation, typeResolver);
var node = parser.Parse(reader);

39
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;
@ -26,7 +27,7 @@ namespace Avalonia.Markup.Parsers
_enableValidation = enableValidation;
}
public (ExpressionNode Node, SourceMode Mode) Parse(Reader r)
public (ExpressionNode Node, SourceMode Mode) Parse(CharacterReader r)
{
var nodes = new List<ExpressionNode>();
var state = State.Start;
@ -81,7 +82,7 @@ namespace Avalonia.Markup.Parsers
return (nodes.FirstOrDefault(), mode);
}
private State ParseStart(Reader r, IList<ExpressionNode> nodes)
private State ParseStart(CharacterReader r, IList<ExpressionNode> nodes)
{
if (ParseNot(r))
{
@ -118,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))
{
@ -137,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))
{
@ -157,7 +158,7 @@ namespace Avalonia.Markup.Parsers
}
}
private State ParseAttachedProperty(Reader r, List<ExpressionNode> nodes)
private State ParseAttachedProperty(CharacterReader r, List<ExpressionNode> nodes)
{
var (ns, owner) = ParseTypeName(r);
@ -184,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, '[', ']');
@ -197,7 +198,7 @@ namespace Avalonia.Markup.Parsers
return State.AfterMember;
}
private State ParseElementName(Reader r, List<ExpressionNode> nodes)
private State ParseElementName(CharacterReader r, List<ExpressionNode> nodes)
{
var name = IdentifierParser.Parse(r);
@ -211,7 +212,7 @@ namespace Avalonia.Markup.Parsers
}
private State ParseRelativeSource(Reader r, List<ExpressionNode> nodes)
private State ParseRelativeSource(CharacterReader r, List<ExpressionNode> nodes)
{
var mode = IdentifierParser.Parse(r);
@ -239,13 +240,13 @@ namespace Avalonia.Markup.Parsers
}
else
{
var typeName = ParseTypeName(new Reader(args[0]));
var typeName = ParseTypeName(new CharacterReader(args[0]));
ancestorType = _typeResolver(typeName.ns, typeName.typeName);
}
}
else
{
var typeName = ParseTypeName(new Reader(args[0]));
var typeName = ParseTypeName(new CharacterReader(args[0]));
ancestorType = _typeResolver(typeName.ns, typeName.typeName);
ancestorLevel = int.Parse(args[1]);
}
@ -260,7 +261,7 @@ namespace Avalonia.Markup.Parsers
return State.AfterMember;
}
private static (string ns, string typeName) ParseTypeName(Reader r)
private static (string ns, string typeName) ParseTypeName(CharacterReader r)
{
string ns, typeName;
ns = string.Empty;
@ -278,38 +279,38 @@ namespace Avalonia.Markup.Parsers
return (ns, typeName);
}
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('^');
}
private static bool ParseDollarSign(Reader r)
private static bool ParseDollarSign(CharacterReader r)
{
return !r.End && r.TakeIf('$');
}
private static bool ParseSharp(Reader r)
private static bool ParseSharp(CharacterReader r)
{
return !r.End && r.TakeIf('#');
}

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 =

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