现在的位置: 首页 > 综合 > 正文

[转]解剖PetShop

2013年12月07日 ⁄ 综合 ⁄ 共 20981字 ⁄ 字号 评论关闭
《解剖PetShop》系列之一 系统架构设计
 
前言:PetShop是一个范例,微软用它来展示.Net企业系统开发的能力。业界有许多.Net与J2EE之争,许多数据是从微软的 PetShop和Sun的PetStore而来。这种争论不可避免带有浓厚的商业色彩,对于我们开发人员而言,没有必要过多关注。然而PetShop随着 版本的不断更新,至现在基于.Net 2.0的PetShop4.0为止,整个设计逐渐变得成熟而优雅,却又很多可以借鉴之处。PetShop是一个小型的项目,系统架构与代码都比较简单,却 也凸现了许多颇有价值的设计与开发理念。本系列试图对PetShop作一个全方位的解剖,依据的代码是PetShop4.0,可以从链接http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnbda/html/bdasamppet4.asp中获得。
一、PetShop的系统架构设计
在软件体系架构设计中,分层式结构是最常见,也是最重要的一种结构。微软推荐的分层式结构一般分为三层,从下至上分别为:数据访问层、业务逻辑层(又或成为领域层)、表示层,如图所示:


图一:三层的分层式结构

数据访问层:有时候也称为是持久层,其功能主要是负责数据库的访问。简单的说法就是实现对数据表的Select,Insert,Update, Delete的操作。如果要加入ORM的元素,那么就会包括对象和数据表之间的mapping,以及对象实体的持久化。在PetShop的数据访问层中, 并没有使用ORM,从而导致了代码量的增加,可以看作是整个设计实现中的一大败笔。
业务逻辑层:是整个系统的核心,它与这个系统的业务(领域)有关。以PetShop为例,业务逻辑层的相关设计,均和网上宠物店特有的逻辑相关,例如查询宠物,下订单,添加宠物到购物车等等。如果涉及到数据库的访问,则调用数据访问层。
表示层:是系统的UI部分,负责使用者与整个系统的交互。在这一层中,理想的状态是不应包括系统的业务逻辑。表示层中的逻辑代码,仅与界面元素有关。在PetShop中,是利用ASP.Net来设计的,因此包含了许多Web控件和相关逻辑。
分层式结构究竟其优势何在?Martin Fowler在《Patterns of Enterprise Application Architecture》一书中给出了答案:
1、开发人员可以只关注整个结构中的其中某一层;
2、可以很容易的用新的实现来替换原有层次的实现;
3、可以降低层与层之间的依赖;
4、有利于标准化;
5、利于各层逻辑的复用。
概括来说,分层式设计可以达至如下目的:分散关注、松散耦合、逻辑复用、标准定义。
一个好的分层式结构,可以使得开发人员的分工更加明确。一旦定义好各层次之间的接口,负责不同逻辑设计的开发人员就可以分散关注,齐头并进。例如 UI人员只需考虑用户界面的体验与操作,领域的设计人员可以仅关注业务逻辑的设计,而数据库设计人员也不必为繁琐的用户交互而头疼了。每个开发人员的任务 得到了确认,开发进度就可以迅速的提高。
松散耦合的好处是显而易见的。如果一个系统没有分层,那么各自的逻辑都紧紧纠缠在一起,彼此间相互依赖,谁都是不可替换的。一旦发生改变,则牵一发 而动全身,对项目的影响极为严重。降低层与层间的依赖性,既可以良好地保证未来的可扩展,在复用性上也是优势明显。每个功能模块一旦定义好统一的接口,就 可以被各个模块所调用,而不用为相同的功能进行重复地开发。
进行好的分层式结构设计,标准也是必不可少的。只有在一定程度的标准化基础上,这个系统才是可扩展的,可替换的。而层与层之间的通信也必然保证了接口的标准化。
“金无足赤,人无完人”,分层式结构也不可避免具有一些缺陷:
1、降低了系统的性能。这是不言而喻的。如果不采用分层式结构,很多业务可以直接造访数据库,以此获取相应的数据,如今却必须通过中间层来完成。
2、有时会导致级联的修改。这种修改尤其体现在自上而下的方向。如果在表示层中需要增加一个功能,为保证其设计符合分层式结构,可能需要在相应的业务逻辑层和数据访问层中都增加相应的代码。
前面提到,PetShop的表示层是用ASP.Net设计的,也就是说,它应是一个BS系统。在.Net中,标准的BS分层式结构如下图所示:


图二:.Net中标准的BS分层式结构

随着PetShop版本的更新,其分层式结构也在不断的完善,例如PetShop2.0,就没有采用标准的三层式结构,如图三:


图三:PetShop 2.0的体系架构

从图中我们可以看到,并没有明显的数据访问层设计。这样的设计虽然提高了数据访问的性能,但也同时导致了业务逻辑层与数据访问的职责混乱。一旦要求 支持的数据库发生变化,或者需要修改数据访问的逻辑,由于没有清晰的分层,会导致项目作大的修改。而随着硬件系统性能的提高,以及充分利用缓存、异步处理 等机制,分层式结构所带来的性能影响几乎可以忽略不计。
PetShop3.0纠正了此前层次不明的问题,将数据访问逻辑作为单独的一层独立出来:


图四:PetShop 3.0的体系架构

PetShop4.0基本上延续了3.0的结构,但在性能上作了一定的改进,引入了缓存和异步处理机制,同时又充分利用了ASP.Net 2.0的新功能MemberShip,因此PetShop4.0的系统架构图如下所示:


图五:PetShop 4.0的体系架构

比较3.0和4.0的系统架构图,其核心的内容并没有发生变化。在数据访问层(DAL)中,仍然采用DAL Interface抽象出数据访问逻辑,并以DAL Factory作为数据访问层对象的工厂模块。对于DAL Interface而言,分别有支持MS-SQL的SQL Server DAL和支持Oracle的Oracle DAL具体实现。而Model模块则包含了数据实体对象。其详细的模块结构图如下所示:


图六:数据访问层的模块结构图

可以看到,在数据访问层中,完全采用了“面向接口编程”思想。抽象出来的IDAL模块,脱离了与具体数据库的依赖,从而使得整个数据访问层利于数据 库迁移。DALFactory模块专门管理DAL对象的创建,便于业务逻辑层访问。SQLServerDAL和OracleDAL模块均实现IDAL模块 的接口,其中包含的逻辑就是对数据库的Select,Insert,Update和Delete操作。因为数据库类型的不同,对数据库的操作也有所不同, 代码也会因此有所区别。
此外,抽象出来的IDAL模块,除了解除了向下的依赖之外,对于其上的业务逻辑层,同样仅存在弱依赖关系,如下图所示:


图七:业务逻辑层的模块结构图

图七中BLL是业务逻辑层的核心模块,它包含了整个系统的核心业务。在业务逻辑层中,不能直接访问数据库,而必须通过数据访问层。注意图中对数据访 问业务的调用,是通过接口模块IDAL来完成的。既然与具体的数据访问逻辑无关,则层与层之间的关系就是松散耦合的。如果此时需要修改数据访问层的具体实 现,只要不涉及到IDAL的接口定义,那么业务逻辑层就不会受到任何影响。毕竟,具体实现的SQLServerDAL和OracalDAL根本就与业务逻 辑层没有半点关系。
因为在PetShop 4.0中引入了异步处理机制。插入订单的策略可以分为同步和异步,两者的插入策略明显不同,但对于调用者而言,插入订单的接口是完全一样的,所以 PetShop 4.0中设计了IBLLStrategy模块。虽然在IBLLStrategy模块中,仅仅是简单的IOrderStategy,但同时也给出了一个范例 和信息,那就是在业务逻辑的处理中,如果存在业务操作的多样化,或者是今后可能的变化,均应利用抽象的原理。或者使用接口,或者使用抽象类,从而脱离对具 体业务的依赖。不过在PetShop中,由于业务逻辑相对简单,这种思想体现得不够明显。也正因为此,PetShop将核心的业务逻辑都放到了一个模块 BLL中,并没有将具体的实现和抽象严格的按照模块分开。所以表示层和业务逻辑层之间的调用关系,其耦合度相对较高:


图八:表示层的模块结构图

在图五中,各个层次中还引入了辅助的模块,如数据访问层的Messaging模块,是为异步插入订单的功能提供,采用了MSMQ (Microsoft Messaging Queue)技术。而表示层的CacheDependency则提供缓存功能。这些特殊的模块,我会在此后的文章中详细介绍。

 

解剖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; }
}


[/size]
Page类实现了ProcessRequest()方法,通过它可以设置Page对象的Request和Response属性,从而完成对用户请求 /相应的控制。然后Page类通过从Control类继承来的Load事件,将View与Model建立关联,如Products.aspx.cs所示:

事件机制恰好是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()方法,如下所示:

注:由于文章太长,请接着看下面回复---

