Tłumaczenie aplikacji internetowych – RAZOR

Tłumaczenie aplikacji internetowych – RAZOR

Wstęp

To jest kolejny artykuł z serii o globalizacji i lokalizacji. Jeśli nie czytałeś poprzednich, koniecznie to nadrób. W tym artykule opisuję TYLKO jak ładować tłumaczenia w aplikacjach internetowych tworzonych w RAZOR. Poprzedni artykuł – Tłumaczenie aplikacji cz. 3 – jak to ogarnąć? – daje całą podstawę.

W aplikacjach internetowych możemy uwzględniać język na kilka sposobów:

  • informacji wysyłanej z przeglądarki (nagłówek żądania)
  • parametru w zapytaniu (np. https://example.com?lang=en)
  • ciasteczka
  • fragmentu URL (np. https://example.com/en-US/)

Popatrzymy na te wszystkie możliwości.

Żeby w ogóle cała machina ruszyła, trzeba skonfigurować lokalizację… To naprawdę proste, wystarczy zrozumieć 🙂

Czym jest middleware pipeline?

Jeśli wiesz, czym jest middleware pipeline w .NetCore, możesz przejść dalej. Jeśli nie wiesz – też możesz, ale dalsza część artykułu będzie trochę niejasna.

Napiszę bardzo ogólnie jak to działa, dużo lepiej jest to opisane w artykule o middleware pipeline 🙂

Pipeline (czyli potok) to seria akcji wykonywanych jedna po drugiej podczas odbierania żądania od klienta i wysyłania odpowiedzi. W metodzie Configure ustawiasz właśnie te komponenty w pipelinie za pomocą metod, których nazwy rozpoczynają się zwyczajowo od Use. Np. UseAuthentication, UseAuthorization itd. Spójrz na przykładowe kody:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
   app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");
}

Dla takiego kodu pipeline będzie prawie pusty:

Dodajmy teraz przekierowanie https:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
   app.UseHttpsRedirection();
   app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");
}

Teraz pipeline będzie wyglądał tak:

Żądanie przejdzie najpierw przez HttpsRedirection, który może sobie na nim pracować i może przekazać wywołanie do kolejnego middleware (ale wcale nie musi). Żądanie może następnie trafić do RouterMiddleware, który wie, jaką stronę ma pokazać. Następnie generowana jest odpowiedź, która przechodzi przez middleware’y w odwrotnej kolejności (w tym momencie nie można już zmodyfikować nagłówków).

Widzisz zatem, że kolejność dodawania middleware’ów do pipeline jest istotna, a nawet dokładnie opisana na stronie Microsoftu: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/middleware/?view=aspnetcore-6.0#middleware-order . No bo co się stanie, jeśli dodasz autoryzację za routerem? Autoryzacja zadziała dopiero za routerem, a więc użytkownik zobaczy już stronę, której nie powinien.

Konfiguracja języków

Najpierw trzeba skonfigurować języki w aplikacji RAZOR. Przede wszystkim zajrzyj do pliku Startup.cs i tam odnajdź metodę ConfigureServices. (jeśli używasz .NET6, możesz nie widzieć Startup.cs, wszystko dzieje się w pliku Program.cs)

Teraz musisz w niej skonfigurować serwis odpowiedzialny za lokalizację. Są takie metody (extensions) w IServiceCollection jak AddControllers*, AddMVC*, czy też AddRazorPages. Każda z nich zwraca obiekt implementujący IMvcBuilder. Z kolei ten, ma w sobie rejestrację lokalizacji (AddViewLocalization()), a więc np:

using Microsoft.AspNetCore.Localization;
using Microsoft.Extensions.DependencyInjection;

//...
public void ConfigureServices(IServiceCollection services)
{
  services.AddControllersWithViews()
      .AddViewLocalization();
}

Najprostszą konfigurację lokalizacji robimy w metodzie Configure – PRZED mapowaniem ścieżek. A więc dodajemy to do pipeline. Wygląda to tak:

IList<CultureInfo> supportedCultures = new List<CultureInfo>
{
    new CultureInfo("en-US"),
    new CultureInfo("pl"),
};

var localizationOptions = new RequestLocalizationOptions
{
    DefaultRequestCulture = new RequestCulture("en-US"),
    SupportedCultures = supportedCultures,
    SupportedUICultures = supportedCultures        
};

app.UseRequestLocalization(localizationOptions);

Teraz przyda się kilka słów wyjaśnienia.

Najpierw trzeba użyć oprogramowania pośredniczącego (middleware) do lokalizacji. Robimy to przez włączenie do pipeline UseRequestLocalization. Można to zrobić na kilka sposobów:

  • app.UseRequestLocalization() – bez parametrów – odczyta lokalizację z nagłówka żądania, który wysyłany jest przez przeglądarkę. I tyle. Niczego tu nie można zmienić.
  • app.UseRequestLocalization(RequestLocalizationOptions) – od razu skonfiguruje middleware RequestLocalization zgodnie z przekazanymi opcjami
  • app.UseRequestLocalization(Action) – podobnie jak wyżej, tyle że przekazujemy tutaj akcję, w której konfigurujemy middleware.

W naszym przykładzie włączamy RequestLocalization do pipeline (pamiętaj, że ZANIM zmapujemy ścieżki), przekazując opcje.

Wróćmy do kodu:

IList<CultureInfo> supportedCultures = new List<CultureInfo>
{
    new CultureInfo("en-US"),
    new CultureInfo("pl"),
};

var localizationOptions = new RequestLocalizationOptions
{
    DefaultRequestCulture = new RequestCulture("en-US"),
    SupportedCultures = supportedCultures,
    SupportedUICultures = supportedCultures        
};

app.UseRequestLocalization(localizationOptions);

Najpierw tworzona jest lista kultur, które wspieramy, a w drugim kroku ustawiamy opcje lokalizacji:

  • przekazujemy domyślną kulturę (DefaultRequestCulture)
  • przypisujemy wspierane kultury UI i zwykłe

Taka konfiguracja daje nam dostęp do:

  • odczytu lokalizacji z przeglądarki (z nagłówka żądania)
  • odczytu lokalizacji z parametrów zapytania (?culture=pl-PL)
  • odczytu lokalizacji z ciasteczka

Czyli konfigurując w taki sposób (z przekazaniem RequestLocalizationOptions) mamy dużo więcej niż po prostu włączając middleware do pipeline bez jego konfiguracji.

To teraz pytanie, skąd system wie, w jaki sposób ma pobrać dane o aktualnej kulturze? Czary? Nie! Z pomocą przychodzi…

RequestCultureProvider

To jest klasa abstrakcyjna, której zadaniem jest zwrócić informacje o kulturze na podstawie danych z żądania. Kilka domyślnych providerów jest już utworzonych i właściwie nie potrzeba więcej, chociaż możesz stworzyć własne (np. odczyt kultury z bazy danych).

W klasie RequestLocalizationOptions (opcje lokalizacyjne) poza obsługiwanymi kulturami znajduje się też lista RequestCultureProvider. Domyślnie utworzone są takie:

QueryStringRequestCultureProvider

zwraca kulturę z zapytania w adresie, np: https://example.com/Home/Index?culture=en-US; świetnie nadaje się to do debugowania. Domyślnie operuje na dwóch kluczach: culture i ui-culture. Wystarczy, że w zapytaniu będzie jeden z nich, drugi otrzyma taką samą wartość. Jeśli są oba, np: ?culture=en-US&ui-culture=en-GB, wtedy inne będą ustawienia dla CurrentCulture i CurrentUICulture.

Oczywiście klucze możesz sobie zmieniać za pomocą właściwości

  • QueryStringKey (domyślnie „culture”)
  • UIQueryStringKey (domyślnie „ui-culture”)

Także zamiast ?culture=en-US będziesz mógł podać np. ?lang=en

CookieRequestCultureProvider

zwraca kulturę z ciasteczka. Sam możesz zdecydować o tym, jak ma nazywać się dane ciasteczko (za pomocą właściwości CookieName). Domyślnie to: „.AspNetCore.Culture”.

Żeby to zadziałało, oczywiście jakieś ciasteczko musi zostać wcześniej zapisane. Ta klasa ma dwie przydatne metody statyczne: ParseCookieValue i MakeCookieValue. MakeCookieValue zwróci Ci dokładną zawartość ciasteczka, jakie musisz zapisać.

AcceptLanguageHeaderRequestCultureProvider

zwraca kulturę zapisaną w przeglądarce (a właściwie wysłaną przez przeglądarkę w nagłówkach).

Kolejność tych providerów jest istotna. Jeśli pierwszy nie zwróci danych, drugi spróbuje. Jeśli w przeglądarce masz zapisaną kulturę pl-PL, ale w zapytaniu w adresie strony wpiszesz ?culture=en-US, zobaczysz stronę po angielsku, ponieważ pierwszy w kolejności jest QueryStringRequestCultureProvider.

Oczywiście manipulując tą listą możesz zmienić kolejność providerów, usuwać ich i dodawać nowych.

Pobieranie języka z adresu

Pewnie nie raz widziałeś (chociażby na stronach Microsoftu) taki sposób przekazywania kultury: https://example.com/en-US/Home/Index

gdzie informacje o niej są zawarte w adresie (w URL). Tutaj też tak można, a z pomocą przychodzi RouteDataRequestCultureProvider. Ten provider nie jest domyślnie tworzony, więc trzeba stworzyć obiekt tej klasy samemu i dodać go do RequestLocalizationOptions na pierwszym miejscu:

IList<CultureInfo> supportedCultures = new List<CultureInfo>
{
    new CultureInfo("en-US"),
    new CultureInfo("pl"),
};

var localizationOptions = new RequestLocalizationOptions
{
    DefaultRequestCulture = new RequestCulture("en-US"),
    SupportedCultures = supportedCultures,
    SupportedUICultures = supportedCultures        
};

var requestProvider = new RouteDataRequestCultureProvider();
localizationOptions.RequestCultureProviders.Insert(0, requestProvider);

app.UseRequestLocalization(localizationOptions);

Żeby to zadziałało, trzeba jeszcze poinformować router, że w ścieżce są informacje o kulturze:

app.MapControllerRoute(
    name: "default",
    pattern: "{culture=en-US}/{controller=Home}/{action=Index}/{id?}");

Tutaj analogicznie jak przy QueryStringRequestCultureProvider możesz zmienić właściwościami klucze culture i uiculture. Oczywiście musisz pamiętać wtedy o zmianie template’a ścieżki.

Tą metodę wywołaj w metodzie Configure, która jest odpowiedzialna za konfigurację zarejestrowanych serwisów – zrób to przed konfiguracją endpointów.

Pobieranie tłumaczenia na widoku

Teraz już możesz pobierać tłumaczenia. Wystarczy, że dodasz do usingów w widokach: Microsoft.AspNetCore.Mvc.Localization i wstrzykniesz interfejs IStringLocalizer:

@using Microsoft.AspNetCore.Mvc.Localization
@inject IStringLocalizer<LangRes> SharedLocalizer
@inject IStringLocalizer<WebLangRes> WebLocalizer

<h>@WebLocalizer[nameof(WebLangRes.HelloMsg)]</h>

Jak widzisz, możesz wstrzyknąć do jednego widoku kilka takich „lokalizerów”. W zmiennej generycznej określasz tylko klasę z Twoimi zasobami (czyli to, co robiliśmy w tym artykule). Ja tutaj mam dwa takie zasoby – jeden główny w jakimś projekcie współdzielonym (LangRes) i drugi tylko w projekcie MVC (WebLangRes), w którym są teksty bardzo ściśle związane z serwisem www.

