汹涌澎湃汹涌澎湃汹涌澎湃汹涌澎湃



新 闻 邮 件

本章将设计和实现一个新闻邮件列表系统,允许用户订阅在线新闻邮件(newsletter),而管理员可以对新闻邮件内容进行管理。首先将介绍邮件列表能为像Beer House这样的网站带来哪些好处,然后对管理邮件列表的不同方面进行详述,尽量使邮件列表管理员的工作轻松一些。最后将开发一个功能强大的新闻邮件模块,并将它集成到Beer House网站中。

7.1 提出问题

“关系营销”是一种在20世纪70年代后期发展起来的,用于创建长期的、更有利可图的客户关系的途径。它可以促使客户从他们长期信任的供应商手中购买产品和服务。当有竞争产品或服务供客户选择时,这种关系的好处表现得最为明显;例如,Beer House会与许多其他类似的网站争夺客户。

Beer House网站采用了现代关系营销理念,允许客户创建一份个人资料(profile)并选择不同的设置来反映个人偏好。其中一个选择是接收定期发送的新闻邮件。在如今的竞争市场上,要对用户访问你的网站增加吸引力,而不是只是宣传新的网站内容。通常采用的形式是进行优惠券和促销活动的宣传。在Twitter和Facebook这样的社交网络中作有效宣传的效果就很好,不过直接将信息发送给现有客户会增加成功机会。很多研究表明,复合式营销策略——使用多个媒介影响客户——可大大提升用户回应率。

好的客户关系当中也包括了多久联系一次客户以及联系哪些客户。我们已经知道,通过个人资料可让客户加入邮件列表。我们还需要知道已发送的内容和发送的时间。最好不要过量发送促销电子邮件,因为这会使客户感到非常厌烦,最终导致失去客户。对于Beer House来说,这可以用前面章节中介绍的基本管理结构实现。因为Beer House的需求很简单,显示发送新闻邮件的日期就够了。在有着更多需求的场景中,需要运用一个调度机制,将要发送的新闻邮件排队并自动在所需的时间进行处理。

在前一版的Beer House中,一次只允许发送一份新闻邮件,仅从市场营销的角度看,这是有意义的。我们将保留这一功能,但会围绕发送新闻邮件对用户界面作更改。通过新闻邮件模块,可以研究如何在.NET中发送电子邮件、如何在后台线程上运行一个较长的进程以及如何使用AJAX监视长时间运行的后台任务的进度。尽管有很多种使用新闻邮件的方式,但本章将只介绍技术要求,而将制定有关新闻邮件的完整营销计划的详细内容留给在线营销书籍作讨论,如David Scott所写的New Rules of Marketing and PR一书(要了解有关该书的信息,可访问 wiley. com/WileyCDA/WileyTitle/productCd-0470379286. html)。

7.2 设计方案

在基本的新闻邮件模块需要解决一些问题,包括管理用户订阅、发送电子邮件和管理内容。前一版的Beer House利用了会员系统和个人资料系统跟踪了想从Beer House接收定期新闻邮件的注册用户。由于这是个非常简单又令人满意的方法,所以我们不准备改变它。发送电子邮件是个要用编程方式处理的复杂事情;幸运的是,.NET提供了大量创建和发送消息的类。特别是,通过遵循电子邮件RFC和支持多部分MIME邮件,可以非常方便地创建同时包含纯文本和HTML格式的电子邮件。每个用户也可以指定用自己喜欢的格式接收电子邮件,并将其存储为个人资料的一部分。

我们将使用前面章节中所使用的Manage/AddEdit管理体系结构完成像管理新闻邮件、发送新闻邮件并跟踪已发送的新闻邮件这些工作。一个重要的不同点在于使用后台线程和AJAX发送和监视新闻邮件的进度。由于发送电子邮件至列表会花很长时间(对于较大的列表而言),如果将此任务移交给后台线程处理可以更好地完成这一任务,那就不需要让页面等待这些电子邮件发送完成。Web服务器被设计为接收对内容的请求,并尽可能快地响应请求。尽管这一进程可管理许多长时间运行的任务,但请求通常会到达时间阈值和超时。这会使用户被挂起,不能确定进程成功或否。

7.2.1 创建和发送电子邮件

.NET框架中有一个包含用于管理电子邮件通信的类的名称空间——.Mail。尽管这个名称空间由60多个类组成,包括所有友元类和密封类(这些是不能直接访问的类),我们将只使用SMTPClient、MailMessage和MailAddress类。在深入了解如何使用这些类发送电子邮件之前,要作一些基本的配置设置。

web.config文件中的元素可以包含一些用于为名称空间中的对象进行配置的子元素,但这里我们将重点放在mailSetting元素上,它包含了一个内容,即smtp部分。简单邮件传输协议(SMTP,rfcs/rfc2554.html)是用于发送电子邮件的协议。邮局协议(POP-3,rfcs/rfc1939.html)是用于检查电子邮件的协议,这里不会涉及,因为Beer House不检查电子邮件。另外还需要一些配置,这样像会员资格控件和新闻邮件模块等可只发送电子邮件,而不必显式设置值。尽管添加这些值到web.config不是发送电子邮件所必需的,但这是个很好的存储位置,这样就不用在网站中需要发送电子邮件的每个位置以编程方式对其进行设置。这并不意味着也不能在运行时显式设置这些值。

SMTPClient类中有Host和From地址的属性。Host是发送电子邮件的服务器的DNS名或IP地址。From地址是用于发送邮件的电子邮件地址,可将其看作信封上的回复地址。电子邮件地址和电子邮件账户是不同的。通常,地址是账户的别名,SMTP协议并不关心地址是账户还是别名。

有许多开发人员询问发送电子邮件的问题,而他们的问题大多在于未在配置文件中配置mailSettings。主要问题是没有配置Host,因为他们不知道他们需要指定主机或不知道SMTP主机地址是什么。仅仅因为存在用于管理发送电子邮件的类,并不意味着它们自动了解通过哪台服务器发送电子邮件;必须在其他某个地方显式设置。对于像Beer House这样的小公司来说,其网站一般驻留在主机提供商所提供的一个共享托管环境中。通常,主机提供商将SMTP服务器地址作为安装或账户信息的一部分进行提供。在较大的公司中,通常需要遵循IT部门的SMTP策略,这包括了使用指定的SMTP服务器。而且,许多IT部门阻塞了SMTP,限制为只能使用指定的服务器。关键是知道网站使用哪个SMTP服务器,按如下所示将它添加到web.config文件中或在运行时进行设置:

7.2.2 管理服务器上的长时间操作

将成批的电子邮件发送给一个较长的接收人列表是很耗时的,可能会使任何用户界面甚至整个桌面应用程序处于停滞状态。Web页面被设计成一个请求并立即响应的模型。IIS或任何Web服务器都被设计为如果某请求处理时间过长就超时或终止它。这可使得服务器上的开销不会因为无限循环等因素而变得无法控制。它还可帮助抵抗网站可能会遭受到的拒绝服务或其他恶意攻击。较长的电子邮件收件人列表很容易导致到达超时阈值。

