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), 在"訂單"這一應用場景中的設計思路:)

       首先請大家看一下這張圖,它標明瞭在定單這一業務流程中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 { getset; }
    [DataMember]
    
public Guid RestaurantId { getset; }
    [DataMember]
    
public Guid MenuItemId { getset; }
    [DataMember]
    
public string MenuItemName { getset; }
    [DataMember]
    
public string MenuItemImageLocation { getset; }
    [DataMember]
    
public int Quantity { getset; }
    [DataMember]
    
public decimal UnitCost { getset; }
    [DataMember]
    
public string Status { getset; }
    [DataMember]
    
public DateTime StatusUpdatedTime { getset; }
    [DataMember]
    
public Guid WorkflowId { getset; }
    [DataMember]
    
public DateTime Eta { getset; }
}

  這個類中的字段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 { getset; }

   [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(04),
            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[
0as SendActivity).ParameterBindings[0].Value = e.InstanceData as RestaurantOrder;
    (container.Activities[
0as SendActivity).ParameterBindings[1].Value = new Dictionary<string,string>((container.Activities[1as ReceiveActivity).Context);
}

    看來它是要將要請求發送的數據(RestaurantOrderContainer提供)綁定到SendActivity上,它包括一個RestaurantOrder,和一個活動的上下文信息.好的,這裏有必要介紹一下SendActivity, 它是在.net 3.5中引入的發送Soap請求的組件,有了它就可以發送服務請求到指定的svc(服務)上並可獲取執行該服務的返回結果.

  而這什麼要使用它,這裏不妨對下一篇文章中的內容做一下介紹:該SendActivity將會調用OrderUpdateService.svc服務,而這個SVC恰恰又是一個狀態機工作流,它主要執行定單狀態的流轉更新操作.所以DinnerNow實現的是一個從順序工作流向另一個狀態機工作流發送SOAP請求的流程.而這些內容會在下一篇文章中進行介紹.  

  今天的內容就先告一段落,大家如果有興趣,歡迎在回覆中進行討論:)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章