petshop经典学习笔记

---- msdn上关于PetShop3.0的介绍----
---- 网上高手的PetShop3.0数据层设计分析报告----
 
                             1.用户注册

关于该系统的大致介绍可以从上面的连接获得,都是中文的。
下面来分析一下PetShop3.0的用户注册部分(我今早上刚研究的,哈,趁热端出来)
PetShop3.0是业务实体和业务逻辑分开的,并且在表示层上也有逻辑处理。业务实体部分从前到后都有用到。实际上,在传递数据的时候就是传递的一个实体,而不是像我们一般用的一个变量一个变量的传,在用户注册中也是这样。
注册页面是CreateAccount.aspx,这里有一个usercontrol:AddressUI,用来收集用户的一般信息,其他的个人网站设定和用户名密码什么的都是分开来取的,通过提取AddressUI.Address来获得一个AddressInfo对象,然后用这些信息创建一个AccountInfo对象,最后调用ProcessFlow.AccountController的CreateAccount方法来完成注册。CreateAccount接收的参数自然是一个AddressInfo类型的对象,返回类型为bool。根据返回值来判断注册是否成功。实际上,它这里假定如果不成功,那就只有一种情况,就是用户名已经被注册了。
接下来的事情就是一层套一层的引用了。把业务实体AccountInfo一层的往下传,最后到达 SQLServerDAL层,这里的Insert方法执行最后的操作。
PetSop.Web.ProcessFlow.AccountController :
public bool CreateAccount(AccountInfo newAccountInfo){
try {
// Creata a new business logic tier
Account account = new Account();
// Call the insert method
account.Insert(newAccountInfo);
// Store the data in session state and store the authenticated cookie
HttpContext.Current.Session[ACCOUNT_KEY] = newAccountInfo;
FormsAuthentication.SetAuthCookie(newAccountInfo.UserId, false);

//Finally forward to the welcome page
HttpContext.Current.Response.Redirect(URL_ACCOUNTCREATE, true);

}
//注意在这里捕获异常,说明用户名已存在。详细描述见下面
catch {
return false;
}
return true;
}


PetShop.BLL.Account :
public void Insert(AccountInfo account) {
// Validate input
if (account.UserId.Trim() == string.Empty)
return;
// Get an instance of the account DAL using the DALFactory
IAccount dal = PetShop.DALFactory.Account.Create();
// Call the DAL to insert the account
dal.Insert(account);
}


最后进入实际的数据操作层

PetShop.SQLServerDAL.Account :
public void Insert(AccountInfo acc) {
SqlParameter[] signOnParms = GetSignOnParameters();
SqlParameter[] accountParms = GetAccountParameters();
SqlParameter[] profileParms = GetProfileParameters();
signOnParms[0].Value = acc.UserId;
signOnParms[1].Value = acc.Password;
SetAccountParameters(accountParms, acc);
SetProfileParameters(profileParms, acc);

using (SqlConnection conn = new SqlConnection(SQLHelper.CONN_STRING_NON_DTC)) {
conn.Open();
using (SqlTransaction trans = conn.BeginTransaction()) {
try {
SQLHelper.ExecuteNonQuery(trans, CommandType.Text, SQL_INSERT_SIGNON, signOnParms);
SQLHelper.ExecuteNonQuery(trans, CommandType.Text, SQL_INSERT_ACCOUNT, accountParms);
SQLHelper.ExecuteNonQuery(trans, CommandType.Text, SQL_INSERT_PROFILE, profileParms);
trans.Commit();

}
//违反约束,抛出异常
catch {
trans.Rollback();
throw;
}
}
}
}


那么它是怎么判断用户名是否已经被注册了呢?原来,在保存用户名和密码的表里有一个主键约束,这样自然就插不进重复的用户名。一旦有相同的用户名进入,就会违反约束,抛出异常,当然在之前还要回滚事务,抛出的异常在表示层CreateAccount方法中被捕获,方法返回false,最后反映到页面上。
我在这里就有一个疑问,这样做不是把异常作为一种控制流程的手段了吗?
《effective java》第39条“只针对不正常的条件才使用异常”。根据这一条,万一不能注册是因为其他不可预料的原因而发生的呢?这也会返回给要注册的用户“Duplicate user ID! Please try again.”信息。而且我以前看过一篇文章,说.net的异常抛出会消耗大量的资源,建议不要把异常做为一种实现的方法。其实这里完全可以用T-SQL编程的手段来预先判断用户名是否存在,然后再采取下一步措施。
 
 
                      2.宠物展示
