Browse Source

Merge pull request #691 from AvaloniaUI/binding-updates2

Update of Data Validation and binding in general
pull/701/head
Steven Kirk 10 years ago
committed by GitHub
parent
commit
2ba2c51c7f
  1. 5
      samples/BindingTest/BindingTest.csproj
  2. 14
      samples/BindingTest/MainWindow.xaml
  3. 17
      samples/BindingTest/ViewModels/DataAnnotationsErrorViewModel.cs
  4. 4
      samples/BindingTest/ViewModels/ExceptionErrorViewModel.cs
  5. 73
      samples/BindingTest/ViewModels/IndeiErrorViewModel.cs
  6. 5
      samples/BindingTest/ViewModels/MainWindowViewModel.cs
  7. 26
      samples/interop/GtkInteropDemo/GtkInteropDemo.v2.ncrunchproject
  8. 26
      samples/interop/WindowsInteropTest/WindowsInteropTest.v2.ncrunchproject
  9. 7
      src/Avalonia.Base/Avalonia.Base.csproj
  10. 267
      src/Avalonia.Base/AvaloniaObject.cs
  11. 11
      src/Avalonia.Base/AvaloniaObjectExtensions.cs
  12. 9
      src/Avalonia.Base/AvaloniaProperty.cs
  13. 85
      src/Avalonia.Base/Data/BindingChainNullException.cs
  14. 59
      src/Avalonia.Base/Data/BindingError.cs
  15. 282
      src/Avalonia.Base/Data/BindingNotification.cs
  16. 5
      src/Avalonia.Base/Data/BindingOperations.cs
  17. 4
      src/Avalonia.Base/Data/IBinding.cs
  18. 17
      src/Avalonia.Base/Data/IValidationStatus.cs
  19. 6
      src/Avalonia.Base/Data/IndexerBinding.cs
  20. 44
      src/Avalonia.Base/Data/ObjectValidationStatus.cs
  21. 11
      src/Avalonia.Base/DirectProperty.cs
  22. 20
      src/Avalonia.Base/DirectPropertyMetadata`1.cs
  23. 5
      src/Avalonia.Base/IDirectPropertyMetadata.cs
  24. 7
      src/Avalonia.Base/IPriorityValueOwner.cs
  25. 26
      src/Avalonia.Base/PriorityBindingEntry.cs
  26. 13
      src/Avalonia.Base/PriorityLevel.cs
  27. 39
      src/Avalonia.Base/PriorityValue.cs
  28. 3
      src/Avalonia.Base/PropertyMetadata.cs
  29. 23
      src/Avalonia.Base/Utilities/ExceptionUtilities.cs
  30. 41
      src/Avalonia.Base/Utilities/TypeUtilities.cs
  31. 54
      src/Avalonia.Base/Utilities/WeakObservable.cs
  32. 1
      src/Avalonia.Controls/Canvas.cs
  33. 9
      src/Avalonia.Controls/Control.cs
  34. 11
      src/Avalonia.Controls/ItemsControl.cs
  35. 2
      src/Avalonia.Controls/Primitives/PopupRoot.cs
  36. 101
      src/Avalonia.Controls/TextBox.cs
  37. 23
      src/Avalonia.Controls/ToolTip.cs
  38. 26
      src/Avalonia.DotNetFrameworkRuntime/Avalonia.DotNetFrameworkRuntime.v2.ncrunchproject
  39. 2
      src/Avalonia.Themes.Default/Accents/BaseLight.xaml
  40. 4
      src/Avalonia.Themes.Default/DefaultTheme.xaml
  41. 34
      src/Avalonia.Themes.Default/TextBox.xaml
  42. 59
      src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs
  43. 3
      src/Markup/Avalonia.Markup.Xaml/Data/MultiBinding.cs
  44. 3
      src/Markup/Avalonia.Markup.Xaml/Data/StyleResourceBinding.cs
  45. 2
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs
  46. 36
      src/Markup/Avalonia.Markup.Xaml/Templates/MemberSelector.cs
  47. 13
      src/Markup/Avalonia.Markup/Avalonia.Markup.csproj
  48. 305
      src/Markup/Avalonia.Markup/Data/BindingExpression.cs
  49. 18
      src/Markup/Avalonia.Markup/Data/EmptyExpressionNode.cs
  50. 188
      src/Markup/Avalonia.Markup/Data/ExpressionNode.cs
  51. 277
      src/Markup/Avalonia.Markup/Data/ExpressionObserver.cs
  52. 213
      src/Markup/Avalonia.Markup/Data/ExpressionSubject.cs
  53. 187
      src/Markup/Avalonia.Markup/Data/IndexerNode.cs
  54. 28
      src/Markup/Avalonia.Markup/Data/LogicalNotNode.cs
  55. 33
      src/Markup/Avalonia.Markup/Data/MarkupBindingChainNullException.cs
  56. 57
      src/Markup/Avalonia.Markup/Data/Plugins/AvaloniaPropertyAccessorPlugin.cs
  57. 81
      src/Markup/Avalonia.Markup/Data/Plugins/DataAnnotationsValidationPlugin.cs
  58. 80
      src/Markup/Avalonia.Markup/Data/Plugins/DataValidatiorBase.cs
  59. 44
      src/Markup/Avalonia.Markup/Data/Plugins/ExceptionValidationPlugin.cs
  60. 37
      src/Markup/Avalonia.Markup/Data/Plugins/IDataValidationPlugin.cs
  61. 5
      src/Markup/Avalonia.Markup/Data/Plugins/IPropertyAccessor.cs
  62. 5
      src/Markup/Avalonia.Markup/Data/Plugins/IPropertyAccessorPlugin.cs
  63. 35
      src/Markup/Avalonia.Markup/Data/Plugins/IValidationPlugin.cs
  64. 29
      src/Markup/Avalonia.Markup/Data/Plugins/IValuePlugin.cs
  65. 107
      src/Markup/Avalonia.Markup/Data/Plugins/IndeiValidationPlugin.cs
  66. 115
      src/Markup/Avalonia.Markup/Data/Plugins/InpcPropertyAccessorPlugin.cs
  67. 44
      src/Markup/Avalonia.Markup/Data/Plugins/ObservableValuePlugin.cs
  68. 68
      src/Markup/Avalonia.Markup/Data/Plugins/PropertyAccessorBase.cs
  69. 11
      src/Markup/Avalonia.Markup/Data/Plugins/PropertyError.cs
  70. 82
      src/Markup/Avalonia.Markup/Data/Plugins/TaskValuePlugin.cs
  71. 46
      src/Markup/Avalonia.Markup/Data/Plugins/ValidatingPropertyAccessorBase.cs
  72. 120
      src/Markup/Avalonia.Markup/Data/PropertyAccessorNode.cs
  73. 14
      src/Markup/Avalonia.Markup/DefaultValueConverter.cs
  74. 9
      src/Markup/Avalonia.Markup/IValueConverter.cs
  75. 1
      src/Markup/Avalonia.Markup/Properties/AssemblyInfo.cs
  76. 1
      src/Markup/Avalonia.Markup/packages.config
  77. 1
      tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj
  78. 7
      tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.v2.ncrunchproject
  79. 68
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs
  80. 143
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs
  81. 12
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs
  82. 4
      tests/Avalonia.Base.UnitTests/AvaloniaPropertyRegistryTests.cs
  83. 2
      tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj
  84. 60
      tests/Avalonia.Controls.UnitTests/TextBoxTests.cs
  85. 93
      tests/Avalonia.Controls.UnitTests/TextBoxTests_DataValidation.cs
  86. 1
      tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj
  87. 92
      tests/Avalonia.LeakTests/ExpressionObserverTests.cs
  88. 30
      tests/Avalonia.Markup.UnitTests/Avalonia.Markup.UnitTests.csproj
  89. 320
      tests/Avalonia.Markup.UnitTests/Data/BindingExpressionTests.cs
  90. 93
      tests/Avalonia.Markup.UnitTests/Data/ExceptionValidatorTests.cs
  91. 222
      tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_DataValidation.cs
  92. 44
      tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Indexer.cs
  93. 44
      tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Lifetime.cs
  94. 17
      tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Negation.cs
  95. 47
      tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Observable.cs
  96. 272
      tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs
  97. 10
      tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_SetValue.cs
  98. 82
      tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Task.cs
  99. 129
      tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Validation.cs
  100. 198
      tests/Avalonia.Markup.UnitTests/Data/ExpressionSubjectTests.cs

5
samples/BindingTest/BindingTest.csproj

@ -50,6 +50,7 @@
<Private>True</Private>
</Reference>
<Reference Include="System" />
<Reference Include="System.ComponentModel.DataAnnotations" />
<Reference Include="System.Core" />
<Reference Include="System.Reactive.Core, Version=3.0.0.0, Culture=neutral, PublicKeyToken=94bc3704cddfc263, processorArchitecture=MSIL">
<HintPath>..\..\packages\System.Reactive.Core.3.0.0\lib\net45\System.Reactive.Core.dll</HintPath>
@ -80,7 +81,9 @@
<Compile Include="TestItemView.xaml.cs">
<DependentUpon>TestItemView.xaml</DependentUpon>
</Compile>
<Compile Include="ViewModels\ExceptionPropertyErrorViewModel.cs" />
<Compile Include="ViewModels\DataAnnotationsErrorViewModel.cs" />
<Compile Include="ViewModels\IndeiErrorViewModel.cs" />
<Compile Include="ViewModels\ExceptionErrorViewModel.cs" />
<Compile Include="ViewModels\MainWindowViewModel.cs" />
<Compile Include="ViewModels\TestItem.cs" />
</ItemGroup>

14
samples/BindingTest/MainWindow.xaml

@ -70,9 +70,19 @@
</TabItem>
<TabItem Header="Property Validation">
<StackPanel Orientation="Horizontal">
<StackPanel Margin="18" Gap="4" Width="200" DataContext="{Binding ExceptionPropertyValidation}">
<StackPanel Margin="18" Gap="4" MinWidth="200" DataContext="{Binding ExceptionDataValidation}">
<TextBlock FontSize="16" Text="Exception Validation"/>
<TextBox Watermark="Less Than 10" UseFloatingWatermark="True" Text="{Binding Path=LessThan10, EnableValidation=True}"/>
<TextBox Watermark="Less Than 10" UseFloatingWatermark="True" Text="{Binding Path=LessThan10}"/>
</StackPanel>
<StackPanel Margin="18" Gap="4" MinWidth="200" DataContext="{Binding IndeiDataValidation}">
<TextBlock FontSize="16" Text="INotifyDataErrorInfo Validation"/>
<TextBox Watermark="Maximum" UseFloatingWatermark="True" Text="{Binding Path=Maximum}"/>
<TextBox Watermark="Value" UseFloatingWatermark="True" Text="{Binding Path=Value}"/>
</StackPanel>
<StackPanel Margin="18" Gap="4" MinWidth="200" DataContext="{Binding DataAnnotationsValidation}">
<TextBlock FontSize="16" Text="Data Annotations Validation"/>
<TextBox Watermark="Phone #" UseFloatingWatermark="True" Text="{Binding PhoneNumber}"/>
<TextBox Watermark="Less Than 10" UseFloatingWatermark="True" Text="{Binding Path=LessThan10}"/>
</StackPanel>
</StackPanel>
</TabItem>

17
samples/BindingTest/ViewModels/DataAnnotationsErrorViewModel.cs

@ -0,0 +1,17 @@
// 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.ComponentModel.DataAnnotations;
namespace BindingTest.ViewModels
{
public class DataAnnotationsErrorViewModel
{
[Phone]
[MaxLength(10)]
public string PhoneNumber { get; set; }
[Range(0, 9)]
public int LessThan10 { get; set; }
}
}

4
samples/BindingTest/ViewModels/ExceptionPropertyErrorViewModel.cs → samples/BindingTest/ViewModels/ExceptionErrorViewModel.cs

@ -6,7 +6,7 @@ using System;
namespace BindingTest.ViewModels
{
public class ExceptionPropertyErrorViewModel : ReactiveObject
public class ExceptionErrorViewModel : ReactiveObject
{
private int _lessThan10;
@ -21,7 +21,7 @@ namespace BindingTest.ViewModels
}
else
{
throw new InvalidOperationException("Value must be less than 10.");
throw new ArgumentOutOfRangeException("Value must be less than 10.");
}
}
}

73
samples/BindingTest/ViewModels/IndeiErrorViewModel.cs

@ -0,0 +1,73 @@
// 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 ReactiveUI;
using System;
using System.ComponentModel;
using System.Collections;
namespace BindingTest.ViewModels
{
public class IndeiErrorViewModel : ReactiveObject, INotifyDataErrorInfo
{
private int _maximum = 10;
private int _value;
private string _valueError;
public IndeiErrorViewModel()
{
this.WhenAnyValue(x => x.Maximum, x => x.Value)
.Subscribe(_ => UpdateErrors());
}
public bool HasErrors
{
get { throw new NotImplementedException(); }
}
public int Maximum
{
get { return _maximum; }
set { this.RaiseAndSetIfChanged(ref _maximum, value); }
}
public int Value
{
get { return _value; }
set { this.RaiseAndSetIfChanged(ref _value, value); }
}
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
public IEnumerable GetErrors(string propertyName)
{
switch (propertyName)
{
case nameof(Value):
return new[] { _valueError };
default:
return null;
}
}
private void UpdateErrors()
{
if (Value <= Maximum)
{
if (_valueError != null)
{
_valueError = null;
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(Value)));
}
}
else
{
if (_valueError == null)
{
_valueError = "Value must be less than Maximum";
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(Value)));
}
}
}
}
}

5
samples/BindingTest/ViewModels/MainWindowViewModel.cs

@ -69,7 +69,8 @@ namespace BindingTest.ViewModels
public ReactiveCommand<object> StringValueCommand { get; }
public ExceptionPropertyErrorViewModel ExceptionPropertyValidation { get; }
= new ExceptionPropertyErrorViewModel();
public DataAnnotationsErrorViewModel DataAnnotationsValidation { get; } = new DataAnnotationsErrorViewModel();
public ExceptionErrorViewModel ExceptionDataValidation { get; } = new ExceptionErrorViewModel();
public IndeiErrorViewModel IndeiDataValidation { get; } = new IndeiErrorViewModel();
}
}

26
samples/interop/GtkInteropDemo/GtkInteropDemo.v2.ncrunchproject

@ -0,0 +1,26 @@
<ProjectConfiguration>
<AutoDetectNugetBuildDependencies>true</AutoDetectNugetBuildDependencies>
<BuildPriority>1000</BuildPriority>
<CopyReferencedAssembliesToWorkspace>false</CopyReferencedAssembliesToWorkspace>
<ConsiderInconclusiveTestsAsPassing>false</ConsiderInconclusiveTestsAsPassing>
<PreloadReferencedAssemblies>false</PreloadReferencedAssemblies>
<AllowDynamicCodeContractChecking>true</AllowDynamicCodeContractChecking>
<AllowStaticCodeContractChecking>false</AllowStaticCodeContractChecking>
<AllowCodeAnalysis>false</AllowCodeAnalysis>
<IgnoreThisComponentCompletely>true</IgnoreThisComponentCompletely>
<RunPreBuildEvents>false</RunPreBuildEvents>
<RunPostBuildEvents>false</RunPostBuildEvents>
<PreviouslyBuiltSuccessfully>false</PreviouslyBuiltSuccessfully>
<InstrumentAssembly>true</InstrumentAssembly>
<PreventSigningOfAssembly>false</PreventSigningOfAssembly>
<AnalyseExecutionTimes>true</AnalyseExecutionTimes>
<DetectStackOverflow>true</DetectStackOverflow>
<IncludeStaticReferencesInWorkspace>true</IncludeStaticReferencesInWorkspace>
<DefaultTestTimeout>60000</DefaultTestTimeout>
<UseBuildConfiguration />
<UseBuildPlatform />
<ProxyProcessPath />
<UseCPUArchitecture>AutoDetect</UseCPUArchitecture>
<MSTestThreadApartmentState>STA</MSTestThreadApartmentState>
<BuildProcessArchitecture>x86</BuildProcessArchitecture>
</ProjectConfiguration>

26
samples/interop/WindowsInteropTest/WindowsInteropTest.v2.ncrunchproject

@ -0,0 +1,26 @@
<ProjectConfiguration>
<AutoDetectNugetBuildDependencies>true</AutoDetectNugetBuildDependencies>
<BuildPriority>1000</BuildPriority>
<CopyReferencedAssembliesToWorkspace>false</CopyReferencedAssembliesToWorkspace>
<ConsiderInconclusiveTestsAsPassing>false</ConsiderInconclusiveTestsAsPassing>
<PreloadReferencedAssemblies>false</PreloadReferencedAssemblies>
<AllowDynamicCodeContractChecking>true</AllowDynamicCodeContractChecking>
<AllowStaticCodeContractChecking>false</AllowStaticCodeContractChecking>
<AllowCodeAnalysis>false</AllowCodeAnalysis>
<IgnoreThisComponentCompletely>true</IgnoreThisComponentCompletely>
<RunPreBuildEvents>false</RunPreBuildEvents>
<RunPostBuildEvents>false</RunPostBuildEvents>
<PreviouslyBuiltSuccessfully>false</PreviouslyBuiltSuccessfully>
<InstrumentAssembly>true</InstrumentAssembly>
<PreventSigningOfAssembly>false</PreventSigningOfAssembly>
<AnalyseExecutionTimes>true</AnalyseExecutionTimes>
<DetectStackOverflow>true</DetectStackOverflow>
<IncludeStaticReferencesInWorkspace>true</IncludeStaticReferencesInWorkspace>
<DefaultTestTimeout>60000</DefaultTestTimeout>
<UseBuildConfiguration />
<UseBuildPlatform />
<ProxyProcessPath />
<UseCPUArchitecture>AutoDetect</UseCPUArchitecture>
<MSTestThreadApartmentState>STA</MSTestThreadApartmentState>
<BuildProcessArchitecture>x86</BuildProcessArchitecture>
</ProjectConfiguration>

7
src/Avalonia.Base/Avalonia.Base.csproj

@ -43,10 +43,9 @@
<Compile Include="..\Shared\SharedAssemblyInfo.cs">
<Link>Properties\SharedAssemblyInfo.cs</Link>
</Compile>
<Compile Include="Data\BindingError.cs" />
<Compile Include="Data\BindingChainNullException.cs" />
<Compile Include="Data\BindingNotification.cs" />
<Compile Include="Data\IndexerBinding.cs" />
<Compile Include="Data\IValidationStatus.cs" />
<Compile Include="Data\ObjectValidationStatus.cs" />
<Compile Include="Diagnostics\INotifyCollectionChangedDebug.cs" />
<Compile Include="Data\AssignBindingAttribute.cs" />
<Compile Include="Data\BindingOperations.cs" />
@ -116,9 +115,11 @@
<Compile Include="Threading\AvaloniaScheduler.cs" />
<Compile Include="Threading\AvaloniaSynchronizationContext.cs" />
<Compile Include="Threading\SingleThreadDispatcher.cs" />
<Compile Include="Utilities\ExceptionUtilities.cs" />
<Compile Include="Utilities\IWeakSubscriber.cs" />
<Compile Include="Utilities\MathUtilities.cs" />
<Compile Include="Utilities\TypeUtilities.cs" />
<Compile Include="Utilities\WeakObservable.cs" />
<Compile Include="Utilities\WeakSubscriptionManager.cs" />
<Compile Include="Utilities\WeakTimer.cs" />
</ItemGroup>

267
src/Avalonia.Base/AvaloniaObject.cs

@ -50,29 +50,6 @@ namespace Avalonia
/// </summary>
private EventHandler<AvaloniaPropertyChangedEventArgs> _propertyChanged;
/// <summary>
/// Defines the <see cref="ValidationStatus"/> property.
/// </summary>
public static readonly DirectProperty<AvaloniaObject, ObjectValidationStatus> ValidationStatusProperty =
AvaloniaProperty.RegisterDirect<AvaloniaObject, ObjectValidationStatus>(nameof(ValidationStatus), c => c.ValidationStatus);
private ObjectValidationStatus validationStatus;
/// <summary>
/// The current validation status of the control.
/// </summary>
public ObjectValidationStatus ValidationStatus
{
get
{
return validationStatus;
}
private set
{
SetAndRaise(ValidationStatusProperty, ref validationStatus, value);
}
}
/// <summary>
/// Initializes a new instance of the <see cref="AvaloniaObject"/> class.
/// </summary>
@ -251,6 +228,10 @@ namespace Avalonia
/// </summary>
/// <param name="property">The property.</param>
/// <returns>True if the property is set, otherwise false.</returns>
/// <remarks>
/// Checks whether a value is assigned to the property, or that there is a binding to the
/// property that is producing a value other than <see cref="AvaloniaProperty.UnsetValue"/>.
/// </remarks>
public bool IsSet(AvaloniaProperty property)
{
Contract.Requires<ArgumentNullException>(property != null);
@ -281,42 +262,11 @@ namespace Avalonia
if (property.IsDirect)
{
var accessor = (IDirectPropertyAccessor)GetRegistered(property);
LogPropertySet(property, value, priority);
accessor.SetValue(this, DirectUnsetToDefault(value, property));
SetDirectValue(property, value);
}
else
{
PriorityValue v;
var originalValue = value;
if (!AvaloniaPropertyRegistry.Instance.IsRegistered(this, property))
{
ThrowNotRegistered(property);
}
if (!TypeUtilities.TryCast(property.PropertyType, value, out value))
{
throw new ArgumentException(string.Format(
"Invalid value for Property '{0}': '{1}' ({2})",
property.Name,
originalValue,
originalValue?.GetType().FullName ?? "(null)"));
}
if (!_values.TryGetValue(property, out v))
{
if (value == AvaloniaProperty.UnsetValue)
{
return;
}
v = CreatePriorityValue(property);
_values.Add(property, v);
}
LogPropertySet(property, value, priority);
v.SetValue(value, (int)priority);
SetStyledValue(property, value, priority);
}
}
@ -371,7 +321,6 @@ namespace Avalonia
GetDescription(source));
IDisposable subscription = null;
IDisposable validationSubcription = null;
if (_directBindings == null)
{
@ -379,19 +328,14 @@ namespace Avalonia
}
subscription = source
.Where(x => !(x is IValidationStatus))
.Select(x => CastOrDefault(x, property.PropertyType))
.Do(_ => { }, () => _directBindings.Remove(subscription))
.Subscribe(x => DirectBindingSet(property, x));
validationSubcription = source
.OfType<IValidationStatus>()
.Subscribe(x => DataValidationChanged(property, x));
.Subscribe(x => SetDirectValue(property, x));
_directBindings.Add(subscription);
return Disposable.Create(() =>
{
validationSubcription.Dispose();
subscription.Dispose();
_directBindings.Remove(subscription);
});
@ -487,28 +431,9 @@ namespace Avalonia
}
/// <inheritdoc/>
void IPriorityValueOwner.DataValidationChanged(PriorityValue sender, IValidationStatus status)
void IPriorityValueOwner.BindingNotificationReceived(PriorityValue sender, BindingNotification notification)
{
var property = sender.Property;
DataValidationChanged(property, status);
}
/// <summary>
/// Called when the validation state on a tracked property is changed.
/// </summary>
/// <param name="property">The property whose validation state changed.</param>
/// <param name="status">The new validation state.</param>
protected virtual void DataValidationChanged(AvaloniaProperty property, IValidationStatus status)
{
}
/// <summary>
/// Updates the validation status of the current object.
/// </summary>
/// <param name="status">The new validation status.</param>
protected void UpdateValidationState(IValidationStatus status)
{
ValidationStatus = ValidationStatus.UpdateValidationStatus(status);
UpdateDataValidation(sender.Property, notification);
}
/// <inheritdoc/>
@ -542,6 +467,18 @@ namespace Avalonia
});
}
/// <summary>
/// Called to update the validation state for properties for which data validation is
/// enabled.
/// </summary>
/// <param name="property">The property.</param>
/// <param name="status">The new validation status.</param>
protected virtual void UpdateDataValidation(
AvaloniaProperty property,
BindingNotification status)
{
}
/// <summary>
/// Called when a avalonia property changes on the object.
/// </summary>
@ -623,22 +560,27 @@ namespace Avalonia
/// <summary>
/// Tries to cast a value to a type, taking into account that the value may be a
/// <see cref="BindingError"/>.
/// <see cref="BindingNotification"/>.
/// </summary>
/// <param name="value">The value.</param>
/// <param name="type">The type.</param>
/// <returns>The cast value, or a <see cref="BindingError"/>.</returns>
/// <returns>The cast value, or a <see cref="BindingNotification"/>.</returns>
private static object CastOrDefault(object value, Type type)
{
var error = value as BindingError;
var notification = value as BindingNotification;
if (error == null)
if (notification == null)
{
return TypeUtilities.CastOrDefault(value, type);
}
else
{
return error;
if (notification.HasValue)
{
notification.SetValue(TypeUtilities.CastOrDefault(notification.Value, type));
}
return notification;
}
}
@ -666,50 +608,6 @@ namespace Avalonia
return result;
}
/// <summary>
/// Sets a property value for a direct property binding.
/// </summary>
/// <param name="property">The property.</param>
/// <param name="value">The value.</param>
/// <returns></returns>
private void DirectBindingSet(AvaloniaProperty property, object value)
{
var error = value as BindingError;
if (error == null)
{
SetValue(property, value);
}
else
{
if (error.UseFallbackValue)
{
SetValue(property, error.FallbackValue);
}
Logger.Error(
LogArea.Binding,
this,
"Error binding to {Target}.{Property}: {Message}",
this,
property,
error.Exception.Message);
}
}
/// <summary>
/// Converts an unset value to the default value for a direct property.
/// </summary>
/// <param name="value">The value.</param>
/// <param name="property">The property.</param>
/// <returns>The value.</returns>
private object DirectUnsetToDefault(object value, AvaloniaProperty property)
{
return value == AvaloniaProperty.UnsetValue ?
((IDirectPropertyMetadata)property.GetMetadata(GetType())).UnsetValue :
value;
}
/// <summary>
/// Gets the default value for a property.
/// </summary>
@ -753,6 +651,109 @@ namespace Avalonia
return result;
}
/// <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)
{
var notification = value as BindingNotification;
if (notification != null)
{
if (notification.ErrorType == BindingErrorType.Error)
{
Logger.Error(
LogArea.Binding,
this,
"Error in binding to {Target}.{Property}: {Message}",
this,
property,
ExceptionUtilities.GetMessage(notification.Error));
}
if (notification.HasValue)
{
value = notification.Value;
}
}
if (notification == null || notification.HasValue)
{
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);
}
if (notification != null)
{
UpdateDataValidation(property, notification);
}
}
/// <summary>
/// Sets the value of a styled 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)
{
var notification = value as BindingNotification;
// We currently accept BindingNotifications for non-direct properties but we just
// strip them to their underlying value.
if (notification != null)
{
if (!notification.HasValue)
{
return;
}
else
{
value = notification.Value;
}
}
var originalValue = value;
if (!AvaloniaPropertyRegistry.Instance.IsRegistered(this, property))
{
ThrowNotRegistered(property);
}
if (!TypeUtilities.TryCast(property.PropertyType, value, out value))
{
throw new ArgumentException(string.Format(
"Invalid value for Property '{0}': '{1}' ({2})",
property.Name,
originalValue,
originalValue?.GetType().FullName ?? "(null)"));
}
PriorityValue v;
if (!_values.TryGetValue(property, out v))
{
if (value == AvaloniaProperty.UnsetValue)
{
return;
}
v = CreatePriorityValue(property);
_values.Add(property, v);
}
LogPropertySet(property, value, priority);
v.SetValue(value, (int)priority);
}
/// <summary>
/// Given a <see cref="AvaloniaProperty"/> returns a registered avalonia property that is
/// equal or throws if not found.

11
src/Avalonia.Base/AvaloniaObjectExtensions.cs

@ -216,7 +216,13 @@ namespace Avalonia
Contract.Requires<ArgumentNullException>(property != null);
Contract.Requires<ArgumentNullException>(binding != null);
var result = binding.Initiate(target, property, anchor);
var metadata = property.GetMetadata(target.GetType()) as IDirectPropertyMetadata;
var result = binding.Initiate(
target,
property,
anchor,
metadata?.EnableDataValidation ?? false);
if (result != null)
{
@ -311,7 +317,8 @@ namespace Avalonia
public InstancedBinding Initiate(
IAvaloniaObject target,
AvaloniaProperty targetProperty,
object anchor = null)
object anchor = null,
bool enableDataValidation = false)
{
return new InstancedBinding(_source);
}

9
src/Avalonia.Base/AvaloniaProperty.cs

@ -360,20 +360,25 @@ namespace Avalonia
/// 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>A <see cref="AvaloniaProperty{TValue}"/></returns>
public static DirectProperty<TOwner, TValue> RegisterDirect<TOwner, TValue>(
string name,
Func<TOwner, TValue> getter,
Action<TOwner, TValue> setter = null,
TValue unsetValue = default(TValue),
BindingMode defaultBindingMode = BindingMode.OneWay)
BindingMode defaultBindingMode = BindingMode.OneWay,
bool enableDataValidation = false)
where TOwner : IAvaloniaObject
{
Contract.Requires<ArgumentNullException>(name != null);
var metadata = new DirectPropertyMetadata<TValue>(
unsetValue: unsetValue,
defaultBindingMode: defaultBindingMode);
defaultBindingMode: defaultBindingMode,
enableDataValidation: enableDataValidation);
var result = new DirectProperty<TOwner, TValue>(name, getter, setter, metadata);
AvaloniaPropertyRegistry.Instance.Register(typeof(TOwner), result);

85
src/Avalonia.Base/Data/BindingChainNullException.cs

@ -0,0 +1,85 @@
// 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;
namespace Avalonia.Data
{
/// <summary>
/// An exception returned through <see cref="BindingNotification"/> signalling that a
/// requested binding expression could not be evaluated because of a null in one of the links
/// of the binding chain.
/// </summary>
public class BindingChainNullException : Exception
{
private string _message;
/// <summary>
/// Initalizes a new instance of the <see cref="BindingChainNullException"/> class.
/// </summary>
public BindingChainNullException()
{
}
/// <summary>
/// Initalizes a new instance of the <see cref="BindingChainNullException"/> class.
/// </summary>
public BindingChainNullException(string message)
{
_message = message;
}
/// <summary>
/// Initalizes a new instance of the <see cref="BindingChainNullException"/> class.
/// </summary>
/// <param name="expression">The expression.</param>
/// <param name="expressionNullPoint">
/// The point in the expression at which the null was encountered.
/// </param>
public BindingChainNullException(string expression, string expressionNullPoint)
{
Expression = expression;
ExpressionNullPoint = expressionNullPoint;
}
/// <summary>
/// Gets the expression that could not be evaluated.
/// </summary>
public string Expression { get; protected set; }
/// <summary>
/// Gets the point in the expression at which the null was encountered.
/// </summary>
public string ExpressionNullPoint { get; protected set; }
/// <inheritdoc/>
public override string Message
{
get
{
if (_message == null)
{
_message = BuildMessage();
}
return _message;
}
}
private string BuildMessage()
{
if (Expression != null && ExpressionNullPoint != null)
{
return $"'{ExpressionNullPoint}' is null in expression '{Expression}'.";
}
else if (ExpressionNullPoint != null)
{
return $"'{ExpressionNullPoint}' is null in expression.";
}
else
{
return "Null encountered in binding expression.";
}
}
}
}

59
src/Avalonia.Base/Data/BindingError.cs

@ -1,59 +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;
namespace Avalonia.Data
{
/// <summary>
/// Represents a recoverable binding error.
/// </summary>
/// <remarks>
/// When produced by a binding source observable, informs the binding system that an error
/// occurred. It can also provide an optional fallback value to be pushed to the binding
/// target.
///
/// Instead of using <see cref="BindingError"/>, one could simply not push a value (in the
/// case of a no fallback value) or push a fallback value, but BindingError also causes an
/// error to be logged with the correct binding target.
/// </remarks>
public class BindingError
{
/// <summary>
/// Initializes a new instance of the <see cref="BindingError"/> class.
/// </summary>
/// <param name="exception">An exception describing the binding error.</param>
public BindingError(Exception exception)
{
Exception = exception;
}
/// <summary>
/// Initializes a new instance of the <see cref="BindingError"/> class.
/// </summary>
/// <param name="exception">An exception describing the binding error.</param>
/// <param name="fallbackValue">The fallback value.</param>
public BindingError(Exception exception, object fallbackValue)
{
Exception = exception;
FallbackValue = fallbackValue;
UseFallbackValue = true;
}
/// <summary>
/// Gets the exception describing the binding error.
/// </summary>
public Exception Exception { get; }
/// <summary>
/// Get the fallback value.
/// </summary>
public object FallbackValue { get; }
/// <summary>
/// Get a value indicating whether the fallback value should be pushed to the binding
/// target.
/// </summary>
public bool UseFallbackValue { get; }
}
}

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

@ -0,0 +1,282 @@
// 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;
namespace Avalonia.Data
{
/// <summary>
/// Defines the types of binding errors for a <see cref="BindingNotification"/>.
/// </summary>
public enum BindingErrorType
{
/// <summary>
/// There was no error.
/// </summary>
None,
/// <summary>
/// There was a binding error.
/// </summary>
Error,
/// <summary>
/// There was a data validation error.
/// </summary>
DataValidationError,
}
/// <summary>
/// Represents a binding notification that can be a valid binding value, or a binding or
/// data validation error.
/// </summary>
public class BindingNotification
{
/// <summary>
/// A binding notification representing the null value.
/// </summary>
public static readonly BindingNotification Null =
new BindingNotification(null);
/// <summary>
/// A binding notification representing <see cref="AvaloniaProperty.UnsetValue"/>.
/// </summary>
public static readonly BindingNotification UnsetValue =
new BindingNotification(AvaloniaProperty.UnsetValue);
// Null cannot be held in WeakReference as it's indistinguishable from an expired value so
// use this value in its place.
private static readonly object NullValue = new object();
private WeakReference<object> _value;
/// <summary>
/// Initializes a new instance of the <see cref="BindingNotification"/> class.
/// </summary>
/// <param name="value">The binding value.</param>
public BindingNotification(object value)
{
_value = new WeakReference<object>(value ?? NullValue);
}
/// <summary>
/// Initializes a new instance of the <see cref="BindingNotification"/> class.
/// </summary>
/// <param name="error">The binding error.</param>
/// <param name="errorType">The type of the binding error.</param>
public BindingNotification(Exception error, BindingErrorType errorType)
{
if (errorType == BindingErrorType.None)
{
throw new ArgumentException($"'errorType' may not be None");
}
Error = error;
ErrorType = errorType;
}
/// <summary>
/// Initializes a new instance of the <see cref="BindingNotification"/> class.
/// </summary>
/// <param name="error">The binding error.</param>
/// <param name="errorType">The type of the binding error.</param>
/// <param name="fallbackValue">The fallback value.</param>
public BindingNotification(Exception error, BindingErrorType errorType, object fallbackValue)
: this(error, errorType)
{
_value = new WeakReference<object>(fallbackValue ?? NullValue);
}
/// <summary>
/// Gets the value that should be passed to the target when <see cref="HasValue"/>
/// is true.
/// </summary>
/// <remarks>
/// If this property is read when <see cref="HasValue"/> is false then it will return
/// <see cref="AvaloniaProperty.UnsetValue"/>.
/// </remarks>
public object Value
{
get
{
if (_value != null)
{
object result;
if (_value.TryGetTarget(out result))
{
return result == NullValue ? null : result;
}
}
// There's the possibility of a race condition in that HasValue can return true,
// and then the value is GC'd before Value is read. We should be ok though as
// we return UnsetValue which should be a safe alternative.
return AvaloniaProperty.UnsetValue;
}
}
/// <summary>
/// Gets a value indicating whether <see cref="Value"/> should be pushed to the target.
/// </summary>
public bool HasValue => _value != null;
/// <summary>
/// Gets the error that occurred on the source, if any.
/// </summary>
public Exception Error { get; set; }
/// <summary>
/// Gets the type of error that <see cref="Error"/> represents, if any.
/// </summary>
public BindingErrorType ErrorType { get; set; }
/// <summary>
/// Compares two instances of <see cref="BindingNotification"/> for equality.
/// </summary>
/// <param name="a">The first instance.</param>
/// <param name="b">The second instance.</param>
/// <returns>true if the two instances are equal; otherwise false.</returns>
public static bool operator ==(BindingNotification a, BindingNotification b)
{
if (object.ReferenceEquals(a, b))
{
return true;
}
if ((object)a == null || (object)b == null)
{
return false;
}
return a.HasValue == b.HasValue &&
a.ErrorType == b.ErrorType &&
(!a.HasValue || object.Equals(a.Value, b.Value)) &&
(a.ErrorType == BindingErrorType.None || ExceptionEquals(a.Error, b.Error));
}
/// <summary>
/// Compares two instances of <see cref="BindingNotification"/> for inequality.
/// </summary>
/// <param name="a">The first instance.</param>
/// <param name="b">The second instance.</param>
/// <returns>true if the two instances are unequal; otherwise false.</returns>
public static bool operator !=(BindingNotification a, BindingNotification b)
{
return !(a == b);
}
/// <summary>
/// Gets a value from an object that may be a <see cref="BindingNotification"/>.
/// </summary>
/// <param name="o">The object.</param>
/// <returns>The value.</returns>
/// <remarks>
/// If <paramref name="o"/> is a <see cref="BindingNotification"/> then returns the binding
/// notification's <see cref="Value"/>. If not, returns the object unchanged.
/// </remarks>
public static object ExtractValue(object o)
{
var notification = o as BindingNotification;
return notification != null ? notification.Value : o;
}
/// <summary>
/// Gets an exception from an object that may be a <see cref="BindingNotification"/>.
/// </summary>
/// <param name="o">The object.</param>
/// <returns>The value.</returns>
/// <remarks>
/// If <paramref name="o"/> is a <see cref="BindingNotification"/> then returns the binding
/// notification's <see cref="Error"/>. If not, returns the object unchanged.
/// </remarks>
public static object ExtractError(object o)
{
var notification = o as BindingNotification;
return notification != null ? notification.Error : o;
}
/// <summary>
/// Compares an object to an instance of <see cref="BindingNotification"/> for equality.
/// </summary>
/// <param name="obj">The object to compare.</param>
/// <returns>true if the two instances are equal; otherwise false.</returns>
public override bool Equals(object obj)
{
return Equals(obj as BindingNotification);
}
/// <summary>
/// Compares a value to an instance of <see cref="BindingNotification"/> for equality.
/// </summary>
/// <param name="other">The value to compare.</param>
/// <returns>true if the two instances are equal; otherwise false.</returns>
public bool Equals(BindingNotification other)
{
return this == other;
}
/// <summary>
/// Gets the hash code for this instance of <see cref="BindingNotification"/>.
/// </summary>
/// <returns>A hash code.</returns>
public override int GetHashCode()
{
return base.GetHashCode();
}
/// <summary>
/// Adds an error to the <see cref="BindingNotification"/>.
/// </summary>
/// <param name="e">The error to add.</param>
/// <param name="type">The error type.</param>
public void AddError(Exception e, BindingErrorType type)
{
Contract.Requires<ArgumentNullException>(e != null);
Contract.Requires<ArgumentException>(type != BindingErrorType.None);
Error = Error != null ? new AggregateException(Error, e) : e;
if (type == BindingErrorType.Error || ErrorType == BindingErrorType.Error)
{
ErrorType = BindingErrorType.Error;
}
}
/// <summary>
/// Removes the <see cref="Value"/> and makes <see cref="HasValue"/> return null.
/// </summary>
public void ClearValue()
{
_value = null;
}
/// <summary>
/// Sets the <see cref="Value"/>.
/// </summary>
public void SetValue(object value)
{
_value = new WeakReference<object>(value ?? NullValue);
}
/// <inheritdoc/>
public override string ToString()
{
switch (ErrorType)
{
case BindingErrorType.None:
return $"{{Value: {Value}}}";
default:
return HasValue ?
$"{{{ErrorType}: {Error}, Fallback: {Value}}}" :
$"{{{ErrorType}: {Error}}}";
}
}
private static bool ExceptionEquals(Exception a, Exception b)
{
return a?.GetType() == b?.GetType() &&
a?.Message == b?.Message;
}
}
}

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

@ -54,7 +54,10 @@ namespace Avalonia.Data
if (source != null)
{
return source.Take(1).Subscribe(x => target.SetValue(property, x, binding.Priority));
return source
.Where(x => BindingNotification.ExtractValue(x) != AvaloniaProperty.UnsetValue)
.Take(1)
.Subscribe(x => target.SetValue(property, x, binding.Priority));
}
else
{

4
src/Avalonia.Base/Data/IBinding.cs

@ -19,12 +19,14 @@ namespace Avalonia.Data
/// order to locate named controls or resources. The <paramref name="anchor"/> parameter
/// can be used to provice this context.
/// </param>
/// <param name="enableDataValidation">Whether data validation should be enabled.</param>
/// <returns>
/// A <see cref="InstancedBinding"/> or null if the binding could not be resolved.
/// </returns>
InstancedBinding Initiate(
IAvaloniaObject target,
AvaloniaProperty targetProperty,
object anchor = null);
object anchor = null,
bool enableDataValidation = false);
}
}

17
src/Avalonia.Base/Data/IValidationStatus.cs

@ -1,17 +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.
namespace Avalonia.Data
{
/// <summary>
/// Contains information on if the current object passed validation.
/// Subclasses of this class contain additional information depending on the method of validation checking.
/// </summary>
public interface IValidationStatus
{
/// <summary>
/// True when the data passes validation; otherwise, false.
/// </summary>
bool IsValid { get; }
}
}

6
src/Avalonia.Base/Data/IndexerBinding.cs

@ -21,7 +21,11 @@ namespace Avalonia.Data
public AvaloniaProperty Property { get; }
private BindingMode Mode { get; }
public InstancedBinding Initiate(IAvaloniaObject target, AvaloniaProperty targetProperty, object anchor = null)
public InstancedBinding Initiate(
IAvaloniaObject target,
AvaloniaProperty targetProperty,
object anchor = null,
bool enableDataValidation = false)
{
var mode = Mode == BindingMode.Default ?
targetProperty.GetMetadata(target.GetType()).DefaultBindingMode :

44
src/Avalonia.Base/Data/ObjectValidationStatus.cs

@ -1,44 +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;
namespace Avalonia.Data
{
/// <summary>
/// An immutable struct that contains validation information for a <see cref="AvaloniaObject"/> that validates a single property.
/// </summary>
public struct ObjectValidationStatus : IValidationStatus
{
private Dictionary<Type, IValidationStatus> currentValidationStatus;
public bool IsValid => currentValidationStatus?.Values.All(status => status.IsValid) ?? true;
/// <summary>
/// Constructs the structure with the given validation information.
/// </summary>
/// <param name="validations">The validation information</param>
public ObjectValidationStatus(Dictionary<Type, IValidationStatus> validations)
:this()
{
currentValidationStatus = validations;
}
/// <summary>
/// Creates a new status with the updated information.
/// </summary>
/// <param name="status">The updated status information.</param>
/// <returns>The new validation status.</returns>
public ObjectValidationStatus UpdateValidationStatus(IValidationStatus status)
{
var newStatus = new Dictionary<Type, IValidationStatus>(currentValidationStatus ??
new Dictionary<Type, IValidationStatus>());
newStatus[status.GetType()] = status;
return new ObjectValidationStatus(newStatus);
}
public IEnumerable<IValidationStatus> StatusInformation => currentValidationStatus.Values;
}
}

11
src/Avalonia.Base/DirectProperty.cs

@ -85,19 +85,26 @@ namespace Avalonia
/// 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> AddOwner<TNewOwner>(
Func<TNewOwner, TValue> getter,
Action<TNewOwner, TValue> setter = null,
TValue unsetValue = default(TValue),
BindingMode defaultBindingMode = BindingMode.OneWay)
BindingMode defaultBindingMode = BindingMode.OneWay,
bool enableDataValidation = false)
where TNewOwner : AvaloniaObject
{
var result = new DirectProperty<TNewOwner, TValue>(
this,
getter,
setter,
new DirectPropertyMetadata<TValue>(unsetValue, defaultBindingMode));
new DirectPropertyMetadata<TValue>(
unsetValue: unsetValue,
defaultBindingMode: defaultBindingMode,
enableDataValidation: enableDataValidation));
AvaloniaPropertyRegistry.Instance.Register(typeof(TNewOwner), result);
return result;

20
src/Avalonia.Base/DirectPropertyMetadata`1.cs

