Render a block instance to string?

Ian
Ian
Vote:
 

Are there any built-in helper methods or approaches that would render an existing ContentReference to a string? Essentially, I'm looking to return a string of the rendered block in a web API response. 

The following code works in .NET MVC, but not .NET Core:

public string Render(IContent matchedContent)
{
    if (matchedContent == null)
        throw new ContentNotFoundException("Content was not found");

    //Resolve the right Template based on Episervers own templating engine
    var model = _templateResolver.Resolve(HttpContext.Current, matchedContent.GetOriginalType(),
        TemplateTypeCategories.MvcPartial | TemplateTypeCategories.MvcPartialController |
        TemplateTypeCategories.MvcController, Array.Empty<string>());

    //Resolve the controller
    ControllerBase contentController = null;
    if (model.TemplateType != null)
        contentController = ServiceLocator.Current.GetInstance(model.TemplateType) as ControllerBase;

    if (contentController == null) // if view without controller
        contentController = new CustomController(model.Name);

    //Mimic the routing of our rendition 
    var routeData = new RouteData();
    routeData.Values.Add("currentContent", matchedContent);
    routeData.Values.Add("controllerType", model.TemplateType);
    routeData.Values.Add("language", ContentLanguage.PreferredCulture.Name);
    routeData.Values.Add("controller", model.Name.Replace("Controller", ""));
    routeData.Values.Add("action", "Index");
    routeData.Values.Add("node", matchedContent.ContentLink.ID);
    routeData.DataTokens["contextmode"] =
        ContextMode.Default; //important to declare that the rendering should not be "edit"-mode, we dont want that, and it will crash

    //Create a fake context, that can be executed based on the route
    var viewContext =
        new ViewContext(
            new ControllerContext(new HttpContextWrapper(HttpContext.Current), routeData, contentController),
            new FakeView(), new ViewDataDictionary(), new TempDataDictionary(), new StringWriter());

    var helper = new HtmlHelper(viewContext, new ViewPage());

    //Render in our fake context
    _contentRenderer.Render(helper, new PartialRequest(), matchedContent, model);

    //Derive the output based on our template and view engine
    return viewContext.Writer.ToString();
}
#296119
Edited, Feb 07, 2023 15:41
Chris Sharp - Feb 07, 2023 16:58
Have you looked into just using the Content Delivery API?
https://docs.developers.optimizely.com/content-cloud/v1.5.0-content-delivery-api/docs
https://docs.developers.optimizely.com/content-cloud/v1.5.0-content-delivery-api/docs/contentapi
Ian - Feb 07, 2023 17:13
Maybe I'm missing it in the documentation, but can the Content Delivery API return a rendered string and not just the raw model data?
Chris Sharp - Feb 07, 2023 17:14
So you just want raw HTML to be returned?
Ian - Feb 07, 2023 17:17
Yeah, exactly. I'm looking for the fully rendered view to be returned as a string.
Vote:
 

Maybe this will work did not test it out.

private readonly ContentApiOptions _options;
private readonly IHtmlHelper _htmlHelper;
private readonly ITempDataProvider _tempDataProvider;
private readonly ICompositeViewEngine _viewEngine;
private readonly IModelMetadataProvider _modelMetadataProvider;

public virtual async Task<string> RenderContentAsString(HttpContext context, IContent content)
{
     var model = _templateResolver.Resolve(context, content.GetOriginalType(),
          TemplateTypeCategories.MvcPartial | TemplateTypeCategories.MvcPartialController |
          TemplateTypeCategories.MvcController, Array.Empty<string>());     

     using (var writer = new StringWriter())
     {
         // we need to set the ViewContext.View to a not null value for creating ViewComponentContext (in CMS Core),
         // otherwise it will throw exception. This view result similar to rendering the content in view mode.
         var viewResult = _viewEngine.GetView(null, model.Path, false);

         var viewContext = new ViewContext
         {
                HttpContext = context,
                Writer = writer,
                RouteData = new RouteData(),
                TempData = new TempDataDictionary(context, _tempDataProvider),
                ActionDescriptor = new ActionDescriptor(),
                View = viewResult.View,
                ViewData = new ViewDataDictionary(_modelMetadataProvider, new ModelStateDictionary()),
                FormContext = new FormContext()
           };

           (_htmlHelper as IViewContextAware)?.Contextualize(viewContext);

           await _htmlHelper.RenderContentDataAsync(content, viewContext, false, model, null);

           writer.Flush();
           return writer.ToString();
       }
}
#296283
Feb 09, 2023 19:29
Ian
Vote:
 

Thanks, Mark! I have edited the code a little bit, but am running into an issue on the `viewResult` line (see comment):

