顯示具有 MVC 標籤的文章。 顯示所有文章
顯示具有 MVC 標籤的文章。 顯示所有文章

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

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


2012年11月29日 星期四

ASP.Net MVC中的WebAPI

image

     ASP.Net MVC在第四版,也就是MVC 4提供了一個嶄新的東西-WebAPI。其實這個東西早在以前的WCF就推出了,只是當時在整個WCF中它僅是其中的一個分支,而在WCF中它是一種Http服務,但畢竟WCF的龐大是眾所周知,而區區一個Http 服務在WCF眼裡看來當然顯得有些微不足道。在當時,Http並非是WCF的唯一新寵但是卻也讓大家耳目一新,不過在大家興高采烈的想要馬上做幾個範例時,在複雜的組態檔設定面前,大家都卻步了。

     Http服務是一種趨勢,它原本是想要試著去做到REST架構,提供開發人員與前端設計人員以一種方便又快速的方式存取後端服務所提供的資源,但是Html5襲捲全球開發人員之後Http服務更是水漲船高。微軟在Visual Studio的設計上從以前到現在都是一直朝著讓開發人員便利且快速開發專案為目標,而這位未來的新寵兒勢必要有一個快速且便利開發的管道,這樣才能擄獲開發人員的心。戴太陽眼鏡

      MVC4的推出委實是一個震撼彈,在當初Preview時它內容之豐富實在是讓人咋舌,其中最讓大家瘋狂的一項特性就是WebAPI。這個前身為Http服務的傢伙晃著晃著居然加入了MVC中,雖然一開始讓人很不解,但是打開它的程式來看就一豁然開朗了。WebAPI在開發簡直就和Controller開發如出一轍,唯一比較不同的地方在於WebAPI它回傳的東西和Controller不一樣,除此之外,它的開發方式以及Filter的套用嚴然就是一個Controller的變種,雖是如此,但WebAPI還是WebAPI它仍沒有改變它是Http服務的本質。

      Http是一種公開的標準,目前在Html5持續媚力燃燒的狀況下Http的應用推陳出新,利如像是影像串流-MotionJPEG以及REST架構…等等。而目前不論手持式裝置或是網頁開發,幾乎都和Http脫離不了關係,為了讓手持式裝置的APP或是網頁的資料總是能夠提供最新資訊,Http就成了最佳的資料獲取管道。或許大家會很好奇,為什麼不是採用目前網路常用的協定,諸如:TCP或是UDP呢?其實,這兩種協定一直都是資料傳輸的老字號,但是並不是所有場合都適用,更甚至在某些場合下是無法使用的,為了避免這種開發上的限制,毫無拘束的Http反倒成了資料傳輸的新寵兒。

     在MVC4專案中,App_Start資料夾底下的WebApiConfig.cs檔案,其實裡面幾乎和同資料夾的RouteConfig.cs一樣,其目的都是在做Routing(路由)設定的工作,我們來看看WebApiConfig.cs的程式碼:
    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
        }
    }

     而RouteConfig.cs則為:

        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
            routes.MapRoute(
                name: "Default",
                url: "{controller}/{action}/{id}",
                defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
            );
        }

   觀其兩者的差異,我們可以從url這個參數中發現。在WebApiConfig中,url的部份是api/{controller}/{id},而RouteConfig則是{controller}/{action}/{id}。而以下範例可以區分誰會被導向WebAPI處理,誰會被導向一般Controller處理。

  兩者的差別就在於Url路徑中WebAPI會特別寫上api這三個小寫英文字。

  接著我們就來實際新增一個WebAPI看看其中的程式內容。

image

image

image

   我們來觀察一下具有空白讀取/謝入動作的API控制器其程式碼內容為何。
    public class DefaultController : ApiController
    {
        // GET api/default
        public IEnumerable Get()
        {
            return new string[] { "value1", "value2" };
        }

        // GET api/default/5
        public string Get(int id)
        {
            return "value";
        }

        // POST api/default
        public void Post([FromBody]string value)
        {

        }

        // PUT api/default/5
        public void Put(int id, [FromBody]string value)
        {

        }

        // DELETE api/default/5
        public void Delete(int id)

        {

        }
    }

   WebAPI遵循REST架構,它利用Http的其中四個動詞(Verb)來對應資料庫操作的新增/刪除/修改/查詢。其對應如下:
  • Get => 查詢
  • Post => 新增
  • Put => 修改
  • Delete => 刪除

        而DefaultController中的內容,其每個方法的上頭都有一對註解,其註解內容就可以大致窺見WebAPI每個方法其Url大致的輪廓。比較特別的地方就在於Post和Put這兩個方法的參數,其參數中代有屬性標籤: FromBody。這個屬性標籤指的是:該參數是由MessageBody這個類別內容自動取得,也就是說,當Request送過來就會自動從其Body中擷取內容變數value的值出來。

       大家應該有個疑問,WebAPI很好用阿而開發模式也和Controller一模一樣,完全省去了WCF煩鎖的組態地獄但卻提供了相同的功用和效果,但是…它和Controller到底是用在什麼場合;什麼場合該用Controller什麼場合該用WebAPI。其實這個問題我曾經想過,後來得到twMVC核心成員-Demo的解答。


總結:WebAPI是一個好用的利器,雖然它被鑲嵌在MVC4中,但是不代表它只能使用MVC4範本來開發,它的本質仍是一種寄宿型服務,它目前僅是寄宿在MVC身上,想當然它也可以自由寄宿在某個Process身上。至此,App_Start這個資料夾的探索已經結束了,再來的章節就是介紹MVC4範本中每個JavaScript的功用即其應用場合。

2012年11月27日 星期二

ASP.Net MVC中的Bundle

image

    ASP.Net MVC在第四版提供了一個貼心的工具-Bundle。而這個工具對於所有網頁開發的人員來說實在是一個至關重要的工具。一個網站除了功能和畫面UI能夠做到吸引人之外,更重要的地方就是網站要能夠讓更多人能夠瀏覽,否則再好的內容若總是只能幾隻小貓進來看,那豈不是太可惜了呢。

     影響一個網站能否讓更多人瀏覽有幾個重點:
  • 功能符合時下大眾需求
  • 畫面能夠第一時間吸引人
  • 友善的操作介面
  • 提供流暢的操作體驗而不會有頓感

    上述的每項牽涉的層面都非常廣泛,但是和今天要介紹的Bundle最息息相關的就屬第四項。網站要能夠提供流暢的操作體驗而不會讓使用者有頓感,其實就是在控制頁面響應能力;當使用者進入網頁或是點選某一個選單功能,結果就看到網頁凍結在那邊等著網站的回應,我想任何使用者都會逐漸感到不耐進而不想再繼續瀏覽下去。具所有開發人員的認知,一個頁面的響應時間不應該超過4秒,但是老實說這個4秒其實對於使用者來說是最大能夠忍受的極限,因此,不能將4秒作為網站響應時間的最終目標,而是應該將其作為危險指標。

     控制網頁響應時間的因素有很多,我大約羅列如下:
  • 後端/前端 程式沒有妥善控制資源(例如沒有關閉SqlConnection或無窮迴圈)
  • 沒有檢查好網站資源(例如使用者端向伺服器端要一張不存在的圖片)
  • 網頁太肥
  • 單一頁面需要向伺服器端索取多個資源檔

    在上述的因素中,其中的最後一項正是Bundle可以減緩的,為什麼只說可以減緩呢?因為Bundle僅能對於網站的JavaScript及CSS檔案做出處理,它的實際作用就是將網站上一堆的JavaScript或是CSS都打包成單一*.js或是*.css檔,如此一來,使用者端就不用開多個連線向伺服器端索取一堆的JavaScript以及CSS檔案了。這在某個程度上可以減緩索取多個資源檔所造成的連線損耗。

    使用者端發出一個Request給伺服器端,伺服器端在接收到這個Request之後,它會需要有一個執行緒去處理這個Request,為什麼要使用執行緒呢?這是因為網站本身的目的就是要能夠在同一時間盡可能的服務多個使用者的Request,為了達成這個目的,使用執行緒是勢在必行。其實,一台伺服器的執行緒個數是有限的,因為一個執行緒是會消耗掉近2MB的記憶體,而硬體資源不可能無限大但是使用者的數量卻可能無限上綱,為了能夠盡可能的服務多個使用者的Request,妥善的控制連線數也成了一個重要的課題。

     MVC 4所提供的Bundle的功能旨在打包JavaScript和CSS,除此之外,它還有一項更迷人的功能-壓縮。
    這裡所指的壓縮是指將原本很龐大的內容藉由將空白字元以及換行字元消滅掉之外,它還將JavaScript中的變數名稱替換成更簡單的a,b,c,d…這種單一字母,這是因為當字數越少則這個檔案相對來說就會更小,如此一來就做到了所謂實質上的壓縮功能。

    除了上述所探討到的執行緒問題之外,頻寬也是一個重要的指標,但是頻寬可能涉及到的層面很廣,除了JavaScript和CSS之外,當然免不了還有網頁的大小以及網頁上其它資源檔的大小,這些都是影響頻寬的因素,而Bundle不是萬靈丹,它僅能針對它能控制的JavaScript和CSS檔案的大小作文章之外,其餘部份它是愛莫能助的。

    接下來我們來看看App_Start資料夾底下的BundleConfig.cs檔案簡略內容:

    public class BundleConfig
    {
        // 如需 Bundling 的詳細資訊,請造訪 http://go.microsoft.com/fwlink/?LinkId=254725
        public static void RegisterBundles(BundleCollection bundles)
        {
            bundles.Add(new ScriptBundle("~/bundles/jquery").Include(
                        "~/Scripts/jquery-{version}.js"));

            bundles.Add(new ScriptBundle("~/bundles/jqueryui").Include(
                        "~/Scripts/jquery-ui-{version}.js"));

            bundles.Add(new ScriptBundle("~/bundles/jqueryval").Include(
                        "~/Scripts/jquery.unobtrusive*",
                        "~/Scripts/jquery.validate*"));

            // 使用開發版本的 Modernizr 進行開發並學習。然後,當您
            // 準備好實際執行時,請使用 http://modernizr.com 上的建置工具,只選擇您需要的測試。
            bundles.Add(new ScriptBundle("~/bundles/modernizr").Include(
                        "~/Scripts/modernizr-*"));

            bundles.Add(new StyleBundle("~/Content/css").Include("~/Content/site.css"));

            bundles.Add(new StyleBundle("~/Content/themes/base/css").Include(
                        "~/Content/themes/base/jquery.ui.core.css",
                        "~/Content/themes/base/jquery.ui.resizable.css",
                        "~/Content/themes/base/jquery.ui.selectable.css",
                        "~/Content/themes/base/jquery.ui.accordion.css",
                        "~/Content/themes/base/jquery.ui.autocomplete.css",
                        "~/Content/themes/base/jquery.ui.button.css",
                        "~/Content/themes/base/jquery.ui.dialog.css",
                        "~/Content/themes/base/jquery.ui.slider.css",
                        "~/Content/themes/base/jquery.ui.tabs.css",
                        "~/Content/themes/base/jquery.ui.datepicker.css",
                        "~/Content/themes/base/jquery.ui.progressbar.css",
                        "~/Content/themes/base/jquery.ui.theme.css"));
        }
    }
   上述程式碼我們可以摸索出一個簡單的處理模式;當你想要針對JavaScript進行打包的時候,你只需要將ScriptBundle物件實體化並加入到bundles物件中,而CSS檔案則是將StyleBundle物件加入到bundles中。唯一幾個比較特別的程式碼如下:

