ASP.NET MVC 电子邮件

13

有没有一种解决方案可以使用ASP.NET MVC视图生成电子邮件模板而无需费力地跳过障碍。

让我详细说明费力跳跃的部分。

var fakeContext = new HttpContext(HttpContext.Current.Request, fakeResponse);
var oldContext = HttpContext.Current;
HttpContext.Current = fakeContext;
var html = new HtmlHelper(new ViewContext(fakeControllerContext,
  new FakeView(), viewDataDictionary, new TempDataDictionary()),
  new ViewPage());
html.RenderPartial(viewName, viewData, viewDataDictionary);
HttpContext.Current = oldContext;

以上代码使用当前HttpContext伪造一个新的Context并使用RenderPartial渲染页面,我们不应该这样做。

另一个非常详细的解决方案使用ControllerContext和.Render: (IEmailTemplateService, Headers/Postback WorkAround) 但基本上是用更多的代码做同样的事情。

我正在寻找一些只需呈现视图而无需POST / GET并生成一个简单的字符串,我可以通过我的邮件代码发送的东西。 一些不会遇到诸如两次发布标题或伪造某些数据的错误的东西。

例如:

//code which does not fire Render, RenderPartial... etc
var email = emailFramework.Create(viewData, view); 

请看下文中的解决方案或者点击此链接:

我使用Spark实现的解决方案:(2009年12月30日)ASP.NET MVC电子邮件模板解决方案


这就是 MS 引擎的工作方式,我们无法改变它;那么为什么要费心呢?解决方案不起作用吗?如果它能够正常工作,那就直接使用它。有很多事情我们必须克服;例如我最近需要将 lambda 传递给自定义属性;但我不能这样做;所以我只能传递字符串并接受它。这个世界并不完美。 - queen3
1
我知道ASP.NET MVC是从它的老兄弟ASP.NET发展而来的,因此存在一些缺点。然而,作为开发人员,有时候提出问题是很重要的,即使对某些人来说这些问题可能毫无意义...也许,这个框架就是要这样工作,也可能不是。我不知道这一点,因为我没有编写它,我只是在问它是否可以按照我认为应该的方式工作。对不起,我只是好奇。编辑:我也非常感兴趣其他解决方案 :) - Andrew
7个回答

11

我希望ASP.NET MVC ViewEngine能够做到这一点,但它在Spark中实现了,只需按照下面最新的链接即可。

更新(12/30/2009)更简洁的版本:ASP.NET MVC电子邮件模板解决方案


(11/16/2009) 或者,Louis DeJardin控制台应用程序版本:

using System;
using Spark;
using Spark.FileSystem;

public class User
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public abstract class EmailView : AbstractSparkView
{
    public User user { get; set; }
}

class Program
{
    static void Main(string[] args)
    {
        // following are one-time steps

        // create engine
        var settings = new SparkSettings()
            .SetPageBaseType(typeof(EmailView));

        var templates = new InMemoryViewFolder();
        var engine = new SparkViewEngine(settings)
                     {
                         ViewFolder = templates
                     };

        // add templates
        templates.Add("sample.spark", @"Dear ${user.Name}, This is an email.Sincerely, Spark View Engine http://constanto.org/unsubscribe/${user.Id}");

        // following are per-render steps

        // render template
        var descriptor = new SparkViewDescriptor()
            .AddTemplate("sample.spark");

        var view = (EmailView)engine.CreateInstance(descriptor);
        view.user = new User { Id = 655321, Name = "Alex" };
        view.RenderView(Console.Out);
        Console.ReadLine();
    }
}

我决定使用这种方法,因为它似乎是做对所有事情的唯一方法,它有以下优点:

  • 不使用任何HttpContext / ControllerContext或干扰路由数据!
  • 可以实现页眉/页脚以允许模板!
  • 您可以使用循环,条件语句等……
  • 它干净、轻量级,特别适合计划完全转移到Spark视图引擎的情况!

请务必阅读以下帖子。所有荣誉归Louis DeJardin所有,请参阅他的教程:): 使用Spark作为通用模板引擎!重新审视电子邮件模板


如果您已经像我们一样使用Spark作为视图引擎,那么这将非常有效。谢谢! - Sean Chambers

8
为什么需要从视图创建电子邮件?为什么不使用普通的模板文件?我经常这样做 - 我创建一个模板,并使用来自城堡项目的 NVelocity 引擎(不要与 nvelocity VIEW 引擎混淆)来呈现模板。
示例:
var nvEngine = new NVelocityEngine();
nvEngine.Context.Add("FullName", fullName);
nvEngine.Context.Add("MallName", voucher.Mall.Name);
nvEngine.Context.Add("ConfirmationCode", voucher.ConfirmationCode);
nvEngine.Context.Add("BasePath", basePath);
nvEngine.Context.Add("TermsLink", termsLink);
nvEngine.Context.Add("LogoFilename", voucher.Mall.LogoFilename);

