0

I have searched all over for an answer to this question, All I can find is information relating to ASP.Net MVC. I am using Razor pages, and have found most of the answers to be either useless or relying on MVC specific actions that I don't understand and know how to use.

What I want to do, if possible, is render a razor page, with its view model, to an HTML string that I can then send as the body of an email message, or I guess use for other things as I need it later.

I'm having trouble with the solutions I have found actually properly passing model data to the view, so when it encounters references to model objects in the view it returns an error "not set to an instance of an object". I can load the page otherwise with no trouble.

The page in question requires passing route data to fetch the needed info from a database, the onget method runs some methods to retrieve and calculate some things, and so on.

I feel like this must be an easy question, but like I said I just can't find any relevant info. In short, I want to pass a command from one page to render a different page, with input from its viewmodel, into the output HTML that I can send via email.

Edit for clarity:

What I'm trying to do, I have an accounting program I am building. It has the ability to generate invoices based on a complex model of several different database objects. (because it's not a simple invoice program) I have the invoice displayed as a razor view, with a PageModel governing behavior that runs a few methods, fetches data from several different sources. Because of the architecture of the program, the invoice is generated on demand, can have payments added, etc.

The invoice is displayed on one page wrapped inside the layout, and a different version of the invoice displayed layout-free for the purpose of printing.

I want to take this second invoice, the page and the code that already exists, fetch it from within a method called in a different page, compile the razor syntax with method from the PageModel to Html, and send that Html as the body of an email. Basically, do what ASP.Net does when you load the page, but instead of sending the Html to the browser, send it to the method and store it as a string.

The problem I'm running into is that none of the solutions I'm finding are capable of doing this -they can run simple tasks, but aren't made to do anything to the extent of what I want.

Right now the program just sends a link to invoice at the hosted URL. That's a fine enough solution, but I'd really like to be able to extract it from the program entirely.

