2013年9月22日 星期日

MVC搭配HTML5的離線瀏覽功能

Web應用程式的主要限制之一就是連線的穩定性。在HTML5之前,曾經想過挖掘瀏覽器所有的能力,讓Web應用程式能夠像一般Windows Form那樣強大和易於使用,但瀏覽器始終令人感到失望。雖然之前已經出現了一些能夠在瀏覽器上暫存/快取的技術,但這些快取技術的設計初衷並非是為了讓Web應用程式能夠完全的離線(Off-line)執行,令人感到沮喪的是,事實上使用這些技術的Web應用程式非常容易發生一些意想不到的問題,而且使用者會感到難以使用。HTML5試圖藉由離線執行快取(Off-line application cache)的技術來填補瀏覽器能力空缺,該技術更加可靠。

為什麼Web應用程式需要離線執行    
一般來說,PC的Web應用程式即便可以完全離線執行也無法帶來太多好處,因為PC一般來說都是一直連線的,真正要期待的是智慧型手機的Web應用程式能夠從離線應用程式快取技術得到多少好處。    

智慧型手機的普及率在逐年成長,但如果能夠填補掉網路斷線的鴻溝,就能夠讓智慧型手機瀏覽器的Web應用程式對於使用者來說更加方便。    

在某些特殊狀況下,整個應用程式能夠離線執行,就意味著僅需要創建一個跨平台的瀏覽器解決方案,而不需要創建太多原生應用程式。    

試想一下,一位銷售人員需要隨時隨地向他的客戶展示商品型錄。他可以使用任何他想要的電子展示設備,當他曾經瀏覽過商品型錄之後,接下來便可以隨時隨地的離線瀏覽。     應用程式快取技術並不只是在離線狀態才有用武之地。亦可以將應用程式快取作為一個超級快取,用於本機儲存資源,如此一來,便可以加速應用程式啟動。伺服器上更新了的資源可以在後台重新載入,載入完畢之後就替換掉本地舊的資源並更新到正在執行中的應用程式上。這種方式非常適合用於PC的重量級Web應用程式。  

清單檔案    

要使用應用程式快取,並不需要撰寫大量的程式碼,可以在一個簡單的檔案文件中定義需要離線使用的資源,這個檔案被稱之為清單(Manifest)檔案。   

一個簡單的清單檔案內容格式如下:
CACHE MANIFEST
# Version 1.0

CACHE:
/home/index
/content/style.css
/scripts/main.js

NETWORK:
/service/status

FALLBACK:
/logo.png /logo_offline.png
 
   需要CACHE MANIFEST標頭放在清單檔案的第一行。

   以數字符號#開頭的代表是註解。這個通常是用在顯示的修改清單檔案以通知瀏覽器更新快取。譬如說,當你更新了一張圖片但沒有修改圖片的名稱時,這種情境下就非常適用,因為瀏覽器並沒有其它方式可以檢測到伺服器上的圖片已經被更新。

   接下來,清單檔案包含了以下三個段落: CACHE, NETWORK以及FALLBACK。在CACHE段落中你可以指定需要快取的資源。需要一直從伺服器下載的資源(即便是在離線的狀況下)則在NETWORK段落中指定。若有大量的資源需要不斷從伺服器上下載,可以在NETWORK段落中使用*符號表示。在FALLBACK段落中,可以指定在離線狀態下可使用的備用資源。

   清單檔案的格式並不嚴謹。以上介紹的段落是可以任何修改順序的,它們甚至可以在一個清單檔案中多次出現。

   在清單檔案中你可以使用相對路徑或是絕對路徑來定位資源。如果你使用相對路徑,則必須以清單檔案的位置作為參考的基準位置來定位資源。

引用清單檔案

   要將清單檔案綁定到應用程式,需要將manifest屬性添加到html標籤上。每個引用清單檔案的頁面本身會預設為快取。然而,仍是建議在清單檔案中列出你想要快取的資源。若某個頁面沒有在清單檔案中被指定,同時也不曾在瀏線非離線狀態下瀏覽過,則當處於離線狀態下就無法看到這個頁面,這是因為瀏覽器無法得知頁面是否存在本機快取中。
<html manifest="cache.manifest" />

檢查快取狀態

   使用應用程式快取API,可以讓我們檢查應用程式的快取狀態。使用window.applicationCache這個屬性就可以查詢當前快取的狀態。該狀態屬性的值是一個介於0~5的數字,每個數字有其對應的狀態:

表1. 快取狀態
狀態描述
0Uncached
頁面不在應用程式快取中。應用程式快取第一次載入時,頁面預設會處於這個狀態。
1Idle
當應用程式快取是最新的時候,瀏覽器會將狀態設定為Idel。
2Checking
當應用程式檢查是否有更新清單檔案時,瀏覽器會處於這個狀態。
3Downloading
當應用程式正在下載新的快取時,瀏覽器將狀態設定為Downloading。
4UpdateReady
當新的快取下載完成時,並且已可以替換掉現有的舊資源,瀏覽器會將狀態設定為UpdateReady
5Obsolete
當找到清單檔案時,瀏覽器會將狀態設定為Obsolete。


SetInterval(function() {
   console.log(window.applicationCache.status)
},500);
事件處理
   除了要檢查快取狀態之外,還可以處理特定事件。
表2. 事件

事件

描述

Checking

當瀏覽器在檢查是否有清單檔案被更新時,這個事件會被觸發。這個通常是第一個被觸發的事件。

Downloading

當瀏覽器開始下載新資源時,該事件會被觸發。

Cached

當所有資源下載完畢並儲存至快取時,該事件會被觸發。

Error

當應用程式快取機制出現問題時,該事件會被觸發。這可能是因為找不到清單檔案,或者是找不到清單檔案中某個指定的資源。亦可能是資源已超過了瀏覽器離線暫存的空間限制。一般來說,每當發生致命錯誤時該事件就會被觸發。

NoUpdate

第一次下載清單檔案時,該事件會被觸發。

Progress

每當應用程式快取下載完一項資源後該事件會被觸發。

UpdateReady

當新資原下載完成並可以更新舊快取中的內容時,此事件會被觸發。

Obsolete

當找不到清單檔案時,該事件會被觸發。

快取置換
   當新的快取下載完畢後,它並不會立刻換掉舊的快取資漁,而是一直到主動通知應用程式該使用新的快取資源時,它才會進行置換。可以藉由處理UpdateReady事件,使用SwapCache將舊快取置換成為新的快取內容。更新資源要在刷新頁面後才能看到。
window.applicationCache.onupdateready = function() {
     window.applicationCache.swapCache();
};
如何讓用戶知道應用程式是可以離線運作的呢?
   沒有那一種瀏覽器會主動通知使用者目前的應用程式是可以支援離線瀏覽的。但是,我們可以自行通知使用者:透過監聽應用程式快取的特定事件,當應用程式已經可以離線作業時通知使用者。甚至還可以將應用程式快取生命週期的每個階段都通知使用者。
   應用程式快取相關事件的處理是直接了當的,其中一個非常有用的事見是Progress事件。每當一個資源下載完畢後這個事件會被觸發,其包含三個非常有用的屬性,可以利用這三個屬性來顯示下載進度:  
   1. lengthComputable  
   2. loaded  
   3. total
   首先,要先檢查lengthComputable屬性來判斷loaded和total屬性是否可用,接著使用loaded和total屬性計算資源下載的百分比進度。
window.applicationCache.onchecking = function(e) {
     updateCacheStatus("檢查新版本");
};

window.applicationCache.ondownloading = function(e) {
     updateCacheStatus("下載新版本的離線資源");
};

window.applicationCache.oncached = function(e) {
     updateCacheStatus("應用程式所需離線資源已下載完畢");
};

window.applicationCache.onnoupdate = function(e) {
     updateCacheStatus("無法順利更新離線資源,找不到清單檔案");
};

window.applicationCache.onprogress = function(e) {
     var message = "下載離線資源";
     if(e.lengthComputable)
          updateCacheStatus(message + Math.round(e.loaded/ e.total*100)+"%");
     else
          updateCacheStatus(message);
};
 
檢測是否在線
   理論上檢測是否在線是非常簡單的,以標準狀態下,使用navigator的onLine屬性就可以檢測出目前瀏覽器是否在線。
console.log(navigator.onLine);
   但事實上並非如此簡單,因為各種瀏覽器對於在線與離線的定義並不相同,譬如說,舊版本的FireFox只有當使用者直接進行在線與離線狀態切換時才會更新onLine屬性的值,而忽略了實際的網路情況。拋開這些實做上的不一致,檢查網路連線狀況本身就不是一件簡單的事,譬如說,假若你的電腦是連線了,但是你的路由器出了問題,這時候應該顯示什麼狀態呢?
   一種大家常用的方法就是檢查Ajax的狀態碼,然後當狀態為不成功時則進入離線機制。
事件處理
   若想要在瀏覽器中改變連線狀態時做一些事,可以透過處理offline和online事件來達成。但是請注意,和檢查onLine屬性一樣,使用這兩個事件亦有相同的問題。
window.addEventListener("offline",function(e) {
     console.log("offline");
}, false);

window.addEventListener("online",function(e) {
     alert("online");
}, false);

瀏覽器支援度
   除了IE舊版本(i.e. 10以下)外,目前主流的瀏覽器都已支援離線Web應用程式。在Caniuse.com上可以查看每種瀏覽器及其版本對這一規範的支援情況。
   對於大部份實做,各個主流瀏覽器基本上都是一致的,但是在實做儲存限制以及對限制的管理上,各個瀏覽器的差異頗大。在測試你的Web應用程式時應考慮這個問題。智慧型設備的瀏覽器在快取大小上可是斤斤計較的。
使用ASP.Net MVC來產生和提交清單檔案
   1. 產生清單檔案
       利用ASP.Net MVC產生和提交清單檔案好好幾種方式,其中最簡單的方式就是讓ASP.Net MVC提供靜態檔案。然而,若我們想要使用內建的ASP.Net MVC特性來解析路由,又或者想要撰寫程式碼來動態產生清單檔案,最好使用自定義的ActionResult。
public class ManifestResult: FileResult{
     public ManifestResult(string version) :base("text/cache-manifest") {}
}
       ManifestResult類別有四個屬性,其中三個屬性對應清單檔案的三個段落,另一個屬性對應版本號碼。CACH和NETWORK段落的兩個屬性僅是字串列舉,而FALLBAK段落的屬性是Dictionary型別,用於將資源對應到FALLBACK指定的資源。
public class ManifestResult: FileResult{
     public string Version {get; set;}
     public IEnumerable<string> CacheResources {get; set;}
     public IEnumerable<string> NetworkResources {get; set;}
     public Dictionary<string, string> FallbackResources {get; set;}

     public ManifestResult(string version): base("text/cache-manifest") {
          Version = version;
          CacheReasources = new List<string>();
          NetworkResources = new List<string>();
          FallbackResources = new Dictionary<string, string>();
     }
}
       要將格式化的清單檔案輸出到Response串流,需要覆寫掉WriteFile函數。
protected override void WriteFile(HttpResponseBase response){

      WriteManifestHeader(response);
      WriteCacheResources(response);
      WriteNetwork(response);
      WriteFallback(response);
}

private void WriteManifestHeader(HttpResponoseBase response) {
      response.Output.WriteLine("Cache Manifest");
      response.Output.WriteLine("#V" + Version ?? string.Empty);
}

private void WriteCacheResources(HttpResponseBase response) {
     response.Output.WriteLine("CACHE:");
     foreach(var cacheResource in CacheResources)
          response.Output.WriteLine(cacheResource);
}

private void WriteNetwork(HttpResponseBase response) {
     response.Output.WriteLine();
     response.Output.WriteLine("NETWORK:");
     foreach(var networkResource in NetworkResources)
          response.Output.WriteLine(networkResource);
}

private void WriteFallback(HttpResponseBase response) {
     response.Output.WriteLine();
     response.Output.WriteLine("FALLBACK:");
     foreach(var fallbackResource in FallbackResources)
          response.Output.WriteLine(fallbackResource.Key + " " + fallResource.Value);
}
提交清單檔案服務

   為了提供清單檔案服務,需要將相對應的Action添加到相對應的控制器中,藉此產生和返回清單檔案的ActionResult。在該Action中,藉由MVC的UrlHelper物件來正確地解析路由。
public ActionResult Manifest(){
     var manifestResult = new ManifestResult("1.0");
     {
          CacheResources = new List<string>() {
               Url.Action("Index", "Home"),
               "/content/style.css",
               "/scripts/main.js"
          },
          NetworkResources = new string[] { Url.Action("Status", "Service")},
          FallbackResources = { {"/logo.png", "/logo_offline.png"}}
     };

     return manifestResult;
}
 
為清單檔案設定路由
   應當為清單檔案設定特定的路由,大多數瀏覽器對於清單檔案的位置並沒有嚴格的規定,而最可靠的跨瀏覽器的方式及為將清單檔案放置在根目錄,並且將其命名為Cache Manifest。當Web應用程式啟動時,下面的程式碼會將這個新的Cache Manifest路由添加到路由表中。
routes.MapRoute("Cache.Manifest", "Cache.Manifest", new { controller = "Resources", action = "Manifest"})
參考項目
1. http://www.infoq.com/cn/articles/Offline-Web-Apps?utm_source=infoq&utm_medium=related_content_link&utm_campaign=relatedContent_news_clk

2013年9月21日 星期六

MVC - Razor在處理Partial, RenderPartial, RenderAction的不同

MVC在處理View的呈現上有三種方式可以引入其它View到本頁中;而這三者所回傳的資料與應用都不太相同。

Partial   
   Partial所回傳的是一個型別為MvcHtmlString的物件。在View中使用這個方法的目的是為了將部份檢視載入到本頁中,寫法如下:
@Html.Partial("ViewName")
RenderPartial
   RenderPartial回傳Void亦即它並不回傳任何資料。RenderPartial會將資料沖刷入Response Buffer中並且一口氣在Response內容中夾帶內容;與Partial不同的是,RenderPartial通常應用於資料量較大的部份檢視。在速度上,亦是RenderPartial較快;但是Partial的好處是可以當成Function來使用,可以自由控制MvcHtmlString的處理。寫法如下:
@Html.RenderPartial("ViewName")

 
上述兩種方法都不會使用到Controller。
RenderAction
   RenderAction會呼叫某個指定的Controller中的某個Action。RenderAction本身會帶ViewData而且會呼叫Server端的Controller執行操作,如此可做到動態部份檢視的效果。寫法如下:
@Html.RenderAction("ControllerName","ActionName")
 

.Net中各種Lock物件的比較

表1. 各種鎖在Intel Core i7 860的成本比較
建構方式 .Net Framework版本 目的 是否可跨Process 成本
lock(Monitor.Enter/Monitor/Exit) 2.0 確保同一時間內僅有一條執行緒可以存取資源。 20ns
Mutex 2.0 確保同一時間內僅有一條執行緒可以存取資源。 1000ns
SemaphoreSlim 4.0 確保同一時間內僅有指定的執行緒個數可以存取資源。 200ns
Semaphore 2.0 確保同一時間內僅有指定的執行緒個數可以存取資源。 1000ns
ReaderWriterLockSlim 3.5 允許多個讀取型執行緒,但僅有一個寫入型執行緒可以存取資源。 40ns
ReaderWriterLock(棄用) 2.0 允許多個讀取型執行緒,但僅有一個寫入型執行緒可以存取資源。 100ns
 
Lock   
 
Lock是一種物件鎖,因此,它所鎖定的只能是一個參考型變數,不能是值類型的變數。當有一個執行緒取得該物件鎖則在它之後的其它執行緒只能等待第一個執行緒釋放物件鎖。在使用上要注意不同的執行緒是否是將同一個物件來看待成鎖;譬如說,在一段Script Block中宣告了一個物件並且針對該物件作為鎖,這個情境是沒有同步資源存取的能力,因為每個執行執行到這段Script Block後就會自行建立物件並且取得物件鎖。    在實務上,通常會挑選類別中private static Object lockObj作為物件鎖的最佳對像;因為它可以確保它是全域中獨一無二的物件,在使用上比較能夠避開誤用物件當成鎖的對像的狀況。   
 
有三種避免作為物件鎖的物件:   
   1. lock(this)   
   2. lock(typeof(SomeType))   
   3. lock("SomeString")   
 
   這三種都有可能發生與預期的鎖定效果有出入的狀況。大多數的集合物件都會提供一個作為物件鎖的內部變數: _syncRoot。但是此一成員在.Net Framework 3.0之後已不再提供給開發人員使用了。
class SomeClass
{
   private static object lockObj = new object();
   public void DoSomething()
   {
      lock (lockObj)
      {
         //工作
       }
    }
}

Mutex
   其作用為一個同步鎖,用來進行兩個工作對同一個資源存取的控制,藉以避免誤動作或是結果錯誤。Mutex和Monitor很類似只有擁有Mutex物件的執行緒才具有存取資源的權限,由於Mutex物件僅能有一個,也因此確立了資源不會同時被多個執行緒所存取;只有在擁有資源存取權的執行緒發出釋放Mutex訊號之後,其它執行緒才能存取資源。
 
   Mutex較Monitor複雜的地方是在於Mutex允許跨執行元進行資源的鎖定。Mutex在內部是呼叫作業系統的API,也因此它可以跨執行元更也因此它消耗的資源更多。Mutex在鎖定時像是Lock,亦即Monitor.Enter()方法而不是Monitor.Wait()。
 
   Mutex的方法:  
   1. WaitOne(): 請求資源存取權,會一直持有鎖定到Mutex收到釋放訊號為止。
   2. ReleaseMutex(): 釋放Mutex。Mutex的內部計數器由CLR維護,每呼叫WaitOne()一次則Mutext計數器+1,當呼叫ReleaseMutex()便將計數器-1。只要計數不為0,等待資源的執行緒就會繼續等待。
 
   Mutex最糟的使用情境就是擁有Mutex物件鎖的執行緒執行工作到一半便自行結束,且結束前沒有對Mutex物件發出釋放訊號,這種狀況是最惡劣的使用Mutex物件情境,故使用Mutex時必定要是Try/Finally中使用,並且在Finally中對Mutex物件發出釋放訊號。
 
   Mutex的建構子有五個重載,而每一個建構子有著不同的操作目的:
   1. Mutex(): 無法進行跨執行元進行資源鎖定,亦為局部(Local)型同步鎖。
 
   2. Mutex(bool): 與1相同,僅能作為局域型同步鎖,而輸入參數是指示實體化Mutex的執行緒是否在實體化後馬上取得同步鎖。
 
   3. Mutex(bool, string): 與2.不同之處在於3.可以跨執行元進行資源鎖定,亦即全域型同步鎖,但是不建議採用這一種方式,因為無法得知是否這個名稱的Mutex物件已在其它執行元建立過並且被取走同步鎖。名稱具大小寫敏感。
 
   4. Mutex(bool, string, out bool): 第三個輸入參數用來指示是否取得同步鎖,是實務中較常用到的。
 
   5. Mutex(bool, string, out bool, MutexSecurity): 最後一個參數是用來進行帳戶安全性控制;控制僅有某幾個帳戶能夠存取某個名稱的Mutex物件的同步鎖。
bool blLock;
Mutex mutex = new Mutex( true, @"Global\MyAPP", out blLock);

try {
   if(blLock)
      //工作
   else
      //等待同步鎖
}
finally {
    mutex.ReleaseMutex();
}

Semaphore/SemaphoreSlim
 
   Semaphore與Mutex類似,但Semaphore可以允許資源給予多個執行緒存取;Semaphore類似一個提供計數的Mutex,可定義執行緒存取個數。同樣地,盡量在Try/Finally中使用Semaphore。
 
   當某些資源是限定僅有幾個執行緒存取時,可以考慮使用Semaphore;譬如,現在僅有3個Port可以使用,而這個時候就以設定Semaphore計數為3,而第四位執行緒就需等待。
 
   在.Net Framework 4.0中提供了Semaphore的輕量級版本-SemaphoreSlim。SemaphoreSlim不具有Semaphore可以跨執行元的能力,故所需資源較Semaphore少。Semaphore同Mutex都是在建構子中指定名稱以作為全域型同步鎖,而名稱同Mutex是大小寫敏感。
 
   Semaphore從機制上來說較類似於Mutex一樣是一種鎖,而不是"通知"。因為Semaphore允許多個執行緒存取同一個資源,因此,Semaphore本身是執行緒非關的,亦即非Semaphore擁有者可以發出釋放訊號,並且可以設定釋放幾個鎖: Release(N),但是當指定釋放的鎖的個數超過Semaphore中所設定的會拋出SemaphoreFullException例外。
Semaphore semaphore = new Semaphore(1, 3);
semaphore.WaitOne();

//執行工作
semaphore.Release();

ReaderWriterLockSlim/ReaderWriterLock
 
   和Monitor相同是.Net所撰寫的原生類別,因此在底層並不會去呼叫到OS的API。ReaderWriterLock在運作上是將讀取資源與寫入資源拆分開來看;當執行緒的工作可以被明顯區分成讀取與寫入兩類並且寫入的工作既短且快時ReaderWriterLock是相當好的同步鎖。
 
   ReaderWriterLock本質上仍是同一時間僅能只有"一組"執行緒存取資源;當寫入組取得同步鎖時,讀取組僅能等待寫入組釋放同步鎖,但是較特別的是寫入組一次僅能有一個執行緒取得同步鎖,但是讀取組可以有多個執行緒同時讀取資源。
 
   ReaderWriterLock在處理不當時非常容易造成"飢餓現象";這是因為若寫入組占用太長時間或是兩組中有某個執行緒忘了釋放資源,這是就會造成另一組執行緒永遠無法存取資源,通常會採用時間限制,若在時限內沒有取得同步鎖時便拋出ApplicationException異常。
 
   ReaderWriterLock不允許一個執行緒同時擔任寫入與讀取組,ReaderWriterLock常用方法:
   1. AcquireReaderLock(): 請求讀取同步鎖。
   2. AcquireWriterLock(): 請求寫入同步鎖。
   3. ReleaseReaderLock(): 讀取鎖-1;當讀取鎖為0時,便換寫入組取得資源存取權。
   4. ReleaseWriterLock(): 釋放寫入鎖。
   5. ReleaseLock(): 釋放鎖;不論當前是處於計數寫入或是讀取。
 
EventWaitHandler一族
 
   EventWaitHandler在應用上都是以"通知"為主軸;在某些情境下希望執行緒在工作完成之後能夠通知已完成,讓整個系統的運行更順暢而不需要以輪詢(Pooling)這種低效能的作法判斷是否執行緒已經完成工作。
 
   EventWaitHandler僅有通知能力本身並不具有資源鎖定的能力,因此,在實務中通常會搭配Lock/Monitor/Mutex一起實做平行系統;共有兩組執行緒,一組擔任生產者,另一組則是擔任消費者並且由Lock/Monitor/Mutex進行資源的鎖定,開始時僅有生產者開始工作另一組消費者則是被阻擋,當生產者完工之後會通知消費者進行消費並且自身進入阻擋狀態,待消費者組完成工作後再通知生產者工作,如此往復循環。
 
   EventWaitHandle有以下幾個常用的方法:
 
   1. SingnalAndWait(): EventWaitHandle的靜態方法;通知另一個EventWaitHandle並且自身進入等待狀態
   2. WaitAny(): EventWaitHandle的靜態方法;等待一組EventWaitHandle中某個EventWaitHandle進入執行狀態
   3. WaitAll(): EventWaitHandle的靜態方法;等待一組執行緒內所有EventWaitHandle進入執行狀態
   4. Set(): 進入執行狀態
   5. Reset(): 進入等待狀態
   EventWaitHandle的建構子不同重載具不同能力:
   1. EventWaitHandle(bool, EventResetMode, String): 第三個參數用在全域型控制時使用,而第二個參數有: AutoReset/ManualReset兩個列舉值;AutoReset是當呼叫Set()之後釋放處於等待的一個執行緒後自動進入等待狀態,其餘執行緒依舊處於等待,ManualReset會釋放所有等待中的執行緒,並在手動呼叫Reset()前,一直處於通行狀態。
 
   2. EventWaitHandle(bool ,EventResetMode ,string ,out bool ,EventWaitHandleSecurity): 後面兩參數同Mutex。
   EventWaitHandle的子類別: AutoResetEvent/ManualResetEvent,其特性就如同EventResetMod一般。
 
   AutoResetEvent(自動重置事件)
 
   在AutoResetEvent的建構子: public AutoResetEvent(bool InitialState),在建構子中使用bool型別的輸入參數來作為初始狀態的設定;若要將AutoResetEvent的初始狀態設定為:
   1. 終止: 輸入值為true
   2. 非終止: 輸入值為false
 
   一般來說,AutoResetEvent常用到的方法有兩個:
 
   1. WaitOne(int millisecondsTimeout): 這個方法是用來讓呼叫它的執行緒進入等待狀態,並且依傳入的參數作為等待秒數,當秒數逾時後原本進入等待狀態的執行緒會自動恢復成執行狀態,而逾時後會回傳false。
 
       執行緒透過呼叫WaitOne這個方法來讓自己進入等待狀態,但是否進入等待狀態還要依AutoResetEvent本身是處於那種初始狀態(i.e. 終止or非終止);若AutoResetEvent本身是處於非終止狀態則執行緒呼叫WaitOne時會進入等待狀態,但AutoResetEvent為終止狀態時呼叫WaitOne卻不會進入等待狀態,但是AutoResetEvent會自動轉變成為非終止狀態,當再次呼叫WaitOne時就能夠讓執行緒進入等待狀態。
 
   2. Signal(): 這個方法是用來讓某一個因呼叫WaitOne而進入等待狀態的執行緒,從等待狀態恢復到執行狀態。
public static AutoResetEvent autoEvent = new AutoResetEvent(false);

public static void Main(string[] args)
{
   Console.WriteLine("Main thread Start at:" + DateTime.Now.ToLongTimeString());
   Thread t = new Thread(TestMethod);
   t.Start();
   Thread.Sleep(3000);
   autoEvent.Set();
   Console.Read();
}

public static void TestMethod()
{
   if(autoEvent.WaitOne(2000))
   {
      Console.WriteLine("Get Signal to Work");
      Console.WriteLine("Method restart run at:" + DateTime.Now.ToLongTimeString());
   }
   else
   {
      Console.WriteLine("Time out to work");
      Conosle.WriteLine("Method restart run at:" + DateTime.Now.ToLongTimeString());
   }
}
 
   ManualResetEvent(手動重置事件)
 
   ManualResetEvent和AutoResetEvent使用上幾乎一致,因為它們都是衍生自EventWaitHandle。不過其區別如下:
 
   1. 當AutoResetEvent為終止狀態時,呼叫WaitOne方法的執行緒不會進入等待狀態,而AutoResetEvent則本身會將狀態改變成為非終止狀態。
   2. ManualResetEvent為終止狀態時呼叫WaitOne方法的執行緒不會進入等待狀態,ManualResetEvent也不會自動進入非終止狀態。
private static ManualResetEvent manualEvent = new ManualResetEvent(true);

public static void Main(string[] args)
{
   Console.WriteLine("Main Thread Start run at:" + DateTime.Now.ToLongTimeString());
   Thread t = new Thread(TestMethod);
   t.Start();
   Console.Read();
}

public static void TestMethod()
{
   manualEvent.WaitOne();
   Console.WriteLine("Method start at:" + DateTime.Now.ToLongTimeString());
   manualEvent.WaitOne(1000);
   Console.WriteLine("Method start at:" + DateTime.Now.ToLongTimeString());
}

ManualResetEvent

完全不會阻擋,縱使設定了逾時時間也一樣。

TPL(Task Parallel Language) – 平行化處理

.Net Framework的平行化程式
   目前的個人電腦以及伺服器工作站已具備二到四核心(i.e.實體CPU),這使得多執行緒可以在同一時間點上一起被執行。在不久的未來,個人電腦將會擁有更多的核心。為了能夠妥善的利用硬體所帶來的資源,可以考慮使用平行化程式將工作分散在多個核心中。在過去,平行化需要較處理執行緒和資源鎖這種細節上的處理,自從.Net Framework 4.0開始,提供了一套新的類別庫以及探測工具來支援平行化程式撰寫。這套新的類別庫可以讓開發能夠寫出更有效率且細顆粒度和可延展平行的程式,並且不需要再對執行緒(Thread)或是執行緒池(ThreadPool)進行操作。下圖描繪了.Net Framework 4.0的平行程式架構:

IC292903
圖1. .Net Framework 4.0的平行程式架構

Task類別
   Task與Task<TResult>類別是TPL的基礎類別,而Task<TResult>為Task的泛型版本。而Task<TResult>中的TResult為Task執行完畢後回傳值的資料型別。
啟動Task
Task task = new Task (() => Console .Write("Hi" ));
task.Start();

   透過實體一個Task物件,接著再呼叫該物件的Start()方法。而Task類別的建構子除了上述例子輸入型別是Action委派型別之外,還有另一種建構子: Task(Action, TaskCreationOptions);而TaskCreationOptions是一個列舉型別,其中較常被使用到的為: LongRunning,該列舉值是在當反覆測試過後判斷這個Task較適合採用長時間執行的方式來運行才選用。TaskCreationOptions是用於告知ThreadPool這個Task不要放在ThreadPool中執行,否則一般的Task都是在ThreadPool中以非同步方式執行。

   上述啟動Task的方式較傳統與一般,但是在實務中會採用更簡便的手法:
Task.Run(() => Console .WriteLine("Hi" ));

   以上述方式即可直接啟動Task而不需實體化Task物件。Task.Run是Task類別的靜態方法,輸入參數是Action委派型別,而Run()的回傳值回一個Task物件。除了Task.Run(Action)之外,Task的Run還有另一種泛型版本:
Task.Run<TResult>( Func<Task<TResult>>);

這個版本的輸入參數由Action委派型別換成是Func<Task<TResult>> ,其範例如下:
Task<int > task=Task .Run<int >(() => { return 1 + 1; });
int result = task.Result;

Task的等待

   一般狀況下,Task是ThreadPool以非同步方式執行,系統想要知道這個Task是否已經執行完畢可以透過查看Task物件的屬性IsCompleted來判斷,亦可以呼叫Task物件的方法:Wait()。
   但是若是採用Wait()方法的方式會令主執行緒暫停。
Task< int> task= Task.Run< int>(() => { return 1 + 1; });
int result = default( int);

if(task.IsCompleted)
     result=task.Result;

Task< int> task= Task.Run< int>(() => { return 1 + 1; });
task.Wait();
int result = task.Result;

   Wait()這個方法還有其它多載: bool Wait(int millisecondsTimeout),該重載方法傳入一個毫秒為單位的整數,Task的Wait為依此傳入的秒數作為等待時間的依據,若在時間內Task執行完畢則回傳True,反之,則是False。

Task< int> task = Task.Run< int>(() => { Thread.Sleep(5000); return 1 + 1; });
bool blOver=task.Wait(4900);
int result = task.Result;

Task的串聯

   Task串聯指的是當一個Task執行完畢之後接著執行另一個Task,通常有兩種方法來達成。
   1. Task物件.GetAwaiter()
   2. Task物件.ContinueWith(Action)

   採用第1種方法,當呼叫GetAwaiter()方法後,會回傳TaskAwaiter結構體,並且該TaskAwaiter結構體具有一個OnCompleted方法,而Task執行完畢後的下一個Task所要執行的動作就是藉由輸入這個物件的OnCompleted事件來達成:
Task< int> task = Task.Run< int>(() => { Thread.Sleep(5000); return 1 + 1; });
TaskAwaiter< int> awaiter = task.GetAwaiter();
awaiter.OnCompleted(() => {
   Console.WriteLine( "hi");
});

第2種方式較常使用,而這種方式和第1種不同的地方在於當發生異常時,第1種方式可以直接截取,而第二種僅能在呼叫Result屬性才會拋出異常,並且會將異常包裹成AggregateException,但第1種的異常不會包裹。
Task< int> task = Task.Run< int>(() => { Thread.Sleep(5000); return 1 + 1; });
Task task2 = task.ContinueWith(o => Console.WriteLine( "hi"));
bool blOver=task.Wait(4900);
int result = task.Result;
Console.WriteLine(blOver);

異常的處理

   和Thread類別不同的地方是 Task拋出的異常是可以被截取,但也不是直接就能截取到,而是藉由呼叫Wait()時或是Result屬性時Task才會拋出異常,並且這個異常會被包裹成AggregateException異常型別。

   實務上,Task的啟動多半都是採用Task.Run(Action)或是
Task.Run<TResult( Func<Task<TResult>>)這兩種方式,在某些情境下不需要關心它們的執行是否已完成,但是對於它們在執行中是否有發生異常還是需要加以關注,而這些異常亦即未觀察到的異常(Unobserved Exception),可以透過註冊一個全域的靜態事件: TaskScheduler.UnobservedTaskException來處理這些異常;當GC要回收Task物件時,若在此之前有某個Task執行發生了異常,則在GC回收之前會執行註冊UnobservedTaskException事件的方法。
TaskScheduler.UnobservedTaskException += (obj, e) => Console.WriteLine(e.Exception.Message);
Task< int> task = Task.Run< int>(() => { Thread.Sleep(5000); return 1 + 1; });

TaskCompletionSource

   TaskCompletionSource是一種承載需要長時間執行工作的Task管理類別,該類別較常使用的有三個方法:
   1. SetResult: 承載最後處理的結果
   2. SetException: 承載異常
   3. SetCanceled: 承載該工作已被取消
   而其較簡易的使用方式如下:
TaskCompletionSource<int > tcs = new TaskCompletionSource <int >();
Task.Run(
   () => {
      Thread.Sleep(5000);
      int i = Enumerable.Range(0, 100).Sum();
      tcs.SetResult(i);
});

Task< int> task = tcs.Task;

TaskCompletionSource的Task屬性即為包裹後所產出的Task物件,接下來即可操作Task物件。依上述程式碼來看TaskCompletionSource類別的執行結果需待Task.Run方法中的工作執行完畢才能取得,這是一種變相的執行緒同步手法,並且最終取得執行的成果。
   TaskCompletionSource在實務應用上可用來作為延遲執行;通常可配合Timer來處理:
TaskCompletionSource<int > tcs = new TaskCompletionSource <int >();
System.Timers. Timer timer = new System.Timers. Timer(5000) { AutoReset = false };

timer.Elapsed += (sender, e) => {
      timer.Dispose();
      //執行某段工作
      tcs.SetResult(result);
};

timer.Start();
Task< int> task = tcs.Task;
int  finalResult= task.Result;

   除了延遲執行外,TaskCompletionSource在處理事件驅動程式(EAP)的重構方面亦大有助益;實務中,EAP很容易讓UI的控制項與商業邏輯混合在一起造成維護上的困擾,故可以藉TaskCompletionSource包裹一些工作,並且再藉由TaskCompletionSource最後產出的Task物件來達成UI與商業邏輯切割。
(重構前)
WebClient wc = new WebClient();

wc.DownloadStringCompleted += (sender, e) => {
   if (e.Cancelled)
      Console.WriteLine( "Cancel");
   else if (e.Error != null)
      Console.WriteLine( "Error");
   else
      Console.WriteLine(e.Result);
};

(重構後)
TaskCompletionSource<string > tcs = new TaskCompletionSource<string >();
WebClient wc = new WebClient();

wc.DownloadStringCompleted += (sender, e) => {
   if (e.Cancelled)
       tcs.SetCanceled();
   else if (e.Error != null)
       tcs.SetException(e.Error);
   else
       tcs.SetResult(e.Result);
};

Task< string> task = tcs.Task;
string strResult= task.Result;
Console.WriteLine(strResult);

   TaskCompletionSource除了在EAP上的應用之外,亦可應用在IAsyncResult模式下:
非同步方法簽名如下:
public static IAsyncResult BeginGetHostAddresses(string hostNameOrAddress, AsyncCallback requestCallback, Object state);

public static IPAddress[] EndGetHostAddresses( IAsyncResult asyncResult);

(重構前)
static void Main()
{
   Dns.BeginGetHostAddresses( "www.yahoo.com", result =>
   {
      IPAddress[] address = Dns.EndGetHostAddresses(result);
      Console.WriteLine(addresses[0]);
   }, null);

   Console.ReadKey();
}

(重構後)
public static Task< IPAddress[]> GetHostAddressesAsTask(string hostNameOrAddress)
{
   var tcs = new TaskCompletionSource<IPAddress []>();
   Dns.BeginGetHostAddresses(hostNameOrAddress, iar =>
   {
      try
      {
         tcs.SetResult( Dns.EndGetHostAddresses(iar));
      }
      catch ( Exception ex)
      {
         tcs.SetException(ex);
      }
   }, null);

   return tcs.Task;
}

大量資料平行化處理

   使用時機: 當集合中所有的元素都有相同的資料操作行為需要同步進行時。集合會被拆分成多個區塊,而不同執行緒可以在同一時間處理各自被分派的區塊。TPL(Task Parallel Library)藉由System.Threading.Tasks.Parallel提供資料平行化處理。這個類別提供For/ForEach以方法導向的實做方式來達到平行化。開發時,撰寫Parallel.For/Parallel.ForEach的迴圈內容就如同在撰寫一般的For/ForEac的迴圈內容,完全不需要處理執行緒或是佇列中的工作項目。
Parallel.ForEach(sourceCollection, item => Process(item));

   當平行化迴圈開始執行時,TPL中的Task scheduler會依硬體資源以及工作負載將sourceCollection切割成數份,Task Scheduler會自動調節工作量,當它認知到工作負載不平衡的時候,可能會使用到更多的執行緒和CPU核心數。

   Parallel.For/Parallel.ForEach提供了多個函式的負載,讓開發人員能夠:監控其它執行緒的狀態、中斷迴圈執行、維護執行緒的本地狀態、釋放執行緒本地狀態、控制平行化的程度...等等。能夠達成這些操作的相關支援類別如下:
   1. ParallelLoopState
   2. ParallelOptions
   3. ParallelLoopResult
   4. CancellationToken
   5. CancellationTokenSource

大量資料平行化處理-For

   在使用Parallel.For時,需注意是否真的需要使用到平行化的For;若For迴圈內的工作其所需資源與複雜度並不高,則可以考慮不使用平行化的Parallel.For而改使用一般的For。
   Parallel.For與一般的For迴圈在撰寫上相當類似,不同的地方在於Parallel.For中反覆執行的是一個"方法",而一般的For迴圈則是一段Script;因此,雖說寫法相當類似但是實際上並不全然相同,譬如說,在一般的For迴圈中若在滿足了一定的條件後中斷(Break),僅需在For迴圈中撰寫if判斷式,並且滿足判斷式後執行break這個指令即可中斷For迴圈的執行,但Parallel.For執行break指令是無任何意義的,因此,要處理一般For迴圈中所能夠達到的邏輯,需要借助ParallelLoopState物件來處理。