Przy takim prostym wywołaniu jak wyżej (tekst w tagu HTML) nic więcej nie trzeba robić. Natomiast jeśli chcesz przekazać tłumaczenie do tag helpera, musisz dołożyć po prostu właściwość Value, np.:

<p>@SharedLocalizer[nameof(LangRes.Contact)]
  <mail prompt="@WebLocalizer[nameof(WebLangRes.MailPrompt)].Value" />
</p>

IHtmlLocalizer

Mamy do dyspozycji jeszcze coś takiego jak IHtmlLocalizer. Działa prawie tak samo jak IStringLocalizer, z tą różnicą, że możesz mu przekazać zasoby z tagami html, np: <b>Hello!</b>. Jednak nie używam go, bo trochę mi śmierdzi wpisywanie kodu html do zasobów.

To tyle. Jeśli czegoś nie zrozumiałeś lub znalazłeś w tekście błąd, daj znać w komentarzu.

Jeśli uważasz ten artykuł za przydatny, udostępnij go.

Podziel się artykułem na:
Tag helpers na poważnie

Tag helpers na poważnie

Hej, w pierwszej części artykułu pisałem o podstawach Tag Helpers w Razor. Jeśli nie czytałeś tego, to koniecznie nadrób zaległości, bo inaczej ten artykuł może być niezrozumiały.

Dzisiaj polecimy dalej i wyżej. Opowiem Ci o bardziej zaawansowanych rzeczach, które możemy zrobić z tag helperami i stworzymy coś miłego dla oka. Dlatego, na Boga, przeczytaj pierwszą część artykułu.

Co robimy?

Chcemy stworzyć tag helper, który ułatwi nam korzystanie z bootstrapowych kart. Jeśli nie wiesz, czym jest bootstrap, to w dużym skrócie można powiedzieć, że jest to zestaw styli css (framework css) gotowych do użycia na Twojej stronie, które bardzo przyspieszają zarówno tworzenie layoutu, jak i samej strony. Ponadto bootstrap posiada kilka ciekawych komponentów w JS takich jak np. karuzela, czy też okno modalne.

My zajmiemy się dzisiaj kartami. Chcemy uzyskać taki efekt:

efekt końcowy tag helpers

HTML do tego wygląda tak:

<div class="row">
    <div class="card-deck">
        <div class="card border-primary mb-3">
            <div class="card-header">Nagłówek</div>
            <img class="card-img-top" src="~/imgs/cat.jpg" alt="Obraz kota" />
            <div class="card-body">
                <h5 class="card-title">Koteł</h5>
                <h6 class="card-subtitle">Miauuu</h6>
                <p class="card-text">Kot jaki jest, każdy widzi</p>
                <a href="#" class="btn btn-primary">Guzik</a>
            </div>
            <div class="card-footer text-muted">Stopka</div>
        </div>

        <div class="card border-primary mb-3">
            <div class="card-header">Nagłówek</div>
            <img class="card-img-top" src="~/imgs/dog.jpg" alt="Obraz pieseła" />
            <div class="card-body">
                <h5 class="card-title">Pieseł</h5>
                <h6 class="card-subtitle">Hau hau</h6>
                <p class="card-text">Pieseł jaki jest, każdy widzi</p>
                <a href="#" class="btn btn-primary">Guzik</a>
            </div>
            <div class="card-footer text-muted">Stopka</div>
        </div>

        <div class="card border-primary mb-3">
            <div class="card-header">Nagłówek</div>
            <img class="card-img-top" src="~/imgs/pig.jpg" alt="Obraz świni" />
            <div class="card-body">
                <h5 class="card-title">Świnieł</h5>
                <h6 class="card-subtitle">Chrum chrum</h6>
                <p class="card-text">Chrumcia jaka jest, każdy widzi</p>
                <a href="#" class="btn btn-primary">Guzik</a>
            </div>
            <div class="card-footer text-muted">Stopka</div>
        </div>
    </div>
</div>

Nie jest to nic wybitnego, po prostu korzystamy z kart. Ten Bootstrapowy komponent jest dokładnie opisany tutaj.

Szybki opis karty

Pojedyncza karta wygląda tak:

karta bootstrap card

Na górze mamy nagłówek, pod nim obrazek, dalej tytuł (Pieseł), podtytuł (Hau hau) i zawartość. Na dole jest stopka, a cała karta jest w ramce. Wyłaniają nam się już poszczególne elementy, więc zacznijmy pisanie właśnie od tagu helper’a dla pojedynczej karty.

Zaczynamy pisanie tag helper’a

Pojedyncza karta

Jak już wiesz, karta składa się z kilku elementów. W tym momencie pominiemy ramkę, bo będzie za nią odpowiadało coś innego. A więc mamy:

  • tekst nagłówka
  • obrazek
  • tytuł i podtytuł
  • treść
  • tekst stopki

Przygotujmy taki szkielet tag helper’a:

public class CardTagHelper: TagHelper
{
	public string Header { get; set; } = string.Empty;
	public string ImgSrc { get; set; } = string.Empty;
	public string Title { get; set; } = string.Empty;
	public string SubTitle { get; set; } = string.Empty;
	public string Footer { get; set; } = string.Empty;

	public override void Process(TagHelperContext context, TagHelperOutput output)
	{
		base.Process(context, output);
	}
}

Póki co, nie ma tutaj niczego nadzwyczajnego. Zupełny szkielet tag helper’a z wymaganymi właściwościami. Zobaczmy, jak powinna wyglądać taka karta w HTML:

<div class="card mb-3">
    <div class="card-header">Nagłówek</div>
    <img class="card-img-top" src="~/imgs/dog.jpg" alt="Obraz pieseła" />
    <div class="card-body">
        <h5 class="card-title">Pieseł</h5>
        <h6 class="card-subtitle">Hau hau</h6>
        <p class="card-text">Pieseł jaki jest, każdy widzi</p>
        <a href="#" class="btn btn-primary">Guzik</a>
    </div>
    <div class="card-footer text-muted">Stopka</div>
</div>

Jak widzisz, najpierw jest główny div, trzymający w ryzach całą kartę. Następnie jest div z nagłówkiem, obrazek, div z ciałem i na końcu stopka. Zacznijmy od tego pierwszego – głównego:

public override void Process(TagHelperContext context, TagHelperOutput output)
{
	base.Process(context, output);

	output.TagName = "div";
	output.AddClass("card", HtmlEncoder.Default);
    output.AddClass("mb-3", HtmlEncoder.Default);
}

Nie ma tu niczego nowego, może poza metodą AddClass. To jest metoda helperowa, aby ją zobaczyć i użyć, musisz dodać using: Microsoft.AspNetCore.Mvc.TagHelpers. To jest po prostu pewne ułatwienie. Równie dobrze można by dodać klasy za pomocą atrybutów.

Teraz w razor (cshtml) użyjemy naszego helper’a. Wpisz po prostu:

<card></card>

Zobacz, jaki HTML został wyrenderowany:

<div class="card mb-3"></div>

Czyli można powiedzieć, że mamy już początek. Zanim pójdziemy dalej, muszę opowiedzieć Ci o 4 właściwościach z klasy TagHelperOutput, o których wcześniej nie mówiliśmy. Dla ułatwienia opowiem to łopatologicznie, niekoniecznie zgodnie z poprawnym nazewnictwem:

  • PreElement – to jest treść HTML, która będzie umieszczona zaraz przed Twoim tagiem
  • PreContent – to jest treść HTML, która będzie umieszczona w Twoim tagu, ale zaraz przed jego zawartością
  • PostContent – to jest treść HTML, która będzie umieszczona w Twoim tagu, po jego zawartości
  • PostElement – to jest treść HTML, która będzie umieszczona zaraz po Twoim tagu.

Aby to lepiej zobrazować, napiszmy sobie krótki kod. Najpierw tag helper (możesz użyć tego, nad którym aktualnie pracujemy):

public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
	output.TagName = "div";
	output.Attributes.Add("info", "main-div");
	output.PreElement.SetHtmlContent("<p>Przed elementem</p>");
	output.PreContent.SetHtmlContent("<p>Przed zawartością</p>");
	output.PostContent.SetHtmlContent("<p>Po zawartości</p>");
	output.PostElement.SetHtmlContent("<p>Po elemencie</p>");
}

Uruchom stronę z tym tag helperem i podejrzyj źródło HTML. Zobaczysz coś takiego:

<p>Przed elementem</p>
<div info="main-div">
    <p>Przed zawartością</p>
    <p>Po zawartości</p>
</div>
<p>Po elemencie</p>

Teraz tak się składa, że to, co wpisaliśmy w Razor nie ma żadnej zawartości. Wewnątrz tagu card nic się nie znajduje. Zmieńmy to:

<card>Zawartość okna</card>

Uruchom teraz swoją stronę i zobacz wynikowy HTML:

<p>Przed elementem</p>
<div info="main-div">
    <p>Przed zawartością</p>
    Zawartość okna
    <p>Po zawartości</p>
</div>
<p>Po elemencie</p>

Jak widzisz, możemy dowolnie sterować tym, co znajduje się zarówno przed tagiem, po nim, jak i w środku. Możemy tam dać dowolny i dowolnie długi kod HTML. I to właśnie wykorzystamy.

Nagłówek

Przypomnijmy sobie zatem na szybko kod bootstrap’a:

<div class="card mb-3">
    <div class="card-header">Nagłówek</div>
    <img class="card-img-top" src="~/imgs/dog.jpg" alt="Obraz pieseła" />
    <div class="card-body">
        <h5 class="card-title">Pieseł</h5>
        <h6 class="card-subtitle">Hau hau</h6>
        <p class="card-text">Pieseł jaki jest, każdy widzi</p>
        <a href="#" class="btn btn-primary">Guzik</a>
    </div>
    <div class="card-footer text-muted">Stopka</div>
</div>

Mamy zrobionego pierwszego diva. Zróbmy teraz nagłówek i obrazek:

public class CardTagHelper: TagHelper
{
	public string Header { get; set; } = string.Empty;
	public string ImgSrc { get; set; } = string.Empty;
	public string Title { get; set; } = string.Empty;
	public string SubTitle { get; set; } = string.Empty;
	public string Footer { get; set; } = string.Empty;

	public override void Process(TagHelperContext context, TagHelperOutput output)
	{
		base.Process(context, output);

		output.TagName = "div";
		output.AddClass("card", HtmlEncoder.Default);
		output.AddClass("mb-3", HtmlEncoder.Default);

		output.PreContent.AppendHtml(RenderHeader());
		output.PreContent.AppendHtml(RenderImg());
	}

	string RenderHeader()
	{
		return $"<div class=\"card-header\">{Header}</div>";
	}

	string RenderImg()
	{
		return $"<img class=\"card-img-top\" src=\"{ImgSrc}\" />";
	}
}

Zobacz, zrobiłem przy okazji dwie metody renderujące odpowiedni kod HTML. Teraz lekko zmieńmy wywołanie tego tag helper’a w Razor. Trzeba po prostu dodać brakujące treści:

<card header="Nagłówek" img-src="imgs/dog.jpg"></card>

Wynikiem tego kodu będzie:

<div class="card mb-3">
    <div class="card-header">Nagłówek</div>
    <img class="card-img-top" src="~/imgs/dog.jpg"/>
</div>

Treść karty i stopka

Super, powoli do przodu. Jeśli uruchomisz teraz program, zobaczysz wielką mordkę uśmiechniętego pieseła. Dodajmy teraz resztę – czyli treść karty i jej stopkę:

public override void Process(TagHelperContext context, TagHelperOutput output)
{
	base.Process(context, output);

	output.TagName = "div";
	output.AddClass("card", HtmlEncoder.Default);
	output.AddClass("mb-3", HtmlEncoder.Default);

	output.PreContent.AppendHtml(RenderHeader());
	output.PreContent.AppendHtml(RenderImg());
	
	output.PreContent.AppendHtml(RenderBody());
	output.PostContent.AppendHtml("</div>");

	output.PostContent.AppendHtml(RenderFooter());
}
string RenderBody()
{
	return $@"
		<div class='card-body'>
			<h5 class='card-title'>{Title}</h5>
			<h6 class='card-subtitle'>{SubTitle}</h6>";
}

string RenderFooter()
{
	return $"<div class=\"card-footer text-muted\">{Footer}</div>";
}

Metody RenderFooter nie ma co omawiać, natomiast zatrzymajmy się przy RenderBody. Jak widzisz wyprodukowaliśmy tu diva, a także tytuł i podtytuł karty. Czyli wszystko zgodnie z wzorcowym HTMLem. I teraz powinna pokazać się cała treść. A gdzie ona jest?

Spójrz jeszcze raz na metodę Process. Dodajemy kod „body” w PreContent. Następnie dodajemy diva zamykającego „body” w PostContent. Coś już świta? Spójrz teraz jak zmieniło się wywołanie tag helper’a w Razor:

<card header="Nagłówek" img-src="imgs/dog.jpg" footer="Stopka" title="Pieseł" sub-title="Hau hau">
    <p class="card-text">Pieseł jaki jest, każdy widzi</p>
    <a href="#" class="btn btn-primary">Guzik</a>
</card>

Co się okazuje? Że to, co damy między znacznikami początku i końca jest zawartością (Content) tag helpera! Nieźle. Uruchom teraz aplikację i zobacz kod wynikowy:

<div class="card mb-3">
    <div class="card-header">Nagłówek</div>
    <img class="card-img-top" src="imgs/dog.jpg">
	    <div class="card-body">
		    <h5 class="card-title">Pieseł</h5>
			<h6 class="card-subtitle">Hau hau</h6>
            <p class="card-text">Pieseł jaki jest, każdy widzi</p>
            <a href="#" class="btn btn-primary">Guzik</a>
        </div>
    <div class="card-footer text-muted">Stopka</div>
</div>

Podświetlone linijki to właśnie te, które stanowią zawartość tag helper’a. Te 11 linijek kodu zamieniliśmy w 4 zdecydowanie prostsze! Hurra ja! Ale ale… Jeszcze nie pora na piwo. To dopiero pierwszy etap zadania.

Grupa kart

Skoro mamy już opracowaną kartę, zajmiemy się teraz grupą kart. Na początek zróbmy najprościej jak się da. Przypominam, jak w HTML wygląda taka grupa:

<div class="card-deck">
    ...
</div>

Klasa będzie na tyle prosta, że sam już powinieneś to napisać:

public override void Process(TagHelperContext context, TagHelperOutput output)
{
	base.Process(context, output);

	output.TagName = "div";
	output.AddClass("card-deck", HtmlEncoder.Default);
}

I to właściwie tyle. Zaskoczony? Popatrz teraz na Razor:

<card-group>
    <card header="Nagłówek" img-src="imgs/cat.jpg" footer="Stopka" title="Koteł" sub-title="Miauuu">
        <p class="card-text">Koteł jaki jest, każdy widzi</p>
        <a href="#" class="btn btn-primary">Guzik</a>
    </card>

    <card header="Nagłówek" img-src="imgs/dog.jpg" footer="Stopka" title="Pieseł" sub-title="Hau hau">
        <p class="card-text">Pieseł jaki jest, każdy widzi</p>
        <a href="#" class="btn btn-primary">Guzik</a>
    </card>

    <card header="Nagłówek" img-src="imgs/pig.jpg" footer="Stopka" title="Świnieł" sub-title="Chrum chrum">
        <p class="card-text">Chrumcia jaka jest, każdy widzi</p>
        <a href="#" class="btn btn-primary">Guzik</a>
    </card>
</card-group>

Dodajemy ramki

Czego jeszcze brakuje? Brakuje ramek. Ale ja chcę, żeby o istnieniu lub nieistnieniu ramki decydował card-group, a nie card. Jak to uzyskać? Z pomocą przychodzi pierwszy parametr metody Process – TagHelperContext.

TagHelperContext

To kontekst, który może być przekazywany między tag helperami. Ten kontekst będzie przekazany do metody Process (lub ProcessAsync) wszystkim tag helperom, które są w środku głównego TagHelpera.

Nie jest to wybitna klasa, ale ma jedną bardzo cenną właściwość: Items. Items to słownik, w którym zarówno kluczem i wartością jest object. Co znaczy, że możesz tam wrzucić dosłownie wszystko. Ten słownik posłuży do przekazywania danych między tag helperami. Stwórzmy sobie klasę o nazwie CardData:

class CardData
{
	public bool ShowBorder { get; set; } = false;
}

Ta klasa przechowuje wartość, czy obramowanie ma być widoczne, czy nie. Uzupełnijmy teraz card-group o ten border:

public class CardGroupTagHelper: TagHelper
{
	public bool ShowBorder { get; set; }
	public override void Process(TagHelperContext context, TagHelperOutput output)
	{
		base.Process(context, output);

		output.TagName = "div";
		output.AddClass("card-deck", HtmlEncoder.Default);

		CardData data = new CardData();
		data.ShowBorder = ShowBorder;
		context.Items[nameof(CardData)] = data;
	}
}

Utworzyliśmy tu klasę CardData i przypisaliśmy jej właściwość ShowBorder. Następnie umieściliśmy utworzony obiekt w słowniku. Widziałem też kody, w których zamiast nameof(CardData) jako klucz, było typeof(CardData). Możesz sobie tam nawet wpisać „Słoń”. Dopóki wiesz jak odczytać te dane, nie ma to żadnego znaczenia. Jednak użycie nameof lub typeof wydaje się najbardziej racjonalne. Więc teraz dokończymy kod karty:

public override void Process(TagHelperContext context, TagHelperOutput output)
{
	base.Process(context, output);

	output.TagName = "div";
	output.AddClass("card", HtmlEncoder.Default);
	output.AddClass("mb-3", HtmlEncoder.Default);

	CardData data = context.Items[nameof(CardData)] as CardData;
	if (data.ShowBorder)
		output.AddClass("border-primary", HtmlEncoder.Default);

	output.PreContent.AppendHtml(RenderHeader());
	output.PreContent.AppendHtml(RenderImg());
	
	output.PreContent.AppendHtml(RenderBody());
	output.PostContent.AppendHtml("</div>");

	output.PostContent.AppendHtml(RenderFooter());
}

Pobraliśmy tutaj obiekt CardData stworzony w rodzicu tego tag helpera (card-group) i reszta jest już oczywista. To na tyle… Prawie…

Ograniczenia

Co się stanie, jeśli do card-group nie dodamy card, tylko coś innego? Np. jakiś obrazek? Być może nic złego, ale na pewno coś dziwnego. Nie chcemy takiej sytuacji. Chcemy się upewnić, że dziećmi card-group mogą być tylko elementy card. Z pomocą przychodzi…

RestrictChildren

To jest atrybut nakładany na głównego tag helpera. Powoduje, że jego dziećmi mogą być jedynie konkretne klasy, np:

[RestrictChildren("card")]
public class CardGroupTagHelper: TagHelper
{
//...
}

Spowoduje to, że dziećmi taga card-group mogą być jedynie tagi o nazwie card. Jeśli teraz w razor umieścisz taki kod:

<card-group show-border="true">
    <mail>Mail</mail>
    <card header="Nagłówek" img-src="imgs/cat.jpg" footer="Stopka" title="Koteł" sub-title="Miauuu">
        <p class="card-text">Koteł jaki jest, każdy widzi</p>
        <a href="#" class="btn btn-primary">Guzik</a>
    </card>
</card-group>

to program się nie skompiluje i dostaniesz błąd: error RZ2010: The tag is not allowed by parent tag helper. Only child tags with name(s) 'card' are allowed.

Oczywiście ograniczenie nie dotyczy tylko tag helperów. Jeśli dasz tam jakiegoś diva (zamiast mail), img, czy cokolwiek innego, to program też się nie skompiluje.

Nie musimy się ograniczać do jednego typu dziecka. W końcu „wszystkie dzieci nasze są”. Możemy podać ich kilka, używając drugiego parametru atrybutu RestrictChildren:

[RestrictChildren("card", "img", "mail")]
public class CardGroupTagHelper: TagHelper
{
//...
}

Możemy podać tyle tagów, ile tylko chcemy.

Nic nie renderuj – czyli SupressOutput

Istnieje pewna metoda, która niejako niweczy całą pracę wykonaną przez TagHelpera. W klasie TagHelperOutput znajduje się SupressOutput. Ona po prostu powoduje brak wyświetlenia czegokolwiek w tym tagu. I tu pewnie zapytasz się – po co to komu? Czasem się to przydaje. Weźmy pod uwagę taki prosty kod:

class Data
{
	public List<string> Children { get; set; } = new List<string>();
}
public class ParentTagHelper: TagHelper
{
	public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
	{
		await base.ProcessAsync(context, output);

		output.TagName = "div";

		Data data = new Data();
		context.Items[nameof(Data)] = data;
		await output.GetChildContentAsync();

		output.Attributes.SetAttribute("count", data.Children.Count);

		string content = "";
		foreach (var child in data.Children)
			content += child + "<br />";

		output.Content.SetHtmlContent(content);
	}
}

public class ChildTagHelper: TagHelper
{
	public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
	{
		await base.ProcessAsync(context, output);

		TagHelperContent content = await output.GetChildContentAsync();
		
		Data data = context.Items[nameof(Data)] as Data;
		data.Children.Add(content.GetContent());
		
		output.SuppressOutput();
	}
}

Przeanalizujmy go. Najpierw deklarujemy klasę Data, która będzie przechowywać jakieś dane tagów. Następnie mamy tag helper’a – Parent. Spójrzmy co on robi:

  • zmienia swój tag na div
  • tworzy obiekt klasy Data
  • dodaje ten obiekt do kotekstu
  • i wreszcie wywołuje metodę GetChildContentAsync.

Metoda GetChildContentAsync() powoduje, że ruszają wszystkie tagi (metoda ProcessAsync()), będące dziećmi tego. Spójrzmy teraz na kod w ChildTagHelper, który właśnie w tym momencie zostanie wywołany:

  • tag helper pobiera swoją zawartość
  • pobiera obiekt data
  • na koniec dodaje swoją zawartość do listy

Wywołanie SupressOutput() spowoduje, że ten tag helper niczego nie wyświetli. Po prostu przekazał pewne dane do kontekstu i na tym zakończył swoją pracę.

Wróćmy teraz do ParentTagHelper. Gdy wszystkie dzieci wykonają swoją robotę, GetChildContentAsync się skończy i ParentTagHelper będzie kontynuował swoją pracę. W tym momencie odczyta z listy ilość dzieci i ustawi ją jako atrybut.

Następnie między wszystkie stringi (które pochodzą z dzieci) wstawi znacznik <br /> i wszystko to ustawi jako swoją zawartość.