bundles.Add(new ScriptBundle("~/bundles/jquery").Include(
                        "~/Scripts/jquery-{version}.js"));
        在上述程式碼中我們可以看到有一個很醒目的東西: {version}。其實這個東西是一個特別的參數,而這是Bundle制定的,該參數的意思是指任何版本。jQuery是一個非常好用且熱門的JavaScript框架套件,以目前的網頁開發來說,不論是網頁設計師或是後端網站的程式設計師,皆要對這個框架套件非常熟才行,在過去,會jQuery是一件令人稱羨的事,而現在會jQuery卻成了必備的技能。jQuery從開始到現在已經有多個版本問世,如果每更新一次jQuery的新版就要去修改BundleConfig.cs,那豈不是太累人了,為此,便提供了像是{version}這種參數讓大家可以更方便的使用Bundle功能。

        更多的Bundle參數可以在這個網址中找到資源。

       我們現在就打開IE並且按下F12啟動檢測工具來看看Bundle的成效。

image

    咦!怎麼好像還是跑到原路徑去抓CSS和JavaScript檔案呢?其實,Bundle這項功能的啟動時機非常特別,它只有在非Debug模式下才能啟用,如果沒有改成非Debug模式,則Bundle則是毫無作用的。其目的很簡單,當你處於Debug模式時,表示你目前正在對該網站進行偵錯,而偵錯的目標很可能是某個自己撰寫的JavaScript,若這個檔案被Bundle壓縮的你完全看不懂,那要如何偵錯呢?

     接下來我們來將網站改成非Debug模式

image

    除了將原本的Debug改成Release之外,還要去修改網站的Web.config。(不是Views資料夾底下的那個歐)

<compilation debug="false" targetFramework="4.5"/>

   我們再來看一次網頁。(不偵錯啟動要按下: Ctrl+F5)

image

總結: Bundle是一個非常重要的功能,在MVC 4中,它的撰寫異常的簡單,為了這份簡單大家記得一定要去善用它歐。

2012年11月26日 星期一

ASP.Net MVC中的Filter