而在用户看来,查看空白网页时并没有得到积极的响应。通常,他们会有挫败感,认为做错了什么事,需要得到及时的反馈。幸运的是,我们可以使用中的后台线程传递这些长时间运行的任务和用AJAX向用户提供表明处理正在进行的反馈。

SMTPClient类构建了允许在后台线程上发送电子邮件的异步方法——SendAsync、SendAsyncCancel和SendCompleted。在发送单个邮件时,它们工作得很好,但如果尝试在发送完第一条邮件之前发送第二条邮件,则会抛出异常。这就是“阻塞调用”。Send方法同步发送电子邮件,意味着该方法使得应用程序在一条消息发送完之后才执行下一行代码。SendAsynch将发送进程移至另一线程,允许调用下一行代码。尽管SendAsynch听起来像是新闻邮件模块所需的解决方案,但实际并不是,因为发送进程已经在后台线程上。

由于新闻邮件会被一个接一个地发送至收件人列表,因此要利用SendAsynch这个并不完善的方法会有些困难。通过同步发送新闻邮件,我们可以准确地增加新闻邮件进度的状态、循环到下一收件人和发送下一电子邮件,而且肯定其会成功处理。发送新闻邮件的进程是在其自己的后台线程上进行的。

在.NET中,创建多线路应用程序是非常方便的;但管理多线程应用程序的实际运行方式的任务对于任何平台来说都是有困难的。System.Threading名称空间中有着大量构建此类应用程序的类。其基本步骤如下所示:

(1) 创建一个指向将在次线程中运行的方法的ThreadStart委托。该方法必须返回void,且不能接受任何输入参数。

(2) 创建一个Thread对象,在其构造函数中采用ThreadStart委托,可以为这个线程设置多个属性,如它的名称(如果需要调试线程并通过名称而非ID来标识它们,这个属性就有用)和优先级。特别是Priority属性,需要小心使用,因为它会严重影响整个应用程序的性能。它属于ThreadPriority类型,这是一个枚举类型,默认值为ThreadPriority.Normal,这就意味着主线程和次线程有同样的优先级,给进程指定的CPU时间会在它们之间平分。ThreadPriority枚举的其他值有AboveNormal、BelowNormal、Highest和Lowest。一般情况下,不要为后台线程将Priority属性的值指定为AboveNormal或Highest。而是应将该属性设置为BelowNormal,这样后台线程不会明显影响到主线程的速度,并且不会妨碍。

(3) 调用Thread对象的Start方法。这个线程启动后,可以通过创建它的线程控制其生命周期。例如,要改变线程的生命周期,可调用Abort方法来终止该线程(以异步方式),调用Join方法使主线程等待直到次线程完成,IsAlive属性返回一个布尔值,表明后台线程是否仍在运行。

下面的代码片断说明了如何启动ExecuteTask方法,该方法能用来在一个后台线程中执行一个长时间的任务:

' create and start a background thread

dim ts as new ThreadStart(Test)

Thread thread = new Thread(ts)

thread.Priority = ThreadPriority.BelowNormal

thread.Name = "TestThread"

thread.Start()

' main thread goes ahead immediately

...

' the method run asynchronously by the background thread

Public sub ExecuteTask()

' execute time consuming processing here

...

End sub

新闻邮件模块只需要一个相当简单的后台线程能有效运行。主要问题在于在用户界面线程和后台进程之间管理对共享数据的访问。ParameterizedThreadStart委托(它指向以对象作为参数的方法)使得向后台线程传递资源变得非常简单。由于对象可以是任何事物,所以可以传递一个自定义对象,把它的属性定义成参数,或者如果愿意也可传递一组对象。下面的代码片断展示了如何调用ExecuteTask方法,并且给它传递一组对象,其中第一个对象是字符串,第二个是整数(被包装在对象中),最后一个是DateTime类型的。ExecuteTask方法接受了对象参数,并把它强制转换成Object数组类型的一个引用,随后它提取单个值,把这些值强制转换成适当的类型,最后执行实际的处理:

' create and start a background thread with some input parameters

Dim parameters As Object() = New Object() {"val1", 10, DateTime.Now}

Dim pts As New ParameterizedThreadStart(ExecuteTask)

Dim thread As New Thread(pts)

thread.Priority = ThreadPriority.BelowNormal

thread.Start(parameters)

' main thread goes ahead immediately

...

' the method run asynchronously by the background thread

Private Sub ExecuteTask(ByVal data As Object)

' extract the parameters from the input data object

Dim parameters As Object() = DirectCast(data, Object())

Dim val1 As String = DirectCast(parameters(0), String)

Dim val2 As Integer = CInt(parameters(1))

Dim val3 As DateTime = DirectCast(parameters(2), DateTime)

' execute time consuming processing here



End Sub

多线程编程最大的一个问题是对共享资源进行同步访问。也就是说,如果有两个线程同时读写同一个变量,那么必须找到一个方法来同步这些操作,确保当一个线程正在写某个变量时另一个线程不能对该变量进行读写。如果不考虑这个因素,程序可能会产生一些不可预知的结果和奇怪的行为,可能会不定期地被锁定,甚至可能引起数据完整性问题。共享资源可能是当前方法作用域内的任何变量或字段,包括类级别的公有变量和私有变量以及静态变量。同步访问这些资源的最简单方法是通过lock语句。它接受一个非空对象(也就是一个引用类型,而非值类型),所有线程可以访问这个对象,该对象通常是一个类级别字段。该对象的类型不重要,因此许多开发人员只是使用根System.Object类型的一个实例。可以在类级别简单地声明一个对象字段,让它引用一个新对象,通过运行在不同线程上的方法使用它。程序代码一旦进入一个锁定阻塞,就必须在另一个线程进入锁定对同一变量进行操作之前退出。下面是一个示例:

Private lockObj As New Object()

Private counter As Integer = 0

Private Sub MethodFromFirstThread()

SyncLock lockObj

counter = counter + 1

End SyncLock

' some other work...

End Sub

Private Sub MethodFromSecondThread()

SyncLock lockObj

If counter >= 10 Then

DoSomething()

End If

End SyncLock

End Sub

然而在很多情况下,我们并不希望完全锁定共享资源来防止进行读写操作。即通常允许多个线程在同一时间读取同一资源,但当一个线程正在读写某个资源时其他任何线程都不能对该资源进行写操作(可以有多个读取操作,但只有独占的写操作)。可以使用ReaderWriterLock 对象来实现这种类型的锁,该对象的AcquireWriterLock 方法可以将它后边的代码保护起来,防止其他线程进行读写,直到调用了ReleaseWriterLock方法为止。如果调用AcquireReaderLock方法(不要将其与AcquireWriterLock混淆),那么另一线程将能进入其自己的AcquireReaderLock 阻塞来读取同一资源,但AcquireWriterLock会等待所有其他线程调用ReleaseReaderLock。下面的示例展示了当有两个不同的线程在读一个共享字段,而另一个线程在写该字段时,如何实现对该字段的同步访问:

