浏览代码

Merge branch 'master' into fix/numericParsing

Eli Arbel 7 年之前
父节点
当前提交
7f62efcd47

+ 4 - 5
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), result);
-            return result;
+            AvaloniaPropertyRegistry.Instance.Register(typeof(TOwner), this);
+            return this;
         }
         }
     }
     }
 }
 }

+ 19 - 31
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
-        /// equal or throws if not found.
+        /// Given a direct property, returns a registered avalonia property that is equivalent or
+        /// 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()}");
-        }
     }
     }
 }
 }

+ 6 - 2
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;
         }
         }
 
 

+ 133 - 150
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.
-            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>
         /// <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.
-                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>
         /// <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>
-        /// <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>
         /// <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>
-        /// <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>
         /// <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">
-        /// 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>
         /// <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('.');
-            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>
         /// <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">
-        /// 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>
         /// <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>
-        /// 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>
         /// <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 inner))
+            if (!_registered.TryGetValue(type, out var inner))
             {
             {
                 inner = new Dictionary<int, AvaloniaProperty>();
                 inner = new Dictionary<int, AvaloniaProperty>();
+                inner.Add(property.Id, property);
                 _registered.Add(type, inner);
                 _registered.Add(type, inner);
             }
             }
-
-            if (!inner.ContainsKey(property.Id))
+            else 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))
-                {
-                    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 - 0
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 - 0
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>

+ 2 - 2
src/Avalonia.Controls/Primitives/TemplatedControl.cs

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

+ 1 - 4
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)
             if (property == null)
             {
             {

+ 1 - 2
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)
-                    .FirstOrDefault(v => v.Name == attachablePropertyName);
+            var prop = AvaloniaPropertyRegistry.Instance.FindRegistered(type, attachablePropertyName);
 
 
             if (prop != null)
             if (prop != null)
             {
             {

+ 21 - 1
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

+ 52 - 3
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 - 0
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 - 0
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>();
+        }
+    }
+}

+ 4 - 5
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs

@@ -92,14 +92,13 @@ namespace Avalonia.Base.UnitTests
         }
         }
 
 
         [Fact]
         [Fact]
-        public void Bind_Throws_Exception_For_Unregistered_Property()
+        public void Bind_Does_Not_Throw_Exception_For_Unregistered_Property()
         {
         {
             Class1 target = new Class1();
             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]
         [Fact]

+ 2 - 2
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_GetValue.cs

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

+ 34 - 5
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetValue.cs

@@ -30,6 +30,16 @@ namespace Avalonia.Base.UnitTests
             Assert.Equal("newvalue", target.GetValue(Class1.FooProperty));
             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]
         [Fact]
         public void SetValue_Raises_PropertyChanged()
         public void SetValue_Raises_PropertyChanged()
         {
         {
@@ -84,14 +94,27 @@ namespace Avalonia.Base.UnitTests
         }
         }
 
 
         [Fact]
         [Fact]
-        public void SetValue_Throws_Exception_For_Unregistered_Property()
+        public void SetValue_Allows_Setting_Unregistered_Property()
         {
         {
             Class1 target = new Class1();
             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]
         [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
         private class ImplictDouble
         {
         {
             public ImplictDouble(double value)
             public ImplictDouble(double value)

+ 33 - 73
tests/Avalonia.Base.UnitTests/AvaloniaPropertyRegistryTests.cs

@@ -4,12 +4,13 @@
 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()
+        public AvaloniaPropertyRegistryTests(ITestOutputHelper s)
         {
         {
             // Ensure properties are registered.
             // Ensure properties are registered.
             AvaloniaProperty p;
             AvaloniaProperty p;
@@ -25,7 +26,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 +36,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()
-        {
-            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);
             Assert.Null(result);
         }
         }
@@ -99,55 +80,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");
-
-            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);
             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 +136,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
+        {
+        }
     }
     }
 }
 }

+ 14 - 0
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 - 0
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs

@@ -11,6 +11,7 @@ using Avalonia.Media;
 using Avalonia.Media.Immutable;
 using Avalonia.Media.Immutable;
 using Avalonia.Styling;
 using Avalonia.Styling;
 using Avalonia.UnitTests;
 using Avalonia.UnitTests;
+using Portable.Xaml;
 using System.Collections;
 using System.Collections;
 using System.ComponentModel;
 using System.ComponentModel;
 using System.Linq;
 using System.Linq;
@@ -125,6 +126,24 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
             Assert.Equal("Foo", ToolTip.GetTip(target));
             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]
         [Fact]
         public void ContentControl_ContentTemplate_Is_Functional()
         public void ContentControl_ContentTemplate_Is_Functional()
         {
         {

+ 66 - 0
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs

@@ -215,5 +215,71 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
                 Assert.Equal("bar", textBlock.Text);
                 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 - 0
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);
+        }
+    }
+}