Browse Source

Merge branch 'master' into fixes/2985-treeview-sort-crash

Steven Kirk 6 years ago
parent
commit
68f3424147
35 changed files with 383 additions and 420 deletions
  1. 10 2
      azure-pipelines.yml
  2. 1 3
      samples/ControlCatalog/App.xaml.cs
  3. 10 2
      src/Avalonia.Base/AvaloniaPropertyRegistry.cs
  4. 14 3
      src/Avalonia.Base/Data/BindingOperations.cs
  5. 12 1
      src/Avalonia.Base/Data/Core/BindingExpression.cs
  6. 3 3
      src/Avalonia.Base/Data/Core/ExpressionObserver.cs
  7. 17 3
      src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs
  8. 11 3
      src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs
  9. 19 6
      src/Avalonia.Base/Reactive/LightweightObservableBase.cs
  10. 1 1
      src/Avalonia.Base/Utilities/IdentifierParser.cs
  11. 20 1
      src/Avalonia.Controls/Application.cs
  12. 15 10
      src/Avalonia.Controls/Primitives/RangeBase.cs
  13. 6 0
      src/Avalonia.Controls/TopLevel.cs
  14. 13 0
      src/Avalonia.Styling/IDataContextProvider.cs
  15. 2 6
      src/Avalonia.Styling/IStyledElement.cs
  16. 1 1
      src/Avalonia.Styling/StyledElement.cs
  17. 8 19
      src/Avalonia.Visuals/Media/FontFamily.cs
  18. 32 2
      src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs
  19. 7 0
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs
  20. 9 2
      src/Markup/Avalonia.Markup/Data/Binding.cs
  21. 16 0
      src/Markup/Avalonia.Markup/Data/MultiBinding.cs
  22. 28 0
      tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs
  23. 59 0
      tests/Avalonia.Benchmarks/Data/BindingsBenchmark.cs
  24. 0 16
      tests/Avalonia.Controls.UnitTests/Primitives/RangeBaseTests.cs
  25. 18 0
      tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs
  26. 30 0
      tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests.cs
  27. 4 81
      tests/Avalonia.Styling.UnitTests/SelectorTests_Child.cs
  28. 1 1
      tests/Avalonia.Styling.UnitTests/SelectorTests_Class.cs
  29. 4 81
      tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs
  30. 2 1
      tests/Avalonia.Styling.UnitTests/SelectorTests_Name.cs
  31. 2 2
      tests/Avalonia.Styling.UnitTests/SelectorTests_Not.cs
  32. 3 2
      tests/Avalonia.Styling.UnitTests/SelectorTests_OfType.cs
  33. 5 4
      tests/Avalonia.Styling.UnitTests/SelectorTests_Or.cs
  34. 0 83
      tests/Avalonia.Styling.UnitTests/TestControlBase.cs
  35. 0 81
      tests/Avalonia.Styling.UnitTests/TestTemplatedControl.cs

+ 10 - 2
azure-pipelines.yml

@@ -34,9 +34,17 @@ jobs:
   pool:
     vmImage: 'macOS-10.14'
   steps:
-  - task: DotNetCoreInstaller@0
+  - task: UseDotNet@2
+    displayName: 'Use .NET Core SDK 3.0.x'
     inputs:
-      version: '2.1.403'
+      packageType: sdk
+      version: 3.0.x
+
+  - task: UseDotNet@2
+    displayName: 'Use .NET Core Runtime 2.1.x'
+    inputs:
+      packageType: runtime
+      version: 2.1.x
 
   - task: CmdLine@2
     displayName: 'Install Mono 5.18'

+ 1 - 3
samples/ControlCatalog/App.xaml.cs

@@ -1,6 +1,4 @@
-using System;
 using Avalonia;
-using Avalonia.Controls;
 using Avalonia.Controls.ApplicationLifetimes;
 using Avalonia.Markup.Xaml;
 
@@ -19,7 +17,7 @@ namespace ControlCatalog
                 desktopLifetime.MainWindow = new MainWindow();
             else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewLifetime)
                 singleViewLifetime.MainView = new MainView();
-            
+
             base.OnFrameworkInitializationCompleted();
         }
     }

+ 10 - 2
src/Avalonia.Base/AvaloniaPropertyRegistry.cs

@@ -173,12 +173,20 @@ namespace Avalonia
             Contract.Requires<ArgumentNullException>(type != null);
             Contract.Requires<ArgumentNullException>(name != null);
 
-            if (name.Contains('.'))
+            if (name.Contains("."))
             {
                 throw new InvalidOperationException("Attached properties not supported.");
             }
 
-            return GetRegistered(type).FirstOrDefault(x => x.Name == name);
+            foreach (AvaloniaProperty x in GetRegistered(type))
+            {
+                if (x.Name == name)
+                {
+                    return x;
+                }
+            }
+
+            return null;
         }
 
         /// <summary>

+ 14 - 3
src/Avalonia.Base/Data/BindingOperations.cs

@@ -2,7 +2,6 @@
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 using System;
-using System.Linq;
 using System.Reactive.Disposables;
 using System.Reactive.Linq;
 
@@ -56,22 +55,34 @@ namespace Avalonia.Data
 
                     if (source != null)
                     {
+                        // Perf: Avoid allocating closure in the outer scope.
+                        var targetCopy = target;
+                        var propertyCopy = property;
+                        var bindingCopy = binding;
+
                         return source
                             .Where(x => BindingNotification.ExtractValue(x) != AvaloniaProperty.UnsetValue)
                             .Take(1)
-                            .Subscribe(x => target.SetValue(property, x, binding.Priority));
+                            .Subscribe(x => targetCopy.SetValue(propertyCopy, x, bindingCopy.Priority));
                     }
                     else
                     {
                         target.SetValue(property, binding.Value, binding.Priority);
                         return Disposable.Empty;
                     }
