Jakob Lithner
Jakob Lithner

Reputation: 4416

Is it possible to check if Blazor ValidationMessageStore has ANY error message

I validate my Blazor form input according to this pattern with ValidationMessageStore:

https://learn.microsoft.com/en-us/aspnet/core/blazor/forms-validation?view=aspnetcore-5.0#basic-validation-1

I then output ValidationMessage on each control.

BUT it is a long form so I also want to indicate to the user somewhere close to the submit button that there are some errors that need to be fixed, and that's why we didn't accept the input yet.

I know I can use a ValidationSummary but I don't want to repeat all possible errors, just have a note.

ValidationMessageStore obviously holds all messages in an internal collection, but they are not accessible. Is it possible to somehow check if there are ANY error messages?

Upvotes: 5

Views: 5079

Answers (3)

Parker
Parker

Reputation: 31

I had almost the same question. Based off of Jakob Lithner's answer, here is the solution I came up with.

In order to access whether or not the ValidationSummary has any error messages, you can bind the EditForm to an EditContext instead of the Model. This way you can directly reference the context programmatically. Here is a simple example using a model object and razor file that will display validation messages under each form that is validated, and will show a general error message if either form is invalid.

ExampleModel.cs

using System.ComponentModel.DataAnnotations;

namespace Example.Pages;

public class ExampleModel
{
    [Required(ErrorMessage = "The object must have a name")]
    [StringLength(100, ErrorMessage = "Object names cannot exceed 100 characters")]
    public string Name { get; set; } = "New Example";
    [StringLength(1000, ErrorMessage = "Description cannot exceed 1000 characters")]
    public string Description { get; set; }
}

ExamplePage.razor

@page "/ExamplePage"

<EditForm EditContext="@EditContext" OnValidSubmit="@HandleValidSubmit">
    <DataAnnotationsValidator/>

    @* Where the edit context is checked for validation messages. *@
    @* This can go anywhere you want it. *@
    @* An alternative to "alert alert-danger" is "validation-message", 
        which contains the style for validation messages *@
    @if (EditContext is not null && EditContext.GetValidationMessages().Any())
    {
        <p class="alert alert-danger text-center">
            One or more errors must be fixed before changes can be saved
        </p>
    }
    
    <div class="mb-3">
        <div class="input-group">
            <span class="input-group-text">Name</span>
            <InputText class="form-control" @bind-Value="ExampleModel.Name"/>
        </div>
        <ValidationMessage For="@(() => ExampleModel.Name)"/>
    </div>

    <div class="mb-3">
        <label class="form-label" for="queryDescription">Object Description</label>
        <InputTextArea 
            class="form-control" 
            id="queryDescription" rows="3" 
            @bind-Value="ExampleModel.Description"/>
        <ValidationMessage For="() => ExampleModel.Description"/>
    </div>

    <div class="btn-group">
        <a class="btn btn-warning" href="/ExamplePage">Cancel</a>
        @* By signifying the type as submit, this is the button that triggers
            the validation event *@
        <button class="btn btn-success" type="submit">Create</button>
    </div>
</EditForm>

@code {
    private ExampleModel ExampleModel { get; set; } = new();
    private EditContext? EditContext { get; set; }

    protected override Task OnInitializedAsync()
    {
        // This binds the Model to the Context
        EditContext = new EditContext(ExampleModel);
        return Task.CompletedTask;
    }

    private void HandleValidSubmit()
    {
        // Process the valid form
    }
}

See the form and validation documentation section on Binding a form for more details on how to use the Context instead of the Model for the EditForm.

Upvotes: 1

Jakob Lithner
Jakob Lithner

Reputation: 4416

I found a simpler solution for my problem. On the EditContext I found a method called GetValidationMessages.

@if (editContext.GetValidationMessages().Any())
{
    <div class="alert alert-danger">
        Some input was incomplete. Please review detailed messages above.
    </div>
}

Upvotes: 7