var htmlTemplate = System.IO.File.ReadAllText(
    Request.MapPath("~/App_Data/Templates/Voucher.html"));

var email = nvEngine.Render(htmlTemplate);

NVelocityEngine类是我编写的一个包装器,用于封装由Castle项目提供的NVelocity端口,如下所示:

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using NVelocity;
using NVelocity.App;

namespace MyProgram
{
    /// <summary>
    /// A wrapper for the NVelocity template processor
    /// </summary>
    public class NVelocityEngine : VelocityEngine
    {
        Hashtable context = new Hashtable();

        /// <summary>
        /// A list of values to be merged with the template
        /// </summary>
        public Hashtable Context
        {
            get { return context; }
        }

        /// <summary>
        /// Default constructor
        /// </summary>
        public NVelocityEngine()
        {
            base.Init();
        }

        /// <summary>
        /// Renders a template by merging it with the context items
        /// </summary>
        public string Render(string template)
        {
            VelocityContext nvContext;

            nvContext = new VelocityContext(context);
            using (StringWriter writer = new StringWriter())
            {
                this.Evaluate(nvContext, writer, "template", template);
                return writer.ToString();
            }
        }
    }
}

这样一来,您就不必干预视图引擎,理论上您甚至可以将其与ASP.NET视图引擎链接起来,就像我在以下控制器方法中所做的那样:

public ActionResult ViewVoucher(string e)
{
    e = e.Replace(' ', '+');
    var decryptedEmail = CryptoHelper.Decrypt(e);
    var voucher = Voucher.FindByEmail(decryptedEmail);
    if (voucher == null) return View("Error", new Exception("Voucher not found."));

    var basePath = new Uri(Request.Url, Url.Content("~/")).ToString();
    var termsLink = new Uri(Request.Url, Url.Action("TermsGC", "Legal")).ToString();
    basePath = basePath.Substring(0, basePath.Length - 1);

    var fullName = voucher.FirstName;
    if (!string.IsNullOrEmpty(voucher.LastName))
        fullName += " " + voucher.LastName;

    var nvEngine = new NVelocityEngine();
    nvEngine.Context.Add("FullName", fullName);
    nvEngine.Context.Add("MallName", voucher.Mall.Name);
    nvEngine.Context.Add("ConfirmationCode", voucher.ConfirmationCode);
    nvEngine.Context.Add("BasePath", basePath);
    nvEngine.Context.Add("TermsLink", termsLink);
    nvEngine.Context.Add("LogoFilename", voucher.Mall.LogoFilename);

    var htmlTemplate = System.IO.File.ReadAllText(
        Request.MapPath("~/App_Data/Templates/Voucher.html"));

    return Content(nvEngine.Render(htmlTemplate));
}

谢谢,这很有趣,我现在会更深入地了解它。 示例: http://www.castleproject.org/others/nvelocity/usingit.html#step3循环: http://www.castleproject.org/others/nvelocity/improvements.html#fancyloops - Andrew
嗨,Chris,那么在简单实现之后,你会如何处理子模板或主模板?我尝试了 #path("master.vm") 但它不起作用... - Andrew
最初的设计不支持嵌套模板,但我想您可以修改 NVelocity 类的 Render 方法以递归地呈现模板,以符合您的需要。另外,您也可以下载 Castle 源代码并修改核心速度引擎代码,但那似乎有些过头了,而且应该更容易通过包装器类来扩展它。 - Chris
太棒了!我只是建议隐藏上下文并提供一个AddContext方法。信息隐藏,迪米特法则等等。 - G-Wiz

7

