Skip to content

Instantly share code, notes, and snippets.

@craigmccauley
Created December 3, 2022 18:37
Show Gist options
  • Save craigmccauley/ff090e76212df682cb8c0eddbab5bc21 to your computer and use it in GitHub Desktop.
Save craigmccauley/ff090e76212df682cb8c0eddbab5bc21 to your computer and use it in GitHub Desktop.
Renders Blazor Components and Razor Views into HTML
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
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.TagHelpers;
using Microsoft.AspNetCore.Mvc.ViewEngines;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.AspNetCore.Routing;
namespace HtmlRendering;
public interface IHtmlRenderer
{
Task<string> RenderBlazorComponentAsync<TComponent>(Dictionary<string, object> parameters) where TComponent : IComponent;
Task<string> RenderRazorViewToStringAsync<TModel>(string viewName, TModel model);
}
/// <summary>
/// Html rendering code mashed together from various stackoverflow answers and blog posts.
/// </summary>
public class HtmlRenderer : IHtmlRenderer
{
private readonly IHttpContextAccessor httpContextAccessor;
private readonly IRazorViewEngine razorViewEngine;
private readonly ITempDataProvider tempDataProvider;
public HtmlRenderer(
IHttpContextAccessor httpContextAccessor,
IRazorViewEngine razorViewEngine,
ITempDataProvider tempDataProvider)
{
this.httpContextAccessor = httpContextAccessor;
this.razorViewEngine = razorViewEngine;
this.tempDataProvider = tempDataProvider;
}
/// <summary>
/// Renders a blazor component as html.
/// </summary>
/// <typeparam name="TComponent">The type of the blazor component.</typeparam>
/// <param name="parameters">Parameters to pass to the blazor component.</param>
/// <returns>Task with string of html from rendered component.</returns>
public async Task<string> RenderBlazorComponentAsync<TComponent>(Dictionary<string, object> parameters) where TComponent : IComponent
{
var componentTagHelper = new ComponentTagHelper
{
ComponentType = typeof(TComponent),
RenderMode = RenderMode.Static,
Parameters = parameters,
ViewContext = new ViewContext { HttpContext = httpContextAccessor.HttpContext },
};
//uniqueid and tagName are arbitrary
var tagHelperContext = new TagHelperContext(
new TagHelperAttributeList(),
new Dictionary<object, object>(),
"uniqueid");
var tagHelperOutput = new TagHelperOutput(
"tagName",
new TagHelperAttributeList(),
(useCachedResult, encoder) => Task.FromResult<TagHelperContent>(new DefaultTagHelperContent()));
await componentTagHelper.ProcessAsync(tagHelperContext, tagHelperOutput);
using var stringWriter = new StringWriter();
tagHelperOutput.Content.WriteTo(stringWriter, HtmlEncoder.Default);
return stringWriter.ToString();
}
/// <summary>
/// Renders a razor view as html.
/// </summary>
/// <typeparam name="TModel">The type of the view model.</typeparam>
/// <param name="viewName">The name of the view. ex. ~/Views/MyView.cshtml</param>
/// <param name="model">The view model.</param>
/// <returns>Task with string of html from rendered view.</returns>
/// <exception cref="InvalidOperationException"></exception>
public async Task<string> RenderRazorViewToStringAsync<TModel>(string viewName, TModel model)
{
var actionContext = new ActionContext(httpContextAccessor.HttpContext, httpContextAccessor.HttpContext.GetRouteData(), new ActionDescriptor());
var view = FindView(actionContext, viewName);
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());
await view.RenderAsync(viewContext);
return output.ToString();
IView FindView(ActionContext actionContext, string viewName)
{
var getViewResult = razorViewEngine.GetView(executingFilePath: null, viewPath: viewName, isMainPage: true);
if (getViewResult.Success)
{
return getViewResult.View;
}
var findViewResult = razorViewEngine.FindView(actionContext, viewName, isMainPage: true);
if (findViewResult.Success)
{
return findViewResult.View;
}
var searchedLocations = getViewResult.SearchedLocations.Concat(findViewResult.SearchedLocations);
var errorMessage = string.Join(
Environment.NewLine,
new[] { $"Unable to find view '{viewName}'. The following locations were searched:" }.Concat(searchedLocations));
throw new InvalidOperationException(errorMessage);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment