ASP.NET Core - 14. Tag Helper
Tag Helper는 View나 Page에서 HTML요소로 변환되는 C# class입니다. Tag Helper은 일반적으로 Application의 Routing설정을 사용하는 Form에서 URL을 생성하거나 특정한 유형의 요소가 일관적으로 표현되도록 하는 데 사용되며 사용자 정의된 특정요소를 필요한 대상의 content요소로 바뀔 수 있도록 합니다. 이번 글에서는 어떻게 Tag helper가 작동하는지와 사용자 정의 tag helper를 어떻게 생성하고 적용할 수 있을지에 대해서도 알아볼 것입니다.
1. Project 준비
예제를 위한 Project는 이전 글에서의 Project를 계속 사용할 것입니다. 다만 Program.cs file을 아래와 같이 변경하여 이전에 작성된 몇몇 설정을 제거하도록 합니다.
using Microsoft.EntityFrameworkCore;
using MyWebApp.Models;
using Microsoft.AspNetCore.Mvc.RazorPages;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<NorthwindContext>(opts => {
opts.UseSqlServer(builder.Configuration["ConnectionStrings:DefaultConnection"]);
opts.EnableSensitiveDataLogging(true);
});
builder.Services.AddControllersWithViews();
builder.Services.AddRazorPages();
builder.Services.AddSingleton<CitiesData>();
var app = builder.Build();
app.UseStaticFiles();
app.MapControllers();
app.MapDefaultControllerRoute();
app.MapRazorPages();
app.Run();
다음으로 Views/Home folder에 있는 Index.cshtml file역시 아래와 같이 변경합니다.
@model Products
@{
Layout = "_SimpleLayout";
}
<table class="table table-striped table-bordered table-sm">
<thead>
<tr>
<th colspan="2">Product Summary</th>
</tr>
</thead>
<tbody>
<tr><th>Name</th><td>@Model?.ProductName</td></tr>
<tr><th>Price</th><td>@Model?.UnitPrice.Value.ToString("c")</td></tr>
<tr><th>Category ID</th><td>@Model?.CategoryId</td></tr>
</tbody>
</table>
변경된 Index.cshtml은 새로운 Layout file에 의존하고 있으므로 _SimpleLayout.cshtml이름의 file을 Views/Shared folder에 아래와 같이 추가합니다.
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>@ViewBag.Title</title>
<link href="/lib/bootstrap/css/bootstrap.min.css" rel="stylesheet" />
</head>
<body>
<div class="m-2">
@RenderBody()
</div>
</body>
</html>
Project를 실행하고 /home으로 URL을 요청하여 다음과 같은 결과가 표시되는지 확인합니다.
2. Tag Helper 생성
tag helper를 이해하기 가장 좋은 방법은 tag helper가 어떻게 동작하는지 또 ASP.NET Core Application에 얼마나 잘 맞아 들어가는지를 볼 수 있는 예제를 직접 만들어 보는 것입니다. 따라서 이번에는 tr 요소에 Bootstrap CSS Class를 설정하는 tag helper를 직접 만들고 적용해 보는 절차를 하나씩 따라가 볼 것입니다.
만들고자 하는 tag helper는 HTML요소에서 tr-color attribute를 인식하고 여기에 class속성을 설정하는 간단한 동작을 수행할 것입니다. 물론 아주 획기적인 기능을 수행하는 것은 아니지만 어떻게 tag helper가 작동하는지를 알아보기에는 충분할 것입니다.
(1) Tag Helper Class 정의하기
Tag helper는 Project의 어디서든 정의될 수 있으나 사용되기 전에 등록되는 절차가 필요하므로 한곳에서 생성하는 것이 관리하기에 유리할 수 있습니다. project에서 TagHelpers folder를 만들고 TrTagHelper.cs이름의 file을 아래와 같이 추가합니다.
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace MyWebApp.TagHelpers
{
public class TrTagHelper : TagHelper
{
public string BgColor { get; set; } = "dark";
public string TextColor { get; set; } = "white";
public override void Process(TagHelperContext context, TagHelperOutput output)
{
output.Attributes.SetAttribute("class", $"bg-{BgColor} text-center text-{TextColor}");
}
}
}
예제의 Tag Helper는 Microsoft.AspNetCore .Razor.TagHelpers namespace에서 정의된 TagHelper class로부터 파생되었습니다. 또한 해당 하위 class에서 재정의된 Process method를 정의하여 요소를 변환시키는 동작을 구현하도록 하고 있습니다.
tag helper의 이름은 TagHelper에 의해 변환될 요소의 이름과 결합됩니다. 위 예제의 경우 TrTagHelper라는 이름을 갖고 있으며 tr요소를 처리하는 tag helper임을 나타내고 있습니다. tag helper가 적용될 수 있는 요소의 범위는 attribute를 통해서 확장되거나 좁혀질 수 있지만 기본적으로는 class명에 의해 동작이 정의됩니다.
비동기 tag helper는Process method대신 ProcessAsync method를 재정의함으로써 생성될 수 있습니다. 하지만 대부분의 helper에서는 작고 HTML요소만을 변경하는데 집중하도록 만들어지기 때문에 비동기가 굳이 필요하지 않습니다.
● Context Data 전달받기
Tag helpers는 자신이 변환해야할 HTML요소에 대한 정보를 Process method의 인수를 통해서 받는데 이때 아래 표의 속성들을 정의하는 TagHelperContext class의 instance를 통해서 전달받습니다.
AllAttributes | 이 속성은 변환될 요소에서 이름과 index에 의해 indexing된 요소의 attributes에 대한 읽기전용 dictionary를 반환합니다. |
Items | 이 속성은 tag helper사이를 조정하기 위해 사용된 dictionary를 반환합니다. |
UniqueId | 이 속성은 변환될 요소에 사용될 수 있는 유일한 식별자를 반환합니다. |
물론 AllAttributes dictionary를 통해서 요소에 대한 attribute의 상세에 접근할 수 있지만 더 좋은 접근법은 아래와 같이 attribute에 해당하는 이름의 속성을 정의하는 것입니다.
public string BgColor { get; set; } = "dark";
public string TextColor { get; set; } = "white";
tag helper의 사용이 시작되면 tag helper에 정의된 속성이 검사 되고 HTML요소에 적용된 attribute와 이름이 일치하는 곳에 값이 할당됩니다. 이러한 처리의 일부분으로서 attribute값은 C#속성의 유형과 일치하도록 변환되므로 bool유형의 속성은 true와 false attribute값을 전달받는 데 사용되며 int유형은 1이나 2와 같은 숫자로 된 속성값을 전달받는 데 사용될 수 있습니다.
따라서 HTML요소의 속성에 해당되지 않는 Property는 설정되지 않으므로 null을 처리하거나 적절한 기본값을 제공하지 않는지 확인해야 합니다.
attribute의 이름은 자동적으로 기본 HTML Style로 부터 변환되므로 bg-color의 경우 C# style인 BgColor가 됩니다. 사실 asp-(Microsoft가 사용하는)와 data-(client로 보내질 사용자 정의 attribute를 위해 예약된)를 제외하고는 어떠한 접두사의 attribute도 사용할 수 있습니다. 예제 tag helper는 bg-color와 text-color attribute로 설정되었으며 Process method에서 tr요소를 구성하는 데 사용됩니다.
tag helper속성에 HTML attribute이름을 사용한다고 해서 읽기가능하거나 이해할 수 있는 class가 되는 것은 아닙니다. 속성을 나타내는 HTMLattribute를 지정하기 위한 HtmlAttributeName attribute를 사용하여 속성 이름과 속성이 나타내는 속성 간의 연결을 끊을 수 있습니다.
● 출력 생성
Process method는 인수로 전달받은 TagHelperOutput개체의 설정을 통해 element를 변환합니다. TagHelperOuput개체는 view에 나타난 HTML 요소를 설명하는 것으로 시작하며 아래 표에 해당하는 속성과 method에 의해 수정됩니다.
TagName | 이 속성은 출력되는 요소의 tag명을 가져오거나 설정하는데 사용됩니다. |
Attributes | 이 속성은 출력되는 요소의 attribute를 포함하는 dictionary를 반환합니다. |
Content | 이 속성은 요소의 content를 설정하는데 사용되는 TagHelperContent개체를 반환합니다. |
GetChildContentAsync() | 이 비동기 method는 변환될 요소의 content에 대한 접근을 제공합니다. |
PreElement | 이 속성은 요소를 출력하기 전에 View에 content를 추가하기 위해 사용되는 TagHelperContext개체를 반환합니다. |
PostElement | 이 속성은 요소를 출력한 이 후 View에 content를 추가하기 위해 사용되는 TagHelperContext개체를 반환합니다. |
PreContent | 이 속성은 요소의 content를 출력하기 전에 View에 content를 추가하기 위해 사용되는 TagHelperContext개체를 반환합니다. |
TagHelperContext | 이 속성은 요소의 content를 출력한 이 후 View에 content를 추가하기 위해 사용되는 TagHelperContext개체를 반환합니다. |
TagMode | 이 속성은 TagMode열거형으로 부터의 값을 사용해 TagMode라는m요소가 출력될 방식을 지정합니다. |
SupressOuput | view에서 요소를 제외하는데 사영되며 자세한 사항은 추후에 살펴볼 것입니다. |
TrTagHelper class에서는 class attribute를 BgColor와 TextColor속성의 값을 포함하여 Bootstrap style을 특정하는 HTML요소로 추가하기 위해 Attributes dictionary를 사용하였습니다. 이것으로 bg-color와 text-color를 속성을 primary, info, danger와 같은 Bootstrap 이름으로 설정함으로써 tr 요소의 background color를 지정할 수 있습니다.
(2) Tag Helper 등록
Tag Helper class는 사용하기 이전에 @addTagHelper지시자를 통해서 등록되어야 합니다. tag helper가 적용될 일련의 view나 page들은 addTagHelper지시자가 사용되는 곳에 따라 달라집니다.
단일 view또는 page에서 지시자는 cshtml file안에서 사용될 수 있는데 tag helper의 범위를 더 넓게 확장하려면 controller를 위한 view folder에서 정의되거나 Razor page를 위한 Pages folder에 정의되는 view import file에서 지시자를 추가할 수 있습니다.
예제에서 tag helper는 Project의 전체에 걸쳐 사용되기를 의도할 것이므로 View와 Pages folder에 있는 _ViewImports.cshtml file에 지시자를 추가할 것입니다. 다만 view component를 적용하기 위해 아래 글에서 사용하던
[.NET/ASP.NET] - ASP.NET Core - 13. View Component
vc 요소역시 tag helper에 해당하는데 이 때문에 _ViewImports.cshtml에는 이미 아래와 같이 @addTagHelper 지시자를 추가하였으므로
@using MyWebApp.Models
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@using MyWebApp.Components
@addTagHelper *, MyWebApp
이전 project를 계속 연결해서 사용하는 경우라면 해당 구문이 이미 추가되었을 수 있습니다. 이때 사용한 지시자에서 첫 번째 인수는 wildcard를 지원하는 tag helper class의 이름을 명시한 것이며 두 번째 인수는 해당 class가 정의된 assembly의 이름을 명시한 것입니다. 특히 @addTagHelper지시자는 wildcard를 통해 해당 project assembly의 모든 namespace를 선택하여 project의 전체 controller view의 어디서든 tag helper를 정의할 수 있습니다. 참고로 Pages folder에 있는 _ViewImports.cshtml Razor Page에서도 위와 동일한 구문이 존재합니다.
@namespace MyWebApp.Pages
@using MyWebApp.Models
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, MyWebApp
위에서 다른 @addTagHelper지시자는 Microsoft에서 제공하는 내장된 tag helper를 사용하기 위한 것입니다.
(3) tag helper의 사용
이제 마지작으로 tag helper를 사용하여 요소를 변환해야 합니다. 따라서 아래 Views/Home의 Index.cshtml의 예제에서 tag helper를 적용할 tr 요소를 추가합니다.
<thead>
<tr bg-color="info" text-color="white">
<th colspan="2">Product Summary</th>
</tr>
</thead>
Project를 실행하고 /home URL을 요청하면 다음과 같은 결과를 볼 수 있습니다.
위 화면에서 bg-color와 text-color attribute가 적용된 tr요소가 변환된 것을 보여주고 있는데 이것이 다는 아닙니다. 기본적으로 tag helper는 지정한 type의 모든 요소에 적용되는데 이때 view의 tr요소에서는 정의된 attribute가 없는 경우 tag helper class에 적용된 기본값을 사용해 변환됩니다.(예제에서 일부 table row에 text가 없는 이유는 row를 번갈아서 다른 style을 적용하기 위한 Bootstrap table-striped class 때문입니다.)
가장 심각한 문제는 view import file에서의 @addTagHelper지시자로 인해 project의 controller나 Razor Page에서 render 되는 모든 view의 tr요소가 적용대상이 되었으므로 /cities와 같은 URL을 통해서도 해당 응답에 사용된 tr요소가 변환되었음을 알 수 있습니다.
(4) Tag Helper의 범위 좁히기
tag helper에 의해 변환될 요소의 범위는 아래와 같이 HtmlTargetElement요소를 통해 조정될 수 있습니다.
[HtmlTargetElement("tr", Attributes = "bg-color,text-color", ParentTag = "thead")]
public class TrTagHelper : TagHelper
예제는 TrTagHelper.cs에서 HtmlTargetElement attribute를 적용한 것으로 해당되는 요소에만 tag helper가 적용되도록 합니다. 여기서 첫 번째 인수는 요소의 유형을 특정하는 것이며 이어서 아래표에 해당하는 속성을 사용할 수 있습니다.
Attributes | 이 속성은 일련의 해당 attribute가 주어진 요소에만 tag helper가 적용되도록 특정하는 것으로서 comma(,)문자를 사용해 설정할 attribute항목을 지정할 수 있습니다. 이때 attribute의 이름은 *문자로도 지정할 수 있는데 이렇게 되면 예를 들어 bg-* 와 같은 경우 bg-color나 bg-size등 해당하는 이름이 포함된 모든 속성을 지정하게 됩니다. |
ParentTag | 이 속성은 주어진 type의 요소안에서만 포함되는 요소에만 tag helper가 적용되도록 합니다. |
TagStructure | 이 속성은 Unspecified, NormalOrSelfClosing, WithoutEndTag 등을 정의하는 해당 속성의 열거값으로 지정한 값에 해당하는 tag구조의 요소를 특정하여 tag helper가 적용되도록 합니다. |
CSS attribute를 지원하는 Attributes속성은 구문을 선택하게 되므로 bg-color attribute를 가진 요소는 [bg-color]와 일치하고 [bg-color=primary]처럼 primary값을 가진 bg-color attribute의 경우에도 일치하며 [bg-color^=p]에서 p로 시작하는 값의 bg-color속성에 해당하는 요소와도 일치합니다.
결론적으로 tag helper로 적용된 attribute의 값에 따라 tr 요소이면서 bg-color와 text-color attribute속성을 가지며 thead 요소의 자식 요소인 경우에만 tag helper가 적용될 것입니다. project를 시작하고 /home/index/1 URL을 요청하여 다음과 같은 결과를 확인합니다.
(5) Tag Helper의 범위 넓히기
HtmlTargetElement attribute는 또한 tag helper의 범위를 넓히는 데도 사용될 수 있어서 요소의 범위를 광범위하게 일치시킬 수 있습니다. 이것은 attribute의 첫번째 인수를 *로 설정함으로써 가능하며 이때 *은 모든 요소를 의미합니다. 따라서 아래 예제는 bg-color와 text-color attribute를 가진 모든 요소에 적용되도록 하는 것입니다.
[HtmlTargetElement("*", Attributes = "bg-color,text-color", ParentTag = "thead")]
public class TrTagHelper : TagHelper
물론 위와 같이 설정하는 경우는 상당한 주의를 요구합니다. 상당히 많은 HTML요소가 범위에 들어가게 되며 변환되지 말아야 할 요소까지도 모든 게 다 포함될 수 있기 때문입니다. 따라서 좀 더 안전하게는 HtmlTargetElement attribute에서 적용이 필요한 각각의 요소를 따로 지정하는 방법이 사용될 수 있습니다.
[HtmlTargetElement("tr", Attributes = "bg-color,text-color")]
[HtmlTargetElement("td", Attributes = "bg-color")]
public class TrTagHelper : TagHelper
각 attribute의 instance는 서로 다른 조건을 지정할 수 있으므로 tag helper는 tr 요소이면서 bg-color와 text-color attribute를 가진 요소와 일치하거나 tr 요소이면서 bg-color attribute만 가진 요소와도 일치할 수 있습니다. 아래 예제에서는 Views/Home folder의 index.cshtml을 변경하여 위에서 수정된 범위에 따라 변환되어야 할 요소를 추가하도록 하였습니다.
<tr>
<th>Price</th>
<td bg-color="dark">@Model?.UnitPrice.Value.ToString("c")</td>
</tr>
project를 실행하여 /home/index/1 URL을 요청하여 다음과 같은 결과를 확인합니다. 해당 요청에서는 2개의 요소가 변환된 응답을 반환할 것입니다.
tag helper의 적용 순위
하나의 요소에 대해서 여러 tag helper를 적용하는 경우 TagHelper base class로부터 상속된 Order속성을 통해 실행될 순서를 조정할 수 있습니다. 순서를 관리한다고 하더라도 문제를 일으킬 소지가 여전히 높기는 하지만 tag helper사이의 충돌을 최소화하는데 도움이 될 수 있습니다.
3. 향상된 Tag Helper 기능
위에서는 tag helper를 어떻게 생성하고 사용할 수 있는지에 대한 기본적인 것만을 다뤄보았습니다. 이번에는 좀 더 나아가 tag helper에 대한 더욱 향상된 사용방법과 이들이 제공하는 기능에 대해 알아보고자 합니다.
(1) 단축 요소 생성
tag helper는 기본적인 HTML요소를 변환하거나 사용자 정의 요소를 일반적인 content로 바꾸는 것에 제한을 두지 않습니다. 이러한 기능은 view를 더욱 간소하게 만들 수 있도록 하며 의도를 더욱 분명하게 만들 수 있습니다. 이와 관련하여 아래 예제는 index view의 thead요소를 사용자 정의요소로 바꾼 것입니다.
<table class="table table-striped table-bordered table-sm">
<tablehead bg-color="dark">Product Summary</tablehead>
<tbody>
<tr><th>Name</th><td>@Model?.ProductName</td></tr>
<tr>
<th>Price</th>
<td bg-color="dark">@Model?.UnitPrice.Value.ToString("c")</td>
</tr>
<tr><th>Category ID</th><td>@Model?.CategoryId</td></tr>
</tbody>
</table>
예제에서 사용된 tablehead요소는 HTML명세상 일부가 아니며 browser는 이를 해석할 수 없습니다. 대신 예제에서는 이 요소를 HTML table의 일부로서 thead요소를 생성하기 위한 단축 요소로서 사용하고 있습니다. 이어서 TagHelpers folder에 TableHeadTagHelper.cs이름의 file을 아래와 같이 추가합니다.
공식적인 HTML요소가 아닌 요소를 처리할 때는 반드시 HtmlTargetElement attribute를 적용하고 요소의 이름을 특정해야 합니다. class명에 기반한 요소의 tag helper 적용규칙은 표준 요소 이름에서만 작동합니다.
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace MyWebApp.TagHelpers
{
[HtmlTargetElement("tablehead")]
public class TableHeadTagHelper : TagHelper
{
public string BgColor { get; set; } = "light";
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
output.TagName = "thead";
output.TagMode = TagMode.StartTagAndEndTag;
output.Attributes.SetAttribute("class", $"bg-{BgColor} text-white text-center");
string content = (await output.GetChildContentAsync()).GetContent();
output.Content.SetHtmlContent($"<tr><th colspan=\"2\">{content}</th></tr>");
}
}
}
예제에서의 tag helper는 비동기이며 ProcessAsync method를 재정의한 것으로 변환하는 요소의 기 content에 접근할 수 있습니다. ProcessAsync method는 기존과는 다른 요소의 생성을 위해 TagHelperOuput개체의 속성을 사용합니다.(TagName속성은 thead요소를 특정하는 데 사용되며 TagMode는 시작과 종료 tag를 사용하는 요소를 특정하는데 사용되고 Attributes.SetAttribute method는 class attribute를 정의하는 데 사용되며 Content속성은 요소의 content를 설정하는 데 사용됩니다.)
요소의 기존 content는 TagHelperContent를 반환하는 비동기 GetChildContentAsync method를 통해 가져오고 있습니다. 이것은 TagHelperOutput.Content속성을 통해 반환되는 것과 같은 개체이며 아래 표의 method를 통해 같은 유형을 사용하여 요소의 content가 분석되고 바뀔 수 있도록 합니다.
GetContent() | 이 method는 HTML요소의 content를 문자열로 반환합니다. |
SetContent(text) | 이 method는 출력되는 요소의 content를 설정합니다. 이때 문자열 인수는 encode되므로 HTML요소와 안전하게 통합될 수 있습니다. |
SetHtmlContent(html) | 이 method는 출력되는 요소의 content를 설정합니다. 이때 문자열 인수는 안전하게 encode되었다고 가정하므로 주의해야 사용해야 합니다. |
Append(text) | 이 method는 지정한 문자열을 안전하게 encode하며 그 결과를 출력되는 요소의 content에 추가합니다. |
AppendHtml(html) | 이 method는 지정한 문자열을 어떠한 형태의 encode를 거치지 않고 출력되는 요소의 content에 추가합니다. |
Clear() | 이 method는 출력되는 요소의 content를 제거합니다. |
위에서 정의한 TableHeadTagHelper class에서 요소의 기존 content는 GetContent method를 통해 읽어 들이고 다시 SetHtmlContent method를 통해 설정하고 있습니다. 이를 통해 변환된 요소의 기존 content를 tr요소와 th요소로 감싸는 효과를 가져오게 됩니다.
project를 실행하여 /home/index/1 URL을 호출하여 tag helper의 효과를 확인합니다.
결과를 보면 tag helper는 아래 단축 요소를
<tablehead bg-color="dark">Product Summary</tablehead>
다음과 같은 요소로 변환되었음을 알 수 있습니다.
<thead class="bg-dark text-white text-center"><tr><th colspan="2">Product Summary</th></tr></thead>
변환된 요소는 bg-color attribute를 포함하고 있지 않음에 주목해야 합니다. tag helper에 의해 정의된 속성과 일치하는 attribute는 출력요소로 부터 제거되었으므로 이들 속성이 필요하다면 반드시 명시적으로 재정의되어야 합니다.
(2) Program에 따라 요소 생성하기
새로운 HTML요소를 생성할 때, 필요한 content를 만들기 위해서 위 예제에서 시도한 것처럼 C# 문자열 형식을 사용할 수 있습니다. 이러한 방법은 잘 작동하기는 하지만 꽤 번거로운 방법일 수 있으며 오타에 주의를 기울어야 합니다. 대신 이보다 더 나은 접근 방법은 Microsoft.AspNetCore.Mvc.Rendering namespace에서 정의된 TagBuilder class를 사용하는 것이며 요소를 더욱 구조화된 방식으로 생성할 수 있습니다. 위 표에서 설명한 TagHelperContent method는 아래 예제에서 보는 바와 같이 tag helper에서 HTML Content를 쉽게 생성할 수 있도록 만드는 TagBuilder개체를 사용합니다.
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
output.TagName = "thead";
output.TagMode = TagMode.StartTagAndEndTag;
output.Attributes.SetAttribute("class", $"bg-{BgColor} text-white text-center");
string content = (await output.GetChildContentAsync()).GetContent();
//output.Content.SetHtmlContent($"<tr><th colspan=\"2\">{content}</th></tr>");
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);
}
위 예제는 각각 TagBuilder개체를 사용해 새로운 요소를 생성하고 이전 예제에서의 문자열 기반 version과 같은 HTML요소를 만들기 위해 구성되었습니다.
(3) Content와 요소를 주변에 추가하기
TagHelperOutput class는 아래 표에서 소개된 4개의 속성을 제공하여 view에 새로운 content를 쉽게 주입할 수 있으며 이러한 방식으로 요소나 혹은 요소의 content를 둘러싸는 형식을 손쉽게 생성할 수 있습니다. 그럼 이제 해당 속성을 사용해 어떻게 content주변과 대상 요소에 원하는 content나 요소를 추가할 수 있는지를 알아볼 것입니다.
PreElement | 이 속성은 view에 있는 대상 요소앞에 새로운 요소를 삽입하는데 사용됩니다. |
PostElement | 이 속성은 view에 있는 대상 요소위에 새로운 요소를 삽입하는데 사용됩니다. |
PreContent | 이 속성은 대상 요소내부에서 현재 존재하는 content앞에 새로운 content를 삽입하는데 사용됩니다. |
PostContent | 이 속성은 대상 요소내부에서 현재 존재하는 content뒤에 새로운 content를 삽입하는데 사용됩니다. |
● 출력요소 주변에 content 추가하기
첫 번째 TagHelperOuput 속성은 PreElement와 PostElement이며 view에 있는 대상 요소의 앞과 뒤에 새로운 요소를 추가하는 데 사용됩니다. 해당 속성을 사용해 보기 위해 project의 TagHelpers folder로 ContentWrapperTagHelper.cs file을 아래와 같이 추가합니다.
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace MyWebApp.TagHelpers
{
[HtmlTargetElement("*", Attributes = "[wrap=true]")]
public class ContentWrapperTagHelper : TagHelper
{
public override void Process(TagHelperContext context, TagHelperOutput output)
{
TagBuilder elem = new TagBuilder("div");
elem.Attributes["class"] = "bg-primary text-white p-2 m-2";
elem.InnerHtml.AppendHtml("Wrapper");
output.PreElement.AppendHtml(elem);
output.PostElement.AppendHtml(elem);
}
}
}
예제의 tag helper는 wrap속성과 함께 true값을 가진 요소를 변환합니다. 이때 PreElement와 PostElement속성을 사용하여 대상 요소의 앞뒤에 div요소를 추가합니다. 다음 예제는 Views/Home foler의 Index.cshtml file을 수정하여 tag helper에 의해 변환될 요소를 추가한 것입니다.
@{
Layout = "_SimpleLayout";
}
<div class="m-2" wrap="true">Inner Content</div>
project를 실행하고 /home/index/1 URL을 요청하여 응답에 포함된 변환된 요소를 확인합니다.
결과를 보면 최초 아래의 요소는
<div class="m-2" wrap="true">Inner Content</div>
다음과 같은 결과가 만들어졌음을 확인할 수 있습니다.
<div class="bg-primary text-white p-2 m-2">Wrapper</div><div class="m-2" wrap="true">Inner Content</div><div class="bg-primary text-white p-2 m-2">Wrapper</div>
대상 요소에 wrap속성이 남아 있음에 주목하시기 바랍니다. 이것은 tag helper class에서 이 attribute에 해당하는 속성을 따로 정의하지 않았기 때문입니다. 결과에서 해당 attribute가 포함되는 것을 막고자 한다면 tag helper class에서 실제 attribute의 값을 사용하지 않는다고 하더라도 이들에 관한 속성을 정의해야 합니다.
● 출력요소 내부에 content 삽입하기
PreContent와 PostContent는 본래 content를 둘러싸는 대상 요소 내부에 content를 삽입하는 속성입니다. 해당 속성을 사용해 보기 위해 TagHelpers folder안에 HighlightTagHelper.cs이름의 file을 아래와 같이 생성합니다.
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace MyWebApp.TagHelpers
{
[HtmlTargetElement("*", Attributes = "[highlight=true]")]
public class HighlightTagHelper: TagHelper
{
public override void Process(TagHelperContext context, TagHelperOutput output)
{
output.PreContent.SetHtmlContent("<b><i>");
output.PostContent.SetHtmlContent("</i></b>");
}
}
}
위 예제의 tag helper는 대상 요소의 content주위에 b와 i요소를 삽입합니다. 이어서 아래 예제는 Views/Home folder의 Index view에서 tag helper가 적용되도록 table의 cell 중 하나에 attribute를 사용하도록 하였습니다.
<tr><th>Name</th><td highlight="true">@Model?.ProductName</td></tr>
<tr>
<th>Price</th>
<td bg-color="dark">@Model?.UnitPrice.Value.ToString("c")</td>
</tr>
<tr><th>Category ID</th><td>@Model?.CategoryId</td></tr>
project를 시작하고 /home/index/1 URL을 요청하여 다음과 같은 변환된 요소가 포함된 응답을 확인합니다.
변환된 요소를 확인해 보면 본래 다음의 요소가
<tr><th>Name</th><td highlight="true">@Model?.ProductName</td></tr>
아래와 같이 바뀌어 있음을 알 수 있습니다.
<td highlight="true"><b><i>Chai</i></b></td>
(4) View context data 가져오기
tag helper를 사용하는 가장 일반적인 경우는 요소를 변환하여 현재 요청이나 혹은 context data로의 접근이 필요한 view model/page model의 세부정보를 포함하도록 하는 것입니다. tag helper의 이러한 유형을 생성해 보기 위해 TagHelpers folder에 RouteDataTagHelper.cs이름의 file을 아래와 같이 추가합니다.
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace MyWebApp.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)
{
output.Attributes.SetAttribute("class", "bg-primary m-2 p-2");
TagBuilder list = new TagBuilder("ul");
list.Attributes["class"] = "list-group";
RouteValueDictionary rd = Context.RouteData.Values;
if (rd.Count > 0) {
foreach (var kvp in rd) {
TagBuilder item = new TagBuilder("li");
item.Attributes["class"] = "list-group-item";
item.InnerHtml.Append($"{kvp.Key}: {kvp.Value}");
list.InnerHtml.AppendHtml(item);
}
output.Content.AppendHtml(list);
}
else
output.Content.Append("No route data");
}
}
}
예제의 tag helper는 값이 true인 route-data attribute를 가진 div요소를 변환하며 routing system에서 구해진 segment변수의 list로 출력요소를 채우도록 하고 있습니다. 이때 route data를 가져오기 위해서 Context라는 속성을 추가했으며 여기에 2개의 attribute를 아래와 같이 적용하였습니다.
[ViewContext]
[HtmlAttributeNotBound]
public ViewContext Context { get; set; } = new();
ViewContext attribute는 tag helper의 새로운 instance가 생성될 때 해당 속성에 routing data를 포함하여 rendering 되는 view의 상세를 제공하는 ViewContext개체가 할당되어야 함을 나타냅니다.
HtmlAttributeNotBound attribute는 div요소에 정의된 일치되는 attribute가 있다면 이 속성에 값이 할당되는 것을 방지하도록 합니다. 이것은 특히 다른 개발자들이 사용하도록 하기 위해 tag helper를 작성하는 경우라면 해당 attribute를 사용하는 것이 좋습니다.
tag helper는 생성자에서 의존성 service를 통해 선언될 수 있으며 의존성 주입을 통해 resolve 됩니다.
아래 예제는 Home controller의 index view로 예제의 tag helper를 통해 변환될 요소를 추가한 것입니다.
@{
Layout = "_SimpleLayout";
}
<div route-data="true"></div>
<table class="table table-striped table-bordered table-sm">
Project를 실행한 후 /home/index/1 URL을 요청하여 다음과 같은 응답을 확인합니다. 해당 응답은 routing system과 일치되는 segment 변수의 항목을 열거하게 됩니다.
(5) Model 표현식을 통해 작업하기
tag helper는 view model에서 작동할 수 있으며 이를 통해 수행하는 변환이나 생성할 출력을 조정할 수 있습니다. 이것이 어떻게 작동하는지를 알아보기 위해 TagHelpers folder에 ModelRowTagHelper.cs이름의 file을 아래와 같이 추가합니다.
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace MyWebApp.TagHelpers
{
[HtmlTargetElement("tr", Attributes = "for")]
public class ModelRowTagHelper : TagHelper
{
public string Format { get; set; } = string.Empty;
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 ?? String.Empty);
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() ?? String.Empty);
output.Content.AppendHtml(td);
}
}
}
예제의 tag helper는 for attribute를 가진 요소를 변환합니다. 여기서 중요한 것은 for attribute의 값을 전달받기 위해 사용되는 for 속성의 유형입니다.
public ModelExpression? For { get; set; }
이때 ModelExpression은 view model일부로서 작업하고자 하는 경우 사용됩니다.
ModelExpression 기능은 view model 또는 page model하에서만 사용될 수 있고 @foreach 표현식과 같은 view안에서 생성되는 변수를 통해서는 사용될 수 없습니다.
아래 예제는 Views/Home folder의 Index.cshtml file에서 tag helper가 사용되는 경우를 표현하고 있습니다.
<table class="table table-striped table-bordered table-sm">
<tablehead bg-color="dark">Product Summary</tablehead>
<tbody>
<tr for="ProductName" />
<tr for="UnitPrice" format="c" />
<tr for="CategoryId" />
</tbody>
</table>
예제에서 for attribute의 값은 view model class에 정의된 속성의 이름입니다. tag helper가 생성될 때 for 속성의 유형이 감지되고 선택한 속성의 서술하는 ModelExpression개체를 할당하게 됩니다.
ASP.NET Core는 요소의 변환을 위해 view model을 사용하는 일련의 유용한 내장 tag helper를 제공하고 있으므로 자신만의 것을 굳이 만들 필요는 없고 따라서 ModelExpression을 직접적으로 사용하는 사례는 드물다고 할 수 있습니다.
예제에서는 tag helper를 위해 3개의 기본적인 기능을 사용했으며 그중 첫 번째는 model속성의 이름을 가져온 것입니다. 따라서 아래와 같은 출력 요소를 포함시킬 수 있는데
th.InnerHtml.Append(For?.Name ?? String.Empty);
이때 Name속성은 model속성의 이름을 반환하고 있습니다. 두 번째는 model속성의 유형을 가져옴으로써 값의 유형자체를 아래와 같이 판단할 수 있게 되었습니다.
f (Format != null && For?.Metadata.ModelType == typeof(decimal))
마지막 세 번째 기능은 속성의 값을 가져오는 것으로서 응답에 해당 값을 포함시킬 수 있게 되었습니다.
td.InnerHtml.Append(For?.Model.ToString() ?? String.Empty);
Project를 실행하여 /home/index/2 URL을 호출하면 다음과 같은 결과를 볼 수 있습니다.
● Page Model을 통해 작업하기
Model표현식을 통한 tag helper는 비록 속성을 선택하는 표현식이 Model속성이 page model class를 반환하는 방식을 고려해야 하지만 Razor Page에서 충분히 적용될 수 있습니다. 아래 예제는 page model에서 Product속성을 정의하는 Pages/Editor.cshtml Razor Page에 tag helper를 적용한 것입니다.
<table class="table table-sm table-striped table-bordered">
<tbody>
<tr for="Products.ProductName" />
<tr for="Products.UnitPrice" format="c" />
</tbody>
</table>
예제에서 for attribute의 값은 tag helper에게 필요한 Model Expression을 제공하는 Products속성을 통해서 Name과 Price를 선택하고 있습니다.
Model Expression은 위 예제에서 null조건 연산자와 함께는 사용할 수 없습니다. 하지만 예제는 Products의 속성이 Products? 형식이므로 Editor.cshtml.cs file을 아래와 같이 변경하여 Products에 대한 유형을 바꾸고 기본값을 할당하도록 하였습니다.
public Products Products { get; set; } = new();
public EditorModel(NorthwindContext ctx)
{
context = ctx;
}
public async Task OnGetAsync(int id)
{
Products = await context.Products.FindAsync(id) ?? new();
}
Project를 실행하여 /editor/1 URL을 호출하고 다음과 같은 응답을 확인합니다.
Page Model에서 한 가지 문제는 ModelExpression.ProductName속성이 예를 들어 단순히 ProductsName이라고 하는 대신에 Products.ProductName을 반환한다는 것입니다. 따라서 ModelRowTagHelper.cs file의 tag helper를 변경하여 Model 표현식 이름에서 마지막 부분만을 표시하도록 처리합니다.
TagBuilder th = new TagBuilder("th");
th.InnerHtml.Append(For?.Name.Split(".").Last() ?? String.Empty);
output.Content.AppendHtml(th);
project를 다시 실행하여 이전과 동일한 URL을 요청하면 다음과 같은 결과를 확인할 수 있습니다.
(6) Tag Helper 간 조정
TagHelperContext.Items속성은 일련의 dictionary를 제공하는데 이 것은 하위요소를 포함한 요소에서 동작하는 tag helper에 의해 사용됩니다. Items collection을 사용해 보기 위해 CoordinatingTagHelpers.cs file을 TagHelplers folder에 아래와 같이 추가합니다.
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace MyWebApp.TagHelpers
{
[HtmlTargetElement("tr", Attributes = "theme")]
public class CoordinatingTagHelpers : TagHelper
{
public string Theme { get; set; } = String.Empty;
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("class", $"bg-{context.Items["theme"]} text-white");
}
}
}
}
첫 번째 tag helper는 theme attribute를 가진 tr요소에서 동작합니다. 예제의 tag helper를 바꾸면 자체 요소를 변환시킬 수 있지만 이 예제에서는 theme attribute의 값을 Items dictionary에 추가하여 tr요소에 포함된 요소상에서 동작하는 tag helper를 사용할 수 있도록 하였습니다. 두 번째 tag helper는 th와 td요소상에서 동작하며 출력되는 요소에서 Bootstrap style을 설정하기 위해 Items dictionary로부터 theme값을 사용하고 있습니다.
아래 예제는 조정 tag helper를 적용하는 Home Controller의 Index view에 요소를 추가한 것입니다.
아래 예제에서는 tag helper에 의존하는 대신 변환된 th와 td요소를 추가하였음에 주목하시기 바랍니다. Tag Helper는 다른 tag helper에 의해 생성된 요소에서는 적용되지 않으며 단지 view에서 정의되는 요소에만 영향을 줍니다.
@model Products
@{
Layout = "_SimpleLayout";
}
<div route-data="true"></div>
<table class="table table-striped table-bordered table-sm">
<tablehead bg-color="dark">Product Summary</tablehead>
<tbody>
<tr theme="primary">
<th>Name</th>
<td>@Model?.ProductName</td>
</tr>
<tr theme="secondary">
<th>Price</th>
<td>@Model?.UnitPrice.Value.ToString("c")</td>
</tr>
<tr theme="info">
<th>Category</th>
<td>@Model?.CategoryId</td>
</tr>
</tbody>
</table>
Project를 실행하여 /home으로 URL을 요청하고 다음과 같은 결과를 확인합니다. theme요소의 값은 tag helper에서 다른 tag helper로 전달되었고 color theme는 변환되는 각 요소에서 attribute를 정의하는 것 없이 적용되었습니다.
(7) 요소 출력 제한하기
tag helper는 Process method에 인수로 전달된 TagHelperOutput개체상의 SuppressOuput method를 호출함으로 요소가 HTML응답에 포함되는 것을 방지하는데도 사용될 수 있습니다. 아래 예제는 view model의 Price속성이 지정된 값 이상을 초과하는 경우에만 표시되어야 하는 요소를 Home controller의 Index view에 추가한 것입니다.
@{
Layout = "_SimpleLayout";
}
<div show-when-gt="100" for="UnitPrice">
<h5 class="bg-danger text-white text-center p-2">
고가품목
</h5>
</div>
예제에서 show-when-gt attribute는 특정한 값을 지정하여 div요소가 해당 값을 초과하는 경우에만 표시될 수 있도록 하고자 하는 것이며 for 속성을 통해 해당 값을 확인할 model 속성을 지정하고 있습니다. 아래 예제는 응답을 포함하여 요소를 관리할 tag helper를 생성하는 것으로 project의 TagHelpers folder에 SelectiveTagHelper.cs이름의 file을 아래와 같이 추가합니다.
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace MyWebApp.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();
}
}
}
}
예제의 tag helper는 속성의 접근을 위해 model expression을 사용하며 해당 속성의 값이 기준치를 초과하는 경우에만 SuppressOutput method를 호출하도록 하고 있습니다. project를 실행하여 /home/index/1 URL을 호출하고 다음과 같은 결과를 확인합니다.
해당 URL에 의해 선택된 product의 UnitPrice속성값은 기준치보다 더 적으므로 요소는 결과에 포함되지 않습니다. 이제 다시 /home/index/29 URL을 요청해 보면 다음과 같은 결과를 볼 수 있는데
이것은 해당 product의 UnitPrice속성이 기준치를 훨씬 초과하기 때문입니다.
4. Tag Helper Component 사용하기
Tag helper component는 tag helper를 serivce로서 적용하는 대안을 제공합니다. 이 기능은 다른 service 혹은 middleware component에 지원하기 위해 tag helper를 설정할 할 때 유용하게 사용될 수 있는 것으로 일반적으로 blazor와 같이 client-side component와 server-side component를 모두 포함하는 진단도구 혹은 기능에 해당하는 것입니다.
(1) Tag Helper Component 생성
Tag helper component는 TagHelperComponent class로부터 상속받았으므로 이전에 사용했던 TagHelper 기반 class와 비슷한 비슷한 api를 제공합니다. tag helper class를 만들어 보기 위해 TimeTagHelperComponent.cs라는 이름의 file을 project의 TagHelpers folder에 아래와 같이 추가합니다.
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace MyWebApp.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("class", "bg-info text-white m-2 p-2");
elem.InnerHtml.Append($"Time: {timestamp}");
output.PreContent.AppendHtml(elem);
}
}
}
}
Tag helper component에서는 변환할 요소를 특정하지 않으며 Process method는 tag helper기능이 구성된 모든 요소에서 호출될 수 있습니다. 기본적으로 tag helper component는 head와 body요소를 변환하는데 적용되는 것입니다. 따라서 tag helper component class는 출력 요소의 TagName속성을 반드시 확인하여 의도한 변환만이 수행될 수 있도록 해야 합니다. 위 에제에서 tag helper component는 body요소를 찾고 timestamp를 포함하는 div요소를 요소의 나머지 content앞에 추가하기 위해 PreContent속성을 사용하고 있습니다.
Tag helper component는 ITagHelperComponent interface를 구현하는 service로서 아래와 같이 등록되도록 합니다.
builder.Services.AddSingleton<CitiesData>();
builder.Services.AddTransient<ITagHelperComponent, TimeTagHelperComponent>();
이때 AddTransient method는 각 요청이 tag helper component class의 자체 instance를 사용해 처리됨을 보장할 수 있게 합니다. project를 실행하여 /home URL을 요청하고 다음과 같은 결과를 확인합니다. Appliection으로부터의 해당 응답(내지는 다른 HTML응답 모두)은 tag helper component에 의해 생성된 content를 포함하고 있음을 알 수 있습니다.
(2) Tag Helper Component 요소 선택 확장
기본적으로 head와 body요소만이 tag helper component에 의해 처리되지만 TagHelperComponentTagHelper라는 꽤 긴 이름의 class로부터 파생된 class를 생성함으로써 추가적인 다른 요소를 선택할 수 있습니다. TableFooterTagHelperComponent.cs이름의 file을 project의 TagHelpers folder에 아래와 같이 추가합니다.
using Microsoft.AspNetCore.Mvc.Razor.TagHelpers;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace MyWebApp.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("class", "bg-dark text-white text-center");
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);
}
}
}
}
TableFooterSelector class는 TagHelperComponentTagHelper로부터 상속받고 있으며 application의 tag helper component에 의해 처리될 요소의 범위를 확장하는 HtmlTargetElement attribute가 적용되었습니다. 예제의 경우 attribute는 table요소를 지정하고 있습니다.
같은 file에서 정의된 TableFooterTagHelperComponent class는 table에 footer를 표시하는 tfoot. 요소를 추가함으로써 table요소를 변환하는 tag helper component입니다.
주의
새로운 TagHelperComponentTagHelper를 생성할 때는 모든 tag helper component가 HtmlTargetAttribute에 의해 선택된 요소를 전달받게 됨에 주의해야 합니다.
tag helper component는 변환할 요소를 전달받기 위해 service로서 등록되어야 하지만 tag helper component tag helper는 자동적으로 감지되고 등록됩니다. 아래 예제는 tag helper component service를 등록한 예를 나타내고 있습니다.
builder.Services.AddTransient<ITagHelperComponent, TimeTagHelperComponent>();
builder.Services.AddTransient<ITagHelperComponent, TableFooterTagHelperComponent>();
project를 실행하여 /home 또는 /cities URL을 요청하면 다음과 같이 각 table에서 footer가 표시되어 있음을 볼 수 있습니다.