继上一篇(初尝dinnernow)之后,通过配置并驱动起了web应用。从今天起本系列文章将以一个购物流程为主线,介绍一下DinnerNow是如何使用WCF,LINQ,ASP.NET Ajax Extensions等技术来架构应用的。
首先请用VS2008打开下面两个解决方案:
安装目录下\solution\DinnerNow - Web\DinnerNow - Web.sln
\solution\DinnerNow - ServicePortfolio2\DinnerNow - ServicePortfolio2.sln
这是关于DinnerNow - Web.sln中项目的说明:
DinnerNow.WebUX 项目包括表示层(UI)的应用逻辑,WCF客户端调用的CS文件(CODE文件夹下)
DinnerNow.Web 项目则提供了一些简单的变量声明和定义,相关的CS代码并不多.
Microsoft.DPE.Samples.CardSpace 是一些关于Card Space数据访问和操作的封装和实例代码.
因此目前网站上的主要代码和功能实现都集中在了DinnerNow.WebUX这个项目.
为了完整的演示一个购买流程,本人将会以执行页面为单位.逐个说明相关页面的程序执行逻辑和功能实现.
在介绍之前,请大家先看一下DinnerNow的系统架构图.相信这会对我们从整体上把握这个产品提供一个切入点.相关图示如下:
首先运行网站的首页http://localhost/dinnernow/default.aspx,如下图:
上图中红框标记部分的部分页面页容如下(SearchBar.ascx):
<
table border
=
"
0
"
cellspacing
=
"
2
"
cellpadding
=
"
2
"
>
<
tr
>
<
td align
=
"
right
"
nowrap
=
"
nowrap
"
class
=
"
boldWhite
"
>
Food Type
</
td
>
<
td align
=
"
left
"
>
<
asp:ObjectDataSource ID
=
"
RestaurantCategoryDataSource
"
runat
=
"
server
"
SelectMethod
=
"
SelectAll
"
TypeName
=
"
DinnerNow.RestaurantCategoryDataSource
"
/>
<
asp:DropDownList ID
=
"
restaurantCategoryList
"
runat
=
"
server
"
DataSourceID
=
"
RestaurantCategoryDataSource
"
DataTextField
=
"
Description
"
DataValueField
=
"
RestaurantId
"
/>
</
td
>
</
tr
>
</
table
>
<
table border
=
"
0
"
cellspacing
=
"
2
"
cellpadding
=
"
2
"
>
<
tr
>
<
td align
=
"
right
"
class
=
"
boldWhite
"
>
Meal
</
td
>
<
td align
=
"
left
"
>
<
asp:ObjectDataSource ID
=
"
MenuTypeDataSource
"
runat
=
"
server
"
SelectMethod
=
"
SelectAll
"
TypeName
=
"
DinnerNow.MenuTypeDataSource
"
/>
<
asp:DropDownList ID
=
"
menuTypeList
"
runat
=
"
server
"
DataSourceID
=
"
MenuTypeDataSource
"
DataTextField
=
"
MenuTypeName
"
DataValueField
=
"
MenuTypeName
"
/>
</
td
>
</
tr
>
</
table
>
可以看出菜单下拉框选项使用ObjectDataSource方式进行加载,而页面代码中的下列两条语句是所加载类型的说明:
TypeName="DinnerNow.RestaurantCategoryDataSource'
TypeName="DinnerNow.MenuTypeDataSource"
这两个类型我们可以在下列路径下找到:
DinnerNow.WebUX\Code\DataSources\RestaurantCategoryDataSource.cs
DinnerNow.WebUX\Code\DataSources\MenuTypeDataSource.cs
它们两个的功能就是调用相应的SelectAll方法如下(仅以MenuTypeDataSource.cs为例):
MenuTypeDataSource.cs
public
IEnumerable
<
RestaurantCategory
>
SelectAll()
{
try
{
using (MenuSearchServiceClient client = new MenuSearchServiceClient("WSHttpBinding_IMenuSearchService"))
{
return client.GetRestaurantCategories();
}
}
catch (Exception)
{
//@TODO: Need to put some error handling in here
}
return null;
}
因为代码太简单没什么可说的,下面就根据其所请求的服务绑定项"WSHttpBinding_IMenuSearchService", 在web.config中查找到如下配置节:
<
endpoint address
=
"
http://localhost/DinnerNow/service/MenuSearch.svc
"
binding
=
"
wsHttpBinding
"
bindingConfiguration
=
"
WSHttpBinding_IMenuSearchService
"
contract
=
"
MenuSearchService.IMenuSearchService
"
name
=
"
WSHttpBinding_IMenuSearchService
"
>
<
identity
>
<
servicePrincipalName value
=
"
host
"
/>
</
identity
>
</
endpoint
>
而相关的MenuSearch.svc(执行)文件就是其所引用的服务地址.好的,看清了这一块之后,我们切换到刚才所说的第二个解决方案中(DinnerNow - ServicePortfolio2.sln),看一下这个SVC中是如何执行相应逻辑的:)
在DinnerNow - ServicePortfolio2.sln中的DinnerNow.ServiceHost项目是服务配置站点,我们可从该站点的web.config文件中找出如下内容:
......
<
service behaviorConfiguration
=
"
DinnerNow.Services.MenuSearchServiceBehavior
"
name
=
"
DinnerNow.Services.MenuSearchService
"
>
<
endpoint address
=
""
binding
=
"
wsHttpBinding
"
contract
=
"
DinnerNow.Services.IMenuSearchService
"
/>
<
endpoint address
=
"
mex
"
binding
=
"
mexHttpBinding
"
contract
=
"
IMetadataExchange
"
/>
<
endpoint address
=
"
ajax
"
behaviorConfiguration
=
"
DinnerNow.Services.MenuSearchServiceAjax
"
binding
=
"
webHttpBinding
"
bindingConfiguration
=
"
AjaxBinding
"
contract
=
"
DinnerNow.Services.IMenuSearchService
"
/>
</
service
>
......
这里定义了当前服务所使用的contract接口(MenuSearchService)以及所使用的服务MenuSearchService(业务逻辑),
而有关这两部分内容定义如下:
[ServiceContract(Namespace
=
"
DinnerNow.Services
"
)]
public
interface
IMenuSearchService
{
[OperationContract]
[WebGet]
IEnumerable<MenuType> GetMenuTypes();
[OperationContract]
[WebGet]
IEnumerable<RestaurantCategory> GetRestaurantCategories();
[OperationContract]
[WebGet]
IEnumerable<RestaurantHeader> FindRestaurant(string postalCode, string menuType, string restaurantCategoryId, string deadline);
[OperationContract]
[WebGet]
IEnumerable<RestaurantMenuItem> GetMenuItemsForMenu(string restaurantId, string menuType);
}
该接口定义了搜索菜单的数据获取方法,相信大家通过字面就能看出个大概了,所以我就不多说什么了.
下面主要说一下MenuSearchService.cs文件(DinnerNow.Services项目下):
[AspNetCompatibilityRequirements(RequirementsMode
=
AspNetCompatibilityRequirementsMode.Allowed)]
public
class
MenuSearchService : IMenuSearchService
{
IMenuSearchService Members#region IMenuSearchService Members
public IEnumerable<Business.Data.MenuType> GetMenuTypes()
{
Business.Menu menu = new DinnerNow.Business.Menu();
return menu.GetMenuTypes();
}
public IEnumerable<Business.Data.RestaurantCategory> GetRestaurantCategories()
{
Business.Menu menu = new DinnerNow.Business.Menu();
return menu.GetRestaurantCategories();
}
public IEnumerable<DinnerNow.Business.Data.RestaurantHeader> FindRestaurant(string postalCode,
string menuType, string restaurantCategoryId, string deadline)
{
Business.Menu menu = new DinnerNow.Business.Menu();
Trace.Write("");
return menu.FindRestaurant(postalCode, menuType, new Guid(restaurantCategoryId),
int.Parse(deadline,CultureInfo.CurrentCulture));
}
public IEnumerable<DinnerNow.Business.Data.RestaurantMenuItem> GetMenuItemsForMenu(string restaurantId, string menuType)
{
Business.Menu menu = new DinnerNow.Business.Menu();
return menu.GetMenuItemsForMenu(new Guid(restaurantId), menuType);
}
#endregion
}
因为我们在网站客户端调用的是如下方法:
using
(MenuSearchServiceClient client
=
new
MenuSearchServiceClient(
"
WSHttpBinding_IMenuSearchService
"
))
{
return client.GetMenuTypes();
}
所以对这个方法的使用应该就是对菜单类型数据的加载,而相应的方法定义在DinnerNow.Business\Menu.cs文件中:
public
IEnumerable
<
DinnerNow.Business.Data.MenuType
>
GetMenuTypes()
{
var s = (from m in db.Menus
select new DinnerNow.Business.Data.MenuType()
{
MenuTypeName = m.MenuType.Trim()
}).Distinct();
return s.ToList();
}
这里使用了linq to sql来执行数据的操作。可以这么说, DinnerNow的数据访问和操作基本上都是使用LINQ语法完成的.
它的作用相当于如下语句(即找出不重复的菜单类型):
SELECT DISTINCT [t1].[value] AS [MenuTypeName] FROM (SELECT LTRIM(RTRIM([t0].[MenuType])) AS [value]
FROM [dbo].[Menu] AS [t0]) AS [t1]
这样,对首页的整个数据加载过程就完成了,当然页面上的数据查询操作又是如何进行的呢?
下面就来说明一下这方面的业务执行流程:
请再切换回DinnerNow - Web.sln解决方案,还是刚才的那个SearchBar.ascx页面,下面的代码即是完成了搜索提交以及查询操作(详情见注释):
<
asp:ScriptManagerProxy ID
=
"
ScriptManagerProxy1
"
runat
=
"
server
"
>
<
services
>
<
asp:ServiceReference Path
=
"
~/service/MenuSearch.svc/ajax
"
/>
</
services
>
</
asp:ScriptManagerProxy
>
<
script type
=
"
text/javascript
"
>
function searchButton_Click()
{
var DeadLine
=
$
get
(
"
<%= deadlineSelect.ClientID %>
"
).value;
if
(DeadLine
==
"
-1
"
||
DeadLine
==
null
)
{
DeadLine
=
"
90
"
;
}
var MenuType
=
$
get
(
"
<%= menuTypeList.ClientID %>
"
).value.trim();
var PostalCode
=
$
get
(
"
<%= postalCodeTextBox.ClientID %>
"
).value;
var RestaurantCategory
=
$
get
(
"
<%= restaurantCategoryList.ClientID %>
"
).value;
var searchUrl
=
"
search.aspx
"
;
var path
=
document.location.pathname.toLowerCase();
var isInSearchAspx
=
path.length
>=
searchUrl.length
&&
path.substr(path.length
-
searchUrl.length,searchUrl.length)
==
searchUrl;
if
(
!
isInSearchAspx)
//
当前页面是否为搜索页(search.aspx)
{
var href
=
"
search.aspx?PostalCode=
"
+
PostalCode
+
"
&MenuType=
"
+
MenuType
+
"
&RestaurantCategory=
"
+
RestaurantCategory
+
"
&DeadLine=
"
+
DeadLine;
document.location.href
=
href;
//
当不在搜索页面则将查询参数绑定后跳转到搜索页面
}
else
{
var service
=
new
DinnerNow.Services.IMenuSearchService();
//
如果在搜索页面,则调用下面的JS方法来查找相当的记录
service.FindRestaurant(PostalCode, MenuType, RestaurantCategory, DeadLine, restaurantSearch_onSuccess, restaurantSearch_onFailed,
null
);
}
return
false
;
}
function restaurantSearch_onSuccess(result)
//
查询成功
{
if
(
typeof
(onRestaurantSeachSuccess)
!=
"
#ff0000
"
)
{
onRestaurantSeachSuccess(result);
}
}
function restaurantSearch_onFailed(result)
//
查询失败
{
alert(
"
The search has failed
"
);
}
</
script
>
因为使用了ASP.NET Ajax Extensions,所以上面的代码段里的service.FindRestaurant(PostalCode, MenuType, RestaurantCategory, DeadLine, restaurantSearch_onSuccess, restaurantSearch_onFailed, null);
写法很接近于我们习惯的C#。
而实际的JS方法如下:
FindRestaurant:function(postalCode,menuType,restaurantCategoryId,deadline,succeededCallback, failedCallback, userContext) {
///
<param name="postalCode" type="String">
System.String
</param>
///
<param name="menuType" type="String">
System.String
</param>
///
<param name="restaurantCategoryId" type="String">
System.String
</param>
///
<param name="deadline" type="String">
System.String
</param>
///
<param name="succeededCallback" type="Function" optional="true" mayBeNull="true"></param>
///
<param name="failedCallback" type="Function" optional="true" mayBeNull="true"></param>
///
<param name="userContext" optional="true" mayBeNull="true"></param>
return
this
._invoke(
this
._get_path(),
'
FindRestaurant
'
,
true
,{postalCode:postalCode,menuType:menuType,restaurantCategoryId:restaurantCategoryId,deadline:deadline},succeededCallback,failedCallback,userContext); }
上面代码中的_invoke就是完成一个ajax请求的方法.而succeededCallback和succeededCallback方法分别是ajax成功或失败后的回调函数参数,也是本例中的方法restaurantSearch_onSuccess,restaurantSearch_onFailed.
而最终ajax请求会成为对如下方法的调用(DinnerNow.Business\Menu.cs文件中):
public
IEnumerable
<
DinnerNow.Business.Data.RestaurantHeader
>
FindRestaurant(
string
postalCode,
string
menuType, Guid restaurantCategoryId,
int
deadline)
{
var results
=
from r
in
db.Restaurants
join m
in
db.Menus on r.RestaurantId equals m.RestaurantId
where
m.MenuType
==
menuType
&&
r.PostalCode
==
postalCode
&&
r.RestaurantCategoryId
==
restaurantCategoryId
select
new
Business.Data.RestaurantHeader()
{
LogoImageLocation
=
r.LogoImageLocation,
Name
=
r.Name,
RestaurantId
=
r.RestaurantId
};
return
results.ToList();
}
这个LINQ语句相当于如下SQL语句(Restaurant,RestaurantId联表查询):
SELECT [t0].[RestaurantId], [t0].[Name], [t0].[LogoImageLocation] FROM [dbo].[Restaurant] AS [t0]
INNER JOIN [dbo].[Menu] AS [t1] ON [t0].[RestaurantId] = [t1].[RestaurantId]
WHERE ([t1].[MenuType] = @p0) AND ([t0].[PostalCode] = @p1) AND ([t0].[RestaurantCategoryId] = @p2)
在搜索这个地方使用了AJAX,主要是为了UE(用户体验).当然在DinnerNow中还有一些地方如选餐, 支付等也使用了AJAX,相信也是出于这方面的考虑)
说到了这里,今天的内容就要告一段落了.纵观DinnerNow的架构,可以说访问数据库的操作基本上都以LINQ To Sql实现方式。而业务流程(服务)则采用WCF的方式进行封装和调用.而网站上只保留了显示逻辑及AJAX请求操作.这样可以说做到了将数据访问层与业务逻辑层的分离.同时也便于团队开发并进行相应分工。
因为本人认为可以将开发小组成员分为三组:
A组负责数据访问接口和相关数据操作(采用LINQ)
B组负责设计业务流程组织(采用WCF, 后面的购买流程中使用了WWF,将会在下文中详加说明)
C组负责前台程序逻辑设计包括ajax调用等等
当然这种分工的好处是让小组成员的长处都能得到发挥,必定有专攻数据操作访问,也有专攻SOA的.有专功LINQ,也有熟练WCF和WF的。当然这只是我的一面之词,目前也只是猜测,如果大家有什么意见,欢迎在回复中进行讨论:)