image

     MVC中的Filter對於整體開發來說總是有著畫龍點睛的效果;一個網站中的Filter並不會很多,但是每個Filter總是能夠完美的發揮它關鍵性的角色,它其實是一個很特別的東西,你可以決定將它套用到整個網站,或是細到某個Action。無論是使用MVC目前現成的Filter或是自行開發Filter,套用Filter其實在簡單不過,因為只需要將其當成一般的屬性標籤,直接將它放在某個Controller或是某個Action上頭就能達到效果。

     Filter如果是想要套用到整個網站,需進行Global的註冊行為,其位置就在App_Start資料夾底下的FilterConfig.cs中,我們打開FilterConfig.cs來看看裡面的內容。

    public class FilterConfig
    {
        public static void RegisterGlobalFilters(GlobalFilterCollection filters)
        {
            filters.Add(new HandleErrorAttribute());
        }
    }

    在靜態方法RegisterGlobalFilters中,僅有一行程式碼,該程式就是將HandleErrorAttribute這個MVC提供的Filter進行Global的註冊,亦即,HandleError這個Filter會套用到整個網站。

  在MVC的專案中,我們可以看到有一個資料夾名為:Filters,該資料夾裡面所放置的就是開發人員自行撰寫的Filter。在預設範本程式碼中,我們可以看到Filters資料夾裡面有一個InitializeSimpleMember.cs檔案,打開該檔案其程式碼如下:

   [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
   public sealed class InitializeSimpleMembershipAttribute : ActionFilterAttribute
   {

       private static SimpleMembershipInitializer _initializer;

       private static object _initializerLock = new object();

       private static bool _isInitialized;

       public override void OnActionExecuting(ActionExecutingContext filterContext)
       {
           // Ensure ASP.NET Simple Membership is initialized only once per app start
           LazyInitializer.EnsureInitialized(ref _initializer, ref _isInitialized, ref _initializerLock);
       }

       private class SimpleMembershipInitializer
       {
           public SimpleMembershipInitializer()
           {
               Database.SetInitializer(null);

               try
               {
                   using (var context = new UsersContext())
                   {
                       if (!context.Database.Exists())
                       {
                           // Create the SimpleMembership database without Entity Framework migration schema

                           ((IObjectContextAdapter)context).ObjectContext.CreateDatabase();

                       }
                   }

                   WebSecurity.InitializeDatabaseConnection("DefaultConnection", "UserProfile", "UserId", "UserName", autoCreateTables: true);
               }

               catch (Exception ex)
               {
                   throw new InvalidOperationException("The ASP.NET Simple Membership database could not be initialized. For more information, please see http://go.microsoft.com/fwlink/?LinkId=256588", ex);

               }
           }
       }
   }

       該程式碼的目的很簡單:當資料庫檔案不存在的時候,自動建立一個資料庫檔案供會員管理使用。由此解了我們前幾篇曾經提到的疑問,為什麼按下了網頁上的註冊之後,在App_Data的實體路徑下會多出兩個和資料庫相關的檔案。其原由就是出在這個Filter。而這個Filter套用在AccountController。

    [Authorize]
    [InitializeSimpleMembership]
    public class AccountController : Controller
    {

            …省略…
    }

        所以當使用者按下了會與AccountController產生戶動的任何頁面元件時,這個Filter就會自動啟動並進行一連串的動作。其實自行撰寫MVC的Filter很簡單,但是要先來瞭解MVC的Filter其內部的運作模式,這樣一來就不會完全摸不著頭緒了。

      其實,一個Action從Request被Router導過來一直到Action處理完畢並將最後一棒交遞給View,其流程圖如下:


actionfilter2_thumb
 
      我們可以看到幾個較特別的步驟
  • OnActionExecuting(Action處理前)
  • OnActionExecuted(Action處理後)
  • OnResultExecuting(結果View處理前)
  • OnResultExecuted(結果View處理後)

    在Filter的開發上,主要有四大類,而上述的四個步驟就是其中兩大類要處理的目標。
  • Authorization(針對驗證/授權處理)
  • Action(針對Action處理時機作特製: Executing/Executed)
  • Result(針對View處理時機作特製: Executing/Executed)
  • Exception(針對例外作處理)

    這四大類各有各自要實作的介面,下圖描述那一類該實作那種介面:

actionfilter_2
 
     要開發一個自定義的Filter,僅需繼承ActionFilter再依自己要針對四大類那一類進行處理就去實作該類的介面。舉個例來說,如果我想要自行定義一個驗證相關的ActionFilter,則程式碼概略如下:

public class LogAttibute:ActionFilterAttribute,IActionFilter
{
     public override void OnActionExecuting(ActionExecutingContext filterContext)
     {  …省略… }
}

   而要套用這個自定義的Filter很簡單;若要是想要套用在HomeController上,則簡略程式碼如下:

[LogAttribute]
public class HomeController : Controller
{ …省略…}

    若想要將這個自定義的Filter套用到全網站,則需要App_Start資料夾底下的FiltersConfig.cs中加入以下程式碼:
 
    public class FilterConfig
    {
        public static void RegisterGlobalFilters(GlobalFilterCollection filters)
        {
            filters.Add(new HandleErrorAttribute());

            filters.Add(new LogAttribute());
        }
    }

總結:在MVC中Filter是一個不可或缺的工具,善用它可以事半功倍。
 
  

2012年11月25日 星期日

ASP.Net MVC中的OAuth

image

         撰寫任何公開的系統都需要有適當的驗證/授權機制作為安全性控管。在ASP.Net MVC 4中,提供了相當完整的機制,除了預設的表單驗證之外,還加入了一個令人振奮的特性-OAuth。而本篇文章將以大家最常會使用到的Facebook作為OAuth的範例作說明。

       打開App_Start底下的AuthConfig.cs這個檔案來看,我們會發現到裡面的內容幾乎全部都被註解了

    public static class AuthConfig
    {

        public static void RegisterAuth()
        {

            // 若要讓此網站的使用者使用其他網站 (如 Microsoft、Facebook 和 Twitter) 的帳戶登入,

            // 您必須更新此網站。如需詳細資訊,請造訪 http://go.microsoft.com/fwlink/?LinkID=252166

            //OAuthWebSecurity.RegisterMicrosoftClient(

            //    clientId: "",

            //    clientSecret: "");

            //OAuthWebSecurity.RegisterTwitterClient(

            //    consumerKey: "",

            //    consumerSecret: "");

            //OAuthWebSecurity.RegisterFacebookClient(

            //    appId: "",

            //    appSecret: "");

            //OAuthWebSecurity.RegisterGoogleClient();
        }
    }
 
從註解的內容我們發現到一個事實,RegisterAuth這個靜態方法的內容具有四個很快就被人觀注的四個東西:
  • RegisterMicrosoftClient
  • RegisterTwitterClient
  • RegisterFacebookClient
  • RegisterGoogleClient
     從上述所列的名稱可以更進一步推敲,這似乎是在針對目前時下最廣為大家使用的四個社群網站:Microsoft, Twitter, Facebook, Google。再來我們回到這些方法的類別: OAuth。
OAuth(开放授权)是一个开放标准,允许用户让第三方应用访问该用户在某一网站上存储的私密的资源(如照片,视频,联系人列表),而无需将用户名和密码提供给第三方应用。
      上述是我引用Wiki對於OAuth的描述,老實說,我還真是看不太懂它的內容。吐舌頭不過我們可以使用另一種較快速吸收的方式來解釋它:利用目前現成的社群網站所提供的驗證服務來對使用者進行驗證。當然,OAuth所提供的能力當然不僅僅於此,甚至可以說,用了它之後就可以對使用者提供更多的服務,例如像是可以取得使用者在Facebook上的一些資訊,而這些資訊可以讓網頁呈現的更豐富也更能讓使用者快速上手,畢竟使用者已經很習慣Facebook的操作介面或是資料呈現,如果自己所提供的網站與Facebook有更多的重疊性,就能提高使用者的接受度。

    在本篇文章中,主要針對的部份是驗證/授權,因此,所需要涵蓋的範圍也比較廣,除了要介紹AuthConfig.cs中的內容之外,也必需要介紹Controllers資料夾底下的AccountController。首先先來看一下AccountController在處理表單驗證的部份。

[Authorize]
    [InitializeSimpleMembership]
    public class AccountController : Controller
    {
        //
        // GET: /Account/Login

        [AllowAnonymous]
        public ActionResult Login(string returnUrl)
        {

            …省略…
        }

        //
        // POST: /Account/Login
        [HttpPost]
        [AllowAnonymous]
        [ValidateAntiForgeryToken]
        public ActionResult Login(LoginModel model, string returnUrl)
        {

            …省略…
        }

        //
        // POST: /Account/LogOff
        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult LogOff()
        {
            …省略…}

        //
        // GET: /Account/Register
        [AllowAnonymous]
        public ActionResult Register()
        {
           …省略…        
        }

        //
        // POST: /Account/Register
        [HttpPost]
        [AllowAnonymous]
        [ValidateAntiForgeryToken]
        public ActionResult Register(RegisterModel model)
        {
            …省略…
        }

        //
        // POST: /Account/Disassociate
        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Disassociate(string provider, string providerUserId)
        {
            …省略…
        }

        //
        // GET: /Account/Manage
        public ActionResult Manage(ManageMessageId? message)
        {
            …省略…        
        }

        //
        // POST: /Account/Manage
        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Manage(LocalPasswordModel model)
        {
            …省略…
        }

}

        AccountController主要有兩大部份,一個是針對表單驗證的部門,另一個則是針對OAuth驗證的部份,而上述程式碼僅是針對表單驗證的部份。在表單驗證的部份一共有下列方法:
  • Login(登入)
  • LogOff(登出)
  • Register(註冊)
  • Disassociate(刪除帳號與第三方網站的關聯)
  • Manage(變更帳戶密碼)

       某些方法有Get與Post分別,所以會在上述程式碼中看到某幾個方法會重覆出現,但是該重覆的方法會有一個很明顯的特徵就是其方法上頭會有一個[HttpPost]的屬性標籤。MVC4與MVC3在AccountController上有很大的不同,除了多了另一個部份是在處理OAuth之外,方法的屬性標籤也變得更多了,茲羅列MVC4在AccountController用到那些屬性標籤:
  • Authorize(需通過授權才能使用)
  • InitializeSimpleMembership(初始化帳戶管理相關機制)
  • AllowAnonymous(在執行Action時,略過驗證/授權的處理)
  • ValidateAntiForgeryToken(避免CSRF攻擊)
  • ChildActionOnly(不允許使用者直接輸入Url呼叫)

       由上面描述可以看到,在AccountController中有許多的屬性標籤都和安全性息息相關,這樣一來,對於安全性的部份開發人員僅需在自己的Action加上適當的屬性標籤就能具有基本的安全性,不再像過去需要撰寫太多的程式碼在處理安全性相關的議題。

        其中要注意某些屬性標籤對於Get和Post的Http操作是有很大的限制的,舉例來說ValidateAntiforgeryToken這個屬性標籤就無法和Get使用,因此,使用ValidateAntiForgeryToken時,務必要在該Action上加上HttpPost屬性標籤,否則會發生錯誤。

        除了上述的方法概述以及屬性標籤之外,其中較為特別的是方法的內容中有個極其顯眼的類別:WebSecurity,這個類別就像是過去的MemberShip,它們的任務和功用都很單純,全都是帳戶的處理。

        介紹完表單驗證之後,我們再回到本篇文章的主題: OAuth。其實MVC的範本中就有相當豐富的OAuth範例,其中牽涉到的部份除了App_Start下的AuthConfig.cs外,再來就是Controllers中的AccountController了,在AuthConfig.cs中,主要在處理的是與第三方驗證單位的設定,觀其四個方法可以知道分別是不同的第三方單位,僅需選擇其中一個使用就可以了。
  
        點擊這個網址可以連結到Facebook的開發人員中心,但使用前需要進行一連串的註冊動作。(使用前需有Facebook帳號)

        步驟1: 輸入Facebook開發人員專屬特區的名字和名稱空間(這個可不用輸入)
image
       
         步驟2: 輸入安全驗證碼

                 image

         步驟3: 取得"應用程式ID/ API鑰匙",並設定AuthConfig.cs的參數

image
             
          而上面的"應用程式ID / API鑰匙"以及”應用程式密鑰”,記得要複製下來準備貼到App_Start底下的AuthConfig中。

image
   
        將”應用程式ID/ API鑰匙”填入appId(字串型式);將”應用程式密鑰”填入appSecret中(字串型式)。做完這堆事情之後,還得前往Facebook的應用開發中心打開WebSite With Facebook Login這項功能。

         步驟4: 啟用WebSite With Facebook Login

image
 
       點擊編輯應用程式,進入下一個面頁開啟WebSite With Facebook Login功能。

image

        萬事具備,再來就可以直接執行MVC專案並進入登入頁面。

image

        點選右側的Facebook登入按鈕。

image

image

image

總結:OAuth在MVC中相當的方便就能夠達成,可以多多參考MVC中有關註冊的範本。

2012年11月22日 星期四

ASP.Net MVC中的Route

image

        在之前的文章中曾經探討過各個MVC專案中的資料夾內容,當時對於App_Start這個資料夾僅是淺淺的帶過去,而本篇文章開始將會一個個介紹這個資料夾中的所有檔案,原因無它,因為這個資料夾中的所有檔案對於未來開發MVC專案來說都是極為重要的檔案。

        在MVC中,除去View Model Controller這幾位主角之外,另外一位舉足輕重的就是Routing。其道理很簡單,因為一個使用者的Request要如何正確且精準的被服務對於網站來說是極其重要的,總不能使用者希望看到A網頁但是網站卻回傳一個B網頁給使用者。

    public class RouteConfig
    {
        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

            routes.MapRoute(
                name: "Default",
                url: "{controller}/{action}/{id}",
                defaults: new { controller = "Home", action = "Index", id =     UrlParameter.Optional }
            );
        }
    }
在App_Start這個資料夾中的RouteConfig.cs,其內容如上面程式所述,這是一個極短的程式碼。就讓我們一行行的解析這段程式碼所達成的功能以及其中的關竅。

      RouteConfig這個類別裡面只有一個方法:RegisterRoutes,這是一個靜態方法;亦即,使用這個方法的物件不需要特別將這個類別實體化,可以直接以 RouteConfig.RegisterRoutes呼叫它。究其方法內容,我們可以將其分成兩個部份來看,一者為規避,另一者為設定。

routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

這段程式碼在描述: 任何以axd結尾並且後續任何串接路徑都不予以理會。舉個例來說:
      http://localhost/Hello.axd/world/You/
       上述這段網址正好符合了{resource}.axd/{*pathInfo}這種Url格式。其實,無論是resource或是pathInfo,這兩個其實都是一個變數而已,本身並不具有什麼特別的意義;以上述的Url來說,我們可以看成
       resource => Hello
      *pathInfo => world/You/

      pathInfo這個變數前面的*號代表著無論有多少個,所以Hello後面無論有多少個都會被視為pathInfo的一部份。再來我們要探討為什麼要忽略符合這個格式的Request呢?其實,在IIS上面除了MVC的網站專案之外,其餘還有非常多的網站也會掛在上面,因此,如果沒有特別去過濾勢必會造成這些網站的Request被Route給誤導了。

      再來就是談談Routing的設定, RegisterRoutes這個方法第二個部份就是在進行路由的設定。

