İçeriğe atla

ASP.NET Core 6/Tag Helper'lar/Kendi Tag Helper Sınıflarımızı Yazma

Vikikitap, özgür kütüphane

Bu bölümde kendi tag helper sınıflarımızı nasıl yazacağımızı ve bu tag helper'ları nasıl istediğimiz view'da kullanacağımızı göreceğiz. Tag helper sınıfları geleneksel olarak TagHelpers klasörüne konulur. Şimdi ismi DivTagHelper.cs olan ve içeriği aşağıdaki gibi olan dosyayı TagHelpers klasöründe oluşturun.

using Microsoft.AspNetCore.Razor.TagHelpers;
namespace WebApp.TagHelpers {
    public class DivTagHelper : TagHelper {
        public string BgColor  { get; set; } = "green";
        public override void Process(TagHelperContext context, TagHelperOutput output) {
            output.Attributes.SetAttribute("style", $"background:{BgColor}");
        }
    }
}

Bu tag helper <div bg-color="renk">...</div> şeklinde belirtilen HTML kodlarını <div style="background:renk">...</div> şekline çevirecektir. Örneğimizde de gördüğünüz üzere tag helper sınıfları Microsoft.AspNetCore.Razor.TagHelpers isim alanındaki TagHelper sınıfından türerler. Kullanıldığı view'dan HTML seçeneği olarak alacağı seçenekleri birer özellik olarak belirtir. Hangi HTML etiketi üzerinde çalışacaksa ismini ona göre alacaktır. Bu örneğimizde <div> etiketleri üzerinde çalışacağı için ismi DivTagHelper olmuştur. Daha sonra göreceğimiz üzere bir tag helper'ın üzerinde çalışacağı HTML etiketleri kümesi attribute'lar vasıtasıyla genişletilip daraltılabilir. HTML etiketi üzerinde yapılacak modifikasyonlar ana sınıftan devralınan Process() metodu override edilerek belirtilir. Bu metodun ikinci parametresi olan TagHelperOutput tipindeki output değişkeniyle çıktı etiketin bir seçeneği ayarlanmaktadır. Bu aşamada view'da ilgili HTML etiketine eklenen sahte seçenekler kaldırılmıştır. Sonuçta örneğimizde <div> etiketine eklenen tek seçenek style olacak ve değeri de background:renk olacaktır. Eğer halihazırda ilgili <div> etiketinin style seçeneği varsa değeri belirtilen değerle değiştirilecektir.

Bağlam verisini alma

[düzenle]

Az önceki örneğimizde Process() metodunun ilk parametresi olan TagHelperContext tipindeki context değişkenini kullanmadık. TagHelperContext sınıfı, view'da kullanılan etiket hakkında bağlam bilgisi sağlar ve aşağıdaki üyeleri içerir:

AllAttributes: İlgili HTML etiketine eklenen bütün seçenekleri anahtar-değer çiftleri şeklinde tutar. Seçeneklere indeks veya isim yoluyla erişilebilir.
Items: Birden fazla tag helper arasında koordinasyon sağlamak amacıyla kullanılır.
UniqueId: İlgili HTML etiketini benzersiz olarak tanımlayan id bilgisini döndürür.

İlgili HTML etiketinin seçeneklerine AllAttributes özelliği ile erişilebilmesine rağmen bunları tag helper sınıfında özellik olarak tanımlamak daha kullanışlıdır. Tag helper bir view'da kullanıldığı zaman sağlanan seçenek değerleri tag helper sınıfında tanımlanan özelliklere otomatik olarak atanır. Tag helper sınıfındaki özellikler string olmak zorunda değildir; int, bool, vb. temel veri tipleri de olabilir. Bu durumda ASP.NET Core tarafından otomatik dönüşüm yapılır. Tag helper sınıfında olup da HTML etiketinde karşılığı olmayan özellikler varsayılan değerine atanır, HTML etiketinde olup da tag helper sınıfında karşılığı olmayan seçenekler ise gözardı edilir, ancak HTML etiketinden silinmez. Bir HTML etiketinin seçenekleri asp- ile veya data- ile başlayamaz. asp- ön eki dahili tag helper'lar için ayrılmıştır. data- ön eki ise web geliştirmede custom seçenekler oluşturmak için yaygın olarak kullanıldığı için yasaklanmıştır.

Varsayılan durumda tag helper sınıfındaki özellik adları HTML etiketindeki seçenek adlarına karşılık gelir. HTML kodundaki "bg-color" C# kodundaki "BgColor"a karşılık gelir. Ancak istenirse bu varsayılan durum değiştirilebilir. Bir tag helper sınıfındaki bir özelliğin hangi HTML seçeneğine karşılık geleceği ilgili özelliğe eklenen HtmlAttributeName attribute'u ile belirtilebilir.

Çıktı üretme

[düzenle]

