'How to reset custom validation errors when using editform in blazor razor page

I have an editform using an editcontext:

    <EditForm OnValidSubmit="HandleValidSubmit" EditContext="_editContext" Context="auth">
      <DataAnnotationsValidator />
      <input type="time" @bind-value="_foodTruck.EndDelivery" @onkeydown="@(q=>ResetValidation("EndDelivery"))" >
        <ValidationMessage For="() => _foodTruck.EndDelivery" />
      <input type="time" @bind-value="_foodTruck.StartDelivery" @onkeydown="@(q=>ResetValidation("StartDelivery"))" >
        <ValidationMessage For="() => _foodTruck.StartDelivery" />
      <input class="btn btn-default" type="submit" value="save" />
    </EditForm>

I do some custom validations in HandleValidSubmit:

EditContext _editContext = new EditContext(_foodTruck);
private async void HandleValidSubmit()
{
  var messageStore = new ValidationMessageStore(_editContext);
  if (_foodTruck.StartDelivery >= _foodTruck.EndDelivery)
  {
    messageStore.Add(_editContext.Field("EndDelivery"), "Bad time entered");
    _editContext.NotifyValidationStateChanged();
  }
 if (!_editContext.Validate()) return;
}

What now happens is that my custom error ("bad time entered") is displayed at the right position. The only issue is: That error does not disappear when I change the value. So HandleValidSubmit is never called again if I click onto the submit button.

I also tried emptying the validationerrors when modifying the fields:

   protected void ResetValidation(string field)
    {
        var messageStore = new ValidationMessageStore(_editContext);        
        messageStore.Clear(_editContext.Field(field));
        messageStore.Clear();
        _editContext.NotifyValidationStateChanged();
    }

This is called by onkeydown. But that doesn't seem to have an effect, either. The Errormessage does not disappear and so HandleValidSubmit isn't called either.



Solution 1:[1]

I solved this by creating a new EditContext on Validation-reset. So I simply added the following line to the ResetValidation-Method:

  _editContext = new EditContext(_foodTruck);

But to be honest: That does not feel right. So I will leave this open for better answers to come (hopefully).

Solution 2:[2]

I had the same issue as the original poster so I decided to poke around in the source code of the EditContext (thank you source.dot.net!). As a result, I've come up with a work-around that should suffice until the Blazor team resolves the issue properly in a future release.

/// <summary>
/// Contains extension methods for working with the <see cref="EditForm"/> class.
/// </summary>
public static class EditFormExtensions
{
    /// <summary>
    /// Clears all validation messages from the <see cref="EditContext"/> of the given <see cref="EditForm"/>.
    /// </summary>
    /// <param name="editForm">The <see cref="EditForm"/> to use.</param>
    /// <param name="revalidate">
    /// Specifies whether the <see cref="EditContext"/> of the given <see cref="EditForm"/> should revalidate after all validation messages have been cleared.
    /// </param>
    /// <param name="markAsUnmodified">
    /// Specifies whether the <see cref="EditContext"/> of the given <see cref="EditForm"/> should be marked as unmodified.
    /// This will affect the assignment of css classes to a form's input controls in Blazor.
    /// </param>
    /// <remarks>
    /// This extension method should be on EditContext, but EditForm is being used until the fix for issue
    /// <see href="https://github.com/dotnet/aspnetcore/issues/12238"/> is officially released.
    /// </remarks>
    public static void ClearValidationMessages(this EditForm editForm, bool revalidate = false, bool markAsUnmodified = false)
    {
        var bindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;

        object GetInstanceField(Type type, object instance, string fieldName)
        {                
            var fieldInfo = type.GetField(fieldName, bindingFlags);
            return fieldInfo.GetValue(instance);
        }

        var editContext = editForm.EditContext == null
            ? GetInstanceField(typeof(EditForm), editForm, "_fixedEditContext") as EditContext
            : editForm.EditContext;

        var fieldStates = GetInstanceField(typeof(EditContext), editContext, "_fieldStates");
        var clearMethodInfo = typeof(HashSet<ValidationMessageStore>).GetMethod("Clear", bindingFlags);

        foreach (DictionaryEntry kv in (IDictionary)fieldStates)
        {
            var messageStores = GetInstanceField(kv.Value.GetType(), kv.Value, "_validationMessageStores");
            clearMethodInfo.Invoke(messageStores, null);
        }

        if (markAsUnmodified)
            editContext.MarkAsUnmodified();

        if (revalidate)
            editContext.Validate();
    }
}

