DinnerNow中的Work Flow应用(上) --- 订单流程

        作为微软最新技术应用的DEMO。dinnernow使用了: IIS7, ASP.NET Ajax Extensions, LINQ, WCF, WF,WPF,Windows PowerShell, Card Space以及 .NET Compact Framework. 本文将会继续订餐流程,来讨论关于WF(Windows Work Flow Foundation), 在"订单"这一应用场景中的设计思路:)
html

       首先请你们看一下这张图,它标明了在定单这一业务流程中WF在DinnerNow架构中所实际执行的方法顺序及所处位置.


       继续上一篇中的选餐页面,当咱们选择了可口的饭菜以后,就要去进入定单及付费流程了.请单击选餐页面中的checkout按钮,这时页面会跳转了checkout.aspx页。当咱们输入了Delivery Address的相关信息并提交信息后,页面会继续停留在当前页上并要求咱们继续输入Payment Option,直到再次提交后,就会看到下面的页面了:
   

当咱们确认输入的相关信息后,点击bringmymeal按钮后,就开始建立相应的订单信息了.下面咱们就开始今天的内容:)
web

     首先请用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有些不妥).ide

  另外上面的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数据库中(会在接下来的内容中加以说明)。ui

  目前咱们仍是要再回到 SubmitOrder() 代码段中,找到datasource.SubmitOrder(order)这行代码,而这行代码要执行的是下面的方法:this

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
spa

       因此这里咱们要切换到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请求的流程.而这些内容会在下一篇文章中进行介绍.  

  今天的内容就先告一段落,你们若是有兴趣,欢迎在回复中进行讨论:)