Parallel.For( 0, 100, (i, loopState)=>
{
   if( i>90)
      loopState.Break();
}

   當i大於90時即刻中斷尚未執行完的所有工作,實際執行時有可能第二個執行緒執行時其值就超過90了,因此,不能預設大約會執行近90次才會中斷。Parallel.For本身的回傳值為PrallelLoopResult結構體,此為一個用於判斷是否所有工作都已經執行完畢,通常會和需要中斷條件的現實需求相互搭配使用,讓開發人員能夠在程式中取得Parallel.For中斷之後關於Parallel.For的執行狀況,例如:是否所有工作都執行完畢(IsCompleted)、最小的上下界索引值(LowestBreakIteration)為何。

上例是基本較常使用到的方法重載。除了這個重載外Parallel.For還有多達12個重載。不同的重載其差別大多是起始和終止的上下界參數資料型別為int或long的差別,除此之外,Parallel還允許更多的控制,諸如執行緒的狀態物件設定以及平行化執行的組態設定等等。
ParallelOptions options = new ParallelOptions();
options.MaxDegreeOfParallelism = 2;
Parallel.For(0, 9, options, (i)=>{
     // DoSomething
});

   上例中使用Parallel.For另一個重載的方法,並且設定其執行時的平行度;當設定平行度為-1時,代表無執行緒的個數限制,設定為1則代表不使用平行化以循序方式執行。
大量資料平行化處理-ForEach

Parallel.ForEach在程式撰寫上十分類似Parallel.For。來源資料集合被切割成數份並且依照系統的硬體資源

使用Task Parallel的陷阱

   Parallel.For與Parallel.ForEach在大多數狀況下針對循序式的迴圈效能能夠有大幅度的改善。但是導入平行處理後勢必也帶來了一定的複雜性,而這些複雜性很可能會造成一些問題。以下接著討論可能的問題:

   1. 不要假設使用了平行處理就一定能加速
       在某些狀況下,平行迴圈可能會比循序式迴圈處理還要慢;使用平行迴圈的最高指導原則就是僅有較少的迭代,但執行複雜度低的委派內容反而會造成效能不彰。由於,有太多可能會影響到效能的因素,是故,建議使用時一定要效能測試。

   2. 避免在委派中撰寫到記憶體區塊共享的程式碼
       平行處理在存取靜態變數這類的共享記憶體型變數與一般的非平行處理程式有所不同。因此,當有大量的執行緒在存取共享記憶體區塊的變數時,勢必會有競爭現象。雖然解決方式為對需要同步化存取的變數進行鎖定,但這也造成了效能上的損害。因此,建議盡量避免撰寫到需要存取共享記憶體區塊的變數,至少也要有所節制。在平行處理中難免會需要存取執行緒的狀態,而最佳解決方案則是使用System.Threading.threadLocal<T>型別的變數來儲存執行緒內部的變數。例如:求取總合時就需要使用一個變數不斷累加數值:
long total = 0;
Parallel.For< long>(0, array.Length, () => 0, (index, loop, subtotal) =>
{
   subtotal += array[index];
   return subtotal;
}, (x) => Interlocked.Add( ref total, x));

   3. 避免過度平行化
       使用平行化迴圈本質是會附帶一些額外的資料切割以及同步化工作執行緒的成本。而平行化所能產生的效能提速是基於所能使用的伺服器CPU核心數。因此,如果僅有一顆核心的伺服器無法讓平行化加速,是故,要注意不要所有大量資料處理都使用平行化。大抵上,平行化多半是用在巢狀迴圈中的最外層迴圈,僅有在下列狀況下才會在內層迴圈中使用:
       3.1. 內部迴圈的內容其執行動作的複雜度高
       3.2. 內部迴圈的內容其執行動作的執行較長
       3.3. 執行系統的硬體具有足夠的核心數來處理夠多數量的執行緒

   4. 避免呼叫非執行緒保護的方法
       在Parallel.For或Parallel.ForEach的委派中呼叫非執行緒保護的方法可能會造成非預期的執行的動作或是異常(Exception)。

   5. 限制執行緒保護方法的呼叫次數
       大多數在.Net Framework中的靜態方法都是屬於執行緒保護的方法,並且能夠讓多執行緒同時呼叫。然而,這會為了達到同步化而帶來重大的效能低落。

   6. 留心執行緒混搭議題
       某些技術,例如COM與STA或WindowForm,WPF等前端技術混搭時,COM必須執行在指定的執行緒上,而WindowForm或WPF的控制項只能由創建它的執行緒可以對其進行操作,這意味著,你無法在COM所在的執行緒中試圖更新控制項,除非有設定執行緒排程:
Task t1 = new Task(() =>
{
   //DoSomething
});

var UISyncContext = TaskScheduler.FromCurrentSynchronizationContext();

Task t2 = t1.ContinueWith((lastResult) =>
{
   //UI控制項設定
}, UISyncContext);

   7. 謹慎使用Parallel.Invoke的等待委派
       在某些情境下,Task會是執行在某條執行緒中。這種效能優化方式很有可能會造成死結。舉例來說,當兩個Task執行同一段委派程式碼時,當這段委派程式碼中使用EventWaitHandler進行程式碼的執行權管理時,若第一個Task在執行之後沒有通知另一個正在等待中的Task,就會造成死結。解決的方法可以在等待委派中指定等待逾時時間,或是在創建這條執行兩個Task的執行緒建構子中設定執行時內部的Task彼此之間不會阻斷。

   8. 不要假設ForEach/For/ForAll<TSource>的迭代永遠都應該平行處理
       其實ForEach/For/ForAll並非一定非要以平行化來處理資料。因此,應該避免在內部程式碼中以多執行緒的邏輯撰寫程式;不需要加上鎖或是事件通知(EventWaitHandler)等機制。

   9. 避免將平行化迴圈寫在UI執行緒中
       若希望在大量資料處理完畢之後將結果顯示在UI上,考慮將平行化迴圈寫在背景執行緒中,並且在執行完畢後呼叫UI執行緒進行結果的顯示。這麼做是為了避免發生系統異常,因為多執行緒允許操作UI上的控制項時,很可能會因為搶占控制項或是等待更甚是死結的現象發生。
(有問題的程式碼)
public void btnOk_Click( object sender, EventArgs e)
{
   int N=100;
   Parallel.For(0, N, i=>
   {
      btnOk.Invoke(( Action) delegate { DisplayProgress(i) });
   });
}

(改善後)
public void btnOk_Click(object sender, EventArgs e)
{
   Task.Factory.StartNew(() =>
   Parallel.For(0, N, i =>
   {
      btnOk.Invoke(( Action) delegate { DisplayProgress(i);});
   });
}

參考項目
1. http://cnn237111.blog.51cto.com/2359144/1102476

SignalR訊息推播總整理

即時推播
   在網頁上要做到即時推播在之前大多採用Ajax的手法完成,但此手法需要較多的伺服器資源亦提高系統硬體建置成本,再者,此一做法需要實做較多細節,也因為造成系統開發上需要較高的建置與維護成本。
   SignalR是一個整合式的套件;它整合了伺服器端與客戶端的系統開發,採用瀏覽器作為客戶端並且使用微軟平臺構建的伺服器就可以借助SignalR來達成多個客戶端同步推播訊息,而這個推播頻道可不受限制的進行單個無狀態請求/回應的資料交換直到明確關閉。
   較特別的是,客戶端除了可以一次傳送多筆訊息給伺服器端之外,亦可發送非同步訊息給伺服器端。

Http 1.1的特性
   Http 1.1具有持續性連接的特性,亦稱作Http Keep-alive或Http connection reuse。是使用同一個TCP連接來發送/接收多個Http的請求/回應,而不是為每一個新的請求/回應就開一條連線。

   採用持續性連接的處理方式可以具有幾個優點:
   1. 因需要啟用的連線較少,相對來說較省資源。
   2. 請求/回應可以達成Http管線化(i.e. 批次處理)
   3. 因連線數少,網路阻塞狀況較少。
   4. 客戶端不需頻繁與伺服器端進行連線建立的交握。
   5. 回報錯誤無需關閉TCP連線。
   以目前較大的頻寬來說,持續性連接並不一定具有優勢;因為,當傳送完需求後連線仍會持續保持連線一段時間,而這段時間卻沒有做任何事。為此,瀏覽器會有一套演算法進行連線的管理。

SignalR四種資料交換方式
   1. WebSocket:
       WebSocket由W3C制定的HTML5標準的API,亦為一種資料通訊協定。其運作方式為瀏覽器開啟一個Socket作為伺服器端與客戶端的雙向通訊頻道,這個頻道會持續存在直到兩方中其中一方主動關閉,Socket此時才會隨之關閉。由於Html5標準當前尚未正式定案,支援的瀏覽器也不多,故,較少採用這種方式。
var ws = new WebSocket("ws://localhost:9999/socket" );
ws.onopen = function () {
    ws.send("Hello");
};

ws.onmessage = function (evt) {
   var recMsg = evt.data;
};

ws.onclose = function () {
   //關閉
};

2. Long Polling
      在Html5定案之前,要做到即時訊息推播以前都是採用這種做法來達成,而這種做法能夠支援較多瀏覽器版本,其內部資料傳輸方式為: 瀏覽器載入網頁之後,先送出一個Request並且為保持連線會在固定時間內送出Request以保持連線,並且等待伺服器傳送訊息;當客戶端要發送一筆訊息給伺服器端時,則會另開一條Http連線傳送資料。這種做法的好處是可以適用多個甚至是舊的瀏覽器版本,但最大的缺點就是所耗費的伺服器資源是最高的。

   3. Server-Send Events
       此一方法亦為Html5所制定的標準API,但目前仍處於草案階段,所以市面上的瀏覽器較少支援此一API。而這個API的內部實做較類似Long Polling,但是它僅接收伺服器端所傳送過來的資料並非是雙向與伺服器端溝通。
var source = new EventSource("/getEvents");
source.onmessage = function (event) {
   //DoSomething
};

4. Forever Frame
      利用iframe標籤指向一個Url,這個網頁要一直處於傳送狀態,接收來自伺服器端的訊息並更新本頁上的Html內容;由於一直連接著,所以可以將最新資料不斷推播到客戶端,缺點是長時間使用後會花掉太多記憶體空間。

SignalR的兩大類別
   SinalR在客戶端與伺服器端之間的資料交換是採用Json格式,而依照需求的不同
SignalR提供了兩種類別:

   1. Persistent Connection: 用於持續式連線,解決長時間連線的需求。客戶端允許主動向伺服器要求資料,而伺服器端在實做上亦不需實做太多細節部份,僅需處理五個事件:
       (1) OnConnected
       (2) OnReconnected
       (3) OnReceived
       (4) OnError
       (5) OnDisconnect

   2. Hub: 訊息交換機,用於解決即時的多客戶端彼此訊息交換。伺服器可以利用Url註冊一或多個Hub,只要連接到其中一個Hub就能與連接到該Hub的所有客戶端交換訊息,除此之外,伺服器端還可以呼叫到客戶端的Js,但其背後仍是以Http作為協定。
SignalR-PersistentConnection實做
步驟1. 安裝必要Nuget套件:
            (1) jQuery
            (2) JSON-js json2
            (3) Microsoft ASP.Net SignalR

步驟2. 撰寫資料傳輸類別(DTO)
public class ChatData
{
   public string Name { get; set; }
   public string Message { get; set; }
   public ChatData()
   {
   }

   public ChatData( string name, string message)
    {
        Name = name;
        Message = message;
     }
}

步驟3. 撰寫PersistentConnection類別
public class ChatConnection : PersistentConnection
{
   private static readonly Dictionary< string, string> _clients = new Dictionary<string , string >();

   protected override Task OnConnected( IRequest request, string connectionId)
   {
       _clients.Add(connectionId, string.Empty);
       ChatData chatData = new ChatData( "Server", "A new user has joined the room.");
       return Connection.Broadcast(chatData);
    }

   protected override Task OnReceived( IRequest request, string connectionId, string data)
   {
       ChatData chatData = JsonConvert.DeserializeObject< ChatData>(data);
       _clients[connectionId] = chatData.Name;
       return Connection.Broadcast(chatData);
   }

   protected override Task OnDisconnected( IRequest request, string connectionId)
   {
       string name = _clients[connectionId];
       ChatData chatData = new ChatData( "Server", string .Format("{0} has left the room.", name));
       _clients.Remove(connectionId);
       return Connection.Broadcast(chatData);
    }
 }

步驟4. 添加Action
public ActionResult ChatR()
{
   var vm = new ChatData();
   return View(vm);
}

步驟5. 添加View
@{
    ViewBag.Title = "ChatR";
}

@using (Html.BeginForm())
{
   @ Html.EditorForModel();
   <input id ="send" value ="send" type ="button" />
   <ul id ="messages" style ="list-style: none;"></ul >
}

@section scripts{
   <script src ="~/Scripts/json2.js"></script>
   <script src ="~/Scripts/jquery.signalR-1.1.3.js"></ script>
   <script type="text/javascript">

   $(document).ready(function () {
      var con = $.connection( "/chat");
      on.received(function (data) {
         $( "#messages").append("<li>" + data.Name + ': ' + data.Message + "</li>");
      });

      con.start()
            .promise()
            .done(function () {
                  $( "#send").click(function () {
                      var myName = $( "#Name").val();
                      var myMessage = $( "#Message").val();
                       con.send(JSON.stringify({ name: myName, message: myMessage }));
    })
  });
}

步驟6. 添加路由規則(BundleConfig.cs)
public class RouteConfig
{
   public static void RegisterRoutes( RouteCollection routes)
   {
      RouteTable.Routes.MapConnection< ChatConnection>("chat" , "/chat" );
          routes.IgnoreRoute( "{resource}.axd/{*pathInfo}");
          routes.MapRoute(
              name: "Default",
              url: "{controller}/{action}/{id}",
               defaults: new { controller = "Home", action = "ChatR" , id = UrlParameter.Optional }
          );
    }
}

(實做畫面)
Image(1)

圖1. PersistentConnection實做結果
SignalR-Hub實做
步驟1. 安裝必要Nuget套件:
            (1) jQuery
            (2) JSON-js json2
            (3) Microsoft ASP.Net SignalR
步驟2. 撰寫Hub類別
[HubName("chatHub")]
public class ChatHub : Hub
{
   private static Dictionary< string, string> dicClients = new Dictionary<string , string >();
   //使用者連現時呼叫
   public void userConnected( string name)
   {
      //進行編碼,防止XSS攻擊
      name = HttpUtility.HtmlEncode(name);
      string message = "歡迎使用者 " + name + " 加入聊天室" ;
      //發送訊息給除了自己的其他
      Clients.Others.addList(Context.ConnectionId, name);
      Clients.Others.hello(message);
      //發送訊息至自己,並且取得上線清單
      Clients.Caller.getList(dicClients.Select(p => new { id = p.Key, name = p.Value }).ToList());
      //新增目前使用者至上線清單
      dicClients.Add(Context.ConnectionId, name);
     }

   public void SendMessageToAll( string msg)
   {
      string strMsg = Encoder.HtmlEncode(msg);
      string strName = dicClients.Where(c => c.Key == Context.ConnectionId).FirstOrDefault().Value;
      strMsg = string.Concat(strName, "說: ", strMsg);
      Clients.All.sendMessageToAll(strMsg);
   }

   public void SendMessageToSomeone( string to, string msg)
   {
       msg = Encoder.HtmlEncode(msg);
       string strFrom = dicClients.Where(c => c.Key == Context.ConnectionId).FirstOrDefault().Value;
       msg = string.Format( @"{0} <span style='color:red'>悄悄對你說:</span>: {1}" , strFrom, msg);
       Clients.Client(to).sendMessageToSomeone(msg);
    }

    public override Task OnDisconnected()
    {
        Clients.All.removeList(Context.ConnectionId);
        dicClients.Remove(Context.ConnectionId);
        return base.OnDisconnected();
     }
 }

步驟3. 添加Action
public ActionResult ChatR()
{
   return View();
}

步驟4. 添加CSS
<style>
#userName{
   display: none;
   color: red;
}

#messageBox, #chatList{
   float: left;
   height: 300px;
   width: 300px;
   overflow: auto;
}

#messageBox{
   border: 1px solid #000;
}

#chatList{
   width: 150px;
   overflow: scroll;
}