Zobacz, jak to wygląda od strony Razor:

<parent>
    <child>Wiersz 1</child>
    <child>Wiersz 2</child>
    <child>Wiersz 3</child>
</parent>

A wyrenderowany HTML będzie wyglądał tak:

<div count="3">Wiersz 1<br />Wiersz 2<br />Wiersz 3<br /></div>

Więc czasem ten SupressOutput jest przydatny. Nie tylko, żeby coś renderować warunkowo, ale też jeśli potrzebujesz mieć w Razor jakieś dzieci, ale tak naprawdę całą zawartością będzie z jakiegoś powodu sterował główny tag.

To na tyle, jeśli chodzi o TagHelpery w Razor. Mam nadzieję, że wszystko jest jasne. Jeśli jednak masz jakiś problem albo znalazłeś w artykule błąd, podziel się w komentarzu.

Podziel się artykułem na:
Różne typy projektów w VisualStudio 2019

Różne typy projektów w VisualStudio 2019

Po pewnych zmianach w Visual Studio 2019, tworzenie projektów może być nieco niezrozumiałe zwłaszcza dla młodszych programistów. Ten artykuł wyjaśnia po kolei typy projektów w VisualStudio i jest kierowany głównie do początkujących.

Jaki utworzyć projekt

Stworzyłem specjalnie na potrzeby tego artykułu prosty i nieidealny algorytm do wyboru typu projektu. Możesz go pobrać stąd

Algorytm wyboru odpowiedniego typu projektu

Co możemy utworzyć

Visual Studio daje nam właściwie nieograniczone możliwości. Pomaga utworzyć projekt na dowolną platformę w konkretnym języku i w wybranej technologii (gdzie można). Tak wygląda okienko tworzenia nowego projektu:

Okienko tworzenia nowego projektu w Visual Studio
Tworzenie nowego projektu

Okienko jest podzielone na dwie główne grupy. Z lewej strony znajduje się lista ostatnich używanych typów projektów. Po prostu możesz kliknąć na to, co już ostatnio tworzyłeś i nie musisz szukać w liście po prawej stronie.

Z prawej strony masz z kolei grupę podzieloną na dwa elementy. Na górze filtr, na dole listę dostępnych typów projektów. One mogą być różne w zależności od tego, jak zainstalowałeś Visual Studio (typy też można dodawać i usuwać z poziomu instalatora: Visual Studio Installer, który znajduje się na Twoim komputerze)

Jeśli teraz zaczniesz wpisywać w filtrze na górze nazwę typu projektu, który chcesz utworzyć, lista automatycznie zacznie się zmieniać. Ja przykładowo wpisałem Console i zobacz, jak wygląda moja lista na obrazku wyżej. Tych projektów jest dużo więcej. Teraz postaram się część z nich omówić:

Add-In

To jest po prostu rozszerzenie (plugin) do Visual Studio. Tak, można pisać własne pluginy. Będę o tym pisał w niedalekiej przyszłości. Tak naprawdę całe Visual Studio składa się z pluginów. Jak widzisz są to projekty VSIX.

VSIX Project

To projekt rozszerzenia VisualStudio z dodanymi już pewnymi plikami i kodami. Takich projektów jest kilka w zależności od języka, w którym piszesz (C#, VisualBasic)

WIdok projektu Add-In

Empty VSIX Project

jest to zupełnie pusty projekt rozszerzenia VisualStudio. Od tego powyżej różni się brakiem kodów i domyślnych plików. Wszystko piszesz od zera. Ma to swoje plusy i minusy.

Widok pustego projektu Add-In

Empty VSIX Project (Community)

to samo, co wyżej z tą różnicą, że zawiera referencje do biblioteki Community.VisualStudio.Toolkit, co ułatwia pisanie rozszerzeń

VSIX Project w/Command (Community)

ten template zawiera to co VSIXProject, dodatkowo posiada referencje do Community.VisualStudio.Toolkit i dodatkowo posiada napisaną już komendę. Ten suffiks w/Command czytaj jako „with command”

Widok projektu Add-In Community

VSIX Project w/Tool Window (Community)

Uuuu, to jest dopiero mocarz. Zawiera wszystko to, co VSIX Project w/Command (Community), ale dodatkowo własne okno narzędziowe (Tool window)

Widok projektu Add-In z okienkiem

Który projekt wybrać do tworzenia rozszerzenia? To oczywiście zależy. Najbardziej obszernym zdecydowanie jest VSIX Project w/Tool Window (Community) i może się wydawać najlepszym. Tam, gdzie używasz okien i komend może to być szybki start. Oczywiście jeśli wcześniej nie tworzyłeś żadnych rozszerzeń, polecam zaczynać od najmniejszego projektu Empty VSIX Project. Nie przytłoczy Cię duża ilość plików na początek i będziesz mógł rozwijać rozszerzenie w swoim tempie, ucząc się powoli nowych rzeczy.

Console

Czym są aplikacje konsolowe?

Aplikacje konsolowe tworzone są na komputery. Często są to jakieś narzędzia dostępne z wiersza poleceń lub proste programy testowe, gdzie szybko chcemy sprawdzić jakieś funkcjonalności.

Konsola

Ale aplikacje konsolowe potrafią być często naprawdę dużymi, poważnymi narzędziami. Czasami posiadają nakładki graficzne, dzięki którym łatwiej je obsługiwać. Zdarzają się też proste gry tekstowe lub z grafiką w postaci ASCII Art.

Typy aplikacji konsolowych

  • Console Application (dostępna w różnych językach: C#, F#, VisualBasic…) – podstawowa aplikacja konsolowa na komputery. Podczas jej tworzenia zostaniesz zapytany o wersję .NetCore, której używać
Wybór framework

Domyślnie w tej wersji Visual Studio aplikacje konsolowe są tworzone w .NetCore. W tym miejscu możesz wybrać wersję, której chcesz używać.

Jest to właściwie najmniejszy template. Domyślnie posiada tylko jeden plik Program.cs z małym fragmentem kodu.

Chociaż długo sam nie potrafiłem tego pojąć, to jednak aplikacje konsolowe są tym typem, od którego powinno zaczynać się naukę programowania. Po prostu jest wtedy dużo prościej pojąć pewne rzeczy.

Blank Node.js console application

są dwie wersje zależne od języka. JavaScript lub TypeScript. Jeśli nie wiesz, czym jest TypeScript, to po prostu JavaScript z typami. Sam TypeScript jest kompilowany później do JavaScript. Generalnie to jest najmniejszy template do tworzenia aplikacji w Node.js

Console App (.Net Framework)

to jest aplikacja konsolowa, zupełnie taka jak domyślna Console Application. Też dostępna w kilku językach. Różnica jest taka, że używając tego templatu, będziesz miał do dyspozycji cały .Net Framework. Ale uwaga, ten typ projektu tworzy aplikacje jedynie dla Windowsa.

Widok projektu konsolowego

Standalone code analysis tool

to służy do tworzenia analizatorów kodu. Jakiś czas temu w VisualStudio pojawiły się takie analizatory kodu, które za pomocą Roslyn potrafią podpowiedzieć Ci kilka rzeczy. Np., że powinieneś dany fragment kodu wywołać w wątku głównym. Albo, że możesz jakiś kod napisać czyściej. Dzięki temu VisualStudio potrafi też automatycznie dodać odpowiednie namespacey do pliku cs. Oczywiście nic nie stoi na przeszkodzie, żeby napisać własne analizatory. Do tego służy ten template.

Jak widzisz mamy tutaj właściwie 4 różne typy aplikacji do wyboru – Kosnola dla Windows, konsola niezależna od systemu (.NetCore)*, konsola w Node.js no i analizator kodu.

Pisząc „konsola niezależna od systemu” mam na myśli, że kiedyś tak może być. Na dzień dzisiejszy aplikacje konsolowe i okienkowe są tworzone tylko pod Windows. Ale w przyszłości może się to zmienić. Wtedy dużo prościej będzie przejść z aplikacji pisanej w .NetCore (lub będzie to w standardzie)

Desktop

To jest to, co lubię najbardziej. Stare, dobre aplikacje na komputery :). Tutaj tworzymy głównie aplikacje okienkowe i wszystko co z nimi powiązane (biblioteki, kontrolki, serwisy), ale możemy też znaleźć templaty do testów jednostkowych.

Opiszę pokrótce niektóre typy:

Windows Forms

  • Windows Forms App – tworzymy aplikację okienkową w technologii WindowsForms. Jedynie dla Windows. Używamy wybranej wersji .NetCore. Dostępne dla C# i VisualBasic
  • Windows Forms Class Library – biblioteka dll, której możemy używać w aplikacjach Windows Forms App. Jedynie dla Windows.
  • Windows Forms Control Library – biblioteka, w której możemy tworzyć dodatkowe kontrolki i używać ich w Windows Forms App. Tutaj też piszemy tylko dla Windows i w .NetCore.
  • Windows Forms * (.NET Framework) – te kilka pozycji jest analogiczne do tych powyżej. Z tą jedynie różnicą, że mamy do dyspozycji pełny .NET Framework

UWAGA! Pamiętaj o tym, że nie możesz mieszać ze sobą technologii .NetCore i .NET Framerowk. To znaczy, że jeśli tworzysz aplikację w .NetCore, to nie będziesz mógł użyć w niej .NET Framework.

WPF (Windows Presentation Foundation)

  • WPF Application – nowsza technologia niż Windows Forms. Używamy kodu XAML do projektowania okienek. Używamy wybranej wersji .NetCore. Dostępne jedynie dla Windows.
  • WPF Class Library – analogicznie jak w Windows Forms. Po prostu dllka, której możemy używać w WPF Application. Używamy wybranej wersji .NetCore i piszemy tylko dla Windows
  • WPF Custom Control Library – analogicznie jak w Windows Forms Control Library. Tworzymy kontrolki dla aplikacji WPF. Używamy wybranej wersji .NetCore i piszemy tylko dla Windows
  • WPF UserControl Library – tutaj mały haczyk. Możemy tworzyć „kontrolki” dziedziczące po UserControl i używać ich w aplikacji WPF. Nie ma to odpowiednika w WindowsForms, ponieważ WPF wymaga nieco innego podejścia. Używamy wybranej wersji .NetCore i piszemy w Windows.
  • WPF Application (.NET Framework) – to samo, co wyżej z tą różnicą, że używasz pełnej wersji .NET Framerowk

UWAGA! Pamiętaj o tym, że nie możesz mieszać ze sobą technologii .NetCore i .NET Framerowk. To znaczy, że jeśli tworzysz aplikację w .NetCore, to nie będziesz mógł użyć w niej .NET Framework.

Universal Windows

To są aplikacje, które mogą działać na wszystkich platformach z Windowsem. Na komputerach, telefonach (z Windowsem), XBox… Generalnie wszędzie tam, gdzie jest zainstalowany Windows. Co ciekawe, sam możesz tworzyć swoje urządzenia z odpowiednim chipem i tworzyć aplikacje, które będą na nim pracowały.

Te aplikacje też wykorzystują XAML do tworzenia okienek, ale robi się je zupełnie inaczej niż aplikacje w WPF. Mają zupełnie inny cykl życia i inne kontrolki.

Windows Service

To są usługi, które pracują w systemie. Po prostu takie aplikacje bez okienek, które pracują cały czas w tle.

Mobile

W tej kategorii znajdziemy templaty do tworzenia aplikacji mobilnych. Głównie w technologii Xamarin. Nie będę się tu za bardzo wysilał, bo też nie ma nad czym. Opiszę tylko w kilku słowach, czym jest Xamarin.

Xamarin

Wszystkie aplikacje oznaczone jako Xamarin, są pisane w technologii Xamarin. No tak, a masło jest z masła. Generalnie Xamarin to taka technologia, która próbuje połączyć ze sobą różne platformy. Android, iOS, Universal Windows i inne. Celem, jaki temu przyświeca jest to, żeby jak największa część kodu była współdzielona pomiędzy te wszystkie aplikacje. Zarówno część wizualna (Xamarin.Forms) jak i cała logika.

Mimo, że technologia ma już kilka lat, to jednak cały czas (maj 2021) są z nią jakieś problemy i czasami pewne rzeczy trzeba napisać inaczej niż by się chciało. Ale cały czas jest rozwijana i ulepszana. I faktycznie z roku na rok wygląda to coraz lepiej.

Co do reszty templatów (np. Photo Editing Extension) nie będę się rozpisywał, bo nie miałem z nimi do czynienia. Można się domyślić, że jeśli chcesz napisać aplikację na telewizor, użyjesz odpowiedniego templatu z oznaczeniem tvOS (chyba, że piszesz na Androida). Jeśli interesuje Cię coś konkretnego z tej listy poza Xamarin, odsyłam do google 🙂

Other

U siebie mam tylko jeden typ – Blank Solution. I mogłoby paść pytanie – po co komu pusta solucja z samym tylko plikiem sln? Trochę jest to przeszłość, a czasami tak po prostu zaczyna się większe projekty. Pozwala Ci to od początku panować całkowicie nad strukturą katalogów i dodatkowych plików, których chcesz używać.

Web

No i doszliśmy do momentu, w którym mamy aplikacje typowo Webowe. Czyli to, co dzisiaj zdecydowanie jest na topie. Visual Studio umożliwia nam tu utworzenie kilku typów:

ASP.NET Core Empty

To jest pusta aplikacja webowa. Możesz z niej zrobić WebAPI, stronę internetową lub co tylko dusza zapragnie w webie. Podczas tworzenia zostaniesz zapytany o pewne ustawienia:

Wybór framework
  • TargetFramework – podajesz tutaj, jakiego frameworka chcesz używać. Zazwyczaj wybierzesz .NetCore 3.1 lub .NET 5
  • Configure for HTTPS – dzisiaj świat na wysokim miejscu stawia bezpieczeństwo. Również w sieci. Jeśli zaznaczysz tę opcję, projekt będzie skonfigurowany do pracy z protokołem HTTPS. Podczas pierwszego debugowania takiego projektu, Visual Studio utworzy i zainstaluje dla Ciebie specjalny certyfikat (Self Signed Certificate), a przeglądarki podczas pierwszego uruchomienia ostrzegą Cię, że ten certyfikat jest niebezpieczny i nie pochodzi z zaufanego źródła. Nie jest to w tym momencie istotne. Ważne, że gdy opublikujesz taką aplikację w sieci, musisz mieć na serwerze poprawny certyfikat. Dzisiaj można go otrzymać za darmo od Let’s Encrypt. Serwer, na którym mam swoje aplikacje – HostedWindows umożliwia utworzenie takiego certyfikatu i automatyczne przedłużenie go, za pomocą jednego kliknięcia.
  • Enable Docker – jeśli nie wiesz, czym jest docker, to moje wyjaśnienie nic Ci nie powie. Jeśli wiesz, to wiesz do czego służy ta opcja 🙂

A czysta aplikacja wygląda tak:

Widok projektu webowego

ASP.NET Core WebAPI

To jest template, który wykorzystasz konkretnie do utworzenia restowego WebAPI. Jeśli nie wiesz, czym jest restowe WebAPI, to bardzo krótko mówiąc…

Wyobraź sobie, że masz aplikację, w której chcesz wyświetlić wszystkich swoich znajomych z Facebooka. Facebook wystawia takie WebAPI (nie jest to restowe, ale dla przykładu załóżmy, że jest) i teraz możesz się z nim skomunikować i zapytać go o listę swoich znajomych, wywołując taki adres: https://api.facebook.com/friends

Ten adres jest tzw. „endpointem”. Podany przeze mnie endpoint nie istnieje w rzeczywistości, został wymyślony na potrzeby artykułu. Ale tak to mniej więcej działa – aplikacja wywołuje odpowiedni adres (endpoint), a serwer, jeśli Twoje uprawnienia na to pozwolą, odpowiada w jakiś ustalony sposób. Najczęściej odpowiedź jest w formie JSON, rzadziej XML.

Podczas tworzenia projektu tego typu, zostaniesz poproszony o dodatkowe informacje:

Wybór framework

Większość jest analogicznie jak wyżej z jedną małą różnicą.

  • Authentication Type – w projekcie WebAPI możesz i powinieneś w jakiś sposób autentykować i autoryzować swoich użytkowników. Ten artykuł zdecydowanie nie jest miejscem na wyjaśnienia co, jak i dlaczego, bo zagadnienie jest dość obszerne. Wkrótce o tym napiszę. Jeśli nie wiesz o co chodzi, to zostaw na None.

Projekt WebAPI nieco różni się od pustego:

Widok projektu WebAPI

Jak widzisz doszedł folder Controllers, zawierający kontrolery i kilka innych plików. Kod też jest inny. Zatem, jeśli chcesz stworzyć WebAPI, to zdecydowanie zacznij od tego projektu. Własne proste WebAPI dzięki temu można stworzyć w 10 minut (co dzisiaj udowodniłem komuś :)).

