Browse Source

Allow any property to be registered anywhere.

Makes Avalonia more like other XAML frameworks, in that any avalonia property can now be set anywhere. The XAML parser however will only allow you to set registered properties.
pull/1499/head
Steven Kirk 8 years ago
parent
commit
44cfcfb04d
  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. 5
      src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs
  8. 3
      src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlSchemaContext.cs
  9. 22
      src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlType.cs
  10. 55
      src/Markup/Avalonia.Markup/Data/Plugins/AvaloniaPropertyAccessorPlugin.cs
  11. 52
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_AddOwner.cs
  12. 52
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Attached.cs
  13. 108
      tests/Avalonia.Base.UnitTests/AvaloniaPropertyRegistryTests.cs

9
src/Avalonia.Base/AttachedProperty.cs

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

50
src/Avalonia.Base/AvaloniaObject.cs

@ -12,7 +12,6 @@ using Avalonia.Diagnostics;
using Avalonia.Logging; using Avalonia.Logging;
using Avalonia.Threading; using Avalonia.Threading;
using Avalonia.Utilities; using Avalonia.Utilities;
using System.Reactive.Concurrency;
namespace Avalonia namespace Avalonia
{ {
@ -218,11 +217,6 @@ namespace Avalonia
} }
else else
{ {
if (!AvaloniaPropertyRegistry.Instance.IsRegistered(this, property))
{
ThrowNotRegistered(property);
}
return GetValueInternal(property); return GetValueInternal(property);
} }
} }
@ -377,11 +371,6 @@ namespace Avalonia
{ {
PriorityValue v; PriorityValue v;
if (!AvaloniaPropertyRegistry.Instance.IsRegistered(this, property))
{
ThrowNotRegistered(property);
}
if (!_values.TryGetValue(property, out v)) if (!_values.TryGetValue(property, out v))
{ {
v = CreatePriorityValue(property); v = CreatePriorityValue(property);
@ -804,11 +793,6 @@ namespace Avalonia
var originalValue = value; var originalValue = value;
if (!AvaloniaPropertyRegistry.Instance.IsRegistered(this, property))
{
ThrowNotRegistered(property);
}
if (!TypeUtilities.TryConvertImplicit(property.PropertyType, value, out value)) if (!TypeUtilities.TryConvertImplicit(property.PropertyType, value, out value))
{ {
throw new ArgumentException(string.Format( throw new ArgumentException(string.Format(
@ -836,18 +820,32 @@ namespace Avalonia
} }
/// <summary> /// <summary>
/// Given a <see cref="AvaloniaProperty"/> returns a registered avalonia property that is /// Given a direct property, returns a registered avalonia property that is equivalent or
/// equal or throws if not found. /// throws if not found.
/// </summary> /// </summary>
/// <param name="property">The property.</param> /// <param name="property">The property.</param>
/// <returns>The registered property.</returns> /// <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) if (result == null)
{ {
ThrowNotRegistered(property); throw new ArgumentException($"Property '{property.Name} not registered on '{this.GetType()}");
} }
return result; return result;
@ -898,15 +896,5 @@ namespace Avalonia
value, value,
priority); 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); defaultBindingMode: defaultBindingMode);
var result = new AttachedProperty<TValue>(name, typeof(TOwner), metadata, inherits); 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; return result;
} }
@ -344,7 +346,9 @@ namespace Avalonia
defaultBindingMode: defaultBindingMode); defaultBindingMode: defaultBindingMode);
var result = new AttachedProperty<TValue>(name, ownerType, metadata, inherits); 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; return result;
} }

283
src/Avalonia.Base/AvaloniaPropertyRegistry.cs