MrC aka Shaun Curtis
MrC aka Shaun Curtis

Reputation: 30074

Take a look at the ValidationSummary code - the validation message store is available. It's not very complicated, so you should be able to build yourself a similar but simpler component to display what you want.

The code is here: https://github.com/dotnet/aspnetcore/blob/main/src/Components/Web/src/Forms/ValidationSummary.cs

// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Components.Rendering;

namespace Microsoft.AspNetCore.Components.Forms
{
    // Note: there's no reason why developers strictly need to use this. It's equally valid to
    // put a @foreach(var message in context.GetValidationMessages()) { ... } inside a form.
    // This component is for convenience only, plus it implements a few small perf optimizations.

    /// <summary>
    /// Displays a list of validation messages from a cascaded <see cref="EditContext"/>.
    /// </summary>
    public class ValidationSummary : ComponentBase, IDisposable
    {
        private EditContext? _previousEditContext;
        private readonly EventHandler<ValidationStateChangedEventArgs> _validationStateChangedHandler;

        /// <summary>
        /// Gets or sets the model to produce the list of validation messages for.
        /// When specified, this lists all errors that are associated with the model instance.
        /// </summary>
        [Parameter] public object? Model { get; set; }

        /// <summary>
        /// Gets or sets a collection of additional attributes that will be applied to the created <c>ul</c> element.
        /// </summary>
        [Parameter(CaptureUnmatchedValues = true)] public IReadOnlyDictionary<string, object>? AdditionalAttributes { get; set; }

        [CascadingParameter] EditContext CurrentEditContext { get; set; } = default!;

        /// <summary>`
        /// Constructs an instance of <see cref="ValidationSummary"/>.
        /// </summary>
        public ValidationSummary()
        {
            _validationStateChangedHandler = (sender, eventArgs) => StateHasChanged();
        }

        /// <inheritdoc />
        protected override void OnParametersSet()
        {
            if (CurrentEditContext == null)
            {
                throw new InvalidOperationException($"{nameof(ValidationSummary)} requires a cascading parameter " +
                    $"of type {nameof(EditContext)}. For example, you can use {nameof(ValidationSummary)} inside " +
                    $"an {nameof(EditForm)}.");
            }

            if (CurrentEditContext != _previousEditContext)
            {
                DetachValidationStateChangedListener();
                CurrentEditContext.OnValidationStateChanged += _validationStateChangedHandler;
                _previousEditContext = CurrentEditContext;
            }
        }

        /// <inheritdoc />
        protected override void BuildRenderTree(RenderTreeBuilder builder)
        {
            // As an optimization, only evaluate the messages enumerable once, and
            // only produce the enclosing <ul> if there's at least one message
            var validationMessages = Model is null ?
                CurrentEditContext.GetValidationMessages() :
                CurrentEditContext.GetValidationMessages(new FieldIdentifier(Model, string.Empty));

            var first = true;
            foreach (var error in validationMessages)
            {
                if (first)
                {
                    first = false;

                    builder.OpenElement(0, "ul");
                    builder.AddMultipleAttributes(1, AdditionalAttributes);
                    builder.AddAttribute(2, "class", "validation-errors");
                }

                builder.OpenElement(3, "li");
                builder.AddAttribute(4, "class", "validation-message");
                builder.AddContent(5, error);
                builder.CloseElement();
            }

            if (!first)
            {
                // We have at least one validation message.
                builder.CloseElement();
            }
        }

        /// <inheritdoc/>
        protected virtual void Dispose(bool disposing)
        {
        }

        void IDisposable.Dispose()
        {
            DetachValidationStateChangedListener();
            Dispose(disposing: true);
        }

        private void DetachValidationStateChangedListener()
        {
            if (_previousEditContext != null)
            {
                _previousEditContext.OnValidationStateChanged -= _validationStateChangedHandler;
            }
        }
    }
}

If you need more help in building the component add some more detail of what you want to the question.

Upvotes: 1

Related Questions