Browse Source

Merge branch 'master' into feature/url-handling

pull/5639/head
Dan Walmsley 5 years ago
committed by GitHub
parent
commit
64a3a62a5e
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 40
      src/Avalonia.Base/AvaloniaObject.cs
  2. 57
      src/Avalonia.Base/PropertyStore/BindingEntry.cs
  3. 30
      src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs
  4. 8
      src/Avalonia.Base/PropertyStore/IBatchUpdate.cs
  5. 9
      src/Avalonia.Base/PropertyStore/IValue.cs
  6. 16
      src/Avalonia.Base/PropertyStore/LocalValueEntry.cs
  7. 121
      src/Avalonia.Base/PropertyStore/PriorityValue.cs
  8. 17
      src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs
  9. 264
      src/Avalonia.Base/ValueStore.cs
  10. 2
      src/Avalonia.Controls/Control.cs
  11. 4
      src/Avalonia.Styling/IStyledElement.cs
  12. 28
      src/Avalonia.Styling/StyledElement.cs
  13. 18
      src/Avalonia.Styling/Styling/PropertySetterBindingInstance.cs
  14. 2
      src/Avalonia.Styling/Styling/PropertySetterInstance.cs
  15. 128
      src/Avalonia.Styling/Styling/PropertySetterLazyInstance.cs
  16. 24
      src/Avalonia.Styling/Styling/Setter.cs
  17. 1
      tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj
  18. 494
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs
  19. 1
      tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj
  20. 74
      tests/Avalonia.Benchmarks/Themes/FluentBenchmark.cs
  21. 1
      tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj
  22. 96
      tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs
  23. 1
      tests/Avalonia.Styling.UnitTests/Avalonia.Styling.UnitTests.csproj
  24. 282
      tests/Avalonia.Styling.UnitTests/StyleTests.cs
  25. 2
      tests/Avalonia.UnitTests/UnitTestApplication.cs

40
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)
{

57
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(
@ -39,10 +42,20 @@ 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();
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,10 +65,17 @@ namespace Avalonia.PropertyStore
{
_subscription?.Dispose();
_subscription = null;
_sink.Completed(Property, this, _value);
_isSubscribed = false;
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)
{
@ -79,13 +99,36 @@ 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,
Optional<object> newValue)
{
sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
owner,
(AvaloniaProperty<T>)property,
oldValue.GetValueOrDefault<T>(),
newValue.GetValueOrDefault<T>(),
Priority));
}
private void UpdateValue(BindingValue<T> value)
{
if (value.HasValue && Property.ValidateValue?.Invoke(value.Value) == false)

30
src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs

@ -1,4 +1,5 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Data;
#nullable enable
@ -17,7 +18,7 @@ namespace Avalonia.PropertyStore
public ConstantValueEntry(
StyledPropertyBase<T> property,
T value,
[AllowNull] T value,
BindingPriority priority,
IValueSink sink)
{
@ -28,7 +29,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 +37,30 @@ 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() { }
public void RaiseValueChanged(
IValueSink sink,
IAvaloniaObject owner,
AvaloniaProperty property,
Optional<object> oldValue,
Optional<object> newValue)
{
sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
owner,
(AvaloniaProperty<T>)property,
oldValue.GetValueOrDefault<T>(),
newValue.GetValueOrDefault<T>(),
Priority));
}
}
}

8
src/Avalonia.Base/PropertyStore/IBatchUpdate.cs

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

9
src/Avalonia.Base/PropertyStore/IValue.cs

@ -9,8 +9,15 @@ 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,
Optional<object> newValue);
}
/// <summary>

16
src/Avalonia.Base/PropertyStore/LocalValueEntry.cs

@ -24,5 +24,21 @@ 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,
Optional<object> newValue)
{
sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
owner,
(AvaloniaProperty<T>)property,
oldValue.GetValueOrDefault<T>(),
newValue.GetValueOrDefault<T>(),
BindingPriority.LocalValue));
}
}
}

