Getting MVC5 Views as string, with nested hosting - c#

I have the following code that is executed by Hangfire (there is no HttpContext) and works perfectly when I run it locally:
class FakeController : ControllerBase
{
protected override void ExecuteCore() { }
public static string RenderViewToString(string controllerName, string viewName, object viewData)
{
using (var writer = new StringWriter())
{
var routeData = new RouteData();
routeData.Values.Add("controller", controllerName);
var fakeControllerContext = new ControllerContext(
new HttpContextWrapper(
new HttpContext(new HttpRequest(null, "http://nopage.no", null)
, new HttpResponse(null))
),
routeData,
new FakeController()
);
var razorViewEngine = new RazorViewEngine();
var razorViewResult = razorViewEngine.FindView(fakeControllerContext, viewName, "", false);
var viewContext = new ViewContext(fakeControllerContext, razorViewResult.View, new ViewDataDictionary(viewData), new TempDataDictionary(), writer);
razorViewResult.View.Render(viewContext, writer);
return writer.ToString();
}
}
}
The way we have our application set up however is as follows:
https://application.net <- One application
https://application.net/admin <- The other application that this code runs on.
When I run the code on https://application.net/admin, I get the following exception:
System.ArgumentException: The virtual path '/' maps to another application, which is not allowed.
It occurs at this line: razorViewEngine.FindView(fakeControllerContext, viewName, "", false).
I tried creating my own RazorViewEngine and overrode some of the methods to find the views, to no avail.
class MyViewEngine : RazorViewEngine
{
protected override bool FileExists(ControllerContext controllerContext, string virtualPath)
{
if (!base.FileExists(controllerContext, virtualPath))
return base.FileExists(controllerContext, "~/../admin" + virtualPath.TrimStart('~'));
return true;
}
protected override IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath)
{
var newViewPath = viewPath;
if (!base.FileExists(controllerContext, viewPath))
newViewPath = "~/../admin/" + viewPath.TrimStart('~');
return base.CreateView(controllerContext, newViewPath, masterPath);
}
}
This failed because it did not want to let me out of the "upmost part of the directory tree".
Is there an easy fix for this, or an alternate way of creating strings from razor views? - The purpose is to create email templates. There will be many emails created, and I don't want to use a HttpClient to ask my own endpoint to create it.

I was able to replicated issue. httpContext was not getting set correctly. Change code this way and no need to override RazorViewEngine:
static string GetUrlRoot()
{
var httpContext = HttpContext.Current;
if (httpContext == null)
{
return "http://localhost";
}
return httpContext.Request.Url.GetLeftPart(UriPartial.Authority) +
httpContext.Request.ApplicationPath;
}
public static string RenderViewToString(string controllerName, string viewName, object viewData)
{
using (var writer = new StringWriter())
{
var routeData = new RouteData();
routeData.Values.Add("controller", controllerName);
var fakeControllerContext = new ControllerContext(
new HttpContextWrapper(
new HttpContext(new HttpRequest(null, GetUrlRoot(), null)
, new HttpResponse(null))
),
routeData,
new FakeController()
);
var razorViewEngine = new RazorViewEngine();
var razorViewResult = razorViewEngine.FindView(fakeControllerContext, viewName, "", false);
var viewContext = new ViewContext(fakeControllerContext, razorViewResult.View, new ViewDataDictionary(viewData), new TempDataDictionary(), writer);
razorViewResult.View.Render(viewContext, writer);
return writer.ToString();
}
}