public string Render(HttpContext context, ContentReference contentReference, IEnumerable<string> tags = null)
{
    var content = _contentLoader.Get<IContent>(contentReference);

    var model = _templateResolver.Resolve(content, content.GetOriginalType(),
        TemplateTypeCategories.MvcPartial 
        | TemplateTypeCategories.MvcPartialComponent
        | TemplateTypeCategories.MvcPartialController
        | TemplateTypeCategories.MvcController, Array.Empty<string>());

    using var writer = new StringWriter();

    // Errors out here -- "'Value cannot be null or empty. Arg_ParamName_Name'"
    var viewResult = _viewEngine.GetView(null, model.Path, false);
    
    var viewContext = new ViewContext
    {
        HttpContext = context,
        Writer = writer,
        RouteData = new RouteData(),
        TempData = new TempDataDictionary(context, _tempDataProvider),
        ActionDescriptor = new ActionDescriptor(),
        View = viewResult.View,
        ViewData = new ViewDataDictionary(_modelMetadataProvider, new ModelStateDictionary()),
        FormContext = new FormContext()
    };

    (_htmlHelper as IViewContextAware)?.Contextualize(viewContext);

    _htmlHelper.RenderContentData(content, false, tags);

    writer.Flush();
    return writer.ToString();
}

It appears that model.Path is always null, and model resolves to our DefaultBlockController (see below) since this particular block does not have it's own dedicated controller. I'm not sure if it matters, but the ContentReference I'm trying to render to a string using this example is a block, not a page. 

I feel like this is close but I'm not sure how to get it over the finish line from here.

[TemplateDescriptor(Inherited = true)]
public class DefaultBlockController : AsyncBlockComponent<BlockData>
{
    protected override async Task<IViewComponentResult> InvokeComponentAsync(BlockData currentBlock)
    {
        var model = CreateModel(currentBlock);
        var blockName = currentBlock.GetOriginalType().Name;
        var viewPath = $"~/Features/Blocks/{blockName}/{blockName}.cshtml";

        return await Task.FromResult(View(viewPath, model));
    }

    private static IBlockViewModel<BlockData> CreateModel(BlockData currentBlock)
    {
        var type = typeof(BlockViewModel<>).MakeGenericType(currentBlock.GetOriginalType());
        return Activator.CreateInstance(type, currentBlock) as IBlockViewModel<BlockData>;
    }
}
#296319
Edited, Feb 10, 2023 4:15
Vote:
 

try 

var contentName = content.GetOriginalType().Name;
var viewResult = _viewEngine.GetView(null, !string.IsNullOrEmpty(model.Path) ? model.Path : $"~/Features/Blocks/{contentName}/{contentName}.cshtml";, false);
#296368
Feb 10, 2023 23:37
Vote:
 

Hello,

Are you looking for something like this?

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;
using System.Threading.Tasks;

namespace A.B.C
{
    public interface IViewRenderService
    {
        Task<string> RenderToStringAsync(string viewName, object model);
    }

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

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

        public async Task<string> RenderToStringAsync(string viewName, object model)
        {
            var httpContext = new DefaultHttpContext { RequestServices = _serviceProvider };
            var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());

            using (var sw = new StringWriter())
            {
                var viewResult = _razorViewEngine.GetView(viewName, viewName, false);

                if (viewResult.View == null)
                {
                    throw new ArgumentNullException($"{viewName} does not match any available view");
                }

                var viewDictionary = new ViewDataDictionary(new EmptyModelMetadataProvider(), new ModelStateDictionary())
                {
                    Model = model
                };

                var viewContext = new ViewContext(
                    actionContext,
                    viewResult.View,
                    viewDictionary,
                    new TempDataDictionary(actionContext.HttpContext, _tempDataProvider),
                    sw,
                    new HtmlHelperOptions()
                );

                await viewResult.View.RenderAsync(viewContext);
                return sw.ToString();
            }
        }
    }
}

Can be called like this:

var myString = await _viewRenderService.RenderToStringAsync("~/Views/Shared/View.cshtml", viewModel);
#296448
Feb 13, 2023 14:10
Ian
Vote:
 

@Mark - thanks, that does the trick! I was hoping to be able to dynamically resolve the view paths, but this should be fine for now. Greatly appreciate everyone's help! Here's the final method:

public string Render(HttpContext context, ContentReference contentReference, IEnumerable<string> tags = null)
{
    var content = _contentLoader.Get<IContent>(contentReference);

    var model = _templateResolver.Resolve(content, content.GetOriginalType(),
        TemplateTypeCategories.MvcPartial
        | TemplateTypeCategories.MvcPartialComponent
        | TemplateTypeCategories.MvcPartialController
        | TemplateTypeCategories.MvcController, Array.Empty<string>());

    using var writer = new StringWriter();

    var contentName = content.GetOriginalType().Name;
    var viewResult = _viewEngine.GetView(null,
        !string.IsNullOrEmpty(model.Path) ? model.Path : $"~/Features/Blocks/{contentName}/{contentName}.cshtml",
        false);

    var viewContext = new ViewContext
    {
        HttpContext = context,
        Writer = writer,
        RouteData = new RouteData(), //routeData,
        TempData = new TempDataDictionary(context, _tempDataProvider),
        ActionDescriptor = new ActionDescriptor(),
        View = viewResult.View,
        ViewData = new ViewDataDictionary(_modelMetadataProvider, new ModelStateDictionary()),
        FormContext = new FormContext()
    };

    (_htmlHelper as IViewContextAware)?.Contextualize(viewContext);

    _htmlHelper.RenderContentData(content, false, tags);

    writer.Flush();
    return writer.ToString();
}
#296530
Feb 14, 2023 20:43
This topic was created over six months ago and has been resolved. If you have a similar question, please create a new topic and refer to this one.
* You are NOT allowed to include any hyperlinks in the post because your account hasn't associated to your company. User profile should be updated.