Sprawdzone sposoby na formularze w Blazorze

Ciężko wyobrazić sobie aplikacje bez danych. Każda aplikacja ma za zadanie operowanie na pewnych danych ale, aby móc je przetwarzać trzeba wcześniej je dostarczyć. Jeżeli Twoja aplikacja ma nie tylko API ale również warstwę do komunikacji z użytkownikiem to bez wątpienia temat formularzy się w niej pojawi.

Na początku mojej przygody z Blazorem formularze były czymś, co sprawiło mi pewne trudności, a wybór spośród wielu bibliotek, które oferowały rozszerzenia dla standardowych formularzy nie był łatwy. Ten czas jest już za mną i teraz chciałbym podzielić się z Tobą moim doświadczeniem i dobrymi praktykami, które stosuje w tworzeniu formularzy.

Czego się nauczysz?

Chciałbym dostarczyć Ci gotowy przepis na tworzenie formularzy wraz z rozwiązaniami najczęściej spotykanych problemów, tak abyś mógł zastosować je w swoich aplikacjach. Temat okazał się większy niż początkowo zakładałem dlatego postanowiłem podzielić wpis na kilka mniejszych, abyś mógł wybrać część, która Cie najbardziej interesuje.

W tym wpisie:

  • Stworzmy formularz oraz powiążemy z nim model,
  • Ustawimy walidacje pól,
  • Dostosujemy walidacje do aktualizowania modelu na bieżąco,
  • Zwalidujemy złożone typy,
  • Obsłużymy przypadki przesyłania danych.

Są to niezbędne podstawy, aby ruszyć dalej z formularzami. W kolejnych wpisach możesz spodziewać się niestandardowych walidacji, walidowania gotowego modelu na starcie, wykorzystania Fluent Validation czy porównanie formularzy dostarczanych przez różne biblioteki Blazora. Gorąco zapraszam Cie do śledzenia kolejnym wpisów!

Teraz wracamy do tworzenia formularzy. Gotowy?

Zacznijmy od podstaw

Na początek zbudujmy prosty formularz do kontaktu, jego model wygląda następujaco:

public class Contact
{
    public string Name { get; set; }
    public string Subject { get; set; }
    public string Email { get; set; }
    public string Description { get; set; }
}

Budujac formularz w Blazorze wykorzystamy <EditForm> do zarządzania nim. W EditForm zostanie ustawiony atrybut EditContext, czyli stan formularza. Pola zbudujemy poprzez <InputText> dla Name oraz Email, <InputTextArea> dla Description, natomiast jako Subject chcemy dostarczyć listę z opcjami do wyboru, pomoże nam w tym <InputSelect>. Do ostylowania formularza wykorzystam Tailwind. Całość wygląda następująco:

@page "/Contact"

<EditForm EditContext="editContext"
          class="w-50 bg-gray px-4 py-3 rounded">
    <h1 class="mb-3 fw-bold">Contact Form</h1>
    <div class="mb-3 form-group">
        <label class="d-block mb-1 ml-1">Name</label>
        <InputText @bind-Value="model.Name"
                   class="form-control w-75"/>
    </div>
    <div class="mb-3 form-group">
        <label class="d-block mb-1 ml-1">Subject</label>
        <InputSelect @bind-Value="model.Subject"
                     class="form-control w-75">
            <option value="">Select</option>
            <option value="pricing">Pricing and Evaluation</option>
            <option value="license">License and Account Management</option>
            <option value="support">Technical Support</option>
            <option value="other">Other</option>
        </InputSelect>
    </div>
    <div class="mb-3 form-group">
        <label class="d-block mb-1 ml-1">Email</label>
        <InputText @bind-Value="model.Email"
                   class="form-control w-75"/>
    </div>
    <div class="mb-3 form-group">
        <label class="d-block mb-1 ml-1">Description</label>
        <InputTextArea @bind-Value="model.Description"
                       rows="4"
                       class="form-control w-100"/>
    </div>
    <div class="mt-4">
        <button type="submit"
                class="btn btn-primary">
            Submit
        </button>
    </div>
</EditForm>

@code {
    EditContext editContext;
    readonly Models.Contact model = new();

    protected override void OnInitialized()
    {
        editContext = new EditContext(model);

        base.OnInitialized();
    }
}

Mamy gotową prostą implementacje formularza. Dobrą praktyką jest przeniesienie zahardcodowanych wartości w Subject do odpowiedniego Providera. Oto jak wygląda formularz kontaktowy:

Pora na walidacje

Kolejnym ważnym elementem każdego formularza jest walidacja, zaczniemy od Data Annotations. Chcemy, aby wszystkie pola były wymagane, wykorzystamy do tego atrybut [Required], dalej zakładamy, że Description nie może być dłuższe niż 2048 znaków, w tym celu wykorzystamy atrybut [MaxLength]. Oczywiście w polu Email chcemy mieć wyłącznie poprawne adresy e-mail, użyjemy do tego atrybutu [EmailAddress], chciałbym jeszcze, aby Subject był zawsze wybrany, wykorzystam do tego [Required] z opcją AllowEmptyStrings ustawioną na false. Zaktualizowany model wygląda następująco:

public class Contact
{
    [Required(ErrorMessage = "Field is required.")]
    public string Name { get; set; }

    [Required(AllowEmptyStrings = false, ErrorMessage = "Field is required.")]
    public string Subject { get; set; }

    [Required(ErrorMessage = "Field is required.")]
    [EmailAddress(ErrorMessage = "Field must contain a valid e-mail address.")]
    public string Email { get; set; }

    [Required(ErrorMessage = "Field is required.")]
    [MaxLength(2048, ErrorMessage = "The description cannot be longer than 2048 characters.")]
    public string Description { get; set; }
}

Pora teraz na aktualizacje komponentu Contact.razor. Aby dodać walidacje dodajemy do EditForm tag <DataAnnotationsValidator />. Następnie do każdego pola dodajemy <ValidationMessage For=”…”/> do wyświetlania informacji o błędzie. Oto jak wygląda fragment walidacja dla pola Name:

...
<EditForm EditContext="editContext"
          class="w-50 bg-gray px-4 py-3 rounded">
    <DataAnnotationsValidator />
    <h1 class="mb-3 fw-bold">Contact Form</h1>
    <div class="mb-3 form-group">
        <label class="d-block mb-1 ml-1">Name</label>
        <InputText @bind-Value="model.Name"
                   class="form-control w-75"/>
        <ValidationMessage For="() => model.Name" />
    </div>
...

Teraz po wyjściu z pola pojawią się błędy walidacji.

Spróbujmy walidacji pola na bieżaco

Wprowadźmy kolejne założenie, do tej pory walidacja pól została wykonywana dopiero na wyjściu z pola. W przypadku Description chcielibyśmy jednak wiedzieć wcześniej, że limit znaków został przekroczony. Możemy to zrobić poprzez @bind:event=”oninput”. Niestety komponent InputTextArea nie wspiera ustawienia innego eventu, więc musimy użyć tagu <textarea>. Ze względu na to, że nie korzystamy z wbudowanego komponentu musimy sami obsłuzyć event @oninput, zaktualizować pole w modelu oraz wywołać metode NotifyFieldChanged, aby pole zostało zwalidowane. Implementacja wygląda następująco:

<div class="mb-3 form-group">
	<label class="d-block mb-1 ml-1">Description</label>
	<textarea class="form-control w-100"
			  rows="4"
			  @oninput="args => UpdateDescription(args.Value.ToString())">
		@model.Description
	</textarea>
	<div>@(model.Description?.Length ?? 0)/2048</div>
	<ValidationMessage For="() => model.Description" />
</div>
...
private void UpdateDescription(string value)
{
	model.Description = value;
	editContext.NotifyFieldChanged(editContext.Field(nameof(Models.Contact.Description)));
}

W przypadku, gdyby istniała potrzeba aktualizowania na bieżąco wielu pól to dobrą praktyką byłoby przygotowanie komponentu z logiką do aktualizacji, aby nie powielać tej samej logiki wielokrotnie. Musieliśmy się sporo natrudzić, aby dostosować formularz do zmian walidacji.

W tym miejscu rekomenduje wykorzystanie zewnętrznych bibliotek, które posiadają gotowe komponenty z opcją aktualizacji na bieżąco, przykładem takiej biblioteki jest MudBlazor.

Typy złożone w walidacji

Znacznie częściej niż płaską strukturę z jedną klasą składającą się jedynie z typów prostych, mamy do czynienia z kilkoma klasami zbudowanymi w pewną hierarchie. W tym wypadku dodanie atrybutów nie wystarczy. W ramach ćwiczeń załóżmy, że pola Name i Email wyciągniemy do osobnej klasy UserInfo, która możemy wyglądać następująco:

public class UserInfo
{
    [Required(ErrorMessage = "Field is required.")]
    public string Name { get; set; }

    [Required(ErrorMessage = "Field is required.")]
    [EmailAddress(ErrorMessage = "Field must contain a valid e-mail address.")]
    public string Email { get; set; }
}

