Browse Source

Merge branch 'master' into fix/numericParsing

pull/1539/head
Eli Arbel 8 years ago
committed by GitHub
parent
commit
7f62efcd47
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 9
      src/Avalonia.Base/AttachedProperty.cs
  2. 50
      src/Avalonia.Base/AvaloniaObject.cs
  3. 8
      src/Avalonia.Base/AvaloniaProperty.cs
  4. 283
      src/Avalonia.Base/AvaloniaPropertyRegistry.cs
  5. 3
      src/Avalonia.Base/DirectProperty.cs
  6. 7
      src/Avalonia.Base/IDirectPropertyAccessor.cs
  7. 4
      src/Avalonia.Controls/Primitives/TemplatedControl.cs
  8. 5
      src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs
  9. 3
      src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlSchemaContext.cs
  10. 22
      src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlType.cs
  11. 55
      src/Markup/Avalonia.Markup/Data/Plugins/AvaloniaPropertyAccessorPlugin.cs
  12. 52
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_AddOwner.cs
  13. 52
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Attached.cs
  14. 9
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs
  15. 4
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_GetValue.cs
  16. 39
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetValue.cs
  17. 106
      tests/Avalonia.Base.UnitTests/AvaloniaPropertyRegistryTests.cs
  18. 14
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/AttachedPropertyOwner.cs
  19. 19
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs
  20. 66
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs
  21. 17
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/TestControl.cs

9
src/Avalonia.Base/AttachedProperty.cs

@ -9,7 +9,7 @@ namespace Avalonia
/// An attached avalonia property.
/// </summary>
/// <typeparam name="TValue">The type of the property's value.</typeparam>
public class AttachedProperty<TValue> : StyledPropertyBase<TValue>
public class AttachedProperty<TValue> : StyledProperty<TValue>
{
/// <summary>
/// Initializes a new instance of the <see cref="AttachedProperty{TValue}"/> class.
@ -35,11 +35,10 @@ namespace Avalonia
/// </summary>
/// <typeparam name="TOwner">The owner type.</typeparam>
/// <returns>The property.</returns>
public StyledProperty<TValue> AddOwner<TOwner>() where TOwner : IAvaloniaObject
public new AttachedProperty<TValue> AddOwner<TOwner>() where TOwner : IAvaloniaObject
{
var result = new StyledProperty<TValue>(this, typeof(TOwner));
AvaloniaPropertyRegistry.Instance.Register(typeof(TOwner), result);
return result;
AvaloniaPropertyRegistry.Instance.Register(typeof(TOwner), this);
return this;
}
}
}

50
src/Avalonia.Base/AvaloniaObject.cs

@ -12,7 +12,6 @@ using Avalonia.Diagnostics;
using Avalonia.Logging;
using Avalonia.Threading;
using Avalonia.Utilities;
using System.Reactive.Concurrency;
namespace Avalonia
{
@ -218,11 +217,6 @@ namespace Avalonia
}
else
{
if (!AvaloniaPropertyRegistry.Instance.IsRegistered(this, property))
{
ThrowNotRegistered(property);
}
return GetValueInternal(property);
}
}
@ -377,11 +371,6 @@ namespace Avalonia
{
PriorityValue v;
if (!AvaloniaPropertyRegistry.Instance.IsRegistered(this, property))
{
ThrowNotRegistered(property);
}
if (!_values.TryGetValue(property, out v))
{
v = CreatePriorityValue(property);
@ -804,11 +793,6 @@ namespace Avalonia
var originalValue = value;
if (!AvaloniaPropertyRegistry.Instance.IsRegistered(this, property))
{
ThrowNotRegistered(property);
}
if (!TypeUtilities.TryConvertImplicit(property.PropertyType, value, out value))
{
throw new ArgumentException(string.Format(
@ -836,18 +820,32 @@ namespace Avalonia
}
/// <summary>
/// Given a <see cref="AvaloniaProperty"/> returns a registered avalonia property that is
/// equal or throws if not found.
/// Given a direct property, returns a registered avalonia property that is equivalent or
/// throws if not found.
/// </summary>
/// <param name="property">The property.</param>
/// <returns>The registered property.</returns>
public AvaloniaProperty GetRegistered(AvaloniaProperty property)
private AvaloniaProperty GetRegistered(AvaloniaProperty property)
{
var result = AvaloniaPropertyRegistry.Instance.FindRegistered(this, property);
var direct = property as IDirectPropertyAccessor;
if (direct == null)
{
throw new AvaloniaInternalException(
"AvaloniaObject.GetRegistered should only be called for direct properties");
}
if (property.OwnerType.IsAssignableFrom(GetType()))
{
return property;
}
var result = AvaloniaPropertyRegistry.Instance.GetRegistered(this)
.FirstOrDefault(x => x == property);
if (result == null)
{
ThrowNotRegistered(property);
throw new ArgumentException($"Property '{property.Name} not registered on '{this.GetType()}");
}
return result;
@ -898,15 +896,5 @@ namespace Avalonia
value,
priority);
}
/// <summary>
/// Throws an exception indicating that the specified property is not registered on this
/// object.
/// </summary>
/// <param name="p">The property</param>
private void ThrowNotRegistered(AvaloniaProperty p)
{
throw new ArgumentException($"Property '{p.Name} not registered on '{this.GetType()}");
}
}
}

8
src/Avalonia.Base/AvaloniaProperty.cs

@ -311,7 +311,9 @@ namespace Avalonia
defaultBindingMode: defaultBindingMode);
var result = new AttachedProperty<TValue>(name, typeof(TOwner), metadata, inherits);
AvaloniaPropertyRegistry.Instance.Register(typeof(THost), result);
var registry = AvaloniaPropertyRegistry.Instance;
registry.Register(typeof(TOwner), result);
registry.RegisterAttached(typeof(THost), result);
return result;
}
@ -344,7 +346,9 @@ namespace Avalonia
defaultBindingMode: defaultBindingMode);
var result = new AttachedProperty<TValue>(name, ownerType, metadata, inherits);
AvaloniaPropertyRegistry.Instance.Register(typeof(THost), result);
var registry = AvaloniaPropertyRegistry.Instance;
registry.Register(ownerType, result);
registry.RegisterAttached(typeof(THost), result);
return result;
}