ASP.NET Core Web App

To jest typowa strona internetowa. Frontend tworzy się w RazorPages. A całość po utworzeniu wygląda tak:

Widok projektu RazorPages

ASP.NET Core App (Model-View-Controller)

To jest już aplikacja webowa, używająca wzorca MVC. Front tworzy się w RazorViews. Generalnie jest to bardzo podobne do RazorPages z pewnymi niuansami. Same dwa templaty (ten i poprzedni) są do siebie bardzo podobne i czasem jest ciężko stwierdzić, który będzie lepszy.

Generalnie zasada jest taka, że do prostych stron używamy raczej Razor Pages (ASP.NET Core App tego pierwszego), natomiast do bardziej skomplikowanych używamy MVC. Dodatkowo ten projekt może zawierać WebAPI. Ale osobiście radziłbym nie mieszać.

Utworzony pusty projekt wygląda tak:

Widok projektu Razor MVC

ASP.NET Web Application (.NET Framework)

To jest stary typ aplikacji webowych, który został zastąpiony .NET Core. Niemniej jednak mnóstwo aplikacji jest w tym utrzymywanych i rozwijanych. Odradzam używanie tego templatu do nowych programów.


No to tyle jeśli chodzi o podstawowe typy projektów. Jest ich jeszcze sporo, dodatkowe przychodzą z innymi pluginami, ale ich nazwy raczej same się opisują.

Jeśli znalazłeś w tekście błąd lub masz problem, podziel się w komentarzu

Podziel się artykułem na:
Tag helpers – podstawy tagów pomocniczych

Tag helpers – podstawy tagów pomocniczych

Pozwolisz, że w artykule będę posługiwał się anglojęzyczną nazwą „tag helper” zamiast „tag pomocniczy”, bo to po prostu brzmi jakby ktoś zajeżdżał tablicę paznokciem.

Czym jest tag helper?

Patrząc od strony Razor (niezależnie czy to RazorPages, czy RazorViews, robi się to identycznie), tag helper to nic innego jak tag w HTML. Ale nie byle jaki. Stworzony przez Ciebie. Kojarzysz z HTML tagi takie jak a, img, p, div itd? No więc tag helper od strony Razor to taki dokładnie tag, który sam napisałeś. Co więcej, tag helpery pozwalają na zmianę zachowania istniejących tagów (np. <a>). Innymi słowy, tag helpery pomagają w tworzeniu wyjściowego HTMLa po stronie serwera.

To jest nowsza i lepsza wersja HTML Helpers znanych jeszcze z czasów ASP.

Na co komu tag helper?

Porównaj te dwa kody z Razor:

<div>
    @Html.Label("FirstName", "Imię:", new {@class="caption"})
</div>
<div>
    <caption-label field="FirstName"></caption-label>
</div>

Który Ci się bardziej podoba? caption-label to właśnie tag helper. Jego zadaniem jest właściwie to samo, co metody Label z klasy Html. Czyli odpowiednie wyrenderowanie kodu HTML. Jednak… no musisz się zgodzić, że kod z tag helperami wygląda duuuużo lepiej.

(tag helper caption-label nie istnieje; nazwa została zmyślona na potrzeby artykułu)

Tworzenie Tag Helper’ów

Tworzenie nowego projektu

Utwórz najpierw nowy projekt WebApplication w Visual Studio. Jeśli nie wiesz, jak to zrobić, przeczytaj ten artykuł.

Nowy Tag Helper

GitHub i NuGet są pełne customowych (kolejne nieprzetłumaczalne słowo) tag helperów. W samym .NetCore też jest ich całkiem sporo. Nic nie stoi na przeszkodzie, żebyś stworzył własny. Zacznijmy od bardzo prostego przykładu.

Stwórzmy tag helper, który wyśle maila po kliknięciu. Czyli wyrenderuje dokładnie taki kod HTML:

<a href="mailto:mail@example.com?subject=Hello!">Wyślij maila</a>

Przede wszystkim musisz zacząć od napisania klasy dziedziczącej po abstrakcyjnej klasie… TagHelper. Chociaż tak naprawdę mógłbyś po prostu napisać klasę implementującą interfejs ITagHelper. Jednak to Ci utrudni pewne rzeczy. Zatem dziedziczymy po klasie TagHelper:

//using Microsoft.AspNetCore.Razor.TagHelpers

public class MailTagHelper: TagHelper
{

}

UWAGA! Nazwa Twojej klasy nie musi zawierać sufiksu TagHelper. Jednak jest to dobrą praktyką (tak samo jak tworzenie tag helperów w osobnym folderze TagHelpers lub projekcie). Stąd klasa nazywa się MailTagHelper, ale w kodzie HTML będziemy używać już tagu mail. Takie połączenie zachodzi automagicznie.

Super, teraz musimy zrobić coś, żeby nasz tag helper <mail> zamienił się na HTMLowy tag <a>. Służą do tego dwie metody:

  • Process – metoda, która zostanie wywołana SYNCHRONICZNIE, gdy serwer napotka na Twój tag helper
  • ProcessAsync – dokładnie tak jak wyżej, z tą różnicą, że to jest jej ASYNCHRONICZNA wersja

Wynika z tego, że musimy przesłonić albo jedną, albo drugą. Przesłanianie obu nie miałoby raczej sensu. Oczywiście będziemy posługiwać się asynchroniczną wersją, zatem przesłońmy tę metodę najprościej jak się da:

//using Microsoft.AspNetCore.Razor.TagHelpers

public class MailTagHelper: TagHelper
{
	public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
	{
		await base.ProcessAsync(context, output);
	}
}

Teraz zastanówmy się, jakie parametry chcemy przekazać do helpera. Ja tu widzę dwa:

  • adres e-mail odbiorcy
  • tytuł maila

Dodajmy więc te parametry do naszej klasy w formie właściwości:

//using Microsoft.AspNetCore.Razor.TagHelpers

public class MailTagHelper: TagHelper
{
	public string Address { get; set; }
	public string Subject { get; set; }
}