If I can't do this the way I'd like there are other solutions. But it would be very nice to be able to use the nicely styled and designed page I already have.

  • Does [this](https://stackoverflow.com/questions/41558671/rendering-html-content-with-html-raw-in-razor-mail-template/41558944#41558944) answer your question? – Ryan Apr 03 '23 at 21:36
  • How would I pass the ViemModel to the RunCompile method? I suppose essentially what I'm trying to do is compile the view inside a function, complete with the route data it needs, and capture the compiled HTML as a string. The route data is all present inside the view model I want to call the render function in. – David Bahler Apr 03 '23 at 22:10
  • This [post](https://stackoverflow.com/questions/40912375/return-view-as-string-in-net-core/69075473#69075473) addresses the same objective you've outlined: "Return View as String in .NET Core" – Dave B Apr 03 '23 at 22:47
  • I have seen this. I do not understand how to use ControllerContext in the context of razor pages. I am assuming this is an MVC action? I am not familiar with MVC controllers, not using MVC myself. Please excuse my ignorance, but I'm not certain how to carry this over, or what the equivalent functions would be for MVVM razor pages – David Bahler Apr 03 '23 at 23:18
  • The answer with the heading "ASP .NET 5" can be called from a Web API controller context in ASP.NET Core. The answer with the heading "ASP.NET Core 3.1" shows a solution without requiring a context parameter. It instead has a custom method `GetActionContext()` to get a context. The solution requires a string path to your Razor page and the model only. I've used the "ASP.NET Core 3.1" answer in ASP.NET Core projects and it works when calling the `RenderViewToStringAsync` method from an OnGet/OnPost method in a Razor page or a Web API controller. – Dave B Apr 04 '23 at 01:52
  • If you provide a minimal code sample with Razor pages and a sample model, it'll be easier to show how to integrate one of the solutions in the referenced post. – Dave B Apr 04 '23 at 01:59
  • Thanks for your explanation. I had overlooked the ASP.Net core 3.1 explanation assuming it was outdated. Looking over it on your suggestion, I think it makes sense and I have a sense of what's going, and it is probably what I am looking for. I'll give it a try tomorrow and see if it works, or rather see if I can get it to work. – David Bahler Apr 04 '23 at 03:36
  • If I understand correctly I need to pass an instance of the PageModel as the model argument to the RenderViewToStringAsync method, correct? So like this: `var model = new PrintModel(_invoiceService, _jobService); var htmlContent = await _renderToStringRenderer.RenderViewToStringAsync("~/Pages/AdminPortal/Invoices/Print.cshtml", model);` – David Bahler Apr 04 '23 at 04:13
  • the question is then, how can I pass route data to the page? This function will actually run until it hits a place in the cshtml where it calls for model data, then returns a notfound error. So, yeah, how do I pass the route data? the OnGetAsync method in the PrintModel page takes an int Id as argument, so that's all I need to route to the page. – David Bahler Apr 04 '23 at 04:15
  • See my blog post here: https://www.mikesdotnetting.com/article/332/rendering-a-partial-to-a-string-in-razor-pages – Mike Brind Apr 04 '23 at 06:21

1 Answers1

0

There are several answers on Stack Overflow and blog posts (like @MikeBrind's post and another here) that have solutions to the title of your question: render a view to HTML.

The title of your question could be "Render a Razor Page to HTML" to better match the scenario you describe after your text "Edit for clarity".

You want code that will execute a route/endpoint but not send a response to a HTTP client (like the browser or a Web API call). Instead, a JavaScript-like e.preventDefault is the desired behavior where HTML outputted by the Razor engine is returned to your code and no HTTP response, the default behavior, is created.

Your scenario is targeting a Razor Page (a .cshtml page with an @page directive at the top of the file and a PageModel and, therefore, configured as an endpoint) and not a Razor View (a .cshtml with no @page directive and no backing PageModel) that many posts present. There's a SO post here, however, that describes the same targeting of a Razor Page; an answer is listed that converts a PageModel into a ViewComponent.

Given the simplicity of retrieving HTML output from a Razor View that most posts present, I recommend refactoring your code so that each route (i.e. Razor Page) along with the {id} route data calls into a DI service or static class inside of their respective OnGet or OnPost methods; and 2. move the invoice HTML to a Razor View page and insert it via:

<partial name="~/Pages/AdminPortal/Invoices/_Invoice.cshtml" model="@Model.InvoiceModel" />

in the Razor Pages presented to your users. It can also be called via:

string htmlContent = await _renderToStringRenderer.RenderViewToStringAsync("~/Pages/AdminPortal/Invoices/_Invoice.cshtml", model);

when you're generating HTML content for an email.

RenderPartialToStringAsync

The following description from @ChrisPratt here has been useful to understand how IRazorPartialToStringRenderer services that @MikeBrind lists in his post or other SO answers:

The actual cshtml file is just a text file for all intents an purposes. There's a filesystem reference to it (i.e. Path from the IView interface), but that's it. It's not "compiled" per se. The view engine gets fed the View<TModel> instance, with its path to the cshtml file, and then it loads the contents of that file and begins parsing it (Razor), the View<TModel> provides the data backing for that parsing process, i.e. when it comes across Razor code, the information is pull from the View<TModel> instance. Once the view has been "rendered". The result is basically just a string

Razor Page vs Razor View

For anyone else confused by the subtle difference in naming convention resulting from Microsoft's evolution from MVC to Razor Pages (@Mike provides another good post here), I often stall for a second when I bring up the Add New Item dialog in Visual Studio when I want to add a Razor Page ... or is it a Razor View I want.

enter image description here

I'll duplicate what's listed earlier in this post. This is how I remind myself:

A Razor Page is a .cshtml page that will an @page directive at the top of the file, a backing or corresponding PageModel .cshtml.cs file, and will be therefore, configured as a route that a client browser or JavaScript fetch API may request.

A Razor View is also .cshtml but (most likely) has no @page directive at top of the file; has no backing PageModel but still (likely) has the @model keyword. A Razor view is often referenced as a partial view; an underscore (_) is added at the beginning of the file name but is not necessary and does not affect its processing by the Razor view engine.

It's possible to have an @page directive at the top of a Razor View. The Razor View can be accessed as a route as long as a model is not used (i.e. the .cshtml file does not contain the @model keyword).

Dave B
  • 1,105
  • 11
  • 17
  • This is an excellent response not only for answering my initial question, but also taking it as an opportunity to demonstrate a lot of valuable knowledge. And at Mike, I've found your Razor Pages in Action book an incredibly valuable resource and reference it often, it's a pleasure to be able to interact with you directly! Anyway, After messing around with the nuts and bolts of things I had started to realize a lot of the things you're pointing out. And it helped me to better grasp the differences between MVC and Razor Pages. This information really helps to tie that all together. – David Bahler Apr 06 '23 at 02:18
  • Basically I had come to the conclusion that doing what I'm trying to do is simply not worth the effort, if even possible. It's just so much simpler to do what you suggested, export the markup into a partial view, and render that. Like you said there are solutions to this problem, but is it worth it? The bright side is running into this scenario made me rethink how the sections of the app I'm working on next should be designed. – David Bahler Apr 06 '23 at 02:22
  • It's been a bit since this was active but I would like to update. I did refactor my code to export anything I might ever want to render into strings of html code as partial views, which also required in a few cases also refactoring the data into static classes to pass to the views, rather than accessing different objects from the page. This has the added benefit of cleaning up my page views by creating a very clear separation of function. – David Bahler Apr 14 '23 at 17:37
  • It does work as intended, I've got the email sender working exactly like I want it. I am going to have to change the markup in the partial view however, and this is something to point out for others who are investigating it, because I use bootstrap and this isn't passed to the email. So I'll have to add some CSS to the partial view. I added `Layout = null` to the partial view. Is there any good reason not to do this? Since razor pages loads the layout before the page, I assume that under normal circumstances this effectively does nothing. – David Bahler Apr 14 '23 at 17:38
  • When I started using a custom 'render view as a string' service, adding `Layout = null` to a partial view was necessary. However, setting the `isMainPage` parameter in `_viewEngine.GetView(executingFilePath: null, viewPath: viewName, isMainPage: true)` to `false` removed the need to set `Layout = null` in the partial view. – Dave B Apr 15 '23 at 14:07
  • What's the purpose, then, of this parameter? Is it something like a relic of older versions of ASP.NET? Is there any reason NOT to set it to false? – David Bahler Apr 15 '23 at 22:32
  • Information about the `isMainPage` parameter is thin other than the [documentation](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.razor.razorviewengine.getview?view=aspnetcore-7.0) page for the `RazorViewEngine.GetView` method. The parameter description is: "Determines if the page being found is the main page for an action." – Dave B Apr 16 '23 at 12:03
  • The `RazorViewEngine.cs` [file](https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.Razor/src/RazorViewEngine.cs) for the ASP.NET source project on GitHub, however, has a comment that better describes the purpose for `isMainPage`: "Only need to lookup _ViewStarts for the main page." A layout is set commonly through `@{ Layout = "_Layout"; }` in a `_ViewStarts.cshtml` file. Setting `isMainPage` to `false` prevents the view engine from searching and entering into `_ViewStarts`; and, therefore, a layout is not applied to the view. – Dave B Apr 16 '23 at 12:07
  • Thanks again for all the helpful information. I have got everything working at this point exactly how I want it, other than for now the end point of some of the views rendered to HTML is an email sender as a placeholder for a pdf generator that I havent added in yet. In some cases I did need to duplicate and then modify the partial views because they included dynamic links that weren't wanted in the html-rendered product. – David Bahler Apr 18 '23 at 21:19