做為微軟最新技術應用的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);


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