| File: Forms\ValidationMessage.cs | Web Access |
| Project: src\aspnetcore\src\Components\Web\src\Microsoft.AspNetCore.Components.Web.csproj (Microsoft.AspNetCore.Components.Web) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Linq.Expressions; using Microsoft.AspNetCore.Components.Rendering; namespace Microsoft.AspNetCore.Components.Forms; /// <summary> /// Displays a list of validation messages for a specified field within a cascaded <see cref="EditContext"/>. /// </summary> public class ValidationMessage<TValue> : ComponentBase, IDisposable { private EditContext? _previousEditContext; private Expression<Func<TValue>>? _previousFieldAccessor; private readonly EventHandler<ValidationStateChangedEventArgs>? _validationStateChangedHandler; private FieldIdentifier _fieldIdentifier; /// <summary> /// Gets or sets a collection of additional attributes that will be applied to the created <c>div</c> element. /// </summary> [Parameter(CaptureUnmatchedValues = true)] public IReadOnlyDictionary<string, object>? AdditionalAttributes { get; set; } [CascadingParameter] EditContext CurrentEditContext { get; set; } = default!; [CascadingParameter] private HtmlFieldPrefix? FieldPrefix { get; set; } /// <summary> /// Specifies the field for which validation messages should be displayed. /// </summary> [Parameter] public Expression<Func<TValue>>? For { get; set; } /// <summary>` /// Constructs an instance of <see cref="ValidationMessage{TValue}"/>. /// </summary> public ValidationMessage() { _validationStateChangedHandler = (sender, eventArgs) => StateHasChanged(); } /// <inheritdoc /> protected override void OnParametersSet() { if (CurrentEditContext == null) { throw new InvalidOperationException($"{GetType()} requires a cascading parameter " + $"of type {nameof(EditContext)}. For example, you can use {GetType()} inside " + $"an {nameof(EditForm)}."); } if (For == null) // Not possible except if you manually specify T { throw new InvalidOperationException($"{GetType()} requires a value for the " + $"{nameof(For)} parameter."); } else if (For != _previousFieldAccessor) { _fieldIdentifier = FieldIdentifier.Create(For); _previousFieldAccessor = For; } if (CurrentEditContext != _previousEditContext) { DetachValidationStateChangedListener(); CurrentEditContext.OnValidationStateChanged += _validationStateChangedHandler; _previousEditContext = CurrentEditContext; } } /// <inheritdoc /> protected override void BuildRenderTree(RenderTreeBuilder builder) { if (AssignedRenderMode is not null) { // Interactive .NET validation RenderInteractiveRenderTree(builder); } else { // Client-side JS validation RenderStaticRenderTree(builder); } } private void RenderInteractiveRenderTree(RenderTreeBuilder builder) { foreach (var message in CurrentEditContext.GetValidationMessages(_fieldIdentifier)) { builder.OpenElement(0, "div"); builder.AddAttribute(1, "class", "validation-message"); builder.AddMultipleAttributes(2, AdditionalAttributes); builder.AddContent(3, message); builder.CloseElement(); } } private void RenderStaticRenderTree(RenderTreeBuilder builder) { var fieldName = FieldPrefix?.GetFieldName(For!) ?? ExpressionFormatter.FormatLambda(For!); var first = true; foreach (var message in CurrentEditContext.GetValidationMessages(_fieldIdentifier)) { builder.OpenElement(0, "div"); builder.AddAttribute(1, "class", "validation-message"); if (first) { // First message element carries data-valmsg-for/replace so the JS client-validation // engine can locate it and swap its content with client-side messages. builder.AddAttribute(2, "data-valmsg-for", fieldName); builder.AddAttribute(3, "data-valmsg-replace", "true"); first = false; } builder.AddMultipleAttributes(4, AdditionalAttributes); builder.AddContent(5, message); builder.CloseElement(); } if (first) { // No server-side messages: render a hidden placeholder so the JS engine has an element // to populate when client validation runs. builder.OpenElement(0, "div"); builder.AddAttribute(1, "class", "validation-message"); builder.AddAttribute(2, "data-valmsg-for", fieldName); builder.AddAttribute(3, "data-valmsg-replace", "true"); // Use the hidden attribute rather than an inline style="display:none" for CSP compliance. builder.AddAttribute(4, "hidden", true); builder.AddMultipleAttributes(5, AdditionalAttributes); builder.CloseElement(); } } /// <summary> /// Called to dispose this instance. /// </summary> /// <param name="disposing"><see langword="true"/> if called within <see cref="IDisposable.Dispose"/>.</param> protected virtual void Dispose(bool disposing) { } void IDisposable.Dispose() { DetachValidationStateChangedListener(); Dispose(disposing: true); } private void DetachValidationStateChangedListener() { _previousEditContext?.OnValidationStateChanged -= _validationStateChangedHandler; } }