涉及到这个主题的页有Category.aspx / Items.aspx / ItemDetails.aspx,分别是大类/小类/详细信息这三个。下面来一个一个的分析
要注意的是,像第一篇说的,数据的传递都是采用直接传递业务实体的方法来完成。这样是不是有很强的面向对象的味道?
Category.aspx
该页面的主体是一个用户自定义控件:SimplePager,该空间继承自Repeater,作用当然是存放宠物信息,他的数据源的获得方式使用了.net的缓存api。具体实现代码为:
if(Cache[categoryKey] != null){
// If the data is already cached, then used the cached copy
products.DataSource = (IList)Cache[categoryKey];
}else{
// If the data is not cached, then create a new products object and request the data
Product product = new Product();
IList productsByCategory = product.GetProductsByCategory(categoryKey);
// Store the results of the call in the Cache and set the time out to 12 hours
Cache.Add(categoryKey, productsByCategory, null, DateTime.Now.AddHours(12), Cache.NoSlidingExpiration , CacheItemPriority.High, null);
products.DataSource = productsByCategory;
}
// Bind the data to the control
products.DataBind();
是使用了很普遍的做法来完成的,新数据的通过PetShop.BLL.Product.GetProductsByCategory方法获得,梢后分析这个方法。SimplePager获得数据后,就像普通的Repeater一样根据模板的设定来显示数据。对SimplePager的分析同样放在后面。
用户点击具体的小类后,就进入小类的页面,通过url传递小类的编号。
Items.aspx
和上面的页面基本一样,因为都是对类别进行的操作嘛。
选择具体的宠物后,就进入详细资料展示页面了。
ItemDetails.aspx
这个页面没有使用缓存,而是直接通过业务逻辑层到数据库去取记录,然后返回到表示层。很简单,一看就懂的那种。
下面来看一下SimplePager控件。
控件的定义中重写了很多Repeater的方法,其用意主要是为了是其具有分页的功能,看一下其中的Render方法。
PetShop.Web.Controls.SimplePager : Repeater
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));
//other .....

}
另外该控件还有自定义的事件PageIndexChanged,用来对页面切换进行响应。DataSource接受实现IList接口的数据,而我们看上面大类页面在显示的时候使用的语句
products.DataSource = productsByCategory;
productsByCategory作为业务逻辑组件一个方法的返回值,正是实现了IList接口的数据。
接下来看该数据是怎么得到的。
基本的过程都是从最后的数据操作组件开始层层的传递过来,因此我们直接看最后的数据操作部分。
PetShop.SQLServerDAL.Product : IProduct
public IList GetProductsByCategory(string category) {
IList productsByCategory = new ArrayList();
SqlParameter parm = new SqlParameter(PARM_CATEGORY, SqlDbType.Char, 10);
parm.Value = category;


//Execute a query to read the products
using (SqlDataReader rdr = SQLHelper.ExecuteReader(SQLHelper.CONN_STRING_NON_DTC, CommandType.Text, SQL_SELECT_PRODUCTS_BY_CATEGORY, parm)) {
while (rdr.Read()){

//向ArrayList里添加一整个的ProductInfo实体
ProductInfo product = new ProductInfo(rdr.GetString(0), rdr.GetString(1), null);
productsByCategory.Add(product);
}
}
return productsByCategory;
}
很明显,返回的是一个ArrayList。这个里面放着该大类的所有小类的资料。一个小类就是一个ProductInfo,而ProductInfo作为一个瘦数据类存放小类的一些基本资料。
现在我们在回头看一下在Category.aspx中SimplePager的部分标记代码
<%# DataBinder.Eval(Container.DataItem, "Id") %>
从数据绑定的知识可以得到,这个id其实是ProductInfo的一个属性。
if(itemsByProduct.Count > 0)
在Items.aspx中进行的数据操作和Category.aspx的基本一样。
Items.aspx.cs里最后有这么一句代码:
productName.Text = ((ItemInfo)itemsByProduct[0]).ProductName;
因为itemsByProduct是一个ArrayList,所以itemsByProduct[0]实际上返回的就是一个ItemInfo。

从上面的分析可以看出,业务逻辑和业务实体分开来的好处。
 
 
         3.模仿购物车的简单可变类
