天天看點

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 { get; set; }

    public Guid RestaurantId { get; set; }

    public Guid MenuItemId { get; set; }

    public string MenuItemName { get; set; }

    public string MenuItemImageLocation { get; set; }

    public int Quantity { get; set; }

    public decimal UnitCost { get; set; }

    public string Status { get; set; }

    public DateTime StatusUpdatedTime { get; set; }

    public Guid WorkflowId { get; set; }

    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);

   }

   public Guid orderID

           return ((System.Guid)(base.GetValue(DinnerNow.OrderProcess.ProcessOrder.orderIDProperty)));

           base.SetValue(DinnerNow.OrderProcess.ProcessOrder.orderIDProperty, value);

DinnerNow中的Work Flow應用(上) --- 訂單流程
DinnerNow中的Work Flow應用(上) --- 訂單流程

    可以看到這裡使用了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)]

         get

           return ((DinnerNow.Business.Data.Order)(base.GetValue(SaveOrderActivity.IncomingOrderProperty)));

           base.SetValue(SaveOrderActivity.IncomingOrderProperty, value);

   [CategoryAttribute("Parameters")]

           return ((System.Guid)(base.GetValue(SaveOrderActivity.orderIDProperty)));

           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方式的資料表操作,如下: 

    //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);

繼續閱讀