+
                 case BindingMode.OneWayToSource:
+                {
+                    // Perf: Avoid allocating closure in the outer scope.
+                    var bindingCopy = binding;
+
                     return Observable.CombineLatest(
                         binding.Observable,
                         target.GetObservable(property),
                         (_, v) => v)
-                    .Subscribe(x => binding.Subject.OnNext(x));
+                    .Subscribe(x => bindingCopy.Subject.OnNext(x));
+                }
+
                 default:
                     throw new ArgumentException("Invalid binding mode.");
             }

+ 12 - 1
src/Avalonia.Base/Data/Core/BindingExpression.cs

@@ -21,6 +21,7 @@ namespace Avalonia.Data.Core
         private readonly ExpressionObserver _inner;
         private readonly Type _targetType;
         private readonly object _fallbackValue;
+        private readonly object _targetNullValue;
         private readonly BindingPriority _priority;
         InnerListener _innerListener;
         WeakReference<object> _value;
@@ -51,7 +52,7 @@ namespace Avalonia.Data.Core
             IValueConverter converter,
             object converterParameter = null,
             BindingPriority priority = BindingPriority.LocalValue)
-            : this(inner, targetType, AvaloniaProperty.UnsetValue, converter, converterParameter, priority)
+            : this(inner, targetType, AvaloniaProperty.UnsetValue, AvaloniaProperty.UnsetValue, converter, converterParameter, priority)
         {
         }
 
@@ -63,6 +64,9 @@ namespace Avalonia.Data.Core
         /// <param name="fallbackValue">
         /// The value to use when the binding is unable to produce a value.
         /// </param>
+        /// <param name="targetNullValue">
+        /// The value to use when the binding result is null.
+        /// </param>
         /// <param name="converter">The value converter to use.</param>
         /// <param name="converterParameter">
         /// A parameter to pass to <paramref name="converter"/>.
@@ -72,6 +76,7 @@ namespace Avalonia.Data.Core
             ExpressionObserver inner, 
             Type targetType,
             object fallbackValue,
+            object targetNullValue,
             IValueConverter converter,
             object converterParameter = null,
             BindingPriority priority = BindingPriority.LocalValue)
@@ -85,6 +90,7 @@ namespace Avalonia.Data.Core
             Converter = converter;
             ConverterParameter = converterParameter;
             _fallbackValue = fallbackValue;
+            _targetNullValue = targetNullValue;
             _priority = priority;
         }
 