Public Shared Lock As New ReaderWriterLock()

Private counter As Integer = 0

Private Sub MethodFromFirstThread()

Lock.AcquireWriterLock(Timeout.Infinite)

counter = counter + 1

Lock.ReleaseWriterLock()

' some other work...

End Sub

Private Sub MethodFromSecondThread()

Lock.AcquireReaderLock(Timeout.Infinite)

If counter >= 10 Then

DoSomething()

End If

Lock.ReleaseReaderLock()

End Sub

Private Sub MethodFromThirdThread()

Lock.AcquireReaderLock(Timeout.Infinite)

If counter 50 Then

DoSomethingElse()

End If

Lock.ReleaseReaderLock()

End Sub

在本网站中,有一个业务类通过运行后台线程来异步发送新闻邮件。发送完每个邮件后,更新多个服务器端变量,这些变量表示了要发送的邮件总数、已发送的邮件数、已发送邮件的百分比以及任务是否完成。随后,来自表示层的,即来自一个不同线程的Web页面将读取这些信息,更新客户端屏幕上的状态信息。由于这些信息是在两个线程之间共享的,因此需要同步访问,为此将使用ReaderWriterLock。

多线程编程是一个非常复杂的主题,对于代码设计需要深思熟虑,要让代码能更好地执行,并且不造成死锁,死锁可能会终止整个应用程序。除非确实需要,应避免创建过多的线程,因为操作系统和线程调度程序(帮助在现有线程之间分配CPU时间的操作系统部分)为了管理它们需要消耗CUP和内存。还有一些类没有进行介绍(如Monitor、Semaphore、Interlocked、ThreadPoll等),因为实现本模块的解决方案没有必要使用它们。

7.2.3 设计数据库表

在新闻邮件模块中只有一个表——tbh_Newsletter(见图7-1)。其中有一些保存主题、纯文本正文和HTML正文的字段和一个新字段DateSent(源自上一版本的DateSent)。剩下的字段是跟踪记录活动的标准字段。DateSent字段是可空的,保存实际发送新闻邮件的日期值。

[pic]

图 7-1

7.2.4 设计配置模块

与本网站中的其他模块一样,新闻邮件模块拥有自己的配置,被定义为web.config中下的元素的属性。该元素被一个NewsletterElement类映射,该类具有表7-1中所示的属性。

表 7-1

|属 性 |描 述 |

|ProviderType |具体提供程序类的完整名称(名称空间加类名),该类实现对某个具体数据存储的数据访问代码 |

|ConnectionStringName |web.config文件的新的部分中,包含了访问模块的数据库的连接字符串的项的名称|

|EnableCaching |一个布尔值,用来表示是否使用数据缓存 |

|CacheDuration |指定了如果没有任何插入、删除、更新等能够使缓存无效的操作,那么将把数据保存在缓存中的秒数 |

|FromEmail |新闻邮件发送者的电子邮件地址,也被用作回复地址 |

|FromDisplayName |新闻邮件发送者的显示名,它将由电子邮件客户端程序显示 |

|HideFromArchiveInterval |一个新闻邮件经过多少天之后才存档 |

|ArchiveIsPublic |一个布尔值,表示民意测验归档是任何人都可访问,还是只能注册用户访问 |

最上面的4个设置对所有模块都是通用的。你可能会想,发送者的电子邮件地址可以从web.config内置的部分读取出来,但通常设置为邮寄者或管理员的电子邮件地址,这个地址用于发送那些用于服务电子邮件,如新注册用户的确认邮件、丢失密码的电子邮件回寄等。在其他情况下,可能需要区分发送者的电子邮件地址,使用员工的地址信息,这样如果用户回复了电子邮件,其回复将被读取。在使用新闻邮件的情况下,可以指定一个特定的电子邮件账户,如newseditor@,供新闻邮件编辑人员使用。

ArchiveIsPublic属性与民意调查模块配置类中的名称类似的属性有相同的意义——它能使管理员决定存档的新闻邮件是否仅对注册用户可读;管理员可能希望将该属性设置为True作为吸引用户订阅的另一个理由。HideFromArchiveInterval属性也很重要,它用于确定发送的新闻邮件必须经过多少天之后才能存档。如果把该属性设为0,那么有些用户可能不计划订阅,只是偶尔到存档的邮件中看看。如果设成15(默认值),那么用户如果不想等上15天才能看到邮件的话就必须去订阅。

7.2.5 设计用户界面服务

设计阶段的最后部分是设计页面和用户控件,来组建模块的表示层。下面是用户界面文件列表,随后在"解决方案"部分将完成这些页面和用户控件:

● ~/Admin/AddEditNewsletter.aspx:此页面允许管理员或编辑人员给当前订阅者发送邮件。当这个页面第一次被载入时,如果另一个新闻邮件正在被发送,那么就提示一个错误信息并且显示一个到邮件发送进度页面的链接,而不是出现显示其他新闻邮件主题和正文的表单。还需要考虑这样的情况,即当页面载入时,还没有新闻邮件处于发送状态,但后来当用户单击Submit按钮来发送新闻邮件时,却发现已经有新闻邮件正在发送了,出现这种情况是由于该用户在他的浏览器上填写表单时,其他用户可能已经从另一个地方开始发送。这种情况下,不发送当前邮件,会向发件人显示一条信息来解释这种情况,并且保持显示当前新闻邮件数据的表单可视,使已经填写的数据不丢失,随后,当其他新闻邮件发送完成后再发送它。该页面还允许查看以前发送的新闻邮件,但不允许进行编辑。

● ~/Admin/ManageNewsletter.aspx:该页面只列出了所有已发送的新闻邮件。通过此页面不能删除新闻邮件,但它提供了查看新闻邮件内容的链接。

● ~/ArchivedNewsletters.aspx:此页面列出所有在最近x天中发送的新闻邮件,x的值在HideFromArchiveInterval自定义配置中设置。只有将web.config中下元素中的ArchiveISPublic设置设成true,匿名用户才能够访问此页面。新闻邮件列表将显示每个邮件发送的日期和它的主题。主题以超链接的方式显示,链接到能够显示邮件完整内容的页面上。

● ~/ShowNewsletter.aspx:此页面显示新闻邮件的正文(纯文本和HTML格式),在查询字符串中传递要显示邮件的ID。

● ~/Controls/NewsletterBox.ascx:此用户控件用于确定当前用户是否已登录,如果没有,则假定他没有注册并且没有订阅邮件,随后将显示一条消息邀请求该用户通过在文本框中输入电子邮件地址来订阅邮件。当用户单击Submit按钮时,此控件就让用户跳转到第4章开发的Register.aspx页面,在该页面的查询字符串中传递用户的电子邮件,以便将它读取出来并预先填写到电子邮件文本框中。如果用户是个会员并已经登录,那么控件将显示一条消息,提醒他可以通过进入EditProfile.aspx页面(同样也是在第4章开发的)来改变他的订阅类型或取消订阅。在这两种情况下,都会在控件的底部显示一个指向新闻邮件存档页面的链接。

● ~/NewsLetterService.asmx:这并不是个真正的用户界面页面,它用于发送新闻邮件并检查发送的新闻邮件的状态。

7.3 解决方案

在介绍了新闻邮件模块的新功能和独特功能之后,我们将把这些内容与之前模块的模式和功能相结合,创建一个能发挥作用的新闻邮件模块。

7.3.1 实现配置模块

可在应用程序的Config文件夹的类库的ConfigSection.vb文件中找到NewslettersElement自定义配置元素的代码,该文件还包含有映射了部分其他元素的类。下面是该类的代码,其中定义了在“设计方案”部分列出的属性、属性的默认值、属性是否为必需的以及它们与中对应属性的映射:

Public Class NewslettersElement

Inherits ConfigurationElement

_

Public Property ConnectionStringName() As String

Get

Return CStr(Me("connectionStringName"))

End Get

Set(ByVal value As String)

Me("connectionStringName") = value

End Set

End Property

Public ReadOnly Property ConnectionString() As String

Get

Dim connStringName As String

If String.IsNullOrEmpty(Me.ConnectionStringName) Then

connStringName =

Globals.Settings.DefaultConnectionStringName

Else

connStringName = Me.ConnectionStringName

End If

Return WebConfigurationManager.ConnectionStrings( _

connStringName).ConnectionString

End Get

End Property

_

Public Property ProviderType() As String

Get

Return CStr(Me("providerType"))

End Get

Set(ByVal value As String)

Me("providerType") = value

End Set

End Property

_

Public Property FromEmail() As String

Get

Return CStr(Me("fromEmail"))

End Get

Set(ByVal value As String)

Me("fromEmail") = value

End Set

End Property

_

Public Property FromDisplayName() As String

Get

Return CStr(Me("fromDisplayName"))

End Get

Set(ByVal value As String)

Me("fromDisplayName") = value

End Set

End Property

_

Public Property HideFromArchiveInterval() As Integer

Get

Return CInt(Me("hideFromArchiveInterval"))

End Get

Set(ByVal value As Integer)

Me("hideFromArchiveInterval") = value

End Set

End Property

_

Public Property ArchiveIsPublic() As Boolean

Get

Return CBool(Me("archiveIsPublic"))

End Get

Set(ByVal value As Boolean)

Me("archiveIsPublic") = value

End Set

End Property

_

Public Property EnableCaching() As Boolean

Get

Return CBool(Me("enableCaching"))

End Get

Set(ByVal value As Boolean)

Me("enableCaching") = value

End Set

End Property

_

Public Property CacheDuration() As Integer

Get

Dim duration As Integer = CInt(Me("cacheDuration"))

If duration > 0 Then

Return duration

Else

Return Globals.Settings.DefaultCacheDuration

End If

End Get

Set(ByVal value As Integer)

Me("cacheDuration") = value

End Set

End Property

_

Public Property URLIndicator() As String

Get

Dim lurlIndicator As String = Me("urlIndicator").ToString

If String.IsNullOrEmpty(lurlIndicator) Then

lurlIndicator = "Newsletter"

End If

Return lurlIndicator

End Get

Set(ByVal Value As String)

Me("urlIndicator") = Value

End Set

End Property

End Class

一个类型为NewslettersElement、名为Newsletters的属性被添加到同一文件TheBeer HouseSection中,它映射部分:

public class TheBeerHouseSection

Inherits ConfigurationSection

'other properties...

_

Public ReadOnly Property Newsletters() As NewslettersElement

Get

Return CType(Me("newsletters"), NewslettersElement)

End Get

End Property

End Class

现在可以利用web.config文件中元素的属性来配置模块。SenderEmail和SenderDisplayName是必需的,其他可选。下面的代码显示了如何配置这两个属性,以及怎样使存档公开(默认为非公开),并且指定邮件只有在发送10天后才能被存档:

7.3.2 实现数据访问层

和其他章节一样,新闻邮件模块(见图7-2)有一个映射到专用的实体数据模型NewsletterModel.emdx的表。相应的存储库(NewletterRepository)和实体扩展类也包括在其中,为Beer House应用程序添加所需的自定义行为。由于没有与tbh _ Newsletters表相关的表,对于向导所创建的模型唯一需要做的事情是将实体的名称改为Newsletter,将实体集的名称改为Newsletters。

[pic]

图 7-2

7.3.3 实现业务逻辑层

新闻邮件模块的业务逻辑层由存储库类、一个扩展的实体类、NewsletterStatus类、SubscriberInfo类和SubscriptionType枚举组成。该存储库包含了在前几章中讨论过的标准CRUD方法,它还添加了一些在后台线程上管理电子邮件发送的成员。Newsletter实体通过添加一些共享的成员进行扩展,这些成员用于在电子邮件处理中存储有关活跃的新闻邮件的信息。SubscriberInfo、NewsletterStatus和SubscriptionType枚举是新闻邮件处理的帮助类。

1. 扩展Newsletter实体

