做爲微軟最新技術應用的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()方法:
{
DinnerNow.ShoppingCartService.SubmitOrder(
submitorder_onSuccess,service_onError,null);
}
因爲上面的代碼最終會去調用ShoppingCartService.cs(位於當前項目的Code文件夾下)中的同名方法:
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文件):
[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)這行代碼,而這行代碼要執行的是下面的方法:
{
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 ,這個文件中的頭部是這樣聲明的:
Factory="System.ServiceModel.Activation.WorkflowServiceHostFactory"
Service="DinnerNow.OrderProcess.ProcessOrder" %>
其中的Factory="System.ServiceModel.Activation.WorkflowServiceHostFactory"會綁定到一個工
作流,且這個工作流會有持久化訪問數據庫的屬性.當然我們可以在web.config(位於這個項目中)找到下面的內容:
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 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 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);
所執行的操作如下:
{
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
{
DinnerNow.Business.OrderProcessing service = new DinnerNow.Business.OrderProcessing();
Order = service.GetRestaurantOrders(orderID);
}
而上面的GetRestaurantOrders(orderID)最終會調用下面的代碼(DinnerNow.Business\OrderProcessing.cs):
{
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,該方法的內容如下:
{
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請求的流程.而這些內容會在下一篇文章中進行介紹.
今天的內容就先告一段落,大家如果有興趣,歡迎在回覆中進行討論:)