using System; using System.Collections.Generic; using System.Globalization; using System.Linq.Expressions; using System.Threading.Tasks; using AntDesign.Forms; using AntDesign.Internal; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Forms; namespace AntDesign { /// /// Base class for any input control that optionally supports an . /// reference:https://github.com/dotnet/aspnetcore/blob/master/src/Components/Web/src/Forms/InputBase.cs /// /// the natural type of the input's value public abstract class AntInputComponentBase : AntDomComponentBase, IControlValueAccessor { private readonly EventHandler _validationStateChangedHandler; private bool _previousParsingAttemptFailed; private ValidationMessageStore _parsingValidationMessages; private Type _nullableUnderlyingType; [CascadingParameter(Name = "FormItem")] private IFormItem FormItem { get; set; } [CascadingParameter(Name = "Form")] protected IForm Form { get; set; } public string[] ValidationMessages { get; set; } = Array.Empty(); private string _formSize; [CascadingParameter(Name = "FormSize")] public string FormSize { get { return _formSize; } set { _formSize = value; Size = value; } } /// /// Gets or sets a collection of additional attributes that will be applied to the created element. /// [Parameter(CaptureUnmatchedValues = true)] public IReadOnlyDictionary AdditionalAttributes { get; set; } private TValue _value; /// /// Gets or sets the value of the input. This should be used with two-way binding. /// /// /// @bind-Value="model.PropertyName" /// [Parameter] public virtual TValue Value { get { return _value; } set { var hasChanged = !EqualityComparer.Default.Equals(value, Value); if (hasChanged) { _value = value; OnValueChange(value); } } } /// /// Gets or sets a callback that updates the bound value. /// [Parameter] public virtual EventCallback ValueChanged { get; set; } /// /// Gets or sets an expression that identifies the bound value. /// [Parameter] public Expression> ValueExpression { get; set; } [Parameter] public Expression>> ValuesExpression { get; set; } /// /// The size of the input box. Note: in the context of a form, /// the `large` size is used. Available: `large` `default` `small` /// [Parameter] public string Size { get; set; } = AntSizeLDSType.Default; /// /// What Culture will be used when converting string to value and value to string /// Useful for InputNumber component. /// [Parameter] public virtual CultureInfo CultureInfo { get; set; } = CultureInfo.CurrentCulture; /// /// Gets the associated . /// protected EditContext EditContext { get; set; } /// /// Gets the for the bound value. /// internal FieldIdentifier FieldIdentifier { get; set; } /// /// Gets or sets the current value of the input. /// protected TValue CurrentValue { get => Value; set { var hasChanged = !EqualityComparer.Default.Equals(value, Value); if (hasChanged) { Value = value; ValueChanged.InvokeAsync(value); if (_isNotifyFieldChanged && (Form?.ValidateOnChange == true)) { EditContext?.NotifyFieldChanged(FieldIdentifier); } } } } /// /// Gets or sets the current value of the input, represented as a string. /// protected string CurrentValueAsString { get => FormatValueAsString(CurrentValue); set { _parsingValidationMessages?.Clear(); bool parsingFailed; 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 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; if (EditContext != null) { if (_parsingValidationMessages == null) { _parsingValidationMessages = new ValidationMessageStore(EditContext); } _parsingValidationMessages.Add(FieldIdentifier, validationErrorMessage); // 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 if ((parsingFailed || _previousParsingAttemptFailed) && EditContext != null) { EditContext.NotifyValidationStateChanged(); _previousParsingAttemptFailed = parsingFailed; } } } private TValue _firstValue; protected bool _isNotifyFieldChanged = true; private bool _isValueGuid; /// /// Constructs an instance of . /// protected AntInputComponentBase() { _validationStateChangedHandler = (sender, eventArgs) => StateHasChanged(); } /// /// Formats the value as a string. Derived classes can override this to determine the formating used for . /// /// The value to format. /// A string representation of the value. protected virtual string FormatValueAsString(TValue value) => value?.ToString(); /// /// Parses a string to create an instance of . Derived classes can override this to change how /// interprets incoming values. /// /// The string value to be parsed. /// An instance of . /// If the value could not be parsed, provides a validation error message. /// True if the value could be parsed; otherwise false. protected virtual bool TryParseValueFromString(string value, out TValue result, out string validationErrorMessage) { if (string.IsNullOrWhiteSpace(value)) { result = default; validationErrorMessage = null; return true; } TValue parsedValue = default; bool success; // BindConverter.TryConvertTo doesn't work for a incomplete Guid fragment. Remove this when the BCL bug is fixed. if (_isValueGuid) { success = Guid.TryParse(value, out Guid parsedGuidValue); if (success) parsedValue = THelper.ChangeType(parsedGuidValue); } else { success = BindConverter.TryConvertTo(value, CultureInfo, out parsedValue); } if (success) { result = parsedValue; validationErrorMessage = null; return true; } else { result = default; validationErrorMessage = $"{FieldIdentifier.FieldName} field isn't valid."; return false; } } /// /// When this method is called, Value is only has been modified, but the ValueChanged is not triggered, so the outside bound Value is not changed. /// /// protected virtual void OnValueChange(TValue value) { } protected override void OnInitialized() { _isValueGuid = THelper.GetUnderlyingType() == typeof(Guid); base.OnInitialized(); FormItem?.AddControl(this); Form?.AddControl(this); _firstValue = Value; } /// public override Task SetParametersAsync(ParameterView parameters) { parameters.SetParameterProperties(this); if (EditContext == null) { // 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() if (Form?.EditContext == null) { return base.SetParametersAsync(ParameterView.Empty); } if (ValueExpression == null && ValuesExpression == null) { return base.SetParametersAsync(ParameterView.Empty); } EditContext = Form?.EditContext; if (ValuesExpression == null) FieldIdentifier = FieldIdentifier.Create(ValueExpression); else FieldIdentifier = FieldIdentifier.Create(ValuesExpression); _nullableUnderlyingType = Nullable.GetUnderlyingType(typeof(TValue)); EditContext.OnValidationStateChanged += _validationStateChangedHandler; } else if (Form?.EditContext != EditContext) { // Not the first run //Be careful when changing this. New EditContext carried from Form should //already have all events transferred from original EditContext. The //transfer is done in Form.BuildEditContext() method. State is lost //though. EditContext = Form?.EditContext; } // For derived components, retain the usual lifecycle with OnInit/OnParametersSet/etc. return base.SetParametersAsync(ParameterView.Empty); } protected override void Dispose(bool disposing) { if (EditContext != null) { EditContext.OnValidationStateChanged -= _validationStateChangedHandler; } Form?.RemoveControl(this); base.Dispose(disposing); } internal virtual void ResetValue() { _isNotifyFieldChanged = false; CurrentValue = _firstValue; _isNotifyFieldChanged = true; } void IControlValueAccessor.Reset() { ResetValue(); } } }