After a lot of trying and failing I ended up abandoning my approach, using Rick Strahl's WestWind.RazorHosting library instead.
Here's the most important part of the documentation I used.
And here is how my code ended up looking at the end.
class FakeController : IDisposable
{
private readonly RazorFolderHostContainer _host;
public FakeController()
{
_host = new RazorFolderHostContainer
{
TemplatePath = $#"{HostingEnvironment.ApplicationPhysicalPath}Views\EmailTemplate\"
};
_host.AddAssemblyFromType(typeof(Controller));
_host.AddAssemblyFromType(typeof(EmailTemplateController.TestViewModel));
_host.Start();
}
public string RenderViewToString(string viewName, object viewData)
{
return _host.RenderTemplate($#"~/{viewName}.cshtml", viewData);
}
public void Dispose()
{
_host.Stop();
}
}
This looks for views in my EmailTemplate folder that's placed in Views, finding *.cshtml files that I want to use. Since it gets them from the Views folder, I'm still able to use the same views with the normal controllers, and hence it will work when I use the views in my endpoints too. Nifty.

Related

How to make an Action show a View or a Serialized ViewModel generaly?

In my whole application I use strong typed View Models. Now I have the task to show the view normaly, or provide the serialized ViewModel if a specific parameter is present.
So, the first thing comes into my mind is this:
public ActionResult SomeAction(int id, string apiMode)
{
var model = new ViewModel(); //Obtain model
if (apiMode == "json")
{
return Json(model);
}
return View(model)
}
Now I am wondering if there is a more generic solution?
It is totaly possible.
We can extend the RazorViewEngine the following way:
public class AppViewEngine : RazorViewEngine
{
private const string ApiModeParam = "API__MODE";
public AppViewEngine()
{
//Performance optimization, if we do not need to lookup for vbhtml
FileExtensions = new[] {"cshtml"};
}
public override ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache)
{
var apiMode = controllerContext.HttpContext.Request.Params[ApiModeParam];
if (apiMode == null)
{
return base.FindPartialView(controllerContext, partialViewName, useCache);
}
return GetApiResult(apiMode);
}
public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
{
var apiMode = controllerContext.HttpContext.Request.Params[ApiModeParam];
if (apiMode == null)
{
return base.FindView(controllerContext, viewName, masterName, useCache);
}
return GetApiResult(apiMode);
}
private ViewEngineResult GetApiResult(string apiMode)
{
switch (apiMode)
{
case "json":
return new ViewEngineResult(new ApiJsonView(), this);
case "xml":
return new ViewEngineResult(new ApiXmlView(), this);
default:
return new ViewEngineResult(new[] { "API__MODE is not supported" });
}
}
}
Now we only need some Views:
public class ApiJsonView : IView
{
public void Render(ViewContext viewContext, TextWriter writer)
{
viewContext.HttpContext.Response.AddHeader("Content-Type", "application/json");
//Json.NET, normaly included in MVC
var serializer = JsonSerializer.Create();
serializer.Serialize(writer, viewContext.ViewData.Model);
}
}
public class ApiXmlView : IView
{
public void Render(ViewContext viewContext, TextWriter writer)
{
viewContext.HttpContext.Response.AddHeader("Content-Type", "application/xml");
var model = viewContext.ViewData.Model;
var serializer = new DataContractSerializer(model.GetType());
using (var xmlWriter = new XmlTextWriter(writer))
{
serializer.WriteObject(xmlWriter, model);
}
}
}
Last thing: We need to register it in global.asax in the Application_Start method:
ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new AppViewEngine());
Now every request, appended with ?API__MODE=json is showing the serialized ViewModel instead of an HTML Page. And the best, it is as simple as:
public ActionResult SomeAction(int id)
{
var model = new ViewModel(); //Obtain model
return View(model)
}

Is there a way to generate Urls with WebAPI?

