Skip to content

Commit

Permalink
Validation: Expose internal APIs (#5340)
Browse files Browse the repository at this point in the history
* Expose internal validation APIs

* Release notes

* Use validation argument as an extra event args info
  • Loading branch information
stsrki authored Mar 6, 2024
1 parent f05a19e commit 57e6547
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -194,4 +194,12 @@
Defining the font size follows the same pattern as all out fluent-based utilties, eg. <Code>TextSize="TextSize.IsLarge.OnMobile.IsSmall.OnDesktop"</Code>.
</Paragraph>

<Heading Size="HeadingSize.Is3">
Validation API
</Heading>

<Paragraph>
To make it easier to customize Blazorise behavior, we have exposed some internal <Code>Validation</Code>, and <Code>Validations</Code> component APIs and made them public. This will enable more customization to your forms and make it possible to implement new validation methods.
</Paragraph>

<NewsPagePostInfo UserName="Mladen Macanović" ImageName="mladen" PostedOn="February 29th, 2024" Read="9 min" />
48 changes: 44 additions & 4 deletions Source/Blazorise/Components/Validation/Validation.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,16 @@ public void Dispose()
cancellationTokenSource = null;
}

internal async Task InitializeInput( IValidationInput inputComponent )
/// <summary>
/// Initializes the input component with the specified validation input. It performs an initial validation
/// if the mode is set to auto validation and validation on load is enabled.
/// </summary>
/// <param name="inputComponent">The validation input component to initialize.</param>
/// <remarks>
/// This method sets the input component and, based on the configuration, may asynchronously validate
/// the component's validation value. It also marks the component as initialized.
/// </remarks>
public async Task InitializeInput( IValidationInput inputComponent )
{
this.inputComponent = inputComponent;

Expand All @@ -110,7 +119,18 @@ internal async Task InitializeInput( IValidationInput inputComponent )
initialized = true;
}

internal async Task InitializeInputPattern<T>( string patternString, T value )
/// <summary>
/// Initializes or updates the input validation pattern. If the pattern string changes,
/// a new regex pattern is created, and the input is re-validated if applicable.
/// </summary>
/// <typeparam name="T">The type of the value to validate against the pattern.</typeparam>
/// <param name="patternString">The regex pattern string for validation.</param>
/// <param name="value">The current value of the input to validate against the new pattern.</param>
/// <remarks>
/// This method optimizes performance by avoiding re-instantiation of the regex pattern if it has not changed.
/// It ensures that validation is only re-triggered if the component has been initialized and validation conditions are met.
/// </remarks>
public async Task InitializeInputPattern<T>( string patternString, T value )
{
if ( !string.IsNullOrEmpty( patternString ) )
{
Expand All @@ -133,7 +153,16 @@ internal async Task InitializeInputPattern<T>( string patternString, T value )
}
}

internal async Task InitializeInputExpression<T>( Expression<Func<T>> expression )
/// <summary>
/// Initializes or updates the input based on a specified expression. This is primarily used for data-annotation validation.
/// </summary>
/// <typeparam name="T">The type of the model the expression evaluates to.</typeparam>
/// <param name="expression">The expression used to identify the field for validation.</param>
/// <remarks>
/// This method is designed to work with models and edit contexts, allowing for dynamic validation based on expressions.
/// It ensures that validation is only re-triggered if the component has been initialized and validation conditions are met.
/// </remarks>
public async Task InitializeInputExpression<T>( Expression<Func<T>> expression )
{
// Data-Annotation validation can only work if parent validationa and expression are defined.
if ( ( ParentValidations is not null || EditContext is not null ) && expression is not null )
Expand Down Expand Up @@ -162,7 +191,18 @@ internal async Task InitializeInputExpression<T>( Expression<Func<T>> expression
}
}

internal Task NotifyInputChanged<T>( T newExpressionValue, bool overrideNewValue = false )
/// <summary>
/// Notifies that an input's value has changed and optionally re-validates the input.
/// </summary>
/// <typeparam name="T">The type of the new value.</typeparam>
/// <param name="newExpressionValue">The new value of the expression.</param>
/// <param name="overrideNewValue">Determines whether to use the new value for validation or the current input component's validation value.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
/// <remarks>
/// If the edit context is set and a field identifier has been established, this method notifies the edit context
/// of the field change. It then conditionally triggers validation based on the component's mode.
/// </remarks>
public Task NotifyInputChanged<T>( T newExpressionValue, bool overrideNewValue = false )
{
var newValidationValue = overrideNewValue
? newExpressionValue
Expand Down
44 changes: 32 additions & 12 deletions Source/Blazorise/Components/Validation/Validations.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,13 @@ public async Task<bool> ValidateAll()

if ( result )
{
RaiseStatusChanged( ValidationStatus.Success, null );
RaiseStatusChanged( ValidationStatus.Success, null, null );

await InvokeAsync( () => ValidatedAll.InvokeAsync() );
}
else if ( HasFailedValidations )
{
RaiseStatusChanged( ValidationStatus.Error, FailedValidations );
RaiseStatusChanged( ValidationStatus.Error, FailedValidations, null );
}

return result;
Expand All @@ -84,7 +84,7 @@ public Task ClearAll()
{
ClearingAll?.Invoke();

RaiseStatusChanged( ValidationStatus.None, null );
RaiseStatusChanged( ValidationStatus.None, null, null );

return Task.CompletedTask;
}
Expand All @@ -102,23 +102,41 @@ private async Task<bool> TryValidateAll()
return validated;
}

internal void NotifyValidationInitialized( IValidation validation )
/// <summary>
/// Notifies the validation system that a new validation component has been initialized and adds it to the list of validations if not already present.
/// </summary>
/// <param name="validation">The validation component to add.</param>
public void NotifyValidationInitialized( IValidation validation )
{
if ( !validations.Contains( validation ) )
{
validations.Add( validation );
}
}

internal void NotifyValidationRemoved( IValidation validation )
/// <summary>
/// Notifies the validation system that a validation component is being removed and removes it from the list of validations if present.
/// </summary>
/// <param name="validation">The validation component to remove.</param>
public void NotifyValidationRemoved( IValidation validation )
{
if ( validations.Contains( validation ) )
{
validations.Remove( validation );
}
}

internal void NotifyValidationStatusChanged( IValidation validation )
/// <summary>
/// Notifies the validation system that the status of a validation component has changed. This method handles the logic for updating the overall validation status based on the mode (Auto or Manual) and the current validation results.
/// </summary>
/// <param name="validation">The validation component whose status has changed.</param>
/// <remarks>
/// In Auto mode, this method triggers the aggregation of validation results and potentially raises a status changed event.
/// It is designed to minimize the number of status changed events by aggregating validation results.
/// Special consideration is needed to ensure that the status changed event is raised only once per validation cycle,
/// even if multiple validations fail.
/// </remarks>
public void NotifyValidationStatusChanged( IValidation validation )
{
// Here we need to call ValidatedAll only when in Auto mode. Manual call is already called through ValidateAll()
if ( Mode == ValidationMode.Manual )
Expand All @@ -130,25 +148,27 @@ internal void NotifyValidationStatusChanged( IValidation validation )

if ( AllValidationsSuccessful )
{
RaiseStatusChanged( ValidationStatus.Success, null );
RaiseStatusChanged( ValidationStatus.Success, null, validation );

ValidatedAll.InvokeAsync();
}
else if ( HasFailedValidations )
{
RaiseStatusChanged( ValidationStatus.Error, FailedValidations );
RaiseStatusChanged( ValidationStatus.Error, FailedValidations, validation );
}
else
{
RaiseStatusChanged( ValidationStatus.None, null );
RaiseStatusChanged( ValidationStatus.None, null, validation );
}
}

private void RaiseStatusChanged( ValidationStatus status, IReadOnlyCollection<string> messages )
private void RaiseStatusChanged( ValidationStatus status, IReadOnlyCollection<string> messages, IValidation validation )
{
_StatusChanged?.Invoke( new( status, messages ) );
var eventArgs = new ValidationsStatusChangedEventArgs( status, messages, validation );

_StatusChanged?.Invoke( eventArgs );

InvokeAsync( () => StatusChanged.InvokeAsync( new( status, messages ) ) );
InvokeAsync( () => StatusChanged.InvokeAsync( eventArgs ) );
}

#endregion
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ public class ValidationsStatusChangedEventArgs : EventArgs
/// <summary>
/// Gets the default <see cref="ValidationsStatusChangedEventArgs"/>.
/// </summary>
public static new readonly ValidationsStatusChangedEventArgs Empty = new( ValidationStatus.None, null );
public static new readonly ValidationsStatusChangedEventArgs Empty = new( ValidationStatus.None, null, null );

/// <summary>
/// A default <see cref="ValidationsStatusChangedEventArgs"/> constructor.
/// </summary>
public ValidationsStatusChangedEventArgs( ValidationStatus status, IReadOnlyCollection<string> messages )
public ValidationsStatusChangedEventArgs( ValidationStatus status, IReadOnlyCollection<string> messages, IValidation validation )
{
Status = status;
Messages = messages;
Expand All @@ -33,4 +33,10 @@ public ValidationsStatusChangedEventArgs( ValidationStatus status, IReadOnlyColl
/// Gets the custom validation message.
/// </summary>
public IReadOnlyCollection<string> Messages { get; }

/// <summary>
/// Gets the <see cref="IValidation"/> reference that initiated status changed event. If <c>null</c>, it means the
/// raise happened from the parent <see cref="Validations"/> component.
/// </summary>
public IValidation Validation { get; }
}

0 comments on commit 57e6547

Please sign in to comment.