Sometimes you may need to render a certain view as string, for example when you want to send an email where usually the body is pure html.
To do so, you need to create a service that takes two arguments:
- the first one is a
string
representing the path to the view you want to render; - the second one is an
object
that is used from the view.
using System; using System.IO; 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.Routing; namespace Project.Utilities { 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.FindView(actionContext, 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(); } } } }
Register Service
ASP.NET Core is designed from the ground up to support and leverage dependency injection. In the Startup
class you can register application services that can be configured for injection throughout your application.
To do so, you have to add your `IViewRenderService` in `IServiceCollection` services. The first generic type represents the type (typically an interface) that will be requested from the container. The second generic type represents the concrete type that will be instantiated by the container and used to fulfill such requests.
We have selected `Scoped` over `Transient` as Scoped objects, which are the same within a request, but different across different requests
public void ConfigureServices(IServiceCollection services) { // Previous configuragion // ... // Add Applciation Services services.AddScoped<IViewRenderService, ViewRenderService>(); }
Example
Let’s assume you have a very simple View under path `Views/Email/Invite.cshtml` which uses an `InviteViewModel`; you may then create a controller that takes IViewRenderService
as a parameter:
[Route("render") public class RenderController : Controller { private readonly IViewRenderService _viewRenderService; public RenderController(IViewRenderService viewRenderService) { _viewRenderService = viewRenderService; } [Route("invite")] public async Task<IActionResult> RenderInviteView() { var viewModel = new InviteViewModel { UserId = "cdb86aea-e3d6-4fdd-9b7f-55e12b710f78", UserName = "iggy", ReferralCode = "55e12b710f78", Credits = 10 }; var result = await _viewRenderService.RenderToStringAsync("Email/Invite", viewModel); return Content(result); } }
Following the above, if you call {URL}/render/invite
the response will be the `Email/Invite` view, rendered as string.
Thanks for the post. This is exactly I was looking for
This is really great, I’m new to c# / .NET CORE and Razor.
I liked this so much, but could not know how to display the model data in the view, can you add simple `Invite.cshtml` file.
Thanks
I tried to run it with minimum dependencies, so got the:
var viewResult = _razorViewEngine.FindView(actionContext, viewName, false);
returns a `NULL`
the dependencies I limited myself to, are:
“Microsoft.AspNetCore.Server.Kestrel”: “1.0.1”,
“Microsoft.AspNetCore.Mvc”: “1.0.1”
what is the missing one?
Thanks
I guess you are missing “Microsoft.AspNetCore.Mvc.Razor” package.
You can install it through NuGet:
Install-Package Microsoft.AspNetCore.Mvc.Razor
Thanks! Works fine for me too, except that it does not render views containing @Html.Partial. it then only returns an empty string.
As in your example, I also intend to use the function for E-Mails, so I try to include Header/Footer with
@Html.Partial(“Header”) and
@Html.Partial(“Footer”)
Header.cshtml and Footer.cshtml are in the same Folder as the main view.
Any idea how to solve this?
Arrr, got it.
@Html.Partial(“Header.cshtml”) (Notice the .cshtml) and it’s working – altough Visual Studio states that it cannot resolve the View then.
I think that `cshtml` is not necessary. Have you tried to declare the path using `~` as prefix. Like for example: `@Html.Partial(“~/folder/Header”)`.
Thank you for your help! I really appreciate it.
Hi, this is one great example for me.
I implement similar thing in my project, just without async call. Like this:
Howverer, when executing line `view.RenderAsync(viewContext).GetAwaiter().GetResult();`
I get exception:
“The model item passed into the ViewDataDictionary is of type ‘Newtonsoft.Json.Linq.JObject’, but this ViewDataDictionary instance requires a model item of type ‘System.String’.”
I’m pretty sure that I passing JObject to ViewDataDictionary (can see in Model property) and also have @model JObject statement in my razor view.
When I worked on “Microsoft.AspNetCore.Mvc.Razor”: “1.0.0” (all other depedencies also was 1.0.0) all was fine, however when I move all to 1.1.0 issues began.
Maybe you have any idea what can be wrong here?
Thanks,
Well I created a sample app to see if it works with .net core 1.1 and didn’t face any problem.
The `project.json` file I used is the following:
And here is the code for my controller, based on your example:
And the `Invite.cshtml` view:
Could you please check if the model you are passing is a `JObject` object?
Great Post!
But I’m having 2 issues.
1. Is the same issue as @Damian on `@Html.Partial(“xx.cshtml”)`. It requires the .cshtml extension to work with this code (works fine without the .cshtml when called through a controller/view).
2. Having issues with all TagHelpers on the view and Partial Views… It errors out on those lines when calling this method, but the views work fine when called normally though a controller.
for instance it stops on this:
`@Model.InventoryClass.InvClass` with the error `ArgumentOutOfRangeException: Index was out of range. Must be non-negative and less than the size of the collection.
Parameter name: index`
The post stripped out my TagHelper html! Here it is again enclosed in [] instead of .
[a asp-controller=”Store” asp-action=”Category” asp-route-cat1Id=”@Model.InvClassID”>@Model.InventoryClass.InvClass</a]
I was able to get TagHelpers working by what was posted here as well multiple other places and merging them all together… I had to inject `IHttpContextAccessor` and get the actual `HttpContext` and get the `RouteData` of the request. The code that injects`IServiceProvider _serviceProvider` and `{RequestServices = _serviceProvider}` seemed to be empty, so I removed it???
I still have to append `.cshtml` in any Partial Views (i.e. `@Html.Partial(“_Accessories.cshtml”, model)` ).
Below is the code I’m using:
In Startup.cs
In IViewRenderService.cs
In ViewRenderService.cs
Awesome! Thx a lot for showing how to make TagHelpers work!
Will this work for converting partial views to string as well? I need to render a partial view _Notify.cshtml in the Shared folder. But the following code :
string viewHtml = _viewRender.Render(“_Notification.cshtml”);
gives an error that it cannot find the view.
Yes, this will work. Use the `RenderToStringAsync` method from `IViewRenderService` as described above and it will work.
If you have a Partial view `_Invite.cshtml` in folder `Views\Email` simply call:
If the partial view is in the Shared folder do I specify “Shared/_Invite” or “_Invite”
Yes, you need to specify the parent folder. By default, it only checks in `Views` folder.
All other folders do not have any special meaning.
So in your case you use: `Shared/_Invite`.
How can i pass ViewBag to the partial view from this ?
I am not sure you can pass `ViewBag` as an argument in this case.
You can pass a dictionary with a string key and an object value, and have the same result.
Nice piece of code. Only issue I had was that I was using a database based View Provider that didn’t get called by the FindView(actionContext, viewName, false) method. Changing to use GetView() fixed the issue. This is discussed here:
https://github.com/aspnet/Mvc/issues/4936
Recieving a index was out of range error when:
await viewResult.View.RenderAsync(viewContext);
is called.
Unable to see why. Help please.
Can you please check if the path for your view is correct?
The view is found here:
var viewResult = _razorViewEngine.GetView(viewName, viewName, false);
the viewResult came back with success but when it reaches:
await viewResult.View.RenderAsync(viewContext);
an exception is thrown saying index is out of range. so I am thinking that if the view was found the path is correct.
I was wondering if it because it is a view model. This is my view code:
@using RedmanQuotePortal.Models.AccountViewModels @model VerifyCodeViewModel @*@ViewBag.Message*@ @section Scripts { @{ await Html.RenderPartialAsync("~/Views/Shared/_ValidationScriptsPartial.cshtml"); } }
This is the Model:
I think the problem is with the partial view. Can you embed the content of the partial view in the main view and give it a try.
Shows following error
RuntimeBinderException: ‘Microsoft.AspNetCore.Mvc.Abstractions.ActionDescriptor’ does not contain a definition for ‘ControllerTypeInfo’
CallSite.Target(Closure , CallSite , object )
AggregateException: One or more errors occurred. (‘Microsoft.AspNetCore.Mvc.Abstractions.ActionDescriptor’ does not contain a definition for ‘ControllerTypeInfo’)
As I can not replicate your issue, can you tell us a little more about your demo?
I’m experiencing a caching issue – if I update the view and refresh browser it does not update. During debugging I can see that the string response from the ViewRenderService.RenderToStringAsync call does not change. The only way to clear the cache is stop/start debugger.
RazorViewEngine caches the view once loaded and then you need to perform a re-build to fetch the new and changed view. You can change this behavior by implementing your own view engine.
I ‘ll let you know, when I find an easy way to purge cache on view change.
Thanks for the reply
Actually it seems using GetView() instead of FindView() eliminates the caching issue.
Nice to hear it! Great job Rob!
Exactly what I was looking for, thanks for the great post ppolyzos!!
What if the view I am trying to render got a parameter. Because I keep getting an error saying that the view doesn’t exist.
For example: “Products/Details/552”
Where the number at the end is a unique id for the product.
I think this is not possible, as it won’t find the actual view is stored. Instead, you can try `await _viewRenderService.RenderToStringAsync(“Products/Details”, viewModel)`, where your viewModel has the id parameter.
To anyone that is having “Cannot find compilation library location for package ‘Microsoft.Win32.Registry'” error on .net core 2.0:
You need to add false to in .csproj
and make sure Views existing on server after publication.
See this comment: https://github.com/aspnet/Mvc/issues/6021#issuecomment-290745919
Thanks for the snippet. I was getting the “ArgumentOutOfRangeException: Index was out of range”, mentioned above – turned out to be an anchor-tag on the form:
Like jmal73 suggested, adding the the routedata to the viewContext helped, but I also had to change how i retrieved the httpContext, like so:
var http = _serviceProvider.GetService();
var httpContext = http.HttpContext;
Now it looks like this:
Thx!
I’m new to Core (actually using .Net Core 2 here). In Startup.cs
I’m getting the error:
The type ‘ProjectName.Services.ViewRenderService’ cannot be used as type parameter ‘TImplementation’ in the generic type or method ‘ServiceCollectionServiceExtensions.AddScoped(IServiceCollection)’. There is no implicit reference conversion from ‘ProjectName.Services.ViewRenderService’ to ‘ProjectName.Services.IViewRenderService’.
You can have a look at the following article to learn more about Dependency Injection in ASP.NET Core.
Now, regarding your error, in your application’s Startup class, in ConfigureServices method and after `services.AddMvc()` you need to register your services in services container.
So you need to add:
Scoped lifetime means that services are created once per request.
In addition make sure that `ViewRenderService` class implements `IViewRenderService` interface.
annnnnd it works!!!! Ty!
Hi,
I keep getting an error that says :
InvalidOperationException: The partial view ‘_partialView’ was not found. The following locations were searched:
/Views//_partialView.cshtml
/Views/Shared/_partialView.cshtml
Cshtml:
await Html.RenderPartialAsync(“~/Views/folder/_partialView”, new viewModel {viewModel = model});
I have tried several different solutions with no success.. How can i fix this ??
Can you please try `@Html.Partial(“~/Shared/_partialView.cshtml”)` without including `Views` in your path.
Hi there,
I have implemented this in a .NET Core 2.0 web application and it works great when I run it locally, however when deployed on Azure (App Service) it stops working with an InvalidOperationException: Couldn’t find view ‘ConfirmAddressEmailTemplate’ and I can’t figure out why.
You guys got any idea what could be wrong here?
It turns out it works only when the views are located in the Views folder, if I put them in another folder outside of the default Views folder it does not work. Strange. Maybe a security thing on Azure?
Cedric, you need to set MvcRazorCompileOnPublish to false.
In your csproj file:
Thank you so much! This is really great help! Managed to get this to work with ASP.NET Core 2.0 MVC. Also, some of the comments here are very very useful – big thank you to everybody!
Very helpful post. Thanks!
Very Helpful ! Many Thanks.
it is mostly a code up to date and usable for mvc core 2 without any dependency.
I am generating a PDF file based on html, and needed at way to be able to render my razor view and pass it to the pdf generator. This was exactly what i was looking for, thanks!
How do I point to a cshtml in another project in the same solution?
thx
One way to do it, to avoid issues with paths, is to embed the template (.cshtml) files in the project and then have a service that points to that file based on the name of the resource.
This is GREAT,
Thanks ((: