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