Solution 3:[3]

Add this.StateHasChanged() at the end of the event action so that it can render the ui elements again and remove the validation message.

EditContext _editContext = new EditContext(_foodTruck);
private async void HandleValidSubmit()
{
  var messageStore = new ValidationMessageStore(_editContext);
  if (_foodTruck.StartDelivery >= _foodTruck.EndDelivery)
  {
    messageStore.Add(_editContext.Field("EndDelivery"), "Bad time entered");
    _editContext.NotifyValidationStateChanged();
     this.StateHasChanged(); //this line
  }
 if (!_editContext.Validate()) return;
}

for the other one

protected void ResetValidation(string field)
{
        var messageStore = new ValidationMessageStore(_editContext);        
        messageStore.Clear(_editContext.Field(field));
        messageStore.Clear();
        _editContext.NotifyValidationStateChanged();
        this.StateHasChanged(); //this line
}

kindly let me know if it works

Solution 4:[4]

I had same problem. I couldn't find straightforward solution. Workaround similar to below worked for me.

Modify EditForm as follows -

<EditForm EditContext="_editContext" OnSubmit="HandleSubmit">

@Code Block

EditContext _editContext;

ValidationMessageStore msgStore;

FoodTruck _foodTruck= new FoodTruck();

protected override void OnInitialized()
{
    _editContext = new EditContext(_foodTruck);
    msgStore = new ValidationMessageStore(_editContext);
}

void HandleSubmit()
{
    msgStore.Clear();
    if(_editContext.Validate()) // <-- Model Validation
    {
        if (_foodTruck.StartDelivery >= _foodTruck.EndDelivery) //<--Custom validation
        {
            msgStore = new ValidationMessageStore(_editContext);
            msgStore.Add(_editContext.Field("EndDelivery"), "Bad time entered");
        }
    }
}

Solution 5:[5]

Had the same issue, solved it in a not-too-hacky way using EditContext.Validate():

I have already implemented a method called EditContext_OnFieldChanged(object sender, FieldChangedEventArgs e) which gets called as soon that a parameter of the model used by the EditForm is used. It´s implemented like this:

protected override void OnInitialized()
{
    EditContext = new EditContext(ModelExample);
    EditContext.OnFieldChanged += EditContext_OnFieldChanged;
}

Here´s the method:

private void EditContext_OnFieldChanged(object sender, FieldChangedEventArgs e)
{
    EditContext.Validate();
    
    // ...
    // other stuff you want to be done when the model changes
}

EditContext.Validate() seems to update all validation messages, even the custom ones.

Solution 6:[6]

The solution for this problem is to call a new EditContext on Validation-reset. The following code will work in the ResetValidation Method:

_editContext = new EditContext(_foodTruck); //Reseting the Context
_editContext.AddDataAnnotationsValidation(); //Enabling subsequent validation calls to work

You can find more details on Custom Data Annotation Validators from the below link, How to create Custom Data Annotation Validators

Solution 7:[7]

This worked for me. On each event OnFieldChange, you can clear the validation message store.

protected override void OnInitialized()
{
    _editContext = new EditContext(genre);
    _msgStore = new ValidationMessageStore(_editContext);
    //_editContext.OnValidationRequested += (s, e) => _msgStore.Clear();
    _editContext.OnFieldChanged += (s, e) => _msgStore.Clear(e.FieldIdentifier);
}

Solution 8:[8]

As this question is still appearing in searches and people are referring to it, this answer explains why the problem exists and shows how to resolve it.

Let's look at the various answers and dispel some urban myths and voodoo:

  1. Resetting the EditContext is not the answer, just a voodoo fix. It breaks more than it fixes: EditContext should never be reset unless you really know what your doing.

  2. Calling StateHasChanged is normally a desperation measure to force the UI to update when basic logic design is flawed. If you have to code in StateHasChanged then you need to seriously ask yourself: Why?

  3. The other answers are hacks. They will work in certain circumstances, but no guarantees in your design.

The root cause of the problem is a misunderstanding of what a ValidationMessageStore is and how to use and manage it.

ValidationMessageStore is a little more complex that first appearances. It isn't a store that holds all the validation messages logged from various sources: _messageStore = new ValidationMessageStore(_editContext); should be a clue, specifically new. You should get your instance when you instantiate the component and then add messages to and clear messages from that instance. Creating one every time you call a method simply doesn't work. You are just creating a new empty ValidationMessageStore.

Here's a working version of the code in the question:

@page "/"

<PageTitle>Index</PageTitle>

@if (loaded)
{
    <EditForm OnValidSubmit="HandleValidSubmit" EditContext="_editContext" Context="auth">
        <DataAnnotationsValidator />
        <div class="p-2">
            End Delivery
            <input type="time" @bind-value="_foodTruck.EndDelivery" @onkeydown="@(()=>ResetValidation("EndDelivery"))">
            <ValidationMessage For="() => _foodTruck.EndDelivery" />
        </div>
        <div class="p-2">
            Start Delivery
            <input type="time" @bind-value="_foodTruck.StartDelivery" @onkeydown="@(()=>ResetValidation("StartDelivery"))">
            <ValidationMessage For="() => _foodTruck.StartDelivery" />

        </div>
        <div class="p-2 text-end">
            <input class="btn btn-primary" type="submit" value="save" />
        </div>
        <div class="p-2 text-end">
            Counter: @counter
        </div>
    </EditForm>
}

@code {
    private FoodTruck _foodTruck = new FoodTruck();
    private EditContext? _editContext;
    private ValidationMessageStore? _messageStore;
    private ValidationMessageStore messageStore => _messageStore!;
    private int counter;
    private bool loaded;

    protected override async Task OnInitializedAsync()
    {
        // emulate gwtting some async data
        await Task.Delay(100);
        FoodTruck _foodTruck = new FoodTruck();
        // assign the mdel data to the Edit Context
        _editContext = new EditContext(_foodTruck);
        // Get the ValidationMessageStore
        _messageStore = new ValidationMessageStore(_editContext);
        loaded = true;
    }

    private void HandleValidSubmit()
    {
        if (_editContext is not null)
        {
            // create a FieldIdentifier for EndDelivery
            var fi = new FieldIdentifier(_foodTruck, "EndDelivery");
            // Clear the specific entry from the message store using the FieldIdentifier
            messageStore.Clear(fi);

            if (_foodTruck.StartDelivery >= _foodTruck.EndDelivery)
            {
                // Add a validation message and raise the validation state change event
                messageStore.Add(fi, "Bad time entered");
                _editContext.NotifyValidationStateChanged();
            }
        }
    }

    protected void ResetValidation(string field)
    {
        counter++;
        if (_editContext is not null)
        {
            // clear the validation message and raise the validation state change event
            messageStore.Clear(new FieldIdentifier(_foodTruck, field));
            _editContext.NotifyValidationStateChanged();
        }
    }

    public class FoodTruck
    {
        public TimeOnly EndDelivery { get; set; }
        public TimeOnly StartDelivery { get; set; }
    }
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Ole Albers
Solution 2
Solution 3
Solution 4 Meer
Solution 5 devbf
Solution 6 getjith
Solution 7 ouflak
Solution 8 MrC aka Shaun Curtis