@ -4,7 +4,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
namespace Avalonia namespace Avalonia
@ -14,23 +13,14 @@ namespace Avalonia
/// </summary> /// </summary>
public class AvaloniaPropertyRegistry public class AvaloniaPropertyRegistry
{ {
/// <summary>
/// The registered properties by type.
/// </summary>
private readonly Dictionary<Type, Dictionary<int, AvaloniaProperty>> _registered = private readonly Dictionary<Type, Dictionary<int, AvaloniaProperty>> _registered =
new Dictionary<Type, Dictionary<int, AvaloniaProperty>>(); 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 = private readonly Dictionary<Type, Dictionary<int, AvaloniaProperty>> _attached =
new Dictionary<Type, Dictionary<int, AvaloniaProperty>>(); 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> /// <summary>
/// Gets the <see cref="AvaloniaPropertyRegistry"/> instance /// Gets the <see cref="AvaloniaPropertyRegistry"/> instance
@ -39,51 +29,68 @@ namespace Avalonia
= new AvaloniaPropertyRegistry(); = new AvaloniaPropertyRegistry();
/// <summary> /// <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> /// </summary>
/// <param name="ownerType">The owner type.</param> /// <param name="type">The type.</param>
/// <returns>A collection of <see cref="AvaloniaProperty"/> definitions.</returns> /// <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. var t = type;
RuntimeHelpers.RunClassConstructor(ownerType.TypeHandle); 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> /// <summary>
/// Gets all <see cref="AvaloniaProperty"/>s registered on a type. /// Gets all attached <see cref="AvaloniaProperty"/>s registered on a type.
/// </summary> /// </summary>
/// <param name="type">The type.</param> /// <param name="type">The type.</param>
/// <returns>A collection of <see cref="AvaloniaProperty"/> definitions.</returns> /// <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); Contract.Requires<ArgumentNullException>(type != null);
while (type != null) if (_attachedCache.TryGetValue(type, out var result))
{ {
// Ensure the type's static ctor has been run. return result;
RuntimeHelpers.RunClassConstructor(type.TypeHandle); }
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) result.AddRange(attached.Values);
{
yield return p.Value;
}
} }
type = type.GetTypeInfo().BaseType; t = t.BaseType;
} }
_attachedCache.Add(type, result);
return result;
} }
/// <summary> /// <summary>
@ -99,142 +106,92 @@ namespace Avalonia
} }
/// <summary> /// <summary>
/// Finds a <see cref="AvaloniaProperty"/> registered on a type. /// Finds a registered non-attached property on a type by name.
/// </summary> /// </summary>
/// <param name="type">The type.</param> /// <param name="type">The type.</param>
/// <param name="property">The property.</param> /// <param name="name">The property name.</param>
/// <returns>The registered property or null if not found.</returns> /// <returns>
/// <remarks> /// The registered property or null if no matching property found.
/// Calling AddOwner on a AvaloniaProperty creates a new AvaloniaProperty that is a /// </returns>
/// different object but is equal according to <see cref="object.Equals(object)"/>. /// <exception cref="InvalidOperationException">
/// </remarks> /// The property name contains a '.'.
public AvaloniaProperty FindRegistered(Type type, AvaloniaProperty property) /// </exception>
public AvaloniaProperty FindRegistered(Type type, string name)
{ {
Type currentType = type; Contract.Requires<ArgumentNullException>(type != null);
Dictionary<int, AvaloniaProperty> cache; Contract.Requires<ArgumentNullException>(name != null);
AvaloniaProperty result;
if (_registeredCache.TryGetValue(type, out cache)) if (name.Contains('.'))
{ {
if (cache.TryGetValue(property.Id, out result)) throw new InvalidOperationException("Attached properties not supported.");
{
return result;
}
} }
while (currentType != null) return GetRegistered(type).FirstOrDefault(x => x.Name == name);
{
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;
} }
/// <summary> /// <summary>
/// Finds <see cref="AvaloniaProperty"/> registered on an object. /// Finds a registered non-attached property on a type by name.
/// </summary> /// </summary>
/// <param name="o">The object.</param> /// <param name="o">The object.</param>
/// <param name="property">The property.</param> /// <param name="name">The property name.</param>
/// <returns>The registered property or null if not found.</returns> /// <returns>
/// <remarks> /// The registered property or null if no matching property found.
/// Calling AddOwner on a AvaloniaProperty creates a new AvaloniaProperty that is a /// </returns>
/// different object but is equal according to <see cref="object.Equals(object)"/>. /// <exception cref="InvalidOperationException">
/// </remarks> /// The property name contains a '.'.
public AvaloniaProperty FindRegistered(object o, AvaloniaProperty property) /// </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> /// <summary>
/// Finds a registered property on a type by name. /// Finds a registered attached property on a type by name.
/// </summary> /// </summary>
/// <param name="type">The type.</param> /// <param name="type">The type.</param>
/// <param name="name"> /// <param name="ownerType">The owner type.</param>
/// The property name. If an attached property it should be in the form /// <param name="name">The property name.</param>
/// "OwnerType.PropertyName".
/// </param>
/// <returns> /// <returns>
/// The registered property or null if no matching property found. /// The registered property or null if no matching property found.
/// </returns> /// </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>(type != null);
Contract.Requires<ArgumentNullException>(ownerType != null);
Contract.Requires<ArgumentNullException>(name != null); Contract.Requires<ArgumentNullException>(name != null);
var parts = name.Split('.'); if (name.Contains('.'))
var types = GetImplementedTypes(type).ToList();
if (parts.Length < 1 || parts.Length > 2)
{ {
throw new ArgumentException("Invalid property name."); throw new InvalidOperationException("Attached properties not supported.");
} }
string propertyName; return GetRegisteredAttached(type).FirstOrDefault(x => x.Name == name);
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);
} }
/// <summary> /// <summary>
/// Finds a registered property on an object by name. /// Finds a registered non-attached property on a type by name.
/// </summary> /// </summary>
/// <param name="o">The object.</param> /// <param name="o">The object.</param>
/// <param name="name"> /// <param name="ownerType">The owner type.</param>
/// The property name. If an attached property it should be in the form /// <param name="name">The property name.</param>
/// "OwnerType.PropertyName".
/// </param>
/// <returns> /// <returns>
/// The registered property or null if no matching property found. /// The registered property or null if no matching property found.
/// </returns> /// </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> return FindRegisteredAttached(o.GetType(), ownerType, name);
/// 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;
}
} }
/// <summary> /// <summary>
@ -245,7 +202,11 @@ namespace Avalonia
/// <returns>True if the property is registered, otherwise false.</returns> /// <returns>True if the property is registered, otherwise false.</returns>
public bool IsRegistered(Type type, AvaloniaProperty property) 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> /// <summary>
@ -256,6 +217,9 @@ namespace Avalonia
/// <returns>True if the property is registered, otherwise false.</returns> /// <returns>True if the property is registered, otherwise false.</returns>
public bool IsRegistered(object o, AvaloniaProperty property) public bool IsRegistered(object o, AvaloniaProperty property)
{ {
Contract.Requires<ArgumentNullException>(o != null);
Contract.Requires<ArgumentNullException>(property != null);
return IsRegistered(o.GetType(), property); return IsRegistered(o.GetType(), property);
} }
@ -274,34 +238,53 @@ namespace Avalonia
Contract.Requires<ArgumentNullException>(type != null); Contract.Requires<ArgumentNullException>(type != null);
Contract.Requires<ArgumentNullException>(property != null); Contract.Requires<ArgumentNullException>(property != null);
Dictionary<int, AvaloniaProperty> inner; if (!_registered.TryGetValue(type, out var inner))
if (!_registered.TryGetValue(type, out inner))
{ {
inner = new Dictionary<int, AvaloniaProperty>(); inner = new Dictionary<int, AvaloniaProperty>();
inner.Add(property.Id, property);
_registered.Add(type, inner); _registered.Add(type, inner);
} }
else if (!inner.ContainsKey(property.Id))
if (!inner.ContainsKey(property.Id))
{ {
inner.Add(property.Id, property); 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)) throw new InvalidOperationException(
{ "Cannot register a non-attached property as attached.");
inner = new Dictionary<int, AvaloniaProperty>(); }
_attached.Add(property.OwnerType, inner);
}
if (!inner.ContainsKey(property.Id)) if (!_attached.TryGetValue(type, out var inner))
{ {
inner.Add(property.Id, property); 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> /// </summary>
public Action<TOwner, TValue> Setter { get; } public Action<TOwner, TValue> Setter { get; }
/// <inheritdoc/>
Type IDirectPropertyAccessor.Owner => typeof(TOwner);
/// <summary> /// <summary>
/// Registers the direct property on another type. /// Registers the direct property on another type.
/// </summary> /// </summary>

7
src/Avalonia.Base/IDirectPropertyAccessor.cs

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

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. AvaloniaProperty property = AvaloniaPropertyRegistry.Instance.FindRegistered(type, propertyName);
var property = AvaloniaPropertyRegistry.Instance.FindRegistered(type, s) ??
AvaloniaPropertyRegistry.Instance.GetAttached(type)
.FirstOrDefault(x => x.Name == propertyName);
if (property == null) 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 type = (getter ?? setter).DeclaringType;
var prop = AvaloniaPropertyRegistry.Instance.GetAttached(type) var prop = AvaloniaPropertyRegistry.Instance.FindRegistered(type, attachablePropertyName);
.FirstOrDefault(v => v.Name == attachablePropertyName);
if (prop != null) 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 public class AvaloniaXamlType : XamlType
{ {
static readonly AvaloniaPropertyTypeConverter propertyTypeConverter = new AvaloniaPropertyTypeConverter();
public AvaloniaXamlType(Type underlyingType, XamlSchemaContext schemaContext) : public AvaloniaXamlType(Type underlyingType, XamlSchemaContext schemaContext) :
base(underlyingType, 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) protected override XamlMember LookupMember(string name, bool skipReadOnlyCheck)
{ {
var m = base.LookupMember(name, 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 //so far Portable.xaml haven't found the member/property
//but what if we have AvaloniaProperty //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. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using System; using System;
using System.Linq;
using System.Reactive.Linq; using System.Reactive.Linq;
using Avalonia.Data; using Avalonia.Data;
@ -15,9 +16,9 @@ namespace Avalonia.Markup.Data.Plugins
/// <inheritdoc/> /// <inheritdoc/>
public bool Match(object obj, string propertyName) 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; return false;
@ -39,7 +40,7 @@ namespace Avalonia.Markup.Data.Plugins
var instance = reference.Target; var instance = reference.Target;
var o = (AvaloniaObject)instance; var o = (AvaloniaObject)instance;
var p = AvaloniaPropertyRegistry.Instance.FindRegistered(o, propertyName); var p = LookupProperty(o, propertyName);
if (p != null) 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 class Accessor : PropertyAccessorBase
{ {
private readonly WeakReference<AvaloniaObject> _reference; 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, AvaloniaObject, 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>();
}
}
}

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

@ -4,12 +4,15 @@
using System.Linq; using System.Linq;
using System.Reactive.Linq; using System.Reactive.Linq;
using Xunit; using Xunit;
using Xunit.Abstractions;
namespace Avalonia.Base.UnitTests namespace Avalonia.Base.UnitTests
{ {
public class AvaloniaPropertyRegistryTests public class AvaloniaPropertyRegistryTests
{ {
public AvaloniaPropertyRegistryTests() ITestOutputHelper s;
public AvaloniaPropertyRegistryTests(ITestOutputHelper s)
{ {
// Ensure properties are registered. // Ensure properties are registered.
AvaloniaProperty p; AvaloniaProperty p;
@ -25,7 +28,7 @@ namespace Avalonia.Base.UnitTests
.Select(x => x.Name) .Select(x => x.Name)
.ToArray(); .ToArray();
Assert.Equal(new[] { "Foo", "Baz", "Qux", "Attached" }, names); Assert.Equal(new[] { "Foo", "Baz", "Qux" }, names);
} }
[Fact] [Fact]
@ -35,61 +38,41 @@ namespace Avalonia.Base.UnitTests
.Select(x => x.Name) .Select(x => x.Name)
.ToArray(); .ToArray();
Assert.Equal(new[] { "Bar", "Flob", "Fred", "Foo", "Baz", "Qux", "Attached" }, names); Assert.Equal(new[] { "Bar", "Flob", "Fred", "Foo", "Baz", "Qux" }, names);
} }
[Fact] [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); Assert.Equal(new[] { "Attached" }, names);
} }
[Fact] [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] [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); Assert.Equal(Class1.FooProperty, result);
} }
[Fact] [Fact]
public void FindRegistered_Finds_Typed_Inherited_Property() public void FindRegistered_Doesnt_Find_Nonregistered_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()
{ {
var result = AvaloniaPropertyRegistry.Instance.FindRegistered(typeof(Class2), "Attached"); var result = AvaloniaPropertyRegistry.Instance.FindRegistered(typeof(Class1), "Bar");
Assert.Null(result); Assert.Null(result);
} }
@ -99,55 +82,34 @@ namespace Avalonia.Base.UnitTests
{ {
var result = AvaloniaPropertyRegistry.Instance.FindRegistered(typeof(AttachedOwner), "Attached"); var result = AvaloniaPropertyRegistry.Instance.FindRegistered(typeof(AttachedOwner), "Attached");
Assert.True(AttachedOwner.AttachedProperty == result); Assert.Same(AttachedOwner.AttachedProperty, result);
} }
[Fact] [Fact]
public void FindRegistered_Finds_AddOwnered_Untyped_Attached_Property() public void FindRegistered_Finds_AddOwnered_Attached_Property()
{ {
var result = AvaloniaPropertyRegistry.Instance.FindRegistered(typeof(Class3), "Attached"); var result = AvaloniaPropertyRegistry.Instance.FindRegistered(typeof(Class3), "Attached");
Assert.True(AttachedOwner.AttachedProperty == result); Assert.Same(AttachedOwner.AttachedProperty, result);
} }
[Fact] [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"); var result = AvaloniaPropertyRegistry.Instance.FindRegistered(typeof(Class2), "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");
Assert.Null(result); Assert.Null(result);
} }
[Fact] [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 private class Class1 : AvaloniaObject
@ -176,18 +138,18 @@ namespace Avalonia.Base.UnitTests
private class Class3 : Class1 private class Class3 : Class1
{ {
public static readonly StyledProperty<string> AttachedProperty = public static readonly AttachedProperty<string> AttachedProperty =
AttachedOwner.AttachedProperty.AddOwner<Class3>(); AttachedOwner.AttachedProperty.AddOwner<Class3>();
} }
public class Class4 : AvaloniaObject
{
}
private class AttachedOwner : Class1 private class AttachedOwner : Class1
{ {
public static readonly AttachedProperty<string> AttachedProperty = public static readonly AttachedProperty<string> AttachedProperty =
AvaloniaProperty.RegisterAttached<AttachedOwner, Class1, string>("Attached"); AvaloniaProperty.RegisterAttached<AttachedOwner, Class1, string>("Attached");
} }
private class AttachedOwner2 : AttachedOwner
{
}
} }
} }

Loading…
Cancel
Save