Ole Albers
Ole Albers

Reputation: 9295

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.

Upvotes: 24

Views: 23696

Answers (12)

axel
axel

Reputation: 1

The latest release of Blazor solves this issue:

https://learn.microsoft.com/en-us/aspnet/core/blazor/forms/validation?view=aspnetcore-9.0

Upvotes: 0

Mike Aerni
Mike Aerni

Reputation: 1

i'm late to the game, but i solved my issue without touching javascript and doing any of the special handling that other folks have mentioned...

I added a bool property to my data model, ClearValidation, that I set to false inside a try/finally block. Then with ClearValidation set, I call the form's Validate function. My FluentValidator has a When() condition so all validation passes. Here's my code snippets:

inside the FluentValidator:

        When(m => !m.ClearValidation && m.AssetAssignmentType == AssetAssignmentType.FlightSquadron, () =>
        {
            RuleFor(model => model.AssignedToFlightSquadronId)
                .NotNull()
                .WithMessage("Fight Squadron must be specified");
        });

my razor component code:

            try
            {
                ChangeAssetAssignmentModel!.ClearValidation = true;
                ChangeAssetAssignmentModel!.AssetAssignmentType = Enum.Parse<AssetAssignmentType>(value);
                _form!.Validate();
            }
            finally
            {
                ChangeAssetAssignmentModel!.ClearValidation = false;
            }

Upvotes: 0

AngularJScott
AngularJScott

Reputation: 1

The problem is the binding only takes effect once you leave the input, sooo.

I think you need to use bind-Value:get and then get the input value from the element using IJSInterop and a JS function on the keydown (press / up) event.

Then you can update your model and call editContent.Validate();

The get displays what was was typed and the IJSInterop updates the model. Then you get "real-time" validation.

Upvotes: 0

Ole Albers
Ole Albers

Reputation: 9295

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).

Upvotes: 16

user142914
user142914

Reputation:

This seems to work perfectly for me:

_editContext.MarkAsUnmodified();

Upvotes: 1

MrC aka Shaun Curtis
MrC aka Shaun Curtis

Reputation: 30072

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; }
    }
}

Upvotes: 8

Long Cao Ho&#224;ng
Long Cao Ho&#224;ng

Reputation: 1

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);
}

Upvotes: 0

getjith
getjith

Reputation: 121

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

Upvotes: 1

devbf
devbf

Reputation: 571

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.

Upvotes: 2

Xam
Xam

Reputation: 311

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();
    }
}

Upvotes: 11

Meer
Meer

Reputation: 808

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");
        }
    }
}

Upvotes: 5

Jesuseyitan
Jesuseyitan

Reputation: 157

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

Upvotes: 1

Related Questions