1

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

Community
  • 1
  • 1
AlexB
  • 7,302
  • 12
  • 56
  • 74

3 Answers3

2

B2K has the right idea - you need to initialize a web request by calling the application from the outside, so it will spin up a new HttpContext to generate the HTML.

You could use the advice here or here to create a background emailer.

One approach is to use the initial request to stringify your email and save it in the database for later mailing.

Alternatively, you could just install MvcMailer or Postal and use their solution.

NightOwl888
  • 55,572
  • 24
  • 139
  • 212
1

Accrding to MSDN :

Any public static (Shared in Visual Basic) members of this type are thread safe. Any instance members are not guaranteed to be thread safe.

The method fails, because the ControllerContext is not enumerated locally and MatchingController matchingController = new MatchingController(); doesn't take effect! So, what's real value for it? Considering that you are calling that in separate Methods(and sometimes threads), subsequently, you can't use such context relevant methods like one that is used, say, [ViewEngineCollection.FindPartialView()][2] since it can't use its controllerContext member (is null).

Solution:

You may wanna use a constructor in case of Application_Start and use the same method. something like this:

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

or using ViewContext.View instead of FindPartialView and rewrite some methods :(

Amirhossein Mehrvarzi
  • 18,024
  • 7
  • 45
  • 70
  • Thanks for your answer. However, the line with `new ControllerContext()` you suggest fails with the following error : "RouteData must contains an element called 'controller' with a non-empty string" (I traduce it in English but it is exactly this). Moreover, I'm not sure to understand your last sentence, could you explain it with more details ? I don't really understand what to rewrite... ? Sorry, I'm new with these "context" problem in MVC – AlexB Feb 08 '15 at 16:20
  • @AlexB http://stackoverflow.com/questions/1191958/get-virtualpath-of-a-view-using-viewcontext is one of its usability for `ViewContext.View` which you can alter your code based on this helper. – Amirhossein Mehrvarzi Feb 08 '15 at 16:48
  • It seems that ViewContext hasn't static property called View, so I instanciated a ViewContext variable and call the View property. However, it seems to be null and the method fails because of it :-( I don't really understand how to rewrite my RenderRazorViewToString method ; you'll think I very new about it (which is true), but have you a working code snippet to share ? – AlexB Feb 08 '15 at 17:33
  • @AlexB No problem, I'm interested in your question. I'll place an update based on this to my answer as soon as possible. Just let me to search :) – Amirhossein Mehrvarzi Feb 09 '15 at 07:00
  • Thank you for your interest Amir ! I finally found a solution following this link : http://stackoverflow.com/questions/7779824/using-c-sharp-webrequest-to-interact-with-an-asp-net-mvc-3-website I don't know if it's the best, but I solved my problem. Thanks again ! – AlexB Feb 09 '15 at 10:13
1

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");
}
B2K
  • 2,541
  • 1
  • 22
  • 34
  • Thank you for your answer. However, Url.AbsoluteAction doesn't seem to exist. I follow your idea and do some research about WebClient() and found this link : http://stackoverflow.com/questions/7779824/using-c-sharp-webrequest-to-interact-with-an-asp-net-mvc-3-website which helped me a lot. So your idea was good, thanks a lot ! – AlexB Feb 09 '15 at 10:11