283
src/Avalonia.Base/AvaloniaPropertyRegistry.cs

@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
namespace Avalonia
@ -14,23 +13,14 @@ namespace Avalonia
/// </summary>
public class AvaloniaPropertyRegistry
{
/// <summary>
/// The registered properties by type.
/// </summary>
private readonly Dictionary<Type, Dictionary<int, AvaloniaProperty>> _registered =
new Dictionary<Type, Dictionary<int, AvaloniaProperty>>();
/// <summary>
/// The registered properties by type cached values to increase performance.
/// </summary>
private readonly Dictionary<Type, Dictionary<int, AvaloniaProperty>> _registeredCache =
new Dictionary<Type, Dictionary<int, AvaloniaProperty>>();
/// <summary>
/// The registered attached properties by owner type.
/// </summary>
private readonly Dictionary<Type, Dictionary<int, AvaloniaProperty>> _attached =
new Dictionary<Type, Dictionary<int, AvaloniaProperty>>();
private readonly Dictionary<Type, List<AvaloniaProperty>> _registeredCache =
new Dictionary<Type, List<AvaloniaProperty>>();
private readonly Dictionary<Type, List<AvaloniaProperty>> _attachedCache =
new Dictionary<Type, List<AvaloniaProperty>>();
/// <summary>
/// Gets the <see cref="AvaloniaPropertyRegistry"/> instance
@ -39,51 +29,68 @@ namespace Avalonia
= new AvaloniaPropertyRegistry();
/// <summary>
/// Gets all attached <see cref="AvaloniaProperty"/>s registered by an owner.
/// Gets all non-attached <see cref="AvaloniaProperty"/>s registered on a type.
/// </summary>
/// <param name="ownerType">The owner type.</param>
/// <param name="type">The type.</param>
/// <returns>A collection of <see cref="AvaloniaProperty"/> definitions.</returns>
public IEnumerable<AvaloniaProperty> GetAttached(Type ownerType)
public IEnumerable<AvaloniaProperty> GetRegistered(Type type)
{
Dictionary<int, AvaloniaProperty> inner;
Contract.Requires<ArgumentNullException>(type != null);
if (_registeredCache.TryGetValue(type, out var result))
{
return result;
}
// Ensure the type's static ctor has been run.
RuntimeHelpers.RunClassConstructor(ownerType.TypeHandle);
var t = type;
result = new List<AvaloniaProperty>();
if (_attached.TryGetValue(ownerType, out inner))
while (t != null)
{
return inner.Values;
// Ensure the type's static ctor has been run.
RuntimeHelpers.RunClassConstructor(t.TypeHandle);
if (_registered.TryGetValue(t, out var registered))
{
result.AddRange(registered.Values);
}
t = t.BaseType;
}
return Enumerable.Empty<AvaloniaProperty>();
_registeredCache.Add(type, result);
return result;
}
/// <summary>
/// Gets all <see cref="AvaloniaProperty"/>s registered on a type.
/// Gets all attached <see cref="AvaloniaProperty"/>s registered on a type.
/// </summary>
/// <param name="type">The type.</param>
/// <returns>A collection of <see cref="AvaloniaProperty"/> definitions.</returns>
public IEnumerable<AvaloniaProperty> GetRegistered(Type type)
public IEnumerable<AvaloniaProperty> GetRegisteredAttached(Type type)
{
Contract.Requires<ArgumentNullException>(type != null);
while (type != null)
if (_attachedCache.TryGetValue(type, out var result))
{
// Ensure the type's static ctor has been run.
RuntimeHelpers.RunClassConstructor(type.TypeHandle);
return result;
}
Dictionary<int, AvaloniaProperty> inner;
var t = type;
result = new List<AvaloniaProperty>();
if (_registered.TryGetValue(type, out inner))
while (t != null)
{
if (_attached.TryGetValue(t, out var attached))
{
foreach (var p in inner)
{
yield return p.Value;
}
result.AddRange(attached.Values);
}
type = type.GetTypeInfo().BaseType;
t = t.BaseType;
}
_attachedCache.Add(type, result);
return result;
}
/// <summary>
@ -99,142 +106,92 @@ namespace Avalonia
}
/// <summary>
/// Finds a <see cref="AvaloniaProperty"/> registered on a type.
/// Finds a registered non-attached property on a type by name.
/// </summary>
/// <param name="type">The type.</param>
/// <param name="property">The property.</param>
/// <returns>The registered property or null if not found.</returns>
/// <remarks>
/// Calling AddOwner on a AvaloniaProperty creates a new AvaloniaProperty that is a
/// different object but is equal according to <see cref="object.Equals(object)"/>.
/// </remarks>
public AvaloniaProperty FindRegistered(Type type, AvaloniaProperty property)
/// <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 FindRegistered(Type type, string name)
{
Type currentType = type;
Dictionary<int, AvaloniaProperty> cache;
AvaloniaProperty result;
Contract.Requires<ArgumentNullException>(type != null);
Contract.Requires<ArgumentNullException>(name != null);
if (_registeredCache.TryGetValue(type, out cache))
if (name.Contains('.'))
{
if (cache.TryGetValue(property.Id, out result))
{
return result;
}
throw new InvalidOperationException("Attached properties not supported.");
}
while (currentType != null)
{
Dictionary<int, AvaloniaProperty> inner;
if (_registered.TryGetValue(currentType, out inner))
{
if (inner.TryGetValue(property.Id, out result))
{
if (cache == null)
{
_registeredCache[type] = cache = new Dictionary<int, AvaloniaProperty>();
}
cache[property.Id] = result;
return result;
}
}
currentType = currentType.GetTypeInfo().BaseType;
}
return null;
return GetRegistered(type).FirstOrDefault(x => x.Name == name);
}
/// <summary>
/// Finds <see cref="AvaloniaProperty"/> registered on an object.
/// Finds a registered non-attached property on a type by name.
/// </summary>
/// <param name="o">The object.</param>
/// <param name="property">The property.</param>
/// <returns>The registered property or null if not found.</returns>
/// <remarks>
/// Calling AddOwner on a AvaloniaProperty creates a new AvaloniaProperty that is a
/// different object but is equal according to <see cref="object.Equals(object)"/>.
/// </remarks>
public AvaloniaProperty FindRegistered(object o, AvaloniaProperty property)
/// <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 FindRegistered(AvaloniaObject o, string name)
{
return FindRegistered(o.GetType(), property);
Contract.Requires<ArgumentNullException>(o != null);
Contract.Requires<ArgumentNullException>(name != null);
return FindRegistered(o.GetType(), name);
}
/// <summary>
/// Finds a registered property on a type by name.
/// Finds a registered attached property on a type by name.
/// </summary>
/// <param name="type">The type.</param>
/// <param name="name">
/// The property name. If an attached property it should be in the form
/// "OwnerType.PropertyName".
/// </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>
public AvaloniaProperty FindRegistered(Type type, string name)
/// <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);
var parts = name.Split('.');
var types = GetImplementedTypes(type).ToList();
if (parts.Length < 1 || parts.Length > 2)
if (name.Contains('.'))
{
throw new ArgumentException("Invalid property name.");
throw new InvalidOperationException("Attached properties not supported.");
}
string propertyName;
var results = GetRegistered(type);
if (parts.Length == 1)
{
propertyName = parts[0];
results = results.Where(x => !x.IsAttached || types.Contains(x.OwnerType.Name));
}
else
{
if (!types.Contains(parts[0]))
{
results = results.Where(x => x.OwnerType.Name == parts[0]);
}
propertyName = parts[1];
}
return results.FirstOrDefault(x => x.Name == propertyName);
return GetRegisteredAttached(type).FirstOrDefault(x => x.Name == name);
}
/// <summary>
/// Finds a registered property on an object by name.
/// Finds a registered non-attached property on a type by name.
/// </summary>
/// <param name="o">The object.</param>
/// <param name="name">
/// The property name. If an attached property it should be in the form
/// "OwnerType.PropertyName".
/// </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>
public AvaloniaProperty FindRegistered(AvaloniaObject o, string name)
/// <exception cref="InvalidOperationException">
/// The property name contains a '.'.
/// </exception>
public AvaloniaProperty FindRegisteredAttached(AvaloniaObject o, Type ownerType, string name)
{
return FindRegistered(o.GetType(), name);
}
Contract.Requires<ArgumentNullException>(o != null);
Contract.Requires<ArgumentNullException>(name != null);
/// <summary>
/// Returns a type and all its base types.
/// </summary>
/// <param name="type">The type.</param>
/// <returns>The type and all its base types.</returns>
private IEnumerable<string> GetImplementedTypes(Type type)
{
while (type != null)
{
yield return type.Name;
type = type.GetTypeInfo().BaseType;
}
return FindRegisteredAttached(o.GetType(), ownerType, name);
}
/// <summary>
@ -245,7 +202,11 @@ namespace Avalonia
/// <returns>True if the property is registered, otherwise false.</returns>
public bool IsRegistered(Type type, AvaloniaProperty property)
{
return FindRegistered(type, property) != null;
Contract.Requires<ArgumentNullException>(type != null);
Contract.Requires<ArgumentNullException>(property != null);
return Instance.GetRegistered(type).Any(x => x == property) ||
Instance.GetRegisteredAttached(type).Any(x => x == property);
}
/// <summary>
@ -256,6 +217,9 @@ namespace Avalonia
/// <returns>True if the property is registered, otherwise false.</returns>
public bool IsRegistered(object o, AvaloniaProperty property)
{
Contract.Requires<ArgumentNullException>(o != null);
Contract.Requires<ArgumentNullException>(property != null);
return IsRegistered(o.GetType(), property);
}
@ -274,34 +238,53 @@ namespace Avalonia
Contract.Requires<ArgumentNullException>(type != null);
Contract.Requires<ArgumentNullException>(property != null);
Dictionary<int, AvaloniaProperty> inner;
if (!_registered.TryGetValue(type, out inner))
if (!_registered.TryGetValue(type, out var inner))
{
inner = new Dictionary<int, AvaloniaProperty>();
inner.Add(property.Id, property);
_registered.Add(type, inner);
}
if (!inner.ContainsKey(property.Id))
else if (!inner.ContainsKey(property.Id))
{
inner.Add(property.Id, property);
}
_registeredCache.Clear();
}
if (property.IsAttached)
/// <summary>
/// Registers an attached <see cref="AvaloniaProperty"/> on a type.
/// </summary>
/// <param name="type">The type.</param>
/// <param name="property">The property.</param>
/// <remarks>
/// You won't usually want to call this method directly, instead use the
/// <see cref="AvaloniaProperty.RegisterAttached{THost, TValue}(string, Type, TValue, bool, Data.BindingMode, Func{THost, TValue, TValue})"/>
/// method.
/// </remarks>
public void RegisterAttached(Type type, AvaloniaProperty property)
{
Contract.Requires<ArgumentNullException>(type != null);
Contract.Requires<ArgumentNullException>(property != null);
if (!property.IsAttached)
{
if (!_attached.TryGetValue(property.OwnerType, out inner))
{
inner = new Dictionary<int, AvaloniaProperty>();
_attached.Add(property.OwnerType, inner);
}
throw new InvalidOperationException(
"Cannot register a non-attached property as attached.");
}
if (!inner.ContainsKey(property.Id))
{
inner.Add(property.Id, property);
}
if (!_attached.TryGetValue(type, out var inner))
{
inner = new Dictionary<int, AvaloniaProperty>();
inner.Add(property.Id, property);
_attached.Add(type, inner);
}
else
{
inner.Add(property.Id, property);
}
_registeredCache.Clear();
_attachedCache.Clear();
}
}
}