由Entity Data Model 向导中创建的Newsletter实体的扩展方法是使用部分类添加用于监视新闻邮件发送时的状态的共享(C#中的静态)成员。共享这些成员,是要使它们可以跨线程使用;值本身将不是为新闻邮件所存储的值。

第一个成员是ReaderWriterLock,用于跨线程锁定共享成员的值。ReaderWriterLock主要用于跨线程同步访问值(主要是读值)。当读或写任一Newletter共享成员时,就会获取该锁。我将在存储库一节中对此作详细讨论。

后四个成员是用于跟踪活跃的新闻邮件发送时的状态的属性。IsSending成员是一个表明是否有活跃的新闻邮件在发送的布尔值。TotalMails属性表明有多少电子邮件随活跃的新闻邮件发送。类似地,SentMails是在活跃期间发送的电子邮件数;每发送一封邮件,其值就加1。最后,PercentageComplete属性是个只读属性,通过将SentMails数与TotalMails值相除计算得到。如果没有TotalMail,也就是目前没有活跃的新闻邮件,则返回0。否则会导致除零异常。

Public Shared Lock As New ReaderWriterLock

Private Shared _isSending As Boolean = False

Public Shared Property IsSending() As Boolean

Get

Return _isSending

End Get

Set(ByVal value As Boolean)

_isSending = value

End Set

End Property

Public Shared ReadOnly Property PercentageCompleted() As Double

Get

If TotalMails = 0 Then

Return 0D

End If

Return CDbl(SentMails) * 100 / CDbl(TotalMails)

End Get

End Property

Private Shared _totalMails As Integer = -1

Public Shared Property TotalMails() As Integer

Get

Return _totalMails

End Get

Set(ByVal value As Integer)

_totalMails = value

End Set

End Property

Private Shared _sentMails As Integer = 0

Public Shared Property SentMails() As Integer

Get

Return _sentMails

End Get

Set(ByVal value As Integer)

_sentMails = value

End Set

End Property

2. 实现NewslettersRepository

通用的CRUD操作包括在NewslettersRepository中,采用之前讨论过的模式。该存储库包含了4个额外的成员,两个管理电子邮件发送,另外两个定制给用户的邮件。

HasPersonalizationPlaceholders方法检查字符串来确定文本中是否有自定义占位符。它使用一组正则表达式来查看是否有与文本中的个性化占位符相匹配的项。可向新闻邮件中添加4个个性化占位符:、、和。当将它们输入到邮件正文中时,占位符两侧会被添加字符。该方法还接受isHtml参数,表明字符串应评估为HTML还是纯文本,这是个很重要的区分。对字符进行HTML编码,将其分别转换成<和>,可以避免将相同字符用作HTML标记中的定界符所带来的分析混乱。

将字符串传递给该方法时,会将它与每个占位符的适当表达式相匹配。如果发现一个匹配,该方法立即返回true,而不检查其余占位符。由于该方法的主要目的是检查是否有占位符要替换,只要发现一个就返回true,从而减少处理器周期。

Private Shared Function HasPersonalizationPlaceholders(ByVal text

As String, ByVal isHtml As Boolean) As Boolean

If isHtml Then

If Regex.IsMatch(text, "<%\s*username\s*%>",

RegexOptions.IgnoreCase Or _

piled) Then

Return True

End If

If Regex.IsMatch(text, "<%\s*email\s*%>",

RegexOptions.IgnoreCase Or _

piled) Then

Return True

End If

If Regex.IsMatch(text, "<%\s*firstname\s*%>",

RegexOptions.IgnoreCase Or _

piled) Then

Return True

End If

If Regex.IsMatch(text, "<%\s*lastname\s*%>",

RegexOptions.IgnoreCase Or _

piled) Then

Return True

End If

Else

If Regex.IsMatch(text, "",

RegexOptions.IgnoreCase Or _

piled) Then

Return True

End If

If Regex.IsMatch(text, "",

RegexOptions.IgnoreCase Or _

piled) Then

Return True

End If

If Regex.IsMatch(text, "",

RegexOptions.IgnoreCase Or _

piled) Then

Return True

End If

If Regex.IsMatch(text, "",

RegexOptions.IgnoreCase Or _

piled) Then

Return True

End If

End If

Return False

End Function

如果发现任一占位符的匹配项,就调用ReplacePersonalizationPlaceholders方法。应注意,在调用ReplacePersonalizationPlaceholders之前不必调用HasPersonalizationPlaceholders,因为用于执行占位符替换的正则表达式将不会替换与占位符不匹配的任何文本。

再次用一组正则表达式处理文本,但不是查找匹配,而是用RegEx执行替换操作。正则表达式(.NET中的RegEx)可以基于模式而不是特定的字符串处理字符串和执行像替换这样的任务。使用Regex.Replace执行记号(toke)替换(像在ReplacePersonalizationPlaceholders方法中做的那样)要更理想,因为String.Replace方法并不与模式匹配,而与传递的确切字符串匹配。正则表达式的执行速度要比String类中Replace、Contains等方法快。Beginning Regular Expression(WileyCDA/WileyTitle/productCd-0764574892.html)是本不错的有关正则表达式的入门书籍。也是可用于学习.NET中正则表达式的主要在线资源之一。

Private Shared Function ReplacePersonalizationPlaceholders(ByVal

text As String, _

ByVal subscriber As SubscriberInfo, ByVal isHtml As

Boolean) As String

If isHtml Then

text = Regex.Replace(text, "<%\s*username\s*%>", _

subscriber.UserName, RegexOptions.IgnoreCase Or

piled)

text = Regex.Replace(text, "<%\s*email\s*%>", _

subscriber.Email, RegexOptions.IgnoreCase Or

piled)

Dim firstName As String = "reader"

If subscriber.FirstName.Length > 0 Then

firstName = subscriber.FirstName

End If

text = Regex.Replace(text, "<%\s*firstname\s*%>",

firstName, _

RegexOptions.IgnoreCase Or piled)

text = Regex.Replace(text, "<%\s*lastname\s*%>", _

subscriber.LastName, RegexOptions.IgnoreCase Or

piled)

Else

text = Regex.Replace(text, "", _

subscriber.UserName, RegexOptions.IgnoreCase Or

piled)

text = Regex.Replace(text, "", _

subscriber.Email, RegexOptions.IgnoreCase Or

piled)

Dim firstName As String = "reader"

If subscriber.FirstName.Length > 0 Then

firstName = subscriber.FirstName

End If

text = Regex.Replace(text, "",

firstName, _

RegexOptions.IgnoreCase Or piled)

text = Regex.Replace(text, "", _

subscriber.LastName, RegexOptions.IgnoreCase Or

piled)

End If

Return text

End Function

NewlettersRepository中的工作方法是SendNewsletter和SendEMails。SendNewsletter函数接受一个Newsletter对象并使用它创建一个后台线程来发送新闻邮件。首先,它在该新闻邮件上创建一个写锁定,初始化用于跟踪发送给收件人的新闻邮件的进度的值。调用AquireWriterLock方法创建了一个锁定,其中所有对Newsletter的共享成员的读写尝试将被锁定,该方法将等待下去直到其他锁定释放。写锁定通过调用ReleaseWriterLock释放。

在初始化共享成员后,创建一组参数来保存包含新闻邮件、主题、纯文本正文、HTML正文和请求的当前上下文的值。这些值作为泛型对象传递给ParameteriedThreadStart对象的构造函数;这样,一旦线程启动时,SendEmails方法就可使用这些值。

Public Shared Function SendNewsletter(ByVal vNewsLetter As

Newsletter) As Integer

If Not IsNothing(vNewsLetter) Then

Newsletter.Lock.AcquireWriterLock(Timeout.Infinite)

Newsletter.TotalMails = -1

Newsletter.SentMails = 0

Newsletter.IsSending = True

Newsletter.Lock.ReleaseWriterLock()

' send the newsletters asyncronously

Dim parameters() As Object = {vNewsLetter.Subject,

vNewsLetter.PlainTextBody, vNewsLetter.HtmlBody, HttpContext.Current}

Dim pts As New ParameterizedThreadStart(AddressOf

SendEmails)

Dim thread As New Thread(pts)

thread.Name = "SendEmails"

thread.Priority = ThreadPriority.BelowNormal

thread.Start(parameters)

End If

End Function

SendEmails方法执行将该新闻邮件的副本发送给所有订阅者的任务。它接受SendNewsletter方法传递的数据,解析它并用于构建邮件。在设置任意电子邮件之前,将TotalMails值设置为初始值0。下一步是替换任意个性化值。接着执行一个循环检查每个会员的个人资料,查看他们是否已经订阅以便接受新闻邮件;如果是,就为会员创建新的SubscriberInfo对象并添加到收件人列表中。在将收件人添加到列表中后,TotalMails值会递增。

Public Shared Sub SendEmails(ByVal data As Object)

Dim parameters() As Object = CType(data, Object())

Dim subject As String = CStr(parameters(0))

Dim plainTextBody As String = CStr(parameters(1))

Dim htmlBody As String = CStr(parameters(2))

Dim context As HttpContext = CType(parameters(3), HttpContext)

Newsletter.Lock.AcquireWriterLock(Timeout.Infinite)

Newsletter.TotalMails = 0

Newsletter.Lock.ReleaseWriterLock()

' check if the plain-text and HTML bodies have personalization

placeholders

' the will need to be replaced on a per-mail basis. If not,

the parsing will

' be completely avoided later.

Dim plainTextIsPersonalized As Boolean =

HasPersonalizationPlaceholders(plainTextBody, False)

Dim htmlIsPersonalized As Boolean =

HasPersonalizationPlaceholders(htmlBody, True)

最后一步是实际发送电子邮件。创建一个新的SMTPClient类。然后,循环订阅者列表,创建单独的邮件并发送给他们。不是使用web.config文件中的元素中定义的from地址,而是使用Newsletter自定义配置部分中定义的from地址。使新闻邮件从不同的地址发出而不是从整个网站的地址发出的原因是,想给它们指定一个特定的来源,达到专用的目的。

检查每个订阅者,查看他们请求哪种类型的电子邮件,纯文本还是HTML,并相应地指派邮件正文。如果订阅者希望是HTML邮件,那将IsBodyHTML属性设置为True,否则为false。也可以设置个性化占位符。这一信息存储在SubscriberInfo类中,这里使用的帮助类用于为每个订阅者存储必要信息。SubscriberInfo类有5个公有成员——Username、Email、FirestName、LastName和SubscriptionType,以及一个接受并设置所有这些值的构造函数。

' retreive all subscribers to the plain-text and HTML

newsletter

Dim subscribers As New List(Of SubscriberInfo)

Dim profile As ProfileBase = CType(context.Profile,

ProfileBase)

For Each user As MembershipUser In Membership.GetAllUsers

Dim userProfile As ProfileBase =

Helpers.GetUserProfile(user.UserName, False)

If Not Helpers.GetSubscriptionType(userProfile)

SubscriptionType.None Then

Dim subscriber As New SubscriberInfo(user.UserName,

user.Email, _

Helpers.GetProfileFirstName(userProfile), _

Helpers.GetProfileLastName(userProfile),

Helpers.GetSubscriptionType(userProfile))

subscribers.Add(subscriber)

Newsletter.Lock.AcquireWriterLock(Timeout.Infinite)

Newsletter.TotalMails += 1

Newsletter.Lock.ReleaseWriterLock()

End If

Next

在设置完所有MailMessage值后,使用SMTPClient的同步成员Send发送消息。每发送一条消息,SentEmails值就加1。在所有消息发送后,将isSending属性设置为false,允许发送其他新闻邮件。

' send the newsletter

Dim smtpClient As New Net.Mail.SmtpClient

For Each subscriber As SubscriberInfo In subscribers

Dim mail As New MailMessage

mail.From = New MailAddress(

Helpers.Settings.Newsletters.FromEmail, _

Helpers.Settings.Newsletters.FromDisplayName)

mail.To.Add(subscriber.Email)

mail.Subject = subject

If subscriber.SubscriptionType =

SubscriptionType.PlainText Then

Dim body As String = plainTextBody

If plainTextIsPersonalized Then

body = ReplacePersonalizationPlaceholders(body,

subscriber, False)

End If

mail.Body = body

mail.IsBodyHtml = False

Else

Dim body As String = htmlBody

If htmlIsPersonalized Then

body = ReplacePersonalizationPlaceholders(body,

subscriber, True)

End If

mail.Body = body

mail.IsBodyHtml = True

End If

Try

smtpClient.Send(mail)

Catch

End Try

Newsletter.Lock.AcquireWriterLock(Timeout.Infinite)

Newsletter.SentMails += 1

Newsletter.Lock.ReleaseWriterLock()

Next

Newsletter.Lock.AcquireWriterLock(Timeout.Infinite)

Newsletter.IsSending = False

Newsletter.Lock.ReleaseWriterLock()

End Sub

7.3.4 实现用户界面

在解决方案的最后一部分,将实现管理页面来发送新闻邮件和检查发送进度,还要实现一些最终用户页面,用来显示已存档的新闻邮件的列表和特定的新闻邮件的内容。最后是将插入到模板页面中的NewsletterBox用户控件,它创建了一个订阅框和一个指向存档页面的链接。

1. AddEditNewsletter.aspx页面

AddEditNewsletter.aspx页面和有着类似名称的页面的作用是一样的,允许管理员编辑单独的记录。不过区别也是较大的,因为它还可以将新闻邮件发送给订阅者和提供发送进度的实时反馈。给新闻邮件添加的一个功能就是可以保存新闻邮件而不发送它,这样可在后面重新获取它。

首先看一下该页面的布局。它有两个主面板,panSend和pan Wait。第一个面板包含了用于编辑新闻邮件或在新闻邮件发送后查看其内容的所有控件。如果有活跃的新闻邮件在发送,就会显示pan Wait,并提供不断的进度更新。页面加载事件处理程序检查是否有新闻邮件发送。如果是个编辑现有新闻邮件的请求,该页面就显示适当的编辑字段,并提供供编辑的存储信息。如果是个对新的新闻邮件的请求,而当前正在传输一个新闻邮件,则显示panWait面板,当前新闻邮件的进度开始更新。否则,显示一个空白页面用于创建新的新闻邮件。

Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs)