POST EDITED BELOW
We can't figure out why UrlHelper is returning null strings when used from the WebApi controller's context.
We've done the neccesary debugging but we can't find out why this happens, the RouteData has the routes in it but it doesn't seem to be working.
For the most part, we use a RenderViewToString function, which loads views that consist of calls to Url.RouteUrl(routeName).
Something that's been tried is creating a custom UrlHelper (but to no avail) and debugging with either UrlHelper's (MVC / HTTP).
Attribute routing is used everywhere with route names.
Example usage code:
public class WebApiController : BaseApiController
{
[HttpPost]
[ResponseType(typeof(string))]
[Route("cart/get/checkout", Name = "api.cart.get.checkout")]
public IHttpActionResult GetCheckOutShoppingCart([FromBody] string data)
{
return Ok(RenderViewToString("CartController", "_CheckOutCartPartial", new ShoppingCartModel(Auth.IsAuthenticated ? Auth.GetCustomer().DefaultShippingInfo.CountryId : 148)
{
AddInsurance = false,
InsuredShipping = insuredShipping,
CurrentDeliveryMethodId = deliveryMethodId,
CurrentPaymentMethodId = paymentMethodId
}));
}
}
BaseApiController class:
public class BaseApiController : ApiController
{
public static string RenderViewToString(string controllerName, string viewName)
{
return RenderViewToString(controllerName, viewName, new Dictionary<string, object>());
}
public static string RenderViewToString(string controllerName, string viewName, object model)
{
using (var writer = new StringWriter())
{
var routeData = new RouteData();
routeData.Values.Add("controller", controllerName);
var fakeControllerContext =
new ControllerContext(
new HttpContextWrapper(new HttpContext(new HttpRequest(null, "http://google.com", null),
new HttpResponse(null))), routeData, new FakeController());
var razorViewEngine = new RazorViewEngine();
var razorViewResult = razorViewEngine.FindView(fakeControllerContext, viewName, "", false);
var viewContext = new ViewContext(fakeControllerContext, razorViewResult.View,
new ViewDataDictionary(model), new TempDataDictionary(), writer);
razorViewResult.View.Render(viewContext, writer);
return writer.ToString();
}
}
public static string RenderViewToString(string controllerName, string viewName, Dictionary<string, Object> data)
{
using (var writer = new StringWriter())
{
var viewData = new ViewDataDictionary();
foreach (var kv in data)
{
viewData[kv.Key] = kv.Value;
}
var routeData = new RouteData();
routeData.Values.Add("controller", controllerName);
var fakeControllerContext =
new ControllerContext(
new HttpContextWrapper(new HttpContext(new HttpRequest(null, "http://google.com", null),
new HttpResponse(null))), routeData, new FakeController());
var razorViewEngine = new RazorViewEngine();
var razorViewResult = razorViewEngine.FindView(fakeControllerContext, viewName, "", false);
var viewContext = new ViewContext(fakeControllerContext, razorViewResult.View, viewData,
new TempDataDictionary(), writer);
razorViewResult.View.Render(viewContext, writer);
return writer.ToString();
}
}
private class FakeController : ControllerBase
{
protected override void ExecuteCore()
{
}
}
}
EDIT
We've put together a class that should in theory work, but it doesn't.
The RouteData has both the MVC and API routes in the collection.
public static class Url
{
public static bool IsWebApiRequest()
{
return
HttpContext.Current.Request.RequestContext.HttpContext.CurrentHandler is
System.Web.Http.WebHost.HttpControllerHandler;
}
public static string RouteUrl(string routeName, object routeValues = null)
{
var url = String.Empty;
try
{
if (IsWebApiRequest())
{
var helper = new System.Web.Http.Routing.UrlHelper();
url = helper.Link(routeName, routeValues);
}
else
{
var helper = new System.Web.Mvc.UrlHelper();
url = helper.RouteUrl(routeName, routeValues);
}
return url;
}
catch
{
return url;
}
}
public static string HttpRouteUrl(string routeName, object routeValues = null)
{
var url = String.Empty;
try
{
if (IsWebApiRequest())
{
var helper = new System.Web.Http.Routing.UrlHelper();
url = helper.Link(routeName, routeValues);
}
else
{
var helper = new System.Web.Mvc.UrlHelper();
url = helper.HttpRouteUrl(routeName, routeValues);
}
return url;
}
catch
{
return url;
}
}
}
This issue has been resolved by building a custom UrlHelper class, which looks into the RouteTable and then returns the url with the patterns replaced.
public static class Link
{
public static string RouteUrl(string routeName, object routeValues = null)
{
var url = String.Empty;
try
{
var route = (Route)RouteTable.Routes[routeName];
if (route == null)
return url;
url = "~/".AbsoluteUrl() + route.Url;
url = url.Replace("{culture}", System.Threading.Thread.CurrentThread.CurrentCulture.TwoLetterISOLanguageName.ToLower());
if (routeValues == null)
return url;
var values = routeValues.GetType().GetProperties();
Array.ForEach(values, pi => url = Regex.Replace(url, "{" + pi.Name + "}", pi.GetValue(routeValues, null).ToString()));
return url;
}
catch
{
var newUrl = RouteUrl("403");
if(newUrl == String.Empty)
throw;
return newUrl;
}
}
public static string HttpRouteUrl(string routeName, object routeValues = null)
{
return RouteUrl(routeName, routeValues);
}
}
Try Uri.Link(routeName, object)
var uri = Url.Link("api.cart.get.checkout", new { id = 1 });
This will inject the given object's properties into the route if the property name matches a route parameter.
WebApi is not MVC. Even though it looks very similar, it's a completely different system.
UrlHelper (which is MVC) will be looking at the MVC route table, and ignore any WebApi routes.
Try creating the route the hard way, essentially hard-coding the controller and action names into the views. :-(
OK, based on your comment, it seems that you're trying to call your Url.RouteUrl(string routeName, object routeValues = null) or Url.HttpRouteUrl(string routeName, object routeValues = null) from your MVC layout.cshtml file.
If that's the case, the Url.IsWebApiRequest() would return false because the layout.cshtml file is only processed as part of handling MVC request and not WebAPI. That will cause the url generation methods to use MVC route collection instead of the WebAPI collection.
Change your Url class so that RouteUrl always builds WebAPI url and HttpRouteUrl only builds MVC url then in your layout file use proper method based on which kind of url you need in the particular context.
public static class Url
{
public static bool IsWebApiRequest()
{
return
HttpContext.Current.Request.RequestContext.HttpContext.CurrentHandler is
System.Web.Http.WebHost.HttpControllerHandler;
}
public static string RouteUrl(string routeName, object routeValues = null)
{
var url = String.Empty;
try
{
var helper = new System.Web.Http.Routing.UrlHelper();
return helper.Link(routeName, routeValues);
}
catch
{
return url;
}
}
public static string HttpRouteUrl(string routeName, object routeValues = null)
{
var url = String.Empty;
try
{
var helper = new System.Web.Mvc.UrlHelper();
return helper.HttpRouteUrl(routeName, routeValues);
}
catch
{
return url;
}
}
}

Output buffering in ASP.NET MVC [duplicate]

I want to output two different views (one as a string that will be sent as an email), and the other the page displayed to a user.
Is this possible in ASP.NET MVC beta?
I've tried multiple examples:
1. RenderPartial to String in ASP.NET MVC Beta
If I use this example, I receive the "Cannot redirect after HTTP
headers have been sent.".
2. MVC Framework: Capturing the output of a view
If I use this, I seem to be unable to do a redirectToAction, as it
tries to render a view that may not exist. If I do return the view, it
is completely messed up and doesn't look right at all.
Does anyone have any ideas/solutions to these issues i have, or have any suggestions for better ones?
Many thanks!
Below is an example. What I'm trying to do is create the GetViewForEmail method:
public ActionResult OrderResult(string ref)
{
//Get the order
Order order = OrderService.GetOrder(ref);
//The email helper would do the meat and veg by getting the view as a string
//Pass the control name (OrderResultEmail) and the model (order)
string emailView = GetViewForEmail("OrderResultEmail", order);
//Email the order out
EmailHelper(order, emailView);
return View("OrderResult", order);
}
Accepted answer from Tim Scott (changed and formatted a little by me):
public virtual string RenderViewToString(
ControllerContext controllerContext,
string viewPath,
string masterPath,
ViewDataDictionary viewData,
TempDataDictionary tempData)
{
Stream filter = null;
ViewPage viewPage = new ViewPage();
//Right, create our view
viewPage.ViewContext = new ViewContext(controllerContext, new WebFormView(viewPath, masterPath), viewData, tempData);
//Get the response context, flush it and get the response filter.
var response = viewPage.ViewContext.HttpContext.Response;
response.Flush();
var oldFilter = response.Filter;
try
{
//Put a new filter into the response
filter = new MemoryStream();
response.Filter = filter;
//Now render the view into the memorystream and flush the response
viewPage.ViewContext.View.Render(viewPage.ViewContext, viewPage.ViewContext.HttpContext.Response.Output);
response.Flush();
//Now read the rendered view.
filter.Position = 0;
var reader = new StreamReader(filter, response.ContentEncoding);
return reader.ReadToEnd();
}
finally
{
//Clean up.
if (filter != null)
{
filter.Dispose();
}
//Now replace the response filter
response.Filter = oldFilter;
}
}
Example usage
Assuming a call from the controller to get the order confirmation email, passing the Site.Master location.
string myString = RenderViewToString(this.ControllerContext, "~/Views/Order/OrderResultEmail.aspx", "~/Views/Shared/Site.Master", this.ViewData, this.TempData);
Here's what I came up with, and it's working for me. I added the following method(s) to my controller base class. (You can always make these static methods somewhere else that accept a controller as a parameter I suppose)
MVC2 .ascx style
protected string RenderViewToString<T>(string viewPath, T model) {
ViewData.Model = model;
using (var writer = new StringWriter()) {
var view = new WebFormView(ControllerContext, viewPath);
var vdd = new ViewDataDictionary<T>(model);
var viewCxt = new ViewContext(ControllerContext, view, vdd,
new TempDataDictionary(), writer);
viewCxt.View.Render(viewCxt, writer);
return writer.ToString();
}
}
Razor .cshtml style
public string RenderRazorViewToString(string viewName, object model)
{
ViewData.Model = model;
using (var sw = new StringWriter())
{
var viewResult = ViewEngines.Engines.FindPartialView(ControllerContext,
viewName);
var viewContext = new ViewContext(ControllerContext, viewResult.View,
ViewData, TempData, sw);
viewResult.View.Render(viewContext, sw);
viewResult.ViewEngine.ReleaseView(ControllerContext, viewResult.View);
return sw.GetStringBuilder().ToString();
}
}
Edit: added Razor code.
This answer is not on my way . This is originally from https://stackoverflow.com/a/2759898/2318354 but here I have show the way to use it with "Static" Keyword to make it common for all Controllers .
For that you have to make static class in class file . (Suppose your Class File Name is Utils.cs )
This example is For Razor.
Utils.cs
public static class RazorViewToString
{
public static string RenderRazorViewToString(this Controller controller, string viewName, object model)
{
controller.ViewData.Model = model;
using (var sw = new StringWriter())
{
var viewResult = ViewEngines.Engines.FindPartialView(controller.ControllerContext, viewName);
var viewContext = new ViewContext(controller.ControllerContext, viewResult.View, controller.ViewData, controller.TempData, sw);
viewResult.View.Render(viewContext, sw);
viewResult.ViewEngine.ReleaseView(controller.ControllerContext, viewResult.View);
return sw.GetStringBuilder().ToString();
}
}
}
Now you can call this class from your controller by adding NameSpace in your Controller File as following way by passing "this" as parameter to Controller.
string result = RazorViewToString.RenderRazorViewToString(this ,"ViewName", model);
As suggestion given by #Sergey this extension method can also call from cotroller as given below
string result = this.RenderRazorViewToString("ViewName", model);
I hope this will be useful to you make code clean and neat.
This works for me:
public virtual string RenderView(ViewContext viewContext)
{
var response = viewContext.HttpContext.Response;
response.Flush();
var oldFilter = response.Filter;
Stream filter = null;
try
{
filter = new MemoryStream();
response.Filter = filter;
viewContext.View.Render(viewContext, viewContext.HttpContext.Response.Output);
response.Flush();
filter.Position = 0;
var reader = new StreamReader(filter, response.ContentEncoding);
return reader.ReadToEnd();
}
finally
{
if (filter != null)
{
filter.Dispose();
}
response.Filter = oldFilter;
}
}
I found a new solution that renders a view to string without having to mess with the Response stream of the current HttpContext (which doesn't allow you to change the response's ContentType or other headers).
Basically, all you do is create a fake HttpContext for the view to render itself:
/// <summary>Renders a view to string.</summary>
public static string RenderViewToString(this Controller controller,
string viewName, object viewData) {
//Create memory writer
var sb = new StringBuilder();
var memWriter = new StringWriter(sb);
//Create fake http context to render the view
var fakeResponse = new HttpResponse(memWriter);
var fakeContext = new HttpContext(HttpContext.Current.Request, fakeResponse);
var fakeControllerContext = new ControllerContext(
new HttpContextWrapper(fakeContext),
controller.ControllerContext.RouteData,
controller.ControllerContext.Controller);
var oldContext = HttpContext.Current;
HttpContext.Current = fakeContext;
//Use HtmlHelper to render partial view to fake context
var html = new HtmlHelper(new ViewContext(fakeControllerContext,
new FakeView(), new ViewDataDictionary(), new TempDataDictionary()),
new ViewPage());
html.RenderPartial(viewName, viewData);
//Restore context
HttpContext.Current = oldContext;
//Flush memory and return output
memWriter.Flush();
return sb.ToString();
}
/// <summary>Fake IView implementation used to instantiate an HtmlHelper.</summary>
public class FakeView : IView {
#region IView Members
public void Render(ViewContext viewContext, System.IO.TextWriter writer) {
throw new NotImplementedException();
}
#endregion
}
This works on ASP.NET MVC 1.0, together with ContentResult, JsonResult, etc. (changing Headers on the original HttpResponse doesn't throw the "Server cannot set content type after HTTP headers have been sent" exception).
Update: in ASP.NET MVC 2.0 RC, the code changes a bit because we have to pass in the StringWriter used to write the view into the ViewContext:
//...
//Use HtmlHelper to render partial view to fake context
var html = new HtmlHelper(
new ViewContext(fakeControllerContext, new FakeView(),
new ViewDataDictionary(), new TempDataDictionary(), memWriter),
new ViewPage());
html.RenderPartial(viewName, viewData);
//...
This article describes how to render a View to a string in different scenarios:
MVC Controller calling another of its own ActionMethods
MVC Controller calling an ActionMethod of another MVC Controller
WebAPI Controller calling an ActionMethod of an MVC Controller
The solution/code is provided as a class called ViewRenderer. It is part of Rick Stahl's WestwindToolkit at GitHub.
Usage (3. - WebAPI example):
string html = ViewRenderer.RenderView("~/Areas/ReportDetail/Views/ReportDetail/Index.cshtml", ReportVM.Create(id));
If you want to forgo MVC entirely, thereby avoiding all the HttpContext mess...
using RazorEngine;
using RazorEngine.Templating; // For extension methods.
string razorText = System.IO.File.ReadAllText(razorTemplateFileLocation);
string emailBody = Engine.Razor.RunCompile(razorText, "templateKey", typeof(Model), model);
This uses the awesome open source Razor Engine here:
https://github.com/Antaris/RazorEngine
Additional tip for ASP NET CORE:
Interface:
public interface IViewRenderer
{
Task<string> RenderAsync<TModel>(Controller controller, string name, TModel model);
}
Implementation:
public class ViewRenderer : IViewRenderer
{
private readonly IRazorViewEngine viewEngine;
public ViewRenderer(IRazorViewEngine viewEngine) => this.viewEngine = viewEngine;
public async Task<string> RenderAsync<TModel>(Controller controller, string name, TModel model)
{
ViewEngineResult viewEngineResult = this.viewEngine.FindView(controller.ControllerContext, name, false);
if (!viewEngineResult.Success)
{
throw new InvalidOperationException(string.Format("Could not find view: {0}", name));
}
IView view = viewEngineResult.View;
controller.ViewData.Model = model;
await using var writer = new StringWriter();
var viewContext = new ViewContext(
controller.ControllerContext,
view,
controller.ViewData,
controller.TempData,
writer,
new HtmlHelperOptions());
await view.RenderAsync(viewContext);
return writer.ToString();
}
}
Registration in Startup.cs
...
services.AddSingleton<IViewRenderer, ViewRenderer>();
...
And usage in controller:
public MyController: Controller
{
private readonly IViewRenderer renderer;
public MyController(IViewRendere renderer) => this.renderer = renderer;
public async Task<IActionResult> MyViewTest
{
var view = await this.renderer.RenderAsync(this, "MyView", model);
return new OkObjectResult(view);
}
}
To render a view to a string in the Service Layer without having to pass ControllerContext around, there is a good Rick Strahl article here http://www.codemag.com/Article/1312081 that creates a generic controller. Code summary below:
// Some Static Class
public static string RenderViewToString(ControllerContext context, string viewPath, object model = null, bool partial = false)
{
// first find the ViewEngine for this view
ViewEngineResult viewEngineResult = null;
if (partial)
viewEngineResult = ViewEngines.Engines.FindPartialView(context, viewPath);
else
viewEngineResult = ViewEngines.Engines.FindView(context, viewPath, null);
if (viewEngineResult == null)
throw new FileNotFoundException("View cannot be found.");
// get the view and attach the model to view data
var view = viewEngineResult.View;
context.Controller.ViewData.Model = model;
string result = null;
using (var sw = new StringWriter())
{
var ctx = new ViewContext(context, view, context.Controller.ViewData, context.Controller.TempData, sw);
view.Render(ctx, sw);
result = sw.ToString();
}
return result;
}
// In the Service Class
public class GenericController : Controller
{ }
public static T CreateController<T>(RouteData routeData = null) where T : Controller, new()
{
// create a disconnected controller instance
T controller = new T();
// get context wrapper from HttpContext if available
HttpContextBase wrapper;
if (System.Web.HttpContext.Current != null)
wrapper = new HttpContextWrapper(System.Web.HttpContext.Current);
else
throw new InvalidOperationException("Cannot create Controller Context if no active HttpContext instance is available.");
if (routeData == null)
routeData = new RouteData();
// add the controller routing if not existing
if (!routeData.Values.ContainsKey("controller") &&
!routeData.Values.ContainsKey("Controller"))
routeData.Values.Add("controller", controller.GetType().Name.ToLower().Replace("controller", ""));
controller.ControllerContext = new ControllerContext(wrapper, routeData, controller);
return controller;
}
Then to render the View in the Service class:
var stringView = RenderViewToString(CreateController<GenericController>().ControllerContext, "~/Path/To/View/Location/_viewName.cshtml", theViewModel, true);
you can get the view in string using this way
protected string RenderPartialViewToString(string viewName, object model)
{
if (string.IsNullOrEmpty(viewName))
viewName = ControllerContext.RouteData.GetRequiredString("action");
if (model != null)
ViewData.Model = model;
using (StringWriter sw = new StringWriter())
{
ViewEngineResult viewResult = ViewEngines.Engines.FindPartialView(ControllerContext, viewName);
ViewContext viewContext = new ViewContext(ControllerContext, viewResult.View, ViewData, TempData, sw);
viewResult.View.Render(viewContext, sw);
return sw.GetStringBuilder().ToString();
}
}
We can call this method in two way
string strView = RenderPartialViewToString("~/Views/Shared/_Header.cshtml", null)
OR
var model = new Person()
string strView = RenderPartialViewToString("~/Views/Shared/_Header.cshtml", model)
I am using MVC 1.0 RTM and none of the above solutions worked for me. But this one did:
Public Function RenderView(ByVal viewContext As ViewContext) As String
Dim html As String = ""
Dim response As HttpResponse = HttpContext.Current.Response
Using tempWriter As New System.IO.StringWriter()
Dim privateMethod As MethodInfo = response.GetType().GetMethod("SwitchWriter", BindingFlags.NonPublic Or BindingFlags.Instance)
Dim currentWriter As Object = privateMethod.Invoke(response, BindingFlags.NonPublic Or BindingFlags.Instance Or BindingFlags.InvokeMethod, Nothing, New Object() {tempWriter}, Nothing)
Try
viewContext.View.Render(viewContext, Nothing)
html = tempWriter.ToString()
Finally
privateMethod.Invoke(response, BindingFlags.NonPublic Or BindingFlags.Instance Or BindingFlags.InvokeMethod, Nothing, New Object() {currentWriter}, Nothing)
End Try
End Using
Return html
End Function
I saw an implementation for MVC 3 and Razor from another website, it worked for me:
public static string RazorRender(Controller context, string DefaultAction)
{
string Cache = string.Empty;
System.Text.StringBuilder sb = new System.Text.StringBuilder();
System.IO.TextWriter tw = new System.IO.StringWriter(sb);
RazorView view_ = new RazorView(context.ControllerContext, DefaultAction, null, false, null);
view_.Render(new ViewContext(context.ControllerContext, view_, new ViewDataDictionary(), new TempDataDictionary(), tw), tw);
Cache = sb.ToString();
return Cache;
}
public static string RenderRazorViewToString(string viewName, object model)
{
ViewData.Model = model;
using (var sw = new StringWriter())
{
var viewResult = ViewEngines.Engines.FindPartialView(ControllerContext, viewName);
var viewContext = new ViewContext(ControllerContext, viewResult.View, ViewData, TempData, sw);
viewResult.View.Render(viewContext, sw);
return sw.GetStringBuilder().ToString();
}
}
public static class HtmlHelperExtensions
{
public static string RenderPartialToString(ControllerContext context, string partialViewName, ViewDataDictionary viewData, TempDataDictionary tempData)
{
ViewEngineResult result = ViewEngines.Engines.FindPartialView(context, partialViewName);
if (result.View != null)
{
StringBuilder sb = new StringBuilder();
using (StringWriter sw = new StringWriter(sb))
{
using (HtmlTextWriter output = new HtmlTextWriter(sw))
{
ViewContext viewContext = new ViewContext(context, result.View, viewData, tempData, output);
result.View.Render(viewContext, output);
}
}
return sb.ToString();
}
return String.Empty;
}
}
More on Razor render- MVC3 View Render to String
Quick tip
For a strongly typed Model just add it to the ViewData.Model property before passing to RenderViewToString. e.g
this.ViewData.Model = new OrderResultEmailViewModel(order);
string myString = RenderViewToString(this.ControllerContext, "~/Views/Order/OrderResultEmail.aspx", "~/Views/Shared/Site.Master", this.ViewData, this.TempData);
To repeat from a more unknown question, take a look at MvcIntegrationTestFramework.
It makes saves you writing your own helpers to stream result and is proven to work well enough. I'd assume this would be in a test project and as a bonus you would have the other testing capabilities once you've got this setup. Main bother would probably be sorting out the dependency chain.
private static readonly string mvcAppPath =
Path.GetFullPath(AppDomain.CurrentDomain.BaseDirectory
+ "\\..\\..\\..\\MyMvcApplication");
private readonly AppHost appHost = new AppHost(mvcAppPath);
[Test]
public void Root_Url_Renders_Index_View()
{
appHost.SimulateBrowsingSession(browsingSession => {
RequestResult result = browsingSession.ProcessRequest("");
Assert.IsTrue(result.ResponseText.Contains("<!DOCTYPE html"));
});
}
Here is a class I wrote to do this for ASP.NETCore RC2. I use it so I can generate html email using Razor.
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewEngines;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Routing;
using System.IO;
using System.Threading.Tasks;
namespace cloudscribe.Web.Common.Razor
{
/// <summary>
/// the goal of this class is to provide an easy way to produce an html string using
/// Razor templates and models, for use in generating html email.
/// </summary>
public class ViewRenderer
{
public ViewRenderer(
ICompositeViewEngine viewEngine,
ITempDataProvider tempDataProvider,
IHttpContextAccessor contextAccesor)
{
this.viewEngine = viewEngine;
this.tempDataProvider = tempDataProvider;
this.contextAccesor = contextAccesor;
}
private ICompositeViewEngine viewEngine;
private ITempDataProvider tempDataProvider;
private IHttpContextAccessor contextAccesor;
public async Task<string> RenderViewAsString<TModel>(string viewName, TModel model)
{
var viewData = new ViewDataDictionary<TModel>(
metadataProvider: new EmptyModelMetadataProvider(),
modelState: new ModelStateDictionary())
{
Model = model
};
var actionContext = new ActionContext(contextAccesor.HttpContext, new RouteData(), new ActionDescriptor());
var tempData = new TempDataDictionary(contextAccesor.HttpContext, tempDataProvider);
using (StringWriter output = new StringWriter())
{
ViewEngineResult viewResult = viewEngine.FindView(actionContext, viewName, true);
ViewContext viewContext = new ViewContext(
actionContext,
viewResult.View,
viewData,
tempData,
output,
new HtmlHelperOptions()
);
await viewResult.View.RenderAsync(viewContext);
return output.GetStringBuilder().ToString();
}
}
}
}
I found a better way to render razor view page when I got error with the methods above, this solution for both web form environment and mvc environment.
No controller is needed.
Here is the code example, in this example I simulated a mvc action with an async http handler:
/// <summary>
/// Enables processing of HTTP Web requests asynchronously by a custom HttpHandler that implements the IHttpHandler interface.
/// </summary>
/// <param name="context">An HttpContext object that provides references to the intrinsic server objects.</param>
/// <returns>The task to complete the http request.</returns>
protected override async Task ProcessRequestAsync(HttpContext context)
{
if (this._view == null)
{
this.OnError(context, new FileNotFoundException("Can not find the mvc view file.".Localize()));
return;
}
object model = await this.LoadModelAsync(context);
WebPageBase page = WebPageBase.CreateInstanceFromVirtualPath(this._view.VirtualPath);
using (StringWriter sw = new StringWriter())
{
page.ExecutePageHierarchy(new WebPageContext(new HttpContextWrapper(context), page, model), sw);
await context.Response.Output.WriteAsync(sw.GetStringBuilder().ToString());
}
}
The easiest way for me was:
public string GetFileAsString(string path)
{
var html = "";
FileStream fileStream = new FileStream(path, FileMode.Open);
using (StreamReader reader = new StreamReader(fileStream))
{
html += reader.ReadLine();
}
return html;
}
I use this for emails and make sure that the file only contains CSS and HTML

Rendering ActionResult to string - comes out in incorrect order

I have an attempted solution at rendering an ActionResult to a string. I do this by passing in my own HttpContext which substitutes the Output text writer with my own TextWriter.
Here's the problem - the elements are rendering out of order. If I render the partial view by querying it directly through the browser, it works fine. If I render it through my substituted text writer, any #Html.Action elements within the razor view are rendered first, regardless of their position within the view.
So, here's my Razor view:
#inherits System.Web.Mvc.WebViewPage<WebsitePresentationLayer.MgrScreenLayoutViewer>
#using System.Web.Mvc.Html;
<div>
#Model.DebugText
</div>
#foreach (var item in #Model.Items)
{
<div>#item.Title</div>
#Html.Action(
"LayoutItem",
new
{
id = item.Id,
uniqueName = item.UniqueName
}
);
}
If I query the view directly via the browser, it renders in the correct order:
#Model.DebugText
Item1.Title
Item1 Action Rendering
Item2.Title
Item2 Action Rendering
If I render this to my TextWriter, it renders in the following order:
Item1 Action Rendering
Item2 Action Rendering
#Model.DebugText
Item1.Title
Item2.Title
Why?
Here's how I'm subsituting the Text writer. (I'm calling this from an ASP.NET WebForms page, so there is already an existing HttpContext)
public static class ActionResultExtensions
{
internal class MyResponseWrapper : HttpResponseWrapper
{
private System.IO.TextWriter _textWriter;
public MyResponseWrapper(HttpResponse wrappedResponse, System.IO.TextWriter textWriter)
: base(wrappedResponse)
{
_textWriter = textWriter;
}
public override System.IO.TextWriter Output
{
get { return this._textWriter; }
set { this._textWriter = value; }
}
}
internal class MyHttpContextWrapper : HttpContextWrapper
{
private readonly System.IO.TextWriter _textWriter;
public MyHttpContextWrapper(System.IO.TextWriter textWriter)
: base(HttpContext.Current)
{
this._textWriter = textWriter;
}
public override HttpResponseBase Response
{
get
{
var httpResponse = HttpContext.Current.Response;
return new MyResponseWrapper(httpResponse, this._textWriter);
}
}
}
public static void Render(this System.Web.Mvc.ActionResult result, System.IO.TextWriter textWriter, System.Web.Routing.RouteData routeData, System.Web.Mvc.ControllerBase controllerBase)
{
var httpContextWrapper = new MyHttpContextWrapper(textWriter);
result.ExecuteResult(new System.Web.Mvc.ControllerContext(httpContextWrapper, routeData, controllerBase));
}
}
public static class MvcUtils
{
public static void RenderControllerAction<T>(Func<T, System.Web.Mvc.ActionResult> f, System.IO.TextWriter writer) where T : System.Web.Mvc.ControllerBase, new()
{
var controller = new T();
// We have to initialise the RouteData so that it knows the name of the controller
// This is used to locate the view
var typeName = controller.GetType().Name;
System.Text.RegularExpressions.Regex regex = new System.Text.RegularExpressions.Regex("(.*)Controller$");
var match = regex.Match(typeName);
if (match.Success)
{
typeName = match.Groups[1].Value;
}
var routeData = new System.Web.Routing.RouteData();
routeData.Values.Add("controller", typeName);
var actionResult = f(controller);
actionResult.Render(writer, routeData, controller);
}
}
And then I finally output it to string using the following code:
StringBuilder sb = new StringBuilder();
StringWriter stringWriter = new StringWriter(sb);
CMS.Website.MvcUtils.RenderControllerAction<PlayerGroupController>
(
c => c.ScreenLayout(this.MgrPlayerGroupViewer.ScreenLayoutId),
stringWriter
);
stringWriter.Flush();
var generatedString = sb.ToString();
I've written an interceptor for the TextWriter, and sure enough, it's receiving three calls to Write(string)
Write(LayoutItem 1 action contents)
Write(LayoutItem 2 action contents)
Write(Model.DebugText and the two item titles)
I went through this about 6 months ago. Goal was to use a partial to populate a jquery popup dialog.
The problem is the View Engine wants to Render them in it's own awkward order...
Try this. LMK if it needs clarification.
public static string RenderPartialViewToString(Controller thisController, string viewName, object model)
{
// assign the model of the controller from which this method was called to the instance of the passed controller (a new instance, by the way)
thisController.ViewData.Model = model;
// initialize a string builder
using (StringWriter sw = new StringWriter())
{
// find and load the view or partial view, pass it through the controller factory
ViewEngineResult viewResult = ViewEngines.Engines.FindPartialView(thisController.ControllerContext, viewName);
ViewContext viewContext = new ViewContext(thisController.ControllerContext, viewResult.View, thisController.ViewData, thisController.TempData, sw);
// render it
viewResult.View.Render(viewContext, sw);
//return the razorized view/partial-view as a string
return sw.ToString();
}
}

t4mvc actionresult Converted to string

I'm trying to get from a T4MVC ActionResult to execute the call and get the entire string from inside a static function in my EmailService class. Any help would be much appreciated
Something like this
static string ExecuteAction(ActionResult result)
{
/* Code goes here */
}
So that I can call the function like this
public static class EmailService
{
public static bool SendWelcomeEmail(string name, string email)
{
var message = ExecuteAction(MVC.Emails.WelcomeEmail(name, email));
/* Other code */
}
}
I have done this very thing
protected virtual string RenderPartialViewToString(string viewName, object model)
{
if (string.IsNullOrEmpty(viewName))
viewName = ControllerContext.RouteData.GetRequiredString("action");
ViewData.Model = model;
using (StringWriter sw = new StringWriter())
{
ViewEngineResult viewResult = ViewEngines.Engines.FindPartialView(ControllerContext, viewName);
ViewContext viewContext = new ViewContext(ControllerContext, viewResult.View, ViewData, TempData, sw);
viewResult.View.Render(viewContext, sw);
return sw.GetStringBuilder().ToString();
}
}
you would implement it like this
string body = RenderPartialViewToString("PasswordChangedEmail");
the partial is just a partial view, however, there isnt any reason a full view couldnt be used. Hope it helps.
You won't be able to use the T4MVC ActionResults to do this as they have an empty ExecuteResult() implementation.
I can see two obvious ways that you may approach this:
1.create an instance of the controller on which the action method lies that you wish to call and then call it. You may decide to get an instance from the ControllerFactory instead of instantiating the class directly, although if you know what type it is, I'd go with instantiating an instance from the type directly.
to get a controller from the default controller factory, you can use
ControllerBuilder.Current.GetControllerFactory().CreateController(RequestContext requestContext, string controllerName);
This returns an IController so you'll need to cast it to the right controller type in order to call an action method. It's easier to just instantiate one directly :)
2.Use an email templating solution such as Postal, ActionMailer, MvcMailer or Kazi's Email Template Engine. All take advantage of Razor so you can have nicely templated email messages.
In case anyone is interested in the answer:
private static RequestContext RequestContext(this HttpContext context)
{
var httpContextBase = new HttpContextWrapper(context);
var routeData = new RouteData();
return new RequestContext(httpContextBase, routeData);
}
private static RouteData GetRoute(this ActionResult url)
{
var data = url.GetRouteValueDictionary();
var route = new RouteData();
foreach (var item in data)
route.Values[item.Key] = item.Value;
return route;
}
public static string ExecuteAsString(this T4MVC_ActionResult result)
{
var controllerName = result.Controller;
var context = HttpContext.Current.RequestContext();
context.RouteData = result.GetRoute();
var controller = (ControllerBase)ControllerBuilder.Current.GetControllerFactory().CreateController(context, controllerName);
controller.ControllerContext = new ControllerContext(context, controller);
var htmlHelper = new HtmlHelper(new ViewContext(
controller.ControllerContext,
new WebFormView(controller.ControllerContext, "HACK"),
new ViewDataDictionary(),
new TempDataDictionary(),
new StringWriter()),
new ViewPage());
return htmlHelper.Action(result).ToString();
}

Categories