11
votes

I have been following this Microsoft article on writing custom Tag Helpers here.

Every where i see code where the element markup is hard coded in C#

Example (taken from the above link)

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;
}

Instead of doing this, is there a way i can load the markup template from a cshtml file? (something similar to loading Partial Views)

My intention is to have individual cshtml files (one for each element type), so that i can style them easily. Also my C# would look clean!

Thanks,

James

3
in mvc we can call on helper object like helper.Partial but not sure about asp.net coreEhsan Sajjad
Depending on your needs, View components could be an option.juunas

3 Answers

6
votes

You can create a partial view, and call it from your TagHelper class. For example:

<!-- Views/Shared/TagHelpers/MyList.cshtml -->
@model YourInfoClass
<ul>
    <li><strong>Version:</strong> @Model.Version</li>
    <li><strong>Copyright Year:</strong> @Model.CopyrightYear</li>
    <li><strong>Approved:</strong> @Model.Approved</li>
    <li><strong>Number of tags to show:</strong> @Model.TagsToShow</li>
</ul>

In your TagHelper:

[HtmlTargetElement("mylist")]
public class MyListTagHelper : TagHelper
{
    private HtmlHelper _htmlHelper;
    private HtmlEncoder _htmlEncoder;

    public MyListTagHelper(IHtmlHelper htmlHelper, HtmlEncoder htmlEncoder)
    {
        _htmlHelper = htmlHelper as HtmlHelper;
        _htmlEncoder = htmlEncoder;
    }

    public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
    {
        output.TagName = "section";
        output.TagMode = TagMode.StartTagAndEndTag;

        var partial = await _htmlHelper.PartialAsync("TagHelpers/MyList", Info);
        var writer = new StringWriter();
        partial.WriteTo(writer, _htmlEncoder);

        output.Content.SetHtmlContent(writer.ToString());
    }
}
3
votes

In this article from Tech Dominator http://blog.techdominator.com/article/using-html-helper-inside-tag-helpers.html they show how to do it in the simplest way that I found.

This is the example that appears in that article. I tested it and it works great:

using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
using System.Threading.Tasks;

namespace UsingCshtmlTemplatesInTagHelpers.TagHelpers
{
    public class Holder
    {
        public string Name { get; set; }
    }

    public class TemplateRendererTagHelper : TagHelper
    {
        [ViewContext]
        [HtmlAttributeNotBound]
        public ViewContext ViewContext { get; set; }

        private IHtmlHelper _htmlHelper;

        public TemplateRendererTagHelper(IHtmlHelper htmlHelper)
        {
            _htmlHelper = htmlHelper;
        }

        public override async Task ProcessAsync(TagHelperContext context
            , TagHelperOutput output)
        {
            (_htmlHelper as IViewContextAware).Contextualize(ViewContext);

            /*
             * Create some data that are going 
             * to be passed to the view
             */
            _htmlHelper.ViewData["Name"] = "Ali";
            _htmlHelper.ViewBag.AnotherName = "Kamel";
            Holder model = new Holder { Name = "Charles Henry" };

            output.TagName = "div";
            /*
             * model is passed explicitly
             * ViewData and ViewBag are passed implicitly
             */
            output.Content.SetHtmlContent(await _htmlHelper.PartialAsync("Template", model));
        }
    }
}
0
votes

I've implemented something like this, but it takes a round about approach by creating a custom ViewRendering service with dependency injection and using that to render the view into a string. I like this approach because it allows my app to use views for many things including email templates, taghelpers, and any other cases where I'd need to render a view into a string for use in code while also allowing me to pass a model to the view for more dynamic elements.

Interface defining the service:

public interface IViewRenderService
{
    string RenderView(string viewName);

    string RenderView<TModel>(string viewName, TModel model);

}

Implementation of the service (the important part):

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Routing;
using System;
using System.IO;

public class ViewRenderService : IViewRenderService
{
    private readonly IRazorViewEngine _viewEngine;
    private readonly ITempDataProvider _tempDataProvider;
    private readonly IServiceProvider _serviceProvider;

    public ViewRenderService(IRazorViewEngine viewEngine, ITempDataProvider tempDataProvider, IServiceProvider serviceProvider)
    {
        _viewEngine = viewEngine;
        _tempDataProvider = tempDataProvider;
        _serviceProvider = serviceProvider;
    }

    public string RenderView(string viewName)
    {
        var actionContext = GetActionContext();

        var viewEngineResult = _viewEngine.FindView(actionContext, viewName, false);

        if (!viewEngineResult.Success)
        {
            throw new InvalidOperationException(string.Format("Couldn't find view '{0}'", viewName));
        }

        var view = viewEngineResult.View;

        using (var output = new StringWriter())
        {
            var viewContext = new ViewContext(
                actionContext,
                view,
                new ViewDataDictionary(
                    metadataProvider: new EmptyModelMetadataProvider(),
                    modelState: new ModelStateDictionary()),
                new TempDataDictionary(
                    actionContext.HttpContext,
                    _tempDataProvider),
                output,
                new HtmlHelperOptions());

            view.RenderAsync(viewContext).GetAwaiter().GetResult();

            return output.ToString();
        }
    }

    public string RenderView<TModel>(string viewName, TModel model)
    {
        var actionContext = GetActionContext();

        var viewEngineResult = _viewEngine.FindView(actionContext, viewName, false);

        if (!viewEngineResult.Success)
        {
            throw new InvalidOperationException(string.Format("Couldn't find view '{0}'", viewName));
        }

        var view = viewEngineResult.View;

        using (var output = new StringWriter())
        {
            var viewContext = new ViewContext(
                actionContext,
                view,
                new ViewDataDictionary<TModel>(
                    metadataProvider: new EmptyModelMetadataProvider(),
                    modelState: new ModelStateDictionary())
                {
                    Model = model
                },
                new TempDataDictionary(
                    actionContext.HttpContext,
                    _tempDataProvider),
                output,
                new HtmlHelperOptions());

            view.RenderAsync(viewContext).GetAwaiter().GetResult();

            return output.ToString();
        }
    }

    private ActionContext GetActionContext()
    {
        var httpContext = new DefaultHttpContext();
        httpContext.RequestServices = _serviceProvider;
        return new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
    }
}

Then in your Startup.cs

    public void ConfigureServices(IServiceCollection services)
    {
        ...

        services.AddTransient<IViewRenderService, ViewRenderService>();

        ...
    }

Finally, you would use this in code like this (in this example a controller):

public class TestController : Controller
{
    private IViewRenderService viewRenderService;

    public TestController(IViewRenderService _viewRenderService)
    {
        viewRenderService = _viewRenderService;
    }

    public async Task<IActionResult> Index()
    {
        // code

        var stringOfView = viewRenderService.RenderView("EmailTemplate/EmailConfirmation");

        // code that does something with the view as a string

        return View();
    }
}

You can put your Views in the Views folder under their own folder (in the example above the view path is thus: /Views/EmailTemplate/EmailConfirmation.cshtml)

If your view requires a model you would pass it in like so:

var stringOfView = viewRenderService.RenderView("folder/view", model);

Hope this helps!