b_levitt
b_levitt

Reputation: 7415

Notify EditContext that field has changed for Blazor validation

I am trying to understand the inner workings of Blazor (and eventually write some middleware). I have a fiddle that binds three different fields in three different ways:

fiddle

Below is the code for the fiddle for easier reference:

@page "/"
@using System.ComponentModel.DataAnnotations
@using System.Linq.Expressions;
@using System.Reflection;
@implements IHasEditContext;

<h1>Hello, world!</h1>

<EditForm EditContext="this.EditContextRef">
    <DataAnnotationsValidator></DataAnnotationsValidator>
    <div class="form-group">
        <InputText @bind-Value="@this.FirstName" class="form-control" />
        <ValidationMessage For="() => this.FirstName"></ValidationMessage>
    </div>
    <div class="form-group">
        <input @bind-value="@this.MiddleName" class="form-control" />
        <ValidationMessage For="() => this.MiddleName"></ValidationMessage>
    </div>
    <div class="form-group">
        <input value="@this.LastName" class="form-control" @onchange="(CreateBinder2(this, () => this.LastName,  this.LastName))" />
        <ValidationMessage For="() => this.LastName"></ValidationMessage>
    </div>


    <input type="submit" value="Go" />
</EditForm>

@code {
    protected override void OnInitialized()
    {
        base.OnInitialized();
        EditContextRef = new EditContext(this);
    }

    //BasicFormValidator Form1Validator = new BasicFormValidator();
    [Required]
    public String FirstName { get; set; } = "delete me and change focus to cause validation";
    [Required]
    public String MiddleName { get; set; } = "delete me and change focus - no validation";
    [Required]
    public String LastName { get; set; } = "delete me and change focus - validation but manually calling NotifyFieldChanged";

    public EditContext EditContextRef { get; set; }

    public static EventCallback<ChangeEventArgs> CreateBinder2(
        IHasEditContext receiver,
        Expression<Func<string?>> propExpression,
        string existingValue,
        System.Globalization.CultureInfo? culture = null)
    {
        var fieldIdentifier = FieldIdentifier.Create(propExpression);
        Action<String> valueSetter = (string v) =>
        {
            PropertyInfo prop = fieldIdentifier.Model.GetType().GetProperty(fieldIdentifier.FieldName, BindingFlags.Public | BindingFlags.Instance);
            prop.SetValue(fieldIdentifier.Model, v);
            receiver.EditContextRef.NotifyFieldChanged(fieldIdentifier);
        };

        return EventCallback.Factory.CreateBinder<string>(receiver, valueSetter, existingValue, culture);
    }


}

Upvotes: 4

Views: 11644

Answers (3)

MrC aka Shaun Curtis
MrC aka Shaun Curtis

Reputation: 30016

Your EditForm includes a Submit button, so when you click the button, EditForm recognises the submit action and calls it's internal HandleSubmitAsync. Even if you have no handlers attached to the EditForm, this method calls Validate on the EditContext. This will validate all validation attributed properties in your model, which in your case is the actual component (page).

There's nothing wrong with your manual OnChange handling and setting, as long as you have a good understanding of how InputBase, EditForm and EditContext work and interreact. You're just reproducing what's wrapped up in InputBase. Calling NotifyFieldChanged maintains the editstate on the EditContext. However, be aware that this is not really a true representation of the editstate, it only maintains the state against the last edit not against the original model.

As this is a demo, using the component as the Model is Ok, but you should have a proper data class for the model (you almost certainly know this already, but as others may read this answer later, ...).

Upvotes: 1

Henk Holterman
Henk Holterman

Reputation: 273179

But i still don't understand how the submit did manage to raise NotifyFieldChanged

It didn't. When you Submit the EditForm then EditContext.Validate() is called and validates all fields (properties) in the Model.

I don't know the exact inner workings of Validate() but you can add this to the fiddle:

 <input type="button" value="Check" @onclick="ValidateThis" />

and

void ValidateThis()
{
    Console.WriteLine("Before " + string.Join(",", EditContextRef.GetValidationMessages()));
    EditContextRef.Validate();
    Console.WriteLine("After  " + string.Join(",", EditContextRef.GetValidationMessages()));
}

The "After" writeline will include MiddleName if that is empty.

Upvotes: 1

Marvin Klein
Marvin Klein

Reputation: 1746

When using the <InputText> component you can use more complex binding super easily.

For example:

<InputText Value="@this.MiddleName"
           ValueChanged="(string middleName) => MiddleNameChanged(middleName)"
           ValueExpression="() => this.MiddleName" />

@code 
{
    private void MiddleNameChanged(string middleName)
    {
        this.MiddleName = middleName;
        // Do whatever you want here
    }
}
    

This works across all other built in Input Components for every datatype they support. It also can be combined with async and await.

If you want to create your own Input for other datatypes I suggest to inherit from InputBase and implement your BindConverter there. Here is an example which shows you how to bind to datetime values with the InputDate component. https://github.com/dotnet/aspnetcore/blob/main/src/Components/Web/src/Forms/InputDate.cs

Upvotes: 0

Related Questions