The following code works fine for rendering a Razor View to a string:
///
/// url: /api/createHtml
///
public ActionResult CreateHtml()
{
// Heavy calculations
MyModel myModel = new MyModel();
myModel.Feature1 = ...;
myModel.Feature2 = ...;
myModel.Feature3 = ...;
ViewData.Model = myModel;
using (var stringWriter = new StringWriter())
{
var viewResult = ViewEngines.Engines.FindPartialView(ControllerContext, "MyView");
var viewContext = new ViewContext(ControllerContext, viewResult.View, ViewData, TempData, stringWriter);
viewResult.View.Render(viewContext, stringWriter);
viewResult.ViewEngine.ReleaseView(ControllerContext, viewResult.View);
string html = stringWriter.GetStringBuilder().ToString();
byte[] htmlBytes = Encoding.ASCII.GetBytes(html);
System.IO.File.WriteAllBytes(Server.MapPath("~/temp/foo.html"), htmlBytes);
}
return JSON(new
{
error = false,
message = "Your view is available at temp/foo.html"
});
}
The above code runs synchronously, meaning that an AJAX request to /api/createHtml/ will finish with the temp/foo.html file created.
I want to do this asynchronously: meaning that the AJAX request returns fast to the user with a message like: "Your view WILL BE available at temp/foo.html". And then the user must go check if the file is ready (by simply polling to the temp directory [or using other method, not important in this question])
So, when I try the same code within a Task, it doesn't work:
///
/// url: /api/createHtml
///
public ActionResult CreateHtml()
{
new Task(() =>
{
// Heavy calculations
MyModel myModel = new MyModel();
myModel.Feature1 = ...;
myModel.Feature2 = ...;
myModel.Feature3 = ...;
ViewData.Model = myModel;
using (var stringWriter = new StringWriter())
{
var viewResult = ViewEngines.Engines.FindPartialView(ControllerContext, "MyView");
var viewContext = new ViewContext(ControllerContext, viewResult.View, ViewData, TempData, stringWriter);
viewResult.View.Render(viewContext, stringWriter); // <--- Problem
viewResult.ViewEngine.ReleaseView(ControllerContext, viewResult.View);
string html = stringWriter.GetStringBuilder().ToString();
byte[] htmlBytes = Encoding.ASCII.GetBytes(html);
System.IO.File.WriteAllBytes(Server.MapPath("~/temp/foo.html"), htmlBytes);
}
}).Start();
return JSON(new
{
error = false,
message = "Your view _WILL BE_ available at temp/foo.html"
});
}
It doesn't work because it throws an Exception at viewResult.View.Render(...)
Value does not fall within the expected range.
It seems that the viewContext passed to viewResult.View.Render(...) is no longer valid in the new thread, as shown here: ASP.NET MVC: Exception rendering view in new thread
Is there a workaround for rendering a view within a Task ?
I know I could use "RazorEngine", a free library that renders razor views without all the controller mumbo jumbo, but I'd prefer to use native code, for reutilization of the code.
POST EDITED:
The few answers thought that I wanted to use "await async". I don't. I don't want to wait for the task to finish.
Well your tasks is probably doing what it suppose to do, but the main threat is not waiting for him.
Try using:
var myTask=new Task(() =>
{
// all of your code
}).Start();
myTask.Wait();
check more info
The best way is using async and await, because Tasks should be used for asynchronous operations, good luck.
Asynchronous action methods, works async only inside the web server. The advantage of async methods is that they release the precious thread back to the thread pool, when they don't need it (as opposed to blocking it)... But the HttpRequest is still synchronous... your browser would synchronously wait for a response from an async action method.
What you need is to spawn a new thread or task to do your long running task and return from the action method.
Take a look at: Long Running Background Tasks in Asp.Net MVC3
Also this question might help: Do asynchronous operations in ASP.NET MVC use a thread from ThreadPool on .NET 4
Related
Although #Html.RenderPartial calls write and returns void, it is still writing to a StringWriter/StringBuilder. Is there a way to render directly to the ResponseStream?
Can this be done with a custom IViewEngine that implements render like PdfView to directly output to the ResponseStream?
ADDITION
ViewResultBase.ExecuteResult shows the ViewContext being built with Response.Output, but debugger shows ViewContext.Writer as a StringWriter
Both of these approaches results in a StringWriter
return PartialView("view", Model)
// or
PartialView("view", Model).ExecuteResult(ControllerContext)
EDIT
It appears that System.Web.WebPages.WebPageBase ExecutePageHeirarchy pushes a temp StringWriter onto the context stack, so I'm not sure if this can be bypassed
IN SUMMARY
RenderPartial, RenderAction do not directly output to the Response.Stream, none of Razor Views will
SOLUTION
It was the new WebPages/Razor rendering engine that wraps everything with a StringWriter to a StringBuilder. The solution was to change my page to use the WebFormViewEngine which does not apply this wrapping.
This below method illustrates one way achieving the outcome you are looking for:
// <summary>
// An extension methods for rendering a model/view into a stream
// </summary>
// <param name="myModel">The model you are trying render to a stream</param>
// <param name="controllerBase">This will come from your executing action</param>
// <returns></returns>
public static Stream GetStream(CustomModel myModel, ControllerBase controllerBase)
{
//we will return this stream
MemoryStream stream = new MemoryStream();
//you can add variables to the view data
controllerBase.ViewData["ViewDataVariable1"] = true;
//set your model
controllerBase.ViewData.Model = myModel;
//The example uses the UTF-8 encoding, you should change that if you are using some other encoding.
//write to a stream
using (StreamWriter writer = new StreamWriter(stream, Encoding.UTF8))
{
using (var sw = new StringWriter())
{
//render the view ~/Views/Shared/_FeedbackMessage.cshtml (can be passed in as a parameter if you want to make it super generic)
var viewResult = ViewEngines.Engines.FindPartialView(controllerBase.ControllerContext, "_FeedbackMessage");
//create a new view context
var viewContext = new ViewContext(controllerBase.ControllerContext, viewResult.View, controllerBase.ViewData, controllerBase.TempData, sw);
//Render the viewengine and let razor do its magic
viewResult.View.Render(viewContext, sw);
viewResult.ViewEngine.ReleaseView(controllerBase.ControllerContext, viewResult.View);
//get StringBuilder from StringWriter sw and write into the stream writer
//you could simply return the StringWriter here if that is what you were interested in doing
writer.Write(sw.GetStringBuilder().ToString());
writer.Flush();
stream.Position = 0;
}
}
//return the stream from the above process
return stream;
}
Perhaps I'm not calling RazorEngine in the correct place.
In my controller action I use the following code to call RazorEngine. But I think this may not be correct as when it calls through to .Execute() and then into MVC's GetActionCache() the HttpContextBase.Items fails with a "method not implemented" exception.
Am I calling RazorEngine in the wrong way? #Html.LabelFor() works fine.
string template = "#Html.EditorFor(model => model.OldPassword)";
string result = string.Empty;
var config = new RazorEngine.Configuration.TemplateServiceConfiguration
{
BaseTemplateType = typeof(System.Web.Mvc.Helpers.HtmlTemplateBase<>)
};
using (var service = new RazorEngine.Templating.TemplateService(config))
{
// Use template service.
RazorEngine.Razor.SetTemplateService(service);
result = RazorEngine.Razor.Parse(template, model);
}
powercat97 over on the github issues page has a workaround for an issue that addresses this.
https://github.com/Antaris/RazorEngine/issues/46
The reason I've had much trouble is that there is no context set. Creating a new ViewContext is not sufficient.
Therefore by calling a view that in turn calls our RazorEngine code via RenderAction() we get the context and the MVC framework has everything it needs when it is called by RazorEngine.
Using the AccountController as an example (HtmlTemplateBase comes from RazorEngine issues with #Html and http://www.haiders.net/post/HtmlTemplateBase.aspx):
public ActionResult Test()
{
var model = new MySite.Models.LocalPasswordModel();
model.OldPassword = "MyOldPwd";
model.NewPassword = "SomeNewPwd";
return PartialView(model);
}
[ChildActionOnly()]
public string TestTemplate(MySite.Models.LocalPasswordModel vm)
{
string result = string.Empty;
string template = "#Html.EditorFor(model => model.OldPassword)";
var config = new RazorEngine.Configuration.TemplateServiceConfiguration
{
BaseTemplateType = typeof(HtmlTemplateBase<>)
};
using (var service = new RazorEngine.Templating.TemplateService(config))
{
// Use template service.
RazorEngine.Razor.SetTemplateService(service);
result = RazorEngine.Razor.Parse(template, vm, "MyTemplateName");
}
return result;
}
and in Test.cshtml:
#model TestRazorEngine.Models.LocalPasswordModel
#{ Html.RenderAction("TestTemplate", new { vm = Model }); }
I know this looks like a duplicate question, but please read the whole question before marking it as duplicate.
First of all, I'm simulating the windows service in my ASP web application to send weekly emails, so in Global.asax I'm running my function that will send the emails.
Now the emails content is in HTML and I want to render the views to get the content. The problem is that in my function, I don't have any of the following :
Controller
ControllerContext
HttpContext
RoutData
... & much more. Which makes sense, because the function was invoked as a callback not as a HTTP request action.
I tried to use the RazorEngine to use the partial as a template by reading the file then using Razor.Parse() method. But I faced a lot of problems from this approach, because nothing is included in the template. What I mean is: it keeps telling me that The name "Html" does not exist in the current context OR 'CompiledRazorTemplates.Dynamic.becdccabecff' does not contain a definition for 'Html' even if I include the System.Web.Mvc.Html.
how can I solve this issue?.
I think the best approach is assuming you developed a real NT service and use HttpClient to send a http request to your partial view and receive the response as string and use it to make up your email. However, you can have HttpContext in RunScheduledTasks method by making some changes in Scheduler class.
public delegate void Callback();
to
public delegate void Callback(HttpContext httpContext);
add cache.Current_HttpContext = HttpContext.Current; to the Run method
public static void Run(string name, int minutes, Callback callbackMethod)
{
_numberOfMinutes = minutes;
CacheItem cache = new CacheItem();
cache.Name = name;
cache.Callback = callbackMethod;
cache.Cache = HttpRuntime.Cache;
cache.LastRun = DateTime.Now;
cache.Current_HttpContext = HttpContext.Current;
AddCacheObject(cache);
}
change CacheCallback to
private static void CacheCallback(string key, object value, CacheItemRemovedReason reason)
{
CacheItem obj_cache = (CacheItem)value;
if (obj_cache.LastRun < DateTime.Now)
{
if (obj_cache.Callback != null)
{
obj_cache.Callback.Invoke(obj_cache.Current_HttpContext);
}
obj_cache.LastRun = DateTime.Now;
}
AddCacheObject(obj_cache);
}
Edited:
How to use HttpClient
HttpClient client = new HttpClient();
HttpResponseMessage response = await client.GetAsync("http://localhost/controller/action/");
response.EnsureSuccessStatusCode();
string responseBody = await response.Content.ReadAsStringAsync();
Without using 3rd party library, one can use this method to generate string of view in Global.asax.cs file
public class EmptyController : Controller { }
public string GetView(string viewName)
{
//Create an instance of empty controller
Controller controller = new EmptyController();
//Create an instance of Controller Context
var ControllerContext = new ControllerContext(Request.RequestContext, controller);
//Create a string writer
using (var sw = new StringWriter())
{
//get the master layout
var master = Request.IsAuthenticated ? "_InternalLayout" : "_ExternalLayout";
//Get the view result using context controller, partial view and the master layout
var viewResult = ViewEngines.Engines.FindView(ControllerContext, viewName, master);
//Crete the view context using the controller context, viewResult's view, string writer and ViewData and TempData
var viewContext = new ViewContext(ControllerContext, viewResult.View, controller.ViewData, controller.TempData, sw);
//Render the view in the string writer
viewResult.View.Render(viewContext, sw);
//release the view
viewResult.ViewEngine.ReleaseView(ControllerContext, viewResult.View);
//return the view stored in string writer as string
return sw.GetStringBuilder().ToString();
}
}
I have an app that only ever loads a full view one time. My reason for doing this is not important. What is important is that the rest of the content is only ever going to come back in partial views. In addition to some content I have some JSON objects I'd like to pass back and forth to and from the server with each AJAX request.
Is there a way to return a JSON object with a view as one of its properties? This would be extremely useful and would save on bandwidth as my current workaround is to make two ajax calls, one for the JSON and one for the partial view which not only takes more time and more bandwidth, but it also requires two separate action methods and some fancy tricks on the server side. Serializing a view into a JSON object would solve all my problems.
What is the best way to accomplish this and what downsides (if any) would there be to doing this?
You can render the view from the controller and return it with the JSON object back to the client.
If you will use my simple helper to render ActionResult to a string then your code will look like:
public JsonResult DoSomething() {
var viewString = View().Capture(ControllerContext);
return new JsonResult {
JsonRequestBehavior = JsonRequestBehavior.AllowGet,
Data = new {
time = DateTime.Now,
html = viewString
}
};
}
Here's an interesting tidbit of code that seems to do what I want and preserves model binding from what I can tell.
protected string RenderPartialViewToString(string viewName, object model)
{
controller.ViewData.Model = model;
using (StringWriter sw = new StringWriter())
{
ViewEngineResult viewResult = ViewEngines.Engines.FindPartialView(controller.ControllerContext, viewName);
ViewContext viewContext = new ViewContext(controller.ControllerContext, viewResult.View, controller.ViewData, controller.TempData, sw);
viewResult.View.Render(viewContext, sw);
return sw.GetStringBuilder().ToString();
}
}
Works like a charm. I just use this and pass the string as a JSON parameter and then on the client I read the parameter and drop it in it's appropriate container. I'm very excited to have this working.
How do I go about creating a mock request for my asp.net-mvc application for unit-testing?
What options do I have?
I am using FormsCollection in my Actions so I can simulate form input data also.
You just have to create a new instance of FormCollection and add the data inside of it.
So you can call something like this without mocking anything.
var result = controller.Create(new FormCollection { { "InvoiceId", "-1" } }) as RedirectToRouteResult;
Otherwise if your code calls something like Request or HttpContext you can use the following extension method (inspired from Scott Hanselman's example)
I am using RhinoMocks.
public static HttpContextBase SetHttpContext(this MockRepository mocks, Controller controller, HttpCookieCollection cookies) {
cookies = cookies ?? new HttpCookieCollection();
var request = mocks.StrictMock<HttpRequestBase>();
var context = mocks.StrictMock<HttpContextBase>();
var response = mocks.StrictMock<HttpResponseBase>();
SetupResult.For(context.Request).Return(request);
SetupResult.For(context.Response).Return(response);
SetupResult.For(request.Cookies).Return(cookies);
SetupResult.For(request.IsSecureConnection).Return(false);
SetupResult.For(response.Cookies).Return(cookies);
if (controller != null)
{
controller.ControllerContext = new ControllerContext(context, new RouteData(), controller);
}
if (!string.IsNullOrEmpty(requestUrl))
{
request.SetupRequestUrl(requestUrl);
SetupResult.For(response.ApplyAppPathModifier(null)).IgnoreArguments().Return(null);
}
return context;
}