routes.MapRoute(
                name: "Default",
                url: "{controller}/{action}/{id}",
                defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
            );
上面這段程式碼的功能其實很簡單,就是在比對符合{controller}/{action}/{id}這種Url格式的Request該被那個Controller的那個Action服務。舉例來說:

     http://localhost/Home/Index

    這段的解析如下:
    controller => Home
    action => Index

   大家會很好奇,那id去那了?可以翻遍了整個Url來看就是整不到可以與id對上的,難道id不存在?
   是的,就我上面所舉的那個Url來說,id確實不存在。再特別提到一點,上面所舉的Url的例子其實與 http://localhost/ 是完全等價的。其原因我們再繼續看MapRoute這個API的第三個參數:

defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }

   defaults這個參數在描述著,若Url沒有提供controller則預設會帶入"Home"作為預設值,而action的預設值則為"Index",如此一來就解釋了為什麼我說 http://localhost/Home/Index 會與 http://localhost  等價了。再來,還要再解釋id的問題。其實,在defaults這個參數中已經言明 id其實是個選擇性的(Optional),因此,id可有可沒有,這完全都不會影響到Url的判斷。

總結:Route對於MVC來說是一門很重要的必修課,因為,再好的MVC設計如果無法讓使用者的Request正確導向正確的Controller,這很容易就會造成使用者明明要的是A網頁,但是網站全傳回B網頁給他。

