I need to render a Razor Page partial to a string.
I want to create a controller action that responds with JSON containing a partial view and other optional parameters.
I am familiar with the following example that renders a View to a string: https://github.com/aspnet/Entropy/blob/dev/samples/Mvc.RenderViewToString/RazorViewToStringRenderer.cs
However, it is not compatible with Pages, as it only searches in the Views directory, so even if I give it an absolute path to the partial it tries to locate my _Layout.cshtml (which it shouldn't even do!) and fails to find it.
I have tried to modify it so it renders pages, but I end up getting a NullReferenceException for ViewData in my partial when attempting to render it. I suspect it has to do with NullView, but I have no idea what to put there instead (the constructor for RazorView requires many objects that I don't know how to get correctly).
The code:
// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0: https://www.apache.org/licenses/LICENSE-2.0 // Modified by OronDF343: Uses pages instead of views. using System; using System.IO; using System.Linq; using System.Threading.Tasks; 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.Mvc.ViewFeatures.Internal; using Microsoft.AspNetCore.Routing; namespace TestAspNetCore.Services { public class RazorPageToStringRenderer { private readonly IRazorViewEngine _viewEngine; private readonly ITempDataProvider _tempDataProvider; private readonly IServiceProvider _serviceProvider; public RazorPageToStringRenderer( IRazorViewEngine viewEngine, ITempDataProvider tempDataProvider, IServiceProvider serviceProvider) { _viewEngine = viewEngine; _tempDataProvider = tempDataProvider; _serviceProvider = serviceProvider; } public async Task<string> RenderPageToStringAsync<TModel>(string viewName, TModel model) { var actionContext = GetActionContext(); var page = FindPage(actionContext, viewName); using (var output = new StringWriter()) { var viewContext = new ViewContext(actionContext, new NullView(), new ViewDataDictionary<TModel>(new EmptyModelMetadataProvider(), new ModelStateDictionary()) { Model = model }, new TempDataDictionary(actionContext.HttpContext, _tempDataProvider), output, new HtmlHelperOptions()); page.ViewContext = viewContext; await page.ExecuteAsync(); return output.ToString(); } } private IRazorPage FindPage(ActionContext actionContext, string pageName) { var getPageResult = _viewEngine.GetPage(executingFilePath: null, pagePath: pageName); if (getPageResult.Page != null) { return getPageResult.Page; } var findPageResult = _viewEngine.FindPage(actionContext, pageName); if (findPageResult.Page != null) { return findPageResult.Page; } var searchedLocations = getPageResult.SearchedLocations.Concat(findPageResult.SearchedLocations); var errorMessage = string.Join( Environment.NewLine, new[] { $"Unable to find page '{pageName}'. The following locations were searched:" }.Concat(searchedLocations)); throw new InvalidOperationException(errorMessage); } private ActionContext GetActionContext() { var httpContext = new DefaultHttpContext { RequestServices = _serviceProvider }; return new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); } } }
ASP.NET MVC4 Web API controller should return Razor view as html in json result property. Message=The method or operation is not implemented. var viewResult = ViewEngines.
Listing 1: Rendering a Razor View to String from within a Controller. The RenderViewToString() method works by receiving a controller context and virtual view path (i.e., ~/views/item/page. cshtml ) and optional model data that are passed to the view.
Razor Pages is sometimes described as implementing the MVVM (Model, View ViewModel) pattern. It doesn't. The MVVM pattern is applied to applications where the presentation and model share the same layer. It is popular in WPF, mobile application development, and some JavaScript libraries.
This is how I did it.
As always register the Service in Startup.cs
services.AddScoped<IViewRenderService, ViewRenderService>();
The Service is defined as follows:
public interface IViewRenderService { Task<string> RenderToStringAsync<T>(string viewName, T model) where T : PageModel; } public class ViewRenderService : IViewRenderService { private readonly IRazorViewEngine _razorViewEngine; private readonly ITempDataProvider _tempDataProvider; private readonly IServiceProvider _serviceProvider; private readonly IHttpContextAccessor _httpContext; private readonly IActionContextAccessor _actionContext; private readonly IRazorPageActivator _activator; public ViewRenderService(IRazorViewEngine razorViewEngine, ITempDataProvider tempDataProvider, IServiceProvider serviceProvider, IHttpContextAccessor httpContext, IRazorPageActivator activator, IActionContextAccessor actionContext) { _razorViewEngine = razorViewEngine; _tempDataProvider = tempDataProvider; _serviceProvider = serviceProvider; _httpContext = httpContext; _actionContext = actionContext; _activator = activator; } public async Task<string> RenderToStringAsync<T>(string pageName, T model) where T : PageModel { var actionContext = new ActionContext( _httpContext.HttpContext, _httpContext.HttpContext.GetRouteData(), _actionContext.ActionContext.ActionDescriptor ); using (var sw = new StringWriter()) { var result = _razorViewEngine.FindPage(actionContext, pageName); if (result.Page == null) { throw new ArgumentNullException($"The page {pageName} cannot be found."); } var view = new RazorView(_razorViewEngine, _activator, new List<IRazorPage>(), result.Page, HtmlEncoder.Default, new DiagnosticListener("ViewRenderService")); var viewContext = new ViewContext( actionContext, view, new ViewDataDictionary<T>(new EmptyModelMetadataProvider(), new ModelStateDictionary()) { Model = model }, new TempDataDictionary( _httpContext.HttpContext, _tempDataProvider ), sw, new HtmlHelperOptions() ); var page = ((Page)result.Page); page.PageContext = new Microsoft.AspNetCore.Mvc.RazorPages.PageContext { ViewData = viewContext.ViewData }; page.ViewContext = viewContext; _activator.Activate(page, viewContext); await page.ExecuteAsync(); return sw.ToString(); } } }
I call it like this
emailView.Body = await this._viewRenderService.RenderToStringAsync("Email/ConfirmAccount", new Email.ConfirmAccountModel { EmailView = emailView, });
"Email/ConfirmAccount" is the path to my Razor page (Under pages). "ConfirmAccountModel" is my page model for that page.
ViewData is null because the ViewData for the Page is set when the PageContext is set, so if this is not set ViewData is null.
I also found that I had to call
_activator.Activate(page, viewContext);
For it all to work. This is not fully tested yet so may not work for all scenarios but should help you get started.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With