Implementare logiche di validazione complesse nelle EditForm di Blazor

di Marco De Sanctis, in ASP.NET Core,

Come sappiamo, Blazor supporta nativamente le data annotation per la validazione dei dati in input: grazie a questa funzionalità, possiamo per esempio decorare una classe User con attributi che ne specifichino i requisiti formali:

public class User
{
    [Required]
    public string Username { get; set; }
        
    [RegularExpression(Constants.UK_PHONE_NUMBER)]
    public string MobilePhone { get; set; }

    [RegularExpression(Constants.UK_PHONE_NUMBER)]
    public string LandLinePhone { get; set; }
}

A questo punto, è sufficiente aggiungere un DataAnnotationValidator (e magari anche un ValidationSummary) al componente EditForm, e il motore di rendering si occuperà automaticamente di verificarne la correttezza prima di procedere al submit:

<EditForm Model="this.NewUser" OnValidSubmit="this.SubmitAsync" class="col-6">
    <DataAnnotationsValidator />
    <ValidationSummary />

    <div class="form-group">
        <label>Username</label>
        <InputText class="form-control" @bind-Value="this.NewUser.Username" />
    </div>
    <div class="form-group">
        <label>Mobile Phone</label>
        <InputText class="form-control" @bind-Value="this.NewUser.MobilePhone" />
    </div>
    <div class="form-group">
        <label>Landline phone</label>
        <InputText class="form-control" @bind-Value="this.NewUser.LandLinePhone" />
    </div>

    <button type="submit" class="btn btn-primary">Save</button>
</EditForm>

@Code {
  public User NewUser { get; set; }

  public async Task SubmitAsync()
  {
      // .. logica per memorizzare l'utente qui ...
  }
}

Nel nostro caso, se provassimo a salvare uno user con dati errati, gli errori ci verrebbero evidenziati come in figura.


Purtroppo le data annotation funzionano bene con regole semplici, ma non sono idonee a rappresentare logiche di validazione più complesse, per esempio quelle che coinvolgono diverse proprietà.

Per questi scopi, una delle possibili soluzioni in Blazor è quella di costruire un custom validator. Immaginiamo di voler far sì che almeno uno tra Mobile Phone e Landline Phone sia obbligatorio. Possiamo implementare questa logica in una classe UserPhoneValidator simile alla seguente:

public class UserPhoneValidator : ComponentBase
{
    private ValidationMessageStore _store;

    [CascadingParameter]
    public EditContext Context { get; set; }

    protected override void OnInitialized()
    {
        base.OnInitialized();

        _store = new ValidationMessageStore(this.Context);

        this.Context.OnValidationRequested += Context_OnValidationRequested; ;
        this.Context.OnFieldChanged += Context_OnFieldChanged;
    }

    private void Context_OnValidationRequested(object sender, ValidationRequestedEventArgs e)
    {
        this.ExecuteValidation();
    }

    private void Context_OnFieldChanged(object sender, FieldChangedEventArgs e)
    {
        if (e.FieldIdentifier.FieldName == "MobilePhone" ||
            e.FieldIdentifier.FieldName == "LandLinePhone")
        {
            this.ExecuteValidation();
        }
    }
     
    // .. altro codice qui ..
}

La nostra classe eredita da ComponentBase, così che possiamo utilizzarla nel markup di Blazor, ed espone una proprietà di tipo EditContext. Si tratta di un CascadingParameter che viene automaticamente assegnato dalla form all'interno della quale poniamo il nostro componente, così che possiamo sia accederne al contenuto, sia ricevere una notifica al verificarsi di alcuni eventi.

Nel nostro caso, abbiamo sottoscritto gli eventi OnValidationRequested e OnFieldChanged, per eseguire la nostra logica sia al submit della form stessa, sia quando il valore di un field viene modificato. In quest'ultimo caso, come possiamo notare, ci preoccupiamo solo dei field relativi ai due numeri di telefono che vogliamo monitorare.

Il codice di ExecuteValidation è abbastanza semplice e sfrutta un ValidationMessageStore per inviare gli esiti della validazione alla form:

private void ExecuteValidation()
{
    var model = (this.Context.Model as User);

    if (model == null)
        return; // this only works with User objects

    _store.Clear();

    if (string.IsNullOrWhiteSpace(model.MobilePhone) &&
        string.IsNullOrWhiteSpace(model.LandLinePhone))
    {
        _store.Add(() => model.MobilePhone, "Either mobile or landline phone must be provided");
        _store.Add(() => model.LandLinePhone, "Either mobile or landline phone must be provided");
    }
}

Come primo passo, puliamo lo store da eventuali messaggi precedenti. Poi, se entrambi i numeri di telefono sono vuoti, aggiungiamo una entry per ciascuno di queste due proprietà.

L'ultimo aspetto da non dimenticare è quello di implementare IDisposable, come in tutti i casi in cui sottoscriviamo eventi, per evitare memory leak:

public class UserPhoneValidator : ComponentBase, IDisposable
{
    // .. altro codice qui ..

    public void Dispose()
    {
        this.Context.OnValidationRequested -= Context_OnValidationRequested; ;
        this.Context.OnFieldChanged -= Context_OnFieldChanged;
    }
}

A questo punto, possiamo finalmente aggiungere il nostro validatore alla form in pagina:

<EditForm Model="this.NewUser" OnValidSubmit="this.Submit" class="col-6">
    <DataAnnotationsValidator />
    <UserPhoneValidator />
    <ValidationSummary />

    ...

    <button type="submit" class="btn btn-primary">Save</button>
</EditForm>

Se abbiamo svolto tutti i passaggi correttamente, vedremo il messaggio di errore apparire contemporaneamente su entrambe le proprietà nel caso siano tutte e due vuote, e sparire nel momento in cui ne valorizziamo una.

Commenti

Visualizza/aggiungi commenti

| Condividi su: Twitter, Facebook, LinkedIn

Per inserire un commento, devi avere un account.

Fai il login e torna a questa pagina, oppure registrati alla nostra community.

Approfondimenti

I più letti di oggi