فهرست منبع

Handle batching binding completion.

Steven Kirk 5 سال پیش
والد
کامیت
c4c4cc8bd8

+ 9 - 3
src/Avalonia.Base/PropertyStore/BindingEntry.cs

@@ -42,7 +42,7 @@ namespace Avalonia.PropertyStore
         }
 
         public StyledPropertyBase<T> Property { get; }
-        public BindingPriority Priority { get; }
+        public BindingPriority Priority { get; private set; }
         public IObservable<BindingValue<T>> Source { get; }
         Optional<object> IValue.GetValue() => _value.ToObject();
 
@@ -66,10 +66,16 @@ namespace Avalonia.PropertyStore
             _subscription?.Dispose();
             _subscription = null;
             _isSubscribed = false;
-            _sink.Completed(Property, this, _value);
+            OnCompleted();
         }
 
-        public void OnCompleted() => _sink.Completed(Property, this, _value);
+        public void OnCompleted()
+        {
+            var oldValue = _value;
+            _value = default;
+            Priority = BindingPriority.Unset;
+            _sink.Completed(Property, this, oldValue);
+        }
 
         public void OnError(Exception error)
         {

+ 9 - 2
src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs

@@ -28,7 +28,7 @@ namespace Avalonia.PropertyStore
         }
 
         public StyledPropertyBase<T> Property { get; }
-        public BindingPriority Priority { get; }
+        public BindingPriority Priority { get; private set; }
         Optional<object> IValue.GetValue() => _value.ToObject();
 
         public Optional<T> GetValue(BindingPriority maxPriority = BindingPriority.Animation)
@@ -36,7 +36,14 @@ namespace Avalonia.PropertyStore
             return Priority >= maxPriority ? _value : Optional<T>.Empty;
         }
 
-        public void Dispose() => _sink.Completed(Property, this, _value);
+        public void Dispose()
+        {
+            var oldValue = _value;
+            _value = default;
+            Priority = BindingPriority.Unset;
+            _sink.Completed(Property, this, oldValue);
+        }
+
         public void Reparent(IValueSink sink) => _sink = sink;
         public void Start() { }
 

+ 12 - 4
src/Avalonia.Base/ValueStore.cs

@@ -67,11 +67,15 @@ namespace Avalonia
                 if (_values.TryGetValue(entry.property, out var slot))
                 {
                     slot.RaiseValueChanged(_sink, _owner, entry.property, entry.oldValue);
+
+                    if (slot.Priority == BindingPriority.Unset)
+                    {
+                        _values.Remove(entry.property);
+                    }
                 }
                 else
                 {
-                    // TODO
-                    throw new NotImplementedException();
+                    throw new AvaloniaInternalException("Value could not be found at the end of batch update.");
                 }
             }
 
@@ -249,13 +253,17 @@ namespace Avalonia
             IPriorityValueEntry entry,
             Optional<T> oldValue)
         {
-            if (_values.TryGetValue(property, out var slot))
+            if (_values.TryGetValue(property, out var slot) && slot == entry)
             {
-                if (slot == entry)
+                if (_batchUpdate is null)
                 {
                     _values.Remove(property);
                     _sink.Completed(property, entry, oldValue);
                 }
+                else
+                {
+                    NotifyValueChanged(property, oldValue, default, BindingPriority.Unset);
+                }
             }
         }
 

+ 41 - 0
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs

@@ -38,6 +38,21 @@ namespace Avalonia.Base.UnitTests
             Assert.Empty(raised);
         }
 
+        [Fact]
+        public void Binding_Completion_Should_Not_Raise_Property_Changes_During_Batch_Update()
+        {
+            var target = new TestClass();
+            var observable = new TestObservable<string>("foo");
+            var raised = new List<string>();
+
+            target.Bind(TestClass.FooProperty, observable, BindingPriority.LocalValue);
+            target.GetObservable(TestClass.FooProperty).Skip(1).Subscribe(x => raised.Add(x));
+            target.BeginBatchUpdate();
+            observable.OnCompleted();
+
+            Assert.Empty(raised);
+        }
+
         [Fact]
         public void SetValue_Change_Should_Be_Raised_After_Batch_Update_1()
         {
@@ -204,6 +219,27 @@ namespace Avalonia.Base.UnitTests
             Assert.Equal("baz", raised[0].NewValue);
         }
 
+        [Fact]
+        public void Binding_Completion_Should_Be_Raised_After_Batch_Update()
+        {
+            var target = new TestClass();
+            var observable = new TestObservable<string>("foo");
+            var raised = new List<AvaloniaPropertyChangedEventArgs>();
+
+            target.Bind(TestClass.FooProperty, observable, BindingPriority.LocalValue);
+            target.PropertyChanged += (s, e) => raised.Add(e);
+
+            target.BeginBatchUpdate();
+            observable.OnCompleted();
+            target.EndBatchUpdate();
+
+            Assert.Equal(1, raised.Count);
+            Assert.Null(target.Foo);
+            Assert.Equal("foo", raised[0].OldValue);
+            Assert.Null(raised[0].NewValue);
+            Assert.Equal(BindingPriority.Unset, raised[0].Priority);
+        }
+
         [Fact]
         public void Bindings_Should_Be_Subscribed_Before_Batch_Update()
         {
@@ -325,14 +361,19 @@ namespace Avalonia.Base.UnitTests
         public class TestObservable<T> : ObservableBase<BindingValue<T>>
         {
             private readonly T _value;
+            private IObserver<BindingValue<T>> _observer;
 
             public TestObservable(T value) => _value = value;
 
             public int SubscribeCount { get; private set; }
 
+            public void OnCompleted() => _observer.OnCompleted();
+            public void OnError(Exception e) => _observer.OnError(e);
+
             protected override IDisposable SubscribeCore(IObserver<BindingValue<T>> observer)
             {
                 ++SubscribeCount;
+                _observer = observer;
                 observer.OnNext(_value);
                 return Disposable.Empty;
             }