Rejestracja Tag Helper’ów

Rejestracja to w tym wypadku to bardzo duże słowo. Niemniej jednak, żeby nasz tag helper był w ogóle widoczny, musimy zrobić jeszcze jedną małą rzecz.

  1. Odnajdź w projekcie plik _ViewImports.cshtml i zmień go tak:
  2. Jeśli Twój tag helper znajduje się w innym namespace (np. umieściłeś go w katalogu TagHelpers), „zaimportuj” ten namespace na początku (moja aplikacja nazywa się WebApplication1):
    @using WebApplication1.TagHelpers
  3. Następnie „zarejestruj” swoje tag helpery w projekcie, dodając na końcu pliku linijkę:
    @addTagHelper *, WebApplication1
  4. Ostatecznie mój plik _ViewImports.cshtml wygląda tak:
@using WebApplication1
@using WebApplication1.TagHelpers
@namespace WebApplication1.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, WebApplication1

A teraz małe wyjaśnienie. Nie musisz tego czytać, ale powinieneś.

Plik _ViewImports jest specjalnym plikiem w .NetCore. Wszystkie „usingi” tutaj umieszczone mają wpływ na cały Twój projekt. To znaczy, że usingi z tego pliku będą w pewien sposób „automatycznie” dodawane do każdego Twojego pliku cshtml.

To znaczy, że jeśli tu je umieścisz, to nie musisz już tego robić w innych plikach cshtml. Oczywiście nigdy nie rób tego „na pałę”, bo posiadanie 100 nieużywanych usingów w jakimś pliku jeszcze nigdy nikomu niczego dobrego nie przyniosło 🙂

Zatem w tej linijce @using WebApplication1.TagHelpers powiedziałeś: „Chcę używać namespace’a WebApplication1.TagHelpers na wszystkich stronach w tym projekcie”.

A co do @addTagHelper. To jest właśnie ta „rejestracja” tag helpera. Zwróć najpierw uwagę na to, co dostałeś domyślnie od kreatora projektu: @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers. Zarejestrował on wszystkie domyślne tag helpery.

A teraz spójrz na tę linijkę, którą napisaliśmy: @addTagHelper *, WebApplication1. Jeśli coś Ci tu nie pasuje, to gratuluję spostrzegawczości. A jeśli masz pewność, że to jest ok, to gratuluję wiedzy 🙂

Można łatwo odnieść wrażenie, że rejestrujesz tutaj wszystkie (*) tag helpery z namespace WebApplication1. Jednak @addTagHelper wymaga podania nazwy projektu, a nie namespace’a. Akurat Bill tak chciał, że domyślne tag helpery znajdują się w projekcie o nazwie Microsoft.AspNetCore.Mvc.TagHelpers. Nasz projekt (a przynajmniej mój) nazywa się WebApplication1.

Przypominam i postaraj się zapamiętać:

Klauzula @addTagHelper wymaga podania nazwy projektu, w którym znajdują się tag helpery, a nie namespace’a.

Pierwsze użycie tag helper’a

OK, skoro już tyle popisaliśmy, to teraz użyjemy naszego tag helpera. On jeszcze w zasadzie niczego nie robi, ale jest piękny, czyż nie?

Przejdź do pliku Index.cshtml i dodaj tam naszego tag helpera. Zanim to jednak zrobisz, zbuduj projekt. Może to być konieczne, żeby VisualStudio wszystko zobaczył i zaktualizował Intellisense.

Widok podświetlenia tag helper w kodzie razor

Specjalnie użyłem obrazka zamiast kodu, żeby Ci pokazać, jak Visual Studio rozpoznaje tag helpery. Pokazuje je na zielono (to zielony, prawda?) i lekko pogrubia. Jeśli u siebie też to widzisz, to znaczy, że wszystko zrobiłeś dobrze.

OK, to teraz możesz postawić breakpointa w metodzie ProcessAsync i uruchomić projekt.

Jeśli breakpoint zadziałał, to wszystko jest ok. Jeśli nie, coś musiało pójść nie tak. Upewnij się, że używasz odpowiedniego namespace i nazwy projektu w pliku _ViewImports.cshtml.

Niech się stanie anchor!

OK, mamy taki kod w tag helperze:

//using Microsoft.AspNetCore.Razor.TagHelpers

public class MailTagHelper: TagHelper
{
	public string Address { get; set; }
	public string Subject { get; set; }

	public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
	{
		await base.ProcessAsync(context, output);
	}
}

Teraz trzeba coś zrobić, żeby zadziałała magia.

Popatrz co masz w parametrze metody ProcessAsync. Masz tam jakiś output. Jak już mówiłem wcześniej, tag helper RENDERUJE odpowiedni kod HTML. Za ten rendering jest odpowiedzialny właśnie parametr output. Spróbujmy go wreszcie wykorzystać. Spójrz na kod metody poniżej:

public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
	await base.ProcessAsync(context, output);

	output.TagName = "a";
}

TagHelperOutput ma taką właściwość jak TagName. Ta właściwość mówi dokładnie: „Jaki tag html ma mieć Twój tag helper?”. Uruchom teraz aplikację i podejrzyj wygenerowany kod html:

<a>Test</a>

Twój tag <mail> zmienił się na <a>.

I o to mniej więcej chodzi w tych helperach. Ale teraz dodajmy resztę rzeczy. Musimy jakoś dodać atrybut href. Robi się to bardzo prosto:

public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
	await base.ProcessAsync(context, output);

	output.TagName = "a";
	output.Attributes.Add("href", $"mailto:{Address}?subject={Subject}");
}

Jak widzisz wszystko załatwiliśmy outputem. Myślę, że ta linijka sama się tłumaczy. Po prostu dodajesz atrybut o nazwie href i wartości mailto:.... itd. Uruchom teraz aplikację i popatrz na magię.

Wszystko byłoby ok, gdybyśmy gdzieś przekazali te parametry Address i Subject. Przekażemy je oczywiście w pliku cshtml:

<div class="text-center">
    <h1 class="display-4">Welcome</h1>
    <mail address="mail@example.com" subject="Hello!">Test</mail>
</div>

Niedobrze. Wszystko działa 🙂

Zwróć tylko uwagę, że wg konwencji klasy w C# nazywamy tzw. PascalCase. Natomiast w tag helperach używasz już tylko małych liter. Tak to zostało zrobione. Jeśli masz kilka wyrazów w nazwie klasy, np. ContentLabelTagHelper, w pliku cshtml te wyrazy oddzielasz myślnikiem: <content-label>. Dotyczy to również atrybutów.

Oczywiście możesz z takim tag helperem zrobić wszystko. Np. zapisać na sztywno mail i temat, np:

public class MailTagHelper: TagHelper
{
	public string Address { get; set; }
	public string Subject { get; set; }

	const string DEFAULT_MAIL = "mail@example.com";
	const string DEFAULT_SUBJECT = "Wiadomość ze strony";

	public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
	{
		await base.ProcessAsync(context, output);

		output.TagName = "a";

		string addres = string.IsNullOrWhiteSpace(Address) ? DEFAULT_MAIL : Address;
		string subject = string.IsNullOrWhiteSpace(Subject) ? DEFAULT_SUBJECT : Subject;

		output.Attributes.Add("href", $"mailto:{addres}?subject={subject}");
	}
}

Przeanalizuj ten kod. Po prostu, jeśli nie podasz adresu e-mail lub tematu wiadomości, zostaną one wzięte z wartości domyślnych. Oczywiście te wartości domyślne mogą być wszędzie. W stałych – jak tutaj – w bazie danych, w pliku… I teraz wystarczy, że w pliku cshtml napiszesz:

<mail>Test</mail>

Fajnie? Dla mnie bomba!

Atrybuty dla Tag Helper

Tag helpery mogą zawierać pewne atrybuty, które nieco zmieniają ich działanie:

HtmlTargetElement

Możemy tu określić nazwę taga, jaką będziemy używać w cshtml, rodzica dla tego taga, a także jego strukturę, np:

[HtmlTargetElement("email")]
public class MailTagHelper: TagHelper
{

}

Od tej pory w kodzie cshtml nie będziemy się już posługiwać tagiem <mail>, tylko <email>. Możemy też podać nazwę tagu rodzica, ale o tym w drugiej części artykułu. Możemy też określić strukturę tagu. Może on wymagać tagu zamknięcia (domyślnie) lub być bez niego, np:

[HtmlTargetElement("email", TagStructure = TagStructure.NormalOrSelfClosing)]
public class MailTagHelper: TagHelper
{

}

W przypadku tak skonstruowanego tagu email nie ma to sensu, ale moglibyśmy to przeprojektować i wtedy tag można zapisać tak: <email text="Napisz do mnie" /> lub tak: <email text="Napisz do mnie"></email>

TagStructure może mieć takie wartości:

  • TagStructure.NormalOrSelfClosing – tag z tagiem zamknięcia, bądź samozamykający się – jak widziałeś wyżej. Czyli możesz napisać zarówno tak: <mail address="a@b.c"></mail> jak i tak: <mail address="a@b.c" />
  • TagStructure.Unspecified – jeśli żaden inny tag helper nie odnosi się do tego elementu, używana jest wartość NormalOrSelfClosing
  • TagStructure.WithoutEndTag – niekonieczny jest tag zamknięcia. Możesz napisać tak: <mail address="a@b.c"> jak i tak: <mail address="a@b.c" />

HtmlTargetElement ma jeszcze jeden ciekawy parametr służący do ustalenia dodatkowych kryteriów. Spójrz na ten kod:

[HtmlTargetElement("email", Attributes = "send")]
public class MailTagHelper: TagHelper
{
    //tutaj bez zmian
}

Aby teraz taki tag helper został dobrze dopasowany, musi być wywołany z atrybutem send:

<email send>Napisz do mnie</email>

Ten kod zadziała i tag zostanie uruchomiony. Ale taki kod już nie zadziała:

<email>Napisz do mnie</email>

Ponieważ ten tag nie ma atrybutu send.

Przy pisaniu własnych tagów raczej nie ma to zbyt wiele sensu, ale popatrz na coś takiego:

<p red>UWAGA! Oni nadchodzą!</p>

I tag helper do tego:

[HtmlTargetElement("p", Attributes = "red")]
public class PRedTagHelper: TagHelper
{
	public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
	{
		await base.ProcessAsync(context, output);

		output.Attributes.Add("style", "color: red");
	}
}

Teraz każdy tekst w paragrafie z atrybutem red będzie napisany czerwonym kolorem. To daje nam naprawdę ogromne możliwości.

HtmlAttributeNotBound

Do tej pory widziałeś, że wszystkie właściwości publiczne tag helpera mogą być używane w plikach cshtml. No więc nadchodzi atrybut, który to zmienia. HtmlAttributeNotBound niejako ukrywa publiczną właściwość dla cshtml.

Stosujemy to, gdy jakaś właściwość (atrybut) nie ma sensu od strony HTML lub jest niepożądana, ale z jakiegoś powodu musi być publiczna. Spójrz na ten kod:

public class MailTagHelper: TagHelper
{
	[HtmlAttributeNotBound]
	public string Address { get; set; }
	public string Subject { get; set; }
	public string Text { get; set; }
}

Teraz właściwość Address nie będzie widoczna w cshtml. Oczywiście tego atrybutu używamy na właściwościach, a nie na klasie.

HtmlAttributeName

Ten atrybut z kolei umożliwia zmianę nazwy właściwości tag helpera w cshtml. Nadpisuje nazwę atrybutu:

public class MailTagHelper: TagHelper
{
	[HtmlAttributeName("Tralala")]
	public string Address { get; set; }
	public string Subject { get; set; }
	public string Text { get; set; }
}

Teraz w pliku cshtml możesz napisać tak:

<email tralala="a@b.c" />

Ale ten atrybut ma jeszcze jedno przeciążenie i potrafi ostro zagrać. Może zwali Cię to z nóg. Dzięki niemu możesz w pliku cshtml napisać co Ci się podoba, a Twój tag helper to ogarnie. Możesz popisać atrybuty, które nie są właściwościami Twojego tag helpera! Spójrz na ten przykład:

<email mb_adr="a@b.c" mb_subject="Temat">Test</email>

I tag helper:

public class MailTagHelper: TagHelper
{
	[HtmlAttributeName(DictionaryAttributePrefix = "mb_")]
	public Dictionary<string, string> Prompts { get; set; } = new Dictionary<string, string>();
	public string Address { get; set; }
	public string Subject { get; set; }
	public string Text { get; set; }
}

Co tu zaszło? Spójrz, co podałeś w parametrze atrybutu HtmlAttributeName. Jest to jakiś prefix. A teraz zauważ, że w kodzie html użyłeś tego prefixu do określenia nowych atrybutów.

Po takiej zabawie, słownik Prompts będzie wyglądał tak:

  • adr: „a@b.c”
  • subject: „Temat”

Zauważ, że prefix został w słowniku automatycznie obcięty i trafiły do niego już konkretne atrybuty.

Oczywiście to przeciążenie może być użyte tylko na właściwości, która implementuje IDictionary. Kluczem musi być string, natomiast wartością może być string, int itd.


To tyle, jeśli chodzi o podstawy tworzenia tag helperów. O bardziej zaawansowanych możliwościach mówię w drugiej części artykułu. Najpierw upewnij się, że dobrze zrozumiałeś wszystko co tu zawarte.

Jeśli masz jakiś problem albo znalazłeś w artykule błąd, podziel się w komentarzu.

Podziel się artykułem na:
Walidacja danych w asp mvc .netcore 3

Walidacja danych w asp mvc .netcore 3

Wstęp

Ten artykuł opisuje czym jest walidacja danych i jak ją zastosować poprawnie w .Net. Jeśli trafiłeś na ten artykuł, szukając, jak w razor zrobić wymaganego checkboxa, to sprawdź ten artykuł.

Na szybko

Atrybuty walidacyjne w modelu:

  • Porównanie dwóch pól – [Compare(„InnePole”)]
  • Maksymalna ilość znaków – [MaxLength(50)]
  • Minimalna ilość znaków – [MinLength(10)]
  • Sprawdzenie wieku – [Range(Minimum = 18)]
  • Pole wymagane – [Required]

Trochę teorii

Czym jest walidacja danych

Walidacja to po prostu sprawdzenie poprawności danych podanych przez użytkownika. Walidacja jest ściśle powiązana z jakimś formularzem, który użytkownik wypełnia. Może być to po prostu rejestracja nowego konta. Wtedy taka walidacja polega na sprawdzeniu, czy użytkownik wypełnił wszystkie wymagane pola, czy jego hasło i nazwa użytkownika spełniają nasze założenia (np. musi być wielka litera albo nazwa użytkownika musi być adresem e-mail) no i oczywiście, czy zaakceptował naszą politykę prywatności i regulamin 🙂

Walidacja danych jest potrzebna żeby nie dopuścić do sytuacji, w której w bazie danych znajdują się głupoty. Zapewnia też większą spójność danych, a także chroni przed pewnymi atakami.

Także każdy formularz wypełniany przez użytkownika powinien być zwalidowany. Pamiętaj, że użytkownik może wpisać wszystko, co mu się podoba. To na Tobie w pewnym sensie leży odpowiedzialność sprawdzenia, czy to co wpisał ma sens.

Są dwa „tryby” walidacji. Po stronie serwera i po stronie klienta.

Walidacja danych po stronie klienta

Walidacja po stronie klienta następuje przed wysłaniem danych do serwera. Czyli np. w przeglądarce internetowej lub aplikacji. Mamy formularz rejestracji, użytkownik wciska guzik „Rejestruj” i w tym momencie musimy sprawdzić poprawność danych. Jeśli dane nie są poprawne, wtedy pokazujemy komunikat. Jeśli uznamy, że są ok – wysyłamy je do serwera. W aplikacjach internetowych takim sprawdzeniem zajmuje się np. JavaScript. Czyli musimy napisać odpowiedni kod w tym… bleee… języku, który sprawdzi nam poprawność wpisanych danych.

Walidacja danych po stronie serwera

No i tutaj mamy to, co backendowcy lubią najbardziej. Czyli dostajemy dane od klienta i za chwilę wrzucimy je do bazy danych. Ale, ale… Jeden z moich profesorów na studiach mawiał:

„Kto szybko daje, ten dwa razy daje”

Zatem nie możemy do końca ufać danym, które otrzymaliśmy. Musimy sprawdzić je drugi raz. I tu wchodzi walidacja danych po stronie serwera – dopiero jeśli tutaj upewnimy się, że wszystko jest ok, możemy wbić dane do bazy lub zrobić z nimi coś innego.

Czyli krótko podsumowując, proces powinien wyglądać tak:

  • Użytkownik wklepuje dane i wciska guzik „OK”
  • Następuje walidacja po stronie klienta (JavaScript)
  • Jeśli walidacja jest ok, to wysyłamy dane do serwera, jeśli nie, to mówimy użytkownikowi, że coś zje… źle wprowadził
  • Po stronie serwera odbieramy dane i SPRAWDZAMY JE JESZCZE RAZ
  • Jeśli dane są ok, to wbijamy je do bazy danych i odpowiadamy klientowi: „Ok, wszystko się udało”. Jeśli dane są złe, odpowiadamy: „Hola hola, coś tu źle wprowadziłeś”.

Taka podwójna walidacja danych nie jest w prawdzie konieczna. Możesz tworzyć systemy jak Ci się podoba. Ale jeśli chcesz ograniczyć dziury, błędy i podatności na ataki w swoim systemie, podwójna walidacja jest obowiązkiem. Pamiętaj, że nie zawsze dostaniesz dane z własnej aplikacji. Czasem ktoś po prostu wyśle „na pałę”. Dlatego walidacja po stronie serwera jest konieczna. Nie opłaca się też nikomu wysyłać na serwer danych, które wiadomo, że są niepoprawne. Bardziej opłaca się sprawdzić je po stronie klienta przed wysłaniem.

Trochę praktyki

Walidacja danych w .NetCore

Na szczęście .NetCore ma pewne mechanizmy do walidacji danych, które teraz pokrótce Ci pokażę. Walidacja w .NetCore składa się z trzech etapów:

  • odpowiednie przygotowanie modelu
  • wywołanie walidacji po stronie klienta
  • wywołanie walidacji po stronie serwera

Przygotowanie modelu

Załóżmy, że mamy klasę, która przechowuje dane rejestracyjne użytkownika (model), możemy jej poszczególne właściwości ubrać w konkretne atrybuty. To jest tzw. „annotation validation„, czyli
walidacja obsługiwana za pomocą „adnotacji”.

Spójrzmy na tę „gołą” klasę:

class RegisterUserViewModel
{
    public string UserName { get; set; }
	public string Password { get; set; }
	public string RepeatedPassword { get; set; }
}

Musimy się upewnić, że:

  • wypełniona jest nazwa użytkownika
  • wypełnione jest hasło
  • podane hasła są identyczne

Za pierwsze 2 założenia odpowiada atrybut Required

//using System.ComponentModel.DataAnnotations;

class RegisterUserViewModel
{
    [Required]
	public string UserName { get; set; }
	
	[Required]
	public string Password { get; set; }
	
	[Required]
	public string RepeatedPassword { get; set; }
}

Teraz, gdy uruchomimy proces walidacji, sprawdzi on te pola. To wygląda mniej więcej tak:

Walidator: Cześć obiekcie, jakie masz pola?
Obiekt: No hej, mam UserName, Password i PasswordRepeated
Walidator: Ok, jakie masz atrybuty walidacyjne na polu UserName?
Obiekt: Required
Walidator: Hej wielki walidatorze Required! Czy pole UserName w danym obiekcie spełnia Twoje założenia?

Wtedy walidator Required sprawdza. Taki walidator mógłby wyglądać w taki sposób (zakładając, że byłby tylko dla stringa, ale jest dla innych typów też):

return !string.IsNullOrWhitespace(value);

Walidator zrobi analogiczne sprawdzenie przy pozostałych polach.

Ok, teraz w drugim kroku chcemy, żeby pole UserName było poprawnym adresem e-mail. Można się do tego posłużyć atrybutem…. EmailAddress:

class RegisterUserViewModel
{
    [Required]
	[EmailAddress]
	public string UserName { get; set; }
	
	[Required]
	public string Password { get; set; }
	
	[Required]
	public string RepeatedPassword { get; set; }
}

(jak widzisz, jedno pole może mieć wiele atrybutów walidacyjnych)

Walidacja adresu e-mail

Poświęciłem na to osobny akapit (chociaż zastanawiam się nad artykułem). Zasadniczo wiemy, jakie są poprawne adresy e-mail:

  • a@b.c
  • jkowalski@gmail.com
  • user@serwer.poczta.pl

Natomiast niepoprawnymi mogą być:

  • @@@@
  • jkowalski(at)gmail.com
  • pitu-pitu@pl

Po staremu

Do wersji .NetFramework 4.7.2 atrybut EmailAddress działał tak, że sprawdzał adres e-mail za pomocą wyrażeń regularnych. Wyrażenia regularne mają jedną mała wadę – są stosunkowo drogie, jeśli chodzi o zasoby – wolne. To jest pewna furtka dla ataku DoS (denial of service). Atak ten polega na przeciążeniu serwera, żeby nie służył już innym użytkownikom.

Okazało się, że duża ilość stringów, a co gorsza dużych stringów, przepychana przez ten walidator, może właśnie mieć takie działanie. Wyrażenia regularne żrą sporo, więc duża ilość dużych stringów może
zablokować serwer. Dlatego Microsoft zmienił trochę sposób działania tego walidatora.

Po nowemu

każdy string, który ma tylko jedną małpę i nie jest ona ani na końcu, ani na początku jest poprawnym adresem e-mail, czyli:

  • a@b – poprawny
  • ja@cie.krece – poprawny
  • @blbla – niepoprawny
  • abc@ – niepoprawny

No i tutaj właśnie zapala się lampka: „a@b” ma być poprawnym e-mailem? No właśnie nie do końca. Ale to sprawdzenie miało być szybkie. Jest szybkie. Ale skoro jest szybkie, to jest też proste. Zasadniczo sprawdza czy podany string MOŻE być poprawnym adresem e-mail.

Można to jednak zmienić. W pliku appsettings trzeba dodać taką linijkę:

<add key="dataAnnotations:dataTypeAttribute:disableRegEx" value="false"/>

Wtedy sprawdzenie adresu e-mail będzie po staremu za pomocą wyrażeń regularnych. Stosuj tylko wtedy, kiedy wiesz co robisz. W gruncie rzeczy to użytkownik powinien podać Ci swój właściwy adres e-mail.

Sprawdzenie hasła

Ok, skoro załatwiliśmy już e-mail, sprawdźmy teraz czy użytkownik podał dwa razy to samo hasło, czy może coś znowu schrzanił:

class RegisterUserViewModel
{
    [Required]
	[EmailAddress]
	public string UserName { get; set; }
	
	[Required]
	public string Password { get; set; }
	
	[Required]
	[Compare("Password")]
	public string RepeatedPassword { get; set; }
}

Jak widzisz odpowiada za to atrybut Compare. W parametrze (otherProperty) podajemy nazwę pola z tej klasy, z którą ma zostać to pole porównane. Tutaj „Password”. Czyli porównamy pole „RepeatedPassword” z polem „Password”. Tylko tutaj też uwaga. Jeśli chodzi o porównanie dwóch stringów, to wykorzystywana jest tu metoda Equals z klasy string. Ta metoda nie jest wrażliwa na ustawienia językowe. Tzn. że w pewnych językach i w pewnych warunkach może stwierdzić, że dwa różne stringi są takie same lub dwa takie same stringi są różne.

Sprawdzenie wieku

Teraz dodatkowo możemy upewnić się, czy użytkownik jest pełnoletni. Do tego może posłużyć atrybut Range, który oznacza, że wartość powinna być z konkretnego zakresu. A więc użytkownik może na przykład podać swój wiek:

class RegisterUserViewModel
{
    [Required]
	[EmailAddress]
	public string UserName { get; set; }
	
	[Required]
	public string Password { get; set; }
	
	[Required]
	[Compare("Password")]
	public string RepeatedPassword { get; set; }
	
	[Required]
	[Range(18, 50)]
	public int Age { get; set; }
}

W powyższym przykładzie sprawdzamy, czy wiek użytkownika jest między 18 a 50 lat. Atrybut Range musi być zastosowany z typem int lub datą. Przy czym stosowanie go z datą nie ma raczej uzasadnienia,
ponieważ musielibyśmy wklepać ją na sztywno, co może przysporzyć sporych problemów w przyszłości. Dlatego stosuj Range raczej do zmiennych int. Możesz podać też samo minimum lub maksimum:

class RegisterUserViewModel
{
    [Required]
	[EmailAddress]
	public string UserName { get; set; }
	
	[Required]
	public string Password { get; set; }
	
	[Required]
	[Compare("Password")]
	public string RepeatedPassword { get; set; }
	
	[Required]
	[Range(Minimum = 18)]
	public int Age { get; set; }
}

(analogicznie istnieje właściwość Maximum)

Pewnie chciałoby się zapytać – jak atrybut range traktuje swoje minimum i maksimum. Czy od minimum, czy minimum jest wykluczone? Możesz sam to sprawdzić, pisząc odpowiedni kod, do czego Cię zachęcam 🙂 Jeśli jednak trafiłeś tutaj tylko po to, żeby dowiedzieć się tej konkretnie rzeczy, to już mówię – minimum i maksimum są dopuszczone. Czyli minimum jest pierwszą liczbą, która przechodzi sprawdzenie, a maksimum – ostatnią.

Jest jeszcze kilka atrybutów, które mogą Ci się przydać. Każdy z nich jest opisany na stronach Microsoftu:

https://docs.microsoft.com/pl-pl/dotnet/api/system.componentmodel.dataannotations.validationattribute?view=net-5.0

Walidacja danych po stronie serwera – jak?

Generalnie w .NetCore nie ma chyba nic prostszego. Robi się to w kontrolerze (to jedna z możliwości) – niezależnie od tego, czy pracujesz nad WebApi, czy nad stroną (Razor Views). Działa to tak:

//przykład dla RazorView, dla WebAPI jest to analogicznie
public class MyController: Controller
{
	[HttpPost]
	public async Task<IActionResult> RegisterUser(RegisterUserViewModel model)
	{
	    if(!ModelState.IsValid)
			return BadRequest();
	}
}

I w zasadzie to wszystko. Kontroler ma taki obiekt ModelState, który przechowuje informacje na temat poprawności przekazanego modelu. Właściwość IsValid określa, czy model jest poprawnie wypełniony (true), czy nie (false). Możesz też poznać wszystkie błędy obecne w modelu, ale uwaga. Na tym etapie (tuż przed dodaniem na serwer) raczej nie informowałbym użytkownika o szczegółowych błędach (chociaż to zależy od Ciebie – Ty wiesz co klient usiłuje zrobić i jak istotne i tajne powinny być dane w każdym przypadku).

UWAGA! .NET8 automatycznie sprawdza poprawność modelu w klasie kontrolera dziedziczącej po ControllerBase.

Jesteśmy w końcu na serwerze, a ktoś te dane na serwer musiał wysłać. Więc albo zrobił to źle klient – wtedy musimy poprawić klienta (bo np. zabrakło walidacji po stronie klienta), albo ktoś próbuje nam w jakiś sposób zaszkodzić. Przy WebAPI jest jeszcze inna opcja – ktoś po prostu tworzy aplikację i nie poradził sobie z poprawnym wysłaniem danych… No cóż… Musi doczytać w dokumentacji.

Jeśli chcesz dowiedzieć się, jak automatycznie walidować ModelState, sprawdź ten artykuł.

Walidacja w RazorPages wygląda też analogicznie. Tutaj obiekt ModelState też istnieje z tym, że w klasie PageModel, np:

public class MyPage: PageModel
{
	public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
			return BadRequest();
			
        return Page();
    }
}

Walidacja danych po stronie klienta

Tutaj kwestia jest też zasadniczo prosta, jeśli chodzi przynajmniej o walidację typową – dostępną w .NetCore.
Gdzieś tam na początku mówiłem, że walidacja po stronie klienta wymaga JavaScriptu. I to niestety prawda. Na szczęście Microsoft stworzył taką bibliotekę jQuery unobtrusive validation.
Ona jest stworzona w taki sposób, żeby współdziałać z widokami dzięki TagHelperom.

Jeśli nie wiesz, czym są TagHelpers, to dosłownie „Tagi pomocnicze” – tagi w sensie tagi html. Przeczytaj ten artykuł, żeby dowiedzieć się więcej.

Przygotowanie

UWAGA! Żeby to zadziałało, musisz dodać do strony 3 skrypty:

<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validate/1.19.1/jquery.validate.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validation-unobtrusive/3.2.11/jquery.validate.unobtrusive.min.js"></script>

(pamiętaj o odpowiednich wersjach bibliotek :))

Pierwszy z nich masz raczej na „dzień dobry” w widoku _Layout.cshtml, pozostałe w _ValidationScriptsPartial.cshtml. Więc domyślnie wystarczyłoby, żebyś dodał na początku strony:

<partial name="_ValidationScriptsPartial" />

Walidacja formularza

Na początek przekazać do widoku konkretny model:

@model RegisterUserViewModel

Następnie musimy zwalidować konkretne pola w formularzu, np:

<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>

Powyżej masz fragment formularza rejestracyjnego. Jeśli nie znasz bootstrap, to tłumaczę pokrótce:
pierwszy div – tworzy „grupę” kontrolek – w tym przypadku label, input i jakiś span (Etykietę, pole do wprowadzenia danych i jakiś span).

Etykieta to po prostu tekst zachęty: „Podaj swój email:”.

asp-for

Teraz pole do wprowadzenia danych – input. Tutaj pojawiła się nowa rzecz – tag pomocniczy „asp-for”. Jeśli wpiszesz sobie asp-for, to Intellisense pokaże Ci wszystkie pola w Twoim modelu. Skąd wie, co jest Twoim modelem? No przecież mu pokazałeś na początku widoku:

@model RegisterUserViewModel

asp-for tworzy pewnego rodzaju powiązanie między kontrolką HTML, a polem w Twoim modelu. Czyli to, co użytkownik wpisze do tej kontrolki, AUTOMAGICZNIE trafi do pola UserName w Twoim modelu. Niczego nie musisz przepisywać. No złoto…

Ale to nie wszytko. Zapewnia to też walidację. Czyli automagicznie zostanie sprawdzone Twoje pole pod kątem poprawności (w tym wypadku Required i EmailAddress).

Komunikaty o błędach

Jeśli walidacja przejdzie, to formularz zostanie wysłany, jeśli nie, no to jakoś użytkownikowi wypadałoby powiedzieć, że znowu coś schrzanił. I od tego mamy ten tajemniczy SPAN.

Zauważ na początek jedną rzecz – span ma tag otwarcia i zamknięcia. Nie możesz napisać tak:

<span asp-validation-for... />

bez tagu zamknięcia coś może nie zadziałać (może być różnie na różnych wersjach). Więc musi być tag zamknięcia.

Ten SPAN wyświetli informacje, jeśli pole zostanie błędnie wypełnione (nie przejdzie walidacji). Tag „asp-validation-for” mówi po prostu dla jakiego pola ma pokazać się informacja o błędzie. Żeby nie zrobić użytkownikowi mindfucka, podaliśmy tutaj pole UserName. Klasa text-danger to jest po prostu bootstrapowa klasa, która powoduje, że wyświetlony tekst będzie w kolorze czerwonym.


Czyli podsumowując:

  • label – etykieta dla pola, mówiąca użytkownikowi co ma wpisać
  • input z tagiem asp-for – pole do wpisania
  • span z tagiem asp-validation-for – informacja w przypadku błędu.

No właśnie, ale jaka informacja? .NetCore pokaże po prostu domyślne info takie, jakie zaprogramowali w Microsofcie. Ale MOŻESZ ustawić własne powiadomienia. Wróćmy do modelu:

class RegisterUserViewModel
{
    [Required(ErrorMessage="Nie, nie, nie. To pole MUSISZ wypełnić")]
	[EmailAddress(ErrorMessage="A takiego! Nie podałeś prawidłowego e-maila")]
	public string UserName { get; set; }
	
	[Required]
	public string Password { get; set; }
	
	[Required]
	[Compare("Password")]
	public string RepeatedPassword { get; set; }
	
	[Required]
	[Range(Minimum = 18)]
	public int Age { get; set; }
}

WSZYSTKIE atrybuty walidacyjne mają właściwość ErrorMessage, do której możesz wpisać komunikat błędu. Komunikaty mogą być też lokalizowane, np:

[Required(ErrorMessageResourceName = nameof(LangRes.Validation_RequiredField), ErrorMessageResourceType = typeof(LangRes))]

I teraz tak. ErrorMessageResourceType to jest Twój typ z zasobami językowymi, który posiada klucz Validation_RequiredField – ten klucz, który używasz.
Jeśli nie wiesz, jak tworzyć wersje językowe, przeczytaj ten artykuł.

Jest jeszcze jeden sposób na pokazanie błędów w widoku. Możesz pokazać wszystkie błędy jeden po drugim, zamiast konkretnych błędów pod konkretnymi kontrolkami. Wystarczy zrobić to:

<form asp-action="Register" method="Post">
	<div asp-validation-summary="All"></div>
	<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>

Wtedy komunikaty o błędach pojawią się na tym dodatkowym divie w postaci listy.

To właściwie już tyle jeśli chodzi o podstawy walidacji w .NetCore.
Gratulacje, dotarłeś do końca 🙂

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

Akceptacja regulaminu – wymagany checkbox

W każdym portalu, wymagana jest akceptacja regulaminu. I sprawa wydaje się prosta, ale z jakiegoś powodu nie jest (osobiście zgłosiłem to do MS wraz z rozwiązaniem). Przeczytaj ten artykuł, żeby dowiedzieć się, jak wymusić zaznaczenie checkboxa przez użytkownika w .NetCore

Podziel się artykułem na: