Browse Source

Implement batching for AvaloniaObject property values.

Cuts down the amount of notifications raised when controls' stying is attached/detached.

Part of fixing #5027.
Steven Kirk 5 years ago
parent
commit
0f238113f5

+ 39 - 1
src/Avalonia.Base/AvaloniaObject.cs

@@ -23,7 +23,7 @@ namespace Avalonia
         private EventHandler<AvaloniaPropertyChangedEventArgs> _propertyChanged;
         private List<IAvaloniaObject> _inheritanceChildren;
         private ValueStore _values;
-        private ValueStore Values => _values ?? (_values = new ValueStore(this));
+        private bool _batchUpdate;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="AvaloniaObject"/> class.
@@ -117,6 +117,22 @@ namespace Avalonia
             set { this.Bind(binding.Property, value); }
         }
 
+        private ValueStore Values
+        {
+            get
+            {
+                if (_values is null)
+                {
+                    _values = new ValueStore(this);
+
+                    if (_batchUpdate)
+                        _values.BeginBatchUpdate();
+                }
+
+                return _values;
+            }
+        }
+
         public bool CheckAccess() => Dispatcher.UIThread.CheckAccess();
 
         public void VerifyAccess() => Dispatcher.UIThread.VerifyAccess();
@@ -434,6 +450,28 @@ namespace Avalonia
             _values?.CoerceValue(property);
         }
 
+        public void BeginBatchUpdate()
+        {
+            if (_batchUpdate)
+            {
+                throw new InvalidOperationException("Batch update already in progress.");
+            }
+
+            _batchUpdate = true;
+            _values?.BeginBatchUpdate();
+        }
+
+        public void EndBatchUpdate()
+        {
+            if (!_batchUpdate)
+            {
+                throw new InvalidOperationException("No batch update in progress.");
+            }
+
+            _batchUpdate = false;
+            _values?.EndBatchUpdate();
+        }
+
         /// <inheritdoc/>
         void IAvaloniaObject.AddInheritanceChild(IAvaloniaObject child)
         {

+ 40 - 4
src/Avalonia.Base/PropertyStore/BindingEntry.cs

@@ -9,8 +9,9 @@ namespace Avalonia.PropertyStore
     /// <summary>
     /// Represents an untyped interface to <see cref="BindingEntry{T}"/>.
     /// </summary>
-    internal interface IBindingEntry : IPriorityValueEntry, IDisposable
+    internal interface IBindingEntry : IBatchUpdate, IPriorityValueEntry, IDisposable
     {
+        void Start(bool ignoreBatchUpdate);
     }
 
     /// <summary>
@@ -22,6 +23,8 @@ namespace Avalonia.PropertyStore
         private readonly IAvaloniaObject _owner;
         private IValueSink _sink;
         private IDisposable? _subscription;
+        private bool _isSubscribed;
+        private bool _batchUpdate;
         private Optional<T> _value;
 
         public BindingEntry(
@@ -43,6 +46,16 @@ namespace Avalonia.PropertyStore
         public IObservable<BindingValue<T>> Source { get; }
         Optional<object> IValue.GetValue() => _value.ToObject();
 
+        public void BeginBatchUpdate() => _batchUpdate = true;
+
+        public void EndBatchUpdate()
+        {
+            _batchUpdate = false;
+
+            if (_sink is ValueStore)
+                Start();
+        }
+
         public Optional<T> GetValue(BindingPriority maxPriority)
         {
             return Priority >= maxPriority ? _value : Optional<T>.Empty;
@@ -52,6 +65,7 @@ namespace Avalonia.PropertyStore
         {
             _subscription?.Dispose();
             _subscription = null;
+            _isSubscribed = false;
             _sink.Completed(Property, this, _value);
         }
 
@@ -79,13 +93,35 @@ namespace Avalonia.PropertyStore
             }
         }
 
-        public void Start()
+        public void Start() => Start(false);
+
+        public void Start(bool ignoreBatchUpdate)
         {
-            _subscription = Source.Subscribe(this);
+            // We can't use _subscription to check whether we're subscribed because it won't be set
+            // until Subscribe has finished, which will be too late to prevent reentrancy.
+            if (!_isSubscribed && (!_batchUpdate || ignoreBatchUpdate))
+            {
+                _isSubscribed = true;
+                _subscription = Source.Subscribe(this);
+            }
         }
 
         public void Reparent(IValueSink sink) => _sink = sink;
-        
+
+        public void RaiseValueChanged(
+            IValueSink sink,
+            IAvaloniaObject owner,
+            AvaloniaProperty property,
+            Optional<object> oldValue)
+        {
+            sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
+                owner,
+                (AvaloniaProperty<T>)property,
+                oldValue.GetValueOrDefault<T>(),
+                _value,
+                Priority));
+        }
+
         private void UpdateValue(BindingValue<T> value)
         {
             if (value.HasValue && Property.ValidateValue?.Invoke(value.Value) == false)

+ 15 - 0
src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs

@@ -38,5 +38,20 @@ namespace Avalonia.PropertyStore
 
         public void Dispose() => _sink.Completed(Property, this, _value);
         public void Reparent(IValueSink sink) => _sink = sink;
+        public void Start() { }
+
+        public void RaiseValueChanged(
+            IValueSink sink,
+            IAvaloniaObject owner,
+            AvaloniaProperty property,
+            Optional<object> oldValue)
+        {
+            sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
+                owner,
+                (AvaloniaProperty<T>)property,
+                oldValue.GetValueOrDefault<T>(),
+                _value,
+                Priority));
+        }
     }
 }

+ 8 - 0
src/Avalonia.Base/PropertyStore/IBatchUpdate.cs

@@ -0,0 +1,8 @@
+namespace Avalonia.PropertyStore
+{
+    internal interface IBatchUpdate
+    {
+        void BeginBatchUpdate();
+        void EndBatchUpdate();
+    }
+}

+ 7 - 1
src/Avalonia.Base/PropertyStore/IValue.cs

@@ -9,8 +9,14 @@ namespace Avalonia.PropertyStore
     /// </summary>
     internal interface IValue
     {
-        Optional<object> GetValue();
         BindingPriority Priority { get; }
+        Optional<object> GetValue();
+        void Start();
+        void RaiseValueChanged(
+            IValueSink sink,
+            IAvaloniaObject owner,
+            AvaloniaProperty property,
+            Optional<object> oldValue);
     }
 
     /// <summary>

+ 15 - 0
src/Avalonia.Base/PropertyStore/LocalValueEntry.cs

@@ -24,5 +24,20 @@ namespace Avalonia.PropertyStore
         }
 
         public void SetValue(T value) => _value = value;
+        public void Start() { }
+
+        public void RaiseValueChanged(
+            IValueSink sink,
+            IAvaloniaObject owner,
+            AvaloniaProperty property,
+            Optional<object> oldValue)
+        {
+            sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
+                owner,
+                (AvaloniaProperty<T>)property,
+                oldValue.GetValueOrDefault<T>(),
+                _value,
+                BindingPriority.LocalValue));
+        }
     }
 }

+ 83 - 26
src/Avalonia.Base/PropertyStore/PriorityValue.cs

@@ -18,7 +18,7 @@ namespace Avalonia.PropertyStore
     /// <see cref="IPriorityValueEntry{T}"/> entries (sorted first by priority and then in the order
     /// they were added) plus a local value.
     /// </remarks>
-    internal class PriorityValue<T> : IValue<T>, IValueSink
+    internal class PriorityValue<T> : IValue<T>, IValueSink, IBatchUpdate
     {
         private readonly IAvaloniaObject _owner;
         private readonly IValueSink _sink;
@@ -26,6 +26,8 @@ namespace Avalonia.PropertyStore
         private readonly Func<IAvaloniaObject, T, T>? _coerceValue;
         private Optional<T> _localValue;
         private Optional<T> _value;
+        private bool _isCalculatingValue;
+        private bool _batchUpdate;
 
         public PriorityValue(
             IAvaloniaObject owner,
@@ -53,6 +55,18 @@ namespace Avalonia.PropertyStore
             existing.Reparent(this);
             _entries.Add(existing);
 
+            if (existing is IBindingEntry binding &&
+                existing.Priority == BindingPriority.LocalValue)
+            {
+                // Bit of a special case here: if we have a local value binding that is being
+                // promoted to a priority value we need to make sure the binding is subscribed
+                // even if we've got a batch operation in progress because otherwise we don't know
+                // whether the binding or a subsequent SetValue with local priority will win. A
+                // notification won't be sent during batch update anyway because it will be
+                // caught and stored for later by the ValueStore.
+                binding.Start(ignoreBatchUpdate: true);
+            }
+
             var v = existing.GetValue();
             
             if (v.HasValue)
@@ -78,6 +92,28 @@ namespace Avalonia.PropertyStore
         public IReadOnlyList<IPriorityValueEntry<T>> Entries => _entries;
         Optional<object> IValue.GetValue() => _value.ToObject();
 
+        public void BeginBatchUpdate()
+        {
+            _batchUpdate = true;
+
+            foreach (var entry in _entries)
+            {
+                (entry as IBatchUpdate)?.BeginBatchUpdate();
+            }
+        }
+
+        public void EndBatchUpdate()
+        {
+            _batchUpdate = false;
+
+            foreach (var entry in _entries)
+            {
+                (entry as IBatchUpdate)?.EndBatchUpdate();
+            }
+
+            UpdateEffectiveValue(null);
+        }
+
         public void ClearLocalValue()
         {
             UpdateEffectiveValue(new AvaloniaPropertyChangedEventArgs<T>(
@@ -137,7 +173,22 @@ namespace Avalonia.PropertyStore
             return binding;
         }
 
-        public void CoerceValue() => UpdateEffectiveValue(null);
+        public void UpdateEffectiveValue() => UpdateEffectiveValue(null);
+        public void Start() => UpdateEffectiveValue(null);
+
+        public void RaiseValueChanged(
+            IValueSink sink,
+            IAvaloniaObject owner,
+            AvaloniaProperty property,
+            Optional<object> oldValue)
+        {
+            sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
+                owner,
+                (AvaloniaProperty<T>)property,
+                oldValue.GetValueOrDefault<T>(),
+                _value,
+                Priority));
+        }
 
         void IValueSink.ValueChanged<TValue>(AvaloniaPropertyChangedEventArgs<TValue> change)
         {
@@ -146,7 +197,7 @@ namespace Avalonia.PropertyStore
                 _localValue = default;
             }
 
-            if (change is AvaloniaPropertyChangedEventArgs<T> c)
+            if (!_isCalculatingValue && change is AvaloniaPropertyChangedEventArgs<T> c)
             {
                 UpdateEffectiveValue(c);
             }
@@ -188,41 +239,47 @@ namespace Avalonia.PropertyStore
 
         public (Optional<T>, BindingPriority) CalculateValue(BindingPriority maxPriority)
         {
-            var reachedLocalValues = false;
+            _isCalculatingValue = true;
 
-            for (var i = _entries.Count - 1; i >= 0; --i)
+            try
             {
-                var entry = _entries[i];
-
-                if (entry.Priority < maxPriority)
+                for (var i = _entries.Count - 1; i >= 0; --i)
                 {
-                    continue;
+                    var entry = _entries[i];
+
+                    if (entry.Priority < maxPriority)
+                    {
+                        continue;
+                    }
+
+                    entry.Start();
+
+                    if (entry.Priority >= BindingPriority.LocalValue &&
+                        maxPriority <= BindingPriority.LocalValue &&
+                        _localValue.HasValue)
+                    {
+                        return (_localValue, BindingPriority.LocalValue);
+                    }
+
+                    var entryValue = entry.GetValue();
+
+                    if (entryValue.HasValue)
+                    {
+                        return (entryValue, entry.Priority);
+                    }
                 }
 
-                if (!reachedLocalValues &&
-                    entry.Priority >= BindingPriority.LocalValue &&
-                    maxPriority <= BindingPriority.LocalValue &&
-                    _localValue.HasValue)
+                if (maxPriority <= BindingPriority.LocalValue && _localValue.HasValue)
                 {
                     return (_localValue, BindingPriority.LocalValue);
                 }
 
-                var entryValue = entry.GetValue();
-
-                if (entryValue.HasValue)
-                {
-                    return (entryValue, entry.Priority);
-                }
+                return (default, BindingPriority.Unset);
             }
-
-            if (!reachedLocalValues &&
-                maxPriority <= BindingPriority.LocalValue &&
-                _localValue.HasValue)
+            finally
             {
-                return (_localValue, BindingPriority.LocalValue);
+                _isCalculatingValue = false;
             }
-
-            return (default, BindingPriority.Unset);
         }
 
         private void UpdateEffectiveValue(AvaloniaPropertyChangedEventArgs<T>? change)

+ 3 - 13
src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs

@@ -1,5 +1,4 @@
 using System;
-using System.Collections.Generic;
 
 namespace Avalonia.Utilities
 {
@@ -17,6 +16,9 @@ namespace Avalonia.Utilities
             _entries = new[] { new Entry { PropertyId = int.MaxValue, Value = default } };
         }
 
+        public int Count => _entries.Length - 1;
+        public TValue this[int index] => _entries[index].Value;
+
         private (int, bool) TryFindEntry(int propertyId)
         {
             if (_entries.Length <= 12)
@@ -147,18 +149,6 @@ namespace Avalonia.Utilities
             }
         }
 
-        public Dictionary<AvaloniaProperty, TValue> ToDictionary()
-        {
-            var dict = new Dictionary<AvaloniaProperty, TValue>(_entries.Length - 1);
-
-            for (int i = 0; i < _entries.Length - 1; ++i)
-            {
-                dict.Add(AvaloniaPropertyRegistry.Instance.FindRegistered(_entries[i].PropertyId), _entries[i].Value);
-            }
-
-            return dict;
-        }
-
         private struct Entry
         {
             internal int PropertyId;

+ 117 - 26
src/Avalonia.Base/ValueStore.cs

@@ -1,4 +1,5 @@
 using System;
+using System.Collections.Generic;
 using Avalonia.Data;
 using Avalonia.PropertyStore;
 using Avalonia.Utilities;
@@ -26,6 +27,7 @@ namespace Avalonia
         private readonly AvaloniaObject _owner;
         private readonly IValueSink _sink;
         private readonly AvaloniaPropertyValueStore<IValue> _values;
+        private List<PropertyUpdate>? _batchUpdate;
 
         public ValueStore(AvaloniaObject owner)
         {
@@ -33,6 +35,49 @@ namespace Avalonia
             _values = new AvaloniaPropertyValueStore<IValue>();
         }
 
+        public void BeginBatchUpdate()
+        {
+            if (_batchUpdate is object)
+            {
+                throw new InvalidOperationException("Batch update already in progress.");
+            }
+
+            _batchUpdate = new List<PropertyUpdate>();
+
+            for (var i = 0; i < _values.Count; ++i)
+            {
+                (_values[i] as IBatchUpdate)?.BeginBatchUpdate();
+            }
+        }
+
+        public void EndBatchUpdate()
+        {
+            if (_batchUpdate is null)
+            {
+                throw new InvalidOperationException("No batch update in progress.");
+            }
+
+            for (var i = 0; i < _values.Count; ++i)
+            {
+                (_values[i] as IBatchUpdate)?.EndBatchUpdate();
+            }
+
+            foreach (var entry in _batchUpdate)
+            {
+                if (_values.TryGetValue(entry.property, out var slot))
+                {
+                    slot.RaiseValueChanged(_sink, _owner, entry.property, entry.oldValue);
+                }
+                else
+                {
+                    // TODO
+                    throw new NotImplementedException();
+                }
+            }
+
+            _batchUpdate = null;
+        }
+
         public bool IsAnimating(AvaloniaProperty property)
         {
             if (_values.TryGetValue(property, out var slot))
@@ -90,23 +135,21 @@ namespace Avalonia
             {
                 // If the property has any coercion callbacks then always create a PriorityValue.
                 var entry = new PriorityValue<T>(_owner, property, this);
-                _values.AddValue(property, entry);
+                AddValue(property, entry);
                 result = entry.SetValue(value, priority);
             }
             else
             {
-                var change = new AvaloniaPropertyChangedEventArgs<T>(_owner, property, default, value, priority);
-
                 if (priority == BindingPriority.LocalValue)
                 {
-                    _values.AddValue(property, new LocalValueEntry<T>(value));
-                    _sink.ValueChanged(change);
+                    AddValue(property, new LocalValueEntry<T>(value));
+                    NotifyValueChanged<T>(property, default, value, priority);
                 }
                 else
                 {
                     var entry = new ConstantValueEntry<T>(property, value, priority, this);
-                    _values.AddValue(property, entry);
-                    _sink.ValueChanged(change);
+                    AddValue(property, entry);
+                    NotifyValueChanged<T>(property, default, value, priority);
                     result = entry;
                 }
             }
@@ -128,15 +171,13 @@ namespace Avalonia
                 // If the property has any coercion callbacks then always create a PriorityValue.
                 var entry = new PriorityValue<T>(_owner, property, this);
                 var binding = entry.AddBinding(source, priority);
-                _values.AddValue(property, entry);
-                binding.Start();
+                AddValue(property, entry);
                 return binding;
             }
             else
             {
                 var entry = new BindingEntry<T>(_owner, property, source, priority, this);
-                _values.AddValue(property, entry);
-                entry.Start();
+                AddValue(property, entry);
                 return entry;
             }
         }
@@ -159,12 +200,7 @@ namespace Avalonia
                     {
                         var old = TryGetValue(property, BindingPriority.LocalValue, out var value) ? value : default;
                         _values.Remove(property);
-                        _sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
-                            _owner,
-                            property,
-                            new Optional<T>(old),
-                            default,
-                            BindingPriority.Unset));
+                        NotifyValueChanged<T>(property, old, default, BindingPriority.Unset);
                     }
                 }
             }
@@ -176,7 +212,7 @@ namespace Avalonia
             {
                 if (slot is PriorityValue<T> p)
                 {
-                    p.CoerceValue();
+                    p.UpdateEffectiveValue();
                 }
             }
         }
@@ -198,7 +234,14 @@ namespace Avalonia
 
         void IValueSink.ValueChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
         {
-            _sink.ValueChanged(change);
+            if (_batchUpdate is object)
+            {
+                NotifyValueChanged<T>(change.Property, change.OldValue, change.NewValue, change.Priority);
+            }
+            else
+            {
+                _sink.ValueChanged(change);
+            }
         }
 
         void IValueSink.Completed<T>(
@@ -240,16 +283,13 @@ namespace Avalonia
                 {
                     var old = l.GetValue(BindingPriority.LocalValue);
                     l.SetValue(value);
-                    _sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
-                        _owner,
-                        property,
-                        old,
-                        value,
-                        priority));
+                    NotifyValueChanged<T>(property, old, value, priority);
                 }
                 else
                 {
                     var priorityValue = new PriorityValue<T>(_owner, property, this, l);
+                    if (_batchUpdate is object)
+                        priorityValue.BeginBatchUpdate();
                     result = priorityValue.SetValue(value, priority);
                     _values.SetValue(property, priorityValue);
                 }
@@ -289,8 +329,59 @@ namespace Avalonia
 
             var binding = priorityValue.AddBinding(source, priority);
             _values.SetValue(property, priorityValue);
-            binding.Start();
+            priorityValue.UpdateEffectiveValue();
             return binding;
         }
+
+        private void AddValue(AvaloniaProperty property, IValue value)
+        {
+            _values.AddValue(property, value);
+            if (_batchUpdate is object && value is IBatchUpdate batch)
+                batch.BeginBatchUpdate();
+            value.Start();
+        }
+
+        private void NotifyValueChanged<T>(
+            AvaloniaProperty<T> property,
+            Optional<T> oldValue,
+            BindingValue<T> newValue,
+            BindingPriority priority)
+        {
+            if (_batchUpdate is object)
+            {
+                var oldValueBoxed = oldValue.ToObject();
+
+                for (var i = 0; i < _batchUpdate.Count; ++i)
+                {
+                    if (_batchUpdate[i].property == property)
+                    {
+                        oldValueBoxed = _batchUpdate[i].oldValue;
+                        _batchUpdate.RemoveAt(i);
+                        break;
+                    }
+                }
+
+                _batchUpdate.Add(new PropertyUpdate
+                {
+                    property = property,
+                    oldValue = oldValueBoxed,
+                });
+            }
+            else
+            {
+                _sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
+                    _owner,
+                    property,
+                    oldValue,
+                    newValue,
+                    priority));
+            }
+        }
+
+        private struct PropertyUpdate
+        {
+            public AvaloniaProperty property;
+            public Optional<object> oldValue;
+        }
     }
 }

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

@@ -18,7 +18,7 @@ namespace Avalonia.Controls
     ///
     /// - A <see cref="Tag"/> property to allow user-defined data to be attached to the control.
     /// </remarks>
-    public class Control : InputElement, IControl, INamed, ISupportInitialize, IVisualBrushInitialize, ISetterValue
+    public class Control : InputElement, IControl, INamed, IVisualBrushInitialize, ISetterValue
     {
         /// <summary>
         /// Defines the <see cref="FocusAdorner"/> property.

+ 3 - 1
src/Avalonia.Styling/IStyledElement.cs

@@ -1,4 +1,5 @@
 using System;
+using System.ComponentModel;
 using Avalonia.Controls;
 using Avalonia.LogicalTree;
 using Avalonia.Styling;
@@ -10,7 +11,8 @@ namespace Avalonia
         IStyleHost,
         ILogical,
         IResourceHost,
-        IDataContextProvider
+        IDataContextProvider,
+        ISupportInitialize
     {
         /// <summary>
         /// Occurs when the control has finished initialization.

+ 23 - 5
src/Avalonia.Styling/StyledElement.cs

@@ -334,7 +334,16 @@ namespace Avalonia
         {
             if (_initCount == 0 && !_styled)
             {
-                AvaloniaLocator.Current.GetService<IStyler>()?.ApplyStyles(this);
+                try
+                {
+                    BeginBatchUpdate();
+                    AvaloniaLocator.Current.GetService<IStyler>()?.ApplyStyles(this);
+                }
+                finally
+                {
+                    EndBatchUpdate();
+                }
+
                 _styled = true;
             }
 
@@ -736,12 +745,21 @@ namespace Avalonia
         {
             if (_appliedStyles is object)
             {
-                foreach (var i in _appliedStyles)
+                BeginBatchUpdate();
+
+                try
                 {
-                    i.Dispose();
-                }
+                    foreach (var i in _appliedStyles)
+                    {
+                        i.Dispose();
+                    }
 
-                _appliedStyles.Clear();
+                    _appliedStyles.Clear();
+                }
+                finally
+                {
+                    EndBatchUpdate();
+                }
             }
 
             _styled = false;

+ 1 - 0
tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj

@@ -3,6 +3,7 @@
     <TargetFrameworks>netcoreapp3.1;net47</TargetFrameworks>
     <OutputType>Library</OutputType>
     <IsTestProject>true</IsTestProject>
+    <LangVersion>latest</LangVersion>
   </PropertyGroup>
   <Import Project="..\..\build\UnitTests.NetCore.targets" />
   <Import Project="..\..\build\UnitTests.NetFX.props" />

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

@@ -0,0 +1,309 @@
+using System;
+using System.Collections.Generic;
+using System.Reactive;
+using System.Reactive.Disposables;
+using System.Reactive.Linq;
+using System.Text;
+using Avalonia.Data;
+using Xunit;
+
+namespace Avalonia.Base.UnitTests
+{
+    public class AvaloniaObjectTests_BatchUpdate
+    {
+        [Fact]
+        public void SetValue_Should_Not_Raise_Property_Changes_During_Batch_Update()
+        {
+            var target = new TestClass();
+            var raised = new List<string>();
+
+            target.GetObservable(TestClass.FooProperty).Skip(1).Subscribe(x => raised.Add(x));
+            target.BeginBatchUpdate();
+            target.SetValue(TestClass.FooProperty, "foo", BindingPriority.LocalValue);
+
+            Assert.Empty(raised);
+        }
+
+        [Fact]
+        public void Binding_Should_Not_Raise_Property_Changes_During_Batch_Update()
+        {
+            var target = new TestClass();
+            var observable = new TestObservable<string>("foo");
+            var raised = new List<string>();
+
+            target.GetObservable(TestClass.FooProperty).Skip(1).Subscribe(x => raised.Add(x));
+            target.BeginBatchUpdate();
+            target.Bind(TestClass.FooProperty, observable, BindingPriority.LocalValue);
+
+            Assert.Empty(raised);
+        }
+
+        [Fact]
+        public void SetValue_Change_Should_Be_Raised_After_Batch_Update_1()
+        {
+            var target = new TestClass();
+            var raised = new List<AvaloniaPropertyChangedEventArgs>();
+
+            target.PropertyChanged += (s, e) => raised.Add(e);
+
+            target.BeginBatchUpdate();
+            target.SetValue(TestClass.FooProperty, "foo", BindingPriority.LocalValue);
+            target.EndBatchUpdate();
+
+            Assert.Equal(1, raised.Count);
+            Assert.Equal("foo", target.Foo);
+            Assert.Null(raised[0].OldValue);
+            Assert.Equal("foo", raised[0].NewValue);
+        }
+
+        [Fact]
+        public void SetValue_Change_Should_Be_Raised_After_Batch_Update_2()
+        {
+            var target = new TestClass();
+            var raised = new List<AvaloniaPropertyChangedEventArgs>();
+
+            target.SetValue(TestClass.FooProperty, "foo", BindingPriority.LocalValue);
+            target.PropertyChanged += (s, e) => raised.Add(e);
+
+            target.BeginBatchUpdate();
+            target.SetValue(TestClass.FooProperty, "bar", BindingPriority.LocalValue);
+            target.SetValue(TestClass.FooProperty, "baz", BindingPriority.LocalValue);
+            target.EndBatchUpdate();
+
+            Assert.Equal(1, raised.Count);
+            Assert.Equal("baz", target.Foo);
+        }
+
+        [Fact]
+        public void SetValue_Changes_Should_Be_Raised_In_Correct_Order_After_Batch_Update()
+        {
+            var target = new TestClass();
+            var raised = new List<AvaloniaPropertyChangedEventArgs>();
+
+            target.PropertyChanged += (s, e) => raised.Add(e);
+
+            target.BeginBatchUpdate();
+            target.SetValue(TestClass.FooProperty, "foo", BindingPriority.LocalValue);
+            target.SetValue(TestClass.BarProperty, "bar", BindingPriority.LocalValue);
+            target.SetValue(TestClass.FooProperty, "baz", BindingPriority.LocalValue);
+            target.EndBatchUpdate();
+
+            Assert.Equal(2, raised.Count);
+            Assert.Equal(TestClass.BarProperty, raised[0].Property);
+            Assert.Equal(TestClass.FooProperty, raised[1].Property);
+            Assert.Equal("baz", target.Foo);
+            Assert.Equal("bar", target.Bar);
+        }
+
+        [Fact]
+        public void SetValue_And_Binding_Changes_Should_Be_Raised_In_Correct_Order_After_Batch_Update_1()
+        {
+            var target = new TestClass();
+            var observable = new TestObservable<string>("baz");
+            var raised = new List<AvaloniaPropertyChangedEventArgs>();
+
+            target.PropertyChanged += (s, e) => raised.Add(e);
+
+            target.BeginBatchUpdate();
+            target.SetValue(TestClass.FooProperty, "foo", BindingPriority.LocalValue);
+            target.SetValue(TestClass.BarProperty, "bar", BindingPriority.LocalValue);
+            target.Bind(TestClass.FooProperty, observable, BindingPriority.LocalValue);
+            target.EndBatchUpdate();
+
+            Assert.Equal(2, raised.Count);
+            Assert.Equal(TestClass.BarProperty, raised[0].Property);
+            Assert.Equal(TestClass.FooProperty, raised[1].Property);
+            Assert.Equal("baz", target.Foo);
+            Assert.Equal("bar", target.Bar);
+        }
+
+        [Fact]
+        public void SetValue_And_Binding_Changes_Should_Be_Raised_In_Correct_Order_After_Batch_Update_2()
+        {
+            var target = new TestClass();
+            var observable = new TestObservable<string>("foo");
+            var raised = new List<AvaloniaPropertyChangedEventArgs>();
+
+            target.PropertyChanged += (s, e) => raised.Add(e);
+
+            target.BeginBatchUpdate();
+            target.Bind(TestClass.FooProperty, observable, BindingPriority.LocalValue);
+            target.SetValue(TestClass.BarProperty, "bar", BindingPriority.LocalValue);
+            target.SetValue(TestClass.FooProperty, "baz", BindingPriority.LocalValue);
+            target.EndBatchUpdate();
+
+            Assert.Equal(2, raised.Count);
+            Assert.Equal(TestClass.BarProperty, raised[0].Property);
+            Assert.Equal(TestClass.FooProperty, raised[1].Property);
+            Assert.Equal("baz", target.Foo);
+            Assert.Equal("bar", target.Bar);
+        }
+
+        [Fact]
+        public void SetValue_And_Binding_Changes_Should_Be_Raised_In_Correct_Order_After_Batch_Update_3()
+        {
+            var target = new TestClass();
+            var observable1 = new TestObservable<string>("foo");
+            var observable2 = new TestObservable<string>("qux");
+            var raised = new List<AvaloniaPropertyChangedEventArgs>();
+
+            target.PropertyChanged += (s, e) => raised.Add(e);
+
+            target.BeginBatchUpdate();
+            target.Bind(TestClass.FooProperty, observable2, BindingPriority.LocalValue);
+            target.Bind(TestClass.FooProperty, observable1, BindingPriority.LocalValue);
+            target.SetValue(TestClass.BarProperty, "bar", BindingPriority.LocalValue);
+            target.SetValue(TestClass.FooProperty, "baz", BindingPriority.LocalValue);
+            target.EndBatchUpdate();
+
+            Assert.Equal(2, raised.Count);
+            Assert.Equal(TestClass.BarProperty, raised[0].Property);
+            Assert.Equal(TestClass.FooProperty, raised[1].Property);
+            Assert.Equal("baz", target.Foo);
+            Assert.Equal("bar", target.Bar);
+        }
+
+        [Fact]
+        public void Binding_Change_Should_Be_Raised_After_Batch_Update_1()
+        {
+            var target = new TestClass();
+            var observable = new TestObservable<string>("foo");
+            var raised = new List<AvaloniaPropertyChangedEventArgs>();
+
+            target.PropertyChanged += (s, e) => raised.Add(e);
+
+            target.BeginBatchUpdate();
+            target.Bind(TestClass.FooProperty, observable, BindingPriority.LocalValue);
+            target.EndBatchUpdate();
+
+            Assert.Equal(1, raised.Count);
+            Assert.Equal("foo", target.Foo);
+            Assert.Null(raised[0].OldValue);
+            Assert.Equal("foo", raised[0].NewValue);
+        }
+
+        [Fact]
+        public void Binding_Change_Should_Be_Raised_After_Batch_Update_2()
+        {
+            var target = new TestClass();
+            var observable1 = new TestObservable<string>("bar");
+            var observable2 = new TestObservable<string>("baz");
+            var raised = new List<AvaloniaPropertyChangedEventArgs>();
+
+            target.SetValue(TestClass.FooProperty, "foo", BindingPriority.LocalValue);
+            target.PropertyChanged += (s, e) => raised.Add(e);
+
+            target.BeginBatchUpdate();
+            target.Bind(TestClass.FooProperty, observable1, BindingPriority.LocalValue);
+            target.Bind(TestClass.FooProperty, observable2, BindingPriority.LocalValue);
+            target.EndBatchUpdate();
+
+            Assert.Equal(1, raised.Count);
+            Assert.Equal("baz", target.Foo);
+            Assert.Equal("foo", raised[0].OldValue);
+            Assert.Equal("baz", raised[0].NewValue);
+        }
+
+        [Fact]
+        public void Bindings_Should_Be_Subscribed_Before_Batch_Update()
+        {
+            var target = new TestClass();
+            var observable1 = new TestObservable<string>("foo");
+            var observable2 = new TestObservable<string>("bar");
+
+            target.Bind(TestClass.FooProperty, observable1, BindingPriority.LocalValue);
+            target.Bind(TestClass.FooProperty, observable2, BindingPriority.LocalValue);
+
+            Assert.Equal(1, observable1.SubscribeCount);
+            Assert.Equal(1, observable2.SubscribeCount);
+        }
+
+        [Fact]
+        public void Non_Active_Binding_Should_Not_Be_Subscribed_Before_Batch_Update()
+        {
+            var target = new TestClass();
+            var observable1 = new TestObservable<string>("foo");
+            var observable2 = new TestObservable<string>("bar");
+
+            target.Bind(TestClass.FooProperty, observable1, BindingPriority.LocalValue);
+            target.Bind(TestClass.FooProperty, observable2, BindingPriority.Style);
+
+            Assert.Equal(1, observable1.SubscribeCount);
+            Assert.Equal(0, observable2.SubscribeCount);
+        }
+
+        [Fact]
+        public void Bindings_Should_Not_Be_Subscribed_During_Batch_Update()
+        {
+            var target = new TestClass();
+            var observable1 = new TestObservable<string>("foo");
+            var observable2 = new TestObservable<string>("bar");
+            var observable3 = new TestObservable<string>("baz");
+
+            target.BeginBatchUpdate();
+            target.Bind(TestClass.FooProperty, observable1, BindingPriority.LocalValue);
+            target.Bind(TestClass.FooProperty, observable2, BindingPriority.LocalValue);
+            target.Bind(TestClass.FooProperty, observable3, BindingPriority.Style);
+
+            Assert.Equal(0, observable1.SubscribeCount);
+            Assert.Equal(0, observable2.SubscribeCount);
+            Assert.Equal(0, observable3.SubscribeCount);
+        }
+
+        [Fact]
+        public void Active_Binding_Should_Be_Subscribed_After_Batch_Uppdate()
+        {
+            var target = new TestClass();
+            var observable1 = new TestObservable<string>("foo");
+            var observable2 = new TestObservable<string>("bar");
+            var observable3 = new TestObservable<string>("baz");
+
+            target.BeginBatchUpdate();
+            target.Bind(TestClass.FooProperty, observable1, BindingPriority.LocalValue);
+            target.Bind(TestClass.FooProperty, observable2, BindingPriority.LocalValue);
+            target.Bind(TestClass.FooProperty, observable3, BindingPriority.Style);
+            target.EndBatchUpdate();
+
+            Assert.Equal(0, observable1.SubscribeCount);
+            Assert.Equal(1, observable2.SubscribeCount);
+            Assert.Equal(0, observable3.SubscribeCount);
+        }
+
+        public class TestClass : AvaloniaObject
+        {
+            public static readonly StyledProperty<string> FooProperty =
+                AvaloniaProperty.Register<TestClass, string>(nameof(Foo));
+
+            public static readonly StyledProperty<string> BarProperty =
+                AvaloniaProperty.Register<TestClass, string>(nameof(Bar));
+
+            public string Foo
+            {
+                get => GetValue(FooProperty);
+                set => SetValue(FooProperty, value);
+            }
+
+            public string Bar
+            {
+                get => GetValue(BarProperty);
+                set => SetValue(BarProperty, value);
+            }
+        }
+
+        public class TestObservable<T> : ObservableBase<BindingValue<T>>
+        {
+            private readonly T _value;
+
+            public TestObservable(T value) => _value = value;
+
+            public int SubscribeCount { get; private set; }
+
+            protected override IDisposable SubscribeCore(IObserver<BindingValue<T>> observer)
+            {
+                ++SubscribeCount;
+                observer.OnNext(_value);
+                return Disposable.Empty;
+            }
+        }
+    }
+}

+ 162 - 0
tests/Avalonia.Base.UnitTests/ValueStoreTests_BatchUpdate.cs

@@ -0,0 +1,162 @@
+using System;
+using System.Collections.Generic;
+using System.Reactive;
+using System.Reactive.Disposables;
+using System.Reactive.Linq;
+using Avalonia.Controls;
+using Avalonia.Data;
+using Xunit;
+
+#nullable enable
+
+namespace Avalonia.Base.UnitTests
+{
+    public class ValueStoreTests_BatchUpdate
+    {
+        ////[Fact]
+        ////public void SetValue_Should_Not_Raise_Property_Changes_During_Batch_Update()
+        ////{
+        ////    var o = new TestClass();
+        ////    var target = CreateTarget(o);
+        ////    var raised = new List<string>();
+
+        ////    o.GetObservable(TestClass.FooProperty).Skip(1).Subscribe(x => raised.Add(x));
+        ////    target.BeginBatchUpdate();
+        ////    target.SetValue(TestClass.FooProperty, "foo", BindingPriority.LocalValue);
+
+        ////    Assert.Empty(raised);
+        ////}
+
+        ////[Fact]
+        ////public void AddBinding_Should_Not_Raise_Property_Changes_During_Batch_Update()
+        ////{
+        ////    var o = new TestClass();
+        ////    var target = CreateTarget(o);
+        ////    var observable = new TestObservable<string>("foo");
+        ////    var raised = new List<string>();
+
+        ////    o.GetObservable(TestClass.FooProperty).Skip(1).Subscribe(x => raised.Add(x));
+        ////    target.BeginBatchUpdate();
+        ////    target.AddBinding(TestClass.FooProperty, observable, BindingPriority.LocalValue);
+
+        ////    Assert.Empty(raised);
+        ////}
+
+        ////[Fact]
+        ////public void SetValue_Change_Should_Be_Raised_After_Batch_Update()
+        ////{
+        ////    var o = new TestClass();
+        ////    var target = CreateTarget(o);
+        ////    var raised = new List<AvaloniaPropertyChangedEventArgs>();
+
+        ////    target.SetValue(TestClass.FooProperty, "foo", BindingPriority.LocalValue);
+        ////    o.PropertyChanged += (s, e) => raised.Add(e);
+
+        ////    target.BeginBatchUpdate();
+        ////    target.SetValue(TestClass.FooProperty, "bar", BindingPriority.LocalValue);
+        ////    target.SetValue(TestClass.FooProperty, "baz", BindingPriority.LocalValue);
+        ////    target.EndBatchUpdate();
+
+        ////    Assert.Equal(1, raised.Count);
+        ////}
+
+        ////[Fact]
+        ////public void Bindings_Should_Be_Subscribed_Before_Batch_Update()
+        ////{
+        ////    var target = CreateTarget();
+        ////    var observable1 = new TestObservable<string>("foo");
+        ////    var observable2 = new TestObservable<string>("bar");
+
+        ////    target.AddBinding(Window.TitleProperty, observable1, BindingPriority.LocalValue);
+        ////    target.AddBinding(Window.TitleProperty, observable2, BindingPriority.LocalValue);
+
+        ////    Assert.Equal(1, observable1.SubscribeCount);
+        ////    Assert.Equal(1, observable2.SubscribeCount);
+        ////}
+
+        ////[Fact]
+        ////public void Non_Active_Binding_Should_Not_Be_Subscribed_Before_Batch_Update()
+        ////{
+        ////    var target = CreateTarget();
+        ////    var observable1 = new TestObservable<string>("foo");
+        ////    var observable2 = new TestObservable<string>("bar");
+
+        ////    target.AddBinding(Window.TitleProperty, observable1, BindingPriority.LocalValue);
+        ////    target.AddBinding(Window.TitleProperty, observable2, BindingPriority.Style);
+
+        ////    Assert.Equal(1, observable1.SubscribeCount);
+        ////    Assert.Equal(0, observable2.SubscribeCount);
+        ////}
+
+        ////[Fact]
+        ////public void Bindings_Should_Not_Be_Subscribed_During_Batch_Update()
+        ////{
+        ////    var target = CreateTarget();
+        ////    var observable1 = new TestObservable<string>("foo");
+        ////    var observable2 = new TestObservable<string>("bar");
+        ////    var observable3 = new TestObservable<string>("baz");
+
+        ////    target.BeginBatchUpdate();
+        ////    target.AddBinding(Window.TitleProperty, observable1, BindingPriority.LocalValue);
+        ////    target.AddBinding(Window.TitleProperty, observable2, BindingPriority.LocalValue);
+        ////    target.AddBinding(Window.TitleProperty, observable3, BindingPriority.Style);
+
+        ////    Assert.Equal(0, observable1.SubscribeCount);
+        ////    Assert.Equal(0, observable2.SubscribeCount);
+        ////    Assert.Equal(0, observable3.SubscribeCount);
+        ////}
+
+        ////[Fact]
+        ////public void Active_Binding_Should_Be_Subscribed_After_Batch_Uppdate()
+        ////{
+        ////    var target = CreateTarget();
+        ////    var observable1 = new TestObservable<string>("foo");
+        ////    var observable2 = new TestObservable<string>("bar");
+        ////    var observable3 = new TestObservable<string>("baz");
+
+        ////    target.BeginBatchUpdate();
+        ////    target.AddBinding(Window.TitleProperty, observable1, BindingPriority.LocalValue);
+        ////    target.AddBinding(Window.TitleProperty, observable2, BindingPriority.LocalValue);
+        ////    target.AddBinding(Window.TitleProperty, observable3, BindingPriority.Style);
+        ////    target.EndBatchUpdate();
+
+        ////    Assert.Equal(0, observable1.SubscribeCount);
+        ////    Assert.Equal(1, observable2.SubscribeCount);
+        ////    Assert.Equal(0, observable3.SubscribeCount);
+        ////}
+
+        ////private ValueStore CreateTarget(AvaloniaObject? o = null)
+        ////{
+        ////    o ??= new TestClass();
+        ////    return o.Values;
+        ////}
+
+        ////public class TestClass : AvaloniaObject
+        ////{
+        ////    public static readonly StyledProperty<string> FooProperty =
+        ////        AvaloniaProperty.Register<TestClass, string>(nameof(Foo));
+
+        ////    public string Foo
+        ////    {
+        ////        get => GetValue(FooProperty);
+        ////        set => SetValue(FooProperty, value);
+        ////    }
+        ////}
+
+        ////public class TestObservable<T> : ObservableBase<BindingValue<T>>
+        ////{
+        ////    private readonly T _value;
+            
+        ////    public TestObservable(T value) => _value = value;
+
+        ////    public int SubscribeCount { get; private set; }
+
+        ////    protected override IDisposable SubscribeCore(IObserver<BindingValue<T>> observer)
+        ////    {
+        ////        ++SubscribeCount;
+        ////        observer.OnNext(_value);
+        ////        return Disposable.Empty;
+        ////    }
+        ////}
+    }
+}

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

@@ -482,9 +482,9 @@ namespace Avalonia.Styling.UnitTests
             };
 
             var target = new Class1();
-            target.BeginInit();
+            target.BeginBatchUpdate();
             styles.TryAttach(target, null);
-            target.EndInit();
+            target.EndBatchUpdate();
 
             Assert.NotNull(target.Child);
             Assert.Equal(1, instantiationCount);