2008年8月31日星期日

Repeater实现分页

首页页面的控件有如下几个

1.repeater1 repeater控件,对他进行分页

2.label1 用来显示和存储页码信息

3.label2 用来显示和存储总页码信息

4.linkButton1 首页

5.linkButton2 上一页

6.linkButton3 下一页

7.linkButton4 末页

8.DropDownlist1 实现页面跳转



DataSet ds = DataBase.news.show("3");这句中是用了一个类库中的静态方法 返回的是一个DataSet,也可以直接在代码里面读取数据库 只要ds是DataSet数据类型就行







using System;
using System.Collections;
using System.Configuration;
using System.Data;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.HtmlControls;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Data.OleDb;

public partial class cysj : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
if (!this.IsPostBack)
{
this.Label1.Text = "1";

this.GridViewBind();

}
}
private void GridViewBind()
{
int curPage = Convert.ToInt32(this.Label1.Text);
DataSet ds = DataBase.news.show("3");
PagedDataSource ps = new PagedDataSource();
ps.DataSource = ds.Tables[0].DefaultView;
ps.AllowPaging = true;
ps.PageSize = 25;
ps.CurrentPageIndex = curPage - 1;
this.LinkButton2.Enabled = true;
this.LinkButton3.Enabled = true;
if (curPage == 1)
{
this.LinkButton2.Enabled = false;
}
if (curPage == ps.PageCount)
{
this.LinkButton3.Enabled = false;
}
this.Repeater1.DataSource = ps;
this.Repeater1.DataBind();
Label2.Text = (ps.PageCount).ToString();
DropDownList1.Items.Clear();
for (int i = 1; i < ps.PageCount + 1; i++)
{
DropDownList1.Items.Add(i.ToString());
}

DropDownList1.SelectedIndex = Convert.ToInt32(Label1.Text) - 1;


}
protected void Button1_Click(object sender, EventArgs e)
{
this.Label1.Text = Convert.ToString(Convert.ToInt32(this.Label1.Text) - 1);
this.GridViewBind();
}

protected void LinkButton1_Click(object sender, EventArgs e)
{
this.Label1.Text = "1";
this.GridViewBind();
}
protected void LinkButton4_Click(object sender, EventArgs e)
{
this.Label1.Text = Label2.Text;
this.GridViewBind();
}
protected void LinkButton2_Click(object sender, EventArgs e)
{
this.Label1.Text = Convert.ToString(Convert.ToInt32(this.Label1.Text) - 1);
this.GridViewBind();
}
protected void LinkButton3_Click(object sender, EventArgs e)
{
this.Label1.Text=Convert.ToString(Convert.ToInt32(this.Label1.Text)+1);
this.GridViewBind();
}
protected void DropDownList1_SelectedIndexChanged(object sender, EventArgs e)
{
this.Label1.Text = DropDownList1.SelectedItem.Text;
this.GridViewBind();
}
}

移动项目开发笔记(asp.net防止页面刷新引起重复提交数据)

项目中遇到刷新后重复的向数据库增加一条相同的记录。引出错误。归纳得出问题是:

asp.net怎样防止防止页面刷新或后退引起重复提交数据的问题:

其实asp.net防止刷新是asp.net开发中经常遇到的问题。通常有多种方法来实现:(下面是一些解决方案)不很全,也很希望朋友们能多多补充。谢谢。



1.请求转发(执行数据库中的更新操作后立即转跳到其他页面,防止页面刷新引起回发操作)、

2.重新加载该页面(通过JavaScript来设置当执行操作后重新加载该页面:)

