Browse Source

Make ValueStore typed.

Major refactor of the Avalonia core to make the styled property store typed.
pull/3258/head
Steven Kirk 6 years ago
parent
commit
6be3acb46c
  1. 26
      src/Avalonia.Animation/Animatable.cs
  2. 729
      src/Avalonia.Base/AvaloniaObject.cs
  3. 146
      src/Avalonia.Base/AvaloniaObjectExtensions.cs
  4. 89
      src/Avalonia.Base/AvaloniaProperty.cs
  5. 40
      src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs
  6. 67
      src/Avalonia.Base/AvaloniaPropertyChangedEventArgs`1.cs
  7. 115
      src/Avalonia.Base/AvaloniaPropertyRegistry.cs
  8. 33
      src/Avalonia.Base/AvaloniaProperty`1.cs
  9. 20
      src/Avalonia.Base/Data/BindingNotification.cs
  10. 14
      src/Avalonia.Base/Data/BindingOperations.cs
  11. 406
      src/Avalonia.Base/Data/BindingValue.cs
  12. 129
      src/Avalonia.Base/Data/Optional.cs
  13. 58
      src/Avalonia.Base/Diagnostics/AvaloniaObjectExtensions.cs
  14. 98
      src/Avalonia.Base/DirectProperty.cs
  15. 159
      src/Avalonia.Base/DirectPropertyBase.cs
  16. 54
      src/Avalonia.Base/IAvaloniaObject.cs
  17. 51
      src/Avalonia.Base/IPriorityValueOwner.cs
  18. 9
      src/Avalonia.Base/IStyledPropertyAccessor.cs
  19. 7
      src/Avalonia.Base/IStyledPropertyMetadata.cs
  20. 160
      src/Avalonia.Base/PriorityBindingEntry.cs
  21. 227
      src/Avalonia.Base/PriorityLevel.cs
  22. 315
      src/Avalonia.Base/PriorityValue.cs
  23. 95
      src/Avalonia.Base/PropertyStore/BindingEntry.cs
  24. 28
      src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs
  25. 18
      src/Avalonia.Base/PropertyStore/IPriorityValueEntry.cs
  26. 17
      src/Avalonia.Base/PropertyStore/IValue.cs
  27. 18
      src/Avalonia.Base/PropertyStore/IValueSink.cs
  28. 143
      src/Avalonia.Base/PropertyStore/PriorityValue.cs
  29. 55
      src/Avalonia.Base/Reactive/AvaloniaPropertyBindingObservable.cs
  30. 61
      src/Avalonia.Base/Reactive/BindingValueAdapter.cs
  31. 35
      src/Avalonia.Base/Reactive/BindingValueExtensions.cs
  32. 63
      src/Avalonia.Base/Reactive/TypedBindingAdapter.cs
  33. 57
      src/Avalonia.Base/Reactive/UntypedBindingAdapter.cs
  34. 84
      src/Avalonia.Base/StyledPropertyBase.cs
  35. 15
      src/Avalonia.Base/StyledPropertyMetadata`1.cs
  36. 21
      src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs
  37. 11
      src/Avalonia.Base/Utilities/TypeUtilities.cs
  38. 282
      src/Avalonia.Base/ValueStore.cs
  39. 24
      src/Avalonia.Controls.DataGrid/DataGrid.cs
  40. 4
      src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs
  41. 16
      src/Avalonia.Controls/AutoCompleteBox.cs
  42. 18
      src/Avalonia.Controls/Button.cs
  43. 4
      src/Avalonia.Controls/Calendar/Calendar.cs
  44. 18
      src/Avalonia.Controls/Calendar/DatePicker.cs
  45. 10
      src/Avalonia.Controls/MenuItem.cs
  46. 6
      src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs
  47. 2
      src/Avalonia.Controls/Primitives/ScrollBar.cs
  48. 27
      src/Avalonia.Controls/Repeater/ItemsRepeater.cs
  49. 2
      src/Avalonia.Controls/ScrollViewer.cs
  50. 8
      src/Avalonia.Controls/TextBox.cs
  51. 7
      src/Avalonia.Layout/StackLayout.cs
  52. 33
      src/Avalonia.Layout/UniformGridLayout.cs
  53. 6
      src/Avalonia.Styling/StyledElement.cs
  54. 6
      src/Avalonia.Visuals/Visual.cs
  55. 22
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_AddOwner.cs
  56. 8
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Attached.cs
  57. 255
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs
  58. 101
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs
  59. 172
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs
  60. 19
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_GetValue.cs
  61. 87
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetValue.cs
  62. 156
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs
  63. 31
      tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs
  64. 66
      tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_DataValidation.cs
  65. 16
      tests/Avalonia.Base.UnitTests/DirectPropertyTests.cs
  66. 371
      tests/Avalonia.Base.UnitTests/PriorityValueTests.cs
  67. 2
      tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs
  68. 6
      tests/Avalonia.Markup.UnitTests/Data/BindingTests_TemplatedParent.cs
  69. 7
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/InitializationOrderTracker.cs
  70. 34
      tests/Avalonia.Styling.UnitTests/SelectorTests_Child.cs
  71. 34
      tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs
  72. 8
      tests/Avalonia.Styling.UnitTests/SetterTests.cs
  73. 34
      tests/Avalonia.Styling.UnitTests/TestControlBase.cs
  74. 34
      tests/Avalonia.Styling.UnitTests/TestTemplatedControl.cs

26
src/Avalonia.Animation/Animatable.cs

@ -65,26 +65,30 @@ namespace Avalonia.Animation
}
}
/// <summary>
/// Reacts to a change in a <see cref="AvaloniaProperty"/> value in
/// order to animate the change if a <see cref="ITransition"/> is set for the property.
/// </summary>
/// <param name="e">The event args.</param>
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e)
protected override void OnPropertyChanged<T>(
AvaloniaProperty<T> property,
Optional<T> oldValue,
BindingValue<T> newValue,
BindingPriority priority)
{
if (_transitions is null || _previousTransitions is null || e.Priority == BindingPriority.Animation) return;
if (_transitions is null || _previousTransitions is null || priority == BindingPriority.Animation)
return;
// PERF-SENSITIVE: Called on every property change. Don't use LINQ here (too many allocations).
foreach (var transition in _transitions)
{
if (transition.Property == e.Property)
if (transition.Property == property)
{
if (_previousTransitions.TryGetValue(e.Property, out var dispose))
if (_previousTransitions.TryGetValue(property, out var dispose))
dispose.Dispose();
var instance = transition.Apply(this, Clock ?? Avalonia.Animation.Clock.GlobalClock, e.OldValue, e.NewValue);
var instance = transition.Apply(
this,
Clock ?? Avalonia.Animation.Clock.GlobalClock,
oldValue.ValueOrDefault(),
newValue.ValueOrDefault());
_previousTransitions[e.Property] = instance;
_previousTransitions[property] = instance;
return;
}
}

729
src/Avalonia.Base/AvaloniaObject.cs

@ -4,11 +4,10 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reactive.Linq;
using Avalonia.Data;
using Avalonia.Diagnostics;
using Avalonia.Logging;
using Avalonia.PropertyStore;
using Avalonia.Threading;
using Avalonia.Utilities;
@ -20,13 +19,13 @@ namespace Avalonia
/// <remarks>
/// This class is analogous to DependencyObject in WPF.
/// </remarks>
public class AvaloniaObject : IAvaloniaObject, IAvaloniaObjectDebug, INotifyPropertyChanged
public class AvaloniaObject : IAvaloniaObject, IAvaloniaObjectDebug, INotifyPropertyChanged, IValueSink
{
private IAvaloniaObject _inheritanceParent;
private List<DirectBindingSubscription> _directBindings;
private List<IDisposable> _directBindings;
private PropertyChangedEventHandler _inpcChanged;
private EventHandler<AvaloniaPropertyChangedEventArgs> _propertyChanged;
private EventHandler<AvaloniaPropertyChangedEventArgs> _inheritablePropertyChanged;
private List<IAvaloniaObject> _inheritanceChildren;
private ValueStore _values;
private ValueStore Values => _values ?? (_values = new ValueStore(this));
@ -57,15 +56,6 @@ namespace Avalonia
remove { _inpcChanged -= value; }
}
/// <summary>
/// Raised when an inheritable <see cref="AvaloniaProperty"/> value changes on this object.
/// </summary>
event EventHandler<AvaloniaPropertyChangedEventArgs> IAvaloniaObject.InheritablePropertyChanged
{
add { _inheritablePropertyChanged += value; }
remove { _inheritablePropertyChanged -= value; }
}
/// <summary>
/// Gets or sets the parent object that inherited <see cref="AvaloniaProperty"/> values
/// are inherited from.
@ -83,47 +73,27 @@ namespace Avalonia
set
{
VerifyAccess();
if (_inheritanceParent != value)
{
if (_inheritanceParent != null)
{
_inheritanceParent.InheritablePropertyChanged -= ParentPropertyChanged;
}
var oldParent = _inheritanceParent;
var valuestore = _values;
var oldInheritanceParent = _inheritanceParent;
_inheritanceParent?.RemoveInheritanceChild(this);
_inheritanceParent = value;
var valuestore = _values;
foreach (var property in AvaloniaPropertyRegistry.Instance.GetRegisteredInherited(GetType()))
{
if (valuestore != null && valuestore.GetValue(property) != AvaloniaProperty.UnsetValue)
if (valuestore?.IsSet(property) == true)
{
// if local value set there can be no change
// If local value set there can be no change.
continue;
}
// get the value as it would have been with the previous InheritanceParent
object oldValue;
if (oldInheritanceParent is AvaloniaObject aobj)
{
oldValue = aobj.GetValueOrDefaultUnchecked(property);
}
else
{
oldValue = ((IStyledPropertyAccessor)property).GetDefaultValue(GetType());
}
object newValue = GetDefaultValue(property);
if (!Equals(oldValue, newValue))
{
RaisePropertyChanged(property, oldValue, newValue, BindingPriority.LocalValue);
}
property.RouteInheritanceParentChanged(this, oldParent);
}
if (_inheritanceParent != null)
{
_inheritanceParent.InheritablePropertyChanged += ParentPropertyChanged;
}
_inheritanceParent?.AddInheritanceChild(this);
}
}
}
@ -167,9 +137,31 @@ namespace Avalonia
public void ClearValue(AvaloniaProperty property)
{
Contract.Requires<ArgumentNullException>(property != null);
property.RouteClearValue(this);
}
/// <summary>
/// Clears a <see cref="AvaloniaProperty"/>'s local value.
/// </summary>
/// <param name="property">The property.</param>
public void ClearValue<T>(AvaloniaProperty<T> property)
{
VerifyAccess();
SetValue(property, AvaloniaProperty.UnsetValue);
switch (property)
{
case StyledPropertyBase<T> styled:
_values.ClearLocalValue(styled);
break;
case DirectPropertyBase<T> direct:
var p = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, direct);
p.InvokeSetter(this, p.GetUnsetValue(GetType()));
break;
case null:
throw new ArgumentNullException(nameof(property));
default:
throw new NotSupportedException("Unsupported AvaloniaProperty type.");
}
}
/// <summary>
@ -210,21 +202,38 @@ namespace Avalonia
/// <returns>The value.</returns>
public object GetValue(AvaloniaProperty property)
{
if (property is null)
return property.RouteGetValue(this);
}
/// <summary>
/// Gets a <see cref="AvaloniaProperty"/> value.
/// </summary>
/// <typeparam name="T">The type of the property.</typeparam>
/// <param name="property">The property.</param>
/// <returns>The value.</returns>
public T GetValue<T>(AvaloniaProperty<T> property)
{
return property switch
{
throw new ArgumentNullException(nameof(property));
}
StyledPropertyBase<T> styled => GetValue(styled),
DirectPropertyBase<T> direct => GetValue(direct),
null => throw new ArgumentNullException(nameof(property)),
_ => throw new NotSupportedException("Unsupported AvaloniaProperty type.")
};
}
/// <summary>
/// Gets a <see cref="AvaloniaProperty"/> value.
/// </summary>
/// <typeparam name="T">The type of the property.</typeparam>
/// <param name="property">The property.</param>
/// <returns>The value.</returns>
public T GetValue<T>(StyledPropertyBase<T> property)
{
property = property ?? throw new ArgumentNullException(nameof(property));
VerifyAccess();
if (property.IsDirect)
{
return ((IDirectPropertyAccessor)GetRegistered(property)).GetValue(this);
}
else
{
return GetValueOrDefaultUnchecked(property);
}
return GetValueOrInheritedOrDefault(property);
}
/// <summary>
@ -233,14 +242,13 @@ namespace Avalonia
/// <typeparam name="T">The type of the property.</typeparam>
/// <param name="property">The property.</param>
/// <returns>The value.</returns>
public T GetValue<T>(AvaloniaProperty<T> property)
public T GetValue<T>(DirectPropertyBase<T> property)
{
if (property is null)
{
throw new ArgumentNullException(nameof(property));
}
property = property ?? throw new ArgumentNullException(nameof(property));
VerifyAccess();
return (T)GetValue((AvaloniaProperty)property);
var registered = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, property);
return registered.InvokeGetter(this);
}
/// <summary>
@ -284,16 +292,33 @@ namespace Avalonia
object value,
BindingPriority priority = BindingPriority.LocalValue)
{
Contract.Requires<ArgumentNullException>(property != null);
VerifyAccess();
property.RouteSetValue(this, value, priority);
}
if (property.IsDirect)
{
SetDirectValue(property, value);
}
else
/// <summary>
/// Sets a <see cref="AvaloniaProperty"/> value.
/// </summary>
/// <typeparam name="T">The type of the property.</typeparam>
/// <param name="property">The property.</param>
/// <param name="value">The value.</param>
/// <param name="priority">The priority of the value.</param>
public void SetValue<T>(
AvaloniaProperty<T> property,
T value,
BindingPriority priority = BindingPriority.LocalValue)
{
switch (property)
{
SetStyledValue(property, value, priority);
case StyledPropertyBase<T> styled:
SetValue(styled, value, priority);
break;
case DirectPropertyBase<T> direct:
SetValue(direct, value);
break;
case null:
throw new ArgumentNullException(nameof(property));
default:
throw new NotSupportedException("Unsupported AvaloniaProperty type.");
}
}
@ -305,13 +330,46 @@ namespace Avalonia
/// <param name="value">The value.</param>
/// <param name="priority">The priority of the value.</param>
public void SetValue<T>(
AvaloniaProperty<T> property,
StyledPropertyBase<T> property,
T value,
BindingPriority priority = BindingPriority.LocalValue)
{
Contract.Requires<ArgumentNullException>(property != null);
property = property ?? throw new ArgumentNullException(nameof(property));
VerifyAccess();
SetValue((AvaloniaProperty)property, value, priority);
LogPropertySet(property, value, priority);
if (value is UnsetValueType)
{
if (priority == BindingPriority.LocalValue)
{
Values.ClearLocalValue(property);
}
else
{
throw new NotSupportedException(
"Canot set property to Unset at non-local value priority.");
}
}
else if (!(value is DoNothingType))
{
Values.SetValue(property, value, priority);
}
}
/// <summary>
/// Sets a <see cref="AvaloniaProperty"/> value.
/// </summary>
/// <typeparam name="T">The type of the property.</typeparam>
/// <param name="property">The property.</param>
/// <param name="value">The value.</param>
public void SetValue<T>(DirectPropertyBase<T> property, T value)
{
property = property ?? throw new ArgumentNullException(nameof(property));
VerifyAccess();
LogPropertySet(property, value, BindingPriority.LocalValue);
SetDirectValueUnchecked(property, value);
}
/// <summary>
@ -325,47 +383,34 @@ namespace Avalonia
/// </returns>
public IDisposable Bind(
AvaloniaProperty property,
IObservable<object> source,
IObservable<BindingValue<object>> source,
BindingPriority priority = BindingPriority.LocalValue)
{
Contract.Requires<ArgumentNullException>(property != null);
Contract.Requires<ArgumentNullException>(source != null);
VerifyAccess();
if (property.IsDirect)
{
if (property.IsReadOnly)
{
throw new ArgumentException($"The property {property.Name} is readonly.");
}
Logger.TryGet(LogEventLevel.Verbose)?.Log(
LogArea.Property,
this,
"Bound {Property} to {Binding} with priority LocalValue",
property,
GetDescription(source));
if (_directBindings == null)
{
_directBindings = new List<DirectBindingSubscription>();
}
return property.RouteBind(this, source, priority);
}
return new DirectBindingSubscription(this, property, source);
}
else
/// <summary>
/// Binds a <see cref="AvaloniaProperty"/> to an observable.
/// </summary>
/// <typeparam name="T">The type of the property.</typeparam>
/// <param name="property">The property.</param>
/// <param name="source">The observable.</param>
/// <param name="priority">The priority of the binding.</param>
/// <returns>
/// A disposable which can be used to terminate the binding.
/// </returns>
public IDisposable Bind<T>(
AvaloniaProperty<T> property,
IObservable<BindingValue<T>> source,
BindingPriority priority = BindingPriority.LocalValue)
{
return property switch
{
Logger.TryGet(LogEventLevel.Verbose)?.Log(
LogArea.Property,
this,
"Bound {Property} to {Binding} with priority {Priority}",
property,
GetDescription(source),
priority);
return Values.AddBinding(property, source, priority);
}
StyledPropertyBase<T> styled => Bind(styled, source, priority),
DirectPropertyBase<T> direct => Bind(direct, source),
null => throw new ArgumentNullException(nameof(property)),
_ => throw new NotSupportedException("Unsupported AvaloniaProperty type."),
};
}
/// <summary>
@ -379,37 +424,96 @@ namespace Avalonia
/// A disposable which can be used to terminate the binding.
/// </returns>
public IDisposable Bind<T>(
AvaloniaProperty<T> property,
IObservable<T> source,
StyledPropertyBase<T> property,
IObservable<BindingValue<T>> source,
BindingPriority priority = BindingPriority.LocalValue)
{
Contract.Requires<ArgumentNullException>(property != null);
property = property ?? throw new ArgumentNullException(nameof(property));
VerifyAccess();
return Bind(property, source.Select(x => (object)x), priority);
return Values.AddBinding(property, source, priority);
}
/// <summary>
/// Forces the specified property to be revalidated.
/// Binds a <see cref="AvaloniaProperty"/> to an observable.
/// </summary>
/// <typeparam name="T">The type of the property.</typeparam>
/// <param name="property">The property.</param>
public void Revalidate(AvaloniaProperty property)
/// <param name="source">The observable.</param>
/// <returns>
/// A disposable which can be used to terminate the binding.
/// </returns>
public IDisposable Bind<T>(
DirectPropertyBase<T> property,
IObservable<BindingValue<T>> source)
{
property = property ?? throw new ArgumentNullException(nameof(property));
source = source ?? throw new ArgumentNullException(nameof(source));
VerifyAccess();
_values?.Revalidate(property);
property = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, property);
if (property.IsReadOnly)
{
throw new ArgumentException($"The property {property.Name} is readonly.");
}
Logger.TryGet(LogEventLevel.Verbose)?.Log(
LogArea.Property,
this,
"Bound {Property} to {Binding} with priority LocalValue",
property,
GetDescription(source));
_directBindings ??= new List<IDisposable>();
return new DirectBindingSubscription<T>(this, property, source);
}
/// <inheritdoc/>
void IAvaloniaObject.AddInheritanceChild(IAvaloniaObject child)
{
_inheritanceChildren ??= new List<IAvaloniaObject>();
_inheritanceChildren.Add(child);
}
internal void PriorityValueChanged(AvaloniaProperty property, int priority, object oldValue, object newValue)
/// <inheritdoc/>
void IAvaloniaObject.RemoveInheritanceChild(IAvaloniaObject child)
{
oldValue = (oldValue == AvaloniaProperty.UnsetValue) ?
GetDefaultValue(property) :
oldValue;
newValue = (newValue == AvaloniaProperty.UnsetValue) ?
GetDefaultValue(property) :
newValue;
_inheritanceChildren?.Remove(child);
}
if (!Equals(oldValue, newValue))
void IAvaloniaObject.InheritedPropertyChanged<T>(
AvaloniaProperty<T> property,
Optional<T> oldValue,
Optional<T> newValue)
{
if (property.Inherits && !IsSet(property))
{
RaisePropertyChanged(property, oldValue, newValue, (BindingPriority)priority);
RaisePropertyChanged(property, oldValue, newValue, BindingPriority.LocalValue);
}
}
/// <inheritdoc/>
Delegate[] IAvaloniaObjectDebug.GetPropertyChangedSubscribers()
{
return _propertyChanged?.GetInvocationList();
}
void IValueSink.ValueChanged<T>(
StyledPropertyBase<T> property,
BindingPriority priority,
Optional<T> oldValue,
BindingValue<T> newValue)
{
oldValue = oldValue.HasValue ? oldValue : GetInheritedOrDefault(property);
newValue = newValue.HasValue ? newValue : newValue.WithValue(GetInheritedOrDefault(property));
LogIfError(property, newValue);
if (!EqualityComparer<T>.Default.Equals(oldValue.Value, newValue.Value))
{
RaisePropertyChanged(property, oldValue, newValue, priority);
Logger.TryGet(LogEventLevel.Verbose)?.Log(
LogArea.Property,
@ -421,39 +525,32 @@ namespace Avalonia
(BindingPriority)priority);
}
}
internal void BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification)
{
LogIfError(property, notification);
UpdateDataValidation(property, notification);
}
/// <inheritdoc/>
Delegate[] IAvaloniaObjectDebug.GetPropertyChangedSubscribers()
{
return _propertyChanged?.GetInvocationList();
}
void IValueSink.Completed(AvaloniaProperty property, IPriorityValueEntry entry) { }
/// <summary>
/// Gets all priority values set on the object.
/// Called for each inherited property when the <see cref="InheritanceParent"/> changes.
/// </summary>
/// <returns>A collection of property/value tuples.</returns>
internal IDictionary<AvaloniaProperty, object> GetSetValues() => Values?.GetSetValues();
/// <summary>
/// Forces revalidation of properties when a property value changes.
/// </summary>
/// <param name="property">The property to that affects validation.</param>
/// <param name="affected">The affected properties.</param>
protected static void AffectsValidation(AvaloniaProperty property, params AvaloniaProperty[] affected)
/// <typeparam name="T">The type of the property value.</typeparam>
/// <param name="property">The property.</param>
/// <param name="oldParent">The old inheritance parent.</param>
internal void InheritanceParentChanged<T>(
StyledPropertyBase<T> property,
IAvaloniaObject oldParent)
{
property.Changed.Subscribe(e =>
var oldValue = oldParent switch
{
foreach (var p in affected)
{
e.Sender.Revalidate(p);
}
});
AvaloniaObject o => o.GetValueOrInheritedOrDefault(property),
null => property.GetDefaultValue(GetType()),
_ => oldParent.GetValue(property)
};
var newValue = GetInheritedOrDefault(property);
if (!EqualityComparer<T>.Default.Equals(oldValue, newValue))
{
RaisePropertyChanged(property, oldValue, newValue);
}
}
/// <summary>
@ -477,18 +574,25 @@ namespace Avalonia
/// enabled.
/// </summary>
/// <param name="property">The property.</param>
/// <param name="status">The new validation status.</param>
protected virtual void UpdateDataValidation(
AvaloniaProperty property,
BindingNotification status)
/// <param name="value">The new binding value for the property.</param>
protected virtual void UpdateDataValidation<T>(
AvaloniaProperty<T> property,
BindingValue<T> value)
{
}
/// <summary>
/// Called when a avalonia property changes on the object.
/// </summary>
/// <param name="e">The event arguments.</param>
protected virtual void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e)
/// <param name="property">The property whose value has changed.</param>
/// <param name="oldValue">The old value of the property.</param>
/// <param name="newValue">The new value of the property.</param>
/// <param name="priority">The priority of the new value.</param>
protected virtual void OnPropertyChanged<T>(
AvaloniaProperty<T> property,
Optional<T> oldValue,
BindingValue<T> newValue,
BindingPriority priority)
{
}
@ -499,40 +603,57 @@ namespace Avalonia
/// <param name="oldValue">The old property value.</param>
/// <param name="newValue">The new property value.</param>
/// <param name="priority">The priority of the binding that produced the value.</param>
protected internal void RaisePropertyChanged(
AvaloniaProperty property,
object oldValue,
object newValue,
protected internal void RaisePropertyChanged<T>(
AvaloniaProperty<T> property,
Optional<T> oldValue,
BindingValue<T> newValue,
BindingPriority priority = BindingPriority.LocalValue)
{
Contract.Requires<ArgumentNullException>(property != null);
VerifyAccess();
property = property ?? throw new ArgumentNullException(nameof(property));
AvaloniaPropertyChangedEventArgs e = new AvaloniaPropertyChangedEventArgs(
this,
property,
oldValue,
newValue,
priority);
VerifyAccess();
property.Notifying?.Invoke(this, true);
try
{
OnPropertyChanged(e);
property.NotifyChanged(e);
AvaloniaPropertyChangedEventArgs<T> e = null;
var hasChanged = property.HasChangedSubscriptions;
if (hasChanged || _propertyChanged != null)
{
e = new AvaloniaPropertyChangedEventArgs<T>(
this,
property,
oldValue,
newValue,
priority);
}
OnPropertyChanged(property, oldValue, newValue, priority);
if (hasChanged)
{
property.NotifyChanged(e);
}
_propertyChanged?.Invoke(this, e);
if (_inpcChanged != null)
{
PropertyChangedEventArgs e2 = new PropertyChangedEventArgs(property.Name);
_inpcChanged(this, e2);
var inpce = new PropertyChangedEventArgs(property.Name);
_inpcChanged(this, inpce);
}
if (property.Inherits)
if (property.Inherits && _inheritanceChildren != null)
{
_inheritablePropertyChanged?.Invoke(this, e);
foreach (var child in _inheritanceChildren)
{
child.InheritedPropertyChanged(
property,
oldValue,
newValue.ToOptional());
}
}
}
finally
@ -561,216 +682,103 @@ namespace Avalonia
return false;
}
DeferredSetter<T> setter = Values.GetDirectDeferredSetter(property);
return setter.SetAndNotify(this, property, ref field, value);
var old = field;
field = value;
RaisePropertyChanged(property, old, value);
return true;
}
/// <summary>
/// Tries to cast a value to a type, taking into account that the value may be a
/// <see cref="BindingNotification"/>.
/// </summary>
/// <param name="value">The value.</param>
/// <param name="type">The type.</param>
/// <returns>The cast value, or a <see cref="BindingNotification"/>.</returns>
private static object CastOrDefault(object value, Type type)
private T GetInheritedOrDefault<T>(StyledPropertyBase<T> property)
{
var notification = value as BindingNotification;
if (notification == null)
if (property.Inherits && InheritanceParent is AvaloniaObject o)
{
return TypeUtilities.ConvertImplicitOrDefault(value, type);
return o.GetValueOrInheritedOrDefault(property);
}
else
{
if (notification.HasValue)
{
notification.SetValue(TypeUtilities.ConvertImplicitOrDefault(notification.Value, type));
}
return notification;
}
return property.GetDefaultValue(GetType());
}
/// <summary>
/// Gets the default value for a property.
/// </summary>
/// <param name="property">The property.</param>
/// <returns>The default value.</returns>
private object GetDefaultValue(AvaloniaProperty property)
private T GetValueOrInheritedOrDefault<T>(StyledPropertyBase<T> property)
{
if (property.Inherits && InheritanceParent is AvaloniaObject aobj)
return aobj.GetValueOrDefaultUnchecked(property);
return ((IStyledPropertyAccessor) property).GetDefaultValue(GetType());
}
var o = this;
var inherits = property.Inherits;
var value = default(T);
/// <summary>
/// Gets the value or default value for a property.
/// </summary>
/// <param name="property">The property.</param>
/// <returns>The default value.</returns>
private object GetValueOrDefaultUnchecked(AvaloniaProperty property)
{
var aobj = this;
var valuestore = aobj._values;
if (valuestore != null)
while (o != null)
{
var result = valuestore.GetValue(property);
if (result != AvaloniaProperty.UnsetValue)
{
return result;
}
}
if (property.Inherits)
{
while (aobj.InheritanceParent is AvaloniaObject parent)
{
aobj = parent;
valuestore = aobj._values;
if (valuestore != null)
{
var result = valuestore.GetValue(property);
if (result != AvaloniaProperty.UnsetValue)
{
return result;
}
}
}
}
return ((IStyledPropertyAccessor)property).GetDefaultValue(GetType());
}
/// <summary>
/// Sets the value of a direct property.
/// </summary>
/// <param name="property">The property.</param>
/// <param name="value">The value.</param>
private void SetDirectValue(AvaloniaProperty property, object value)
{
void Set()
{
var notification = value as BindingNotification;
var values = o._values;
if (notification != null)
if (values?.TryGetValue(property, out value) == true)
{
LogIfError(property, notification);
value = notification.Value;
return value;
}
if (notification == null || notification.ErrorType == BindingErrorType.Error || notification.HasValue)
if (!inherits)
{
var metadata = (IDirectPropertyMetadata)property.GetMetadata(GetType());
var accessor = (IDirectPropertyAccessor)GetRegistered(property);
var finalValue = value == AvaloniaProperty.UnsetValue ?
metadata.UnsetValue : value;
LogPropertySet(property, value, BindingPriority.LocalValue);
accessor.SetValue(this, finalValue);
break;
}
if (notification != null)
{
UpdateDataValidation(property, notification);
}
o = o.InheritanceParent as AvaloniaObject;
}
if (Dispatcher.UIThread.CheckAccess())
{
Set();
}
else
{
Dispatcher.UIThread.Post(Set);
}
return property.GetDefaultValue(GetType());
}
/// <summary>
/// Sets the value of a styled property.
/// Sets the value of a direct property.
/// </summary>
/// <param name="property">The property.</param>
/// <param name="value">The value.</param>
/// <param name="priority">The priority of the value.</param>
private void SetStyledValue(AvaloniaProperty property, object value, BindingPriority priority)
private void SetDirectValueUnchecked<T>(DirectPropertyBase<T> property, T value)
{
var notification = value as BindingNotification;
var p = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, property);
// We currently accept BindingNotifications for non-direct properties but we just
// strip them to their underlying value.
if (notification != null)
if (value is UnsetValueType)
{
if (!notification.HasValue)
{
return;
}
else
{
value = notification.Value;
}
p.InvokeSetter(this, p.GetUnsetValue(GetType()));
}
var originalValue = value;
if (!TypeUtilities.TryConvertImplicit(property.PropertyType, value, out value))
else if (!(value is DoNothingType))
{
throw new ArgumentException(string.Format(
"Invalid value for Property '{0}': '{1}' ({2})",
property.Name,
originalValue,
originalValue?.GetType().FullName ?? "(null)"));
p.InvokeSetter(this, value);
}
LogPropertySet(property, value, priority);
Values.AddValue(property, value, (int)priority);
}
/// <summary>
/// Given a direct property, returns a registered avalonia property that is equivalent or
/// throws if not found.
/// Sets the value of a direct property.
/// </summary>
/// <param name="property">The property.</param>
/// <returns>The registered property.</returns>
private AvaloniaProperty GetRegistered(AvaloniaProperty property)
/// <param name="value">The value.</param>
private void SetDirectValueUnchecked<T>(DirectPropertyBase<T> property, BindingValue<T> value)
{
var direct = property as IDirectPropertyAccessor;
if (direct == null)
{
throw new AvaloniaInternalException(
"AvaloniaObject.GetRegistered should only be called for direct properties");
}
var p = AvaloniaPropertyRegistry.Instance.FindRegisteredDirect(this, property);
if (property.OwnerType.IsAssignableFrom(GetType()))
if (p == null)
{
return property;
throw new ArgumentException($"Property '{property.Name} not registered on '{this.GetType()}");
}
var result = AvaloniaPropertyRegistry.Instance.GetRegistered(this)
.FirstOrDefault(x => x == property);
LogIfError(property, value);
if (result == null)
switch (value.Type)
{
throw new ArgumentException($"Property '{property.Name} not registered on '{this.GetType()}");
case BindingValueType.UnsetValue:
case BindingValueType.BindingError:
var fallback = value.HasValue ? value : value.WithValue(property.GetUnsetValue(GetType()));
property.InvokeSetter(this, fallback);
break;
case BindingValueType.DataValidationError:
property.InvokeSetter(this, value);
break;
case BindingValueType.Value:
case BindingValueType.BindingErrorWithFallback:
case BindingValueType.DataValidationErrorWithFallback:
property.InvokeSetter(this, value);
break;
}
return result;
}
/// <summary>
/// Called when a property is changed on the current <see cref="InheritanceParent"/>.
/// </summary>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event args.</param>
/// <remarks>
/// Checks for changes in an inherited property value.
/// </remarks>
private void ParentPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
{
Contract.Requires<ArgumentNullException>(e != null);
if (e.Property.Inherits && !IsSet(e.Property))
if (p.IsDataValidationEnabled)
{
RaisePropertyChanged(e.Property, e.OldValue, e.NewValue, BindingPriority.LocalValue);
UpdateDataValidation(property, value);
}
}
@ -779,7 +787,7 @@ namespace Avalonia
/// </summary>
/// <param name="o">The observable.</param>
/// <returns>The description.</returns>
private string GetDescription(IObservable<object> o)
private string GetDescription(object o)
{
var description = o as IDescription;
return description?.Description ?? o.ToString();
@ -789,12 +797,12 @@ namespace Avalonia
/// Logs a mesage if the notification represents a binding error.
/// </summary>
/// <param name="property">The property being bound.</param>
/// <param name="notification">The binding notification.</param>
private void LogIfError(AvaloniaProperty property, BindingNotification notification)
/// <param name="value">The binding notification.</param>
private void LogIfError<T>(AvaloniaProperty property, BindingValue<T> value)
{
if (notification.ErrorType == BindingErrorType.Error)
if (value.HasError)
{
if (notification.Error is AggregateException aggregate)
if (value.Error is AggregateException aggregate)
{
foreach (var inner in aggregate.InnerExceptions)
{
@ -803,7 +811,7 @@ namespace Avalonia
}
else
{
LogBindingError(property, notification.Error);
LogBindingError(property, value.Error);
}
}
}
@ -814,7 +822,7 @@ namespace Avalonia
/// <param name="property">The property.</param>
/// <param name="value">The new value.</param>
/// <param name="priority">The priority.</param>
private void LogPropertySet(AvaloniaProperty property, object value, BindingPriority priority)
private void LogPropertySet<T>(AvaloniaProperty<T> property, T value, BindingPriority priority)
{
Logger.TryGet(LogEventLevel.Verbose)?.Log(
LogArea.Property,
@ -825,16 +833,16 @@ namespace Avalonia
priority);
}
private class DirectBindingSubscription : IObserver<object>, IDisposable
private class DirectBindingSubscription<T> : IObserver<BindingValue<T>>, IDisposable
{
readonly AvaloniaObject _owner;
readonly AvaloniaProperty _property;
IDisposable _subscription;
private readonly AvaloniaObject _owner;
private readonly DirectPropertyBase<T> _property;
private readonly IDisposable _subscription;
public DirectBindingSubscription(
AvaloniaObject owner,
AvaloniaProperty property,
IObservable<object> source)
DirectPropertyBase<T> property,
IObservable<BindingValue<T>> source)
{
_owner = owner;
_property = property;
@ -850,11 +858,22 @@ namespace Avalonia
public void OnCompleted() => Dispose();
public void OnError(Exception error) => Dispose();
public void OnNext(object value)
public void OnNext(BindingValue<T> value)
{
var castValue = CastOrDefault(value, _property.PropertyType);
_owner.SetDirectValue(_property, castValue);
if (Dispatcher.UIThread.CheckAccess())
{
_owner.SetDirectValueUnchecked(_property, value);
}
else
{
// To avoid allocating closure in the outer scope we need to capture variables
// locally. This allows us to skip most of the allocations when on UI thread.
var instance = _owner;
var property = _property;
var newValue = value;
Dispatcher.UIThread.Post(() => instance.SetDirectValueUnchecked(property, newValue));
}
}
}
}

146
src/Avalonia.Base/AvaloniaObjectExtensions.cs

@ -68,6 +68,51 @@ namespace Avalonia
return new AvaloniaPropertyObservable<T>(o, property);
}
/// <summary>
/// Gets an observable for a <see cref="AvaloniaProperty"/>.
/// </summary>
/// <param name="o">The object.</param>
/// <param name="property">The property.</param>
/// <returns>
/// An observable which fires immediately with the current value of the property on the
/// object and subsequently each time the property value changes.
/// </returns>
/// <remarks>
/// The subscription to <paramref name="o"/> is created using a weak reference.
/// </remarks>
public static IObservable<BindingValue<object>> GetBindingObservable(
this IAvaloniaObject o,
AvaloniaProperty property)
{
Contract.Requires<ArgumentNullException>(o != null);
Contract.Requires<ArgumentNullException>(property != null);
return new AvaloniaPropertyBindingObservable<object>(o, property);
}
/// <summary>
/// Gets an observable for a <see cref="AvaloniaProperty"/>.
/// </summary>
/// <param name="o">The object.</param>
/// <typeparam name="T">The property type.</typeparam>
/// <param name="property">The property.</param>
/// <returns>
/// An observable which fires immediately with the current value of the property on the
/// object and subsequently each time the property value changes.
/// </returns>
/// <remarks>
/// The subscription to <paramref name="o"/> is created using a weak reference.
/// </remarks>
public static IObservable<BindingValue<T>> GetBindingObservable<T>(
this IAvaloniaObject o,
AvaloniaProperty<T> property)
{
Contract.Requires<ArgumentNullException>(o != null);
Contract.Requires<ArgumentNullException>(property != null);
return new AvaloniaPropertyBindingObservable<T>(o, property);
}
/// <summary>
/// Gets an observable that listens for property changed events for an
/// <see cref="AvaloniaProperty"/>.
@ -134,6 +179,107 @@ namespace Avalonia
o.GetObservable(property));
}
/// <summary>
/// Gets a subject for a <see cref="AvaloniaProperty"/>.
/// </summary>
/// <param name="o">The object.</param>
/// <param name="property">The property.</param>
/// <param name="priority">
/// The priority with which binding values are written to the object.
/// </param>
/// <returns>
/// An <see cref="ISubject{Object}"/> which can be used for two-way binding to/from the
/// property.
/// </returns>
public static ISubject<BindingValue<object>> GetBindingSubject(
this IAvaloniaObject o,
AvaloniaProperty property,
BindingPriority priority = BindingPriority.LocalValue)
{
return Subject.Create<BindingValue<object>>(
Observer.Create<BindingValue<object>>(x =>
{
if (x.HasValue)
{
o.SetValue(property, x.Value, priority);
}
}),
o.GetBindingObservable(property));
}
/// <summary>
/// Gets a subject for a <see cref="AvaloniaProperty"/>.
/// </summary>
/// <typeparam name="T">The property type.</typeparam>
/// <param name="o">The object.</param>
/// <param name="property">The property.</param>
/// <param name="priority">
/// The priority with which binding values are written to the object.
/// </param>
/// <returns>
/// An <see cref="ISubject{T}"/> which can be used for two-way binding to/from the
/// property.
/// </returns>
public static ISubject<BindingValue<T>> GetBindingSubject<T>(
this IAvaloniaObject o,
AvaloniaProperty<T> property,
BindingPriority priority = BindingPriority.LocalValue)
{
return Subject.Create<BindingValue<T>>(
Observer.Create<BindingValue<T>>(x =>
{
if (x.HasValue)
{
o.SetValue(property, x.Value, priority);
}
}),
o.GetBindingObservable(property));
}
/// <summary>
/// Binds a <see cref="AvaloniaProperty"/> to an observable.
/// </summary>
/// <param name="target">The object.</param>
/// <param name="property">The property.</param>
/// <param name="source">The observable.</param>
/// <param name="priority">The priority of the binding.</param>
/// <returns>
/// A disposable which can be used to terminate the binding.
/// </returns>
public static IDisposable Bind(
this IAvaloniaObject target,
AvaloniaProperty property,
IObservable<object> source,
BindingPriority priority = BindingPriority.LocalValue)
{
return target.Bind(
property,
source.ToBindingValue(),
priority);
}
/// <summary>
/// Binds a <see cref="AvaloniaProperty"/> to an observable.
/// </summary>
/// <param name="target">The object.</param>
/// <param name="property">The property.</param>
/// <param name="source">The observable.</param>
/// <param name="priority">The priority of the binding.</param>
/// <returns>
/// A disposable which can be used to terminate the binding.
/// </returns>
public static IDisposable Bind<T>(
this IAvaloniaObject target,
AvaloniaProperty<T> property,
IObservable<T> source,
BindingPriority priority = BindingPriority.LocalValue)
{
return target.Bind(
property,
source.ToBindingValue(),
priority);
}
/// <summary>
/// Binds a property on an <see cref="IAvaloniaObject"/> to an <see cref="IBinding"/>.
/// </summary>

89
src/Avalonia.Base/AvaloniaProperty.cs

@ -14,7 +14,7 @@ namespace Avalonia
/// <summary>
/// Base class for avalonia properties.
/// </summary>
public class AvaloniaProperty : IEquatable<AvaloniaProperty>
public abstract class AvaloniaProperty : IEquatable<AvaloniaProperty>
{
/// <summary>
/// Represents an unset property value.
@ -183,6 +183,8 @@ namespace Avalonia
/// </summary>
internal int Id { get; }
internal bool HasChangedSubscriptions => _changed?.HasObservers ?? false;
/// <summary>
/// Provides access to a property's binding via the <see cref="AvaloniaObject"/>
/// indexer.
@ -255,7 +257,6 @@ namespace Avalonia
/// <param name="defaultValue">The default value of the property.</param>
/// <param name="inherits">Whether the property inherits its value.</param>
/// <param name="defaultBindingMode">The default binding mode for the property.</param>
/// <param name="validate">A validation function.</param>
/// <param name="notifying">
/// A method that gets called before and after the property starts being notified on an
/// object; the bool argument will be true before and false afterwards. This callback is
@ -267,7 +268,6 @@ namespace Avalonia
TValue defaultValue = default(TValue),
bool inherits = false,
BindingMode defaultBindingMode = BindingMode.OneWay,
Func<TOwner, TValue, TValue> validate = null,
Action<IAvaloniaObject, bool> notifying = null)
where TOwner : IAvaloniaObject
{
@ -275,7 +275,6 @@ namespace Avalonia
var metadata = new StyledPropertyMetadata<TValue>(
defaultValue,
validate: Cast(validate),
defaultBindingMode: defaultBindingMode);
var result = new StyledProperty<TValue>(
@ -298,7 +297,6 @@ namespace Avalonia
/// <param name="defaultValue">The default value of the property.</param>
/// <param name="inherits">Whether the property inherits its value.</param>
/// <param name="defaultBindingMode">The default binding mode for the property.</param>
/// <param name="validate">A validation function.</param>
/// <returns>A <see cref="AvaloniaProperty{TValue}"/></returns>
public static AttachedProperty<TValue> RegisterAttached<TOwner, THost, TValue>(
string name,
@ -312,7 +310,6 @@ namespace Avalonia
var metadata = new StyledPropertyMetadata<TValue>(
defaultValue,
validate: Cast(validate),
defaultBindingMode: defaultBindingMode);
var result = new AttachedProperty<TValue>(name, typeof(TOwner), metadata, inherits);
@ -332,7 +329,6 @@ namespace Avalonia
/// <param name="defaultValue">The default value of the property.</param>
/// <param name="inherits">Whether the property inherits its value.</param>
/// <param name="defaultBindingMode">The default binding mode for the property.</param>
/// <param name="validate">A validation function.</param>
/// <returns>A <see cref="AvaloniaProperty{TValue}"/></returns>
public static AttachedProperty<TValue> RegisterAttached<THost, TValue>(
string name,
@ -347,7 +343,6 @@ namespace Avalonia
var metadata = new StyledPropertyMetadata<TValue>(
defaultValue,
validate: Cast(validate),
defaultBindingMode: defaultBindingMode);
var result = new AttachedProperty<TValue>(name, ownerType, metadata, inherits);
@ -365,9 +360,7 @@ namespace Avalonia
/// <param name="name">The name of the property.</param>
/// <param name="getter">Gets the current value of the property.</param>
/// <param name="setter">Sets the value of the property.</param>
/// <param name="unsetValue">
/// The value to use when the property is set to <see cref="AvaloniaProperty.UnsetValue"/>
/// </param>
/// <param name="unsetValue">The value to use when the property is cleared.</param>
/// <param name="defaultBindingMode">The default binding mode for the property.</param>
/// <param name="enableDataValidation">
/// Whether the property is interested in data validation.
@ -383,13 +376,18 @@ namespace Avalonia
where TOwner : IAvaloniaObject
{
Contract.Requires<ArgumentNullException>(name != null);
Contract.Requires<ArgumentNullException>(getter != null);
var metadata = new DirectPropertyMetadata<TValue>(
unsetValue: unsetValue,
defaultBindingMode: defaultBindingMode,
enableDataValidation: enableDataValidation);
defaultBindingMode: defaultBindingMode);
var result = new DirectProperty<TOwner, TValue>(name, getter, setter, metadata);
var result = new DirectProperty<TOwner, TValue>(
name,
getter,
setter,
metadata,
enableDataValidation);
AvaloniaPropertyRegistry.Instance.Register(typeof(TOwner), result);
return result;
}
@ -483,6 +481,12 @@ namespace Avalonia
/// </summary>
internal bool HasNotifyInitializedObservers => _initialized.HasObservers;
/// <summary>
/// Notifies the <see cref="Initialized"/> observable.
/// </summary>
/// <param name="o">The object being initialized.</param>
internal abstract void NotifyInitialized(IAvaloniaObject o);
/// <summary>
/// Notifies the <see cref="Initialized"/> observable.
/// </summary>
@ -501,6 +505,42 @@ namespace Avalonia
_changed.OnNext(e);
}
/// <summary>
/// Routes an untyped ClearValue call to a typed call.
/// </summary>
/// <param name="o">The object instance.</param>
internal abstract void RouteClearValue(IAvaloniaObject o);
/// <summary>
/// Routes an untyped GetValue call to a typed call.
/// </summary>
/// <param name="o">The object instance.</param>
internal abstract object RouteGetValue(IAvaloniaObject o);
/// <summary>
/// Routes an untyped SetValue call to a typed call.
/// </summary>
/// <param name="o">The object instance.</param>
/// <param name="value">The value.</param>
/// <param name="priority">The priority.</param>
internal abstract void RouteSetValue(
IAvaloniaObject o,
object value,
BindingPriority priority);
/// <summary>
/// Routes an untyped Bind call to a typed call.
/// </summary>
/// <param name="o">The object instance.</param>
/// <param name="source">The binding source.</param>
/// <param name="priority">The priority.</param>
internal abstract IDisposable RouteBind(
IAvaloniaObject o,
IObservable<BindingValue<object>> source,
BindingPriority priority);
internal abstract void RouteInheritanceParentChanged(AvaloniaObject o, IAvaloniaObject oldParent);
/// <summary>
/// Overrides the metadata for the property on the specified type.
/// </summary>
@ -555,28 +595,15 @@ namespace Avalonia
return _defaultMetadata;
}
[DebuggerHidden]
private static Func<IAvaloniaObject, TValue, TValue> Cast<TOwner, TValue>(Func<TOwner, TValue, TValue> f)
where TOwner : IAvaloniaObject
{
if (f != null)
{
return (o, v) => (o is TOwner) ? f((TOwner)o, v) : v;
}
else
{
return null;
}
}
}
/// <summary>
/// Class representing the <see cref="AvaloniaProperty.UnsetValue"/>.
/// </summary>
public class UnsetValueType
public sealed class UnsetValueType
{
internal UnsetValueType() { }
/// <summary>
/// Returns the string representation of the <see cref="AvaloniaProperty.UnsetValue"/>.
/// </summary>

40
src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs

@ -4,32 +4,20 @@
using System;
using Avalonia.Data;
#nullable enable
namespace Avalonia
{
/// <summary>
/// Provides information for a avalonia property change.
/// </summary>
public class AvaloniaPropertyChangedEventArgs : EventArgs
public abstract class AvaloniaPropertyChangedEventArgs : EventArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="AvaloniaPropertyChangedEventArgs"/> class.
/// </summary>
/// <param name="sender">The object that the property changed on.</param>
/// <param name="property">The property that changed.</param>
/// <param name="oldValue">The old value of the property.</param>
/// <param name="newValue">The new value of the property.</param>
/// <param name="priority">The priority of the binding that produced the value.</param>
public AvaloniaPropertyChangedEventArgs(
AvaloniaObject sender,
AvaloniaProperty property,
object oldValue,
object newValue,
IAvaloniaObject sender,
BindingPriority priority)
{
Sender = sender;
Property = property;
OldValue = oldValue;
NewValue = newValue;
Priority = priority;
}
@ -37,7 +25,7 @@ namespace Avalonia
/// Gets the <see cref="AvaloniaObject"/> that the property changed on.
/// </summary>
/// <value>The sender object.</value>
public AvaloniaObject Sender { get; private set; }
public IAvaloniaObject Sender { get; }
/// <summary>
/// Gets the property that changed.
@ -45,30 +33,36 @@ namespace Avalonia
/// <value>
/// The property that changed.
/// </value>
public AvaloniaProperty Property { get; private set; }
public AvaloniaProperty Property => GetProperty();
/// <summary>
/// Gets the old value of the property.
/// </summary>
/// <value>
/// The old value of the property.
/// The old value of the property or <see cref="AvaloniaProperty.UnsetValue"/> if the
/// property previously had no value.
/// </value>
public object OldValue { get; private set; }
public object? OldValue => GetOldValue();
/// <summary>
/// Gets the new value of the property.
/// </summary>
/// <value>
/// The new value of the property.
/// The new value of the property or <see cref="AvaloniaProperty.UnsetValue"/> if the
/// property previously had no value.
/// </value>
public object NewValue { get; private set; }
public object? NewValue => GetNewValue();
/// <summary>
/// Gets the priority of the binding that produced the value.
/// </summary>
/// <value>
/// The priority of the binding that produced the value.
/// The priority of the new value.
/// </value>
public BindingPriority Priority { get; private set; }
protected abstract AvaloniaProperty GetProperty();
protected abstract object? GetOldValue();
protected abstract object? GetNewValue();
}
}

67
src/Avalonia.Base/AvaloniaPropertyChangedEventArgs`1.cs

@ -0,0 +1,67 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using Avalonia.Data;
#nullable enable
namespace Avalonia
{
/// <summary>
/// Provides information for a avalonia property change.
/// </summary>
public class AvaloniaPropertyChangedEventArgs<T> : AvaloniaPropertyChangedEventArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="AvaloniaPropertyChangedEventArgs"/> class.
/// </summary>
/// <param name="sender">The object that the property changed on.</param>
/// <param name="property">The property that changed.</param>
/// <param name="oldValue">The old value of the property.</param>
/// <param name="newValue">The new value of the property.</param>
/// <param name="priority">The priority of the binding that produced the value.</param>
public AvaloniaPropertyChangedEventArgs(
IAvaloniaObject sender,
AvaloniaProperty<T> property,
Optional<T> oldValue,
BindingValue<T> newValue,
BindingPriority priority)
: base(sender, priority)
{
Property = property;
OldValue = oldValue;
NewValue = newValue;
}
/// <summary>
/// Gets the property that changed.
/// </summary>
/// <value>
/// The property that changed.
/// </value>
public new AvaloniaProperty<T> Property { get; }
/// <summary>
/// Gets the old value of the property.
/// </summary>
/// <value>
/// The old value of the property.
/// </value>
public new Optional<T> OldValue { get; private set; }
/// <summary>
/// Gets the new value of the property.
/// </summary>
/// <value>
/// The new value of the property.
/// </value>
public new BindingValue<T> NewValue { get; private set; }
protected override AvaloniaProperty GetProperty() => Property;
protected override object? GetOldValue() => OldValue.ValueOrDefault(AvaloniaProperty.UnsetValue);
protected override object? GetNewValue() => NewValue.ValueOrDefault(AvaloniaProperty.UnsetValue);
}
}

115
src/Avalonia.Base/AvaloniaPropertyRegistry.cs

@ -20,10 +20,14 @@ namespace Avalonia
new Dictionary<Type, Dictionary<int, AvaloniaProperty>>();
private readonly Dictionary<Type, Dictionary<int, AvaloniaProperty>> _attached =
new Dictionary<Type, Dictionary<int, AvaloniaProperty>>();
private readonly Dictionary<Type, Dictionary<int, AvaloniaProperty>> _direct =
new Dictionary<Type, Dictionary<int, AvaloniaProperty>>();
private readonly Dictionary<Type, List<AvaloniaProperty>> _registeredCache =
new Dictionary<Type, List<AvaloniaProperty>>();
private readonly Dictionary<Type, List<AvaloniaProperty>> _attachedCache =
new Dictionary<Type, List<AvaloniaProperty>>();
private readonly Dictionary<Type, List<AvaloniaProperty>> _directCache =
new Dictionary<Type, List<AvaloniaProperty>>();
private readonly Dictionary<Type, List<PropertyInitializationData>> _initializedCache =
new Dictionary<Type, List<PropertyInitializationData>>();
private readonly Dictionary<Type, List<AvaloniaProperty>> _inheritedCache =
@ -105,6 +109,37 @@ namespace Avalonia
return result;
}
/// <summary>
/// Gets all direct <see cref="AvaloniaProperty"/>s registered on a type.
/// </summary>
/// <param name="type">The type.</param>
/// <returns>A collection of <see cref="AvaloniaProperty"/> definitions.</returns>
public IEnumerable<AvaloniaProperty> GetRegisteredDirect(Type type)
{
Contract.Requires<ArgumentNullException>(type != null);
if (_directCache.TryGetValue(type, out var result))
{
return result;
}
var t = type;
result = new List<AvaloniaProperty>();
while (t != null)
{
if (_direct.TryGetValue(t, out var direct))
{
result.AddRange(direct.Values);
}
t = t.BaseType;
}
_directCache.Add(type, result);
return result;
}
/// <summary>
/// Gets all inherited <see cref="AvaloniaProperty"/>s registered on a type.
/// </summary>
@ -150,13 +185,29 @@ namespace Avalonia
/// </summary>
/// <param name="o">The object.</param>
/// <returns>A collection of <see cref="AvaloniaProperty"/> definitions.</returns>
public IEnumerable<AvaloniaProperty> GetRegistered(AvaloniaObject o)
public IEnumerable<AvaloniaProperty> GetRegistered(IAvaloniaObject o)
{
Contract.Requires<ArgumentNullException>(o != null);
return GetRegistered(o.GetType());
}
/// <summary>
/// Finds a direct property as registered on an object.
/// </summary>
/// <param name="o">The object.</param>
/// <param name="property">The direct property.</param>
/// <returns>
/// The registered property or null if no matching property found.
/// </returns>
public DirectPropertyBase<T> GetRegisteredDirect<T>(
IAvaloniaObject o,
DirectPropertyBase<T> property)
{
return FindRegisteredDirect(o, property) ??
throw new ArgumentException($"Property '{property.Name} not registered on '{o.GetType()}");
}
/// <summary>
/// Finds a registered property on a type by name.
/// </summary>
@ -192,7 +243,7 @@ namespace Avalonia
/// <exception cref="InvalidOperationException">
/// The property name contains a '.'.
/// </exception>
public AvaloniaProperty FindRegistered(AvaloniaObject o, string name)
public AvaloniaProperty FindRegistered(IAvaloniaObject o, string name)
{
Contract.Requires<ArgumentNullException>(o != null);
Contract.Requires<ArgumentNullException>(name != null);
@ -200,6 +251,34 @@ namespace Avalonia
return FindRegistered(o.GetType(), name);
}
/// <summary>
/// Finds a direct property as registered on an object.
/// </summary>
/// <param name="o">The object.</param>
/// <param name="property">The direct property.</param>
/// <returns>
/// The registered property or null if no matching property found.
/// </returns>
public DirectPropertyBase<T> FindRegisteredDirect<T>(
IAvaloniaObject o,
DirectPropertyBase<T> property)
{
if (property.Owner == o.GetType())
{
return property;
}
foreach (var p in GetRegisteredDirect(o.GetType()))
{
if (p == property)
{
return (DirectPropertyBase<T>)p;
}
}
return null;
}
/// <summary>
/// Finds a registered property by Id.
/// </summary>
@ -265,6 +344,22 @@ namespace Avalonia
inner.Add(property.Id, property);
}
if (property.IsDirect)
{
if (!_direct.TryGetValue(type, out inner))
{
inner = new Dictionary<int, AvaloniaProperty>();
inner.Add(property.Id, property);
_direct.Add(type, inner);
}
else if (!inner.ContainsKey(property.Id))
{
inner.Add(property.Id, property);
}
_directCache.Clear();
}
if (!_properties.ContainsKey(property.Id))
{
_properties.Add(property.Id, property);
@ -318,18 +413,6 @@ namespace Avalonia
var type = o.GetType();
void Notify(AvaloniaProperty property, object value)
{
var e = new AvaloniaPropertyChangedEventArgs(
o,
property,
AvaloniaProperty.UnsetValue,
value,
BindingPriority.Unset);
property.NotifyInitialized(e);
}
if (!_initializedCache.TryGetValue(type, out var initializationData))
{
var visited = new HashSet<AvaloniaProperty>();
@ -370,9 +453,7 @@ namespace Avalonia
continue;
}
object value = data.IsDirect ? data.DirectAccessor.GetValue(o) : data.Value;
Notify(data.Property, value);
data.Property.NotifyInitialized(o);
}
}

