Search code examples
.netasp.net-mvcasp.net-mvc-3taskcontrollercontext

Render a Razor view to string => ControllerContext is null when called from recurrent task


I'm using ASP.NET MVC3
I have a .cshtml view and I want to stringify it to be incorporated in an email body.
Here is the method I use :

//Renders a view to a string
private string RenderRazorViewToString(string viewName, object model)
{
    ViewData.Model = model;

    using (var sw = new System.IO.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();
    }
}

When I call this method from an ActionResult method which is called from an Ajax call, this perfectly works.

However, I'm facing an unusual situation :

In my Global.asax file, I have a method called every 10 minutes whose goal is to verify if some special records have been made in database these last 10 minutes, and if so, sends an email. Of course, the email body is this stringified view.

Here is a piece of my code : This method is very inspired of this post

/* File : Gloabal.asax.cs */

private static CacheItemRemovedCallback OnMatchingCacheRemove = null;

protected void Application_Start()
{
    // ...
    AddMatchingTask("SendEmail", 600);
}

private void AddMatchingTask(string name, int seconds)
{
    OnMatchingCacheRemove = new CacheItemRemovedCallback(CacheItemMatchingRemoved);
    HttpRuntime.Cache.Insert(name, seconds, null, DateTime.UtcNow.AddSeconds(seconds), Cache.NoSlidingExpiration, CacheItemPriority.NotRemovable, OnMatchingCacheRemove);
}


//This method is called every 600 seconds
public void CacheItemMatchingRemoved(string k, object v, CacheItemRemovedReason r)
{
    using (MyEntities context = new MyEntities())
    {
        var qMatching = from m in context.MY_TABLE
                        where m.IsNew == true
                        select m;

        if (qMatching.Any())
        {
            MatchingController matchingController = new MatchingController();
            matchingController.SendEmail();
        }
    }

    // re-add our task so it recurs
    AddMatchingTask(k, Convert.ToInt32(v));
 }

The SendEmail() method should create the body of the email, getting the view and putting it in an HTML string to send

public void SendEmail()
{
     /* [...] Construct a model myModel */
     
     /* Then create the body of the mail */
     string htmlContent = RenderRazorViewToString("~/Views/Mailing/MatchingMail.cshtml", myModel);  
}
    

Here, RenderRazorViewToString() (the method's body is given at the top of this post) fails at this line :

var viewResult = ViewEngines.Engines.FindPartialView(ControllerContext, viewName);

ControllerContext cannot be null

Why, in this case only, ControllerContext is null ? I have read this post, but if I understood it correctly, this is because I manually instantiated my Controller writing :

MatchingController matchingController = new MatchingController();

However, I don't know how to proceed otherwise...

Any help will be very appreciated.
Thank you


Solution

  • Instead of trying to simulate a web hit, you can actually trigger a web hit to allow you to render the view with proper context. Take the result and store it into the body of your email. I had to do something similar for insurance quotes. Here is my code, followed by an adaptation for your needs.

        public ActionResult StartInsuranceQuote()
        {
    
            using (var client = new WebClient())
            {
                var values = new NameValueCollection
                {
                    { "sid", DataSession.Id.ExtractSid() }
                };
                client.UploadValuesAsync(new Uri(Url.AbsoluteAction("QuoteCallback", "Quote")), values);
            }
            return PartialView();                
        }
    

    The key to this would be populating the values collection from your model. Since you didn't provide that I'll assume some of the properties for illustration:

        public void SendEmail(YourViewModel model)
        {
            using (var client = new WebClient())
            {
                var values = new NameValueCollection
                {
                    { "Name",  model.Name },
                    { "Product", model.Product },
                    { "Color", model.Color },
                    { "Comment", model.Comment }
                };
                string body = client.UploadValues(new Uri(Url.AbsoluteAction("GenerateBody", "RenderEmail")), values);
    
                // send email here
            }
        }
    

    RenderEmailController:

        public ActionResult GenerateBody()
        {
            return View();
        }
    

    GenerateBody.cshtml:

    @foreach (string key in Request.Form.AllKeys)
    {
        Response.Write(key + "=" + Request[key] + "<br />");
    }
    

    UPDATED: AbsoluteAction is an extension method, included below

    public static string AbsoluteAction(this UrlHelper url, string actionName, string controllerName, object routeValues = null)
    {
        if (url.RequestContext.HttpContext.Request.Url != null)
        {
            string scheme = url.RequestContext.HttpContext.Request.Url.Scheme;
            return url.Action(actionName, controllerName, routeValues, scheme);
        }
        throw new Exception("Absolute Action: Url is null");
    }