Handles Me.Load

If Not Me.IsPostBack Then

Dim isSending As Boolean = False

Newsletter.Lock.AcquireReaderLock(Timeout.Infinite)

isSending = Newsletter.IsSending

Newsletter.Lock.ReleaseReaderLock()

If NewsLetterId > 0 Then

BindNewsletter()

ElseIf isSending Then

ShowSending()

Else

ClearInfo()

End If

End If

txtHtmlBody.BasePath = Me.BaseUrl + "FCKeditor/"

End Sub

panSend包含了一组接受新闻邮件主题、纯文本正文和HTML正文的平行排列的控件(见图7-3)。对于每个输入控件,都有一个Literal控件在新闻邮件发送后显示相应值。HTML正文是使用FCKeditor创建的。该面板还包含了两个按钮,一个用于保存和提交新闻邮件,而另一个只保存新闻邮件。

[pic]

图 7-3

Fill the fields below with the newsletter’s subject,

the body in plain-text and

HTML format. Only the plain-text body is compulsory.

If you don’t specify the HTML

version, the plain-text body will be used for HTML

subscriptions as well.

The Subject field is

required.

The plain-text body is

required.

注意,主题和纯文本正文文本框还有RequiredFieldValidator控件,用来强制在保存或发送新闻邮件前在这两个字段中输入值。如果没有指定HTML正文,那就为HTML版本使用纯文本版本。Send按钮还带有显示确认对话框的JavaScript,要求用户确定想发送新闻邮件。这样,当用户只是想保存新闻邮件时,就不会导致意外地发送它。在实际中,如果将草稿意外地发送给订阅者,可能会导致一些问题。尽管确认对话框并不能保证万无一失,但它还是为这一问题提供了一个小小的保护层。

