:::: MENU ::::

Tuesday, July 19, 2016

Updated May 5 2016: Updated code to work with ASP.NET Core RC2
In a previous blog post we talked about how to create a simple tag helper in ASP.NET Core MVC. In today’s post we take this one step further and create a more complex tag helper that is made up of multiple parts.

A Tag Helper for Bootstrap Modal Dialogs

Creating a modal dialog in bootstrap requires some verbose html.
Bootstrap Modal
<div class="modal fade" tabindex="-1" role="dialog">
  <div class="modal-dialog">
    <div class="modal-content">
      <div class="modal-header">
        <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
        <h4 class="modal-title">Modal title</h4>
      </div>
      <div class="modal-body">
        <p>One fine body…</p>
      </div>
      <div class="modal-footer">
        <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
        <button type="button" class="btn btn-primary">Save changes</button>
      </div>
    </div>
  </div>
</div>
Using a tag helper here would help simplify the markup but this is a little more complicated than the Progress Bag example. In this case, we have HTML content that we want to add in 2 different places: the   element and the   element.
The solution here wasn’t immediately obvious. I had a chance to talk to Taylor Mullen at the MVP Summit ASP.NET Hackathon in November and he pointed me in the right direction. The solution is to use 3 different tag helpers that can communicate with each other through theTagHelperContext.
Ultimately, we want our tag helper markup to look like this:
Bootstrap Modal using a Tag Helper
<modal title="Modal title">
    <modal-body>
        <p>One fine body…</p>
    </modal-body>
    <modal-footer>
        <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
        <button type="button" class="btn btn-primary">Save changes</button>
    </modal-footer>
</modal>
This solution uses 3 tag helpers: modal, modal-body and modal-footer. The contents of the modal-body tag will be placed inside the  while the contents of the  tag will be placed inside the   element. The modal tag helper is the one that will coordinate all this.

Restricting Parents and Children

First things first, we want to make sure that  and  can only be placed inside the  tag and that the tag can only contain those 2 tags. To do this, we set the RestrictChildren attribute on the modal tag helper and the ParentTagproperty of the HtmlTargetElement attribute on the modal body and modal footer tag helpers:
[RestrictChildren("modal-body", "modal-footer")]
public class ModalTagHelper : TagHelper
{
     //...
}

[HtmlTargetElement("modal-body", ParentTag = "modal")]
public class ModalBodyTagHelper : TagHelper
{
    //...
}

[HtmlTargetElement("modal-footer", ParentTag = "modal")]
public class ModalFooterTagHelper : TagHelper
{
    //...
}
Now if we try to put any other tag in the  tag, Razor will give me a helpful error message.
Restrict children

Getting contents from the children

The next step is to create a context class that will be used to keep track of the contents of the 2 child tag helpers.
public class ModalContext
{
    public IHtmlContent Body { get; set; }
    public IHtmlContent Footer { get; set; }
}
At the beginning of the ProcessAsync method of the Modal tag helper, create a new instance of ModalContext and add it to the currentTagHelperContext:
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
    var modalContext = new ModalContext();
    context.Items.Add(typeof(ModalTagHelper), modalContext);
    //...
}
Now, in the modal body and modal footer tag helpers we will get the instance of that ModalContext via the TagHelperContext. Instead of rendering the output, these child tag helpers will set the the Body and Footer properties of the ModalContext.
[HtmlTargetElement("modal-body", ParentTag = "modal")]
public class ModalBodyTagHelper : TagHelper
{
    public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
    {
        var childContent = await output.GetChildContentAsync();
        var modalContext = (ModalContext)context.Items[typeof(ModalTagHelper)];
        modalContext.Body = childContent;
        output.SuppressOutput();
    }
}
Back in the modal tag helper, we call output.GetChildContentAsync() which will cause the child tag helpers to execute and set the properties on the ModalContext. After that, we just set the output as we normally would in a tag helper, placing the Body and Footer in the appropriate elements.
Modal tag helper
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
    var modalContext = new ModalContext();
    context.Items.Add(typeof(ModalTagHelper), modalContext);

    await output.GetChildContentAsync();

    var template =