33
src/Avalonia.Base/AvaloniaProperty`1.cs

@ -2,6 +2,8 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using Avalonia.Data;
using Avalonia.Utilities;
namespace Avalonia
{
@ -9,7 +11,7 @@ namespace Avalonia
/// A typed avalonia property.
/// </summary>
/// <typeparam name="TValue">The value type of the property.</typeparam>
public class AvaloniaProperty<TValue> : AvaloniaProperty
public abstract class AvaloniaProperty<TValue> : AvaloniaProperty
{
/// <summary>
/// Initializes a new instance of the <see cref="AvaloniaProperty{TValue}"/> class.
@ -40,5 +42,34 @@ namespace Avalonia
: base(source, ownerType, metadata)
{
}
internal override void RouteClearValue(IAvaloniaObject o)
{
o.ClearValue<TValue>(this);
}
protected BindingValue<object> TryConvert(object value)
{
if (value == UnsetValue)
{
return BindingValue<object>.Unset;
}
else if (value == BindingOperations.DoNothing)
{
return BindingValue<object>.DoNothing;
}
if (!TypeUtilities.TryConvertImplicit(PropertyType, value, out var converted))
{
var error = new ArgumentException(string.Format(
"Invalid value for Property '{0}': '{1}' ({2})",
Name,
value,
value?.GetType().FullName ?? "(null)"));
return BindingValue<object>.BindingError(error);
}
return converted;
}
}
}

20
src/Avalonia.Base/Data/BindingNotification.cs

@ -236,6 +236,26 @@ namespace Avalonia.Data
_value = value;
}
public BindingValue<object> ToBindingValue()
{
if (ErrorType == BindingErrorType.None)
{
return HasValue ? new BindingValue<object>(Value) : BindingValue<object>.Unset;
}
else if (ErrorType == BindingErrorType.Error)
{
return BindingValue<object>.BindingError(
Error,
HasValue ? new Optional<object>(Value) : Optional<object>.Empty);
}
else
{
return BindingValue<object>.DataValidationError(
Error,
HasValue ? new Optional<object>(Value) : Optional<object>.Empty);
}
}
/// <inheritdoc/>
public override string ToString()
{

14
src/Avalonia.Base/Data/BindingOperations.cs

@ -5,12 +5,13 @@ using System;
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using Avalonia.Reactive;
namespace Avalonia.Data
{
public static class BindingOperations
{
public static readonly object DoNothing = new object();
public static readonly object DoNothing = new DoNothingType();
/// <summary>
/// Applies an <see cref="InstancedBinding"/> a property on an <see cref="IAvaloniaObject"/>.
@ -77,4 +78,15 @@ namespace Avalonia.Data
}
}
}
public sealed class DoNothingType
{
internal DoNothingType() { }
/// <summary>
/// Returns the string representation of <see cref="BindingOperations.DoNothing"/>.
/// </summary>
/// <returns>The string "(do nothing)".</returns>
public override string ToString() => "(do nothing)";
}
}

406
src/Avalonia.Base/Data/BindingValue.cs

@ -0,0 +1,406 @@
using System;
using Avalonia.Utilities;
#nullable enable
namespace Avalonia.Data
{
/// <summary>
/// Describes the type of a <see cref="BindingValue{T}"/>.
/// </summary>
public enum BindingValueType
{
/// <summary>
/// An unset value: the target property will revert to its unbound state until a new
/// binding value is produced.
/// </summary>
UnsetValue = 0,
/// <summary>
/// Do nothing: the binding value will be ignored.
/// </summary>
DoNothing = 1,
/// <summary>
/// A simple value.
/// </summary>
Value = 2 | HasValue,
/// <summary>
/// A binding error, such as a missing source property.
/// </summary>
BindingError = 3 | HasError,
/// <summary>
/// A data validation error.
/// </summary>
DataValidationError = 4 | HasError,
/// <summary>
/// A binding error with a fallback value.
/// </summary>
BindingErrorWithFallback = BindingError | HasValue,
/// <summary>
/// A data validation error with a fallback value.
/// </summary>
DataValidationErrorWithFallback = DataValidationError | HasValue,
TypeMask = 0x00ff,
HasValue = 0x0100,
HasError = 0x0200,
}
/// <summary>
/// A value passed into a binding.
/// </summary>
/// <typeparam name="T">The value type.</typeparam>
/// <remarks>
/// The avalonia binding system is typed, and as such additional state is stored in this
/// structure. A binding value can be in a number of states, described by the
/// <see cref="Type"/> property:
///
/// - <see cref="BindingValueType.Value"/>: a simple value
/// - <see cref="BindingValueType.UnsetValue"/>: the target property will revert to its unbound
/// state until a new binding value is produced. Represented by
/// <see cref="AvaloniaProperty.UnsetValue"/> in an untyped context
/// - <see cref="BindingValueType.DoNothing"/>: the binding value will be ignored. Represented
/// by <see cref="BindingOperations.DoNothing"/> in an untyped context
/// - <see cref="BindingValueType.BindingError"/>: a binding error, such as a missing source
/// property, with an optional fallback value
/// - <see cref="BindingValueType.DataValidationError"/>: a data validation error, with an
/// optional fallback value
///
/// To create a new binding value you can:
///
/// - For a simple value, call the <see cref="BindingValue{T}"/> constructor or use an implicit
/// conversion from <typeparamref name="T"/>
/// - For an unset value, use <see cref="Unset"/> or simply `default`
/// - For other types, call one of the static factory methods
/// </remarks>
public readonly struct BindingValue<T>
{
private readonly T _value;
/// <summary>
/// Initializes a new instance of the <see cref="BindingValue{T}"/> struct with a type of
/// <see cref="BindingValueType.Value"/>
/// </summary>
/// <param name="value">The value.</param>
public BindingValue(T value)
{
ValidateValue(value);
_value = value;
Type = BindingValueType.Value;
Error = null;
}
private BindingValue(BindingValueType type, T value, Exception? error)
{
_value = value;
Type = type;
Error = error;
}
/// <summary>
/// Gets a value indicating whether the binding value represents either a binding or data
/// validation error.
/// </summary>
public bool HasError => Type.HasFlagCustom(BindingValueType.HasError);
/// <summary>
/// Gets a value indicating whether the binding value has a value.
/// </summary>
public bool HasValue => Type.HasFlagCustom(BindingValueType.HasValue);
/// <summary>
/// Gets the type of the binding value.
/// </summary>
public BindingValueType Type { get; }
/// <summary>
/// Gets the binding value or fallback value.
/// </summary>
/// <exception cref="InvalidOperationException">
/// <see cref="HasValue"/> is false.
/// </exception>
public T Value => HasValue ? _value : throw new InvalidOperationException("BindingValue has no value.");
/// <summary>
/// Gets the binding or data validation error.
/// </summary>
public Exception? Error { get; }
/// <summary>
/// Converts the binding value to an <see cref="Optional{T}"/>.
/// </summary>
/// <returns></returns>
public Optional<T> ToOptional() => HasValue ? new Optional<T>(Value) : default;
/// <inheritdoc/>
public override string ToString() => HasError ? $"Error: {Error!.Message}" : Value?.ToString() ?? "(null)";
/// <summary>
/// Converts the value to untyped representation, using <see cref="AvaloniaProperty.UnsetValue"/>,
/// <see cref="BindingOperations.DoNothing"/> and <see cref="BindingNotification"/> where
/// appropriate.
/// </summary>
/// <returns>The untyped representation of the binding value.</returns>
public object? ToUntyped()
{
return Type switch
{
BindingValueType.UnsetValue => AvaloniaProperty.UnsetValue,
BindingValueType.DoNothing => BindingOperations.DoNothing,
BindingValueType.Value => Value,
BindingValueType.BindingError =>
new BindingNotification(Error, BindingErrorType.Error),
BindingValueType.BindingErrorWithFallback =>
new BindingNotification(Error, BindingErrorType.Error, Value),
BindingValueType.DataValidationError =>
new BindingNotification(Error, BindingErrorType.DataValidationError),
BindingValueType.DataValidationErrorWithFallback =>
new BindingNotification(Error, BindingErrorType.DataValidationError, Value),
_ => throw new NotSupportedException("Invalida BindingValueType."),
};
}
/// <summary>
/// Returns a new binding value with the specified value.
/// </summary>
/// <param name="value">The new value.</param>
/// <returns>The new binding value.</returns>
/// <exception cref="InvalidOperationException">
/// The binding type is <see cref="BindingValueType.UnsetValue"/> or
/// <see cref="BindingValueType.DoNothing"/>.
/// </exception>
public BindingValue<T> WithValue(T value)
{
if (Type == BindingValueType.DoNothing)
{
throw new InvalidOperationException("Cannot add value to DoNothing binding value.");
}
var type = Type == BindingValueType.UnsetValue ? BindingValueType.Value : Type;
return new BindingValue<T>(type | BindingValueType.HasValue, value, Error);
}
/// <summary>
/// Gets the value of the binding value if present, otherwise a default value.
/// </summary>
/// <param name="defaultValue">The default value.</param>
/// <returns>The value.</returns>
public T ValueOrDefault(T defaultValue = default) => HasValue ? Value : defaultValue;
/// <summary>
/// Gets the value of the binding value if present, otherwise a default value.
/// </summary>
/// <param name="defaultValue">The default value.</param>
/// <returns>
/// The value if present and of the correct type, `default(TResult)` if the value is
/// present but not of the correct type or null, or <paramref name="defaultValue"/> if the
/// value is not present.
/// </returns>
public TResult ValueOrDefault<TResult>(TResult defaultValue = default)
{
return HasValue ?
Value is TResult result ? result : default
: defaultValue;
}
/// <summary>
/// Creates a <see cref="BindingValue{T}"/> from an object, handling the special values
/// <see cref="AvaloniaProperty.UnsetValue"/> and <see cref="BindingOperations.DoNothing"/>.
/// </summary>
/// <param name="value">The untyped value.</param>
/// <returns>The typed binding value.</returns>
public static BindingValue<T> FromUntyped(object? value)
{
return value switch
{
UnsetValueType _ => Unset,
DoNothingType _ => DoNothing,
BindingNotification n => n.ToBindingValue().Cast<T>(),
_ => (T)value
};
}
/// <summary>
/// Creates a binding value from an instance of the underlying value type.
/// </summary>
/// <param name="value">The value.</param>
public static implicit operator BindingValue<T>(T value) => new BindingValue<T>(value);
/// <summary>
/// Creates a binding value from an <see cref="Optional{T}"/>.
/// </summary>
/// <param name="optional">The optional value.</param>
public static implicit operator BindingValue<T>(Optional<T> optional)
{
return optional.HasValue ? optional.Value : Unset;
}
/// <summary>
/// Returns a binding value with a type of <see cref="BindingValueType.UnsetValue"/>.
/// </summary>
public static BindingValue<T> Unset => new BindingValue<T>(BindingValueType.UnsetValue, default, null);
/// <summary>
/// Returns a binding value with a type of <see cref="BindingValueType.DoNothing"/>.
/// </summary>
public static BindingValue<T> DoNothing => new BindingValue<T>(BindingValueType.DoNothing, default, null);
/// <summary>
/// Returns a binding value with a type of <see cref="BindingValueType.BindingError"/>.
/// </summary>
/// <param name="e">The binding error.</param>
public static BindingValue<T> BindingError(Exception e)
{
e = e ?? throw new ArgumentNullException("e");
return new BindingValue<T>(BindingValueType.BindingError, default, e);
}
/// <summary>
/// Returns a binding value with a type of <see cref="BindingValueType.BindingErrorWithFallback"/>.
/// </summary>
/// <param name="e">The binding error.</param>
/// <param name="fallbackValue">The fallback value.</param>
public static BindingValue<T> BindingError(Exception e, T fallbackValue)
{
e = e ?? throw new ArgumentNullException("e");
return new BindingValue<T>(BindingValueType.BindingErrorWithFallback, fallbackValue, e);
}
/// <summary>
/// Returns a binding value with a type of <see cref="BindingValueType.BindingError"/> or
/// <see cref="BindingValueType.BindingErrorWithFallback"/>.
/// </summary>
/// <param name="e">The binding error.</param>
/// <param name="fallbackValue">The fallback value.</param>
public static BindingValue<T> BindingError(Exception e, Optional<T> fallbackValue)
{
e = e ?? throw new ArgumentNullException("e");
return new BindingValue<T>(
fallbackValue.HasValue ?
BindingValueType.BindingErrorWithFallback :
BindingValueType.BindingError,
fallbackValue.HasValue ? fallbackValue.Value : default,
e);
}
/// <summary>
/// Returns a binding value with a type of <see cref="BindingValueType.DataValidationError"/>.
/// </summary>
/// <param name="e">The data validation error.</param>
public static BindingValue<T> DataValidationError(Exception e)
{
e = e ?? throw new ArgumentNullException("e");
return new BindingValue<T>(BindingValueType.DataValidationError, default, e);
}
/// <summary>
/// Returns a binding value with a type of <see cref="BindingValueType.DataValidationErrorWithFallback"/>.
/// </summary>
/// <param name="e">The data validation error.</param>
/// <param name="fallbackValue">The fallback value.</param>
public static BindingValue<T> DataValidationError(Exception e, T fallbackValue)
{
e = e ?? throw new ArgumentNullException("e");
return new BindingValue<T>(BindingValueType.DataValidationErrorWithFallback, fallbackValue, e);
}
/// <summary>
/// Returns a binding value with a type of <see cref="BindingValueType.DataValidationError"/> or
/// <see cref="BindingValueType.DataValidationErrorWithFallback"/>.
/// </summary>
/// <param name="e">The binding error.</param>
/// <param name="fallbackValue">The fallback value.</param>
public static BindingValue<T> DataValidationError(Exception e, Optional<T> fallbackValue)
{
e = e ?? throw new ArgumentNullException("e");
return new BindingValue<T>(
fallbackValue.HasValue ?
BindingValueType.DataValidationError :
BindingValueType.DataValidationErrorWithFallback,
fallbackValue.HasValue ? fallbackValue.Value : default,
e);
}
private static void ValidateValue(T value)
{
if (value is UnsetValueType)
{
throw new InvalidOperationException("AvaloniaValue.UnsetValue is not a valid value for BindingValue<>.");
}
if (value is DoNothingType)
{
throw new InvalidOperationException("BindingOperations.DoNothing is not a valid value for BindingValue<>.");
}
}
}
public static class BindingValueExtensions
{
/// <summary>
/// Casts the type of a <see cref="BindingValue{T}"/> using only the C# cast operator.
/// </summary>
/// <typeparam name="T">The target type.</typeparam>
/// <param name="value">The binding value.</param>
/// <returns>The cast value.</returns>
public static BindingValue<T> Cast<T>(this BindingValue<object> value)
{
return value.Type switch
{
BindingValueType.DoNothing => BindingValue<T>.DoNothing,
BindingValueType.UnsetValue => BindingValue<T>.Unset,
BindingValueType.Value => new BindingValue<T>((T)value.Value),
BindingValueType.BindingError => BindingValue<T>.BindingError(value.Error!),
BindingValueType.BindingErrorWithFallback => BindingValue<T>.BindingError(
value.Error!,
(T)value.Value),
BindingValueType.DataValidationError => BindingValue<T>.DataValidationError(value.Error!),
BindingValueType.DataValidationErrorWithFallback => BindingValue<T>.DataValidationError(
value.Error!,
(T)value.Value),
_ => throw new NotSupportedException("Invalid BindingValue type."),
};
}
/// <summary>
/// Casts the type of a <see cref="BindingValue{T}"/> using the implicit conversions
/// allowed by the C# language.
/// </summary>
/// <typeparam name="T">The target type.</typeparam>
/// <param name="value">The binding value.</param>
/// <returns>The cast value.</returns>
/// <remarks>
/// Note that this method uses reflection and as such may be slow.
/// </remarks>
public static BindingValue<T> Convert<T>(this BindingValue<object> value)
{
return value.Type switch
{
BindingValueType.DoNothing => BindingValue<T>.DoNothing,
BindingValueType.UnsetValue => BindingValue<T>.Unset,
BindingValueType.Value => new BindingValue<T>(TypeUtilities.ConvertImplicit<T>(value.Value)),
BindingValueType.BindingError => BindingValue<T>.BindingError(value.Error!),
BindingValueType.BindingErrorWithFallback => BindingValue<T>.BindingError(
value.Error!,
TypeUtilities.ConvertImplicit<T>(value.Value)),
BindingValueType.DataValidationError => BindingValue<T>.DataValidationError(value.Error!),
BindingValueType.DataValidationErrorWithFallback => BindingValue<T>.DataValidationError(
value.Error!,
TypeUtilities.ConvertImplicit<T>(value.Value)),
_ => throw new NotSupportedException("Invalid BindingValue type."),
};
}
}
}

129
src/Avalonia.Base/Data/Optional.cs

@ -0,0 +1,129 @@
using System;
using System.Collections.Generic;
#nullable enable
namespace Avalonia.Data
{
/// <summary>
/// An optional typed value.
/// </summary>
/// <typeparam name="T">The value type.</typeparam>
/// <remarks>
/// This struct is similar to <see cref="Nullable{T}"/> except it also accepts reference types:
/// note that null is a valid value for reference types. It is also similar to
/// <see cref="BindingValue{T}"/> but has only two states: "value present" and "value missing".
///
/// To create a new optional value you can:
///
/// - For a simple value, call the <see cref="Optional{T}"/> constructor or use an implicit
/// conversion from <typeparamref name="T"/>
/// - For an missing value, use <see cref="Empty"/> or simply `default`
/// </remarks>
public struct Optional<T>
{
private readonly T _value;
/// <summary>
/// Initializes a new instance of the <see cref="Optional{T}"/> struct with value.
/// </summary>
/// <param name="value">The value.</param>
public Optional(T value)
{
_value = value;
HasValue = true;
}
/// <summary>
/// Gets a value indicating whether a value is present.
/// </summary>
public bool HasValue { get; }
/// <summary>
/// Gets the value.
/// </summary>
/// <exception cref="InvalidOperationException">
/// <see cref="HasValue"/> is false.
/// </exception>
public T Value => HasValue ? _value : throw new InvalidOperationException("Optional has no value.");
/// <inheritdoc/>
public override bool Equals(object obj) => obj is Optional<T> o && this == o;
/// <inheritdoc/>
public override int GetHashCode() => HasValue ? Value!.GetHashCode() : 0;
/// <summary>
/// Casts the value (if any) to an <see cref="object"/>.
/// </summary>
/// <returns>The cast optional value.</returns>
public Optional<object> ToObject() => HasValue ? new Optional<object>(Value) : default;
/// <inheritdoc/>
public override string ToString() => HasValue ? Value?.ToString() ?? "(null)" : "(empty)";
/// <summary>
/// Gets the value if present, otherwise a default value.
/// </summary>
/// <param name="defaultValue">The default value.</param>
/// <returns>The value.</returns>
public T ValueOrDefault(T defaultValue = default) => HasValue ? Value : defaultValue;
/// <summary>
/// Gets the value if present, otherwise a default value.
/// </summary>
/// <param name="defaultValue">The default value.</param>
/// <returns>
/// The value if present and of the correct type, `default(TResult)` if the value is
/// present but not of the correct type or null, or <paramref name="defaultValue"/> if the
/// value is not present.
/// </returns>
public TResult ValueOrDefault<TResult>(TResult defaultValue = default)
{
return HasValue ?
Value is TResult result ? result : default
: defaultValue;
}
/// <summary>
/// Creates an <see cref="Optional{T}"/> from an instance of the underlying value type.
/// </summary>
/// <param name="value">The value.</param>
public static implicit operator Optional<T>(T value) => new Optional<T>(value);
/// <summary>
/// Compares two <see cref="Optional{T}"/>s for inequality.
/// </summary>
/// <param name="x">The first value.</param>
/// <param name="y">The second value.</param>
/// <returns>True if the values are unequal; otherwise false.</returns>
public static bool operator !=(Optional<T> x, Optional<T> y) => !(x == y);
/// <summary>
/// Compares two <see cref="Optional{T}"/>s for equality.
/// </summary>
/// <param name="x">The first value.</param>
/// <param name="y">The second value.</param>
/// <returns>True if the values are equal; otherwise false.</returns>
public static bool operator==(Optional<T> x, Optional<T> y)
{
if (!x.HasValue && !y.HasValue)
{
return true;
}
else if (x.HasValue && y.HasValue)
{
return EqualityComparer<T>.Default.Equals(x.Value, y.Value);
}
else
{
return false;
}
}
/// <summary>
/// Returns an <see cref="Optional{T}"/> without a value.
/// </summary>
public static Optional<T> Empty => default;
}
}

58
src/Avalonia.Base/Diagnostics/AvaloniaObjectExtensions.cs

@ -1,6 +1,7 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using Avalonia.Data;
namespace Avalonia.Diagnostics
@ -21,35 +22,36 @@ namespace Avalonia.Diagnostics
/// </returns>
public static AvaloniaPropertyValue GetDiagnostic(this AvaloniaObject o, AvaloniaProperty property)
{
var set = o.GetSetValues();
throw new NotImplementedException();
////var set = o.GetSetValues();
if (set.TryGetValue(property, out var obj))
{
if (obj is PriorityValue value)
{
return new AvaloniaPropertyValue(
property,
o.GetValue(property),
(BindingPriority)value.ValuePriority,
value.GetDiagnostic());
}
else
{
return new AvaloniaPropertyValue(
property,
obj,
BindingPriority.LocalValue,
"Local value");
}
}
else
{
return new AvaloniaPropertyValue(
property,
o.GetValue(property),
BindingPriority.Unset,
"Unset");
}
////if (set.TryGetValue(property, out var obj))
////{
//// if (obj is PriorityValue value)
//// {
//// return new AvaloniaPropertyValue(
//// property,
//// o.GetValue(property),
//// (BindingPriority)value.ValuePriority,
//// value.GetDiagnostic());
//// }
//// else
//// {
//// return new AvaloniaPropertyValue(
//// property,
//// obj,
//// BindingPriority.LocalValue,
//// "Local value");
//// }
////}
////else
////{
//// return new AvaloniaPropertyValue(
//// property,
//// o.GetValue(property),
//// BindingPriority.Unset,
//// "Unset");
////}
}
}
}

98
src/Avalonia.Base/DirectProperty.cs

@ -16,7 +16,7 @@ namespace Avalonia
/// <see cref="AvaloniaProperty"/> system. They hold a getter and an optional setter which
/// allows the avalonia property system to read and write the current value.
/// </remarks>
public class DirectProperty<TOwner, TValue> : AvaloniaProperty<TValue>, IDirectPropertyAccessor
public class DirectProperty<TOwner, TValue> : DirectPropertyBase<TValue>, IDirectPropertyAccessor
where TOwner : IAvaloniaObject
{
/// <summary>
@ -26,12 +26,16 @@ namespace Avalonia
/// <param name="getter">Gets the current value of the property.</param>
/// <param name="setter">Sets the value of the property. May be null.</param>
/// <param name="metadata">The property metadata.</param>
/// <param name="enableDataValidation">
/// Whether the property is interested in data validation.
/// </param>
public DirectProperty(
string name,
Func<TOwner, TValue> getter,
Action<TOwner, TValue> setter,
DirectPropertyMetadata<TValue> metadata)
: base(name, typeof(TOwner), metadata)
DirectPropertyMetadata<TValue> metadata,
bool enableDataValidation)
: base(name, typeof(TOwner), metadata, enableDataValidation)
{
Contract.Requires<ArgumentNullException>(getter != null);
@ -46,12 +50,16 @@ namespace Avalonia
/// <param name="getter">Gets the current value of the property.</param>
/// <param name="setter">Sets the value of the property. May be null.</param>
/// <param name="metadata">Optional overridden metadata.</param>
/// <param name="enableDataValidation">
/// Whether the property is interested in data validation.
/// </param>
private DirectProperty(
AvaloniaProperty<TValue> source,
DirectPropertyBase<TValue> source,
Func<TOwner, TValue> getter,
Action<TOwner, TValue> setter,
DirectPropertyMetadata<TValue> metadata)
: base(source, typeof(TOwner), metadata)
DirectPropertyMetadata<TValue> metadata,
bool enableDataValidation)
: base(source, typeof(TOwner), metadata, enableDataValidation)
{
Contract.Requires<ArgumentNullException>(getter != null);
@ -65,6 +73,9 @@ namespace Avalonia
/// <inheritdoc/>
public override bool IsReadOnly => Setter == null;
/// <inheritdoc/>
public override Type Owner => typeof(TOwner);
/// <summary>
/// Gets the getter function.
/// </summary>
@ -75,9 +86,6 @@ namespace Avalonia
/// </summary>
public Action<TOwner, TValue> Setter { get; }
/// <inheritdoc/>
Type IDirectPropertyAccessor.Owner => typeof(TOwner);
/// <summary>
/// Registers the direct property on another type.
/// </summary>
@ -99,6 +107,45 @@ namespace Avalonia
BindingMode defaultBindingMode = BindingMode.Default,
bool enableDataValidation = false)
where TNewOwner : AvaloniaObject
{
var metadata = new DirectPropertyMetadata<TValue>(
unsetValue: unsetValue,
defaultBindingMode: defaultBindingMode);
metadata.Merge(GetMetadata<TOwner>(), this);
var result = new DirectProperty<TNewOwner, TValue>(
(DirectPropertyBase<TValue>)this,
getter,
setter,
metadata,
enableDataValidation);
AvaloniaPropertyRegistry.Instance.Register(typeof(TNewOwner), result);
return result;
}
/// <summary>
/// Registers the direct property on another type.
/// </summary>
/// <typeparam name="TNewOwner">The type of the additional owner.</typeparam>
/// <param name="getter">Gets the current value of the property.</param>
/// <param name="setter">Sets the value of the property.</param>
/// <param name="unsetValue">
/// The value to use when the property is set to <see cref="AvaloniaProperty.UnsetValue"/>
/// </param>
/// <param name="defaultBindingMode">The default binding mode for the property.</param>
/// <param name="enableDataValidation">
/// Whether the property is interested in data validation.
/// </param>
/// <returns>The property.</returns>
public DirectProperty<TNewOwner, TValue> AddOwnerWithDataValidation<TNewOwner>(
Func<TNewOwner, TValue> getter,
Action<TNewOwner,TValue> setter,
TValue unsetValue = default(TValue),
BindingMode defaultBindingMode = BindingMode.Default,
bool enableDataValidation = false)
where TNewOwner : AvaloniaObject
{
var metadata = new DirectPropertyMetadata<TValue>(
unsetValue: unsetValue,
@ -111,12 +158,33 @@ namespace Avalonia
this,
getter,
setter,
metadata);
metadata,
enableDataValidation);
AvaloniaPropertyRegistry.Instance.Register(typeof(TNewOwner), result);
return result;
}
/// <inheritdoc/>
internal override TValue InvokeGetter(IAvaloniaObject instance)
{
return Getter((TOwner)instance);
}
/// <inheritdoc/>
internal override void InvokeSetter(IAvaloniaObject instance, BindingValue<TValue> value)
{
if (Setter == null)
{
throw new ArgumentException($"The property {Name} is readonly.");
}
if (value.HasValue)
{
Setter((TOwner)instance, value.Value);
}
}
/// <inheritdoc/>
object IDirectPropertyAccessor.GetValue(IAvaloniaObject instance)
{
@ -133,5 +201,15 @@ namespace Avalonia
Setter((TOwner)instance, (TValue)value);
}
internal void WrapSetter(TOwner instance, BindingValue<TValue> value)
{
if (Setter == null)
{
throw new ArgumentException($"The property {Name} is readonly.");
}
Setter(instance, value.Value);
}
}
}

159
src/Avalonia.Base/DirectPropertyBase.cs

@ -0,0 +1,159 @@
using System;
using Avalonia.Data;
using Avalonia.Reactive;
#nullable enable
namespace Avalonia
{
/// <summary>
/// Base class for direct properties.
/// </summary>
/// <typeparam name="TValue">The type of the property's value.</typeparam>
/// <remarks>
/// Whereas <see cref="DirectProperty{TOwner, TValue}"/> is typed on the owner type, this base
/// class provides a non-owner-typed interface to a direct poperty.
/// </remarks>
public abstract class DirectPropertyBase<TValue> : AvaloniaProperty<TValue>
{
/// <summary>
/// Initializes a new instance of the <see cref="DirectPropertyBase{TValue}"/> class.
/// </summary>
/// <param name="name">The name of the property.</param>
/// <param name="ownerType">The type of the class that registers the property.</param>
/// <param name="metadata">The property metadata.</param>
/// <param name="enableDataValidation">
/// Whether the property is interested in data validation.
/// </param>
protected DirectPropertyBase(
string name,
Type ownerType,
PropertyMetadata metadata,
bool enableDataValidation)
: base(name, ownerType, metadata)
{
IsDataValidationEnabled = enableDataValidation;
}
/// <summary>
/// Initializes a new instance of the <see cref="AvaloniaProperty"/> class.
/// </summary>
/// <param name="source">The property to copy.</param>
/// <param name="ownerType">The new owner type.</param>
/// <param name="metadata">Optional overridden metadata.</param>
/// <param name="enableDataValidation">
/// Whether the property is interested in data validation.
/// </param>
protected DirectPropertyBase(
AvaloniaProperty source,
Type ownerType,
PropertyMetadata metadata,
bool enableDataValidation)
: base(source, ownerType, metadata)
{
IsDataValidationEnabled = enableDataValidation;
}
/// <summary>
/// Gets the type that registered the property.
/// </summary>
public abstract Type Owner { get; }
/// <summary>
/// Gets a value that indicates whether data validation is enabled for the property.
/// </summary>
public bool IsDataValidationEnabled { get; }
/// <summary>
/// Gets the value of the property on the instance.
/// </summary>
/// <param name="instance">The instance.</param>
/// <returns>The property value.</returns>
internal abstract TValue InvokeGetter(IAvaloniaObject instance);
/// <summary>
/// Sets the value of the property on the instance.
/// </summary>
/// <param name="instance">The instance.</param>
/// <param name="value">The value.</param>
internal abstract void InvokeSetter(IAvaloniaObject instance, BindingValue<TValue> value);
/// <summary>
/// Gets the unset value for the property on the specified type.
/// </summary>
/// <param name="type">The type.</param>
/// <returns>The unset value.</returns>
public TValue GetUnsetValue(Type type)
{
type = type ?? throw new ArgumentNullException(nameof(type));
return GetMetadata(type).UnsetValue;
}
/// <summary>
/// Gets the property metadata for the specified type.
/// </summary>
/// <param name="type">The type.</param>
/// <returns>
/// The property metadata.
/// </returns>
public new DirectPropertyMetadata<TValue> GetMetadata(Type type)
{
return (DirectPropertyMetadata<TValue>)base.GetMetadata(type);
}
/// <inheritdoc/>
internal override void NotifyInitialized(IAvaloniaObject o)
{
var e = new AvaloniaPropertyChangedEventArgs<TValue>(
o,
this,
default,
InvokeGetter(o),
BindingPriority.Unset);
NotifyInitialized(e);
}
/// <inheritdoc/>
internal override object? RouteGetValue(IAvaloniaObject o)
{
return o.GetValue<TValue>(this);
}
/// <inheritdoc/>
internal override void RouteSetValue(
IAvaloniaObject o,
object value,
BindingPriority priority)
{
var v = TryConvert(value);
if (v.HasValue)
{
o.SetValue<TValue>(this, (TValue)v.Value, priority);
}
else if (v.Type == BindingValueType.UnsetValue)
{
o.ClearValue(this);
}
else if (v.HasError)
{
throw v.Error!;
}
}
/// <inheritdoc/>
internal override IDisposable RouteBind(
IAvaloniaObject o,
IObservable<BindingValue<object>> source,
BindingPriority priority)
{
var adapter = TypedBindingAdapter<TValue>.Create(o, this, source);
return o.Bind<TValue>(this, adapter, priority);
}
internal override void RouteInheritanceParentChanged(AvaloniaObject o, IAvaloniaObject oldParent)
{
throw new NotSupportedException("Direct properties do not support inheritance.");
}
}
}

54
src/Avalonia.Base/IAvaloniaObject.cs

@ -17,9 +17,16 @@ namespace Avalonia
event EventHandler<AvaloniaPropertyChangedEventArgs> PropertyChanged;
/// <summary>
/// Raised when an inheritable <see cref="AvaloniaProperty"/> value changes on this object.
/// Clears a <see cref="AvaloniaProperty"/>'s local value.
/// </summary>
event EventHandler<AvaloniaPropertyChangedEventArgs> InheritablePropertyChanged;
/// <param name="property">The property.</param>
public void ClearValue(AvaloniaProperty property);
/// <summary>
/// Clears a <see cref="AvaloniaProperty"/>'s local value.
/// </summary>
/// <param name="property">The property.</param>
public void ClearValue<T>(AvaloniaProperty<T> property);
/// <summary>
/// Gets a <see cref="AvaloniaProperty"/> value.
@ -84,7 +91,7 @@ namespace Avalonia
/// </returns>
IDisposable Bind(
AvaloniaProperty property,
IObservable<object> source,
IObservable<BindingValue<object>> source,
BindingPriority priority = BindingPriority.LocalValue);
/// <summary>
@ -99,7 +106,46 @@ namespace Avalonia
/// </returns>
IDisposable Bind<T>(
AvaloniaProperty<T> property,
IObservable<T> source,
IObservable<BindingValue<T>> source,
BindingPriority priority = BindingPriority.LocalValue);
/// <summary>
/// Registers an object as an inheritance child.
/// </summary>
/// <param name="child">The inheritance child.</param>
/// <remarks>
/// Inheritance children will recieve a call to
/// <see cref="InheritedPropertyChanged{T}(AvaloniaProperty{T}, Optional{T}, Optional{T})"/>
/// when an inheritable property value changes on the parent.
/// </remarks>
void AddInheritanceChild(IAvaloniaObject child);
/// <summary>
/// Unregisters an object as an inheritance child.
/// </summary>
/// <param name="child">The inheritance child.</param>
/// <remarks>
/// Removes an inheritance child that was added by a call to
/// <see cref="AddInheritanceChild(IAvaloniaObject)"/>.
/// </remarks>
void RemoveInheritanceChild(IAvaloniaObject child);
//void InheritanceParentChanged<T>(
// StyledPropertyBase<T> property,
// IAvaloniaObject oldParent,
// IAvaloniaObject newParent);
/// <summary>
/// Called when an inheritable property changes on an object registered as an inheritance
/// parent.
/// </summary>
/// <typeparam name="T">The type of the value.</typeparam>
/// <param name="property">The property that has changed.</param>
/// <param name="oldValue">The old property value.</param>
/// <param name="newValue">The new property value.</param>
void InheritedPropertyChanged<T>(
AvaloniaProperty<T> property,
Optional<T> oldValue,
Optional<T> newValue);
}
}

51
src/Avalonia.Base/IPriorityValueOwner.cs

@ -1,51 +0,0 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using Avalonia.Data;
using Avalonia.Utilities;
namespace Avalonia
{
/// <summary>
/// An owner of a <see cref="PriorityValue"/>.
/// </summary>
internal interface IPriorityValueOwner
{
/// <summary>
/// Called when a <see cref="PriorityValue"/>'s value changes.
/// </summary>
/// <param name="property">The the property that has changed.</param>
/// <param name="priority">The priority of the value.</param>
/// <param name="oldValue">The old value.</param>
/// <param name="newValue">The new value.</param>
void Changed(AvaloniaProperty property, int priority, object oldValue, object newValue);
/// <summary>
/// Called when a <see cref="BindingNotification"/> is received by a
/// <see cref="PriorityValue"/>.
/// </summary>
/// <param name="property">The the property that has changed.</param>
/// <param name="notification">The notification.</param>
void BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification);
/// <summary>
/// Returns deferred setter for given non-direct property.
/// </summary>
/// <param name="property">Property.</param>
/// <returns>Deferred setter for given property.</returns>
DeferredSetter<object> GetNonDirectDeferredSetter(AvaloniaProperty property);
/// <summary>
/// Logs a binding error.
/// </summary>
/// <param name="property">The property the error occurred on.</param>
/// <param name="e">The binding error.</param>
void LogError(AvaloniaProperty property, Exception e);
/// <summary>
/// Ensures that the current thread is the UI thread.
/// </summary>
void VerifyAccess();
}
}

9
src/Avalonia.Base/IStyledPropertyAccessor.cs

@ -18,14 +18,5 @@ namespace Avalonia
/// The default value.
/// </returns>
object GetDefaultValue(Type type);
/// <summary>
/// Gets a validation function for the property on the specified type.
/// </summary>
/// <param name="type">The type.</param>
/// <returns>
/// The validation function, or null if no validation function exists.
/// </returns>
Func<IAvaloniaObject, object, object> GetValidationFunc(Type type);
}
}

7
src/Avalonia.Base/IStyledPropertyMetadata.cs

@ -14,10 +14,5 @@ namespace Avalonia
/// Gets the default value for the property.
/// </summary>
object DefaultValue { get; }
/// <summary>
/// Gets the property's validation function.
/// </summary>
Func<IAvaloniaObject, object, object> Validate { get; }
}
}
}

160
src/Avalonia.Base/PriorityBindingEntry.cs

@ -1,160 +0,0 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Runtime.ExceptionServices;
using Avalonia.Data;
using Avalonia.Threading;
namespace Avalonia
{
/// <summary>
/// A registered binding in a <see cref="PriorityValue"/>.
/// </summary>
internal class PriorityBindingEntry : IDisposable, IObserver<object>
{
private readonly PriorityLevel _owner;
private IDisposable _subscription;
/// <summary>
/// Initializes a new instance of the <see cref="PriorityBindingEntry"/> class.
/// </summary>
/// <param name="owner">The owner.</param>
/// <param name="index">
/// The binding index. Later bindings should have higher indexes.
/// </param>
public PriorityBindingEntry(PriorityLevel owner, int index)
{
_owner = owner;
Index = index;
}
/// <summary>
/// Gets the observable associated with the entry.
/// </summary>
public IObservable<object> Observable { get; private set; }
/// <summary>
/// Gets a description of the binding.
/// </summary>
public string Description
{
get;
private set;
}
/// <summary>
/// Gets the binding entry index. Later bindings will have higher indexes.
/// </summary>
public int Index
{
get;
}
/// <summary>
/// Gets a value indicating whether the binding has completed.
/// </summary>
public bool HasCompleted { get; private set; }
/// <summary>
/// The current value of the binding.
/// </summary>
public object Value
{
get;
private set;
}
/// <summary>
/// Starts listening to the binding.
/// </summary>
/// <param name="binding">The binding.</param>
public void Start(IObservable<object> binding)
{
Contract.Requires<ArgumentNullException>(binding != null);
if (_subscription != null)
{
throw new Exception("PriorityValue.Entry.Start() called more than once.");
}
Observable = binding;
Value = AvaloniaProperty.UnsetValue;
if (binding is IDescription)
{
Description = ((IDescription)binding).Description;
}
_subscription = binding.Subscribe(this);
}
/// <summary>
/// Ends the binding subscription.
/// </summary>
public void Dispose()
{
_subscription?.Dispose();
}
void IObserver<object>.OnNext(object value)
{
void Signal(PriorityBindingEntry instance, object newValue)
{
var notification = newValue as BindingNotification;
if (notification != null)
{
if (notification.HasValue || notification.ErrorType == BindingErrorType.Error)
{
instance.Value = notification.Value;
instance._owner.Changed(instance);
}
if (notification.ErrorType != BindingErrorType.None)
{
instance._owner.Error(instance, notification);
}
}
else
{
instance.Value = newValue;
instance._owner.Changed(instance);
}
}
if (Dispatcher.UIThread.CheckAccess())
{
Signal(this, value);
}
else
{
// To avoid allocating closure in the outer scope we need to capture variables
// locally. This allows us to skip most of the allocations when on UI thread.
var instance = this;
var newValue = value;
Dispatcher.UIThread.Post(() => Signal(instance, newValue));
}
}
void IObserver<object>.OnCompleted()
{
HasCompleted = true;
if (Dispatcher.UIThread.CheckAccess())
{
_owner.Completed(this);
}
else
{
Dispatcher.UIThread.Post(() => _owner.Completed(this));
}
}
void IObserver<object>.OnError(Exception error)
{
ExceptionDispatchInfo.Capture(error).Throw();
}
}
}

227
src/Avalonia.Base/PriorityLevel.cs

@ -1,227 +0,0 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using Avalonia.Data;
namespace Avalonia
{
/// <summary>
/// Stores bindings for a priority level in a <see cref="PriorityValue"/>.
/// </summary>
/// <remarks>
/// <para>
/// Each priority level in a <see cref="PriorityValue"/> has a current <see cref="Value"/>,
/// a list of <see cref="Bindings"/> and a <see cref="DirectValue"/>. When there are no
/// bindings present, or all bindings return <see cref="AvaloniaProperty.UnsetValue"/> then
/// <code>Value</code> will equal <code>DirectValue</code>.
/// </para>
/// <para>
/// When there are bindings present, then the latest added binding that doesn't return
/// <code>UnsetValue</code> will take precedence. The active binding is returned by the
/// <see cref="ActiveBindingIndex"/> property (which refers to the active binding's
/// <see cref="PriorityBindingEntry.Index"/> property rather than the index in
/// <code>Bindings</code>).
/// </para>
/// <para>
/// If <code>DirectValue</code> is set while a binding is active, then it will replace the
/// current value until the active binding fires again.
/// </para>
/// </remarks>
internal class PriorityLevel
{
private object _directValue;
private int _nextIndex;
/// <summary>
/// Initializes a new instance of the <see cref="PriorityLevel"/> class.
/// </summary>
/// <param name="owner">The owner.</param>
/// <param name="priority">The priority.</param>
public PriorityLevel(
PriorityValue owner,
int priority)
{
Contract.Requires<ArgumentNullException>(owner != null);
Owner = owner;
Priority = priority;
Value = _directValue = AvaloniaProperty.UnsetValue;
ActiveBindingIndex = -1;
Bindings = new LinkedList<PriorityBindingEntry>();
}
/// <summary>
/// Gets the owner of the level.
/// </summary>
public PriorityValue Owner { get; }
/// <summary>
/// Gets the priority of this level.
/// </summary>
public int Priority { get; }
/// <summary>
/// Gets or sets the direct value for this priority level.
/// </summary>
public object DirectValue
{
get
{
return _directValue;
}
set
{
Value = _directValue = value;
Owner.LevelValueChanged(this);
}
}
/// <summary>
/// Gets the current binding for the priority level.
/// </summary>
public object Value { get; private set; }
/// <summary>
/// Gets the <see cref="PriorityBindingEntry.Index"/> value of the active binding, or -1
/// if no binding is active.
/// </summary>
public int ActiveBindingIndex { get; private set; }
/// <summary>
/// Gets the bindings for the priority level.
/// </summary>
public LinkedList<PriorityBindingEntry> Bindings { get; }
/// <summary>
/// Adds a binding.
/// </summary>
/// <param name="binding">The binding to add.</param>
/// <returns>A disposable used to remove the binding.</returns>
public IDisposable Add(IObservable<object> binding)
{
Contract.Requires<ArgumentNullException>(binding != null);
var entry = new PriorityBindingEntry(this, _nextIndex++);
var node = Bindings.AddFirst(entry);
entry.Start(binding);
return new RemoveBindingDisposable(node, Bindings, this);
}
/// <summary>
/// Invoked when an entry in <see cref="Bindings"/> changes value.
/// </summary>
/// <param name="entry">The entry that changed.</param>
public void Changed(PriorityBindingEntry entry)
{
if (entry.Index >= ActiveBindingIndex)
{
if (entry.Value != AvaloniaProperty.UnsetValue)
{
Value = entry.Value;
ActiveBindingIndex = entry.Index;
Owner.LevelValueChanged(this);
}
else
{
ActivateFirstBinding();
}
}
}
/// <summary>
/// Invoked when an entry in <see cref="Bindings"/> completes.
/// </summary>
/// <param name="entry">The entry that completed.</param>
public void Completed(PriorityBindingEntry entry)
{
Bindings.Remove(entry);
if (entry.Index >= ActiveBindingIndex)
{
ActivateFirstBinding();
}
}
/// <summary>
/// Invoked when an entry in <see cref="Bindings"/> encounters a recoverable error.
/// </summary>
/// <param name="entry">The entry that completed.</param>
/// <param name="error">The error.</param>
public void Error(PriorityBindingEntry entry, BindingNotification error)
{
Owner.LevelError(this, error);
}
/// <summary>
/// Activates the first binding that has a value.
/// </summary>
private void ActivateFirstBinding()
{
foreach (var binding in Bindings)
{
if (binding.Value != AvaloniaProperty.UnsetValue)
{
Value = binding.Value;
ActiveBindingIndex = binding.Index;
Owner.LevelValueChanged(this);
return;
}
}
Value = DirectValue;
ActiveBindingIndex = -1;
Owner.LevelValueChanged(this);
}
private sealed class RemoveBindingDisposable : IDisposable
{
private readonly LinkedList<PriorityBindingEntry> _bindings;
private readonly PriorityLevel _priorityLevel;
private LinkedListNode<PriorityBindingEntry> _binding;
public RemoveBindingDisposable(
LinkedListNode<PriorityBindingEntry> binding,
LinkedList<PriorityBindingEntry> bindings,
PriorityLevel priorityLevel)
{
_binding = binding;
_bindings = bindings;
_priorityLevel = priorityLevel;
}
public void Dispose()
{
LinkedListNode<PriorityBindingEntry> binding = Interlocked.Exchange(ref _binding, null);
if (binding == null)
{
// Some system is trying to remove binding twice.
Debug.Assert(false);
return;
}
PriorityBindingEntry entry = binding.Value;
if (!entry.HasCompleted)
{
_bindings.Remove(binding);
entry.Dispose();
if (entry.Index >= _priorityLevel.ActiveBindingIndex)
{
_priorityLevel.ActivateFirstBinding();
}
}
}
}
}
}

315
src/Avalonia.Base/PriorityValue.cs

@ -1,315 +0,0 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Avalonia.Data;
using Avalonia.Logging;
using Avalonia.Utilities;
namespace Avalonia
{
/// <summary>
/// Maintains a list of prioritized bindings together with a current value.
/// </summary>
/// <remarks>
/// Bindings, in the form of <see cref="IObservable{Object}"/>s are added to the object using
/// the <see cref="Add"/> method. With the observable is passed a priority, where lower values
/// represent higher priorities. The current <see cref="Value"/> is selected from the highest
/// priority binding that doesn't return <see cref="AvaloniaProperty.UnsetValue"/>. Where there
/// are multiple bindings registered with the same priority, the most recently added binding
/// has a higher priority. Each time the value changes, the
/// <see cref="IPriorityValueOwner.Changed"/> method on the
/// owner object is fired with the old and new values.
/// </remarks>
internal sealed class PriorityValue : ISetAndNotifyHandler<(object,int)>
{
private readonly Type _valueType;
private readonly SingleOrDictionary<int, PriorityLevel> _levels = new SingleOrDictionary<int, PriorityLevel>();
private readonly Func<object, object> _validate;
private (object value, int priority) _value;
private DeferredSetter<object> _setter;
/// <summary>
/// Initializes a new instance of the <see cref="PriorityValue"/> class.
/// </summary>
/// <param name="owner">The owner of the object.</param>
/// <param name="property">The property that the value represents.</param>
/// <param name="valueType">The value type.</param>
/// <param name="validate">An optional validation function.</param>
public PriorityValue(
IPriorityValueOwner owner,
AvaloniaProperty property,
Type valueType,
Func<object, object> validate = null)
{
Owner = owner;
Property = property;
_valueType = valueType;
_value = (AvaloniaProperty.UnsetValue, int.MaxValue);
_validate = validate;
}
/// <summary>
/// Gets a value indicating whether the property is animating.
/// </summary>
public bool IsAnimating
{
get
{
return ValuePriority <= (int)BindingPriority.Animation &&
GetLevel(ValuePriority).ActiveBindingIndex != -1;
}
}
/// <summary>
/// Gets the owner of the value.
/// </summary>
public IPriorityValueOwner Owner { get; }
/// <summary>
/// Gets the property that the value represents.
/// </summary>
public AvaloniaProperty Property { get; }
/// <summary>
/// Gets the current value.
/// </summary>
public object Value => _value.value;
/// <summary>
/// Gets the priority of the binding that is currently active.
/// </summary>
public int ValuePriority => _value.priority;
/// <summary>
/// Adds a new binding.
/// </summary>
/// <param name="binding">The binding.</param>
/// <param name="priority">The binding priority.</param>
/// <returns>
/// A disposable that will remove the binding.
/// </returns>
public IDisposable Add(IObservable<object> binding, int priority)
{
return GetLevel(priority).Add(binding);
}
/// <summary>
/// Sets the value for a specified priority.
/// </summary>
/// <param name="value">The value.</param>
/// <param name="priority">The priority</param>
public void SetValue(object value, int priority)
{
GetLevel(priority).DirectValue = value;
}
/// <summary>
/// Gets the currently active bindings on this object.
/// </summary>
/// <returns>An enumerable collection of bindings.</returns>
public IEnumerable<PriorityBindingEntry> GetBindings()
{
foreach (var level in _levels)
{
foreach (var binding in level.Value.Bindings)
{
yield return binding;
}
}
}
/// <summary>
/// Returns diagnostic string that can help the user debug the bindings in effect on
/// this object.
/// </summary>
/// <returns>A diagnostic string.</returns>
public string GetDiagnostic()
{
var b = new StringBuilder();
var first = true;
foreach (var level in _levels)
{
if (!first)
{
b.AppendLine();
}
b.Append(ValuePriority == level.Key ? "*" : string.Empty);
b.Append("Priority ");
b.Append(level.Key);
b.Append(": ");
b.AppendLine(level.Value.Value?.ToString() ?? "(null)");
b.AppendLine("--------");
b.Append("Direct: ");
b.AppendLine(level.Value.DirectValue?.ToString() ?? "(null)");
foreach (var binding in level.Value.Bindings)
{
b.Append(level.Value.ActiveBindingIndex == binding.Index ? "*" : string.Empty);
b.Append(binding.Description ?? binding.Observable.GetType().Name);
b.Append(": ");
b.AppendLine(binding.Value?.ToString() ?? "(null)");
}
first = false;
}
return b.ToString();
}
/// <summary>
/// Called when the value for a priority level changes.
/// </summary>
/// <param name="level">The priority level of the changed entry.</param>
public void LevelValueChanged(PriorityLevel level)
{
if (level.Priority <= ValuePriority)
{
if (level.Value != AvaloniaProperty.UnsetValue)
{
UpdateValue(level.Value, level.Priority);
}
else
{
foreach (var i in _levels.Values.OrderBy(x => x.Priority))
{
if (i.Value != AvaloniaProperty.UnsetValue)
{
UpdateValue(i.Value, i.Priority);
return;
}
}
UpdateValue(AvaloniaProperty.UnsetValue, int.MaxValue);
}
}
}
/// <summary>
/// Called when a priority level encounters an error.
/// </summary>
/// <param name="level">The priority level of the changed entry.</param>
/// <param name="error">The binding error.</param>
public void LevelError(PriorityLevel level, BindingNotification error)
{
Owner.LogError(Property, error.Error);
}
/// <summary>
/// Causes a revalidation of the value.
/// </summary>
public void Revalidate()
{
if (_validate != null)
{
PriorityLevel level;
if (_levels.TryGetValue(ValuePriority, out level))
{
UpdateValue(level.Value, level.Priority);
}
}
}
/// <summary>
/// Gets the <see cref="PriorityLevel"/> with the specified priority, creating it if it
/// doesn't already exist.
/// </summary>
/// <param name="priority">The priority.</param>
/// <returns>The priority level.</returns>
private PriorityLevel GetLevel(int priority)
{
PriorityLevel result;
if (!_levels.TryGetValue(priority, out result))
{
result = new PriorityLevel(this, priority);
_levels.Add(priority, result);
}
return result;
}
/// <summary>
/// Updates the current <see cref="Value"/> and notifies all subscribers.
/// </summary>
/// <param name="value">The value to set.</param>
/// <param name="priority">The priority level that the value came from.</param>
private void UpdateValue(object value, int priority)
{
var newValue = (value, priority);
if (newValue == _value)
{
return;
}
if (_setter == null)
{
_setter = Owner.GetNonDirectDeferredSetter(Property);
}
_setter.SetAndNotifyCallback(Property, this, ref _value, newValue);
}
void ISetAndNotifyHandler<(object, int)>.HandleSetAndNotify(AvaloniaProperty property, ref (object, int) backing, (object, int) value)
{
SetAndNotify(ref backing, value);
}
private void SetAndNotify(ref (object value, int priority) backing, (object value, int priority) update)
{
var val = update.value;
var notification = val as BindingNotification;
object castValue;
if (notification != null)
{
val = (notification.HasValue) ? notification.Value : null;
}
if (TypeUtilities.TryConvertImplicit(_valueType, val, out castValue))
{
var old = backing.value;
if (_validate != null && castValue != AvaloniaProperty.UnsetValue)
{
castValue = _validate(castValue);
}
backing = (castValue, update.priority);
if (notification?.HasValue == true)
{
notification.SetValue(castValue);
}
if (notification == null || notification.HasValue)
{
Owner?.Changed(Property, ValuePriority, old, Value);
}
if (notification != null)
{
Owner?.BindingNotificationReceived(Property, notification);
}
}
else
{
Logger.TryGet(LogEventLevel.Error)?.Log(
LogArea.Binding,
Owner,
"Binding produced invalid value for {$Property} ({$PropertyType}): {$Value} ({$ValueType})",
Property.Name,
_valueType,
val,
val?.GetType());
}
}
}
}

95
src/Avalonia.Base/PropertyStore/BindingEntry.cs

@ -0,0 +1,95 @@
using System;
using Avalonia.Data;
using Avalonia.Threading;
#nullable enable
namespace Avalonia.PropertyStore
{
internal interface IBindingEntry : IPriorityValueEntry, IDisposable
{
}
internal class BindingEntry<T> : IBindingEntry, IPriorityValueEntry<T>, IObserver<BindingValue<T>>
{
private readonly IAvaloniaObject _owner;
private IValueSink _sink;
private IDisposable? _subscription;
public BindingEntry(
IAvaloniaObject owner,
StyledPropertyBase<T> property,
IObservable<BindingValue<T>> source,
BindingPriority priority,
IValueSink sink)
{
_owner = owner;
Property = property;
Source = source;
Priority = priority;
_sink = sink;
}
public StyledPropertyBase<T> Property { get; }
public BindingPriority Priority { get; }
public IObservable<BindingValue<T>> Source { get; }
public Optional<T> Value { get; private set; }
Optional<object> IValue.Value => Value.ToObject();
BindingPriority IValue.ValuePriority => Priority;
public void Dispose()
{
_subscription?.Dispose();
_subscription = null;
_sink.Completed(Property, this);
}
public void OnCompleted() => _sink.Completed(Property, this);
public void OnError(Exception error)
{
throw new NotImplementedException();
}
public void OnNext(BindingValue<T> value)
{
if (Dispatcher.UIThread.CheckAccess())
{
UpdateValue(value);
}
else
{
// To avoid allocating closure in the outer scope we need to capture variables
// locally. This allows us to skip most of the allocations when on UI thread.
var instance = this;
var newValue = value;
Dispatcher.UIThread.Post(() => instance.UpdateValue(newValue));
}
}
public void Start()
{
_subscription = Source.Subscribe(this);
}
public void Reparent(IValueSink sink) => _sink = sink;
private void UpdateValue(BindingValue<T> value)
{
if (value.Type == BindingValueType.DoNothing)
{
return;
}
var old = Value;
if (value.Type != BindingValueType.DataValidationError)
{
Value = value.ToOptional();
}
_sink.ValueChanged(Property, Priority, old, value);
}
}
}

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

@ -0,0 +1,28 @@
using System;
using Avalonia.Data;
#nullable enable
namespace Avalonia.PropertyStore
{
internal class ConstantValueEntry<T> : IPriorityValueEntry<T>
{
public ConstantValueEntry(
StyledPropertyBase<T> property,
T value,
BindingPriority priority)
{
Property = property;
Value = value;
Priority = priority;
}
public StyledPropertyBase<T> Property { get; }
public BindingPriority Priority { get; }
public Optional<T> Value { get; private set; }
Optional<object> IValue.Value => Value.ToObject();
BindingPriority IValue.ValuePriority => Priority;
public void Reparent(IValueSink sink) { }
}
}

18
src/Avalonia.Base/PropertyStore/IPriorityValueEntry.cs

@ -0,0 +1,18 @@
using System;
using Avalonia.Data;
#nullable enable
namespace Avalonia.PropertyStore
{
internal interface IPriorityValueEntry : IValue
{
BindingPriority Priority { get; }
void Reparent(IValueSink sink);
}
internal interface IPriorityValueEntry<T> : IPriorityValueEntry, IValue<T>
{
}
}

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

@ -0,0 +1,17 @@
using Avalonia.Data;
#nullable enable
namespace Avalonia.PropertyStore
{
internal interface IValue
{
Optional<object> Value { get; }
BindingPriority ValuePriority { get; }
}
internal interface IValue<T> : IValue
{
new Optional<T> Value { get; }
}
}

18
src/Avalonia.Base/PropertyStore/IValueSink.cs

@ -0,0 +1,18 @@
using System;
using Avalonia.Data;
#nullable enable
namespace Avalonia.PropertyStore
{
internal interface IValueSink
{
void ValueChanged<T>(
StyledPropertyBase<T> property,
BindingPriority priority,
Optional<T> oldValue,
BindingValue<T> newValue);
void Completed(AvaloniaProperty property, IPriorityValueEntry entry);
}
}

143
src/Avalonia.Base/PropertyStore/PriorityValue.cs

@ -0,0 +1,143 @@
using System;
using System.Collections.Generic;
using Avalonia.Data;
#nullable enable
namespace Avalonia.PropertyStore
{
internal class PriorityValue<T> : IValue<T>, IValueSink
{
private readonly IAvaloniaObject _owner;
private readonly IValueSink _sink;
private readonly List<IPriorityValueEntry<T>> _entries = new List<IPriorityValueEntry<T>>();
private Optional<T> _localValue;
public PriorityValue(
IAvaloniaObject owner,
StyledPropertyBase<T> property,
IValueSink sink)
{
_owner = owner;
Property = property;
_sink = sink;
}
public PriorityValue(
IAvaloniaObject owner,
StyledPropertyBase<T> property,
IValueSink sink,
IPriorityValueEntry<T> existing)
: this(owner, property, sink)
{
existing.Reparent(this);
_entries.Add(existing);
if (existing.Value.HasValue)
{
Value = existing.Value;
ValuePriority = existing.Priority;
}
}
public StyledPropertyBase<T> Property { get; }
public Optional<T> Value { get; private set; }
public BindingPriority ValuePriority { get; private set; }
public IReadOnlyList<IPriorityValueEntry<T>> Entries => _entries;
Optional<object> IValue.Value => Value.ToObject();
public void ClearLocalValue() => UpdateEffectiveValue();
public void SetValue(T value, BindingPriority priority)
{
if (priority == BindingPriority.LocalValue)
{
_localValue = value;
}
else
{
var insert = FindInsertPoint(priority);
_entries.Insert(insert, new ConstantValueEntry<T>(Property, value, priority));
}
UpdateEffectiveValue();
}
public BindingEntry<T> AddBinding(IObservable<BindingValue<T>> source, BindingPriority priority)
{
var binding = new BindingEntry<T>(_owner, Property, source, priority, this);
var insert = FindInsertPoint(binding.Priority);
_entries.Insert(insert, binding);
return binding;
}
void IValueSink.ValueChanged<TValue>(
StyledPropertyBase<TValue> property,
BindingPriority priority,
Optional<TValue> oldValue,
BindingValue<TValue> newValue)
{
_localValue = default;
UpdateEffectiveValue();
}
void IValueSink.Completed(AvaloniaProperty property, IPriorityValueEntry entry)
{
_entries.Remove((IPriorityValueEntry<T>)entry);
UpdateEffectiveValue();
}
private int FindInsertPoint(BindingPriority priority)
{
var result = _entries.Count;
for (var i = 0; i < _entries.Count; ++i)
{
if (_entries[i].Priority < priority)
{
result = i;
break;
}
}
return result;
}
private void UpdateEffectiveValue()
{
var reachedLocalValues = false;
var value = default(Optional<T>);
for (var i = _entries.Count - 1; i >= 0; --i)
{
var entry = _entries[i];
if (!reachedLocalValues && entry.Priority >= BindingPriority.LocalValue)
{
reachedLocalValues = true;
if (_localValue.HasValue)
{
value = _localValue;
ValuePriority = BindingPriority.LocalValue;
break;
}
}
if (entry.Value.HasValue)
{
value = entry.Value;
ValuePriority = entry.Priority;
break;
}
}
if (value != Value)
{
var old = Value;
Value = value;
_sink.ValueChanged(Property, ValuePriority, old, value);
}
}
}
}

55
src/Avalonia.Base/Reactive/AvaloniaPropertyBindingObservable.cs

@ -0,0 +1,55 @@
using System;
using Avalonia.Data;
#nullable enable
namespace Avalonia.Reactive
{
internal class AvaloniaPropertyBindingObservable<T> : LightweightObservableBase<BindingValue<T>>, IDescription
{
private readonly WeakReference<IAvaloniaObject> _target;
private readonly AvaloniaProperty _property;
private T _value;
public AvaloniaPropertyBindingObservable(
IAvaloniaObject target,
AvaloniaProperty property)
{
_target = new WeakReference<IAvaloniaObject>(target);
_property = property;
}
public string Description => $"{_target.GetType().Name}.{_property.Name}";
protected override void Initialize()
{
if (_target.TryGetTarget(out var target))
{
_value = (T)target.GetValue(_property);
target.PropertyChanged += PropertyChanged;
}
}
protected override void Deinitialize()
{
if (_target.TryGetTarget(out var target))
{
target.PropertyChanged -= PropertyChanged;
}
}
protected override void Subscribed(IObserver<BindingValue<T>> observer, bool first)
{
observer.OnNext(new BindingValue<T>(_value));
}
private void PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
{
if (e.Property == _property)
{
_value = (T)e.NewValue;
PublishNext(new BindingValue<T>(_value));
}
}
}
}

61
src/Avalonia.Base/Reactive/BindingValueAdapter.cs

@ -0,0 +1,61 @@
using System;
using System.Reactive.Subjects;
using Avalonia.Data;
#nullable enable
namespace Avalonia.Reactive
{
internal class BindingValueAdapter<T> : SingleSubscriberObservableBase<BindingValue<T>>,
IObserver<T>
{
private readonly IObservable<T> _source;
private IDisposable? _subscription;
public BindingValueAdapter(IObservable<T> source) => _source = source;
public void OnCompleted() => PublishCompleted();
public void OnError(Exception error) => PublishError(error);
public void OnNext(T value) => PublishNext(BindingValue<T>.FromUntyped(value));
protected override void Subscribed() => _subscription = _source.Subscribe(this);
protected override void Unsubscribed() => _subscription?.Dispose();
}
internal class BindingValueSubjectAdapter<T> : SingleSubscriberObservableBase<BindingValue<T>>,
ISubject<BindingValue<T>>
{
private readonly ISubject<T> _source;
private readonly Inner _inner;
private IDisposable? _subscription;
public BindingValueSubjectAdapter(ISubject<T> source)
{
_source = source;
_inner = new Inner(this);
}
public void OnCompleted() => _source.OnCompleted();
public void OnError(Exception error) => _source.OnError(error);
public void OnNext(BindingValue<T> value)
{
if (value.HasValue)
{
_source.OnNext(value.Value);
}
}
protected override void Subscribed() => _subscription = _source.Subscribe(_inner);
protected override void Unsubscribed() => _subscription?.Dispose();
private class Inner : IObserver<T>
{
private readonly BindingValueSubjectAdapter<T> _owner;
public Inner(BindingValueSubjectAdapter<T> owner) => _owner = owner;
public void OnCompleted() => _owner.PublishCompleted();
public void OnError(Exception error) => _owner.PublishError(error);
public void OnNext(T value) => _owner.PublishNext(BindingValue<T>.FromUntyped(value));
}
}
}

35
src/Avalonia.Base/Reactive/BindingValueExtensions.cs

@ -0,0 +1,35 @@
using System;
using System.Reactive.Subjects;
using Avalonia.Data;
#nullable enable
namespace Avalonia.Reactive
{
public static class BindingValueExtensions
{
public static IObservable<BindingValue<T>> ToBindingValue<T>(this IObservable<T> source)
{
source = source ?? throw new ArgumentNullException(nameof(source));
return new BindingValueAdapter<T>(source);
}
public static ISubject<BindingValue<T>> ToBindingValue<T>(this ISubject<T> source)
{
source = source ?? throw new ArgumentNullException(nameof(source));
return new BindingValueSubjectAdapter<T>(source);
}
public static IObservable<object> ToUntyped<T>(this IObservable<BindingValue<T>> source)
{
source = source ?? throw new ArgumentNullException(nameof(source));
return new UntypedBindingAdapter<T>(source);
}
public static ISubject<object> ToUntyped<T>(this ISubject<BindingValue<T>> source)
{
source = source ?? throw new ArgumentNullException(nameof(source));
return new UntypedBindingSubjectAdapter<T>(source);
}
}
}

63
src/Avalonia.Base/Reactive/TypedBindingAdapter.cs

@ -0,0 +1,63 @@
using System;
using Avalonia.Data;
using Avalonia.Logging;
#nullable enable
namespace Avalonia.Reactive
{
internal class TypedBindingAdapter<T> : SingleSubscriberObservableBase<BindingValue<T>>,
IObserver<BindingValue<object>>
{
private readonly IAvaloniaObject _target;
private readonly AvaloniaProperty<T> _property;
private readonly IObservable<BindingValue<object>> _source;
private IDisposable? _subscription;
public TypedBindingAdapter(
IAvaloniaObject target,
AvaloniaProperty<T> property,
IObservable<BindingValue<object>> source)
{
_target = target;
_property = property;
_source = source;
}
public void OnNext(BindingValue<object> value)
{
try
{
PublishNext(value.Convert<T>());
}
catch (InvalidCastException e)
{
Logger.TryGet(LogEventLevel.Error)?.Log(
LogArea.Binding,
_target,
"Binding produced invalid value for {$Property} ({$PropertyType}): {$Value} ({$ValueType})",
_property.Name,
_property.PropertyType,
value.Value,
value.Value?.GetType());
PublishNext(BindingValue<T>.BindingError(e));
}
}
public void OnCompleted() => PublishCompleted();
public void OnError(Exception error) => PublishError(error);
public static IObservable<BindingValue<T>> Create(
IAvaloniaObject target,
AvaloniaProperty<T> property,
IObservable<BindingValue<object>> source)
{
return source is IObservable<BindingValue<T>> result ?
result :
new TypedBindingAdapter<T>(target, property, source);
}
protected override void Subscribed() => _subscription = _source.Subscribe(this);
protected override void Unsubscribed() => _subscription?.Dispose();
}
}

57
src/Avalonia.Base/Reactive/UntypedBindingAdapter.cs

@ -0,0 +1,57 @@
using System;
using System.Reactive.Subjects;
using Avalonia.Data;
#nullable enable
namespace Avalonia.Reactive
{
internal class UntypedBindingAdapter<T> : SingleSubscriberObservableBase<object?>,
IObserver<BindingValue<T>>
{
private readonly IObservable<BindingValue<T>> _source;
private IDisposable? _subscription;
public UntypedBindingAdapter(IObservable<BindingValue<T>> source) => _source = source;
public void OnCompleted() => PublishCompleted();
public void OnError(Exception error) => PublishError(error);
public void OnNext(BindingValue<T> value) => value.ToUntyped();
protected override void Subscribed() => _subscription = _source.Subscribe(this);
protected override void Unsubscribed() => _subscription?.Dispose();
}
internal class UntypedBindingSubjectAdapter<T> : SingleSubscriberObservableBase<object?>,
ISubject<object?>
{
private readonly ISubject<BindingValue<T>> _source;
private readonly Inner _inner;
private IDisposable? _subscription;
public UntypedBindingSubjectAdapter(ISubject<BindingValue<T>> source)
{
_source = source;
_inner = new Inner(this);
}
public void OnCompleted() => _source.OnCompleted();
public void OnError(Exception error) => _source.OnError(error);
public void OnNext(object? value)
{
_source.OnNext(BindingValue<T>.FromUntyped(value));
}
protected override void Subscribed() => _subscription = _source.Subscribe(_inner);
protected override void Unsubscribed() => _subscription?.Dispose();
private class Inner : IObserver<BindingValue<T>>
{
private readonly UntypedBindingSubjectAdapter<T> _owner;
public Inner(UntypedBindingSubjectAdapter<T> owner) => _owner = owner;
public void OnCompleted() => _owner.PublishCompleted();
public void OnError(Exception error) => _owner.PublishError(error);
public void OnNext(BindingValue<T> value) => _owner.PublishNext(value.ToUntyped());
}
}
}

84
src/Avalonia.Base/StyledPropertyBase.cs

@ -2,14 +2,17 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using Avalonia.Data;
using Avalonia.Reactive;
namespace Avalonia
{
/// <summary>
/// Base class for styled properties.
/// </summary>
public class StyledPropertyBase<TValue> : AvaloniaProperty<TValue>, IStyledPropertyAccessor
public abstract class StyledPropertyBase<TValue> : AvaloniaProperty<TValue>, IStyledPropertyAccessor
{
private bool _inherits;
@ -124,48 +127,75 @@ namespace Avalonia
}
/// <summary>
/// Overrides the validation function for the specified type.
/// Gets the string representation of the property.
/// </summary>
/// <typeparam name="THost">The type.</typeparam>
/// <param name="validate">The validation function.</param>
public void OverrideValidation<THost>(Func<THost, TValue, TValue> validate)
where THost : IAvaloniaObject
/// <returns>The property's string representation.</returns>
public override string ToString()
{
Func<IAvaloniaObject, TValue, TValue> f;
return Name;
}
if (validate != null)
/// <inheritdoc/>
object IStyledPropertyAccessor.GetDefaultValue(Type type) => GetDefaultBoxedValue(type);
/// <inheritdoc/>
internal override void NotifyInitialized(IAvaloniaObject o)
{
var e = new AvaloniaPropertyChangedEventArgs<TValue>(
o,
this,
default,
o.GetValue(this),
BindingPriority.Unset);
NotifyInitialized(e);
}
/// <inheritdoc/>
internal override object RouteGetValue(IAvaloniaObject o)
{
return o.GetValue<TValue>(this);
}
/// <inheritdoc/>
internal override void RouteSetValue(
IAvaloniaObject o,
object value,
BindingPriority priority)
{
var v = TryConvert(value);
if (v.HasValue)
{
f = Cast(validate);
o.SetValue<TValue>(this, (TValue)v.Value, priority);
}
else
else if (v.Type == BindingValueType.UnsetValue)
{
// Passing null to the validation function means that the property metadata merge
// will take the base validation function, so instead use an empty validation.
f = (o, v) => v;
o.ClearValue(this);
}
else if (v.HasError)
{
throw v.Error;
}
base.OverrideMetadata(typeof(THost), new StyledPropertyMetadata<TValue>(validate: f));
}
/// <summary>
/// Gets the string representation of the property.
/// </summary>
/// <returns>The property's string representation.</returns>
public override string ToString()
/// <inheritdoc/>
internal override IDisposable RouteBind(
IAvaloniaObject o,
IObservable<BindingValue<object>> source,
BindingPriority priority)
{
return Name;
var adapter = TypedBindingAdapter<TValue>.Create(o, this, source);
return o.Bind<TValue>(this, adapter, priority);
}
/// <inheritdoc/>
Func<IAvaloniaObject, object, object> IStyledPropertyAccessor.GetValidationFunc(Type type)
internal override void RouteInheritanceParentChanged(
AvaloniaObject o,
IAvaloniaObject oldParent)
{
Contract.Requires<ArgumentNullException>(type != null);
return ((IStyledPropertyMetadata)base.GetMetadata(type)).Validate;
o.InheritanceParentChanged(this, oldParent);
}
/// <inheritdoc/>
object IStyledPropertyAccessor.GetDefaultValue(Type type) => GetDefaultBoxedValue(type);
private object GetDefaultBoxedValue(Type type)
{
Contract.Requires<ArgumentNullException>(type != null);

15
src/Avalonia.Base/StyledPropertyMetadata`1.cs

