committed by
GitHub
115 changed files with 4343 additions and 2178 deletions
@ -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; } |
|||
} |
|||
} |
|||
@ -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))); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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> |
|||
@ -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> |
|||
@ -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."; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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; } |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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; } |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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)); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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> |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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))); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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>; |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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>(); |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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(); } |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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…
Reference in new issue