Prechádzať zdrojové kódy

Merge branch 'master' into master

Steven Kirk 7 rokov pred
rodič
commit
a5b535e4af
57 zmenil súbory, kde vykonal 716 pridanie a 428 odobranie
  1. 4 5
      src/Avalonia.Base/AttachedProperty.cs
  2. 19 31
      src/Avalonia.Base/AvaloniaObject.cs
  3. 6 2
      src/Avalonia.Base/AvaloniaProperty.cs
  4. 133 150
      src/Avalonia.Base/AvaloniaPropertyRegistry.cs
  5. 3 0
      src/Avalonia.Base/DirectProperty.cs
  6. 7 0
      src/Avalonia.Base/IDirectPropertyAccessor.cs
  7. 1 1
      src/Avalonia.Controls/ColumnDefinitions.cs
  8. 4 3
      src/Avalonia.Controls/ContextMenu.cs
  9. 6 8
      src/Avalonia.Controls/GridLength.cs
  10. 31 8
      src/Avalonia.Controls/Primitives/Popup.cs
  11. 26 0
      src/Avalonia.Controls/Primitives/PopupRoot.cs
  12. 2 2
      src/Avalonia.Controls/Primitives/TemplatedControl.cs
  13. 1 1
      src/Avalonia.Controls/RowDefinitions.cs
  14. 4 2
      src/Avalonia.Themes.Default/MenuItem.xaml
  15. 18 22
      src/Avalonia.Visuals/CornerRadius.cs
  16. 2 3
      src/Avalonia.Visuals/Matrix.cs
  17. 2 3
      src/Avalonia.Visuals/Point.cs
  18. 2 3
      src/Avalonia.Visuals/Rect.cs
  19. 4 5
      src/Avalonia.Visuals/RelativePoint.cs
  20. 6 7
      src/Avalonia.Visuals/RelativeRect.cs
  21. 2 2
      src/Avalonia.Visuals/Rendering/DefaultRenderLoop.cs
  22. 2 3
      src/Avalonia.Visuals/Size.cs
  23. 13 11
      src/Avalonia.Visuals/Thickness.cs
  24. 1 4
      src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs
  25. 1 1
      src/Markup/Avalonia.Markup.Xaml/Converters/CornerRadiusTypeConverter.cs
  26. 1 1
      src/Markup/Avalonia.Markup.Xaml/Converters/GridLengthTypeConverter.cs
  27. 1 1
      src/Markup/Avalonia.Markup.Xaml/Converters/MatrixTypeConverter.cs
  28. 1 1
      src/Markup/Avalonia.Markup.Xaml/Converters/PointTypeConverter.cs
  29. 1 1
      src/Markup/Avalonia.Markup.Xaml/Converters/PointsListTypeConverter.cs
  30. 1 1
      src/Markup/Avalonia.Markup.Xaml/Converters/RectTypeConverter.cs
  31. 1 1
      src/Markup/Avalonia.Markup.Xaml/Converters/RelativePointTypeConverter.cs
  32. 1 1
      src/Markup/Avalonia.Markup.Xaml/Converters/RelativeRectTypeConverter.cs
  33. 1 1
      src/Markup/Avalonia.Markup.Xaml/Converters/SizeTypeConverter.cs
  34. 1 1
      src/Markup/Avalonia.Markup.Xaml/Converters/ThicknessTypeConverter.cs
  35. 1 2
      src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlSchemaContext.cs
  36. 21 1
      src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlType.cs
  37. 52 3
      src/Markup/Avalonia.Markup/Data/Plugins/AvaloniaPropertyAccessorPlugin.cs
  38. 0 7
      src/Windows/Avalonia.Win32/ScreenImpl.cs
  39. 7 11
      src/Windows/Avalonia.Win32/Win32Platform.cs
  40. 52 0
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_AddOwner.cs
  41. 52 0
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Attached.cs
  42. 4 5
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs
  43. 2 2
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_GetValue.cs
  44. 34 5
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetValue.cs
  45. 33 73
      tests/Avalonia.Base.UnitTests/AvaloniaPropertyRegistryTests.cs
  46. 9 9
      tests/Avalonia.Controls.UnitTests/GridLengthTests.cs
  47. 9 9
      tests/Avalonia.Markup.UnitTests/Data/BindingExpressionTests.cs
  48. 14 0
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/AttachedPropertyOwner.cs
  49. 19 0
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs
  50. 66 0
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs
  51. 17 0
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/TestControl.cs
  52. 4 4
      tests/Avalonia.Visuals.UnitTests/CornerRadiusTests.cs
  53. 1 1
      tests/Avalonia.Visuals.UnitTests/Media/MatrixTests.cs
  54. 1 1
      tests/Avalonia.Visuals.UnitTests/Media/RectTests.cs
  55. 2 2
      tests/Avalonia.Visuals.UnitTests/RelativePointTests.cs
  56. 3 3
      tests/Avalonia.Visuals.UnitTests/RelativeRectTests.cs
  57. 4 4
      tests/Avalonia.Visuals.UnitTests/ThicknessTests.cs

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

+ 1 - 1
src/Avalonia.Controls/ColumnDefinitions.cs

@@ -27,7 +27,7 @@ namespace Avalonia.Controls
         public ColumnDefinitions(string s)
         public ColumnDefinitions(string s)
             : this()
             : this()
         {
         {
-            AddRange(GridLength.ParseLengths(s, CultureInfo.InvariantCulture).Select(x => new ColumnDefinition(x)));
+            AddRange(GridLength.ParseLengths(s).Select(x => new ColumnDefinition(x)));
         }
         }
     }
     }
 }
 }

+ 4 - 3
src/Avalonia.Controls/ContextMenu.cs

@@ -19,7 +19,7 @@ namespace Avalonia.Controls
         {
         {
             ContextMenuProperty.Changed.Subscribe(ContextMenuChanged);
             ContextMenuProperty.Changed.Subscribe(ContextMenuChanged);
 
 
-            MenuItem.ClickEvent.AddClassHandler<ContextMenu>(x => x.OnContextMenuClick, handledEventsToo: true);            
+            MenuItem.ClickEvent.AddClassHandler<ContextMenu>(x => x.OnContextMenuClick, handledEventsToo: true);
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -75,13 +75,14 @@ namespace Avalonia.Controls
         {
         {
             if (control != null)
             if (control != null)
             {
             {
-                if(_popup == null)
+                if (_popup == null)
                 {
                 {
                     _popup = new Popup()
                     _popup = new Popup()
                     {
                     {
                         PlacementMode = PlacementMode.Pointer,
                         PlacementMode = PlacementMode.Pointer,
                         PlacementTarget = control,
                         PlacementTarget = control,
-                        StaysOpen = false                                         
+                        StaysOpen = false,
+                        ObeyScreenEdges = true
                     };
                     };
 
 
                     _popup.Closed += PopupClosed;
                     _popup.Closed += PopupClosed;

+ 6 - 8
src/Avalonia.Controls/GridLength.cs

@@ -180,9 +180,8 @@ namespace Avalonia.Controls
         /// Parses a string to return a <see cref="GridLength"/>.
         /// Parses a string to return a <see cref="GridLength"/>.
         /// </summary>
         /// </summary>
         /// <param name="s">The string.</param>
         /// <param name="s">The string.</param>
-        /// <param name="culture">The current culture.</param>
         /// <returns>The <see cref="GridLength"/>.</returns>
         /// <returns>The <see cref="GridLength"/>.</returns>
-        public static GridLength Parse(string s, CultureInfo culture)
+        public static GridLength Parse(string s)
         {
         {
             s = s.ToUpperInvariant();
             s = s.ToUpperInvariant();
 
 
@@ -193,12 +192,12 @@ namespace Avalonia.Controls
             else if (s.EndsWith("*"))
             else if (s.EndsWith("*"))
             {
             {
                 var valueString = s.Substring(0, s.Length - 1).Trim();
                 var valueString = s.Substring(0, s.Length - 1).Trim();
-                var value = valueString.Length > 0 ? double.Parse(valueString, culture) : 1;
+                var value = valueString.Length > 0 ? double.Parse(valueString, CultureInfo.InvariantCulture) : 1;
                 return new GridLength(value, GridUnitType.Star);
                 return new GridLength(value, GridUnitType.Star);
             }
             }
             else
             else
             {
             {
-                var value = double.Parse(s, culture);
+                var value = double.Parse(s, CultureInfo.InvariantCulture);
                 return new GridLength(value, GridUnitType.Pixel);
                 return new GridLength(value, GridUnitType.Pixel);
             }
             }
         }
         }
@@ -207,15 +206,14 @@ namespace Avalonia.Controls
         /// Parses a string to return a collection of <see cref="GridLength"/>s.
         /// Parses a string to return a collection of <see cref="GridLength"/>s.
         /// </summary>
         /// </summary>
         /// <param name="s">The string.</param>
         /// <param name="s">The string.</param>
-        /// <param name="culture">The current culture.</param>
         /// <returns>The <see cref="GridLength"/>.</returns>
         /// <returns>The <see cref="GridLength"/>.</returns>
-        public static IEnumerable<GridLength> ParseLengths(string s, CultureInfo culture)
+        public static IEnumerable<GridLength> ParseLengths(string s)
         {
         {
-            using (var tokenizer = new StringTokenizer(s, culture))
+            using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture))
             {
             {
                 while (tokenizer.TryReadString(out var item))
                 while (tokenizer.TryReadString(out var item))
                 {
                 {
-                    yield return Parse(item, culture);
+                    yield return Parse(item);
                 }
                 }
             }
             }
         }
         }

+ 31 - 8
src/Avalonia.Controls/Primitives/Popup.cs

@@ -40,6 +40,12 @@ namespace Avalonia.Controls.Primitives
         public static readonly StyledProperty<PlacementMode> PlacementModeProperty =
         public static readonly StyledProperty<PlacementMode> PlacementModeProperty =
             AvaloniaProperty.Register<Popup, PlacementMode>(nameof(PlacementMode), defaultValue: PlacementMode.Bottom);
             AvaloniaProperty.Register<Popup, PlacementMode>(nameof(PlacementMode), defaultValue: PlacementMode.Bottom);
 
 
+        /// <summary>
+        /// Defines the <see cref="ObeyScreenEdges"/> property.
+        /// </summary>
+        public static readonly StyledProperty<bool> ObeyScreenEdgesProperty =
+            AvaloniaProperty.Register<Popup, bool>(nameof(ObeyScreenEdges));
+
         /// <summary>
         /// <summary>
         /// Defines the <see cref="HorizontalOffset"/> property.
         /// Defines the <see cref="HorizontalOffset"/> property.
         /// </summary>
         /// </summary>
@@ -136,6 +142,16 @@ namespace Avalonia.Controls.Primitives
             set { SetValue(PlacementModeProperty, value); }
             set { SetValue(PlacementModeProperty, value); }
         }
         }
 
 
+        /// <summary>
+        /// Gets or sets a value indicating whether the popup positions itself within the nearest screen boundary
+        /// when its opened at a position where it would otherwise overlap the screen edge.
+        /// </summary>
+        public bool ObeyScreenEdges
+        {
+            get => GetValue(ObeyScreenEdgesProperty);
+            set => SetValue(ObeyScreenEdgesProperty, value);
+        }
+
         /// <summary>
         /// <summary>
         /// Gets or sets the Horizontal offset of the popup in relation to the <see cref="PlacementTarget"/>
         /// Gets or sets the Horizontal offset of the popup in relation to the <see cref="PlacementTarget"/>
         /// </summary>
         /// </summary>
@@ -216,12 +232,12 @@ namespace Avalonia.Controls.Primitives
                 var window = _topLevel as Window;
                 var window = _topLevel as Window;
                 if (window != null)
                 if (window != null)
                 {
                 {
-                    window.Deactivated += WindowDeactivated;                  
+                    window.Deactivated += WindowDeactivated;
                 }
                 }
                 else
                 else
                 {
                 {
                     var parentPopuproot = _topLevel as PopupRoot;
                     var parentPopuproot = _topLevel as PopupRoot;
-                    if(parentPopuproot != null && parentPopuproot.Parent!=null)
+                    if (parentPopuproot != null && parentPopuproot.Parent != null)
                     {
                     {
                         ((Popup)(parentPopuproot.Parent)).Closed += ParentClosed;
                         ((Popup)(parentPopuproot.Parent)).Closed += ParentClosed;
                     }
                     }
@@ -234,13 +250,18 @@ namespace Avalonia.Controls.Primitives
 
 
             _popupRoot.Show();
             _popupRoot.Show();
 
 
+            if (ObeyScreenEdges)
+            {
+                _popupRoot.SnapInsideScreenEdges();
+            }
+
             _ignoreIsOpenChanged = true;
             _ignoreIsOpenChanged = true;
             IsOpen = true;
             IsOpen = true;
             _ignoreIsOpenChanged = false;
             _ignoreIsOpenChanged = false;
 
 
             Opened?.Invoke(this, EventArgs.Empty);
             Opened?.Invoke(this, EventArgs.Empty);
         }
         }
-        
+
         /// <summary>
         /// <summary>
         /// Closes the popup.
         /// Closes the popup.
         /// </summary>
         /// </summary>
@@ -346,8 +367,10 @@ namespace Avalonia.Controls.Primitives
         /// <returns>The popup's position in screen coordinates.</returns>
         /// <returns>The popup's position in screen coordinates.</returns>
         protected virtual Point GetPosition()
         protected virtual Point GetPosition()
         {
         {
-            return GetPosition(PlacementTarget ?? this.GetVisualParent<Control>(), PlacementMode, PopupRoot, 
+            var result = GetPosition(PlacementTarget ?? this.GetVisualParent<Control>(), PlacementMode, PopupRoot,
                 HorizontalOffset, VerticalOffset);
                 HorizontalOffset, VerticalOffset);
+
+            return result;
         }
         }
 
 
         internal static Point GetPosition(Control target, PlacementMode placement, PopupRoot popupRoot, double horizontalOffset, double verticalOffset)
         internal static Point GetPosition(Control target, PlacementMode placement, PopupRoot popupRoot, double horizontalOffset, double verticalOffset)
@@ -399,8 +422,8 @@ namespace Avalonia.Controls.Primitives
         {
         {
             if (!StaysOpen)
             if (!StaysOpen)
             {
             {
-                if(!IsChildOrThis((IVisual)e.Source))
-                {                     
+                if (!IsChildOrThis((IVisual)e.Source))
+                {
                     Close();
                     Close();
                     e.Handled = true;
                     e.Handled = true;
                 }
                 }
@@ -412,12 +435,12 @@ namespace Avalonia.Controls.Primitives
             IVisual root = child.GetVisualRoot();
             IVisual root = child.GetVisualRoot();
             while (root is PopupRoot)
             while (root is PopupRoot)
             {
             {
-                if (root == PopupRoot) return true;              
+                if (root == PopupRoot) return true;
                 root = ((PopupRoot)root).Parent.GetVisualRoot();
                 root = ((PopupRoot)root).Parent.GetVisualRoot();
             }
             }
             return false;
             return false;
         }
         }
-        
+
         private void WindowDeactivated(object sender, EventArgs e)
         private void WindowDeactivated(object sender, EventArgs e)
         {
         {
             if (!StaysOpen)
             if (!StaysOpen)

+ 26 - 0
src/Avalonia.Controls/Primitives/PopupRoot.cs

@@ -2,10 +2,12 @@
 // 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 Avalonia.Controls.Platform;
 using Avalonia.Controls.Platform;
 using Avalonia.Controls.Presenters;
 using Avalonia.Controls.Presenters;
 using Avalonia.Interactivity;
 using Avalonia.Interactivity;
 using Avalonia.Layout;
 using Avalonia.Layout;
+using Avalonia.LogicalTree;
 using Avalonia.Media;
 using Avalonia.Media;
 using Avalonia.Platform;
 using Avalonia.Platform;
 using Avalonia.Styling;
 using Avalonia.Styling;
@@ -75,6 +77,30 @@ namespace Avalonia.Controls.Primitives
         /// <inheritdoc/>
         /// <inheritdoc/>
         public void Dispose() => PlatformImpl?.Dispose();
         public void Dispose() => PlatformImpl?.Dispose();
 
 
+        /// <summary>
+        /// Moves the Popups position so that it doesnt overlap screen edges.
+        /// This method can be called immediately after Show has been called.
+        /// </summary>
+        public void SnapInsideScreenEdges()
+        {
+            var window = this.GetSelfAndLogicalAncestors().OfType<Window>().First();
+            
+            var screen = window.Screens.ScreenFromPoint(Position);
+
+            var screenX = Position.X + Bounds.Width - screen.Bounds.X;
+            var screenY = Position.Y + Bounds.Height - screen.Bounds.Y;
+
+            if (screenX > screen.Bounds.Width)
+            {
+                Position = Position.WithX(Position.X - (screenX - screen.Bounds.Width));
+            }
+
+            if (screenY > screen.Bounds.Height)
+            {
+                Position = Position.WithY(Position.Y - (screenY - screen.Bounds.Height));
+            }
+        }
+
         /// <inheritdoc/>
         /// <inheritdoc/>
         protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
         protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
         {
         {

+ 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 - 1
src/Avalonia.Controls/RowDefinitions.cs

@@ -27,7 +27,7 @@ namespace Avalonia.Controls
         public RowDefinitions(string s)
         public RowDefinitions(string s)
             : this()
             : this()
         {
         {
-            AddRange(GridLength.ParseLengths(s, CultureInfo.InvariantCulture).Select(x => new RowDefinition(x)));
+            AddRange(GridLength.ParseLengths(s).Select(x => new RowDefinition(x)));
         }
         }
     }
     }
 }
 }

+ 4 - 2
src/Avalonia.Themes.Default/MenuItem.xaml

@@ -45,7 +45,8 @@
             <Popup Name="PART_Popup"
             <Popup Name="PART_Popup"
                    PlacementMode="Right"
                    PlacementMode="Right"
                    StaysOpen="True"
                    StaysOpen="True"
-                   IsOpen="{TemplateBinding Path=IsSubMenuOpen, Mode=TwoWay}">
+                   IsOpen="{TemplateBinding Path=IsSubMenuOpen, Mode=TwoWay}"
+                   ObeyScreenEdges="True">
               <Border Background="{TemplateBinding Background}"
               <Border Background="{TemplateBinding Background}"
                       BorderBrush="{DynamicResource ThemeBorderMidBrush}"
                       BorderBrush="{DynamicResource ThemeBorderMidBrush}"
                       BorderThickness="1">
                       BorderThickness="1">
@@ -92,7 +93,8 @@
             </ContentPresenter>
             </ContentPresenter>
             <Popup Name="PART_Popup"
             <Popup Name="PART_Popup"
                    IsOpen="{TemplateBinding Path=IsSubMenuOpen, Mode=TwoWay}"
                    IsOpen="{TemplateBinding Path=IsSubMenuOpen, Mode=TwoWay}"
-                   StaysOpen="True">
+                   StaysOpen="True" 
+                   ObeyScreenEdges="True">
               <Border Background="{TemplateBinding Background}"
               <Border Background="{TemplateBinding Background}"
                       BorderBrush="{DynamicResource ThemeBorderMidBrush}"
                       BorderBrush="{DynamicResource ThemeBorderMidBrush}"
                       BorderThickness="1">
                       BorderThickness="1">

+ 18 - 22
src/Avalonia.Visuals/CornerRadius.cs

@@ -4,6 +4,7 @@
 using System;
 using System;
 using System.Globalization;
 using System.Globalization;
 using System.Linq;
 using System.Linq;
+using Avalonia.Utilities;
 
 
 namespace Avalonia
 namespace Avalonia
 {
 {
@@ -53,31 +54,26 @@ namespace Avalonia
             return $"{TopLeft},{TopRight},{BottomRight},{BottomLeft}";
             return $"{TopLeft},{TopRight},{BottomRight},{BottomLeft}";
         }
         }
 
 
-        public static CornerRadius Parse(string s, CultureInfo culture)
+        public static CornerRadius Parse(string s)
         {
         {
-            var parts = s.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries)
-                .Select(x => x.Trim())
-                .ToList();
-
-            switch (parts.Count)
+            using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Thickness"))
             {
             {
-                case 1:
-                    var uniform = double.Parse(parts[0], culture);
-                    return new CornerRadius(uniform);
-                case 2:
-                    var top = double.Parse(parts[0], culture);
-                    var bottom = double.Parse(parts[1], culture);
-                    return new CornerRadius(top, bottom);
-                case 4:
-                    var topLeft = double.Parse(parts[0], culture);
-                    var topRight = double.Parse(parts[1], culture);
-                    var bottomRight = double.Parse(parts[2], culture);
-                    var bottomLeft = double.Parse(parts[3], culture);
-                    return new CornerRadius(topLeft, topRight, bottomRight, bottomLeft);
-                default:
+                if (tokenizer.TryReadDouble(out var a))
+                {
+                    if (tokenizer.TryReadDouble(out var b))
                     {
                     {
-                        throw new FormatException("Invalid CornerRadius.");
+                        if (tokenizer.TryReadDouble(out var c))
+                        {
+                            return new CornerRadius(a, b, c, tokenizer.ReadDouble());
+                        }
+
+                        return new CornerRadius(a, b);
                     }
                     }
+
+                    return new CornerRadius(a);
+                }
+
+                throw new FormatException("Invalid CornerRadius.");
             }
             }
         }
         }
 
 
@@ -85,7 +81,7 @@ namespace Avalonia
         {
         {
             return cr1.TopLeft.Equals(cr2.TopLeft)
             return cr1.TopLeft.Equals(cr2.TopLeft)
                    && cr1.TopRight.Equals(cr2.TopRight)
                    && cr1.TopRight.Equals(cr2.TopRight)
-                   && cr1.BottomRight.Equals(cr2.BottomRight) 
+                   && cr1.BottomRight.Equals(cr2.BottomRight)
                    && cr1.BottomLeft.Equals(cr2.BottomLeft);
                    && cr1.BottomLeft.Equals(cr2.BottomLeft);
         }
         }
 
 

+ 2 - 3
src/Avalonia.Visuals/Matrix.cs

@@ -314,11 +314,10 @@ namespace Avalonia
         /// Parses a <see cref="Matrix"/> string.
         /// Parses a <see cref="Matrix"/> string.
         /// </summary>
         /// </summary>
         /// <param name="s">The string.</param>
         /// <param name="s">The string.</param>
-        /// <param name="culture">The current culture.</param>
         /// <returns>The <see cref="Matrix"/>.</returns>
         /// <returns>The <see cref="Matrix"/>.</returns>
-        public static Matrix Parse(string s, CultureInfo culture)
+        public static Matrix Parse(string s)
         {
         {
-            using (var tokenizer = new StringTokenizer(s, culture, exceptionMessage: "Invalid Matrix"))
+            using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Matrix"))
             {
             {
                 return new Matrix(
                 return new Matrix(
                     tokenizer.ReadDouble(),
                     tokenizer.ReadDouble(),

+ 2 - 3
src/Avalonia.Visuals/Point.cs

@@ -170,11 +170,10 @@ namespace Avalonia
         /// Parses a <see cref="Point"/> string.
         /// Parses a <see cref="Point"/> string.
         /// </summary>
         /// </summary>
         /// <param name="s">The string.</param>
         /// <param name="s">The string.</param>
-        /// <param name="culture">The current culture.</param>
         /// <returns>The <see cref="Thickness"/>.</returns>
         /// <returns>The <see cref="Thickness"/>.</returns>
-        public static Point Parse(string s, CultureInfo culture)
+        public static Point Parse(string s)
         {
         {
-            using (var tokenizer = new StringTokenizer(s, culture, exceptionMessage: "Invalid Point"))
+            using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Point"))
             {
             {
                 return new Point(
                 return new Point(
                     tokenizer.ReadDouble(),
                     tokenizer.ReadDouble(),

+ 2 - 3
src/Avalonia.Visuals/Rect.cs

@@ -487,11 +487,10 @@ namespace Avalonia
         /// Parses a <see cref="Rect"/> string.
         /// Parses a <see cref="Rect"/> string.
         /// </summary>
         /// </summary>
         /// <param name="s">The string.</param>
         /// <param name="s">The string.</param>
-        /// <param name="culture">The current culture.</param>
         /// <returns>The parsed <see cref="Rect"/>.</returns>
         /// <returns>The parsed <see cref="Rect"/>.</returns>
-        public static Rect Parse(string s, CultureInfo culture)
+        public static Rect Parse(string s)
         {
         {
-            using (var tokenizer = new StringTokenizer(s, culture, exceptionMessage: "Invalid Rect"))
+            using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Rect"))
             {
             {
                 return new Rect(
                 return new Rect(
                     tokenizer.ReadDouble(),
                     tokenizer.ReadDouble(),

+ 4 - 5
src/Avalonia.Visuals/RelativePoint.cs

@@ -154,11 +154,10 @@ namespace Avalonia
         /// Parses a <see cref="RelativePoint"/> string.
         /// Parses a <see cref="RelativePoint"/> string.
         /// </summary>
         /// </summary>
         /// <param name="s">The string.</param>
         /// <param name="s">The string.</param>
-        /// <param name="culture">The current culture.</param>
         /// <returns>The parsed <see cref="RelativePoint"/>.</returns>
         /// <returns>The parsed <see cref="RelativePoint"/>.</returns>
-        public static RelativePoint Parse(string s, CultureInfo culture)
+        public static RelativePoint Parse(string s)
         {
         {
-            using (var tokenizer = new StringTokenizer(s, culture, exceptionMessage: "Invalid RelativePoint"))
+            using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid RelativePoint"))
             {
             {
                 var x = tokenizer.ReadString();
                 var x = tokenizer.ReadString();
                 var y = tokenizer.ReadString();
                 var y = tokenizer.ReadString();
@@ -180,8 +179,8 @@ namespace Avalonia
                 }
                 }
 
 
                 return new RelativePoint(
                 return new RelativePoint(
-                    double.Parse(x, culture) * scale,
-                    double.Parse(y, culture) * scale,
+                    double.Parse(x, CultureInfo.InvariantCulture) * scale,
+                    double.Parse(y, CultureInfo.InvariantCulture) * scale,
                     unit);
                     unit);
             }
             }
         }
         }

+ 6 - 7
src/Avalonia.Visuals/RelativeRect.cs

@@ -167,11 +167,10 @@ namespace Avalonia
         /// Parses a <see cref="RelativeRect"/> string.
         /// Parses a <see cref="RelativeRect"/> string.
         /// </summary>
         /// </summary>
         /// <param name="s">The string.</param>
         /// <param name="s">The string.</param>
-        /// <param name="culture">The current culture.</param>
         /// <returns>The parsed <see cref="RelativeRect"/>.</returns>
         /// <returns>The parsed <see cref="RelativeRect"/>.</returns>
-        public static RelativeRect Parse(string s, CultureInfo culture)
+        public static RelativeRect Parse(string s)
         {
         {
-            using (var tokenizer = new StringTokenizer(s, culture, exceptionMessage: "Invalid RelativeRect"))
+            using (var tokenizer = new StringTokenizer(s, exceptionMessage: "Invalid RelativeRect"))
             {
             {
                 var x = tokenizer.ReadString();
                 var x = tokenizer.ReadString();
                 var y = tokenizer.ReadString();
                 var y = tokenizer.ReadString();
@@ -202,10 +201,10 @@ namespace Avalonia
                 }
                 }
 
 
                 return new RelativeRect(
                 return new RelativeRect(
-                    double.Parse(x, culture) * scale,
-                    double.Parse(y, culture) * scale,
-                    double.Parse(width, culture) * scale,
-                    double.Parse(height, culture) * scale,
+                    double.Parse(x, CultureInfo.InvariantCulture) * scale,
+                    double.Parse(y, CultureInfo.InvariantCulture) * scale,
+                    double.Parse(width, CultureInfo.InvariantCulture) * scale,
+                    double.Parse(height, CultureInfo.InvariantCulture) * scale,
                     unit);
                     unit);
             }
             }
         }
         }

+ 2 - 2
src/Avalonia.Visuals/Rendering/DefaultRenderLoop.cs

@@ -41,12 +41,12 @@ namespace Avalonia.Rendering
         {
         {
             add
             add
             {
             {
+                _tick += value;
+
                 if (_subscriberCount++ == 0)
                 if (_subscriberCount++ == 0)
                 {
                 {
                     Start();
                     Start();
                 }
                 }
-
-                _tick += value;
             }
             }
 
 
             remove
             remove

+ 2 - 3
src/Avalonia.Visuals/Size.cs

@@ -150,11 +150,10 @@ namespace Avalonia
         /// Parses a <see cref="Size"/> string.
         /// Parses a <see cref="Size"/> string.
         /// </summary>
         /// </summary>
         /// <param name="s">The string.</param>
         /// <param name="s">The string.</param>
-        /// <param name="culture">The current culture.</param>
         /// <returns>The <see cref="Size"/>.</returns>
         /// <returns>The <see cref="Size"/>.</returns>
-        public static Size Parse(string s, CultureInfo culture)
+        public static Size Parse(string s)
         {
         {
-            using (var tokenizer = new StringTokenizer(s, culture, exceptionMessage: "Invalid Size"))
+            using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Size"))
             {
             {
                 return new Size(
                 return new Size(
                     tokenizer.ReadDouble(),
                     tokenizer.ReadDouble(),

+ 13 - 11
src/Avalonia.Visuals/Thickness.cs

@@ -165,25 +165,27 @@ namespace Avalonia
         /// Parses a <see cref="Thickness"/> string.
         /// Parses a <see cref="Thickness"/> string.
         /// </summary>
         /// </summary>
         /// <param name="s">The string.</param>
         /// <param name="s">The string.</param>
-        /// <param name="culture">The current culture.</param>
         /// <returns>The <see cref="Thickness"/>.</returns>
         /// <returns>The <see cref="Thickness"/>.</returns>
-        public static Thickness Parse(string s, CultureInfo culture)
+        public static Thickness Parse(string s)
         {
         {
-            using (var tokenizer = new StringTokenizer(s, culture, exceptionMessage: "Invalid Thickness"))
+            using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Thickness"))
             {
             {
-                var a = tokenizer.ReadDouble();
-
-                if (tokenizer.TryReadDouble(out var b))
+                if(tokenizer.TryReadDouble(out var a))
                 {
                 {
-                    if (tokenizer.TryReadDouble(out var c))
+                    if (tokenizer.TryReadDouble(out var b))
                     {
                     {
-                        return new Thickness(a, b, c, tokenizer.ReadDouble());
+                        if (tokenizer.TryReadDouble(out var c))
+                        {
+                            return new Thickness(a, b, c, tokenizer.ReadDouble());
+                        }
+
+                        return new Thickness(a, b);
                     }
                     }
 
 
-                    return new Thickness(a, b);
+                    return new Thickness(a);
                 }
                 }
-                
-                return new Thickness(a);
+
+                throw new FormatException("Invalid Thickness.");
             }
             }
         }
         }
 
 

+ 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 - 1
src/Markup/Avalonia.Markup.Xaml/Converters/CornerRadiusTypeConverter.cs

@@ -13,7 +13,7 @@ namespace Avalonia.Markup.Xaml.Converters
 
 
         public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
         public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
         {
         {
-            return CornerRadius.Parse((string)value, culture);
+            return CornerRadius.Parse((string)value);
         }
         }
     }
     }
 }
 }

+ 1 - 1
src/Markup/Avalonia.Markup.Xaml/Converters/GridLengthTypeConverter.cs

@@ -18,7 +18,7 @@ namespace Avalonia.Markup.Xaml.Converters
 
 
         public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
         public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
         {
         {
-            return GridLength.Parse((string)value, culture);
+            return GridLength.Parse((string)value);
         }
         }
     }
     }
 }
 }

+ 1 - 1
src/Markup/Avalonia.Markup.Xaml/Converters/MatrixTypeConverter.cs

@@ -17,7 +17,7 @@ namespace Avalonia.Markup.Xaml.Converters
 
 
         public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
         public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
         {
         {
-            return Matrix.Parse((string)value, culture);
+            return Matrix.Parse((string)value);
         }
         }
     }
     }
 }
 }

+ 1 - 1
src/Markup/Avalonia.Markup.Xaml/Converters/PointTypeConverter.cs

@@ -17,7 +17,7 @@ namespace Avalonia.Markup.Xaml.Converters
 
 
         public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
         public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
         {
         {
-            return Point.Parse((string)value, culture);
+            return Point.Parse((string)value);
         }
         }
     }
     }
 }
 }

+ 1 - 1
src/Markup/Avalonia.Markup.Xaml/Converters/PointsListTypeConverter.cs

@@ -23,7 +23,7 @@ namespace Avalonia.Markup.Xaml.Converters
             var result = new List<Point>(pointStrs.Length);
             var result = new List<Point>(pointStrs.Length);
             foreach (var pointStr in pointStrs)
             foreach (var pointStr in pointStrs)
             {
             {
-                result.Add(Point.Parse(pointStr, culture));
+                result.Add(Point.Parse(pointStr));
             }
             }
 
 
             return result;
             return result;

+ 1 - 1
src/Markup/Avalonia.Markup.Xaml/Converters/RectTypeConverter.cs

@@ -17,7 +17,7 @@ namespace Avalonia.Markup.Xaml.Converters
 
 
         public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
         public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
         {
         {
-            return Rect.Parse((string)value, culture);
+            return Rect.Parse((string)value);
         }
         }
     }
     }
 }
 }

+ 1 - 1
src/Markup/Avalonia.Markup.Xaml/Converters/RelativePointTypeConverter.cs

@@ -17,7 +17,7 @@ namespace Avalonia.Markup.Xaml.Converters
 
 
         public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
         public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
         {
         {
-            return RelativePoint.Parse((string)value, culture);
+            return RelativePoint.Parse((string)value);
         }
         }
     }
     }
 }
 }

+ 1 - 1
src/Markup/Avalonia.Markup.Xaml/Converters/RelativeRectTypeConverter.cs

@@ -17,7 +17,7 @@ namespace Avalonia.Markup.Xaml.Converters
 
 
         public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
         public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
         {
         {
-            return RelativeRect.Parse((string)value, culture);
+            return RelativeRect.Parse((string)value);
         }
         }
     }
     }
 }
 }