#list li{cursor: pointer;}

#bar{clear: both;}

p{margin: 0;}

</style>

步驟5. 添加View
<p id="userName"> Hi! </ p>
<div id="messageBox">
   <p >聊天室內容 </p>
   <ul id ="messageList"></ul>
</div>

<div id="chatList">
   <p >上線清單 </p>
   <ul id ="list">
   </ul >
</div>

<div id="bar">
   <select id ="box">
      <option value="all"> 所有人</option >
   </select >
   <input type ="text" id ="message" />
   <input type ="button" id ="send" value ="發送" />
</div>

@section scripts{
   <script src ="~/Scripts/json2.js"></script>
   <script src ="~/Scripts/jquery.signalR-1.1.3.js"></ script>
   <script type ="text/javascript"></script>
   <script src ="/signalr/hubs"></script>
   <script src ="~/Scripts/Self/InitialSignalR.js"></ script>
}

步驟5. 添加路由規則(BundleConfig.cs)
public class RouteConfig
{
   public static void RegisterRoutes( RouteCollection routes)
   {
      RouteTable.Routes.MapConnection< ChatConnection>("chat" , "/chat" );
          routes.IgnoreRoute( "{resource}.axd/{*pathInfo}");
          routes.MapRoute(
             name: "Default",
             url: "{controller}/{action}/{id}",
             defaults: new { controller = "Home", action = "ChatR" , id = UrlParameter.Optional }
       );
    }
}

步驟6. 註冊Hub(Global.asax.cs)
protected void Application_Start()
{
   RouteTable.Routes.MapHubs();
    .....
}

步驟7. 撰寫Js
var userID = "" ;

$(function () {
   while (userID.length == 0) {
      userID = window.prompt( "請輸入使用者名稱" );
      if (!userID)
         userID = "";
      }

     $("#userName").append(userID).show();

     //建立與Server端的Hub的物件,注意Hub的開頭字母一定要為小寫
     var chat = $.connection.chatHub;

     //取得所有上線清單
    chat.client.getList = function (userList) {
       var li = "";
       $.each(userList, function (index, data) {
           li += "<li id='" + data.id + "'>" + data.name + "</li>";
       });
       $( "#list").html(li);
    }

   //新增一筆上線人員
    chat.client.addList = function (id, name) {
       var li = "<li id='" + id + "'>" + name + "</li>" ;
       $( "#list").append(li);
    }

   //移除一筆上線人員
    chat.client.removeList = function (id) {
       $( "#" + id).remove();
    }

   //全體聊天
    chat.client.sendMessageToAll = function (message) {
       $( "#messageList").append("<li>" + message + "</li>");
    }

   //密語聊天
    chat.client.SendMessageToSomeone = function (message) {
       $( "#messageList").append("<li>" + message + "</li>");
    }

    chat.client.hello = function (message) {
        $( "#messageList").append("<li>" + message + "</li");
    }

    //將連線打開
    $.connection.hub.start().done( function () {
       //當連線完成後,呼叫Server端的userConnected方法,並傳送使用者姓名給Server
       chat.server.userConnected(userID);
    });

    $("#send").click( function () {
      var to = $( "#box").val();
      //當to為all代表全體聊天,否則為私密聊天
      if (to == "all") {
          chat.server.sendMessageToAll($( "#message").val());
      } else {
          chat.server.sendMessage(to, $( "#message").val());
      }
      $( "#message").val('' );
   });

    $("#list").on( "click","li" , function () {
      var $this = $( this);
      var id = $this.attr( "id");
      var text = $this.text();

      //防止重複加入密語清單
     if ($( "#box").has("." + id).length > 0)
        return false;
     var option = "<option></option>"
        $( "#box").append(option).find("option:last" ).val(id).text(text).attr({ "selected": "selected" }).addClass(id);
    });
});