@@ -196,6 +202,11 @@ namespace Avalonia.Data.Core
         /// <inheritdoc/>
         private object ConvertValue(object value)
         {
+            if (value == null && _targetNullValue != AvaloniaProperty.UnsetValue)
+            {
+                return _targetNullValue;
+            }
+
             if (value == BindingOperations.DoNothing)
             {
                 return value;

+ 3 - 3
src/Avalonia.Base/Data/Core/ExpressionObserver.cs

@@ -21,7 +21,7 @@ namespace Avalonia.Data.Core
         /// An ordered collection of property accessor plugins that can be used to customize
         /// the reading and subscription of property values on a type.
         /// </summary>
-        public static readonly IList<IPropertyAccessorPlugin> PropertyAccessors =
+        public static readonly List<IPropertyAccessorPlugin> PropertyAccessors =
             new List<IPropertyAccessorPlugin>
             {
                 new AvaloniaPropertyAccessorPlugin(),
@@ -33,7 +33,7 @@ namespace Avalonia.Data.Core
         /// An ordered collection of validation checker plugins that can be used to customize
         /// the validation of view model and model data.
         /// </summary>
-        public static readonly IList<IDataValidationPlugin> DataValidators =
+        public static readonly List<IDataValidationPlugin> DataValidators =
             new List<IDataValidationPlugin>
             {
                 new DataAnnotationsValidationPlugin(),
@@ -45,7 +45,7 @@ namespace Avalonia.Data.Core
         /// An ordered collection of stream plugins that can be used to customize the behavior
         /// of the '^' stream binding operator.
         /// </summary>
-        public static readonly IList<IStreamPlugin> StreamHandlers =
+        public static readonly List<IStreamPlugin> StreamHandlers =
             new List<IStreamPlugin>
             {
                 new TaskStreamPlugin(),

+ 17 - 3
src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs

@@ -2,7 +2,7 @@
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 using System;
-using System.Reactive.Linq;
+using System.Runtime.ExceptionServices;
 
 namespace Avalonia.Data.Core.Plugins
 {
@@ -76,7 +76,7 @@ namespace Avalonia.Data.Core.Plugins
             return false;
         }
 
-        private class Accessor : PropertyAccessorBase
+        private class Accessor : PropertyAccessorBase, IObserver<object>
         {
             private readonly WeakReference<AvaloniaObject> _reference;
             private readonly AvaloniaProperty _property;
@@ -117,7 +117,7 @@ namespace Avalonia.Data.Core.Plugins
 
             protected override void SubscribeCore()
             {
-                _subscription = Instance?.GetObservable(_property).Subscribe(PublishValue);
+                _subscription = Instance?.GetObservable(_property).Subscribe(this);
             }
 
             protected override void UnsubscribeCore()
@@ -125,6 +125,20 @@ namespace Avalonia.Data.Core.Plugins
                 _subscription?.Dispose();
                 _subscription = null;
             }
+
+            void IObserver<object>.OnCompleted()
+            {
+            }
+
+            void IObserver<object>.OnError(Exception error)
+            {
+                ExceptionDispatchInfo.Capture(error).Throw();
+            }
+
+            void IObserver<object>.OnNext(object value)
+            {
+                PublishValue(value);
+            }
         }
     }
 }

+ 11 - 3
src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs

@@ -2,8 +2,6 @@
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 using System;
-using System.Linq;
-using System.Reactive.Linq;
 using Avalonia.Data.Core.Plugins;
 
 namespace Avalonia.Data.Core
@@ -41,7 +39,17 @@ namespace Avalonia.Data.Core
         {
             reference.TryGetTarget(out object target);
 
-            var plugin = ExpressionObserver.PropertyAccessors.FirstOrDefault(x => x.Match(target, PropertyName));
+            IPropertyAccessorPlugin plugin = null;
+
+            foreach (IPropertyAccessorPlugin x in ExpressionObserver.PropertyAccessors)
+            {
+                if (x.Match(target, PropertyName))
+                {
+                    plugin = x;
+                    break;
+                }
+            }
+
             var accessor = plugin?.Start(reference, PropertyName);
 
             if (_enableValidation && Next == null)

+ 19 - 6
src/Avalonia.Base/Reactive/LightweightObservableBase.cs

@@ -116,20 +116,33 @@ namespace Avalonia.Reactive
         {
             if (Volatile.Read(ref _observers) != null)
             {
-                IObserver<T>[] observers;
-
+                IObserver<T>[] observers = null;
+                IObserver<T> singleObserver = null;
                 lock (this)
                 {
                     if (_observers == null)
                     {
                         return;
                     }
-                    observers = _observers.ToArray();
+                    if (_observers.Count == 1)
+                    {
+                        singleObserver = _observers[0];
+                    }
+                    else
+                    {
+                        observers = _observers.ToArray();
+                    }
                 }
-
-                foreach (var observer in observers)
+                if (singleObserver != null)
                 {
-                    observer.OnNext(value);
+                    singleObserver.OnNext(value);
+                }
+                else
+                {
+                    foreach (var observer in observers)
+                    {
+                        observer.OnNext(value);
+                    }
                 }
             }
         }

+ 1 - 1
src/Avalonia.Base/Utilities/IdentifierParser.cs

@@ -15,7 +15,7 @@ namespace Avalonia.Utilities
         {
             if (IsValidIdentifierStart(r.Peek))
             {
-                return r.TakeWhile(IsValidIdentifierChar);
+                return r.TakeWhile(c => IsValidIdentifierChar(c));
             }
             else
             {

+ 20 - 1
src/Avalonia.Controls/Application.cs

@@ -32,7 +32,7 @@ namespace Avalonia
     /// method.
     /// - Tracks the lifetime of the application.
     /// </remarks>
-    public class Application : AvaloniaObject, IGlobalDataTemplates, IGlobalStyles, IStyleRoot, IResourceNode
+    public class Application : AvaloniaObject, IDataContextProvider, IGlobalDataTemplates, IGlobalStyles, IStyleRoot, IResourceNode
     {
         /// <summary>
         /// The application-global data templates.
@@ -45,6 +45,12 @@ namespace Avalonia
         private Styles _styles;
         private IResourceDictionary _resources;
 
+        /// <summary>
+        /// Defines the <see cref="DataContext"/> property.
+        /// </summary>
+        public static readonly StyledProperty<object> DataContextProperty =
+            StyledElement.DataContextProperty.AddOwner<Application>();
+
         /// <inheritdoc/>
         public event EventHandler<ResourcesChangedEventArgs> ResourcesChanged;
 
@@ -56,6 +62,19 @@ namespace Avalonia
             Name = "Avalonia Application";
         }
 
+        /// <summary>
+        /// Gets or sets the Applications's data context.
+        /// </summary>
+        /// <remarks>
+        /// The data context property specifies the default object that will
+        /// be used for data binding.
+        /// </remarks>
+        public object DataContext
+        {
+            get { return GetValue(DataContextProperty); }
+            set { SetValue(DataContextProperty, value); }
+        }
+
         /// <summary>
         /// Gets the current instance of the <see cref="Application"/> class.
         /// </summary>

+ 15 - 10
src/Avalonia.Controls/Primitives/RangeBase.cs

@@ -75,7 +75,10 @@ namespace Avalonia.Controls.Primitives
 
             set
             {
-                ValidateDouble(value, "Minimum");
+                if (!ValidateDouble(value))
+                {
+                    return;
+                }
 
                 if (IsInitialized)
                 {
@@ -102,7 +105,10 @@ namespace Avalonia.Controls.Primitives
 
             set
             {
-                ValidateDouble(value, "Maximum");
+                if (!ValidateDouble(value))
+                {
+                    return;
+                }
 
                 if (IsInitialized)
                 {
@@ -129,7 +135,10 @@ namespace Avalonia.Controls.Primitives
 
             set
             {
-                ValidateDouble(value, "Value");
+                if (!ValidateDouble(value))
+                {
+                    return;
+                }
 
                 if (IsInitialized)
                 {
@@ -164,16 +173,12 @@ namespace Avalonia.Controls.Primitives
         }
 
         /// <summary>
-        /// Throws an exception if the double value is NaN or Inf.
+        /// Checks if the double value is not inifinity nor NaN.
         /// </summary>
         /// <param name="value">The value.</param>
-        /// <param name="property">The name of the property being set.</param>
-        private static void ValidateDouble(double value, string property)
+        private static bool ValidateDouble(double value)
         {
-            if (double.IsInfinity(value) || double.IsNaN(value))
-            {
-                throw new ArgumentException($"{value} is not a valid value for {property}.");
-            }
+            return !double.IsInfinity(value) || !double.IsNaN(value);
         }
 
         /// <summary>

+ 6 - 0
src/Avalonia.Controls/TopLevel.cs

@@ -269,6 +269,12 @@ namespace Avalonia.Controls
         /// </summary>
         protected virtual void HandleClosed()
         {
+            var logicalArgs = new LogicalTreeAttachmentEventArgs(this);
+            ((ILogical)this).NotifyDetachedFromLogicalTree(logicalArgs);
+
+            var visualArgs = new VisualTreeAttachmentEventArgs(this, this);
+            OnDetachedFromVisualTreeCore(visualArgs);
+
             (this as IInputRoot).MouseDevice?.TopLevelClosed(this);
             PlatformImpl = null;
             OnClosed(EventArgs.Empty);

+ 13 - 0
src/Avalonia.Styling/IDataContextProvider.cs

@@ -0,0 +1,13 @@
+namespace Avalonia
+{
+    /// <summary>
+    /// Defines an element with a data context that can be used for binding.
+    /// </summary>
+    public interface IDataContextProvider : IAvaloniaObject
+    {
+        /// <summary>
+        /// Gets or sets the element's data context.
+        /// </summary>
+        object DataContext { get; set; }
+    }
+}

+ 2 - 6
src/Avalonia.Styling/IStyledElement.cs

@@ -10,7 +10,8 @@ namespace Avalonia
         IStyleHost,
         ILogical,
         IResourceProvider,
-        IResourceNode
+        IResourceNode,
+        IDataContextProvider
     {
         /// <summary>
         /// Occurs when the control has finished initialization.
@@ -27,11 +28,6 @@ namespace Avalonia
         /// </summary>
         new Classes Classes { get; set; }
 
-        /// <summary>
-        /// Gets or sets the control's data context.
-        /// </summary>
-        object DataContext { get; set; }
-
         /// <summary>
         /// Gets the control's logical parent.
         /// </summary>

+ 1 - 1
src/Avalonia.Styling/StyledElement.cs

@@ -24,7 +24,7 @@ namespace Avalonia
     /// - Implements <see cref="ILogical"/> to form part of a logical tree.
     /// - A collection of class strings for custom styling.
     /// </summary>
-    public class StyledElement : Animatable, IStyledElement, ISetLogicalParent, ISetInheritanceParent
+    public class StyledElement : Animatable, IDataContextProvider, IStyledElement, ISetLogicalParent, ISetInheritanceParent
     {
         /// <summary>
         /// Defines the <see cref="DataContext"/> property.

+ 8 - 19
src/Avalonia.Visuals/Media/FontFamily.cs

@@ -184,36 +184,25 @@ namespace Avalonia.Media
         {
             unchecked
             {
-                var hash = (int)2186146271;
-
-                if (Key != null)
-                {
-                    hash = (hash * 15768619) ^ Key.GetHashCode();
-                }
-                else
-                {
-                    hash = (hash * 15768619) ^ FamilyNames.GetHashCode();
-                }
-
-                if (Key != null)
-                {
-                    hash = (hash * 15768619) ^ Key.GetHashCode();
-                }
-
-                return hash;
+                return ((FamilyNames != null ? FamilyNames.GetHashCode() : 0) * 397) ^ (Key != null ? Key.GetHashCode() : 0);
             }
         }
 
         public override bool Equals(object obj)
         {
+            if (ReferenceEquals(this, obj))
+            {
+                return true;
+            }
+
             if (!(obj is FontFamily other))
             {
                 return false;
             }
 
-            if (Key != null)
+            if (!Equals(Key, other.Key))
             {
-                return other.FamilyNames.Equals(FamilyNames) && other.Key.Equals(Key);
+                return false;
             }
 
             return other.FamilyNames.Equals(FamilyNames);

+ 32 - 2
src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs

@@ -111,7 +111,24 @@ namespace Avalonia.Media.Fonts
         /// </returns>
         public override int GetHashCode()
         {
-            return ToString().GetHashCode();
+            if (Count == 0)
+            {
+                return 0;
+            }
+
+            unchecked
+            {
+                int hash = 17;
+
+                for (var i = 0; i < Names.Count; i++)
+                {
+                    string name = Names[i];
+
+                    hash = hash * 23 + name.GetHashCode();
+                }
+
+                return hash;
+            }
         }
 
         /// <summary>
@@ -128,7 +145,20 @@ namespace Avalonia.Media.Fonts
                 return false;
             }
 
-            return other.ToString().Equals(ToString());
+            if (other.Count != Count)
+            {
+                return false;
+            }
+
+            for (int i = 0; i < Count; i++)
+            {
+                if (Names[i] != other.Names[i])
+                {
+                    return false;
+                }
+            }
+
+            return true;
         }
 
         public int Count => Names.Count;

+ 7 - 0
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs

@@ -52,6 +52,13 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
             // the context.
             object anchor = context.GetFirstParent<IControl>();
 
+            if(anchor is null)
+            {
+                // Try to find IDataContextProvider, this was added to allow us to find
+                // a datacontext for Application class when using NativeMenuItems.
+                anchor = context.GetFirstParent<IDataContextProvider>();
+            }
+
             // If a control was not found, then try to find the highest-level style as the XAML
             // file could be a XAML file containing only styles.
             return anchor ??

+ 9 - 2
src/Markup/Avalonia.Markup/Data/Binding.cs

@@ -26,6 +26,7 @@ namespace Avalonia.Data
         public Binding()
         {
             FallbackValue = AvaloniaProperty.UnsetValue;
+            TargetNullValue = AvaloniaProperty.UnsetValue;
         }
 
         /// <summary>
@@ -60,6 +61,11 @@ namespace Avalonia.Data
         /// </summary>
         public object FallbackValue { get; set; }
 
+        /// <summary>
+        /// Gets or sets the value to use when the binding result is null.
+        /// </summary>
+        public object TargetNullValue { get; set; }
+
         /// <summary>
         /// Gets or sets the binding mode.
         /// </summary>
@@ -209,6 +215,7 @@ namespace Avalonia.Data
                 observer,
                 targetType,
                 fallback,
+                TargetNullValue,
                 converter ?? DefaultValueConverter.Instance,
                 ConverterParameter,
                 Priority);
@@ -224,9 +231,9 @@ namespace Avalonia.Data
         {
             Contract.Requires<ArgumentNullException>(target != null);
 
-            if (!(target is IStyledElement))
+            if (!(target is IDataContextProvider))
             {
-                target = anchor as IStyledElement;
+                target = anchor as IDataContextProvider;
 
                 if (target == null)
                 {

+ 16 - 0
src/Markup/Avalonia.Markup/Data/MultiBinding.cs

@@ -37,6 +37,11 @@ namespace Avalonia.Data
         /// </summary>
         public object FallbackValue { get; set; }
 
+        /// <summary>
+        /// Gets or sets the value to use when the binding result is null.
+        /// </summary>
+        public object TargetNullValue { get; set; }
+
         /// <summary>
         /// Gets or sets the binding mode.
         /// </summary>
@@ -57,6 +62,12 @@ namespace Avalonia.Data
         /// </summary>
         public string StringFormat { get; set; }
 
+        public MultiBinding()
+        {
+            FallbackValue = AvaloniaProperty.UnsetValue;
+            TargetNullValue = AvaloniaProperty.UnsetValue;
+        }
+
         /// <inheritdoc/>
         public InstancedBinding Initiate(
             IAvaloniaObject target,
@@ -102,6 +113,11 @@ namespace Avalonia.Data
             var culture = CultureInfo.CurrentCulture;
             var converted = converter.Convert(values, targetType, ConverterParameter, culture);
 
+            if (converted == null)
+            {
+                converted = TargetNullValue;
+            }
+
             if (converted == AvaloniaProperty.UnsetValue)
             {
                 converted = FallbackValue;

+ 28 - 0
tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs

@@ -139,6 +139,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
                 ExpressionObserver.Create(data, o => o.StringValue),
                 typeof(int),
                 42,
+                AvaloniaProperty.UnsetValue,
                 DefaultValueConverter.Instance);
             var result = await target.Take(1);
 
@@ -160,6 +161,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
                 ExpressionObserver.Create(data, o => o.StringValue, true),
                 typeof(int),
                 42,
+                AvaloniaProperty.UnsetValue,
                 DefaultValueConverter.Instance);
             var result = await target.Take(1);
 
@@ -181,6 +183,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
                 ExpressionObserver.Create(data, o => o.StringValue),
                 typeof(int),
                 "bar",
+                AvaloniaProperty.UnsetValue,
                 DefaultValueConverter.Instance);
             var result = await target.Take(1);
 
@@ -203,6 +206,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
                 ExpressionObserver.Create(data, o => o.StringValue, true),
                 typeof(int),
                 "bar",
+                AvaloniaProperty.UnsetValue,
                 DefaultValueConverter.Instance);
             var result = await target.Take(1);
 
@@ -238,6 +242,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
                 ExpressionObserver.Create(data, o => o.DoubleValue),
                 typeof(string),
                 "9.8",
+                AvaloniaProperty.UnsetValue,
                 DefaultValueConverter.Instance);
 
             target.OnNext("foo");
@@ -353,6 +358,29 @@ namespace Avalonia.Base.UnitTests.Data.Core
             GC.KeepAlive(data);
         }
 
+        [Fact]
+        public async Task Null_Value_Should_Use_TargetNullValue()
+        {
+            var data = new Class1 { StringValue = "foo" };
+
+            var target = new BindingExpression(
+                ExpressionObserver.Create(data, o => o.StringValue),
+                typeof(string),
+                AvaloniaProperty.UnsetValue,
+                "bar",
+                DefaultValueConverter.Instance);
+
+            object result = null;
+            target.Subscribe(x => result = x);
+
+            Assert.Equal("foo", result);
+            
+            data.StringValue = null;
+            Assert.Equal("bar", result);
+
+            GC.KeepAlive(data);
+        }
+
         private class Class1 : NotifyingBase
         {
             private string _stringValue;

+ 59 - 0
tests/Avalonia.Benchmarks/Data/BindingsBenchmark.cs

@@ -0,0 +1,59 @@
+using Avalonia.Data;
+using BenchmarkDotNet.Attributes;
+
+namespace Avalonia.Benchmarks.Data
+{
+    [MemoryDiagnoser, InProcess]
+    public class BindingsBenchmark
+    {
+        [Benchmark]
+        public void TwoWayBinding_Via_Binding()
+        {
+            var instance = new TestClass();
+
+            var binding = new Binding(nameof(TestClass.BoundValue), BindingMode.TwoWay)
+            {
+                Source = instance
+            };
+
+            instance.Bind(TestClass.IntValueProperty, binding);
+        }
+
+        [Benchmark]
+        public void UpdateTwoWayBinding_Via_Binding()
+        {
+            var instance = new TestClass();
+
+            var binding = new Binding(nameof(TestClass.BoundValue), BindingMode.TwoWay)
+            {
+                Source = instance
+            };
+
+            instance.Bind(TestClass.IntValueProperty, binding);
+            for (int i = 0; i < 60; i++)
+            {
+                instance.IntValue = i;
+            }
+        }
+        private class TestClass : AvaloniaObject
+        {
+            public static readonly StyledProperty<int> IntValueProperty =
+                AvaloniaProperty.Register<TestClass, int>(nameof(IntValue));
+
+            public static readonly StyledProperty<int> BoundValueProperty =
+                AvaloniaProperty.Register<TestClass, int>(nameof(BoundValue));
+
+            public int IntValue
+            {
+                get => GetValue(IntValueProperty);
+                set => SetValue(IntValueProperty, value);
+            }
+
+            public int BoundValue
+            {
+                get => GetValue(BoundValueProperty);
+                set => SetValue(BoundValueProperty, value);
+            }
+        }
+    }
+}

+ 0 - 16
tests/Avalonia.Controls.UnitTests/Primitives/RangeBaseTests.cs

@@ -82,22 +82,6 @@ namespace Avalonia.Controls.UnitTests.Primitives
             Assert.Equal(50, target.Value);
         }
 
-        [Fact]
-        public void Properties_Should_Not_Accept_Nan_And_Inifinity()
-        {
-            var target = new TestRange();
-
-            Assert.Throws<ArgumentException>(() => target.Minimum = double.NaN);
-            Assert.Throws<ArgumentException>(() => target.Minimum = double.PositiveInfinity);
-            Assert.Throws<ArgumentException>(() => target.Minimum = double.NegativeInfinity);
-            Assert.Throws<ArgumentException>(() => target.Maximum = double.NaN);
-            Assert.Throws<ArgumentException>(() => target.Maximum = double.PositiveInfinity);
-            Assert.Throws<ArgumentException>(() => target.Maximum = double.NegativeInfinity);
-            Assert.Throws<ArgumentException>(() => target.Value = double.NaN);
-            Assert.Throws<ArgumentException>(() => target.Value = double.PositiveInfinity);
-            Assert.Throws<ArgumentException>(() => target.Value = double.NegativeInfinity);
-        }
-
         [Theory]
         [InlineData(true)]
         [InlineData(false)]

+ 18 - 0
tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs

@@ -405,6 +405,24 @@ namespace Avalonia.Markup.UnitTests.Data
             Assert.Equal(42, target.Value);
         }
 
+        [Fact]
+        public void Should_Return_TargetNullValue_When_Value_Is_Null()
+        {
+            var target = new TextBlock();
+            var source = new Source { Foo = null };
+
+            var binding = new Binding
+            {
+                Source = source,
+                Path = "Foo",
+                TargetNullValue = "(null)",
+            };
+
+            target.Bind(TextBlock.TextProperty, binding);
+
+            Assert.Equal("(null)", target.Text);
+        }
+
         [Fact]
         public void Null_Path_Should_Bind_To_DataContext()
         {

+ 30 - 0
tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests.cs

@@ -94,6 +94,28 @@ namespace Avalonia.Markup.UnitTests.Data
             Assert.Equal("fallback", target.Text);
         }
 
+        [Fact]
+        public void Should_Return_TargetNullValue_When_Value_Is_Null()
+        {
+            var target = new TextBlock();
+
+            var binding = new MultiBinding
+            {
+                Converter = new NullValueConverter(),
+                Bindings = new[]
+                {
+                    new Binding { Path = "A" },
+                    new Binding { Path = "B" },
+                    new Binding { Path = "C" },
+                },
+                TargetNullValue = "(null)",
+            };
+
+            target.Bind(TextBlock.TextProperty, binding);
+
+            Assert.Equal("(null)", target.Text);
+        }
+
         private class ConcatConverter : IMultiValueConverter
         {
             public object Convert(IList<object> values, Type targetType, object parameter, CultureInfo culture)
@@ -109,5 +131,13 @@ namespace Avalonia.Markup.UnitTests.Data
                 return AvaloniaProperty.UnsetValue;
             }
         }
+
+        private class NullValueConverter : IMultiValueConverter
+        {
+            public object Convert(IList<object> values, Type targetType, object parameter, CultureInfo culture)
+            {
+                return null;
+            }
+        }
     }
 }

+ 4 - 81
tests/Avalonia.Styling.UnitTests/SelectorTests_Child.cs

@@ -81,89 +81,12 @@ namespace Avalonia.Styling.UnitTests
             Assert.Equal("TestLogical1 > TestLogical3", selector.ToString());
         }
 
-        public abstract class TestLogical : ILogical, IStyleable
+        public abstract class TestLogical : Control
         {
-            public TestLogical()
+            public ILogical LogicalParent
             {
-                Classes = new Classes();
-            }
-
-            public event EventHandler<AvaloniaPropertyChangedEventArgs> PropertyChanged;
-            public event EventHandler<AvaloniaPropertyChangedEventArgs> InheritablePropertyChanged;
-            public event EventHandler<LogicalTreeAttachmentEventArgs> AttachedToLogicalTree;
-            public event EventHandler<LogicalTreeAttachmentEventArgs> DetachedFromLogicalTree;
-
-            public Classes Classes { get; }
-
-            public string Name { get; set; }
-
-            public bool IsAttachedToLogicalTree { get; }
-
-            public IAvaloniaReadOnlyList<ILogical> LogicalChildren { get; set; }
-
-            public ILogical LogicalParent { get; set; }
-
-            public Type StyleKey { get; }
-
-            public ITemplatedControl TemplatedParent { get; }
-
-            IObservable<IStyleable> IStyleable.StyleDetach { get; }
-
-            IAvaloniaReadOnlyList<string> IStyleable.Classes => Classes;
-
-            public object GetValue(AvaloniaProperty property)
-            {
-                throw new NotImplementedException();
-            }
-
-            public T GetValue<T>(AvaloniaProperty<T> property)
-            {
-                throw new NotImplementedException();
-            }
-
-            public void SetValue(AvaloniaProperty property, object value, BindingPriority priority)
-            {
-                throw new NotImplementedException();
-            }
-
-            public void SetValue<T>(AvaloniaProperty<T> property, T value, BindingPriority priority = BindingPriority.LocalValue)
-            {
-                throw new NotImplementedException();
-            }
-
-            public IDisposable Bind(AvaloniaProperty property, IObservable<object> source, BindingPriority priority)
-            {
-                throw new NotImplementedException();
-            }
-
-            public bool IsAnimating(AvaloniaProperty property)
-            {
-                throw new NotImplementedException();
-            }
-
-            public bool IsSet(AvaloniaProperty property)
-            {
-                throw new NotImplementedException();
-            }
-
-            public IDisposable Bind<T>(AvaloniaProperty<T> property, IObservable<T> source, BindingPriority priority = BindingPriority.LocalValue)
-            {
-                throw new NotImplementedException();
-            }
-
-            public void NotifyAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
-            {
-                throw new NotImplementedException();
-            }
-
-            public void NotifyDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
-            {
-                throw new NotImplementedException();
-            }
-
-            public void NotifyResourcesChanged(ResourcesChangedEventArgs e)
-            {
-                throw new NotImplementedException();
+                get => Parent;
+                set => ((ISetLogicalParent)this).SetParent(value);
             }
         }
 

+ 1 - 1
tests/Avalonia.Styling.UnitTests/SelectorTests_Class.cs

@@ -144,7 +144,7 @@ namespace Avalonia.Styling.UnitTests
             Assert.Equal(new[] { true, false }, result);
         }
 
-        public class Control1 : TestControlBase
+        public class Control1 : Control
         {
         }
     }

+ 4 - 81
tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs

@@ -111,89 +111,12 @@ namespace Avalonia.Styling.UnitTests
             Assert.Equal("TestLogical1.foo TestLogical3", selector.ToString());
         }
 
-        public abstract class TestLogical : ILogical, IStyleable
+        public abstract class TestLogical : Control
         {
-            public TestLogical()
+            public ILogical LogicalParent
             {
-                Classes = new Classes();
-            }
-
-            public event EventHandler<AvaloniaPropertyChangedEventArgs> PropertyChanged;
-            public event EventHandler<AvaloniaPropertyChangedEventArgs> InheritablePropertyChanged;
-            public event EventHandler<LogicalTreeAttachmentEventArgs> AttachedToLogicalTree;
-            public event EventHandler<LogicalTreeAttachmentEventArgs> DetachedFromLogicalTree;
-
-            public Classes Classes { get; }
-
-            public string Name { get; set; }
-
-            public bool IsAttachedToLogicalTree { get; }
-
-            public IAvaloniaReadOnlyList<ILogical> LogicalChildren { get; set; }
-
-            public ILogical LogicalParent { get; set; }
-
-            public Type StyleKey { get; }
-
-            public ITemplatedControl TemplatedParent { get; }
-
-            IAvaloniaReadOnlyList<string> IStyleable.Classes => Classes;
-
-            IObservable<IStyleable> IStyleable.StyleDetach { get; }
-
-            public object GetValue(AvaloniaProperty property)
-            {
-                throw new NotImplementedException();
-            }
-
-            public T GetValue<T>(AvaloniaProperty<T> property)
-            {
-                throw new NotImplementedException();
-            }
-
-            public void SetValue(AvaloniaProperty property, object value, BindingPriority priority)
-            {
-                throw new NotImplementedException();
-            }
-
-            public void SetValue<T>(AvaloniaProperty<T> property, T value, BindingPriority priority = BindingPriority.LocalValue)
-            {
-                throw new NotImplementedException();
-            }
-
-            public IDisposable Bind(AvaloniaProperty property, IObservable<object> source, BindingPriority priority = BindingPriority.LocalValue)
-            {
-                throw new NotImplementedException();
-            }
-
-            public bool IsAnimating(AvaloniaProperty property)
-            {
-                throw new NotImplementedException();
-            }
-
-            public bool IsSet(AvaloniaProperty property)
-            {
-                throw new NotImplementedException();
-            }
-
-            public IDisposable Bind<T>(AvaloniaProperty<T> property, IObservable<T> source, BindingPriority priority = BindingPriority.LocalValue)
-            {
-                throw new NotImplementedException();
-            }
-
-            public void NotifyAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
-            {
-                throw new NotImplementedException();
-            }
-
-            public void NotifyDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
-            {
-                throw new NotImplementedException();
-            }
-
-            public void NotifyResourcesChanged(ResourcesChangedEventArgs e)
-            {
-                throw new NotImplementedException();
+                get => Parent;
+                set => ((ISetLogicalParent)this).SetParent(value);
             }
         }
 

+ 2 - 1
tests/Avalonia.Styling.UnitTests/SelectorTests_Name.cs

@@ -1,6 +1,7 @@
 // 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 Avalonia.Controls;
 using Moq;
 using Xunit;
 
@@ -52,7 +53,7 @@ namespace Avalonia.Styling.UnitTests
             Assert.Equal("Control1#foo", target.ToString());
         }
 
-        public class Control1 : TestControlBase
+        public class Control1 : Control
         {
         }
     }

