Map Razor Page to Row

Problem:

I need to make the Razor page partial for the string.

Why I want this:

I want to create a controller action that responds using JSON containing a partial view and other optional parameters.

Attempts:

I am familiar with the following example, which displays the view in a string: https://github.com/aspnet/Entropy/blob/dev/samples/Mvc.RenderViewToString/RazorViewToStringRenderer.cs

However, it is incompatible with Pages, because it searches only in the views directory, so even if I give it an absolute path to the part, it tries to find my _Layout.cshtml (which it shouldn't even do!) And cannot find it.

I tried changing it to display pages, but I ended up getting a NullReferenceException for ViewData in partial when trying to render it. I suspect this is due to NullView, but I have no idea what to put there (the constructor for RazorView requires a lot of objects, which I don’t know how to correctly).

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()); } } } 
+15
source share
4 answers

Here is how I did it.

How to always register a Service in Startup.cs

 services.AddScoped<IViewRenderService, ViewRenderService>(); 

A 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 that

  emailView.Body = await this._viewRenderService.RenderToStringAsync("Email/ConfirmAccount", new Email.ConfirmAccountModel { EmailView = emailView, }); 

"Email / ConfirmAccount" is the path to my Razor page (under the pages). "ConfirmAccountModel" is my page for this page.

ViewData is null because the ViewData for the page is set when the PageContext parameter is set, so if this is not set, ViewData is null.

I also found that I needed to call

 _activator.Activate(page, viewContext); 

Work for all this. This has not yet been fully tested, so it may not work for all scenarios, but should help you get started.

+11
source

I had the same problem.

I looked at the source code of RazorViewEngine and found out that the page is being searched using the page's route data:

 var routeData = new RouteData(); routeData.Values.Add("page", "/Folder/MyPage"); 

It works for me with the full path "/ Folder / MyPage" in the routeData file and the page name "MyPage" in the GetPage call.

+2
source

If, like me, you do not get GetRouteData() from _httpContext.HttpContext and _actionContext will be null, you can create an extension:

 using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Razor; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.ViewEngines; using Microsoft.AspNetCore.Mvc.ViewFeatures; using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Text.Encodings.Web; using System.Threading.Tasks; namespace Utils { public static class PageExtensions { public static async Task<string> RenderViewAsync(this PageModel pageModel, string pageName) { var actionContext = new ActionContext( pageModel.HttpContext, pageModel.RouteData, pageModel.PageContext.ActionDescriptor ); using (var sw = new StringWriter()) { IRazorViewEngine _razorViewEngine = pageModel.HttpContext.RequestServices.GetService(typeof(IRazorViewEngine)) as IRazorViewEngine; IRazorPageActivator _activator = pageModel.HttpContext.RequestServices.GetService(typeof(IRazorPageActivator)) as IRazorPageActivator; var result = _razorViewEngine.FindPage(actionContext, pageName); if (result.Page == null) { throw new ArgumentNullException($"The page {pageName} cannot be found."); } var page = result.Page; var view = new RazorView(_razorViewEngine, _activator, new List<IRazorPage>(), page, HtmlEncoder.Default, new DiagnosticListener("ViewRenderService")); var viewContext = new ViewContext( actionContext, view, pageModel.ViewData, pageModel.TempData, sw, new HtmlHelperOptions() ); var pageNormal = ((Page)result.Page); pageNormal.PageContext = pageModel.PageContext; pageNormal.ViewContext = viewContext; _activator.Activate(pageNormal, viewContext); await page.ExecuteAsync(); return sw.ToString(); } } } } 

Note. This code only displays the page being called and skips the layout.

You just need to call from your PageModel like this:

var s = this.RenderViewAsync("sendEmail").Result;

"sendEmail" is the name of your PageModel view , and the path is /Pages/sendEmail.cshtml .

+2
source

Here is the route I took. Very simple and enjoyable ...

 using System; using System.IO; using System.Net; namespace gMIS.Rendering { public static class RazorPage { public static string RenderToString(string url) { try { //Grab page WebRequest request = WebRequest.Create(url); WebResponse response = request.GetResponse(); Stream data = response.GetResponseStream(); string html = String.Empty; using (StreamReader sr = new StreamReader(data)) { html = sr.ReadToEnd(); } return html; } catch (Exception err) { return {Handle as you see fit}; } } } } 

Called as such ....

 var msg = RazorPage.RenderToString(url); 

Example:

 var pathToRazorPageFolder = request.PathToRazorPageFolder(); var msg = RazorPage.RenderToString($"{pathToRazorPageFolder}/Task_Summary?userGuid={userGuid}&taskId={task.Task_ID}&includelink=true&linkuserGuid={linkUserGuid}"); 

The above example uses this extension, which I created to get the base path to my application.

 namespace Microsoft.AspNetCore.Http { public static class RequestExtension { public static string PathToRazorPageFolder(this HttpRequest request) { if (request != null) { var requestPath = request.Path.ToString(); var returnPathToFolder = request.Scheme + "://" + request.Host + requestPath.Substring(0, requestPath.LastIndexOf("/")); ; return returnPathToFolder; } else { return "HttpRequest was null"; } } } } 

I know that this does not use Dependency Injection, but man is simple. And it just works. And it works with any page, no matter how it is placed. Whether this page is inside or even outside of your application.

0
source

Source: https://habr.com/ru/post/1272300/


All Articles