- Razor页面
- 通用
- 客户端开发
- 标记帮助程序
- 高级
在 ASP.NET Core 中创作标记帮助程序
标记帮助程序入门
本教程介绍标记帮助程序编程。 标记帮助程序简介描述了标记帮助程序提供的优势。
标记帮助程序是实现 ITagHelper
接口的任何类。 但是,在创作标记帮助程序时,通常从 TagHelper
派生,这样可以访问 Process
方法。
创建一个名为 AuthoringTagHelpers 的新 ASP.NET Core 项目 。 此项目不需要身份验证。
创建一个名为“TagHelpers”的文件夹来保存标记帮助程序 。 “TagHelpers”文件夹不是必需的,但它是合理的约定 。 现在让我们开始编写一些简单的标记帮助程序。
最小的标记帮助程序
在本部分中,你将编写一个更新电子邮件标记的标记帮助程序。 例如:
<email>Support</email>
服务器将使用电子邮件标记帮助程序将该标记转换为以下内容:
<a href="mailto:Support@contoso.com">Support@contoso.com</a>
即,使其成为电子邮件链接的定位标记。 如果你正在编写博客引擎,并且需要它将营销、支持和其他联系人的电子邮件全部发送到同一个域,则可能需要执行此操作。
将以下
EmailTagHelper
类添加到“TagHelpers”文件夹 。using Microsoft.AspNetCore.Razor.TagHelpers; using System.Threading.Tasks; namespace AuthoringTagHelpers.TagHelpers { public class EmailTagHelper : TagHelper { public override void Process(TagHelperContext context, TagHelperOutput output) { output.TagName = "a"; // Replaces <email> with <a> tag } } }
标记帮助程序使用面向根类名称的元素的命名约定(减去类名称的 TagHelper 部分) 。 在此示例中,EmailTagHelper 的根名称是 email,因此
<email>
标记将作为目标名称 。 此命名约定应适用于大多数标记帮助程序,稍后将介绍如何重写它。EmailTagHelper
类派生自TagHelper
。TagHelper
类提供编写标记帮助程序的方法和属性。重写的
Process
方法控制标记帮助程序在执行时的操作。TagHelper
类还提供具有相同参数的异步版本 (ProcessAsync
)。Process
(和ProcessAsync
)的上下文参数包含与执行当前 HTML 标记相关的信息。Process
(和ProcessAsync
)的输出参数包含监控状态的 HTML 元素,它代表用于生成 HTML 标记和内容的原始源。类名称的后缀是 TagHelper,这不是必需的,但被认为是最佳做法约定 。 可将类声明为:
public class Email : TagHelper
要使
EmailTagHelper
类可用于所有 Razor 视图,请将addTagHelper
指令添加到 Views/_ViewImports.cshtml 文件:@using AuthoringTagHelpers @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers @addTagHelper *, AuthoringTagHelpers
上面的代码使用通配符语法来指定程序集中的所有标记帮助程序都将可用。
@addTagHelper
之后的第一个字符串指定要加载的标记帮助程序(对所有标记帮助程序使用“*”),第二个字符串“AuthoringTagHelpers”指定标记帮助程序所在的程序集。 另请注意,第二行使用通配符语法引入了 ASP.NET Core MVC 标记帮助程序(标记帮助程序简介中讨论了这些帮助程序。)要使标记帮助程序可用于 Razor 视图,请使用@addTagHelper
指令。 或者,也可以提供标记帮助程序的完全限定的名称 (FQN),如下所示:
@using AuthoringTagHelpers @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers @addTagHelper AuthoringTagHelpers.TagHelpers.EmailTagHelper, AuthoringTagHelpers
若要使用 FQN 将标记帮助程序添加到视图中,请依次添加 FQN (AuthoringTagHelpers.TagHelpers.EmailTagHelper
) 和程序集名称 (AuthoringTagHelpers ,不一定是 namespace
)。 大多数开发者更喜欢使用通配符语法。 标记帮助程序简介详细介绍了标记帮助程序的添加和删除方法,以及层次结构和通配符语法。
使用以下更改更新 Views/Home/Contact.cshtml 文件中的标记 :
@{ ViewData["Title"] = "Contact"; } <h2>@ViewData["Title"].</h2> <h3>@ViewData["Message"]</h3> <address> One Microsoft Way<br /> Redmond, WA 98052<br /> <abbr title="Phone">P:</abbr> 425.555.0100 </address> <address> <strong>Support:</strong><email>Support</email><br /> <strong>Marketing:</strong><email>Marketing</email> </address>
运行应用并使用你喜爱的浏览器来查看 HTML 源,以便验证电子邮件标记是否替换为定位标记(例如,
<a>Support</a>
)。 Support 和 Marketing 呈现为链接,但它们不具备使其正常工作的href
属性 。 此问题将在下一部分得以解决。
SetAttribute 和 SetContent
在本部分中,我们将更新 EmailTagHelper
,使其能够为电子邮件创建有效的定位标记。 我们将对其进行更新以获取 Razor 视图中的信息(采用 mail-to
属性的形式)并使用该信息来生成定位点。
使用以下内容更新 EmailTagHelper
类:
public class EmailTagHelper : TagHelper { private const string EmailDomain = "contoso.com"; // Can be passed via <email mail-to="..." />. // PascalCase gets translated into kebab-case. public string MailTo { get; set; } public override void Process(TagHelperContext context, TagHelperOutput output) { output.TagName = "a"; // Replaces <email> with <a> tag var address = MailTo + "@" + EmailDomain; output.Attributes.SetAttribute("href", "mailto:" + address); output.Content.SetContent(address); } }
标记帮助程序采用 Pascal 大小写格式的类和属性名将转换为各自相应的短横线格式。 因此,要使用
MailTo
属性,请使用<email mail-to="value"/>
等效项。最后一行为最小功能标记帮助程序设置已完成的内容。
突出显示的行显示了添加属性的语法:
public override void Process(TagHelperContext context, TagHelperOutput output) { output.TagName = "a"; // Replaces <email> with <a> tag var address = MailTo + "@" + EmailDomain; output.Attributes.SetAttribute("href", "mailto:" + address); output.Content.SetContent(address); }
只要属性集合中当前不存在“href”属性,该方法就适用于此属性。 也可使用 output.Attributes.Add
方法将标记帮助程序属性添加到标记属性集合的末尾。
使用以下更改更新 Views/Home/Contact.cshtml 文件中的标记 :
@{ ViewData["Title"] = "Contact Copy"; } <h2>@ViewData["Title"].</h2> <h3>@ViewData["Message"]</h3> <address> One Microsoft Way Copy Version <br /> Redmond, WA 98052-6399<br /> <abbr title="Phone">P:</abbr> 425.555.0100 </address> <address> <strong>Support:</strong><email mail-to="Support"></email><br /> <strong>Marketing:</strong><email mail-to="Marketing"></email> </address>
运行应用并验证它是否生成正确的链接。
备注
如果打算编写电子邮件标记自结束 (<email mail-to="Rick" />
),最终输出也将为自结束。 要启用只使用开始标记 (<email mail-to="Rick">
) 来编写标记的功能,必须用以下内容来标记类:
[HtmlTargetElement("email", TagStructure = TagStructure.WithoutEndTag)] public class EmailVoidTagHelper : TagHelper { private const string EmailDomain = "contoso.com"; // Code removed for brevity
鉴于自结束电子邮件标记帮助程序,输出将为 <a href="mailto:Rick@contoso.com" />
。 自结束定位标记不是有效的 HTML,因此你不想创建这样的标记,但你可能想要创建一个自结束的标记帮助程序。 标记帮助程序在读取标记后设置 TagMode
属性的类型。
ProcessAsync
在本部分中,我们将编写异步电子邮件帮助程序。
将
EmailTagHelper
类替换为以下代码:public class EmailTagHelper : TagHelper { private const string EmailDomain = "contoso.com"; public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) { output.TagName = "a"; // Replaces <email> with <a> tag var content = await output.GetChildContentAsync(); var target = content.GetContent() + "@" + EmailDomain; output.Attributes.SetAttribute("href", "mailto:" + target); output.Content.SetContent(target); } }
注意:
此版本使用异步
ProcessAsync
方法。 异步GetChildContentAsync
返回包含TagHelperContent
的Task
。使用
output
参数获取 HTML 元素的内容。
对 Views/Home/Contact.cshtml 文件进行以下更改,以便标记帮助程序能够获取目标电子邮件 。
@{ ViewData["Title"] = "Contact"; } <h2>@ViewData["Title"].</h2> <h3>@ViewData["Message"]</h3> <address> One Microsoft Way<br /> Redmond, WA 98052<br /> <abbr title="Phone">P:</abbr> 425.555.0100 </address> <address> <strong>Support:</strong><email>Support</email><br /> <strong>Marketing:</strong><email>Marketing</email> </address>
运行应用并验证它是否生成有效的电子邮件链接。
RemoveAll、PreContent.SetHtmlContent 和 PostContent.SetHtmlContent
将以下
BoldTagHelper
类添加到“TagHelpers”文件夹 。using Microsoft.AspNetCore.Razor.TagHelpers; namespace AuthoringTagHelpers.TagHelpers { [HtmlTargetElement(Attributes = "bold")] public class BoldTagHelper : TagHelper { public override void Process(TagHelperContext context, TagHelperOutput output) { output.Attributes.RemoveAll("bold"); output.PreContent.SetHtmlContent("<strong>"); output.PostContent.SetHtmlContent("</strong>"); } } }
[HtmlTargetElement]
属性传递一个属性参数,该参数指定包含名为“bold”的 HTML 属性的任何 HTML 元素都将匹配,并且该类中的Process
重写方法将会运行。 在此示例中,Process
方法删除了“bold”属性,并用<strong></strong>
围住了包含的标记。因为你不想替换现有的标记内容,所以必须用
PreContent.SetHtmlContent
方法编写开头的<strong>
标记,并用PostContent.SetHtmlContent
方法编写结尾的</strong>
标记。
修改 About.cshtml 视图,以包含
bold
属性值 。 完成的代码如下所示。@{ ViewData["Title"] = "About"; } <h2>@ViewData["Title"].</h2> <h3>@ViewData["Message"]</h3> <p bold>Use this area to provide additional information.</p> <bold> Is this bold?</bold>
运行应用。 可以使用你喜爱的浏览器来检查源并验证标记。
上面的
[HtmlTargetElement]
属性仅针对提供属性名称“bold”的 HTML 标记。 标记帮助程序未修改<bold>
元素。标注出
[HtmlTargetElement]
属性行,它将默认为目标<bold>
标记,也就是<bold>
格式的 HTML 标记。 请记住,默认的命名约定会将类名称 BoldTagHelper 与<bold>
标记相匹配 。运行应用并验证
<bold>
标记是否由标记帮助程序进行处理。
用多个 [HtmlTargetElement]
属性修饰类会导致目标出现逻辑 OR。 例如,使用下面的代码时,系统将匹配出 bold 标记或 bold 属性。
[HtmlTargetElement("bold")] [HtmlTargetElement(Attributes = "bold")] public class BoldTagHelper : TagHelper { public override void Process(TagHelperContext context, TagHelperOutput output) { output.Attributes.RemoveAll("bold"); output.PreContent.SetHtmlContent("<strong>"); output.PostContent.SetHtmlContent("</strong>"); } }
将多个属性添加到同一语句时,运行时会将其视为逻辑 AND。 例如,在下面的代码中,HTML 元素必须命名为“bold”并具有名为“bold”的属性 (<bold bold />
) 才能匹配。
[HtmlTargetElement("bold", Attributes = "bold")]
也可使用 [HtmlTargetElement]
更改目标元素的名称。 例如,如果你希望 BoldTagHelper
以 <MyBold>
标记为目标,则可使用以下属性:
[HtmlTargetElement("MyBold")]
将模型传递到标记帮助程序
添加“Models”文件夹 。
将以下
WebsiteContext
类添加到“模型”文件夹 :using System; namespace AuthoringTagHelpers.Models { public class WebsiteContext { public Version Version { get; set; } public int CopyrightYear { get; set; } public bool Approved { get; set; } public int TagsToShow { get; set; } } }
将以下
WebsiteInformationTagHelper
类添加到“TagHelpers”文件夹 。using System; using AuthoringTagHelpers.Models; using Microsoft.AspNetCore.Razor.TagHelpers; namespace AuthoringTagHelpers.TagHelpers { public class WebsiteInformationTagHelper : TagHelper { public WebsiteContext Info { get; set; } public override void Process(TagHelperContext context, TagHelperOutput output) { output.TagName = "section"; output.Content.SetHtmlContent( $@"<ul><li><strong>Version:</strong> {Info.Version}</li> <li><strong>Copyright Year:</strong> {Info.CopyrightYear}</li> <li><strong>Approved:</strong> {Info.Approved}</li> <li><strong>Number of tags to show:</strong> {Info.TagsToShow}</li></ul>"); output.TagMode = TagMode.StartTagAndEndTag; } } }
如前所述,标记帮助程序会将标记帮助程序采用 Pascal 大小写格式的 C# 类名和属性转换为短横线格式。 因此,要在 Razor 中使用
WebsiteInformationTagHelper
,请编写<website-information />
。未显式标识具有
[HtmlTargetElement]
属性的目标元素,因此website-information
的默认值将成为目标元素。 如果应用了以下属性(请注意,它虽不是短横线格式,但却与类名相匹配):
[HtmlTargetElement("WebsiteInformation")]
短横线格式标记
<website-information />
不匹配。 若要使用[HtmlTargetElement]
属性,请使用短横线格式,如下所示:[HtmlTargetElement("Website-Information")]
自结束的元素没有任何内容。 在此示例中,Razor 标记将使用自结束标记,但标记帮助程序将创建 section 元素(这不是自结束元素,并且你将在
section
元素中编写内容)。 因此,需要将TagMode
设置为StartTagAndEndTag
以写入输出。 或者,可以标注出行设置TagMode
并用结束标记编写标记。 (本教程后面将提供示例标记。)下一行中的
$
(美元符号)使用内插字符串:
$@"<ul><li><strong>Version:</strong> {Info.Version}</li>
将以下标记添加到 About.cshtml 视图 。 突出显示的标记显示 Web 站点信息。
@using AuthoringTagHelpers.Models @{ ViewData["Title"] = "About"; WebsiteContext webContext = new WebsiteContext { Version = new Version(1, 3), CopyrightYear = 1638, Approved = true, TagsToShow = 131 }; } <h2>@ViewData["Title"].</h2> <h3>@ViewData["Message"]</h3> <p bold>Use this area to provide additional information.</p> <bold> Is this bold?</bold> <h3> web site info </h3> <website-information info="webContext" />
备注
在 Razor 中,标记如下所示:
<website-information info="webContext" />
Razor 知道
info
属性是一个类,而不是字符串,并且你想要编写 C# 代码。 编写任何非字符串标记帮助程序属性时,都不应使用@
字符。运行应用,并导航到“关于”视图查看 Web 站点信息。
备注
可使用带有结束标记的以下标记,并在标记帮助程序中删除带有
TagMode.StartTagAndEndTag
的行:<website-information info="webContext" > </website-information>
条件标记帮助程序
条件标记帮助程序在传递 true 值时呈现输出。
将以下
ConditionTagHelper
类添加到“TagHelpers”文件夹 。using Microsoft.AspNetCore.Razor.TagHelpers; namespace AuthoringTagHelpers.TagHelpers { [HtmlTargetElement(Attributes = nameof(Condition))] public class ConditionTagHelper : TagHelper { public bool Condition { get; set; } public override void Process(TagHelperContext context, TagHelperOutput output) { if (!Condition) { output.SuppressOutput(); } } } }
将 Views/Home/Index.cshtml 文件的内容 替换为以下标记:
@using AuthoringTagHelpers.Models @model WebsiteContext @{ ViewData["Title"] = "Home Page"; } <div> <h3>Information about our website (outdated):</h3> <Website-InforMation info="Model" /> <div condition="Model.Approved"> <p> This website has <strong surround="em">@Model.Approved</strong> been approved yet. Visit www.contoso.com for more information. </p> </div> </div>
将
Home
控制器中的Index
方法替换为以下代码:public IActionResult Index(bool approved = false) { return View(new WebsiteContext { Approved = approved, CopyrightYear = 2015, Version = new Version(1, 3, 3, 7), TagsToShow = 20 }); }
运行应用并浏览到主页。 条件
div
中的标记不会呈现。 将查询字符串?approved=true
追加到 URL(例如,http://localhost:1235/Home/Index?approved=true
)。approved
设置为 true,并将显示条件标记。
备注
使用 nameof 运算符将属性指定为目标,而不是像使用 bold 标记帮助程序那样指定字符串:
[HtmlTargetElement(Attributes = nameof(Condition))] // [HtmlTargetElement(Attributes = "condition")] public class ConditionTagHelper : TagHelper { public bool Condition { get; set; } public override void Process(TagHelperContext context, TagHelperOutput output) { if (!Condition) { output.SuppressOutput(); } } }
如果代码被重构,nameof 运算符将保护它(可能需要将名称更改为 RedCondition
)。
避免标记帮助程序冲突
在本部分中,你将编写一对自动链接标记帮助程序。 第一个标记帮助程序会将包含以 HTTP 开头的 URL 的标记替换为包含相同 URL 的 HTML 定位标记(从而产生指向 URL 的链接)。 第二个标记帮助程序也会对以 WWW 开头的 URL 执行相同的操作。
由于这两个帮助程序密切相关,并且你将来可能会重构它们,因此可将其保存在同一文件中。
将以下
AutoLinkerHttpTagHelper
类添加到“TagHelpers”文件夹 。[HtmlTargetElement("p")] public class AutoLinkerHttpTagHelper : TagHelper { public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) { var childContent = await output.GetChildContentAsync(); // Find Urls in the content and replace them with their anchor tag equivalent. output.Content.SetHtmlContent(Regex.Replace( childContent.GetContent(), @"\b(?:https?://)(\S+)\b", "<a target=\"_blank\" href=\"$0\">$0</a>")); // http link version} } }
备注
AutoLinkerHttpTagHelper
类以p
元 素为目标,并使用正则表达式来创建定位点。将以下标记添加到 Views/Home/Contact.cshtml 文件的末尾 :
@{ ViewData["Title"] = "Contact"; } <h2>@ViewData["Title"].</h2> <h3>@ViewData["Message"]</h3> <address> One Microsoft Way<br /> Redmond, WA 98052<br /> <abbr title="Phone">P:</abbr> 425.555.0100 </address> <address> <strong>Support:</strong><email>Support</email><br /> <strong>Marketing:</strong><email>Marketing</email> </address> <p>Visit us at http://docs.asp.net or at www.microsoft.com</p>
运行应用并验证标记帮助程序是否正确呈现定位点。
更新
AutoLinker
类以包含AutoLinkerWwwTagHelper
,这会将 www 文本转换为还包含原始 www 文本的定位标记。 更新后的代码在下方突出显示:[HtmlTargetElement("p")] public class AutoLinkerHttpTagHelper : TagHelper { public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) { var childContent = await output.GetChildContentAsync(); // Find Urls in the content and replace them with their anchor tag equivalent. output.Content.SetHtmlContent(Regex.Replace( childContent.GetContent(), @"\b(?:https?://)(\S+)\b", "<a target=\"_blank\" href=\"$0\">$0</a>")); // http link version} } } [HtmlTargetElement("p")] public class AutoLinkerWwwTagHelper : TagHelper { public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) { var childContent = await output.GetChildContentAsync(); // Find Urls in the content and replace them with their anchor tag equivalent. output.Content.SetHtmlContent(Regex.Replace( childContent.GetContent(), @"\b(www\.)(\S+)\b", "<a target=\"_blank\" href=\"http://$0\">$0</a>")); // www version } } }
运行应用。 请注意 www 文本呈现为链接,但 HTTP 文本不是。 如果将中断点放在这两个类中,可以看到 HTTP 标记帮助程序类首先运行。 问题是,标记帮助程序输出已缓存,当运行 WWW 标记帮助程序时,它会覆盖 HTTP 标记帮助程序的缓存输出。 本教程稍后将介绍如何控制标记帮助程序中的运行顺序。 我们将用以下方法修复代码:
public class AutoLinkerHttpTagHelper : TagHelper { public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) { var childContent = output.Content.IsModified ? output.Content.GetContent() : (await output.GetChildContentAsync()).GetContent(); // Find Urls in the content and replace them with their anchor tag equivalent. output.Content.SetHtmlContent(Regex.Replace( childContent, @"\b(?:https?://)(\S+)\b", "<a target=\"_blank\" href=\"$0\">$0</a>")); // http link version} } } [HtmlTargetElement("p")] public class AutoLinkerWwwTagHelper : TagHelper { public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) { var childContent = output.Content.IsModified ? output.Content.GetContent() : (await output.GetChildContentAsync()).GetContent(); // Find Urls in the content and replace them with their anchor tag equivalent. output.Content.SetHtmlContent(Regex.Replace( childContent, @"\b(www\.)(\S+)\b", "<a target=\"_blank\" href=\"http://$0\">$0</a>")); // www version } }
备注
在自动链接标记帮助程序的第一版中,使用以下代码获取了目标的内容:
var childContent = await output.GetChildContentAsync();
也就是说,使用传递到
ProcessAsync
方法的TagHelperOutput
调用GetChildContentAsync
。 如前所述,由于输出已缓存,因此将调用最后运行的标记帮助程序。 使用以下代码解决了这个问题:var childContent = output.Content.IsModified ? output.Content.GetContent() : (await output.GetChildContentAsync()).GetContent();
上面的代码检查内容是否已修改,如果已修改,则从输出缓冲区获取内容。
运行应用并验证这两个链接是否按预期工作。 虽然它可能显示自动链接器标记帮助程序是正确且完整的,但它有一个细微的问题。 如果首先运行 WWW 标记帮助程序,则 www 链接不正确。 通过添加
Order
重载更新代码来控制标记运行的顺序。Order
属性确定相对于面向同一元素的其他标记帮助程序的执行顺序。 默认顺序值为零,并首先执行具有较低值的实例。public class AutoLinkerHttpTagHelper : TagHelper { // This filter must run before the AutoLinkerWwwTagHelper as it searches and replaces http and // the AutoLinkerWwwTagHelper adds http to the markup. public override int Order { get { return int.MinValue; } }
上面的代码可以保证 HTTP 标记帮助程序在 WWW 标记帮助程序之前运行。 将
Order
更改为MaxValue
并验证为 WWW 标记生成的标记是否不正确。
检查和检索子内容
标记帮助程序提供多个属性来检索内容。
- 可将
GetChildContentAsync
的结果追加到output.Content
。 - 可使用
GetContent
检查GetChildContentAsync
的结果。 - 如果修改
output.Content
,则不会执行或呈现 TagHelper 主体,除非像自动链接器示例中那样调用GetChildContentAsync
:
public class AutoLinkerHttpTagHelper : TagHelper { public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) { var childContent = output.Content.IsModified ? output.Content.GetContent() : (await output.GetChildContentAsync()).GetContent(); // Find Urls in the content and replace them with their anchor tag equivalent. output.Content.SetHtmlContent(Regex.Replace( childContent, @"\b(?:https?://)(\S+)\b", "<a target=\"_blank\" href=\"$0\">$0</a>")); // http link version} } }
- 对
GetChildContentAsync
的多次调用返回相同的值,且不重新执行TagHelper
主体,除非传入一个指示不使用缓存结果的 false 参数。
加载缩小的分部视图 TagHelper
在生产环境中,可以通过加载缩小的分部视图来提升性能。 若要在生产环境中利用缩小的分部视图,请执行以下操作:
- 创建/设置缩小分部视图的预生成过程。
- 使用以下代码在非开发环境中加载缩小的分部视图。
public class MinifiedVersionPartialTagHelper : PartialTagHelper { public MinifiedVersionPartialTagHelper(ICompositeViewEngine viewEngine, IViewBufferScope viewBufferScope) : base(viewEngine, viewBufferScope) { } public override Task ProcessAsync(TagHelperContext context, TagHelperOutput output) { // Append ".min" to load the minified partial view. if (!IsDevelopment()) { Name += ".min"; } return base.ProcessAsync(context, output); } private bool IsDevelopment() { return Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == EnvironmentName.Development; } }