+ 2 - 2
tests/Avalonia.Styling.UnitTests/SelectorTests_Not.cs

@@ -103,11 +103,11 @@ namespace Avalonia.Styling.UnitTests
             Assert.Equal(typeof(Control1), target.TargetType);
         }
 
-        public class Control1 : TestControlBase
+        public class Control1 : Control
         {
         }
 
-        public class Control2 : TestControlBase
+        public class Control2 : Control
         {
         }
     }

+ 3 - 2
tests/Avalonia.Styling.UnitTests/SelectorTests_OfType.cs

@@ -1,6 +1,7 @@
 // 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 Avalonia.Controls;
 using Moq;
 using Xunit;
 
@@ -44,11 +45,11 @@ namespace Avalonia.Styling.UnitTests
             Assert.Equal(SelectorMatchResult.AlwaysThisType, target.Match(control).Result);
         }
 
-        public class Control1 : TestControlBase
+        public class Control1 : Control
         {
         }
 
-        public class Control2 : TestControlBase
+        public class Control2 : Control
         {
         }
     }

+ 5 - 4
tests/Avalonia.Styling.UnitTests/SelectorTests_Or.cs

@@ -1,6 +1,7 @@
 // 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 Avalonia.Controls;
 using Xunit;
 
 namespace Avalonia.Styling.UnitTests