+ 1 - 1
src/Markup/Avalonia.Markup.Xaml/Converters/SizeTypeConverter.cs

@@ -17,7 +17,7 @@ namespace Avalonia.Markup.Xaml.Converters
 
 
         public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
         public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
         {
         {
-            return Size.Parse((string)value, culture);
+            return Size.Parse((string)value);
         }
         }
     }
     }
 }
 }

+ 1 - 1
src/Markup/Avalonia.Markup.Xaml/Converters/ThicknessTypeConverter.cs

@@ -17,7 +17,7 @@ namespace Avalonia.Markup.Xaml.Converters
 
 
         public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
         public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
         {
         {
-            return Thickness.Parse((string)value, culture);
+            return Thickness.Parse((string)value);
         }
         }
     }
     }
 }
 }

+ 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;

+ 0 - 7
src/Windows/Avalonia.Win32/ScreenImpl.cs

@@ -2,16 +2,9 @@
 // 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 Avalonia.Controls;
 using Avalonia.Platform;
 using Avalonia.Platform;
-using Avalonia.Utilities;
 using static Avalonia.Win32.Interop.UnmanagedMethods;
 using static Avalonia.Win32.Interop.UnmanagedMethods;
 
 
-#if NETSTANDARD
-using Win32Exception = Avalonia.Win32.NetStandard.AvaloniaWin32Exception;
-#endif
-
 namespace Avalonia.Win32
 namespace Avalonia.Win32
 {
 {
     public class ScreenImpl : IScreenImpl
     public class ScreenImpl : IScreenImpl

+ 7 - 11
src/Windows/Avalonia.Win32/Win32Platform.cs

@@ -1,27 +1,23 @@
 // 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 Avalonia.Input.Platform;
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
-using System.Reactive.Disposables;
+using System.ComponentModel;
 using System.Diagnostics.CodeAnalysis;
 using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Reactive.Disposables;
 using System.Runtime.InteropServices;
 using System.Runtime.InteropServices;
 using System.Threading;
 using System.Threading;
+using Avalonia.Controls;
 using Avalonia.Controls.Platform;
 using Avalonia.Controls.Platform;
 using Avalonia.Input;
 using Avalonia.Input;
+using Avalonia.Input.Platform;
 using Avalonia.Platform;
 using Avalonia.Platform;
-using Avalonia.Win32.Input;
-using Avalonia.Win32.Interop;
-using Avalonia.Controls;
 using Avalonia.Rendering;
 using Avalonia.Rendering;
 using Avalonia.Threading;
 using Avalonia.Threading;
-using System.IO;
-#if NETSTANDARD
-using Win32Exception = Avalonia.Win32.NetStandard.AvaloniaWin32Exception;
-#else
-using System.ComponentModel;
-#endif
+using Avalonia.Win32.Input;
+using Avalonia.Win32.Interop;
 
 
 namespace Avalonia
 namespace Avalonia
 {
 {

+ 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
+        {
+        }
     }
     }
 }
 }

+ 9 - 9
tests/Avalonia.Controls.UnitTests/GridLengthTests.cs

@@ -13,7 +13,7 @@ namespace Avalonia.Controls.UnitTests
         [Fact]
         [Fact]
         public void Parse_Should_Parse_Auto()
         public void Parse_Should_Parse_Auto()
         {
         {
-            var result = GridLength.Parse("Auto", CultureInfo.InvariantCulture);
+            var result = GridLength.Parse("Auto");
 
 
             Assert.Equal(GridLength.Auto, result);
             Assert.Equal(GridLength.Auto, result);
         }
         }
@@ -21,7 +21,7 @@ namespace Avalonia.Controls.UnitTests
         [Fact]
         [Fact]
         public void Parse_Should_Parse_Auto_Lowercase()
         public void Parse_Should_Parse_Auto_Lowercase()
         {
         {
-            var result = GridLength.Parse("auto", CultureInfo.InvariantCulture);
+            var result = GridLength.Parse("auto");
 
 
             Assert.Equal(GridLength.Auto, result);
             Assert.Equal(GridLength.Auto, result);
         }
         }
@@ -29,7 +29,7 @@ namespace Avalonia.Controls.UnitTests
         [Fact]
         [Fact]
         public void Parse_Should_Parse_Star()
         public void Parse_Should_Parse_Star()
         {
         {
-            var result = GridLength.Parse("*", CultureInfo.InvariantCulture);
+            var result = GridLength.Parse("*");
 
 
             Assert.Equal(new GridLength(1, GridUnitType.Star), result);
             Assert.Equal(new GridLength(1, GridUnitType.Star), result);
         }
         }
