Browse Source

Merge pull request #856 from donandren/issues/855_test

Unit tests and fixes for binding StackOverflowExceptions
Jeremy Koritzinsky 8 years ago
parent
commit
33f6dde459

+ 80 - 11
src/Avalonia.Base/AvaloniaObject.cs

@@ -51,6 +51,21 @@ namespace Avalonia
         /// </summary>
         private EventHandler<AvaloniaPropertyChangedEventArgs> _propertyChanged;
 
+        private DeferredSetter<AvaloniaProperty, object> _directDeferredSetter;
+
+        /// <summary>
+        /// Delayed setter helper for direct properties. Used to fix #855.
+        /// </summary>
+        private DeferredSetter<AvaloniaProperty, object> DirectPropertyDeferredSetter
+        {
+            get
+            {
+                return _directDeferredSetter ??
+                    (_directDeferredSetter = new DeferredSetter<AvaloniaProperty, object>());
+            }
+        }
+
+
         /// <summary>
         /// Initializes a new instance of the <see cref="AvaloniaObject"/> class.
         /// </summary>
@@ -552,6 +567,45 @@ namespace Avalonia
             }
         }
 
+        /// <summary>
+        /// A callback type for encapsulating complex logic for setting direct properties.
+        /// </summary>
+        /// <typeparam name="T">The type of the property.</typeparam>
+        /// <param name="value">The value to which to set the property.</param>
+        /// <param name="field">The backing field for the property.</param>
+        /// <param name="notifyWrapper">A wrapper for the property-changed notification.</param>
+        protected delegate void SetAndRaiseCallback<T>(T value, ref T field, Action<Action> notifyWrapper);
+
+        /// <summary>
+        /// Sets the backing field for a direct avalonia property, raising the 
+        /// <see cref="PropertyChanged"/> event if the value has changed.
+        /// </summary>
+        /// <typeparam name="T">The type of the property.</typeparam>
+        /// <param name="property">The property.</param>
+        /// <param name="field">The backing field.</param>
+        /// <param name="setterCallback">A callback called to actually set the value to the backing field.</param>
+        /// <param name="value">The value.</param>
+        /// <returns>
+        /// True if the value changed, otherwise false.
+        /// </returns>
+        protected bool SetAndRaise<T>(
+            AvaloniaProperty<T> property,
+            ref T field,
+            SetAndRaiseCallback<T> setterCallback,
+            T value)
+        {
+            Contract.Requires<ArgumentNullException>(setterCallback != null);
+            return DirectPropertyDeferredSetter.SetAndNotify(
+                property,
+                ref field,
+                (object val, ref T backing, Action<Action> notify) =>
+                {
+                    setterCallback((T)val, ref backing, notify);
+                    return true;
+                },
+                value);
+        }
+
         /// <summary>
         /// Sets the backing field for a direct avalonia property, raising the 
         /// <see cref="PropertyChanged"/> event if the value has changed.
@@ -566,17 +620,32 @@ namespace Avalonia
         protected bool SetAndRaise<T>(AvaloniaProperty<T> property, ref T field, T value)
         {
             VerifyAccess();
-            if (!object.Equals(field, value))
-            {
-                var old = field;
-                field = value;
-                RaisePropertyChanged(property, old, value, BindingPriority.LocalValue);
-                return true;
-            }
-            else
-            {
-                return false;
-            }
+            return SetAndRaise(
+                property,
+                ref field,
+                (T val, ref T backing, Action<Action> notifyWrapper)
+                    => SetAndRaiseCore(property, ref backing, val, notifyWrapper),
+                value);
+        }
+
+        /// <summary>
+        /// Default assignment logic for SetAndRaise.
+        /// </summary>
+        /// <typeparam name="T">The type of the property.</typeparam>
+        /// <param name="property">The property.</param>
+        /// <param name="field">The backing field.</param>
+        /// <param name="value">The value.</param>
+        /// <param name="notifyWrapper">A wrapper for the property-changed notification.</param>
+        /// <returns>
+        /// True if the value changed, otherwise false.
+        /// </returns>
+        private bool SetAndRaiseCore<T>(AvaloniaProperty property, ref T field, T value, Action<Action> notifyWrapper)
+        {
+            var old = field;
+            field = value;
+
+            notifyWrapper(() => RaisePropertyChanged(property, old, value, BindingPriority.LocalValue));
+            return true;
         }
 
         /// <summary>

+ 4 - 20
src/Avalonia.Base/AvaloniaObjectExtensions.cs

@@ -138,17 +138,9 @@ namespace Avalonia
             AvaloniaProperty property,
             BindingPriority priority = BindingPriority.LocalValue)
         {
-            // TODO: Subject.Create<T> is not yet in stable Rx : once it is, remove the 
-            // AnonymousSubject classes and use Subject.Create<T>.
-            var output = new Subject<object>();
-            var result = new AnonymousSubject<object>(
-                Observer.Create<object>(
-                    x => output.OnNext(x),
-                    e => output.OnError(e),
-                    () => output.OnCompleted()),
+            return Subject.Create<object>(
+                Observer.Create<object>(x => o.SetValue(property, x, priority)),
                 o.GetObservable(property));
-            o.Bind(property, output, priority);
-            return result;
         }
 
         /// <summary>
@@ -169,17 +161,9 @@ namespace Avalonia
             AvaloniaProperty<T> property,
             BindingPriority priority = BindingPriority.LocalValue)
         {
-            // TODO: Subject.Create<T> is not yet in stable Rx : once it is, remove the 
-            // AnonymousSubject classes from this file and use Subject.Create<T>.
-            var output = new Subject<T>();
-            var result = new AnonymousSubject<T>(
-                Observer.Create<T>(
-                    x => output.OnNext(x),
-                    e => output.OnError(e),
-                    () => output.OnCompleted()),
+            return Subject.Create<T>(
+                Observer.Create<T>(x => o.SetValue(property, x, priority)),
                 o.GetObservable(property));
-            o.Bind(property, output, priority);
-            return result;
         }
 
         /// <summary>

+ 30 - 21
src/Avalonia.Base/PriorityValue.cs

@@ -28,8 +28,10 @@ namespace Avalonia
     {
         private readonly Type _valueType;
         private readonly SingleOrDictionary<int, PriorityLevel> _levels = new SingleOrDictionary<int, PriorityLevel>();
-        private object _value;
+
         private readonly Func<object, object> _validate;
+        private static readonly DeferredSetter<PriorityValue, (object value, int priority)> delayedSetter = new DeferredSetter<PriorityValue, (object, int)>();
+        private (object value, int priority) _value;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="PriorityValue"/> class.
@@ -47,8 +49,7 @@ namespace Avalonia
             Owner = owner;
             Property = property;
             _valueType = valueType;
-            _value = AvaloniaProperty.UnsetValue;
-            ValuePriority = int.MaxValue;
+            _value = (AvaloniaProperty.UnsetValue, int.MaxValue);
             _validate = validate;
         }
 
@@ -77,16 +78,12 @@ namespace Avalonia
         /// <summary>
         /// Gets the current value.
         /// </summary>
-        public object Value => _value;
+        public object Value => _value.value;
 
         /// <summary>
         /// Gets the priority of the binding that is currently active.
         /// </summary>
-        public int ValuePriority
-        {
-            get;
-            private set;
-        }
+        public int ValuePriority => _value.priority;
 
         /// <summary>
         /// Adds a new binding.
@@ -246,25 +243,36 @@ namespace Avalonia
         /// <param name="priority">The priority level that the value came from.</param>
         private void UpdateValue(object value, int priority)
         {
-            var notification = value as BindingNotification;
+            delayedSetter.SetAndNotify(this,
+                ref _value,
+                UpdateCore,
+                (value, priority));
+        }
+
+        private bool UpdateCore(
+            (object value, int priority) update,
+            ref (object value, int priority) backing,
+            Action<Action> notify)
+        {
+            var val = update.value;
+            var notification = val as BindingNotification;
             object castValue;
 
             if (notification != null)
             {
-                value = (notification.HasValue) ? notification.Value : null;
+                val = (notification.HasValue) ? notification.Value : null;
             }
 
-            if (TypeUtilities.TryConvertImplicit(_valueType, value, out castValue))
+            if (TypeUtilities.TryConvertImplicit(_valueType, val, out castValue))
             {
-                var old = _value;
+                var old = backing.value;
 
                 if (_validate != null && castValue != AvaloniaProperty.UnsetValue)
                 {
                     castValue = _validate(castValue);
                 }
 
-                ValuePriority = priority;
-                _value = castValue;
+                backing = (castValue, update.priority);
 
                 if (notification?.HasValue == true)
                 {
@@ -273,7 +281,7 @@ namespace Avalonia
 
                 if (notification == null || notification.HasValue)
                 {
-                    Owner?.Changed(this, old, _value);
+                    notify(() => Owner?.Changed(this, old, Value));
                 }
 
                 if (notification != null)
@@ -284,14 +292,15 @@ namespace Avalonia
             else
             {
                 Logger.Error(
-                    LogArea.Binding, 
+                    LogArea.Binding,
                     Owner,
                     "Binding produced invalid value for {$Property} ({$PropertyType}): {$Value} ({$ValueType})",
-                    Property.Name, 
-                    _valueType, 
-                    value,
-                    value?.GetType());
+                    Property.Name,
+                    _valueType,
+                    val,
+                    val?.GetType());
             }
+            return true;
         }
     }
 }

+ 0 - 16
src/Avalonia.Base/Reactive/AnonymousSubject`1.cs

@@ -1,16 +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.Subjects;
-
-namespace Avalonia.Reactive
-{
-    public class AnonymousSubject<T> : AnonymousSubject<T, T>, ISubject<T>
-    {
-        public AnonymousSubject(IObserver<T> observer, IObservable<T> observable)
-            : base(observer, observable)
-        {
-        }
-    }
-}

+ 0 - 49
src/Avalonia.Base/Reactive/AnonymousSubject`2.cs