@@ -78,7 +79,7 @@ namespace Avalonia.Styling.UnitTests
                 default(Selector).OfType<Control1>().Class("foo"),
                 default(Selector).OfType<Control2>().Class("bar"));
 
-            Assert.Equal(typeof(TestControlBase), target.TargetType);
+            Assert.Equal(typeof(Control), target.TargetType);
         }
 
         [Fact]
@@ -91,15 +92,15 @@ namespace Avalonia.Styling.UnitTests
             Assert.Equal(null, target.TargetType);
         }
 
-        public class Control1 : TestControlBase
+        public class Control1 : Control
         {
         }
 
-        public class Control2 : TestControlBase
+        public class Control2 : Control
         {
         }
 
-        public class Control3 : TestControlBase
+        public class Control3 : Control
         {
         }
     }

+ 0 - 83
tests/Avalonia.Styling.UnitTests/TestControlBase.cs

@@ -1,83 +0,0 @@
-// 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 System.Reactive;
-using Avalonia.Collections;
-using Avalonia.Controls;
-using Avalonia.Data;
-
-namespace Avalonia.Styling.UnitTests
-{
-    public class TestControlBase : IStyleable
-    {
-        public TestControlBase()
-        {
-            Classes = new Classes();
-            SubscribeCheckObservable = new TestObservable();
-        }
-
-#pragma warning disable CS0067 // Event not used
-        public event EventHandler<AvaloniaPropertyChangedEventArgs> PropertyChanged;
-        public event EventHandler<AvaloniaPropertyChangedEventArgs> InheritablePropertyChanged;
-#pragma warning restore CS0067
-
-        public string Name { get; set; }
-
-        public virtual Classes Classes { get; set; }
-
-        public Type StyleKey => GetType();
-
-        public TestObservable SubscribeCheckObservable { get; private set; }
-
-        public ITemplatedControl TemplatedParent
-        {
-            get;
-            set;
-        }
-
-        IAvaloniaReadOnlyList<string> IStyleable.Classes => Classes;
-
-        IObservable<IStyleable> IStyleable.StyleDetach { get; }
-
-        public object GetValue(AvaloniaProperty property)
-        {
-            throw new NotImplementedException();
-        }
-
-        public T GetValue<T>(AvaloniaProperty<T> property)
-        {
-            throw new NotImplementedException();
-        }
-
-        public void SetValue(AvaloniaProperty property, object value, BindingPriority priority)
-        {
-            throw new NotImplementedException();
-        }
-
-        public void SetValue<T>(AvaloniaProperty<T> property, T value, BindingPriority priority = BindingPriority.LocalValue)
-        {
-            throw new NotImplementedException();
-        }
-
-        public bool IsAnimating(AvaloniaProperty property)
-        {
-            throw new NotImplementedException();
-        }
-
-        public bool IsSet(AvaloniaProperty property)
-        {
-            throw new NotImplementedException();
-        }
-
-        public IDisposable Bind(AvaloniaProperty property, IObservable<object> source, BindingPriority priority = BindingPriority.LocalValue)
-        {
-            throw new NotImplementedException();
-        }
-
-        public IDisposable Bind<T>(AvaloniaProperty<T> property, IObservable<T> source, BindingPriority priority = BindingPriority.LocalValue)
-        {
-            throw new NotImplementedException();
-        }
-    }
-}

+ 0 - 81
tests/Avalonia.Styling.UnitTests/TestTemplatedControl.cs

@@ -1,81 +0,0 @@
-// 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 System.Reactive;
-using Avalonia.Collections;
-using Avalonia.Controls;
-using Avalonia.Data;
-
-namespace Avalonia.Styling.UnitTests
-{
-    public abstract class TestTemplatedControl : ITemplatedControl, IStyleable
-    {
-        public event EventHandler<AvaloniaPropertyChangedEventArgs> PropertyChanged;
-        public event EventHandler<AvaloniaPropertyChangedEventArgs> InheritablePropertyChanged;
-
-        public abstract Classes Classes
-        {
-            get;
-        }
-
-        public abstract string Name
-        {
-            get;
-        }
-
-        public abstract Type StyleKey
-        {
-            get;
-        }
-
-        public abstract ITemplatedControl TemplatedParent
-        {
-            get;
-        }
-
-        IAvaloniaReadOnlyList<string> IStyleable.Classes => Classes;
-
-        IObservable<IStyleable> IStyleable.StyleDetach { get; }
-
-        public object GetValue(AvaloniaProperty property)
-        {
-            throw new NotImplementedException();
-        }
-
-        public T GetValue<T>(AvaloniaProperty<T> property)
-        {
-            throw new NotImplementedException();
-        }
-
-        public void SetValue(AvaloniaProperty property, object value, BindingPriority priority)
-        {
-            throw new NotImplementedException();
-        }
-
-        public void SetValue<T>(AvaloniaProperty<T> property, T value, BindingPriority priority = BindingPriority.LocalValue)
-        {
-            throw new NotImplementedException();
-        }
-
-        public IDisposable Bind(AvaloniaProperty property, IObservable<object> source, BindingPriority priority = BindingPriority.LocalValue)
-        {
-            throw new NotImplementedException();
-        }
-
-        public IDisposable Bind<T>(AvaloniaProperty<T> property, IObservable<T> source, BindingPriority priority = BindingPriority.LocalValue)
-        {
-            throw new NotImplementedException();
-        }
-
-        public bool IsAnimating(AvaloniaProperty property)
-        {
-            throw new NotImplementedException();
-        }
-
-        public bool IsSet(AvaloniaProperty property)
-        {
-            throw new NotImplementedException();
-        }
-    }
-}