Bir tag helper'ın üreteceği çıktı Process() metodunun içinde TagHelperOutput tipindeki output değişkeni kullanılarak belirtilir. Bu değişken ilk olarak HTML etiketinin mevcut durumuyla doldurulur, üzerinde aşağıda belirtilen TagHelperOutput sınıfının üyeleri kullanılarak modifikasyon yapılır ve sonuç çıktı üretilir.

TagName: İlgili HTML etiketinin etiket ismini almak ve değiştirmek için kullanılır.
Attributes: Çıktının seçeneklerini oluşturmak için kullanılabilecek bir sözlük döndürür.
Content: Çıktının içeriğini oluşturmak için kullanılabilecek bir TagHelperContent nesnesi döndürür.
GetChildContentAsync(): HTML etiketinin mevcut içeriğine erişmek için kullanılır.
PreElement: Tag helper'ın kullanıldığı yerin hemen öncesine içerik eklemek için kullanılır.
PostElement: Tag helper'ın kullanıldığı yerin hemen sonrasına içerik eklemek için kullanılır.
PreContent: Tag helper'ın içeriğinden hemen öncesine içerik eklemek için kullanılır.
PostContent: Tag helper'ın içeriğinden hemen sonrasına içerik eklemek için kullanılır.
TagMode: Üretilecek HTML etiketinin modunu belirtmek için kullanılır.
SupressOutput(): Bir HTML etiketinin view'dan çıkartılmasını sağlar.

Tag helper'ları kaydettirme

[düzenle]

Bir tag helper'ın bir view'da kullanılabilmesi için öncelikle @addTagHelper direktifi ile kaydettirilmesi gerekmektedir. Bu direktif direkt ilgili view'da kullanılacağı gibi daha çok yerde etkili olabilmesi amacıyla _ViewImports.cshtml dosyalarına da koyulabilmektedir. Örneğimizde @addTagHelper direktifini şöyle kullanırız:

@addTagHelper *, WebApp

Burada WebApp projemizin ismidir ve kodumuz WebApp projesindeki hangi isim alanında olursa olsun bütün tag helper sınıflarına erişim hakkı kazandırmaktadır. Bu kodu, controller view'larında tag helper sınıflarına erişim hakkı kazanmak için Views klasöründeki _ViewImports.cshtml dosyasına, Razor sayfalarında tag helper sınıflarına erişim hakkı kazanmak için Pages klasöründeki _ViewImports.cshtml dosyasına koyabiliriz. Ayrıca:

@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

kodu ile Microsoft'un sunduğu bütün dahili tag helper sınıflarına erişim hakkı kazanırız.

Tag helper'ı kullanma

[düzenle]

Tag helper'ı kullanma hakkı elde ettikten sonra herhangi bir view'da kullanmak son derece basittir. Örnek:

<div bg-color="blue">Bu div'in arkaplanı mavi</div>

Yazdığımız tag helper sınıfının ilgili div'in içeriğini değiştirmediğine dikkat ediniz. Tag helper sınıfımızın adını DivTagHelper yaptığımız için yapılan değişiklikler bg-color seçeneği belirtsin-belirtmesin bütün <div> etiketlerini etkileyecektir. bg-color seçeneği belirtmemiş bütün <div> etiketlerinin arkaplanı tag helper sınıfındaki varsayılan değer olan yeşil olacaktır. Eğer tag helper sınıfında bir varsayılan değer belirtmeseydik bütün div'ler <div style="background:">...</div> şeklinde modifiye edilecekti. İkisi de eşit şekilde kötüdür. Yapmamız gereken şey tag helper'ın etki ettiği etiketleri tag helper sınıf isminden bağımsız hale getirmektir -ki bu da bir sonraki konumuzdur.

Tag helperin kapsamını daraltma

[düzenle]

Varsayılan durumda bir tag helper'in kapsamı ismine göre belirlenir ve isminde belirtilen bütün HTML etiketlerini kapsar. Bu varsayılan durum HtmlTargetElement attribute'u ile değiştirilebilir. Örnek:

[HtmlTargetElement("div", Attributes = "bg-color", ParentTag = "body")]
public class DivTagHelper : TagHelper { //...

Bu tag helper sadece bg-color attribute'u olan ve üst etiketi <body> olan etiketleri hedeflemektedir. Attribute'ların birden fazla olması durumunda virgül ile ayrılması gerekir. Eğer etiketin attribute'unun belirli bir ön ekle başlamasını yeterli sayıyorsak yıldız işaretini kullanabiliriz. Örnek:

[HtmlTargetElement("div", Attributes = "bg-color, text-color")]
public class DivTagHelper : TagHelper { //...

Bu tag helper yalnızca bg-color ve text-color attribute'larını içeren <div> etiketlerine uygulanacaktır. Başka bir örnek:

[HtmlTargetElement("div", Attributes = "bg-*")]
public class DivTagHelper : TagHelper { //...

Bu tag helper bg- ön ekiyle başlayan herhangi bir attribute içeren bütün <div> etiketlerine uygulanacaktır. HtmlTargetElement attribute'unun TagStructure özelliği ile HTML etiketinin kapatılma cinsine göre de seçim yapılabilir. TagStructure enumu 3 farklı değer alabilir:

Unspecified: TagStructure seçeneğinin belirtilmediği duruma denktir. NormalOrSelfClosing değeri ayarlanır.

NormalOrSelfClosing: Normal HTML etiketleri veya kendi kendini kapatan HTML etiketleri seçilir. (<div>...</div> veya <br />)

WithoutEndTag: Yalnızca ayrı bir bitiş etiketi olmayan etiketler seçilir. (<br> veya <br />)

CSS seçicileri bir tag helper'ın etki edeceği etiketleri seçmek için kullanılabilir. Bu seçici belirtimi yine Attributes özelliğine değer olarak verilir. Örneğin [bgcolor] ile sadece bg-color seçeneği olan etiketler seçilir,[bgcolor=primary]ile bgcolor seçeneğinin değeri primary olan etiketler seçilir, [bg-color^=p]ile bg-color seçeneğinin değeri p ile başlayan etiketler seçilir.

Tag helper'ların kapsamını genişletme

[düzenle]

Örnek:

[HtmlTargetElement("*", Attributes = "bg-color, text-color")]
public class DivTagHelper : TagHelper { //...

Bu örneğimizde tag helper bg-color ve text-color seçeneklerini içeren bütün etiketlere uygulanacaktır. Bir tag helper'a birden fazla HtmlTargetElement attribute'u uygulanabilir. Örnek:

[HtmlTargetElement("tr", Attributes = "bg-color, text-color")]
[HtmlTargetElement("td", Attributes = "bg-color")]
public class DivTagHelper : TagHelper { //...

Bu tag helper bg-color ve text-color seçeneği olan <tr> etiketlerine veya bg-color seçeneği olan <td> etiketlerine uygulanacaktır.

Tag helper'ların sırasını belirtme

[düzenle]

Bir etikete birden fazla tag helper uygulanabilir. TagHelper sınıfında devralınan Order özelliğiyle ilgili tag helper'ların hangi sırada uygulanacağı belirtilebilir. Order değeri daha düşük olan tag helper daha önce uygulanacaktır.

HTML etiketlerini üretme

[düzenle]

Bu kısımda biraz daha detaya gireceğiz ve "Çıktı üretme" kısmında gördüğümüz üyelerin kullanımını göreceğiz. Öncelikle hayali bir HTML etiketini gerçek HTML etiketine dönüştüren bir tag helper yazalım:

using Microsoft.AspNetCore.Razor.TagHelpers;
namespace WebApp.TagHelpers
{
    [HtmlTargetElement("kod")]
    public class KodTagHelper : TagHelper {
        public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
        {
            output.TagName = "div";
            output.TagMode = TagMode.StartTagAndEndTag;
            output.Attributes.SetAttribute("style","border:solid;background:#bbb;white-space:pre;font-family:monospace;");
            string content = (await output.GetChildContentAsync()).GetContent();
            output.Content.SetHtmlContent(content);
        }
    }
}

Bu tag helper sayesinde bir view'da <kod> ve </kod> ile çevrelediğimiz her metnin gri renkli bir arkaplanı, siyah renkli bir çerçevesi olacak; ilgili aralığa girilmiş metindeki bütün beyaz boşluklar korunacak ve her bir karakterin sabit genişlikte olduğu kod yazımına uygun font stili kullanılacaktır. Kuşkusuz bu tag helper daha da geliştirilebilir. Dışarıdan etiket seçenekleri şeklinde seçenekler alıp, bunları sınıfta özelliklerde tutup üretilen çıktıya etki etmesi sağlanabilir. Örneğin kullanıcı kod bloğunun çerçeve rengini veya arkaplan rengini belirtebilir. Ayrıca sahte HTML etiketinin içeriğindeki kodlar olduğu gibi çıktı <div> etiketinin içine yazılmaktadır. Çıktı <div> etiketinin içine içeriği yazmadan önce veya sonra bir şeyler de yazılıp kod bloğu süslenebilir.

Eğer HTML spesifikasyonunda olmayan bir etikete uygulanacak tag helper yazıyorsak tag helper sınıfına mutlaka HttpTargetElement attribute'unu eklemeliyiz. Örneğimizde 8. satırda <kod> etiketi <div> etiketine dönüştürülmüştür. 9. satırda <div> etiketinin <div>...</div> şeklinde başlangıç ve bitişi olacağını belirtiyoruz. 10. satırda ilgili <div> etiketinin style seçeneğinin değerini ayarlıyoruz. 11. satırda <kod> etiketinin mevcut içeriğini alıyoruz. output değişkeni üzerinden çağrılan GetChildContentAsync() metodu mevcut etiketin içeriğini bir TagHelperContent olarak döndürür. Bunun üzerinden çağrılan GetContent() metodu ilgili içeriği string'e çevirir. TagHelperOutput sınıfının Content özelliği de TagHelperContent tipindedir ve etiketin içeriğine bir şeyler yazmaya yarar. TagHelperContent nesnesi üzerinden erişilen SetHtmlContent() metodu ilgili etiketin içeriğini string olarak ayarlar. Gerek etiketin önceki içeriğine erişmeye yarayan GetChildContentAsync() metodunun, gerekse de etiketin içeriğini tekrar yazmaya yarayan Content özelliğinin geri dönüş tipi olan TagHelperContent sınıfının önemli üyeleri şunlardır:

GetContent(): İlgili TagHelperContent nesnesini string'e çevirir.

SetContent(metin): İçeriğe yazma amacıyla kullanılır. Metinde HTML kodu varsa HTML kodu olarak algılanmayacak şekilde kodlama yapılır.

SetHtmlContent(html): İçeriğe yazma amacıyla kullanılır. Metindeki HTML kodları HTML olarak alınır.

Append(metin): Mevcut içeriğe ekleme yapar. HTML kodları HTML olarak algılanmayacak şekilde kodlanır.

AppendHtml(html): Mevcut içeriğe ekleme yapar. HTML kodları HTML olarak algılanır.

Clear(): Mevcut içeriği silmeye yarar.

NOT1: Bir tag helper'a HTML etiket seçenekleri ve tag helper sınıfındaki özellikler aracılığıyla sağlanan custom seçenekler HTML etiketi oluşturulma süreci başlamadan önce silinir. Eğer ilgili seçeneklerin tag helper'da herhangi bir işlevi yoksa silinmez. Eğer silinen bir seçeneğin korunması isteniyorsa tekrar ilgili seçenek ve değeri tag helper içinde eklenmelidir. Örneğin table tag helper'ının bir view'da şöyle kullanıldığını düşünelim:

<table satir="3" sutun="4" style="background:gray"></table>

Burada muhtemelen tag helper sınıfında Satir ve Sutun tipinde iki özellik vardır ve seçeneklere verilen değerler bu özelliklere atanmaktadır. İşte bu durumda tag helper sınıfı söz konusu <table> etiketinin satir ve sutun isimli seçenekleri silecektir. style seçeneğinin tag helper sınıfında özellik karşılığı olmadığı için bu seçenek korunacaktır.

NOT2: Eğer bir tag helper'ın uygulanacağı etiket seçiminde belirli bir seçeneğin olması şartı belirtildiyse, ancak o seçenek tag helper sınıfında özellik olarak tanımlanmadıysa üretilen HTML'de söz konusu seçenek korunur. Yani üretilen HTML'de bir seçeneğin korunup korunmaması yalnızca o seçeneğin ilgili tag helper sınıfında özellik olarak tanımlanıp tanımlanmamasına bağlıdır, başka herhangi bir şeye bağlı değildir.

NOT3: Tag helper sınıfında Process() veya ProcessAsync() metotlarından birini tanımlayabiliriz. Eğer HTML kodu oluşturma süreci asenkron bir metoda çağrı içeriyorsa ve bu çağrıyı await'leyebilmek istiyorsak ProcessAsync() metodunu kullanmalıyız.

TagBuilder sınıfı

[düzenle]

Kuşkusuz SetHtmlContent() metoduyla üretilecek yeni HTML etiketinin içeriğine HTML yazılabilir. Ancak bu yaklaşım hataya açık olacaktır. Bunun yerine Microsoft.AspNetCore.Mvc.Rendering isim alanında bulunan TagBuilder sınıfını kullanarak daha şık şekilde HTML etiketleri üretebiliriz. Üretilen bu TagBuilder nesneleri TagHelperContent sınıfının biraz önce gördüğümüz metotlarına parametre olarak verilebilir. Örnek:

// ...
string content = (await output.GetChildContentAsync()).GetContent();
TagBuilder header = new TagBuilder("th");
header.Attributes["colspan"] = "2";
header.InnerHtml.Append(content);
TagBuilder row = new TagBuilder("tr");
row.InnerHtml.AppendHtml(header);
output.Content.SetHtmlContent(row);

Burası bir tag helper sınıfının ProcessAsync() metodunun bir kısmıdır. Burada 2. satırda mevcut HTML etiketinin içeriği alınmaktadır. 3. ve 4. satırlarda <th colspan="2"></th> şeklinde bir HTML kodu üretilmektedir. 5. satırda mevcut içerik ürettiğimiz <th> etiketinin içeriğine konulmaktadır. Mevcut içerik Deneme ise bu aşamada üretilen HTML kodu <th colspan="2">Deneme</th>olacaktır. 6. ve 7. satırlarda <tr> isimli yeni bir HTML etiketi oluşturulmakta ve içeriğine daha önce üretiğimiz <th> etiketi konulmaktadır. Son tahlilde ürettiğimiz HTML kodu <tr><th colspan="2">Deneme</th></tr> olacaktır. Daha sonra bu kod nihai çıktıyı üretmek için output değişkenine yazılmaktadır.

Bir etiketin ve içeriğinin öncesine ve sonrasına içerik ekleme

[düzenle]

TagHelperOutput sınıfı 4 özellik vasıtasıyla mevcut HTML etiketinin öncesine ve sonrasına ve mevcut HTML etiketinin içeriğinin öncesine ve sonrasına içerik eklememize imkan vermektedir. Bu özellikler PreElement, PostElement, PreContent ve PostContent'tir. İsmi Element ile bitenler bir HTML etiketinin öncesine ve sonrasına ekleme yapmaya yararken ismi Content ile bitenler bir HTML etiketinin içeriğinin öncesine ve sonrasına ekleme yapmaya yararlar.

Etiketin öncesine ve sonrasına içerik ekleme

[düzenle]

Örnek:

TagBuilder elem = new TagBuilder("div");
elem.Attributes["style"] = "background:pink";
elem.InnerHtml.AppendHtml("Wrapper");
output.PreElement.AppendHtml(elem);
output.PostElement.AppendHtml(elem);

Burası bir tag helper sınıfının Process() metodunun içidir. Bu tag helper'ın uygulandığı etiket ve içeriği korunmakta, ancak etiketin öncesi ve sonrasına pembe arkaplanlı ve içeriğinde "Wrapper" yazan birer <div> eklenmektedir.

Etiketin içeriğinin öncesi ve sonrasına içerik ekleme

[düzenle]

Örnek:

output.PreContent.SetHtmlContent("<b><i>");
output.PostContent.SetHtmlContent("</i></b>");

Burası bir tag helper'ın Process() metodunun içidir. Burada ilgili HTML etiketi ve içeriği korunmakta, ancak içeriğindeki yazıların koyu ve eğik olması sağlanmaktadır.

Tag helper sınıfında geçerli view bağlamına erişme

[düzenle]

Daha önce tag helper'ların normalde uzun olabilecek HTML kodlarını kısa bir şekilde yazabilmek yanında HTML etiketlerine dinamiklik sağlayabileceğini de söylemiştik. İşte bu bölümde bu yönde bir adım atacağız. Şimdiye kadar tag helper'lar kullanıldığı view'ın bağlam verisinden bağımsız olarak HTML kodu üretiyordu. Şimdi tag helper'ın kullanıldığı view'ın bağlam verisine bağlı olarak tag helper'ın ürettiği HTML kodunu özelleştirebileceğiz. Şimdi TagHelpers klasörüne RouteDataTagHelper.cs isimli bir dosya ekleyin ve içeriği şöyle olsun:

using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace WebApp.TagHelpers
{
    [HtmlTargetElement("div", Attributes = "[route-data=true]")]
    public class RouteDataTagHelper : TagHelper {
        [ViewContext]
        [HtmlAttributeNotBound]
        public ViewContext Context { get; set; } = new();
        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            TagBuilder list = new TagBuilder("ul");
            RouteValueDictionary rd = Context.RouteData.Values;
            if (rd.Count > 0)
            {
                foreach (var kvp in rd)
                {
                    TagBuilder item = new TagBuilder("li");
                    item.InnerHtml.Append($"{kvp.Key}: {kvp.Value}");
                    list.InnerHtml.AppendHtml(item);
                }
                output.Content.AppendHtml(list);
            }
            else
            {
                output.Content.Append("No route data");
            }
        }
    }
}

Öncelikle bu tag helper yalnızca route-data seçeneği true olan <div> etiketlerini kapsamaktadır. ViewContext tipindeki ve Context ismindeki özellik tag helper'ın kullanıldığı view'ın bağlam verisine erişim sağlamaktadır. Bu özellik aracılığıyla view'ın bağlam bilgisine erişebilmek için ilgili özellik ViewContext attribute'u ile işaretlenmelidir. Ayrıca ilgili özelliğe HTML etiketinin olası context seçeneğinin değeri atanmasın diye HtmlAttributeNotBound attribute'u ile işaretlenmiştir. Tag helper basitçe rota segmentlerininin her birini bir liste ögesine çevirerek geriye <ul> ve </ul> etiketleri ile öevrelenmiş, her biri <li> ve </li> etiketleri arasına alınmış rota değişken adları ve değerlerini döndürmektedir. Eğer herhangi bir rota değişkeni yoksa geriye "No route data" metnini döndürmektedir. Herhangi bir view'daki <div route-data="true"></div>koduyla ilgili tag helper kullanılabilir.

NOT: Tag helper sınıfları tıpkı controller sınıflarında olduğu gibi yapıcı metot yoluyla bağımlılık enjeksiyonu yapabilir.

Model ifadeleriyle çalışma

[düzenle]

Bir önceki örneğimizde view'ın bağlam verisine erişerek tag helper'ın çalışmasını rota değişkenlerine göre özelleştirmiştik. Bu bölümde view'daki model verisinin bir özelliğine tag helper sınıfından erişerek tag helper'ın çalışmasını model verisine göre özelleştireceğiz. Şimdi TagHelpers klasörüne ModelRowTagHelper.cs isimli dosyayı ekleyin ve içeriği şöyle olsun:

using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace WebApp.TagHelpers
{
    [HtmlTargetElement("tr", Attributes = "for")]
    public class ModelRowTagHelper : TagHelper
    {
        public string Format { get; set; }
        public ModelExpression For { get; set; }
        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            output.TagMode = TagMode.StartTagAndEndTag;
            TagBuilder th = new TagBuilder("th");
            th.InnerHtml.Append(For.Name);
            output.Content.AppendHtml(th);
            TagBuilder td = new TagBuilder("td");
            if (Format != null && For.Metadata.ModelType == typeof(decimal))
            {
                td.InnerHtml.Append(((decimal)For.Model).ToString(Format));
            }
            else
            {
                td.InnerHtml.Append(For.Model.ToString());
            }
            output.Content.AppendHtml(td);
        }
    }
}

Şimdi bu tag helper Views/Home klasöründeki Index.cshtml view'ı tarafından şöyle kullanılsın:

@model Product
<table>
	<tbody>
		<tr for="Name" />
		<tr for="Price" format="c" />
		<tr for="CategoryId" />
	</tbody>
</table>

Bu tag helper for seçeneği olan <tr> etiketlerini etkilemektedir. Tag helper sınıfındaki ModelExpression tipinden olan For özelliği view'ın model nesnesinin bir özelliğini tutmaktadır. For özelliğinin hangi özelliği tuttuğu view'daki HTML'deki for seçeneğinde belirtilir. Daha sonra ModelExpression tipindeki For özelliği üzerinden Name ve Model özellikleri çağrılarak ilgili model özelliğinin ismine ve değerine erişilebilir. Örneğimizde tag helper sınıfının 13. satırında view'da kendi kendini kapatan olarak yazılan <tr> etiketlerinin <tr>...</tr> şeklinde eşli hale dönüştürülmesini sağlıyoruz.14, 15 ve 16. satırlarda bir tablo başlık hücresi ekliyoruz. Bu hücrenin içeriği ilgili model özelliğinin ismi olacak. 17. satırda aynı tablo satırına bir içerik hücresi ekliyoruz. 18, 19, 20 ve 21. satırda eğer HTML etiketinin format seçeneğinin değeri sağlanmışsa ve gönderilen model özelliğinin tipi decimal'se ilgili tutarı belirtilen formatta formatlayarak içerik hücresinin içine ekliyoruz. 22, 23, 24 ve 25. satırlarda eğer HTML etiketinin format seçeneğinin değeri sağlanmamışsa veya gönderilen model özelliğinin tipi decimal değilse basitçe model özelliğinin string karşılığını içerik hücresine ekliyoruz. 26. satırda, oluşturulan bu içerik hücresini de çıktıya ekliyoruz. Sonuç olarak Index view'ına gönderilen Product nesnesinin Name özelliği Lifejacket, Price özelliği 48.95, CategoryId özelliği 1 ise view'daki üç <tr> etiketi için tag helper sınıfı tarafından üretilecek HTML kodları şöyle olacaktır:

<tr>
	<th>Name</th>
	<td>Lifejacket</td>
</tr>
<tr>
	<th>Price</th>
	<td>48.95 TL</td>
</tr>
<tr>
	<th>CategoryId</th>
	<td>1</td>
</tr>

Bu şekilde sadece view'ın (controller view'ı veya Razor view'ı) model nesnesinin özellikleri tag helper'a aktarılabilir. Normal değişkenler aktarılamaz.

Model ifadelerinin Razor sayfalarında kullanımı

[düzenle]

Model ifadelerinin Razor sayfalarında kullanımı biraz önce gördüğümüz aynı mantıkla yapılır. Tek fark Razor sayfalarının görünüm kısmının direkt model nesnesi almak yerine model nesnesini özellik olarak içeren bir sayfa modeli nesnesi almasıdır. Bunu anlamanın en iyi yolu bir örnek üzerinde görmektir. Şimdi projenizin Pages klasöründeki Editor.cshtml dosyasını şöyle değiştirin:

@page "{id:long}"
@model EditorModel
@{
	Layout = null;
}
<!DOCTYPE html>
<html>
<head>
</head>
<body>
	<div>Editor</div>
	<div>
		<table>
			<tbody>
				<tr for="Product.Name" />
				<tr for="Product.Price" format="c" />
			</tbody>
		</table>
	</div>
</body>
</html>

Şimdi bu sayfanın kod kısmı olan Editor.cshtml.cs dosyasını şöyle değiştirelim:

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using WebApp.Models;
namespace WebApp.Pages
{
    public class EditorModel : PageModel
    {
        private DataContext context;
        public Product Product { get; set; }
        public EditorModel(DataContext ctx)
        {
            context = ctx;
        }
        public async Task OnGetAsync(long id)
        {
            Product = await context.Products.FindAsync(id);
        }
    }
}

Projemiz bu haldeyken tag helper'da ModelExpression sınıfındaki Name özelliği ile eriştiğimiz özellik isimleri Product.Name ve Product.Price olarak gelecektir. Bu sorunun üstesinden gelmek için tag helper sınıfının ilgili kısmını şöyle değiştirmeliyiz:

TagBuilder th = new TagBuilder("th");
th.InnerHtml.Append(For.Name.Split(".").Last());
output.Content.AppendHtml(th);

Bu sorunun üstesinden gelmenin başka bir yolu tag helper'a model özelliği ismini belirten ve HTML kısmında yeni bir seçenek olarak belirtilecek yeni bir özellik eklemektir. Örnek:

<tr for="Product.Name" Name="Name" />

Tabii bu kodun istenildiği gibi çalışabilmesi için tag helper sınıfına Name isminde yeni bir özellik tanımlanması, ekrana çıktı verirken de bu Name özelliğinin değerinin kullanılması gerekmektedir.

Tag helper'lar arasında koordinasyon sağlama

[düzenle]

Daha önce üstünkörü geçtiğimiz TagHelperContext sınıfının Items özelliği tag helper sınıfları arasındaki koordinasyonu sağlamak için kullanılabilir. Items özelliği bir sözlük döndürür. Bu sözlüğe bir tag helper'ın eklediği girdiye o tag helper'ın uygulandığı etiketin çocuklarına, torunlarına, vs. etki eden tag helper tarafından erişilebilir. Şimdi projenizin TagHelpers klasörüne CoordinatingTagHelpers.cs isimli ve içeriği aşağıdaki gibi olan dosyayı ekleyin:

using Microsoft.AspNetCore.Razor.TagHelpers;
namespace WebApp.TagHelpers
{
    [HtmlTargetElement("tr", Attributes = "theme")]
    public class RowTagHelper : TagHelper
    {
        public string Theme { get; set; }
        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            context.Items["theme"] = Theme;
        }
    }
    [HtmlTargetElement("th")]
    [HtmlTargetElement("td")]
    public class CellTagHelper : TagHelper
    {
        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            if (context.Items.ContainsKey("theme"))
            {
                output.Attributes.SetAttribute("style",$"background:{context.Items["theme"]}");
            }
        }
    }
}

Kod aslında oldukça basit ve kendini açıklayıcı gözüküyor. İlk tag helper theme seçeneği olan tr etiketlerine uygulanır ve Items sözlüğüne yeni bir girdi ekler. Bu girdinin değeri o tr etiketinin içindeki th ve td etiketleri tarafından erişilebilir. İkinci tag helper tam da bunu yapıyor ve Items sözlüğündeki bir girdinin değerine göre th veya td etiketinin arkaplan rengini ayarlıyor. Burada sadece üst tr etiketinin theme seçeneği olan th ve td etiketlerinin arkaplanı ayarlanacaktır.

UYARI: Tag helper'lar sadece view'da açık olarak yazılmış etiketlere etki ederler. Başka bir tag helper tarafından üretilmiş etiketlere etki etmezler.

Çıktı üretilmeyi engelleme

[düzenle]

Bazen etiketin kendisinin sağlayacağı çıktı da dahil olmak üzere hiçbir çıktı üretilmemesini sağlamak isteyebiliriz. Örnek:

@model Product
<div show-when-gt="500" for="Price">
	<h5>
		Warning: Expensive Item
	</h5>
</div>

Şimdi bu view'ın uyguladığı tag helper'ı oluşturalım:

using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace WebApp.TagHelpers
{
    [HtmlTargetElement("div", Attributes = "show-when-gt, for")]
    public class SelectiveTagHelper : TagHelper
    {
        public decimal ShowWhenGt { get; set; }
        public ModelExpression For { get; set; }
        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            if (For.Model.GetType() == typeof(decimal) && (decimal)For.Model <= ShowWhenGt)
            {
                output.SuppressOutput();
            }
        }
    }
}

Bu örneğimizde view'ımız bir Product nesnesi almakta, Product nesnesinin Price özelliği tag helper'a aktarılmaktadır. Eğer bu Price özelliğinin değeri belirtilen değere eşit veya daha azsa ekrana çıktı üretilmemektedir. Aynı işlemi elbette ki view'da if bloğu açarak da yapabilirdik.

Tag helper component'lar

[düzenle]

Tag helper component'lar sayesinde tag helper'lar servis olarak sağlanabilir. Tag helper'ları servis olarak tanımlamak, eğer tag helper başka bir servis veya middleware'de kullanılacaksa faydalı olabilmektedir.

Tag helper component'ın oluşturulması

[düzenle]

Şimdi projenizin TagHelpers klasöründe ismi TimeTagHelperComponent.cs olan ve içeriği aşağıdaki gibi olan dosyayı oluşturun:

using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace WebApp.TagHelpers
{
    public class TimeTagHelperComponent : TagHelperComponent
    {
        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            string timestamp = DateTime.Now.ToLongTimeString();
            if (output.TagName == "body")
            {
                TagBuilder elem = new TagBuilder("div");
                elem.Attributes.Add("style", "background:gray;");
                elem.InnerHtml.Append($"Time: {timestamp}");
                output.PreContent.AppendHtml(elem);
            }
        }
    }
}

