Отобразить представление Razor в string => ControllerContext имеет значение null при вызове из повторяющейся задачи

Я использую ASP.NET MVC3
у меня есть .cshtml просмотреть, и я хочу, чтобы это было включено в текст сообщения электронной почты.
Вот метод, который я использую:

//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();
    }
}

Когда я вызываю этот метод из ActionResult метод, который вызывается из вызова Ajax, это прекрасно работает.

Однако я сталкиваюсь с необычной ситуацией:

В моем Global.asax файл, у меня есть метод, который вызывается каждые 10 минут, цель которого - проверить, были ли сделаны какие-то специальные записи в базе данных за последние 10 минут, и если да, отправляет электронное письмо. Конечно, тело письма - это строковое представление.

Вот часть моего кода: Этот метод очень вдохновлен этим постом

/* 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));
 }

SendEmail() Метод должен создать тело письма, получить представление и поместить его в строку HTML для отправки.

public void SendEmail()
{
     /* [...] Construct a model myModel */

     /* Then create the body of the mail */
     string htmlContent = RenderRazorViewToString("~/Views/Mailing/MatchingMail.cshtml", myModel);  
}

Вот, RenderRazorViewToString() (тело метода дано в верхней части этого поста) терпит неудачу в этой строке:

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

ControllerContext не может быть нулевым

Почему только в этом случае ControllerContext является null? Я прочитал этот пост, но если я правильно его понял, это потому, что я вручную создал запись моего контроллера:

MatchingController matchingController = new MatchingController();

Однако я не знаю, как поступить иначе...

Любая помощь будет очень ценится.
Спасибо

3 ответа

Решение

Вместо того, чтобы пытаться симулировать веб-попадание, вы на самом деле можете инициировать веб-попадание, чтобы позволить вам визуализировать представление в правильном контексте. Возьмите результат и сохраните его в теле письма. Мне пришлось сделать что-то подобное для страховых котировок. Вот мой код с последующей адаптацией под ваши нужды.

    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();                
    }

Ключом к этому будет заполнение коллекции значений из вашей модели. Так как вы не предоставили это, я приму некоторые свойства для иллюстрации:

    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 />");
}

ОБНОВЛЕНО: AbsoluteAction - это метод расширения, включенный ниже

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 правильная идея - вам нужно инициализировать веб-запрос, вызвав приложение извне, чтобы оно генерировало новый HttpContext для генерации HTML.

Вы можете использовать совет здесь или здесь, чтобы создать фоновый e-mail.

Один из подходов состоит в том, чтобы использовать первоначальный запрос, чтобы структурировать вашу электронную почту и сохранить ее в базе данных для последующей рассылки.

Кроме того, вы можете просто установить MvcMailer или Postal и использовать их решение.

По MSDN:

Любые открытые статические (Shared в Visual Basic) члены этого типа являются потокобезопасными. Любые члены экземпляра не гарантированно являются потокобезопасными.

Метод не работает, потому что ControllerContext не перечисляется локально и MatchingController matchingController = new MatchingController(); не вступает в силу! Итак, какова реальная ценность для этого? Учитывая, что вы вызываете это в отдельных методах (а иногда и в потоках), впоследствии вы не можете использовать такие контекстно-релевантные методы, как, например, тот, который используется, скажем, [ViewEngineCollection.FindPartialView()][2] так как он не может использовать его controllerContext член (есть null).

Решение:

Вы можете использовать конструктор в случае Application_Start и использовать тот же метод. что-то вроде этого:

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

или используя ViewContext.View вместо FindPartialView и переписать некоторые методы:(

Другие вопросы по тегам