@@ -1,49 +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.Subjects;
-
-namespace Avalonia.Reactive
-{
-    public class AnonymousSubject<T, U> : ISubject<T, U>
-    {
-        private readonly IObserver<T> _observer;
-        private readonly IObservable<U> _observable;
-
-        public AnonymousSubject(IObserver<T> observer, IObservable<U> observable)
-        {
-            _observer = observer;
-            _observable = observable;
-        }
-
-        public void OnCompleted()
-        {
-            _observer.OnCompleted();
-        }
-
-        public void OnError(Exception error)
-        {
-            if (error == null)
-                throw new ArgumentNullException("error");
-
-            _observer.OnError(error);
-        }
-
-        public void OnNext(T value)
-        {
-            _observer.OnNext(value);
-        }
-
-        public IDisposable Subscribe(IObserver<U> observer)
-        {
-            if (observer == null)
-                throw new ArgumentNullException("observer");
-
-            //
-            // [OK] Use of unsafe Subscribe: non-pretentious wrapping of an observable sequence.
-            //
-            return _observable.Subscribe/*Unsafe*/(observer);
-        }
-    }
-}

+ 156 - 0
src/Avalonia.Base/Utilities/DeferredSetter.cs

@@ -0,0 +1,156 @@
+using System;
+using System.Collections.Generic;
+using System.Reactive.Disposables;
+using System.Runtime.CompilerServices;
+using System.Text;
+
+namespace Avalonia.Utilities
+{
+    /// <summary>
+    /// A utility class to enable deferring assignment until after property-changed notifications are sent.
+    /// </summary>
+    /// <typeparam name="TProperty">The type of the object that represents the property.</typeparam>
+    /// <typeparam name="TSetRecord">The type of value with which to track the delayed assignment.</typeparam>
+    class DeferredSetter<TProperty, TSetRecord>
+        where TProperty: class
+    {
+        private struct NotifyDisposable : IDisposable
+        {
+            private readonly SettingStatus status;
+
+            internal NotifyDisposable(SettingStatus status)
+            {
+                this.status = status;
+                status.Notifying = true;
+            }
+
+            public void Dispose()
+            {
+                status.Notifying = false;
+            }
+        }
+
+        /// <summary>
+        /// Information on current setting/notification status of a property.
+        /// </summary>
+        private class SettingStatus
+        {
+            public bool Notifying { get; set; }
+
+            private Queue<TSetRecord> pendingValues;
+            
+            public Queue<TSetRecord> PendingValues
+            {
+                get
+                {
+                    return pendingValues ?? (pendingValues = new Queue<TSetRecord>());
+                }
+            }
+        }
+
+        private readonly ConditionalWeakTable<TProperty, SettingStatus> setRecords = new ConditionalWeakTable<TProperty, SettingStatus>();
+
+        /// <summary>
+        /// Mark the property as currently notifying.
+        /// </summary>
+        /// <param name="property">The property to mark as notifying.</param>
+        /// <returns>Returns a disposable that when disposed, marks the property as done notifying.</returns>
+        private NotifyDisposable MarkNotifying(TProperty property)
+        {
+            Contract.Requires<InvalidOperationException>(!IsNotifying(property));
+            
+            return new NotifyDisposable(setRecords.GetOrCreateValue(property));
+        }
+
+        /// <summary>
+        /// Check if the property is currently notifying listeners.
+        /// </summary>
+        /// <param name="property">The property.</param>
+        /// <returns>If the property is currently notifying listeners.</returns>
+        private bool IsNotifying(TProperty property)
+            => setRecords.TryGetValue(property, out var value) && value.Notifying;
+
+        /// <summary>
+        /// Add a pending assignment for the property.
+        /// </summary>
+        /// <param name="property">The property.</param>
+        /// <param name="value">The value to assign.</param>
+        private void AddPendingSet(TProperty property, TSetRecord value)
+        {
+            Contract.Requires<InvalidOperationException>(IsNotifying(property));
+
+            setRecords.GetOrCreateValue(property).PendingValues.Enqueue(value);
+        }
+
+        /// <summary>
+        /// Checks if there are any pending assignments for the property.
+        /// </summary>
+        /// <param name="property">The property to check.</param>
+        /// <returns>If the property has any pending assignments.</returns>
+        private bool HasPendingSet(TProperty property)
+        {
+            return setRecords.TryGetValue(property, out var status) && status.PendingValues.Count != 0;
+        }
+
+        /// <summary>
+        /// Gets the first pending assignment for the property.
+        /// </summary>
+        /// <param name="property">The property to check.</param>
+        /// <returns>The first pending assignment for the property.</returns>
+        private TSetRecord GetFirstPendingSet(TProperty property)
+        {
+            return setRecords.GetOrCreateValue(property).PendingValues.Dequeue();
+        }
+
+        public delegate bool SetterDelegate<TValue>(TSetRecord record, ref TValue backing, Action<Action> notifyCallback);
+
+        /// <summary>
+        /// Set the property and notify listeners while ensuring we don't get into a stack overflow as happens with #855 and #824
+        /// </summary>
+        /// <param name="property">The property to set.</param>
+        /// <param name="backing">The backing field for the property</param>
+        /// <param name="setterCallback">
+        /// A callback that actually sets the property.
+        /// The first parameter is the value to set, and the second is a wrapper that takes a callback that sends the property-changed notification.
+        /// </param>
+        /// <param name="value">The value to try to set.</param>
+        public bool SetAndNotify<TValue>(
+            TProperty property,
+            ref TValue backing,
+            SetterDelegate<TValue> setterCallback,
+            TSetRecord value)
+        {
+            Contract.Requires<ArgumentNullException>(setterCallback != null);
+            if (!IsNotifying(property))
+            {
+                bool updated = false;
+                if (!object.Equals(value, backing))
+                {
+                    updated = setterCallback(value, ref backing, notification =>
+                    {
+                        using (MarkNotifying(property))
+                        {
+                            notification();
+                        }
+                    });
+                }
+                while (HasPendingSet(property))
+                {
+                    updated |= setterCallback(GetFirstPendingSet(property), ref backing, notification =>
+                    {
+                        using (MarkNotifying(property))
+                        {
+                            notification();
+                        }
+                    });
+                }
+                return updated;
+            }
+            else if(!object.Equals(value, backing))
+            {
+                AddPendingSet(property, value);
+            }
+            return false;
+        }
+    }
+}

+ 44 - 26
src/Avalonia.Controls/Primitives/SelectingItemsControl.cs

@@ -151,15 +151,23 @@ namespace Avalonia.Controls.Primitives
             {
                 if (_updateCount == 0)
                 {
-                    var old = SelectedIndex;
-                    var effective = (value >= 0 && value < Items?.Cast<object>().Count()) ? value : -1;
-
-                    if (old != effective)
+                    SetAndRaise(SelectedIndexProperty, ref _selectedIndex, (int val, ref int backing, Action<Action> notifyWrapper) =>
                     {
-                        _selectedIndex = effective;
-                        RaisePropertyChanged(SelectedIndexProperty, old, effective, BindingPriority.LocalValue);
-                        SelectedItem = ElementAt(Items, effective);
-                    }
+                        var old = backing;
+                        var effective = (val >= 0 && val < Items?.Cast<object>().Count()) ? val : -1;
+
+                        if (old != effective)
+                        {
+                            backing = effective;
+                            notifyWrapper(() =>
+                                RaisePropertyChanged(
+                                    SelectedIndexProperty,
+                                    old,
+                                    effective,
+                                    BindingPriority.LocalValue));
+                            SelectedItem = ElementAt(Items, effective);
+                        }
+                    }, value);
                 }
                 else
                 {
@@ -183,31 +191,41 @@ namespace Avalonia.Controls.Primitives
             {
                 if (_updateCount == 0)
                 {
-                    var old = SelectedItem;
-                    var index = IndexOf(Items, value);
-                    var effective = index != -1 ? value : null;
-
-                    if (!object.Equals(effective, old))
+                    SetAndRaise(SelectedItemProperty, ref _selectedItem, (object val, ref object backing, Action<Action> notifyWrapper) =>
                     {
-                        _selectedItem = effective;
-                        RaisePropertyChanged(SelectedItemProperty, old, effective, BindingPriority.LocalValue);
-                        SelectedIndex = index;
+                        var old = backing;
+                        var index = IndexOf(Items, val);
+                        var effective = index != -1 ? val : null;
 
-                        if (effective != null)
+                        if (!object.Equals(effective, old))
                         {
-                            if (SelectedItems.Count != 1 || SelectedItems[0] != effective)
+                            backing = effective;
+
+                            notifyWrapper(() =>
+                                RaisePropertyChanged(
+                                    SelectedItemProperty,
+                                    old,
+                                    effective,
+                                    BindingPriority.LocalValue));
+
+                            SelectedIndex = index;
+
+                            if (effective != null)
+                            {
+                                if (SelectedItems.Count != 1 || SelectedItems[0] != effective)
+                                {
+                                    _syncingSelectedItems = true;
+                                    SelectedItems.Clear();
+                                    SelectedItems.Add(effective);
+                                    _syncingSelectedItems = false;
+                                }
+                            }
+                            else if (SelectedItems.Count > 0)
                             {
-                                _syncingSelectedItems = true;
                                 SelectedItems.Clear();
-                                SelectedItems.Add(effective);
-                                _syncingSelectedItems = false;
                             }
                         }
-                        else if (SelectedItems.Count > 0)
-                        {
-                            SelectedItems.Clear();
-                        }
-                    }
+                    }, value);
                 }
                 else
                 {

+ 87 - 12
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs

@@ -2,22 +2,22 @@
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 using System;
-using System.Collections;
-using System.Collections.Generic;
+using System.ComponentModel;
+using System.Reactive.Concurrency;
 using System.Reactive.Linq;
 using System.Reactive.Subjects;
-using Microsoft.Reactive.Testing;
+using System.Threading;
+using System.Threading.Tasks;
 using Avalonia.Data;
 using Avalonia.Logging;
-using Avalonia.UnitTests;
-using Xunit;
-using System.Threading.Tasks;
+using Avalonia.Markup.Xaml.Data;
 using Avalonia.Platform;
-using System.Threading;
-using Moq;
-using System.Reactive.Disposables;
-using System.Reactive.Concurrency;
 using Avalonia.Threading;
+using Avalonia.UnitTests;
+using Avalonia.Diagnostics;
+using Microsoft.Reactive.Testing;
+using Moq;
+using Xunit;
 
 namespace Avalonia.Base.UnitTests
 {
@@ -363,7 +363,7 @@ namespace Avalonia.Base.UnitTests
                 Assert.True(called);
             }
         }
-        
+
         [Fact]
         public async Task Bind_With_Scheduler_Executes_On_Scheduler()
         {
@@ -387,6 +387,37 @@ namespace Avalonia.Base.UnitTests
             }
         }
 
