0

In Blazor I see many examples of a form (EditForm with EditContext) using a model with DataAnnotations attributes being used as validators. The problem with these examples is that they all use the OnValidSubmit event or method to do something with the model after clicking the submit button. But I don't want a submit button!

Now what I want to achieve is a single input, with validation and some kind of event after validation. When it fires (for example when the user leaves the field with Tab, Enter, or mouseclick somewhere else), I know that the input is valid and I can update my model or something else that is related to my input field. To be clear, I do not have (and do not want) a submit button.

How can I achieve this?

Thanks, Herman

2 Answers 2

1

New answer

You can assign some properties on an InputText, with which you can achieve this. You will still need an EditForm though, for the validation. This updates everytime the user hits enter or leaves the InputText:

@using Microsoft.AspNetCore.Components.Forms
@using System.ComponentModel.DataAnnotations;

<EditForm Model="Model" @ref="Form" >
    <DataAnnotationsValidator/>
    <InputText Value="@Model.Property" 
    ValueChanged="ValueChanged"
    ValueExpression="() => Model.Property"/>
</EditForm>

@code{
    EditForm? Form { get; set; }
    YourModel Model { get; set; } = new();
    public void ValueChanged(string value) {
        Model.Property = value;
        if(Form?.EditContext?.Validate()??false)
        {
            // valid
        }else
        {
            // invalid
        }
    }
}

old answer

You could create a component for that. To make it easier, you could derive from the base type (like InputText or InputCheckbox). I made one for InputText:

@using Microsoft.AspNetCore.Components.Forms
@inherits InputText
<input attributes="@AdditionalAttributes"
       class="@CssClass"
       value="@CurrentValue"
       @oninput="EventCallback.Factory.CreateBinder<string?>(this,
       value => CurrentValueAsString = value, CurrentValueAsString)" />

This is a simple one which updates the bound value on every change of it's own value. In this way, you do not really need a submit button. It also includes a label for displaying the DisplayName.

Now you can use this and bind any value to it from your parent component, just like any other InputText. For validation, you will still need the EditForm though.

<EditForm Model="inputModel" OnValidSubmit="ValidSubmit">
    <BetterInputText @bind-value="TextProperty" />
</EditForm>

@code{
    public string TextProperty { get; set; }
}
Sign up to request clarification or add additional context in comments.

3 Comments

Thanks Jonathan for your input. In my opinion this is not a solution for my problem, but only an execution switch. What I mean is in stead of everything fired (validation attribute, EditContext events) on a Tab or Enter, it is now fired on every keystroke. But the problem remains the same, I don't know if the validation was successfull once I know that my property is updated. What I'm looking for is some event that tells me: "Hey, this property is changed and it validated as well"
Yeah it helped. It is now more or less the same answer as @MrCakaShaunCurtis but I combined your answer of creating a separate component with the manual validation answer and got it working. I have to admit it is not ideal because of the multiple calls to the validation attribute (which was answered by @MrCakaShaunCurtis), but it does what it needs to do.
happy to have been helpful👍 Good luck with your project!
1

Update

For reference, if you want to just validate a single field you can forget all the validation stuff and just add it to the component like this:

public class MyInputText : InputText
{
    [Parameter] public Func<string?, string?>? Validation { get; set; }

    private bool _validstate;

    protected override bool TryParseValueFromString(string? value, out string? result, [NotNullWhen(false)] out string? validationErrorMessage)
    {
        var oldState = _validstate;
        _validstate = false;
        result = value;
        validationErrorMessage = null;

        if (this.Validation is not null)
        {
            validationErrorMessage = this.Validation(value);
            _validstate = validationErrorMessage is null;
        }

        if (_validstate != oldState)
            this.EditContext.NotifyValidationStateChanged();
        
        return _validstate;
    }
}

And:

@page "/"
@using System.ComponentModel.DataAnnotations

<PageTitle>Index</PageTitle>

<EditForm EditContext=_editContext>
    <MyInputText class="form-control" Validation=this.Validate @bind-Value=_model.Value />
    <ValidationSummary />
</EditForm>

<div class="bg-dark text-white m-2 p-1">
    <pre>Value : @_model.Value</pre>
</div>

@code {
    private Model _model = new();
    private EditContext _editContext = default!;
    private bool _validationState;

    protected override void OnInitialized()
    {
        _editContext = new EditContext(_model);
        _editContext.OnValidationStateChanged += this.OnValidationStateChanged;
    }

    private void OnValidationStateChanged(object? sender, ValidationStateChangedEventArgs e)
    {
        var messages = _editContext.GetValidationMessages();
        if (messages is null || messages.Count() == 0)
        {
            // we're valid
            // Do something
        }
    }

    public string? Validate(string? value)
    {
            if (value is null)
            return "No way man!";

        if (value.Length < 10)
            return "Way Too Short man!";

        return null;

    }

    public void Dispose()
        => _editContext.OnValidationStateChanged -= this.OnValidationStateChanged;

    public class Model
    {
        public string? Value { get; set; }
    }
}

Original

Here's a demo page that shows how to run code on a field update. It demonstrates how to run validation manually.

@page "/"
@using System.ComponentModel.DataAnnotations

<PageTitle>Index</PageTitle>

<EditForm EditContext=_editContext>
<InputText class="form-control" @bind-Value=_model.Value />
<DataAnnotationsValidator />
</EditForm>

<div class="bg-dark text-white m-2 p-1">
    <pre>Value : @_model.Value</pre>
    <pre>Validation State : @_validationState</pre>
</div>

@code {
    private Model _model = new();
    private EditContext _editContext = default!;
    private bool _validationState;

    protected override void OnInitialized()
    {
        _editContext = new EditContext(_model);
        _editContext.OnFieldChanged += this.OnFieldChanged;
    }


    private void OnFieldChanged(object? sender, FieldChangedEventArgs e)
    {
        _validationState = _editContext.Validate();

        if(_validationState)
        {
            // Do something
        }
    }

    public void Dispose()
        => _editContext.OnFieldChanged -= this.OnFieldChanged;

    public class Model
    {
        [Required] public string? Value { get; set; }
    }
}

6 Comments

Small note: I think you don't really need to unsubscribe the event. The target is the Page and that is also the owner of the EditContext.
@HH. In this case true. I tend to always do it in answers. You and I know when we need to and don't need to, but others won't and think it's OK not to do it.
Thanks @MrCakaShaunCurtis for your answer. I've implemented it to see if it fits my needs and it does a little bit. The manual validation helps, because it calls the validator attributes and returns true when valid, so that I'm able to update my model accordingly. But... after the manual validation, the validator attribute is called once more, just because it is coupled to the bind model property. So actually it is called twice.
The reason for the double validate is that DataAnnotationsValidator registers for the OnFieldChanged event on the EditContext and runs a validation on the specific property (defined by a FieldIdentifier object). The problem in using OnFieldchanged as a trigger is you can't guarantee that the validator has run before your event handler is invoked. You may be checking the validation store before the validation is run.
Ok, I see. That makes sense. As I already described in the comments to the answer of Jonathan I combined both answers into my solution. I choose to accept the answer of Jonathan because he pointed me in the direction of a separate component. At the end both answers are good.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.