$@"<div class='modal-dialog' role='document'>
<div class='modal-content'>
<div class='modal-header'>
<button type = 'button' class='close' data-dismiss='modal' aria-label='Close'><span aria-hidden='true'>×</span></button>
<h4 class='modal-title' id='{context.UniqueId}Label'>{Title}</h4>
</div>
<div class='modal-body'>";

    output.TagName = "div";
    output.Attributes["role"] = "dialog";
    output.Attributes["id"] = Id;
    output.Attributes["aria-labelledby"] = $"{context.UniqueId}Label";
    output.Attributes["tabindex"] = "-1";
    var classNames = "modal fade";
    if (output.Attributes.ContainsName("class"))
    {
        classNames = string.Format("{0} {1}", output.Attributes["class"].Value, classNames);
    }
    output.Attributes.SetAttribute("class", classNames);
    output.Content.AppendHtml(template);
    if (modalContext.Body != null)
    {
        output.Content.AppendHtml(modalContext.Body); //Setting the body contents
    }
    output.Content.AppendHtml("</div>");
    if (modalContext.Footer != null)
    {
        output.Content.AppendHtml("<div class='modal-footer'>");
        output.Content.AppendHtml(modalContext.Footer); //Setting the footer contents
        output.Content.AppendHtml("</div>");
    }
    
    output.Content.AppendHtml("</div></div>");
}

Conclusion

Composing complex tag helpers with parent / child relationships is fairly straight forward. In my opinion, the approach here is much easier to understand than the “multiple transclusion” approach used to solve the same problem in Angular 1. It would be easy to unit test and as always, Visual Studio provides error messages directly in the HTML editor to guide anyone who is using your tag helper.
You can check out the full source code on the Tag Helper Samples repo.
In a previous post we explored the new View Component feature of ASP.NET Core MVC. In today’s post we take a look at how view components can be implemented in a separate class library and shared across multiple web applications.

Creating a class library

First, add a a new .NET Core class library to your solution.
Add class library
This is the class library where we will add our view components but before we can do that we have to add a reference to the MVC and Razor bits.
"dependencies": {
    "NETStandard.Library": "1.6.0",
    "Microsoft.AspNetCore.Mvc": "1.0.0",
    "Microsoft.AspNetCore.Razor.Tools": {
        "version": "1.0.0-preview2-final",
        "type": "build"
    }
},
"tools": {
    "Microsoft.AspNetCore.Razor.Tools": "1.0.0-preview2-final"
}
Now we can add a view component class to the project. I created a simple example view component called SimpleViewComponent.
[ViewComponent(Name = "ViewComponentLibrary.Simple")]
public class SimpleViewComponent : ViewComponent
{
    public IViewComponentResult Invoke(int number)
    {
        return View(number + 1);
    }
}
By convention, MVC would have assigned the name Simple to this view component. This view component is implemented in a class library with the intention of using it across multiple web apps which opens up the possibility of naming conflicts with other view components. To avoid naming conflicts, I overrode the name using the [ViewComponent] attribute and prefixed the name with the name of my class library.
Next, I added a Default.cshtml view to the ViewComponentLibrary in the Views\Shared\Components\Simple folder.
@model Int32

Hello from an external View Component!

Your number is @Model


For this view to be recognized by the web application, we need to include the cshtml files as embedded resources in the class library. Currently, this is done by adding the following setting to the project.json file.
"buildOptions": {
    "embed": "Views/**/*.cshtml"
}

Referencing external view components

The first step in using the external view components in our web application project is to add a reference to the class library. Once the reference is added, we need tell the Razor view engine that views are stored as resources in the external view library. We can do this by adding some additional configuration code to the ConfigureServices method in Startup.cs. The additional code creates a newEmbeddedFileProvider for the class library then adds that file provider to the RazorViewEngineOptions.
public void ConfigureServices(IServiceCollection services)
{
    // Add framework services.
    services.AddApplicationInsightsTelemetry(Configuration);

    services.AddMvc();

    //Get a reference to the assembly that contains the view components
    var assembly = typeof(ViewComponentLibrary.ViewComponents.SimpleViewComponent).GetTypeInfo().Assembly;

    //Create an EmbeddedFileProvider for that assembly
    var embeddedFileProvider = new EmbeddedFileProvider(
        assembly,
        "ViewComponentLibrary"
    );

    //Add the file provider to the Razor view engine
    services.Configure(options =>
    {                
        options.FileProviders.Add(embeddedFileProvider);
    });
}
Now everything is wired up and we can invoke the view component just like we would for any other view component in our ASP.NET Core MVC application.
class
="row">

@await Component.InvokeAsync("ViewComponentLibrary.Simple", new { number = 5 })

Wrapping it up

Storing view components in a separate assembly allows them to be shared across multiple projects. It also opens up the possibility of creating a simple plugin architecture for your application. We will explore the plugin idea in more detail in a future post.
You can take a look at the full source code on GitHub.
More