121
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>(
@ -134,10 +170,37 @@ namespace Avalonia.PropertyStore
var binding = new BindingEntry<T>(_owner, Property, source, priority, this);
var insert = FindInsertPoint(binding.Priority);
_entries.Insert(insert, binding);
if (_batchUpdate)
{
binding.BeginBatchUpdate();
if (priority == BindingPriority.LocalValue)
{
binding.Start(ignoreBatchUpdate: true);
}
}
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,
Optional<object> newValue)
{
sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
owner,
(AvaloniaProperty<T>)property,
oldValue.GetValueOrDefault<T>(),
newValue.GetValueOrDefault<T>(),
Priority));
}
void IValueSink.ValueChanged<TValue>(AvaloniaPropertyChangedEventArgs<TValue> change)
{
@ -146,7 +209,7 @@ namespace Avalonia.PropertyStore
_localValue = default;
}
if (change is AvaloniaPropertyChangedEventArgs<T> c)
if (!_isCalculatingValue && change is AvaloniaPropertyChangedEventArgs<T> c)
{
UpdateEffectiveValue(c);
}
@ -188,41 +251,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)

17
src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
@ -22,6 +22,9 @@ namespace Avalonia.Utilities
_entries = s_emptyEntries;
}
public int Count => _entries.Length - 1;
public TValue this[int index] => _entries[index].Value;
private (int, bool) TryFindEntry(int propertyId)
{
if (_entries.Length <= 12)
@ -163,18 +166,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;

264
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 BatchUpdate? _batchUpdate;
public ValueStore(AvaloniaObject owner)
{
@ -33,6 +35,25 @@ 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))
@ -90,23 +111,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 +147,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;
}
}
@ -149,23 +166,32 @@ namespace Avalonia
{
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 (_batchUpdate is null)
{
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);
}
}
}
@ -176,7 +202,7 @@ namespace Avalonia
{
if (slot is PriorityValue<T> p)
{
p.CoerceValue();
p.UpdateEffectiveValue();
}
}
}
@ -198,7 +224,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 +242,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
{
_batchUpdate.ValueChanged(property, oldValue.ToObject());
}
}
}
@ -240,16 +280,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);
}
@ -273,6 +310,11 @@ namespace Avalonia
if (slot is IPriorityValueEntry<T> e)
{
priorityValue = new PriorityValue<T>(_owner, property, this, e);
if (_batchUpdate is object)
{
priorityValue.BeginBatchUpdate();
}
}
else if (slot is PriorityValue<T> p)
{
@ -289,8 +331,162 @@ 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 null)
{
_sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
_owner,
property,
oldValue,
newValue,
priority));
}
else
{
_batchUpdate.ValueChanged(property, oldValue.ToObject());
}
}
private class BatchUpdate
{
private ValueStore _owner;
private List<Notification>? _notifications;
private int _batchUpdateCount;
private int _iterator = -1;
public BatchUpdate(ValueStore owner) => _owner = owner;
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.
if (slot.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;
}
}
}
}

2
src/Avalonia.Controls/Control.cs

@ -20,7 +20,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.

4
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.

28
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;
}
@ -748,12 +757,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;

18
src/Avalonia.Styling/Styling/PropertySetterBindingInstance.cs

@ -92,6 +92,7 @@ namespace Avalonia.Styling
{
if (!_isActive)
{
_innerSubscription ??= _binding.Observable.Subscribe(_inner);
_isActive = true;
PublishNext();
}
@ -102,6 +103,8 @@ namespace Avalonia.Styling
if (_isActive)
{
_isActive = false;
_innerSubscription?.Dispose();
_innerSubscription = null;
PublishNext();
}
}
@ -122,9 +125,6 @@ namespace Avalonia.Styling
sub.Dispose();
}
_innerSubscription?.Dispose();
_innerSubscription = null;
base.Dispose();
}
@ -148,7 +148,17 @@ namespace Avalonia.Styling
protected override void Subscribed()
{
_innerSubscription = _binding.Observable.Subscribe(_inner);
if (_isActive)
{
if (_innerSubscription is null)
{
_innerSubscription ??= _binding.Observable.Subscribe(_inner);
}
else
{
PublishNext();
}
}
}
protected override void Unsubscribed()

2
src/Avalonia.Styling/Styling/PropertySetterInstance.cs