@ -16,16 +16,13 @@ namespace Avalonia
/// Initializes a new instance of the <see cref="StyledPropertyMetadata{TValue}"/> class.
/// </summary>
/// <param name="defaultValue">The default value of the property.</param>
/// <param name="validate">A validation function.</param>
/// <param name="defaultBindingMode">The default binding mode.</param>
public StyledPropertyMetadata(
TValue defaultValue = default,
Func<IAvaloniaObject, TValue, TValue> validate = null,
BindingMode defaultBindingMode = BindingMode.Default)
: base(defaultBindingMode)
{
DefaultValue = new BoxedValue<TValue>(defaultValue);
Validate = validate;
}
/// <summary>
@ -33,15 +30,8 @@ namespace Avalonia
/// </summary>
internal BoxedValue<TValue> DefaultValue { get; private set; }
/// <summary>
/// Gets the validation callback.
/// </summary>
public Func<IAvaloniaObject, TValue, TValue> Validate { get; private set; }
object IStyledPropertyMetadata.DefaultValue => DefaultValue.Boxed;
Func<IAvaloniaObject, object, object> IStyledPropertyMetadata.Validate => Cast(Validate);
/// <inheritdoc/>
public override void Merge(PropertyMetadata baseMetadata, AvaloniaProperty property)
{
@ -53,11 +43,6 @@ namespace Avalonia
{
DefaultValue = src.DefaultValue;
}
if (Validate == null)
{
Validate = src.Validate;
}
}
}

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

@ -129,6 +129,27 @@ namespace Avalonia.Utilities
_entries[TryFindEntry(property.Id).Item1].Value = value;
}
public void Remove(AvaloniaProperty property)
{
var (index, found) = TryFindEntry(property.Id);
if (found)
{
Entry[] entries = new Entry[_entries.Length - 1];
int ix = 0;
for (int i = 0; i < _entries.Length; ++i)
{
if (i != index)
{
entries[ix++] = _entries[i];
}
}
_entries = entries;
}
}
public Dictionary<AvaloniaProperty, TValue> ToDictionary()
{
var dict = new Dictionary<AvaloniaProperty, TValue>(_entries.Length - 1);

11
src/Avalonia.Base/Utilities/TypeUtilities.cs

@ -289,6 +289,17 @@ namespace Avalonia.Utilities
return TryConvertImplicit(type, value, out object result) ? result : Default(type);
}
public static T ConvertImplicit<T>(object value)
{
if (TryConvertImplicit(typeof(T), value, out var result))
{
return (T)result;
}
throw new InvalidCastException(
$"Unable to convert object '{value ?? "(null)"}' of type '{value?.GetType()}' to type '{typeof(T)}'.");
}
/// <summary>
/// Gets the default value for the specified type.
/// </summary>

282
src/Avalonia.Base/ValueStore.cs

@ -1,205 +1,231 @@
using System;
using System.Collections.Generic;
using Avalonia.Data;
using Avalonia.PropertyStore;
using Avalonia.Utilities;
#nullable enable
namespace Avalonia
{
internal class ValueStore : IPriorityValueOwner
internal class ValueStore : IValueSink
{
private readonly AvaloniaPropertyValueStore<object> _propertyValues;
private readonly AvaloniaPropertyValueStore<object> _deferredSetters;
private readonly AvaloniaObject _owner;
private readonly IValueSink _sink;
private readonly AvaloniaPropertyValueStore<object> _values;
public ValueStore(AvaloniaObject owner)
{
_owner = owner;
_propertyValues = new AvaloniaPropertyValueStore<object>();
_deferredSetters = new AvaloniaPropertyValueStore<object>();
_sink = _owner = owner;
_values = new AvaloniaPropertyValueStore<object>();
}
public IDisposable AddBinding(
AvaloniaProperty property,
IObservable<object> source,
BindingPriority priority)
public bool IsAnimating(AvaloniaProperty property)
{
PriorityValue priorityValue;
if (_propertyValues.TryGetValue(property, out var v))
if (_values.TryGetValue(property, out var slot))
{
priorityValue = v as PriorityValue;
if (priorityValue == null)
if (slot is IValue v)
{
priorityValue = CreatePriorityValue(property);
priorityValue.SetValue(v, (int)BindingPriority.LocalValue);
_propertyValues.SetValue(property, priorityValue);
return v.ValuePriority < BindingPriority.LocalValue;
}
}
else
{
priorityValue = CreatePriorityValue(property);
_propertyValues.AddValue(property, priorityValue);
}
return priorityValue.Add(source, (int)priority);
return false;
}
public void AddValue(AvaloniaProperty property, object value, int priority)
public bool IsSet(AvaloniaProperty property)
{
PriorityValue priorityValue;
return TryGetValueUntyped(property, out _);
}
if (_propertyValues.TryGetValue(property, out var v))
public bool TryGetValue<T>(StyledPropertyBase<T> property, out T value)
{
if (_values.TryGetValue(property, out var slot))
{
priorityValue = v as PriorityValue;
if (priorityValue == null)
if (slot is IValue<T> v)
{
if (priority == (int)BindingPriority.LocalValue)
{
Validate(property, ref value);
_propertyValues.SetValue(property, value);
Changed(property, priority, v, value);
return;
}
else
if (v.Value.HasValue)
{
priorityValue = CreatePriorityValue(property);
priorityValue.SetValue(v, (int)BindingPriority.LocalValue);
_propertyValues.SetValue(property, priorityValue);
value = v.Value.Value;
return true;
}
}
}
else
{
if (value == AvaloniaProperty.UnsetValue)
else
{
return;
value = (T)slot;
return true;
}
}
value = default!;
return false;
}
if (priority == (int)BindingPriority.LocalValue)
public bool TryGetValueUntyped(AvaloniaProperty property, out object? value)
{
if (_values.TryGetValue(property, out var slot))
{
if (slot is IValue v)
{
Validate(property, ref value);
_propertyValues.AddValue(property, value);
Changed(property, priority, AvaloniaProperty.UnsetValue, value);
return;
if (v.Value.HasValue)
{
value = v.Value.Value;
return true;
}
}
else
{
priorityValue = CreatePriorityValue(property);
_propertyValues.AddValue(property, priorityValue);
value = slot;
return true;
}
}
priorityValue.SetValue(value, priority);
}
public void BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification)
{
_owner.BindingNotificationReceived(property, notification);
}
public void Changed(AvaloniaProperty property, int priority, object oldValue, object newValue)
{
_owner.PriorityValueChanged(property, priority, oldValue, newValue);
value = default;
return false;
}
public IDictionary<AvaloniaProperty, object> GetSetValues()
public void SetValue<T>(StyledPropertyBase<T> property, T value, BindingPriority priority)
{
return _propertyValues.ToDictionary();
if (_values.TryGetValue(property, out var slot))
{
SetExisting(slot, property, value, priority);
}
else if (priority == BindingPriority.LocalValue)
{
_values.AddValue(property, (object)value!);
_sink.ValueChanged(property, priority, default, value);
}
else
{
var entry = new ConstantValueEntry<T>(property, value, priority);
_values.AddValue(property, entry);
_sink.ValueChanged(property, priority, default, value);
}
}
public void LogError(AvaloniaProperty property, Exception e)
public IDisposable AddBinding<T>(
StyledPropertyBase<T> property,
IObservable<BindingValue<T>> source,
BindingPriority priority)
{
_owner.LogBindingError(property, e);
if (_values.TryGetValue(property, out var slot))
{
return BindExisting(slot, property, source, priority);
}
else
{
var entry = new BindingEntry<T>(_owner, property, source, priority, this);
_values.AddValue(property, entry);
entry.Start();
return entry;
}
}
public object GetValue(AvaloniaProperty property)
public void ClearLocalValue<T>(StyledPropertyBase<T> property)
{
var result = AvaloniaProperty.UnsetValue;
if (_propertyValues.TryGetValue(property, out var value))
if (_values.TryGetValue(property, out var slot))
{
result = (value is PriorityValue priorityValue) ? priorityValue.Value : value;
}
if (slot is PriorityValue<T> p)
{
p.ClearLocalValue();
}
else
{
var remove = slot is ConstantValueEntry<T> c ?
c.Priority == BindingPriority.LocalValue :
!(slot is IPriorityValueEntry<T>);
return result;
if (remove)
{
var old = TryGetValue(property, out var value) ? value : default;
_values.Remove(property);
_sink.ValueChanged(
property,
BindingPriority.LocalValue,
old,
BindingValue<T>.Unset);
}
}
}
}
public bool IsAnimating(AvaloniaProperty property)
void IValueSink.ValueChanged<T>(
StyledPropertyBase<T> property,
BindingPriority priority,
Optional<T> oldValue,
BindingValue<T> newValue)
{
return _propertyValues.TryGetValue(property, out var value) && value is PriorityValue priority && priority.IsAnimating;
_sink.ValueChanged(property, priority, oldValue, newValue);
}
public bool IsSet(AvaloniaProperty property)
void IValueSink.Completed(AvaloniaProperty property, IPriorityValueEntry entry)
{
if (_propertyValues.TryGetValue(property, out var value))
if (_values.TryGetValue(property, out var slot))
{
return ((value as PriorityValue)?.Value ?? value) != AvaloniaProperty.UnsetValue;
if (slot == entry)
{
_values.Remove(property);
}
}
return false;
}
public void Revalidate(AvaloniaProperty property)
private void SetExisting<T>(
object slot,
StyledPropertyBase<T> property,
T value,
BindingPriority priority)
{
if (_propertyValues.TryGetValue(property, out var value))
if (slot is IPriorityValueEntry<T> e)
{
(value as PriorityValue)?.Revalidate();
var priorityValue = new PriorityValue<T>(_owner, property, this, e);
_values.SetValue(property, priorityValue);
priorityValue.SetValue(value, priority);
}
}
public void VerifyAccess() => _owner.VerifyAccess();
private PriorityValue CreatePriorityValue(AvaloniaProperty property)
{
var validate = ((IStyledPropertyAccessor)property).GetValidationFunc(_owner.GetType());
Func<object, object> validate2 = null;
if (validate != null)
else if (slot is PriorityValue<T> p)
{
validate2 = v => validate(_owner, v);
p.SetValue(value, priority);
}
else if (priority == BindingPriority.LocalValue)
{
var old = (T)slot;
_values.SetValue(property, (object)value!);
_sink.ValueChanged(property, priority, old, value);
}
else
{
var existing = new ConstantValueEntry<T>(property, (T)slot, BindingPriority.LocalValue);
var priorityValue = new PriorityValue<T>(_owner, property, this, existing);
priorityValue.SetValue(value, priority);
_values.SetValue(property, priorityValue);
}
return new PriorityValue(
this,
property,
property.PropertyType,
validate2);
}
private void Validate(AvaloniaProperty property, ref object value)
private IDisposable BindExisting<T>(
object slot,
StyledPropertyBase<T> property,
IObservable<BindingValue<T>> source,
BindingPriority priority)
{
var validate = ((IStyledPropertyAccessor)property).GetValidationFunc(_owner.GetType());
PriorityValue<T> priorityValue;
if (validate != null && value != AvaloniaProperty.UnsetValue)
if (slot is IPriorityValueEntry<T> e)
{
value = validate(_owner, value);
priorityValue = new PriorityValue<T>(_owner, property, this, e);
}
}
private DeferredSetter<T> GetDeferredSetter<T>(AvaloniaProperty property)
{
if (_deferredSetters.TryGetValue(property, out var deferredSetter))
else if (slot is PriorityValue<T> p)
{
return (DeferredSetter<T>)deferredSetter;
priorityValue = p;
}
else
{
var existing = new ConstantValueEntry<T>(property, (T)slot, BindingPriority.LocalValue);
priorityValue = new PriorityValue<T>(_owner, property, this, existing);
}
var newDeferredSetter = new DeferredSetter<T>();
_deferredSetters.AddValue(property, newDeferredSetter);
return newDeferredSetter;
}
public DeferredSetter<object> GetNonDirectDeferredSetter(AvaloniaProperty property)
{
return GetDeferredSetter<object>(property);
}
public DeferredSetter<T> GetDirectDeferredSetter<T>(AvaloniaProperty<T> property)
{
return GetDeferredSetter<T>(property);
var binding = priorityValue.AddBinding(source, priority);
_values.SetValue(property, priorityValue);
binding.Start();
return binding;
}
}
}

24
src/Avalonia.Controls.DataGrid/DataGrid.cs

@ -201,8 +201,8 @@ namespace Avalonia.Controls
public static readonly StyledProperty<double> ColumnHeaderHeightProperty =
AvaloniaProperty.Register<DataGrid, double>(
nameof(ColumnHeaderHeight),
defaultValue: double.NaN,
validate: ValidateColumnHeaderHeight);
defaultValue: double.NaN/*,
validate: ValidateColumnHeaderHeight*/);
private static double ValidateColumnHeaderHeight(DataGrid grid, double value)
{
@ -261,8 +261,8 @@ namespace Avalonia.Controls
public static readonly StyledProperty<int> FrozenColumnCountProperty =
AvaloniaProperty.Register<DataGrid, int>(
nameof(FrozenColumnCount),
validate: ValidateFrozenColumnCount);
nameof(FrozenColumnCount)/*,
validate: ValidateFrozenColumnCount*/);
/// <summary>
/// Gets or sets the number of columns that the user cannot scroll horizontally.
@ -395,8 +395,8 @@ namespace Avalonia.Controls
public static readonly StyledProperty<double> MaxColumnWidthProperty =
AvaloniaProperty.Register<DataGrid, double>(
nameof(MaxColumnWidth),
defaultValue: DATAGRID_defaultMaxColumnWidth,
validate: ValidateMaxColumnWidth);
defaultValue: DATAGRID_defaultMaxColumnWidth/*,
validate: ValidateMaxColumnWidth*/);
private static double ValidateMaxColumnWidth(DataGrid grid, double value)
{
@ -433,8 +433,8 @@ namespace Avalonia.Controls
public static readonly StyledProperty<double> MinColumnWidthProperty =
AvaloniaProperty.Register<DataGrid, double>(
nameof(MinColumnWidth),
defaultValue: DATAGRID_defaultMinColumnWidth,
validate: ValidateMinColumnWidth);
defaultValue: DATAGRID_defaultMinColumnWidth/*,
validate: ValidateMinColumnWidth*/);
private static double ValidateMinColumnWidth(DataGrid grid, double value)
{
@ -482,8 +482,8 @@ namespace Avalonia.Controls
public static readonly StyledProperty<double> RowHeightProperty =
AvaloniaProperty.Register<DataGrid, double>(
nameof(RowHeight),
defaultValue: double.NaN,
validate: ValidateRowHeight);
defaultValue: double.NaN/*,
validate: ValidateRowHeight*/);
private static double ValidateRowHeight(DataGrid grid, double value)
{
if (value < DataGridRow.DATAGRIDROW_minimumHeight)
@ -510,8 +510,8 @@ namespace Avalonia.Controls
public static readonly StyledProperty<double> RowHeaderWidthProperty =
AvaloniaProperty.Register<DataGrid, double>(
nameof(RowHeaderWidth),
defaultValue: double.NaN,
validate: ValidateRowHeaderWidth);
defaultValue: double.NaN/*,
validate: ValidateRowHeaderWidth*/);
private static double ValidateRowHeaderWidth(DataGrid grid, double value)
{
if (value < DATAGRID_minimumRowHeaderWidth)

4
src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs

@ -67,8 +67,8 @@ namespace Avalonia.Controls
public static readonly StyledProperty<double> SublevelIndentProperty =
AvaloniaProperty.Register<DataGridRowGroupHeader, double>(
nameof(SublevelIndent),
defaultValue: DataGrid.DATAGRID_defaultRowGroupSublevelIndent,
validate: ValidateSublevelIndent);
defaultValue: DataGrid.DATAGRID_defaultRowGroupSublevelIndent/*,
validate: ValidateSublevelIndent*/);
private static double ValidateSublevelIndent(DataGridRowGroupHeader header, double value)
{

16
src/Avalonia.Controls/AutoCompleteBox.cs

@ -377,8 +377,8 @@ namespace Avalonia.Controls
/// dependency property.</value>
public static readonly StyledProperty<int> MinimumPrefixLengthProperty =
AvaloniaProperty.Register<AutoCompleteBox, int>(
nameof(MinimumPrefixLength), 1,
validate: ValidateMinimumPrefixLength);
nameof(MinimumPrefixLength), 1/*,
validate: ValidateMinimumPrefixLength*/);
/// <summary>
/// Identifies the
@ -391,8 +391,8 @@ namespace Avalonia.Controls
public static readonly StyledProperty<TimeSpan> MinimumPopulateDelayProperty =
AvaloniaProperty.Register<AutoCompleteBox, TimeSpan>(
nameof(MinimumPopulateDelay),
TimeSpan.Zero,
validate: ValidateMinimumPopulateDelay);
TimeSpan.Zero/*,
validate: ValidateMinimumPopulateDelay*/);
/// <summary>
/// Identifies the
@ -405,8 +405,8 @@ namespace Avalonia.Controls
public static readonly StyledProperty<double> MaxDropDownHeightProperty =
AvaloniaProperty.Register<AutoCompleteBox, double>(
nameof(MaxDropDownHeight),
double.PositiveInfinity,
validate: ValidateMaxDropDownHeight);
double.PositiveInfinity/*,
validate: ValidateMaxDropDownHeight*/);
/// <summary>
/// Identifies the
@ -494,8 +494,8 @@ namespace Avalonia.Controls
public static readonly StyledProperty<AutoCompleteFilterMode> FilterModeProperty =
AvaloniaProperty.Register<AutoCompleteBox, AutoCompleteFilterMode>(
nameof(FilterMode),
defaultValue: AutoCompleteFilterMode.StartsWith,
validate: ValidateFilterMode);
defaultValue: AutoCompleteFilterMode.StartsWith/*,
validate: ValidateFilterMode*/);
/// <summary>
/// Identifies the

18
src/Avalonia.Controls/Button.cs

@ -306,18 +306,13 @@ namespace Avalonia.Controls
}
}
}
protected override void OnPointerCaptureLost(PointerCaptureLostEventArgs e)
{
IsPressed = false;
}
protected override void UpdateDataValidation(AvaloniaProperty property, BindingNotification status)
protected override void UpdateDataValidation<T>(AvaloniaProperty<T> property, BindingValue<T> value)
{
base.UpdateDataValidation(property, status);
base.UpdateDataValidation(property, value);
if (property == CommandProperty)
{
if (status?.ErrorType == BindingErrorType.Error)
if (value.Type == BindingValueType.BindingError)
{
if (_commandCanExecute)
{
@ -328,6 +323,11 @@ namespace Avalonia.Controls
}
}
protected override void OnPointerCaptureLost(PointerCaptureLostEventArgs e)
{
IsPressed = false;
}
/// <summary>
/// Called when the <see cref="Command"/> property changes.
/// </summary>

4
src/Avalonia.Controls/Calendar/Calendar.cs

@ -351,8 +351,8 @@ namespace Avalonia.Controls
public static readonly StyledProperty<CalendarMode> DisplayModeProperty =
AvaloniaProperty.Register<Calendar, CalendarMode>(
nameof(DisplayMode),
validate: ValidateDisplayMode);
nameof(DisplayMode)/*,
validate: ValidateDisplayMode*/);
/// <summary>
/// Gets or sets a value indicating whether the calendar is displayed in
/// months, years, or decades.

18
src/Avalonia.Controls/Calendar/DatePicker.cs

@ -189,14 +189,14 @@ namespace Avalonia.Controls
public static readonly StyledProperty<DatePickerFormat> SelectedDateFormatProperty =
AvaloniaProperty.Register<DatePicker, DatePickerFormat>(
nameof(SelectedDateFormat),
defaultValue: DatePickerFormat.Short,
validate: ValidateSelectedDateFormat);
defaultValue: DatePickerFormat.Short/*,
validate: ValidateSelectedDateFormat*/);
public static readonly StyledProperty<string> CustomDateFormatStringProperty =
AvaloniaProperty.Register<DatePicker, string>(
nameof(CustomDateFormatString),
defaultValue: "d",
validate: ValidateDateFormatString);
defaultValue: "d"/*,
validate: ValidateDateFormatString*/);
public static readonly DirectProperty<DatePicker, string> TextProperty =
AvaloniaProperty.RegisterDirect<DatePicker, string>(
@ -512,11 +512,17 @@ namespace Avalonia.Controls
base.OnTemplateApplied(e);
}
protected override void UpdateDataValidation(AvaloniaProperty property, BindingNotification status)
protected override void OnPropertyChanged<T>(
AvaloniaProperty<T> property,
Optional<T> oldValue,
BindingValue<T> newValue,
BindingPriority priority)
{
base.OnPropertyChanged(property, oldValue, newValue, priority);
if (property == SelectedDateProperty)
{
DataValidationErrors.SetError(this, status.Error);
DataValidationErrors.SetError(this, newValue.Error);
}
}

10
src/Avalonia.Controls/MenuItem.cs

@ -26,8 +26,8 @@ namespace Avalonia.Controls
/// </summary>
public static readonly DirectProperty<MenuItem, ICommand> CommandProperty =
Button.CommandProperty.AddOwner<MenuItem>(
menuItem => menuItem.Command,
(menuItem, command) => menuItem.Command = command,
menuItem => menuItem.Command,
(menuItem, command) => menuItem.Command = command,
enableDataValidation: true);
/// <summary>
@ -394,12 +394,12 @@ namespace Avalonia.Controls
}
}
protected override void UpdateDataValidation(AvaloniaProperty property, BindingNotification status)
protected override void UpdateDataValidation<T>(AvaloniaProperty<T> property, BindingValue<T> value)
{
base.UpdateDataValidation(property, status);
base.UpdateDataValidation(property, value);
if (property == CommandProperty)
{
if (status?.ErrorType == BindingErrorType.Error)
if (value.Type == BindingValueType.BindingError)
{
if (_commandCanExecute)
{

6
src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs

@ -58,7 +58,7 @@ namespace Avalonia.Controls
/// Defines the <see cref="Increment"/> property.
/// </summary>
public static readonly StyledProperty<double> IncrementProperty =
AvaloniaProperty.Register<NumericUpDown, double>(nameof(Increment), 1.0d, validate: OnCoerceIncrement);
AvaloniaProperty.Register<NumericUpDown, double>(nameof(Increment), 1.0d/*, validate: OnCoerceIncrement*/);
/// <summary>
/// Defines the <see cref="IsReadOnly"/> property.
@ -70,13 +70,13 @@ namespace Avalonia.Controls
/// Defines the <see cref="Maximum"/> property.
/// </summary>
public static readonly StyledProperty<double> MaximumProperty =
AvaloniaProperty.Register<NumericUpDown, double>(nameof(Maximum), double.MaxValue, validate: OnCoerceMaximum);
AvaloniaProperty.Register<NumericUpDown, double>(nameof(Maximum), double.MaxValue/*, validate: OnCoerceMaximum*/);
/// <summary>
/// Defines the <see cref="Minimum"/> property.
/// </summary>
public static readonly StyledProperty<double> MinimumProperty =
AvaloniaProperty.Register<NumericUpDown, double>(nameof(Minimum), double.MinValue, validate: OnCoerceMinimum);
AvaloniaProperty.Register<NumericUpDown, double>(nameof(Minimum), double.MinValue/*, validate: OnCoerceMinimum*/);
/// <summary>
/// Defines the <see cref="ParsingNumberStyle"/> property.

2
src/Avalonia.Controls/Primitives/ScrollBar.cs

@ -73,7 +73,7 @@ namespace Avalonia.Controls.Primitives
this.GetObservable(ViewportSizeProperty).Select(_ => Unit.Default),
this.GetObservable(VisibilityProperty).Select(_ => Unit.Default))
.Select(_ => CalculateIsVisible());
Bind(IsVisibleProperty, isVisible, BindingPriority.Style);
this.Bind(IsVisibleProperty, isVisible, BindingPriority.Style);
}
/// <summary>

27
src/Avalonia.Controls/Repeater/ItemsRepeater.cs

@ -7,6 +7,7 @@ using System;
using System.Collections;
using System.Collections.Specialized;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Layout;
@ -374,41 +375,37 @@ namespace Avalonia.Controls
_viewportManager.ResetScrollers();
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs args)
protected override void OnPropertyChanged<T>(AvaloniaProperty<T> property, Optional<T> oldValue, BindingValue<T> newValue, BindingPriority priority)
{
var property = args.Property;
if (property == ItemsProperty)
{
var newValue = (IEnumerable)args.NewValue;
var newDataSource = newValue as ItemsSourceView;
if (newValue != null && newDataSource == null)
var newEnumerable = newValue.ValueOrDefault<IEnumerable>();
var newDataSource = newEnumerable as ItemsSourceView;
if (newEnumerable != null && newDataSource == null)
{
newDataSource = new ItemsSourceView(newValue);
newDataSource = new ItemsSourceView(newEnumerable);
}
OnDataSourcePropertyChanged(ItemsSourceView, newDataSource);
}
else if (property == ItemTemplateProperty)
{
OnItemTemplateChanged((IDataTemplate)args.OldValue, (IDataTemplate)args.NewValue);
OnItemTemplateChanged(oldValue.ValueOrDefault<IDataTemplate>(), newValue.ValueOrDefault<IDataTemplate>());
}
else if (property == LayoutProperty)
{
OnLayoutChanged((AttachedLayout)args.OldValue, (AttachedLayout)args.NewValue);
OnLayoutChanged(oldValue.ValueOrDefault<AttachedLayout>(), newValue.ValueOrDefault<AttachedLayout>());
}
else if (property == HorizontalCacheLengthProperty)
{
_viewportManager.HorizontalCacheLength = (double)args.NewValue;
_viewportManager.HorizontalCacheLength = newValue.ValueOrDefault<double>();
}
else if (property == VerticalCacheLengthProperty)
{
_viewportManager.VerticalCacheLength = (double)args.NewValue;
}
else
{
base.OnPropertyChanged(args);
_viewportManager.VerticalCacheLength = newValue.ValueOrDefault<double>();
}
base.OnPropertyChanged(property, oldValue, newValue, priority);
}
internal IControl GetElementImpl(int index, bool forceCreate, bool supressAutoRecycle)

2
src/Avalonia.Controls/ScrollViewer.cs

@ -161,8 +161,6 @@ namespace Avalonia.Controls
/// </summary>
static ScrollViewer()
{
AffectsValidation(ExtentProperty, OffsetProperty);
AffectsValidation(ViewportProperty, OffsetProperty);
HorizontalScrollBarVisibilityProperty.Changed.AddClassHandler<ScrollViewer>((x, e) => x.ScrollBarVisibilityChanged(e));
VerticalScrollBarVisibilityProperty.Changed.AddClassHandler<ScrollViewer>((x, e) => x.ScrollBarVisibilityChanged(e));
}

8
src/Avalonia.Controls/TextBox.cs

@ -63,7 +63,7 @@ namespace Avalonia.Controls
AvaloniaProperty.Register<TextBox, int>(nameof(MaxLength), defaultValue: 0);
public static readonly DirectProperty<TextBox, string> TextProperty =
TextBlock.TextProperty.AddOwner<TextBox>(
TextBlock.TextProperty.AddOwnerWithDataValidation<TextBox>(
o => o.Text,
(o, v) => o.Text = v,
defaultBindingMode: BindingMode.TwoWay,
@ -133,7 +133,7 @@ namespace Avalonia.Controls
return ScrollBarVisibility.Hidden;
}
});
Bind(
this.Bind(
ScrollViewer.HorizontalScrollBarVisibilityProperty,
horizontalScrollBarVisibility,
BindingPriority.Style);
@ -700,11 +700,11 @@ namespace Avalonia.Controls
}
}
protected override void UpdateDataValidation(AvaloniaProperty property, BindingNotification status)
protected override void UpdateDataValidation<T>(AvaloniaProperty<T> property, BindingValue<T> value)
{
if (property == TextProperty)
{
DataValidationErrors.SetError(this, status.Error);
DataValidationErrors.SetError(this, value.Error);
}
}

7
src/Avalonia.Layout/StackLayout.cs

@ -5,6 +5,7 @@
using System;
using System.Collections.Specialized;
using Avalonia.Data;
namespace Avalonia.Layout
{
@ -293,11 +294,11 @@ namespace Avalonia.Layout
InvalidateLayout();
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e)
protected override void OnPropertyChanged<T>(AvaloniaProperty<T> property, Optional<T> oldValue, BindingValue<T> newValue, BindingPriority priority)
{
if (e.Property == OrientationProperty)
if (property == OrientationProperty)
{
var orientation = (Orientation)e.NewValue;
var orientation = newValue.ValueOrDefault<Orientation>();
//Note: For StackLayout Vertical Orientation means we have a Vertical ScrollOrientation.
//Horizontal Orientation means we have a Horizontal ScrollOrientation.

33
src/Avalonia.Layout/UniformGridLayout.cs

@ -5,6 +5,7 @@
using System;
using System.Collections.Specialized;
using Avalonia.Data;
namespace Avalonia.Layout
{
@ -436,40 +437,42 @@ namespace Avalonia.Layout
gridState.ClearElementOnDataSourceChange(context, args);
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs args)
protected override void OnPropertyChanged<T>(AvaloniaProperty<T> property, Optional<T> oldValue, BindingValue<T> newValue, BindingPriority priority)
{
if (args.Property == OrientationProperty)
if (property == OrientationProperty)
{
var orientation = (Orientation)args.NewValue;
var orientation = newValue.ValueOrDefault<Orientation>();
//Note: For UniformGridLayout Vertical Orientation means we have a Horizontal ScrollOrientation. Horizontal Orientation means we have a Vertical ScrollOrientation.
//i.e. the properties are the inverse of each other.
var scrollOrientation = (orientation == Orientation.Horizontal) ? ScrollOrientation.Vertical : ScrollOrientation.Horizontal;
_orientation.ScrollOrientation = scrollOrientation;
}
else if (args.Property == MinColumnSpacingProperty)
else if (property == MinColumnSpacingProperty)
{
_minColumnSpacing = (double)args.NewValue;
_minColumnSpacing = newValue.ValueOrDefault<double>();
}
else if (args.Property == MinRowSpacingProperty)
else if (property == MinRowSpacingProperty)
{
_minRowSpacing = (double)args.NewValue;
_minRowSpacing = newValue.ValueOrDefault<double>();
}
else if (args.Property == ItemsJustificationProperty)
else if (property == ItemsJustificationProperty)
{
_itemsJustification = (UniformGridLayoutItemsJustification)args.NewValue;
_itemsJustification = newValue.ValueOrDefault<UniformGridLayoutItemsJustification>();
;
}
else if (args.Property == ItemsStretchProperty)
else if (property == ItemsStretchProperty)
{
_itemsStretch = (UniformGridLayoutItemsStretch)args.NewValue;
_itemsStretch = newValue.ValueOrDefault<UniformGridLayoutItemsStretch>();
;
}
else if (args.Property == MinItemWidthProperty)
else if (property == MinItemWidthProperty)
{
_minItemWidth = (double)args.NewValue;
_minItemWidth = newValue.ValueOrDefault<double>();
}
else if (args.Property == MinItemHeightProperty)
else if (property == MinItemHeightProperty)
{
_minItemHeight = (double)args.NewValue;
_minItemHeight = newValue.ValueOrDefault<double>();
}
InvalidateLayout();

6
src/Avalonia.Styling/StyledElement.cs

@ -478,7 +478,11 @@ namespace Avalonia
OnAttachedToLogicalTreeCore(e);
}
RaisePropertyChanged(ParentProperty, old, Parent, BindingPriority.LocalValue);
RaisePropertyChanged(
ParentProperty,
new Optional<IStyledElement>(old),
new BindingValue<IStyledElement>(Parent),
BindingPriority.LocalValue);
}
}

6
src/Avalonia.Visuals/Visual.cs

@ -433,7 +433,11 @@ namespace Avalonia
/// <param name="newParent">The new visual parent.</param>
protected virtual void OnVisualParentChanged(IVisual oldParent, IVisual newParent)
{
RaisePropertyChanged(VisualParentProperty, oldParent, newParent, BindingPriority.LocalValue);
RaisePropertyChanged(
VisualParentProperty,
new Optional<IVisual>(oldParent),
new BindingValue<IVisual>(newParent),
BindingPriority.LocalValue);
}
protected override sealed void LogBindingError(AvaloniaProperty property, Exception e)

22
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_AddOwner.cs

@ -1,7 +1,6 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using Xunit;
namespace Avalonia.Base.UnitTests
@ -16,31 +15,12 @@ namespace Avalonia.Base.UnitTests
Assert.Equal("foodefault", target.GetValue(Class2.FooProperty));
}
[Fact]
public void AddOwnered_Property_Does_Not_Retain_Validation()
{
var target = new Class2();
target.SetValue(Class2.FooProperty, "throw");
}
private class Class1 : AvaloniaObject
{
public static readonly StyledProperty<string> FooProperty =
AvaloniaProperty.Register<Class1, string>(
"Foo",
"foodefault",
validate: ValidateFoo);
private static string ValidateFoo(AvaloniaObject arg1, string arg2)
{
if (arg2 == "throw")
{
throw new IndexOutOfRangeException();
}
return arg2;
}
"foodefault");
}
private class Class2 : AvaloniaObject

8
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Attached.cs

