Silverlight 4 帶來印表、提權、WebCam 等豐富能力,但業務開發核心痛點仍是:**如何將伺服器端業務邏輯與資料,安全、高效地投射到客戶端?** WCF RIA Services (舊稱 .NET RIA Services) 正是為此而生。本文以 KharaPOS (銷售點系統) 為例,展示 RIA Services 如何自然承載 Smart UI / Transaction Script / Domain Model / Application Service Layer / Bounded Context 五大企業應用架構模式 (參考 Fowler《企業應用架構模式》)。

Product、CatalogEntry、BusinessUnit、Manufacturer。「把邏輯寫在 UI 裡」聽起來是反模式,但對「查詢/編輯字典表」極其高效。RIA Services + VS 設計器 5 步驟產出可編輯 DataGrid:
ADO.NET Entity Data Model (Database First)。DomainDataSource + DataGrid。this.categoryDomainDataSource.SubmitChanges();適用:簡單管理介面 (產品目錄、員工字典)。建議:僅用於低複雜度 CRUD,避免邏輯外洩。
當業務涉及多實體協調 (如「為業務單元建立目錄條目」),UI 直接操作 CatalogEntry 過於繁瑣且易出錯。解法:在 Domain Service 暴露粗粒度方法:
public void CatalogProductForBusinessUnit(Product product, int businessUnitId)
{
var entry = ObjectContext.CreateObject();
entry.BusinessUnitId = businessUnitId;
entry.ProductId = product.Id;
entry.DateAdded = DateTime.Now;
ObjectContext.CatalogEntries.AddObject(entry);
ObjectContext.SaveChanges();
}
context.CatalogProductForBusinessUnit(product, buId); context.SubmitChanges();CatalogEntry 給客戶端,強制走腳本,杜絕邏輯碎片化。進階技巧:自定義服務方法僅能接受**一個實體參數**,需傳多實體時,可用 HttpContext.Current.Session 暫存:
public void StoreBusinessUnit(BusinessUnit bu)
=> HttpContext.Current.Session[typeof(BusinessUnit).FullName + bu.Id] = bu;
public void CatalogProductForBusinessUnit(Product product, int businessUnitId)
{
var bu = (BusinessUnit)HttpContext.Current.Session[typeof(BusinessUnit).FullName + businessUnitId];
// 使用 bu...
}
當規則複雜 (如「依歷史銷售比例自動選擇季節性產品」),Transaction Script 會淪為巨大的 if-else 堆砌。轉向 Rich Domain Model:將邏輯封裝於實體內部。
public partial class BusinessUnit
{
public void SelectSeasonalProductsForBusinessUnit(DateTime seasonStart, DateTime seasonEnd)
{
// 1. 計算季度總銷售額
var totalSales = (from s in Sales
where s.DateOfSale > seasonStart && s.DateOfSale < seasonEnd
select s.LineItems.Sum(l => l.Cost)).Sum();
// 2. 取得該業務單元涉及的廠商
var manufacturers = Catalogs.Select(c => c.Product.Manufacturer).Distinct();
// 3. 廠商分組統計銷售佔比
var salesByMfr = (from s in Sales
where s.DateOfSale > seasonStart && s.DateOfSale < seasonEnd
from li in s.LineItems
join m in manufacturers on li.Product.ManufacturerId equals m.Id
select new { Manufacturer = m, Amount = li.Cost })
.GroupBy(x => x.Manufacturer);
// 4. 依佔比決定目錄項目
foreach (var g in salesByMfr)
SelectCatalogItemsBasedOnPercentage(g.Key, g.Sum(x => x.Amount) / totalSales);
}
private void SelectCatalogItemsBasedOnPercentage(Manufacturer m, decimal pct) { /* ... */ }
}
bu.SelectSeasonalProductsForBusinessUnit(start, end); context.SubmitChanges();若不想讓客戶端直接操作領域模型 (保護不變量、隱藏依賴),在 Domain Service 再包一層薄外觀:
// Domain Service (Application Service Layer)
public void ExecuteSeasonalSelection(int businessUnitId, DateTime start, DateTime end)
{
var bu = ObjectContext.BusinessUnits.Single(b => b.Id == businessUnitId);
bu.SelectSeasonalProductsForBusinessUnit(start, end); // 委派給領域模型
ObjectContext.SaveChanges();
}
ExecuteSeasonalSelection,不知內部領域結構。KharaPOS 同時擁有 Catalog Context (目錄) 與 Sales Context (銷售),共享 Product/BusinessUnit/LineItem 等實體,但規則不同:
Sale/LineItem 為**唯讀**參考。對應 RIA Services:建立兩個 DomainService (CatalogDomainService, SalesDomainService),各自引用對應 EF Model,共享實體類放在共用組件 (或 shared.cs)。
| 模式 | 適用場景 | RIA 實現關鍵 | 邏輯執行位置 |
|---|---|---|---|
| Smart UI | 簡單字典/主檔維護 | DomainDataSource + DataGrid | 客戶端 (UI 代碼) |
| Transaction Script | 多實體協調、伺服器端強控制 | DomainService 公開方法、Session 暫存 | 伺服器端 |
| Domain Model | 複雜規則、頻繁變更、需封裝 | partial class + shared.cs、虛擬屬性 | 客戶端 (提交時持久化) |
| Application Service Layer | 對外 API 穩定、隱藏領域細節 | DomainService 門面方法委派領域 | 伺服器端 |
| Bounded Context | 大系統多子域、共享實體規則異 | 多 DomainService + 共用實體組件 | 視具體模式而定 |
RIA Services 讓從 Smart UI 到 Bounded Context 的演進極為平滑,**但也正因太容易,團隊易陷入「Smart UI 滿天飛」的成功陷阱** (Brad Abrams)。架構師需把關:依複雜度選擇適當模式,嚴守邊界,守護領域純粹性。KharaPOS 原始碼開源,歡迎實地觀摩!