+        [Fact]
+        public void SetValue_Should_Not_Cause_StackOverflow_And_Have_Correct_Values()
+        {
+            var viewModel = new TestStackOverflowViewModel()
+            {
+                Value = 50
+            };
+
+            var target = new Class1();
+
+            target.Bind(Class1.DoubleValueProperty,
+                new Binding("Value") { Mode = BindingMode.TwoWay, Source = viewModel });
+
+            var child = new Class1();
+
+            child[!!Class1.DoubleValueProperty] = target[!!Class1.DoubleValueProperty];
+
+            Assert.Equal(1, viewModel.SetterInvokedCount);
+
+            // Issues #855 and #824 were causing a StackOverflowException at this point.
+            target.DoubleValue = 51.001;
+
+            Assert.Equal(2, viewModel.SetterInvokedCount);
+
+            double expected = 51;
+
+            Assert.Equal(expected, viewModel.Value);
+            Assert.Equal(expected, target.DoubleValue);
+            Assert.Equal(expected, child.DoubleValue);
+        }
+
         [Fact]
         public void IsAnimating_On_Property_With_No_Value_Returns_False()
         {
@@ -445,6 +476,15 @@ namespace Avalonia.Base.UnitTests
 
             public static readonly StyledProperty<double> QuxProperty =
                 AvaloniaProperty.Register<Class1, double>("Qux", 5.6);
+
+            public static readonly StyledProperty<double> DoubleValueProperty =
+                        AvaloniaProperty.Register<Class1, double>(nameof(DoubleValue));
+
+            public double DoubleValue
+            {
+                get { return GetValue(DoubleValueProperty); }
+                set { SetValue(DoubleValueProperty, value); }
+            }
         }
 
         private class Class2 : Class1
@@ -471,5 +511,40 @@ namespace Avalonia.Base.UnitTests
                 return InstancedBinding.OneTime(_source);
             }
         }
+
+        private class TestStackOverflowViewModel : INotifyPropertyChanged
+        {
+            public int SetterInvokedCount { get; private set; }
+
+            public const int MaxInvokedCount = 1000;
+
+            private double _value;
+
+            public event PropertyChangedEventHandler PropertyChanged;
+
+            public double Value
+            {
+                get { return _value; }
+                set
+                {
+                    if (_value != value)
+                    {
+                        SetterInvokedCount++;
+                        if (SetterInvokedCount < MaxInvokedCount)
+                        {
+                            _value = (int)value;
+                            if (_value > 75) _value = 75;
+                            if (_value < 25) _value = 25;
+                        }
+                        else
+                        {
+                            _value = value;
+                        }
+
+                        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Value)));
+                    }
+                }
+            }
+        }
     }
-}
+}

+ 93 - 10
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs

@@ -3,10 +3,11 @@
 
 using System;
 using System.Collections.Generic;
+using System.ComponentModel;
 using System.Reactive.Subjects;
-using Avalonia;
 using Avalonia.Data;
 using Avalonia.Logging;
+using Avalonia.Markup.Xaml.Data;
 using Avalonia.UnitTests;
 using Xunit;
 
@@ -208,7 +209,7 @@ namespace Avalonia.Base.UnitTests
         {
             var target = new Class1();
 
-            Assert.Throws<ArgumentException>(() => 
+            Assert.Throws<ArgumentException>(() =>
                 target.SetValue(Class1.BarProperty, "newvalue"));
         }
 
@@ -217,7 +218,7 @@ namespace Avalonia.Base.UnitTests
         {
             var target = new Class1();
 
-            Assert.Throws<ArgumentException>(() => 
+            Assert.Throws<ArgumentException>(() =>
                 target.SetValue((AvaloniaProperty)Class1.BarProperty, "newvalue"));
         }
 
@@ -227,7 +228,7 @@ namespace Avalonia.Base.UnitTests
             var target = new Class1();
             var source = new Subject<string>();
 
-            Assert.Throws<ArgumentException>(() => 
+            Assert.Throws<ArgumentException>(() =>
                 target.Bind(Class1.BarProperty, source));
         }
 
@@ -439,12 +440,46 @@ namespace Avalonia.Base.UnitTests
             Assert.Equal(BindingMode.OneWayToSource, bar.GetMetadata<Class2>().DefaultBindingMode);
         }
 
+        [Fact]
+        public void SetValue_Should_Not_Cause_StackOverflow_And_Have_Correct_Values()
+        {
+            var viewModel = new TestStackOverflowViewModel()
+            {
+                Value = 50
+            };
+
+            var target = new Class1();
+
+            target.Bind(Class1.DoubleValueProperty, new Binding("Value")
+                                                    {
+                                                        Mode = BindingMode.TwoWay,
+                                                        Source = viewModel
+                                                    });
+
+            var child = new Class1();
+
+            child[!!Class1.DoubleValueProperty] = target[!!Class1.DoubleValueProperty];
+
+            Assert.Equal(1, viewModel.SetterInvokedCount);
+
+            // Issues #855 and #824 were causing a StackOverflowException at this point.
+            target.DoubleValue = 51.001;
+
+            Assert.Equal(2, viewModel.SetterInvokedCount);
+
+            double expected = 51;
+
+            Assert.Equal(expected, viewModel.Value);
+            Assert.Equal(expected, target.DoubleValue);
+            Assert.Equal(expected, child.DoubleValue);
+        }
+
         private class Class1 : AvaloniaObject
         {
             public static readonly DirectProperty<Class1, string> FooProperty =
                 AvaloniaProperty.RegisterDirect<Class1, string>(
-                    "Foo", 
-                    o => o.Foo, 
+                    "Foo",
+                    o => o.Foo,
                     (o, v) => o.Foo = v,
                     unsetValue: "unset");
 
@@ -453,14 +488,21 @@ namespace Avalonia.Base.UnitTests
 
             public static readonly DirectProperty<Class1, int> BazProperty =
                 AvaloniaProperty.RegisterDirect<Class1, int>(
-                    "Bar", 
-                    o => o.Baz, 
-                    (o,v) => o.Baz = v,
+                    "Bar",
+                    o => o.Baz,
+                    (o, v) => o.Baz = v,
                     unsetValue: -1);
 
+            public static readonly DirectProperty<Class1, double> DoubleValueProperty =
+                AvaloniaProperty.RegisterDirect<Class1, double>(
+                    nameof(DoubleValue),
+                    o => o.DoubleValue,
+                    (o, v) => o.DoubleValue = v);
+
             private string _foo = "initial";
             private readonly string _bar = "bar";
             private int _baz = 5;
+            private double _doubleValue;
 
             public string Foo
             {
@@ -478,6 +520,12 @@ namespace Avalonia.Base.UnitTests
                 get { return _baz; }
                 set { SetAndRaise(BazProperty, ref _baz, value); }
             }
+
+            public double DoubleValue
+            {
+                get { return _doubleValue; }
+                set { SetAndRaise(DoubleValueProperty, ref _doubleValue, value); }
+            }
         }
 
         private class Class2 : AvaloniaObject
@@ -497,5 +545,40 @@ namespace Avalonia.Base.UnitTests
                 set { SetAndRaise(FooProperty, ref _foo, value); }
             }
         }