@ -17,19 +17,35 @@ namespace Avalonia
/// The value to use when the property is set to <see cref="AvaloniaProperty.UnsetValue"/>
/// </param>
/// <param name="defaultBindingMode">The default binding mode.</param>
/// <param name="enableDataValidation">
/// Whether the property is interested in data validation.
/// </param>
public DirectPropertyMetadata(
TValue unsetValue = default(TValue),
BindingMode defaultBindingMode = BindingMode.Default)
BindingMode defaultBindingMode = BindingMode.Default,
bool enableDataValidation = false)
: base(defaultBindingMode)
{
UnsetValue = unsetValue;
EnableDataValidation = enableDataValidation;
}
/// <summary>
/// Gets the to use when the property is set to <see cref="AvaloniaProperty.UnsetValue"/>.
/// Gets the value to use when the property is set to <see cref="AvaloniaProperty.UnsetValue"/>.
/// </summary>
public TValue UnsetValue { get; private set; }
/// <summary>
/// Gets a value indicating whether the property is interested in data validation.
/// </summary>
/// <remarks>
/// 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 recieving data
/// validation messages so this feature must be explicitly enabled by setting this flag.
/// </remarks>
public bool EnableDataValidation { get; }
/// <inheritdoc/>
object IDirectPropertyMetadata.UnsetValue => UnsetValue;

5
src/Avalonia.Base/IDirectPropertyMetadata.cs

@ -12,5 +12,10 @@ namespace Avalonia
/// Gets the to use when the property is set to <see cref="AvaloniaProperty.UnsetValue"/>.
/// </summary>
object UnsetValue { get; }
/// <summary>
/// Gets a value indicating whether the property is interested in data validation.
/// </summary>
bool EnableDataValidation { get; }
}
}

7
src/Avalonia.Base/IPriorityValueOwner.cs

@ -19,10 +19,11 @@ namespace Avalonia
void Changed(PriorityValue sender, object oldValue, object newValue);
/// <summary>
/// Called when the validation state of a <see cref="PriorityValue"/> changes.
/// Called when a <see cref="BindingNotification"/> is received by a
/// <see cref="PriorityValue"/>.
/// </summary>
/// <param name="sender">The source of the change.</param>
/// <param name="status">The validation status.</param>
void DataValidationChanged(PriorityValue sender, IValidationStatus status);
/// <param name="notification">The notification.</param>
void BindingNotificationReceived(PriorityValue sender, BindingNotification notification);
}
}

26
src/Avalonia.Base/PriorityBindingEntry.cs

@ -93,22 +93,24 @@ namespace Avalonia
private void ValueChanged(object value)
{
var bindingError = value as BindingError;
var notification = value as BindingNotification;
if (bindingError != null)
if (notification != null)
{
_owner.Error(this, bindingError);
if (notification.HasValue)
{
Value = notification.Value;
_owner.Changed(this);
}
if (notification.ErrorType != BindingErrorType.None)
{
_owner.Error(this, notification);
}
}
var validationStatus = value as IValidationStatus;
if (validationStatus != null)
{
_owner.Validation(this, validationStatus);
}
else if (bindingError == null || bindingError.UseFallbackValue)
else
{
Value = bindingError == null ? value : bindingError.FallbackValue;
Value = value;
_owner.Changed(this);
}
}

13
src/Avalonia.Base/PriorityLevel.cs

@ -159,22 +159,11 @@ namespace Avalonia
/// </summary>
/// <param name="entry">The entry that completed.</param>
/// <param name="error">The error.</param>
public void Error(PriorityBindingEntry entry, BindingError error)
public void Error(PriorityBindingEntry entry, BindingNotification error)
{
_owner.LevelError(this, error);
}
/// <summary>
/// Invoked when an entry in <see cref="Bindings"/> reports validation status.
/// </summary>
/// <param name="entry">The entry that completed.</param>
/// <param name="validationStatus">The validation status.</param>
public void Validation(PriorityBindingEntry entry, IValidationStatus validationStatus)
{
_owner.LevelValidation(this, validationStatus);
}
/// <summary>
/// Activates the first binding that has a value.
/// </summary>

39
src/Avalonia.Base/PriorityValue.cs

@ -77,7 +77,6 @@ namespace Avalonia
/// </summary>
/// <param name="binding">The binding.</param>
/// <param name="priority">The binding priority.</param>
/// <param name="validation">Validation settings for the binding.</param>
/// <returns>
/// A disposable that will remove the binding.
/// </returns>
@ -179,31 +178,21 @@ namespace Avalonia
}
}
/// <summary>
/// Called whenever a priority level validation state changes.
/// </summary>
/// <param name="priorityLevel">The priority level of the changed entry.</param>
/// <param name="validationStatus">The validation status.</param>
public void LevelValidation(PriorityLevel priorityLevel, IValidationStatus validationStatus)
{
_owner.DataValidationChanged(this, validationStatus);
}
/// <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, BindingError error)
public void LevelError(PriorityLevel level, BindingNotification error)
{
Logger.Log(
LogEventLevel.Error,
LogArea.Binding,
_owner,
"Error binding to {Target}.{Property}: {Message}",
"Error in binding to {Target}.{Property}: {Message}",
_owner,
Property,
error.Exception.Message);
error.Error.Message);
}
/// <summary>
@ -248,8 +237,14 @@ namespace Avalonia
/// <param name="priority">The priority level that the value came from.</param>
private void UpdateValue(object value, int priority)
{
var notification = value as BindingNotification;
object castValue;
if (notification != null)
{
value = (notification.HasValue) ? notification.Value : null;
}
if (TypeUtilities.TryCast(_valueType, value, out castValue))
{
var old = _value;
@ -261,7 +256,21 @@ namespace Avalonia
ValuePriority = priority;
_value = castValue;
_owner?.Changed(this, old, _value);
if (notification?.HasValue == true)
{
notification.SetValue(castValue);
}
if (notification == null || notification.HasValue)
{
_owner?.Changed(this, old, _value);
}
if (notification != null)
{
_owner?.BindingNotificationReceived(this, notification);
}
}
else
{

3
src/Avalonia.Base/PropertyMetadata.cs

@ -17,7 +17,8 @@ namespace Avalonia
/// Initializes a new instance of the <see cref="PropertyMetadata"/> class.
/// </summary>
/// <param name="defaultBindingMode">The default binding mode.</param>
public PropertyMetadata(BindingMode defaultBindingMode = BindingMode.Default)
public PropertyMetadata(
BindingMode defaultBindingMode = BindingMode.Default)
{
_defaultBindingMode = defaultBindingMode;
}

23
src/Avalonia.Base/Utilities/ExceptionUtilities.cs

@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Avalonia.Utilities
{
internal static class ExceptionUtilities
{
public static string GetMessage(Exception e)
{
var aggregate = e as AggregateException;
if (aggregate != null)
{
return string.Join(" | ", aggregate.InnerExceptions.Select(x => x.Message));
}
return e.Message;
}
}
}

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

@ -27,6 +27,21 @@ namespace Avalonia.Utilities
{ typeof(short), new List<Type> { typeof(byte) } }
};
private static readonly Type[] NumericTypes = new[]
{
typeof(Byte),
typeof(Decimal),
typeof(Double),
typeof(Int16),
typeof(Int32),
typeof(Int64),
typeof(SByte),
typeof(Single),
typeof(UInt16),
typeof(UInt32),
typeof(UInt64),
};
/// <summary>
/// Returns a value indicating whether null can be assigned to the specified type.
/// </summary>
@ -208,5 +223,31 @@ namespace Avalonia.Utilities
return null;
}
}
/// <summary>
/// Determines if a type is numeric. Nullable numeric types are considered numeric.
/// </summary>
/// <returns>
/// True if the type is numberic; otherwise false.
/// </returns>
/// <remarks>
/// Boolean is not considered numeric.
/// </remarks>
public static bool IsNumeric(Type type)
{
if (type == null)
{
return false;
}
if (type.GetTypeInfo().IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
{
return IsNumeric(Nullable.GetUnderlyingType(type));
}
else
{
return NumericTypes.Contains(type);
}
}
}
}

54
src/Avalonia.Base/Utilities/WeakObservable.cs

@ -0,0 +1,54 @@
// 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;
using System.Reactive.Linq;
namespace Avalonia.Utilities
{
/// <summary>
/// Provides extension methods for working with weak event handlers.
/// </summary>
public static class WeakObservable
{
/// <summary>
/// Converts a .NET event conforming to the standard .NET event pattern into an observable
/// sequence, subscribing weakly.
/// </summary>
/// <typeparam name="TEventArgs">The type of the event args.</typeparam>
/// <param name="target">Object instance that exposes the event to convert.</param>
/// <param name="eventName">Name of the event to convert.</param>
/// <returns></returns>
public static IObservable<EventPattern<object, TEventArgs>> FromEventPattern<TEventArgs>(
object target,
string eventName)
where TEventArgs : EventArgs
{
Contract.Requires<ArgumentNullException>(target != null);
Contract.Requires<ArgumentNullException>(eventName != null);
return Observable.Create<EventPattern<object, TEventArgs>>(observer =>
{
var handler = new Handler<TEventArgs>(observer);
WeakSubscriptionManager.Subscribe(target, eventName, handler);
return () => WeakSubscriptionManager.Unsubscribe(target, eventName, handler);
}).Publish().RefCount();
}
private class Handler<TEventArgs> : IWeakSubscriber<TEventArgs> where TEventArgs : EventArgs
{
private IObserver<EventPattern<object, TEventArgs>> _observer;
public Handler(IObserver<EventPattern<object, TEventArgs>> observer)
{
_observer = observer;
}
public void OnEvent(object sender, TEventArgs e)
{
_observer.OnNext(new EventPattern<object, TEventArgs>(sender, e));
}
}
}
}

1
src/Avalonia.Controls/Canvas.cs

@ -47,6 +47,7 @@ namespace Avalonia.Controls
/// </summary>
static Canvas()
{
ClipToBoundsProperty.OverrideDefaultValue<Canvas>(false);
AffectsCanvasArrange(LeftProperty, TopProperty, RightProperty, BottomProperty);
}

9
src/Avalonia.Controls/Control.cs

@ -108,7 +108,6 @@ namespace Avalonia.Controls
PseudoClass(IsEnabledCoreProperty, x => !x, ":disabled");
PseudoClass(IsFocusedProperty, ":focus");
PseudoClass(IsPointerOverProperty, ":pointerover");
PseudoClass(ValidationStatusProperty, status => !status.IsValid, ":invalid");
}
/// <summary>
@ -400,13 +399,6 @@ namespace Avalonia.Controls
/// </summary>
protected IPseudoClasses PseudoClasses => Classes;
/// <inheritdoc/>
protected override void DataValidationChanged(AvaloniaProperty property, IValidationStatus status)
{
base.DataValidationChanged(property, status);
ValidationStatus.UpdateValidationStatus(status);
}
/// <summary>
/// Sets the control's logical parent.
/// </summary>
@ -729,7 +721,6 @@ namespace Avalonia.Controls
_isAttachedToLogicalTree = false;
_styleDetach.OnNext(this);
this.TemplatedParent = null;
OnDetachedFromLogicalTree(e);
foreach (var child in LogicalChildren.OfType<Control>())

11
src/Avalonia.Controls/ItemsControl.cs

@ -285,17 +285,6 @@ namespace Avalonia.Controls
LogicalChildren.RemoveAll(toRemove);
}
/// <inheritdoc/>
protected override void OnTemplateChanged(AvaloniaPropertyChangedEventArgs e)
{
base.OnTemplateChanged(e);
if (e.NewValue == null)
{
ItemContainerGenerator?.Clear();
}
}
/// <summary>
/// Caled when the <see cref="Items"/> property changes.
/// </summary>

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

@ -130,7 +130,7 @@ namespace Avalonia.Controls.Primitives
control.ApplyTemplate();
if (!(control is IPresenter && control.TemplatedParent == templatedParent))
if (!(control is IPresenter) && control.TemplatedParent == templatedParent)
{
foreach (IControl child in control.GetVisualChildren())
{

101
src/Avalonia.Controls/TextBox.cs

@ -35,6 +35,14 @@ namespace Avalonia.Controls
o => o.CaretIndex,
(o, v) => o.CaretIndex = v);
public static readonly DirectProperty<TextBox, IEnumerable<Exception>> DataValidationErrorsProperty =
AvaloniaProperty.RegisterDirect<TextBox, IEnumerable<Exception>>(
nameof(DataValidationErrors),
o => o.DataValidationErrors);
public static readonly StyledProperty<bool> IsReadOnlyProperty =
AvaloniaProperty.Register<TextBox, bool>(nameof(IsReadOnly));
public static readonly DirectProperty<TextBox, int> SelectionStartProperty =
AvaloniaProperty.RegisterDirect<TextBox, int>(
nameof(SelectionStart),
@ -51,7 +59,8 @@ namespace Avalonia.Controls
TextBlock.TextProperty.AddOwner<TextBox>(
o => o.Text,
(o, v) => o.Text = v,
defaultBindingMode: BindingMode.TwoWay);
defaultBindingMode: BindingMode.TwoWay,
enableDataValidation: true);
public static readonly StyledProperty<TextAlignment> TextAlignmentProperty =
TextBlock.TextAlignmentProperty.AddOwner<TextBox>();
@ -65,9 +74,6 @@ namespace Avalonia.Controls
public static readonly StyledProperty<bool> UseFloatingWatermarkProperty =
AvaloniaProperty.Register<TextBox, bool>("UseFloatingWatermark");
public static readonly StyledProperty<bool> IsReadOnlyProperty =
AvaloniaProperty.Register<TextBox, bool>(nameof(IsReadOnly));
struct UndoRedoState : IEquatable<UndoRedoState>
{
public string Text { get; }
@ -89,6 +95,8 @@ namespace Avalonia.Controls
private bool _canScrollHorizontally;
private TextPresenter _presenter;
private UndoRedoHelper<UndoRedoState> _undoRedoHelper;
private bool _ignoreTextChanges;
private IEnumerable<Exception> _dataValidationErrors;
static TextBox()
{
@ -145,6 +153,18 @@ namespace Avalonia.Controls
}
}
public IEnumerable<Exception> DataValidationErrors
{
get { return _dataValidationErrors; }
private set { SetAndRaise(DataValidationErrorsProperty, ref _dataValidationErrors, value); }
}
public bool IsReadOnly
{
get { return GetValue(IsReadOnlyProperty); }
set { SetValue(IsReadOnlyProperty, value); }
}
public int SelectionStart
{
get
@ -177,7 +197,13 @@ namespace Avalonia.Controls
public string Text
{
get { return _text; }
set { SetAndRaise(TextProperty, ref _text, value); }
set
{
if (!_ignoreTextChanges)
{
SetAndRaise(TextProperty, ref _text, value);
}
}
}
public TextAlignment TextAlignment
@ -198,12 +224,6 @@ namespace Avalonia.Controls
set { SetValue(UseFloatingWatermarkProperty, value); }
}
public bool IsReadOnly
{
get { return GetValue(IsReadOnlyProperty); }
set { SetValue(IsReadOnlyProperty, value); }
}
public TextWrapping TextWrapping
{
get { return GetValue(TextWrappingProperty); }
@ -235,14 +255,6 @@ namespace Avalonia.Controls
HandleTextInput(e.Text);
}
protected override void DataValidationChanged(AvaloniaProperty property, IValidationStatus status)
{
if (property == TextProperty)
{
UpdateValidationState(status);
}
}
private void HandleTextInput(string input)
{
if (!IsReadOnly)
@ -254,7 +266,7 @@ namespace Avalonia.Controls
DeleteSelection();
caretIndex = CaretIndex;
text = Text ?? string.Empty;
Text = text.Substring(0, caretIndex) + input + text.Substring(caretIndex);
SetTextInternal(text.Substring(0, caretIndex) + input + text.Substring(caretIndex));
CaretIndex += input.Length;
SelectionStart = SelectionEnd = CaretIndex;
_undoRedoHelper.DiscardRedo();
@ -367,7 +379,8 @@ namespace Avalonia.Controls
if (!DeleteSelection() && CaretIndex > 0)
{
CaretIndex -= DeleteCharacter(CaretIndex - 1);
SetTextInternal(text.Substring(0, caretIndex - 1) + text.Substring(caretIndex));
--CaretIndex;
}
break;
@ -380,7 +393,7 @@ namespace Avalonia.Controls
if (!DeleteSelection() && caretIndex < text.Length)
{
DeleteCharacter(CaretIndex);
SetTextInternal(text.Substring(0, caretIndex) + text.Substring(caretIndex + 1));
}
break;
@ -478,6 +491,35 @@ namespace Avalonia.Controls
}
}
protected override void UpdateDataValidation(AvaloniaProperty property, BindingNotification status)
{
if (property == TextProperty)
{
var classes = (IPseudoClasses)Classes;
DataValidationErrors = UnpackException(status.Error);
classes.Set(":error", DataValidationErrors != null);
}
}
private static IEnumerable<Exception> UnpackException(Exception exception)
{
if (exception != null)
{
var aggregate = exception as AggregateException;
var exceptions = aggregate == null ?
(IEnumerable<Exception>)new[] { exception } :
aggregate.InnerExceptions;
var filtered = exceptions.Where(x => !(x is BindingChainNullException)).ToList();
if (filtered.Count > 0)
{
return filtered;
}
}
return null;
}
private int CoerceCaretIndex(int value)
{
var text = Text;
@ -663,7 +705,7 @@ namespace Avalonia.Controls
var start = Math.Min(selectionStart, selectionEnd);
var end = Math.Max(selectionStart, selectionEnd);
var text = Text;
Text = text.Substring(0, start) + text.Substring(end);
SetTextInternal(text.Substring(0, start) + text.Substring(end));
SelectionStart = SelectionEnd = CaretIndex = start;
return true;
}
@ -710,6 +752,19 @@ namespace Avalonia.Controls
return i;
}
private void SetTextInternal(string value)
{
try
{
_ignoreTextChanges = true;
SetAndRaise(TextProperty, ref _text, value);
}
finally
{
_ignoreTextChanges = false;
}
}
private void SetSelectionForControlBackspace(InputModifiers modifiers)
{
SelectionStart = CaretIndex;

23
src/Avalonia.Controls/ToolTip.cs

@ -105,20 +105,17 @@ namespace Avalonia.Controls
{
if (control != null && control.IsVisible && control.GetVisualRoot() != null)
{
if (s_popup == null)
if (s_popup != null)
{
s_popup = new PopupRoot
{
Content = new ToolTip(),
};
((ISetLogicalParent)s_popup).SetParent(control);
throw new AvaloniaInternalException("Previous ToolTip not disposed.");
}
var cp = MouseDevice.Instance?.GetPosition(control);
var position = control.PointToScreen(cp ?? new Point(0, 0)) + new Vector(0, 22);
((ToolTip)s_popup.Content).Content = GetTip(control);
s_popup = new PopupRoot();
((ISetLogicalParent)s_popup).SetParent(control);
s_popup.Content = new ToolTip { Content = GetTip(control) };
s_popup.Position = position;
s_popup.Show();
@ -148,9 +145,15 @@ namespace Avalonia.Controls
if (control == s_current)
{
if (s_popup != null && s_popup.IsVisible)
if (s_popup != null)
{
s_popup.Hide();
// Clear the ToolTip's Content in case it has control content: this will
// reset its visual parent allowing it to be used again.
((ToolTip)s_popup.Content).Content = null;
// Dispose of the popup.
s_popup.Dispose();
s_popup = null;
}
s_show.OnNext(null);

26
src/Avalonia.DotNetFrameworkRuntime/Avalonia.DotNetFrameworkRuntime.v2.ncrunchproject

@ -0,0 +1,26 @@
<ProjectConfiguration>
<AutoDetectNugetBuildDependencies>true</AutoDetectNugetBuildDependencies>
<BuildPriority>1000</BuildPriority>
<CopyReferencedAssembliesToWorkspace>false</CopyReferencedAssembliesToWorkspace>
<ConsiderInconclusiveTestsAsPassing>false</ConsiderInconclusiveTestsAsPassing>
<PreloadReferencedAssemblies>false</PreloadReferencedAssemblies>
<AllowDynamicCodeContractChecking>true</AllowDynamicCodeContractChecking>
<AllowStaticCodeContractChecking>false</AllowStaticCodeContractChecking>
<AllowCodeAnalysis>false</AllowCodeAnalysis>
<IgnoreThisComponentCompletely>false</IgnoreThisComponentCompletely>
<RunPreBuildEvents>false</RunPreBuildEvents>
<RunPostBuildEvents>false</RunPostBuildEvents>
<PreviouslyBuiltSuccessfully>true</PreviouslyBuiltSuccessfully>
<InstrumentAssembly>true</InstrumentAssembly>
<PreventSigningOfAssembly>false</PreventSigningOfAssembly>
<AnalyseExecutionTimes>true</AnalyseExecutionTimes>
<DetectStackOverflow>true</DetectStackOverflow>
<IncludeStaticReferencesInWorkspace>true</IncludeStaticReferencesInWorkspace>
<DefaultTestTimeout>60000</DefaultTestTimeout>
<UseBuildConfiguration />
<UseBuildPlatform />
<ProxyProcessPath />
<UseCPUArchitecture>AutoDetect</UseCPUArchitecture>
<MSTestThreadApartmentState>STA</MSTestThreadApartmentState>
<BuildProcessArchitecture>x86</BuildProcessArchitecture>
</ProjectConfiguration>

2
src/Avalonia.Themes.Default/Accents/BaseLight.xaml

@ -18,6 +18,8 @@
<SolidColorBrush x:Key="ThemeAccentBrush2">#99119EDA</SolidColorBrush>
<SolidColorBrush x:Key="ThemeAccentBrush3">#66119EDA</SolidColorBrush>
<SolidColorBrush x:Key="ThemeAccentBrush4">#33119EDA</SolidColorBrush>
<SolidColorBrush x:Key="ErrorBrush">Red</SolidColorBrush>
<SolidColorBrush x:Key="ErrorBrushLight">#10ff0000</SolidColorBrush>
<sys:Double x:Key="ThemeBorderThickness">2</sys:Double>
<sys:Double x:Key="ThemeDisabledOpacity">0.5</sys:Double>

4
src/Avalonia.Themes.Default/DefaultTheme.xaml

@ -1,4 +1,7 @@
<Styles xmlns="https://github.com/avaloniaui">
<!-- Define ToolTip first so its styles can be overriden by other controls (e.g. TextBox) -->
<StyleInclude Source="resm:Avalonia.Themes.Default.ToolTip.xaml?assembly=Avalonia.Themes.Default"/>
<StyleInclude Source="resm:Avalonia.Themes.Default.FocusAdorner.xaml?assembly=Avalonia.Themes.Default"/>
<StyleInclude Source="resm:Avalonia.Themes.Default.Button.xaml?assembly=Avalonia.Themes.Default"/>
<StyleInclude Source="resm:Avalonia.Themes.Default.Carousel.xaml?assembly=Avalonia.Themes.Default"/>
@ -28,7 +31,6 @@
<StyleInclude Source="resm:Avalonia.Themes.Default.TextBox.xaml?assembly=Avalonia.Themes.Default"/>
<StyleInclude Source="resm:Avalonia.Themes.Default.ToggleButton.xaml?assembly=Avalonia.Themes.Default"/>
<StyleInclude Source="resm:Avalonia.Themes.Default.Expander.xaml?assembly=Avalonia.Themes.Default"/>
<StyleInclude Source="resm:Avalonia.Themes.Default.ToolTip.xaml?assembly=Avalonia.Themes.Default"/>
<StyleInclude Source="resm:Avalonia.Themes.Default.TreeView.xaml?assembly=Avalonia.Themes.Default"/>
<StyleInclude Source="resm:Avalonia.Themes.Default.TreeViewItem.xaml?assembly=Avalonia.Themes.Default"/>
<StyleInclude Source="resm:Avalonia.Themes.Default.Window.xaml?assembly=Avalonia.Themes.Default"/>

34
src/Avalonia.Themes.Default/TextBox.xaml

@ -27,10 +27,19 @@
</MultiBinding>
</TextBlock.IsVisible>
</TextBlock>
<ScrollViewer CanScrollHorizontally="{TemplateBinding CanScrollHorizontally}"
HorizontalScrollBarVisibility="{TemplateBinding (ScrollViewer.HorizontalScrollBarVisibility)}"
VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}">
<DockPanel LastChildFill="True">
<Canvas Name="error" DockPanel.Dock="Right" Width="14" Height="14" Margin="4 0 1 0">
<ToolTip.Tip>
<ItemsControl Items="{TemplateBinding DataValidationErrors}" MemberSelector="Message"/>
</ToolTip.Tip>
<Path Data="M14,7 A7,7 0 0,0 0,7 M0,7 A7,7 0 1,0 14,7 M7,3l0,5 M7,9l0,2" Stroke="{StyleResource ErrorBrush}" StrokeThickness="2"/>
</Canvas>
<ScrollViewer CanScrollHorizontally="{TemplateBinding CanScrollHorizontally}"
HorizontalScrollBarVisibility="{TemplateBinding (ScrollViewer.HorizontalScrollBarVisibility)}"
VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}">
<Panel>
<TextBlock Name="watermark"
Opacity="0.5"
@ -45,8 +54,9 @@
TextWrapping="{TemplateBinding TextWrapping}"/>
</Panel>
</ScrollViewer>
</DockPanel>
</DockPanel>
</Border>
</ControlTemplate>
</Setter>
@ -57,7 +67,17 @@
<Style Selector="TextBox:focus /template/ Border#border">
<Setter Property="BorderBrush" Value="{StyleResource ThemeBorderDarkBrush}"/>
</Style>
<Style Selector="TextBox:invalid /template/ Border#border">
<Setter Property="BorderBrush" Value="Red"/>
<Style Selector="TextBox:error /template/ Border#border">
<Setter Property="BorderBrush" Value="{StyleResource ErrorBrush}"/>
</Style>
<Style Selector="TextBox /template/ Canvas#error">
<Setter Property="IsVisible" Value="False"/>
</Style>
<Style Selector="TextBox:error /template/ Canvas#error">
<Setter Property="IsVisible" Value="True"/>
</Style>
<Style Selector="TextBox /template/ ToolTip">
<Setter Property="Background" Value="{StyleResource ErrorBrushLight}"/>
<Setter Property="BorderBrush" Value="{StyleResource ErrorBrush}"/>
</Style>
</Styles>

59
src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs

@ -27,10 +27,12 @@ namespace Avalonia.Markup.Xaml.Data
/// Initializes a new instance of the <see cref="Binding"/> class.
/// </summary>
/// <param name="path">The binding path.</param>
public Binding(string path)
/// <param name="mode">The binding mode.</param>
public Binding(string path, BindingMode mode = BindingMode.Default)
: this()
{
Path = path;
Mode = mode;
}
/// <summary>
@ -78,21 +80,18 @@ namespace Avalonia.Markup.Xaml.Data
/// </summary>
public object Source { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the property should be validated.
/// </summary>
public bool EnableValidation { get; set; }
/// <inheritdoc/>
public InstancedBinding Initiate(
IAvaloniaObject target,
AvaloniaProperty targetProperty,
object anchor = null)
object anchor = null,
bool enableDataValidation = false)
{
Contract.Requires<ArgumentNullException>(target != null);
var pathInfo = ParsePath(Path);
ValidateState(pathInfo);
enableDataValidation = enableDataValidation && Priority == BindingPriority.LocalValue;
ExpressionObserver observer;
@ -105,7 +104,7 @@ namespace Avalonia.Markup.Xaml.Data
}
else if (Source != null)
{
observer = CreateSourceObserver(Source, pathInfo.Path);
observer = CreateSourceObserver(Source, pathInfo.Path, enableDataValidation);
}
else if (RelativeSource == null || RelativeSource.Mode == RelativeSourceMode.DataContext)
{
@ -113,7 +112,8 @@ namespace Avalonia.Markup.Xaml.Data
target,
pathInfo.Path,
targetProperty == Control.DataContextProperty,
anchor);
anchor,
enableDataValidation);
}
else if (RelativeSource.Mode == RelativeSourceMode.TemplatedParent)
{
@ -135,7 +135,7 @@ namespace Avalonia.Markup.Xaml.Data
fallback = null;
}
var subject = new ExpressionSubject(
var subject = new BindingExpression(
observer,
targetProperty?.PropertyType ?? typeof(object),
fallback,
@ -197,7 +197,8 @@ namespace Avalonia.Markup.Xaml.Data
IAvaloniaObject target,
string path,
bool targetIsDataContext,
object anchor)
object anchor,
bool enableDataValidation)
{
Contract.Requires<ArgumentNullException>(target != null);
@ -220,19 +221,16 @@ namespace Avalonia.Markup.Xaml.Data
() => target.GetValue(Control.DataContextProperty),
path,
update,
EnableValidation);
enableDataValidation);
return result;
}
else
{
return new ExpressionObserver(
target.GetObservable(Visual.VisualParentProperty)
.OfType<IAvaloniaObject>()
.Select(x => x.GetObservable(Control.DataContextProperty))
.Switch(),
GetParentDataContext(target),
path,
EnableValidation);
enableDataValidation);
}
}
@ -240,18 +238,23 @@ namespace Avalonia.Markup.Xaml.Data
{
Contract.Requires<ArgumentNullException>(target != null);
var description = $"#{elementName}.{path}";
var result = new ExpressionObserver(
ControlLocator.Track(target, elementName),
path,
EnableValidation);
false,
description);
return result;
}
private ExpressionObserver CreateSourceObserver(object source, string path)
private ExpressionObserver CreateSourceObserver(
object source,
string path,
bool enabledDataValidation)
{
Contract.Requires<ArgumentNullException>(source != null);
return new ExpressionObserver(source, path, EnableValidation);
return new ExpressionObserver(source, path, enabledDataValidation);
}
private ExpressionObserver CreateTemplatedParentObserver(
@ -272,6 +275,22 @@ namespace Avalonia.Markup.Xaml.Data
return result;
}
private IObservable<object> GetParentDataContext(IAvaloniaObject target)
{
// The DataContext is based on the visual parent and not the logical parent: this may
// seem unintuitive considering the fact that property inheritance works on the logical
// tree, but consider a ContentControl with a ContentPresenter. The ContentControl's
// Content property is bound to a value which becomes the ContentPresenter's
// DataContext - it is from this that the child hosted by the ContentPresenter needs to
// inherit its DataContext.
return target.GetObservable(Visual.VisualParentProperty)
.Select(x =>
{
return (x as IAvaloniaObject)?.GetObservable(Control.DataContextProperty) ??
Observable.Return((object)null);
}).Switch();
}
private class PathInfo
{
public string Path { get; set; }

3
src/Markup/Avalonia.Markup.Xaml/Data/MultiBinding.cs

@ -53,7 +53,8 @@ namespace Avalonia.Markup.Xaml.Data
public InstancedBinding Initiate(
IAvaloniaObject target,
AvaloniaProperty targetProperty,
object anchor = null)
object anchor = null,
bool enableDataValidation = false)
{
if (Converter == null)
{

3
src/Markup/Avalonia.Markup.Xaml/Data/StyleResourceBinding.cs

@ -37,7 +37,8 @@ namespace Avalonia.Markup.Xaml.Data
public InstancedBinding Initiate(
IAvaloniaObject target,
AvaloniaProperty targetProperty,
object anchor = null)
object anchor = null,
bool enableDataValidation = false)
{
if (Name == "Red")
{

2
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs

@ -29,7 +29,6 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
Mode = Mode,
Path = Path,
Priority = Priority,
EnableValidation = EnableValidation,
};
}
@ -41,6 +40,5 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
public string Path { get; set; }
public BindingPriority Priority { get; set; } = BindingPriority.LocalValue;
public object Source { get; set; }
public bool EnableValidation { get; set; }
}
}

36
src/Markup/Avalonia.Markup.Xaml/Templates/MemberSelector.cs

@ -5,14 +5,13 @@ using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Markup.Data;
using System;
using System.Reactive.Linq;
namespace Avalonia.Markup.Xaml.Templates
{
public class MemberSelector : IMemberSelector
{
private ExpressionNode _expressionNode;
private string _memberName;
private ExpressionNode _memberValueNode;
public string MemberName
{
@ -22,8 +21,6 @@ namespace Avalonia.Markup.Xaml.Templates
if (_memberName != value)
{
_memberName = value;
_expressionNode = null;
_memberValueNode = null;
}
}
}
@ -35,34 +32,11 @@ namespace Avalonia.Markup.Xaml.Templates
return o;
}
if (_expressionNode == null)
{
_expressionNode = ExpressionNodeBuilder.Build(MemberName);
_memberValueNode = _expressionNode;
while (_memberValueNode.Next != null)
{
_memberValueNode = _memberValueNode.Next;
}
}
_expressionNode.Target = new WeakReference(o);
object result = _memberValueNode.CurrentValue.Target;
_expressionNode.Target = null;
if (result == AvaloniaProperty.UnsetValue)
{
return null;
}
else if (result is BindingError)
{
return null;
}
var expression = new ExpressionObserver(o, MemberName);
object result = AvaloniaProperty.UnsetValue;
return result;
expression.Subscribe(x => result = x);
return (result == AvaloniaProperty.UnsetValue || result is BindingNotification) ? null : result;
}
}
}

13
src/Markup/Avalonia.Markup/Avalonia.Markup.csproj

@ -41,14 +41,17 @@
<Compile Include="..\..\Shared\SharedAssemblyInfo.cs">
<Link>Properties\SharedAssemblyInfo.cs</Link>
</Compile>
<Compile Include="Data\MarkupBindingChainNullException.cs" />
<Compile Include="Data\CommonPropertyNames.cs" />
<Compile Include="Data\EmptyExpressionNode.cs" />
<Compile Include="Data\ExpressionNodeBuilder.cs" />
<Compile Include="Data\ExpressionParseException.cs" />
<Compile Include="Data\ExpressionSubject.cs" />
<Compile Include="Data\BindingExpression.cs" />
<Compile Include="ControlLocator.cs" />
<Compile Include="Data\Plugins\DataAnnotationsValidationPlugin.cs" />
<Compile Include="Data\Plugins\ExceptionValidationPlugin.cs" />
<Compile Include="Data\Plugins\IndeiValidationPlugin.cs" />
<Compile Include="Data\Plugins\IValidationPlugin.cs" />
<Compile Include="Data\Plugins\IDataValidationPlugin.cs" />
<Compile Include="Data\Plugins\AvaloniaPropertyAccessorPlugin.cs" />
<Compile Include="Data\Plugins\InpcPropertyAccessorPlugin.cs" />
<Compile Include="Data\Plugins\IPropertyAccessor.cs" />
@ -59,8 +62,12 @@
<Compile Include="Data\Parsers\IdentifierParser.cs" />
<Compile Include="Data\Parsers\ExpressionParser.cs" />
<Compile Include="Data\Parsers\Reader.cs" />
<Compile Include="Data\Plugins\ObservableValuePlugin.cs" />
<Compile Include="Data\Plugins\TaskValuePlugin.cs" />
<Compile Include="Data\Plugins\IValuePlugin.cs" />
<Compile Include="Data\Plugins\PropertyAccessorBase.cs" />
<Compile Include="Data\Plugins\PropertyError.cs" />
<Compile Include="Data\Plugins\ValidatingPropertyAccessorBase.cs" />
<Compile Include="Data\Plugins\DataValidatiorBase.cs" />
<Compile Include="Data\PropertyAccessorNode.cs" />
<Compile Include="Data\ExpressionNode.cs" />
<Compile Include="Data\ExpressionObserver.cs" />

305
src/Markup/Avalonia.Markup/Data/BindingExpression.cs

@ -0,0 +1,305 @@
// 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.Globalization;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using Avalonia.Data;
using Avalonia.Logging;
using Avalonia.Utilities;
namespace Avalonia.Markup.Data
{
/// <summary>
/// Binds to an expression on an object using a type value converter to convert the values
/// that are send and received.
/// </summary>
public class BindingExpression : ISubject<object>, IDescription
{
private readonly ExpressionObserver _inner;
private readonly Type _targetType;
private readonly object _fallbackValue;
private readonly BindingPriority _priority;
private readonly Subject<object> _errors = new Subject<object>();
/// <summary>
/// Initializes a new instance of the <see cref="ExpressionObserver"/> class.
/// </summary>
/// <param name="inner">The <see cref="ExpressionObserver"/>.</param>
/// <param name="targetType">The type to convert the value to.</param>
public BindingExpression(ExpressionObserver inner, Type targetType)
: this(inner, targetType, DefaultValueConverter.Instance)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ExpressionObserver"/> class.
/// </summary>
/// <param name="inner">The <see cref="ExpressionObserver"/>.</param>
/// <param name="targetType">The type to convert the value to.</param>
/// <param name="converter">The value converter to use.</param>
/// <param name="converterParameter">
/// A parameter to pass to <paramref name="converter"/>.
/// </param>
/// <param name="priority">The binding priority.</param>
public BindingExpression(
ExpressionObserver inner,
Type targetType,
IValueConverter converter,
object converterParameter = null,
BindingPriority priority = BindingPriority.LocalValue)
: this(inner, targetType, AvaloniaProperty.UnsetValue, converter, converterParameter, priority)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ExpressionObserver"/> class.
/// </summary>
/// <param name="inner">The <see cref="ExpressionObserver"/>.</param>
/// <param name="targetType">The type to convert the value to.</param>
/// <param name="fallbackValue">
/// The value to use when the binding is unable to produce a value.
/// </param>
/// <param name="converter">The value converter to use.</param>
/// <param name="converterParameter">
/// A parameter to pass to <paramref name="converter"/>.
/// </param>
/// <param name="priority">The binding priority.</param>
public BindingExpression(
ExpressionObserver inner,
Type targetType,
object fallbackValue,
IValueConverter converter,
object converterParameter = null,
BindingPriority priority = BindingPriority.LocalValue)
{
Contract.Requires<ArgumentNullException>(inner != null);
Contract.Requires<ArgumentNullException>(targetType != null);
Contract.Requires<ArgumentNullException>(converter != null);
_inner = inner;
_targetType = targetType;
Converter = converter;
ConverterParameter = converterParameter;
_fallbackValue = fallbackValue;
_priority = priority;
}
/// <summary>
/// Gets the converter to use on the expression.
/// </summary>
public IValueConverter Converter { get; }
/// <summary>
/// Gets a parameter to pass to <see cref="Converter"/>.
/// </summary>
public object ConverterParameter { get; }
/// <inheritdoc/>
string IDescription.Description => _inner.Expression;
/// <inheritdoc/>
public void OnCompleted()
{
}
/// <inheritdoc/>
public void OnError(Exception error)
{
}
/// <inheritdoc/>
public void OnNext(object value)
{
using (_inner.Subscribe(_ => { }))
{
var type = _inner.ResultType;
if (type != null)
{
var converted = Converter.ConvertBack(
value,
type,
ConverterParameter,
CultureInfo.CurrentUICulture);
if (converted == AvaloniaProperty.UnsetValue)
{
converted = TypeUtilities.Default(type);
_inner.SetValue(converted, _priority);
}
else if (converted is BindingNotification)
{
var notification = converted as BindingNotification;
if (notification.ErrorType == BindingErrorType.None)
{
throw new AvaloniaInternalException(
"IValueConverter should not return non-errored BindingNotification.");
}
_errors.OnNext(notification);
if (_fallbackValue != AvaloniaProperty.UnsetValue)
{
if (TypeUtilities.TryConvert(
type,
_fallbackValue,
CultureInfo.InvariantCulture,
out converted))
{
_inner.SetValue(converted, _priority);
}
else
{
Logger.Error(
LogArea.Binding,
this,
"Could not convert FallbackValue {FallbackValue} to {Type}",
_fallbackValue,
type);
}
}
}
else
{
_inner.SetValue(converted, _priority);
}
}
}
}
/// <inheritdoc/>
public IDisposable Subscribe(IObserver<object> observer)
{
return _inner.Select(ConvertValue).Merge(_errors).Subscribe(observer);
}
private object ConvertValue(object value)
{
var notification = value as BindingNotification;
if (notification == null)
{
var converted = Converter.Convert(
value,
_targetType,
ConverterParameter,
CultureInfo.CurrentUICulture);
notification = converted as BindingNotification;
if (notification?.ErrorType == BindingErrorType.None)
{
converted = notification.Value;
}
if (_fallbackValue != AvaloniaProperty.UnsetValue &&
(converted == AvaloniaProperty.UnsetValue || converted is BindingNotification))
{
var fallback = ConvertFallback();
converted = Merge(converted, fallback);
}
return converted;
}
else
{
return ConvertValue(notification);
}
}
private BindingNotification ConvertValue(BindingNotification notification)
{
if (notification.HasValue)
{
var converted = ConvertValue(notification.Value);
notification = Merge(notification, converted);
}
else if (_fallbackValue != AvaloniaProperty.UnsetValue)
{
var fallback = ConvertFallback();
notification = Merge(notification, fallback);
}
return notification;
}
private BindingNotification ConvertFallback()
{
object converted;
if (_fallbackValue == AvaloniaProperty.UnsetValue)
{
throw new AvaloniaInternalException("Cannot call ConvertFallback with no fallback value");
}
if (TypeUtilities.TryConvert(
_targetType,
_fallbackValue,
CultureInfo.InvariantCulture,
out converted))
{
return new BindingNotification(converted);
}
else
{
return new BindingNotification(
new InvalidCastException(
$"Could not convert FallbackValue '{_fallbackValue}' to '{_targetType}'"),
BindingErrorType.Error);
}
}
private static BindingNotification Merge(object a, BindingNotification b)
{
var an = a as BindingNotification;
if (an != null)
{
Merge(an, b);
return an;
}
else
{
return b;
}
}
private static BindingNotification Merge(BindingNotification a, object b)
{
var bn = b as BindingNotification;
if (bn != null)
{
Merge(a, bn);
}
else
{
a.SetValue(b);
}
return a;
}
private static BindingNotification Merge(BindingNotification a, BindingNotification b)
{
if (b.HasValue)
{
a.SetValue(b.Value);
}
else
{
a.ClearValue();
}
if (b.Error != null)
{
a.AddError(b.Error, b.ErrorType);
}
return a;
}
}
}

18
src/Markup/Avalonia.Markup/Data/EmptyExpressionNode.cs

@ -0,0 +1,18 @@
// 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.Linq;
namespace Avalonia.Markup.Data
{
internal class EmptyExpressionNode : ExpressionNode
{
public override string Description => ".";
protected override IObservable<object> StartListeningCore(WeakReference reference)
{
return Observable.Return(reference.Target);
}
}
}

188
src/Markup/Avalonia.Markup/Data/ExpressionNode.cs

@ -2,124 +2,188 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using Avalonia.Data;
namespace Avalonia.Markup.Data
{
internal abstract class ExpressionNode : IObservable<object>
internal abstract class ExpressionNode : ISubject<object>
{
protected static readonly WeakReference UnsetReference =
new WeakReference(AvaloniaProperty.UnsetValue);
private WeakReference _target;
private Subject<object> _subject;
private WeakReference _value = UnsetReference;
private WeakReference _target = UnsetReference;
private IDisposable _valueSubscription;
private IObserver<object> _observer;
private IDisposable _valuePluginSubscription;
public abstract string Description { get; }
public ExpressionNode Next { get; set; }
public WeakReference Target
{
get
{
return _target;
}
get { return _target; }
set
{
var newInstance = value?.Target;
var oldInstance = _target?.Target;
Contract.Requires<ArgumentNullException>(value != null);
if (!object.Equals(oldInstance, newInstance))
{
if (oldInstance != null)
{
Unsubscribe(oldInstance);
}
var oldTarget = _target?.Target;
var newTarget = value.Target;
var running = _valueSubscription != null;
if (!ReferenceEquals(oldTarget, newTarget))
{
_valueSubscription?.Dispose();
_valueSubscription = null;
_valuePluginSubscription?.Dispose();
_target = value;
if (newInstance != null)
if (running)
{
SubscribeAndUpdate(_target);
}
else
{
CurrentValue = UnsetReference;
}
if (Next != null)
{
Next.Target = _value;
_valueSubscription = StartListening();
}
}
}
}
public WeakReference CurrentValue
public IDisposable Subscribe(IObserver<object> observer)
{
get
if (_observer != null)
{
return _value;
throw new AvaloniaInternalException("ExpressionNode can only be subscribed once.");
}
set
_observer = observer;
var nextSubscription = Next?.Subscribe(this);
_valueSubscription = StartListening();
return Disposable.Create(() =>
{
_value = value;
_valueSubscription?.Dispose();
_valueSubscription = null;
_valuePluginSubscription?.Dispose();
_valuePluginSubscription = null;
nextSubscription?.Dispose();
_observer = null;
});
}
if (Next != null)
{
Next.Target = value;
}
void IObserver<object>.OnCompleted()
{
throw new AvaloniaInternalException("ExpressionNode.OnCompleted should not be called.");
}
_subject?.OnNext(value.Target);
}
void IObserver<object>.OnError(Exception error)
{
throw new AvaloniaInternalException("ExpressionNode.OnError should not be called.");
}
public virtual bool SetValue(object value, BindingPriority priority)
void IObserver<object>.OnNext(object value)
{
return Next?.SetValue(value, priority) ?? false;
NextValueChanged(value);
}
public virtual IDisposable Subscribe(IObserver<object> observer)
protected virtual IObservable<object> StartListeningCore(WeakReference reference)
{
if (Next != null)
return Observable.Return(reference.Target);
}
protected virtual void NextValueChanged(object value)
{
var bindingBroken = BindingNotification.ExtractError(value) as MarkupBindingChainNullException;
bindingBroken?.AddNode(Description);
_observer.OnNext(value);
}
private IDisposable StartListening()
{
var target = _target.Target;
IObservable<object> source;
if (target == null)
{
source = Observable.Return(TargetNullNotification());
}
else if (target == AvaloniaProperty.UnsetValue)
{
return Next.Subscribe(observer);
source = Observable.Empty<object>();
}
else
{
if (_subject == null)
{
_subject = new Subject<object>();
}
observer.OnNext(CurrentValue.Target);
return _subject.Subscribe(observer);
source = StartListeningCore(_target);
}
}
protected virtual void SubscribeAndUpdate(WeakReference reference)
{
CurrentValue = reference;
return source.Subscribe(TargetValueChanged);
}
protected virtual void SendValidationStatus(IValidationStatus status)
private void TargetValueChanged(object value)
{
//Even if elements only bound to sub-values, send validation changes along so they will be surfaced to the UI level.
if (_subject != null)
var notification = value as BindingNotification;
if (notification == null)
{
_subject.OnNext(status);
if (!HandleSpecialValue(value))
{
if (Next != null)
{
Next.Target = new WeakReference(value);
}
else
{
_observer.OnNext(value);
}
}
}
else
{
Next?.SendValidationStatus(status);
if (notification.Error != null)
{
_observer.OnNext(notification);
}
else if (notification.HasValue)
{
if (!HandleSpecialValue(notification.Value))
{
if (Next != null)
{
Next.Target = new WeakReference(notification.Value);
}
else
{
_observer.OnNext(value);
}
}
}
}
}
private bool HandleSpecialValue(object value)
{
if (_valuePluginSubscription == null)
{
var reference = new WeakReference(value);
foreach (var plugin in ExpressionObserver.ValueHandlers)
{
if (plugin.Match(reference))
{
_valuePluginSubscription = plugin.Start(reference)?.Subscribe(TargetValueChanged);
return true;
}
}
}
return false;
}
protected virtual void Unsubscribe(object target)
private BindingNotification TargetNullNotification()
{
return new BindingNotification(
new MarkupBindingChainNullException(),
BindingErrorType.Error,
AvaloniaProperty.UnsetValue);
}
}
}

277
src/Markup/Avalonia.Markup/Data/ExpressionObserver.cs

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using Avalonia.Data;
using Avalonia.Markup.Data.Plugins;
@ -31,41 +32,57 @@ namespace Avalonia.Markup.Data
/// An ordered collection of validation checker plugins that can be used to customize
/// the validation of view model and model data.
/// </summary>
public static readonly IList<IValidationPlugin> ValidationCheckers =
new List<IValidationPlugin>
public static readonly IList<IDataValidationPlugin> DataValidators =
new List<IDataValidationPlugin>
{
new DataAnnotationsValidationPlugin(),
new IndeiValidationPlugin(),
new ExceptionValidationPlugin(),
};
private readonly WeakReference _root;
private readonly Func<object> _rootGetter;
private readonly IObservable<object> _rootObservable;
private readonly IObservable<Unit> _update;
private IDisposable _rootObserverSubscription;
private IDisposable _updateSubscription;
private int _count;
/// <summary>
/// An ordered collection of value handlers that can be used to customize the handling
/// of certain values.
/// </summary>
public static readonly IList<IValuePlugin> ValueHandlers =
new List<IValuePlugin>
{
new TaskValuePlugin(),
new ObservableValuePlugin(),
};
private static readonly object UninitializedValue = new object();
private readonly ExpressionNode _node;
private bool _enableValidation;
private readonly Subject<Unit> _finished;
private readonly object _root;
private IObservable<object> _result;
/// <summary>
/// Initializes a new instance of the <see cref="ExpressionObserver"/> class.
/// </summary>
/// <param name="root">The root object.</param>
/// <param name="expression">The expression.</param>
/// <param name="enableValidation">Whether property validation should be enabled.</param>
public ExpressionObserver(object root, string expression, bool enableValidation = false)
/// <param name="enableDataValidation">Whether data validation should be enabled.</param>
/// <param name="description">
/// A description of the expression. If null, <paramref name="expression"/> will be used.
/// </param>
public ExpressionObserver(
object root,
string expression,
bool enableDataValidation = false,
string description = null)
{
Contract.Requires<ArgumentNullException>(expression != null);
_root = new WeakReference(root);
_enableValidation = enableValidation;
if (!string.IsNullOrWhiteSpace(expression))
if (root == AvaloniaProperty.UnsetValue)
{
_node = ExpressionNodeBuilder.Build(expression, enableValidation);
root = null;
}
Expression = expression;
Description = description ?? expression;
_node = Parse(expression, enableDataValidation);
_root = new WeakReference(root);
}
/// <summary>
@ -73,24 +90,24 @@ namespace Avalonia.Markup.Data
/// </summary>
/// <param name="rootObservable">An observable which provides the root object.</param>
/// <param name="expression">The expression.</param>
/// <param name="enableValidation">Whether property validation should be enabled.</param>
/// <param name="enableDataValidation">Whether data validation should be enabled.</param>
/// <param name="description">
/// A description of the expression. If null, <paramref name="expression"/> will be used.
/// </param>
public ExpressionObserver(
IObservable<object> rootObservable,
string expression,
bool enableValidation = false)
bool enableDataValidation = false,
string description = null)
{
Contract.Requires<ArgumentNullException>(rootObservable != null);
Contract.Requires<ArgumentNullException>(expression != null);
_rootObservable = rootObservable;
_enableValidation = enableValidation;
if (!string.IsNullOrWhiteSpace(expression))
{
_node = ExpressionNodeBuilder.Build(expression, enableValidation);
}
Expression = expression;
Description = description ?? expression;
_node = Parse(expression, enableDataValidation);
_finished = new Subject<Unit>();
_root = rootObservable;
}
/// <summary>
@ -99,27 +116,28 @@ namespace Avalonia.Markup.Data
/// <param name="rootGetter">A function which gets the root object.</param>
/// <param name="expression">The expression.</param>
/// <param name="update">An observable which triggers a re-read of the getter.</param>
/// <param name="enableValidation">Whether property validation should be enabled.</param>
/// <param name="enableDataValidation">Whether data validation should be enabled.</param>
/// <param name="description">
/// A description of the expression. If null, <paramref name="expression"/> will be used.
/// </param>
public ExpressionObserver(
Func<object> rootGetter,
string expression,
IObservable<Unit> update,
bool enableValidation = false)
bool enableDataValidation = false,
string description = null)
{
Contract.Requires<ArgumentNullException>(rootGetter != null);
Contract.Requires<ArgumentNullException>(expression != null);
Contract.Requires<ArgumentNullException>(update != null);
_rootGetter = rootGetter;
_update = update;
_enableValidation = enableValidation;
if (!string.IsNullOrWhiteSpace(expression))
{
_node = ExpressionNodeBuilder.Build(expression, enableValidation);
}
Expression = expression;
Description = description ?? expression;
_node = Parse(expression, enableDataValidation);
_finished = new Subject<Unit>();
_node.Target = new WeakReference(rootGetter());
_root = update.Select(x => rootGetter());
}
/// <summary>
@ -129,27 +147,20 @@ namespace Avalonia.Markup.Data
/// <param name="priority">The binding priority to use.</param>
/// <returns>
/// True if the value could be set; false if the expression does not evaluate to a
/// property.
/// property. Note that the <see cref="ExpressionObserver"/> must be subscribed to
/// before setting the target value can work, as setting the value requires the
/// expression to be evaluated.
/// </returns>
public bool SetValue(object value, BindingPriority priority = BindingPriority.LocalValue)
{
IncrementCount();
if (_rootGetter != null && _node != null)
{
_node.Target = new WeakReference(_rootGetter());
}
try
{
return _node?.SetValue(value, priority) ?? false;
}
finally
{
DecrementCount();
}
return (Leaf as PropertyAccessorNode)?.SetTargetValue(value, priority) ?? false;
}
/// <summary>
/// Gets a description of the expression being observed.
/// </summary>
public string Description { get; }
/// <summary>
/// Gets the expression being observed.
/// </summary>
@ -159,41 +170,7 @@ namespace Avalonia.Markup.Data
/// Gets the type of the expression result or null if the expression could not be
/// evaluated.
/// </summary>
public Type ResultType
{
get
{
IncrementCount();
try
{
if (_node != null)
{
return (Leaf as PropertyAccessorNode)?.PropertyType;
}
else if (_rootGetter != null)
{
return _rootGetter()?.GetType();
}
else
{
return _root.Target?.GetType();
}
}
finally
{
DecrementCount();
}
}
}
/// <inheritdoc/>
string IDescription.Description => Expression;
/// <summary>
/// Gets the root expression node. Used for testing.
/// </summary>
internal ExpressionNode Node => _node;
public Type ResultType => (Leaf as PropertyAccessorNode)?.PropertyType;
/// <summary>
/// Gets the leaf node.
@ -211,94 +188,88 @@ namespace Avalonia.Markup.Data
/// <inheritdoc/>
protected override IDisposable SubscribeCore(IObserver<object> observer)
{
IncrementCount();
if (_node != null)
if (_result == null)
{
IObservable<object> source = _node;
var source = (IObservable<object>)_node;
if (_rootObservable != null)
{
source = source.TakeUntil(_rootObservable.LastOrDefaultAsync());
}
else if (_update != null)
if (_finished != null)
{
source = source.TakeUntil(_update.LastOrDefaultAsync());
source = source.TakeUntil(_finished);
}
var subscription = source.Subscribe(observer);
return Disposable.Create(() =>
{
DecrementCount();
subscription.Dispose();
});
_result = Observable.Using(StartRoot, _ => source)
.Select(ToWeakReference)
.Publish(UninitializedValue)
.RefCount()
.Where(x => x != UninitializedValue)
.Select(Translate);
}
else if (_rootObservable != null)
return _result.Subscribe(observer);
}
private static ExpressionNode Parse(string expression, bool enableDataValidation)
{
if (!string.IsNullOrWhiteSpace(expression))
{
return _rootObservable.Subscribe(observer);
return ExpressionNodeBuilder.Build(expression, enableDataValidation);
}
else
{
if (_update == null)
{
return Observable.Never<object>()
.StartWith(_root.Target)
.Subscribe(observer);
}
else
{
return _update
.Select(_ => _rootGetter())
.StartWith(_rootGetter())
.Subscribe(observer);
}
return new EmptyExpressionNode();
}
}
private void IncrementCount()
private static object ToWeakReference(object o)
{
return o is BindingNotification ? o : new WeakReference(o);
}
private object Translate(object o)
{
if (_count++ == 0 && _node != null)
var weak = o as WeakReference;
if (weak != null)
{
if (_rootGetter != null)
{
_node.Target = new WeakReference(_rootGetter());
return weak.Target;
}
else
{
var broken = BindingNotification.ExtractError(o) as MarkupBindingChainNullException;
if (_update != null)
if (broken != null)
{
// We've received notification of a broken expression due to a null value
// somewhere in the chain. If this null value occurs at the first node then we
// ignore it, as its likely that e.g. the DataContext has not yet been set up.
if (broken.HasNodes)
{
_updateSubscription = _update.Subscribe(x =>
_node.Target = new WeakReference(_rootGetter()));
broken.Commit(Description);
}
else
{
o = AvaloniaProperty.UnsetValue;
}
}
else if (_rootObservable != null)
{
_rootObserverSubscription = _rootObservable.Subscribe(x =>
_node.Target = new WeakReference(x));
}
else
{
_node.Target = _root;
}
return o;
}
}
private void DecrementCount()
private IDisposable StartRoot()
{
if (--_count == 0 && _node != null)
{
if (_rootObserverSubscription != null)
{
_rootObserverSubscription.Dispose();
_rootObserverSubscription = null;
}
var observable = _root as IObservable<object>;
if (_updateSubscription != null)
{
_updateSubscription.Dispose();
_updateSubscription = null;
}
_node.Target = null;
if (observable != null)
{
return observable.Subscribe(
x => _node.Target = new WeakReference(x != AvaloniaProperty.UnsetValue ? x : null),
_ => _finished.OnNext(Unit.Default),
() => _finished.OnNext(Unit.Default));
}
else
{
_node.Target = (WeakReference)_root;
return Disposable.Empty;
}
}
}

213
src/Markup/Avalonia.Markup/Data/ExpressionSubject.cs

@ -1,213 +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.Globalization;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using Avalonia.Data;
using Avalonia.Logging;
using Avalonia.Utilities;
namespace Avalonia.Markup.Data
{
/// <summary>
/// Turns an <see cref="ExpressionObserver"/> into a subject that can be bound two-way with
/// a value converter.
/// </summary>
public class ExpressionSubject : ISubject<object>, IDescription
{
private readonly ExpressionObserver _inner;
private readonly Type _targetType;
private readonly object _fallbackValue;
private readonly BindingPriority _priority;
/// <summary>
/// Initializes a new instance of the <see cref="ExpressionObserver"/> class.
/// </summary>
/// <param name="inner">The <see cref="ExpressionObserver"/>.</param>
/// <param name="targetType">The type to convert the value to.</param>
public ExpressionSubject(ExpressionObserver inner, Type targetType)
: this(inner, targetType, DefaultValueConverter.Instance)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ExpressionObserver"/> class.
/// </summary>
/// <param name="inner">The <see cref="ExpressionObserver"/>.</param>
/// <param name="targetType">The type to convert the value to.</param>
/// <param name="converter">The value converter to use.</param>
/// <param name="converterParameter">
/// A parameter to pass to <paramref name="converter"/>.
/// </param>
/// <param name="priority">The binding priority.</param>
public ExpressionSubject(
ExpressionObserver inner,
Type targetType,
IValueConverter converter,
object converterParameter = null,
BindingPriority priority = BindingPriority.LocalValue)
: this(inner, targetType, AvaloniaProperty.UnsetValue, converter, converterParameter, priority)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ExpressionObserver"/> class.
/// </summary>
/// <param name="inner">The <see cref="ExpressionObserver"/>.</param>
/// <param name="targetType">The type to convert the value to.</param>
/// <param name="fallbackValue">
/// The value to use when the binding is unable to produce a value.
/// </param>
/// <param name="converter">The value converter to use.</param>
/// <param name="converterParameter">
/// A parameter to pass to <paramref name="converter"/>.
/// </param>
/// <param name="priority">The binding priority.</param>
public ExpressionSubject(
ExpressionObserver inner,
Type targetType,
object fallbackValue,
IValueConverter converter,
object converterParameter = null,
BindingPriority priority = BindingPriority.LocalValue)
{
Contract.Requires<ArgumentNullException>(inner != null);
Contract.Requires<ArgumentNullException>(targetType != null);
Contract.Requires<ArgumentNullException>(converter != null);
_inner = inner;
_targetType = targetType;
Converter = converter;
ConverterParameter = converterParameter;
_fallbackValue = fallbackValue;
_priority = priority;
}
/// <summary>
/// Gets the converter to use on the expression.
/// </summary>
public IValueConverter Converter { get; }
/// <summary>
/// Gets a parameter to pass to <see cref="Converter"/>.
/// </summary>
public object ConverterParameter { get; }
/// <inheritdoc/>
string IDescription.Description => _inner.Expression;
/// <inheritdoc/>
public void OnCompleted()
{
}
/// <inheritdoc/>
public void OnError(Exception error)
{
}
/// <inheritdoc/>
public void OnNext(object value)
{
var type = _inner.ResultType;
if (type != null)
{
var converted = Converter.ConvertBack(
value,
type,
ConverterParameter,
CultureInfo.CurrentUICulture);
if (converted == AvaloniaProperty.UnsetValue)
{
converted = TypeUtilities.Default(type);
_inner.SetValue(converted, _priority);
}
else if (converted is BindingError)
{
var error = converted as BindingError;
Logger.Error(
LogArea.Binding,
this,
"Error binding to {Expression}: {Message}",
_inner.Expression,
error.Exception.Message);
if (_fallbackValue != AvaloniaProperty.UnsetValue)
{
if (TypeUtilities.TryConvert(
type,
_fallbackValue,
CultureInfo.InvariantCulture,
out converted))
{
_inner.SetValue(converted, _priority);
}
else
{
Logger.Error(
LogArea.Binding,
this,
"Could not convert FallbackValue {FallbackValue} to {Type}",
_fallbackValue,
type);
}
}
}
else
{
_inner.SetValue(converted, _priority);
}
}
}
/// <inheritdoc/>
public IDisposable Subscribe(IObserver<object> observer)
{
return _inner.Select(ConvertValue).Subscribe(observer);
}
private object ConvertValue(object value)
{
var converted =
value as BindingError ??
value as IValidationStatus ??
Converter.Convert(
value,
_targetType,
ConverterParameter,
CultureInfo.CurrentUICulture);
if (_fallbackValue != AvaloniaProperty.UnsetValue &&
(converted == AvaloniaProperty.UnsetValue ||
converted is BindingError))
{
var error = converted as BindingError;
if (TypeUtilities.TryConvert(
_targetType,
_fallbackValue,
CultureInfo.InvariantCulture,
out converted))
{
if (error != null)
{
converted = new BindingError(error.Exception, converted);
}
}
else
{
converted = new BindingError(
new InvalidCastException(
$"Could not convert FallbackValue '{_fallbackValue}' to '{_targetType}'"));
}
}
return converted;
}
}
}

187
src/Markup/Avalonia.Markup/Data/IndexerNode.cs

@ -10,130 +10,49 @@ using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Reactive.Linq;
namespace Avalonia.Markup.Data
{
internal class IndexerNode : ExpressionNode,
IWeakSubscriber<NotifyCollectionChangedEventArgs>,
IWeakSubscriber<PropertyChangedEventArgs>
internal class IndexerNode : ExpressionNode
{
public IndexerNode(IList<string> arguments)
{
Arguments = arguments;
}
public IList<string> Arguments { get; }
public override string Description => "[" + string.Join(",", Arguments) + "]";
void IWeakSubscriber<NotifyCollectionChangedEventArgs>.OnEvent(object sender, NotifyCollectionChangedEventArgs e)
protected override IObservable<object> StartListeningCore(WeakReference reference)
{
var update = false;
if (sender is IList)
{
object indexObject;
if (!TypeUtilities.TryConvert(typeof(int), Arguments[0], CultureInfo.InvariantCulture, out indexObject))
{
return;
}
var index = (int)indexObject;
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
update = index >= e.NewStartingIndex;
break;
case NotifyCollectionChangedAction.Remove:
update = index >= e.OldStartingIndex;
break;
case NotifyCollectionChangedAction.Replace:
update = index >= e.NewStartingIndex &&
index < e.NewStartingIndex + e.NewItems.Count;
break;
case NotifyCollectionChangedAction.Move:
update = (index >= e.NewStartingIndex &&
index < e.NewStartingIndex + e.NewItems.Count) ||
(index >= e.OldStartingIndex &&
index < e.OldStartingIndex + e.OldItems.Count);
break;
case NotifyCollectionChangedAction.Reset:
update = true;
break;
}
}
else
{
update = true;
}
if (update)
{
CurrentValue = new WeakReference(GetValue(sender));
}
}
void IWeakSubscriber<PropertyChangedEventArgs>.OnEvent(object sender, PropertyChangedEventArgs e)
{
var typeInfo = sender.GetType().GetTypeInfo();
if (typeInfo.GetDeclaredProperty(e.PropertyName) == null)
{
return;
}
if (typeInfo.GetDeclaredProperty(e.PropertyName).GetIndexParameters().Any())
{
CurrentValue = new WeakReference(GetValue(sender));
}
}
protected override void SubscribeAndUpdate(WeakReference reference)
{
object target = reference.Target;
CurrentValue = new WeakReference(GetValue(target));
var target = reference.Target;
var incc = target as INotifyCollectionChanged;
if (incc != null)
{
WeakSubscriptionManager.Subscribe<NotifyCollectionChangedEventArgs>(
incc,
nameof(incc.CollectionChanged),
this);
}
var inpc = target as INotifyPropertyChanged;
if (inpc != null)
{
WeakSubscriptionManager.Subscribe<PropertyChangedEventArgs>(
inpc,
nameof(inpc.PropertyChanged),
this);
}
}
protected override void Unsubscribe(object target)
{
var incc = target as INotifyCollectionChanged;
var inputs = new List<IObservable<object>>();
if (incc != null)
{
WeakSubscriptionManager.Unsubscribe<NotifyCollectionChangedEventArgs>(
incc,
nameof(incc.CollectionChanged),
this);
inputs.Add(WeakObservable.FromEventPattern<NotifyCollectionChangedEventArgs>(
target,
nameof(incc.CollectionChanged))
.Where(x => ShouldUpdate(x.Sender, x.EventArgs))
.Select(_ => GetValue(target)));
}
var inpc = target as INotifyPropertyChanged;
if (inpc != null)
{
WeakSubscriptionManager.Unsubscribe<PropertyChangedEventArgs>(
inpc,
nameof(inpc.PropertyChanged),
this);
inputs.Add(WeakObservable.FromEventPattern<PropertyChangedEventArgs>(
target,
nameof(inpc.PropertyChanged))
.Where(x => ShouldUpdate(x.Sender, x.EventArgs))
.Select(_ => GetValue(target)));
}
return Observable.Merge(inputs).StartWith(GetValue(target));
}
public IList<string> Arguments { get; }
private object GetValue(object target)
{
var typeInfo = target.GetType().GetTypeInfo();
@ -141,18 +60,23 @@ namespace Avalonia.Markup.Data
var dictionary = target as IDictionary;
var indexerProperty = GetIndexer(typeInfo);
var indexerParameters = indexerProperty?.GetIndexParameters();
if (indexerProperty != null && indexerParameters.Length == Arguments.Count)
{
var convertedObjectArray = new object[indexerParameters.Length];
for (int i = 0; i < Arguments.Count; i++)
{
object temp = null;
if (!TypeUtilities.TryConvert(indexerParameters[i].ParameterType, Arguments[i], CultureInfo.InvariantCulture, out temp))
{
return AvaloniaProperty.UnsetValue;
}
convertedObjectArray[i] = temp;
}
var intArgs = convertedObjectArray.OfType<int>().ToArray();
// Try special cases where we can validate indicies
@ -166,16 +90,18 @@ namespace Avalonia.Markup.Data
{
if (intArgs.Length == Arguments.Count && intArgs[0] >= 0 && intArgs[0] < list.Count)
{
return list[intArgs[0]];
return list[intArgs[0]];
}
return AvaloniaProperty.UnsetValue;
}
else if (dictionary != null)
{
if (dictionary.Contains(convertedObjectArray[0]))
{
return dictionary[convertedObjectArray[0]];
return dictionary[convertedObjectArray[0]];
}
return AvaloniaProperty.UnsetValue;
}
else
@ -187,11 +113,11 @@ namespace Avalonia.Markup.Data
else
{
// Fallback to unchecked access
return indexerProperty.GetValue(target, convertedObjectArray);
return indexerProperty.GetValue(target, convertedObjectArray);
}
}
// Multidimensional arrays end up here because the indexer search picks up the IList indexer instead of the
// multidimensional indexer, which doesn't take the same number of arguments
// multidimensional indexer, which doesn't take the same number of arguments
else if (typeInfo.IsArray)
{
return GetValueFromArray((Array)target);
@ -220,13 +146,16 @@ namespace Avalonia.Markup.Data
private bool ConvertArgumentsToInts(out int[] intArgs)
{
intArgs = new int[Arguments.Count];
for (int i = 0; i < Arguments.Count; ++i)
{
object value;
if (!TypeUtilities.TryConvert(typeof(int), Arguments[i], CultureInfo.InvariantCulture, out value))
{
return false;
}
intArgs[i] = (int)value;
}
return true;
@ -235,7 +164,8 @@ namespace Avalonia.Markup.Data
private static PropertyInfo GetIndexer(TypeInfo typeInfo)
{
PropertyInfo indexer;
for (;typeInfo != null; typeInfo = typeInfo.BaseType?.GetTypeInfo())
for (; typeInfo != null; typeInfo = typeInfo.BaseType?.GetTypeInfo())
{
// Check for the default indexer name first to make this faster.
// This will only be false when a class in VB has a custom indexer name.
@ -243,14 +173,16 @@ namespace Avalonia.Markup.Data
{
return indexer;
}
foreach (var property in typeInfo.DeclaredProperties)
{
if (property.GetIndexParameters().Any())
{
return property;
}
}
}
}
return null;
}
@ -273,5 +205,46 @@ namespace Avalonia.Markup.Data
return false;
}
}
private bool ShouldUpdate(object sender, NotifyCollectionChangedEventArgs e)
{
if (sender is IList)
{
object indexObject;
if (!TypeUtilities.TryConvert(typeof(int), Arguments[0], CultureInfo.InvariantCulture, out indexObject))
{
return false;
}
var index = (int)indexObject;
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
return index >= e.NewStartingIndex;
case NotifyCollectionChangedAction.Remove:
return index >= e.OldStartingIndex;
case NotifyCollectionChangedAction.Replace:
return index >= e.NewStartingIndex &&
index < e.NewStartingIndex + e.NewItems.Count;
case NotifyCollectionChangedAction.Move:
return (index >= e.NewStartingIndex &&
index < e.NewStartingIndex + e.NewItems.Count) ||
(index >= e.OldStartingIndex &&
index < e.OldStartingIndex + e.OldItems.Count);
case NotifyCollectionChangedAction.Reset:
return true;
}
}
return false;
}
private bool ShouldUpdate(object sender, PropertyChangedEventArgs e)
{
var typeInfo = sender.GetType().GetTypeInfo();
return typeInfo.GetDeclaredProperty(e.PropertyName)?.GetIndexParameters().Any() ?? false;
}
}
}

28
src/Markup/Avalonia.Markup/Data/LogicalNotNode.cs

@ -3,21 +3,17 @@
using System;
using System.Globalization;
using System.Reactive.Linq;
using Avalonia.Data;
namespace Avalonia.Markup.Data
{
internal class LogicalNotNode : ExpressionNode
{
public override bool SetValue(object value, BindingPriority priority)
{
return false;
}
public override string Description => "!";
public override IDisposable Subscribe(IObserver<object> observer)
protected override void NextValueChanged(object value)
{
return Next.Select(Negate).Subscribe(observer);
base.NextValueChanged(Negate(value));
}
private static object Negate(object v)
@ -34,6 +30,12 @@ namespace Avalonia.Markup.Data
{
return !result;
}
else
{
return new BindingNotification(
new InvalidCastException($"Unable to convert '{s}' to bool."),
BindingErrorType.Error);
}
}
else
{
@ -42,9 +44,17 @@ namespace Avalonia.Markup.Data
var boolean = Convert.ToBoolean(v, CultureInfo.InvariantCulture);
return !boolean;
}
catch
catch (InvalidCastException)
{
// The error message here is "Unable to cast object of type 'System.Object'
// to type 'System.IConvertible'" which is kinda useless so provide our own.
return new BindingNotification(
new InvalidCastException($"Unable to convert '{v}' to bool."),
BindingErrorType.Error);
}
catch (Exception e)
{
// TODO: Maybe should log something here.
return new BindingNotification(e, BindingErrorType.Error);
}
}
}

33
src/Markup/Avalonia.Markup/Data/MarkupBindingChainNullException.cs

@ -0,0 +1,33 @@
using System.Collections.Generic;
using System.Linq;
using Avalonia.Data;
namespace Avalonia.Markup.Data
{
internal class MarkupBindingChainNullException : BindingChainNullException
{
private IList<string> _nodes = new List<string>();
public MarkupBindingChainNullException()
{
}
public MarkupBindingChainNullException(string expression, string expressionNullPoint)
: base(expression, expressionNullPoint)
{
_nodes = null;
}
public bool HasNodes => _nodes.Count > 0;
public void AddNode(string node) => _nodes.Add(node);
public void Commit(string expression)
{
Expression = expression;
ExpressionNullPoint = string.Join(".", _nodes.Reverse())
.Replace(".!", "!")
.Replace(".[", "[");
_nodes = null;
}
}
}

57
src/Markup/Avalonia.Markup/Data/Plugins/AvaloniaPropertyAccessorPlugin.cs

@ -4,7 +4,6 @@
using System;
using System.Reactive.Linq;
using Avalonia.Data;
using Avalonia.Logging;
namespace Avalonia.Markup.Data.Plugins
{
@ -13,36 +12,22 @@ namespace Avalonia.Markup.Data.Plugins
/// </summary>
public class AvaloniaPropertyAccessorPlugin : IPropertyAccessorPlugin
{
/// <summary>
/// Checks whether this plugin can handle accessing the properties of the specified object.
/// </summary>
/// <param name="reference">A weak reference to the object.</param>
/// <returns>True if the plugin can handle the object; otherwise false.</returns>
public bool Match(WeakReference reference)
{
Contract.Requires<ArgumentNullException>(reference != null);
return reference.Target is AvaloniaObject;
}
/// <inheritdoc/>
public bool Match(WeakReference reference) => reference.Target is AvaloniaObject;
/// <summary>
/// Starts monitoring the value of a property on an object.
/// </summary>
/// <param name="reference">A weak reference to the object.</param>
/// <param name="propertyName">The property name.</param>
/// <param name="changed">A function to call when the property changes.</param>
/// <returns>
/// An <see cref="IPropertyAccessor"/> interface through which future interactions with the
/// property will be made.
/// </returns>
public IPropertyAccessor Start(
WeakReference reference,
string propertyName,
Action<object> changed)
public IPropertyAccessor Start(WeakReference reference, string propertyName)
{
Contract.Requires<ArgumentNullException>(reference != null);
Contract.Requires<ArgumentNullException>(propertyName != null);
Contract.Requires<ArgumentNullException>(changed != null);
var instance = reference.Target;
var o = (AvaloniaObject)instance;
@ -50,13 +35,13 @@ namespace Avalonia.Markup.Data.Plugins
if (p != null)
{
return new Accessor(new WeakReference<AvaloniaObject>(o), p, changed);
return new Accessor(new WeakReference<AvaloniaObject>(o), p);
}
else if (instance != AvaloniaProperty.UnsetValue)
{
var message = $"Could not find AvaloniaProperty '{propertyName}' on '{instance}'";
var exception = new MissingMemberException(message);
return new PropertyError(new BindingError(exception));
return new PropertyError(new BindingNotification(exception, BindingErrorType.Error));
}
else
{
@ -64,23 +49,19 @@ namespace Avalonia.Markup.Data.Plugins
}
}
private class Accessor : IPropertyAccessor
private class Accessor : PropertyAccessorBase
{
private readonly WeakReference<AvaloniaObject> _reference;
private readonly AvaloniaProperty _property;
private IDisposable _subscription;
public Accessor(
WeakReference<AvaloniaObject> reference,
AvaloniaProperty property,
Action<object> changed)
public Accessor(WeakReference<AvaloniaObject> reference, AvaloniaProperty property)
{
Contract.Requires<ArgumentNullException>(reference != null);
Contract.Requires<ArgumentNullException>(property != null);
_reference = reference;
_property = property;
_subscription = Instance.GetWeakObservable(property).Skip(1).Subscribe(changed);
}
public AvaloniaObject Instance
@ -93,17 +74,10 @@ namespace Avalonia.Markup.Data.Plugins
}
}
public Type PropertyType => _property.PropertyType;
public object Value => Instance.GetValue(_property);
public override Type PropertyType => _property.PropertyType;
public override object Value => Instance?.GetValue(_property);
public void Dispose()
{
_subscription?.Dispose();
_subscription = null;
}
public bool SetValue(object value, BindingPriority priority)
public override bool SetValue(object value, BindingPriority priority)
{
if (!_property.IsReadOnly)
{
@ -113,6 +87,17 @@ namespace Avalonia.Markup.Data.Plugins
return false;
}
protected override void Dispose(bool disposing)
{
_subscription?.Dispose();
_subscription = null;
}
protected override void SubscribeCore(IObserver<object> observer)
{
_subscription = Instance.GetWeakObservable(_property).Subscribe(observer);
}
}
}
}

81
src/Markup/Avalonia.Markup/Data/Plugins/DataAnnotationsValidationPlugin.cs

@ -0,0 +1,81 @@
// 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.ComponentModel.DataAnnotations;
using System.Linq;
using System.Reflection;
using Avalonia.Data;
namespace Avalonia.Markup.Data.Plugins
{
/// <summary>
/// Validates properties on that have <see cref="ValidationAttribute"/>s.
/// </summary>
public class DataAnnotationsValidationPlugin : IDataValidationPlugin
{
/// <inheritdoc/>
public bool Match(WeakReference reference, string memberName)
{
return reference.Target?
.GetType()
.GetRuntimeProperty(memberName)?
.GetCustomAttributes<ValidationAttribute>()
.Any() ?? false;
}
/// <inheritdoc/>
public IPropertyAccessor Start(WeakReference reference, string name, IPropertyAccessor inner)
{
return new Accessor(reference, name, inner);
}
private class Accessor : DataValidatiorBase
{
private ValidationContext _context;
public Accessor(WeakReference reference, string name, IPropertyAccessor inner)
: base(inner)
{
_context = new ValidationContext(reference.Target);
_context.MemberName = name;
}
public override bool SetValue(object value, BindingPriority priority)
{
return base.SetValue(value, priority);
}
protected override void InnerValueChanged(object value)
{
var errors = new List<ValidationResult>();
if (Validator.TryValidateProperty(value, _context, errors))
{
base.InnerValueChanged(value);
}
else
{
base.InnerValueChanged(new BindingNotification(
CreateException(errors),
BindingErrorType.DataValidationError,
value));
}
}
private Exception CreateException(IList<ValidationResult> errors)
{
if (errors.Count == 1)
{
return new ValidationException(errors[0].ErrorMessage);
}
else
{
return new AggregateException(
errors.Select(x => new ValidationException(x.ErrorMessage)));
}
}
}
}
}

80
src/Markup/Avalonia.Markup/Data/Plugins/DataValidatiorBase.cs

@ -0,0 +1,80 @@
// 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.Markup.Data.Plugins
{
/// <summary>
/// Base class for data validators.
/// </summary>
/// <remarks>
/// Data validators are <see cref="IPropertyAccessor"/>s that are returned from an
/// <see cref="IDataValidationPlugin"/>. They wrap an inner <see cref="IPropertyAccessor"/>
/// and convert any values received from the inner property accessor into
/// <see cref="BindingNotification"/>s.
/// </remarks>
public abstract class DataValidatiorBase : PropertyAccessorBase, IObserver<object>
{
private readonly IPropertyAccessor _inner;
/// <summary>
/// Initializes a new instance of the <see cref="DataValidatiorBase"/> class.
/// </summary>
/// <param name="inner">The inner property accessor.</param>
protected DataValidatiorBase(IPropertyAccessor inner)
{
_inner = inner;
}
/// <inheritdoc/>
public override Type PropertyType => _inner.PropertyType;
/// <inheritdoc/>
public override object Value => _inner.Value;
/// <inheritdoc/>
public override bool SetValue(object value, BindingPriority priority) => _inner.SetValue(value, priority);
/// <summary>
/// Should never be called: the inner <see cref="IPropertyAccessor"/> should never notify
/// completion.
/// </summary>
void IObserver<object>.OnCompleted() { }
/// <summary>
/// Should never be called: the inner <see cref="IPropertyAccessor"/> should never notify
/// an error.
/// </summary>
void IObserver<object>.OnError(Exception error) { }
/// <summary>
/// Called when the inner <see cref="IPropertyAccessor"/> notifies with a new value.
/// </summary>
/// <param name="value">The value.</param>
void IObserver<object>.OnNext(object value) => InnerValueChanged(value);
/// <inheritdoc/>
protected override void Dispose(bool disposing) => _inner.Dispose();
/// <summary>
/// Begins listening to the inner <see cref="IPropertyAccessor"/>.
/// </summary>
protected override void SubscribeCore(IObserver<object> observer) => _inner.Subscribe(this);
/// <summary>
/// Called when the inner <see cref="IPropertyAccessor"/> notifies with a new value.
/// </summary>
/// <param name="value">The value.</param>
/// <remarks>
/// Notifies the observer that the value has changed. The value will be wrapped in a
/// <see cref="BindingNotification"/> if it is not already a binding notification.
/// </remarks>
protected virtual void InnerValueChanged(object value)
{
var notification = value as BindingNotification ?? new BindingNotification(value);
Observer.OnNext(notification);
}
}
}

44
src/Markup/Avalonia.Markup/Data/Plugins/ExceptionValidationPlugin.cs

@ -10,23 +10,21 @@ namespace Avalonia.Markup.Data.Plugins
/// <summary>
/// Validates properties that report errors by throwing exceptions.
/// </summary>
public class ExceptionValidationPlugin : IValidationPlugin
public class ExceptionValidationPlugin : IDataValidationPlugin
{
public static ExceptionValidationPlugin Instance { get; } = new ExceptionValidationPlugin();
/// <inheritdoc/>
public bool Match(WeakReference reference) => true;
public bool Match(WeakReference reference, string memberName) => true;
/// <inheritdoc/>
public IPropertyAccessor Start(WeakReference reference, string name, IPropertyAccessor accessor, Action<IValidationStatus> callback)
public IPropertyAccessor Start(WeakReference reference, string name, IPropertyAccessor inner)
{
return new ExceptionValidationChecker(reference, name, accessor, callback);
return new Validator(reference, name, inner);
}
private class ExceptionValidationChecker : ValidatingPropertyAccessorBase
private class Validator : DataValidatiorBase
{
public ExceptionValidationChecker(WeakReference reference, string name, IPropertyAccessor accessor, Action<IValidationStatus> callback)
: base(reference, name, accessor, callback)
public Validator(WeakReference reference, string name, IPropertyAccessor inner)
: base(inner)
{
}
@ -34,39 +32,19 @@ namespace Avalonia.Markup.Data.Plugins
{
try
{
var success = base.SetValue(value, priority);
SendValidationCallback(new ExceptionValidationStatus(null));
return success;
return base.SetValue(value, priority);
}
catch (TargetInvocationException ex)
{
SendValidationCallback(new ExceptionValidationStatus(ex.InnerException));
Observer.OnNext(new BindingNotification(ex.InnerException, BindingErrorType.DataValidationError));
}
catch (Exception ex)
{
SendValidationCallback(new ExceptionValidationStatus(ex));
Observer.OnNext(new BindingNotification(ex, BindingErrorType.DataValidationError));
}
return false;
}
}
/// <summary>
/// Describes the current validation status after setting a property value.
/// </summary>
public class ExceptionValidationStatus : IValidationStatus
{
internal ExceptionValidationStatus(Exception exception)
{
Exception = exception;
return false;
}
/// <summary>
/// The thrown exception. If there was no thrown exception, null.
/// </summary>
public Exception Exception { get; }
/// <inheritdoc/>
public bool IsValid => Exception == null;
}
}
}

37
src/Markup/Avalonia.Markup/Data/Plugins/IDataValidationPlugin.cs

@ -0,0 +1,37 @@
// 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.Markup.Data.Plugins
{
/// <summary>
/// Defines how data validation is observed by an <see cref="ExpressionObserver"/>.
/// </summary>
public interface IDataValidationPlugin
{
/// <summary>
/// Checks whether this plugin can handle data validation on the specified object.
/// </summary>
/// <param name="reference">A weak reference to the object.</param>
/// <param name="memberName">The name of the member to validate.</param>
/// <returns>True if the plugin can handle the object; otherwise false.</returns>
bool Match(WeakReference reference, string memberName);
/// <summary>
/// Starts monitoring the data validation state of a property on an object.
/// </summary>
/// <param name="reference">A weak reference to the object.</param>
/// <param name="propertyName">The property name.</param>
/// <param name="inner">The inner property accessor used to aceess the property.</param>
/// <returns>
/// An <see cref="IPropertyAccessor"/> interface through which future interactions with the
/// property will be made.
/// </returns>
IPropertyAccessor Start(
WeakReference reference,
string propertyName,
IPropertyAccessor inner);
}
}

5
src/Markup/Avalonia.Markup/Data/Plugins/IPropertyAccessor.cs

@ -10,11 +10,14 @@ namespace Avalonia.Markup.Data.Plugins
/// Defines an accessor to a property on an object returned by a
/// <see cref="IPropertyAccessorPlugin"/>
/// </summary>
public interface IPropertyAccessor : IDisposable
public interface IPropertyAccessor : IObservable<object>, IDisposable
{
/// <summary>
/// Gets the type of the property.
/// </summary>
/// <exception cref="InvalidOperationException">
/// The accessor has not been subscribed to yet.
/// </exception>
Type PropertyType { get; }
/// <summary>

5
src/Markup/Avalonia.Markup/Data/Plugins/IPropertyAccessorPlugin.cs

@ -2,7 +2,6 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections;
namespace Avalonia.Markup.Data.Plugins
{
@ -24,14 +23,12 @@ namespace Avalonia.Markup.Data.Plugins
/// </summary>
/// <param name="reference">A weak reference to the object.</param>
/// <param name="propertyName">The property name.</param>
/// <param name="changed">A function to call when the property changes.</param>
/// <returns>
/// An <see cref="IPropertyAccessor"/> interface through which future interactions with the
/// property will be made.
/// </returns>
IPropertyAccessor Start(
WeakReference reference,
string propertyName,
Action<object> changed);
string propertyName);
}
}

35
src/Markup/Avalonia.Markup/Data/Plugins/IValidationPlugin.cs

@ -1,35 +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;
namespace Avalonia.Markup.Data.Plugins
{
/// <summary>
/// Defines how view model data validation is observed by an <see cref="ExpressionObserver"/>.
/// </summary>
public interface IValidationPlugin
{
/// <summary>
/// Checks whether the data uses a validation scheme supported by this plugin.
/// </summary>
/// <param name="reference">A weak reference to the data.</param>
/// <returns><c>true</c> if this plugin can observe the validation; otherwise, <c>false</c>.</returns>
bool Match(WeakReference reference);
/// <summary>
/// Starts monitoring the validation state of an object for the given property.
/// </summary>
/// <param name="reference">A weak reference to the object.</param>
/// <param name="name">The property name.</param>
/// <param name="accessor">An underlying <see cref="IPropertyAccessor"/> to access the property.</param>
/// <param name="callback">A function to call when the validation state changes.</param>
/// <returns>
/// A <see cref="ValidatingPropertyAccessorBase"/> subclass through which future interactions with the
/// property will be made.
/// </returns>
IPropertyAccessor Start(WeakReference reference, string name, IPropertyAccessor accessor, Action<IValidationStatus> callback);
}
}

29
src/Markup/Avalonia.Markup/Data/Plugins/IValuePlugin.cs

@ -0,0 +1,29 @@
// 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;
namespace Avalonia.Markup.Data.Plugins
{
/// <summary>
/// Defines how values are observed by an <see cref="ExpressionObserver"/>.
/// </summary>
public interface IValuePlugin
{
/// <summary>
/// Checks whether this plugin handles the specified value.
/// </summary>
/// <param name="reference">A weak reference to the value.</param>
/// <returns>True if the plugin can handle the value; otherwise false.</returns>
bool Match(WeakReference reference);
/// <summary>
/// Starts producing output based on the specified value.
/// </summary>
/// <param name="reference">A weak reference to the object.</param>
/// <returns>
/// An observable that produces the output for the value.
/// </returns>
IObservable<object> Start(WeakReference reference);
}
}

107
src/Markup/Avalonia.Markup/Data/Plugins/IndeiValidationPlugin.cs

@ -2,7 +2,7 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using Avalonia.Data;
@ -13,79 +13,106 @@ namespace Avalonia.Markup.Data.Plugins
/// <summary>
/// Validates properties on objects that implement <see cref="INotifyDataErrorInfo"/>.
/// </summary>
public class IndeiValidationPlugin : IValidationPlugin
public class IndeiValidationPlugin : IDataValidationPlugin
{
/// <inheritdoc/>
public bool Match(WeakReference reference)
{
return reference.Target is INotifyDataErrorInfo;
}
public bool Match(WeakReference reference, string memberName) => reference.Target is INotifyDataErrorInfo;
/// <inheritdoc/>
public IPropertyAccessor Start(WeakReference reference, string name, IPropertyAccessor accessor, Action<IValidationStatus> callback)
public IPropertyAccessor Start(WeakReference reference, string name, IPropertyAccessor accessor)
{
return new IndeiValidationChecker(reference, name, accessor, callback);
return new Validator(reference, name, accessor);
}
private class IndeiValidationChecker : ValidatingPropertyAccessorBase, IWeakSubscriber<DataErrorsChangedEventArgs>
private class Validator : DataValidatiorBase, IWeakSubscriber<DataErrorsChangedEventArgs>
{
public IndeiValidationChecker(WeakReference reference, string name, IPropertyAccessor accessor, Action<IValidationStatus> callback)
: base(reference, name, accessor, callback)
WeakReference _reference;
string _name;
public Validator(WeakReference reference, string name, IPropertyAccessor inner)
: base(inner)
{
_reference = reference;
_name = name;
}
void IWeakSubscriber<DataErrorsChangedEventArgs>.OnEvent(object sender, DataErrorsChangedEventArgs e)
{
var target = reference.Target as INotifyDataErrorInfo;
if (e.PropertyName == _name || string.IsNullOrEmpty(e.PropertyName))
{
Observer.OnNext(CreateBindingNotification(Value));
}
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
var target = _reference.Target as INotifyDataErrorInfo;
if (target != null)
{
if (target.HasErrors)
{
SendValidationCallback(new IndeiValidationStatus(target.GetErrors(name)));
}
WeakSubscriptionManager.Subscribe(
WeakSubscriptionManager.Unsubscribe(
target,
nameof(target.ErrorsChanged),
this);
}
}
public override void Dispose()
protected override void SubscribeCore(IObserver<object> observer)
{
base.Dispose();
var target = _reference.Target as INotifyDataErrorInfo;
if (target != null)
{
WeakSubscriptionManager.Unsubscribe(
WeakSubscriptionManager.Subscribe(
target,
nameof(target.ErrorsChanged),
this);
}
base.SubscribeCore(observer);
}
public void OnEvent(object sender, DataErrorsChangedEventArgs e)
protected override void InnerValueChanged(object value)
{
if (e.PropertyName == _name || string.IsNullOrEmpty(e.PropertyName))
base.InnerValueChanged(CreateBindingNotification(value));
}
private BindingNotification CreateBindingNotification(object value)
{
var target = (INotifyDataErrorInfo)_reference.Target;
if (target != null)
{
var indei = _reference.Target as INotifyDataErrorInfo;
SendValidationCallback(new IndeiValidationStatus(indei.GetErrors(e.PropertyName)));
var errors = target.GetErrors(_name)?
.Cast<String>()
.Where(x => x != null).ToList();
if (errors?.Count > 0)
{
return new BindingNotification(
GenerateException(errors),
BindingErrorType.DataValidationError,
value);
}
}
return new BindingNotification(value);
}
}
/// <summary>
/// Describes the current validation status of a property as reported by an object that implements <see cref="INotifyDataErrorInfo"/>.
/// </summary>
public class IndeiValidationStatus : IValidationStatus
{
internal IndeiValidationStatus(IEnumerable errors)
private Exception GenerateException(IList<string> errors)
{
Errors = errors;
if (errors.Count == 1)
{
return new Exception(errors[0]);
}
else
{
return new AggregateException(
errors.Select(x => new Exception(x)));
}
}
/// <inheritdoc/>
public bool IsValid => !Errors?.OfType<object>().Any() ?? true;
/// <summary>
/// The errors on the given property and on the object as a whole.
/// </summary>
public IEnumerable Errors { get; }
}
}
}

115
src/Markup/Avalonia.Markup/Data/Plugins/InpcPropertyAccessorPlugin.cs

@ -9,7 +9,6 @@ using System.Reflection;
using Avalonia.Data;
using Avalonia.Logging;
using Avalonia.Utilities;
using System.Collections;
namespace Avalonia.Markup.Data.Plugins
{
@ -19,124 +18,130 @@ namespace Avalonia.Markup.Data.Plugins
/// </summary>
public class InpcPropertyAccessorPlugin : IPropertyAccessorPlugin
{
/// <summary>
/// Checks whether this plugin can handle accessing the properties of the specified object.
/// </summary>
/// <param name="reference">The object.</param>
/// <returns>True if the plugin can handle the object; otherwise false.</returns>
public bool Match(WeakReference reference)
{
Contract.Requires<ArgumentNullException>(reference != null);
return true;
}
/// <inheritdoc/>
public bool Match(WeakReference reference) => true;
/// <summary>
/// Starts monitoring the value of a property on an object.
/// </summary>
/// <param name="reference">The object.</param>
/// <param name="propertyName">The property name.</param>
/// <param name="changed">A function to call when the property changes.</param>
/// <returns>
/// An <see cref="IPropertyAccessor"/> interface through which future interactions with the
/// property will be made.
/// </returns>
public IPropertyAccessor Start(
WeakReference reference,
string propertyName,
Action<object> changed)
public IPropertyAccessor Start(WeakReference reference, string propertyName)
{
Contract.Requires<ArgumentNullException>(reference != null);
Contract.Requires<ArgumentNullException>(propertyName != null);
Contract.Requires<ArgumentNullException>(changed != null);
var instance = reference.Target;
var p = instance.GetType().GetRuntimeProperties().FirstOrDefault(_ => _.Name == propertyName);
if (p != null)
{
return new Accessor(reference, p, changed);
return new Accessor(reference, p);
}
else
{
var message = $"Could not find CLR property '{propertyName}' on '{instance}'";
var exception = new MissingMemberException(message);
return new PropertyError(new BindingError(exception));
return new PropertyError(new BindingNotification(exception, BindingErrorType.Error));
}
}
private class Accessor : IPropertyAccessor, IWeakSubscriber<PropertyChangedEventArgs>
private class Accessor : PropertyAccessorBase, IWeakSubscriber<PropertyChangedEventArgs>
{
private readonly WeakReference _reference;
private readonly PropertyInfo _property;
private readonly Action<object> _changed;
private bool _eventRaised;
public Accessor(
WeakReference reference,
PropertyInfo property,
Action<object> changed)
public Accessor(WeakReference reference, PropertyInfo property)
{
Contract.Requires<ArgumentNullException>(reference != null);
Contract.Requires<ArgumentNullException>(property != null);
_reference = reference;
_property = property;
_changed = changed;
}
var inpc = reference.Target as INotifyPropertyChanged;
public override Type PropertyType => _property.PropertyType;
if (inpc != null)
public override object Value
{
get
{
WeakSubscriptionManager.Subscribe<PropertyChangedEventArgs>(
inpc,
nameof(inpc.PropertyChanged),
this);
var o = _reference.Target;
return (o != null) ? _property.GetValue(o) : null;
}
else
}
public override bool SetValue(object value, BindingPriority priority)
{
if (_property.CanWrite)
{
Logger.Warning(
LogArea.Binding,
this,
"Bound to property {Property} on {Source} which does not implement INotifyPropertyChanged",
property.Name,
reference.Target,
reference.Target.GetType());
_eventRaised = false;
_property.SetValue(_reference.Target, value);
if (!_eventRaised)
{
SendCurrentValue();
}
return true;
}
}
public Type PropertyType => _property.PropertyType;
return false;
}
public object Value => _property.GetValue(_reference.Target);
void IWeakSubscriber<PropertyChangedEventArgs>.OnEvent(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == _property.Name || string.IsNullOrEmpty(e.PropertyName))
{
_eventRaised = true;
SendCurrentValue();
}
}
public void Dispose()
protected override void Dispose(bool disposing)
{
var inpc = _reference.Target as INotifyPropertyChanged;
if (inpc != null)
{
WeakSubscriptionManager.Unsubscribe<PropertyChangedEventArgs>(
WeakSubscriptionManager.Unsubscribe(
inpc,
nameof(inpc.PropertyChanged),
this);
}
}
public bool SetValue(object value, BindingPriority priority)
protected override void SubscribeCore(IObserver<object> observer)
{
if (_property.CanWrite)
SendCurrentValue();
SubscribeToChanges();
}
private void SendCurrentValue()
{
try
{
_property.SetValue(_reference.Target, value);
return true;
var value = Value;
Observer.OnNext(value);
}
return false;
catch { }
}
void IWeakSubscriber<PropertyChangedEventArgs>.OnEvent(object sender, PropertyChangedEventArgs e)
private void SubscribeToChanges()
{
if (e.PropertyName == _property.Name || string.IsNullOrEmpty(e.PropertyName))
var inpc = _reference.Target as INotifyPropertyChanged;
if (inpc != null)
{
_changed(Value);
WeakSubscriptionManager.Subscribe<PropertyChangedEventArgs>(
inpc,
nameof(inpc.PropertyChanged),
this);
}
}
}

44
src/Markup/Avalonia.Markup/Data/Plugins/ObservableValuePlugin.cs

@ -0,0 +1,44 @@
// 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.Linq;
using System.Reactive.Subjects;
using System.Reflection;
using System.Threading.Tasks;
using System.Windows.Input;
using Avalonia.Data;
namespace Avalonia.Markup.Data.Plugins
{
/// <summary>
/// Handles binding to <see cref="IObservable{T}"/>s in an <see cref="ExpressionObserver"/>.
/// </summary>
public class ObservableValuePlugin : IValuePlugin
{
/// <summary>
/// Checks whether this plugin handles the specified value.
/// </summary>
/// <param name="reference">A weak reference to the value.</param>
/// <returns>True if the plugin can handle the value; otherwise false.</returns>
public virtual bool Match(WeakReference reference)
{
var target = reference.Target;
// ReactiveCommand is an IObservable but we want to bind to it, not its value.
return target is IObservable<object> && !(target is ICommand);
}
/// <summary>
/// Starts producing output based on the specified value.
/// </summary>
/// <param name="reference">A weak reference to the object.</param>
/// <returns>
/// An observable that produces the output for the value.
/// </returns>
public virtual IObservable<object> Start(WeakReference reference)
{
return reference.Target as IObservable<object>;
}
}
}

68
src/Markup/Avalonia.Markup/Data/Plugins/PropertyAccessorBase.cs

@ -0,0 +1,68 @@
// 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.Markup.Data.Plugins
{
/// <summary>
/// Defines a default base implementation for a <see cref="IPropertyAccessor"/>.
/// </summary>
/// <remarks>
/// <see cref="IPropertyAccessor"/> is an observable that will only be subscribed to one time.
/// In addition, the subscription can be disposed by calling <see cref="Dispose()"/> on the
/// property accessor itself - this prevents needing to hold two references for a subscription.
/// </remarks>
public abstract class PropertyAccessorBase : IPropertyAccessor
{
/// <inheritdoc/>
public abstract Type PropertyType { get; }
/// <inheritdoc/>
public abstract object Value { get; }
/// <summary>
/// Stops the subscription.
/// </summary>
public void Dispose() => Dispose(true);
/// <inheritdoc/>
public abstract bool SetValue(object value, BindingPriority priority);
/// <summary>
/// The currently subscribed observer.
/// </summary>
protected IObserver<object> Observer { get; private set; }
/// <inheritdoc/>
public IDisposable Subscribe(IObserver<object> observer)
{
Contract.Requires<ArgumentNullException>(observer != null);
if (Observer != null)
{
throw new InvalidOperationException(
"A property accessor can be subscribed to only once.");
}
Observer = observer;
SubscribeCore(observer);
return this;
}
/// <summary>
/// Stops listening to the property.
/// </summary>
/// <param name="disposing">
/// True if the <see cref="Dispose()"/> method was called, false if the object is being
/// finalized.
/// </param>
protected virtual void Dispose(bool disposing) => Observer = null;
/// <summary>
/// When overridden in a derived class, begins listening to the property.
/// </summary>
protected abstract void SubscribeCore(IObserver<object> observer);
}
}

11
src/Markup/Avalonia.Markup/Data/Plugins/PropertyError.cs

@ -1,4 +1,5 @@
using System;
using System.Reactive.Disposables;
using Avalonia.Data;
namespace Avalonia.Markup.Data.Plugins
@ -8,13 +9,13 @@ namespace Avalonia.Markup.Data.Plugins
/// </summary>
public class PropertyError : IPropertyAccessor
{
private BindingError _error;
private BindingNotification _error;
/// <summary>
/// Initializes a new instance of the <see cref="PropertyError"/> class.
/// </summary>
/// <param name="error">The error to report.</param>
public PropertyError(BindingError error)
public PropertyError(BindingNotification error)
{
_error = error;
}
@ -35,5 +36,11 @@ namespace Avalonia.Markup.Data.Plugins
{
return false;
}
public IDisposable Subscribe(IObserver<object> observer)
{
observer.OnNext(_error);
return Disposable.Empty;
}
}
}

82
src/Markup/Avalonia.Markup/Data/Plugins/TaskValuePlugin.cs

@ -0,0 +1,82 @@
// 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.Concurrency;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using System.Reflection;
using System.Threading.Tasks;
using Avalonia.Data;
namespace Avalonia.Markup.Data.Plugins
{
/// <summary>
/// Handles binding to <see cref="Task"/>s in an <see cref="ExpressionObserver"/>.
/// </summary>
public class TaskValuePlugin : IValuePlugin
{
/// <summary>
/// Checks whether this plugin handles the specified value.
/// </summary>
/// <param name="reference">A weak reference to the value.</param>
/// <returns>True if the plugin can handle the value; otherwise false.</returns>
public virtual bool Match(WeakReference reference) => reference.Target is Task;
/// <summary>
/// Starts producing output based on the specified value.
/// </summary>
/// <param name="reference">A weak reference to the object.</param>
/// <returns>
/// An observable that produces the output for the value.
/// </returns>
public virtual IObservable<object> Start(WeakReference reference)
{
var task = reference.Target as Task;
if (task != null)
{
var resultProperty = task.GetType().GetTypeInfo().GetDeclaredProperty("Result");
if (resultProperty != null)
{
switch (task.Status)
{
case TaskStatus.RanToCompletion:
case TaskStatus.Faulted:
return HandleCompleted(task);
default:
var subject = new Subject<object>();
task.ContinueWith(
x => HandleCompleted(task).Subscribe(subject),
TaskScheduler.FromCurrentSynchronizationContext())
.ConfigureAwait(false);
return subject;
}
}
}
return Observable.Empty<object>();
}
protected IObservable<object> HandleCompleted(Task task)
{
var resultProperty = task.GetType().GetTypeInfo().GetDeclaredProperty("Result");
if (resultProperty != null)
{
switch (task.Status)
{
case TaskStatus.RanToCompletion:
return Observable.Return(resultProperty.GetValue(task));
case TaskStatus.Faulted:
return Observable.Return(new BindingNotification(task.Exception, BindingErrorType.Error));
default:
throw new AvaloniaInternalException("HandleCompleted called for non-completed Task.");
}
}
return Observable.Empty<object>();
}
}
}

46
src/Markup/Avalonia.Markup/Data/Plugins/ValidatingPropertyAccessorBase.cs

@ -1,46 +0,0 @@
using System;
using Avalonia.Data;
namespace Avalonia.Markup.Data.Plugins
{
/// <summary>
/// A base class for validating <see cref="IPropertyAccessor"/>s that wraps an <see cref="IPropertyAccessor"/> and forwards method calls to it.
/// </summary>
public abstract class ValidatingPropertyAccessorBase : IPropertyAccessor
{
protected readonly WeakReference _reference;
protected readonly string _name;
private readonly IPropertyAccessor _accessor;
private readonly Action<IValidationStatus> _callback;
protected ValidatingPropertyAccessorBase(WeakReference reference, string name, IPropertyAccessor accessor, Action<IValidationStatus> callback)
{
_reference = reference;
_name = name;
_accessor = accessor;
_callback = callback;
}
/// <inheritdoc/>
public Type PropertyType => _accessor.PropertyType;
/// <inheritdoc/>
public object Value => _accessor.Value;
/// <inheritdoc/>
public virtual void Dispose() => _accessor.Dispose();
/// <inheritdoc/>
public virtual bool SetValue(object value, BindingPriority priority) => _accessor.SetValue(value, priority);
/// <summary>
/// Sends the validation status to the callback specified in construction.
/// </summary>
/// <param name="status">The validation status.</param>
protected void SendValidationCallback(IValidationStatus status)
{
_callback?.Invoke(status);
}
}
}

120
src/Markup/Avalonia.Markup/Data/PropertyAccessorNode.cs

@ -3,11 +3,8 @@
using System;
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Input;
using Avalonia.Data;
using Avalonia.Markup.Data.Plugins;
@ -15,9 +12,8 @@ namespace Avalonia.Markup.Data
{
internal class PropertyAccessorNode : ExpressionNode
{
private readonly bool _enableValidation;
private IPropertyAccessor _accessor;
private IDisposable _subscription;
private bool _enableValidation;
public PropertyAccessorNode(string propertyName, bool enableValidation)
{
@ -25,118 +21,44 @@ namespace Avalonia.Markup.Data
_enableValidation = enableValidation;
}
public override string Description => PropertyName;
public string PropertyName { get; }
public Type PropertyType => _accessor?.PropertyType;
public override bool SetValue(object value, BindingPriority priority)
public bool SetTargetValue(object value, BindingPriority priority)
{
if (Next != null)
if (_accessor != null)
{
return Next.SetValue(value, priority);
try { return _accessor.SetValue(value, priority); } catch { }
}
else
{
if (_accessor != null)
{
return _accessor.SetValue(value, priority);
}
return false;
}
return false;
}
protected override void SubscribeAndUpdate(WeakReference reference)
protected override IObservable<object> StartListeningCore(WeakReference reference)
{
var instance = reference.Target;
var plugin = ExpressionObserver.PropertyAccessors.FirstOrDefault(x => x.Match(reference));
var accessor = plugin?.Start(reference, PropertyName);
if (instance != null && instance != AvaloniaProperty.UnsetValue)
if (_enableValidation && Next == null)
{
var accessorPlugin = ExpressionObserver.PropertyAccessors.FirstOrDefault(x => x.Match(reference));
if (accessorPlugin != null)
foreach (var validator in ExpressionObserver.DataValidators)
{
_accessor = ExceptionValidationPlugin.Instance.Start(
reference,
PropertyName,
accessorPlugin.Start(reference, PropertyName, SetCurrentValue),
SendValidationStatus);
if (_enableValidation)
{
foreach (var validationPlugin in ExpressionObserver.ValidationCheckers)
{
if (validationPlugin.Match(reference))
{
_accessor = validationPlugin.Start(reference, PropertyName, _accessor, SendValidationStatus);
}
}
}
if (_accessor != null)
if (validator.Match(reference, PropertyName))
{
SetCurrentValue(_accessor.Value);
return;
accessor = validator.Start(reference, PropertyName, accessor);
}
}
}
CurrentValue = UnsetReference;
}
protected override void Unsubscribe(object target)
{
_accessor?.Dispose();
_accessor = null;
}
private void SetCurrentValue(object value)
{
var observable = value as IObservable<object>;
var command = value as ICommand;
var task = value as Task;
bool set = false;
// HACK: ReactiveCommand is an IObservable but we want to bind to it, not its value.
// We may need to make this a more general solution.
if (observable != null && command == null)
{
CurrentValue = UnsetReference;
set = true;
_subscription = observable
.ObserveOn(SynchronizationContext.Current)
.Subscribe(x => CurrentValue = new WeakReference(x));
}
else if (task != null)
{
var resultProperty = task.GetType().GetTypeInfo().GetDeclaredProperty("Result");
if (resultProperty != null)
// Ensure that _accessor is set for the duration of the subscription.
return Observable.Using(
() =>
{
if (task.Status == TaskStatus.RanToCompletion)
{
CurrentValue = new WeakReference(resultProperty.GetValue(task));
set = true;
}
else
{
task.ContinueWith(
x => CurrentValue = new WeakReference(resultProperty.GetValue(task)),
TaskScheduler.FromCurrentSynchronizationContext())
.ConfigureAwait(false);
}
}
}
else
{
CurrentValue = new WeakReference(value);
set = true;
}
if (!set)
{
CurrentValue = UnsetReference;
}
_accessor = accessor;
return Disposable.Create(() => _accessor = null);
},
_ => accessor);
}
}
}

14
src/Markup/Avalonia.Markup/DefaultValueConverter.cs

@ -43,8 +43,18 @@ namespace Avalonia.Markup
if (value != null)
{
var message = $"Could not convert '{value}' to '{targetType}'";
return new BindingError(new InvalidCastException(message));
string message;
if (TypeUtilities.IsNumeric(targetType))
{
message = $"'{value}' is not a valid number.";
}
else
{
message = $"Could not convert '{value}' to '{targetType.Name}'.";
}
return new BindingNotification(new InvalidCastException(message), BindingErrorType.Error);
}
return AvaloniaProperty.UnsetValue;

9
src/Markup/Avalonia.Markup/IValueConverter.cs

@ -3,6 +3,7 @@
using System;
using System.Globalization;
using Avalonia.Data;
namespace Avalonia.Markup
{
@ -21,8 +22,8 @@ namespace Avalonia.Markup
/// <returns>The converted value.</returns>
/// <remarks>
/// This method should not throw exceptions. If the value is not convertible, return
/// <see cref="AvaloniaProperty.UnsetValue"/>. Any exception thrown will be treated as
/// an application exception.
/// a <see cref="BindingNotification"/> in an error state. Any exceptions thrown will be
/// treated as an application exception.
/// </remarks>
object Convert(object value, Type targetType, object parameter, CultureInfo culture);
@ -36,8 +37,8 @@ namespace Avalonia.Markup
/// <returns>The converted value.</returns>
/// <remarks>
/// This method should not throw exceptions. If the value is not convertible, return
/// <see cref="AvaloniaProperty.UnsetValue"/>. Any exception thrown will be treated as
/// an application exception.
/// a <see cref="BindingNotification"/> in an error state. Any exceptions thrown will be
/// treated as an application exception.
/// </remarks>
object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture);
}

1
src/Markup/Avalonia.Markup/Properties/AssemblyInfo.cs

@ -8,4 +8,3 @@ using System.Runtime.CompilerServices;
[assembly: AssemblyTitle("Avalonia.Markup")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Markup")]
[assembly: InternalsVisibleTo("Avalonia.Markup.UnitTests")]
[assembly: InternalsVisibleTo("Avalonia.Markup.Xaml")]

1
src/Markup/Avalonia.Markup/packages.config

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="System.ComponentModel.Annotations" version="4.1.0" targetFramework="portable45-net45+win8" />
<package id="System.Reactive" version="3.0.0" targetFramework="portable45-net45+win8" />
<package id="System.Reactive.Core" version="3.0.0" targetFramework="portable45-net45+win8" />
<package id="System.Reactive.Interfaces" version="3.0.0" targetFramework="portable45-net45+win8" />

1
tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj

@ -90,6 +90,7 @@
<Otherwise />
</Choose>
<ItemGroup>
<Compile Include="AvaloniaObjectTests_DataValidation.cs" />
<Compile Include="Collections\CollectionChangedTracker.cs" />
<Compile Include="Collections\AvaloniaDictionaryTests.cs" />
<Compile Include="Collections\AvaloniaListTests.cs" />

7
tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.v2.ncrunchproject

@ -17,10 +17,11 @@
<DetectStackOverflow>true</DetectStackOverflow>
<IncludeStaticReferencesInWorkspace>true</IncludeStaticReferencesInWorkspace>
<DefaultTestTimeout>60000</DefaultTestTimeout>
<UseBuildConfiguration />
<UseBuildPlatform />
<ProxyProcessPath />
<UseBuildConfiguration></UseBuildConfiguration>
<UseBuildPlatform></UseBuildPlatform>
<ProxyProcessPath></ProxyProcessPath>
<UseCPUArchitecture>AutoDetect</UseCPUArchitecture>
<MSTestThreadApartmentState>STA</MSTestThreadApartmentState>
<BuildProcessArchitecture>x86</BuildProcessArchitecture>
<HiddenWarnings>LongTestTimesWithoutParallelExecution</HiddenWarnings>
</ProjectConfiguration>

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

@ -54,6 +54,36 @@ namespace Avalonia.Base.UnitTests
Assert.False(target.IsSet(Class1.QuxProperty));
}
[Fact]
public void OneTime_Binding_Ignores_UnsetValue()
{
var target = new Class1();
var source = new Subject<object>();
target.Bind(Class1.QuxProperty, new TestOneTimeBinding(source));
source.OnNext(AvaloniaProperty.UnsetValue);
Assert.Equal(5.6, target.GetValue(Class1.QuxProperty));
source.OnNext(6.7);
Assert.Equal(6.7, target.GetValue(Class1.QuxProperty));
}
[Fact]
public void OneTime_Binding_Ignores_Binding_Errors()
{
var target = new Class1();
var source = new Subject<object>();
target.Bind(Class1.QuxProperty, new TestOneTimeBinding(source));
source.OnNext(new BindingNotification(new Exception(), BindingErrorType.Error));
Assert.Equal(5.6, target.GetValue(Class1.QuxProperty));
source.OnNext(6.7);
Assert.Equal(6.7, target.GetValue(Class1.QuxProperty));
}
[Fact]
public void Bind_Throws_Exception_For_Unregistered_Property()
{
@ -273,31 +303,36 @@ namespace Avalonia.Base.UnitTests
target.Bind(Class1.QuxProperty, source);
source.OnNext(6.7);
source.OnNext(new BindingError(new InvalidOperationException("Foo")));
source.OnNext(new BindingNotification(
new InvalidOperationException("Foo"),
BindingErrorType.Error));
Assert.Equal(6.7, target.GetValue(Class1.QuxProperty));
}
[Fact]
public void BindingError_With_FallbackValue_Causes_Target_Update()
public void BindingNotification_With_FallbackValue_Causes_Target_Update()
{
var target = new Class1();
var source = new Subject<object>();
target.Bind(Class1.QuxProperty, source);
source.OnNext(6.7);
source.OnNext(new BindingError(new InvalidOperationException("Foo"), 8.9));
source.OnNext(new BindingNotification(
new InvalidOperationException("Foo"),
BindingErrorType.Error,
8.9));
Assert.Equal(8.9, target.GetValue(Class1.QuxProperty));
}
[Fact]
public void Bind_Logs_BindingError()
public void Bind_Logs_Binding_Error()
{
var target = new Class1();
var source = new Subject<object>();
var called = false;
var expectedMessageTemplate = "Error binding to {Target}.{Property}: {Message}";
var expectedMessageTemplate = "Error in binding to {Target}.{Property}: {Message}";
LogCallback checkLogMessage = (level, area, src, mt, pv) =>
{
@ -313,7 +348,9 @@ namespace Avalonia.Base.UnitTests
{
target.Bind(Class1.QuxProperty, source);
source.OnNext(6.7);
source.OnNext(new BindingError(new InvalidOperationException("Foo")));
source.OnNext(new BindingNotification(
new InvalidOperationException("Foo"),
BindingErrorType.Error));
Assert.Equal(6.7, target.GetValue(Class1.QuxProperty));
Assert.True(called);
@ -345,5 +382,24 @@ namespace Avalonia.Base.UnitTests
public static readonly StyledProperty<string> BarProperty =
AvaloniaProperty.Register<Class2, string>("Bar", "bardefault");
}
private class TestOneTimeBinding : IBinding
{
private IObservable<object> _source;
public TestOneTimeBinding(IObservable<object> source)
{
_source = source;
}
public InstancedBinding Initiate(
IAvaloniaObject target,
AvaloniaProperty targetProperty,
object anchor = null,
bool enableDataValidation = false)
{
return new InstancedBinding(_source, BindingMode.OneTime);
}
}
}
}

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

@ -0,0 +1,143 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Subjects;
using Avalonia.Data;
using Xunit;
namespace Avalonia.Base.UnitTests
{
public class AvaloniaObjectTests_DataValidation
{
[Fact]
public void Setting_Non_Validated_Property_Does_Not_Call_UpdateDataValidation()
{
var target = new Class1();
target.SetValue(Class1.NonValidatedDirectProperty, 6);
Assert.Empty(target.Notifications);
}
[Fact]
public void Setting_Non_Validated_Direct_Property_Does_Not_Call_UpdateDataValidation()
{
var target = new Class1();
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.ValidatedDirectProperty, new BindingNotification(6));
target.SetValue(Class1.ValidatedDirectProperty, new BindingNotification(new Exception(), BindingErrorType.Error));
target.SetValue(Class1.ValidatedDirectProperty, new BindingNotification(new Exception(), BindingErrorType.DataValidationError));
target.SetValue(Class1.ValidatedDirectProperty, 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.AsBinding(),
};
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.Empty(target.Notifications);
}
[Fact]
public void Binding_Validated_Direct_Property_Calls_UpdateDataValidation()
{
var source = new Subject<object>();
var target = new Class1
{
[!Class1.ValidatedDirectProperty] = source.AsBinding(),
};
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());
}
private class Class1 : AvaloniaObject
{
public static readonly StyledProperty<int> NonValidatedProperty =
AvaloniaProperty.Register<Class1, int>(
nameof(NonValidated));
public static readonly DirectProperty<Class1, int> NonValidatedDirectProperty =
AvaloniaProperty.RegisterDirect<Class1, int>(
nameof(NonValidatedDirect),
o => o.NonValidatedDirect,
(o, v) => o.NonValidatedDirect = v);
public static readonly DirectProperty<Class1, int> ValidatedDirectProperty =
AvaloniaProperty.RegisterDirect<Class1, int>(
nameof(ValidatedDirect),
o => o.ValidatedDirect,
(o, v) => o.ValidatedDirect = v,
enableDataValidation: true);
private int _nonValidatedDirect;
private int _direct;
public int NonValidated
{
get { return GetValue(NonValidatedProperty); }
set { SetValue(NonValidatedProperty, value); }
}
public int NonValidatedDirect
{
get { return _direct; }
set { SetAndRaise(NonValidatedDirectProperty, ref _nonValidatedDirect, value); }
}
public int ValidatedDirect
{
get { return _direct; }
set { SetAndRaise(ValidatedDirectProperty, ref _direct, value); }
}
public IList<BindingNotification> Notifications { get; } = new List<BindingNotification>();
protected override void UpdateDataValidation(AvaloniaProperty property, BindingNotification notification)
{
Notifications.Add(notification);
}
}
}
}

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

@ -284,7 +284,6 @@ namespace Avalonia.Base.UnitTests
Assert.Equal("newvalue", target.Foo);
}
[Fact]
public void UnsetValue_Is_Used_On_AddOwnered_Property()
{
@ -360,7 +359,7 @@ namespace Avalonia.Base.UnitTests
target.Bind(Class1.FooProperty, source);
source.OnNext("initial");
source.OnNext(new BindingError(new InvalidOperationException("Foo")));
source.OnNext(new BindingNotification(new InvalidOperationException("Foo"), BindingErrorType.Error));
Assert.Equal("initial", target.GetValue(Class1.FooProperty));
}
@ -373,7 +372,10 @@ namespace Avalonia.Base.UnitTests
target.Bind(Class1.FooProperty, source);
source.OnNext("initial");
source.OnNext(new BindingError(new InvalidOperationException("Foo"), "fallback"));
source.OnNext(new BindingNotification(
new InvalidOperationException("Foo"),
BindingErrorType.Error,
"fallback"));
Assert.Equal("fallback", target.GetValue(Class1.FooProperty));
}
@ -389,7 +391,7 @@ namespace Avalonia.Base.UnitTests
{
if (level == LogEventLevel.Error &&
area == LogArea.Binding &&
mt == "Error binding to {Target}.{Property}: {Message}" &&
mt == "Error in binding to {Target}.{Property}: {Message}" &&
pv.Length == 3 &&
pv[0] is Class1 &&
object.ReferenceEquals(pv[1], Class1.FooProperty) &&
@ -403,7 +405,7 @@ namespace Avalonia.Base.UnitTests
{
target.Bind(Class1.FooProperty, source);
source.OnNext("baz");
source.OnNext(new BindingError(new InvalidOperationException("Binding Error Message")));
source.OnNext(new BindingNotification(new InvalidOperationException("Binding Error Message"), BindingErrorType.Error));
}
Assert.True(called);

4
tests/Avalonia.Base.UnitTests/AvaloniaPropertyRegistryTests.cs

@ -25,7 +25,7 @@ namespace Avalonia.Base.UnitTests
.Select(x => x.Name)
.ToArray();
Assert.Equal(new[] { "Foo", "Baz", "Qux", "Attached", "ValidationStatus" }, names);
Assert.Equal(new[] { "Foo", "Baz", "Qux", "Attached" }, names);
}
[Fact]
@ -35,7 +35,7 @@ namespace Avalonia.Base.UnitTests
.Select(x => x.Name)
.ToArray();
Assert.Equal(new[] { "Bar", "Flob", "Fred", "Foo", "Baz", "Qux", "Attached", "ValidationStatus" }, names);
Assert.Equal(new[] { "Bar", "Flob", "Fred", "Foo", "Baz", "Qux", "Attached" }, names);
}
[Fact]

2
tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj

@ -98,7 +98,7 @@
<Compile Include="LayoutTransformControlTests.cs" />
<Compile Include="Presenters\ItemsPresenterTests_Virtualization_Simple.cs" />
<Compile Include="Presenters\ItemsPresenterTests_Virtualization.cs" />
<Compile Include="TextBoxTests_ValidationState.cs" />
<Compile Include="TextBoxTests_DataValidation.cs" />
<Compile Include="UserControlTests.cs" />
<Compile Include="DockPanelTests.cs" />
<Compile Include="EnumerableExtensions.cs" />

60
tests/Avalonia.Controls.UnitTests/TextBoxTests.cs

@ -1,8 +1,11 @@
// 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.Controls.Presenters;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Markup.Xaml.Data;
using Avalonia.Platform;
using Avalonia.UnitTests;
using Moq;
@ -21,6 +24,52 @@ namespace Avalonia.Controls.UnitTests
}
[Fact]
public void Typing_Beginning_With_0_Should_Not_Modify_Text_When_Bound_To_Int()
{
using (UnitTestApplication.Start(Services))
{
var source = new Class1();
var target = new TextBox
{
DataContext = source,
Template = CreateTemplate(),
};
target.ApplyTemplate();
target.Bind(TextBox.TextProperty, new Binding(nameof(Class1.Foo), BindingMode.TwoWay));
Assert.Equal("0", target.Text);
target.CaretIndex = 1;
target.RaiseEvent(new TextInputEventArgs
{
RoutedEvent = InputElement.TextInputEvent,
Text = "2",
});
Assert.Equal("02", target.Text);
}
}
private static TestServices Services => TestServices.MockThreadingInterface.With(
standardCursorFactory: Mock.Of<IStandardCursorFactory>());
private IControlTemplate CreateTemplate()
{
return new FuncControlTemplate<TextBox>(control =>
new TextPresenter
{
Name = "PART_TextPresenter",
[!!TextPresenter.TextProperty] = new Binding
{
Path = "Text",
Mode = BindingMode.TwoWay,
Priority = BindingPriority.TemplatedParent,
RelativeSource = new RelativeSource(RelativeSourceMode.TemplatedParent),
},
});
}
public void Control_Backspace_Should_Remove_The_Word_Before_The_Caret_If_There_Is_No_Selection()
{
AvaloniaLocator.CurrentMutable
@ -109,5 +158,16 @@ namespace Avalonia.Controls.UnitTests
Key = key
});
}
private class Class1 : NotifyingBase
{
private int _foo;
public int Foo
{
get { return _foo; }
set { _foo = value; RaisePropertyChanged(); }
}
}
}
}

93
tests/Avalonia.Controls.UnitTests/TextBoxTests_ValidationState.cs → tests/Avalonia.Controls.UnitTests/TextBoxTests_DataValidation.cs

@ -5,71 +5,84 @@ using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Markup.Xaml.Data;
using Avalonia.Platform;
using Avalonia.UnitTests;
using Moq;
using Xunit;
namespace Avalonia.Controls.UnitTests
{
public class TextBoxTests_ValidationState
public class TextBoxTests_DataValidation
{
[Fact]
public void Setter_Exceptions_Should_Set_ValidationState()
public void Setter_Exceptions_Should_Set_Error_Pseudoclass()
{
using (UnitTestApplication.Start(TestServices.MockThreadingInterface))
using (UnitTestApplication.Start(Services))
{
var target = new TextBox();
var binding = new Binding(nameof(ExceptionTest.LessThan10));
binding.Source = new ExceptionTest();
binding.EnableValidation = true;
target.Bind(TextBox.TextProperty, binding);
var target = new TextBox
{
DataContext = new ExceptionTest(),
[!TextBox.TextProperty] = new Binding(nameof(ExceptionTest.LessThan10), BindingMode.TwoWay),
Template = CreateTemplate(),
};
Assert.True(target.ValidationStatus.IsValid);
target.Text = "20";
Assert.False(target.ValidationStatus.IsValid);
target.Text = "1";
Assert.True(target.ValidationStatus.IsValid);
}
}
target.ApplyTemplate();
[Fact(Skip = "TODO: Not yet passing")]
public void Unconvertable_Value_Should_Set_ValidationState()
{
using (UnitTestApplication.Start(TestServices.MockThreadingInterface))
{
var target = new TextBox();
var binding = new Binding(nameof(ExceptionTest.LessThan10));
binding.Source = new ExceptionTest();
binding.EnableValidation = true;
target.Bind(TextBox.TextProperty, binding);
Assert.True(target.ValidationStatus.IsValid);
target.Text = "foo";
Assert.False(target.ValidationStatus.IsValid);
Assert.False(target.Classes.Contains(":error"));
target.Text = "20";
Assert.True(target.Classes.Contains(":error"));
target.Text = "1";
Assert.True(target.ValidationStatus.IsValid);
Assert.False(target.Classes.Contains(":error"));
}
}
[Fact]
public void Indei_Should_Set_ValidationState()
public void Setter_Exceptions_Should_Set_DataValidationErrors()
{
using (UnitTestApplication.Start(TestServices.MockThreadingInterface))
using (UnitTestApplication.Start(Services))
{
var target = new TextBox();
var binding = new Binding(nameof(ExceptionTest.LessThan10));
binding.Source = new IndeiTest();
binding.EnableValidation = true;
target.Bind(TextBox.TextProperty, binding);
var target = new TextBox
{
DataContext = new ExceptionTest(),
[!TextBox.TextProperty] = new Binding(nameof(ExceptionTest.LessThan10), BindingMode.TwoWay),
Template = CreateTemplate(),
};
target.ApplyTemplate();
Assert.True(target.ValidationStatus.IsValid);
Assert.Null(target.DataValidationErrors);
target.Text = "20";
Assert.False(target.ValidationStatus.IsValid);
Assert.Equal(1, target.DataValidationErrors.Count());
Assert.IsType<InvalidOperationException>(target.DataValidationErrors.Single());
target.Text = "1";
Assert.True(target.ValidationStatus.IsValid);
Assert.Null(target.DataValidationErrors);
}
}
private static TestServices Services => TestServices.MockThreadingInterface.With(
standardCursorFactory: Mock.Of<IStandardCursorFactory>());
private IControlTemplate CreateTemplate()
{
return new FuncControlTemplate<TextBox>(control =>
new TextPresenter
{
Name = "PART_TextPresenter",
[!!TextPresenter.TextProperty] = new Binding
{
Path = "Text",
Mode = BindingMode.TwoWay,
Priority = BindingPriority.TemplatedParent,
RelativeSource = new RelativeSource(RelativeSourceMode.TemplatedParent),
},
});
}
private class ExceptionTest
{
private int _lessThan10;

1
tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj

@ -98,6 +98,7 @@
<ItemGroup>
<Compile Include="AvaloniaObjectTests.cs" />
<Compile Include="ControlTests.cs" />
<Compile Include="ExpressionObserverTests.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>

92
tests/Avalonia.LeakTests/ExpressionObserverTests.cs

@ -0,0 +1,92 @@
using System;
using System.Collections.Generic;
using Avalonia.Collections;
using Avalonia.Markup.Data;
using Avalonia.UnitTests;
using JetBrains.dotMemoryUnit;
using Xunit;
using Xunit.Abstractions;
namespace Avalonia.LeakTests
{
[DotMemoryUnit(FailIfRunWithoutSupport = false)]
public class ExpressionObserverTests
{
public ExpressionObserverTests(ITestOutputHelper atr)
{
DotMemoryUnitTestOutput.SetOutputMethod(atr.WriteLine);
}
[Fact]
public void Should_Not_Keep_Source_Alive_ObservableCollection()
{
Func<ExpressionObserver> run = () =>
{
var source = new { Foo = new AvaloniaList<string> {"foo", "bar"} };
var target = new ExpressionObserver(source, "Foo");
target.Subscribe(_ => { });
return target;
};
var result = run();
dotMemory.Check(memory =>
Assert.Equal(0, memory.GetObjects(where => where.Type.Is<AvaloniaList<string>>()).ObjectsCount));
}
[Fact]
public void Should_Not_Keep_Source_Alive_ObservableCollection_With_DataValidation()
{
Func<ExpressionObserver> run = () =>
{
var source = new { Foo = new AvaloniaList<string> { "foo", "bar" } };
var target = new ExpressionObserver(source, "Foo", true);
target.Subscribe(_ => { });
return target;
};
var result = run();
dotMemory.Check(memory =>
Assert.Equal(0, memory.GetObjects(where => where.Type.Is<AvaloniaList<string>>()).ObjectsCount));
}
[Fact]
public void Should_Not_Keep_Source_Alive_NonIntegerIndexer()
{
Func<ExpressionObserver> run = () =>
{
var source = new { Foo = new NonIntegerIndexer() };
var target = new ExpressionObserver(source, "Foo");
target.Subscribe(_ => { });
return target;
};
var result = run();
dotMemory.Check(memory =>
Assert.Equal(0, memory.GetObjects(where => where.Type.Is<NonIntegerIndexer>()).ObjectsCount));
}
private class NonIntegerIndexer : NotifyingBase
{
private readonly Dictionary<string, string> _storage = new Dictionary<string, string>();
public string this[string key]
{
get
{
return _storage[key];
}
set
{
_storage[key] = value;
RaisePropertyChanged(CommonPropertyNames.IndexerName);
}
}
}
}
}

30
tests/Avalonia.Markup.UnitTests/Avalonia.Markup.UnitTests.csproj

@ -10,10 +10,11 @@
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Avalonia.Markup.UnitTests</RootNamespace>
<AssemblyName>Avalonia.Markup.UnitTests</AssemblyName>
<TargetFrameworkVersion>v4.6</TargetFrameworkVersion>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<NuGetPackageImportStamp>
</NuGetPackageImportStamp>
<TargetFrameworkProfile />
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
@ -34,7 +35,7 @@
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.Reactive.Testing, Version=3.0.0.0, Culture=neutral, PublicKeyToken=94bc3704cddfc263, processorArchitecture=MSIL">
<HintPath>..\..\packages\Microsoft.Reactive.Testing.3.0.0\lib\net46\Microsoft.Reactive.Testing.dll</HintPath>
<HintPath>..\..\packages\Microsoft.Reactive.Testing.3.0.0\lib\net45\Microsoft.Reactive.Testing.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Microsoft.VisualStudio.QualityTools.UnitTestFramework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" />
@ -43,9 +44,13 @@
<Private>True</Private>
</Reference>
<Reference Include="System" />
<Reference Include="System.ComponentModel.Annotations">
<HintPath>C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETPortable\v4.5\Profile\Profile7\System.ComponentModel.Annotations.dll</HintPath>
</Reference>
<Reference Include="System.ComponentModel.DataAnnotations" />
<Reference Include="System.Core" />
<Reference Include="System.Reactive.Core, Version=3.0.0.0, Culture=neutral, PublicKeyToken=94bc3704cddfc263, processorArchitecture=MSIL">
<HintPath>..\..\packages\System.Reactive.Core.3.0.0\lib\net46\System.Reactive.Core.dll</HintPath>
<HintPath>..\..\packages\System.Reactive.Core.3.0.0\lib\net45\System.Reactive.Core.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System.Reactive.Interfaces, Version=3.0.0.0, Culture=neutral, PublicKeyToken=94bc3704cddfc263, processorArchitecture=MSIL">
@ -53,11 +58,15 @@
<Private>True</Private>
</Reference>
<Reference Include="System.Reactive.Linq, Version=3.0.0.0, Culture=neutral, PublicKeyToken=94bc3704cddfc263, processorArchitecture=MSIL">
<HintPath>..\..\packages\System.Reactive.Linq.3.0.0\lib\net46\System.Reactive.Linq.dll</HintPath>
<HintPath>..\..\packages\System.Reactive.Linq.3.0.0\lib\net45\System.Reactive.Linq.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System.Reactive.PlatformServices, Version=3.0.0.0, Culture=neutral, PublicKeyToken=94bc3704cddfc263, processorArchitecture=MSIL">
<HintPath>..\..\packages\System.Reactive.PlatformServices.3.0.0\lib\net46\System.Reactive.PlatformServices.dll</HintPath>
<HintPath>..\..\packages\System.Reactive.PlatformServices.3.0.0\lib\net45\System.Reactive.PlatformServices.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System.Reactive.Windows.Threading, Version=3.0.0.0, Culture=neutral, PublicKeyToken=94bc3704cddfc263, processorArchitecture=MSIL">
<HintPath>..\..\packages\System.Reactive.Windows.Threading.3.0.0\lib\net45\System.Reactive.Windows.Threading.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System.Xml.Linq" />
@ -66,6 +75,7 @@
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
<Reference Include="WindowsBase" />
<Reference Include="xunit.abstractions, Version=2.0.0.0, Culture=neutral, PublicKeyToken=8d05b1bb7a6fdb6c, processorArchitecture=MSIL">
<HintPath>..\..\packages\xunit.abstractions.2.0.0\lib\net35\xunit.abstractions.dll</HintPath>
<Private>True</Private>
@ -85,7 +95,10 @@
</ItemGroup>
<ItemGroup>
<Compile Include="ControlLocatorTests.cs" />
<Compile Include="Data\ExceptionValidatorTests.cs" />
<Compile Include="Data\IndeiBase.cs" />
<Compile Include="Data\Plugins\DataAnnotationsValidationPluginTests.cs" />
<Compile Include="Data\Plugins\IndeiValidationPluginTests.cs" />
<Compile Include="Data\Plugins\ExceptionValidationPluginTests.cs" />
<Compile Include="Data\ExpressionNodeBuilderTests.cs" />
<Compile Include="Data\ExpressionNodeBuilderTests_Errors.cs" />
<Compile Include="Data\ExpressionObserverTests_Lifetime.cs" />
@ -97,9 +110,8 @@
<Compile Include="Data\ExpressionObserverTests_Property.cs" />
<Compile Include="Data\ExpressionObserverTests_SetValue.cs" />
<Compile Include="Data\ExpressionObserverTests_Task.cs" />
<Compile Include="Data\ExpressionObserverTests_Validation.cs" />
<Compile Include="Data\ExpressionSubjectTests.cs" />
<Compile Include="Data\IndeiValidatorTests.cs" />
<Compile Include="Data\ExpressionObserverTests_DataValidation.cs" />
<Compile Include="Data\BindingExpressionTests.cs" />
<Compile Include="DefaultValueConverterTests.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="UnitTestSynchronizationContext.cs" />

320
tests/Avalonia.Markup.UnitTests/Data/BindingExpressionTests.cs

@ -0,0 +1,320 @@
// 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.Globalization;
using System.Reactive.Linq;
using System.Threading;
using Avalonia.Data;
using Avalonia.Markup.Data;
using Avalonia.UnitTests;
using Moq;
using Xunit;
namespace Avalonia.Markup.UnitTests.Data
{
public class BindingExpressionTests
{
[Fact]
public async void Should_Get_Simple_Property_Value()
{
var data = new Class1 { StringValue = "foo" };
var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(string));
var result = await target.Take(1);
Assert.Equal("foo", result);
}
[Fact]
public void Should_Set_Simple_Property_Value()
{
var data = new Class1 { StringValue = "foo" };
var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(string));
target.OnNext("bar");
Assert.Equal("bar", data.StringValue);
}
[Fact]
public async void Should_Convert_Get_String_To_Double()
{
Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture;
var data = new Class1 { StringValue = "5.6" };
var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(double));
var result = await target.Take(1);
Assert.Equal(5.6, result);
}
[Fact]
public async void Getting_Invalid_Double_String_Should_Return_BindingError()
{
var data = new Class1 { StringValue = "foo" };
var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(double));
var result = await target.Take(1);
Assert.IsType<BindingNotification>(result);
}
[Fact]
public async void Should_Coerce_Get_Null_Double_String_To_UnsetValue()
{
var data = new Class1 { StringValue = null };
var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(double));
var result = await target.Take(1);
Assert.Equal(AvaloniaProperty.UnsetValue, result);
}
[Fact]
public void Should_Convert_Set_String_To_Double()
{
Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture;
var data = new Class1 { StringValue = (5.6).ToString() };
var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(double));
target.OnNext(6.7);
Assert.Equal((6.7).ToString(), data.StringValue);
}
[Fact]
public async void Should_Convert_Get_Double_To_String()
{
Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture;
var data = new Class1 { DoubleValue = 5.6 };
var target = new BindingExpression(new ExpressionObserver(data, "DoubleValue"), typeof(string));
var result = await target.Take(1);
Assert.Equal((5.6).ToString(), result);
}
[Fact]
public void Should_Convert_Set_Double_To_String()
{
Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture;
var data = new Class1 { DoubleValue = 5.6 };
var target = new BindingExpression(new ExpressionObserver(data, "DoubleValue"), typeof(string));
target.OnNext("6.7");
Assert.Equal(6.7, data.DoubleValue);
}
[Fact]
public async void Should_Return_BindingNotification_With_FallbackValue_For_NonConvertibe_Target_Value()
{
Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture;
var data = new Class1 { StringValue = "foo" };
var target = new BindingExpression(
new ExpressionObserver(data, "StringValue"),
typeof(int),
42,
DefaultValueConverter.Instance);
var result = await target.Take(1);
Assert.Equal(
new BindingNotification(
new InvalidCastException("'foo' is not a valid number."),
BindingErrorType.Error,
42),
result);
}
[Fact]
public async void Should_Return_BindingNotification_With_FallbackValue_For_NonConvertibe_Target_Value_With_Data_Validation()
{
Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture;
var data = new Class1 { StringValue = "foo" };
var target = new BindingExpression(
new ExpressionObserver(data, "StringValue", true),
typeof(int),
42,
DefaultValueConverter.Instance);
var result = await target.Take(1);
Assert.Equal(
new BindingNotification(
new InvalidCastException("'foo' is not a valid number."),
BindingErrorType.Error,
42),
result);
}
[Fact]
public async void Should_Return_BindingNotification_For_Invalid_FallbackValue()
{
Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture;
var data = new Class1 { StringValue = "foo" };
var target = new BindingExpression(
new ExpressionObserver(data, "StringValue"),
typeof(int),
"bar",
DefaultValueConverter.Instance);
var result = await target.Take(1);
Assert.Equal(
new BindingNotification(
new AggregateException(
new InvalidCastException("Could not convert 'foo' to 'System.Int32'"),
new InvalidCastException("Could not convert FallbackValue 'bar' to 'System.Int32'")),
BindingErrorType.Error),
result);
}
[Fact]
public async void Should_Return_BindingNotification_For_Invalid_FallbackValue_With_Data_Validation()
{
Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture;
var data = new Class1 { StringValue = "foo" };
var target = new BindingExpression(
new ExpressionObserver(data, "StringValue", true),
typeof(int),
"bar",
DefaultValueConverter.Instance);
var result = await target.Take(1);
Assert.Equal(
new BindingNotification(
new AggregateException(
new InvalidCastException("Could not convert 'foo' to 'System.Int32'"),
new InvalidCastException("Could not convert FallbackValue 'bar' to 'System.Int32'")),
BindingErrorType.Error),
result);
}
[Fact]
public void Setting_Invalid_Double_String_Should_Not_Change_Target()
{
var data = new Class1 { DoubleValue = 5.6 };
var target = new BindingExpression(new ExpressionObserver(data, "DoubleValue"), typeof(string));
target.OnNext("foo");
Assert.Equal(5.6, data.DoubleValue);
}
[Fact]
public void Setting_Invalid_Double_String_Should_Use_FallbackValue()
{
var data = new Class1 { DoubleValue = 5.6 };
var target = new BindingExpression(
new ExpressionObserver(data, "DoubleValue"),
typeof(string),
"9.8",
DefaultValueConverter.Instance);
target.OnNext("foo");
Assert.Equal(9.8, data.DoubleValue);
}
[Fact]
public void Should_Coerce_Setting_Null_Double_To_Default_Value()
{
var data = new Class1 { DoubleValue = 5.6 };
var target = new BindingExpression(new ExpressionObserver(data, "DoubleValue"), typeof(string));
target.OnNext(null);
Assert.Equal(0, data.DoubleValue);
}
[Fact]
public void Should_Coerce_Setting_UnsetValue_Double_To_Default_Value()
{
var data = new Class1 { DoubleValue = 5.6 };
var target = new BindingExpression(new ExpressionObserver(data, "DoubleValue"), typeof(string));
target.OnNext(AvaloniaProperty.UnsetValue);
Assert.Equal(0, data.DoubleValue);
}
[Fact]
public void Should_Pass_ConverterParameter_To_Convert()
{
var data = new Class1 { DoubleValue = 5.6 };
var converter = new Mock<IValueConverter>();
var target = new BindingExpression(
new ExpressionObserver(data, "DoubleValue"),
typeof(string),
converter.Object,
converterParameter: "foo");
target.Subscribe(_ => { });
converter.Verify(x => x.Convert(5.6, typeof(string), "foo", CultureInfo.CurrentUICulture));
}
[Fact]
public void Should_Pass_ConverterParameter_To_ConvertBack()
{
var data = new Class1 { DoubleValue = 5.6 };
var converter = new Mock<IValueConverter>();
var target = new BindingExpression(
new ExpressionObserver(data, "DoubleValue"),
typeof(string),
converter.Object,
converterParameter: "foo");
target.OnNext("bar");
converter.Verify(x => x.ConvertBack("bar", typeof(double), "foo", CultureInfo.CurrentUICulture));
}
[Fact]
public void Should_Handle_DataValidation()
{
var data = new Class1 { DoubleValue = 5.6 };
var converter = new Mock<IValueConverter>();
var target = new BindingExpression(new ExpressionObserver(data, "DoubleValue", true), typeof(string));
var result = new List<object>();
target.Subscribe(x => result.Add(x));
target.OnNext(1.2);
target.OnNext("3.4");
target.OnNext("bar");
Assert.Equal(
new[]
{
new BindingNotification("5.6"),
new BindingNotification("1.2"),
new BindingNotification("3.4"),
new BindingNotification(
new InvalidCastException("'bar' is not a valid number."),
BindingErrorType.Error)
},
result);
}
private class Class1 : NotifyingBase
{
private string _stringValue;
private double _doubleValue;
public string StringValue
{
get { return _stringValue; }
set { _stringValue = value; RaisePropertyChanged(); }
}
public double DoubleValue
{
get { return _doubleValue; }
set { _doubleValue = value; RaisePropertyChanged(); }
}
}
}
}

93
tests/Avalonia.Markup.UnitTests/Data/ExceptionValidatorTests.cs

@ -1,93 +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.ComponentModel;
using System.Runtime.CompilerServices;
using Avalonia.Data;
using Avalonia.Markup.Data.Plugins;
using Xunit;
namespace Avalonia.Markup.UnitTests.Data
{
public class ExceptionValidatorTests
{
public class Data : INotifyPropertyChanged
{
private int nonValidated;
public int NonValidated
{
get { return nonValidated; }
set { nonValidated = value; NotifyPropertyChanged(); }
}
private int mustBePositive;
public int MustBePositive
{
get { return mustBePositive; }
set
{
if (value <= 0)
{
throw new ArgumentOutOfRangeException(nameof(value));
}
mustBePositive = value;
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
[Fact]
public void Setting_Non_Validating_Triggers_Validation()
{
var inpcAccessorPlugin = new InpcPropertyAccessorPlugin();
var validatorPlugin = new ExceptionValidationPlugin();
var data = new Data();
var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.NonValidated), _ => { });
IValidationStatus status = null;
var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.NonValidated), accessor, s => status = s);
validator.SetValue(5, BindingPriority.LocalValue);
Assert.NotNull(status);
}
[Fact]
public void Setting_Validating_Property_To_Valid_Value_Returns_Successful_ValidationStatus()
{
var inpcAccessorPlugin = new InpcPropertyAccessorPlugin();
var validatorPlugin = new ExceptionValidationPlugin();
var data = new Data();
var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive), _ => { });
IValidationStatus status = null;
var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive), accessor, s => status = s);
validator.SetValue(5, BindingPriority.LocalValue);
Assert.True(status.IsValid);
}
[Fact]
public void Setting_Validating_Property_To_Invalid_Value_Returns_Failed_ValidationStatus()
{
var inpcAccessorPlugin = new InpcPropertyAccessorPlugin();
var validatorPlugin = new ExceptionValidationPlugin();
var data = new Data();
var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive), _ => { });
IValidationStatus status = null;
var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive), accessor, s => status = s);
validator.SetValue(-5, BindingPriority.LocalValue);
Assert.False(status.IsValid);
}
}
}

222
tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_DataValidation.cs

@ -0,0 +1,222 @@
// 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;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
using Avalonia.Data;
using Avalonia.Markup.Data;
using Avalonia.UnitTests;
using Xunit;
namespace Avalonia.Markup.UnitTests.Data
{
public class ExpressionObserverTests_DataValidation
{
[Fact]
public void Doesnt_Send_DataValidationError_When_DataValidatation_Not_Enabled()
{
var data = new ExceptionTest { MustBePositive = 5 };
var observer = new ExpressionObserver(data, nameof(data.MustBePositive), false);
var validationMessageFound = false;
observer.OfType<BindingNotification>()
.Where(x => x.ErrorType == BindingErrorType.DataValidationError)
.Subscribe(_ => validationMessageFound = true);
observer.SetValue(-5);
Assert.False(validationMessageFound);
}
[Fact]
public void Exception_Validation_Sends_DataValidationError()
{
var data = new ExceptionTest { MustBePositive = 5 };
var observer = new ExpressionObserver(data, nameof(data.MustBePositive), true);
var validationMessageFound = false;
observer.OfType<BindingNotification>()
.Where(x => x.ErrorType == BindingErrorType.DataValidationError)
.Subscribe(_ => validationMessageFound = true);
observer.SetValue(-5);
Assert.True(validationMessageFound);
}
[Fact]
public void Indei_Validation_Does_Not_Subscribe_When_DataValidatation_Not_Enabled()
{
var data = new IndeiTest { MustBePositive = 5 };
var observer = new ExpressionObserver(data, nameof(data.MustBePositive), false);
observer.Subscribe(_ => { });
Assert.Equal(0, data.ErrorsChangedSubscriptionCount);
}
[Fact]
public void Enabled_Indei_Validation_Subscribes()
{
var data = new IndeiTest { MustBePositive = 5 };
var observer = new ExpressionObserver(data, nameof(data.MustBePositive), true);
var sub = observer.Subscribe(_ => { });
Assert.Equal(1, data.ErrorsChangedSubscriptionCount);
sub.Dispose();
Assert.Equal(0, data.ErrorsChangedSubscriptionCount);
}
[Fact]
public void Validation_Plugins_Send_Correct_Notifications()
{
var data = new IndeiTest();
var observer = new ExpressionObserver(data, nameof(data.MustBePositive), true);
var result = new List<object>();
observer.Subscribe(x => result.Add(x));
observer.SetValue(5);
observer.SetValue(-5);
observer.SetValue("foo");
observer.SetValue(5);
Assert.Equal(new[]
{
new BindingNotification(0),
// Value is notified twice as ErrorsChanged is always called by IndeiTest.
new BindingNotification(5),
new BindingNotification(5),
// Value is first signalled without an error as validation hasn't been updated.
new BindingNotification(-5),
new BindingNotification(new Exception("Must be positive"), BindingErrorType.DataValidationError, -5),
// Exception is thrown by trying to set value to "foo".
new BindingNotification(
new ArgumentException("Object of type 'System.String' cannot be converted to type 'System.Int32'."),
BindingErrorType.DataValidationError),
// Value is set then validation is updated.
new BindingNotification(new Exception("Must be positive"), BindingErrorType.DataValidationError, 5),
new BindingNotification(5),
}, result);
}
[Fact]
public void Doesnt_Subscribe_To_Indei_Of_Intermediate_Object_In_Chain()
{
var data = new Container
{
Inner = new IndeiTest()
};
var observer = new ExpressionObserver(
data,
$"{nameof(Container.Inner)}.{nameof(IndeiTest.MustBePositive)}",
true);
observer.Subscribe(_ => { });
// We may want to change this but I've never seen an example of data validation on an
// intermediate object in a chain so for the moment I'm not sure what the result of
// validating such a thing should look like.
Assert.Equal(0, data.ErrorsChangedSubscriptionCount);
Assert.Equal(1, ((IndeiTest)data.Inner).ErrorsChangedSubscriptionCount);
}
[Fact]
public void Sends_Correct_Notifications_With_Property_Chain()
{
var container = new Container();
var inner = new IndeiTest();
var observer = new ExpressionObserver(
container,
$"{nameof(Container.Inner)}.{nameof(IndeiTest.MustBePositive)}",
true);
var result = new List<object>();
observer.Subscribe(x => result.Add(x));
Assert.Equal(new[]
{
new BindingNotification(
new MarkupBindingChainNullException("Inner.MustBePositive", "Inner"),
BindingErrorType.Error,
AvaloniaProperty.UnsetValue),
}, result);
}
public class ExceptionTest : NotifyingBase
{
private int _mustBePositive;
public int MustBePositive
{
get { return _mustBePositive; }
set
{
if (value <= 0)
{
throw new ArgumentOutOfRangeException(nameof(value));
}
_mustBePositive = value;
RaisePropertyChanged();
}
}
}
private class IndeiTest : IndeiBase
{
private int _mustBePositive;
private Dictionary<string, IList<string>> _errors = new Dictionary<string, IList<string>>();
public int MustBePositive
{
get { return _mustBePositive; }
set
{
_mustBePositive = value;
RaisePropertyChanged();
if (value >= 0)
{
_errors.Remove(nameof(MustBePositive));
RaiseErrorsChanged(nameof(MustBePositive));
}
else
{
_errors[nameof(MustBePositive)] = new[] { "Must be positive" };
RaiseErrorsChanged(nameof(MustBePositive));
}
}
}
public override bool HasErrors => _mustBePositive >= 0;
public override IEnumerable GetErrors(string propertyName)
{
IList<string> result;
_errors.TryGetValue(propertyName, out result);
return result;
}
}
private class Container : IndeiBase
{
private object _inner;
public object Inner
{
get { return _inner; }
set { _inner = value; RaisePropertyChanged(); }
}
public override bool HasErrors => false;
public override IEnumerable GetErrors(string propertyName) => null;
}
}
}

44
tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Indexer.cs

@ -189,58 +189,22 @@ namespace Avalonia.Markup.UnitTests.Data
var expected = new[] { "bar", "bar2" };
Assert.Equal(expected, result);
Assert.Equal(0, data.Foo.SubscriptionCount);
}
[Fact]
public void Should_Not_Keep_Source_Alive_ObservableCollection()
{
Func<Tuple<ExpressionObserver, WeakReference>> run = () =>
{
var source = new { Foo = new AvaloniaList<string> { "foo", "bar" } };
var target = new ExpressionObserver(source, "Foo");
return Tuple.Create(target, new WeakReference(source.Foo));
};
var result = run();
result.Item1.Subscribe(x => { });
GC.Collect();
Assert.Null(result.Item2.Target);
}
[Fact]
public void Should_Not_Keep_Source_Alive_NonIntegerIndexer()
{
Func<Tuple<ExpressionObserver, WeakReference>> run = () =>
{
var source = new NonIntegerIndexer();
var target = new ExpressionObserver(source, "Foo");
return Tuple.Create(target, new WeakReference(source));
};
var result = run();
result.Item1.Subscribe(x => { });
GC.Collect();
Assert.Null(result.Item2.Target);
Assert.Equal(0, data.Foo.PropertyChangedSubscriptionCount);
}
private class NonIntegerIndexer : NotifyingBase
{
private Dictionary<string, string> storage = new Dictionary<string, string>();
private readonly Dictionary<string, string> _storage = new Dictionary<string, string>();
public string this[string key]
{
get
{
return storage[key];
return _storage[key];
}
set
{
storage[key] = value;
_storage[key] = value;
RaisePropertyChanged(CommonPropertyNames.IndexerName);
}
}

44
tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Lifetime.cs

@ -27,6 +27,19 @@ namespace Avalonia.Markup.UnitTests.Data
Assert.True(completed);
}
[Fact]
public void Should_Complete_When_Source_Observable_Errors()
{
var source = new BehaviorSubject<object>(1);
var target = new ExpressionObserver(source, "Foo");
var completed = false;
target.Subscribe(_ => { }, () => completed = true);
source.OnError(new Exception());
Assert.True(completed);
}
[Fact]
public void Should_Complete_When_Update_Observable_Completes()
{
@ -40,6 +53,19 @@ namespace Avalonia.Markup.UnitTests.Data
Assert.True(completed);
}
[Fact]
public void Should_Complete_When_Update_Observable_Errors()
{
var update = new Subject<Unit>();
var target = new ExpressionObserver(() => 1, "Foo", update);
var completed = false;
target.Subscribe(_ => { }, () => completed = true);
update.OnError(new Exception());
Assert.True(completed);
}
[Fact]
public void Should_Unsubscribe_From_Source_Observable()
{
@ -55,7 +81,7 @@ namespace Avalonia.Markup.UnitTests.Data
scheduler.Start();
}
Assert.Equal(new[] { AvaloniaProperty.UnsetValue, "foo" }, result);
Assert.Equal(new[] { "foo" }, result);
Assert.All(source.Subscriptions, x => Assert.NotEqual(Subscription.Infinite, x.Unsubscribe));
}
@ -77,22 +103,6 @@ namespace Avalonia.Markup.UnitTests.Data
Assert.All(update.Subscriptions, x => Assert.NotEqual(Subscription.Infinite, x.Unsubscribe));
}
[Fact]
public void Should_Set_Node_Target_To_Null_On_Unsubscribe()
{
var target = new ExpressionObserver(new { Foo = "foo" }, "Foo");
var result = new List<object>();
using (target.Subscribe(x => result.Add(x)))
using (target.Subscribe(_ => { }))
{
Assert.NotNull(target.Node.Target);
}
Assert.Equal(new[] { "foo" }, result);
Assert.Null(target.Node.Target);
}
private Recorded<Notification<object>> OnNext(long time, object value)
{
return new Recorded<Notification<object>>(time, Notification.CreateOnNext<object>(value));

17
tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Negation.cs

@ -3,6 +3,7 @@
using System;
using System.Reactive.Linq;
using Avalonia.Data;
using Avalonia.Markup.Data;
using Xunit;
@ -61,23 +62,31 @@ namespace Avalonia.Markup.UnitTests.Data
}
[Fact]
public async void Should_Return_UnsetValue_For_String_Not_Convertible_To_Boolean()
public async void Should_Return_BindingNotification_For_String_Not_Convertible_To_Boolean()
{
var data = new { Foo = "foo" };
var target = new ExpressionObserver(data, "!Foo");
var result = await target.Take(1);
Assert.Equal(AvaloniaProperty.UnsetValue, result);
Assert.Equal(
new BindingNotification(
new InvalidCastException($"Unable to convert 'foo' to bool."),
BindingErrorType.Error),
result);
}
[Fact]
public async void Should_Return_Empty_For_Value_Not_Convertible_To_Boolean()
public async void Should_Return_BindingNotification_For_Value_Not_Convertible_To_Boolean()
{
var data = new { Foo = new object() };
var target = new ExpressionObserver(data, "!Foo");
var result = await target.Take(1);
Assert.Equal(AvaloniaProperty.UnsetValue, result);
Assert.Equal(
new BindingNotification(
new InvalidCastException($"Unable to convert 'System.Object' to bool."),
BindingErrorType.Error),
result);
}
[Fact]

