ASP.NET Core – TagHelper


前言

以前写的 , 这篇是整理版.

参考

Docs – Author Tag Helpers in ASP.NET Core

Creating a Custom Tag Helper in ASP.NET Core: Gathering Data

介绍

TagHelper 有点像 Angular 的指令 directive, 绝大部分情况下都是用来装修 element 的. 比如 add class.

下面是 ASP.NET Core build-in 的 tag, 应该可以感觉出来它都用在什么地方了. 我用它来实现 router link active 的功能.

Create Tag Helper

[HtmlTargetElement(Attributes = "[routerLinkActive]")]
public class RouterLinkActiveTagHelper : TagHelper
{
    public override void Process(TagHelperContext context, TagHelperOutput output)
    {
        base.Process(context, output);
    }
}

创建一个 RouterLinkActiveTagHelper class 继承 TagHelper

HtmlTargetElement 负责定义 selector, 和 Angular 一样它需要一个 selector 做匹配

Process 负责对 element 做任何修改, 比如 add class, innerHtml, appendHtml 等等.

还有一种 selector 是 for tag 的. 比较少用到, 也是和 Angular 一样的概念, directive 通常是匹配 attribute 但其实是可以匹配 element tag 的 (有些 Angular 玩家都不知道呢).

[HtmlTargetElement("my-email", TagStructure = TagStructure.NormalOrSelfClosing)]

要使用前需要添加 view import

第一个参数是具体的 namespace + class name, * 代表匹配全部

第二个参数是 Assembly 的名字

@addTagHelper TestWeb.TagHelpers.*, TestWeb

表示, TestWeb Assembly 里面, TestWeb.TagHelpers namespace 下的所有 class

然后就可以使用了

<body>
  <a routerLinkActive href="/contact">Contacta>
body>

data-* 和 asp-* 是不合格的 selector

asp- 是 ASP.NET Core 保留和专用的 selector, 所以我们用不到.

data- 不能用是因为

所以切记, 不要用 asp- 和 data- 作为 selector

@Input()

@input 原至 Angular, 就是在使用指令的时候, 传入一些变量作为操控.

<a routerLinkActive company-name="Abc" company-age="15" href="/contact">Contacta>

传入 company name 和 age

通过 property 接收

[HtmlAttributeName("company-name")]
public string CompanyName { get; set; } = "";

public int? CompanyAge { get; set; }

它默认是 kebab-case map to PascalCase, 如果想取别名就使用 HtmlAttributeName 声明就可以了.

有自动做类型转换. empty string != null 哦, null 指的是完全没有放 attribute.

Process

public override void Process(TagHelperContext context, TagHelperOutput output)
{
    if (output.Attributes.TryGetAttribute("class", out var attribute))
    {
        var value = attribute.Value; // readonly
    }
    output.AddClass("active", HtmlEncoder.Default);
    output.Attributes.Add("data-attribute", "value");
    output.Content.SetHtmlContent("

Email1

"); output.Content.AppendHtml("

Email2

"); base.Process(context, output); }

常见的操作有, add class, append html, inner html, get attribute 等等.

Read ViewContext

ViewContext 包含了常用到的 ViewBag, ViewData, RouteData, HttpContext 等等.

[ViewContext]
[HtmlAttributeNotBound]
public ViewContext ViewContext { get; set; } = null!;

通过 ViewContent Attribute 声明就可以了.

Parent Child 沟通

参考: The Very Basics of Nesting for Tag Helpers

这个会比较复杂一些. 

[HtmlTargetElement("email", TagStructure = TagStructure.NormalOrSelfClosing)]
public class EmailTagHelper : TagHelper
{
    public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
    {
        context.Items.Add(typeof(EmailContext), new EmailContext());
        var childContent = await output.GetChildContentAsync();
        var emailChildren = ((EmailContext)context.Items[typeof(EmailContext)]).EmailChildren;
        var contentString = childContent.GetContent(); // get content string
    }
}

[HtmlTargetElement("email-child", TagStructure = TagStructure.NormalOrSelfClosing)]
public class EmailChildTagHelper : TagHelper
{
    public override void Process(TagHelperContext context, TagHelperOutput output)
    {
        var emailContext = (EmailContext)context.Items[typeof(EmailContext)];
        emailContext.EmailChildren.Add(this);
    }
}

parent 通过 Context.Items 传入一个容器 (也可以传入子层需要的资料), child 把资料放入容器中. 

parent await GetChildContentAsync, 等待子层完成后, 再打开容器, 把子层放进去的资料拿出来. 

这样就可以 parent child 沟通了, 看上去容器是多余的, 子层为什么不能也使用 context.Items.Add 的方式回传给 parent 呢? 

因为它是一个 copy... 我也不知道为什么它要这样设计.

RouterLinkActive

用法

当前 URL 和当前 element 的 href 或者子孙 element 的 href 吻合的话, routerLinkActive 就要 add class "active".

[HtmlTargetElement(Attributes = "[routerLinkActive]")]
public class RouterLinkActiveTagHelper : TagHelper
{
    [ViewContext]
    [HtmlAttributeNotBound]
    public ViewContext ViewContext { get; set; } = null!;

    public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
    {
        var selfAndDescendantLinks = new List<string>();

        var selfLink = GetSelfLinkFromHref();
        if (selfLink != null)
        {
            selfAndDescendantLinks.Add(selfLink);
        }

        var descendantLinks = await GetDescendantLinksAsync();
        selfAndDescendantLinks.AddRange(descendantLinks);

        // note 解忧: 
        // link 是 encode 的, 而 Request.Path 是 decode 的, 所以需要加 .ToString 让它 encode, 这样才能 match with link
        var requestPathWithQuery = ViewContext.HttpContext.Request.Path.ToString() + ViewContext.HttpContext.Request.QueryString.ToString();
        if (selfAndDescendantLinks.Contains(requestPathWithQuery))
        {
            output.AddClass("active", HtmlEncoder.Default);
        }

        await base.ProcessAsync(context, output);

        string? GetSelfLinkFromHref()
        {
            output.Attributes.TryGetAttribute("href", out var hrefAttribute);
            return hrefAttribute?.Value.ToString();
        }
        async Taskstring>> GetDescendantLinksAsync()
        {
            var childHtml = output.Content.IsModified ? output.Content.GetContent() : (await output.GetChildContentAsync()).GetContent();
            var regex = new Regex(@"");
            var matchedHrefs = regex.Matches(childHtml);
            return matchedHrefs.ToArray().Select(matchedHref => matchedHref.Groups[1].Value).ToList();
        }
    }
}

找出 href 的 link, 然后和当前 URL 做匹配, 匹配到的话就添加 class active.

相关