2012年11月21日 星期三

ASP.Net MVC中的Model

image

        打開MVC預設範例的Models資料夾可以看到內部僅有一個副檔名為CS的檔案。依照我們先前所說,MVC中的Model其實很單純,它僅將眼界放在自己的任務上,它不會去理會其它Model或是Controller,對於自己的職責範圍它總是盡力作到,而外面的世界與它全然無關。正因為這樣的特性,我們可以說Model本身是一個獨立的最小單元,它僅負責它自己該負責的部份,它不會涉入非它責任範圍的任何事項。


       在Model的工作中,我們可以簡單歸納成下列幾項:
  • 資料存取
  • 商業邏輯

      在第一項資料存取上,Model對於任何資料的存取或是擷取都是由它負責來完成,可別小看這項工作,有的時候它可能需要從遠端伺服器上下載一份文件檔或是傳上文件檔。大多的範例都是以資料庫的資料存取作為範例,不諱言,這的確也是MVC網站絕大部份時間所需要去做的事情。也因此,在網路上查詢MVC的Model時總是會在網路上找到許多有關資料庫存取的相關文章,其中較多的就是微軟努力在推行的ORM框架:Entity Framework。

    Entity Framework目前已經推行到Entity Framework 5了,這款微軟自家的ORM框架相較於由Java轉生過來的NHibernate來說是比較輕量級一點的,但是功能上當然也比NHibernate少了一些。總的來說,其實我個人還是挺看好微軟這套ORM框架的,除了它有特定的團隊在維護升級之外,它提供了許多誘人的特性,如下列:
  • Migration
  • 支援三種模式: Database First, Model First, Code First
  • Linq語法
  • Entity Framework 5在效能上的提升(有環境上的限制)
  • Data Annotation
  • Entity Relation的Fluent設定

   Entity Framework在Migration上做得很徹底,而這也是我很喜歡的一個功能,在過去開發有關資料庫的系統,如果資料表欄位需要作任何異動,對於系統來說都是一件麻煩的事情;首先,你必須要進入Database中並且進行資料表的修改,這是一件令人非常頭痛的事情。在Entity Framework上,倘若目前仍處理開發設計階段,僅需在Package Console中下幾行指令,就能很快速又簡單的修改資料表的欄位,這真的是一件利多。(我的筆電很爛,通常開了Visual Studio又要再開啟SSMS,這個時候記憶體就不太夠了)

      在前幾代的Entity Framework僅支援Database First這種特性,但是隨著版本的演進慢慢的開始支援了Model First;亦即,開發人員可以直接在Visual Studio拉模型到設計區塊中,並且設定好一些屬性就能夠快速建立及產生一個資料庫的所有資料表,這在當時的確相當驚人,因為這改變了過去系統的開發流程,從先進資料庫設定一堆資料表到了現在可以從Visual Studio中設定。但這樣仍無法滿足所有的開發人員。很多時候如果系統並不是那麼巨型,且客戶要求的時程非常的短促,這通常會將許多SA和SD的時間壓縮,會很迫切的需要快速轉入實做中,若這個時候還需要在那邊慢慢拉模型,這對於性子較急的開發人員或專案經理來說,簡直就是地獄

       除了時程問題之外,考慮另一個議題:複用性。許多時候,團隊在開發系統上會採用領域模型分析,亦即,會先將本次要開發的系統其最關鍵核心的部份建構出Domail Model(領域模型),而這個領域模型可能在下一次的專案或是延伸專案中會被使用到,這便產生了複用的需求,在過去的Entity Framework中,無論是Database First抑或是Model First都無法滿足此類要求,因為這些模式的產物很難完全移轉到其它專案身上,縱使可以移轉也需要再進行一番加工才行。為此,Code First這種模式產生了!

      Code First一如其名,這種模式就是什麼都不用管,直接看著Domail Model圖將上面的領域物件寫成一個個的類別到系統中,不需要先進資料庫也不需要打開Entity Framework專屬的設計畫面,只要很簡單的新增幾個類別就結束了。接下來我們就來實際欣賞MVC專案中Code First的真實樣貌。

     打開AccountModels.cs這個檔案,其內容如下:
 
namespace MvcApplication1.Models
{
    public class UsersContext : DbContext
    {
        public UsersContext()
            : base("DefaultConnection")
        {
        }

        public DbSet UserProfiles { get; set; }
    }

    [Table("UserProfile")]
    public class UserProfile
    {
        [Key]
        [DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
        public int UserId { get; set; }
        public string UserName { get; set; }
    }

    public class RegisterExternalLoginModel
    {
        [Required]
        [Display(Name = "使用者名稱")]
        public string UserName { get; set; }

        public string ExternalLoginData { get; set; }
    }

    public class LocalPasswordModel
    {
        [Required]
        [DataType(DataType.Password)]
        [Display(Name = "目前密碼")]
        public string OldPassword { get; set; }

