Browse Source

Fix data validation errors not showing for OneWayToSource bindings (#22103)

* Add failing tests for OneWayToSource validation (#8235)

Data validation errors from validation attributes (e.g. [MaxLength]) and
INotifyDataErrorInfo are not displayed when binding with
Mode=OneWayToSource. Add tests covering both the DataAnnotations and INDEI
cases when writing to the source.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013gwdaykrFyoMZFeVmWMydb

* Publish data validation errors for OneWayToSource bindings (#8235)

In OneWayToSource mode, BindingExpression.OnNodeValueChanged discarded any
data validation error produced when writing the value to the source, so
validation attributes and INotifyDataErrorInfo errors were never displayed.

Publish (and clear) the error via UnchangedValue so that the error state
reaches the target without pushing a value back to it, preserving the
one-way-to-source semantics.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013gwdaykrFyoMZFeVmWMydb

* Added passing test for #15378.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
pull/22115/head
Steven Kirk 1 month ago
committed by GitHub
parent
commit
31d6979377
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 12
      src/Avalonia.Base/Data/Core/BindingExpression.cs
  2. 105
      tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.DataValidation.cs

12
src/Avalonia.Base/Data/Core/BindingExpression.cs

@ -221,6 +221,18 @@ internal class BindingExpression : UntypedBindingExpressionBase, IDescription, I
var forceUpdate = _mode == BindingMode.OneWay || _updateTargetDepth > 0;
ConvertAndPublishValue(value, error, forceUpdate);
}
else if (IsDataValidationEnabled)
{
// In OneWayToSource mode the value must not be published to the target, but any
// data validation error produced when writing to the source still has to be
// published (or cleared) so that it can be displayed (issue #8235). Publishing
// UnchangedValue leaves the target's value untouched.
var error = dataValidationError is not null ?
new BindingError(dataValidationError, BindingErrorType.DataValidationError) :
null;
PublishValue(UnchangedValue, error);
}
}
else if (_mode == BindingMode.OneWayToSource && nodeIndex == _nodes.Count - 2 && value is not null)
{

105
tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.DataValidation.cs

@ -271,6 +271,102 @@ public partial class BindingExpressionTests
GC.KeepAlive(data);
}
[Fact]
public void Indei_Validation_Updates_Data_Validation_When_Writing_To_Source_OneWayToSource()
{
// Issue #8235: validation errors should be displayed for OneWayToSource bindings.
var data = new IndeiViewModel();
var target = CreateTargetWithSource(
data,
o => o.MustBePositive,
enableDataValidation: true,
mode: BindingMode.OneWayToSource);
Assert.Equal(0, data.MustBePositive);
AssertNoError(target, TargetClass.IntProperty);
target.Int = 5;
Assert.Equal(5, data.MustBePositive);
AssertNoError(target, TargetClass.IntProperty);
target.Int = -5;
Assert.Equal(-5, data.MustBePositive);
AssertBindingError(target, TargetClass.IntProperty, new DataValidationException("Must be positive"), BindingErrorType.DataValidationError);
target.Int = 5;
Assert.Equal(5, data.MustBePositive);
AssertNoError(target, TargetClass.IntProperty);
GC.KeepAlive(data);
}
[Fact]
public void DataAnnotations_Validation_Updates_Data_Validation_When_Writing_To_Source_OneWayToSource()
{
// Issue #8235: validation attributes should be displayed for OneWayToSource bindings.
if (!BindingPlugins.DataValidators.Any(x => x is DataAnnotationsValidationPlugin))
BindingPlugins.DataValidators.Insert(0, new DataAnnotationsValidationPlugin());
var data = new DataAnnotationsViewModel();
var target = CreateTargetWithSource(
data,
o => o.MaxLengthString,
enableDataValidation: true,
mode: BindingMode.OneWayToSource);
target.String = "1234";
Assert.Equal("1234", data.MaxLengthString);
AssertNoError(target, TargetClass.StringProperty);
target.String = "123456";
Assert.Equal("123456", data.MaxLengthString);
AssertBindingError(
target,
TargetClass.StringProperty,
new DataValidationException("Too long!"),
BindingErrorType.DataValidationError);
GC.KeepAlive(data);
}
[Fact]
public void Conversion_Error_Is_Cleared_When_Value_Becomes_Valid_OneWayToSource()
{
// Issue #15378.
var data = new ViewModel();
var target = CreateTargetWithSource(
data,
o => o.DoubleValue,
targetProperty: TargetClass.ObjectProperty,
enableDataValidation: true,
mode: BindingMode.OneWayToSource);
target.Object = 5.0;
Assert.Equal(5.0, data.DoubleValue);
AssertNoError(target, TargetClass.ObjectProperty);
target.Object = null;
AssertBindingError(
target,
TargetClass.ObjectProperty,
new InvalidCastException("Could not convert '(null)' (null) to System.Double."),
BindingErrorType.DataValidationError);
target.Object = 5.0;
Assert.Equal(5.0, data.DoubleValue);
AssertNoError(target, TargetClass.ObjectProperty);
GC.KeepAlive(data);
}
[Fact]
public void Does_Not_Subscribe_To_Indei_Of_Intermediate_Object_In_Chain()
{
@ -445,6 +541,15 @@ public partial class BindingExpressionTests
get { return _requiredString; }
set { _requiredString = value; RaisePropertyChanged(); }
}
private string? _maxLengthString;
[MaxLength(5, ErrorMessage = "Too long!")]
public string? MaxLengthString
{
get { return _maxLengthString; }
set { _maxLengthString = value; RaisePropertyChanged(); }
}
}
private class IndeiDataAnnotationsViewModel : IndeiBase

Loading…
Cancel
Save