(成果畫面)
Image(2)

圖2. Hub實做結果
 
WebConfig的修改

   Hub在實做上較特別的部份在於<script src ="/signalr/hubs"></script>這個Js;該Js檔是由伺服器端動態產生,但實際佈署到伺服器上後會發生錯誤,主要是因為IIS誤任這個Request是要指向某一個Controller的Action,為避免此一錯誤,需在WebConfig中確認下面這一段:

<system.webServer>
  <validation validateIntegratedModeConfiguration="false" />
  <modules runAllManagedModulesForAllRequests="true" />
  ...
</system.webServer>


參考項目
1. http://www.dotblogs.com.tw/jasonyah/archive/2013/05/30/chatroom-with-signalr-realtime-web-application.aspx


[譯文]Unit of Work

原文鏈結:http://msdn.microsoft.com/en-us/magazine/dd882510.aspx

   Unit Of Work是眾多企業級系統設計樣式中較常見的一種。如Martion Fowler所說,Unit Of Work本身即為"用於解決一連串的物件因交易而受到影響,並且將物件狀態的異動寫入之餘處理同步問題"。在建構系統時Unit Of Work樣式並非是必要的,但是可以在許多持久化工具中看到它。NHibernate的ITransaction介面、Linq to SQL的DataContext,以及Entity Framework的ObjectContext皆有實做Unit Of Work。
   在某些時候,可能會需要撰寫適合專屬於某應用程式的Unit Of Work介面或從現行使用的持久化工具中提供的Unit Of Work包裝成一個獨立的類別。會需要這麼做很有可能是需要在交易管理中加入應用程式專屬的Log,追蹤,或是錯誤處理。藉由封裝應用程式中指定的持久化工具,即可在往後隨意替換持久化工具。若想要導入測試到系統開發中,許多持久化工具提供的Unit Of Work都無法做到自動化單元測試。
   若想要建構專屬於自己的Unit Of Work,其介面會像是如下:
public interface IUnitOfWork{
   void MarkDirty(object entity);
   void MarkNew(object entity);
   void MarkDeleted(object entity);
   void Commit();
   void Rollback();
}

   Unit Of Work會提供用於改變實體物件狀態的方法: new/deleted。(i.e. 在許多實務案例上,取名為MarkDirty並不是必要的,因為Unit Of Work本身擁有許多自動偵測實體是否已經改變的方式。)Unit Of Work亦提供用於Commit或是RollBack的方法。
   可以將Unit Of Work想成是一個用來處理交易的類別。Unit Of Work的職責如下:
   1. 管理交易。
   2. 處理資料庫的新增/刪除/修改。
   3. 預防重覆修改。在Unit Of Work物件的內部,不同的程式碼也許會將同一個部件標記成異動狀態,但是Unit Of Work僅只會發出一次Update命令到資料庫。
   使用Unit Of Work樣式的價值是解放系統在上述三種情境的處理,而把系統開發的焦點放在商業邏輯上。

使用方式
   最佳使用Unit Of Work的方法便是允許不同的類別或服務放到單個的交易邏輯中。這裡的關鍵之處在於要如何讓不同的類別或服務放到單個交易邏輯中。傳統上,可以使用MTS/COM+或是System.Transactions這個命名空間中的類別。於私來說,傾向使用Unit Of Work樣式讓不相關的類別或服務放到單個交易邏輯,因為它可以讓程式碼更易於被讀懂,並且簡化單元測試。
   在範例Invoicing系統中,它會在任何間點上對invoce實體進行零散的操作。由於商業邏輯的改變是相當頻繁的, 並且有時會需要額外擴充操作Invoce實體的方法。是故,可以套用命令樣式並且創建一個名為IInvoiceCommand介面,此介面用於零散的Invoce實體操作。
public interface IInvoiceCommand {
   void Exec(Invoice invoice, IUnitofWork unitOfWork);
}

    IInvoiceCommand介面僅有一個簡單的Exec方法,該方法僅在某種操作Invoice實體的行為發動時被呼叫。任何IInvoiceCommand物件都應該藉由IUnitOfWork介面來持久化任何Invoice實體的異動到資料庫中。

public class InvoiceCommandProcessor 
{
   private readonly IInvoiceCommand[] m_cmds;
   private readonly IUnitOfWorkFactory m_unitOfWorkFactory;

   public InvoiceCommandProcessor(IInvoiceCommand[] cmds, IUnitOfWorkFactory unitOfWorkFactory)
   {
      m_cmds = cmds;
      m_unitOfWorkFactory = unitOfWorkFactory;
   }

   public void runCommands(Invoice invoice)
   {
      IUnitOfWork unitOfWork = m_unitOfWorkFactory.StartNew();
      try{
            foreach(IInvoiceCommand cmd in m_cmds) {
               cmd.Exec(invoice, unitOfWork);
            }
            m_unitOfWork.Commit();
         }catch(Exception) { 
            m_unitOfWork.Rollback(); }
   }
}

   使用這個方式就可以快樂地混合並且匹配不同的IInvoiceCommand的實做來任意新增或移除Invoice系統的商業規則之餘還能整合交易功能。
   若商業模型中Invoice現在多了延後或是未付費這兩個新需求,那麼就得再創建一個新的IInvoiceCommand類別用於當延遲時要發出警告。
public class LaterInvoiceAlertCommand: IInvoiceCommand{

   public void Exec(Invoice invoice, IUnitOfWork unitOfWork) {
      bool isLate = isTheInvoiceLate(invoice);
      if(!isLate) return;
      AgentAlert alert = createLasteAlertFor(invoice);
            unitOfWork.MarkNew(alert);
   }
}

   最佳的解決方案即為LateInvoiceAlertCommand能夠在不同資料庫上開發與測試或是把其它IInvoiceCommand物件放到同一個交易中。首先,測試IInvoiceCommand物件之間在Unit Of Work中的互動,試著去假造一個IUnitOfWork介面的實做物件來進行測試,並且命其名為StubUnitOfWork。
public class StubUnitOfWork : IUnitOfWork {
   public bool WasCommitted;
   public bool WasRolledback;

   public void MarkDirty(object entity) {
      throw new System.NotImplementedException();
   }

   public ArrayList NewObjects = new ArrayList();

   public void MarkNew(object entity) {
         NewObjects.Add(entity);
   }
}

   現在已經有了一個極佳的Unit Of Work的假造(fake)物件,該物件與資料庫無任何相依關係,LateInvoiceAlertCommand的測試程式如下所示:
[TestFixture]
public class When_Creating_an_alert_for_an_invoice_that_is_more_than_45_days_old {
   private StudUnitOfWork theUnitOfWork;
   private Invoice theLateInvoice;

   [SetUp]
   public void SetUp() {
      //假造的IUnitOfWork物件用來紀錄完成那些工作
      theUnitOfWork = new StubUnitOfWork();
      //假若已有一個Invoice物件,並且它是45天以前創建的且尚未完工
      theLateInvoice = new Invoice { InvoiceDate = DateTime.Today.AddDays(-50), Completed = false};
      //實際運行LateInvoiceAlertCommand用來測試Invoice
      new LateInvoiceAlertCommand().Execute(theLateInvoice, theUnitOfWork);
   }

   [Test]
   public void the_command_should_create_a_new_AgentAlert_with_the_UnitOfWork(){
      //僅是檢驗是否有新的AgentAlert物件被註冊到Unit Of Work
      theUnitOfWork.NewObjects[0].ShouldBeOfType<AgentAlert>();
   }

   [Test]
   public void the_new_AgentAlert_should_have_XXXX() {
      var alert = theUnitOfWork.NewObjects[0].ShouldBeOfType<AgentAlert>();
      //檢驗新AgentAlert物件的屬性是否正確
   }
}

省略持久化

   當選擇或是設計專案的持久化解決方案時,會特別去關注在商業邏輯中帶有持久化機制所帶來的影響。理想上,設計、建置、測試商業邏輯的關聯性...等等,這些工作的資料庫和持久化程式碼是相互獨立的。是否有個解決方案能夠提供理想中的持久化省略機制或是POCO?
   首先,什麼是省略持久化,並且它是打那來的?在書籍"Applying Domain-Driven Design and Patterns: With Example in C# and .Net"(Pearson Education,Inc., 2006), 作者Jimmy Nilsson定義POCO為: 一群專注在當前手邊的商業問題,並且不含任何與架構相關的東西在裡頭的類別... 這些類別應當專注在當前手邊的商業問題"。商業模型中的類別不該夾雜非商業邏輯外的東西。
   現在,我們該注意些什麼?其實省略持久化僅是一個不同的設計方式,而並非是一個必要的設計方式。評估一套持久化工具,通常會問自己下列幾個問題,雖然省略持久化並不能滿足所有的問題,但是省略持久化多半是比僅是為了架構因素就把持久化機制放入商業物件中來得好。

   1. 商業邏輯可以在不同資料庫下執行嗎?
       這是一個非常重要的問題。一個成功的軟體專案是要能夠快速反饋的。換句話說,欲縮短"我想撰寫一些新東西"以及"我已證明那些新式的程式碼是可運作的,所以我要繼續創新"或"新式程式碼有問題,所以我現在要馬上修改"的時程和所需花的功。

       可以觀察到當團隊能夠實行單元測試或僅是實行嘗試在系統中放入一些新式程式碼而不是整個系統,如此便能夠更具生產力。相對的,當系統架構是設計成商業邏輯與架構彼此是緊耦合時,這個系統就會極難開發。

       重新思考一下,當發現尚未結束的Invoice已超過45天時,需要創建一個新的Agent Alert,這樣的商業邏輯使用案例。商業邏輯或許會將45天改成30天。為了要驗證Invoice在Alert方面的邏輯異動,就需要先寫一段程式碼用來紀錄Invoice目前是否已結束以及紀錄是新或是已超過30天。這些就是架構上需要特別考量的部份。是否能夠正確的撰寫並且單元測試新添加邏輯,或著需要先跳脫技術框架?

       為了要能夠讓系統快速反饋,最重要的就是有一個不含架構描述的商業邏輯模型。