@ -7,7 +7,7 @@ using Avalonia.Reactive;
namespace Avalonia.Styling
{
/// <summary>
/// A <see cref="Setter"/> which has been instance on a control.
/// A <see cref="Setter"/> which has been instanced on a control.
/// </summary>
/// <typeparam name="T">The target property type.</typeparam>
internal class PropertySetterInstance<T> : SingleSubscriberObservableBase<BindingValue<T>>,

128
src/Avalonia.Styling/Styling/PropertySetterLazyInstance.cs

@ -0,0 +1,128 @@
using System;
using Avalonia.Data;
using Avalonia.Reactive;
#nullable enable
namespace Avalonia.Styling
{
/// <summary>
/// A <see cref="Setter"/> which has been instanced on a control and whose value is lazily
/// evaluated.
/// </summary>
/// <typeparam name="T">The target property type.</typeparam>
internal class PropertySetterLazyInstance<T> : SingleSubscriberObservableBase<BindingValue<T>>,
ISetterInstance
{
private readonly IStyleable _target;
private readonly StyledPropertyBase<T>? _styledProperty;
private readonly DirectPropertyBase<T>? _directProperty;
private readonly Func<T> _valueFactory;
private BindingValue<T> _value;
private IDisposable? _subscription;
private bool _isActive;
public PropertySetterLazyInstance(
IStyleable target,
StyledPropertyBase<T> property,
Func<T> valueFactory)
{
_target = target;
_styledProperty = property;
_valueFactory = valueFactory;
}
public PropertySetterLazyInstance(
IStyleable target,
DirectPropertyBase<T> property,
Func<T> valueFactory)
{
_target = target;
_directProperty = property;
_valueFactory = valueFactory;
}
public void Start(bool hasActivator)
{
_isActive = !hasActivator;
if (_styledProperty is object)
{
var priority = hasActivator ? BindingPriority.StyleTrigger : BindingPriority.Style;
_subscription = _target.Bind(_styledProperty, this, priority);
}
else
{
_subscription = _target.Bind(_directProperty, this);
}
}
public void Activate()
{
if (!_isActive)
{
_isActive = true;
PublishNext();
}
}
public void Deactivate()
{
if (_isActive)
{
_isActive = false;
PublishNext();
}
}
public override void Dispose()
{
if (_subscription is object)
{
var sub = _subscription;
_subscription = null;
sub.Dispose();
}
else if (_isActive)
{
if (_styledProperty is object)
{
_target.ClearValue(_styledProperty);
}
else
{
_target.ClearValue(_directProperty);
}
}
base.Dispose();
}
protected override void Subscribed() => PublishNext();
protected override void Unsubscribed() { }
private T GetValue()
{
if (_value.HasValue)
{
return _value.Value;
}
_value = _valueFactory();
return _value.Value;
}
private void PublishNext()
{
if (_isActive)
{
GetValue();
PublishNext(_value);
}
else
{
PublishNext(default);
}
}
}
}

24
src/Avalonia.Styling/Styling/Setter.cs

@ -68,18 +68,10 @@ namespace Avalonia.Styling
throw new InvalidOperationException("Setter.Property must be set.");
}
var value = Value;
if (value is ITemplate template &&
!typeof(ITemplate).IsAssignableFrom(Property.PropertyType))
{
value = template.Build();
}
var data = new SetterVisitorData
{
target = target,
value = value,
value = Value,
};
Property.Accept(this, ref data);
@ -97,6 +89,13 @@ namespace Avalonia.Styling
property,
binding);
}
else if (data.value is ITemplate template && !typeof(ITemplate).IsAssignableFrom(property.PropertyType))
{
data.result = new PropertySetterLazyInstance<T>(
data.target,
property,
() => (T)template.Build());
}
else
{
data.result = new PropertySetterInstance<T>(
@ -117,6 +116,13 @@ namespace Avalonia.Styling
property,
binding);
}
else if (data.value is ITemplate template && !typeof(ITemplate).IsAssignableFrom(property.PropertyType))
{
data.result = new PropertySetterLazyInstance<T>(
data.target,
property,
() => (T)template.Build());
}
else
{
data.result = new PropertySetterInstance<T>(

1
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" />

494
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs

@ -0,0 +1,494 @@
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 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()
{
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 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 ClearValue_Change_Should_Be_Raised_After_Batch_Update_1()
{
var target = new TestClass();
var raised = new List<AvaloniaPropertyChangedEventArgs>();
target.Foo = "foo";
target.PropertyChanged += (s, e) => raised.Add(e);
target.BeginBatchUpdate();
target.ClearValue(TestClass.FooProperty);
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()
{
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 LocalValue_Bindings_Should_Be_Subscribed_During_Batch_Update()
{
var target = new TestClass();
var observable1 = new TestObservable<string>("foo");
var observable2 = new TestObservable<string>("bar");
var raised = new List<AvaloniaPropertyChangedEventArgs>();
target.PropertyChanged += (s, e) => raised.Add(e);
// We need to subscribe to LocalValue bindings 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. Notifications however shouldn't be sent.
target.BeginBatchUpdate();
target.Bind(TestClass.FooProperty, observable1, BindingPriority.LocalValue);
target.Bind(TestClass.FooProperty, observable2, BindingPriority.LocalValue);
Assert.Equal(1, observable1.SubscribeCount);
Assert.Equal(1, observable2.SubscribeCount);
Assert.Empty(raised);
}
[Fact]
public void Style_Bindings_Should_Not_Be_Subscribed_During_Batch_Update()
{
var target = new TestClass();
var observable1 = new TestObservable<string>("foo");
var observable2 = new TestObservable<string>("bar");
target.BeginBatchUpdate();
target.Bind(TestClass.FooProperty, observable1, BindingPriority.Style);
target.Bind(TestClass.FooProperty, observable2, BindingPriority.StyleTrigger);
Assert.Equal(0, observable1.SubscribeCount);
Assert.Equal(0, observable2.SubscribeCount);
}
[Fact]
public void Active_Style_Binding_Should_Be_Subscribed_After_Batch_Uppdate_1()
{
var target = new TestClass();
var observable1 = new TestObservable<string>("foo");
var observable2 = new TestObservable<string>("bar");
target.BeginBatchUpdate();
target.Bind(TestClass.FooProperty, observable1, BindingPriority.Style);
target.Bind(TestClass.FooProperty, observable2, BindingPriority.Style);
target.EndBatchUpdate();
Assert.Equal(0, observable1.SubscribeCount);
Assert.Equal(1, observable2.SubscribeCount);
}
[Fact]
public void Active_Style_Binding_Should_Be_Subscribed_After_Batch_Uppdate_2()
{
var target = new TestClass();
var observable1 = new TestObservable<string>("foo");
var observable2 = new TestObservable<string>("bar");
target.BeginBatchUpdate();
target.Bind(TestClass.FooProperty, observable1, BindingPriority.StyleTrigger);
target.Bind(TestClass.FooProperty, observable2, BindingPriority.Style);
target.EndBatchUpdate();
Assert.Equal(1, observable1.SubscribeCount);
Assert.Equal(0, observable2.SubscribeCount);
}
[Fact]
public void Change_Can_Be_Triggered_By_Ending_Batch_Update_1()
{
var target = new TestClass();
var raised = new List<AvaloniaPropertyChangedEventArgs>();
target.PropertyChanged += (s, e) => raised.Add(e);
target.BeginBatchUpdate();
target.Foo = "foo";
target.PropertyChanged += (s, e) =>
{
if (e.Property == TestClass.FooProperty && (string)e.NewValue == "foo")
target.Bar = "bar";
};
target.EndBatchUpdate();
Assert.Equal("foo", target.Foo);
Assert.Equal("bar", target.Bar);
Assert.Equal(2, raised.Count);
Assert.Equal(TestClass.FooProperty, raised[0].Property);
Assert.Equal(TestClass.BarProperty, raised[1].Property);
}
[Fact]
public void Change_Can_Be_Triggered_By_Ending_Batch_Update_2()
{
var target = new TestClass();
var raised = new List<AvaloniaPropertyChangedEventArgs>();
target.PropertyChanged += (s, e) => raised.Add(e);
target.BeginBatchUpdate();
target.Foo = "foo";
target.Bar = "baz";
target.PropertyChanged += (s, e) =>
{
if (e.Property == TestClass.FooProperty && (string)e.NewValue == "foo")
target.Bar = "bar";
};
target.EndBatchUpdate();
Assert.Equal("foo", target.Foo);
Assert.Equal("bar", target.Bar);
Assert.Equal(2, raised.Count);
}
[Fact]
public void Batch_Update_Can_Be_Triggered_By_Ending_Batch_Update()
{
var target = new TestClass();
var raised = new List<AvaloniaPropertyChangedEventArgs>();
target.PropertyChanged += (s, e) => raised.Add(e);
target.BeginBatchUpdate();
target.Foo = "foo";
target.Bar = "baz";
// Simulates the following scenario:
// - A control is added to the logical tree
// - A batch update is started to apply styles
// - Ending the batch update triggers something which removes the control from the logical tree
// - A new batch update is started to detach styles
target.PropertyChanged += (s, e) =>
{
if (e.Property == TestClass.FooProperty && (string)e.NewValue == "foo")
{
target.BeginBatchUpdate();
target.ClearValue(TestClass.FooProperty);
target.ClearValue(TestClass.BarProperty);
target.EndBatchUpdate();
}
};
target.EndBatchUpdate();
Assert.Null(target.Foo);
Assert.Null(target.Bar);
Assert.Equal(2, raised.Count);
Assert.Equal(TestClass.FooProperty, raised[0].Property);
Assert.Null(raised[0].OldValue);
Assert.Equal("foo", raised[0].NewValue);
Assert.Equal(TestClass.FooProperty, raised[1].Property);
Assert.Equal("foo", raised[1].OldValue);
Assert.Null(raised[1].NewValue);
}
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;
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;
}
}
}
}

1
tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj

@ -12,6 +12,7 @@
<ProjectReference Include="..\..\src\Avalonia.Input\Avalonia.Input.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Interactivity\Avalonia.Interactivity.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Layout\Avalonia.Layout.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj" />
<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" />

74
tests/Avalonia.Benchmarks/Themes/FluentBenchmark.cs

@ -0,0 +1,74 @@
using System;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Markup.Xaml.Styling;
using Avalonia.Platform;
using Avalonia.Shared.PlatformSupport;
using Avalonia.Styling;
using Avalonia.UnitTests;
using BenchmarkDotNet.Attributes;
using Moq;
namespace Avalonia.Benchmarks.Themes
{
[MemoryDiagnoser]
public class FluentBenchmark
{
private readonly IDisposable _app;
private readonly TestRoot _root;
public FluentBenchmark()
{
_app = CreateApp();
_root = new TestRoot(true, null)
{
Renderer = new NullRenderer()
};
_root.LayoutManager.ExecuteInitialLayoutPass();
}
public void Dispose()
{
_app.Dispose();
}
[Benchmark]
public void RepeatButton()
{
var button = new RepeatButton();
_root.Child = button;
_root.LayoutManager.ExecuteLayoutPass();
}
private static IDisposable CreateApp()
{
var services = new TestServices(
assetLoader: new AssetLoader(),
globalClock: new MockGlobalClock(),
platform: new AppBuilder().RuntimePlatform,
renderInterface: new MockPlatformRenderInterface(),
standardCursorFactory: Mock.Of<ICursorFactory>(),
styler: new Styler(),
theme: () => LoadFluentTheme(),
threadingInterface: new NullThreadingPlatform(),
fontManagerImpl: new MockFontManagerImpl(),
textShaperImpl: new MockTextShaperImpl(),
windowingPlatform: new MockWindowingPlatform());
return UnitTestApplication.Start(services);
}
private static Styles LoadFluentTheme()
{
AssetLoader.RegisterResUriParsers();
return new Styles
{
new StyleInclude(new Uri("avares://Avalonia.Benchmarks"))
{
Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/FluentDark.xaml")
}
};
}
}
}

1
tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.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" />

96
tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Controls.Presenters;
@ -809,6 +810,82 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
Assert.Equal(0xff506070, brush.Color.ToUint32());
}
[Fact]
public void Resource_In_Non_Matching_Style_Is_Not_Resolved()
{
using var app = StyledWindow();
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions'>
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<local:TrackingResourceProvider/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
<Window.Styles>
<Style Selector='Border.nomatch'>
<Setter Property='Tag' Value='{DynamicResource foo}'/>
</Style>
<Style Selector='Border'>
<Setter Property='Tag' Value='{DynamicResource bar}'/>
</Style>
</Window.Styles>
<Border Name='border'/>
</Window>";
var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
var border = window.FindControl<Border>("border");
Assert.Equal("bar", border.Tag);
var resourceProvider = (TrackingResourceProvider)window.Resources.MergedDictionaries[0];
Assert.Equal(new[] { "bar" }, resourceProvider.RequestedResources);
}
[Fact]
public void Resource_In_Non_Active_Style_Is_Not_Resolved()
{
using var app = StyledWindow();
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions'>
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<local:TrackingResourceProvider/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
<Window.Styles>
<Style Selector='Border'>
<Setter Property='Tag' Value='{DynamicResource foo}'/>
</Style>
<Style Selector='Border'>
<Setter Property='Tag' Value='{DynamicResource bar}'/>
</Style>
</Window.Styles>
<Border Name='border'/>
</Window>";
var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
var border = window.FindControl<Border>("border");
Assert.Equal("bar", border.Tag);
var resourceProvider = (TrackingResourceProvider)window.Resources.MergedDictionaries[0];
Assert.Equal(new[] { "bar" }, resourceProvider.RequestedResources);
}
private IDisposable StyledWindow(params (string, string)[] assets)
{
var services = TestServices.StyledWindow.With(
@ -839,4 +916,23 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
};
}
}
public class TrackingResourceProvider : IResourceProvider
{
public IResourceHost Owner { get; private set; }
public bool HasResources => true;
public List<object> RequestedResources { get; } = new List<object>();
public event EventHandler OwnerChanged;
public void AddOwner(IResourceHost owner) => Owner = owner;
public void RemoveOwner(IResourceHost owner) => Owner = null;
public bool TryGetResource(object key, out object value)
{
RequestedResources.Add(key);
value = key;
return true;
}
}
}