47
tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Observable.cs

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using Avalonia.Data;
using Avalonia.Markup.Data;
using Avalonia.UnitTests;
using Xunit;
@ -27,7 +28,7 @@ namespace Avalonia.Markup.UnitTests.Data
source.OnNext("bar");
sync.ExecutePostedCallbacks();
Assert.Equal(new[] { AvaloniaProperty.UnsetValue, "foo", "bar" }, result);
Assert.Equal(new[] { "foo", "bar" }, result);
}
}
@ -44,10 +45,50 @@ namespace Avalonia.Markup.UnitTests.Data
data.Next.OnNext(new Class2("foo"));
sync.ExecutePostedCallbacks();
Assert.Equal(new[] { AvaloniaProperty.UnsetValue, "foo" }, result);
Assert.Equal(new[] { "foo" }, result);
sub.Dispose();
Assert.Equal(0, data.SubscriptionCount);
Assert.Equal(0, data.PropertyChangedSubscriptionCount);
}
}
[Fact]
public void Should_Get_Simple_Observable_Value_With_DataValidation_Enabled()
{
using (var sync = UnitTestSynchronizationContext.Begin())
{
var source = new BehaviorSubject<string>("foo");
var data = new { Foo = source };
var target = new ExpressionObserver(data, "Foo", true);
var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x));
source.OnNext("bar");
sync.ExecutePostedCallbacks();
// What does it mean to have data validation on an observable? Without a use-case
// it's hard to know what to do here so for the moment the value is returned.
Assert.Equal(new[] { "foo", "bar" }, result);
}
}
[Fact]
public void Should_Get_Property_Value_From_Observable_With_DataValidation_Enabled()
{
using (var sync = UnitTestSynchronizationContext.Begin())
{
var data = new Class1();
var target = new ExpressionObserver(data, "Next.Foo", true);
var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x));
data.Next.OnNext(new Class2("foo"));
sync.ExecutePostedCallbacks();
Assert.Equal(new[] { new BindingNotification("foo") }, result);
sub.Dispose();
Assert.Equal(0, data.PropertyChangedSubscriptionCount);
}
}

272
tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs

@ -32,6 +32,8 @@ namespace Avalonia.Markup.UnitTests.Data
var data = new { Foo = "foo" };
var target = new ExpressionObserver(data, "Foo");
target.Subscribe(_ => { });
Assert.Equal(typeof(string), target.ResultType);
}
@ -55,6 +57,46 @@ namespace Avalonia.Markup.UnitTests.Data
Assert.Equal("foo", result);
}
[Fact]
public async void Should_Return_UnsetValue_For_Root_Null()
{
var data = new Class3 { Foo = "foo" };
var target = new ExpressionObserver(default(object), "Foo");
var result = await target.Take(1);
Assert.Equal(AvaloniaProperty.UnsetValue, result);
}
[Fact]
public async void Should_Return_UnsetValue_For_Root_UnsetValue()
{
var data = new Class3 { Foo = "foo" };
var target = new ExpressionObserver(AvaloniaProperty.UnsetValue, "Foo");
var result = await target.Take(1);
Assert.Equal(AvaloniaProperty.UnsetValue, result);
}
[Fact]
public async void Should_Return_UnsetValue_For_Observable_Root_Null()
{
var data = new Class3 { Foo = "foo" };
var target = new ExpressionObserver(Observable.Return(default(object)), "Foo");
var result = await target.Take(1);
Assert.Equal(AvaloniaProperty.UnsetValue, result);
}
[Fact]
public async void Should_Return_UnsetValue_For_Observable_Root_UnsetValue()
{
var data = new Class3 { Foo = "foo" };
var target = new ExpressionObserver(Observable.Return(AvaloniaProperty.UnsetValue), "Foo");
var result = await target.Take(1);
Assert.Equal(AvaloniaProperty.UnsetValue, result);
}
[Fact]
public async void Should_Get_Simple_Property_Chain()
{
@ -71,21 +113,44 @@ namespace Avalonia.Markup.UnitTests.Data
var data = new { Foo = new { Bar = new { Baz = "baz" } } };
var target = new ExpressionObserver(data, "Foo.Bar.Baz");
target.Subscribe(_ => { });
Assert.Equal(typeof(string), target.ResultType);
}
[Fact]
public async void Should_Return_BindingError_For_Broken_Chain()
public async void Should_Return_BindingNotification_Error_For_Broken_Chain()
{
var data = new { Foo = new { Bar = 1 } };
var target = new ExpressionObserver(data, "Foo.Bar.Baz");
var result = await target.Take(1);
Assert.IsType<BindingError>(result);
Assert.IsType<BindingNotification>(result);
var error = result as BindingError;
Assert.IsType<MissingMemberException>(error.Exception);
Assert.Equal("Could not find CLR property 'Baz' on '1'", error.Exception.Message);
Assert.Equal(
new BindingNotification(
new MissingMemberException("Could not find CLR property 'Baz' on '1'"), BindingErrorType.Error),
result);
}
[Fact]
public void Should_Return_BindingNotification_Error_For_Chain_With_Null_Value()
{
var data = new { Foo = default(object) };
var target = new ExpressionObserver(data, "Foo.Bar.Baz");
var result = new List<object>();
target.Subscribe(x => result.Add(x));
Assert.Equal(
new[]
{
new BindingNotification(
new MarkupBindingChainNullException("Foo.Bar.Baz", "Foo"),
BindingErrorType.Error,
AvaloniaProperty.UnsetValue),
},
result);
}
[Fact]
@ -111,7 +176,7 @@ namespace Avalonia.Markup.UnitTests.Data
sub.Dispose();
Assert.Equal(0, data.SubscriptionCount);
Assert.Equal(0, data.PropertyChangedSubscriptionCount);
}
[Fact]
@ -139,7 +204,7 @@ namespace Avalonia.Markup.UnitTests.Data
sub.Dispose();
Assert.Equal(0, data.SubscriptionCount);
Assert.Equal(0, data.PropertyChangedSubscriptionCount);
}
[Fact]
@ -151,13 +216,14 @@ namespace Avalonia.Markup.UnitTests.Data
var sub = target.Subscribe(x => result.Add(x));
((Class2)data.Next).Bar = "baz";
((Class2)data.Next).Bar = null;
Assert.Equal(new[] { "bar", "baz" }, result);
Assert.Equal(new[] { "bar", "baz", null }, result);
sub.Dispose();
Assert.Equal(0, data.SubscriptionCount);
Assert.Equal(0, data.Next.SubscriptionCount);
Assert.Equal(0, data.PropertyChangedSubscriptionCount);
Assert.Equal(0, data.Next.PropertyChangedSubscriptionCount);
}
[Fact]
@ -170,39 +236,60 @@ namespace Avalonia.Markup.UnitTests.Data
var sub = target.Subscribe(x => result.Add(x));
var old = data.Next;
data.Next = new Class2 { Bar = "baz" };
data.Next = new Class2 { Bar = null };
Assert.Equal(new[] { "bar", "baz" }, result);
Assert.Equal(new[] { "bar", "baz", null }, result);
sub.Dispose();
Assert.Equal(0, data.SubscriptionCount);
Assert.Equal(0, data.Next.SubscriptionCount);
Assert.Equal(0, old.SubscriptionCount);
Assert.Equal(0, data.PropertyChangedSubscriptionCount);
Assert.Equal(0, data.Next.PropertyChangedSubscriptionCount);
Assert.Equal(0, old.PropertyChangedSubscriptionCount);
}
[Fact]
public void Should_Track_Property_Chain_Breaking_With_Null_Then_Mending()
{
var data = new Class1 { Next = new Class2 { Bar = "bar" } };
var target = new ExpressionObserver(data, "Next.Bar");
var data = new Class1
{
Next = new Class2
{
Next = new Class2
{
Bar = "bar"
}
}
};
var target = new ExpressionObserver(data, "Next.Next.Bar");
var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x));
var old = data.Next;
data.Next = null;
data.Next = new Class2 { Bar = "baz" };
data.Next = old;
Assert.Equal(new[] { "bar", AvaloniaProperty.UnsetValue, "baz" }, result);
Assert.Equal(
new object[]
{
"bar",
new BindingNotification(
new MarkupBindingChainNullException("Next.Next.Bar", "Next.Next"),
BindingErrorType.Error,
AvaloniaProperty.UnsetValue),
"bar"
},
result);
sub.Dispose();
Assert.Equal(0, data.SubscriptionCount);
Assert.Equal(0, data.Next.SubscriptionCount);
Assert.Equal(0, old.SubscriptionCount);
Assert.Equal(0, data.PropertyChangedSubscriptionCount);
Assert.Equal(0, data.Next.PropertyChangedSubscriptionCount);
Assert.Equal(0, old.PropertyChangedSubscriptionCount);
}
[Fact]
public void Should_Track_Property_Chain_Breaking_With_Object_Then_Mending()
public void Should_Track_Property_Chain_Breaking_With_Missing_Member_Then_Mending()
{
var data = new Class1 { Next = new Class2 { Bar = "bar" } };
var target = new ExpressionObserver(data, "Next.Bar");
@ -214,17 +301,23 @@ namespace Avalonia.Markup.UnitTests.Data
data.Next = breaking;
data.Next = new Class2 { Bar = "baz" };
Assert.Equal(3, result.Count);
Assert.Equal("bar", result[0]);
Assert.IsType<BindingError>(result[1]);
Assert.Equal("baz", result[2]);
Assert.Equal(
new object[]
{
"bar",
new BindingNotification(
new MissingMemberException("Could not find CLR property 'Bar' on 'Avalonia.Markup.UnitTests.Data.ExpressionObserverTests_Property+WithoutBar'"),
BindingErrorType.Error),
"baz",
},
result);
sub.Dispose();
Assert.Equal(0, data.SubscriptionCount);
Assert.Equal(0, data.Next.SubscriptionCount);
Assert.Equal(0, breaking.SubscriptionCount);
Assert.Equal(0, old.SubscriptionCount);
Assert.Equal(0, data.PropertyChangedSubscriptionCount);
Assert.Equal(0, data.Next.PropertyChangedSubscriptionCount);
Assert.Equal(0, breaking.PropertyChangedSubscriptionCount);
Assert.Equal(0, old.PropertyChangedSubscriptionCount);
}
[Fact]
@ -258,17 +351,59 @@ namespace Avalonia.Markup.UnitTests.Data
scheduler.Start();
}
Assert.Equal(new[] { AvaloniaProperty.UnsetValue, "foo", "bar" }, result);
Assert.Equal(new[] { "foo", "bar" }, result);
Assert.All(source.Subscriptions, x => Assert.NotEqual(Subscription.Infinite, x.Unsubscribe));
}
[Fact]
public void Subscribing_Multiple_Times_Should_Return_Values_To_All()
{
var data = new Class1 { Foo = "foo" };
var target = new ExpressionObserver(data, "Foo");
var result1 = new List<object>();
var result2 = new List<object>();
var result3 = new List<object>();
target.Subscribe(x => result1.Add(x));
target.Subscribe(x => result2.Add(x));
data.Foo = "bar";
target.Subscribe(x => result3.Add(x));
Assert.Equal(new[] { "foo", "bar" }, result1);
Assert.Equal(new[] { "foo", "bar" }, result2);
Assert.Equal(new[] { "bar" }, result3);
}
[Fact]
public void Subscribing_Multiple_Times_Should_Only_Add_PropertyChanged_Handlers_Once()
{
var data = new Class1 { Foo = "foo" };
var target = new ExpressionObserver(data, "Foo");
var sub1 = target.Subscribe(x => { });
var sub2 = target.Subscribe(x => { });
Assert.Equal(1, data.PropertyChangedSubscriptionCount);
sub1.Dispose();
sub2.Dispose();
Assert.Equal(0, data.PropertyChangedSubscriptionCount);
}
[Fact]
public void SetValue_Should_Set_Simple_Property_Value()
{
var data = new Class1 { Foo = "foo" };
var target = new ExpressionObserver(data, "Foo");
Assert.True(target.SetValue("bar"));
using (target.Subscribe(_ => { }))
{
Assert.True(target.SetValue("bar"));
}
Assert.Equal("bar", data.Foo);
}
@ -278,7 +413,11 @@ namespace Avalonia.Markup.UnitTests.Data
var data = new Class1 { Next = new Class2 { Bar = "bar" } };
var target = new ExpressionObserver(data, "Next.Bar");
Assert.True(target.SetValue("baz"));
using (target.Subscribe(_ => { }))
{
Assert.True(target.SetValue("baz"));
}
Assert.Equal("baz", ((Class2)data.Next).Bar);
}
@ -288,25 +427,48 @@ namespace Avalonia.Markup.UnitTests.Data
var data = new Class1 { Next = new WithoutBar()};
var target = new ExpressionObserver(data, "Next.Bar");
Assert.False(target.SetValue("baz"));
using (target.Subscribe(_ => { }))
{
Assert.False(target.SetValue("baz"));
}
}
[Fact]
public void SetValue_Should_Return_False_For_Missing_Object()
public void SetValue_Should_Notify_New_Value_With_Inpc()
{
var data = new Class1();
var target = new ExpressionObserver(data, "Next.Bar");
var target = new ExpressionObserver(data, "Foo");
var result = new List<object>();
Assert.False(target.SetValue("baz"));
target.Subscribe(x => result.Add(x));
target.SetValue("bar");
Assert.Equal(new[] { null, "bar" }, result);
}
[Fact]
public async void Should_Handle_Null_Root()
public void SetValue_Should_Notify_New_Value_Without_Inpc()
{
var target = new ExpressionObserver((object)null, "Foo");
var result = await target.Take(1);
var data = new Class1();
var target = new ExpressionObserver(data, "Bar");
var result = new List<object>();
Assert.Equal(AvaloniaProperty.UnsetValue, result);
target.Subscribe(x => result.Add(x));
target.SetValue("bar");
Assert.Equal(new[] { null, "bar" }, result);
}
[Fact]
public void SetValue_Should_Return_False_For_Missing_Object()
{
var data = new Class1();
var target = new ExpressionObserver(data, "Next.Bar");
using (target.Subscribe(_ => { }))
{
Assert.False(target.SetValue("baz"));
}
}
[Fact]
@ -325,10 +487,17 @@ namespace Avalonia.Markup.UnitTests.Data
root = null;
update.OnNext(Unit.Default);
Assert.Equal(new[] { "foo", "bar", AvaloniaProperty.UnsetValue }, result);
Assert.Equal(0, first.SubscriptionCount);
Assert.Equal(0, second.SubscriptionCount);
Assert.Equal(
new object[]
{
"foo",
"bar",
AvaloniaProperty.UnsetValue,
},
result);
Assert.Equal(0, first.PropertyChangedSubscriptionCount);
Assert.Equal(0, second.PropertyChangedSubscriptionCount);
}
[Fact]
@ -351,7 +520,7 @@ namespace Avalonia.Markup.UnitTests.Data
private interface INext
{
int SubscriptionCount { get; }
int PropertyChangedSubscriptionCount { get; }
}
private class Class1 : NotifyingBase
@ -390,6 +559,7 @@ namespace Avalonia.Markup.UnitTests.Data
private class Class2 : NotifyingBase, INext
{
private string _bar;
private INext _next;
public string Bar
{
@ -400,6 +570,16 @@ namespace Avalonia.Markup.UnitTests.Data
RaisePropertyChanged(nameof(Bar));
}
}
public INext Next
{
get { return _next; }
set
{
_next = value;
RaisePropertyChanged(nameof(Next));
}
}
}
private class Class3 : Class1

10
tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_SetValue.cs

@ -18,7 +18,10 @@ namespace Avalonia.Markup.UnitTests.Data
var data = new { Foo = "foo" };
var target = new ExpressionObserver(data, "Foo");
target.SetValue("bar");
using (target.Subscribe(_ => { }))
{
target.SetValue("bar");
}
Assert.Equal("foo", data.Foo);
}
@ -29,7 +32,10 @@ namespace Avalonia.Markup.UnitTests.Data
var data = new Class1 { Foo = new Class2 { Bar = "bar" } };
var target = new ExpressionObserver(data, "Foo.Bar");
target.SetValue("foo");
using (target.Subscribe(_ => { }))
{
target.SetValue("foo");
}
Assert.Equal("foo", data.Foo.Bar);
}

82
tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Task.cs

@ -4,8 +4,8 @@
using System;
using System.Collections.Generic;
using System.Reactive.Linq;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Data;
using Avalonia.Markup.Data;
using Avalonia.UnitTests;
using Xunit;
@ -28,7 +28,7 @@ namespace Avalonia.Markup.UnitTests.Data
tcs.SetResult("foo");
sync.ExecutePostedCallbacks();
Assert.Equal(new object[] { AvaloniaProperty.UnsetValue, "foo" }, result.ToArray());
Assert.Equal(new[] { "foo" }, result);
}
}
@ -43,7 +43,7 @@ namespace Avalonia.Markup.UnitTests.Data
var sub = target.Subscribe(x => result.Add(x));
Assert.Equal(new object[] { "foo" }, result.ToArray());
Assert.Equal(new[] { "foo" }, result);
}
}
@ -61,10 +61,84 @@ namespace Avalonia.Markup.UnitTests.Data
tcs.SetResult(new Class2("foo"));
sync.ExecutePostedCallbacks();
Assert.Equal(new object[] { AvaloniaProperty.UnsetValue, "foo" }, result.ToArray());
Assert.Equal(new[] { "foo" }, result);
}
}
[Fact]
public void Should_Return_BindingNotification_Error_On_Task_Exception()
{
using (var sync = UnitTestSynchronizationContext.Begin())
{
var tcs = new TaskCompletionSource<string>();
var data = new { Foo = tcs.Task };
var target = new ExpressionObserver(data, "Foo");
var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x));
tcs.SetException(new NotSupportedException());
sync.ExecutePostedCallbacks();
Assert.Equal(
new[]
{
new BindingNotification(
new AggregateException(new NotSupportedException()),
BindingErrorType.Error)
},
result);
}
}
[Fact]
public void Should_Return_BindingNotification_Error_For_Faulted_Task()
{
using (var sync = UnitTestSynchronizationContext.Begin())
{
var data = new { Foo = TaskFromException(new NotSupportedException()) };
var target = new ExpressionObserver(data, "Foo");
var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x));
Assert.Equal(
new[]
{
new BindingNotification(
new AggregateException(new NotSupportedException()),
BindingErrorType.Error)
},
result);
}
}
[Fact]
public void Should_Get_Simple_Task_Value_With_Data_DataValidation_Enabled()
{
using (var sync = UnitTestSynchronizationContext.Begin())
{
var tcs = new TaskCompletionSource<string>();
var data = new { Foo = tcs.Task };
var target = new ExpressionObserver(data, "Foo", true);
var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x));
tcs.SetResult("foo");
sync.ExecutePostedCallbacks();
// What does it mean to have data validation on a Task? Without a use-case it's
// hard to know what to do here so for the moment the value is returned.
Assert.Equal(new [] { "foo" }, result);
}
}
private Task TaskFromException(Exception e)
{
var tcs = new TaskCompletionSource<object>();
tcs.SetException(e);
return tcs.Task;
}
private class Class1 : NotifyingBase
{
public Class1(Task<Class2> next)

129
tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Validation.cs

@ -1,129 +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;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reactive.Linq;
using System.Runtime.CompilerServices;
using Avalonia.Data;
using Avalonia.Markup.Data;
using Xunit;
namespace Avalonia.Markup.UnitTests.Data
{
public class ExpressionObserverTests_Validation
{
[Fact]
public void Exception_Validation_Sends_ValidationUpdate()
{
var data = new ExceptionTest { MustBePositive = 5 };
var observer = new ExpressionObserver(data, nameof(data.MustBePositive), false);
var validationMessageFound = false;
observer.Where(o => o is IValidationStatus).Subscribe(_ => validationMessageFound = true);
observer.SetValue(-5);
Assert.True(validationMessageFound);
}
[Fact]
public void Disabled_Indei_Validation_Does_Not_Subscribe()
{
var data = new IndeiTest { MustBePositive = 5 };
var observer = new ExpressionObserver(data, nameof(data.MustBePositive), false);
observer.Subscribe(_ => { });
Assert.Equal(0, data.SubscriptionCount);
}
[Fact]
public void Enabled_Indei_Validation_Subscribes()
{
var data = new IndeiTest { MustBePositive = 5 };
var observer = new ExpressionObserver(data, nameof(data.MustBePositive), true);
var sub = observer.Subscribe(_ => { });
Assert.Equal(1, data.SubscriptionCount);
sub.Dispose();
Assert.Equal(0, data.SubscriptionCount);
}
public class ExceptionTest : INotifyPropertyChanged
{
private int _mustBePositive;
public int MustBePositive
{
get { return _mustBePositive; }
set
{
if (value <= 0)
{
throw new ArgumentOutOfRangeException(nameof(value));
}
_mustBePositive = value;
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
private class IndeiTest : INotifyDataErrorInfo
{
private int _mustBePositive;
private Dictionary<string, IList<string>> _errors = new Dictionary<string, IList<string>>();
private EventHandler<DataErrorsChangedEventArgs> _errorsChanged;
public int MustBePositive
{
get { return _mustBePositive; }
set
{
if (value >= 0)
{
_mustBePositive = value;
_errors.Remove(nameof(MustBePositive));
_errorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(MustBePositive)));
}
else
{
_errors[nameof(MustBePositive)] = new[] { "Must be positive" };
_errorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(MustBePositive)));
}
}
}
public bool HasErrors => _mustBePositive >= 0;
public int SubscriptionCount { get; private set; }
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged
{
add
{
_errorsChanged += value;
++SubscriptionCount;
}
remove
{
_errorsChanged -= value;
--SubscriptionCount;
}
}
public IEnumerable GetErrors(string propertyName)
{
IList<string> result;
_errors.TryGetValue(propertyName, out result);
return result;
}
}
}
}

198
tests/Avalonia.Markup.UnitTests/Data/ExpressionSubjectTests.cs

@ -1,198 +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.ComponentModel;
using System.Globalization;
using System.Reactive.Linq;
using Moq;
using Avalonia.Data;
using Avalonia.Markup.Data;
using Xunit;
using System.Threading;
namespace Avalonia.Markup.UnitTests.Data
{
public class ExpressionSubjectTests
{
[Fact]
public async void Should_Get_Simple_Property_Value()
{
var data = new Class1 { StringValue = "foo" };
var target = new ExpressionSubject(new ExpressionObserver(data, "StringValue"), typeof(string));
var result = await target.Take(1);
Assert.Equal("foo", result);
}
[Fact]
public void Should_Set_Simple_Property_Value()
{
var data = new Class1 { StringValue = "foo" };
var target = new ExpressionSubject(new ExpressionObserver(data, "StringValue"), typeof(string));
target.OnNext("bar");
Assert.Equal("bar", data.StringValue);
}
[Fact]
public async void Should_Convert_Get_String_To_Double()
{
Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture;
var data = new Class1 { StringValue = "5.6" };
var target = new ExpressionSubject(new ExpressionObserver(data, "StringValue"), typeof(double));
var result = await target.Take(1);
Assert.Equal(5.6, result);
}
[Fact]
public async void Getting_Invalid_Double_String_Should_Return_BindingError()
{
var data = new Class1 { StringValue = "foo" };
var target = new ExpressionSubject(new ExpressionObserver(data, "StringValue"), typeof(double));
var result = await target.Take(1);
Assert.IsType<BindingError>(result);
}
[Fact]
public async void Should_Coerce_Get_Null_Double_String_To_UnsetValue()
{
var data = new Class1 { StringValue = null };
var target = new ExpressionSubject(new ExpressionObserver(data, "StringValue"), typeof(double));
var result = await target.Take(1);
Assert.Equal(AvaloniaProperty.UnsetValue, result);
}
[Fact]
public void Should_Convert_Set_String_To_Double()
{
Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture;
var data = new Class1 { StringValue = (5.6).ToString() };
var target = new ExpressionSubject(new ExpressionObserver(data, "StringValue"), typeof(double));
target.OnNext(6.7);
Assert.Equal((6.7).ToString(), data.StringValue);
}
[Fact]
public async void Should_Convert_Get_Double_To_String()
{
Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture;
var data = new Class1 { DoubleValue = 5.6 };
var target = new ExpressionSubject(new ExpressionObserver(data, "DoubleValue"), typeof(string));
var result = await target.Take(1);
Assert.Equal((5.6).ToString(), result);
}
[Fact]
public void Should_Convert_Set_Double_To_String()
{
Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture;
var data = new Class1 { DoubleValue = 5.6 };
var target = new ExpressionSubject(new ExpressionObserver(data, "DoubleValue"), typeof(string));
target.OnNext("6.7");
Assert.Equal(6.7, data.DoubleValue);
}
[Fact]
public void Setting_Invalid_Double_String_Should_Not_Change_Target()
{
var data = new Class1 { DoubleValue = 5.6 };
var target = new ExpressionSubject(new ExpressionObserver(data, "DoubleValue"), typeof(string));
target.OnNext("foo");
Assert.Equal(5.6, data.DoubleValue);
}
[Fact]
public void Setting_Invalid_Double_String_Should_Use_FallbackValue()
{
var data = new Class1 { DoubleValue = 5.6 };
var target = new ExpressionSubject(
new ExpressionObserver(data, "DoubleValue"),
typeof(string),
"9.8",
DefaultValueConverter.Instance);
target.OnNext("foo");
Assert.Equal(9.8, data.DoubleValue);
}
[Fact]
public void Should_Coerce_Setting_Null_Double_To_Default_Value()
{
var data = new Class1 { DoubleValue = 5.6 };
var target = new ExpressionSubject(new ExpressionObserver(data, "DoubleValue"), typeof(string));
target.OnNext(null);
Assert.Equal(0, data.DoubleValue);
}
[Fact]
public void Should_Coerce_Setting_UnsetValue_Double_To_Default_Value()
{
var data = new Class1 { DoubleValue = 5.6 };
var target = new ExpressionSubject(new ExpressionObserver(data, "DoubleValue"), typeof(string));
target.OnNext(AvaloniaProperty.UnsetValue);
Assert.Equal(0, data.DoubleValue);
}
[Fact]
public void Should_Pass_ConverterParameter_To_Convert()
{
var data = new Class1 { DoubleValue = 5.6 };
var converter = new Mock<IValueConverter>();
var target = new ExpressionSubject(
new ExpressionObserver(data, "DoubleValue"),
typeof(string),
converter.Object,
converterParameter: "foo");
target.Subscribe(_ => { });
converter.Verify(x => x.Convert(5.6, typeof(string), "foo", CultureInfo.CurrentUICulture));
}
[Fact]
public void Should_Pass_ConverterParameter_To_ConvertBack()
{
var data = new Class1 { DoubleValue = 5.6 };
var converter = new Mock<IValueConverter>();
var target = new ExpressionSubject(
new ExpressionObserver(data, "DoubleValue"),
typeof(string),
converter.Object,
converterParameter: "foo");
target.OnNext("bar");
converter.Verify(x => x.ConvertBack("bar", typeof(double), "foo", CultureInfo.CurrentUICulture));
}
private class Class1 : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public string StringValue { get; set; }
public double DoubleValue { get; set; }
}
}
}

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save