admin888 最后编辑于 2007-08-22 10:59:24

#1  

 
只看楼主 2007-08-18 21:34
回复:《解剖PetShop》系列之六 PetShop之表示层设计
 
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 = "<p><br>Can not process the order. Your cart is empty.</p><p class=SignUpLabel><a class=linkNewUser href=Default.aspx>Continue shopping</a></p>";
            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。
由于文章过长,请继续看接下面回复---

admin888 最后编辑于 2007-08-22 10:57:03

#2  

 
只看楼主 2007-08-18 21:34
回复:《解剖PetShop》系列之六 PetShop之表示层设计
 
由于这两个控件都改变了其父类控件的呈现方式,故而,我们可以通过重写父类的Render虚方法,完成控件的自定义。例如CustomGrid控件:

public class CustomGrid : Repeater…
//Static constants
    protected const string HTML1 = "<table cellpadding=0
cellspacing=0><tr><td colspan=2>";
    protected const string HTML2 = "</td></tr><tr><td class=paging align=left>";
    protected const string HTML3 = "</td><td align=right class=paging>";
    protected const string HTML4 = "</td></tr></table>";
    private static readonly Regex RX = new Regex(@"^&page=/d+",
RegexOptions.Compiled);
    private const string LINK_PREV = "<a href=?page={0}>&#060; Previous</a>";
    private const string LINK_MORE = "<a href=?page={0}>More &#062;</a>";
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<CartItemInfo> 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页面下,我们可以加入该用户控件,如下所示:

<PetShopControl:shoppingcartcontrol id="ShoppingCartControl1" runat="server"></PetShopControl:shoppingcartcontrol>

由于ShoppingCartControl用户控件已经实现了用于呈现购物车数据的逻辑,那么在ShoppingCart.aspx.cs中,就 可以不用负责这些逻辑,在充分完成对象重用的过程中,同时又达到了职责分离的目的。用户控件的设计者与页面设计者可以互不干扰,分头完成自己的设计。特别 是对于页面设计者而言,他可以是单一的UI设计人员角色,仅需要关注用户界面是否美观与友好,对于表示层中对领域对象的调用与操作就可以不必理会,整个页 面的代码也显得结构清晰、逻辑清楚,无疑也“干净”了不少。

admin888 最后编辑于 2007-08-22 11:02:16

#3  

 
只看楼主 2007-08-18 21:34
回复:《解剖PetShop》系列之六 PetShop之表示层设计
 
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<CartItemInfo> GetCartItems(string userName, string appName,
bool isShoppingCart);
void SetCartItems(int uniqueID, ICollection<CartItemInfo> 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<string> GetInactiveProfiles(int authenticationOption,
DateTime userInactiveSinceDate, string appName);
bool DeleteProfile(string userName, string appName); 
IList<CustomProfileInfo> 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配置文件中配置如下的配置节:

<profile automaticSaveEnabled="false" defaultProvider="ShoppingCartProvider">
<providers>
  <add name="ShoppingCartProvider" connectionStringName="SQLProfileConnString" type="PetShop.Profile.PetShopProfileProvider" applicationName=".NET Pet Shop 4.0"/>
  <add name="WishListProvider" connectionStringName="SQLProfileConnString" type="PetShop.Profile.PetShopProfileProvider" applicationName=".NET Pet Shop 4.0"/>
  <add name="AccountInfoProvider" connectionStringName="SQLProfileConnString" type="PetShop.Profile.PetShopProfileProvider" applicationName=".NET Pet Shop 4.0"/>
</providers>
<properties>
  <add name="ShoppingCart" type="PetShop.BLL.Cart" allowAnonymous="true" provider="ShoppingCartProvider"/>
  <add name="WishList" type="PetShop.BLL.Cart" allowAnonymous="true" provider="WishListProvider"/>
  <add name="AccountInfo" type="PetShop.Model.AddressInfo" allowAnonymous="false" provider="AccountInfoProvider"/>
</properties>
</profile>

在配置文件中,针对ShoppingCart、WishList和AccountInfo(它们的类型分别为PetShop.BLL.Cart、 PetShop.BLL.Cart、PetShop.Model.AddressInfo)属性分别定义了ShoppingCartProvider、 WishListProvider、AccountInfoProvider,它们的类型均为 PetShop.Profile.PetShopProfileProvider类型。至于Profile的信息究竟是存储在何种类型的数据库中,则由以 下的配置节决定:

<add key="ProfileDAL" value="PetShop.SQLProfileDAL"/>

而键值为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));
 

抱歉!评论已关闭.