W klasie Contact zamieniamy pola Name i Email na jedno pole UserInfo. Musimy je jeszcze oznaczyć atrybutem [ValidateComplexType] oraz zainstalować bibliotekę Microsoft.AspNetCore.Blazor.DataAnnotations.Validation. W komponencie Contact.razor musimy jeszcze poprawić mapowania pól na model uwzględniając UserInfo oraz zastąpić <DataAnnotationsValidator /> przez <ObjectGraphDataAnnotationsValidator />. Po ponownym uruchomieniu aplikacji widzimy, że walidacja dalej działa:

Prześlijmy dane

Na koniec zostało nam jeszcze odebrać dane z formularza. Możemy to wykonać poprzez obsługę OnValidSubmit w EditForm. W mojej implementacji dodam metode Send, która wyświetli w konsoli dane przesłane przez formularz. Wygląda to następująco:

<EditForm EditContext="editContext"
          class="w-50 bg-gray px-4 py-3 rounded"
          OnValidSubmit="Send">
...
private void Send()
{
	Console.WriteLine($"Data - name: {model.UserInfo.Name}, email: {model.UserInfo.Email}, subject: {model.Subject}, description: {model.Description}.");
}

Po naciśnięciu przycisku Submit dane zostaną wyświetlone w konsoli developerskiej, w moim przypadku, wyglądało to następująco:

Możemy jeszcze obsłużyć przypadek, gdy formularz zawiera błędy i poprosić o ich poprawienie. W tym celu wykorzystamy metode OnInvalidSubmit ustawiając w niej zmienną submittedInvalidForm na true, a następnie wyświetlimy komunikat. Oto moja implementacja:

<EditForm EditContext="editContext"
          class="w-50 bg-gray px-4 py-3 rounded"
          OnValidSubmit="Send"
          OnInvalidSubmit="() => submittedInvalidForm = true">
    <ObjectGraphDataAnnotationsValidator/>
    <h1 class="mb-3 fw-bold">Contact Form</h1>
    @if (submittedInvalidForm)
    {
        <div class="px-3 py-2 mb-2 alert-warning">Hey! There are errors in the form you filled in, please correct them.</div>
    }

Po wciśnięciu przycisku submit z błędami w formularzu zostanie wyświetlony następujący komunikat:

Metode OnInvalidSubmit możemy wykorzystać do zliczania ilości prób zalogowania czy do dodawania logów aplikacji. Mamy jeszcze OnSubmit, która wywoła się niezależnie od tego czy formularz jest wypełniony poprawnie czy nie.

Podsumowanie

Udało nam się przebrnąć przez podstawy. Umiesz już utworzyć formularz, zwalidować go, wiesz jak dodać werfyikacje bezpośrednio po wprowadzeniu nowego znaku. Potrafisz również zwalidować bardziej skomplikowane obiekty, a na koniec obsłużyć przesyłanie danych. Jesteśmy na dobrej drodze do opanowania formularzy, lecz zanim ruszymy dalej chciałbym podzielić się z Tobą moim doświadczeniem w tym obszarze.

Dobre praktyki

Implementacja, którą stworzyliśmy sprawdzi się w prostszych aplikacjach bądź tam, gdzie wystarczy nam podstawowe zachowanie formularza. Jeżeli chcesz stworzyć prototyp i zależy Ci na czasie to podany sposób sprawdzi się idealnie, natomiast w bardziej zaawansowanych aplikacjach, które będziesz utrzymywać przez dłuższy czas warto wymienić Data Annotations na Fluent Validation, a zamiast wbudowanych komponentów wykorzystać bardziej rozbudowane z zewnętrznych bibliotek takich jak MudBlazor.

Jesteś już po pierwszym starciu z formularzami w Blazorze i mam nadzieję, że jesteś gotowy na więcej, bo już za chwile ruszamy z kolejnymi tematami. Będą to niestandardowe walidacje, wypełnianie formularza podczas tworzenia komponentu oraz wykorzystanie Fluent Validation, lecz zanim ruszymy dalej daj znać czy przedstawiony temat pozwolił Ci rozwiązać Twoje problemy, a może dzięki niemu mogłeś szybko wdrożyć formularze w swojej aplikacji? Czekam na wiadomość od ciebie!

Jeżeli uznasz ten wpis za przydatny to proszę udostępnij go innym. Zapraszam również do mojego newslettera na którym regularnie udostępniam przedpremierowo artykuły, materiały wideo i wiele więcej.

Share via
Copy link
Powered by Social Snap