1
tests/Avalonia.Styling.UnitTests/Avalonia.Styling.UnitTests.csproj

@ -4,6 +4,7 @@
<OutputType>Library</OutputType>
<NoWarn>CS0067</NoWarn>
<IsTestProject>true</IsTestProject>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<Import Project="..\..\build\UnitTests.NetCore.targets" />
<Import Project="..\..\build\UnitTests.NetFX.props" />

282
tests/Avalonia.Styling.UnitTests/StyleTests.cs

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.UnitTests;
using Moq;
@ -217,6 +218,278 @@ namespace Avalonia.Styling.UnitTests
Assert.Equal(new[] { "foodefault", "Bar" }, values);
}
[Fact]
public void Inactive_Values_Should_Not_Be_Made_Active_During_Style_Attach()
{
using var app = UnitTestApplication.Start(TestServices.RealStyler);
var root = new TestRoot
{
Styles =
{
new Style(x => x.OfType<Class1>())
{
Setters =
{
new Setter(Class1.FooProperty, "Foo"),
},
},
new Style(x => x.OfType<Class1>())
{
Setters =
{
new Setter(Class1.FooProperty, "Bar"),
},
}
}
};
var values = new List<string>();
var target = new Class1();
target.GetObservable(Class1.FooProperty).Subscribe(x => values.Add(x));
root.Child = target;
Assert.Equal(new[] { "foodefault", "Bar" }, values);
}
[Fact]
public void Inactive_Bindings_Should_Not_Be_Made_Active_During_Style_Attach()
{
using var app = UnitTestApplication.Start(TestServices.RealStyler);
var root = new TestRoot
{
Styles =
{
new Style(x => x.OfType<Class1>())
{
Setters =
{
new Setter(Class1.FooProperty, new Binding("Foo")),
},
},
new Style(x => x.OfType<Class1>())
{
Setters =
{
new Setter(Class1.FooProperty, new Binding("Bar")),
},
}
}
};
var values = new List<string>();
var target = new Class1
{
DataContext = new
{
Foo = "Foo",
Bar = "Bar",
}
};
target.GetObservable(Class1.FooProperty).Subscribe(x => values.Add(x));
root.Child = target;
Assert.Equal(new[] { "foodefault", "Bar" }, values);
}
[Fact]
public void Inactive_Values_Should_Not_Be_Made_Active_During_Style_Detach()
{
using var app = UnitTestApplication.Start(TestServices.RealStyler);
var root = new TestRoot
{
Styles =
{
new Style(x => x.OfType<Class1>())
{
Setters =
{
new Setter(Class1.FooProperty, "Foo"),
},
},
new Style(x => x.OfType<Class1>())
{
Setters =
{
new Setter(Class1.FooProperty, "Bar"),
},
}
}
};
var target = new Class1();
root.Child = target;
var values = new List<string>();
target.GetObservable(Class1.FooProperty).Subscribe(x => values.Add(x));
root.Child = null;
Assert.Equal(new[] { "Bar", "foodefault" }, values);
}
[Fact]
public void Inactive_Values_Should_Not_Be_Made_Active_During_Style_Detach_2()
{
using var app = UnitTestApplication.Start(TestServices.RealStyler);
var root = new TestRoot
{
Styles =
{
new Style(x => x.OfType<Class1>().Class("foo"))
{
Setters =
{
new Setter(Class1.FooProperty, "Foo"),
},
},
new Style(x => x.OfType<Class1>())
{
Setters =
{
new Setter(Class1.FooProperty, "Bar"),
},
}
}
};
var target = new Class1 { Classes = { "foo" } };
root.Child = target;
var values = new List<string>();
target.GetObservable(Class1.FooProperty).Subscribe(x => values.Add(x));
root.Child = null;
Assert.Equal(new[] { "Foo", "foodefault" }, values);
}
[Fact]
public void Inactive_Bindings_Should_Not_Be_Made_Active_During_Style_Detach()
{
using var app = UnitTestApplication.Start(TestServices.RealStyler);
var root = new TestRoot
{
Styles =
{
new Style(x => x.OfType<Class1>())
{
Setters =
{
new Setter(Class1.FooProperty, new Binding("Foo")),
},
},
new Style(x => x.OfType<Class1>())
{
Setters =
{
new Setter(Class1.FooProperty, new Binding("Bar")),
},
}
}
};
var target = new Class1
{
DataContext = new
{
Foo = "Foo",
Bar = "Bar",
}
};
root.Child = target;
var values = new List<string>();
target.GetObservable(Class1.FooProperty).Subscribe(x => values.Add(x));
root.Child = null;
Assert.Equal(new[] { "Bar", "foodefault" }, values);
}
[Fact]
public void Template_In_Non_Matching_Style_Is_Not_Built()
{
var instantiationCount = 0;
var template = new FuncTemplate<Class1>(() =>
{
++instantiationCount;
return new Class1();
});
Styles styles = new Styles
{
new Style(x => x.OfType<Class1>().Class("foo"))
{
Setters =
{
new Setter(Class1.ChildProperty, template),
},
},
new Style(x => x.OfType<Class1>())
{
Setters =
{
new Setter(Class1.ChildProperty, template),
},
}
};
var target = new Class1();
styles.TryAttach(target, null);
Assert.NotNull(target.Child);
Assert.Equal(1, instantiationCount);
}
[Fact]
public void Template_In_Inactive_Style_Is_Not_Built()
{
var instantiationCount = 0;
var template = new FuncTemplate<Class1>(() =>
{
++instantiationCount;
return new Class1();
});
Styles styles = new Styles
{
new Style(x => x.OfType<Class1>())
{
Setters =
{
new Setter(Class1.ChildProperty, template),
},
},
new Style(x => x.OfType<Class1>())
{
Setters =
{
new Setter(Class1.ChildProperty, template),
},
}
};
var target = new Class1();
target.BeginBatchUpdate();
styles.TryAttach(target, null);
target.EndBatchUpdate();
Assert.NotNull(target.Child);
Assert.Equal(1, instantiationCount);
}
[Fact]
public void Style_Should_Detach_When_Control_Removed_From_Logical_Tree()
{
@ -453,12 +726,21 @@ namespace Avalonia.Styling.UnitTests
public static readonly StyledProperty<string> FooProperty =
AvaloniaProperty.Register<Class1, string>(nameof(Foo), "foodefault");
public static readonly StyledProperty<Class1> ChildProperty =
AvaloniaProperty.Register<Class1, Class1>(nameof(Child));
public string Foo
{
get { return GetValue(FooProperty); }
set { SetValue(FooProperty, value); }
}
public Class1 Child
{
get => GetValue(ChildProperty);
set => SetValue(ChildProperty, value);
}
protected override Size MeasureOverride(Size availableSize)
{
throw new NotImplementedException();

2
tests/Avalonia.UnitTests/UnitTestApplication.cs

@ -25,6 +25,7 @@ namespace Avalonia.UnitTests
public UnitTestApplication(TestServices services)
{
_services = services ?? new TestServices();
AvaloniaLocator.CurrentMutable.BindToSelf<Application>(this);
RegisterServices();
}
@ -36,7 +37,6 @@ namespace Avalonia.UnitTests
{
var scope = AvaloniaLocator.EnterScope();
var app = new UnitTestApplication(services);
AvaloniaLocator.CurrentMutable.BindToSelf<Application>(app);
Dispatcher.UIThread.UpdateServices();
return Disposable.Create(() =>
{

Loading…
Cancel
Save