3
src/Avalonia.Base/DirectProperty.cs

@ -75,6 +75,9 @@ namespace Avalonia
/// </summary>
public Action<TOwner, TValue> Setter { get; }
/// <inheritdoc/>
Type IDirectPropertyAccessor.Owner => typeof(TOwner);
/// <summary>
/// Registers the direct property on another type.
/// </summary>

7
src/Avalonia.Base/IDirectPropertyAccessor.cs

@ -1,6 +1,8 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
namespace Avalonia
{
/// <summary>
@ -14,6 +16,11 @@ namespace Avalonia
/// </summary>
bool IsReadOnly { get; }
/// <summary>
/// Gets the class that registered the property.
/// </summary>
Type Owner { get; }
/// <summary>
/// Gets the value of the property on the instance.
/// </summary>

4
src/Avalonia.Controls/Primitives/TemplatedControl.cs

@ -207,7 +207,7 @@ namespace Avalonia.Controls.Primitives
/// <param name="control">The control.</param>
/// <returns>The property value.</returns>
/// <see cref="SetIsTemplateFocusTarget(Control, bool)"/>
public bool GetIsTemplateFocusTarget(Control control)
public static bool GetIsTemplateFocusTarget(Control control)
{
return control.GetValue(IsTemplateFocusTargetProperty);
}
@ -223,7 +223,7 @@ namespace Avalonia.Controls.Primitives
/// attached property is set to true on an element in the control template, then the focus
/// adorner will be shown around that control instead.
/// </remarks>
public void SetIsTemplateFocusTarget(Control control, bool value)
public static void SetIsTemplateFocusTarget(Control control, bool value)
{
control.SetValue(IsTemplateFocusTargetProperty, value);
}

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

@ -53,10 +53,7 @@ namespace Avalonia.Markup.Xaml.Converters
}
}
// First look for non-attached property on the type and then look for an attached property.
var property = AvaloniaPropertyRegistry.Instance.FindRegistered(type, s) ??
AvaloniaPropertyRegistry.Instance.GetAttached(type)
.FirstOrDefault(x => x.Name == propertyName);
AvaloniaProperty property = AvaloniaPropertyRegistry.Instance.FindRegistered(type, propertyName);
if (property == null)
{

3
src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlSchemaContext.cs

@ -200,8 +200,7 @@ namespace Avalonia.Markup.Xaml.PortableXaml
var type = (getter ?? setter).DeclaringType;
var prop = AvaloniaPropertyRegistry.Instance.GetAttached(type)
.FirstOrDefault(v => v.Name == attachablePropertyName);
var prop = AvaloniaPropertyRegistry.Instance.FindRegistered(type, attachablePropertyName);
if (prop != null)
{

22
src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlType.cs

@ -19,16 +19,36 @@ namespace Avalonia.Markup.Xaml.PortableXaml
public class AvaloniaXamlType : XamlType
{
static readonly AvaloniaPropertyTypeConverter propertyTypeConverter = new AvaloniaPropertyTypeConverter();
public AvaloniaXamlType(Type underlyingType, XamlSchemaContext schemaContext) :
base(underlyingType, schemaContext)
{
}
protected override XamlMember LookupAttachableMember(string name)
{
var m = base.LookupAttachableMember(name);
if (m == null)
{
// Might be an AddOwnered attached property.
var avProp = AvaloniaPropertyRegistry.Instance.FindRegistered(UnderlyingType, name);
if (avProp?.IsAttached == true)
{
return new AvaloniaPropertyXamlMember(avProp, this);
}
}
return m;
}
protected override XamlMember LookupMember(string name, bool skipReadOnlyCheck)
{
var m = base.LookupMember(name, skipReadOnlyCheck);
if (m == null)
if (m == null && !name.Contains("."))
{
//so far Portable.xaml haven't found the member/property
//but what if we have AvaloniaProperty

55
src/Markup/Avalonia.Markup/Data/Plugins/AvaloniaPropertyAccessorPlugin.cs

@ -2,6 +2,7 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Linq;
using System.Reactive.Linq;
using Avalonia.Data;
@ -15,9 +16,9 @@ namespace Avalonia.Markup.Data.Plugins
/// <inheritdoc/>
public bool Match(object obj, string propertyName)
{
if (obj is AvaloniaObject a)
if (obj is AvaloniaObject o)
{
return AvaloniaPropertyRegistry.Instance.FindRegistered(a, propertyName) != null;
return LookupProperty(o, propertyName) != null;
}
return false;
@ -39,7 +40,7 @@ namespace Avalonia.Markup.Data.Plugins
var instance = reference.Target;
var o = (AvaloniaObject)instance;
var p = AvaloniaPropertyRegistry.Instance.FindRegistered(o, propertyName);
var p = LookupProperty(o, propertyName);
if (p != null)
{
@ -57,6 +58,54 @@ namespace Avalonia.Markup.Data.Plugins
}
}
private static AvaloniaProperty LookupProperty(AvaloniaObject o, string propertyName)
{
if (!propertyName.Contains("."))
{
return AvaloniaPropertyRegistry.Instance.FindRegistered(o, propertyName);
}
else
{
var split = propertyName.Split('.');
if (split.Length == 2)
{
// HACK: We need a way to resolve types here using something like IXamlTypeResolver.
// We don't currently have that so we have to make our best guess.
var type = split[0];
var name = split[1];
var registry = AvaloniaPropertyRegistry.Instance;
var registered = registry.GetRegisteredAttached(o.GetType())
.Concat(registry.GetRegistered(o.GetType()));
foreach (var p in registered)
{
if (p.Name == name && IsOfType(p.OwnerType, type))
{
return p;
}
}
}
}
return null;
}
private static bool IsOfType(Type type, string typeName)
{
while (type != null)
{
if (type.Name == typeName)
{
return true;
}
type = type.BaseType;
}
return false;
}
private class Accessor : PropertyAccessorBase
{
private readonly WeakReference<AvaloniaObject> _reference;

52
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_AddOwner.cs

@ -0,0 +1,52 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using Xunit;
namespace Avalonia.Base.UnitTests
{
public class AvaloniaObjectTests_AddOwner
{
[Fact]
public void AddOwnered_Property_Retains_Default_Value()
{
var target = new Class2();
Assert.Equal("foodefault", target.GetValue(Class2.FooProperty));
}
[Fact]
public void AddOwnered_Property_Does_Not_Retain_Validation()
{
var target = new Class2();
target.SetValue(Class2.FooProperty, "throw");
}
private class Class1 : AvaloniaObject
{
public static readonly StyledProperty<string> FooProperty =
AvaloniaProperty.Register<Class1, string>(
"Foo",
"foodefault",
validate: ValidateFoo);
private static string ValidateFoo(AvaloniaObject arg1, string arg2)
{
if (arg2 == "throw")
{
throw new IndexOutOfRangeException();
}
return arg2;
}
}
private class Class2 : AvaloniaObject
{
public static readonly StyledProperty<string> FooProperty =
Class1.FooProperty.AddOwner<Class2>();
}
}
}

52
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Attached.cs

@ -0,0 +1,52 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using Xunit;
namespace Avalonia.Base.UnitTests
{
public class AvaloniaObjectTests_Attached
{
[Fact]
public void AddOwnered_Property_Retains_Default_Value()
{
var target = new Class2();
Assert.Equal("foodefault", target.GetValue(Class2.FooProperty));
}
[Fact]
public void AddOwnered_Property_Retains_Validation()
{
var target = new Class2();
Assert.Throws<IndexOutOfRangeException>(() => target.SetValue(Class2.FooProperty, "throw"));
}
private class Class1 : AvaloniaObject
{
public static readonly AttachedProperty<string> FooProperty =
AvaloniaProperty.RegisterAttached<Class1, Class2, string>(
"Foo",
"foodefault",
validate: ValidateFoo);
private static string ValidateFoo(AvaloniaObject arg1, string arg2)
{
if (arg2 == "throw")
{
throw new IndexOutOfRangeException();
}
return arg2;
}
}
private class Class2 : AvaloniaObject
{
public static readonly AttachedProperty<string> FooProperty =
Class1.FooProperty.AddOwner<Class2>();
}
}
}

9
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs

@ -92,14 +92,13 @@ namespace Avalonia.Base.UnitTests
}
[Fact]
public void Bind_Throws_Exception_For_Unregistered_Property()
public void Bind_Does_Not_Throw_Exception_For_Unregistered_Property()
{
Class1 target = new Class1();
Assert.Throws<ArgumentException>(() =>
{
target.Bind(Class2.BarProperty, Observable.Return("foo"));
});
target.Bind(Class2.BarProperty, Observable.Never<string>().StartWith("foo"));
Assert.Equal("foo", target.GetValue(Class2.BarProperty));
}
[Fact]

4
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_GetValue.cs

@ -46,11 +46,11 @@ namespace Avalonia.Base.UnitTests
}
[Fact]
public void GetValue_Throws_Exception_For_Unregistered_Property()
public void GetValue_Doesnt_Throw_Exception_For_Unregistered_Property()
{
var target = new Class3();
Assert.Throws<ArgumentException>(() => target.GetValue(Class1.FooProperty));
Assert.Equal("foodefault", target.GetValue(Class1.FooProperty));
}
private class Class1 : AvaloniaObject

39
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetValue.cs

@ -30,6 +30,16 @@ namespace Avalonia.Base.UnitTests
Assert.Equal("newvalue", target.GetValue(Class1.FooProperty));
}
[Fact]
public void SetValue_Sets_Attached_Value()
{
Class2 target = new Class2();
target.SetValue(AttachedOwner.AttachedProperty, "newvalue");
Assert.Equal("newvalue", target.GetValue(AttachedOwner.AttachedProperty));
}
[Fact]
public void SetValue_Raises_PropertyChanged()
{
@ -84,14 +94,27 @@ namespace Avalonia.Base.UnitTests
}
[Fact]
public void SetValue_Throws_Exception_For_Unregistered_Property()
public void SetValue_Allows_Setting_Unregistered_Property()
{
Class1 target = new Class1();
Assert.Throws<ArgumentException>(() =>
{
target.SetValue(Class2.BarProperty, "invalid");
});
Assert.False(AvaloniaPropertyRegistry.Instance.IsRegistered(target, Class2.BarProperty));
target.SetValue(Class2.BarProperty, "bar");
Assert.Equal("bar", target.GetValue(Class2.BarProperty));
}
[Fact]
public void SetValue_Allows_Setting_Unregistered_Attached_Property()
{
Class1 target = new Class1();
Assert.False(AvaloniaPropertyRegistry.Instance.IsRegistered(target, AttachedOwner.AttachedProperty));
target.SetValue(AttachedOwner.AttachedProperty, "bar");
Assert.Equal("bar", target.GetValue(AttachedOwner.AttachedProperty));
}
[Fact]
@ -189,6 +212,12 @@ namespace Avalonia.Base.UnitTests
}
}
private class AttachedOwner
{
public static readonly AttachedProperty<string> AttachedProperty =
AvaloniaProperty.RegisterAttached<AttachedOwner, Class2, string>("Attached");
}
private class ImplictDouble
{
public ImplictDouble(double value)

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

@ -4,12 +4,13 @@
using System.Linq;
using System.Reactive.Linq;
using Xunit;
using Xunit.Abstractions;
namespace Avalonia.Base.UnitTests
{
public class AvaloniaPropertyRegistryTests
{
public AvaloniaPropertyRegistryTests()
public AvaloniaPropertyRegistryTests(ITestOutputHelper s)
{
// Ensure properties are registered.
AvaloniaProperty p;
@ -25,7 +26,7 @@ namespace Avalonia.Base.UnitTests
.Select(x => x.Name)
.ToArray();
Assert.Equal(new[] { "Foo", "Baz", "Qux", "Attached" }, names);
Assert.Equal(new[] { "Foo", "Baz", "Qux" }, names);
}
[Fact]
@ -35,61 +36,41 @@ namespace Avalonia.Base.UnitTests
.Select(x => x.Name)
.ToArray();
Assert.Equal(new[] { "Bar", "Flob", "Fred", "Foo", "Baz", "Qux", "Attached" }, names);
Assert.Equal(new[] { "Bar", "Flob", "Fred", "Foo", "Baz", "Qux" }, names);
}
[Fact]
public void GetAttached_Returns_Registered_Properties_For_Base_Types()
public void GetRegisteredAttached_Returns_Registered_Properties()
{
string[] names = AvaloniaPropertyRegistry.Instance.GetAttached(typeof(AttachedOwner)).Select(x => x.Name).ToArray();
string[] names = AvaloniaPropertyRegistry.Instance.GetRegisteredAttached(typeof(Class1))
.Select(x => x.Name)
.ToArray();
Assert.Equal(new[] { "Attached" }, names);
}
[Fact]
public void FindRegistered_Finds_Untyped_Property()
public void GetRegisteredAttached_Returns_Registered_Properties_For_Base_Types()
{
var result = AvaloniaPropertyRegistry.Instance.FindRegistered(typeof(Class1), "Foo");
string[] names = AvaloniaPropertyRegistry.Instance.GetRegisteredAttached(typeof(Class2))
.Select(x => x.Name)
.ToArray();
Assert.Equal(Class1.FooProperty, result);
Assert.Equal(new[] { "Attached" }, names);
}
[Fact]
public void FindRegistered_Finds_Typed_Property()
public void FindRegistered_Finds_Property()
{
var result = AvaloniaPropertyRegistry.Instance.FindRegistered(typeof(Class1), "Class1.Foo");
var result = AvaloniaPropertyRegistry.Instance.FindRegistered(typeof(Class1), "Foo");
Assert.Equal(Class1.FooProperty, result);
}
[Fact]
public void FindRegistered_Finds_Typed_Inherited_Property()
{
var result = AvaloniaPropertyRegistry.Instance.FindRegistered(typeof(Class2), "Class1.Foo");
Assert.Equal(Class2.FooProperty, result);
}
[Fact]
public void FindRegistered_Finds_Inherited_Property_With_Derived_Type_Name()
{
var result = AvaloniaPropertyRegistry.Instance.FindRegistered(typeof(Class2), "Class2.Foo");
Assert.Equal(Class2.FooProperty, result);
}
[Fact]
public void FindRegistered_Finds_Attached_Property()
{
var result = AvaloniaPropertyRegistry.Instance.FindRegistered(typeof(Class2), "AttachedOwner.Attached");
Assert.Equal(AttachedOwner.AttachedProperty, result);
}
[Fact]
public void FindRegistered_Doesnt_Finds_Unqualified_Attached_Property()
public void FindRegistered_Doesnt_Find_Nonregistered_Property()
{
var result = AvaloniaPropertyRegistry.Instance.FindRegistered(typeof(Class2), "Attached");
var result = AvaloniaPropertyRegistry.Instance.FindRegistered(typeof(Class1), "Bar");
Assert.Null(result);
}
@ -99,55 +80,34 @@ namespace Avalonia.Base.UnitTests
{
var result = AvaloniaPropertyRegistry.Instance.FindRegistered(typeof(AttachedOwner), "Attached");
Assert.True(AttachedOwner.AttachedProperty == result);
Assert.Same(AttachedOwner.AttachedProperty, result);
}
[Fact]
public void FindRegistered_Finds_AddOwnered_Untyped_Attached_Property()
public void FindRegistered_Finds_AddOwnered_Attached_Property()
{
var result = AvaloniaPropertyRegistry.Instance.FindRegistered(typeof(Class3), "Attached");
Assert.True(AttachedOwner.AttachedProperty == result);
Assert.Same(AttachedOwner.AttachedProperty, result);
}
[Fact]
public void FindRegistered_Finds_AddOwnered_Typed_Attached_Property()
public void FindRegistered_Doesnt_Find_Non_AddOwnered_Attached_Property()
{
var result = AvaloniaPropertyRegistry.Instance.FindRegistered(typeof(Class3), "Class3.Attached");
Assert.True(AttachedOwner.AttachedProperty == result);
}
[Fact]
public void FindRegistered_Finds_AddOwnered_AttachedTyped_Attached_Property()
{
var result = AvaloniaPropertyRegistry.Instance.FindRegistered(typeof(Class3), "AttachedOwner.Attached");
Assert.True(AttachedOwner.AttachedProperty == result);
}
[Fact]
public void FindRegistered_Finds_AddOwnered_BaseTyped_Attached_Property()
{
var result = AvaloniaPropertyRegistry.Instance.FindRegistered(typeof(Class3), "Class1.Attached");
Assert.True(AttachedOwner.AttachedProperty == result);
}
[Fact]
public void FindRegistered_Doesnt_Find_Nonregistered_Property()
{
var result = AvaloniaPropertyRegistry.Instance.FindRegistered(typeof(Class1), "Bar");
var result = AvaloniaPropertyRegistry.Instance.FindRegistered(typeof(Class2), "Attached");
Assert.Null(result);
}
[Fact]
public void FindRegistered_Doesnt_Find_Nonregistered_Attached_Property()
public void FindRegisteredAttached_Finds_Property()
{
var result = AvaloniaPropertyRegistry.Instance.FindRegistered(typeof(Class4), "AttachedOwner.Attached");
var result = AvaloniaPropertyRegistry.Instance.FindRegisteredAttached(
typeof(Class1),
typeof(AttachedOwner),
"Attached");
Assert.Null(result);
Assert.Equal(AttachedOwner.AttachedProperty, result);
}
private class Class1 : AvaloniaObject
@ -176,18 +136,18 @@ namespace Avalonia.Base.UnitTests
private class Class3 : Class1
{
public static readonly StyledProperty<string> AttachedProperty =
public static readonly AttachedProperty<string> AttachedProperty =
AttachedOwner.AttachedProperty.AddOwner<Class3>();
}
public class Class4 : AvaloniaObject
{
}
private class AttachedOwner : Class1
{
public static readonly AttachedProperty<string> AttachedProperty =
AvaloniaProperty.RegisterAttached<AttachedOwner, Class1, string>("Attached");
}
private class AttachedOwner2 : AttachedOwner
{
}
}
}

14
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/AttachedPropertyOwner.cs

@ -0,0 +1,14 @@
using System;
using Avalonia.Controls;
namespace Avalonia.Markup.Xaml.UnitTests.Xaml
{
public class AttachedPropertyOwner
{
public static readonly AttachedProperty<double> DoubleProperty =
AvaloniaProperty.RegisterAttached<AttachedPropertyOwner, Control, double>("Double");
public static double GetDouble(Control control) => control.GetValue(DoubleProperty);
public static void SetDouble(Control control, double value) => control.SetValue(DoubleProperty, value);
}
}

19
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs

@ -11,6 +11,7 @@ using Avalonia.Media;
using Avalonia.Media.Immutable;
using Avalonia.Styling;
using Avalonia.UnitTests;
using Portable.Xaml;
using System.Collections;
using System.ComponentModel;
using System.Linq;
@ -125,6 +126,24 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
Assert.Equal("Foo", ToolTip.GetTip(target));
}
[Fact]
public void NonExistent_Property_Throws()
{
var xaml =
@"<ContentControl xmlns='https://github.com/avaloniaui' DoesntExist='foo'/>";
Assert.Throws<XamlObjectWriterException>(() => AvaloniaXamlLoader.Parse<ContentControl>(xaml));
}
[Fact]
public void Non_Attached_Property_With_Attached_Property_Syntax_Throws()
{
var xaml =
@"<ContentControl xmlns='https://github.com/avaloniaui' TextBlock.Text='foo'/>";
Assert.Throws<XamlObjectWriterException>(() => AvaloniaXamlLoader.Parse<ContentControl>(xaml));
}
[Fact]
public void ContentControl_ContentTemplate_Is_Functional()
{

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

@ -215,5 +215,71 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
Assert.Equal("bar", textBlock.Text);
}
}
[Fact]
public void Binding_To_Namespaced_Attached_Property_Works()
{
using (UnitTestApplication.Start(TestServices.MockWindowingPlatform))
{
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'>
<TextBlock local:AttachedPropertyOwner.Double='{Binding}'/>
</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));
}
}
[Fact]
public void Binding_To_AddOwnered_Attached_Property_Works()
{
using (UnitTestApplication.Start(TestServices.MockWindowingPlatform))
{
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'>
<local:TestControl Double='{Binding}'/>
</Window>";
var loader = new AvaloniaXamlLoader();
var window = (Window)loader.Load(xaml);
var testControl = (TestControl)window.Content;
window.DataContext = 5.6;
window.ApplyTemplate();
Assert.Equal(5.6, testControl.Double);
}
}
[Fact]
public void Binding_To_Attached_Property_Using_AddOwnered_Type_Works()
{
using (UnitTestApplication.Start(TestServices.MockWindowingPlatform))
{
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'>
<TextBlock local:TestControl.Double='{Binding}'/>
</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));
}
}
}
}

17
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/TestControl.cs

@ -0,0 +1,17 @@
using System;
using Avalonia.Controls;
namespace Avalonia.Markup.Xaml.UnitTests.Xaml
{
public class TestControl : Control
{
public static readonly StyledProperty<double> DoubleProperty =
AttachedPropertyOwner.DoubleProperty.AddOwner<TestControl>();
public double Double
{
get => GetValue(DoubleProperty);
set => SetValue(DoubleProperty, value);
}
}
}
Loading…
Cancel
Save