做为微软最新技术应用的DEMO。dinnernow使用了: IIS7, ASP.NET Ajax Extensions, LINQ, WCF, WF,WPF,Windows PowerShell, Card Space以及 .NET Compact Framework. 本文将会继续订餐流程,来讨论关于WF(Windows Work Flow Foundation), 在"订单"这一应用场景中的设计思路:)
首先请大家看一下这张图,它标明了在定单这一业务流程中WF在DinnerNow架构中所实际执行的方法顺序及所处位置.
继续上一篇中的选餐页面,当我们选择了可口的饭菜之后,就要去进入定单及付费流程了.请单击选餐页面中的checkout按钮,这时页面会跳转了checkout.aspx页。当我们输入了Delivery Address的相关信息并提交信息后,页面会继续停留在当前页上并要求我们继续输入Payment Option,直到再次提交后,就会看到下面的页面了:
当我们确认输入的相关信息后,点击bringmymeal按钮后,就开始创建相应的订单信息了.下面我们就开始今天的内容:)
首先请用VS2008打开下面两个解决方案:
安装目录下\solution\DinnerNow - Web\DinnerNow - Web.sln
\solution\DinnerNow - ServicePortfolio2\DinnerNow - ServicePortfolio2.sln
先切换到DinnerNow - Web.sln下的DinnerNow.WebUX项目中的check.aspx文件,找到SubmitOrder()方法:
function
SubmitOrder()
{
DinnerNow.ShoppingCartService.SubmitOrder(
submitorder_onSuccess,service_onError,
null
);
}
因为上面的代码最终会去调用ShoppingCartService.cs(位于当前项目的Code文件夹下)中的同名方法:
[OperationContract]
public
void
SubmitOrder()
{
ShoppingCartDataSource datasource
=
(ShoppingCartDataSource)HttpContext.Current.Session[StateKeys.ShoppingCart];
Order order
=
(Order)HttpContext.Current.Session[StateKeys.Order];
//
获取刚才输入的订单信息
order.SubmittedDate
=
DateTime.Now;
order.OrderItems
=
(from oi
in
shoppingCart.Items
select
new
OrderItem()
{
Eta
=
DateTime.Now.AddMinutes(oi.DeliveryTime),
MenuItemId
=
new
Guid(oi.MenuItemIdentifier),
MenuItemName
=
oi.MenuItemName,
Quantity
=
oi.Quantity,
RestaurantId
=
new
Guid(oi.RestaurantIdentifier),
RestaurantName
=
oi.RestaurantName,
Status
=
"
Ordered
"
,
StatusUpdatedTime
=
DateTime.Now,
UnitCost
=
oi.Price
}).ToArray
<
DinnerNow.WebUX.OrderProcessingService.OrderItem
>
();
order.Total
=
(from oi
in
shoppingCart.Items
select
new
{ orderAmount
=
oi.Price
*
oi.Quantity }).Sum(o
=>
o.orderAmount);
datasource.SubmitOrder(order);
HttpContext.Current.Session[StateKeys.ShoppingCart]
=
null
;
}
上面代码中的HttpContext.Current.Session[StateKeys.Order]里面存储的就是刚才我们输入的相关订单信息(我个人认为这里使用Session有些不妥).
另外上面的OrderItems的类型声明如下(来自DinnerNow - ServicePortfolio2.sln下的DinnerNow.Business
项目中的Data\Order.cs文件):
[DataContract]
[Serializable]
public
class
OrderItem
{
[DataMember]
public
string
RestaurantName {
get
;
set
; }
[DataMember]
public
Guid RestaurantId {
get
;
set
; }
[DataMember]
public
Guid MenuItemId {
get
;
set
; }
[DataMember]
public
string
MenuItemName {
get
;
set
; }
[DataMember]
public
string
MenuItemImageLocation {
get
;
set
; }
[DataMember]
public
int
Quantity {
get
;
set
; }
[DataMember]
public
decimal
UnitCost {
get
;
set
; }
[DataMember]
public
string
Status {
get
;
set
; }
[DataMember]
public
DateTime StatusUpdatedTime {
get
;
set
; }
[DataMember]
public
Guid WorkflowId {
get
;
set
; }
[DataMember]
public
DateTime Eta {
get
;
set
; }
}
这个类中的字段WorkflowId保存是订单所使用的工作流id, 这个值在订单创建时不会被赋值,但当订单状态更新时会被赋值.而相应的工作流的状态会持久化到SQLSERVER数据库中(会在接下来的内容中加以说明)。
目前我们还是要再回到 SubmitOrder() 代码段中,找到datasource.SubmitOrder(order)这行代码,而这行代码要执行的是下面的方法:
public
void
SubmitOrder(Order order)
{
using
(DinnerNow.WebUX.OrderProcessingService.ProcessOrderClient proxy
=
new
ProcessOrderClient())
{
proxy.Processorder(order);
//
该行代码将来执行绑定到SVC服务上的工作流
}
this
.items
=
new
List
<
ShoppingCartItem
>
();
}
上面代码中的ProcessOrderClient类实际上添加SVC引用时系统自动生成的类.
而引用的路径即: http://localhost/dinnernow/service/OrderProcess.svc
该文件位于ServicePortfolio2.sln下的DinnerNow.ServiceHost/OrderProcess.svc
所以这里我们要切换到ServicePortfolio2.sln解决方案下.并打开DinnerNow.ServiceHost项目中的
OrderProcess.svc ,这个文件中的头部是这样声明的:
<%
@ ServiceHost Language
=
"
C#
"
Debug
=
"
true
"
Factory
=
"
System.ServiceModel.Activation.WorkflowServiceHostFactory
"
Service
=
"
DinnerNow.OrderProcess.ProcessOrder
"
%>
其中的Factory="System.ServiceModel.Activation.WorkflowServiceHostFactory"会绑定到一个工
作流,且这个工作流会有持久化访问数据库的属性.当然我们可以在web.config(位于这个项目中)找到下面的内容:
<
workflowRuntime name
=
"
WorkflowServiceHostRuntime
"
validateOnCreate
=
"
true
"
enablePerformanceCounters
=
"
true
"
>
<
services
>
<
add type
=
"
System.Workflow.Runtime.Hosting.SharedConnectionWorkflowCommitWorkBatchService, System.Workflow.Runtime, Version=3.0.00000.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35
"
/>
<
add type
=
"
System.Workflow.Runtime.Hosting.SqlWorkflowPersistenceService, System.Workflow.Runtime, Version=3.0.00000.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35
"
/>
<
add type
=
"
System.Workflow.Runtime.Tracking.SqlTrackingService,System.Workflow.Runtime,Version=3.0.00000.0, Culture=neutral,PublicKeyToken=31bf3856ad364e35
"
/>
</
services
>
<
commonParameters
>
<
add name
=
"
ConnectionString
"
value
=
"
Data Source=daizhj\daizhj;User ID=sa;Password=123123;Initial Catalog=dinnernow;Pooling=true
"
/>
</
commonParameters
>
</
workflowRuntime
>
而属性ConnectionString就是持久化时用到的数据库链接串.
看到这里,本文的内容将会转到WF上了,因为上面的C#代码中的proxy.Processorder(order)方法在是WF中绑定的.按照上面的SVC声明,找到ProcessOrder.xoml文件(位于当前解决方案中DinnerNow.OrderProcess项目下).
这里我们通过双击该文件查看其工作流的大致流程:
我们在第一个Activity上击鼠标右键在菜单中找到"属性",如下图:
其中的ServiceOperationInfo中的绑定属性就是该Activity所实现的操作,这里写的是:
DinnerNow.OrderProcess.IProcessOrder.Processorder,看来对于外部发来的SOAP请求要先到达这里被活动处理.
通过上图中的属性我们可以看出这个Activity是一个ReceiveActivity, 这个组件是.net 3.5 才引入的,如下图所示:
该组件主要用于处理Web服务请求.当然有进就有出,在这个顺序工作流中还使用了另外一个新引入的组件:SendActivity, 将会在后面加以说明:)
即然找到了请求的操作,下一步就要好好看一下这个工作流了,因为顺序工作流本地比较好理解,这里就直接一个一个activity加以说明了.
首先请加开Workflow\ProcessOrder.xoml.cs文件,它的前半部分代码内容如下:
public
partial
class
ProcessOrder : SequentialWorkflowActivity
{
public
static
DependencyProperty incomingOrderProperty
=
DependencyProperty.Register(
"
incomingOrder
"
,
typeof
(DinnerNow.Business.Data.Order),
typeof
(DinnerNow.OrderProcess.ProcessOrder));
public
static
DependencyProperty orderIDProperty
=
DependencyProperty.Register(
"
orderID
"
,
typeof
(System.Guid),
typeof
(DinnerNow.OrderProcess.ProcessOrder));
public
RestaurantOrder[] Order {
get
;
set
; }
[DesignerSerializationVisibilityAttribute(DesignerSerializationVisibility.Visible)]
[BrowsableAttribute(
true
)]
public
DinnerNow.Business.Data.Order IncomingOrder
{
get
{
return
((DinnerNow.Business.Data.Order)(
base
.GetValue(DinnerNow.OrderProcess.ProcessOrder.incomingOrderProperty)));
}
set
{
base
.SetValue(DinnerNow.OrderProcess.ProcessOrder.incomingOrderProperty, value);
}
}
[DesignerSerializationVisibilityAttribute(DesignerSerializationVisibility.Visible)]
[BrowsableAttribute(
true
)]
public
Guid orderID
{
get
{
return
((System.Guid)(
base
.GetValue(DinnerNow.OrderProcess.ProcessOrder.orderIDProperty)));
}
set
{
base
.SetValue(DinnerNow.OrderProcess.ProcessOrder.orderIDProperty, value);
}
}
}
可以看到这里使用了DependencyProperty(依赖属性), 有关这方面的内容可以参见这篇文章:
《WF编程》系列之38 - 依赖属性
这里使用它是为了活动数据绑定(在activity之间进行数据传递),因为在上面提到过,客户端网站要提到定单信息过来,而订单的数据将会绑定到这个工作流文件的IncomingOrder属性上.
现在有了数据,该执行创建定单的操作了,而这个任务交给了下一个Activity---"saveOrderActivity1", 我们可以从该活动的类型上看出它是一个SaveOrderActivity(DinnerNow.WorkflowActivities项目下),而这个活动类型的代码段如下:
public
partial
class
SaveOrderActivity: Activity
{
public
static
DependencyProperty IncomingOrderProperty
=
DependencyProperty.Register(
"
IncomingOrder
"
,
typeof
(DinnerNow.Business.Data.Order),
typeof
(SaveOrderActivity));
public
static
DependencyProperty orderIDProperty
=
DependencyProperty.Register(
"
orderID
"
,
typeof
(System.Guid),
typeof
(SaveOrderActivity));
public
SaveOrderActivity()
{
InitializeComponent();
}
[Description(
"
Customer Order
"
)]
[Browsable(
true
)]
[Category(
"
Order
"
)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
public
DinnerNow.Business.Data.Order IncomingOrder
{
get
{
return
((DinnerNow.Business.Data.Order)(
base
.GetValue(SaveOrderActivity.IncomingOrderProperty)));
}
set
{
base
.SetValue(SaveOrderActivity.IncomingOrderProperty, value);
}
}
[DesignerSerializationVisibilityAttribute(DesignerSerializationVisibility.Visible)]
[BrowsableAttribute(
true
)]
[CategoryAttribute(
"
Parameters
"
)]
public
Guid orderID
{
get
{
return
((System.Guid)(
base
.GetValue(SaveOrderActivity.orderIDProperty)));
}
set
{
base
.SetValue(SaveOrderActivity.orderIDProperty, value);
}
}
protected
override
ActivityExecutionStatus Execute(ActivityExecutionContext executionContext)
{
OrderService service
=
new
OrderService();
orderID
=
service.CreateOrder(IncomingOrder);
return
ActivityExecutionStatus.Closed;
}
}
当然这里也用了DependencyProperty来接收从上一个活动(ReceiveActivity)传递来的参数,并对其进行操作。清注意上面代码段中的Execute方法,它即是用来创建订单的代码.该方法中的语句:
service.CreateOrder(IncomingOrder);
所执行的操作如下:
public
Guid CreateOrder(DinnerNow.Business.Data.Order newOrder)
{
using
(Business.OrderProcessing op
=
new
DinnerNow.Business.OrderProcessing())
{
return
op.CreateOrder(newOrder);
}
}
当然上面代码中的OrderProcessing是对订单(CRUD)服务的外部封装.我个人认为这种封装还是很好
必要的(绝不是脱裤子放屁,多此一举),它会让业务操作和数据(库)操作相分离,因为上面代码中的:
op.CreateOrder(newOrder)
将会是LINQ方式的数据表操作,如下:
public
Guid CreateOrder(DinnerNow.Business.Data.Order newOrder)
{
//
Check customer exists in database
var customerId
=
(from c
in
db.Customers
where
c.UserName
==
newOrder.CustomerUserName
select c.CustomerId).FirstOrDefault();
if
(customerId
==
Guid.Empty)
{
//
need to add a new customer OR is this a bug?
customerId
=
Guid.NewGuid();
db.Customers.InsertOnSubmit(
new
DinnerNow.Data.Customer() { CustomerId
=
customerId, UserName
=
newOrder.CustomerUserName, UserId
=
Guid.Empty });
}
var orderId
=
(newOrder.OrderId
==
Guid.Empty
?
Guid.NewGuid() : newOrder.OrderId);
var order
=
new
DinnerNow.Data.Order()
{
City
=
newOrder.City,
ContactTelephone
=
newOrder.ContactTelephone,
CustomerID
=
customerId,
OrderId
=
orderId,
OrderPayments
=
new
DinnerNow.Data.OrderPayment()
{
Address
=
newOrder.Payment.Address,
City
=
newOrder.Payment.City,
Country
=
newOrder.Payment.Country,
CreditCardNumber
=
newOrder.Payment.CardNumber.Substring(
0
,
4
),
CreditCardType
=
newOrder.Payment.CreditCardType,
ExpirationDate
=
newOrder.Payment.ExpirationDate,
PostalCode
=
newOrder.Payment.PostalCode,
State
=
newOrder.Payment.State,
NameOnCard
=
newOrder.Payment.NameOnCard,
OrderID
=
orderId,
PaymentID
=
Guid.NewGuid()
},
PostalCode
=
newOrder.PostalCode,
State
=
newOrder.State,
StreetAddress
=
newOrder.StreetAddress,
SubmittedDate
=
newOrder.SubmittedDate,
Total
=
newOrder.Total
};
order.OrderDetails.AddRange(from od
in
newOrder.OrderItems
select
new
DinnerNow.Data.OrderDetail()
{
OrderDetailId
=
Guid.NewGuid(),
OrderId
=
orderId,
MenuItemId
=
od.MenuItemId,
Quantity
=
od.Quantity,
RestaurantId
=
od.RestaurantId,
UnitCost
=
od.UnitCost,
Status
=
"
New Order
"
,
StatusUpdatedTime
=
DateTime.Now,
ETA
=
od.Eta
});
db.Orders.InsertOnSubmit(order);
db.SubmitChanges();
return
order.OrderId;
}
上面代码主要是向数据库中的Order(订单表),OrderDetail(订单名称表)两个表中插入记录.同时将插入订单记录的编号返回给依赖属性Guid orderID.
而接下来的工作就是要去获取该订单与餐馆绑定关系信息了.这个任务被下一个Activity所执行,即:
CreateRestaurantOrdersCode
private
void
CreateRestaurantOrders(
object
sender, EventArgs e)
{
DinnerNow.Business.OrderProcessing service
=
new
DinnerNow.Business.OrderProcessing();
Order
=
service.GetRestaurantOrders(orderID);
}
而上面的GetRestaurantOrders(orderID)最终会调用下面的代码(DinnerNow.Business\OrderProcessing.cs):
public
DinnerNow.Business.Data.RestaurantOrder[] GetRestaurantOrders(Guid orderID)
{
var ordersByRestaurant
=
(from od
in
db.OrderDetails
where
od.OrderId
==
orderID
select
new
DinnerNow.Business.Data.RestaurantOrder()
{
OrderId
=
od.OrderId,
RestaurantId
=
od.RestaurantId
}).Distinct();
return
ordersByRestaurant.ToArray();
}
因为代码很简单,就不多说了:)
执行完上述操作后,就需要将新创建的订单转入业务流程了,这时会根据业务操作的不断推进,从而使该订单状态不断得到更新,但因为这些后续操作不是在几秒或几分钟分就要完成了,它要在餐馆与订餐人中的不断交互中推进,就像是在淘宝买东西差不多,有的交易可以要十天半个月甚至更长时间内才会完成.所以在工作流中使用的持久化,也就是前面所说的数据库存储:)
好了,完成了这个ACTIVITY之后,工作流的下一站就是replicatorActivity1,我们可以在该活动的属性页中找到它的相关设置如下图:
可以看出它的执行方式是: parallel(并行)初始化childData: Activity=ProcessOrder, Path=Order
而它的ChildInitialize方法(构造方法)为:ChildInit,该方法的内容如下:
private
void
ChildInit(
object
sender, ReplicatorChildEventArgs e)
{
RestaurantOrderContainer container
=
e.Activity
as
RestaurantOrderContainer;
(container.Activities[
0
]
as
SendActivity).ParameterBindings[
0
].Value
=
e.InstanceData
as
RestaurantOrder;
(container.Activities[
0
]
as
SendActivity).ParameterBindings[
1
].Value
=
new
Dictionary
<
string
,
string
>
((container.Activities[
1
]
as
ReceiveActivity).Context);
}
看来它是要将要请求发送的数据(RestaurantOrderContainer提供)绑定到SendActivity上,它包括一个RestaurantOrder,和一个活动的上下文信息.好的,这里有必要介绍一下SendActivity, 它是在.net 3.5中引入的发送Soap请求的组件,有了它就可以发送服务请求到指定的svc(服务)上并可获取执行该服务的返回结果.
而这什么要使用它,这里不妨对下一篇文章中的内容做一下介绍:该SendActivity将会调用OrderUpdateService.svc服务,而这个SVC恰恰又是一个状态机工作流,它主要执行定单状态的流转更新操作.所以DinnerNow实现的是一个从顺序工作流向另一个状态机工作流发送SOAP请求的流程.而这些内容会在下一篇文章中进行介绍.
今天的内容就先告一段落,大家如果有兴趣,欢迎在回复中进行讨论:)