今天晚上看了近两个小时的购物车,基本把原理弄明白了,先写一个类似结构的类来简单的演示一下
Store类模仿购物车内的物品
public class Store
{
private string name;
private int id;
private DateTime time;
public Store(string name,int id,DateTime time)
{
this.name=name;
this.id=id;
this.time=time;
}
//属性
public string Name
{
get{return this.name;}
}
public int Id
{
get{return this.id;}
}
public DateTime Time
{
get{return this.time;}
}
}
StoreList类模仿购物车
public class StoreList : IEnumerable
{
ArrayList al=new ArrayList();
public StoreList()
{}
//向车内添加物品
public void Add(Store st)
{
this.al.Add(st);
}
//返回全部物品
public ArrayList List
{
get{return this.al;}
}

//实现IEnumerable接口
#region IEnumerable 成员
public IEnumerator GetEnumerator()
{
return this.al.GetEnumerator();
}
#endregion
//添加一个索引器,注意没有判断索引数的合法性
public Store this[int index]
{
get{return (Store)al[index];}
}
//物品的数量
public int Count
{
get{return al.Count;}
}
}
最后的演示页面
public class TestStore : System.Web.UI.Page
{
protected System.Web.UI.WebControls.Button addStore;
protected System.Web.UI.WebControls.Label showMsg;

private void Page_Load(object sender, System.EventArgs e)
{
show();
}
#region Web 窗体设计器生成的代码
override protected void OnInit(EventArgs e)
{
//
// CODEGEN: 该调用是 ASP.NET Web 窗体设计器所必需的。
//
InitializeComponent();
base.OnInit(e);
}

///
/// 设计器支持所需的方法 - 不要使用代码编辑器修改
/// 此方法的内容。
///

private void InitializeComponent()
{
this.addStore.Click += new System.EventHandler(this.addStore_Click);
this.Load += new System.EventHandler(this.Page_Load);
}
#endregion
//点击添加按钮后的处理事件,向保存在Session中的购物车添加一个商品
private void addStore_Click(object sender, System.EventArgs e)
{
Store st=new Store("alex",0,DateTime.Now);

//检查Session内是否存有购物车,如没有,则添加一个
if(Session["stxx"]==null)
{
StoreList sl=new StoreList();
Session["stxx"]=sl;
}

//从Session中得到购物车,然后向里面添加一个商品
StoreList sls=(StoreList)Session["stxx"];
sls.Add(st);

//注意这里,最后分析这个
//Session["stxx"]=sls;

}
//展示购物车内的商品
private void show()
{
StringBuilder sb=new StringBuilder();
if(Session["stxx"]!=null)
{
StoreList sls=(StoreList)Session["stxx"];

//利用索引循环取出商品
for(int i=0;isb.Append(sls[i].Time.ToString()+"
");
showMsg.Text=sb.ToString();
}
}
}
Store是一个瘦实体类,而StoreList是一个可变类。StoreList类通过里面的ArrayList保存Store类,并提供了相应的方法来对Store进行操作。
来看这个:
//从Session中得到购物车,然后向里面添加一个商品
StoreList sls=(StoreList)Session["stxx"];
sls.Add(st);

//注意这里,最后分析这个
//Session["stxx"]=sls;

这里涉及到一个关于Session的问题,由于我们的StoreList保存在了Session中,所以每次操作都要先从Session里把StoreList取出来,但是在操作完后,并没有再把StoreList保存回Session,这主要是因为我们提取出来的并不是Session里保存的值,而只是得到了对Session里保存的值的引用,所以之后的操作其实都是在对Session里保存的值进行,就没有必要最后再保存了。
 
             4.购物车
 
终于到购物车了,在看这个之前应该已经明白了第三篇的那个模型,这样购物车基本也就明白了。
来看一下ShoppingCart.aspx这个页。
当你看好了一个宠物,比如可爱的Golden Retriever,嘿嘿,那就点add to cart按钮,这时就会跳到ShoppingCart.aspx,url里带了这个宠物的id号,根据该id号程序将该宠物放到cart里面。然后你可以再去挑别的宠物,比如一只猫(……),虽然这不是什么好主意。然后该宠物的id号又会被传到ShoppingCart.aspx,并添加到cart里面。在ShoppingCart.aspx里,你可以更改想要领养的宠物的数量,然后程序会根据你要求的数量来计算所需的钱以及该宠物是否还有剩余。在你做出决定后可以点proceed to checkout进入定单生成的环节。
上面是大体的流程。下面来看.net petshop是怎么实现这个cart的
基本的实现主要是BLL里的Cart和Model里的CartItemInfo,而Web.ProcessFlow的CartControler则负责具体的实现。想一想第三篇里的那个模型,具体到这里,每挑选一个宠物,就有一个CartItemInfo通过CartControler添加到了保存在Session里的Cart里面,最后生成定单的时候就从Session里把Cart的值取出来(CartControler有生成定单的方法,下一篇再说)。
来看一下ShoppingCart.aspx.cs里向Cart添加CartItemInfo的代码
// Create an instance of the cart controller
ProcessFlow.CartController cartController = new ProcessFlow.CartController();
myCart = cartController.GetCart(true);
if (!Page.IsPostBack){
// Get the itemdId from the query string
string itemId = Request["itemId"];
if (itemId != null){
// Clean the input string
itemId = WebComponents.CleanString.InputText(itemId, 50);
myCart.Add(itemId);
cartController.StoreCart(myCart);

}
}
先看这一句 myCart = cartController.GetCart(true);
GetCart方法用来生成一个Cart,它是先在Session里检查,如Session里没有保存Cart,就生成一个新的,否则就把保存在Session里的Cart取出来,然后使用Add方法把新的宠物加到Cart里。
public Cart GetCart(bool create){

// Fetch the cart object from session state
Cart myCart = (Cart)HttpContext.Current.Session[CART_KEY];
if ( myCart == null ){
if (create){
myCart = new Cart();
}else{
HttpContext.Current.Server.Transfer(URL_NOCART);
return null;
}
}

return myCart;
}
下面是Add方法的实现
public void Add(string ItemId) {
// _items是在Cart里保存的宠物集合,通过遍历来判断这是否是一个新类别
foreach (CartItemInfo cartItem in _items) {
if (ItemId == cartItem.ItemId) {
cartItem.Quantity++;
cartItem.InStock = (GetInStock(ItemId) - cartItem.Quantity) >= 0 ? true : false;
_total = _total+(cartItem.Price*cartItem.Quantity);
return;
}
}
//是一个新类别,则把一个CartItemInfo加入Cart,可以在这里看到CartItemInfo的组成
Item item = new Item();
ItemInfo data = item.GetItem(ItemId);
CartItemInfo newItem = new CartItemInfo(ItemId,data.Name, (data.Quantity >= 1), 1, (decimal)data.Price);
_items.Add(newItem);
_total = _total+(data.Price);
}
这就完成了添加。然后是数量的更改。
// Check for update button
if (e.CommandName == CMD_UPDATE){
TextBox txt;
int qty;
int index;
// Go through each item on the page
for (int i = 0, j = cart.Items.Count; i < j; i++){
// lookup the control
txt = (TextBox)cart.Items[i].FindControl(ID_TXT);
try{
qty = int.Parse(txt.Text);
index = cart.CurrentPageIndex * cart.PageSize + i;


// If the new qty is zero, remove the item from the cart
if (qty <= 0)
myCart.RemoveAt(index);

// Update the item with the new quantity
else
myCart[index].Quantity = qty;
}
catch {}
}

}else

// otherwise the command is to remove the an item
myCart.Remove((string)e.CommandArgument);
因为宠物的添加是先于数量的更改进行的,所以到了这里Session里肯定有保存Cart。数量的更改直接同过索引器来完成,更改后直接就会保存。Remove和RemoveAt都实现了从Cart删除指定的CartItemInfo。

ShoppingCart.aspx页还有其他的东西,比如一个继承自SimplePager的自定义控件,,还有判断是否显示用户喜好的宠物列表的判断。
Refresh方法用来重新计算total的值,哈,不过我不清楚微软究竟想拿这个值显示什么?在我下的这个版本里,根本就不是subtotla的总和,而是price的总和,但问题在于,当你把一种宠物从Cart里移除的时候,它竟然会total=total-subtotal,因此常常会出现负数……

购物车从开始到最后销毁,都是在和Session打交道,没有任何与数据库的交互。我不是很了解面向对象技术,但我觉得oo在这里得到了很好的体现。
 
 
           5.生成定单
点proceed to checkout后,就进入Checkout.aspx,确认后进入OrderBilling.aspx,在这里可以修改你的信息,完成后点continue,会出现个人信息的只读页面,最终确认后就进入OrderProcess.aspx,在这里是定单的详细情况,并且是只读的,到这里,定单被添加到数据库,购物完成。
Checkout.aspx把数据从Session中取出来,然后显示到页面,没什么好说的。
OrderBilling.aspx,这个页面一开始显示的信息可写,我们看OnLoad事件中,是用ProcessFlow.AccountController.GetAccountInfo获得用户的信息CreditCardInfo,然后显示在一些可写的控件中,比如用户控件:StaticAddress。当点击确认后,使用StoreCreditCard把刚才获得的用户信息保存到Sessin,准备呆会用。
OrderProcess.aspx是最终的定单生成页面。主要就是一个方法:
ProcessFlow.CartController.PurchaseCart 来看它的实现
public OrderInfo PurchaseCart(){
// Fetch the cart from session
Cart myCart = (Cart)HttpContext.Current.Session[CART_KEY];
// Make some checks on the cart
if ( ( myCart == null ) || ( myCart.Count==0 ) ) {
HttpContext.Current.Server.Transfer(URL_NOCART);
//HttpContext.Current.Response.Redirect(URL_NOCART, false);
return null;
}else{

// Build up the order
OrderInfo newOrder = new OrderInfo();
newOrder.UserId = ((AccountInfo)HttpContext.Current.Session[ACCOUNT_KEY]).UserId;
newOrder.CreditCard = (CreditCardInfo)HttpContext.Current.Session[CREDITCARD_KEY];
newOrder.BillingAddress = (AddressInfo)HttpContext.Current.Session[BILLING_KEY];
newOrder.ShippingAddress = (AddressInfo)HttpContext.Current.Session[SHIPPING_KEY];

newOrder.LineItems = (LineItemInfo[])myCart.GetOrderLineItems().ToArray(typeof(LineItemInfo));

newOrder.OrderTotal = myCart.Total;
newOrder.Date = DateTime.Now;

// Send the order to the middle tier
OrderInsert order = new OrderInsert();
//向数据库插入数据
newOrder.OrderId = order.Insert(newOrder);

// clear the session objects used
HttpContext.Current.Session[CART_KEY] = null;
HttpContext.Current.Session[CREDITCARD_KEY] = null;
HttpContext.Current.Session[BILLING_KEY] = null;
HttpContext.Current.Session[SHIPPING_KEY] = null;
return newOrder;
}
}
Order主要是由保存在Session里的值形成,其中包括购物车。
在Order有一个LineItemInfo,它是由Cart.GetOrderLineItems方法依据cart里CartItemInfo的值返回的,是定单的物品部分,和CartItemInfo相比,主要是多了一个Line属性,这个Line是用来表示物品在定单内的序号。
下面是Insert的实现。
public int Insert(OrderInfo order) {
// Get an instance of the Order DAL using the DALFactory
IOrder dal = PetShop.DALFactory.Order.Create();
// Call the insert method in the DAL to insert the header
int orderId = dal.Insert(order);
// Get an instance of the Inventory business component
Inventory inventory = new Inventory();
//向数据库插入库存信息
inventory.TakeStock( order.LineItems);

// As part of the sample application we have created a user
// you can tested distributed transactions with
// If the order has been created with the user 'Acid',
// then throw an exception which will rollback the entire transaction

if (order.UserId == ACID_USER_ID)
throw new ApplicationException(ACID_ERROR_MSG);
/ / Set the orderId so that it can be returned to the caller
return orderId;
}
 
             6.实体模型 
还记得用户注册时收集信息的方式吗?下面这几句:

//......
AddressInfo address = addr.Address;
//.....
AccountInfo accountInfo = new AccountInfo(userId, password, email, address, language, favCategory, showFavorites, showBanners);
//.....
ccountController.CreateAccount(accountInfo)

AddressInfo和AccountInfo都是瘦实体类,整个程序都用这种模型来完成信息传递。这些类的定义都在Model组件中。应该是整个petshop3.0中最重要的组件之一。
Model:瘦数据类或业务实体,在应用程序各层之间传递数据的瘦数据类。这些是用 C# 类实现的,每个字段都以属性的形式公开。 每个类都标记为“serializable”,启用进程间传输。
.net是面向对象的,这大家都知道,可以在具体实现的时候应该怎么做呢?总之我是完全的不明白。不过看了petshop3.0中Model组件的应用,算是又长了见识。
在程序中总有那么一些信息可以归为一类,比如商品信息,用户信息……拿用户信息来说,应该有用户名、密码、住址、电话、个人爱好等具体的信息。在petshop3.0,这些就可以总归为一个实体,而其中的信息都是这个类的属性,在其中住址、电话等信息又可以单独的归为一个实体,因为他们是描述个人具体信息的,并且是可变的(描述的不是很准确,可能有错误)。所以我们看到petshop3.0把这些信息归成一个AddressInfo实体,而该实体又成为AccountInfo实体的一个属性。具体应用的时候,这些实体都是作为一个整体使用,因为你总不能把一个人切开吧?再从c#的角度看,这些都是类型,可以看成用户的自定义类型,所以我们看到AddressUI这个用户控件把AddressInfo做为属性对外公开。
实体模型的使用贯穿了整个petshop3.0,从表面看,几乎每一层都引用了Model组件。还是以用户这块来说下去,表示层收集数据的时候,会把用户填的信息作为一个实体来收集,完成后用户接口处理组件会把信息实体做为参数传送给业务逻辑组件,业务逻辑部分再把信息实体做为参数经工厂传送给数据处理组件,在这里做最后处理,提交到数据库或返回错误。
在整个过程中,根本就不存在单独的值类型的传递。这样做的好处很明显,使逻辑操作更加透明和易于理解,便于构建复杂系统的模型,可以封装一些不需要向程序公开的信息,等等。
此类代码都处都是,一个比较明显的例子就是CartItemInfo,该实体模型表述放到购物车内的商品。该实体并不保存到数据库,完全是存在于进程中的。由于该实体的存在,大大简化了购物车这一抽象实体的组成。
实际上,在Model组件中根本就没有Cart这个实体。Cart的实现由BLL组件来完成。这个Cart并非和上面一样的瘦实体类,而是另外一种实体模型,因为它不仅包含着作为实体本身的信息,而且还有自己的行为。按照msdn上的叫法,这是“带有CRUD行为的业务实体”。我们可以把这个Cart看成一个智能机器人,他会自己挑选商品,并给出现在车内的信息。
通过类图来对比一下Cart和CartItemInfo。

      

CartItemInfo只有属性,而Cart则包括属性和方法。
要注意的是,Cart归属于业务逻辑组件,这是因为它的作用更多的是执行其行为,它的最主要的属性就是一个索引器,通过这个来获得CartItemInfo。

最后看一下CartItemInfo的代码,了解此类模型的基本构成。

///
/// Business entity used to model items in a shopping cart
///

[Serializable]
public class CartItemInfo {
private const string YES = "Yes";
private const string NO = "No";
// Internal member variables
private int _quantity = 1;
private bool _inStock = false;
private string _itemId = null;
private string _name;
private decimal _price;
///
/// Default constructor
///

/// Every cart item requires an itemId

public CartItemInfo(string itemId) {
this._itemId = itemId;
}
///
/// Constructor with specified initial values
///

/// Id of item to add to cart
/// Name of item
/// Is the item in stock
/// Quantity to purchase
/// Price of item

public CartItemInfo(string itemId, string name, bool inStock, int qty, decimal price) {

this._itemId = itemId;
this._name = name;
this._quantity = qty;
this._price = price;
this._inStock = inStock;
}
// Properties
public int Quantity {
get { return _quantity; }
set { _quantity = value; }
}
public bool InStock {
get { return _inStock; }
set { _inStock = value; }
}
public decimal Subtotal {
get { return (decimal)(this._quantity * this._price); }
}
public string ItemId {
get { return _itemId; }
}
public string Name {
get { return _name; }
}
public decimal Price {
get { return _price; }
}
}
下面是msdn上关于业务实体的一些资料。顺便转过来好了:)
----
原文地址 ------
定义自定义业务实体组件
表示业务实体的自定义类通常包含以下成员:
  • 用于在本地缓存业务实体的数据的专用字段。这些字段在数据访问逻辑组件从数据库检索数据时保存数据库数据的一个快照。
  • 用于访问实体的状态和访问实体内数据的子集及层次结构的公共属性。这些属性的名称可以与数据库的列名称相同,但这并不是一个绝对要求。可以根据您的应用程序的需要选择属性名,而不必使用数据库中的名称。
  • 用以使用实体组件中的数据执行本地化处理的方法和属性。
  • 用以通知实体组件内部状态变化的事件。
图 9 所示为使用自定义实体类的方法。注意,实体类并不知道数据访问逻辑组件或基础数据库;所有数据库访问都由数据访问逻辑组件执行,以集中数据访问策略和业务逻辑。此外,在层间传递业务实体数据的方式与表示业务实体的格式也没有直接关系;例如,可以在本地将业务实体表示为对象,而用另一种方法(如标量值或 XML)将业务实体数据传递到其他层。
图 9:自定义业务实体组件的作用(单击缩略图以查看大图像)
定义自定义业务实体组件的建议
在实现自定义实体组件时,请考虑以下建议:
  • 选择使用结构还是使用类。对于不包含分层数据或集合的简单业务实体,可以考虑定义一个结构来表示业务实体。对于复杂的业务实体或要求继承的业务实体,可将实体定义为类。有关结构和类这两种类型的比较,请参阅 Structures and Classes
  • 表示业务实体的状态。对于数字、字符串等简单值,可以使用等价的 .NET 数据类型来定义字段。有关说明如何定义自定义实体的代码示例,请参阅附录中的如何定义业务实体组件
  • 表示自定义业务实体组件中的子集合和层次结构。表示自定义实体中的数据的子集合和层次结构的方法有两种:
    • .NET 集合(例如 ArrayList)。.NET 集合类为大小可调的集合提供了一个方便的编程模型,还为将数据绑定到用户界面控件提供了内置的支持。
    • DataSet。DataSet 适合于存储来自关系数据库或 XML 文件的数据的集合和层次结构。此外,如果需要过滤、排序或绑定子集合,也应首选 DataSet。
      有关说明如何表示自定义实体中数据的集合和层次结构的代码示例,请参阅附录中的 如何表示自定义实体中数据的集合和层次结构
  • 支持用户界面客户端的数据绑定。如果自定义实体将要由用户界面使用并且希望利用自动数据绑定,可能需要在自定义实体中实现数据绑定。请考虑以下方案:
    • Windows 窗体中的数据绑定。您可以将实体实例的数据绑定到控件而不必在自定义实体中实现数据绑定接口。也可以绑定实体的数组或 .NET 集合。
    • Web 窗体中的数据绑定。如果不实现 IBindingList 接口,则不能将实体实例的数据绑定到 Web 窗体中的控件。然而,如果只想绑定集合,则可以使用数组或 .NET 集合而不必在自定义实体中实现 IBindingList 接口。
      有关说明如何将自定义实体绑定到用户界面控件的代码示例,请参阅附录中的 如何将业务实体组件绑定到用户界面控件
  • 公开内部数据更改的事件。公开事件可以获得丰富的客户端用户界面设计,因为它使得无论数据显示在哪里都可以对其进行刷新。事件应当只针对内部状态,而不是针对服务器上的数据更改。有关说明如何公开自定义实体类中的事件的代码示例,请参阅附录中的如何公开业务实体组件中的事件
  • 使业务实体可序列化。使业务实体可序列化可以将业务实体的状态保持在中间状态而不进行数据库交互。这样可以方便脱机应用程序的开发和复杂用户界面过程的设计,即在完成前不会影响业务数据。序列化有两种类型:
    • 使用 XmlSerializer 类进行 XML 序列化。如果只需要把公共字段和公共读/写属性序列化为 XML,则可以使用 XML 序列化。注意,如果从 Web 服务返回业务实体数据,对象将通过 XML 序列化自动序列化为 XML。
      您可以对业务实体执行 XML 序列化而无需在实体中实现任何附加代码。然而,只有对象中的公共字段和公共读/写属性被序列化为 XML。专用字段、索引生成器、专用属性、只读属性及对象图不会被序列化。您可以使用自定义实体中的属性控制结果 XML。有关将自定义实体组件序列化为 XML 格式的详细信息,请参阅附录中的 如何将业务实体组件序列化为 XML 格式
    • 使用 BinaryFormatter 或 SoapFormatter 类进行格式序列化。如果需要序列化对象的所有公共字段、专用字段及对象图,或者需要与远程服务器之间传递实体组件,则可以使用格式序列化。
      格式类将序列化对象的所有公共和专用字段及属性。BinaryFormatter 将对象序列化为二进制格式,SoapFormatter 将对象序列化为 SOAP 格式。使用 BinaryFormatter 的序列化比使用 SoapFormatter 的序列化速度要快。要使用任何一个格式类,都必须将实体类标记为 [Serializable] 属性。如果需要显式控制序列化格式,您的类还必须实现 ISerializable 接口。有关如何使用格式序列化的详细信息,请参阅附录中的 如何将业务实体组件序列化为二进制格式 如何将业务实体组件序列化为 SOAP 格式
    注意:还原序列化某个对象时,不会调用默认的构造函数。对还原序列化添加这项约束,是出于性能方面的考虑。
定义自定义实体的优点如下:
  • 代码易读。要访问自定义实体类中的数据,可以使用有类型的方法和属性,如以下代码所示:
     
           
    在上述示例中,产品是一个名为 ProductEntity 的自定义实体类的一个实例。ProductDALC 类有一个名为 GetProduct 的方法,后者创建一个 ProductEntity 对象,将某个特定产品的数据填充到该对象,然后返回 ProductEntity 对象。调用应用程序可以使用 ProductName 等属性访问 ProductEntity 对象中的数据,并且可以调用方法以操作该对象。
  • 封装。自定义实体可以包含方法以封装简单的业务规则。这些方法操作缓存在实体组件中的业务实体数据,而不是访问数据库中的实时数据。请考虑以下示例:
     
           
    在上述示例中,调用应用程序对 ProductEntity 对象调用一个名为 IncreaseUnitPriceBy 的方法。在调用应用程序对 ProductDALC 对象调用相应方法,从而将 ProductEntity 对象保存到数据库之前,这一更改并不是永久性的。
  • 构建复杂系统的模型。在构建复杂域问题(在不同业务实体之间存在很多交互)的模型时,可以定义自定义实体类,从而将复杂性隐藏在经过很好定义的类接口的后面。
  • 本地化验证。自定义实体类可以在其属性存取器中执行简单的验证测试以检测无效的业务实体数据。有关详细信息,请参阅如何在业务实体组件的属性存取器中验证数据
  • 专用字段。您可以隐藏不希望向调用程序公开的信息。
定义自定义实体的缺点如下:
  • 业务实体集合。自定义实体表示的是单个业务实体,而不是一个业务实体集合。要保存多个业务实体,调用应用程序必须创建一个数组或一个 .NET 集合。
  • 序列化。您必须在自定义实体中实现自己的序列化机制。可以使用属性来控制实体组件的序列化方式,也可以通过实现 ISerializable 接口来控制自己的序列化。
  • 表示业务实体中的复杂关系和层次结构。您必须在业务实体组件中实现自己的关系和层次结构表示机制。如前面所述,DataSet 通常是实现这一目的的最简单方式。
  • 搜索和排序数据。您必须定义自己的机制来支持实体的搜索和排序。例如,可以通过实现 IComparable 接口以便将实体组件保存在一个 SortedList 集合或 Hashtable 集合中。
  • 部署。您必须在所有物理层部署包含自定义实体的程序集。
  • 支持企业服务 (COM+) 客户端。如果一个自定义实体将由 COM+ 客户端使用,则必须为包含该实体的程序集提供一个严格名称,并且必须在客户端计算机上注册。通常,该程序集安装在全局程序集缓存中。
  • 可扩展性问题。如果修改了数据库架构,则可能需要修改自定义实体类并重新部署程序集。
定义带有 CRUD 行为的自定义业务实体组件
在定义一个自定义实体时,可以提供方法以完全封装对基础数据访问逻辑组件的 CRUD 操作。这是比较传统的面向对象的方法,可能适用于复杂的对象域。客户端应用程序不再直接访问数据访问逻辑组件类,而是创建一个实体组件并对该实体组件调用 CRUD 方法。这些方法将调用基础的数据访问逻辑组件。
图 10 所示为带有 CRUD 行为的自定义实体类的作用。
图 10:带有 CRUD 行为的自定义业务实体组件的作用(单击缩略图以查看大图像)
定义带有 CRUD 行为的自定义实体类的优点如下:
  • 封装。自定义实体可以封装由基础数据访问逻辑组件定义的操作。
  • 与调用程序的接口。调用程序必须只处理一个接口来保持业务实体数据。不必直接访问数据访问逻辑组件。
  • 专用字段。您可以隐藏不希望向调用程序公开的信息。
定义带有 CRUD 行为的自定义实体类的缺点如下:
  • 处理业务实体集合。自定义实体中的方法属于单个业务实体实例。要支持业务实体集合,可以定义静态方法以读取或返回一个数组或一个实体组件集合。
  • 开发时间长。传统的面向对象方法通常比使用现有对象(如 DataSet)需要更多的设计和开发工作。
                7.SimplePager
看一下SQLServerDAL,前面有高人说了,那个SQLHelper类似于daab for .net,因此在这里我就不多话了,对daab for .net的讨论可以参看 msdn的相关文章
来看Account.cs,里面有一个GetAddress方法,用来通过用户名获得用户资料。在这里面主要是分两步,第一步是获得参数并给其赋值,第二步是使用SqlDataReader来从数据库取值。
public AddressInfo GetAddress(string userId) {
AddressInfo address= null;
//获得参数列表

SqlParameter[] addressParms = GetAddressParameters();
//给参数赋值
addressParms[0].Value = userId;
//使用SqlDataReader来从数据库取值
using (SqlDataReader rdr = SQLHelper.ExecuteReader(SQLHelper.CONN_STRING_NON_DTC, CommandType.Text, SQL_SELECT_ADDRESS, addressParms)) {
if (rdr.Read()) {
address = new AddressInfo(rdr.GetString(0), rdr.GetString(1), rdr.GetString(2), rdr.GetString(3), rdr.GetString(4), rdr.GetString(5), rdr.GetString(6), rdr.GetString(7), rdr.GetString(8));
}
}
return address;
}

参数的获得比较复杂,先来看SqlDataReader的使用吧
using (SqlDataReader rdr = SQLHelper.ExecuteReader(SQLHelper.CONN_STRING_NON_DTC, CommandType.Text, SQL_SELECT_ADDRESS, addressParms)) {
if (rdr.Read()) {
address = new AddressInfo(rdr.GetString(0), rdr.GetString(1), rdr.GetString(2), rdr.GetString(3), rdr.GetString(4), rdr.GetString(5), rdr.GetString(6), rdr.GetString(7), rdr.GetString(8));
}
}

很简单的应用,SQLHelper.ExecuteReader的确是起到了和daab for .net差不多的作用。因为是获得个人资料,所以这里只有一条记录,就算是要处理需要分页的大数据量时,它也是这样用SqlDataReader返回所有的记录,但是之后就会把这些记录全部保存在Cache中,这样以后就不用查询数据库了。
GetAddressParameters方法用来获得此次操作需要的参数,它也是使用了SQLHelper的相关方法来完成的。
private static SqlParameter[] GetAddressParameters() {
//如果以前有保存参数,那就取出来

SqlParameter[] parms = SQLHelper.GetCachedParameters(SQL_SELECT_ADDRESS);
if (parms == null) {
parms = new SqlParameter[] {
new SqlParameter(PARM_USER_ID, SqlDbType.VarChar, 80)};
//创建新的参数数组,然后“缓存”起来
SQLHelper.CacheParameters(SQL_SELECT_ADDRESS, parms);
}
return parms;
}
我们可以接着看SQLHelper中的细节部分

//parmCache的定义
private static Hashtable parmCache = Hashtable.Synchronized(new Hashtable());

//“缓存”参数
public static void CacheParameters(string cacheKey, params SqlParameter[] cmdParms) {
parmCache[cacheKey] = cmdParms;
}
///
/// Retrieve cached parameters
///

/// key used to lookup parameters
/// Cached SqlParamters array

public static SqlParameter[] GetCachedParameters(string cacheKey) {
//从缓存中取出值
SqlParameter[] cachedParms = (SqlParameter[])parmCache[cacheKey];
if (cachedParms == null)
return null;
SqlParameter[] clonedParms = new SqlParameter[cachedParms.Length];
//使用clone通过遍历得到一个新的参数数组
for (int i = 0, j = cachedParms.Length; i < j; i++)
clonedParms[i] = (SqlParameter)((ICloneable)cachedParms[i]).Clone();
return clonedParms;
}
用来缓存参数的parmCache实际上一个private static Hashtable,因为是static,所以自然是独一无二的了,创建一次就可以永远使用,起到和缓存差不多的效果。
在返回参数时要用clone来创建一个新的参数数组返回,是因为parmCache只有一个,而返回的参数数组则至少一个人用一个。更为重要的是,现在保存在parmCache的参数列表都是没有具体值的参数,而返回后则根据每个用户的不同情况而被赋于不同的值。

从这里子可以看出,有一个自己的daab是多么的重要。先前在论坛看到过有人喜欢用拖的方式来进行数据库的连接,那种做法我觉得不是很好,手写代码很重要的,写一个自己顺手的daab更是重要。
 