@@ -37,7 +37,7 @@ namespace Avalonia.Controls.UnitTests
         [Fact]
         [Fact]
         public void Parse_Should_Parse_Star_Value()
         public void Parse_Should_Parse_Star_Value()
         {
         {
-            var result = GridLength.Parse("2*", CultureInfo.InvariantCulture);
+            var result = GridLength.Parse("2*");
 
 
             Assert.Equal(new GridLength(2, GridUnitType.Star), result);
             Assert.Equal(new GridLength(2, GridUnitType.Star), result);
         }
         }
@@ -45,7 +45,7 @@ namespace Avalonia.Controls.UnitTests
         [Fact]
         [Fact]
         public void Parse_Should_Parse_Pixel_Value()
         public void Parse_Should_Parse_Pixel_Value()
         {
         {
-            var result = GridLength.Parse("2", CultureInfo.InvariantCulture);
+            var result = GridLength.Parse("2");
 
 
             Assert.Equal(new GridLength(2, GridUnitType.Pixel), result);
             Assert.Equal(new GridLength(2, GridUnitType.Pixel), result);
         }
         }
@@ -53,13 +53,13 @@ namespace Avalonia.Controls.UnitTests
         [Fact]
         [Fact]
         public void Parse_Should_Throw_FormatException_For_Invalid_String()
         public void Parse_Should_Throw_FormatException_For_Invalid_String()
         {
         {
-            Assert.Throws<FormatException>(() => GridLength.Parse("2x", CultureInfo.InvariantCulture));
+            Assert.Throws<FormatException>(() => GridLength.Parse("2x"));
         }
         }
 
 
         [Fact]
         [Fact]
         public void ParseLengths_Accepts_Comma_Separators()
         public void ParseLengths_Accepts_Comma_Separators()
         {
         {
-            var result = GridLength.ParseLengths("*,Auto,2*,4", CultureInfo.InvariantCulture).ToList();
+            var result = GridLength.ParseLengths("*,Auto,2*,4").ToList();
 
 
             Assert.Equal(
             Assert.Equal(
                 new[]
                 new[]
@@ -75,7 +75,7 @@ namespace Avalonia.Controls.UnitTests
         [Fact]
         [Fact]
         public void ParseLengths_Accepts_Space_Separators()
         public void ParseLengths_Accepts_Space_Separators()
         {
         {
-            var result = GridLength.ParseLengths("* Auto 2* 4", CultureInfo.InvariantCulture).ToList();
+            var result = GridLength.ParseLengths("* Auto 2* 4").ToList();
 
 
             Assert.Equal(
             Assert.Equal(
                 new[]
                 new[]
@@ -91,7 +91,7 @@ namespace Avalonia.Controls.UnitTests
         [Fact]
         [Fact]
         public void ParseLengths_Accepts_Comma_Separators_With_Spaces()
         public void ParseLengths_Accepts_Comma_Separators_With_Spaces()
         {
         {
-            var result = GridLength.ParseLengths("*, Auto, 2* ,4", CultureInfo.InvariantCulture).ToList();
+            var result = GridLength.ParseLengths("*, Auto, 2* ,4").ToList();
 
 
             Assert.Equal(
             Assert.Equal(
                 new[]
                 new[]

+ 9 - 9
tests/Avalonia.Markup.UnitTests/Data/BindingExpressionTests.cs

@@ -58,7 +58,7 @@ namespace Avalonia.Markup.UnitTests.Data
         [Fact]
         [Fact]
         public async Task Should_Convert_Get_String_To_Double()
         public async Task Should_Convert_Get_String_To_Double()
         {
         {
-            var data = new Class1 { StringValue = "5.6" };
+            var data = new Class1 { StringValue = $"{5.6}" };
             var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(double));
             var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(double));
             var result = await target.Take(1);
             var result = await target.Take(1);
 
 
@@ -94,12 +94,12 @@ namespace Avalonia.Markup.UnitTests.Data
         [Fact]
         [Fact]
         public void Should_Convert_Set_String_To_Double()
         public void Should_Convert_Set_String_To_Double()
         {
         {
-            var data = new Class1 { StringValue = (5.6).ToString() };
+            var data = new Class1 { StringValue = $"{5.6}" };
             var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(double));
             var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(double));
 
 
             target.OnNext(6.7);
             target.OnNext(6.7);
 
 
-            Assert.Equal((6.7).ToString(), data.StringValue);
+            Assert.Equal($"{6.7}", data.StringValue);
 
 
             GC.KeepAlive(data);
             GC.KeepAlive(data);
         }
         }
@@ -111,7 +111,7 @@ namespace Avalonia.Markup.UnitTests.Data
             var target = new BindingExpression(new ExpressionObserver(data, "DoubleValue"), typeof(string));
             var target = new BindingExpression(new ExpressionObserver(data, "DoubleValue"), typeof(string));
             var result = await target.Take(1);
             var result = await target.Take(1);
 
 
-            Assert.Equal((5.6).ToString(), result);
+            Assert.Equal($"{5.6}", result);
 
 
             GC.KeepAlive(data);
             GC.KeepAlive(data);
         }
         }
@@ -122,7 +122,7 @@ namespace Avalonia.Markup.UnitTests.Data
             var data = new Class1 { DoubleValue = 5.6 };
             var data = new Class1 { DoubleValue = 5.6 };
             var target = new BindingExpression(new ExpressionObserver(data, "DoubleValue"), typeof(string));
             var target = new BindingExpression(new ExpressionObserver(data, "DoubleValue"), typeof(string));
 
 
-            target.OnNext("6.7");
+            target.OnNext($"{6.7}");
 
 
             Assert.Equal(6.7, data.DoubleValue);
             Assert.Equal(6.7, data.DoubleValue);
 
 
@@ -318,15 +318,15 @@ namespace Avalonia.Markup.UnitTests.Data
 
 
             target.Subscribe(x => result.Add(x));
             target.Subscribe(x => result.Add(x));
             target.OnNext(1.2);
             target.OnNext(1.2);
-            target.OnNext("3.4");
+            target.OnNext($"{3.4}");
             target.OnNext("bar");
             target.OnNext("bar");
 
 
             Assert.Equal(
             Assert.Equal(
                 new[]
                 new[]
                 {
                 {
-                    new BindingNotification("5.6"),
-                    new BindingNotification("1.2"),
-                    new BindingNotification("3.4"),
+                    new BindingNotification($"{5.6}"),
+                    new BindingNotification($"{1.2}"),
+                    new BindingNotification($"{3.4}"),
                     new BindingNotification(
                     new BindingNotification(
                         new InvalidCastException("'bar' is not a valid number."),
                         new InvalidCastException("'bar' is not a valid number."),
                         BindingErrorType.Error)
                         BindingErrorType.Error)

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

+ 4 - 4
tests/Avalonia.Visuals.UnitTests/CornerRadiusTests.cs

@@ -11,7 +11,7 @@ namespace Avalonia.Visuals.UnitTests
         [Fact]
         [Fact]
         public void Parse_Parses_Single_Uniform_Radius()
         public void Parse_Parses_Single_Uniform_Radius()
         {
         {
-            var result = CornerRadius.Parse("3.4", CultureInfo.InvariantCulture);
+            var result = CornerRadius.Parse("3.4");
 
 
             Assert.Equal(new CornerRadius(3.4), result);
             Assert.Equal(new CornerRadius(3.4), result);
         }
         }
@@ -19,7 +19,7 @@ namespace Avalonia.Visuals.UnitTests
         [Fact]
         [Fact]
         public void Parse_Parses_Top_Bottom()
         public void Parse_Parses_Top_Bottom()
         {
         {
-            var result = CornerRadius.Parse("1.1,2.2", CultureInfo.InvariantCulture);
+            var result = CornerRadius.Parse("1.1,2.2");
 
 
             Assert.Equal(new CornerRadius(1.1, 2.2), result);
             Assert.Equal(new CornerRadius(1.1, 2.2), result);
         }
         }
@@ -27,7 +27,7 @@ namespace Avalonia.Visuals.UnitTests
         [Fact]
         [Fact]
         public void Parse_Parses_TopLeft_TopRight_BottomRight_BottomLeft()
         public void Parse_Parses_TopLeft_TopRight_BottomRight_BottomLeft()
         {
         {
-            var result = CornerRadius.Parse("1.1,2.2,3.3,4.4", CultureInfo.InvariantCulture);
+            var result = CornerRadius.Parse("1.1,2.2,3.3,4.4");
 
 
             Assert.Equal(new CornerRadius(1.1, 2.2, 3.3, 4.4), result);
             Assert.Equal(new CornerRadius(1.1, 2.2, 3.3, 4.4), result);
         }
         }
@@ -35,7 +35,7 @@ namespace Avalonia.Visuals.UnitTests
         [Fact]
         [Fact]
         public void Parse_Accepts_Spaces()
         public void Parse_Accepts_Spaces()
         {
         {
-            var result = CornerRadius.Parse("1.1 2.2 3.3 4.4", CultureInfo.InvariantCulture);
+            var result = CornerRadius.Parse("1.1 2.2 3.3 4.4");
 
 
             Assert.Equal(new CornerRadius(1.1, 2.2, 3.3, 4.4), result);
             Assert.Equal(new CornerRadius(1.1, 2.2, 3.3, 4.4), result);
         }
         }

+ 1 - 1
tests/Avalonia.Visuals.UnitTests/Media/MatrixTests.cs

@@ -8,7 +8,7 @@ namespace Avalonia.Visuals.UnitTests.Media
         [Fact]
         [Fact]
         public void Parse_Parses()
         public void Parse_Parses()
         {
         {
-            var matrix = Matrix.Parse("1,2,3,-4,5 6", CultureInfo.CurrentCulture);
+            var matrix = Matrix.Parse("1,2,3,-4,5 6");
             var expected = new Matrix(1, 2, 3, -4, 5, 6);
             var expected = new Matrix(1, 2, 3, -4, 5, 6);
             Assert.Equal(expected, matrix);
             Assert.Equal(expected, matrix);
         }
         }

+ 1 - 1
tests/Avalonia.Visuals.UnitTests/Media/RectTests.cs

@@ -8,7 +8,7 @@ namespace Avalonia.Visuals.UnitTests.Media
         [Fact]
         [Fact]
         public void Parse_Parses()
         public void Parse_Parses()
         {
         {
-            var rect = Rect.Parse("1,2 3,-4", CultureInfo.CurrentCulture);
+            var rect = Rect.Parse("1,2 3,-4");
             var expected = new Rect(1, 2, 3, -4);
             var expected = new Rect(1, 2, 3, -4);
             Assert.Equal(expected, rect);
             Assert.Equal(expected, rect);
         }
         }

+ 2 - 2
tests/Avalonia.Visuals.UnitTests/RelativePointTests.cs

@@ -11,7 +11,7 @@ namespace Avalonia.Visuals.UnitTests
         [Fact]
         [Fact]
         public void Parse_Should_Accept_Absolute_Value()
         public void Parse_Should_Accept_Absolute_Value()
         {
         {
-            var result = RelativePoint.Parse("4,5", CultureInfo.InvariantCulture);
+            var result = RelativePoint.Parse("4,5");
 
 
             Assert.Equal(new RelativePoint(4, 5, RelativeUnit.Absolute), result);
             Assert.Equal(new RelativePoint(4, 5, RelativeUnit.Absolute), result);
         }
         }
@@ -19,7 +19,7 @@ namespace Avalonia.Visuals.UnitTests
         [Fact]
         [Fact]
         public void Parse_Should_Accept_Relative_Value()
         public void Parse_Should_Accept_Relative_Value()
         {
         {
-            var result = RelativePoint.Parse("25%, 50%", CultureInfo.InvariantCulture);
+            var result = RelativePoint.Parse("25%, 50%");
 
 
             Assert.Equal(new RelativePoint(0.25, 0.5, RelativeUnit.Relative), result);
             Assert.Equal(new RelativePoint(0.25, 0.5, RelativeUnit.Relative), result);
         }
         }

+ 3 - 3
tests/Avalonia.Visuals.UnitTests/RelativeRectTests.cs

@@ -14,7 +14,7 @@ namespace Avalonia.Visuals.UnitTests
         [Fact]
         [Fact]
         public void Parse_Should_Accept_Absolute_Value()
         public void Parse_Should_Accept_Absolute_Value()
         {
         {
-            var result = RelativeRect.Parse("4,5,50,60", CultureInfo.InvariantCulture);
+            var result = RelativeRect.Parse("4,5,50,60");
 
 
             Assert.Equal(new RelativeRect(4, 5, 50, 60, RelativeUnit.Absolute), result, Compare);
             Assert.Equal(new RelativeRect(4, 5, 50, 60, RelativeUnit.Absolute), result, Compare);
         }
         }
@@ -22,7 +22,7 @@ namespace Avalonia.Visuals.UnitTests
         [Fact]
         [Fact]
         public void Parse_Should_Accept_Relative_Value()
         public void Parse_Should_Accept_Relative_Value()
         {
         {
-            var result = RelativeRect.Parse("10%, 20%, 40%, 70%", CultureInfo.InvariantCulture);
+            var result = RelativeRect.Parse("10%, 20%, 40%, 70%");
 
 
             Assert.Equal(new RelativeRect(0.1, 0.2, 0.4, 0.7, RelativeUnit.Relative), result, Compare);
             Assert.Equal(new RelativeRect(0.1, 0.2, 0.4, 0.7, RelativeUnit.Relative), result, Compare);
         }
         }
@@ -31,7 +31,7 @@ namespace Avalonia.Visuals.UnitTests
         public void Parse_Should_Throw_Mixed_Values()
         public void Parse_Should_Throw_Mixed_Values()
         {
         {
             Assert.Throws<FormatException>(() =>
             Assert.Throws<FormatException>(() =>
-                RelativeRect.Parse("10%, 20%, 40, 70%", CultureInfo.InvariantCulture));
+                RelativeRect.Parse("10%, 20%, 40, 70%"));
         }
         }
     }
     }
 }
 }

+ 4 - 4
tests/Avalonia.Visuals.UnitTests/ThicknessTests.cs

@@ -11,7 +11,7 @@ namespace Avalonia.Visuals.UnitTests
         [Fact]
         [Fact]
         public void Parse_Parses_Single_Uniform_Size()
         public void Parse_Parses_Single_Uniform_Size()
         {
         {
-            var result = Thickness.Parse("1.2", CultureInfo.InvariantCulture);
+            var result = Thickness.Parse("1.2");
 
 
             Assert.Equal(new Thickness(1.2), result);
             Assert.Equal(new Thickness(1.2), result);
         }
         }
@@ -19,7 +19,7 @@ namespace Avalonia.Visuals.UnitTests
         [Fact]
         [Fact]
         public void Parse_Parses_Horizontal_Vertical()
         public void Parse_Parses_Horizontal_Vertical()
         {
         {
-            var result = Thickness.Parse("1.2,3.4", CultureInfo.InvariantCulture);
+            var result = Thickness.Parse("1.2,3.4");
 
 
             Assert.Equal(new Thickness(1.2, 3.4), result);
             Assert.Equal(new Thickness(1.2, 3.4), result);
         }
         }
@@ -27,7 +27,7 @@ namespace Avalonia.Visuals.UnitTests
         [Fact]
         [Fact]
         public void Parse_Parses_Left_Top_Right_Bottom()
         public void Parse_Parses_Left_Top_Right_Bottom()
         {
         {
-            var result = Thickness.Parse("1.2, 3.4, 5, 6", CultureInfo.InvariantCulture);
+            var result = Thickness.Parse("1.2, 3.4, 5, 6");
 
 
             Assert.Equal(new Thickness(1.2, 3.4, 5, 6), result);
             Assert.Equal(new Thickness(1.2, 3.4, 5, 6), result);
         }
         }
@@ -35,7 +35,7 @@ namespace Avalonia.Visuals.UnitTests
         [Fact]
         [Fact]
         public void Parse_Accepts_Spaces()
         public void Parse_Accepts_Spaces()
         {
         {
-            var result = Thickness.Parse("1.2 3.4 5 6", CultureInfo.InvariantCulture);
+            var result = Thickness.Parse("1.2 3.4 5 6");
 
 
             Assert.Equal(new Thickness(1.2, 3.4, 5, 6), result);
             Assert.Equal(new Thickness(1.2, 3.4, 5, 6), result);
         }
         }