尝试使用Spark视图引擎(http://www.sparkviewengine.com/)。它易于使用,优于标准引擎,并且不需要伪造上下文。

您也可以使用此答案的函数Render a view as a string ,但需要伪造上下文。这是标准视图引擎的工作方式,您无法做任何更改。

这是我的扩展类,用于将视图生成为字符串。第一个适用于标准视图引擎,第二个适用于Spark:

public static class ControllerHelper
{
    /// <summary>Renders a view to string.</summary>
    public static string RenderViewToString(this Controller controller,
                                            string viewName, object viewData)
    {
        //Getting current response
        var response = HttpContext.Current.Response;
        //Flushing
        response.Flush();

        //Finding rendered view
        var view = ViewEngines.Engines.FindPartialView(controller.ControllerContext, viewName).View;
        //Creating view context
        var viewContext = new ViewContext(controller.ControllerContext, view,
                                          controller.ViewData, controller.TempData);

        //Since RenderView goes straight to HttpContext.Current, we have to filter and cut out our view
        var oldFilter = response.Filter;
        Stream filter = new MemoryStream(); ;
        try
        {
            response.Filter = filter;
            viewContext.View.Render(viewContext, null);
            response.Flush();
            filter.Position = 0;
            var reader = new StreamReader(filter, response.ContentEncoding);
            return reader.ReadToEnd();
        }
        finally
        {
            filter.Dispose();
            response.Filter = oldFilter;
        } 
    }

    /// <summary>Renders a view to string.</summary>
    public static string RenderSparkToString(this Controller controller,
                                            string viewName, object viewData)
    {
        var view = ViewEngines.Engines.FindPartialView(controller.ControllerContext, viewName).View;
        //Creating view context
        var viewContext = new ViewContext(controller.ControllerContext, view,
                                          controller.ViewData, controller.TempData);

        var sb = new StringBuilder();
        var writer = new StringWriter(sb);

        viewContext.View.Render(viewContext, writer);
        writer.Flush();
        return sb.ToString();
    }
}

嗨LukLed,我熟悉Spark视图引擎并曾考虑过此解决方案,你可能是对的,这最终可能是最好的方法。最终我可能会被迫走上这条路。然而,我觉得在我的项目中添加第二个视图引擎只是在本应该是简单解决方案的基础上添加代码,实际上并没有真正解决原始问题,而只是在其周围工作。你提到的另外两种方法正是我想避免的。+1但肯定有更好的解决方案:( - Andrew
2
有一个解决方案 - 使用绕过Web表单引擎的解决方法 - 单个视图引擎 - Spark ;-) - queen3
视图引擎的工作是接收视图数据和视图/模板。生成其字符串(html/xml等)表示形式,并通过POST发送回浏览器。但是,这些是两个独立的部分。渲染与数据传输方式无关... - Andrew
在Spark视图引擎中,您可以直接呈现到流。在标准引擎的情况下,没有其他解决方案。查看Reflector,您会发现标准实现是设计为直接输出内容到HttpContext.Current.Response。虽然IView的Render方法有两个参数(ViewContext viewContext,TextWriter writer),但第二个参数被忽略,而是使用HttpContext.Current.Response。 - LukLed

4
如果您想进行简单的文本替换,.NET有相关功能:
        ListDictionary replacements = new ListDictionary();

        // Replace hard coded values with objects values
        replacements.Add("{USERNAME}", "NewUser");            
        replacements.Add("{SITE_URL}", "http://yourwebsite.com");
        replacements.Add("{SITE_NAME}", "My site's name");

        string FromEmail= "from@yourwebsite.com";
        string ToEmail = "newuser@gmail.com";

        //Create MailDefinition
        MailDefinition md = new MailDefinition();

        //specify the location of template
        md.BodyFileName = "~/Templates/Email/Welcome.txt";
        md.IsBodyHtml = true;
        md.From = FromEmail;
        md.Subject = "Welcome to youwebsite.com ";

        System.Web.UI.Control ctrl = new System.Web.UI.Control { ID = "IDontKnowWhyThisIsRequiredButItWorks" };

        MailMessage message = md.CreateMailMessage(ToEmail , replacements, ctrl);

        //Send the message
        SmtpClient client = new SmtpClient();

        client.Send(message);

欢迎文件(Welcome.txt)

    Welcome - {SITE_NAME}<br />
    <br />
    Thank you for registering at {SITE_NAME}<br />
    <br />
    Your account is activated and ready to go! <br />
    To login, visit <a href="{SITE_URL}">{SITE_NAME}</a> and use the following credentials:
    <br />
    username: <b>{USERNAME}</b><br />
    password: use the password you registered with
    <br />
    <br />

    - {SITE_NAME} Team

再次说明,这仅适用于简单的字符串替换。如果您计划发送更多数据,您需要正确格式化它,然后再进行替换。


你尝试在ASP.NET MVC应用程序中使用过这个吗?这段代码会抛出一个异常。 - Kirschstein
2
我只在ASP.NET MVC中使用它。抛出了什么异常? - Omar
我也使用同样的方法没有问题。真正重要的是在CreateMailMessage的“owner”选项中添加“new System.Web.UI.Control()”,因为在MVC中你没有其他东西可以放在那里。 - Bradley Mountford

3
您可以考虑使用MvcMailer NuGet - 它正是您所需的,而且做得很干净。请查看NuGet软件包此处项目文档
希望对您有所帮助!

1

我创建了一个重载LukLed的RenderSparkToString方法,使您可以在视图中使用火花布局:

public static string RenderSparkToString(this Controller controller,
                                        string viewName, string masterName, object viewData)
{
    var view = ViewEngines.Engines.FindView(controller.ControllerContext, viewName, masterName).View;
    //Creating view context
    var viewContext = new ViewContext(controller.ControllerContext, view,
                                      controller.ViewData, controller.TempData);

    var sb = new StringBuilder();
    var writer = new StringWriter(sb);

    viewContext.View.Render(viewContext, writer);
    writer.Flush();
    return sb.ToString();
}

我同意Andrew的观点。我希望使用Web表单视图引擎有更简单的方法来完成这个任务。


0

虽然这是一个有点老的帖子,但我鼓励您看一下MvcMailer NuGet包 - 它极大地简化了整个过程,并使邮件发送器表现得更好。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接