图7-4显示了对于已保存的新闻邮件如何呈现页面。文本框和FCKEditor被Literal控件取代,新闻邮件的纯文本和HTML版本以黄色背景显示。

[pic]

图 7-4

panWait包含了一系列DIV元素,当发送每份电子邮件并且SentMails值增加时,将用于显示进度条。dProgress DIV有一个由JavaScript在检查新闻邮件的进度时设置的文本值。进度条是通过AJAX驱动的脚本增加的,该脚本定期检查新闻邮件的进度并更改progressbar DIV元素的样式。

Another newsletter is currently being sent.

Please wait until it completes before

compiling and sending a new one.

Checking to see if sending a NewsLetter....

和我们在讨论文章评论时一样,可使用AJAX调用一个Web服务来监视新闻邮件的进度并更新progressbar和dProgress DIV。NewsletterService.asmx有一个成员GetNewsletterStatus,它使用JSON返回带有更新的值的NewsletterStatus对象。JSON是JavaScript Object Notation的缩写,是个轻量级的对象交换格式,通常与AJAX一起用于执行与服务器的通信。一般而言,Web服务使用较大的XML数据结构传输数据;而JSON使用简单的描述模式在客户机和服务器之间传输数据。RFC 4672中对其作了定义(mail-archive/web/ietf-announce/ current/msg02778.html)。也可通过学习有关JSON的知识。当在客户机和服务器间交换数据时, AJAX和.NET Web服务会自动使用JSON串行化和反串行化数据。

_

_

Public Function GetNewsLetterStatus() As NewsletterStatus

Dim lNewsletterStatus As New NewsletterStatus

Newsletter.Lock.AcquireReaderLock(Timeout.Infinite)

lNewsletterStatus.SentMails = Newsletter.SentMails

lNewsletterStatus.TotalMails = Newsletter.TotalMails

lNewsletterStatus.IsSending = Newsletter.IsSending

Newsletter.Lock.ReleaseReaderLock()

Return lNewsletterStatus

End Function

新闻邮件模块也有其自己的JavaScript文件,~/scripts/Newsletter.js。该文件调用GetNewsletterStatus方法并更新页面。函数UpdateStatus调用Web服务,并在对Web服务的调用结束后指派使用GetNewsletterStatus回调方法。

function UpdateStatus() {

NewsLetterService.GetNewsLetterStatus(GetNewsLetterStatusCompleted);

}

GetNewsletterStatusCompleted函数接受结果,其实际上是一个NewsletterStatus对象。NewsletterStatus是另一个帮助类,可以通过AJAX显式使用它将状态信息从服务器经由Web服务发送到页面。它是一个包含了4个属性的类,可以报告电子邮件数、总发送数、发送百分比以及当前是否正在发送新闻邮件。

Public Class NewsletterStatus

Private _isSending As Boolean = False

Public Property IsSending() As Boolean

Get

Return _isSending

End Get

Set(ByVal value As Boolean)

_isSending = value

End Set

End Property

Public ReadOnly Property PercentageCompleted() As Double

Get

If TotalMails = 0 Then

Return 0D

End If

Return CDbl(SentMails) * 100 / CDbl(TotalMails)

End Get

End Property

Private _totalMails As Integer = -1

Public Property TotalMails() As Integer

Get

Return _totalMails

End Get

Set(ByVal value As Integer)

_totalMails = value

End Set

End Property

Private _sentMails As Integer = 0

Public Property SentMails() As Integer

Get

Return _sentMails

End Get

Set(ByVal value As Integer)

_sentMails = value

End Set

End Property

End Class

该函数接受这些值,设置progressbar DIV的宽度百分比和dIsSending DIV的文本,表明新闻邮件的进度。最后,只要有新闻邮件被发送,该函数就设置一个定时器,每5秒调用一次UpdateStatus函数。根据实际情况,可将此时间调得更快或更慢些。对于只有少量订阅者的新闻邮件,1秒的间隔更合适,因为其处理不需要花多长时间。

function GetNewsLetterStatusCompleted(result) {

var lNewsletterStatus = result;

var percentage = lNewsletterStatus.PercentageCompleted;

var sentMails = lNewsletterStatus.SentMails;

var totalMails = lNewsletterStatus.TotalMails;

var isSending = lNewsletterStatus.IsSending;

if (totalMails < 0)

totalMails = '???';

var dIsSending = $get('dIsSending');

var progBar = $get('progressbar');

progBar.style.width = percentage + '%';

dIsSending.innerHTML = '' + percentage + '% Complete: '

'' + sentMails + ' out of ' + totalMails + '

emails have been sent.';

if (isSending) {

dIsSending.innerHTML = dIsSending.innerHTML + 'Currently

sending a NewsLetter.... ';

setTimeout(UpdateStatus, 5000);

} else {

dIsSending.innerHTML = dIsSending.innerHTML + '

Not sending a NewsLetter.... ';

}

}

使用像Fiddler()这样的工具,可以检查浏览器和服务器之间的数据通信情况。下面所示是从服务器返回的对状态更新请求进行响应的JSON。在这里,没有从客户端传递任何数据,因为GetNewsletterStatus没有接受任何参数。

