diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs
index d89d6f3690..f3a046ef80 100644
--- a/src/Avalonia.Base/AvaloniaObject.cs
+++ b/src/Avalonia.Base/AvaloniaObject.cs
@@ -784,6 +784,11 @@ namespace Avalonia
}
}
+ internal void OnUpdateDataValidation(AvaloniaProperty property, BindingValueType state, Exception? error)
+ {
+ UpdateDataValidation(property, state, error);
+ }
+
///
/// Gets a description of an observable that van be used in logs.
///
diff --git a/src/Avalonia.Base/AvaloniaObjectExtensions.cs b/src/Avalonia.Base/AvaloniaObjectExtensions.cs
index 6231483ff8..9fbf680a5c 100644
--- a/src/Avalonia.Base/AvaloniaObjectExtensions.cs
+++ b/src/Avalonia.Base/AvaloniaObjectExtensions.cs
@@ -199,13 +199,11 @@ namespace Avalonia
property = property ?? throw new ArgumentNullException(nameof(property));
binding = binding ?? throw new ArgumentNullException(nameof(binding));
- var metadata = property.GetMetadata(target.GetType()) as IDirectPropertyMetadata;
-
var result = binding.Initiate(
target,
property,
anchor,
- metadata?.EnableDataValidation ?? false);
+ property.GetMetadata(target.GetType()).EnableDataValidation ?? false);
if (result != null)
{
diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs
index 45ab293a89..24244c5068 100644
--- a/src/Avalonia.Base/AvaloniaProperty.cs
+++ b/src/Avalonia.Base/AvaloniaProperty.cs
@@ -227,6 +227,7 @@ namespace Avalonia
/// The default binding mode for the property.
/// A value validation callback.
/// A value coercion callback.
+ /// Whether the property is interested in data validation.
/// A
public static StyledProperty Register(
string name,
@@ -234,7 +235,8 @@ namespace Avalonia
bool inherits = false,
BindingMode defaultBindingMode = BindingMode.OneWay,
Func? validate = null,
- Func? coerce = null)
+ Func? coerce = null,
+ bool enableDataValidation = false)
where TOwner : AvaloniaObject
{
_ = name ?? throw new ArgumentNullException(nameof(name));
@@ -242,7 +244,8 @@ namespace Avalonia
var metadata = new StyledPropertyMetadata(
defaultValue,
defaultBindingMode: defaultBindingMode,
- coerce: coerce);
+ coerce: coerce,
+ enableDataValidation: enableDataValidation);
var result = new StyledProperty(
name,
@@ -253,7 +256,7 @@ namespace Avalonia
AvaloniaPropertyRegistry.Instance.Register(typeof(TOwner), result);
return result;
}
-
+
///
///
/// A method that gets called before and after the property starts being notified on an
@@ -267,6 +270,7 @@ namespace Avalonia
BindingMode defaultBindingMode,
Func? validate,
Func? coerce,
+ bool enableDataValidation,
Action? notifying)
where TOwner : AvaloniaObject
{
@@ -275,7 +279,8 @@ namespace Avalonia
var metadata = new StyledPropertyMetadata(
defaultValue,
defaultBindingMode: defaultBindingMode,
- coerce: coerce);
+ coerce: coerce,
+ enableDataValidation: enableDataValidation);
var result = new StyledProperty(
name,
diff --git a/src/Avalonia.Base/AvaloniaPropertyMetadata.cs b/src/Avalonia.Base/AvaloniaPropertyMetadata.cs
index 2963567b14..62bb65351f 100644
--- a/src/Avalonia.Base/AvaloniaPropertyMetadata.cs
+++ b/src/Avalonia.Base/AvaloniaPropertyMetadata.cs
@@ -13,10 +13,13 @@ namespace Avalonia
/// Initializes a new instance of the class.
///
/// The default binding mode.
+ /// Whether the property is interested in data validation.
public AvaloniaPropertyMetadata(
- BindingMode defaultBindingMode = BindingMode.Default)
+ BindingMode defaultBindingMode = BindingMode.Default,
+ bool? enableDataValidation = null)
{
_defaultBindingMode = defaultBindingMode;
+ EnableDataValidation = enableDataValidation;
}
///
@@ -31,6 +34,17 @@ namespace Avalonia
}
}
+ ///
+ /// Gets a value indicating whether the property is interested in data validation.
+ ///
+ ///
+ /// Data validation is validation performed at the target of a binding, for example in a
+ /// view model using the INotifyDataErrorInfo interface. Only certain properties on a
+ /// control (such as a TextBox's Text property) will be interested in receiving data
+ /// validation messages so this feature must be explicitly enabled by setting this flag.
+ ///
+ public bool? EnableDataValidation { get; private set; }
+
///
/// Merges the metadata with the base metadata.
///
@@ -44,6 +58,8 @@ namespace Avalonia
{
_defaultBindingMode = baseMetadata.DefaultBindingMode;
}
+
+ EnableDataValidation ??= baseMetadata.EnableDataValidation;
}
}
}
diff --git a/src/Avalonia.Base/DirectPropertyMetadata`1.cs b/src/Avalonia.Base/DirectPropertyMetadata`1.cs
index fe1cdd0e65..451ff6ce00 100644
--- a/src/Avalonia.Base/DirectPropertyMetadata`1.cs
+++ b/src/Avalonia.Base/DirectPropertyMetadata`1.cs
@@ -21,10 +21,9 @@ namespace Avalonia
TValue unsetValue = default!,
BindingMode defaultBindingMode = BindingMode.Default,
bool? enableDataValidation = null)
- : base(defaultBindingMode)
+ : base(defaultBindingMode, enableDataValidation)
{
UnsetValue = unsetValue;
- EnableDataValidation = enableDataValidation;
}
///
@@ -32,16 +31,6 @@ namespace Avalonia
///
public TValue UnsetValue { get; private set; }
- ///
- /// Gets a value indicating whether the property is interested in data validation.
- ///
- ///
- /// Data validation is validation performed at the target of a binding, for example in a
- /// view model using the INotifyDataErrorInfo interface. Only certain properties on a
- /// control (such as a TextBox's Text property) will be interested in receiving data
- /// validation messages so this feature must be explicitly enabled by setting this flag.
- ///
- public bool? EnableDataValidation { get; private set; }
///
object? IDirectPropertyMetadata.UnsetValue => UnsetValue;
@@ -51,19 +40,9 @@ namespace Avalonia
{
base.Merge(baseMetadata, property);
- var src = baseMetadata as DirectPropertyMetadata;
-
- if (src != null)
+ if (baseMetadata is DirectPropertyMetadata src)
{
- if (UnsetValue == null)
- {
- UnsetValue = src.UnsetValue;
- }
-
- if (EnableDataValidation == null)
- {
- EnableDataValidation = src.EnableDataValidation;
- }
+ UnsetValue ??= src.UnsetValue;
}
}
}
diff --git a/src/Avalonia.Base/PropertyStore/BindingEntryBase.cs b/src/Avalonia.Base/PropertyStore/BindingEntryBase.cs
index e1ff0970c2..a841803ee1 100644
--- a/src/Avalonia.Base/PropertyStore/BindingEntryBase.cs
+++ b/src/Avalonia.Base/PropertyStore/BindingEntryBase.cs
@@ -16,27 +16,37 @@ namespace Avalonia.PropertyStore
private IDisposable? _subscription;
private bool _hasValue;
private TValue? _value;
- private TValue? _defaultValue;
- private bool _isDefaultValueInitialized;
+ private UncommonFields? _uncommon;
protected BindingEntryBase(
+ AvaloniaObject target,
ValueFrame frame,
AvaloniaProperty property,
IObservable> source)
+ : this(target, frame, property, (object)source)
{
- Frame = frame;
- Source = source;
- Property = property;
}
protected BindingEntryBase(
+ AvaloniaObject target,
ValueFrame frame,
AvaloniaProperty property,
IObservable source)
+ : this(target, frame, property, (object)source)
+ {
+ }
+
+ private BindingEntryBase(
+ AvaloniaObject target,
+ ValueFrame frame,
+ AvaloniaProperty property,
+ object source)
{
Frame = frame;
- Source = source;
Property = property;
+ Source = source;
+ if (property.GetMetadata(target.GetType()).EnableDataValidation == true)
+ _uncommon = new() { _hasDataValidation = true };
}
public bool HasValue
@@ -68,6 +78,20 @@ namespace Avalonia.PropertyStore
return _value!;
}
+ public bool GetDataValidationState(out BindingValueType state, out Exception? error)
+ {
+ if (_uncommon?._hasDataValidation == true)
+ {
+ state = _uncommon._dataValidationState;
+ error = _uncommon._dataValidationError;
+ return true;
+ }
+
+ state = BindingValueType.Value;
+ error = null;
+ return false;
+ }
+
public void Start() => Start(true);
public void OnCompleted() => BindingCompleted();
@@ -111,16 +135,28 @@ namespace Avalonia.PropertyStore
{
static void Execute(BindingEntryBase instance, BindingValue value)
{
- if (instance.Frame.Owner is null)
+ if (instance.Frame.Owner is not { } valueStore)
return;
- LoggingUtils.LogIfNecessary(instance.Frame.Owner.Owner, instance.Property, value);
+ var owner = valueStore.Owner;
+ var property = instance.Property;
+ var originalType = value.Type;
+
+ LoggingUtils.LogIfNecessary(owner, property, value);
- var effectiveValue = value.HasValue ? value.Value : instance.GetCachedDefaultValue();
+ if (!value.HasValue && value.Type != BindingValueType.DataValidationError)
+ value = value.WithValue(instance.GetCachedDefaultValue());
- if (!instance._hasValue || !EqualityComparer.Default.Equals(instance._value, effectiveValue))
+ if (instance._uncommon?._hasDataValidation == true)
{
- instance._value = effectiveValue;
+ instance._uncommon._dataValidationState = value.Type;
+ instance._uncommon._dataValidationError = value.Error;
+ }
+
+ if (value.HasValue &&
+ (!instance._hasValue || !EqualityComparer.Default.Equals(instance._value, value.Value)))
+ {
+ instance._value = value.Value;
instance._hasValue = true;
if (instance._subscription is not null && instance._subscription != s_creatingQuiet)
instance.Frame.Owner?.OnBindingValueChanged(instance, instance.Frame.Priority);
@@ -152,13 +188,23 @@ namespace Avalonia.PropertyStore
private TValue GetCachedDefaultValue()
{
- if (!_isDefaultValueInitialized)
+ if (_uncommon?._isDefaultValueInitialized != true)
{
- _defaultValue = GetDefaultValue(Frame.Owner!.Owner.GetType());
- _isDefaultValueInitialized = true;
+ _uncommon ??= new();
+ _uncommon._defaultValue = GetDefaultValue(Frame.Owner!.Owner.GetType());
+ _uncommon._isDefaultValueInitialized = true;
}
- return _defaultValue!;
+ return _uncommon._defaultValue!;
+ }
+
+ private class UncommonFields
+ {
+ public TValue? _defaultValue;
+ public bool _isDefaultValueInitialized;
+ public bool _hasDataValidation;
+ public BindingValueType _dataValidationState;
+ public Exception? _dataValidationError;
}
}
}
diff --git a/src/Avalonia.Base/PropertyStore/DirectBindingObserver.cs b/src/Avalonia.Base/PropertyStore/DirectBindingObserver.cs
index cbe2435953..4bf98e3f7b 100644
--- a/src/Avalonia.Base/PropertyStore/DirectBindingObserver.cs
+++ b/src/Avalonia.Base/PropertyStore/DirectBindingObserver.cs
@@ -9,11 +9,13 @@ namespace Avalonia.PropertyStore
IDisposable
{
private readonly ValueStore _owner;
+ private readonly bool _hasDataValidation;
private IDisposable? _subscription;
public DirectBindingObserver(ValueStore owner, DirectPropertyBase property)
{
_owner = owner;
+ _hasDataValidation = property.GetMetadata(owner.Owner.GetType())?.EnableDataValidation ?? false;
Property = property;
}
@@ -33,10 +35,17 @@ namespace Avalonia.PropertyStore
{
_subscription?.Dispose();
_subscription = null;
+ OnCompleted();
+ }
+
+ public void OnCompleted()
+ {
_owner.OnLocalValueBindingCompleted(Property, this);
+
+ if (_hasDataValidation)
+ _owner.Owner.OnUpdateDataValidation(Property, BindingValueType.UnsetValue, null);
}
- public void OnCompleted() => _owner.OnLocalValueBindingCompleted(Property, this);
public void OnError(Exception error) => OnCompleted();
public void OnNext(T value)
diff --git a/src/Avalonia.Base/PropertyStore/DirectUntypedBindingObserver.cs b/src/Avalonia.Base/PropertyStore/DirectUntypedBindingObserver.cs
index 5d60b44bef..1cf108df9b 100644
--- a/src/Avalonia.Base/PropertyStore/DirectUntypedBindingObserver.cs
+++ b/src/Avalonia.Base/PropertyStore/DirectUntypedBindingObserver.cs
@@ -10,11 +10,13 @@ namespace Avalonia.PropertyStore
IDisposable
{
private readonly ValueStore _owner;
+ private readonly bool _hasDataValidation;
private IDisposable? _subscription;
public DirectUntypedBindingObserver(ValueStore owner, DirectPropertyBase property)
{
_owner = owner;
+ _hasDataValidation = property.GetMetadata(owner.Owner.GetType())?.EnableDataValidation ?? false;
Property = property;
}
@@ -30,6 +32,9 @@ namespace Avalonia.PropertyStore
_subscription?.Dispose();
_subscription = null;
_owner.OnLocalValueBindingCompleted(Property, this);
+
+ if (_hasDataValidation)
+ _owner.Owner.OnUpdateDataValidation(Property, BindingValueType.UnsetValue, null);
}
public void OnCompleted() => _owner.OnLocalValueBindingCompleted(Property, this);
diff --git a/src/Avalonia.Base/PropertyStore/EffectiveValue.cs b/src/Avalonia.Base/PropertyStore/EffectiveValue.cs
index 78f0ad46b7..11a4dd7893 100644
--- a/src/Avalonia.Base/PropertyStore/EffectiveValue.cs
+++ b/src/Avalonia.Base/PropertyStore/EffectiveValue.cs
@@ -11,9 +11,6 @@ namespace Avalonia.PropertyStore
///
internal abstract class EffectiveValue
{
- private IValueEntry? _valueEntry;
- private IValueEntry? _baseValueEntry;
-
///
/// Gets the current effective value as a boxed value.
///
@@ -29,6 +26,16 @@ namespace Avalonia.PropertyStore
///
public BindingPriority BasePriority { get; protected set; }
+ ///
+ /// Gets the active value entry for the current effective value.
+ ///
+ public IValueEntry? ValueEntry { get; private set; }
+
+ ///
+ /// Gets the active value entry for the current base value.
+ ///
+ public IValueEntry? BaseValueEntry { get; private set; }
+
///
/// Gets a value indicating whether the was overridden by a call to
/// .
@@ -63,14 +70,14 @@ namespace Avalonia.PropertyStore
{
if (Priority == BindingPriority.Unset)
{
- _valueEntry?.Unsubscribe();
- _valueEntry = null;
+ ValueEntry?.Unsubscribe();
+ ValueEntry = null;
}
if (BasePriority == BindingPriority.Unset)
{
- _baseValueEntry?.Unsubscribe();
- _baseValueEntry = null;
+ BaseValueEntry?.Unsubscribe();
+ BaseValueEntry = null;
}
}
@@ -135,40 +142,34 @@ namespace Avalonia.PropertyStore
// value, then the current entry becomes our base entry.
if (Priority > BindingPriority.LocalValue && Priority < BindingPriority.Inherited)
{
- Debug.Assert(_valueEntry is not null);
- _baseValueEntry = _valueEntry;
- _valueEntry = null;
+ Debug.Assert(ValueEntry is not null);
+ BaseValueEntry = ValueEntry;
+ ValueEntry = null;
}
- if (_valueEntry != entry)
+ if (ValueEntry != entry)
{
- _valueEntry?.Unsubscribe();
- _valueEntry = entry;
+ ValueEntry?.Unsubscribe();
+ ValueEntry = entry;
}
}
else if (Priority <= BindingPriority.Animation)
{
// We've received a non-animation value and have an active animation value, so the
// new entry becomes our base entry.
- if (_baseValueEntry != entry)
+ if (BaseValueEntry != entry)
{
- _baseValueEntry?.Unsubscribe();
- _baseValueEntry = entry;
+ BaseValueEntry?.Unsubscribe();
+ BaseValueEntry = entry;
}
}
- else if (_valueEntry != entry)
+ else if (ValueEntry != entry)
{
// Both the current value and the new value are non-animation values, so the new
// entry replaces the existing entry.
- _valueEntry?.Unsubscribe();
- _valueEntry = entry;
+ ValueEntry?.Unsubscribe();
+ ValueEntry = entry;
}
}
-
- protected void UnsubscribeValueEntries()
- {
- _valueEntry?.Unsubscribe();
- _baseValueEntry?.Unsubscribe();
- }
}
}
diff --git a/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs b/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs
index c469034f9b..0788b39459 100644
--- a/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs
+++ b/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Data;
+using static Avalonia.Rendering.Composition.Animations.PropertySetSnapshot;
namespace Avalonia.PropertyStore
{
@@ -61,6 +62,12 @@ namespace Avalonia.PropertyStore
UpdateValueEntry(value, priority);
SetAndRaiseCore(owner, (StyledProperty)value.Property, GetValue(value), priority, false);
+
+ if (priority > BindingPriority.LocalValue &&
+ value.GetDataValidationState(out var state, out var error))
+ {
+ owner.Owner.OnUpdateDataValidation(value.Property, state, error);
+ }
}
public void SetLocalValueAndRaise(
@@ -128,12 +135,10 @@ namespace Avalonia.PropertyStore
public override void DisposeAndRaiseUnset(ValueStore owner, AvaloniaProperty property)
{
- UnsubscribeValueEntries();
- DisposeAndRaiseUnset(owner, (StyledProperty)property);
- }
+ ValueEntry?.Unsubscribe();
+ BaseValueEntry?.Unsubscribe();
- public void DisposeAndRaiseUnset(ValueStore owner, StyledProperty property)
- {
+ var p = (StyledProperty)property;
BindingPriority priority;
T oldValue;
@@ -150,9 +155,16 @@ namespace Avalonia.PropertyStore
if (!EqualityComparer.Default.Equals(oldValue, Value))
{
- owner.Owner.RaisePropertyChanged(property, Value, oldValue, priority, true);
+ owner.Owner.RaisePropertyChanged(p, Value, oldValue, priority, true);
if (property.Inherits)
- owner.OnInheritedEffectiveValueDisposed(property, Value);
+ owner.OnInheritedEffectiveValueDisposed(p, Value);
+ }
+
+ if (ValueEntry?.GetDataValidationState(out _, out _) ??
+ BaseValueEntry?.GetDataValidationState(out _, out _) ??
+ false)
+ {
+ owner.Owner.OnUpdateDataValidation(p, BindingValueType.UnsetValue, null);
}
}
diff --git a/src/Avalonia.Base/PropertyStore/IValueEntry.cs b/src/Avalonia.Base/PropertyStore/IValueEntry.cs
index 271d85f8bc..5898bef491 100644
--- a/src/Avalonia.Base/PropertyStore/IValueEntry.cs
+++ b/src/Avalonia.Base/PropertyStore/IValueEntry.cs
@@ -1,4 +1,5 @@
using System;
+using Avalonia.Data;
namespace Avalonia.PropertyStore
{
@@ -22,6 +23,16 @@ namespace Avalonia.PropertyStore
///
object? GetValue();
+ ///
+ /// Gets the data validation state if supported.
+ ///
+ /// The binding validation state.
+ /// The current binding error, if any.
+ ///
+ /// True if the entry supports data validation, otherwise false.
+ ///
+ bool GetDataValidationState(out BindingValueType state, out Exception? error);
+
///
/// Called when the value entry is removed from the value store.
///
diff --git a/src/Avalonia.Base/PropertyStore/ImmediateValueEntry.cs b/src/Avalonia.Base/PropertyStore/ImmediateValueEntry.cs
index d8a353dc70..16b96eff5d 100644
--- a/src/Avalonia.Base/PropertyStore/ImmediateValueEntry.cs
+++ b/src/Avalonia.Base/PropertyStore/ImmediateValueEntry.cs
@@ -1,4 +1,5 @@
using System;
+using Avalonia.Data;
namespace Avalonia.PropertyStore
{
@@ -27,5 +28,12 @@ namespace Avalonia.PropertyStore
object? IValueEntry.GetValue() => _value;
T IValueEntry.GetValue() => _value;
+
+ bool IValueEntry.GetDataValidationState(out BindingValueType state, out Exception? error)
+ {
+ state = BindingValueType.Value;
+ error = null;
+ return false;
+ }
}
}
diff --git a/src/Avalonia.Base/PropertyStore/ImmediateValueFrame.cs b/src/Avalonia.Base/PropertyStore/ImmediateValueFrame.cs
index 7e9f3ab312..222d857aa3 100644
--- a/src/Avalonia.Base/PropertyStore/ImmediateValueFrame.cs
+++ b/src/Avalonia.Base/PropertyStore/ImmediateValueFrame.cs
@@ -18,7 +18,7 @@ namespace Avalonia.PropertyStore
StyledProperty property,
IObservable> source)
{
- var e = new TypedBindingEntry(this, property, source);
+ var e = new TypedBindingEntry(Owner!.Owner, this, property, source);
Add(e);
return e;
}
@@ -27,7 +27,7 @@ namespace Avalonia.PropertyStore
StyledProperty property,
IObservable source)
{
- var e = new TypedBindingEntry(this, property, source);
+ var e = new TypedBindingEntry(Owner!.Owner, this, property, source);
Add(e);
return e;
}
@@ -36,7 +36,7 @@ namespace Avalonia.PropertyStore
StyledProperty property,
IObservable