Tag helper component'lar üzerinde çalıştıkları etiketi belirtemezler. Bundan dolayı tag helper component, üzerinde çalıştığı etiketin body olduğundan emin olmak için gerekli kontrolü yapmaktadır. Tag helper component'lar üzerinde çalışması için yapılandırıldıkları her etiket için çalışmaktadırlar. Varsayılan durumda tag helper component'lar yalnızca head ve body etiketlerinin üzerinde çalışması için yapılandırılmışlardır. Bir tag helper component'ın çalışabilmesi için aşağıdaki gibi Program.cs dosyasında servis olarak eklenmesi gerekmektedir:

// Bu kodun çalışabilmesi için öcelikle Microsoft.AspNetCore.Razor.TagHelpers ve WebApp.TagHelpers isim alanlarını import etmelisiniz.
builder.Services.AddTransient<ITagHelperComponent, TimeTagHelperComponent>();

Bu eklemenin sonucunda programımızdaki bütün view'ların (controller view'ları veya Razor view'ları) ilk içeriği daima ilgili zaman damgası olacaktır.

Tag helper component'ın uygulanabileceği etiket kümesini genişletme

[düzenle]

Varsayılan durumda tag helper component'ların programdaki view'ların yalnızca head ve body etiketlerine uygulanabileceğini söylemiştik. Bu bölümde bu kümeyi genişleteceğiz. Şimdi projenizin TagHelpers klasörüne içeriği aşağıdaki gibi olan ve ismi TableFooterTagHelperComponent.cs olan dosyayı ekleyin:

using Microsoft.AspNetCore.Mvc.Razor.TagHelpers;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace WebApp.TagHelpers
{
    [HtmlTargetElement("table")]
    public class TableFooterSelector : TagHelperComponentTagHelper
    {
        public TableFooterSelector(ITagHelperComponentManager mgr, ILoggerFactory log) : base(mgr, log) { }
    }
    public class TableFooterTagHelperComponent : TagHelperComponent {
        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            if (output.TagName == "table")
            {
                TagBuilder cell = new TagBuilder("td");
                cell.Attributes.Add("colspan", "2");
                cell.Attributes.Add("style", "background:yellow;");
                cell.InnerHtml.Append("Table Footer");
                TagBuilder row = new TagBuilder("tr");
                row.InnerHtml.AppendHtml(cell);
                TagBuilder footer = new TagBuilder("tfoot");
                footer.InnerHtml.AppendHtml(row);
                output.PostContent.AppendHtml(footer);
            }
        }
    }
}

Tag helper component sınıfı hangi HTML etiketlerine etki edeceğini seçememektedir. Ama TagHelperComponentTagHelper sınıfından türeyen TableFooterSelector sınıfı projedeki bütün tag helper component'ların etki edeceğini etiket kümesini genişletebilmektedir. Yukarıdaki tag helper component sınıfının Process metodunun ürettiği HTML kodu şöyledir:

<tfoot><tr><td colspan="2" style="background:yellow;">Table Footer</td></tr></tfoot>

Yeni yazdığımız tag helper component sınıfının çalışabilmesi için projemizin Program.cs dosyasına yeni yazdığımız tag helper component'ın servis aboneliğini eklemeliyiz:

builder.Services.AddTransient<ITagHelperComponent, TimeTagHelperComponent>();
builder.Services.AddTransient<ITagHelperComponent, TableFooterTagHelperComponent>();

Örneğimizde de gördüğünüz üzere önceki servis aboneliğini tutuyoruz. Bu sayede her iki tag helper component da projemizde etkin olacaktır. Ancak dikkat edelim ki yaptığımız genişletme sayesinde projemizdeki bütün tag helper component'lar <head>, <body> ve <table> etiketlerine etki edecektir. Projemizdeki genişletmeyi artırmak için TagHelperComponentTagHelper sınıfından türeyen yeni sınıflar oluşturabiliriz. TagHelperComponentTagHelper sınıflarının etkin olabilmesi için servis aboneliği yapılmasına ihtiyaç duyulmaz. TagHelperComponentTagHelper sınıfından türeyen sınıflar otomatik olarak keşfedilir ve tag helper component'ların etki ettiği etiket kümesini genişletmek için kullanılır.