+
+        private class TestStackOverflowViewModel : INotifyPropertyChanged
+        {
+            public int SetterInvokedCount { get; private set; }
+
+            public const int MaxInvokedCount = 1000;
+
+            private double _value;
+
+            public event PropertyChangedEventHandler PropertyChanged;
+
+            public double Value
+            {
+                get { return _value; }
+                set
+                {
+                    if (_value != value)
+                    {
+                        SetterInvokedCount++;
+                        if (SetterInvokedCount < MaxInvokedCount)
+                        {
+                            _value = (int)value;
+                            if (_value > 75) _value = 75;
+                            if (_value < 25) _value = 25;
+                        }
+                        else
+                        {
+                            _value = value;
+                        }
+
+                        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Value)));
+                    }
+                }
+            }
+        }
     }
-}
+}

+ 1 - 0
tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj

@@ -49,6 +49,7 @@
     <Reference Include="System.Xml" />
   </ItemGroup>
   <ItemGroup>
+    <Compile Include="Base\Properties.cs" />
     <Compile Include="Layout\Measure.cs" />
     <Compile Include="Styling\ApplyStyling.cs" />
     <Compile Include="Program.cs" />

+ 43 - 0
tests/Avalonia.Benchmarks/Base/Properties.cs

@@ -0,0 +1,43 @@
+using System;
+using System.Reactive.Subjects;
+using BenchmarkDotNet.Attributes;
+
+namespace Avalonia.Benchmarks.Base
+{
+    [MemoryDiagnoser]
+    public class AvaloniaObjectBenchmark
+    {
+        private Class1 target = new Class1();
+        private Subject<int> intBinding = new Subject<int>();
+
+        public AvaloniaObjectBenchmark()
+        {
+            target.SetValue(Class1.IntProperty, 123);
+        }
+
+        [Benchmark]
+        public void ClearAndSetIntProperty()
+        {
+            target.ClearValue(Class1.IntProperty);
+            target.SetValue(Class1.IntProperty, 123);
+        }
+
+        [Benchmark]
+        public void BindIntProperty()
+        {
+            using (target.Bind(Class1.IntProperty, intBinding))
+            {
+                for (var i = 0; i < 100; ++i)
+                {
+                    intBinding.OnNext(i);
+                }
+            }
+        }
+
+        class Class1 : AvaloniaObject
+        {
+            public static readonly AvaloniaProperty<int> IntProperty =
+                AvaloniaProperty.Register<Class1, int>("Int");
+        }
+    }
+}

+ 70 - 1
tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs

@@ -1,11 +1,15 @@
 // 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.Collections.Generic;
+using System.ComponentModel;
 using System.Linq;
 using Avalonia.Controls.Presenters;
 using Avalonia.Controls.Templates;
+using Avalonia.Data;
 using Avalonia.Input;
 using Avalonia.LogicalTree;
+using Avalonia.Markup.Xaml.Data;
 using Avalonia.Styling;
 using Avalonia.VisualTree;
 using Xunit;
@@ -199,6 +203,71 @@ namespace Avalonia.Controls.UnitTests
             Assert.Equal(1, target.SelectedIndex);
         }
 