                        8.数据访问
看一下SQLServerDAL,前面有高人说了,那个SQLHelper类似于daab for .net,因此在这里我就不多话了,对daab for .net的讨论可以参看 msdn的相关文章
来看Account.cs,里面有一个GetAddress方法,用来通过用户名获得用户资料。在这里面主要是分两步,第一步是获得参数并给其赋值,第二步是使用SqlDataReader来从数据库取值。
public AddressInfo GetAddress(string userId) {
AddressInfo address= null;
//获得参数列表

SqlParameter[] addressParms = GetAddressParameters();
//给参数赋值
addressParms[0].Value = userId;
//使用SqlDataReader来从数据库取值
using (SqlDataReader rdr = SQLHelper.ExecuteReader(SQLHelper.CONN_STRING_NON_DTC, CommandType.Text, SQL_SELECT_ADDRESS, addressParms)) {
if (rdr.Read()) {
address = new AddressInfo(rdr.GetString(0), rdr.GetString(1), rdr.GetString(2), rdr.GetString(3), rdr.GetString(4), rdr.GetString(5), rdr.GetString(6), rdr.GetString(7), rdr.GetString(8));
}
}
return address;
}

参数的获得比较复杂,先来看SqlDataReader的使用吧
using (SqlDataReader rdr = SQLHelper.ExecuteReader(SQLHelper.CONN_STRING_NON_DTC, CommandType.Text, SQL_SELECT_ADDRESS, addressParms)) {
if (rdr.Read()) {
address = new AddressInfo(rdr.GetString(0), rdr.GetString(1), rdr.GetString(2), rdr.GetString(3), rdr.GetString(4), rdr.GetString(5), rdr.GetString(6), rdr.GetString(7), rdr.GetString(8));
}
}

很简单的应用,SQLHelper.ExecuteReader的确是起到了和daab for .net差不多的作用。因为是获得个人资料,所以这里只有一条记录,就算是要处理需要分页的大数据量时,它也是这样用SqlDataReader返回所有的记录,但是之后就会把这些记录全部保存在Cache中,这样以后就不用查询数据库了。
GetAddressParameters方法用来获得此次操作需要的参数,它也是使用了SQLHelper的相关方法来完成的。
private static SqlParameter[] GetAddressParameters() {
//如果以前有保存参数,那就取出来

SqlParameter[] parms = SQLHelper.GetCachedParameters(SQL_SELECT_ADDRESS);
if (parms == null) {
parms = new SqlParameter[] {
new SqlParameter(PARM_USER_ID, SqlDbType.VarChar, 80)};
//创建新的参数数组,然后“缓存”起来
SQLHelper.CacheParameters(SQL_SELECT_ADDRESS, parms);
}
return parms;
}
我们可以接着看SQLHelper中的细节部分

//parmCache的定义
private static Hashtable parmCache = Hashtable.Synchronized(new Hashtable());

//“缓存”参数
public static void CacheParameters(string cacheKey, params SqlParameter[] cmdParms) {
parmCache[cacheKey] = cmdParms;
}
///
/// Retrieve cached parameters
///

/// key used to lookup parameters
/// Cached SqlParamters array

public static SqlParameter[] GetCachedParameters(string cacheKey) {
//从缓存中取出值
SqlParameter[] cachedParms = (SqlParameter[])parmCache[cacheKey];
if (cachedParms == null)
return null;
SqlParameter[] clonedParms = new SqlParameter[cachedParms.Length];
//使用clone通过遍历得到一个新的参数数组
for (int i = 0, j = cachedParms.Length; i < j; i++)
clonedParms[i] = (SqlParameter)((ICloneable)cachedParms[i]).Clone();
return clonedParms;
}
用来缓存参数的parmCache实际上一个private static Hashtable,因为是static,所以自然是独一无二的了,创建一次就可以永远使用,起到和缓存差不多的效果。
在返回参数时要用clone来创建一个新的参数数组返回,是因为parmCache只有一个,而返回的参数数组则至少一个人用一个。更为重要的是,现在保存在parmCache的参数列表都是没有具体值的参数,而返回后则根据每个用户的不同情况而被赋于不同的值。

从这里子可以看出,有一个自己的daab是多么的重要。先前在论坛看到过有人喜欢用拖的方式来进行数据库的连接,那种做法我觉得不是很好,手写代码很重要的,写一个自己顺手的daab更是重要。
 