{"d":{"__type":"TheBeerHouse.NewsletterStatus","IsSending":true,"Percen

tageCompleted":20,"TotalMails":20,"SentMails":4}}

当进度条展开时,显示了啤酒杯(见图7-5)。我想这要比普通的进度条更有趣。相应的样式定义了大小为32×32像素的啤酒杯背景图片,而且设置其为沿x或水平方向重复。

[pic]

图 7-5

由于DIV的宽度是由JavaScript进行扩展的,所以将显示越来越多的啤酒杯。对于此布局,进度条容器DIV的宽度被定义为544像素,恰好是17个啤酒杯的宽度。这样,当新闻邮件发送结束时,就不会显示不完整的啤酒杯,否则会使很多最终用户感到疑惑。这一技巧可用于任何进度条场景中,从而提供更有趣的进度条形式而不只是纯色或渐变色的进度条。

#progressbar

{

width: 0px;

height: 32px;

background-repeat: repeat-x;

background: url(images/glass1_small.jpg);

}

正如前面提过的,AddEditNewsletter页面不仅保存和发送新闻邮件,还可以存储新闻邮件草稿供在以后进行编辑和发送。区别在于设置表明新闻邮件实际发送日期的DateSent值。当加载新闻邮件时,检查该值,如果值为null,就显示文本框。如果有值,就显示文本。"单击"事件处理程序会调用SaveNewsletter方法并传递true或false值表明在将新闻邮件保存到数据库后是否发送它。如果该新闻邮件要发送,而另一新闻邮件已在发送,那就显示等待面板,但对该新闻邮件的更改会提交到数据库。如果该新闻邮件可发送,就存储它并开始发送。

Private Sub SaveNewsletter(ByVal bSendNow As Boolean)

Dim isSending As Boolean = False

If bSendNow Then

Newsletter.Lock.AcquireReaderLock(Timeout.Infinite)

isSending = Newsletter.IsSending

Newsletter.Lock.ReleaseReaderLock()

If isSending Then

bSendNow = False

panWait.Visible = True

panSend.Visible = False

End If

End If

Using lNewsletterrpt As New NewslettersRepository

Dim lNewsletter As Newsletter = lNewsletterrpt.AddNewsletter(

NewsLetterId, txtSubject.Text, _txtPlainTextBody.Text, txtHtmlBody.Value,

bSendNow)

If bSendNow And Not isSending Then

panWait.Visible = True

panSend.Visible = False

NewslettersRepository.SendNewsletter(lNewsletter)

ShowSending()

End If

End Using

End Sub

2. ArchivedNewsletters.aspx页面

ArchivedNewsletters页面显示了一个之前发送的新闻邮件的列表。这一列表显示了新闻邮件的主题和发送日期。主题链接至ShowNewsletterpage,传递NewsletterId。该列表被绑定到一个从存储库的GetNewsletters方法的重载版本(接受一个截止日期)检索的List(of Newsletter)。这一功能使得旧的新闻邮件可以随着时间推移慢慢退出列表。当然,可以用任何日期确保列表包括一个对发送的每份新闻邮件的引用。

Public Function GetNewsletters(ByVal vToDate As DateTime)

As List(Of Newsletter)

Return (From lNewsletter In Newsletterctx.Newsletters _

Where lNewsletter.DateSent < vToDate Order By

lNewsletter.DateSent Descending).ToList()

End Function

如果指定了仅有会员可查看已存档的新闻邮件,那未经身份验证的用户将重定向到网站的登录页面。这一检查操作是在页面加载事件中完成的,而不是作为网站的web.config中的安全性设置。匿名访问已存档的新闻邮件是个可选的设置,通过将其作为模块特定的设置,可以使管理更轻松一些。

Protected Sub Page_Load(ByVal sender As Object,

ByVal e As System.EventArgs) Handles Me.Load

' check whether this page can be accessed by anonymous

users. If not, and if the

' current user is not authenticated, redirect to the login page

If Not Me.User.Identity.IsAuthenticated AndAlso Not

Helpers.Settings.Newsletters.ArchiveIsPublic Then

Me.RequestLogin()

End If

If Not IsPostBack Then

BindArchivedNewsLetters()

End If

End Sub

ShowNewsletter.aspx页面在将新闻邮件内容的值绑定到页面之前,加入了同样的安全性检查。

3. NewsletterBox用户控件

NewsletterBox用户控件(~/Controls/NewsletterBox.ascx中)根据用户的身份验证状态显示相应的输出。如果用户是登录进网站的,就显示LoggedInTemplate,提供有关如何改变订阅状态的指示。它还有一个指向ArchivedNewsletter页面的链接。如果用户没有通过身份验证,那就显示一个简单的注册字段,告诉用户通过提供电子邮件地址来注册获取新闻邮件。在用户提交了他们的电子邮件地址后,就会要求用户使用Register.aspx页面创建他们的账户。由于LoginView会自动根据用户身份验证状态显示正确模板,所以不需要编写任何代码。

Newsletter

图7-6显示了上述代码基于用户订阅类型呈现的内容。

[pic]

图 7-6

7.4 小结

本章实现一个用于给已订阅邮件列表的注册用户发送新闻邮件的完整模块。该模块是通过后台线程(而不是处理请求的主线程)发送电子邮件的,这样就没有页面超时的风险,最重要的是不会给编辑人员留下一个会持续等待几分钟或更长的空白页面。为了给编辑人员提供一些有关邮件发送情况的反馈,AddEditNewsletter页面使用AJAX调用一个Web服务来更新进度条,并且每两秒钟就显示更新的状态信息。最后,最终用户可在一个存档页面中看到以前发送的邮件。

为了实现此模块,我们使用了一些高级功能,例如多线程编程、脚本回调,以及用SmtpClient和MailMessage类来编撰和发送电子邮件。不过,尽管此模块可良好地运行,但还有一些地方值得改进。下面列出了一些改进建议:

● 可以将附件与新闻邮件一起发送。如果想发送带有图片的HTML邮件,那这是非常有用的。目前,仅能通过在服务器上引用图片完整的URL来发送带有图片的电子邮件。

● 能够对新闻电子邮件的优先级进行设置。

● 对不同的主题有不同的邮件列表,例如聚会、新文章或商店里的新品。这就要求有更多的个人资料属性和一个扩展的SendNewsletter页面,这样就可以选择目标邮件列表。

● 可以更进一步扩展个性化占位符列表,例如,可以包括针对订阅者所在地区的占位符。还可以构建一个解析器来管理定义新闻邮件正文中的自定义标记,依照用户的个人资料,添加或排除他们的内容。例如,... 中的内容仅被包含在那些个人资料中选择Language属性为Italian的订阅者的新闻邮件中。或是...中包含的内容仅针对居住在New York州的订阅者。实现这个功能并不困难,它可让编辑人员创建特别有针对性的新闻邮件,这对于商业目的而言是特别重要的。

● 当发送邮件时,如果由于电子邮件地址无效,而没到达它的目的地,用户是不会得到错误或异常信息的。SMTP服务器做它的工作却不会让你知道结果。然而,如果邮件的目标地址不存在的话,通常会向发送方返回一个错误信息,说明由于地址不存在,邮件没发送成功。这些错误信息将发送给服务器的邮件管理员,然后再被转发给网站管理员。这时,当得到这样一条信息时,可以手动把账户的Newsletter个人资料属性设成none。然而,还有一个更好的、更自动化的方法,即写一个程序(可能是个Windows服务)通过对传入的信息进行分析来找到错误信息,然后自动执行退订操作。

● 创建一个新闻邮件队列系统,允许管理员在创建新闻邮件后,指定一个特定的时间进行发送。

● 扩展整个网站的用户界面,在每个页面上添加新闻邮件状态图像,这样新闻邮件的管理员可了解其进度而不必依赖新闻邮件的管理页面。

在前面几章中,已经开发了一些模块用于增强网站和用户之间的交流,如民意调查模块和本章的新闻邮件模块。在第8章中将实现一个论坛模块,这是用户之间进行交流的一种重要方式。

-----------------------

70

第 章

................
................

In order to avoid copyright disputes, this page is only a partial summary.

Google Online Preview   Download

To fulfill the demand for quickly locating and searching documents.

It is intelligent file search solution for home and business.

Literature Lottery

Related searches