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