Pokażę Ci jak zrobić własną walidację na bardzo użytecznym przykładzie. Zrobimy mechanizm sprawdzenia, czy użytkownik wyraził zgodę na regulamin. Jeśli nie wiesz, jak działa walidacja w .NetCore, sprawdź najpierw ten artykuł.

Stworzenie własnej walidacji składa się z dwóch kroków.

  • utworzenie własnego atrybutu
  • dodanie sprawdzenia po stronie JavaScript.

Zasadniczo jest to dość proste, ale trzeba pamiętać o pewnej rzeczy… o tym później

Walidacja po stronie klienta

Required nie wystarczy

W .NET można tworzyć własne atrybuty. To wiadomo. Atrybut to po prostu klasa, która dziedziczy po specyficznej klasie i zawiera… konkretne atrybuty 🙂

Z jakiegoś powodu w .NetCore nie ma domyślnie możliwości sprawdzenia, czy checkbox został zaznaczony. Wydawać by się mogło, że wystarczy:

class RegisterUserViewModel
{
	[Required]
	public bool TermsAndConditions { get; set; }
}

Widziałem też takie przykłady:

class RegisterUserViewModel
{
	[Range(typeof(bool), "true", "true")]
	public bool TermsAndConditions { get; set; }
}

niestety z checkboxem i jego wartością jest nieco inaczej. I powinien zostać do tego stworzony nowy atrybut. Rozwiązanie, które tutaj podaję zaproponowałem do Microsoftu. Być może w kolejnych wersjach wdrożą coś analogicznego.

Dopisane: W Blazor można walidować checkbox za pomocą atrybutu Required

Tworzenie własnego atrybutu

Zatem stwórzmy własny atrybut walidacyjny. Takie atrybuty powinny dziedziczyć po klasie ValidationAttribute. Jest to klasa abstrakcyjna, która zawiera pewną wspólną logikę dla wszystkich walidacji.

ValidationAttribute zapewnia walidację po stronie serwera. Jeśli chcesz mieć zapewnioną dodatkowo walidację po stronie klienta, musisz zaimplementować interfejs IClientModelValidator.

IClientModelValidator

Tutaj na chwilę się zatrzymamy. Walidacja po stronie klienta zawsze odbywa się za pomocą JavaScript (lub podobnych). .NetCore to znacznie ułatwia, gdyż wprowadza własny prosty mechanizm. Spójrz na ten prosty formularz:

<form asp-action="Register" method="Post">
	<div class="form-group">
		<label for="email">Podaj swój email:</label>
		<input class="form-control" type="email" asp-for="UserName" />
		<span asp-validation-for="UserName" class="text-danger"></span>
	</div>
</form>

Widzisz tutaj pomocnicze tagi: asp-for i asp-validation-for. W wynikowym HTMLu będzie to wyglądało mniej-więcej tak:

<div class="form-group">
	<label for="email">Podaj swój email:</label>
	<input class="form-control" type="email" data-val="true" data-val-email="The Email field is not a valid e-mail address." data-val-required="The UserName field is required." id="UserName" name="UserName" value="">
	<span class="text-danger field-validation-valid" data-valmsg-for="UserName" data-valmsg-replace="true"></span>
</div>

Jak widzisz, tagi pomocnicze trochę tutaj nagrzebały w kodzie. Nagrzebały pod kątem Microsoftowej biblioteki jQuery Unobtrusive Validation. Ta biblioteka, można powiedzieć, „szuka” atrybutów data-val (skrót od „data validation”), których wartość jest ustawiona na true. Oznacza to, że takie pole podlega walidacji. Następnie tworzone są odpowiednie komunikaty, gdy walidacja się nie powiedzie:

  • data-val-email – komunikat dla atrybutu [EmailAddress]
  • data-val-required – komunikat dla atrybutu [Required]

Te komunikaty są później umieszczane w elemencie, który ma data-valmsg-for i data-valmsg-replace (true).

To się dzieje automatycznie – Microsoft porobił takie mechanizmy. Tutaj wchodzi interfejs IClientModelValidator. Zajmijmy się najpierw nim:

Implementacja IClientModelValidator

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
public class CheckBoxCheckedAttribute : ValidationAttribute, IClientModelValidator
{
	public void AddValidation(ClientModelValidationContext context)
	{
		if (context == null)
		{
			throw new ArgumentNullException(nameof(context));
		}

		MergeAttribute(context.Attributes, "data-val", "true");
		MergeAttribute(context.Attributes, "data-val-chboxreq", "Nie zaznaczyłeś checkboxa!");
	}
	
	private static void MergeAttribute(IDictionary<string, string> attributes, string key, string value)
	{
		if (!attributes.ContainsKey(key))
		{
			attributes.Add(key, value);
		}
	}
}

context.Attributes to słownik, który zawiera atrybuty dodane już do pola

Użycie atrybutu

Utworzyliśmy klasę CheckBoxCheckedAttribute, implementując interfejs IClientModelValidator.
Teraz tak. Tag pomocniczy asp-for sprawdza wszystkie walidacje na danym polu, czyli jeśli mamy model:

class RegisterUserViewModel
{
	[CheckBoxChecked]
	public bool TermsAndConditions { get; set; }
}

wtedy asp-for weźmie wszystkie klasy atrybutów dla danego pola, które implementują interfejs IClientModelValidator i uruchomi metodę AddValidation.

To, co „zwróci” ta metoda, będzie dopisane do pola . Jak widzisz, zadaniem metody AddValidation jest właściwie tylko odpowiednie dodanie atrybutów do pola input, a więc:

  • data-val usatwione na true
  • data-val-chboxreq – którego wartością jest odpowiedni komunikat. Czyli teraz dopiszemy checkboxa w formularzu:
<form asp-action="Register" method="Post">
	<div class="form-group">
	    <div class="form-check">
	        <input type="checkbox" class="form-check-input" asp-for="TermsAndConditions" />
	        <label class="form-check-label" asp-for="TermsAndConditions">Akceptuję warunki regulaminu(acceptLabel)</label>
	    </div>
	    <span asp-validation-for="TermsAndConditions" class="text-danger"></span>
	</div>	
</form>

A ostatecznie pole input będzie wyglądało podobnie do tego:

<input type="checkbox" class="form-check-input" data-val="true" data-val-chboxreq="Nie zaznaczyłeś checkboxa!" />

Zauważ, że klasa CheckBoxCheckedAttribute dodała w metodzie AddValidation te dwa atrybuty (data-val i data-val-chboxreq), które są wymagane do poprawnego działania walidacji po stronie klienta.

Oczywiście nie musisz pisać nazywać tego data-val-chboxreq. Ważne, żeby było data-val- i w miarę unikalna, jednoznaczna końcówka. Równie dobrze mógłbyś napisać: data-val-checkbox-zaznaczony i sprawdzać później wartość tego atrybutu.

Czyli podsumowując tę część:

Tag asp-for powoduje wywołanie metody AddValidation z interfejsu IClientModelValidator, która jest odpowiedzialna za dodanie atrybutów walidacyjnych do pola . Oczywiście nic nie stoi na przeszkodzie, żebyś dodał tam jeszcze inne atrybuty. Pamiętaj, że musisz atrybut implementujący IClientModelValidator dodać do odpowiedniego pola w swoim modelu (viewmodelu).

OK, skoro już wiesz do czego służy IClientModelValidator, to teraz polecimy dalej. Pierwsza część walidacji jest zrobiona. Teraz druga. JavaScript.

Walidacja w JavaScript

Jak już mówiłem, Microsoft ma tą swoją bibliotekę jQuery Unobtrusive Validation, która działa razem z jQuery Validation.

Ten fragment kodu możesz dodać albo tylko na stronie z formularzem, albo lepiej – w widoku _ValidationScriptsPartial. Wtedy będzie dostępny zawsze, gdy dodajesz jakąś walidację:

<script>
	$.validator.addMethod("chboxreq", function (value, element, param) {
	    if (element.type != "checkbox")
	        return false;
	
	    return element.checked;
	});
	
	$.validator.unobtrusive.adapters.addBool("chboxreq");	