                   9.工厂的资料
1.ms介绍
为了简化数据库访问类的使用,我们选择 GoF (译注:指 Erich Gamma 等著《设计模式》一书)概述的工厂设计模式,通过反射动态在运行时加载正确的数据访问对象。 工厂设计模式是这样实现的: 创建一个 C# 接口,其中对于数据库访问类必须公开的每个方法都要声明一个方法。 对于每一个要支持的数据库,都创建一个实现数据库特定代码的具体类,以执行接口也称“协定”中的每一项操作。 为了支持运行时确定加载哪一个具体类,需要创建第三个类,也就是工厂类,它从配置文件中读入一个值以确定应该使用反射加载哪一个程序集。 通过 .NET 的反射命名空间,可以加载某个特定程序集并用该程序集创建某个对象的实例。 为了使应用程序更安全,为版本控制提供更好的支持,我们可以在应用程序配置文件(也就是这里的 web.config. )中添加要加载的程序集文件的“证据”,这意味着 .NET 框架将只加载我们在编译期间签过名而且有正确版本号的程序集。 图 10 说明了业务逻辑类、工厂类和数据库访问类是如何相互操作的。 这一创建的解决方案最重要的优势是数据库访问类可以在业务逻辑类之后编译,只要数据访问类实现了 IDAL 接口。 这意味着,如果要创建应用程序的 DB2 版本,我们不需要改动业务逻辑层(或者 UI 层)。 创建 DB2 兼容版本的步骤如下:
1.创建 DB2 的数据库访问类,它应该实现 IDAL 接口。
2.将 DB2 访问类编译成一个程序集。
3.测试和部署新的数据程序集到一台运行中的服务器上。
4.更改配置文件,指向新的数据库访问类。
无需更改或重新编译业务逻辑组件。

图 10. .NET Pet Shop 中 DAL 工厂类的实现

2.数据分析

5 PetShop.DALFactory 数据访问工厂

那么DALFactory是如何决定应该用SqlServerDAL还是用OracleDAL的呢?我们接着分析。
以下是PetShop.DALFactory.Account类的实现:
namespace PetShop.DALFactory {
 
///
/// Factory implementaion for the Account DAL object
///
public class Account
{
public static PetShop.IDAL.IAccount Create()?//<<<
{?
/// Look up the DAL implementation we should be using
string path = System.Configuration.ConfigurationSettings.AppSettings["WebDAL"];
string className = path + ".Account";
 
// Using the evidence given in the config file load the appropriate assembly and class
return (PetShop.IDAL.IAccount) Assembly.Load(path).CreateInstance(className);
}
}
}
以下则是web.config中节点中的一部分:
?
上面的Create()方法返回IAccount接口,用System.Configuration.ConfigurationSettings.AppSettings["WebDAL"];则可以得到Web.config的节点中的关于系统中应该使用哪个数据访问层(SqlserverDAL还是OracleDAL)的信息。因为我在安装PetShop3.0时选择的是Sqlserver所以在此是:value="PetShop.SQLServerDAL",如果用的是Oracle那就是value="PetShop.OracleDAL" 了吧!而且这个文件也应该是可以更改的。接下来className=path+”.Account”返回的应该是PetShop.SQLServerDAL.Account,然后再用Assembly.Load加载PetShop.SQLServerDAL.dll,同时创建PetShop.SQLServerDAL.Account的实例,并以接口(PetShop.IDAL.IAccount)类型返回。这样BLL调用IAccount接口时就会用PetShop.SQLServerDAL.Account类的实现代码。(回上面第4再看一下)
 
看!这样根据系统当前Web.config文件的配置描述(这也应该是系统运行时实际的配置),BLL层只要像下面这样:
// Get an instance of the account DAL using the DALFactory
IAccount dal = PetShop.DALFactory.Account.Create();
AccountInfo account = dal.SignIn(userId, password);//<
就可以直接调用接口方法通过下层DAL层操作数据库了(在此具体为用户账号相关操作),而BLL层并不用知道应该通过SqlserverDAL还是OracleDAL访问数据库,这由都DAL Factory决定,你用的是什么数据库以及底层细节,更不用BLL知道,这样做的好处是对于BLL层以及更上层的程序不会或很少机率会因为底层程序变动影响,因为BLL层中调用接口就行了,只要那个接口定义没变,一切仍然OK.

3.相关sdk文档资料。

Assembly 类
定义一个 Assembly,它是可重用、无版本冲突并且可自我描述的公共语言运行库应用程序构造块。
有关此类型所有成员的列表,请参阅 Assembly 成员
 
      
public static Assembly Load(string assemblyString);
描述
通过给定程序集的长格式名称加载程序集。
参数
assemblyString
程序集名称的长格式。
返回值
加载的程序集。

public object CreateInstance(string typeName);
描述
使用区分大小写的搜索,从此程序集中查找指定的类型,然后使用系统激活器创建它的实例。
参数
typeName
要查找的类型的 Type.FullName。
返回值
表示该类型的 Object 的实例,其区域性、参数、联编程序和激活属性设置为空引用(Visual Basic 中为 Nothing),并且 BindingFlags 设置为 Public 或 Instance,或者设置为空引用 (Nothing)(如果没有找到 typeName)。

工厂模式是设计模式的一种,以我理解就像Factory这个词一样,对于用户来说,工厂里产品如何生产的你不用知道,你只要去用工厂里生产出来的东西就可以了。MSPetShop3.0用工厂模式来实现了对SqlServer和Oracle数据库访问的操作,而用户(business Logic Layer)不用知道也不用关心后台用的是哪一种数据库,它只要用接口就行了,接口中定义了要用的方法,当调用接口时会根据具体的情况再去调用底层数据访问操作。而现在这个DALFactory就是关键,当BLL层要操作数据库时,DALFactory会根据具体情况再去使用本文上面介绍的SqlServerDAL和OracleDAL中的一个。这样系统上层只管调用,而下层来实现细节,上级只管发号施令,下级去干活。对于上层来说实现细节被隐藏了。
 

你可能感兴趣的:(专业资料)