        [Required]
        [StringLength(100, ErrorMessage = "{0} 長度至少必須為 {2} 個字元。", MinimumLength = 6)]
        [DataType(DataType.Password)]
        [Display(Name = "新密碼")]
        public string NewPassword { get; set; }

        [DataType(DataType.Password)]
        [Display(Name = "確認新密碼")]
        [Compare("NewPassword", ErrorMessage = "新密碼與確認密碼不相符。")]
        public string ConfirmPassword { get; set; }
    }

    public class LoginModel
    {
        [Required]
        [Display(Name = "使用者名稱")]
        public string UserName { get; set; }

        [Required]
        [DataType(DataType.Password)]
        [Display(Name = "密碼")]
        public string Password { get; set; }

        [Display(Name = "記住我?")]
        public bool RememberMe { get; set; }
    }

    public class RegisterModel
    {
        [Required]
        [Display(Name = "使用者名稱")]
        public string UserName { get; set; }

        [Required]
        [StringLength(100, ErrorMessage = "{0} 長度至少必須為 {2} 個字元。", MinimumLength = 6)]
        [DataType(DataType.Password)]
        [Display(Name = "密碼")]
        public string Password { get; set; }

        [DataType(DataType.Password)]
        [Display(Name = "確認密碼")]
        [Compare("Password", ErrorMessage = "密碼和確認密碼不相符。")]
        public string ConfirmPassword { get; set; }
    }

    public class ExternalLogin
    {
        public string Provider { get; set; }
        public string ProviderDisplayName { get; set; }
        public string ProviderUserId { get; set; }
    }
}
在上述程式碼中,一共有7個類別在裡面,其中第2個是在定義資料表,而最上面那一個是Entity Framework中一個很重要的類別。
    (在一般系統開發慣例來說,通常是不會把這麼多個類別塞在一個檔案中,大多時候都是一個類別一個檔案)

   這下可有趣了,這麼多個類別怎麼只有一個類別是在定義資料表,而其它類別呢?它們又是在做些什麼用途?我們先來看看前面兩個類別在做些什麼,再來討論下面那些類別。
    [Table("UserProfile")]
    public class UserProfile
    {
        [Key]
        [DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
        public int UserId { get; set; }
        public string UserName { get; set; }
    }

這個類別所定義的資料表很簡單,它僅有兩個欄位: UserId以及UserName。其中比較特別的部份是UserId的上頭有兩個標籤屬性,一者為[Key]而另一個則看起來複雜的樣子。其實望文生義可以很快速的瞭解到,這兩個標籤在說明UserId對於這張表來說是主索引鍵,而第二個標籤在說明它的值是自動產生的,而產生出來的值會依照其屬性的資料型別int作值的遞增;也就是說,第一筆資料的UserId是0而第二筆是1如此類推之。

      類別上的標籤屬性就更加讓人容易明白,它就很直白的說明這是一張資料表,而其資料表的名稱為UserProfile(註:資料表的名稱可以和類別名稱不一樣)。這些標籤屬性其實有個專有名詞: Data Annotation
    public class UsersContext : DbContext
    {
        public UsersContext()
            : base("DefaultConnection")
        {
        }

        public DbSet UserProfiles { get; set; }
    }

任何一個ORM的框架都勢必會有一個本文(Context),大家可以把它當作成一個容器,一個系統可以有多個Context但是這些Context彼此互相獨立,其目的在於確保資料的一致性和可追蹤的特性。上面這個類別繼承了DbContext,一旦它繼承了這個類別它就具有Code First的Context所有應有的能力。在上述程式碼中可以看到這個類別中有一個屬性:

      public DbSet<UserProfile> UserProfile {get; set;}

     可以將其解讀成這個資料庫中有一張資料表:UserProfiles。而建構式中傳遞給父類別建構子的字串其實我們可以在Web.config(註:不是Views資料夾中的Web.config)中找到:

 <connectionStrings>
    <add name="DefaultConnection" connectionString="Data Source=(LocalDb)\v11.0;Initial Catalog=aspnet-MvcApplication1-20121118193713;Integrated Security=SSPI;AttachDBFilename=|DataDirectory|\aspnet-MvcApplication1-20121118193713.mdf" providerName="System.Data.SqlClient" />
  </connectionStrings>
 
   意思就是說明這個Context的資料庫連線字串為Web.config中所定義的。

    話題回到之前所討論的問題:其它類別是在做什麼的?
    其實,Model在系統開發上有好幾種,像是這種負責資料庫存取的Model我們會稱其為Data Access Object(資料存取物件),而為了讓View能夠輕鬆用來呈現畫面的就稱之為View Model(視圖物件),若僅是單傳用來在函式之間傳遞的則為Data Transfer Object(資料傳遞物件)。在AccountModels.cs檔案中的其它類別: RegisterExternalLoginModel, LocalPasswordModel, LoginModel, RegisterModel, ExternalLogin都屬於View Model。

總結:本章大略講解了AccountModels.cs這個檔案中的內容,但並非深入到Model的精髓,之後的文章會再深入到Model的開發技巧包括常用的設計樣式: 倉儲樣式, 單一工作樣式。