@ -16,14 +16,6 @@ namespace Avalonia.Base.UnitTests
Assert.Equal("foodefault", target.GetValue(Class2.FooProperty));
}
[Fact]
public void AddOwnered_Property_Retains_Validation()
{
var target = new Class2();
Assert.Throws<IndexOutOfRangeException>(() => target.SetValue(Class2.FooProperty, "throw"));
}
[Fact]
public void AvaloniaProperty_Initialized_Is_Called_For_Attached_Property()
{

255
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs

@ -3,18 +3,15 @@
using System;
using System.ComponentModel;
using System.Reactive.Concurrency;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Data;
using Avalonia.Logging;
using Avalonia.Markup.Data;
using Avalonia.Platform;
using Avalonia.Threading;
using Avalonia.UnitTests;
using Avalonia.Diagnostics;
using Microsoft.Reactive.Testing;
using Moq;
using Xunit;
@ -26,13 +23,158 @@ namespace Avalonia.Base.UnitTests
[Fact]
public void Bind_Sets_Current_Value()
{
Class1 target = new Class1();
Class1 source = new Class1();
var target = new Class1();
var source = new Class1();
var property = Class1.FooProperty;
source.SetValue(Class1.FooProperty, "initial");
target.Bind(Class1.FooProperty, source.GetObservable(Class1.FooProperty));
source.SetValue(property, "initial");
target.Bind(property, source.GetObservable(property));
Assert.Equal("initial", target.GetValue(Class1.FooProperty));
Assert.Equal("initial", target.GetValue(property));
}
[Fact]
public void Bind_Raises_PropertyChanged()
{
var target = new Class1();
var source = new Subject<BindingValue<string>>();
bool raised = false;
target.PropertyChanged += (s, e) =>
raised = e.Property == Class1.FooProperty &&
(string)e.OldValue == "foodefault" &&
(string)e.NewValue == "newvalue" &&
e.Priority == BindingPriority.LocalValue;
target.Bind(Class1.FooProperty, source);
source.OnNext("newvalue");
Assert.True(raised);
}
[Fact]
public void PropertyChanged_Not_Raised_When_Value_Unchanged()
{
var target = new Class1();
var source = new Subject<BindingValue<string>>();
var raised = 0;
target.PropertyChanged += (s, e) => ++raised;
target.Bind(Class1.FooProperty, source);
source.OnNext("newvalue");
source.OnNext("newvalue");
Assert.Equal(1, raised);
}
[Fact]
public void Setting_LocalValue_Overrides_Binding_Until_Binding_Produces_Next_Value()
{
var target = new Class1();
var source = new Subject<string>();
var property = Class1.FooProperty;
target.Bind(property, source);
source.OnNext("foo");
Assert.Equal("foo", target.GetValue(property));
target.SetValue(property, "bar");
Assert.Equal("bar", target.GetValue(property));
source.OnNext("baz");
Assert.Equal("baz", target.GetValue(property));
}
[Fact]
public void Completing_LocalValue_Binding_Reverts_To_Default_Value_Even_When_Local_Value_Set_Earlier()
{
var target = new Class1();
var source = new Subject<string>();
var property = Class1.FooProperty;
target.Bind(property, source);
source.OnNext("foo");
target.SetValue(property, "bar");
source.OnNext("baz");
source.OnCompleted();
Assert.Equal("foodefault", target.GetValue(property));
}
[Fact]
public void Setting_Style_Value_Overrides_Binding_Permanently()
{
var target = new Class1();
var source = new Subject<string>();
target.Bind(Class1.FooProperty, source, BindingPriority.Style);
source.OnNext("foo");
Assert.Equal("foo", target.GetValue(Class1.FooProperty));
target.SetValue(Class1.FooProperty, "bar", BindingPriority.Style);
Assert.Equal("bar", target.GetValue(Class1.FooProperty));
source.OnNext("baz");
Assert.Equal("bar", target.GetValue(Class1.FooProperty));
}
[Fact]
public void Second_LocalValue_Binding_Overrides_First()
{
var property = Class1.FooProperty;
var target = new Class1();
var source1 = new Subject<string>();
var source2 = new Subject<string>();
target.Bind(property, source1, BindingPriority.LocalValue);
target.Bind(property, source2, BindingPriority.LocalValue);
source1.OnNext("foo");
Assert.Equal("foo", target.GetValue(property));
source2.OnNext("bar");
Assert.Equal("bar", target.GetValue(property));
source1.OnNext("baz");
Assert.Equal("bar", target.GetValue(property));
}
[Fact]
public void Completing_Second_LocalValue_Binding_Reverts_To_First()
{
var property = Class1.FooProperty;
var target = new Class1();
var source1 = new Subject<string>();
var source2 = new Subject<string>();
target.Bind(property, source1, BindingPriority.LocalValue);
target.Bind(property, source2, BindingPriority.LocalValue);
source1.OnNext("foo");
source2.OnNext("bar");
source1.OnNext("baz");
source2.OnCompleted();
Assert.Equal("baz", target.GetValue(property));
}
[Fact]
public void Completing_StyleTrigger_Binding_Reverts_To_StyleBinding()
{
var property = Class1.FooProperty;
var target = new Class1();
var source1 = new Subject<string>();
var source2 = new Subject<string>();
target.Bind(property, source1, BindingPriority.Style);
target.Bind(property, source2, BindingPriority.StyleTrigger);
source1.OnNext("foo");
source2.OnNext("bar");
source2.OnCompleted();
source1.OnNext("baz");
Assert.Equal("baz", target.GetValue(property));
}
[Fact]
@ -126,7 +268,7 @@ namespace Avalonia.Base.UnitTests
public void Observable_Is_Unsubscribed_When_Subscription_Disposed()
{
var scheduler = new TestScheduler();
var source = scheduler.CreateColdObservable<object>();
var source = scheduler.CreateColdObservable<string>();
var target = new Class1();
var subscription = target.Bind(Class1.FooProperty, source);
@ -191,13 +333,13 @@ namespace Avalonia.Base.UnitTests
obj2.SetValue(Class1.FooProperty, "second", BindingPriority.Style);
Assert.Equal("second", obj1.GetValue(Class1.FooProperty));
Assert.Equal("first", obj1.GetValue(Class1.FooProperty));
Assert.Equal("second", obj2.GetValue(Class1.FooProperty));
obj1.SetValue(Class1.FooProperty, "third", BindingPriority.Style);
Assert.Equal("third", obj1.GetValue(Class1.FooProperty));
Assert.Equal("third", obj2.GetValue(Class1.FooProperty));
Assert.Equal("second", obj2.GetValue(Class1.FooProperty));
}
[Fact]
@ -302,41 +444,62 @@ namespace Avalonia.Base.UnitTests
}
[Fact]
public void BindingError_Does_Not_Cause_Target_Update()
public void Binding_Error_Reverts_To_Default_Value()
{
var target = new Class1();
var source = new Subject<object>();
var source = new Subject<BindingValue<string>>();
target.Bind(Class1.QuxProperty, source);
source.OnNext(6.7);
source.OnNext(new BindingNotification(
new InvalidOperationException("Foo"),
BindingErrorType.Error));
target.Bind(Class1.FooProperty, source);
source.OnNext("initial");
source.OnNext(BindingValue<string>.BindingError(new InvalidOperationException("Foo")));
Assert.Equal(5.6, target.GetValue(Class1.QuxProperty));
Assert.Equal("foodefault", target.GetValue(Class1.FooProperty));
}
[Fact]
public void BindingNotification_With_FallbackValue_Causes_Target_Update()
public void Binding_Error_With_FallbackValue_Causes_Target_Update()
{
var target = new Class1();
var source = new Subject<object>();
var source = new Subject<BindingValue<string>>();
target.Bind(Class1.QuxProperty, source);
source.OnNext(6.7);
source.OnNext(new BindingNotification(
new InvalidOperationException("Foo"),
BindingErrorType.Error,
8.9));
target.Bind(Class1.FooProperty, source);
source.OnNext("initial");
source.OnNext(BindingValue<string>.BindingError(new InvalidOperationException("Foo"), "bar"));
Assert.Equal("bar", target.GetValue(Class1.FooProperty));
}
[Fact]
public void DataValidationError_Does_Not_Cause_Target_Update()
{
var target = new Class1();
var source = new Subject<BindingValue<string>>();
target.Bind(Class1.FooProperty, source);
source.OnNext("initial");
source.OnNext(BindingValue<string>.DataValidationError(new InvalidOperationException("Foo")));
Assert.Equal("initial", target.GetValue(Class1.FooProperty));
}
[Fact]
public void DataValidationError_With_FallbackValue_Causes_Target_Update()
{
var target = new Class1();
var source = new Subject<BindingValue<string>>();
Assert.Equal(8.9, target.GetValue(Class1.QuxProperty));
target.Bind(Class1.FooProperty, source);
source.OnNext("initial");
source.OnNext(BindingValue<string>.DataValidationError(new InvalidOperationException("Foo"), "bar"));
Assert.Equal("bar", target.GetValue(Class1.FooProperty));
}
[Fact]
public void Bind_Logs_Binding_Error()
{
var target = new Class1();
var source = new Subject<object>();
var source = new Subject<BindingValue<double>>();
var called = false;
var expectedMessageTemplate = "Error in binding to {Target}.{Property}: {Message}";
@ -354,9 +517,7 @@ namespace Avalonia.Base.UnitTests
{
target.Bind(Class1.QuxProperty, source);
source.OnNext(6.7);
source.OnNext(new BindingNotification(
new InvalidOperationException("Foo"),
BindingErrorType.Error));
source.OnNext(BindingValue<double>.BindingError(new InvalidOperationException("Foo")));
Assert.Equal(5.6, target.GetValue(Class1.QuxProperty));
Assert.True(called);
@ -367,7 +528,7 @@ namespace Avalonia.Base.UnitTests
public async Task Bind_With_Scheduler_Executes_On_Scheduler()
{
var target = new Class1();
var source = new Subject<object>();
var source = new Subject<double>();
var currentThreadId = Thread.CurrentThread.ManagedThreadId;
var threadingInterfaceMock = new Mock<IPlatformThreadingInterface>();
@ -426,13 +587,13 @@ namespace Avalonia.Base.UnitTests
}
[Fact]
public void IsAnimating_On_Property_With_Animation_Value_Returns_False()
public void IsAnimating_On_Property_With_Animation_Value_Returns_True()
{
var target = new Class1();
target.SetValue(Class1.FooProperty, "foo", BindingPriority.Animation);
Assert.False(target.IsAnimating(Class1.FooProperty));
Assert.True(target.IsAnimating(Class1.FooProperty));
}
[Fact]
@ -457,6 +618,30 @@ namespace Avalonia.Base.UnitTests
Assert.True(target.IsAnimating(Class1.FooProperty));
}
[Fact]
public void IsAnimating_On_Property_With_Local_Value_And_Animation_Binding_Returns_True()
{
var target = new Class1();
var source = new BehaviorSubject<string>("foo");
target.SetValue(Class1.FooProperty, "bar");
target.Bind(Class1.FooProperty, source, BindingPriority.Animation);
Assert.True(target.IsAnimating(Class1.FooProperty));
}
[Fact]
public void IsAnimating_Returns_True_When_Animated_Value_Is_Same_As_Local_Value()
{
var target = new Class1();
var source = new BehaviorSubject<string>("foo");
target.SetValue(Class1.FooProperty, "foo");
target.Bind(Class1.FooProperty, source, BindingPriority.Animation);
Assert.True(target.IsAnimating(Class1.FooProperty));
}
[Fact]
public void TwoWay_Binding_Should_Not_Call_Setter_On_Creation()
{

101
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs

@ -11,59 +11,31 @@ namespace Avalonia.Base.UnitTests
public class AvaloniaObjectTests_DataValidation
{
[Fact]
public void Setting_Non_Validated_Property_Does_Not_Call_UpdateDataValidation()
public void Binding_Non_Validated_Styled_Property_Does_Not_Call_UpdateDataValidation()
{
var target = new Class1();
var source = new Subject<BindingValue<int>>();
target.SetValue(Class1.NonValidatedDirectProperty, 6);
target.Bind(Class1.NonValidatedProperty, source);
source.OnNext(6);
source.OnNext(BindingValue<int>.BindingError(new Exception()));
source.OnNext(BindingValue<int>.DataValidationError(new Exception()));
source.OnNext(6);
Assert.Empty(target.Notifications);
}
[Fact]
public void Setting_Non_Validated_Direct_Property_Does_Not_Call_UpdateDataValidation()
public void Binding_Non_Validated_Direct_Property_Does_Not_Call_UpdateDataValidation()
{
var target = new Class1();
var source = new Subject<BindingValue<int>>();
target.SetValue(Class1.NonValidatedDirectProperty, 6);
Assert.Empty(target.Notifications);
}
[Fact]
public void Setting_Validated_Direct_Property_Calls_UpdateDataValidation()
{
var target = new Class1();
target.SetValue(Class1.ValidatedDirectIntProperty, new BindingNotification(6));
target.SetValue(Class1.ValidatedDirectIntProperty, new BindingNotification(new Exception(), BindingErrorType.Error));
target.SetValue(Class1.ValidatedDirectIntProperty, new BindingNotification(new Exception(), BindingErrorType.DataValidationError));
target.SetValue(Class1.ValidatedDirectIntProperty, new BindingNotification(7));
Assert.Equal(
new[]
{
new BindingNotification(6),
new BindingNotification(new Exception(), BindingErrorType.Error),
new BindingNotification(new Exception(), BindingErrorType.DataValidationError),
new BindingNotification(7),
},
target.Notifications.AsEnumerable());
}
[Fact]
public void Binding_Non_Validated_Property_Does_Not_Call_UpdateDataValidation()
{
var source = new Subject<object>();
var target = new Class1
{
[!Class1.NonValidatedProperty] = source.ToBinding(),
};
source.OnNext(new BindingNotification(6));
source.OnNext(new BindingNotification(new Exception(), BindingErrorType.Error));
source.OnNext(new BindingNotification(new Exception(), BindingErrorType.DataValidationError));
source.OnNext(new BindingNotification(7));
target.Bind(Class1.NonValidatedDirectProperty, source);
source.OnNext(6);
source.OnNext(BindingValue<int>.BindingError(new Exception()));
source.OnNext(BindingValue<int>.DataValidationError(new Exception()));
source.OnNext(6);
Assert.Empty(target.Notifications);
}
@ -71,26 +43,23 @@ namespace Avalonia.Base.UnitTests
[Fact]
public void Binding_Validated_Direct_Property_Calls_UpdateDataValidation()
{
var source = new Subject<object>();
var target = new Class1
{
[!Class1.ValidatedDirectIntProperty] = source.ToBinding(),
};
source.OnNext(new BindingNotification(6));
source.OnNext(new BindingNotification(new Exception(), BindingErrorType.Error));
source.OnNext(new BindingNotification(new Exception(), BindingErrorType.DataValidationError));
source.OnNext(new BindingNotification(7));
Assert.Equal(
new[]
{
new BindingNotification(6),
new BindingNotification(new Exception(), BindingErrorType.Error),
new BindingNotification(new Exception(), BindingErrorType.DataValidationError),
new BindingNotification(7),
},
target.Notifications.AsEnumerable());
var target = new Class1();
var source = new Subject<BindingValue<int>>();
target.Bind(Class1.ValidatedDirectIntProperty, source);
source.OnNext(6);
source.OnNext(BindingValue<int>.BindingError(new Exception()));
source.OnNext(BindingValue<int>.DataValidationError(new Exception()));
source.OnNext(7);
var result = target.Notifications.Cast<BindingValue<int>>().ToList();
Assert.Equal(4, result.Count);
Assert.Equal(BindingValueType.Value, result[0].Type);
Assert.Equal(6, result[0].Value);
Assert.Equal(BindingValueType.BindingError, result[1].Type);
Assert.Equal(BindingValueType.DataValidationError, result[2].Type);
Assert.Equal(BindingValueType.Value, result[3].Type);
Assert.Equal(7, result[3].Value);
}
[Fact]
@ -171,11 +140,13 @@ namespace Avalonia.Base.UnitTests
set { SetAndRaise(ValidatedDirectStringProperty, ref _directString, value); }
}
public IList<BindingNotification> Notifications { get; } = new List<BindingNotification>();
public IList<object> Notifications { get; } = new List<object>();
protected override void UpdateDataValidation(AvaloniaProperty property, BindingNotification notification)
protected override void UpdateDataValidation<T>(
AvaloniaProperty<T> property,
BindingValue<T> value)
{
Notifications.Add(notification);
Notifications.Add(value);
}
}

172
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs

@ -7,12 +7,9 @@ using System.ComponentModel;
using System.Reactive.Subjects;
using System.Threading;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Data;
using Avalonia.Logging;
using Avalonia.Platform;
using Avalonia.Threading;
using Avalonia.Markup.Data;
using Avalonia.UnitTests;
using Moq;
using Xunit;
@ -22,7 +19,7 @@ namespace Avalonia.Base.UnitTests
public class AvaloniaObjectTests_Direct
{
[Fact]
public void GetValue_Gets_Value()
public void GetValue_Gets_Default_Value()
{
var target = new Class1();
@ -109,6 +106,62 @@ namespace Avalonia.Base.UnitTests
Assert.True(raised);
}
[Fact]
public void Setting_Object_Property_To_UnsetValue_Reverts_To_Default_Value()
{
Class1 target = new Class1();
target.SetValue(Class1.FrankProperty, "newvalue");
target.SetValue(Class1.FrankProperty, AvaloniaProperty.UnsetValue);
Assert.Equal("Kups", target.GetValue(Class1.FrankProperty));
}
[Fact]
public void Setting_Object_Property_To_DoNothing_Does_Nothing()
{
Class1 target = new Class1();
target.SetValue(Class1.FrankProperty, "newvalue");
target.SetValue(Class1.FrankProperty, BindingOperations.DoNothing);
Assert.Equal("newvalue", target.GetValue(Class1.FrankProperty));
}
[Fact]
public void Bind_Raises_PropertyChanged()
{
var target = new Class1();
var source = new Subject<BindingValue<string>>();
bool raised = false;
target.PropertyChanged += (s, e) =>
raised = e.Property == Class1.FooProperty &&
(string)e.OldValue == "initial" &&
(string)e.NewValue == "newvalue" &&
e.Priority == BindingPriority.LocalValue;
target.Bind(Class1.FooProperty, source);
source.OnNext("newvalue");
Assert.True(raised);
}
[Fact]
public void PropertyChanged_Not_Raised_When_Value_Unchanged()
{
var target = new Class1();
var source = new Subject<BindingValue<string>>();
var raised = 0;
target.PropertyChanged += (s, e) => ++raised;
target.Bind(Class1.FooProperty, source);
source.OnNext("newvalue");
source.OnNext("newvalue");
Assert.Equal(1, raised);
}
[Fact]
public void SetValue_On_Unregistered_Property_Throws_Exception()
{
@ -117,6 +170,35 @@ namespace Avalonia.Base.UnitTests
Assert.Throws<ArgumentException>(() => target.SetValue(Class1.BarProperty, "value"));
}
[Fact]
public void ClearValue_Restores_Default_value()
{
var target = new Class1();
Assert.Equal("initial", target.GetValue(Class1.FooProperty));
}
[Fact]
public void ClearValue_Raises_PropertyChanged()
{
Class1 target = new Class1();
var raised = 0;
target.SetValue(Class1.FooProperty, "newvalue");
target.PropertyChanged += (s, e) =>
{
Assert.Same(target, s);
Assert.Equal(Class1.FooProperty, e.Property);
Assert.Equal("newvalue", (string)e.OldValue);
Assert.Equal("unset", (string)e.NewValue);
++raised;
};
target.ClearValue(Class1.FooProperty);
Assert.Equal(1, raised);
}
[Fact]
public void GetObservable_Returns_Values()
{
@ -170,7 +252,7 @@ namespace Avalonia.Base.UnitTests
}
[Fact]
public void Bind_NonGeneric_Uses_UnsetValue()
public void Bind_NonGeneric_Accepts_UnsetValue()
{
var target = new Class1();
var source = new Subject<object>();
@ -194,7 +276,7 @@ namespace Avalonia.Base.UnitTests
source.OnNext(45);
Assert.Null(target.Foo);
Assert.Equal("unset", target.Foo);
}
[Fact]
@ -207,7 +289,7 @@ namespace Avalonia.Base.UnitTests
source.OnNext("foo");
Assert.Equal(0, target.Baz);
Assert.Equal(-1, target.Baz);
}
[Fact]
@ -358,31 +440,67 @@ namespace Avalonia.Base.UnitTests
Assert.True(raised);
}
[Fact]
public void Binding_Error_Reverts_To_Default_Value()
{
var target = new Class1();
var source = new Subject<BindingValue<string>>();
target.Bind(Class1.FooProperty, source);
source.OnNext("initial");
source.OnNext(BindingValue<string>.BindingError(new InvalidOperationException("Foo")));
Assert.Equal("unset", target.GetValue(Class1.FooProperty));
}
[Fact]
public void Binding_Error_With_FallbackValue_Causes_Target_Update()
{
var target = new Class1();
var source = new Subject<BindingValue<string>>();
target.Bind(Class1.FooProperty, source);
source.OnNext("initial");
source.OnNext(BindingValue<string>.BindingError(new InvalidOperationException("Foo"), "bar"));
Assert.Equal("bar", target.GetValue(Class1.FooProperty));
}
[Fact]
public void DataValidationError_Does_Not_Cause_Target_Update()
{
var target = new Class1();
var source = new Subject<object>();
var source = new Subject<BindingValue<string>>();
target.Bind(Class1.FooProperty, source);
source.OnNext("initial");
source.OnNext(new BindingNotification(new InvalidOperationException("Foo"), BindingErrorType.DataValidationError));
source.OnNext(BindingValue<string>.DataValidationError(new InvalidOperationException("Foo")));
Assert.Equal("initial", target.GetValue(Class1.FooProperty));
}
[Fact]
public void DataValidationError_With_FallbackValue_Causes_Target_Update()
{
var target = new Class1();
var source = new Subject<BindingValue<string>>();
target.Bind(Class1.FooProperty, source);
source.OnNext("initial");
source.OnNext(BindingValue<string>.DataValidationError(new InvalidOperationException("Foo"), "bar"));
Assert.Equal("bar", target.GetValue(Class1.FooProperty));
}
[Fact]
public void BindingError_With_FallbackValue_Causes_Target_Update()
{
var target = new Class1();
var source = new Subject<object>();
var source = new Subject<BindingValue<string>>();
target.Bind(Class1.FooProperty, source);
source.OnNext("initial");
source.OnNext(new BindingNotification(
new InvalidOperationException("Foo"),
BindingErrorType.Error,
"fallback"));
source.OnNext(BindingValue<string>.BindingError(new InvalidOperationException("Foo"), "fallback"));
Assert.Equal("fallback", target.GetValue(Class1.FooProperty));
}
@ -391,7 +509,7 @@ namespace Avalonia.Base.UnitTests
public void Binding_To_Direct_Property_Logs_BindingError()
{
var target = new Class1();
var source = new Subject<object>();
var source = new Subject<BindingValue<string>>();
var called = false;
LogCallback checkLogMessage = (level, area, src, mt, pv) =>
@ -412,7 +530,7 @@ namespace Avalonia.Base.UnitTests
{
target.Bind(Class1.FooProperty, source);
source.OnNext("baz");
source.OnNext(new BindingNotification(new InvalidOperationException("Binding Error Message"), BindingErrorType.Error));
source.OnNext(BindingValue<string>.BindingError(new InvalidOperationException("Binding Error Message")));
}
Assert.True(called);
@ -447,7 +565,8 @@ namespace Avalonia.Base.UnitTests
"foo",
o => "foo",
null,
new DirectPropertyMetadata<string>(defaultBindingMode: BindingMode.TwoWay));
new DirectPropertyMetadata<string>(defaultBindingMode: BindingMode.TwoWay),
false);
var bar = foo.AddOwner<Class2>(o => "bar");
Assert.Equal(BindingMode.TwoWay, bar.GetMetadata<Class1>().DefaultBindingMode);
@ -461,7 +580,8 @@ namespace Avalonia.Base.UnitTests
"foo",
o => "foo",
null,
new DirectPropertyMetadata<string>(defaultBindingMode: BindingMode.TwoWay));
new DirectPropertyMetadata<string>(defaultBindingMode: BindingMode.TwoWay),
false);
var bar = foo.AddOwner<Class2>(o => "bar", defaultBindingMode: BindingMode.OneWayToSource);
Assert.Equal(BindingMode.TwoWay, bar.GetMetadata<Class1>().DefaultBindingMode);
@ -527,10 +647,18 @@ namespace Avalonia.Base.UnitTests
o => o.DoubleValue,
(o, v) => o.DoubleValue = v);
public static readonly DirectProperty<Class1, object> FrankProperty =
AvaloniaProperty.RegisterDirect<Class1, object>(
nameof(Frank),
o => o.Frank,
(o, v) => o.Frank = v,
unsetValue: "Kups");
private string _foo = "initial";
private readonly string _bar = "bar";
private int _baz = 5;
private double _doubleValue;
private object _frank;
public string Foo
{
@ -554,6 +682,12 @@ namespace Avalonia.Base.UnitTests
get { return _doubleValue; }
set { SetAndRaise(DoubleValueProperty, ref _doubleValue, value); }
}
public object Frank
{
get { return _frank; }
set { SetAndRaise(FrankProperty, ref _frank, value); }
}
}
private class Class2 : AvaloniaObject
@ -609,4 +743,4 @@ namespace Avalonia.Base.UnitTests
}
}
}
}
}

19
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_GetValue.cs

@ -2,6 +2,7 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Reactive.Subjects;
using Xunit;
namespace Avalonia.Base.UnitTests
@ -27,11 +28,23 @@ namespace Avalonia.Base.UnitTests
[Fact]
public void GetValue_Returns_Set_Value()
{
Class1 target = new Class1();
var target = new Class1();
var property = Class1.FooProperty;
target.SetValue(property, "newvalue");
Assert.Equal("newvalue", target.GetValue(property));
}
[Fact]
public void GetValue_Returns_Bound_Value()
{
var target = new Class1();
var property = Class1.FooProperty;
target.SetValue(Class1.FooProperty, "newvalue");
target.Bind(property, new BehaviorSubject<string>("newvalue"));
Assert.Equal("newvalue", target.GetValue(Class1.FooProperty));
Assert.Equal("newvalue", target.GetValue(property));
}
[Fact]

87
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetValue.cs

@ -20,6 +20,27 @@ namespace Avalonia.Base.UnitTests
Assert.Equal("foodefault", target.GetValue(Class1.FooProperty));
}
[Fact]
public void ClearValue_Raises_PropertyChanged()
{
Class1 target = new Class1();
var raised = 0;
target.SetValue(Class1.FooProperty, "newvalue");
target.PropertyChanged += (s, e) =>
{
Assert.Same(target, s);
Assert.Equal(Class1.FooProperty, e.Property);
Assert.Equal("newvalue", (string)e.OldValue);
Assert.Equal("foodefault", (string)e.NewValue);
++raised;
};
target.ClearValue(Class1.FooProperty);
Assert.Equal(1, raised);
}
[Fact]
public void SetValue_Sets_Value()
{
@ -59,6 +80,25 @@ namespace Avalonia.Base.UnitTests
Assert.True(raised);
}
[Fact]
public void SetValue_Style_Priority_Raises_PropertyChanged()
{
Class1 target = new Class1();
bool raised = false;
target.PropertyChanged += (s, e) =>
{
raised = s == target &&
e.Property == Class1.FooProperty &&
(string)e.OldValue == "foodefault" &&
(string)e.NewValue == "newvalue";
};
target.SetValue(Class1.FooProperty, "newvalue", BindingPriority.Style);
Assert.True(raised);
}
[Fact]
public void SetValue_Doesnt_Raise_PropertyChanged_If_Value_Not_Changed()
{
@ -177,6 +217,28 @@ namespace Avalonia.Base.UnitTests
Assert.Equal("three", target.GetValue(Class1.FooProperty));
}
[Fact]
public void SetValue_Style_Doesnt_Override_LocalValue()
{
Class1 target = new Class1();
target.SetValue(Class1.FooProperty, "one", BindingPriority.LocalValue);
Assert.Equal("one", target.GetValue(Class1.FooProperty));
target.SetValue(Class1.FooProperty, "two", BindingPriority.Style);
Assert.Equal("one", target.GetValue(Class1.FooProperty));
}
[Fact]
public void SetValue_LocalValue_Overrides_Style()
{
Class1 target = new Class1();
target.SetValue(Class1.FooProperty, "one", BindingPriority.Style);
Assert.Equal("one", target.GetValue(Class1.FooProperty));
target.SetValue(Class1.FooProperty, "two", BindingPriority.LocalValue);
Assert.Equal("two", target.GetValue(Class1.FooProperty));
}
[Fact]
public void Setting_UnsetValue_Reverts_To_Default_Value()
{
@ -188,10 +250,35 @@ namespace Avalonia.Base.UnitTests
Assert.Equal("foodefault", target.GetValue(Class1.FooProperty));
}
[Fact]
public void Setting_Object_Property_To_UnsetValue_Reverts_To_Default_Value()
{
Class1 target = new Class1();
target.SetValue(Class1.FrankProperty, "newvalue");
target.SetValue(Class1.FrankProperty, AvaloniaProperty.UnsetValue);
Assert.Equal("Kups", target.GetValue(Class1.FrankProperty));
}
[Fact]
public void Setting_Object_Property_To_DoNothing_Does_Nothing()
{
Class1 target = new Class1();
target.SetValue(Class1.FrankProperty, "newvalue");
target.SetValue(Class1.FrankProperty, BindingOperations.DoNothing);
Assert.Equal("newvalue", target.GetValue(Class1.FrankProperty));
}
private class Class1 : AvaloniaObject
{
public static readonly StyledProperty<string> FooProperty =
AvaloniaProperty.Register<Class1, string>("Foo", "foodefault");
public static readonly StyledProperty<object> FrankProperty =
AvaloniaProperty.Register<Class1, object>("Frank", "Kups");
}
private class Class2 : Class1

156
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs

@ -1,156 +0,0 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Reactive.Subjects;
using Xunit;
namespace Avalonia.Base.UnitTests
{
public class AvaloniaObjectTests_Validation
{
[Fact]
public void SetValue_Causes_Validation()
{
var target = new Class1();
target.SetValue(Class1.QuxProperty, 5);
Assert.Throws<ArgumentOutOfRangeException>(() => target.SetValue(Class1.QuxProperty, 25));
Assert.Equal(5, target.GetValue(Class1.QuxProperty));
}
[Fact]
public void SetValue_Causes_Coercion()
{
var target = new Class1();
target.SetValue(Class1.QuxProperty, 5);
Assert.Equal(5, target.GetValue(Class1.QuxProperty));
target.SetValue(Class1.QuxProperty, -5);
Assert.Equal(0, target.GetValue(Class1.QuxProperty));
target.SetValue(Class1.QuxProperty, 15);
Assert.Equal(10, target.GetValue(Class1.QuxProperty));
}
[Fact]
public void Revalidate_Causes_Recoercion()
{
var target = new Class1();
target.SetValue(Class1.QuxProperty, 7);
Assert.Equal(7, target.GetValue(Class1.QuxProperty));
target.MaxQux = 5;
target.Revalidate(Class1.QuxProperty);
}
[Fact]
public void Validation_Can_Be_Overridden()
{
var target = new Class2();
Assert.Throws<ArgumentOutOfRangeException>(() => target.SetValue(Class1.QuxProperty, 5));
}
[Fact]
public void Validation_Can_Be_Overridden_With_Null()
{
var target = new Class3();
target.SetValue(Class1.QuxProperty, 50);
Assert.Equal(50, target.GetValue(Class1.QuxProperty));
}
[Fact]
public void Binding_To_UnsetValue_Doesnt_Throw()
{
var target = new Class1();
var source = new Subject<object>();
target.Bind(Class1.QuxProperty, source);
source.OnNext(AvaloniaProperty.UnsetValue);
}
[Fact]
public void Attached_Property_Should_Be_Validated()
{
var target = new Class2();
target.SetValue(Class1.AttachedProperty, 15);
Assert.Equal(10, target.GetValue(Class1.AttachedProperty));
}
[Fact]
public void PropertyChanged_Event_Uses_Coerced_Value()
{
var inst = new Class1();
inst.PropertyChanged += (sender, e) =>
{
Assert.Equal(10, e.NewValue);
};
inst.SetValue(Class1.QuxProperty, 15);
}
private class Class1 : AvaloniaObject
{
public static readonly StyledProperty<int> QuxProperty =
AvaloniaProperty.Register<Class1, int>("Qux", validate: Validate);
public static readonly AttachedProperty<int> AttachedProperty =
AvaloniaProperty.RegisterAttached<Class1, Class2, int>("Attached", validate: Validate);
public Class1()
{
MaxQux = 10;
ErrorQux = 20;
}
public int MaxQux { get; set; }
public int ErrorQux { get; }
private static int Validate(Class1 instance, int value)
{
if (value > instance.ErrorQux)
{
throw new ArgumentOutOfRangeException();
}
return Math.Min(Math.Max(value, 0), ((Class1)instance).MaxQux);
}
private static int Validate(Class2 instance, int value)
{
return Math.Min(value, 10);
}
}
private class Class2 : AvaloniaObject
{
public static readonly StyledProperty<int> QuxProperty =
Class1.QuxProperty.AddOwner<Class2>();
static Class2()
{
QuxProperty.OverrideValidation<Class2>(Validate);
}
private static int Validate(Class2 instance, int value)
{
if (value < 100)
{
throw new ArgumentOutOfRangeException();
}
return value;
}
}
private class Class3 : Class2
{
static Class3()
{
QuxProperty.OverrideValidation<Class3>(null);
}
}
}
}

31
tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs

@ -140,6 +140,37 @@ namespace Avalonia.Base.UnitTests
{
OverrideMetadata(typeof(T), metadata);
}
internal override void NotifyInitialized(IAvaloniaObject o)
{
throw new NotImplementedException();
}
internal override IDisposable RouteBind(
IAvaloniaObject o,
IObservable<BindingValue<object>> source,
BindingPriority priority)
{
throw new NotImplementedException();
}
internal override object RouteGetValue(IAvaloniaObject o)
{
throw new NotImplementedException();
}
internal override void RouteInheritanceParentChanged(AvaloniaObject o, IAvaloniaObject oldParent)
{
throw new NotImplementedException();
}
internal override void RouteSetValue(
IAvaloniaObject o,
object value,
BindingPriority priority)
{
throw new NotImplementedException();
}
}
private class Class1 : AvaloniaObject

66
tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_DataValidation.cs

@ -16,39 +16,39 @@ namespace Avalonia.Base.UnitTests.Data.Core
{
public class ExpressionObserverTests_DataValidation : IClassFixture<InvariantCultureFixture>
{
[Fact]
public void Doesnt_Send_DataValidationError_When_DataValidatation_Not_Enabled()
{
var data = new ExceptionTest { MustBePositive = 5 };
var observer = ExpressionObserver.Create(data, o => o.MustBePositive, false);
var validationMessageFound = false;
observer.OfType<BindingNotification>()
.Where(x => x.ErrorType == BindingErrorType.DataValidationError)
.Subscribe(_ => validationMessageFound = true);
observer.SetValue(-5);
Assert.False(validationMessageFound);
GC.KeepAlive(data);
}
[Fact]
public void Exception_Validation_Sends_DataValidationError()
{
var data = new ExceptionTest { MustBePositive = 5 };
var observer = ExpressionObserver.Create(data, o => o.MustBePositive, true);
var validationMessageFound = false;
observer.OfType<BindingNotification>()
.Where(x => x.ErrorType == BindingErrorType.DataValidationError)
.Subscribe(_ => validationMessageFound = true);
observer.SetValue(-5);
Assert.True(validationMessageFound);
GC.KeepAlive(data);
}
////[Fact]
////public void Doesnt_Send_DataValidationError_When_DataValidatation_Not_Enabled()
////{
//// var data = new ExceptionTest { MustBePositive = 5 };
//// var observer = ExpressionObserver.Create(data, o => o.MustBePositive, false);
//// var validationMessageFound = false;
//// observer.OfType<BindingNotification>()
//// .Where(x => x.ErrorType == BindingErrorType.DataValidationError)
//// .Subscribe(_ => validationMessageFound = true);
//// observer.SetValue(-5);
//// Assert.False(validationMessageFound);
//// GC.KeepAlive(data);
////}
////[Fact]
////public void Exception_Validation_Sends_DataValidationError()
////{
//// var data = new ExceptionTest { MustBePositive = 5 };
//// var observer = ExpressionObserver.Create(data, o => o.MustBePositive, true);
//// var validationMessageFound = false;
//// observer.OfType<BindingNotification>()
//// .Where(x => x.ErrorType == BindingErrorType.DataValidationError)
//// .Subscribe(_ => validationMessageFound = true);
//// observer.SetValue(-5);
//// Assert.True(validationMessageFound);
//// GC.KeepAlive(data);
////}
[Fact]
public void Indei_Validation_Does_Not_Subscribe_When_DataValidatation_Not_Enabled()

16
tests/Avalonia.Base.UnitTests/DirectPropertyTests.cs

@ -34,8 +34,9 @@ namespace Avalonia.Base.UnitTests
var target = new DirectProperty<Class1, string>(
"test",
o => null,
null,
new DirectPropertyMetadata<string>());
null,
new DirectPropertyMetadata<string>(),
false);
Assert.True(target.IsDirect);
}
@ -71,17 +72,6 @@ namespace Avalonia.Base.UnitTests
Assert.Same(p1.Initialized, p2.Initialized);
}
[Fact]
public void IsAnimating_On_DirectProperty_With_Binding_Returns_False()
{
var target = new Class1();
var source = new BehaviorSubject<string>("foo");
target.Bind(Class1.FooProperty, source, BindingPriority.Animation);
Assert.False(target.IsAnimating(Class1.FooProperty));
}
private class Class1 : AvaloniaObject
{
public static readonly DirectProperty<Class1, string> FooProperty =

371
tests/Avalonia.Base.UnitTests/PriorityValueTests.cs

@ -1,314 +1,239 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Utilities;
using Moq;
using System;
using System;
using System.Linq;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using System.Reactive.Disposables;
using Avalonia.Data;
using Avalonia.PropertyStore;
using Moq;
using Xunit;
namespace Avalonia.Base.UnitTests
{
public class PriorityValueTests
{
private static readonly AvaloniaProperty TestProperty =
new StyledProperty<string>(
"Test",
typeof(PriorityValueTests),
new StyledPropertyMetadata<string>());
private static readonly IValueSink NullSink = Mock.Of<IValueSink>();
private static readonly IAvaloniaObject Owner = Mock.Of<IAvaloniaObject>();
private static readonly StyledProperty<string> TestProperty = new StyledProperty<string>(
"Test",
typeof(PriorityValueTests),
new StyledPropertyMetadata<string>());
[Fact]
public void Initial_Value_Should_Be_UnsetValue()
public void Constructor_Should_Set_Value_Based_On_Initial_Entry()
{
var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
var target = new PriorityValue<string>(
Owner,
TestProperty,
NullSink,
new ConstantValueEntry<string>(TestProperty, "1", BindingPriority.StyleTrigger));
Assert.Same(AvaloniaProperty.UnsetValue, target.Value);
Assert.Equal("1", target.Value.Value);
Assert.Equal(BindingPriority.StyleTrigger, target.ValuePriority);
}
[Fact]
public void First_Binding_Sets_Value()
public void SetValue_LocalValue_Should_Not_Add_Entries()
{
var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
var target = new PriorityValue<string>(
Owner,
TestProperty,
NullSink);
target.Add(Single("foo"), 0);
target.SetValue("1", BindingPriority.LocalValue);
target.SetValue("2", BindingPriority.LocalValue);
Assert.Equal("foo", target.Value);
Assert.Empty(target.Entries);
}
[Fact]
public void Changing_Binding_Should_Set_Value()
public void SetValue_Non_LocalValue_Should_Add_Entries()
{
var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
var subject = new BehaviorSubject<string>("foo");
var target = new PriorityValue<string>(
Owner,
TestProperty,
NullSink);
target.Add(subject, 0);
Assert.Equal("foo", target.Value);
subject.OnNext("bar");
Assert.Equal("bar", target.Value);
}
target.SetValue("1", BindingPriority.Style);
target.SetValue("2", BindingPriority.Animation);
[Fact]
public void Setting_Direct_Value_Should_Override_Binding()
{
var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
var result = target.Entries
.OfType<ConstantValueEntry<string>>()
.Select(x => x.Value.Value)
.ToList();
target.Add(Single("foo"), 0);
target.SetValue("bar", 0);
Assert.Equal("bar", target.Value);
Assert.Equal(new[] { "1", "2" }, result);
}
[Fact]
public void Binding_Firing_Should_Override_Direct_Value()
public void Binding_With_Same_Priority_Should_Be_Appended()
{
var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
var source = new BehaviorSubject<object>("initial");
target.Add(source, 0);
Assert.Equal("initial", target.Value);
target.SetValue("first", 0);
Assert.Equal("first", target.Value);
source.OnNext("second");
Assert.Equal("second", target.Value);
}
var target = new PriorityValue<string>(Owner, TestProperty, NullSink);
var source1 = new Source("1");
var source2 = new Source("2");
[Fact]
public void Earlier_Binding_Firing_Should_Not_Override_Later()
{
var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
var nonActive = new BehaviorSubject<object>("na");
var source = new BehaviorSubject<object>("initial");
target.Add(nonActive, 1);
target.Add(source, 1);
Assert.Equal("initial", target.Value);
target.SetValue("first", 1);
Assert.Equal("first", target.Value);
nonActive.OnNext("second");
Assert.Equal("first", target.Value);
}
target.AddBinding(source1, BindingPriority.LocalValue);
target.AddBinding(source2, BindingPriority.LocalValue);
[Fact]
public void Binding_Completing_Should_Revert_To_Direct_Value()
{
var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
var source = new BehaviorSubject<object>("initial");
target.Add(source, 0);
Assert.Equal("initial", target.Value);
target.SetValue("first", 0);
Assert.Equal("first", target.Value);
source.OnNext("second");
Assert.Equal("second", target.Value);
source.OnCompleted();
Assert.Equal("first", target.Value);
}
[Fact]
public void Binding_With_Lower_Priority_Has_Precedence()
{
var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
target.Add(Single("foo"), 1);
target.Add(Single("bar"), 0);
target.Add(Single("baz"), 1);
var result = target.Entries
.OfType<BindingEntry<string>>()
.Select(x => x.Source)
.OfType<Source>()
.Select(x => x.Id)
.ToList();
Assert.Equal("bar", target.Value);
Assert.Equal(new[] { "1", "2" }, result);
}
[Fact]
public void Later_Binding_With_Same_Priority_Should_Take_Precedence()
public void Binding_With_Higher_Priority_Should_Be_Appended()
{
var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
var target = new PriorityValue<string>(Owner, TestProperty, NullSink);
var source1 = new Source("1");
var source2 = new Source("2");
target.Add(Single("foo"), 1);
target.Add(Single("bar"), 0);
target.Add(Single("baz"), 0);
target.Add(Single("qux"), 1);
target.AddBinding(source1, BindingPriority.LocalValue);
target.AddBinding(source2, BindingPriority.Animation);
Assert.Equal("baz", target.Value);
}
var result = target.Entries
.OfType<BindingEntry<string>>()
.Select(x => x.Source)
.OfType<Source>()
.Select(x => x.Id)
.ToList();
[Fact]
public void Changing_Binding_With_Lower_Priority_Should_Set_Not_Value()
{
var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
var subject = new BehaviorSubject<string>("bar");
target.Add(Single("foo"), 0);
target.Add(subject, 1);
Assert.Equal("foo", target.Value);
subject.OnNext("baz");
Assert.Equal("foo", target.Value);
Assert.Equal(new[] { "1", "2" }, result);
}
[Fact]
public void UnsetValue_Should_Fall_Back_To_Next_Binding()
public void Binding_With_Lower_Priority_Should_Be_Prepended()
{
var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
var subject = new BehaviorSubject<object>("bar");
var target = new PriorityValue<string>(Owner, TestProperty, NullSink);
var source1 = new Source("1");
var source2 = new Source("2");
target.Add(subject, 0);
target.Add(Single("foo"), 1);
target.AddBinding(source1, BindingPriority.LocalValue);
target.AddBinding(source2, BindingPriority.Style);
Assert.Equal("bar", target.Value);
var result = target.Entries
.OfType<BindingEntry<string>>()
.Select(x => x.Source)
.OfType<Source>()
.Select(x => x.Id)
.ToList();
subject.OnNext(AvaloniaProperty.UnsetValue);
Assert.Equal("foo", target.Value);
Assert.Equal(new[] { "2", "1" }, result);
}
[Fact]
public void Adding_Value_Should_Call_OnNext()
public void Second_Binding_With_Lower_Priority_Should_Be_Inserted_In_Middle()
{
var owner = GetMockOwner();
var target = new PriorityValue(owner.Object, TestProperty, typeof(string));
target.Add(Single("foo"), 0);
var target = new PriorityValue<string>(Owner, TestProperty, NullSink);
var source1 = new Source("1");
var source2 = new Source("2");
var source3 = new Source("3");
owner.Verify(x => x.Changed(target.Property, target.ValuePriority, AvaloniaProperty.UnsetValue, "foo"));
}
[Fact]
public void Changing_Value_Should_Call_OnNext()
{
var owner = GetMockOwner();
var target = new PriorityValue(owner.Object, TestProperty, typeof(string));
var subject = new BehaviorSubject<object>("foo");
target.AddBinding(source1, BindingPriority.LocalValue);
target.AddBinding(source2, BindingPriority.Style);
target.AddBinding(source3, BindingPriority.Style);
target.Add(subject, 0);
subject.OnNext("bar");
var result = target.Entries
.OfType<BindingEntry<string>>()
.Select(x => x.Source)
.OfType<Source>()
.Select(x => x.Id)
.ToList();
owner.Verify(x => x.Changed(target.Property, target.ValuePriority, "foo", "bar"));
Assert.Equal(new[] { "2", "3", "1" }, result);
}
[Fact]
public void Disposing_A_Binding_Should_Revert_To_Next_Value()
public void Competed_Binding_Should_Be_Removed()
{
var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
target.Add(Single("foo"), 0);
var disposable = target.Add(Single("bar"), 0);
var target = new PriorityValue<string>(Owner, TestProperty, NullSink);
var source1 = new Source("1");
var source2 = new Source("2");
var source3 = new Source("3");
Assert.Equal("bar", target.Value);
disposable.Dispose();
Assert.Equal("foo", target.Value);
}
[Fact]
public void Disposing_A_Binding_Should_Remove_BindingEntry()
{
var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
target.AddBinding(source1, BindingPriority.LocalValue).Start();
target.AddBinding(source2, BindingPriority.Style).Start();
target.AddBinding(source3, BindingPriority.Style).Start();
source3.OnCompleted();
target.Add(Single("foo"), 0);
var disposable = target.Add(Single("bar"), 0);
var result = target.Entries
.OfType<BindingEntry<string>>()
.Select(x => x.Source)
.OfType<Source>()
.Select(x => x.Id)
.ToList();
Assert.Equal(2, target.GetBindings().Count());
disposable.Dispose();
Assert.Single(target.GetBindings());
Assert.Equal(new[] { "2", "1" }, result);
}
[Fact]
public void Completing_A_Binding_Should_Revert_To_Previous_Binding()
public void Value_Should_Come_From_Last_Entry()
{
var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
var source = new BehaviorSubject<object>("bar");
var target = new PriorityValue<string>(Owner, TestProperty, NullSink);
var source1 = new Source("1");
var source2 = new Source("2");
var source3 = new Source("3");
target.Add(Single("foo"), 0);
target.Add(source, 0);
target.AddBinding(source1, BindingPriority.LocalValue).Start();
target.AddBinding(source2, BindingPriority.Style).Start();
target.AddBinding(source3, BindingPriority.Style).Start();
Assert.Equal("bar", target.Value);
source.OnCompleted();
Assert.Equal("foo", target.Value);
Assert.Equal("1", target.Value.Value);
}
[Fact]
public void Completing_A_Binding_Should_Revert_To_Lower_Priority()
public void LocalValue_Should_Override_LocalValue_Binding()
{
var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
var source = new BehaviorSubject<object>("bar");
var target = new PriorityValue<string>(Owner, TestProperty, NullSink);
var source1 = new Source("1");
target.Add(Single("foo"), 1);
target.Add(source, 0);
target.AddBinding(source1, BindingPriority.LocalValue).Start();
target.SetValue("2", BindingPriority.LocalValue);
Assert.Equal("bar", target.Value);
source.OnCompleted();
Assert.Equal("foo", target.Value);
Assert.Equal("2", target.Value.Value);
}
[Fact]
public void Completing_A_Binding_Should_Remove_BindingEntry()
public void LocalValue_Should_Override_Style_Binding()
{
var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
var subject = new BehaviorSubject<object>("bar");
var target = new PriorityValue<string>(Owner, TestProperty, NullSink);
var source1 = new Source("1");
target.Add(Single("foo"), 0);
target.Add(subject, 0);
target.AddBinding(source1, BindingPriority.Style).Start();
target.SetValue("2", BindingPriority.LocalValue);
Assert.Equal(2, target.GetBindings().Count());
subject.OnCompleted();
Assert.Single(target.GetBindings());
Assert.Equal("2", target.Value.Value);
}
[Fact]
public void Direct_Value_Should_Be_Coerced()
public void LocalValue_Should_Not_Override_Animation_Binding()
{
var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(int), x => Math.Min((int)x, 10));
var target = new PriorityValue<string>(Owner, TestProperty, NullSink);
var source1 = new Source("1");
target.SetValue(5, 0);
Assert.Equal(5, target.Value);
target.SetValue(15, 0);
Assert.Equal(10, target.Value);
}
target.AddBinding(source1, BindingPriority.Animation).Start();
target.SetValue("2", BindingPriority.LocalValue);
[Fact]
public void Bound_Value_Should_Be_Coerced()
{
var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(int), x => Math.Min((int)x, 10));
var source = new Subject<object>();
target.Add(source, 0);
source.OnNext(5);
Assert.Equal(5, target.Value);
source.OnNext(15);
Assert.Equal(10, target.Value);
Assert.Equal("1", target.Value.Value);
}
[Fact]
public void Revalidate_Should_ReCoerce_Value()
private class Source : IObservable<BindingValue<string>>
{
var max = 10;
var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(int), x => Math.Min((int)x, max));
var source = new Subject<object>();
target.Add(source, 0);
source.OnNext(5);
Assert.Equal(5, target.Value);
source.OnNext(15);
Assert.Equal(10, target.Value);
max = 12;
target.Revalidate();
Assert.Equal(12, target.Value);
}
private IObserver<BindingValue<string>> _observer;
/// <summary>
/// Returns an observable that returns a single value but does not complete.
/// </summary>
/// <typeparam name="T">The type of the observable.</typeparam>
/// <param name="value">The value.</param>
/// <returns>The observable.</returns>
private IObservable<T> Single<T>(T value)
{
return Observable.Never<T>().StartWith(value);
}
public Source(string id) => Id = id;
public string Id { get; }
private static Mock<IPriorityValueOwner> GetMockOwner()
{
var owner = new Mock<IPriorityValueOwner>();
owner.Setup(o => o.GetNonDirectDeferredSetter(It.IsAny<AvaloniaProperty>())).Returns(new DeferredSetter<object>());
return owner;
public IDisposable Subscribe(IObserver<BindingValue<string>> observer)
{
_observer = observer;
observer.OnNext(Id);
return Disposable.Empty;
}
public void OnCompleted() => _observer.OnCompleted();
}
}
}

2
tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs

@ -780,7 +780,7 @@ namespace Avalonia.Markup.UnitTests.Data
public OldDataContextTest()
{
Bind(BarProperty, this.GetObservable(FooProperty));
this.Bind(BarProperty, this.GetObservable(FooProperty));
}
}

6
tests/Avalonia.Markup.UnitTests/Data/BindingTests_TemplatedParent.cs

@ -35,7 +35,7 @@ namespace Avalonia.Markup.UnitTests.Data
target.Verify(x => x.Bind(
TextBox.TextProperty,
It.IsAny<IObservable<object>>(),
It.IsAny<IObservable<BindingValue<object>>>(),
BindingPriority.TemplatedParent));
}
@ -55,7 +55,7 @@ namespace Avalonia.Markup.UnitTests.Data
target.Verify(x => x.Bind(
TextBox.TextProperty,
It.IsAny<ISubject<object>>(),
It.IsAny<IObservable<BindingValue<object>>>(),
BindingPriority.TemplatedParent));
}
@ -68,7 +68,7 @@ namespace Avalonia.Markup.UnitTests.Data
result.Setup(x => x.GetValue(Control.TemplatedParentProperty)).Returns(templatedParent);
result.Setup(x => x.GetValue((AvaloniaProperty)Control.TemplatedParentProperty)).Returns(templatedParent);
result.Setup(x => x.GetValue((AvaloniaProperty)TextBox.TextProperty)).Returns(text);
result.Setup(x => x.Bind(It.IsAny<AvaloniaProperty>(), It.IsAny<IObservable<object>>(), It.IsAny<BindingPriority>()))
result.Setup(x => x.Bind(It.IsAny<AvaloniaProperty>(), It.IsAny<IObservable<BindingValue<object>>>(), It.IsAny<BindingPriority>()))
.Returns(Disposable.Empty);
return result;
}

7
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/InitializationOrderTracker.cs

@ -2,6 +2,7 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.LogicalTree;
using System.Collections.Generic;
using System.ComponentModel;
@ -20,10 +21,10 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
base.OnAttachedToLogicalTree(e);
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e)
protected override void OnPropertyChanged<T>(AvaloniaProperty<T> property, Optional<T> oldValue, BindingValue<T> newValue, BindingPriority priority)
{
Order.Add($"Property {e.Property.Name} Changed");
base.OnPropertyChanged(e);
Order.Add($"Property {property.Name} Changed");
base.OnPropertyChanged(property, oldValue, newValue, priority);
}
void ISupportInitialize.BeginInit()

34
tests/Avalonia.Styling.UnitTests/SelectorTests_Child.cs

@ -131,7 +131,7 @@ namespace Avalonia.Styling.UnitTests
throw new NotImplementedException();
}
public IDisposable Bind(AvaloniaProperty property, IObservable<object> source, BindingPriority priority)
public IDisposable Bind(AvaloniaProperty property, IObservable<BindingValue<object>> source, BindingPriority priority)
{
throw new NotImplementedException();
}
@ -146,7 +146,7 @@ namespace Avalonia.Styling.UnitTests
throw new NotImplementedException();
}
public IDisposable Bind<T>(AvaloniaProperty<T> property, IObservable<T> source, BindingPriority priority = BindingPriority.LocalValue)
public IDisposable Bind<T>(AvaloniaProperty<T> property, IObservable<BindingValue<T>> source, BindingPriority priority = BindingPriority.LocalValue)
{
throw new NotImplementedException();
}
@ -165,6 +165,36 @@ namespace Avalonia.Styling.UnitTests
{
throw new NotImplementedException();
}
public void ClearValue(AvaloniaProperty property)
{
throw new NotImplementedException();
}
public void ClearValue<T>(AvaloniaProperty<T> property)
{
throw new NotImplementedException();
}
public void AddInheritanceChild(IAvaloniaObject child)
{
throw new NotImplementedException();
}
public void RemoveInheritanceChild(IAvaloniaObject child)
{
throw new NotImplementedException();
}
public void InheritanceParentChanged<T>(StyledPropertyBase<T> property, IAvaloniaObject oldParent, IAvaloniaObject newParent)
{
throw new NotImplementedException();
}
public void InheritedPropertyChanged<T>(AvaloniaProperty<T> property, Optional<T> oldValue, Optional<T> newValue)
{
throw new NotImplementedException();
}
}
public class TestLogical1 : TestLogical

34
tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs

@ -161,7 +161,7 @@ namespace Avalonia.Styling.UnitTests
throw new NotImplementedException();
}
public IDisposable Bind(AvaloniaProperty property, IObservable<object> source, BindingPriority priority = BindingPriority.LocalValue)
public IDisposable Bind(AvaloniaProperty property, IObservable<BindingValue<object>> source, BindingPriority priority = BindingPriority.LocalValue)
{
throw new NotImplementedException();
}
@ -176,7 +176,7 @@ namespace Avalonia.Styling.UnitTests
throw new NotImplementedException();
}
public IDisposable Bind<T>(AvaloniaProperty<T> property, IObservable<T> source, BindingPriority priority = BindingPriority.LocalValue)
public IDisposable Bind<T>(AvaloniaProperty<T> property, IObservable<BindingValue<T>> source, BindingPriority priority = BindingPriority.LocalValue)
{
throw new NotImplementedException();
}
@ -195,6 +195,36 @@ namespace Avalonia.Styling.UnitTests
{
throw new NotImplementedException();
}
public void ClearValue(AvaloniaProperty property)
{
throw new NotImplementedException();
}
public void ClearValue<T>(AvaloniaProperty<T> property)
{
throw new NotImplementedException();
}
public void AddInheritanceChild(IAvaloniaObject child)
{
throw new NotImplementedException();
}
public void RemoveInheritanceChild(IAvaloniaObject child)
{
throw new NotImplementedException();
}
public void InheritanceParentChanged<T>(StyledPropertyBase<T> property, IAvaloniaObject oldParent, IAvaloniaObject newParent)
{
throw new NotImplementedException();
}
public void InheritedPropertyChanged<T>(AvaloniaProperty<T> property, Optional<T> oldValue, Optional<T> newValue)
{
throw new NotImplementedException();
}
}
public class TestLogical1 : TestLogical

8
tests/Avalonia.Styling.UnitTests/SetterTests.cs

@ -83,7 +83,7 @@ namespace Avalonia.Styling.UnitTests
control.Verify(x => x.Bind(
TextBlock.TextProperty,
It.IsAny<IObservable<object>>(),
It.IsAny<IObservable<BindingValue<object>>>(),
BindingPriority.Style));
}
@ -99,7 +99,7 @@ namespace Avalonia.Styling.UnitTests
control.Verify(x => x.Bind(
TextBlock.TextProperty,
It.IsAny<IObservable<object>>(),
It.IsAny<IObservable<BindingValue<object>>>(),
BindingPriority.StyleTrigger));
}
@ -114,7 +114,7 @@ namespace Avalonia.Styling.UnitTests
control.Verify(x => x.Bind(
TextBlock.TextProperty,
It.IsAny<IObservable<object>>(),
It.IsAny<IObservable<BindingValue<object>>>(),
BindingPriority.Style));
}
@ -130,7 +130,7 @@ namespace Avalonia.Styling.UnitTests
control.Verify(x => x.Bind(
TextBlock.TextProperty,
It.IsAny<IObservable<object>>(),
It.IsAny<IObservable<BindingValue<object>>>(),
BindingPriority.StyleTrigger));
}

34
tests/Avalonia.Styling.UnitTests/TestControlBase.cs

@ -70,12 +70,42 @@ namespace Avalonia.Styling.UnitTests
throw new NotImplementedException();
}
public IDisposable Bind(AvaloniaProperty property, IObservable<object> source, BindingPriority priority = BindingPriority.LocalValue)
public IDisposable Bind(AvaloniaProperty property, IObservable<BindingValue<object>> source, BindingPriority priority = BindingPriority.LocalValue)
{
throw new NotImplementedException();
}
public IDisposable Bind<T>(AvaloniaProperty<T> property, IObservable<T> source, BindingPriority priority = BindingPriority.LocalValue)
public IDisposable Bind<T>(AvaloniaProperty<T> property, IObservable<BindingValue<T>> source, BindingPriority priority = BindingPriority.LocalValue)
{
throw new NotImplementedException();
}
public void ClearValue(AvaloniaProperty property)
{
throw new NotImplementedException();
}
public void ClearValue<T>(AvaloniaProperty<T> property)
{
throw new NotImplementedException();
}
public void AddInheritanceChild(IAvaloniaObject child)
{
throw new NotImplementedException();
}
public void RemoveInheritanceChild(IAvaloniaObject child)
{
throw new NotImplementedException();
}
public void InheritanceParentChanged<T>(StyledPropertyBase<T> property, IAvaloniaObject oldParent, IAvaloniaObject newParent)
{
throw new NotImplementedException();
}
public void InheritedPropertyChanged<T>(AvaloniaProperty<T> property, Optional<T> oldValue, Optional<T> newValue)
{
throw new NotImplementedException();
}

34
tests/Avalonia.Styling.UnitTests/TestTemplatedControl.cs

@ -58,12 +58,12 @@ namespace Avalonia.Styling.UnitTests
throw new NotImplementedException();
}
public IDisposable Bind(AvaloniaProperty property, IObservable<object> source, BindingPriority priority = BindingPriority.LocalValue)
public IDisposable Bind(AvaloniaProperty property, IObservable<BindingValue<object>> source, BindingPriority priority = BindingPriority.LocalValue)
{
throw new NotImplementedException();
}
public IDisposable Bind<T>(AvaloniaProperty<T> property, IObservable<T> source, BindingPriority priority = BindingPriority.LocalValue)
public IDisposable Bind<T>(AvaloniaProperty<T> property, IObservable<BindingValue<T>> source, BindingPriority priority = BindingPriority.LocalValue)
{
throw new NotImplementedException();
}
@ -77,5 +77,35 @@ namespace Avalonia.Styling.UnitTests
{
throw new NotImplementedException();
}
public void ClearValue(AvaloniaProperty property)
{
throw new NotImplementedException();
}
public void ClearValue<T>(AvaloniaProperty<T> property)
{
throw new NotImplementedException();
}
public void AddInheritanceChild(IAvaloniaObject child)
{
throw new NotImplementedException();
}
public void RemoveInheritanceChild(IAvaloniaObject child)
{
throw new NotImplementedException();
}
public void InheritanceParentChanged<T>(StyledPropertyBase<T> property, IAvaloniaObject oldParent, IAvaloniaObject newParent)
{
throw new NotImplementedException();
}
public void InheritedPropertyChanged<T>(AvaloniaProperty<T> property, Optional<T> oldValue, Optional<T> newValue)
{
throw new NotImplementedException();
}
}
}

Loading…
Cancel
Save