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: