@ -1,4 +1,6 @@
using System ;
using System.Collections.Generic ;
using System.Diagnostics.CodeAnalysis ;
using Avalonia.Data ;
using Avalonia.PropertyStore ;
using Avalonia.Utilities ;
@ -26,6 +28,7 @@ namespace Avalonia
private readonly AvaloniaObject _ owner ;
private readonly IValueSink _ sink ;
private readonly AvaloniaPropertyValueStore < IValue > _ values ;
private BatchUpdate ? _ batchUpdate ;
public ValueStore ( AvaloniaObject owner )
{
@ -33,9 +36,28 @@ namespace Avalonia
_ values = new AvaloniaPropertyValueStore < IValue > ( ) ;
}
public void BeginBatchUpdate ( )
{
_ batchUpdate ? ? = new BatchUpdate ( this ) ;
_ batchUpdate . Begin ( ) ;
}
public void EndBatchUpdate ( )
{
if ( _ batchUpdate is null )
{
throw new InvalidOperationException ( "No batch update in progress." ) ;
}
if ( _ batchUpdate . End ( ) )
{
_ batchUpdate = null ;
}
}
public bool IsAnimating ( AvaloniaProperty property )
{
if ( _ values . TryGetValue ( property , out var slot ) )
if ( TryGetValue ( property , out var slot ) )
{
return slot . Priority < BindingPriority . LocalValue ;
}
@ -45,7 +67,7 @@ namespace Avalonia
public bool IsSet ( AvaloniaProperty property )
{
if ( _ values . TryGetValue ( property , out var slot ) )
if ( TryGetValue ( property , out var slot ) )
{
return slot . GetValue ( ) . HasValue ;
}
@ -58,7 +80,7 @@ namespace Avalonia
BindingPriority maxPriority ,
out T value )
{
if ( _ values . TryGetValue ( property , out var slot ) )
if ( TryGetValue ( property , out var slot ) )
{
var v = ( ( IValue < T > ) slot ) . GetValue ( maxPriority ) ;
@ -82,7 +104,7 @@ namespace Avalonia
IDisposable ? result = null ;
if ( _ values . TryGetValue ( property , out var slot ) )
if ( TryGetValue ( property , out var slot ) )
{
result = SetExisting ( slot , property , value , priority ) ;
}
@ -90,23 +112,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 ;
}
}
@ -119,7 +139,7 @@ namespace Avalonia
IObservable < BindingValue < T > > source ,
BindingPriority priority )
{
if ( _ values . TryGetValue ( property , out var slot ) )
if ( TryGetValue ( property , out var slot ) )
{
return BindExisting ( slot , property , source , priority ) ;
}
@ -128,62 +148,69 @@ 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 ;
}
}
public void ClearLocalValue < T > ( StyledPropertyBase < T > property )
{
if ( _ values . TryGetValue ( property , out var slot ) )
if ( TryGetValue ( property , out var slot ) )
{
if ( slot is PriorityValue < T > p )
{
p . ClearLocalValue ( ) ;
}
else
else if ( slot . Priority = = BindingPriority . LocalValue )
{
var remove = slot is ConstantValueEntry < T > c ?
c . Priority = = BindingPriority . LocalValue :
! ( slot is IPriorityValueEntry < T > ) ;
var old = TryGetValue ( property , BindingPriority . LocalValue , out var value ) ? value : default ;
if ( remove )
// During batch update values can't be removed immediately because they're needed to raise
// a correctly-typed _sink.ValueChanged notification. They instead mark themselves for removal
// by setting their priority to Unset.
if ( ! IsBatchUpdating ( ) )
{
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 ) ) ;
}
else if ( slot is IDisposable d )
{
d . Dispose ( ) ;
}
else
{
// Local value entries are optimized and contain only a single value field to save space,
// so there's no way to mark them for removal at the end of a batch update. Instead convert
// them to a constant value entry with Unset priority in the event of a local value being
// cleared during a batch update.
var sentinel = new ConstantValueEntry < T > ( property , default , BindingPriority . Unset , _ sink ) ;
_ values . SetValue ( property , sentinel ) ;
}
NotifyValueChanged < T > ( property , old , default , BindingPriority . Unset ) ;
}
}
}
public void CoerceValue < T > ( StyledPropertyBase < T > property )
{
if ( _ values . TryGetValue ( property , out var slot ) )
if ( TryGetValue ( property , out var slot ) )
{
if ( slot is PriorityValue < T > p )
{
p . CoerceValue ( ) ;
p . UpdateEffectiv eValue( ) ;
}
}
}
public Diagnostics . AvaloniaPropertyValue ? GetDiagnostic ( AvaloniaProperty property )
{
if ( _ values . TryGetValue ( property , out var slot ) )
if ( TryGetValue ( property , out var slot ) )
{
var slotValue = slot . GetValue ( ) ;
return new Diagnostics . AvaloniaPropertyValue (
@ -198,7 +225,17 @@ namespace Avalonia
void IValueSink . ValueChanged < T > ( AvaloniaPropertyChangedEventArgs < T > change )
{
_ sink . ValueChanged ( change ) ;
if ( _ batchUpdate is object )
{
if ( change . IsEffectiveValueChange )
{
NotifyValueChanged < T > ( change . Property , change . OldValue , change . NewValue , change . Priority ) ;
}
}
else
{
_ sink . ValueChanged ( change ) ;
}
}
void IValueSink . Completed < T > (
@ -206,13 +243,18 @@ namespace Avalonia
IPriorityValueEntry entry ,
Optional < T > oldValue )
{
if ( _ values . TryGetValue ( property , out var slot ) )
// We need to include remove sentinels here so call `_values.TryGetValue` directly.
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
{
_ batchUpdate . ValueChanged ( property , oldValue . ToObject ( ) ) ;
}
}
}
@ -240,16 +282,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 ( IsBatchUpdating ( ) )
priorityValue . BeginBatchUpdate ( ) ;
result = priorityValue . SetValue ( value , priority ) ;
_ values . SetValue ( property , priorityValue ) ;
}
@ -273,6 +312,11 @@ namespace Avalonia
if ( slot is IPriorityValueEntry < T > e )
{
priorityValue = new PriorityValue < T > ( _ owner , property , this , e ) ;
if ( IsBatchUpdating ( ) )
{
priorityValue . BeginBatchUpdate ( ) ;
}
}
else if ( slot is PriorityValue < T > p )
{
@ -289,8 +333,181 @@ 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 ( IsBatchUpdating ( ) & & 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 null )
{
_ sink . ValueChanged ( new AvaloniaPropertyChangedEventArgs < T > (
_ owner ,
property ,
oldValue ,
newValue ,
priority ) ) ;
}
else
{
_ batchUpdate . ValueChanged ( property , oldValue . ToObject ( ) ) ;
}
}
private bool IsBatchUpdating ( ) = > _ batchUpdate ? . IsBatchUpdating = = true ;
private bool TryGetValue ( AvaloniaProperty property , [ MaybeNullWhen ( false ) ] out IValue value )
{
return _ values . TryGetValue ( property , out value ) & & ! IsRemoveSentinel ( value ) ;
}
private static bool IsRemoveSentinel ( IValue value )
{
// Local value entries are optimized and contain only a single value field to save space,
// so there's no way to mark them for removal at the end of a batch update. Instead a
// ConstantValueEntry with a priority of Unset is used as a sentinel value.
return value is IConstantValueEntry t & & t . Priority = = BindingPriority . Unset ;
}
private class BatchUpdate
{
private ValueStore _ owner ;
private List < Notification > ? _ notifications ;
private int _ batchUpdateCount ;
private int _ iterator = - 1 ;
public BatchUpdate ( ValueStore owner ) = > _ owner = owner ;
public bool IsBatchUpdating = > _ batchUpdateCount > 0 ;
public void Begin ( )
{
if ( _ batchUpdateCount + + = = 0 )
{
var values = _ owner . _ values ;
for ( var i = 0 ; i < values . Count ; + + i )
{
( values [ i ] as IBatchUpdate ) ? . BeginBatchUpdate ( ) ;
}
}
}
public bool End ( )
{
if ( - - _ batchUpdateCount > 0 )
return false ;
var values = _ owner . _ values ;
// First call EndBatchUpdate on all bindings. This should cause the active binding to be subscribed
// but notifications will still not be raised because the owner ValueStore will still have a reference
// to this batch update object.
for ( var i = 0 ; i < values . Count ; + + i )
{
( values [ i ] as IBatchUpdate ) ? . EndBatchUpdate ( ) ;
// Somehow subscribing to a binding caused a new batch update. This shouldn't happen but in case it
// does, abort and continue batch updating.
if ( _ batchUpdateCount > 0 )
return false ;
}
if ( _ notifications is object )
{
// Raise all batched notifications. Doing this can cause other notifications to be added and even
// cause a new batch update to start, so we need to handle _notifications being modified by storing
// the index in field.
_ iterator = 0 ;
for ( ; _ iterator < _ notifications . Count ; + + _ iterator )
{
var entry = _ notifications [ _ iterator ] ;
if ( values . TryGetValue ( entry . property , out var slot ) )
{
var oldValue = entry . oldValue ;
var newValue = slot . GetValue ( ) ;
// Raising this notification can cause a new batch update to be started, which in turn
// results in another change to the property. In this case we need to update the old value
// so that the *next* notification has an oldValue which follows on from the newValue
// raised here.
_ notifications [ _ iterator ] = new Notification
{
property = entry . property ,
oldValue = newValue ,
} ;
// Call _sink.ValueChanged with an appropriately typed AvaloniaPropertyChangedEventArgs<T>.
slot . RaiseValueChanged ( _ owner . _ sink , _ owner . _ owner , entry . property , oldValue , newValue ) ;
// During batch update values can't be removed immediately because they're needed to raise
// the _sink.ValueChanged notification. They instead mark themselves for removal by setting
// their priority to Unset. We need to re-read the slot here because raising ValueChanged
// could have caused it to be updated.
if ( values . TryGetValue ( entry . property , out var updatedSlot ) & &
updatedSlot . Priority = = BindingPriority . Unset )
{
values . Remove ( entry . property ) ;
}
}
else
{
throw new AvaloniaInternalException ( "Value could not be found at the end of batch update." ) ;
}
// If a new batch update was started while ending this one, abort.
if ( _ batchUpdateCount > 0 )
return false ;
}
}
_ iterator = int . MaxValue - 1 ;
return true ;
}
public void ValueChanged ( AvaloniaProperty property , Optional < object > oldValue )
{
_ notifications ? ? = new List < Notification > ( ) ;
for ( var i = 0 ; i < _ notifications . Count ; + + i )
{
if ( _ notifications [ i ] . property = = property )
{
oldValue = _ notifications [ i ] . oldValue ;
_ notifications . RemoveAt ( i ) ;
if ( i < = _ iterator )
- - _ iterator ;
break ;
}
}
_ notifications . Add ( new Notification
{
property = property ,
oldValue = oldValue ,
} ) ;
}
private struct Notification
{
public AvaloniaProperty property ;
public Optional < object > oldValue ;
}
}
}
}