3. 不保存缓存(即提交后表单上的数据不会被浏览器的缓存保存,如果此时再遇到刷新或者后退请求时, 就会显示“网页已过期”,数据也就不会重复提交了,这就起到了阻止刷新重复提交的效果。

(参见: http://www.cnblogs.com/zpq521/archive/2007/08/14/854631.html))

4.MSDN的一种解决方案: 它是通过重新定义 System.Web.UI.Page 类来实现加载页面时,是“刷新”、“后退”请求,还是正常请求,其他的页面则继承了自定义的这 个Page类

(参见:http://msdn.microsoft.com/zh-cn/library/ms379557(VS.80).aspx )



当然可能会有其他的解决方案,希望朋友们提出自己的解决方案。谢谢。

数据库设计的三点经验

1.吃透需求和业务
吃透了需求和业务,才能设计出满足要求的数据库结构。



2.不断改进

需求的理解需要一个过程,根据对需求理解的加深不断改进数据库设计。

从具体开发实现的角度改进数据库设计。

从安全性和性能要求的角度改进数据库设计。



3.使用可读性强,统一的命名规则。

便于开发和维护。

一个关于如何学习大型数据库问题的问题

在数据库的学习中,要涉及到大量的数据,那么从哪里可以得到那些大量的数据呢?如果没有这些数据,又如何去学习、去实践呢?难道非要等到实际的工程中才能用到这些知识吗?可是我一直认为检验自己对某一项知识是否掌握,最重要的标准是看是否在实际中能够灵活应用。再说,在实际的工程环境中,也不允许你把理论用于实践。毕竟,如果运用的失败,会给组织带来巨大的损失。如何解决这个矛盾呢?难道就没有一些现成的数据能供自己只在学习中用吗?有人说可以边做边学,不做我认为边做边学是一种很被动的学习方式,特别是在实际项目中,因为你不知道是否你遇到的每一个实际问题,你都能解决!

可能有的人不知道我究竟说的是什么,我现在就用一个实例来说明:比如说我现在在学习数据库理论,显然检验自己对这部分知识是否掌握的标准是能否把这些理论用在大型数据库中,因为数据库理论最开始是起源于IBM的大型数据库,(这一点可能不是很准确,我的意思是说数据库理论可能最先在大型数据库中得到应用。)再说只有面对大型数据库,才能充分体现你的数据库理论掌握水平和数据库设计水平。那么我怎样去得到一个大型数据库,仅仅供我学习用了。当然,这个大型数据库必须包含很多实际中的大量数据。

不要告诉我只能在实际项目中学习,因为实际中的项目不是拿来学习的,他经不起失败。如果是这样,会出现这样一种情况:
我通过钻研数据库教材,觉得自己掌握的很不错,可就有一条,没有实践。如果带着没有任何实践经验的数据库理论与做项目,特别是大型项目,我不敢保证我会成功,因为毕竟只是学习了一下书本上的理论,而实际中我遇到一些无法解决的问题,这些问题并不是因为我理论掌握的不好。这时候该怎么办呢?

说了这么多,其实可以用一句话来总结上述所说的:我需要在充分掌握数据库理论的前提下,淋漓尽致的把自己所掌握的理论知识充分用在一个大型项目中,通过解决很多复杂实际问题,来印证自己掌握的理论知识。让自己的感性知识和理性知识得到完美的结合。只有这样,我才有底气说:我终于掌握了数据库。可是如何从哪里找到这样的一个数据库供我练习用呢?

注:虽然可能我的这些话,可能有人会认为很偏激,但对这类人,我想问这样一个问题:你怎样知道你数据库学好了?(别告诉我只是考试得高分,我始终认为考试得高分,只是学习的一个必然结果。显然,实际中的问题要比考试难的多。)

2008年8月28日星期四

程序员面试,你准备好了吗?

今天看到几篇面试相关的文章。面试是我最近想的很多的问题,有所思,有所得,从小部门面试的角度谈谈所思所得吧。

面试就是在短时间内对应聘者的经验、能力、潜力进行评估。我一般比较关注这些方面:(1)品格;(2)经验;(3)解决问题的能力;(4)技术特长;(5)潜力。面试时间一般为1小时,笔试+机试+面谈。

下面结合偶常用的一份C++/C#程序员面试题谈谈体会。

面试题一般分为3~4部分。第一部分是自我评估,第二部分是上机编程,第三部分是领域相关的东东,第四部分是考查能力与潜力的选做题。

(1)自我评估

自我评估一般有三道题:

(a)请估计你的编码量,在合适的选项上画上√。(机器生成的代码不算)

(b)你使用过什么源代码管理工具?(只使用过一、两次的不算,可多选)

(c)你习惯使用什么样的开发工具/IDE?(可多选)

主要考查个人经验和品质。如果这一部分胡填瞎吹,在后面会露馅。这里面还能看出一个人解决问题的能力。有一个哥们折腾了半天,问:这个√我在“插入”(word)怎么找不到啊!(偶想:不会copy嘛!)。这种哥们,显然解决问题的能力不强,可以直接拒之。可能会冤枉他,但冤枉的概率很低。

一般来说,偶觉得需要评估拟采用语言及其相关语言的编码量,比如招聘c#程序员,偶一般评估其java, c#, sql, vb.net的编码量。有时候这哥们不会c#,java和sql很熟其实也不错。

IDE和源代码管理工具评估大概能评价一个人的技术风格。比如,用dreamweaver的侧重于页面,用visual studio的侧重于后台,linux下用vi, emacs, cvs的具有一定的黑客特质,用kdevelop, eclipse/cdt的侧重于实用主义。

(2)机试

一般来说,就一个hello world题。主要考察基本的OO设计及编码风格。下面是原题:

C#版:

(a)写一个类Message,该类具备以下成员:

·一个名为message,储存消息文本;

·一个方法Show(),打印消息文本;

·一个构造函数,根据传递的字符串,初始化消息文本。

·其它你认为需要的东西

(b)调用Message的实例,打印“hello world”。。

C++版:

(a)写一个C++类Message,该类具备以下成员:

·一个名为message,类型为string(或char*)的私有变量,储存消息文本;

·一个方法show(),打印消息文本;

·一个构造函数,根据传递的字符串,初始化消息文本。

(b)写一个程序,通过调用Message的实例,打印“hello world”。编写make文件;make;运行程序。

大家写出来的结果真是五花八门,精彩之极。能在1小时内写出实现功能的C++应聘者不到50%,C#的高一些。在10分钟内写出,并且写的让人看的很舒服的,大概只有20%,没有一年的编码经验是写不出很漂亮的代码的。

以前常用一个较难的题:界面上有一条线段,鼠标在线段的附近点击,写一个算法,判断是否会选中线段。发现效果不好,一般人在一小时内搞不定。

(3)领域相关

这个和应聘的职位相关。如果招聘网络程序员,偶会让他讲讲几种IO的区别,抓几个包分析分析。如果招聘Asp.Net Web程序员,偶会让他讲讲DataTable, DataSet, DataAdapter的角色与功能。如果他说不会,用NHibernate什么的,那就让他谈谈这方面的体会。这种题目相对开放。主要考查他有没有相关领域的开发经验以及经验的深浅。到这里,不出大差错的话,差不多就可以录用了。当然,如果应聘者较多,就需要比较选择题的成绩了。

(4)选做题

选做题是非常开放的题目。基本上分模式、算法、库的考查。

C#版的如下:

(a) 谈谈你对facade模式的理解与心得

(b) 谈谈你对.net framework的理解与使用心得

(c) 想产生一个数列:T(n) = T(n-1),n,T(n-1),T(0) = 1

比如T(3)是1,2,1,3,1,2,1

T(4)是1,2,1,3,1,2,1,4,1,2,1,3,1,2,1

不用递归有什么效率高的算法吗?

C++版的如下:

(a) 谈谈你对facade模式的理解与心得

(b) 谈谈你对C++ 程序内存管理的理解与心得

(c) 谈谈你对C++ 标准库的理解与使用心得

(d) 想产生一个数列:T(n) = T(n-1),n,T(n-1),T(0) = 1

比如T(3)是1,2,1,3,1,2,1

T(4)是1,2,1,3,1,2,1,4,1,2,1,3,1,2,1

这些问题很开放。有些人上来能谈的有条有理,这个人大概在这些方面进行过思考过。如果一上来不知道怎么谈,可以诱导他,慢慢的谈出他在这方面的经验、心得和体会。比如谈.net framework,可以问问常用的类,谈谈framework的结构,你觉得什么地方好用,什么地方不好用,怎么选择、试用、使用、扩展第三方库?

对于“精通”。一般来说,在简历中写精通的人90%都是吹牛,直接拒掉冤枉的概率只有10%。就算一个人是真正的精通,那也不是企业最需要的人。企业最需要的是能解决问题,有思路的技术人员,其次才是精通某项技术的技术人员。《.NET方向高级开发人员面试时应该事先考虑的问题》中的题目,一半偶不会用来面试人,一些可能放在选做题里面,一些可能放在领域相关题里面(假设我要招聘这方面的开发人员),一些是用来考察一个人的品质是否诚实,权重不会很大。当然,偶招聘的一般是一般开发人员,不是高级开发人员。:P 不过,对于高级开发人员,偶一般也不会面试这些,偏架构的一般会从《企业应用架构模式》,《without ejb》中选题,还有就是开发过程。偏技术的可以给他一个问题让他解决,或者给他一段IL汇编代码,让他看,或者在可上网的条件下问他一个现实中可能出现的非常刁的问题。

多线程学习

多线程的使用
一些简单概念:
Thread.Start():启动线程的执行;
Thread.Abort():以开始终止此线程的过程。如果线程已经在终止,则不能通过Thread.Start()来启动线程。
Thread.Suspend():挂起线程,或者如果线程已挂起,则不起作用;
Thread.Resume():继续已挂起的线程;
Thread.Interrupt():中断处于 Wait Sleep Join 线程状态的线程;
Thread.Join():阻塞调用线程,直到某个线程终止时为止
Thread.Sleep():将当前线程阻塞指定的毫秒数;

Thread.Abort()方法使得系统悄悄的销毁了线程而且不通知用户。一旦实施Thread.Abort()操作,该线程不能被重新启动。调用了这个方法并不是意味着线
程立即销毁,为了确定线程是否被销毁,我们可以调用Thread.Join()来确定其销毁,Thread.Join()是一个阻塞调用,直到线程的确是终止了才返回。

线程的一般定义
private void StartThread( ref Thread thread, ThreadStart threadStart )
{
if ( thread != null )
{
thread.Abort(); //终止此线程的过程。如果线程已经在终止,则不能通过Thread.Start()来启动线程。
thread.Join(); //调用Thread.Abort()操作并不是意味着线程立即销毁,因此为了确定线程是否被销毁,我们可以调用Thread.Join()来确定其销毁
thread = null; //初始化
}
thread = new Thread( threadStart ); // 重新实例化一个线程
thread.IsBackground = true; //线程在后台执行
thread.Start();
}
private void StopThread(Thread thread )
{
if ( thread != null )
{
thread.Abort();
thread.Join();
thread = null;
}
}

上传图片方法大全 [网摘]

看了清清月儿的这篇文章让自己受益匪浅,但是觉得还有一些问题。上传图片后还有原来的图片文件存在,觉得这样很不爽,调用file类的delete方法删除原来没有生成水印的图片另外自己又加了一个限制图片大小的函数
1.最简单的单文件上传(没花头)
效果图:说明:这是最基本的文件上传,在asp.net1.x中没有这个FileUpload控件,只有html的上传控件,那时候要把html控件转化为服务器控件,很不好用。其实所有文件上传的美丽效果都是从这个FileUpload控件衍生,第一个例子虽然简单却是根本。
后台代码:using System;using System.Data;using System.Configuration;using System.Collections;using System.Web;using System.Web.Security;using System.Web.UI;using System.Web.UI.WebControls;using System.Web.UI.WebControls.WebParts;using System.Web.UI.HtmlControls;
public partial class _Default : System.Web.UI.Page{ protected void Page_Load(object sender, EventArgs e) {
} protected void bt_upload_Click(object sender, EventArgs e) { try { if (FileUpload1.PostedFile.FileName == "") { this.lb_info.Text = "请选择文件!"; } else { string filepath = FileUpload1.PostedFile.FileName; string filename = filepath.Substring(filepath.LastIndexOf("\\") + 1); string serverpath = Server.MapPath("images/") + filename; FileUpload1.PostedFile.SaveAs(serverpath); this.lb_info.Text = "上传成功!"; } } catch (Exception ex) { this.lb_info.Text = "上传发生错误!原因是:" + ex.ToString(); } }}前台代码:
单文件上传

2.多文件上传
效果图:
后台代码:using System;using System.Data;using System.Configuration;using System.Collections;using System.Web;using System.Web.Security;using System.Web.UI;using System.Web.UI.WebControls;using System.Web.UI.WebControls.WebParts;using System.Web.UI.HtmlControls;
public partial class _Default : System.Web.UI.Page{ protected void Page_Load(object sender, EventArgs e) {
} protected void bt_upload_Click(object sender, EventArgs e) { if ((FileUpload1.PostedFile.FileName == "" && FileUpload2.PostedFile.FileName == "")&&FileUpload3.PostedFile.FileName == "") { this.lb_info.Text = "请选择文件!"; } else { HttpFileCollection myfiles = Request.Files; for (int i = 0; i < mypost =" myfiles[i];"> 0) { string filepath = mypost.FileName; string filename = filepath.Substring(filepath.LastIndexOf("\\") + 1); string serverpath = Server.MapPath("images/") + filename; mypost.SaveAs(serverpath); this.lb_info.Text = "上传成功!"; } } catch (Exception error) { this.lb_info.Text = "上传发生错误!原因:" + error.ToString(); }
} } } }前台代码: <%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default.aspx.cs" Inherits="_Default" %>

head runat="server">head>
多文件上传

3.客户端检查上传文件类型(以上传图片为例)
效果图:后台代码和1.最简单的单文件上传一样;前台代码:<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default.aspx.cs" Inherits="_Default" %>

head runat="server"> 清清月儿 http://blog.csdn.net/21aspnet/a>>
文件上传判断
说明:点击上传时先触发客户端事件Check_FileType;
4.服务器端检查上传文件类型(以上传图片为例)
效果图:
后台代码:using System;using System.Data;using System.Configuration;using System.Collections;using System.Web;using System.Web.Security;using System.Web.UI;using System.Web.UI.WebControls;using System.Web.UI.WebControls.WebParts;using System.Web.UI.HtmlControls;
public partial class _Default : System.Web.UI.Page{ protected void Page_Load(object sender, EventArgs e) {
} protected void bt_upload_Click(object sender, EventArgs e) { try { if (FileUpload1.PostedFile.FileName == "") { this.lb_info.Text = "请选择文件!"; } else { string filepath = FileUpload1.PostedFile.FileName; if (IsAllowedExtension(FileUpload1) == true) {
string filename = filepath.Substring(filepath.LastIndexOf("\\") + 1); string serverpath = Server.MapPath("images/") + filename; FileUpload1.PostedFile.SaveAs(serverpath); this.lb_info.Text = "上传成功!"; } else { this.lb_info.Text = "请上传图片"; } } } catch (Exception error) { this.lb_info.Text = "上传发生错误!原因:" + error.ToString(); } } public static bool IsAllowedExtension(FileUpload hifile) { string strOldFilePath = "", strExtension = ""; string[] arrExtension = { ".gif", ".jpg", ".jpeg", ".bmp", ".png" }; if (hifile.PostedFile.FileName != string.Empty) { strOldFilePath = hifile.PostedFile.FileName; strExtension = strOldFilePath.Substring(strOldFilePath.LastIndexOf(".")); for (int i = 0; i < arrExtension.Length; i++) { if (strExtension.Equals(arrExtension[i])) { return true; } } } return false; }
}
5.服务器端检查上传文件类型(可以检测真正文件名) 其实方法4并不好,因为用户可以把XXX.txt伪装为XXX.jpg。
效果图:
后台代码:using System;using System.Data;using System.Configuration;using System.Collections;using System.Web;using System.Web.Security;using System.Web.UI;using System.Web.UI.WebControls;using System.Web.UI.WebControls.WebParts;using System.Web.UI.HtmlControls;
public partial class _Default : System.Web.UI.Page{ //清清月儿 http://blog.csdn.net/21aspnet protected void Page_Load(object sender, EventArgs e) {
} protected void bt_upload_Click(object sender, EventArgs e) { try { if (FileUpload1.PostedFile.FileName == "") { this.lb_info.Text = "请选择文件!"; } else { string filepath = FileUpload1.PostedFile.FileName; if (IsAllowedExtension(FileUpload1) == true) { string filename = filepath.Substring(filepath.LastIndexOf("\\") + 1); string serverpath = Server.MapPath("images/") + filename; FileUpload1.PostedFile.SaveAs(serverpath); this.lb_info.Text = "上传成功!"; } else { this.lb_info.Text = "请上传图片"; } } } catch (Exception error) { this.lb_info.Text = "上传发生错误!原因:" + error.ToString(); } } public static bool IsAllowedExtension(FileUpload hifile) { System.IO.FileStream fs = new System.IO.FileStream(hifile.PostedFile.FileName, System.IO.FileMode.Open, System.IO.FileAccess.Read); System.IO.BinaryReader r = new System.IO.BinaryReader(fs); string fileclass = ""; byte buffer; try { buffer = r.ReadByte(); fileclass = buffer.ToString(); buffer = r.ReadByte(); fileclass += buffer.ToString();
} catch { } r.Close(); fs.Close(); if (fileclass == "255216" fileclass == "7173")//说明255216是jpg;7173是gif;6677是BMP,13780是PNG;7790是exe,8297是rar { return true; } else { return false; }
}
}
6.上传文件文件名唯一性处理(时间戳+SessionID)
效果图:说明:年月日时分秒+临时session+原文件名 如果大家怕还会重复可以加GUID后台代码:
try { if (FileUpload1.PostedFile.FileName == "") { this.lb_info.Text = "请选择文件!"; } else { string filepath = FileUpload1.PostedFile.FileName; string filename = filepath.Substring(filepath.LastIndexOf("\\") + 1); string serverpath = Server.MapPath("images/") + System.DateTime.Now.ToString("yyy-MM-dd-hh-mm-ss") + Session.SessionID + filename; FileUpload1.PostedFile.SaveAs(serverpath); this.lb_info.Text = "上传成功!"; } } catch (Exception error) { this.lb_info.Text = "上传发生错误!原因:" + error.ToString(); }
注:GUID的方法:Guid myGuid=Guid.NewGuid();
7.上传图片生成等比例缩略图
效果图:
缩略图代码:ImageThumbnail.csusing System;using System.IO;using System.Drawing;using System.Drawing.Imaging;
public class ImageThumbnail{ public Image ResourceImage; private int ImageWidth; private int ImageHeight; public string ErrorMessage;
public ImageThumbnail(string ImageFileName) { ResourceImage = Image.FromFile(ImageFileName); ErrorMessage = ""; }
public bool ThumbnailCallback() { return false; }
// 方法1,按大小 public bool ReducedImage(int Width, int Height, string targetFilePath) { try { Image ReducedImage; Image.GetThumbnailImageAbort callb = new Image.GetThumbnailImageAbort(ThumbnailCallback); ReducedImage = ResourceImage.GetThumbnailImage(Width, Height, callb, IntPtr.Zero); ReducedImage.Save(@targetFilePath, ImageFormat.Jpeg); ReducedImage.Dispose(); return true; } catch (Exception e) { ErrorMessage = e.Message; return false; } }
// 方法2,按百分比 缩小60% Percent为0.6 targetFilePath为目标路径 public bool ReducedImage(double Percent, string targetFilePath) { try { Image ReducedImage; Image.GetThumbnailImageAbort callb = new Image.GetThumbnailImageAbort(ThumbnailCallback); ImageWidth = Convert.ToInt32(ResourceImage.Width * Percent); ImageHeight = (ResourceImage.Height)*ImageWidth/ ResourceImage.Width;//等比例缩放 ReducedImage = ResourceImage.GetThumbnailImage(ImageWidth, ImageHeight, callb, IntPtr.Zero); ReducedImage.Save(@targetFilePath, ImageFormat.Jpeg); ReducedImage.Dispose(); return true; } catch (Exception e) { ErrorMessage = e.Message; return false; } }
}
后台代码:using System;using System.Data;using System.Configuration;using System.Collections;using System.Web;using System.Web.Security;using System.Web.UI;using System.Web.UI.WebControls;using System.Web.UI.WebControls.WebParts;using System.Web.UI.HtmlControls;public partial class _Default : System.Web.UI.Page{
protected void Page_Load(object sender, EventArgs e) {
} protected void bt_upload_Click(object sender, EventArgs e) { try { if (FileUpload1.PostedFile.FileName == "") { this.lb_info.Text = "请选择文件!"; } else { string filepath = FileUpload1.PostedFile.FileName; string filename = filepath.Substring(filepath.LastIndexOf("\\") + 1); string serverpath1 = Server.MapPath("images/") + filename; string serverpath2 = Server.MapPath("images/") + System.DateTime.Now.ToString("yyy-MM-dd-hh-mm-ss") + Session.SessionID + filename; FileUpload1.PostedFile.SaveAs(serverpath1); ImageThumbnail img = new ImageThumbnail(filepath); img.ReducedImage(0.4, serverpath2);//0.4表示缩小40% this.lb_info.Text = "上传成功!"; } } catch (Exception error) { this.lb_info.Text = "上传发生错误!原因:" + error.ToString(); } }
}8.上传图片加水印(文字水印,图片水印,文字+图片水印)
效果图:原图
水印
给图片加水印以后(注意右上角+正下方)
代码:DrawImg.cs 出自http://www.codeproject.com/csharp/watermark.aspusing System;using System.Drawing;using System.Drawing.Imaging;using System.Drawing.Drawing2D;public class DrawImg{ private string WorkingDirectory = string.Empty ; //路径 private string ImageName = string.Empty; //被处理的图片 private string ImageWater = string.Empty; //水印图片 private string FontString = string.Empty; //水印文字
enum DealType{NONE,WaterImage,WaterFont,DoubleDo}; //枚举命令
private DealType dealtype;
public DrawImg() {}
public string PublicWorkingDirectory { get { return WorkingDirectory; } set { WorkingDirectory = value; } }
public string PublicImageName { get { return ImageName; } set { ImageName = value; } }
public string PublicImageWater { get { return ImageWater; } set //设置了水印图片的话说明是要水印图片效果的 { dealtype = DealType.WaterImage; ImageWater = value; } }
public string PublicFontString { get { return FontString; } set //设置了水印文字的话说明是要水印文字效果的 { dealtype = DealType.WaterFont; FontString = value; } }

public void DealImage() { IsDouble();
switch( dealtype ) { case DealType.WaterFont: WriteFont(); break; case DealType.WaterImage: WriteImg(); break; case DealType.DoubleDo: WriteFontAndImg(); break; }
}
private void IsDouble() { if(ImageWater+""!="" && FontString+""!="") { dealtype = DealType.DoubleDo; } }
private void WriteFont() { //set a working directory //string WorkingDirectory = @"C:\Watermark_src\WaterPic";
//define a string of text to use as the Copyright message //string Copyright = "Copyright ?2002 - AP Photo/David Zalubowski";
//create a image object containing the photograph to watermark Image imgPhoto = Image.FromFile(WorkingDirectory + ImageName); int phWidth = imgPhoto.Width; int phHeight = imgPhoto.Height;
//create a Bitmap the Size of the original photograph Bitmap bmPhoto = new Bitmap(phWidth, phHeight, PixelFormat.Format24bppRgb);
bmPhoto.SetResolution(imgPhoto.HorizontalResolution, imgPhoto.VerticalResolution);
//load the Bitmap into a Graphics object Graphics grPhoto = Graphics.FromImage(bmPhoto);
//------------------------------------------------------------ //Step #1 - Insert Copyright message //------------------------------------------------------------
//Set the rendering quality for this Graphics object grPhoto.SmoothingMode = SmoothingMode.AntiAlias;
//Draws the photo Image object at original size to the graphics object. grPhoto.DrawImage( imgPhoto, // Photo Image object new Rectangle(0, 0, phWidth, phHeight), // Rectangle structure 0, // x-coordinate of the portion of the source image to draw. 0, // y-coordinate of the portion of the source image to draw. phWidth, // Width of the portion of the source image to draw. phHeight, // Height of the portion of the source image to draw. GraphicsUnit.Pixel); // Units of measure
//------------------------------------------------------- //to maximize the size of the Copyright message we will //test multiple Font sizes to determine the largest posible //font we can use for the width of the Photograph //define an array of point sizes you would like to consider as possiblities //------------------------------------------------------- int[] sizes = new int[]{16,14,12,10,8,6,4};
Font crFont = null; SizeF crSize = new SizeF();
//Loop through the defined sizes checking the length of the Copyright string //If its length in pixles is less then the image width choose this Font size. for (int i=0 ;i<7; i++) { //set a Font object to Arial (i)pt, Bold //crFont = new Font("arial", sizes[i], FontStyle.Bold);
crFont = new Font("arial",sizes[i],FontStyle.Bold);
//Measure the Copyright string in this Font crSize = grPhoto.MeasureString(FontString, crFont);
if((ushort)crSize.Width < (ushort)phWidth) break; }
//Since all photographs will have varying heights, determine a //position 5% from the bottom of the image int yPixlesFromBottom = (int)(phHeight *.05);
//Now that we have a point size use the Copyrights string height //to determine a y-coordinate to draw the string of the photograph float yPosFromBottom = ((phHeight - yPixlesFromBottom)-(crSize.Height/2));
//Determine its x-coordinate by calculating the center of the width of the image float xCenterOfImg = (phWidth/2);
//Define the text layout by setting the text alignment to centered StringFormat StrFormat = new StringFormat(); StrFormat.Alignment = StringAlignment.Center;
//define a Brush which is semi trasparent black (Alpha set to 153) SolidBrush semiTransBrush2 = new SolidBrush(Color.FromArgb(153, 0, 0, 0));
//Draw the Copyright string grPhoto.DrawString(FontString, //string of text crFont, //font semiTransBrush2, //Brush new PointF(xCenterOfImg+1,yPosFromBottom+1), //Position StrFormat);
//define a Brush which is semi trasparent white (Alpha set to 153) SolidBrush semiTransBrush = new SolidBrush(Color.FromArgb(153, 255, 255, 255));
//Draw the Copyright string a second time to create a shadow effect //Make sure to move this text 1 pixel to the right and down 1 pixel grPhoto.DrawString(FontString, //string of text crFont, //font semiTransBrush, //Brush new PointF(xCenterOfImg,yPosFromBottom), //Position StrFormat); imgPhoto = bmPhoto; grPhoto.Dispose();
//save new image to file system. imgPhoto.Save(WorkingDirectory + ImageName + "_finally.jpg", ImageFormat.Jpeg); imgPhoto.Dispose(); //Text alignment }
private void WriteImg() { //set a working directory //string WorkingDirectory = @"C:\Watermark_src\WaterPic";
//create a image object containing the photograph to watermark Image imgPhoto = Image.FromFile(WorkingDirectory + ImageName); int phWidth = imgPhoto.Width; int phHeight = imgPhoto.Height;
//create a Bitmap the Size of the original photograph Bitmap bmPhoto = new Bitmap(phWidth, phHeight, PixelFormat.Format24bppRgb);
bmPhoto.SetResolution(imgPhoto.HorizontalResolution, imgPhoto.VerticalResolution);
//load the Bitmap into a Graphics object Graphics grPhoto = Graphics.FromImage(bmPhoto);
//create a image object containing the watermark Image imgWatermark = new Bitmap(WorkingDirectory + ImageWater); int wmWidth = imgWatermark.Width; int wmHeight = imgWatermark.Height;
//Set the rendering quality for this Graphics object grPhoto.SmoothingMode = SmoothingMode.AntiAlias;
//Draws the photo Image object at original size to the graphics object. grPhoto.DrawImage( imgPhoto, // Photo Image object new Rectangle(0, 0, phWidth, phHeight), // Rectangle structure 0, // x-coordinate of the portion of the source image to draw. 0, // y-coordinate of the portion of the source image to draw. phWidth, // Width of the portion of the source image to draw. phHeight, // Height of the portion of the source image to draw. GraphicsUnit.Pixel); // Units of measure
//------------------------------------------------------------ //Step #2 - Insert Watermark image //------------------------------------------------------------
//Create a Bitmap based on the previously modified photograph Bitmap Bitmap bmWatermark = new Bitmap(bmPhoto); bmWatermark.SetResolution(imgPhoto.HorizontalResolution, imgPhoto.VerticalResolution); //Load this Bitmap into a new Graphic Object Graphics grWatermark = Graphics.FromImage(bmWatermark);
//To achieve a transulcent watermark we will apply (2) color //manipulations by defineing a ImageAttributes object and //seting (2) of its properties. ImageAttributes imageAttributes = new ImageAttributes();
//The first step in manipulating the watermark image is to replace //the background color with one that is trasparent (Alpha=0, R=0, G=0, B=0) //to do this we will use a Colormap and use this to define a RemapTable ColorMap colorMap = new ColorMap();
//My watermark was defined with a background of 100% Green this will //be the color we search for and replace with transparency colorMap.OldColor = Color.FromArgb(255, 0, 255, 0); colorMap.NewColor = Color.FromArgb(0, 0, 0, 0);
ColorMap[] remapTable = {colorMap};
imageAttributes.SetRemapTable(remapTable, ColorAdjustType.Bitmap);
//The second color manipulation is used to change the opacity of the //watermark. This is done by applying a 5x5 matrix that contains the //coordinates for the RGBA space. By setting the 3rd row and 3rd column //to 0.3f we achive a level of opacity float[][] colorMatrixElements = { new float[] {1.0f, 0.0f, 0.0f, 0.0f, 0.0f}, new float[] {0.0f, 1.0f, 0.0f, 0.0f, 0.0f}, new float[] {0.0f, 0.0f, 1.0f, 0.0f, 0.0f}, new float[] {0.0f, 0.0f, 0.0f, 0.3f, 0.0f}, new float[] {0.0f, 0.0f, 0.0f, 0.0f, 1.0f}}; ColorMatrix wmColorMatrix = new ColorMatrix(colorMatrixElements);
imageAttributes.SetColorMatrix(wmColorMatrix, ColorMatrixFlag.Default, ColorAdjustType.Bitmap);
//For this example we will place the watermark in the upper right //hand corner of the photograph. offset down 10 pixels and to the //left 10 pixles
int xPosOfWm = ((phWidth - wmWidth)-10); int yPosOfWm = 10;
grWatermark.DrawImage(imgWatermark, new Rectangle(xPosOfWm,yPosOfWm,wmWidth,wmHeight), //Set the detination Position 0, // x-coordinate of the portion of the source image to draw. 0, // y-coordinate of the portion of the source image to draw. wmWidth, // Watermark Width wmHeight, // Watermark Height GraphicsUnit.Pixel, // Unit of measurment imageAttributes); //ImageAttributes Object
//Replace the original photgraphs bitmap with the new Bitmap imgPhoto = bmWatermark; grPhoto.Dispose(); grWatermark.Dispose();
//save new image to file system. imgPhoto.Save(WorkingDirectory + ImageName +"_finally.jpg", ImageFormat.Jpeg); imgPhoto.Dispose(); imgWatermark.Dispose();
}
private void WriteFontAndImg() { //create a image object containing the photograph to watermark Image imgPhoto = Image.FromFile(WorkingDirectory + ImageName); int phWidth = imgPhoto.Width; int phHeight = imgPhoto.Height;
//create a Bitmap the Size of the original photograph Bitmap bmPhoto = new Bitmap(phWidth, phHeight, PixelFormat.Format24bppRgb);
bmPhoto.SetResolution(imgPhoto.HorizontalResolution, imgPhoto.VerticalResolution);
//load the Bitmap into a Graphics object Graphics grPhoto = Graphics.FromImage(bmPhoto);
//create a image object containing the watermark Image imgWatermark = new Bitmap(WorkingDirectory + ImageWater); int wmWidth = imgWatermark.Width; int wmHeight = imgWatermark.Height;
//------------------------------------------------------------ //Step #1 - Insert Copyright message //------------------------------------------------------------
//Set the rendering quality for this Graphics object grPhoto.SmoothingMode = SmoothingMode.AntiAlias;
//Draws the photo Image object at original size to the graphics object. grPhoto.DrawImage( imgPhoto, // Photo Image object new Rectangle(0, 0, phWidth, phHeight), // Rectangle structure 0, // x-coordinate of the portion of the source image to draw. 0, // y-coordinate of the portion of the source image to draw. phWidth, // Width of the portion of the source image to draw. phHeight, // Height of the portion of the source image to draw. GraphicsUnit.Pixel); // Units of measure
//------------------------------------------------------- //to maximize the size of the Copyright message we will //test multiple Font sizes to determine the largest posible //font we can use for the width of the Photograph //define an array of point sizes you would like to consider as possiblities //------------------------------------------------------- int[] sizes = new int[]{16,14,12,10,8,6,4};//这里可以修改文字
Font crFont = null; SizeF crSize = new SizeF();
//Loop through the defined sizes checking the length of the Copyright string //If its length in pixles is less then the image width choose this Font size. for (int i=0 ;i<7; i++) { //set a Font object to Arial (i)pt, Bold crFont = new Font("arial", sizes[i], FontStyle.Bold); //Measure the Copyright string in this Font crSize = grPhoto.MeasureString(FontString, crFont);
if((ushort)crSize.Width < (ushort)phWidth) break; }
//Since all photographs will have varying heights, determine a //position 5% from the bottom of the image int yPixlesFromBottom = (int)(phHeight *.05);
//Now that we have a point size use the Copyrights string height //to determine a y-coordinate to draw the string of the photograph float yPosFromBottom = ((phHeight - yPixlesFromBottom)-(crSize.Height/2));
//Determine its x-coordinate by calculating the center of the width of the image float xCenterOfImg = (phWidth/2);
//Define the text layout by setting the text alignment to centered StringFormat StrFormat = new StringFormat(); StrFormat.Alignment = StringAlignment.Center;
//define a Brush which is semi trasparent black (Alpha set to 153) SolidBrush semiTransBrush2 = new SolidBrush(Color.FromArgb(153, 0, 0, 0));
//Draw the Copyright string grPhoto.DrawString(FontString, //string of text crFont, //font semiTransBrush2, //Brush new PointF(xCenterOfImg+1,yPosFromBottom+1), //Position StrFormat);
//define a Brush which is semi trasparent white (Alpha set to 153) SolidBrush semiTransBrush = new SolidBrush(Color.FromArgb(153, 255, 255, 255));
//Draw the Copyright string a second time to create a shadow effect //Make sure to move this text 1 pixel to the right and down 1 pixel grPhoto.DrawString(FontString, //string of text crFont, //font semiTransBrush, //Brush new PointF(xCenterOfImg,yPosFromBottom), //Position StrFormat); //Text alignment

//------------------------------------------------------------ //Step #2 - Insert Watermark image //------------------------------------------------------------
//Create a Bitmap based on the previously modified photograph Bitmap Bitmap bmWatermark = new Bitmap(bmPhoto); bmWatermark.SetResolution(imgPhoto.HorizontalResolution, imgPhoto.VerticalResolution); //Load this Bitmap into a new Graphic Object Graphics grWatermark = Graphics.FromImage(bmWatermark);
//To achieve a transulcent watermark we will apply (2) color //manipulations by defineing a ImageAttributes object and //seting (2) of its properties. ImageAttributes imageAttributes = new ImageAttributes();
//The first step in manipulating the watermark image is to replace //the background color with one that is trasparent (Alpha=0, R=0, G=0, B=0) //to do this we will use a Colormap and use this to define a RemapTable ColorMap colorMap = new ColorMap();
//My watermark was defined with a background of 100% Green this will //be the color we search for and replace with transparency colorMap.OldColor = Color.FromArgb(255, 0, 255, 0); colorMap.NewColor = Color.FromArgb(0, 0, 0, 0);
ColorMap[] remapTable = {colorMap};
imageAttributes.SetRemapTable(remapTable, ColorAdjustType.Bitmap);
//The second color manipulation is used to change the opacity of the //watermark. This is done by applying a 5x5 matrix that contains the //coordinates for the RGBA space. By setting the 3rd row and 3rd column //to 0.3f we achive a level of opacity float[][] colorMatrixElements = { new float[] {1.0f, 0.0f, 0.0f, 0.0f, 0.0f}, new float[] {0.0f, 1.0f, 0.0f, 0.0f, 0.0f}, new float[] {0.0f, 0.0f, 1.0f, 0.0f, 0.0f}, new float[] {0.0f, 0.0f, 0.0f, 0.3f, 0.0f}, new float[] {0.0f, 0.0f, 0.0f, 0.0f, 1.0f}}; ColorMatrix wmColorMatrix = new ColorMatrix(colorMatrixElements);
imageAttributes.SetColorMatrix(wmColorMatrix, ColorMatrixFlag.Default, ColorAdjustType.Bitmap);
//For this example we will place the watermark in the upper right //hand corner of the photograph. offset down 10 pixels and to the //left 10 pixles
int xPosOfWm = ((phWidth - wmWidth)-10); int yPosOfWm = 10;
grWatermark.DrawImage(imgWatermark, new Rectangle(xPosOfWm,yPosOfWm,wmWidth,wmHeight), //Set the detination Position 0, // x-coordinate of the portion of the source image to draw. 0, // y-coordinate of the portion of the source image to draw. wmWidth, // Watermark Width wmHeight, // Watermark Height GraphicsUnit.Pixel, // Unit of measurment imageAttributes); //ImageAttributes Object
//Replace the original photgraphs bitmap with the new Bitmap imgPhoto = bmWatermark; grPhoto.Dispose(); grWatermark.Dispose();
//save new image to file system. imgPhoto.Save(WorkingDirectory + ImageName +"_finally.jpg", ImageFormat.Jpeg); imgPhoto.Dispose(); imgWatermark.Dispose();
}}
//水印图片加水印文字// ReDrawImg img = new ReDrawImg();// img .PublicWorkingDirectory = @"C:\Watermark_src\WaterPic\";// img .PublicImageName = "watermark_photo.jpg";// img .PublicImageWater = "watermark.bmp";// img .PublicFontString = "清清月儿";// img .DealImage(); //水印文字 ReDrawImg img = new ReDrawImg(); img .PublicWorkingDirectory = @"C:\Watermark_src\WaterPic\"; img .PublicImageName = "watermark_photo.jpg"; img .PublicFontString = @"清清月儿"; img .DealImage();
//水印图片// ReDrawImg img = new ReDrawImg();// img .PublicWorkingDirectory = @"C:\Watermark_src\WaterPic\";// img .PublicImageName = "watermark_photo.jpg";// img .PublicImageWater = "watermark.bmp"; // img .DealImage(); 后台代码:using System;using System.Data;using System.Configuration;using System.Collections;using System.Web;using System.Web.Security;using System.Web.UI;using System.Web.UI.WebControls;using System.Web.UI.WebControls.WebParts;using System.Web.UI.HtmlControls;public partial class _Default : System.Web.UI.Page{
protected void Page_Load(object sender, EventArgs e) {
} protected void bt_upload_Click(object sender, EventArgs e) { try { if (FileUpload1.PostedFile.FileName == "") { this.lb_info.Text = "请选择文件!"; } else { string filepath = FileUpload1.PostedFile.FileName; string filename = filepath.Substring(filepath.LastIndexOf("\\") + 1); HttpPostedFile UpFile = FileUpload1.PostedFile;//获取上传图片对象 int filelength = UpFile.ContentLength;//获取上传图片的大小(字节大小) if (CheckBytes(filelength)==true) { // string serverpath1 = Server.MapPath("images/") + filename; string serverpath2 = Server.MapPath("images/") + System.DateTime.Now.ToString("yyy-MM-dd-hh-mm-ss") + Session.SessionID + filename; string newname = System.DateTime.Now.ToString("yyy-MM-dd-hh-mm-ss") + Session.SessionID + filename;//上传到指定文件夹后的图片的新名称 FileUpload1.PostedFile.SaveAs(serverpath2); //ImageThumbnail img = new ImageThumbnail(filepath); //img.ReducedImage(0.4, serverpath2); DrawImg img = new DrawImg(); img.PublicWorkingDirectory = Server.MapPath("images/"); // img.PublicImageName = filename;//作者的例子是上传图片已经存在要上传到的文件夹里 img.PublicImageName =newname;//自己要上传的文件一般不会存在要上传到的文件夹里。这里获取上传成功后的图片文件名称。 img.PublicFontString = "http://blog.csdn.net/21aspnet"; img.PublicImageWater = "yyy.jpg"; img.DealImage(); this.lb_info.Text = "上传成功!"; GC.Collect();//释放进程,否则删不掉图片文件 File.Delete(serverpath); } else { this.lb_info.Text = "图片大小不能大于2M!"; } } } catch (Exception error) { this.lb_info.Text = "上传发生错误!原因:" + error.ToString(); } }
public bool CheckBytes(int FileLength)//限制图片大小 { bool Result = true; int Length = 2097152;//2M if (FileLength > Length) { Result = false; } return Result; }}

ASP.NET Cache 方案

ASP.NET 网站应用程序是无状态的,客户端每执行一次 PostBack 动作,所有的对象都需要重新建立;当然这样的执行模式相当没有效率,所以在 ASP.NET 应用程序中有 Application、Session、Cache 这三类资料暂存区。Application 的生命周期与应用程序相同,Session 的生命周期与个别使用者相同,而 Cache 的生命周期则由程序设计师自订,Cache 相对于 Application 及 Session 来说显示更有弹性。
选择合适的 Cache 方案能有效提升网站的执行效能,设定 Cache 过期的条件可分为下列三种。
1.绝对时间:插入对象过期并从快取移除的时间。
2.浮动时间:上次存取插入对象的时间与该对象到期的时间之间隔。如果这个数值等于二十分钟,那么对象将会在最后存取的二十分钟后过期并从快取中移除。
3.档案相依:项目的档案或快取索引键相依性。例如,假设您根据 XML 档案中的数据建立 DataSet 对象,您可以将 DataSet 加入含有 CacheDependency 对象的快取,该对象使 DataSet 相依于此 XML 档案。如果 XML 档案变更,则会从快取中移除 DataSet。
笔者常使用 Cache 来暂存经常存取的设定数据,一般作法是根据 XML 档案建立「设定类别」,这个类别再放入 Cache,并设定快取过期条件为「浮动时间+XML 档案相依」。这样的快取方案有二个优点。
1.该对象存取的频率愈多,则会优先置于 Cache,不易从 Cache 被移除。
2.当设定的 XML 档案内容变更时,Cache 则会被移除,以保持最新资料。

这个快取方案的算法如下
Step1:要求对象。
Step2:检查 Cache 中是否有该对象。
Step3:若 Cache 中有该对象,则取得对象跳离程序。反之则执行 Step4。
Step4:由 XML 档案建立对象,并存入 Cache,并传回该物件。

假设 TMySetting 对象要使用上述的快取方案,程序代码实作如下。程序代码中处理反序列化的 TBSerializerUtil.XmlFileToObject 方法 ,请参阅「物件序列化函式库」一文。



'''
''' 依键值取得 TMySetting 对象。
'''

''' 键值。
Public Function GetMySetting(ByVal Key As String) As TMySeting
Dim sCacheKey As String
Dim oCache As System.Web.Caching.Cache
Dim oMySeting As TMySeting
Dim sFileName As String

'快取键值为「型别名称.键值」
sCacheKey = String.Format("{0}.{1}", GetType(TMySeting).Name, Key)

'ASP.NET 快取物件
oCache = System.Web.HttpContext.Current.Cache

If oCache(sCacheKey) IsNot Nothing Then
'直接由 Cache 取得该对象
oMySeting = CType(oCache(sCacheKey), TMySeting)
Else
'由 XML 档案建立对象
sFileName = "对象对应的文件名称"
oMySeting = CType(TBSerializerUtil.XmlFileToObject(sFileName, GetType(TMySeting)), TMySeting)

'将对象存入 Cache,并设定快取移除时机
'1.档案相依
'2.浮动时间为 5 分钟
oCache.Insert(sCacheKey, oMySeting, _
New System.Web.Caching.CacheDependency(sFileName), _
System.Web.Caching.Cache.NoAbsoluteExpiration, New TimeSpan(0, 5, 0))
End If
Return oMySeting
End Function


后记:
在 EnterpriseLibrary 中也有处理快取的 CacheManager 对象,它类似 ASP.NET 的 Cache 物件。若是撰写三层式架构,也可以使用 EnterpriseLibrary 的 CacheManager 来取代 ASP.NET Cache,往后有时间再来介绍 EnterpriseLibrary 的 CacheManager 对象。

部署 aspnet2.0 应用时常见的错误


在部署webservices时老是会遇到 http 404, 500 错误, 有很多次遇到的问题都是以前出现并解决过的, 只是自己忘记了,

又要重新google资料, 浪费了很多时间, 所以写这篇BLOG作为参考, 如有遗漏, 欢迎补充




问题列表:



404 错误


解决办法:

打开IIS中的 web服务扩展,如果是禁止的, 请疯狂点击 “允许”按钮, 以表达心中的郁闷


打开站点的属性,确保 Aspnet 的版本是2.0的


在对应的 framework 安装目录下运行 aspnet_regiis /i (默认的路径是 C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727)


服务是否都已经打开, 需要的服务有 "ASP.NET 状态服务“, "IIS Admin Services", ”Remote Procedure Call“


500 错误

解决办法

500错误遇到的比较少, 如果使用了 404 错误中的所有方法也不能解决问题, 需要再看一下 ”Distributed Transaction Coordinator“


这个服务是否打开, 一般都是由于这个问题引起的。

VSS 2005 使用配置

服务器端设置

VSS2005的Internet访问

本次配置VSS2005运行环境:Win2003/DotNet2.0

注:VSS要求iis 支持 asp.net 2.0, 因为VSS2005的运行库是2.0,如果同时有1.1,则要在IIS的设置将source safe的run-time改为2.0(asp.net2.0的run-time在安装VSS2005时自动会帮你装上)

一、VSS2005服务器的设置
1、用具有管理员权限的账房登录WEB服务器;
2、运行Visual SourceSafe Administrator Visual SourceSafe Administrator (如果还没有在WEB服务器上安装VSS2005,请先安装)在 vss administrator中建立一个新的VSS DB。然后先用本地方式打开,如D:\vss,然后在server->configure中勾上“enable sourcesafe internet for this computer"



此时VSS2005将自动在IIS中建立虚拟目录:SourceSafe

3、在资源管理器中共享vss 目录,设置好权限(Administrator取得该目录的所有权限);

4、然后在vss administrator中重新用\\computer name\vss打开




点击“Add...”按钮,选取第一项“Connect to an existing database”




下一步,输入Web服务器下的Vss共享目录(这里要注意一点细节,如果输入IP地址很有可能在下一步会出现一点小问题,当然如果网络没有什么问题是可以的,俺建议还是使用点击“Browse...”从网上邻居上选择刚才共享的Vss目录)



下一步,完成后打开数据库(因为需要通过INTERNET远程访问VSS,所以打开数据库时不能使用类似于D:\VSS之类的目录名,而应是\\机器名\VSS数据库所在的共享名,例如:\\MyServer\vss,要求数据库;),再进入server->configure菜单,再勾上“enable sourcesafe internet for this database"

点击确定,此时VSS2005将自动又在虚拟目录Sourcesafe下再建立两个虚拟目录:VssUpload_db1, VssDownload_db1
中间会有一些提示,不管他,回答yes就是了。(第一次时会提示是否需要使用SSL,如果需要可以配置使用SSL。)
IIS自动创建的虚拟目录如下(IIS下完全不需要手工建立虚拟目录):


5、为了确认是否设置好,再次打开上面的对话框,看一下“Enable SourceSafe Internet for this database”是否被选中了,如果没有则说明你没有使用“\\机器名\VSS数据库所在的共享名”打开数据库,请重新采用这种方式打开再试;

客户端设置

二、客户端的设置(只能在VS2005中使用VSS INTERNET插件来访问上面设置好的数据库,直接使用权用VSS客户端是不可以的)
1、运行VS2005;
2、选择:工具-选项,打开选项对话框

3、选中“显示所有设置”,选择:源代码管理-插件选择,在右边选择:Microsoft Visual SourceSafe(Internet)

4、选择:源代码管理-插件设置,在右边点击高级按钮会出现:

如果在服务器设定部分选择了使用SSL就选中上面的复选框,否则不选择,单击OK确定。
5、保存选项并关闭它;
6、好了,现在可以把代码添加进去了,在需要添加到VSS的项目或工程上右击,选择“将解决方案添加到源代码管理”:

点击“Add...”按钮



输入服务器IP地址或域名(俺的测试机器为192.168.1.2,如果你使用的是域名可以输入如www.web3.cn),再输入Web服务器共享的Vss文件夹格式为\\compter name\vss(此地址并不是实际上你的机器要访问的地址,而是通过如http://www.web3.cn/SourceSafe/VssService.asmx)




下一步,要求您输入用户和密码(此处输入的是Win系统的用户和密码,也就是刚才设置共享权限的用户,至于VSS2005的密码是另外输入的,这个要分清楚)




选中数据库,点击“Open”打开来进行访问的,\\compter name\vss地址是提供给服务器读取本地共享目录



7、单击“OK”按钮按提示就可以向VSS2005中上传源代码了(如果没有使用SSL,中间会有安全警告,不用管它,继续)

3、选中“显示所有设置”,选择:源代码管理-插件选择,在右边选择:Microsoft Visual SourceSafe(Internet)


4、选择:源代码管理-插件设置,在右边点击高级按钮会出现:

如果在服务器设定部分选择了使用SSL就选中上面的复选框,否则不选择,单击OK确定。
5、保存选项并关闭它;
6、好了,现在可以把代码添加进去了,在需要添加到VSS的项目或工程上右击,选择“将解决方案添加到源代码管理”:

点击“Add...”按钮


输入服务器IP地址或域名(俺的测试机器为192.168.1.2,如果你使用的是域名可以输入如www.web3.cn),再输入Web服务器共享的Vss文件夹格式为\\compter name\vss(此地址并不是实际上你的机器要访问的地址,而是通过如http://www.web3.cn/SourceSafe/VssService.asmx)



下一步,要求您输入用户和密码(此处输入的是Win系统的用户和密码,也就是刚才设置共享权限的用户,至于VSS2005的密码是另外输入的,这个要分清楚)




选中数据库,点击“Open”打开来进行访问的,\\compter name\vss地址是提供给服务器读取本地共享目录



7、单击“OK”按钮按提示就可以向VSS2005中上传源代码了(如果没有使用SSL,中间会有安全警告,不用管它,继续)

EXCEL模板读写说明

简述
1.1 使用范围
该操作适用于VS2005及以上版本。



函数调用说明
1.1.1 引用的添加
右键添加引用Excel Library,VS2005为11.0版本,VS2008为12.0版本



添加引用后会在程序的bin目录下有一个Interop.Excel.dll文件,在应用程序页面添加引用的命名空间 using Excel;





1.1.2 模板的读写方法
读模板的话,首先模板存放在某个路径下,根据模板把从数据库里取出的数据写回EXCEL然后生成一个新的EXCEL存放都另一个路径以供下载,模板不变。

//建立一个Excel.Application的新进程

Excel.Application app = new Excel.Application();

if (app == null)

{

return;

}

app.Visible = false;

app.UserControl = true;

Workbooks workbooks = app.Workbooks;

_Workbook workbook = workbooks.Add(template_path + """测试.xls");//这里的Add方法里的参数就是模板的路径 Sheets sheets = workbook.Worksheets;

_Worksheet worksheet = (_Worksheet)sheets.get_Item(1); //这里表示模板只有一个sheet表

if (worksheet == null)

{

return;

}



如果没有实际的物理模板,可以继承一个空的模板进行读写

_Workbook workbook = workbooks.Add(XlWBATemplate.xlWBATWorksheet);//这里的Add方法里的参数就相当于继承了一个空模板

单元格的赋值如下

worksheet.Cells[i, i] = "达达集团";//表示EXCEL表的第i行第i列,对其它行列的赋值也一样

1.1.3 方法的调用
关于单元格处理的操作类在ExcelOperate.cs类文件里

设置连续单元格边框颜色

worksheet.get_Range(worksheet.Cells[i,1],worksheet.Cells[i, 14]).Borders.Color = System.Drawing.ColorTranslator.ToOle(System.Drawing.Color.Black);//表示第i行的第1列到14列的边框颜色为黑色,只需给定开始单元格和结束单元格

设置行高

excelOperate.SetRowHeight(worksheet, worksheet.Cells[i,1],worksheet.Cells[i, 14], 24.75);//行高设定为24.75

设置连续单元格的背景颜色

excelOperate.SetBgColor(worksheet, worksheet.Cells[i,1],worksheet.Cells[i,14], System.Drawing.Color.Silver);//设置背景颜色为银白色

设置连续单元格的字体大小

excelOperate.SetFontSize(worksheet, worksheet.Cells[i,1],worksheet.Cells[i,14],16);//字体设为16号大小

单元格字体的设置

excelOperate.SetBold(worksheet, worksheet.Cells[i,1],worksheet.Cells[i,14]);//黑体字

单元格字体颜色设置

excelOperate.SetColor(worksheet, worksheet.Cells[i,1],worksheet.Cells[i,14], System.Drawing.Color.Red); //体字颜色设为红色

单元格内容剧中显示设置

excelOperate.SetHAlignCenter(worksheet, worksheet.Cells[i,1],worksheet.Cells[i,14]);

合并单元格

worksheet.get_Range(worksheet.Cells[i,1],worksheet.Cells[i,14]).Merge(Missing.Value);//合并第i行1至14列的单元格 worksheet.get_Range (worksheet.Cells[i,1],worksheet.Cells[i,1]).Value2 = "无赖集团";//给合并后的单元格赋值,合并后的单元格为第i行的第一个单元格

(注:如果在程序中动态合并单元格,合并前必须保证各单元格的内容为空,所以一般在合并相同内容的单元格之前,先保存原有的值,清空要合并的单元格的值,然后进行合并操作再赋值即可,之所以要清空是因为我们在操作实际的EXCEL合并单元格的时候,如果各单元格有值,鼠标操作合并的时候会提示“选定区域包含多重数值。合并到一个单元格后只能保留最左上角的数据” ,点确定后单元格会保留最左上单元格的值,而程序执行过程中遇到这种情况会直接导致程序执行中断,无法正常导出)



保存生成的EXCEL文件

workbook.SaveAs(save_path, Missing.Value, Missing.Value, Missing.Value, Missing.Value, Missing.Value, Excel.XlSaveAsAccessMode.xlNoChange, Missing.Value, Missing.Value, Missing.Value, Missing.Value, Missing.Value);//save_path为要保存到的文件路径

excelOperate.Dispose(worksheet, workbook, app);//生成好EXCEL文件后关闭Excel进程



EXCEL模板中含多个sheet的操作

1. 如果模板中只包含固定的多各sheet(以2各为例),可以这样进行读写访问

_Worksheet worksheet1 = (_Worksheet)sheets.get_Item(1);//读第一个sheet

_Worksheet worksheet2 = (_Worksheet)sheets.get_Item(2);//读第二个sheet

赋值操作和上面说的类似,如下:

Worksheet1.Cells[i, i] = "赖达犯罪集团";

Worksheet2.Cells[i, i] = "赖达破坏集团";



2. 如果要在应用程序中动态生成多个sheet(以其中一个sheet为样本生成),如要按月份生成每月一个sheet,那么模板中只需包含一个sheet模板,可根据选择的月份个数在应用程序中复制相同的几个sheet即可。

for (int i = 1; i < monthCount; i++)

worksheet.Copy(Missing.Value, workbook.Worksheets[1]);//月统计工作薄,如果选择了三个月,即monthCount=3,那么就会再复制两个相同的sheet,然后进行读写操作,如下面的代码片段:



int item_id = 1;

_Worksheet ws = null;

for (int i = 0; i < tableMM.Rows.Count; i++)

{

……

if (tableMM.Rows[i]["DATE_MONTH"].ToString() == Month)

{

ws = (_Worksheet)sheets.get_Item(item_id);

ws.Cells[3 + i, 1] = rowNum;

ws.Cells[3 + i, 2] = tableMM.Rows[i]["PROJNO"];

continue;

}

……

ws.Name = "XX月份";//sheet的名称

item_id++;

}

同步更新缓存中的数据集(转贴)

将一个取自SQL Server的数据集放入缓存中了现在想数据库更新的时候缓存中的数据集同步更新


第一步

修改web,config

















第二步.定义cachedData测试类

using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using System.Web.Caching;
using System.Data.SqlClient;

///
/// Summary description for CachedData
///

public class CachedData
{
private string Key;
private string _Source;
///
/// 指示数据从哪儿读取的
///

public string Source { get { return _Source; } }
public CachedData()
{
Key = "Categories";
_Source = "未知";
}

//读取数据
public DataView getFromCache() {
if (HttpRuntime.Cache[Key] == null)
{
//取数据
SqlConnection conn = new SqlConnection();
conn.ConnectionString = ConfigurationManager.ConnectionStrings["NorthwindConnectionString"].ConnectionString;
SqlCommand comm = new SqlCommand("SELECT [CategoryID], [CategoryName], [Description] FROM [Categories]", conn);
SqlDataAdapter sda = new SqlDataAdapter(comm);
DataSet ds = new DataSet();
conn.Open();
sda.Fill(ds);
DataView dv = ds.Tables[0].DefaultView;
conn.Close();

//启用更改通知
SqlCacheDependencyAdmin.EnableNotifications(ConfigurationManager.ConnectionStrings["NorthwindConnectionString"].ConnectionString);
//连接到 SQL Server 数据库并为 SqlCacheDependency 更改通知准备数据库表
SqlCacheDependencyAdmin.EnableTableForNotifications(ConfigurationManager.ConnectionStrings["NorthwindConnectionString"].ConnectionString, "Categories");
//制定缓存策略
SqlCacheDependency scd = new SqlCacheDependency("Categories", "Categories");
//插入缓存
HttpRuntime.Cache.Insert(Key, dv, scd);
_Source = "Database";
return dv;
}
else {
//从缓存中取值
_Source = "cache";
return (DataView)HttpRuntime.Cache[Key];

}
}
}


3.测试页面

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default.aspx.cs" Inherits="_Default" %>







 



其对应cs文件

using System;
using System.Data;
using System.Data.SqlClient;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;

public partial class _Default : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{

BindRepeater1();

}
private void BindRepeater1(){
CachedData cd=new CachedData();
this.Repeater1.DataSource = cd.getFromCache();
this.Repeater1.DataBind();
this.Label1.Text = cd.Source;
}
}

.Net 2.0 缓存使用(转贴)

在访问量大,但更新较少的网站中使用缓存,可以大大提高运行效率;加上.NET 2.0提供的缓存依赖机制,我们可以很方便的对缓存进行管理更新;以下是本人学习的一点心得体会,希望能够起到抛砖引玉的作用。
建立缓存依赖:



/**////
/// 建立缓存依赖项
///

///
private AggregateCacheDependency TableDependency()
{
AggregateCacheDependency dependency = new AggregateCacheDependency();
dependency.Add(new SqlCacheDependency("MSPetShop4", "表名称"));

return dependency;
}



一个非常简单的方法,首先我们先看看两个.NET 2.0新增的两个类:

AggregateCacheDependency在System.Web.Caching命名空间中,AggregateCacheDependency主要作用是用于组合 ASP.NET 应用程序的 Cache 对象中存储的项和 CacheDependency 对象的数组之间的多个依赖项。

SqlCacheDependency也存在于System.Web.Caching命名空间中,这个类用于建立ASP.NET应用程序的Cache对象中存储的项和特定SQL Server数据库表之间的联系。

SqlCacheDependency是如何建立Cache对象中存储的项和特定SQL Server数据库表之间的联系的呢?看一下Web.Config配置文件就一目了然了。



















配置节中配置了数据库信息,SqlCacheDependency类会自动完成对此配置节信息的读取以建立和数据库之间的联系。(注意)name="MSPetShop4"必须和new

SqlCacheDependency("MSPetShop4", "表名称")中的数据库名称相一致。更多的配置信息可以查看(MSDN帮助文档)。

使数据库支持SqlCacheDependency特性:

要使得7.0或者2000版本的SQL Server支持SqlCacheDependency特性,需要对数据库服务器执行相关的配置步骤。有两种方法配置SQL Server:

使用aspnet_regsql命令行工具,或者使用SqlCacheDependencyAdmin类。

aspnet_regsql工具位于Windows\Microsoft.NET\Framework\[版本]文件夹中,如果要配置SqlCacheDependency,则需要以命令行的方式执行。

以下是该工具的命令参数说明:
-? 显示该工具的帮助功能;
-S 后接的参数为数据库服务器的名称或者IP地址;
-U 后接的参数为数据库的登陆用户名;
-P 后接的参数为数据库的登陆密码;
-E 当使用windows集成验证时,使用该功能;
-d 后接参数为对哪一个数据库采用SqlCacheDependency功能;
-t 后接参数为对哪一个表采用SqlCacheDependency功能;
-ed 允许对数据库使用SqlCacheDependency功能;
-dd 禁止对数据库采用SqlCacheDependency功能;
-et 允许对数据表采用SqlCacheDependency功能;
-dt 禁止对数据表采用SqlCacheDependency功能;
-lt 列出当前数据库中有哪些表已经采用sqlcachedependency功能。

比如在petshop4.0的数据库中使用SqlCacheDependency特性:aspnet_regsql -S localhost -E -d MSPetShop4 -ed

以上面的命令为例,说明将对名为MSPetShop4的数据库采用SqlCacheDependency功能,且SQL Server采用了windows集成验证方式。我们还可以

对相关的数据表执行aspnet_regsql命令,如:
aspnet_regsql -S localhost -E -d MSPetShop4 -t Item -et
aspnet_regsql -S localhost -E -d MSPetShop4 -t Product -et
aspnet_regsql -S localhost -E -d MSPetShop4 -t Category -et

最后为使用缓存:



protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
string key = "TableCache"; //缓存名称
DataSet data = (DataSet)HttpRuntime.Cache[key]; //获取缓存

// 判断缓存数据为空
if (data == null)
{
// 获取数据
data = GetDataSource();

// 创建缓存依赖
AggregateCacheDependency cd = TableDependency();

// 创建缓存
HttpRuntime.Cache.Add(key, data, cd, DateTime.Now.AddHours(1), Cache.NoSlidingExpiration,

CacheItemPriority.High, null);
}

GridView1.DataSource = data; //绑定数据
GridView1.DataBind();
}
}




获取数据源的方法,结合实际使用做修改。



private DataSet GetDataSource()
{
string ConnectionStringLocal = ConfigurationManager.ConnectionStrings["LocalConnString"].ConnectionString;
SqlConnection connPubs = new SqlConnection(ConnectionStringLocal);
SqlDataAdapter dad = new SqlDataAdapter("SELECT TOP 50 * FROM Product", connPubs);
DataSet ds = new DataSet();
dad.Fill(ds);

return ds;
}

就这么简单:)

Windows2003下配置IIS访问ASP页面

windows 2003 粉墨登场,很多赶时髦的用户已经早就试用了,但尽管2003 号称安全性有很大突破,但其
默认支持.net 架构,而抛弃使用了很久的大众化的Asp 的路线缺饱受质疑,需要我们手动去配置很多东西。
在 IIS 6.0 中,默认设置是特别严格和安全的,这样可以最大限度地减少因以前太宽松的超时和限制而造
成的攻击。比如说默认配置数据库属性实施的最大 ASP 张贴大小为 204,800 个字节,并将各个字段限制
为 100 KB。在 IIS 6.0 之前的版本中,没有张贴限制。导致我们学校里面的应用系统往2003 移植经常会
出错。现汇总解决方案如下。
一、启用Asp 支持
Windows Server 2003 默认安装,是不安装 IIS 6 的,需要另外安装。安装完 IIS 6,还需要单独开启对
于 ASP 的支持。
第一步,启用Asp,进入:控制面板 -> 管理工具 ->IIS(Internet 服务器)- Web 服务扩展 -> Active Server
Pages -> 允许
控制面板 -> 管理工具 ->IIS(Internet 服务器)- Web 服务扩展 -> 在服务端的包含文件 -> 允许
第二步,启用父路径支持。
IIS-网站-主目录-配置-选项-启用父路径
第三步,权限分配
IIS-网站-(具体站点)-(右键)权限-Users 完全控制
二、解决windows2003 最大只能上载200K 的限制。
先在服务里关闭iis admin service 服务
找到windows\system32\inesrv\下的metabase.xml,
打开,找到ASPMaxRequestEntityAllowed 把他修改为需要的值,
然后重启iis admin service 服务
1、在web 服务扩展 允许 active server pages 和在服务器端的包含文件
2、修改各站点的属性
主目录-配置-选项-启用父路径
3、使之可以上传大于 200k 的文件(修改成您要的大小就可以了,如在后面补两个0,就允许20m 了)
c:\WINDOWS\system32\inetsrv\MetaBase.xml
(企业版的windows2003 在第592 行,默认为 AspMaxRequestEntityAllowed="204800" 即200K
将其加两个0,即改为,现在最大就可以上载20M 了。
AspMaxRequestEntityAllowed="20480000"

2008年8月27日星期三

PetShop之表示层设计 《解剖PetShop》系列之六

《解剖PetShop》系列之六六 PetShop之表示层设计
表示层(Presentation Layer)的设计可以给系统客户最直接的体验和最十足的信心。正如人与人的相交相识一样,初次见面的感觉总是永难忘怀的。一件交付给客户使用的产品,如果在用户界面(User Interface,UI)上缺乏吸引人的特色,界面不友好,操作不够体贴,即使这件产品性能非常优异,架构设计合理,业务逻辑都满足了客户的需求,却仍然难以讨得客户的欢心。俗语云:“佛要金装,人要衣装”,特别是对于Web应用程序而言,Web网页就好比人的衣装,代表着整个系统的身份与脸面,是招徕“顾客”的最大卖点。
“献丑不如藏拙”,作为艺术细胞缺乏的我,并不打算在用户界面的美术设计上大做文章,是以本书略过不提。本章所关注的表示层设计,还是以架构设计的角度,阐述在表示层设计中对模式的应用,ASP.NET控件的设计与运用,同时还包括了对ASP.NET 2.0新特色的介绍。
6.1 MVC模式
表示层设计中最重要的模式是MVC(Model-View-Controller,即模型-视图-控制器)模式。MVC模式最早是由SmallTalk语言研究团提出的,被广泛应用在用户交互应用程序中。Controller根据用户请求(Response)修改Model的属性,此时Event(事件)被触发,所有依赖于Model的View对象会自动更新,并基于Model对象产生一个响应(Response)信息,返回给Controller。Martin Fowler在《企业应用架构模式》一书中,展示了MVC模式应用的全过程,如图6-1所示:
图6-1 典型的MVC模式
如果将MVC模式拆解为三个独立的部分:Model、View、Controller,我们可以通过GOF设计模式来实现和管理它们之间的关系。在体系架构设计中,业务逻辑层的领域对象以及数据访问层的数据值对象都属于MVC模式的Model对象。如果要管理Model与View之间的关系,可以利用Observer模式,View作为观察者,一旦Model的属性值发生变化,就会通知View基于Model的值进行更新。而Controller作为控制用户请求/响应的对象,则可以利用Mediator模式,专门负责请求/响应任务之间的调节。而对于View本身,在面向组件设计思想的基础上,我们通常将它设计为组件或者控件,这些组件或者控件根据自身特性的不同,共同组成一种类似于递归组合的对象结构,因而我们可以利用Composite模式来设计View对象。
然而在.NET平台下,我们并不需要自己去实现MVC模式。对于View对象而言,ASP.NET已经提供了常用的Web控件,我们也可以通过继承System.Web.UI.UserControl,自定义用户控件,并利用ASPX页面组合Web控件来实现视图。ASP.NET定义了System.Web.UI.Page类,它相当于MVC模式的Controller对象,可以处理用户的请求。由于利用了codebehind技术,使得用户界面的显示与UI实现逻辑完全分离,也即是说,View对象与Controller对象成为相对独立的两部分,从而有利于代码的重用性。比较ASP而言,这种编程方式更符合开发人员的编程习惯,同时有利于开发人员与UI设计人员的分工与协作。至于Model对象,则为业务逻辑层的领域对象。此外,.NET平台通过ADO.NET提供了DataSet对象,便于与Web控件的数据源绑定。
6.2 Page Controller模式的应用
通观PetShop的表示层设计,充分利用了ASP.NET的技术特点,通过Web页面与用户控件控制和展现视图,并利用codebehind技术将业务逻辑层的领域对象加入到表示层实现逻辑中,一个典型的Page Controller模式呼之欲出。
Page Controller模式是Martin Fowler在《企业应用架构模式》中最重要的表示层模式之一。在.NET平台下,Page Controller模式的实现非常简单,以Products.aspx页面为例。首先在aspx页面中,进行如下的设置:
<%@ Page AutoEventWireup="true" Language="C#" MasterPageFile="~/MasterPage.master" Title="Products" Inherits="PetShop.Web.Products" CodeFile="~/Products.aspx.cs" %>
Aspx页面继承自System.Web.UI.Page类。Page类对象通过继承System.Web.UI.Control类,从而拥有了Web控件的特性,同时它还实现了IHttpHandler接口。作为ASP.NET处理HTTP Web请求的接口,提供了如下的定义:
[AspNetHostingPermission(SecurityAction.InheritanceDemand, Level=AspNetHostingPermissionLevel.Minimal), AspNetHostingPermission(SecurityAction.LinkDemand, Level=AspNetHostingPermissionLevel.Minimal)]public interface IHttpHandler{ void ProcessRequest(HttpContext context); bool IsReusable { get; }}
Page类实现了ProcessRequest()方法,通过它可以设置Page对象的Request和Response属性,从而完成对用户请求/相应的控制。然后Page类通过从Control类继承来的Load事件,将View与Model建立关联,如Products.aspx.cs所示:
public partial class Products : System.Web.UI.Page { protected void Page_Load(object sender, EventArgs e) { //get page header and title Page.Title = WebUtility.GetCategoryName(Request.QueryString["categoryId"]); }}
事件机制恰好是observer模式的实现,当ASPX页面的Load事件被激发后,系统通过WebUtility类(在第28章中有对WebUtility类的详细介绍)的GetCategoryName()方法,获得Category值,并将其显示在页面的Title上。Page对象作为Controller,就好似一个调停者,用于协调View与Model之间的关系。
由于ASPX页面中还可以包含Web控件,这些控件对象同样是作为View对象,通过Page类型对象完成对它们的控制。例如在CheckOut.aspx页面中,当用户发出CheckOut的请求后,作为System.Web.UI.WebControls.Winzard控件类型的wzdCheckOut,会在整个向导过程结束时,触发FinishButtonClick事件,并在该事件中调用领域对象Order的Insert()方法,如下所示:
public partial class CheckOut : System.Web.UI.Page protected void wzdCheckOut_FinishButtonClick(object sender, WizardNavigationEventArgs e) { if (Profile.ShoppingCart.CartItems.Count > 0) { if (Profile.ShoppingCart.Count > 0) { // display ordered items CartListOrdered.Bind(Profile.ShoppingCart.CartItems); // display total and credit card information ltlTotalComplete.Text = ltlTotal.Text; ltlCreditCardComplete.Text = ltlCreditCard.Text; // create order OrderInfo order = new OrderInfo(int.MinValue, DateTime.Now, User.Identity.Name, GetCreditCardInfo(), billingForm.Address, shippingForm.Address, Profile.ShoppingCart.Total, Profile.ShoppingCart.GetOrderLineItems(), null); // insert Order newOrder = new Order(); newOrder.Insert(order); // destroy cart Profile.ShoppingCart.Clear(); Profile.Save(); } } else { lblMsg.Text = "


Can not process the order. Your cart is empty.

Continue shopping

"; wzdCheckOut.Visible = false; } }
在上面的一段代码中,非常典型地表达了Model与View之间的关系。它通过获取控件的属性值,作为参数值传递给数据值对象OrderInfo,从而利用页面上产生的订单信息创建订单对象,然后再调用领域对象Order的Inser()方法将OrderInfo对象插入到数据表中。此外,它还对领域对象ShoppingCart的数据项作出判断,如果其值等于0,就在页面中显示UI提示信息。此时,View的内容决定了Model的值,而Model值反过来又决定了View的显示内容。
6.3 ASP.NET控件
ASP.NET控件是View对象最重要的组成部分,它充分利用了面向对象的设计思想,通过封装与继承构建一个个控件对象,使得用户在开发Web页面时,能够重用这些控件,甚至自定义自己的控件。在第8章中,我已经介绍了.NET Framework中控件的设计思想,通过引入一种“复合方式”的Composite模式实现了控件树。在ASP.NET控件中,System.Web.UI.Control就是这棵控件树的根,它定义了所有ASP.NET控件共有的属性、方法和事件,并负责管理和控制控件的整个执行生命周期。Control基类并没有包含UI的特定功能,如果需要提供与UI相关的方法属性,就需要从System.Web.UI.WebControls.WebControl类派生。该类实际上也是Control类的子类,但它附加了诸如ForeColor、BackColor、Font等属性。除此之外,还有一个重要的类是System.Web.UI.UserControl,即用户控件类,它同样是Control类的子类。我们可以自定义一些用户控件派生自UserControl,在Visual Studio的Design环境下,我们可以通过拖动控件的方式将多种类型的控件组合成一个自定义用户控件,也可以在codebehind方式下,为自定义用户控件类添加新的属性和方法。整个ASP.NET控件类的层次结构如图6-2所示:
图6-2 ASP.NET控件类的层次结构
ASP.NET控件的执行生命周期如表6-1所示:
阶段
控件需要执行的操作
要重写的方法或事件
初始化
初始化在传入 Web 请求生命周期内所需的设置。
Init 事件(OnInit 方法)
加载视图状态
在此阶段结束时,就会自动填充控件的 ViewState 属性,控件可以重写 LoadViewState 方法的默认实现,以自定义状态还原。
LoadViewState 方法
处理回发数据
处理传入窗体数据,并相应地更新属性。注意:只有处理回发数据的控件参与此阶段。
LoadPostData 方法(如果已实现 IPostBackDataHandler)
加载
执行所有请求共有的操作,如设置数据库查询。此时,树中的服务器控件已创建并初始化、状态已还原并且窗体控件反映了客户端的数据。
Load 事件(OnLoad 方法)
发送回发更改通知
引发更改事件以响应当前和以前回发之间的状态更改。注意:只有引发回发更改事件的控件参与此阶段。
RaisePostDataChangedEvent 方法(如果已实现 IPostBackDataHandler)
处理回发事件
处理引起回发的客户端事件,并在服务器上引发相应的事件。 注意:只有处理回发事件的控件参与此阶段。
RaisePostBackEvent 方法(如果已实现 IPostBackEventHandler)
预呈现
在呈现输出之前执行任何更新。可以保存在预呈现阶段对控件状态所做的更改,而在呈现阶段所对的更改则会丢失。
PreRender 事件(OnPreRender 方法)
保存状态
在此阶段后,自动将控件的 ViewState 属性保持到字符串对象中。此字符串对象被发送到客户端并作为隐藏变量发送回来。为了提高效率,控件可以重写 SaveViewState 方法以修改 ViewState 属性。
SaveViewState 方法
呈现
生成呈现给客户端的输出。
Render 方法
处置
执行销毁控件前的所有最终清理操作。在此阶段必须释放对昂贵资源的引用,如数据库链接。
Dispose 方法
卸载
执行销毁控件前的所有最终清理操作。控件作者通常在 Dispose 中执行清除,而不处理此事件。
UnLoad 事件(On UnLoad 方法)
表6-1 ASP.NET控件的执行生命周期
在这里,控件设计利用了Template Method模式,Control基类提供了大部分protected虚方法,留待其子类改写其方法。以PetShop 4.0为例,就定义了两个ASP.NET控件,它们都属于System.Web.UI.WebControls.WebControl的子类。其中,CustomList控件派生自System.Web.UI.WebControls.DataList,CustomGrid控件则派生自System.Web.UI.WebControls.Repeater。
由于这两个控件都改变了其父类控件的呈现方式,故而,我们可以通过重写父类的Render虚方法,完成控件的自定义。例如CustomGrid控件:
public class CustomGrid : Repeater…//Static constants protected const string HTML1 = "
"; protected const string HTML2 = "
"; protected const string HTML3 = ""; protected const string HTML4 = "
"; private static readonly Regex RX = new Regex(@"^&page=\d+", RegexOptions.Compiled); private const string LINK_PREV = "; private const string LINK_MORE = "";private const string KEY_PAGE = "page"; private const string COMMA = "?"; private const string AMP = "&";override protected void Render(HtmlTextWriter writer) { //Check there is some data attached if (ItemCount == 0) { writer.Write(emptyText); return; } //Mask the query string query = Context.Request.Url.Query.Replace(COMMA, AMP); query = RX.Replace(query, string.Empty); // Write out the first part of the control, the table header writer.Write(HTML1); // Call the inherited method base.Render(writer); // Write out a table row closure writer.Write(HTML2); //Determin whether next and previous buttons are required //Previous button? if (currentPageIndex > 0) writer.Write(string.Format(LINK_PREV, (currentPageIndex - 1) + query)); //Close the table data tag writer.Write(HTML3); //Next button? if (currentPageIndex < PageCount) writer.Write(string.Format(LINK_MORE, (currentPageIndex + 1) + query)); //Close the table writer.Write(HTML4); }
由于CustomGrid继承自Repeater控件,因而它同时还继承了Repeater的DataSource属性,这是一个虚属性,它默认的set访问器属性如下:
public virtual object DataSource{ get {… } set { if (((value != null) && !(value is IListSource)) && !(value is IEnumerable)) { throw new ArgumentException(SR.GetString("Invalid_DataSource_Type", new object[] { this.ID })); } this.dataSource = value; this.OnDataPropertyChanged(); }}
对于CustomGrid而言,DataSource属性有着不同的设置行为,因而在定义CustomGrid控件的时候,需要改写DataSource虚属性,如下所示:
private IList dataSource;private int itemCount;override public object DataSource { set { //This try catch block is to avoid issues with the VS.NET designer //The designer will try and bind a datasource which does not derive from ILIST try { dataSource = (IList)value; ItemCount = dataSource.Count; } catch { dataSource = null; ItemCount = 0; } }}
当设置的value对象值不为IList类型时,set访问器就将捕获异常,然后将dataSource字段设置为null。
由于我们改写了DataSource属性,因而改写Repeater类的OnDataBinding()方法也就势在必行。此外,CustomGrid还提供了分页的功能,我们也需要实现分页的相关操作。与DataSource属性不同,Repeater类的OnDataBinding()方法实际上是继承和改写了Control基类的OnDataBinding()虚方法,而我们又在此基础上改写了Repeater类的OnDataBinding()方法:
override protected void OnDataBinding(EventArgs e) { //Work out which items we want to render to the page int start = CurrentPageIndex * pageSize; int size = Math.Min(pageSize, ItemCount - start); IList page = new ArrayList(); //Add the relevant items from the datasource for (int i = 0; i < size; i++) page.Add(dataSource[start + i]); //set the base objects datasource base.DataSource = page; base.OnDataBinding(e);}
此外,CustomGrid控件类还增加了许多属于自己的属性和方法,例如PageSize、PageCount属性以及SetPage()方法等。正是因为ASP.NET控件引入了Composite模式与Template Method模式,当我们在自定义控件时,就可以通过继承与改写的方式来完成控件的设计。自定义ASP.NET控件一方面可以根据系统的需求实现特定的功能,也能够最大限度地实现对象的重用,既可以减少编码量,同时也有利于未来对程序的扩展与修改。在PetShop 4.0中,除了自定义了上述WebControl控件的子控件外,最主要的还是利用了用户控件。在Controls文件夹下,一共定义了11个用户控件,内容涵盖客户地址信息、信用卡信息、购物车信息、期望列表(Wish List)信息以及导航信息、搜索结果信息等。它们相当于是一些组合控件,除了包含了子控件的方法和属性外,也定义了一些必要的UI实现逻辑。以ShoppingCartControl用户控件为例,它会在该控件被呈现(Render)之前,做一些数据准备工作,获取购物车数据,并作为数据源绑定到其下的Repeater控件:
public partial class ShoppingCartControl : System.Web.UI.UserControl protected void Page_PreRender(object sender, EventArgs e) { if (!IsPostBack) { BindCart(); } } private void BindCart() { ICollection cart = Profile.ShoppingCart.CartItems; if (cart.Count > 0) { repShoppingCart.DataSource = cart; repShoppingCart.DataBind(); PrintTotal(); plhTotal.Visible = true; } else { repShoppingCart.Visible = false; plhTotal.Visible = false; lblMsg.Text = "Your cart is empty."; } }
在ShoppingCart页面下,我们可以加入该用户控件,如下所示:

由于ShoppingCartControl用户控件已经实现了用于呈现购物车数据的逻辑,那么在ShoppingCart.aspx.cs中,就可以不用负责这些逻辑,在充分完成对象重用的过程中,同时又达到了职责分离的目的。用户控件的设计者与页面设计者可以互不干扰,分头完成自己的设计。特别是对于页面设计者而言,他可以是单一的UI设计人员角色,仅需要关注用户界面是否美观与友好,对于表示层中对领域对象的调用与操作就可以不必理会,整个页面的代码也显得结构清晰、逻辑清楚,无疑也“干净”了不少。
6.4 ASP.NET 2.0新特性
由于PetShop 4.0是基于.NET Framework 2.0平台开发的电子商务系统,因而它在表示层也引入了许多ASP.NET 2.0的新特性,例如MemberShip、Profile、Master Page、登录控件等特性。接下来,我将结合PetShop 4.0的设计分别介绍它们的实现。
6.4.1 Profile特性
Profile提供的功能是针对用户的个性化服务。在ASP.NET 1.x版本时,我们可以利用Session、Cookie等方法来存储用户的状态信息。然而Session对象是具有生存期的,一旦生存期结束,该对象保留的值就会失效。Cookie将用户信息保存在客户端,它具有一定的安全隐患,一些重要的信息不能存储在Cookie中。一旦客户端禁止使用Cookie,则该功能就将失去应用的作用。
Profile的出现解决了如上的烦恼,它可以将用户的个人化信息保存在指定的数据库中。ASP.NET 2.0的Profile功能默认支持Access数据库和SQL Server数据库,如果需要支持其他数据库,可以编写相关的ProfileProvider类。Profile对象是强类型的,我们可以为用户信息建立属性,以PetShop 4.0为例,它建立了ShoppingCart、WishList和AccountInfo属性。
由于Profile功能需要访问数据库,因而在数据访问层(DAL)定义了和Product等数据表相似的模块结构。首先定义了一个IProfileDAL接口模块,包含了接口IPetShopProfileProvider:
public interface IPetShopProfileProvider { AddressInfo GetAccountInfo(string userName, string appName); void SetAccountInfo(int uniqueID, AddressInfo addressInfo); IList GetCartItems(string userName, string appName, bool isShoppingCart); void SetCartItems(int uniqueID, ICollection cartItems, bool isShoppingCart); void UpdateActivityDates(string userName, bool activityOnly, string appName); int GetUniqueID(string userName, bool isAuthenticated, bool ignoreAuthenticationType, string appName); int CreateProfileForUser(string userName, bool isAuthenticated, string appName); IList GetInactiveProfiles(int authenticationOption, DateTime userInactiveSinceDate, string appName); bool DeleteProfile(string userName, string appName); IList GetProfileInfo(int authenticationOption, string usernameToMatch, DateTime userInactiveSinceDate, string appName, out int totalRecords);}
因为PetShop 4.0版本分别支持SQL Server和Oracle数据库,因而它分别定义了两个不同的PetShopProfileProvider类,实现IPetShopProfileProvider接口,并放在两个不同的模块SQLProfileDAL和OracleProfileDAL中。具体的实现请参见PetShop 4.0的源代码。同样的,PetShop 4.0为Profile引入了工厂模式,定义了模块ProfileDALFActory,工厂类DataAccess的定义如下:
public sealed class DataAccess { private static readonly string profilePath = ConfigurationManager.AppSettings["ProfileDAL"]; public static PetShop.IProfileDAL.IPetShopProfileProvider CreatePetShopProfileProvider() { string className = profilePath + ".PetShopProfileProvider"; return (PetShop.IProfileDAL.IPetShopProfileProvider)Assembly.Load(profilePath).CreateInstance(className); }}
在业务逻辑层(BLL)中,单独定义了模块Profile,它添加了对BLL、IProfileDAL和ProfileDALFactory模块的程序集。在该模块中,定义了密封类PetShopProfileProvider,它继承自System.Web.Profile.ProfileProvider类,该类作为Profile的Provider基类,用于在自定义配置文件中实现相关的配置文件服务。在PetShopProfileProvider类中,重写了父类ProfileProvider中的一些方法,例如Initialize()、GetPropertyValues()、SetPropertyValues()、DeleteProfiles()等方法。此外,还为ShoppingCart、WishList、AccountInfo属性提供了Get和Set方法。至于Provider的具体实现,则调用工厂类DataAccess创建的具体类型对象,如下所示:private static readonly IPetShopProfileProvider dal = DataAccess.CreatePetShopProfileProvider();
定义了PetShop.Profile.PetShopProfileProvider类后,才可以在web.config配置文件中配置如下的配置节:

在配置文件中,针对ShoppingCart、WishList和AccountInfo(它们的类型分别为PetShop.BLL.Cart、PetShop.BLL.Cart、PetShop.Model.AddressInfo)属性分别定义了ShoppingCartProvider、WishListProvider、AccountInfoProvider,它们的类型均为PetShop.Profile.PetShopProfileProvider类型。至于Profile的信息究竟是存储在何种类型的数据库中,则由以下的配置节决定:
而键值为ProfileDAL的值,正是Profile的工厂类PetShop.ProfileDALFactory.DataAccess在利用反射技术创建IPetShopProfileProvider类型对象时获取的。
在表示层中,可以利用页面的Profile属性访问用户的个性化属性,例如在ShoppingCart页面的codebehind代码ShoppingCart.aspx.cs中,调用Profile的ShoppingCart属性:
public partial class ShoppingCart : System.Web.UI.Page { protected void Page_PreInit(object sender, EventArgs e) { if (!IsPostBack) { string itemId = Request.QueryString["addItem"]; if (!string.IsNullOrEmpty(itemId)) { Profile.ShoppingCart.Add(itemId); Profile.Save(); // Redirect to prevent duplictations in the cart if user hits "Refresh" Response.Redirect("~/ShoppingCart.aspx", true); } } }}
在上述的代码中,Profile属性的值从何而来?实际上,在我们为web.config配置文件中对Profile进行配置后,启动Web应用程序,ASP.NET会根据该配置文件中的相关配置创建一个ProfileCommon类的实例。该类继承自System.Web.Profile.ProfileBase类。然后调用从父类继承来的GetPropertyValue和SetPropertyValue方法,检索和设置配置文件的属性值。然后,ASP.NET将创建好的ProfileCommon实例设置为页面的Profile属性值。因而,我们可以通过智能感知获取Profile的ShoppingCart属性,同时也可以利用ProfileCommon继承自ProfileBase类的Save()方法,根据属性值更新Profile的数据源。
6.4.2 Membership特性
PetShop 4.0并没有利用Membership的高级功能,而是直接让Membership特性和ASP.NET 2.0新增的登录控件进行绑定。由于.NET Framework 2.0已经定义了针对SQL Server的SqlMembershipProvider,因此对于PetShop 4.0而言,实现Membership比之实现Profile要简单,仅仅需要为Oracle数据库定义MembershipProvider即可。在PetShop.Membership模块中,定义了OracleMembershipProvider类,它继承自System.Web.Security.MembershipProvider抽象类。
OracleMembershipProvider类的实现具有极高的参考价值,如果我们需要定义自己的MembershipProvider类,可以参考该类的实现。事实上OracleMemberShip类的实现并不复杂,在该类中,主要是针对用户及用户安全而实现相关的行为。由于在父类MembershipProvider中,已经定义了相关操作的虚方法,因此我们需要作的是重写这些虚方法。由于与Membership有关的信息都是存储在数据库中,因而OracleMembershipProvider与SqlMembershipProvider类的主要区别还是在于对数据库的访问。对于SQL Server而言,我们利用aspnet_regsql工具为Membership建立了相关的数据表以及存储过程。也许是因为知识产权的原因,Microsoft并没有为Oracle数据库提供类似的工具,因而需要我们自己去创建membership的数据表。此外,由于没有创建Oracle数据库的存储过程,因而OracleMembershipProvider类中的实现是直接调用SQL语句。以CreateUser()方法为例,剔除那些繁杂的参数判断与安全性判断,SqlMembershipProvider类的实现如下:
public override MembershipUser CreateUser(string username, string password, string email, string passwordQuestion, string passwordAnswer, bool isApproved, object providerUserKey, out MembershipCreateStatus status){ MembershipUser user1; //前面的代码略; try { SqlConnectionHolder holder1 = null; try { holder1 = SqlConnectionHelper.GetConnection(this._sqlConnectionString, true); this.CheckSchemaVersion(holder1.Connection); DateTime time1 = this.RoundToSeconds(DateTime.UtcNow); SqlCommand command1 = new SqlCommand("dbo.aspnet_Membership_CreateUser", holder1.Connection); command1.CommandTimeout = this.CommandTimeout; command1.CommandType = CommandType.StoredProcedure; command1.Parameters.Add(this.CreateInputParam("@ApplicationName", SqlDbType.NVarChar, this.ApplicationName)); command1.Parameters.Add(this.CreateInputParam("@UserName", SqlDbType.NVarChar, username)); command1.Parameters.Add(this.CreateInputParam("@Password", SqlDbType.NVarChar, text2)); command1.Parameters.Add(this.CreateInputParam("@PasswordSalt", SqlDbType.NVarChar, text1)); command1.Parameters.Add(this.CreateInputParam("@Email", SqlDbType.NVarChar, email)); command1.Parameters.Add(this.CreateInputParam("@PasswordQuestion", SqlDbType.NVarChar, passwordQuestion)); command1.Parameters.Add(this.CreateInputParam("@PasswordAnswer", SqlDbType.NVarChar, text3)); command1.Parameters.Add(this.CreateInputParam("@IsApproved", SqlDbType.Bit, isApproved)); command1.Parameters.Add(this.CreateInputParam("@UniqueEmail", SqlDbType.Int, this.RequiresUniqueEmail ? 1 : 0)); command1.Parameters.Add(this.CreateInputParam("@PasswordFormat", SqlDbType.Int, (int) this.PasswordFormat)); command1.Parameters.Add(this.CreateInputParam("@CurrentTimeUtc", SqlDbType.DateTime, time1)); SqlParameter parameter1 = this.CreateInputParam("@UserId", SqlDbType.UniqueIdentifier, providerUserKey); parameter1.Direction = ParameterDirection.InputOutput; command1.Parameters.Add(parameter1); parameter1 = new SqlParameter("@ReturnValue", SqlDbType.Int); parameter1.Direction = ParameterDirection.ReturnValue; command1.Parameters.Add(parameter1); command1.ExecuteNonQuery(); int num3 = (parameter1.Value != null) ? ((int) parameter1.Value) : -1; if ((num3 <> 11)) { num3 = 11; } status = (MembershipCreateStatus) num3; if (num3 != 0) { return null; } providerUserKey = new Guid(command1.Parameters["@UserId"].Value.ToString()); time1 = time1.ToLocalTime(); user1 = new MembershipUser(this.Name, username, providerUserKey, email, passwordQuestion, null, isApproved, false, time1, time1, time1, time1, new DateTime(0x6da, 1, 1)); } finally { if (holder1 != null) { holder1.Close(); holder1 = null; } } } catch { throw; } return user1;}
代码中,aspnet_Membership_CreateUser为aspnet_regsql工具为membership创建的存储过程,它的功能就是创建一个用户。
OracleMembershipProvider类中对CreateUser()方法的定义如下:
public override MembershipUser CreateUser(string username, string password, string email, string passwordQuestion, string passwordAnswer, bool isApproved, object userId, out MembershipCreateStatus status) { //前面的代码略; //Create connection OracleConnection connection = new OracleConnection(OracleHelper.ConnectionStringMembership); connection.Open(); OracleTransaction transaction = connection.BeginTransaction(IsolationLevel.ReadCommitted); try { DateTime dt = DateTime.Now; bool isUserNew = true; // Step 1: Check if the user exists in the Users table: create if not int uid = GetUserID(transaction, applicationId, username, true, false, dt, out isUserNew); if(uid == 0) { // User not created successfully! status = MembershipCreateStatus.ProviderError; return null; } // Step 2: Check if the user exists in the Membership table: Error if yes. if(IsUserInMembership(transaction, uid)) { status = MembershipCreateStatus.DuplicateUserName; return null; } // Step 3: Check if Email is duplicate if(IsEmailInMembership(transaction, email, applicationId)) { status = MembershipCreateStatus.DuplicateEmail; return null; } // Step 4: Create user in Membership table int pFormat = (int)passwordFormat; if(!InsertUser(transaction, uid, email, pass, pFormat, salt, "", "", isApproved, dt)) { status = MembershipCreateStatus.ProviderError; return null; } // Step 5: Update activity date if user is not new if(!isUserNew) { if(!UpdateLastActivityDate(transaction, uid, dt)) { status = MembershipCreateStatus.ProviderError; return null; } } status = MembershipCreateStatus.Success; return new MembershipUser(this.Name, username, uid, email, passwordQuestion, null, isApproved, false, dt, dt, dt, dt, DateTime.MinValue); } catch(Exception) { if(status == MembershipCreateStatus.Success) status = MembershipCreateStatus.ProviderError; throw; } finally { if(status == MembershipCreateStatus.Success) transaction.Commit(); else transaction.Rollback(); connection.Close(); connection.Dispose(); }}
代码中,InsertUser()方法就是负责用户的创建,而在之前则需要判断创建的用户是否已经存在。InsertUser()方法的定义如下:
private static bool InsertUser(OracleTransaction transaction, int userId, string email, string password, int passFormat, string passSalt, string passQuestion, string passAnswer, bool isApproved, DateTime dt) { string insert = "INSERT INTO MEMBERSHIP (USERID, EMAIL, PASSWORD, PASSWORDFORMAT, PASSWORDSALT, PASSWORDQUESTION, PASSWORDANSWER, ISAPPROVED, CREATEDDATE, LASTLOGINDATE, LASTPASSWORDCHANGEDDATE) VALUES (:UserID, :Email, :Pass, :PasswordFormat, :PasswordSalt, :PasswordQuestion, :PasswordAnswer, :IsApproved, :CDate, :LLDate, :LPCDate)"; OracleParameter[] insertParms = { new OracleParameter(":UserID", OracleType.Number, 10), new OracleParameter(":Email", OracleType.VarChar, 128), new OracleParameter(":Pass", OracleType.VarChar, 128), new OracleParameter(":PasswordFormat", OracleType.Number, 10), new OracleParameter(":PasswordSalt", OracleType.VarChar, 128), new OracleParameter(":PasswordQuestion", OracleType.VarChar, 256), new OracleParameter(":PasswordAnswer", OracleType.VarChar, 128), new OracleParameter(":IsApproved", OracleType.VarChar, 1), new OracleParameter(":CDate", OracleType.DateTime), new OracleParameter(":LLDate", OracleType.DateTime), new OracleParameter(":LPCDate", OracleType.DateTime) }; insertParms[0].Value = userId; insertParms[1].Value = email; insertParms[2].Value = password; insertParms[3].Value = passFormat; insertParms[4].Value = passSalt; insertParms[5].Value = passQuestion; insertParms[6].Value = passAnswer; insertParms[7].Value = OracleHelper.OraBit(isApproved); insertParms[8].Value = dt; insertParms[9].Value = dt; insertParms[10].Value = dt; if(OracleHelper.ExecuteNonQuery(transaction, CommandType.Text, insert, insertParms) != 1) return false; else return true;}
在为Membership建立了Provider类后,还需要在配置文件中配置相关的配置节,例如SqlMembershipProvider的配置:

对于OracleMembershipProvider而言,配置大致相似:

有关配置节属性的意义,可以参考MSDN等相关文档。
6.4.3 ASP.NET登录控件
这里所谓的登录控件并不是指一个控件,而是ASP.NET 2.0新提供的一组用于解决用户登录的控件。登录控件与Membership进行集成,快速简便地实现用户登录的处理。ASP.NET登录控件包括Login控件、LoginView控件、LoginStatus控件、LoginName控件、PasswordRescovery控件、CreateUserWizard控件以及ChangePassword控件。PetShop 4.0犹如一本展示登录控件用法的完美教程。我们可以从诸如SignIn、NewUser等页面中,看到ASP.NET登录控件的使用方法。例如在SignIn.aspx中,用到了Login控件。在该控件中,可以包含TextBox、Button等类型的控件,用法如下所示:

又例如NewUser.aspx中对CreateUserWizard控件的使用:

使用了登录控件后,我们毋需编写与用户登录相关的代码,登录控件已经为我们完成了相关的功能,这就大大地简化了这个系统的设计与实现。
6.4.4 Master Page特性
Master Page相当于是整个Web站点的统一模板,建立的Master Page文件扩展名为.master。它可以包含静态文本、html元素和服务器控件。Master Page由特殊的@Master指令识别,如:
<%@ Master Language="C#" CodeFile="MasterPage.master.cs" Inherits="MasterPage" %>
使用Master Page可以为网站建立一个统一的样式,且能够利用它方便地创建一组控件和代码,然后将其应用于一组页。对于那些样式与功能相似的页而言,利用Master Page就可以集中处理为Master Page,一旦进行修改,就可以在一个位置上进行更新。
在PetShop 4.0中,建立了名为MasterPage.master的Master Page,它包含了header、LoginView控件、导航菜单以及用于呈现内容的html元素,如图6-3所示:
图6-3 PetShop 4.0的Master Page
@Master指令的定义如下:
<%@ Master Language="C#" AutoEventWireup="true" CodeFile="MasterPage.master.cs" Inherits="PetShop.Web.MasterPage" %>
Master Page同样利用codebehind技术,以PetShop 4.0的Master Page为例,codebehind的代码放在文件MasterPage.master.cs中:
public partial class MasterPage : System.Web.UI.MasterPage { private const string HEADER_PREFIX = ".NET Pet Shop :: {0}"; protected void Page_PreRender(object sender, EventArgs e) { ltlHeader.Text = Page.Header.Title; Page.Header.Title = string.Format(HEADER_PREFIX, Page.Header.Title); } protected void btnSearch_Click(object sender, EventArgs e) { WebUtility.SearchRedirect(txtSearch.Text); }}
注意Master Page页面不再继承自System.Web.UI.Page,而是继承System.Web.UI.MasterPage类。与Page类继承TemplateControl类不同,它是UserControl类的子类。因此,可以应用在Master Page上的有效指令与UserControl的可用指令相同,例如AutoEventWireup、ClassName、CodeFile、EnableViewState、WarningLevel等。
每一个与Master Page相关的内容页必须在@Page指令的MasterPageFile属性中引用相关的Master Page。例如PetShop 4.0中的CheckOut内容页,其@Page指令的定义如下:
<%@ Page Language="C#" MasterPageFile="~/MasterPage.master" AutoEventWireup="true" CodeFile="CheckOut.aspx.cs" Inherits="PetShop.Web.CheckOut" Title="Check Out" %>
Master Page可以进行嵌套,例如我们建立了父Master Page页面Parent.master,那么在子Master Page中,可以利用master属性指定其父MasterPage:<%@ Master Language="C#" master="Parent.master"%>
而内容页则可以根据情况指向Parent.master或者Child.master页面。
虽然说Master Page大部分情况下是以声明方式创建,但我们也可以建立一个类继承System.Web.UI.MasterPage,从而完成对Master Page的编程式创建。但在采用这种方式的同时,应该同时创建.master文件。此外对Master Page的调用也可以利用编程的方式完成,例如动态地添加Master Page,我们重写内容页的Page_PreInit()方法,如下所示:
void Page_PreInit(Object sender, EventArgs e){ this.MasterPageFile = "~/NewMaster.master";}
之所以重写Page_PreInit()方法,是因为Master Page会在内容页初始化阶段进行合并,也即是说是在PreInit阶段完成Master Page的分配。ASP.NET 2.0引入的新特性,并不仅仅限于上述介绍的内容。例如Theme、Wizard控件等新特性在PetShop 4.0中也得到了大量的应用。虽然ASP.NET 2.0及时地推陈出新,对表示层的设计有所改善,然而作为ASP.NET 2.0的其中一部分,它们仅仅是对现有框架缺失的弥补与改进,属于“锦上添花”的范畴,对于整个表示层设计技术而言,起到的推动作用却非常有限。
直到AJAX(Asynchronous JavaScript and XML)的出现,整个局面才大为改观。虽然AJAX技术带有几分“旧瓶装新酒”的味道,然而它从诞生之初,就具备了王者气象,大有席卷天下之势。各种支持AJAX技术的框架如雨后春笋般纷纷吐出新芽,支撑起百花齐放的繁荣,气势汹汹地营造出唯AJAX独尊的态势。如今,AJAX已经成为了Web应用的主流开发技术,许多业界大鳄都呲牙咧嘴开始了对这一块新领地的抢滩登陆。例如IBM、Oracle、Yahoo等公司都纷纷启动了开源的AJAX项目。微软也不甘落后,及时地推出了ASP.NET AJAX,这是一个基于ASP.NET的AJAX框架,它包括了ASP.NET AJAX服务端组件和ASP.NET AJAX客户端组件,并集成在Visual Studio中,为ASP.NET开发者提供了一个强大的AJAX应用环境。
我现在还无法预知AJAX技术在未来的走向,然而单单从表示层设计的角度而言,AJAX技术亦然带了一场全新的革命。我们或者可以期待未来的PetShop 5.0,可以在表示层设计上带来更多的惊喜。

PetShop之业务逻辑层设计 《解剖PetShop》系列之五

《解剖PetShop》系列之五
五 PetShop之业务逻辑层设计业务逻辑层(Business Logic Layer)无疑是系统架构中体现核心价值的部分。它的关注点主要集中在业务规则的制定、业务流程的实现等与业务需求有关的系统设计,也即是说它是与系统所应对的领域(Domain)逻辑有关,很多时候,我们也将业务逻辑层称为领域层。例如Martin Fowler在《Patterns of Enterprise Application Architecture》一书中,将整个架构分为三个主要的层:表示层、领域层和数据源层。作为领域驱动设计的先驱Eric Evans,对业务逻辑层作了更细致地划分,细分为应用层与领域层,通过分层进一步将领域逻辑与领域逻辑的解决方案分离。
业务逻辑层在体系架构中的位置很关键,它处于数据访问层与表示层中间,起到了数据交换中承上启下的作用。由于层是一种弱耦合结构,层与层之间的依赖是向下的,底层对于上层而言是“无知”的,改变上层的设计对于其调用的底层而言没有任何影响。如果在分层设计时,遵循了面向接口设计的思想,那么这种向下的依赖也应该是一种弱依赖关系。因而在不改变接口定义的前提下,理想的分层式架构,应该是一个支持可抽取、可替换的“抽屉”式架构。正因为如此,业务逻辑层的设计对于一个支持可扩展的架构尤为关键,因为它扮演了两个不同的角色。对于数据访问层而言,它是调用者;对于表示层而言,它却是被调用者。依赖与被依赖的关系都纠结在业务逻辑层上,如何实现依赖关系的解耦,则是除了实现业务逻辑之外留给设计师的任务。
5.1 与领域专家合作设计业务逻辑层最大的障碍不在于技术,而在于对领域业务的分析与理解。很难想象一个不熟悉该领域业务规则和流程的架构设计师能够设计出合乎客户需求的系统架构。几乎可以下定结论的是,业务逻辑层的设计过程必须有领域专家的参与。在我曾经参与开发的项目中,所涉及的领域就涵盖了电力、半导体、汽车等诸多行业,如果缺乏这些领域的专家,软件架构的设计尤其是业务逻辑层的设计就无从谈起。这个结论唯一的例外是,架构设计师同时又是该领域的专家。然而,正所谓“千军易得,一将难求”,我们很难寻觅到这样卓越出众的人才。
领域专家在团队中扮演的角色通常称为Business Consultor(业务咨询师),负责提供与领域业务有关的咨询,与架构师一起参与架构与数据库的设计,撰写需求文档和设计用例(或者用户故事User Story)。如果在测试阶段,还应该包括撰写测试用例。理想的状态是,领域专家应该参与到整个项目的开发过程中,而不仅仅是需求阶段。
领域专家可以是专门聘请的对该领域具有较深造诣的咨询师,也可以是作为需求提供方的客户。在极限编程(Extreme Programming)中,就将客户作为领域专家引入到整个开发团队中。它强调了现场客户原则。现场客户需要参与到计划游戏、开发迭代、编码测试等项目开发的各个阶段。由于领域专家与设计师以及开发人员组成了一个团队,贯穿开发过程的始终,就可以避免需求理解错误的情况出现。即使项目的开发与实际需求不符,也可以在项目早期及时修正,从而避免了项目不必要的延期,加强了对项目过程和成本的控制。正如Steve McConnell在构建活动的前期准备中提及的一个原则:发现错误的时间要尽可能接近引入该错误的时间。需求的缺陷在系统中潜伏的时间越长,代价就越昂贵。如果在项目开发中能够与领域专家充分的合作,就可以最大效果地规避这样一种恶性的链式反应。
传统的软件开发模型同样重视与领域专家的合作,但这种合作主要集中在需求分析阶段。例如瀑布模型,就非常强调早期计划与需求调研。然而这种未雨绸缪的早期计划方式,对架构师与需求调研人员的技能要求非常高,它强调需求文档的精确性,一旦分析出现偏差,或者需求发生变更,当项目开发进入设计阶段后,由于缺乏与领域专家沟通与合作的机制,开发人员估量不到这些错误与误差,因而难以及时作出修正。一旦这些问题像毒瘤一般在系统中蔓延开来,逐渐暴露在开发人员面前时,已经成了一座难以逾越的高山。我们需要消耗更多的人力物力,才能够修正这些错误,从而导致开发成本成数量级的增加,甚至于导致项目延期。当然还有一个好的选择,就是放弃整个项目。这样的例子不胜枚举,事实上,项目开发的“滑铁卢”,究其原因,大部分都是因为业务逻辑分析上出现了问题。
迭代式模型较之瀑布模型有很大地改进,因为它允许变更、优化系统需求,整个迭代过程实际上就是与领域专家的合作过程,通过向客户演示迭代所产生的系统功能,从而及时获取反馈,并逐一解决迭代演示中出现的问题,保证系统向着合乎客户需求的方向演化。因而,迭代式模型往往能够解决早期计划不足的问题,它允许在发现缺陷的时候,在需求变更的时候重新设计、重新编码并重新测试。
无论采用何种开发模型,与领域专家的合作都将成为项目成败与否的关键。这基于一个软件开发的普遍真理,那就是世界上没有不变的需求。一句经典名言是:“没有不变的需求,世上的软件都改动过3次以上,唯一一个只改动过两次的软件的拥有者已经死了,死在去修改需求的路上。”一语道尽了软件开发的残酷与艰辛!
那么应该如何加强与领域专家的合作呢?James Carey和Brent Carlson根据他们在参与的IBM SanFrancisco项目中获得的经验,提出了Innocent Questions模式,其意义即“改进领域专家和技术专家的沟通质量”。在一个项目团队中,如果我们没有一位既能担任首席架构师,同时又是领域专家的人选,那么加强领域专家与技术专家的合作就显得尤为重要了。毕竟,作为一个领域专家而言,可能并不熟悉软件设计方法学,也不具备面向对象开发和架构设计的能力,同样,大部分技术专家很有可能对该项目所涉及的业务领域仅停留在一知半解的地步。如果领域专家与技术专家不能有效沟通,则整个项目的前途就岌岌可危了。
Innocent Questions模式提出的解决方案包括:(1)选用可以与人和谐相处的人员组建开发团队;(2)清楚地定义角色和职权;(3)明确定义需要的交互点;(4)保持团队紧密;(5)雇佣优秀的人。
事实上,这已经从技术的角度上升到对团队的管理层次了。就好比篮球运动一样,即使你的球队集合了五名世界上最顶尖最有天赋的球员,如果各自为战,要想取得比赛的胜利依旧是非常困难的。团队精神与权责分明才是取得胜利的保障,软件开发同样如此。
与领域专家合作的基础是保证开发团队中永远保留至少一名领域专家。他可以是系统的客户,第三方公司的咨询师,最理想是自己公司雇佣的专家。如果项目中缺乏这样的一个人,那么我的建议是去雇佣他,如果你不想看到项目遭遇“西伯利亚寒流”的话。
确定领域专家的角色任务与职责。必须要让团队中的每一个人明确领域专家在整个团队中究竟扮演什么样的角色,他的职责是什么。一个合格的领域专家必须对业务领域有足够深入的理解,他应该是一个能够俯瞰整个系统需求、总揽全局的人物。在项目开发过程中,将由他负责业务规则和流程的制定,负责与客户的沟通,需求的调研与讨论,并于设计师一起参与系统架构的设计。编档是领域专家必须参与的工作,无论是需求文档还是设计文档,以及用例的编写,领域专家或者提出意见,或者作为撰写的作者,至少他也应该是评审委员会的重要成员。
规范业务领域的术语和技术术语。领域专家和技术专家必须在保证不产生二义性的语义环境下进行沟通与交流。如果出现理解上的分歧,我们必须及时解决,通过讨论确立术语标准。很难想象两个语言不通的人能够相互合作愉快,解决的办法是加入一位翻译人员。在领域专家与技术专家之间搭建一座语义上的桥梁,使其能够相互理解、相互认同。还有一个办法是在团队内部开展培训活动。尤其对于开发人员而言,或多或少地了解一些业务领域知识,对于项目的开发有很大的帮助。在我参与过的半导体领域的项目开发,团队就专门邀请了半导体行业的专家就生产过程的业务逻辑进行了全方位的介绍与培训。正所谓“磨刀不误砍柴工”,虽然我们消费了培训的时间,但对于掌握了业务规则与流程的开发人员,却能够提升项目开发进度,总体上节约了开发成本。
加强与客户的沟通。客户同时也可以作为团队的领域专家,极限编程的现场客户原则是最好的示例。但现实并不都如此的完美,在无法要求客户成为开发团队中的固定一员时,聘请或者安排一个专门的领域专家,加强与客户的沟通,就显得尤为重要。项目可以通过领域专家获得客户的及时反馈。而通过领域专家去了解变更了的需求,会在最大程度上减少需求误差的可能。
5.2 业务逻辑层的模式应用Martin Fowler在《企业应用架构模式》一书中对领域层(即业务逻辑层)的架构模式作了整体概括,他将业务逻辑设计分为三种主要的模式:Transaction Script、Domain Model和Table Module。
Transaction Script模式将业务逻辑看作是一个个过程,是比较典型的面向过程开发模式。应用Transaction Script模式可以不需要数据访问层,而是利用SQL语句直接访问数据库。为了有效地管理SQL语句,可以将与数据库访问有关的行为放到一个专门的Gateway类中。应用Transaction Script模式不需要太多面向对象知识,简单直接的特性是该模式全部价值之所在。因而,在许多业务逻辑相对简单的项目中,应用Transaction Script模式较多。
Domain Model模式是典型的面向对象设计思想的体现。它充分考虑了业务逻辑的复杂多变,引入了Strategy模式等设计模式思想,并通过建立领域对象以及抽象接口,实现模式的可扩展性,并利用面向对象思想与身俱来的特性,如继承、封装与多态,用于处理复杂多变的业务逻辑。唯一制约该模式应用的是对象与关系数据库的映射。我们可以引入ORM工具,或者利用Data Mapper模式来完成关系向对象的映射。
与Domain Model模式相似的是Table Module模式,它同样具有面向对象设计的思想,唯一不同的是它获得的对象并非是单纯的领域对象,而是DataSet对象。如果为关系数据表与对象建立一个简单的映射关系,那么Domain Model模式就是为数据表中的每一条记录建立一个领域对象,而Table Module模式则是将整个数据表看作是一个完整的对象。虽然利用DataSet对象会丢失面向对象的基本特性,但它在为表示层提供数据源支持方面却有着得天独厚的优势。尤其是在.Net平台下,ADO.NET与Web控件都为Table Module模式提供了生长的肥沃土壤。
5.3 PetShop的业务逻辑层设计PetShop在业务逻辑层设计中引入了Domain Model模式,这与数据访问层对于数据对象的支持是分不开的。由于PetShop并没有对宠物网上商店的业务逻辑进行深入,也省略了许多复杂细节的商务逻辑,因而在Domain Model模式的应用上并不明显。最典型地应该是对Order领域对象的处理方式,通过引入Strategy模式完成对插入订单行为的封装。关于这一点,我已在第27章有了详尽的描述,这里就不再赘述。
本应是系统架构设计中最核心的业务逻辑层,由于简化了业务流程的缘故,使得PetShop在这一层的设计有些乏善可陈。虽然在业务逻辑层中,针对B2C业务定义了相关的领域对象,但这些领域对象仅仅是完成了对数据访问层中数据对象的简单封装而已,其目的仅在于分离层次,以支持对各种数据库的扩展,同时将SQL语句排除在业务逻辑层外,避免了SQL语句的四处蔓延。
最能体现PetShop业务逻辑的除了对订单的管理之外,还包括购物车(Shopping Cart)与Wish List的管理。在PetShop的BLL模块中,定义了Cart类来负责相关的业务逻辑,定义如下:[Serializable]public class Cart{ private Dictionary cartItems = new Dictionary(); public decimal Total { get { decimal total = 0; foreach (CartItemInfo item in cartItems.Values) total += item.Price * item.Quantity; return total; } } public void SetQuantity(string itemId, int qty) { cartItems[itemId].Quantity = qty; } public int Count { get { return cartItems.Count; } } public void Add(string itemId) { CartItemInfo cartItem; if (!cartItems.TryGetValue(itemId, out cartItem)) { Item item = new Item(); ItemInfo data = item.GetItem(itemId); if (data != null) { CartItemInfo newItem = new CartItemInfo(itemId, data.ProductName, 1, (decimal)data.Price, data.Name, data.CategoryId, data.ProductId); cartItems.Add(itemId, newItem); } } else cartItem.Quantity++; } //其他方法略;}
Cart类通过一个Dictionary对象来负责对购物车内容的存储,同时定义了Add、Remove、Clear等方法,来实现对购物车内容的管理。
在前面我提到PetShop业务逻辑层中的领域对象仅仅是完成对数据对象的简单封装,但这种分离层次的方法在架构设计中依然扮演了举足轻重的作用。以Cart类的Add()方法为例,在方法内部引入了PetShop.BLL.Item领域对象,并调用了Item对象的GetItem()方法。如果没有在业务逻辑层封装Item对象,而是直接调用数据访问层的Item数据对象,为保证层次间的弱依赖关系,就需要调用工厂对象的工厂方法来创建PetShop.IDAL.IItem接口类型对象。一旦数据访问层的Item对象被多次调用,就会造成重复代码,既不离于程序的修改与扩展,也导致程序结构生长为臃肿的态势。
此外,领域对象对数据访问层数据对象的封装,也有利于表示层对业务逻辑层的调用。在三层式架构中,表示层应该是对于数据访问层是“无知”的,这样既减少了层与层间的依赖关系,也能有效避免“循环依赖”的后果。
值得商榷的是Cart类的Total属性。其值的获取是通过遍历购物车集合,然后累加价格与商品数量的乘积。这里显然简化了业务逻辑,而没有充分考虑需求的扩展。事实上,这种获取购物车总价格的算法,在大多数情况下仅仅是其中的一种策略而已,我们还应该考虑折扣的情况。例如,当总价格超过100元时,可以给与顾客一定的折扣,这是与网站的促销计划相关的。除了给与折扣的促销计划外,网站也可以考虑赠送礼品的促销策略,因此我们有必要引入Strategy模式,定义接口IOnSaleStrategy:public interface IOnSaleStrategy{ decimal CalculateTotalPrice(Dictionary cartItems);}
如此一来,我们可以为Cart类定义一个有参数的构造函数:private IOnSaleStrategy m_onSale;public Cart(IOnSaleStrategy onSale){ m_onSale = onSale;}
那么Total属性就可以修改为:public decimal Total{ get {return m_onSale.CalculateTotalPrice(cartItems);}}
如此一来,就可以使得Cart类能够有效地支持网站推出的促销计划,也符合开-闭原则。同样的,这种设计方式也是Domain Model模式的体现。修改后的设计如图5-1所示:
图5-1 引入Strategy模式
作为一个B2C的电子商务架构,它所涉及的业务领域已为大部分设计师与开发人员所熟悉,因而在本例中,与领域专家的合作显得并不那么重要。然而,如果我们要开发一个成功的电子商务网站,与领域专家的合作仍然是必不可少的。以订单的管理而言,如果考虑复杂的商业应用,就需要管理订单的跟踪(Tracking),与网上银行的合作,账户安全性,库存管理,物流管理,以及客户关系管理(CRM)。整个业务过程却涵盖了诸如电子商务、银行、物流、客户关系学等诸多领域,如果没有领域专家的参与,业务逻辑层的设计也许会“败走麦城”。
5.4 与数据访问层的通信业务逻辑层需要与数据访问层通信,利用数据访问层访问数据库,因此业务逻辑层与数据访问层之间就存在依赖关系。在数据访问层引入接口程序集以及数据工厂的设计前提下,能够做到两者间关系为弱依赖。我们从业务逻辑层的引用程序集中可以看到,BLL模块并没有引用SQLServerDAL和OracleDAL程序集。在业务逻辑层中,有关数据访问层中数据对象的调用,均利用多态原理定义了抽象的接口类型对象,然后利用工厂对象的工厂方法创建具体的数据对象。如PetShop.BLL.PetShop领域对象所示:namespace PetShop.BLL{ public class Product { //根据工厂对象创建IProduct接口类型实例; private static readonly IProduct dal = PetShop.DALFactory.DataAccess.CreateProduct(); //调用IProduct对象的接口方法GetProductByCategory(); public IListGetProductsByCategory(string category) { // 如果为空则新建List对象; if(string.IsNullOrEmpty(category)) return new List ();
// 通过数据访问层的数据对象访问数据库; return dal.GetProductsByCategory(category); } //其他方法略; }}
在领域对象Product类中,利用数据访问层的工厂类DALFactory.DataAccess创建PetShop.IDAL.IProduct类型的实例,如此就可以解除对具体程序集SQLServerDAL或OracleDAL的依赖。只要PetShop.IDAL的接口方法不变,即使修改了IDAL接口模块的具体实现,都不会影响业务逻辑层的实现。这种松散的弱耦合关系,才能够最大程度地支持架构的可扩展。
领域对象Product实际上还完成了对数据对象Product的封装,它们暴露在外的接口方法是一致地,正是通过封装,使得表示层可以完全脱离数据库以及数据访问层,表示层的调用者仅需要关注业务逻辑层的实现逻辑,以及领域对象暴露的接口和调用方式。事实上,只要设计合理,规范了各个层次的接口方法,三层式架构的设计完全可以分离开由不同的开发人员同时开发,这就可以有效地利用开发资源,缩短项目开发周期。
5.5 面向接口设计也许是业务逻辑比较简单地缘故,在业务逻辑层的设计中,并没有秉承在数据访问层中面向接口设计的思想。除了完成对插入订单策略的抽象外,整个业务逻辑层仅以BLL模块实现,没有为领域对象定义抽象的接口。因而PetShop的表示层与业务逻辑层就存在强依赖关系,如果业务逻辑层中的需求发生变更,就必然会影响表示层的实现。唯一可堪欣慰的是,由于我们采用分层式架构将用户界面与业务领域逻辑完全分离,一旦用户界面发生更改,例如将B/S架构修改为C/S架构,那么业务逻辑层的实现模块是可以完全重用的。
然而,最理想的方式仍然是面向接口设计。根据第28章对ASP.NET缓存的分析,我们可以将表示层App_Code下的Proxy类与Utility类划分到业务逻辑层中,并修改这些静态类为实例类,并将这些类中与业务领域有关的方法抽象为接口,然后建立如数据访问层一样的抽象工厂。通过“依赖注入”方式,解除与具体领域对象类的依赖,使得表示层仅依赖于业务逻辑层的接口程序集以及工厂模块。
那么,这样的设计是否有“过度设计”的嫌疑呢?我们需要依据业务逻辑的需求情况而定。此外,如果我们需要引入缓存机制,为领域对象创建代理类,那么为领域对象建立接口,就显得尤为必要。我们可以建立一个专门的接口模块IBLL,用以定义领域对象的接口。以Product领域对象为例,我们可以建立IProduct接口:public interface IProduct{ IList GetProductByCategory(string category); IList GetProductByCategory(string[] keywords); ProductInfo GetProduct(string productId);}
在BLL模块中可以引入对IBLL程序集的依赖,则领域对象Product的定义如下:public class Product:IProduct{ public IList GetProductByCategory(string category) { //实现略; } public IList GetProductByCategory(string[] keywords) { //实现略; } public ProductInfo GetProduct(string productId) { //实现略; }}
然后我们可以为代理对象建立专门的程序集BLLProxy,它不仅引入对IBLL程序集的依赖,同时还将依赖于BLL程序集。此时代理对象ProductDataProxy的定义如下:using PetShop.IBLL;using PetShop.BLL;namespace PetShop.BLLProxy{ public class ProductDataProxy:IProduct { public IList GetProductByCategory(string category) { Product product = new Product(); //其他实现略; } public IList GetProductByCategory(string[] keywords) { //实现略; } public ProductInfo GetProduct(string productId) { //实现略; } }}
如此的设计正是典型的Proxy模式,其类结构如图5-2所示:
图5-2 Proxy模式
参照数据访问层的设计方法,我们可以为领域对象及代理对象建立抽象工厂,并在web.config中配置相关的配置节,然后利用反射技术创建具体的对象实例。如此一来,表示层就可以仅仅依赖PetShop.IBLL程序集以及工厂模块,如此就可以解除表示层与具体领域对象之间的依赖关系。表示层与修改后的业务逻辑层的关系如图5-3所示:
图5-3 修改后的业务逻辑层与表示层的关系
图5-4则是PetShop 4.0原有设计的层次关系图:
图5-4 PetShop 4.0中表示层与业务逻辑层的关系
通过比较图5-3与图5-4,虽然后者不管是模块的个数,还是模块之间的关系,都相对更加简单,然而Web Component组件与业务逻辑层之间却是强耦合的,这样的设计不利于应对业务扩展与需求变更。通过引入接口模块IBLL与工厂模块BLLFactory,解除了与具体模块BLL的依赖关系。这种设计对于业务逻辑相对比较复杂的系统而言,更符合面向对象的设计思想,有利于我们建立可抽取、可替换的“抽屉”式三层架构。