14

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.

Ole Albers
  • 8,715
  • 10
  • 73
  • 166
  • 1
    Were you able to find a solution to this problem? – WBuck Sep 10 '20 at 02:23
  • 1
    anohter aproach is use a custom data anotation validators, see e.g. this SO: https://stackoverflow.com/questions/3413715/how-to-create-custom-data-annotation-validators. You has in context the whole object, so you can add this validator to "StartDelivery" – Eliseo Oct 01 '20 at 06:31

9 Answers9

14

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

Ole Albers
  • 8,715
  • 10
  • 73
  • 166
  • 1
    This fixed a bug for me where the previous validation errors were not removed. – Rye bread May 06 '20 at 07:47
  • Same solution helped me, but I had to call _editContext.AddDataAnnotationsValidation(); in the reset, to get subsequent validation calls to work. – Tegge Aug 27 '21 at 08:18
7

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();
    }
}
Xam
  • 271
  • 3
  • 12
  • 1
    thank you, this worked the best. Net7, Blazor wasm – JasonS Nov 15 '22 at 00:24
  • This is great! I created a variation on this where you pass in a single field name to be cleared. This is useful when you have a validation on field A that is dependent on field B. When field B changes, anf dangling validation is cleared. – Jersey Dude Nov 15 '22 at 14:23
  • When using this solution, remember to check for different kinds of `null`. I had problem with `messageStores` being `null` sometimes, which made `Invoke` throw an error. – Mjaustro Mar 22 '23 at 12:36
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");
        }
    }
}
Meer
  • 678
  • 6
  • 12
3

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; }
    }
}
MrC aka Shaun Curtis
  • 19,075
  • 3
  • 13
  • 31
  • To clear validation errors for a specific field I reset the field then called `validationStore.Clear(fieldIdentifier)` which seemed like the obvious approach. But that didn't work. Then I found your answer and tried `editContext.NotifyFieldChanged(fieldIdentifier)` which worked. Thanks! The API here seems poorly named... I thought clearing the messages for a specific field would be the way to do it, but the error messages remain until I do it your way. Do you know why? – lonix Oct 22 '22 at 11:15
  • 1
    `ValidationMessageStore` is a bit of a strange beast. Each object creates and manages it's own data within the message store. Each object can only manage it's own bit. So one object can't clear the messages owned by another object. There's no way to globally clear the messages for a specific field identifier. Hope that makes sense. – MrC aka Shaun Curtis Oct 22 '22 at 19:28
  • 2
    I've just re-read the other answers - what a load of kludges and voodoo! – MrC aka Shaun Curtis Oct 22 '22 at 19:45
2

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

Jesuseyitan
  • 149
  • 1
  • 6
2

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.

devbf
  • 412
  • 5
  • 15
1

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

getjith
  • 111
  • 1
  • 4
0

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);
}
ouflak
  • 2,458
  • 10
  • 44
  • 49
0

This seems to work perfectly for me:

_editContext.MarkAsUnmodified();
Bryan
  • 3,224
  • 9
  • 41
  • 58