</script>
  • najpierw dodajemy metodę do walidatora jQuery.
  • metoda ma zostać uruchomiona, gdy trafi na atrybut data-val-chboxreq. Zauważ, że w parametrze podajemy tylko tę ostatnią część – chboxreq.
  • funkcja przyjmuje 3 parametry – wartość (atrybut value elementu HTML), element – czyli element HTML i param – dodatkowy parametr.

Jak widzisz funkcja walidująca jest bardzo prosta – najpierw jest sprawdzenie, czy element jest checkboxem, potem funkcja sprawdza, czy checkbox jest zaznaczony.

Aby to wszystko zadziałało, trzeba jeszcze dodać ten „adapter”, używając $.validator.unobtrusive.adapters
Nie będę tutaj opisywał walidatorów w jQuery, bo:

  1. Mógłbym zaciemnić obraz
  2. Nie znam się na nich
  3. Możesz sam wyszukać w dokumentacjach, jeśli będziesz chciał robić jakieś dziwne rzeczy.

I teraz pewna uwaga, o której pisałem na początku. Ten cały kod nie chciał mi kiedyś zadziałać i męczyłem się chyba 2 godziny zanim wpadłem dlaczego (oczywiście nie byłbym sobą, gdybym nie zgłosił tego do Microsoftu :)). Ten kod nie zadziała z poziomu TypeScript. Nie wiem, czemu. Po prostu MUSI być bezpośrednio w JavaScript.

Walidacja po stronie serwera

Aby wszystko zadziałało poprawnie, musimy dodać jeszcze walidację po stronie serwera. Zauważ, że walidacja kliencka została zrobiona w JavaScript. Walidacja po stronie serwera odbywa się już bezpośrednio w klasie CheckBoxCheckedAttribute. Wystarczy przesłonić metodę IsValid:

public override bool IsValid(object value)
{
	return (value is bool && (bool)value);
}

To tyle.

Komunikaty o błędach

Teraz mała uwaga na koniec. Klasa ValidationAttribute zawiera już pewne mechanizmy, które pomagają.
Jak już wiesz, każdy atrybut walidacyjny z .NET ma pewne właściwości: ErrorMessage, ErrorMessageResourceName, ErrorMessageResourceType… Bierze się to właśnie z klasy ValidationAttribute. Zatem klasa CheckBoxCheckedAttribute też ma te właściwości. Więc możesz napisać:

class RegisterUserViewModel
{
	[CheckBoxChecked(ErrorMessage = "Musisz zaakceptować warunki")]
	public bool TermsAndConditions { get; set; }
}

lub (w aplikacji wielojęzycznej):

class RegisterUserViewModel
{
	[CheckBoxChecked(ErrorMessageResourceName = nameof(LangRes.ValidationError_AcceptTerms), ErrorMessageResourceType = typeof(LangRes))]
	public bool TermsAndConditions { get; set; }
}

Jeśli nie wiesz, jak zrobić lokalizację do swojego programu, sprawdź ten artykuł

wOczywiście musisz pamiętać, żeby w metodzie AddValidation zwrócić poprawny komunikat. I teraz uwaga. Niezależnie od tego, czy posłużysz się ErrorMessage, czy ErrorMessageResourceName i ErrorMessageResourceType, klasa ValidationAttribute przechowa już dla Ciebie konkretny komunikat. Trzyma to we właściwości ErrorMessageString, a więc zamiast komunikatu bezpośrednio w kodzie powinieneś się posłużyć tą właściwością:

public void AddValidation(ClientModelValidationContext context)
{
	if (context == null)
	{
		throw new ArgumentNullException(nameof(context));
	}

	MergeAttribute(context.Attributes, "data-val", "true");
	MergeAttribute(context.Attributes, "data-val-chboxreq", ErrorMessageString);
}

Przejdźmy na kolejny poziom

Jeśli nie do końca rozumiesz ten artykuł albo chcesz czegoś więcej, przy okazji napisałem jak zrobić walidator do sprawdzania rozmiaru przesyłanego pliku. Zachęcam do zapoznania się z tym artykułem o przesyłaniu plików w Razor, Blazor i WebApi.

To tyle.
Jeśli masz jakieś wątpliwości lub znalazłeś błąd w artykule, podziel się w komentarzu.

Podziel się artykułem na: