ant-design-blazor/components/core/Base/AntInputComponentBase.cs

311 lines
11 KiB
C#
Raw Normal View History

using System;
2019-12-08 00:51:07 +08:00
using System.Collections.Generic;
using System.Globalization;
2019-12-08 00:51:07 +08:00
using System.Linq.Expressions;
using System.Threading.Tasks;
using AntDesign.Forms;
using AntDesign.Internal;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
2019-12-08 00:51:07 +08:00
namespace AntDesign
2019-12-08 00:51:07 +08:00
{
/// <summary>
/// Base class for any input control that optionally supports an <see cref="EditContext"/>.
/// reference:https://github.com/dotnet/aspnetcore/blob/master/src/Components/Web/src/Forms/InputBase.cs
2019-12-08 00:51:07 +08:00
/// </summary>
/// <typeparam name="TValue">the natural type of the input's value</typeparam>
public abstract class AntInputComponentBase<TValue> : AntDomComponentBase, IControlValueAccessor
2019-12-08 00:51:07 +08:00
{
private readonly EventHandler<ValidationStateChangedEventArgs> _validationStateChangedHandler;
private bool _previousParsingAttemptFailed;
private ValidationMessageStore _parsingValidationMessages;
private Type _nullableUnderlyingType;
2019-12-08 00:51:07 +08:00
[CascadingParameter(Name = "FormItem")]
2020-06-03 20:29:28 +08:00
private IFormItem FormItem { get; set; }
[CascadingParameter(Name = "Form")]
protected IForm Form { get; set; }
public string[] ValidationMessages { get; set; } = Array.Empty<string>();
private string _formSize;
[CascadingParameter(Name = "FormSize")]
public string FormSize
{
get
{
return _formSize;
}
set
{
_formSize = value;
Size = value;
}
}
/// <summary>
/// Gets or sets a collection of additional attributes that will be applied to the created element.
/// </summary>
[Parameter(CaptureUnmatchedValues = true)]
public IReadOnlyDictionary<string, object> AdditionalAttributes { get; set; }
private TValue _value;
/// <summary>
/// Gets or sets the value of the input. This should be used with two-way binding.
/// </summary>
/// <example>
/// @bind-Value="model.PropertyName"
/// </example>
2019-12-08 00:51:07 +08:00
[Parameter]
public virtual TValue Value
{
get { return _value; }
set
{
var hasChanged = !EqualityComparer<TValue>.Default.Equals(value, Value);
if (hasChanged)
{
_value = value;
OnValueChange(value);
}
}
}
/// <summary>
/// Gets or sets a callback that updates the bound value.
/// </summary>
[Parameter]
public virtual EventCallback<TValue> ValueChanged { get; set; }
/// <summary>
/// Gets or sets an expression that identifies the bound value.
/// </summary>
[Parameter]
public Expression<Func<TValue>> ValueExpression { get; set; }
[Parameter]
public string Size { get; set; } = AntSizeLDSType.Default;
/// <summary>
/// Gets the associated <see cref="EditContext"/>.
/// </summary>
protected EditContext EditContext { get; set; }
/// <summary>
/// Gets the <see cref="FieldIdentifier"/> for the bound value.
/// </summary>
internal FieldIdentifier FieldIdentifier { get; set; }
/// <summary>
/// Gets or sets the current value of the input.
/// </summary>
protected TValue CurrentValue
2019-12-08 00:51:07 +08:00
{
get => Value;
2019-12-08 00:51:07 +08:00
set
{
var hasChanged = !EqualityComparer<TValue>.Default.Equals(value, Value);
if (hasChanged)
2019-12-08 00:51:07 +08:00
{
Value = value;
ValueChanged.InvokeAsync(value);
if (_isNotifyFieldChanged)
{
EditContext?.NotifyFieldChanged(FieldIdentifier);
}
2019-12-08 00:51:07 +08:00
}
}
}
/// <summary>
/// Gets or sets the current value of the input, represented as a string.
2019-12-08 00:51:07 +08:00
/// </summary>
protected string CurrentValueAsString
{
get => FormatValueAsString(CurrentValue);
set
{
_parsingValidationMessages?.Clear();
2019-12-08 00:51:07 +08:00
bool parsingFailed;
2019-12-08 00:51:07 +08:00
if (_nullableUnderlyingType != null && string.IsNullOrEmpty(value))
{
// Assume if it's a nullable type, null/empty inputs should correspond to default(T)
// Then all subclasses get nullable support almost automatically (they just have to
// not reject Nullable<T> based on the type itself).
parsingFailed = false;
CurrentValue = default;
}
else if (TryParseValueFromString(value, out var parsedValue, out var validationErrorMessage))
{
parsingFailed = false;
CurrentValue = parsedValue;
}
else
{
parsingFailed = true;
2020-06-03 20:29:28 +08:00
if (EditContext != null)
{
2020-06-03 20:29:28 +08:00
if (_parsingValidationMessages == null)
{
_parsingValidationMessages = new ValidationMessageStore(EditContext);
}
2020-06-03 20:29:28 +08:00
_parsingValidationMessages.Add(FieldIdentifier, validationErrorMessage);
2020-06-03 20:29:28 +08:00
// Since we're not writing to CurrentValue, we'll need to notify about modification from here
EditContext.NotifyFieldChanged(FieldIdentifier);
}
}
// We can skip the validation notification if we were previously valid and still are
2020-06-03 20:29:28 +08:00
if ((parsingFailed || _previousParsingAttemptFailed) && EditContext != null)
{
EditContext.NotifyValidationStateChanged();
_previousParsingAttemptFailed = parsingFailed;
}
}
}
2019-12-08 00:51:07 +08:00
private TValue _firstValue;
private bool _isNotifyFieldChanged = true;
2019-12-08 00:51:07 +08:00
/// <summary>
/// Constructs an instance of <see cref="InputBase{TValue}"/>.
/// </summary>
protected AntInputComponentBase()
{
_validationStateChangedHandler = (sender, eventArgs) => StateHasChanged();
}
2019-12-08 00:51:07 +08:00
/// <summary>
/// Formats the value as a string. Derived classes can override this to determine the formating used for <see cref="CurrentValueAsString"/>.
2019-12-08 00:51:07 +08:00
/// </summary>
/// <param name="value">The value to format.</param>
/// <returns>A string representation of the value.</returns>
protected virtual string FormatValueAsString(TValue value)
=> value?.ToString();
2019-12-08 00:51:07 +08:00
/// <summary>
/// Parses a string to create an instance of <typeparamref name="TValue"/>. Derived classes can override this to change how
/// <see cref="CurrentValueAsString"/> interprets incoming values.
2019-12-08 00:51:07 +08:00
/// </summary>
/// <param name="value">The string value to be parsed.</param>
/// <param name="result">An instance of <typeparamref name="TValue"/>.</param>
/// <param name="validationErrorMessage">If the value could not be parsed, provides a validation error message.</param>
/// <returns>True if the value could be parsed; otherwise false.</returns>
protected virtual bool TryParseValueFromString(string value, out TValue result, out string validationErrorMessage)
{
if (string.IsNullOrWhiteSpace(value))
{
result = default;
validationErrorMessage = null;
return true;
}
var success = BindConverter.TryConvertTo<TValue>(
value, CultureInfo.CurrentCulture, out var parsedValue);
if (success)
{
result = parsedValue;
validationErrorMessage = null;
return true;
}
else
{
result = default;
validationErrorMessage = $"{FieldIdentifier.FieldName} field isn't valid.";
return false;
}
}
2019-12-08 00:51:07 +08:00
protected virtual void OnValueChange(TValue value)
{
}
protected override void OnInitialized()
{
base.OnInitialized();
FormItem?.AddControl(this);
Form?.AddControl(this);
_firstValue = Value;
}
2019-12-08 00:51:07 +08:00
/// <inheritdoc />
public override Task SetParametersAsync(ParameterView parameters)
{
parameters.SetParameterProperties(this);
if (EditContext == null)
2019-12-08 00:51:07 +08:00
{
// This is the first run
// Could put this logic in OnInit, but its nice to avoid forcing people who override OnInit to call base.OnInit()
2019-12-08 00:51:07 +08:00
if (Form?.EditContext == null)
{
return base.SetParametersAsync(ParameterView.Empty);
2019-12-08 00:51:07 +08:00
}
if (ValueExpression == null)
2019-12-08 00:51:07 +08:00
{
return base.SetParametersAsync(ParameterView.Empty);
2019-12-08 00:51:07 +08:00
}
EditContext = Form?.EditContext;
FieldIdentifier = FieldIdentifier.Create(ValueExpression);
_nullableUnderlyingType = Nullable.GetUnderlyingType(typeof(TValue));
EditContext.OnValidationStateChanged += _validationStateChangedHandler;
}
else if (Form?.EditContext != EditContext)
{
// Not the first run
// We don't support changing EditContext because it's messy to be clearing up state and event
// handlers for the previous one, and there's no strong use case. If a strong use case
// emerges, we can consider changing this.
throw new InvalidOperationException($"{GetType()} does not support changing the " +
$"{nameof(EditContext)} dynamically.");
2019-12-08 00:51:07 +08:00
}
// For derived components, retain the usual lifecycle with OnInit/OnParametersSet/etc.
2019-12-08 00:51:07 +08:00
return base.SetParametersAsync(ParameterView.Empty);
}
protected override void Dispose(bool disposing)
{
if (EditContext != null)
{
EditContext.OnValidationStateChanged -= _validationStateChangedHandler;
}
base.Dispose(disposing);
}
internal virtual void ResetValue()
{
_isNotifyFieldChanged = false;
CurrentValue = _firstValue;
_isNotifyFieldChanged = true;
}
void IControlValueAccessor.Reset()
{
ResetValue();
}
2019-12-08 00:51:07 +08:00
}
}