在过去Win32编程时代,我们看到的程序界面都是由静态编程语言,从一个按钮的尺寸到布局,一行行地绘制出来。比如现在我们创建一个.Net WinForm窗体,打开其对应的designer.cs文件,就会看到长篇累牍窗体绘制代码。
打从VB和Delphi出现后,尽管通过界面设计器自动生成代码成为主流,然而随之互联网时代的发展,我们需要丰富多样的软件界面,更灵活地应对需求变动。比如一个软件往往有多种界面,包括C/S和B/S,C/S中包括Windows/Linux/IPhone,B/S包括IE/Firefox/Chrome。无论是先设计业务,还是先构勒界面,最好能独立、清晰、明确的进行。更不想影响业务逻辑。将业务逻辑和用户界面分离,实现低耦合,变得势在必行。
对于三层架构的概念,相信就是初学者也耳熟能详了。对于数据访问层(DAL)和业务逻辑层(Business Logic Layer)之间的分离,比较容易理解,也不难实现。但对于业务逻辑层和界面层(UI)的分离,就不是那么泾渭分明,至少我在很长时间里都比较模糊。
这一方面由于理念认识的距离,一方面由于以前的框架没给予有力支持。
我主要从事的是.Net Web开发,闲暇里也做一些C/S程序练练手。在初学编程时,我更倾向C/S开发,因WinForm中用的是统一的强类型语言,而B/S或Asp.Net开发中,众所周知,还需要掌握HTML/CSS/JavaScript。但随之这些技能障碍被逐渐克服,以及Asp.Net开发经验的积累,我的认识发生颠覆性变化,感觉开发Asp.Net要比WinForm更清晰,更容易把握,至少不会为改一个字体或效果重新发布整个程序,除非你把这些植写到配置文件中。
可如果你运用了像Asp.Net那样的视图分离技术,相当于,一切UI皆可配置,而且还可以内容与样式分离,只修改样式时可以避免修改页面导致的自动编译,只要更新CSS即可。
视图分离另一个好处是直观,无须费神地分析一大段代码,只要瞄一眼视图模板的结构,一切就了然于心。无论是设计、开发或维护,都可以大大提高效率。正如下面语句,
var content = string.Format(@"Dear {0}, Thank you for {1}. {2}", receiver, thing, sender);
要优于:
var content = "Dear " + receiver + ",\r\nThank you for " + thing + ".\r\n" + sender;
在过去,应该是在2.0以前,Asp.Net只有页面,还有很多人固执地以C/S开发的思想做B/S,在页面后端类中生成控件,甚至直接输出html文本。 而随着MVC、Razor等技术的推广,页面的概念逐渐被模板取代。WPF和Silverlight技术,就是Asp.Net的视图分离思想在C/S开发上的移植。时至今日,视图分离技术取得了压倒性的主流地位。
还可以再兴几个视图分离技术的例子。
Visual Studio集成的ADO.Net Entity设计器(请参考园子的AEF专题):
Visual Studio集成的工作流设计器(请参考麒麟同学的WF专题):
RDLC格式报表(请参考MSDN演练:创建 ReportViewer 报表):
这些视图都是基于扩展的XML定义。Html自然还是最常见XML扩展。随着浏览器差异的缩小,当你掌握Html/CSS后,开发B/S系统体验要更胜于C/S。
视图分离已不只是一门技术,而是软件设计中的重要思想,它分离了界面与业务逻辑的耦合,用途非常广泛。
最近开发一些系统服务,服务会定时做一些数据存取和分析工作,然后要将运行结果用邮件发送出去。邮件内容,要反映服务执行过程的细节。开始时做完是先定一个StringBuilder,每执行完一步,就写入执行结果,这样对简单的业务流程没问题。但实际上由于有些执行流程很长,分支很多,生成邮件内容的代码充斥各个角落,写起来很烦,维护时的可读性也很差。有时邮件格式要求稍为复杂一点,比如包含表格,开发负担会更重。 而如果最终呈现的邮件内容在在一些内在逻辑(比如对数据分析后,如果满足一定条件就生成一个报表),那就非常繁琐,会弄得你抓狂。
常言道,穷则思变,我把目光瞄向了视图分离。下面,将介绍邮件生成将如何运用视图分离的技术。
第一种方式很简单,也很好用,就是做一个邮件文本模板,将动态的要运行时生成的内容,用特殊符号标记。其实和String.Format方法的原理一样,不过待替换的字段最好不用{0}、{1}这样编号,而是用名称比如{地址}、{电话}等,可读性更好。在具体应用中,只要将动态内容以键值对形式添加到Dictionary中,而无须关心顺序问题。
这种方式用得很多,我看到普遍的问题是,大家都忽视了对替换内容与模板之间匹配的验证。Dictionary中的各个键,应该与模板中被替换的字段一致,不能多不能少,否则应该抛出异常,这可以有效提高系统可维护性。
MailBuilder类是用于创建内容基于模板的邮件,下面是它的属性:
public class MailBuilder { /// <summary> /// The fields to be replaced. /// </summary> protected Dictionary<string, string> fields = new Dictionary<string, string>(); /// <summary> /// The resources embedded in the mail. /// </summary> protected List<LinkedResource> resources = new List<LinkedResource>(); /// <summary> /// The attachments to be sent out with the mail. /// </summary> protected List<Attachment> attachments = new List<Attachment>(); protected SmtpClient client = new SmtpClient(); #region Properties /// <summary> /// SMTP server location. /// </summary> public string Host { get; set; } /// <summary> /// Smtp credentials. /// </summary> public ICredentialsByHost Credentials { get; set; } /// <summary> /// Smtp port. /// </summary> public int Port { get; set; } /// <summary> /// Content of mail template. /// </summary> public string Template { get; set; } string tempFile; /// <summary> /// File path of mail template. /// </summary> public string TemplateFile { get { return tempFile; } set { tempFile = value; this.Template = File.ReadAllText(tempFile); } } /// <summary> /// Subject of mail /// </summary> public string Subject { get; set; } /// <summary> /// FROM address /// </summary> public string From { get; set; } /// <summary> /// TO addresses /// </summary> public string[] To { get; set; } /// <summary> /// CC addresses /// </summary> public string[] CC { get; set; } /// <summary> /// BCC addresses /// </summary> public string[] Bcc { get; set; } /// <summary> /// Priority /// </summary> public MailPriority Priority { get; set; } /// <summary> /// Indicates result message for sending email. /// </summary> public string Result { get; private set; } /// <summary> /// Get or sets the reply to address of the mail message. /// </summary> public string ReplyTo { get; set; } /// <summary> /// Indicate whether need to return receipt. If needed, the ReplyTo value should be set first. /// </summary> public bool ReturnReceipt { get; set; } #endregion …… …… }
这个类除有添加替换字符串AddField方法外,还支持添加DataTable和实体集合,将被转换为相应的Html表格内容。
/// <summary> /// Add a key-value pair /// </summary> /// <param name="key">key's name</param> /// <param name="value">value</param> public void AddTable(string key, DataTable dt) /// <summary> /// Add entities to render them as a html table. /// </summary> /// <typeparam name="T">Entity type</typeparam> /// <param name="key"></param> /// <param name="entities">Entities to be rendered</param> /// <param name="columns">The corresponding html table's columns.</param> /// <param name="rowValues">Function to get values of a row from an entity.</param> public void AddEntities<T>(string key, IEnumerable<T> entities, string[] columns, Func<T, object[]> rowValues)
经过一段时间的使用,感觉还是相当顺手的,因此觉得拿出晒晒,希望大家不吝赐教。
纯文本邮件模板虽然简单方便,但对于实现逻辑复杂一点的邮件,就有点力不从心。比如邮件要示,未尾的提示,要根据前面的内容决定是否显示,我们总不能为一点差别用两个不同模板吧。于是,这种情况下我们面临新的课题—动态模板技术。其实这也不难,因为我们早就有合适的方案,就是老而弥坚XML + XSLT技术。
这种方式,模板换成了XSLT文件。XSLT不只可以转换XML数据,还可以将动态内容以参数方式传入,.Net甚至支持XSLT中嵌入Javascript和C#。不过不推荐嵌入C#调用外部函数,那会破坏视图的本质,一般前两种方式足够了。我倾向于XML传值,有层次感,贴一下转换的XSLT的代码:
/// <summary> /// Transform XSLT file to HTML. /// </summary> /// <param name="template">Template's content.</param> /// <param name="dicField">The fields to be filled in corresponding place of the template</param> public static string TransformXsl(string template, Dictionary<string, string> dicField) { var elements = from field in dicField select new XElement(field.Key, field.Value.Contains("<table") ? (object)XElement.Parse(field.Value) : (object)field.Value); var xe = new XElement("Root", elements); using (var writer = new StringWriter()) using (var reader = XmlReader.Create(new StringReader(template))) { var xslTransform = new System.Xml.Xsl.XslCompiledTransform(); xslTransform.Load(reader); xslTransform.Transform(xe.CreateReader(), null, writer); return writer.ToString(); } }