本文来自《ASP.NET AJAX程序设计——第II卷:客户端Microsoft AJAX Library相关》第9章第3节。
9.3 示例程序:实现IDragSource和IDropTarget接口将商品拖放至购物车中
本章第1节介绍了ASP.NET AJAX客户端拖放框架中的DragDropManager对象以及IDragSource和IDropTarget两个重要接口。下面就让我们通过一个简单但却足够完善的示例程序来说明其具体的使用方法。
9.3.1 示例程序功能
本示例程序模拟了一个网上商店。程序加载之后将自动从服务器端Web Service中取得当前的商品信息,并显示在页面中。同样显示在页面中的还有用户的购物车。如图9-6所示。
图9-6 网上商店的初始界面
用户可以非常直观地在左侧的商品列表中用鼠标选择并将某种商品拖动到右侧的购物车中,类似于实际生活中在超市购物的场景,如图9-7所示。是不是很酷呢?
图9-7 通过拖动商品到购物车中完成购物过程
在购物车范围内松开鼠标左键,商品即被添加到了购物车中。每拖放一次某商品,购物车中该商品的数量就会加1。图9-8显示了购物车中包含一本《Atlas基础教程》、两本《ASP.NET AJAX程序设计 第I卷》和三本《CSS禅意花园》时的样式。
图9-8 在购物车中添加多个商品
在图9-8中点击购物车中的“Order”按钮,程序将把当前购物车中的商品传回服务器端Web Service中进行处理。然后把Web Service的处理结果显示给用户,如图9-9所示。
图9-9 用服务器端Web Service实现购买功能
9.3.2 编写服务器端Web Service
既然程序需要从服务器端取得数据,还需要将购物车中的商品提交回服务器处理,那么处理数据的Web Service定是必不可少的。首先创建一个名为ShoppingService的Web Service:
[System.Web.Script.Services.ScriptService]
public class ShoppingService : System.Web.Services.WebService
{
}
在其中创建一个名为Data的私有属性,表示当前网上商店中的产品集合:
private List<Product> _products;
private List<Product> Products
{
get
{
if (_products == null)
{
_products = new List<Product>();
_products.Add(new Product(1, "Atlas基础教程", 39));
_products.Add(new Product(2, "ASP.NET AJAX程序设计第I卷", 49));
_products.Add(new Product(3, "ASP.NET AJAX程序设计第II卷", 55));
_products.Add(new Product(4, "ASP.NET AJAX程序设计第III卷", 39));
_products.Add(new Product(5, "CSS禅意花园", 49));
}
return _products;
}
}
出于演示的目的,我们并没有查询数据库得到商品集合,而是直接硬编码了几种商品(这些商品都是我已经出版或即将要出版的图书)。在实际开发中,这部分数据通常都需要查询数据库得到。
上述代码中用到的Product类表示一种商品,其定义如下。Id、Name和Price属性分别代表商品的ID、名称和价格:
public class Product
{
private int _id;
public int Id
{
get { return _id; }
set { _id = value; }
}
private string _name;
public string Name
{
get { return _name; }
set { _name = value; }
}
private int _price;
public int Price
{
get { return _price; }
set { _price = value; }
}
public Product()
{
}
public Product(int id, string name, int price)
{
this._id = id;
this._name = name;
this._price = price;
}
}
回到ShoppingService这个Web Service中。编写一个名为GetProducts()的方法,用来返回现有的商品:
[WebMethod]
public Product[] GetProducts()
{
return Products.ToArray();
}
还需要一个名为Order()的方法,用来处理客户端提交回的购物车中商品的信息,并返回处理结果。注意Order()的方法的参数类型为Dictionary<string, int>,其中每个条目的Key为该商品的Id,Value为购物车中该商品的个数:
[WebMethod]
public string Order(Dictionary<string, int> productsToBeOrdered)
{
// 商品总数
int totalQuantity = 0;
// 商品总价格
int totalPrice = 0;
// 计算商品总数及商品总价格
foreach (KeyValuePair<string, int> productQuantity in productsToBeOrdered)
{
foreach (Product product in Products)
{
if (product.Id.ToString() == productQuantity.Key)
{
totalQuantity += productQuantity.Value;
totalPrice += (product.Price * productQuantity.Value);
break;
}
}
}
// 进行其他处理
// ......
// 返回客户端处理结果
return string.Format(
"You've ordered {0} product(s) and the total price is {1}. Thank you!",
totalQuantity,
totalPrice
);
}
这样即完成了服务器端Web Service的编写。
9.3.3 编写DraggableProductBehavior实现可拖动的商品
接下来定义一个名为DraggableProductBehavior的行为,该行为将实现Sys.Preview.UI.IDragSource接口,用来附加到表示产品的DOM元素上并为其提供拖动支持。
新建一个名为ShoppingCart.js的JavaScript文件,先来注册Dflying命名空间:
Type.registerNamespace("Dflying");
然后声明该DraggableProductBehavior的实例字段:
Dflying.DraggableProductBehavior = function(element) {
// 初始化基类
Dflying.DraggableProductBehavior.initializeBase(this, [element]);
// 按下鼠标键时的事件处理函数
this._mouseDownHandler = Function.createDelegate(this, this._handleMouseDown);
// 该可拖动对象所表示的产品
this._product = null;
// 拖动时跟随鼠标的半透明元素
this._visual = null;
}
接下来是DraggableProductBehavior的原型定义。篇幅所限,这里不能对代码进行详细解释,请参考其中的注释仔细阅读。需要特别留意的是IDragSource接口中各个方法的实现方式,以及其中粗体部分告知DragDropManager开始拖放操作的语法:
Dflying.DraggableProductBehavior.prototype = {
// IDragSource接口中的方法
// 取得该可拖动对象的数据类型——"Product"
get_dragDataType: function() {
return "Product";
},
// 取得该可拖动对象的数据
getDragData: function(context) {
return this._product;
},
// 可拖动对象的拖拽模式——拷贝
get_dragMode: function() {
return Sys.Preview.UI.DragMode.Copy;
},
// 拖动开始时的处理方法
onDragStart: function() {
},
// 拖动进行时的处理方法
onDrag: function() {
},
// 拖动结束时的处理方法
onDragEnd: function(canceled) {
if (this._visual)
this.get_element().parentNode.removeChild(this._visual);
},
// product属性
get_product: function(product) {
return this._product;
},
set_product: function(product) {
this._product = product;
},
// 初始化方法
initialize: function() {
$addHandler(this.get_element(), "mousedown", this._mouseDownHandler);
},
// mousedown事件处理函数
_handleMouseDown: function(ev) {
// DragDropManager需要该项设定
window._event = ev;
// 设置拖动时跟随鼠标的半透明元素的样式
this._visual = this.get_element().cloneNode(true);
this._visual.style.opacity = "0.7";
this._visual.style.filter =
"progid:DXImageTransform.Microsoft.BasicImage(opacity=0.7)";
this._visual.style.zIndex = 99999;
this.get_element().parentNode.appendChild(this._visual);
var location = Sys.UI.DomElement.getLocation(this.get_element());
Sys.UI.DomElement.setLocation(this._visual, location.x, location.y);
// 告知DragDropManager开始拖放操作
Sys.Preview.UI.DragDropManager.startDragDrop(this, this._visual, null);
},
// 析构方法
dispose: function() {
if (this._mouseDownHandler)
$removeHandler(this.get_element(), "mousedown", this._mouseDownHandler);
this._mouseDownHandler = null;
Dflying.DraggableProductBehavior.callBaseMethod(this, 'dispose');
}
}
最后在ASP.NET AJAX客户端框架中注册该DraggableProductBehavior行为。可以看到该行为实现了IDragSource接口:
Dflying.DraggableProductBehavior.registerClass(
"Dflying.DraggableProductBehavior",
Sys.UI.Behavior,
Sys.Preview.UI.IDragSource
);
9.3.4 编写ShoppingCartBehavior实现可接受商品投放的购物车
依然在ShoppingCart.js这个JavaScript文件中,我们还要定义一个名为ShoppingCartBehavior的行为,该行为将实现Sys.Preview.UI.IDropTarget接口,用来附加到表示购物车的DOM元素上并让其能够接受可拖动对象的投放。
首先声明该ShoppingCartBehavior的实例字段:
Dflying.ShoppingCartBehavior = function(element) {
// 初始化基类
Dflying.ShoppingCartBehavior.initializeBase(this, [element]);
// 购物车中的产品列表
this._products = new Object();
}
接下来是ShoppingCartBehavior的原型定义。同样限于篇幅,这里不一一详细说明。请注意代码中IDropTarget接口中各个方法的实现方式,以及其中粗体部分在DragDropManager中注册/取消注册该投放目标对象的语法:
Dflying.ShoppingCartBehavior.prototype = {
// IDropTarget接口中的方法
// 返回购物车元素
get_dropTargetElement: function() {
return this.get_element();
},
// 判断某可拖动元素是否能够投放在该投放目标对象中
canDrop: function(dragMode, dataType, data) {
return (dataType == "Product" && data);
},
// 将某可拖动元素投放在该投放目标对象中
drop : function(dragMode, dataType, data) {
if (dataType == "Product" && data) {
// 购物车中尚无本产品,设置数量为1
if (this._products[data.Id] == null) {
this._products[data.Id] = {Product: data, Quantity: 1};
}
// 购物车中已经有本产品,将其数量加1
else {
this._products[data.Id].Quantity++;
}
// 刷新购物车的UI
this._refreshShoppingCart();
// 将购物车背景颜色设置回白色
this.get_element().style.backgroundColor = "#fff";
}
},
// 某可拖动元素位于该投放目标对象中时的处理方法
onDragEnterTarget : function(dragMode, dataType, data) {
if (dataType == "Product" && data) {
// 设置购物车的背景颜色为灰色
this.get_element().style.backgroundColor = "#E0E0E0";
}
},
// 某可拖动元素离开该投放目标对象时的处理方法
onDragLeaveTarget : function(dragMode, dataType, data) {
if (dataType == "Product" && data) {
// 将购物车背景颜色设置回白色
this.get_element().style.backgroundColor = "#fff";
}
},
// 某可拖动元素在该投放目标对象中拖动时的处理方法
onDragInTarget : function(dragMode, dataType, data) {
},
// 根据当前购物车中的产品列表刷新购物车的UI
_refreshShoppingCart: function() {
var cartBuilder = new Sys.StringBuilder();
for (var id in this._products) {
cartBuilder.append("<div>");
cartBuilder.append(this._products[id].Product.Name);
cartBuilder.append(" * ");
cartBuilder.append(this._products[id].Quantity);
cartBuilder.append("</div>");
}
this.get_element().innerHTML = cartBuilder.toString();
},
// 返回表示当前购物车中的产品Id以及数量的对象
getProductsToBeOrdered: function() {
var productsToBeOrdered = new Object();
for (var id in this._products) {
productsToBeOrdered[id] = this._products[id].Quantity;
}
return productsToBeOrdered;
},
// 初始化方法
initialize: function() {
// 初始化基类
Dflying.ShoppingCartBehavior.callBaseMethod(this, "initialize");
// 在DragDropManager中注册该投放目标对象
Sys.Preview.UI.DragDropManager.registerDropTarget(this);
},
// 析构方法
dispose: function() {
// 在DragDropManager中取消注册该投放目标对象
Sys.Preview.UI.DragDropManager.unregisterDropTarget(this);
Dflying.ShoppingCartBehavior.callBaseMethod(this, "dispose");
}
}
最后在ASP.NET AJAX客户端框架中注册该ShoppingCartBehavior行为。可以看到该行为实现了IDropTarget接口:
Dflying.ShoppingCartBehavior.registerClass("Dflying.ShoppingCartBehavior",
Sys.UI.Behavior, Sys.Preview.UI.IDropTarget);
在ShoppingCart.js文件的末尾,不要忘记调用Application对象的notifyScriptLoaded()方法,通知ASP.NET AJAX客户端框架该脚本文件已经顺利加载:
if(typeof(Sys)!=='undefined')Sys.Application.notifyScriptLoaded();
9.3.5 编写页面代码
新建一个ASP.NET页面,在其中添加一个ScriptManager控件并引入必要的脚本文件(PreviewScript.js、PreviewDragDrop.js和ShoppingCart.js)以及9.3.2节中编写的Web Service:
<asp:ScriptManager ID="sm" runat="server">
<Scripts>
<asp:ScriptReference Assembly="Microsoft.Web.Preview"
Name="PreviewScript.js" />
<asp:ScriptReference Assembly="Microsoft.Web.Preview"
Name="PreviewDragDrop.js" />
<asp:ScriptReference Path="ShoppingCart.js" />
</Scripts>
<Services>
<asp:ServiceReference Path="ShoppingService.asmx" />
</Services>
</asp:ScriptManager>
然后编写购物车的相关HTML。其中id为shoppingCart的<div />表示购物车元素。id为btnOrder的<input />表示提交订单的按钮:
<div id="shoppingCartContainer">
<div><strong>Shopping Cart</strong></div>
<div id="shoppingCart">Drop Products Here...</div>
<input id="btnOrder" type="button" value="Order"
onclick="return btnOrder_onclick()" />
</div>
网站中产品,即可拖动元素的容器相关的HTML非常简单——一个<div />而已。随后我们将在程序运行时根据Web Service返回的商品集合动态创建容器中的各个表示商品的元素:
<h1>Dflying's Books</h1>
<div id="productContainer"></div>
示例程序中购物车和商品列表相关的CSS Class定义如下:
#shoppingCartContainer
{
float: right;
width: 220px;
border: 1px solid black;
margin: 3px;
}
#productContainer div
{
width: 250px;
padding: 3px;
margin: 3px;
border: 1px solid #666;
background-color: #fff;
cursor: move;
}
9.3.6 通过Web Service取得商品并显示在页面中
在客户端应用程序初始化之后,我们需要异步调用服务器端Web Service得到商店中产品的信息,并顺便为购物车添加ShoppingCartBehavior行为:
function pageLoad(sender, args) {
// 调用Web Service得到商店中的产品集合
ShoppingService.GetProducts(onProductsGot);
// 为购物车添加ShoppingCartBehavior行为
$create(
Dflying.ShoppingCartBehavior,
{"name": "myShoppingCartBehavior"},
null,
null,
$get("shoppingCart")
);
}
在onProductsGot()回调函数中,我们要根据返回的商品集合动态创建容器中的各个表示商品的元素,并为其一一添加DraggableProductBehavior行为:
function onProductsGot(result) {
// 获取显示各个产品的容器
var productContainer = $get("productContainer");
// 遍历服务器端返回的产品集合
for (var index = 0; index < result.length; ++ index) {
// 当前产品
var thisProduct = result[index];
// 根据该产品信息创建DOM元素,并添加到产品容器中
var productElem = document.createElement("div");
productElem.innerHTML = thisProduct.Name + " - RMB: "
+ thisProduct.Price;
productContainer.appendChild(productElem);
// 为该产品添加DraggableProductBehavior行为
$create(
Dflying.DraggableProductBehavior,
{"product": thisProduct}, // 设置product属性
null,
null,
productElem
);
}
}
9.3.7 将购物车中的商品提交回Web Service处理
当用户点击购物车中的“Order”按钮时,我们需要把购物车中当前的产品信息(包括Id和数量)传回服务器端进行处理。
“Order”按钮的click事件的处理函数如下。其中首先使用Sys.UI.Behavior.getBehaviorByName()方法得到购物车所附加的ShoppingCartBehavior行为,然后取得当前购物车中各个产品的Id和数量,最后将这部分信息回传给Web Service进行处理:
function btnOrder_onclick() {
// 得到购物车所附加的ShoppingCartBehavior行为
var shoppingCartBehavior = Sys.UI.Behavior.getBehaviorByName(
$get("shoppingCart"),
"myShoppingCartBehavior"
);
// 取得当前购物车中各个产品的Id和数量
var productsToBeOrdered =
shoppingCartBehavior.getProductsToBeOrdered();
// 调用Web Service处理订单
ShoppingService.Order(productsToBeOrdered, onOrdered);
}
在productsToBeOrdered()回调函数中,我们只是简单地使用alert()方法将服务器端的响应提示给用户:
function onOrdered(result) {
alert(result);
}
这样即完成了本示例程序的编写。运行该程序并尝试用这种崭新的拖放方式进行购物,你将看到如图9-6、图9-7、图9-8和图9-9中所示的界面。这种简单直观的购物体验是不是很令人赞叹呢?
在本示例程序中,我们完整地演示了ASP.NET AJAX客户端拖放框架支持的自定义拖放效果的强大功能,以及IDragSource和IDropTarget两个重要接口在拖放全过程中提供的完善的可定制能力。在实际应用中,我们完全可以简单地通过实现这两个接口来实现那些用户梦寐以求的炫目的拖放效果。