public class Invoice : MySpecialEntityType{
   private long m_id;
   public Invoice(long id){
      m_id = id;
      loadDataFromDatabase(id);
   }

   public Invoice() {
      m_id = fetchNextId();
   }
}

       使用這種設計方式,就無法建立一個單純的Invoice類別而不去連線到資料庫。若設計成不需要任何與資料庫相關的POCO則就能夠撰寫自動測試並且在測試程式中存取資料庫,並且這個測試程式需要花比撰寫測試程式還要多的開發時間在設定測試資料。為了要簡化設定,需要在資料庫中設定非空值的欄位和資料來滿足需求的整合。

       若有那種"我已經超過31天了,然後..."在程式碼中,而且已經有人這麼撰寫了。不妨將Invoice類別搬移到省略持久化的函數中。
public class Invoice{
   private long m_id;
   public Invoice() {}
   public bool IsOpen { get; set;}
   public DateTime? InvoiceDate {get; set;}
}

          現在,當想要測試落後的Invoice的警告邏輯,可以更快且簡單的建構一個未結束的Invoice並且它已經超過31的測試案例。
[SetUp]
public void SetUp()
{
   Invoice theLateInvoice = new Invoice() {
      InvoiceDate = DateTime.Today.AddDays(-31),
      IsOpen = true
   }
}

          在上述函數中,已將商業邏輯從Invoice類別的持久化程式碼中分離出來,如此一來,便能夠快速建立Invoice日於記憶體中,可以更快速的對商業邏輯進行單元測試。

          當選擇了一套持久化工具後,要注意關於延持載入是否有含括在工具中。某些工具會透過虛擬代理人(Virtual Proxy)樣式來實做更具效率的通透式延遲載入,並且不會在商業邏輯程式碼中出現關於延遲載入的任何邏輯。其它的工具則是相依於程式碼產生技術,並且內嵌可在商業實體物件的延遲載入並且能夠在執行時期做到較有效率的實體物件之間的緊耦合在持久化架構中。

          還有一件極重要的事情就是套件作者是否能夠讓開發人員在自動化測試時做到讓單元測試執行速度更快。自動化測試會牽涉到資料存取或是網路服務存取,這會讓測試執行速度變得極慢。這聽起來似乎不是什麼問題,但在專案不斷進行時,執行速度慢的自動測試會降低團隊的生產力且消弭了自動化測試的價值。

   2. 是否可以設計出與資料庫模型無關的領域模型
       目前已解耦合商業邏輯層與資料庫,接下來就是要設計成與資料庫綱要無關的商業邏輯物件。通常會在商業邏輯物件中加入商業邏輯的行為。資料庫換句話說應該是被設計成在讀寫方面都具有高效能之外還具有參考整合性檢驗的能力。

       為了要驗證領域模型與資料庫分離的經典案例,先將範例領域切換到能源交易系統。該系統是用於石油買賣/運送的追蹤和付費額度。在系統雛型階段,一個交易可能會包含一定額度的購買以及紀錄運送額度。額度即為評估的單位,並且採用累加來計算。在這個交易系統中,追蹤額度有種單位:
public enum UnitOfMeasure{
      Barrels,
      Tons,
      MetricTonnes
}

       倘若領域模型與資料庫綱要綁死,則會如下所示:

public class FlatTradeDetail{
   public UnitOfMeasure PurchasesdUnitOfMeasure {get; set;}
   public double PurchasedAmount {get; set;}
   public UnitOfMeasure DeliveredUnitOfMeasure {get; set;}
   public double DeliveredAmount {get; set;}
}

       當結構與資料庫結構一致且採用的命名與資料庫也一致,這樣的結構是無法挾帶商業邏輯所需的邏輯。

       目前專案要做一個能源交易系統,而系統中一大部份的商業邏輯都是在對額度進行:比較/相減/相加,但總是需要假設評估的單位有可能會進行不同的單位轉換。使用這種直接從資料庫產出的程式碼很難添加商業邏輯到裡面。
       創建一個具有額度行為的模型。
public class Quantity{
   private readonly UnitOfMeasure m_uom;
   private readonly double m_amount;

   public UnitOfMeasure Uom {
      get { return m_uom; }
   }

   public double Amount {
      get { return m_amount; }
   }

   public Quantity(UnitOfMeasure uom, double amount){
      m_uom = uom;
      m_amount = amount;
   }

   public Quantity ConvertTo(UnitOfMeasure uom)
   {
      //回傳一個新的Quantity物件, 該物件與Unit Of Measure具有相等數量
   }

    public Quantity Subtract(Quantity other)
   {
      double newAmount = m_amount - other.ConvertTo(m_uom).Amount;
      return new Quantity(m_uom, newAmount);
   }
}

       當使用Quantity類別作為模型,並且重用其行為於額度評估單位(Unit of measure quantities)計算,而TradeDetail類別將會是如下所述:
public class TradeDetail {
   private Quantity m_urchasedQuantity;
   private Quantity m_deliveredQuantity;
 
   public Quantity Available(){
      return m_purchasedQuantity.Subtract(m_deliveredQuantity);
   }

   public bool CanDeliver(Quantity requested) {
      return Available().IsGreaterThan(requested);
   }
}

       Quantity類別會讓TradeDetail的邏輯易於被實做,但是現在的物件模型已經和資料庫綱要大不相同了,理想上,持久化工具應該要支援兩者的物件轉換。

       這種物件模型與資料庫模型的差異問題通常都不會有簡單的商業邏輯中。在能源交易系統中,Active Record架構可以簡化實體類別的程式碼產生。或者,可以採用資料庫對應工具來產出資料庫綱要對應的實體物件。

   3. 持久化策略對於商業邏輯的影響
       現實中,任何持久化工具對於實體類別都會有各種不同的影響。舉個例來說,使用NHibernate作為持久化工具。因為NHibernate實做延遲載入屬性的關聯,而許多屬性必須要被標記為virtual就只是為了能延遲載入,如下所示:
public class Invoice {
   public virtual Customer Customer { get; set;}
}

       Customer屬性被標記為virtual沒啥原因,就單純是為了讓NHibernate能夠創建一個Invoice的動態代理人,讓Invoice物件能夠作到延遲載入Customer屬性。

       強制將屬性標記成virutal來提供延遲載入的功能會帶來一些潛在的問題。在DDD開發中一個常見的方式即為建構領域模型的類別,並且這些類別的屬性值需進行檢查;通常會將屬性存取範圍設定為Internal,這是為了強制將商業邏輯封裝在物件中。
       為了達到這個設計上的哲理,Invoice類別修改為:
public class Invoice {
   private readonly DateTime m_invoiceDate;
   private readonly Customer m_customer;
   private bool m_isOpen;

   public bool IsOpen {
      get { return m_isOpen; }
   }

   public DateTime InvoiceDate {
      get { return m_invoiceDate; }
   }

   public Customer Customer {
      get { return m_customer; }
   }

   public Invoice(DateTime invoiceDate, Customer customer)
   {
      m_invoiceDate = invoiceDate;
      m_customer = customer;
   }

   public void AddDetail(InvoiceDatailMessage detail) {
      //判斷新的Invoice細節應該在創建後加入到這個Invoice物件中
   }

   public CloseInvoiceResponse Close(CloseInvoiceRequest request) {
      //Invoice自行判斷目前自身的狀態是允許關閉
      //m_isOpen欄位僅能由Invoice自行設定
   }
}

       這種設計的手法會因為系統特質的不同而有不同,但若採用上述的設計方式會影響到持久化工具的選擇。

       在最終版本的Invoice類別中僅有一個非預設建構子,並且強迫要創建Invoice物件就只能投入Invoice時間和Customer才能順利創建。大多數的持久化工具需要預設建構子。
       同樣的,這個版本的Invoice類別對於自己的欄位僅提供getter而沒有setter。再一次提醒,許多持久化工具都是將值設定到setter中。倘若想要使用更多DDD的實體物件設計方式,就必須考慮持久化工具是否可以支援欄位的映射,私有屬性,或是非預設建構子。

更多關於Unit Of Work

   還有一些關於Unit Of Work樣式需要考量的問題。若對於怎麼將Unit Of Work應用在專案感興趣,那麼這些議題就需要好好研究。

   1. 拆分Repository與Unit Of Work的方式;可將所有和讀取資料放在Repository,而寫入資料方面操作則放在Unit Of Work中。或者為了能夠更方便追蹤實體物件狀態的改變,將所有讀取與寫入操作都在Unit Of Work中實做。

   2. 如何將Unit Of Work與其它類別一起放置到一個交易中?很多人都會採用IoC容器來正確地將Unit Of Work放置到HttpContext/執行緒,或是其它範圍性策略。

   省略持久化在.Net社群中一直是有爭議的。此時,省略持久化對於大多數的持久化工具來說是不支援的。NHibernate是較佳的持久化選擇,但採用NHibernate就必須考量領域模型的設計要能符合NHibernate。