由于ASP.NET Web API具有與ASP.NET MVC類似的編程方式,再加上目前市面上專門介紹ASP.NET Web API 的書籍少之又少(我們看到的相關內容往往是某本介紹ASP.NET MVC的書籍“額外奉送”的),以至于很多人會覺得ASP.NET Web API僅僅是ASP.NET MVC的一個小小的擴展而已,自身并沒有太多“大書特書”的地方。而真實的情況下是:ASP.NET Web API不僅僅具有一個完全獨立的消息處理管道,而且這個管道比為ASP.NET MVC設計的管道更為復雜,功能也更為強大。雖然被命名為“ASP.NET Web API”,但是這個消息處理管道卻是獨立于ASP.NET平臺的,這也是為什么ASP.NET Web API支持多種寄宿方式的根源所在。[本文已經同步到《How ASP.NET Web API Works?》]
為了讓讀者朋友們先對ASP.NET Web API具有一個感性認識,接下來我們以實例演示的形式創建一個簡單的ASP.NET Web API應用。這是一個用于實現“聯系人管理”的單頁Web應用,我們以Ajax的形式調用Web API實現針對聯系人的CRUD操作。[源代碼從這里下載]
目錄
構建解決方案
定義Web API
以Web Host方式寄宿Web API
以Self Host方式寄宿Web API
利用HttpClient調用Web API
創建一個“聯系人管理器”應用
一、構建解決方案
Visual Studio為我們提供了專門用于創建ASP.NET Web API應用的項目模板,借助于此項目模板提供的向導,我們可以“一鍵式”創建一個完整的ASP.NET Web API項目。在項目創建過程中,Visual Studio會自動為我們添加必要的程序集引用和配置,甚至會為我們自動生成相關的代碼,總之一句話:這種通過向導生成的項目在被創建之后其本身就是一個可執行的應用。
對于IDE提供的這種旨在提高生產效率的自動化機制,我個人自然是推崇的,但是我更推薦讀者朋友們去了解一下這些自動化機制具體為我們做了什么?做這些的目的何在?哪些是必需的,哪些又是不必要的?正是基于這樣的目的,在接下來演示的實例中,我們將摒棄Visual Studio為我們提供的向導,完全在創建的空項目中編寫我們的程序。這些空項目體現在如右圖所示的解決方案結構中。
如右圖所示,整個解決方案一共包含6個項目,上面介紹的作為“聯系人管理器”的單頁Web應用對應著項目WebApp,下面的列表給出了包括它在內的所有項目的類型和扮演的角色。
- ·Common:這是一個空的類庫項目,僅僅定義了表示聯系人的數據類型而已。之所以將數據類型定義在獨立的項目中,只要是考慮到它會被多個項目(WebApi和ConsoleApp)所使用。
- WebApi:這是一個空的類庫項目,表現為HttpController類型的Web API就定義在此項目中,它具有對Common的項目引用。
- WebHost:這是一個空的ASP.NET Web應用,它實現了針對ASP.NET Web API的Web Host寄宿,該項目具有針對WebApi的項目引用。
- SelfHost:這是一個空的控制臺應用,旨在模擬ASP.NET Web API的Self Host寄宿模式,它同樣具有針對WebApi的項目引用。
- WebApp:這是一個空的ASP.NET Web應用,代表“聯系人管理器”的網頁就存在于該項目之中,至于具體的聯系人管理功能,自然通過以Ajax的形式調用Web API來完成。
- ConsoleApp:這是一個空的控制臺應用,我們用它來模擬如何利用客戶端代理來實現對Web API的遠程調用,它具有針對Common的項目引用。
二、定義Web API
在正式定義Web API之前,我們需要在項目Common中定義代表聯系人的數據類型Contact。簡單起見,我們僅僅為Contact定義了如下幾個簡單的屬性,它們分別代表聯系人的ID、姓名、聯系電話、電子郵箱和聯系地址。
1: public class Contact
2: {
3: public string Id { get; set; }
4: public string Name { get; set; }
5: public string PhoneNo { get; set; }
6: public string EmailAddress { get; set; }
7: public string Address { get; set; }
8: }
表現為HttpController的Web API定義在WebApi項目之中,我們一般將ApiController作為繼承的基類。ApiController定義在“System.Web.Http.dll”程序集中,我們可以在目錄“%ProgramFiles%\Microsoft ASP.NET\ASP.NET Web Stack 5\Packages\”中找到這個程序集。具體來說,該程序集存在于子目錄“Microsoft.AspNet.WebApi.Core.5.0.0\lib\net45”中。
Web API體現在如下所示的ContactsController類型中。在該類型中,我們定義了Get、Post、Put和Delete這4個Action方法,它們分別實現了針對聯系人的查詢、添加、修改和刪除操作。Action方法Get具有一個表示聯系人ID的可缺省參數,如果該參數存在則返回對應的聯系人,否則返回整個聯系人列表。由于ASP.NET Web API默認實現了Action方法與HTTP方法的映射,所以方法名也體現了它們各自所能處理請求必須采用的HTTP方法。
1: public class ContactsController: ApiController
2: {
3: static List<Contact> contacts;
4: static int counter = 2;
5:
6: static ContactsController()
7: {
8: contacts = new List<Contact>();
9: contacts.Add(new Contact { Id = "001", Name = "張三",
10: PhoneNo = "0512-12345678", EmailAddress = "zhangsan@gmail.com",
11: Address = "江蘇省蘇州市星湖街328號" });
12: contacts.Add(new Contact { Id = "002", Name = "李四",
13: PhoneNo = "0512-23456789", EmailAddress = "lisi@gmail.com",
14: Address = "江蘇省蘇州市金雞湖大道328號" });
15: }
16:
17: public IEnumerable<Contact> Get(string id = null)
18: {
19: return from contact in contacts
20: where contact.Id == id || string.IsNullOrEmpty(id)
21: select contact;
22: }
23:
24: public void Post(Contact contact)
25: {
26: Interlocked.Increment(ref counter);
27: contact.Id = counter.ToString("D3");
28: contacts.Add(contact);
29: }
30:
31: public void Put(Contact contact)
32: {
33: contacts.Remove(contacts.First(c => c.Id == contact.Id));
34: contacts.Add(contact);
35: }
36:
37: public void Delete(string id)
38: {
39: contacts.Remove(contacts.First(c => c.Id == id));
40: }
41: }
簡單起見,我們利用一個靜態字段(contacts)表示存儲的聯系人列表。當ContactsController類型被加載的時候,我們添加了兩個ID分別為“001”和“002”的聯系人記錄。至于實現聯系人CRUD操作的Action方法,我們也省略了必要的驗證,對于本書后續的演示的實例,我們基本上也會采用這種“簡寫”的風格。
三、以Web Host方式寄宿Web API
我們在上面已經提到過了,雖然被命名為ASP.NET Web API,但是其核心的消息處理管道卻是獨立于ASP.NET平臺的,所以我們可以對相同的Web API實施不同的寄宿方式。寄宿的本質就是利用一個具體的應用程序為Web API提供一個運行的環境,并最終解決“請求的接收和響應的回復”問題。作為寄宿的一種主要形式,Web Host就是創建一個ASP.NET Web應用作為Web API的宿主。
采用Web Host方式寄宿Web API的宿主程序WebHost是一個空的ASP.NET應用。除了讓它引用定義ContactsController的WebApi項目之外,我們還需要為其添加如下這些必需的程序集引用。除了程序集“System.Net.Http.dll”(它屬于.NET Framework 原生的程序集)之外,其余3個均可以在目錄“%ProgramFiles%\Microsoft ASP.NET\ASP.NET Web Stack 5\Packages\”中找到。
- System.Web.Http.dll(\ Microsoft.AspNet.WebApi.Core.5.0.0\lib\net45\)
- System.Net.Formatting.Http.dll(\Microsoft.AspNet.WebApi.Client.5.0.0\lib\net45\)
- System.Web.Http.WebHost.dll(\Microsoft.AspNet.WebApi.WebHost.5.0.0\lib\net45\)
- System.Net.Http.dll
與ASP.NET MVC一樣,如果采用Web Host的方式來寄宿Web API,ASP.NET自身的路由系統會成為接收請求的第一道屏障。在將請求遞交給ASP.NET Web API自己的消息處理管道之前,路由系統會解析出當前請求訪問的目標HttpController和Action的名稱。我們需要做的就是根據需求注冊相應的路由,這也是采用Web Host寄宿方式所需的唯一操作。
我們在WebHost項目中添加一個Global.asax文件,并按照如下的形式在其Application_Start方法中注冊了一個模板為“api/{controller}/{id}”的路由。此模板由3部分組成,靜態文本“api”表示其前綴,后面是兩個路由參數。前者({controller})表示目標HttpController的名稱,后者({id})可以映射為目標Action方法的同名參數(比如ContractsController的Get方法的參數id),這是一個可以缺省的路由參數(RouteParameter.Optional)。
1: public class Global : System.Web.HttpApplication
2: {
3: protected void Application_Start(object sender, EventArgs e)
4: {
5: GlobalConfiguration.Configuration.Routes.MapHttpRoute(
6: Name : "DefaultApi",
7: routeTemplate : "api/{controller}/{id}",
8: defaults : new { id = RouteParameter.Optional });
9: }
10: }
如上面的代碼片斷所示,路由注冊是通過調用代表全局路由表的HttpRouteCollection對象的擴展方法MapHttpRoute來完成的。GlobalConfiguration的靜態屬性Configuration返回一個代表當前配置的HttpConfiguration對象,全局路由表就注冊在它的Routes屬性上。
如果你了解ASP.NET MVC的路由注冊,可能覺得奇怪:注冊路由的模板中并沒有表示目標Action的路由參數,ASP .NET Web API如何根據請求確定哪個Action方法應該被調用呢?答案其實很簡單:它能根據請求采用HTTP方法來確定目標Action方法。當然,在注冊路由模板中提供代表Action名稱的路由參數({action})也是支持的。
在默認情況下,通過Visual Studio(VS 2012或者VS 2013,本書采用的是后者)創建的Web應用總是使用IIS Express作為服務器,它會自動為我們指定一個可用的端口號。為了更好地模擬真實發布環境,同時避免“跨域資源共享”帶來的困擾,我們采用本地IIS作為服務器。如下圖所示,WebHost項目在IIS中映射的Web應用采用的URL為“http://localhost/webhost”。
實際上到此為止,Web API的Web Host寄宿工作就已經完成,我們可以利用瀏覽器來調用寄宿的Web API來判斷寄宿工作是否成功。由于瀏覽器在默認情況下訪問我們在地址欄中輸入的地址總是采用HTTP-GET請求,所以我們只能利用它來調用支持HTTP-GET的Action方法,即定義在ContactsController中的Get方法。
根據我們注冊的路由,如果我們訪問目標地址“http://localhost/webhost/api/contacts”可以獲得所有聯系人列表;如果目標地址為“http://localhost/webhost/api/contacts/001”,則可以得到ID為“001”的聯系人信息,右圖證實了這一點。
從右圖可以看到,我們采用的瀏覽器為Chrome,獲取的聯系人列表總是表示為XML,這是為什么呢?在前面介紹REST的時候,我們曾經提及一種旨在識別客戶端期望的資源表示形式并被稱為“內容協商”的機制,它可以根據請求攜帶的相關信息來判斷客戶端所期望的響應資源表現形式。
對于ASP.NET Web API來說,它會優先利用請求報頭“Accept”攜帶的媒體類型來確定響應內容采用的表現形式。如下所示的是Chrome訪問“http://localhost/webhost/api/contacts/001”發送請求的內容,它之所以會得到以XML表示的響應是因為“Accept”報頭指定的媒體類型列表中只有“application/xml”被ASP.NET Web API支持。如果我們采用IE,請求的“Accept”報頭將攜帶不同的媒體類型列表,我們實際上會得到以JSON格式表示的響應結果。
1: GET http://localhost/webhost/api/contacts/001 HTTP/1.1
2: Host: localhost
3: Connection: keep-alive
4: Cache-Control: max-age=0
5: Accept: text/html,application/xhtml+xml,application/xml ;q=0.9,image/webp,*/*;q=0.8
6: User-Agent: Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.63 Safari/537.36
7: Accept-Encoding: gzip,deflate,sdch
8: Accept-Language: en-US,en;q=0.8,zh-CN;q=0.6,zh-TW;q=0.4
為了進一步驗證并演示ASP.NET Web API的內容協商機制,我們現在改用Fiddler來發送調用Web API的HTTP請求。如左圖所示,我們利用Fiddler發送了一個針對目標地址“http://localhost/webhost/api/contacts/001”的HTTP-GET請求,并添加了一個值為“application/json”的“Accept”報頭,請求發送之后確實得到了以JSON格式表示的聯系人列表。
支持PUT和DELETE請求
在定義ContactsController的時候,我們嚴格按照RESTful Web API關于“使用標準的HTTP方法”的指導方針,分別采用GET、POST、PUT和DELETE作為獲取、創建、修改和刪除聯系人的操作所支持的HTTP方法。但是IIS在默認情況下并不提供針對 PUT和DELETE請求的支持。
如右圖所示,我們利用Fiddler發送了一個針對地址“http://localhost/webhost/api/contacts/001”的HTTP-DELETE請求,旨在刪除ID為“001”的聯系人。但是遺憾的是,我們得到了一個狀態為“405,Method Not Allowed”的響應,意味著服務端并不支持HTTP-DELETE方法。
IIS拒絕PUT和DELETE請求是由默認注冊的一個名為“WebDAVModule”的自定義HttpModule導致的。WebDAV的全稱為“Web-based Distributed Authoring and Versioning”,它是一個在多用戶之間輔助協同編輯和管理在線文檔的HTTP擴展。該擴展使應用程序可以直接將文件寫到 Web Server 上,同時支持文件的加鎖和版本控制。
微軟是推動WebDAV成為一個標準的主導力量,它自己利用自定義的HttpModule實現了IIS針對WebDAV的支持。但是這個默認注冊(注冊名稱為“WebDAVModule”)會拒絕HTTP方法為PUT和DELETE的請求,如果我們的站點不需要提供針對WebDAV的支持,解決這個問題最為直接的方式就是利用如下的配置將注冊的HttpModule移除。
1: <configuration>
2: ...
3: <system.webServer>
4: <modules runAllManagedModulesForAllRequests="true">
5: <remove name="WebDAVModule" />
6: </modules>
7: </system.webServer>
8: </configuration>
四、 以Self Host方式寄宿Web API
與WCF類似,寄宿Web API不一定需要IIS的支持,我們可以采用Self Host的方式使用任意類型的應用程序(控制臺、Windows Forms應用、WPF應用甚至是Windows Service)作為宿主。對于我們演示的實例來說,項目SelfHost代表的控制臺程序就是一個采用Self Host寄宿模式的宿主。
對于SelfHost這么一個空的控制臺應用來說,除了需要添加針對WebApi的項目引用之外,還需要添加如下4個程序集引用。除了程序集“System.Net.Http.dll”(它屬于.NET Framework 原生的程序集)之外,其余3個均可以在目錄“%ProgramFiles%\Microsoft ASP.NET\ASP.NET Web Stack 5\Packages\”中找到。
- System.Web.Http.dll(\ Microsoft.AspNet.WebApi.Core.5.0.0\lib\net45\)
- System.Net.Formatting.Http.dll(\Microsoft.AspNet.WebApi.Client.5.0.0\lib\net45\)
- System.Web.Http.SelfHost.dll(\Microsoft.AspNet.WebApi.SelfHost.5.0.0\lib\net45\)
- System.Net.Http.dll
通過上面的介紹我們可以看到以Web Host的方式寄宿Web API需要做的唯一一件事情是路由注冊。但是對于Self Host來說,除了必需的路由注冊外,我們還需要完成額外的一件事情,即手工加載定義了HttpController類型的程序集。整個寄宿工作通過如下幾行簡單的代碼就可以實現。
1: class Program
2: {
3: static void Main(string[] args)
4: {
5: Assembly.Load("WebApi, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null");
6:
7: HttpSelfHostConfiguration configuration = new HttpSelfHostConfiguration("http://localhost/selfhost");
8: using (HttpSelfHostServer httpServer = new HttpSelfHostServer(configuration))
9: {
10: httpServer.Configuration.Routes.MapHttpRoute(
11: name : "DefaultApi",
12: routeTemplate : "api/{controller}/{id}",
13: defaults : new { id = RouteParameter.Optional });
14:
15: httpServer.OpenAsync();
16: Console.Read();
17: }
18: }
19: }
ASP.NET Web API的Self Host寄宿方式通過HttpSelfHostServer來完成。如上面的代碼片斷所示,在手工加載了定義ContactsController類型的程序集“WebApi.dll”之后,我們根據指定的基地址(“http://localhost/selfhost”),注冊路由的URL模板將是以此作為基地址的相對地址)創建了一個HttpSelfHostConfiguration對象,HttpSelfHostServer由該對象創建。接下來,我們利用創建的HttpSelfHostConfiguration對象(對應著HttpSelfHostServer的Configuration屬性)的Routes得到全局路由表,并調用擴展方法MapHttpRoute注冊了與Web Host寄宿方式一樣的路由。當我們調用OpenAsync方法成功開啟HttpSelfHostServer之后,服務器開始監聽來自網絡的調用請求。
如果讀者朋友們對WCF比較熟悉的話,應該清楚在進行WCF服務寄宿的時候我們必須指定寄宿服務的類型,但是對于ASP.NET Web API的寄宿來說,不論是Web Host還是Self Host,我們都無需指定HttpController的類型。換句話說,WCF服務寄宿是針對具體某個服務類型的,而ASP.NET Web API的寄宿則是批量進行的。
ASP.NET Web API的批量寄宿源自它對HttpController類型的智能解析,它會從“提供的”的程序集列表中解析出所有HttpController類型(所有實現了IHttpController接口的類型)。對于Web Host來說,它會利用BuildManager獲得當前項目直接或者間接引用的程序集,但是對于Self Host來說,HttpController類型的解析在默認情況下只會針對加載到當前應用程序域中的程序集列表,這也是我們為何需要手工加載定義了ContactsController類型的程序集的原因所在。
如果現在運行這個作為宿主的控制臺程序,我們依然可以對寄宿其中的Web API發起調用。同樣采用瀏覽器作為測試工具,在分別訪問目標地址“http://localhost/selfhost/api/contacts”和“http://localhost/selfhost/api/contacts/001”后,我們依然會得到上面的結果。
五、利用HttpClient調用Web API
對于一個.NET客戶端程序,它可以利用HttpClient來進行Web API的調用。由于Web API的調用本質上就是一次普通的發送請求/接收響應的過程,所以HttpClient其實可以作為一般意義上發送HTTP請求的工具。在ConsoleApp代表的控制臺應用中,我們利用HttpClient來調用以Self Host方式寄宿的Web API。
由于我們需要使用到代表聯系人的數據類型Contact,所以需要為該項目添加針對Common的項目引用。HttpClient定義在程序集“System.Net.Http.dll”中,所以針對該程序集的引用也是必需的。除此之外,我們還需要添加針對程序集“System.Net.Formatting.Http.dll”的引用,因為序列化請求和反序列化響應的相關類型定義在此程序集中。
如下所示的是整個Web API調用程序的定義,我們利用HttpClient調用Web API實現了針對聯系人的獲取、添加、修改和刪除。由于HttpClient提供的大部分方法都采用針對Task的異步編程形式,所以我們將所有的操作定義在一個標記為“async”的靜態方法Process中,以便我們可以使用“await”關鍵字編寫同步代碼。
1: class Program
2: {
3: static void Main(string[] args)
4: {
5: Process();
6: Console.Read();
7: }
8:
9: private async static void Process()
10: {
11: //獲取當前聯系人列表
12: HttpClient httpClient = new HttpClient();
13: HttpResponseMessage response = await httpClient.GetAsync("http://localhost/selfhost/api/contacts");
14: IEnumerable<Contact> contacts = await response.Content.ReadAsAsync<IEnumerable<Contact>>();
15: Console.WriteLine("當前聯系人列表:");
16: ListContacts(contacts);
17:
18: //添加新的聯系人
19: Contact contact = new Contact { Name = "王五", PhoneNo = "0512-34567890", EmailAddress = "wangwu@gmail.com" };
20: await httpClient.PostAsJsonAsync<Contact>("http://localhost/selfhost/api/contacts", contact);
21: Console.WriteLine("添加新聯系人“王五”:");
22: response = await httpClient.GetAsync("http://localhost/selfhost/api/contacts");
23: contacts = await response.Content.ReadAsAsync<IEnumerable<Contact>>();
24: ListContacts(contacts);
25:
26: //修改現有的某個聯系人
27: response = await httpClient.GetAsync("http://localhost/selfhost/api/contacts/001");
28: contact = (await response.Content.ReadAsAsync<IEnumerable<Contact>>()).First();
29: contact.Name = "趙六";
30: contact.EmailAddress = "zhaoliu@gmail.com";
31: await httpClient.PutAsJsonAsync<Contact>("http://localhost/selfhost/api/contacts/001", contact);
32: Console.WriteLine("修改聯系人“001”信息:");
33: response = await httpClient.GetAsync("http://localhost/selfhost/api/contacts");
34: contacts = await response.Content.ReadAsAsync<IEnumerable<Contact>>();
35: ListContacts(contacts);
36:
37: //刪除現有的某個聯系人
38: await httpClient.DeleteAsync("http://localhost/selfhost/api/contacts/002");
39: Console.WriteLine("刪除聯系人“002”:");
40: response = await httpClient.GetAsync("http://localhost/selfhost/api/contacts");
41: contacts = await response.Content.ReadAsAsync<IEnumerable<Contact>>();
42: ListContacts(contacts);
43: }
44:
45: private static void ListContacts(IEnumerable<Contact> contacts)
46: {
47: foreach (Contact contact in contacts)
48: {
49: Console.WriteLine("{0,-6}{1,-6}{2,-20}{3,-10}", contact.Id, contact.Name, contact.EmailAddress, contact.PhoneNo);
50: }
51: Console.WriteLine();
52: }
53: }
如上面的代碼片段所示,我們創建了一個HttpClient對象并調用其GetAsync方法向目標地址“http://localhost/selfhost/api/contacts”發送了一個GET請求,返回的對象HttpResponseMessage表示接收到的響應。該HttpResponseMessage對象的Content屬性返回一個表示響應主體內容的HttpContent對象,我們調用其ReadAsAsync<T>方法讀取響應主體內容并將其反序列化成一個Contact集合。我們將表示當前聯系人列表的Contact集合輸出在控制臺上。
我們接下來調用HttpClient的PostAsJsonAsync<T>方法向目標地址“http://localhost/selfhost/api/contacts”發送一個POST請求以添加一個新的聯系人。正如方法名稱所體現的,作為參數的Contact對象將以JSON格式被寫入請求的主體部分。請求被正常發送并接收到響應之后,我們會打印出當前聯系人列表。
在此之后,我們向目標地址“http://localhost/selfhost/api/contacts/001”發送一個GET請求以獲取ID為“001”的聯系人。在修改了聯系人的姓名(“趙六”)和電子郵箱(“zhaoliu@gmail.com”)之后,我們將其作為參數調用HttpClient的PutAsJsonAsync<T>方法,以此向目標地址“http://localhost/selfhost/api/contacts/001”發送一個PUT請求以更新對應聯系人的相關信息。聯系人信息是否正常更新同樣通過輸出當前所有聯系人列表來證實。
我們最后調用HttpClient的DeleteAsync方法向地址“http://localhost/selfhost/api/contacts/002”發送一個DELETE請求以刪除ID為“002”的聯系人并通過輸出當前所有聯系人列表來證實刪除參數是否成功完成。
我們在運行宿主程序SelfHost之后啟動此ConsoleApp程序,會在控制臺上得到下所示的輸出結果,由此可以看出通過調用HttpClient的GetAsync、PostAsJsonAsync、PutAsJsonAsync和DeleteAsync方法幫助我們成功完成了針對聯系人的獲取、添加、修改和刪除。
1: 當前聯系人列表:
2: 001 張三 zhangsan@gmail.com 0512-12345678
3: 002 李四 lisi@gmail.com 0512-23456789
4:
5: 添加新聯系人“王五”:
6: 001 張三 zhangsan@gmail.com 0512-12345678
7: 002 李四 lisi@gmail.com 0512-23456789
8: 003 王五 wangwu@gmail.com 0512-34567890
9:
10: 修改聯系人“001”信息:
11: 002 李四 lisi@gmail.com 0512-23456789
12: 003 王五 wangwu@gmail.com 0512-34567890
13: 001 趙六 zhaoliu@gmail.com 0512-12345678
14:
15: 刪除聯系人“002”:
16: 003 王五 wangwu@gmail.com 0512-34567890
17: 001 趙六 zhaoliu@gmail.com 0512-12345678
六、創建一個“聯系人管理器”應用
我們最后來創建一個叫做“聯系人管理器”的Web應用。這是一個單網頁應用,我們采用Ajax的請求的形式調用以Web Host模式寄宿的Web API實現針對聯系人的CRUD操作。在正式介紹編程實現之前,我們不妨來看看該應用運行起來的效果。
如右圖所示,當頁面被加載之后,當前聯系人列表會以表格的形式呈現出來。我們可以利用每條聯系人記錄右側的“修改”和“刪除”鏈接實現針對當前聯系人的編輯和刪除。除此之外,我們還可以點擊左下方的“添加聯系人”按鈕添加一個新的聯系人。
如果我們點擊“刪除”鏈接,當前聯系人會直接被刪除。如果我們點擊了“修改”鏈接或者“添加聯系人”按鈕,被修改或者添加的聯系人信息會顯示在如左圖所示的一個彈出的“模態”對話框中。在我們輸入聯系人相關資料后點擊“保存”按鈕,聯系人會被成功修改或者添加。被修改的現有聯系人信息或者被添加的聯系人會立即體現在列表之中。
雖然這僅僅是一個簡單的Web應用,但是我刻意使用了3個主流的Web前端開發框架,它們分別是jQuery、Bootstrap和KnockOut,這三個框架的使用體現在頁面引用的CSS和JavaScript文件上。
1: <!DOCTYPE html>
2: <html xmlns="http://www.w3.org/1999/xhtml">
3: <head>
4: <title>聯系人管理器</title>
5: <link href="css/bootstrap.min.css" rel="stylesheet">
6: </head>
7: <body>
8: ...
9: <script src="Scripts/jquery-1.10.2.min.js"></script>
1:
2: <script src="Scripts/bootstrap.min.js"></script>
2: <script src="Scripts/knockout-3.0.0.js"></script>
2: <script src="Scripts/viewmodel.js"></script>
10: </body>
11: </html>
jQuery,這個“地球人都知道”的JavaScript框架,我們無須對它作任何介紹了。Bootstrap 是集 HTML、CSS 和 JavaScript 于一體,是由微博的先驅 Twitter 在2011年8月開源的整套前端解決方案,Web 開發人員利用它能夠輕松搭建出具有清爽風格的界面以及實現良好的交互效果的Web應用。Bootstrap是ASP.NET MVC 5默認支持的框架,當我們利用Visual Stduio創建一個ASP.NET MVC項目時,項目目錄下就包含了Bootstrap相關的CSS和JavaScript文件。
在本例中,我們主要利用jQuery來實現以Ajax方式調用Web API,同時它也是其他兩個框架(Bootstrap和KnockOut)的基礎框架。至于Bootstrap,我們則主要使用它的頁面布局功能和它提供的CSS。除此之外,“編輯聯系人”對話框就是利用Bootstrap提供的JavaScript組件實現的。
MVVM與Knockout
考慮到可能有人對Knockout(以下簡稱KO)這個JavaScript框架不太熟悉,在這里我們對它作一下概括性的介紹。KO是微軟將應用于WPF/Silverlight的MVVM模式在Web上的嘗試,這是一個非常有用的JavaScript框架。對于面向數據的Web應用來說,MVVM模式是一項不錯的選擇,它借助框架提供的“綁定”機制使我們無需過多關注UI(HTML)的細節,只需要操作綁定的數據源。MVVM最早被微軟應用于WPF/SL的開發,所以針對Web的MVVM框架來說,Knockout(以下簡稱KO)無疑是“根正苗紅”。
MVVM可以看成是MVC模式的一個變體,Controller被View Model取代,但兩者具有不同的職能,三元素之間的交互也不相同。以通過KO實現的MVVM為例,其核心是“綁定”,我個人又將其分為“數據的綁定”和“行為的綁定”。所謂數據的綁定,就是將View Model定義的數據綁定到View中的UI元素(HTML元素)上,KO同時支持單向和雙向綁定。行為綁定體現為事件注冊,即View中UI元素的事件(比如某個<button>元素的click事件)與View Model定義的方法(function)進行綁定。
如右圖所示,用戶行為(比如某個用戶點擊了頁面上的某個按鈕)首先觸發View的某個事件,與之綁定的定義在View Model中的EventHandler(View Model的某個方法成員)被自動執行。它可以執行Model,并修改自身維護的數據,如果View和View Model的數據綁定是雙向的,用戶在界面上輸入的數據可以被View Model捕獲,View Model對數據的更新可以自動反映在View上。這樣的好處顯而易見:我們在通過JavaScript定義UI處理邏輯的時候,無需關注View的細節(View上的HTML),只需要對自身的數據進行操作即可。
我們通過一個簡單的例子來說明兩種綁定在KO中的實現。假設我們需要設計如左圖所示的“地址編輯器頁面”,在頁面加載的時候它會將默認的地址信息綁定到表示省、市、區和街道的文本框和顯示完整地址信息的<span>元素上,當用戶在文本框中輸入新的值并點擊“確認”按鈕后,顯示的完整地址會相應的變化。
我們可以利用KO按照如下的方式來實現地址信息的綁定和處理用戶提交的編輯確認請求。我們首先需要通過一個函數來創建表示View Model的“類”,需要綁定的數據和函數將作為該類的成員,組成View的HTML元素則通過內聯的“data-bind”屬性實現數據綁定和事件注冊。我們最終需要創建View Model對象,并將其作為參數調用ko.applyBindings方法將綁定應用到當前頁面。
1: <div>
2: <div><label>省:</label><input data-bind="value: province" /></div>
3: <div><label>市:</label><input data-bind="value: city" /></div>
4: <div><label>區:</label><input data-bind="value: district" /></div>
5: <div><label>街道:</label><input data-bind="value: street"/>
6: <div><label>地址:</label><span data-bind="text: address"></span></div>
7: <div><input type="button" data-bind="click: format" value="確定"/></div>
8: </div>
9:
10: <script type="text/javascript" >
1:
2: function AddressModel() {
3: var self = this;
4: self.province = ko.observable("江蘇省");
5: self.city = ko.observable("蘇州市");
6: self.district = ko.observable("工業園區");
7: self.street = ko.observable("星湖街328號");
8: self.address = ko.observable();
9:
10: self.format = function () {
11: if (self.province() && self.city() && self.district() && self.street()){
12: var address = self.province() + " " + self.city() + " " + self.district() + " " + self.street();
13: self.address(address);
14: }
15: else {
16: alert("請提供完整的地址信息");
17: }
18: };
19:
20: self.format();
21: }
22:
23: ko.applyBindings(new AddressModel());
</script>
如上面的代碼片段所示,我們定義了一個名為AddressModel的類作為整個“地址編輯”頁面的View Model,AddressModel的五個數據成員(province、city、district、street和address)表示地址的四個組成部分和格式化的地址。它們都是基于雙向綁定的Observable類型成員,意味著用戶的輸入能夠即時改變綁定的數據源,而數據源的改變也能即時地反映在綁定的HTML元素上。Observable數據成員是一個通過調用ko.observable方法創建的函數,方法調用指定的參數表示更新的數據。
AddressModel的另一個成員format是一個自定義的函數,該函數進行地址格式化并用格式化的地址更新address字段。由于address字段是一個Observable成員,一旦它的值發生改變,被綁定的HTML元素的值將會自動更新。
AddressModel的六個字段分別綁定在六個HTML元素上,其中province、city、district和street字段綁定到代表對應文本框的Value屬性上(data-bind="value: {成員名稱}"),而address字段則綁定到用于顯示格式化地址的<span>元素的Text屬性上(data-bind="text: {成員名稱}"),用于格式化地址的format字段則與“確定”按鈕的click事件進行綁定(data-bind="click: {成員名稱}")。真正的綁定工作發生在ko.applyBindings方法被調用的時候。
ViewModel
接下來我們來看看“聯系人管理器”這個Web頁面究竟如何來定義。具體來說,該頁面的內容包含兩個部分,HTML標簽和JavaScript代碼。對于后者,其主要體現在具有如下定義的View Model上,我們將它定義在獨立的JavaScript文件(viewmodel.js)中。
1: function ViewModel() {
2: self = this;
3: self.contacts = ko.observableArray(); //當前聯系人列表
4: self.contact = ko.observable(); //當前編輯聯系人
5:
6: //獲取當前聯系人列表
7: self.load = function () {
8: $.ajax({
9: url : "http://localhost/webhost/api/contacts",
10: type : "GET",
11: success : function (result) {
12: self.contacts(result);
13: }
14: });
15: };
16:
17: //彈出編輯聯系人對話框
18: self.showDialog = function (data) {
19: //通過Id判斷"添加/修改"操作
20: if (!data.Id) {
21: data = { ID: "", Name: "", PhoneNo: "", EmailAddress: "",
22: Address: "" }
23: }
24: self.contact(data);
25: $(".modal").modal('show');
26: };
27:
28: //調用Web API添加/修改聯系人信息
29: self.save = function () {
30: $(".modal").modal('hide');
31: if (self.contact().Id) {
32: $.ajax({
33: url : "http://localhost/webhost/api/contacts/" + self.contact.Id,
34: type : "PUT",
35: data : self.contact(),
36: success : function () {self.load();}
37: });
38: }
39: else {
40: $.ajax({
41: url : "http://localhost/webhost/api/contacts",
42: type : "POST",
43: data : self.contact(),
44: success : function () {self.load();}
45: });
46: }
47: };
48:
49: //刪除現有聯系人
50: self.delete = function (data) {
51: $.ajax({
52: url : "http://localhost/webhost/api/contacts/" + data.Id,
53: type : "DELETE",
54: success : function () {self.load();}
55: });
56: };
57:
58: self.load();
59: }
60:
61: $(function () {
62: ko.applyBindings(new ViewModel());
63: });
對于上面定義的作為整個頁面View Model的“類型”(ViewModel)來說,它具有兩個“數據”成員(其實是函數)contacts和contact,前者表示當前聯系人列表,后者則表示當前修改或者添加的聯系人。contacts和contact分別通過調用方法observableArray和observable創建,所以它們均支持雙向綁定。這兩個數據成員分別被綁定到呈現當前聯系人的表格和用于編輯聯系人信息的對話框中。除了這兩個數據成員之外,我們還定義了4個方法成員。
- load:發送Ajax請求調用Web API以獲取當前聯系人列表,并將得到的聯系人列表“賦值”給contacts屬性。
- showDialog:彈出“編輯聯系人信息”對話框。我們通過指定的聯系人對象是否具有Id來判斷當前操作是“修改”還是“添加”。對于后者,我們會創建一個新的對象作為添加的聯系人對象。被修改或者添加的聯系人對象被“賦值”給contact屬性。對話框的彈出通過調用表示對話框的<div>的modal方法實現,該方法是由Bootstrap提供的。
- save:發送Ajax請求調用Web API以添加新的聯系人或者修改現有某個聯系人的信息。contact屬性作為提交的數據,至于“添加”還是“修改”,同樣是通過它是否具有相應的Id來決定。聯系人成功添加或者修改之后,load方法被調用以刷新當前聯系人列表。
- delete:發送Ajax請求調用Web API以刪除指定的聯系人。聯系人成功刪除之后,load方法被調用以刷新當前聯系人列表。
HTML
如下所示的是頁面主體部分包含的HTML,ViewModel的相關成員會綁定到相應HTML元素上。整個內容大體包含兩個部分,第一部分用于呈現當前聯系人列表,第二部分在用于定義彈出的對話框。
1: <!--當前聯系人列表-->
2: <div id="content">
3: <table class="table table-striped">
4: <thead>
5: <tr>
6: <th>姓名</th>
7: <th>聯系電話</th>
8: <th>電子郵件</th>
9: <th></th>
10: </tr>
11: </thead>
12: <tbody data-bind="foreach: contacts">
13: <tr>
14: <td data-bind="text: Name"></td>
15: <td data-bind="text: PhoneNo"></td>
16: <td data-bind="text: EmailAddress"></td>
17: <td>
18: <a href="#" data-bind="click: $root.showDialog">修改</a>
19: <a href="#" data-bind="click: $root.delete">刪除</a>
20: </td>
21: </tr>
22: </tbody>
23: </table>
24: <a href="#" class="btn btn-primary" data-bind="click: showDialog">添加新聯系人</a>
25: </div>
26:
27: <!--添加/修改聯系人對話框-->
28: <div class="modal fade">
29: <div class="modal-dialog">
30: <div class="modal-content">
31: <div class="modal-header">
32: <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
33: <h4 class="modal-title">編輯聯系人信息</h4>
34: </div>
35: <div class="modal-body form-horizontal" data-bind="with: contact">
36: <div class="form-group">
37: <label for="name" class="col-sm-2 control-label">姓名:</label>
38: <div class="col-sm-10">
39: <input type="text" class="form-control" id="name" placeholder="姓名" data-bind="value:Name">
40: </div>
41: </div>
42: <div class="form-group">
43: <label for="phoneNo" class="col-sm-2 control-label"> 聯系電話:</label>
44: <div class="col-sm-10">
45: <input type="text" class="form-control" id="phoneNo" placeholder="聯系電話" data-bind="value:PhoneNo">
46: </div>
47: </div>
48: <div class="form-group">
49: <label for="emailAddress" class="col-sm-2 control-label"> 電子郵箱:</label>
50: <div class="col-sm-10">
51: <input type="text" class="form-control" id="emailAddress" placeholder="電子郵箱" data-bind="value:EmailAddress">
52: </div>
53: </div>
54: <div class="form-group">
55: <label for="address" class="col-sm-2 control-label"> 地址:</label>
56: <div class="col-sm-10">
57: <input type="text" class="form-control" id="address" placeholder="地址" data-bind="value:Address">
58: </div>
59: </div>
60: </div>
61: <div class="modal-footer">
62: <a href="#" class="btn btn-default" data-dismiss="modal">關閉</a>
63: <a href="#" class="btn btn-primary" data-bind="click: save">保存</a>
64: </div>
65: </div>
66: </div>
67: </div>
第一部分的核心是呈現聯系人列表的<table>元素,其主體具有一個針對contacts成員的foreach綁定(<tbody data-bind="foreach: contacts">),該綁定利用內嵌的<tr>元素綁定列表中的每個聯系人。至于聯系人的具體某個屬性,則對應著相應的<td>元素,兩者之間是一個text綁定(<td data-bind="text: Name"></td>)。
表格中的每行右側的“修改”和“刪除”鏈接各自具有一個針對showDialog 和delete方法成員的click綁定(<a href="#" data-bind="click: $root.showDialog">修改</a>)。之所以需要在成員名稱前面添加“$root”前綴,是因為KO總是會從當前綁定上下文中去獲取綁定的成員。由于這兩個鏈接HTML內嵌于foreach綁定之中,所以當前綁定上下文實際上是contacts屬性中某個聯系人對象。“$root”前綴的目的在于告訴KO綁定的是ViewModel自身的成員。值得一提的是,當綁定的方法被執行時,KO會將當前綁定上下文作為參數。
在表示“編輯聯系人信息”對話框的主體部分,我們通過一個with綁定(<div data-bind="with: contact">)將綁定上下文設定為ViewModel的contact屬性,內嵌其中的4個文本框分別利用一個value綁定(比如<input type="text" data-bind="value:Name">)與對應的成員進行關聯。ViewModel中最終用于添加/修改聯系人的方法save則通過一個click綁定(<三data-bind="click: save">保存</a>)與“保存”按鈕關聯在一起。
文章列表