+        [Fact]
+        public void SelectedItem_Should_Not_Cause_StackOverflow()
+        {
+            var viewModel = new TestStackOverflowViewModel()
+            {
+                Items = new List<string> { "foo", "bar", "baz" }
+            };
+
+            var target = new ListBox
+            {
+                Template = new FuncControlTemplate(CreateListBoxTemplate),
+                DataContext = viewModel,
+                Items = viewModel.Items
+            };
+
+            target.Bind(ListBox.SelectedItemProperty,
+                new Binding("SelectedItem") { Mode = BindingMode.TwoWay });
+
+            Assert.Equal(0, viewModel.SetterInvokedCount);
+
+            // In Issue #855, a Stackoverflow occured here.
+            target.SelectedItem = viewModel.Items[2];
+
+            Assert.Equal(viewModel.Items[1], target.SelectedItem);
+            Assert.Equal(1, viewModel.SetterInvokedCount);
+        }
+
+        private class TestStackOverflowViewModel : INotifyPropertyChanged
+        {
+            public List<string> Items { get; set; }
+
+            public int SetterInvokedCount { get; private set; }
+
+            public const int MaxInvokedCount = 1000;
+
+            private string _selectedItem;
+
+            public event PropertyChangedEventHandler PropertyChanged;
+
+            public string SelectedItem
+            {
+                get { return _selectedItem; }
+                set
+                {
+                    if (_selectedItem != value)
+                    {
+                        SetterInvokedCount++;
+
+                        int index = Items.IndexOf(value);
+
+                        if (MaxInvokedCount > SetterInvokedCount && index > 0)
+                        {
+                            _selectedItem = Items[index - 1];
+                        }
+                        else
+                        {
+                            _selectedItem = value;
+                        }
+
+                        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SelectedItem)));
+                    }
+                }
+            }
+        }
+
         private Control CreateListBoxTemplate(ITemplatedControl parent)
         {
             return new ScrollViewer
@@ -237,4 +306,4 @@ namespace Avalonia.Controls.UnitTests
             target.Presenter.ApplyTemplate();
         }
     }
-}
+}

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

@@ -2,7 +2,12 @@
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 using System;
+using System.ComponentModel;
 using Avalonia.Controls.Primitives;
+using Avalonia.Controls.Templates;
+using Avalonia.Data;
+using Avalonia.Markup.Xaml.Data;
+using Avalonia.Styling;
 using Xunit;
 
 namespace Avalonia.Controls.UnitTests.Primitives
@@ -87,8 +92,111 @@ namespace Avalonia.Controls.UnitTests.Primitives
             Assert.Throws<ArgumentException>(() => target.Value = double.NegativeInfinity);
         }
 
+        [Theory]
+        [InlineData(true)]
+        [InlineData(false)]
+        public void SetValue_Should_Not_Cause_StackOverflow(bool useXamlBinding)
+        {
+            var viewModel = new TestStackOverflowViewModel()
+            {
+                Value = 50
+            };
+
+            Track track = null;
+
+            var target = new TestRange()
+            {
+                Template = new FuncControlTemplate<RangeBase>(c =>
+                {
+                    track = new Track()
+                    {
+                        Width = 100,
+                        Orientation = Orientation.Horizontal,
+                        [~~Track.MinimumProperty] = c[~~RangeBase.MinimumProperty],
+                        [~~Track.MaximumProperty] = c[~~RangeBase.MaximumProperty],
+
+                        Name = "PART_Track",
+                        Thumb = new Thumb()
+                    };
+
+                    if (useXamlBinding)
+                    {
+                        track.Bind(Track.ValueProperty, new Binding("Value")
+                                                    {
+                                                        Mode = BindingMode.TwoWay,
+                                                        Source = c,
+                                                        Priority = BindingPriority.Style
+                                                    });
+                    }
+                    else
+                    {
+                        track[~~Track.ValueProperty] = c[~~RangeBase.ValueProperty];
+                    }
+
+                    return track;
+                }),
+                Minimum = 0,
+                Maximum = 100,
+                DataContext = viewModel
+            };
+
+            target.Bind(TestRange.ValueProperty, new Binding("Value") { Mode = BindingMode.TwoWay });
+
+            target.ApplyTemplate();
+            track.Measure(new Size(100, 0));
+            track.Arrange(new Rect(0, 0, 100, 0));
+
+            Assert.Equal(1, viewModel.SetterInvokedCount);
+
+            // Issues #855 and #824 were causing a StackOverflowException at this point.
+            target.Value = 51.001;
+
+            Assert.Equal(2, viewModel.SetterInvokedCount);
+
+            double expected = 51;
+
+            Assert.Equal(expected, viewModel.Value);
+            Assert.Equal(expected, target.Value);
+            Assert.Equal(expected, track.Value);
+        }
+
         private class TestRange : RangeBase
         {
         }
+
+        private class TestStackOverflowViewModel : INotifyPropertyChanged
+        {
+            public int SetterInvokedCount { get; private set; }
+
+            public const int MaxInvokedCount = 1000;
+
+            private double _value;
+
+            public event PropertyChangedEventHandler PropertyChanged;
+
+            public double Value
+            {
+                get { return _value; }
+                set
+                {
+                    if (_value != value)
+                    {
+                        SetterInvokedCount++;
+                        if (SetterInvokedCount < MaxInvokedCount)
+                        {
+                            _value = (int)value;
+                            if (_value > 75) _value = 75;
+                            if (_value < 25) _value = 25;
+                        }
+                        else
+                        {
+                            _value = value;
+                        }
+
+                        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Value)));
+                    }
+                }
+            }
+        }
     }
 }

+ 1 - 0
tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj

@@ -21,6 +21,7 @@
     <ProjectReference Include="..\..\src\Avalonia.Visuals\Avalonia.Visuals.csproj" />
     <ProjectReference Include="..\..\src\Avalonia.Styling\Avalonia.Styling.csproj" />
     <ProjectReference Include="..\..\src\Avalonia.Themes.Default\Avalonia.Themes.Default.csproj" />
+    <ProjectReference Include="..\Avalonia.Base.UnitTests\Avalonia.Base.UnitTests.csproj" />
     <ProjectReference Include="..\Avalonia.UnitTests\Avalonia.UnitTests.csproj" />
   </ItemGroup>
   <ItemGroup>

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

@@ -338,6 +338,145 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data
             Assert.Equal("foo", target.Content);
         }
 
+        [Fact]
+        public void StyledProperty_SetValue_Should_Not_Cause_StackOverflow_And_Have_Correct_Values()
+        {
+            var viewModel = new TestStackOverflowViewModel()
+            {
+                Value = 50
+            };
+
+            var target = new StyledPropertyClass();
+
+            target.Bind(StyledPropertyClass.DoubleValueProperty,
+                new Binding("Value") { Mode = BindingMode.TwoWay, Source = viewModel });
+
+            var child = new StyledPropertyClass();
+
+            child.Bind(StyledPropertyClass.DoubleValueProperty,
+                new Binding("DoubleValue")
+                {
+                    Mode = BindingMode.TwoWay,
+                    Source = target
+                });
+
+            Assert.Equal(1, viewModel.SetterInvokedCount);
+
+            //here in real life stack overflow exception is thrown issue #855 and #824
+            target.DoubleValue = 51.001;
+
+            Assert.Equal(2, viewModel.SetterInvokedCount);
+
+            double expected = 51;
+
+            Assert.Equal(expected, viewModel.Value);
+            Assert.Equal(expected, target.DoubleValue);
+            Assert.Equal(expected, child.DoubleValue);
+        }
+
+        [Fact]
+        public void SetValue_Should_Not_Cause_StackOverflow_And_Have_Correct_Values()
+        {
+            var viewModel = new TestStackOverflowViewModel()
+            {
+                Value = 50
+            };
+
+            var target = new DirectPropertyClass();
+
+            target.Bind(DirectPropertyClass.DoubleValueProperty, new Binding("Value")
+            {
+                Mode = BindingMode.TwoWay,
+                Source = viewModel
+            });
+
+            var child = new DirectPropertyClass();
+
+            child.Bind(DirectPropertyClass.DoubleValueProperty,
+                new Binding("DoubleValue")
+                {
+                    Mode = BindingMode.TwoWay,
+                    Source = target
+                });
+
+            Assert.Equal(1, viewModel.SetterInvokedCount);
+
+            //here in real life stack overflow exception is thrown issue #855 and #824
+            target.DoubleValue = 51.001;
+
+            Assert.Equal(2, viewModel.SetterInvokedCount);
+
+            double expected = 51;
+
+            Assert.Equal(expected, viewModel.Value);
+            Assert.Equal(expected, target.DoubleValue);
+            Assert.Equal(expected, child.DoubleValue);
+        }
+
+        private class StyledPropertyClass : AvaloniaObject
+        {
+            public static readonly StyledProperty<double> DoubleValueProperty =
+                        AvaloniaProperty.Register<StyledPropertyClass, double>(nameof(DoubleValue));
+
+            public double DoubleValue
+            {
+                get { return GetValue(DoubleValueProperty); }
+                set { SetValue(DoubleValueProperty, value); }
+            }
+        }
+
+        private class DirectPropertyClass : AvaloniaObject
+        {
+            public static readonly DirectProperty<DirectPropertyClass, double> DoubleValueProperty =
+                AvaloniaProperty.RegisterDirect<DirectPropertyClass, double>(
+                    nameof(DoubleValue),
+                    o => o.DoubleValue,
+                    (o, v) => o.DoubleValue = v);
+
+            private double _doubleValue;
+            public double DoubleValue
+            {
+                get { return _doubleValue; }
+                set { SetAndRaise(DoubleValueProperty, ref _doubleValue, value); }
+            }
+        }
+
+        private class TestStackOverflowViewModel : INotifyPropertyChanged
+        {
+            public int SetterInvokedCount { get; private set; }
+
+            public const int MaxInvokedCount = 1000;
+
+            private double _value;
+
+            public event PropertyChangedEventHandler PropertyChanged;
+
+            public double Value
+            {
+                get { return _value; }
+                set
+                {
+                    if (_value != value)
+                    {
+                        SetterInvokedCount++;
+                        if (SetterInvokedCount < MaxInvokedCount)
+                        {
+                            _value = (int)value;
+                            if (_value > 75) _value = 75;
+                            if (_value < 25) _value = 25;
+                        }
+                        else
+                        {
+                            _value = value;
+                        }
+
+                        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Value)));
+                    }
+                }
+            }
+        }
+        
+
         [Fact]
         public void Binding_With_Null_Path_Works()
         {