日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > asp.net >内容正文

asp.net

ASP.NET MVC: 构建不带 Web 窗体的 Web 应用程序(转载)

發布時間:2024/7/19 asp.net 37 豆豆
生活随笔 收集整理的這篇文章主要介紹了 ASP.NET MVC: 构建不带 Web 窗体的 Web 应用程序(转载) 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

我 從事專業開發迄今為止已有 15 年,在此之前,我利用業余時間從事開發至少也有 10 年了。與我這一代的大多數人一樣,我是從 8 位計算機起步,然后轉用 PC 平臺的。隨著計算機的復雜性日益增加,我編寫的應用程序涵蓋了從小型游戲到個人數據管理再到控制外部硬件的各項功能。 不過,在我職業生涯的前半段,我編寫的所有軟件都有一個共同點:即,都是運行在用戶桌面 上的本地應用程序。我最早是在 90 年代初期聽說萬維網這件新生事物。那時我發現,通過構建 Web 應用程序,可以讓我輸入我的考勤卡信息而不必再費時費力從工作場所趕回辦公室。 一言以蔽之,我感覺很是困惑。我當時滿腦子是面向桌面的理念,很難接納這種無狀態的 Web。要添加很多讓人頭疼的調試、我沒有 UNIX 服務器的超級用戶訪問權限,再加上這個奇怪的角括號,這些因素使年輕時的我止步不前,又重返桌面開發渡過了幾年時光。 我遠離了 Web 開發領域,雖然這領域顯然很重要,但我并沒有真正理解其編程模型。然后,Microsoft? .NET Framework 和 ASP.NET 發行了。盡管它與桌面應用程序編程有許多相似之處,但終于有了可以讓我從事 Web 應用程序編程的框架。我可以構建窗口(頁面),將控件與事件掛鉤,而設計器使我不必處理那些討厭的角括號。最妙的是,ASP.NET 會通過查看狀態自動為我處理 Web 的無狀態性質!我又重新找回了程序員的快樂 ... 至少在一段時間內是如此。 隨著經驗的增加,我的設計內容也隨之豐富。我早已掌握了幾種最佳實踐,并將其應用到桌面 應用程序編程。其中的兩種就是:
  • 分離關注點:不要將 UI 邏輯與基礎行為混合在一起。
  • 自動單元測試:編寫自動測試以驗證您的代碼是否按預期執行。
這些是適用于任何技術的基本原則。分離關注點是一項可幫助您處理復雜問題的基本原則。在 同一個對象內混合多種責任(如計算剩余的工時、設置數據格式并繪圖)會給維護帶來很大的負擔。而自動測試對于獲得生產質量的代碼同時仍保持條理性至關重 要,尤其是當您更新現有項目時更是如此。 ASP.NET Web 窗體使入門變得非常簡單,但另一方面,要將我的設計理念應用到 Web 應用程序卻并非易事。Web 窗體堅持以 UI 為側重點;其基本單位為頁面。首先設計 UI 并拖曳控件。只需將應用程序邏輯融入頁面的事件處理程序(與為 Windows? 應用程序啟用的 Visual Basic? 非常相似)就萬事大吉,這一點非常吸引人。 但進一步的頁面單元測試常常有很大困難。您必須先啟動所有 ASP.NET,然后才能在“頁面”對象的生命周期內運行該對象。盡管可以通過發送 HTTP請求到服務器或自動化瀏覽器來測試 Web 應用程序,但這類測試非常脆弱(更換一個控制 ID 測試就會中斷)、難以設置(您必須以完全相同的方式在每位開發人員的計算機上設置該服務器)并且運行緩慢。 當我開始構建更復雜的 Web 應用程序時,Web 窗體提供的抽象概念(如控件、視圖狀態和頁面生命周期)就開始添亂而不是幫忙了。我需要花越來越多的時間來配置數據綁定(并編寫大量的事件處理程序對其進 行正確配置)。我不得不想辦法縮減視圖狀態的大小以便更快加載我的頁面。Web 窗體要求每個 URL 均存在物理文件,這對于動態站點(例如 wiki)非常困難。而成功編寫一個自定義的 WebControl 是一個非常復雜的過程,需要全面了解頁面生命周期和 Visual Studio? 設計器。 自從在 Microsoft 工作開始,我就一直與其他人分享關于各種 .NET 難題的體驗并希望可以解決一些難題。最近,作為開發人員參加有關模式與實踐的 Web 客戶端軟件工廠項目 (codeplex.com/websf) 時,我遇到了一個這樣的機會。特別是,模式與實踐交付的內容之一就是自動單元測試。在 Web 客戶端軟件工廠中,我們建議使用 Model View Presenter (MVP) 模式構建可測試的 Web 窗體。 簡而言之,MVP 并非將您的邏輯放入頁面中,而是讓您構建自己的頁面,頁面 (View) 只需調用單獨的對象,即 Presenter。Presenter 對象隨即執行響應視圖上活動必需的任何邏輯,通常通過使用其它對象 (Model) 訪問數據庫、執行業務邏輯等。一旦這些步驟完成后,Presenter 會更新視圖。這種方法提供了可測試性,因為表示器從 ASP.NET 管道中隔離出來;它與視圖通過界面進行通信并可脫離頁面獨立進行測試。 MVP 的這種功能實現有點笨;您需要單獨的視圖界面,并且您必須在源代碼文件中編寫許多事件轉發函數。但如果您想要在 Web 窗體應用程序中得到可測試的 UI,這差不多是最佳途徑。任何改進均需要在基礎平臺中做出更改。
模型視圖控制器模式 幸運的是,ASP.NET 團隊聽取了象我這樣的開發人員的意見,并且已經著手開發一種新的 Web 應用程序框架,該框架與您所熟知并喜愛的 Web 窗體處于同一層級,但采用一組完全不同的設計目標:
  • 使用 HTTP 和 HTML—不隱藏。
  • 可測試性貫穿整個框架之內。
  • 幾乎在每個點均可擴展。
  • 對輸出進行總體控制。
由于此新框架基于模型視圖控制器 (MVC) 模式,因此其名稱為 ASP.NET MVC。MVC 模式最初在 70 年代發明,是 Smalltalk 技術的一部分。正如我將在本文中所展示的,它實際上非常適合 Web 的性質。MVC 將您的 UI 分為三種不同的對象:用于接收和處理輸入的控制器;包含您域邏輯的模型;以及用于生成輸出的視圖。在 Web 環境中,輸入為 HTTP 請求,而請求流程與圖 1 類似。 Figure 1?MVC 模式請求流程?(單 擊該圖像獲得較大視圖) 這實際上與 Web 窗體中的過程完全不同。在 Web 窗體模型中,輸入進入頁面(視圖),然后視圖負責處理輸入并生成輸出。而 MVC 中這些責任是分開的。 因此,您可能立即會產生以下一種想法:“嘿,這太好了。我應該如何使用它?”或“為什么 我要編寫這些對象,以前只需要編寫一個對象?”這兩個問題都問得很好,最好通過示例來進行解釋。因此,我將使用 MVC Framework 編寫一個小型 Web 應用程序以說明其優點。
創建控制器 要繼續進行,您將需要安裝 Visual Studio 2008 并獲得 MVC Framework 的副本。在撰寫本文時,ASP.NET 擴展的 2007 年 12 月社區技術預覽 (CTP) 中已提供了這些內容 (asp.net/downloads/3.5-extensions)。您可能想要獲取擴展 CTP 和 MVC 工具包,其中包括一些非常有用的幫助程序對象。一旦下載并安裝 CTP 后,您將在“新建項目”對話框中獲得名為“ASP.NET MVC Web 應用程序”的新項目類型。 選擇“MVC Web 應用程序”項目后,會為您提供一個與常用網站或應用程序稍有不同的解決方案。該解決方案模板會創建一個帶有一些新目錄的 Web 應用程序(如圖 2 中所示)。特別是 Controllers 目錄包含各種控制器類,而 Views 目錄(及其所有子目錄)包含了各種視圖。 Figure 2?MVC 項目結構? 我將會編寫一個非常簡單的控制器,返回 URL 中傳遞的名稱。右鍵單擊 Controllers 文件夾并選擇“添加項目”以顯示常用的“添加項目”對話框以及一些新增加的內容,包括 MVC 控制器類和幾個 MVC 視圖組件。在此例中,我將添加一個非常富有想象力、名為 HelloController 的類: 復制代碼 using System;
using System.Web;
using System.Web.Mvc;
namespace HelloFromMVC.Controllers
{
public class HelloController : Controller
{
[ControllerAction]
public void Index()
{
...
}
}
}
控制器類比頁面簡單得多。實際上,唯一真正必需做的就是從 System.Web.Mvc.Controller 中衍生并將 [ControllerAction] 屬性置于您的操作方法中。操作是調用以響應特定 URL 請求的一種方法。操作負責執行所需的一切處理,然后呈現一個視圖。我將通過編寫一個將名稱傳遞到視圖的簡單操作著手,如下所示: 復制代碼 [ControllerAction]
public void HiThere(string id)
{
ViewData["Name"] = id;
RenderView("HiThere");
}
操作方法會通過 ID 參數從 URL 接收該名稱(稍后會介紹方法),將其存儲在 ViewData 集合中,然后呈現名為 HiThere 的視圖。 在討論如何調用此方法,或該視圖的顯示內容之前,我希望說一說可測試性。還記得我之前關 于測試 Web 窗體頁面類有多難的評論嗎?控制器的測試簡單得多。實際上,控制器可以直接實例化,而調用操作方法無需任何附加的基礎結構。您不需要 HTTP 上下文,也不需要服務器,只要測試工具即可。作為示例,我在圖 3 中為此類包括了 Visual Studio Team System (VSTS) 測試單元。 ?Figure?3?Controller Unit Test 復制代碼 namespace HelloFromMVC.Tests
{
[TestClass]
public class HelloControllerFixture
{
[TestMethod]
public void HiThereShouldRenderCorrectView()
{
TestableHelloController controller = new
TestableHelloController();
controller.HiThere("Chris");
Assert.AreEqual("Chris", controller.Name);
Assert.AreEqual("HiThere", controller.ViewName);
}
}
class TestableHelloController : HelloController
{
public string Name;
public string ViewName;
protected override void RenderView(
string viewName, string master, object data)
{
this.ViewName = viewName;
this.Name = (string)ViewData["Name"];
}
}
}
下面將進行幾項操作。實際的測試相當簡單:實例化該控制器,使用預期的數據調用該方法, 然后檢查呈現的視圖是否正確。我通過創建測試專用的子類覆蓋 RenderView 方法進行檢查。這可以縮短實際創建 HTML 的時間。我只關心是否將正確的數據發送到視圖以及是否呈現了正確的視圖。我不關心此測試視圖本身的底層詳細信息。
創建視圖 當然,最終我必須生成一些 HTML,因此,讓我們創建該 HiThere 視圖。要進行此操作,首先,我將在解決方案中的 Views 文件夾下創建名為 Hello 的新文件夾。默認情況下,控制器將在 Views\<控制器前綴> 文件夾(控制器前綴為控制器類的名稱去掉 "Controller" 字樣)中查找視圖。因此,對于 HelloController 呈現的視圖,它會在 Views\Hello 中查找。解決方案的查找結果如圖 4 所示。 Figure 4?將視圖添加到項目中?(單 擊該圖像獲得較大視圖) 視圖的 HTML 如下所示: 復制代碼 <html >
<head runat="server">
<title>Hi There!</title>
</head>
<body>
<div>
<h1>Hello, <%= ViewData["Name"] %></h1>
</div>
</body>
</html>
應注意以下幾件事。沒有 runat="server" 標記。沒有 form 標記。沒有控件聲明。實際上,這看起來更象傳統的 ASP 而不是 ASP.NET。請注意,MVC 視圖僅負責生成輸出,因此其不需要任何 Web 窗體頁面所需的事件處理或復雜控件。 MVC Framework 借用了 .aspx 文件格式作為一種有用的文本模板語言。如果需要,甚至可以使用源代碼,但默認情況下,源代碼文件如下所示: 復制代碼 using System;
using System.Web;
using System.Web.Mvc;
namespace HelloFromMVC.Views.Hello
{
public partial class HiThere : ViewPage
{
}
}
沒有頁面初始化或加載方法,沒有事件處理程序,除了基類聲明以外沒有任何內容,基類聲明 為 ViewPage 而不是 Page。這就是 MVC 視圖所需的一切。運行該應用程序,導航至 http://localhost:<端口>/Hello/HiThere/Chris,您將看到如圖 5 所示的內容。 Figure 5?成功的 MVC 視圖?(單 擊該圖像獲得較大視圖) 如果您看到的并非如圖 5 所示,而是難以理解的意外情況,請不要驚慌。如果您將 HiThere.aspx 文件設置為 Visual Studio 中的活動文檔,則當按 F5 后,Visual Studio 將嘗試直接訪問 .aspx 文件。由于 MVC 視圖要求控制器在顯示前運行,因此嘗試直接導航至該頁面將不起作用。只需將該 URL 編輯為與圖 5 中所示的內容相匹配,即可正常工作。 MVC Framework 如何知道調用我的操作方法?該 URL 甚至沒有文件擴展名。答案是 URL 路由。如果您仔細查看 global.asax.cs 文件,則會看到如圖 6 所示的代碼段。全局 RouteTable 會存儲 Route 對象的集合。每個 Route 說明一個 URL 窗體以及對其進行何種操作。默認情況下,會向該表中添加兩個路由。第一個是該方法的內容。它說明每個 URL 在服務器名后均由三部分組成,第一部分應為控制器名,第二部分為操作名稱,而第三部分為 ID 參數。 ?Figure?6?Route Table 復制代碼 public class Global : System.Web.HttpApplication
{
protected void Application_Start(object sender, EventArgs e)
{
// Change Url= to Url="[controller].mvc/[action]/[id]"
// to enable automatic support on IIS6
RouteTable.Routes.Add(new Route
{
Url = "[controller]/[action]/[id]",
Defaults = new { action = "Index", id = (string)null },
RouteHandler = typeof(MvcRouteHandler)
});
RouteTable.Routes.Add(new Route
{
Url = "Default.aspx",
Defaults = new {
controller = "Home",
action = "Index",
id = (string)null },
RouteHandler = typeof(MvcRouteHandler)
});
}
}
復制代碼 Url = "[controller]/[action]/[id]"
此默認路由是能讓我的 HiThere 方法得以調用的路由。請記住此 URL:http://localhost/Hello/HiThere/Chris?此路由將 Hello 與控制器、HiThere 與操作以及 Chris 與 ID 一一對應。MVC Framework 隨即創建 HelloController 實例,調用 HiThere 方法,然后將 Chris 作為 ID 參數的值傳遞。 此默認路由為您提供了許多功能,但您也可以添加自己的路由。例如,我想要一個真正友好的 站點,好友們只需輸入他們的姓名即可獲得個性化的問候。如果我在路由表的頂部添加以下路由 復制代碼 RouteTable.Routes.Add(new Route
{
Url = "[id]",
Defaults = new {
controller = "Hello",
action = "HiThere" },
RouteHandler = typeof(MvcRouteHandler)
});
隨后,我只需訪問 ,我的操作仍處于調用狀態,而我將會看到熟悉的友好問候。 系統如何知道調用哪個控制器和操作?答案是 Defaults 參數。它利用新的 C# 3.0 匿名類型語法來創建一個偽詞典。Route 上的 Defaults 對象可包含任意附加的信息,對于 MVC,它還可以包含一些眾所周知的條目:即控制器和操作。如果 URL 中沒有指定控制器或操作,則其將使用 Defaults 中的名稱。這就是為什么即使我在 URL 中忽略它們,但仍可以將我的請求映射到正確的控制器和操作。 還有一件事需要注意:還記得我說過“添加到表格的頂部”嗎?如果您將其置于底部,將會出 現錯誤。路由根據先到先得的原則進行工作。當處理 URL 時,路由系統會自上至下瀏覽表格,并且使用第一個匹配的路由。在本例中,默認路由 "[controller]/[action]/[id]" 匹配,因為它們是操作和 ID 的默認值。這樣,它會繼續查找 ChrisController,但我沒有控制器,因此會出現錯誤。
稍大的示例 現在,我已經說明了 MVC Framework 的基礎知識,將為您展示一個更大的示例,實現比僅顯示字符串更多的功能。wiki 是一種可以在瀏覽器中進行編輯的網站。可以輕松地添加或編輯頁面。我使用 MVC Framework 編寫了一個小型的示例 wiki?!熬庉嫶隧撁妗逼聊蝗?strong>圖 7 所示。 Figure 7?編輯主頁?(單擊該 圖像獲得較大視圖) 您可以檢查本文的代碼下載以查看如何實現底層 wiki 邏輯?,F在我想重點說明 MVC Framework 如何使 Web 上的 wiki 獲取變得簡單。讓我們先設計 URL 結構。我想要以下各項:
  • /[pagename] 顯示該名稱的頁面。
  • /[pagename]?version=n 顯示頁面的請求版本,其中 0 = 當前版本,1 = 以前的版本,以此類推。
  • /Edit/[pagename] 打開該頁的編輯屏幕。
  • /CreateNewVersion/[pagename] 是為提交編輯而傳入的 URL。
讓我們從 wiki 頁面的基本顯示開始。我為它創建了一個名為 WikiPageController 的新類。接下來,我會添加一個名為 ShowPage 的操作。啟動的 WikiPageController 如圖 8 所示。ShowPage 方法相當簡單。WikiSpace 和 WikiPage 類分別表示一組 wiki 頁面和特定的頁面(及其修訂)。此操作只需加載模型并調用 RenderView。但此處的 "new WikiPageViewData" 行是什么意思? ?Figure?8?WikiPageController Implementation of ShowPage 復制代碼 public class WikiPageController : Controller
{
ISpaceRepository repository;
public ISpaceRepository Repository
{
get {
if (repository == null)
{
repository = new FileBasedSpaceRepository(
Request.MapPath("~/WikiPages"));
}
return repository;
}
set { repository = value; }
}
[ControllerAction]
public void ShowPage(string pageName, int? version)
{
WikiSpace space = new WikiSpace(Repository);
WikiPage page = space.GetPage(pageName);
RenderView("showpage",
new WikiPageViewData
{
Name = pageName,
Page = page,
Version = version ?? 0
});
}
}
我前面的示例說明了一種將數據從控制器傳遞到視圖的方法:即 ViewData 詞典。詞典非常方便,但也很危險。它們幾乎包含一切內容,您不能獲取內容的任何 IntelliSense?,并且由于 ViewData 詞典屬于 Dictionary<string, object> 類型,它將消耗內容,您必須計算所有一切。 當您了解在視圖中將需要什么數據后,就可以傳遞強類型化的 ViewData 對象。在我的示例中,我創建了一個簡單的對象 (WikiPageViewData),如圖 9 中所示。此對象將 wiki 頁面信息帶到視圖,同時還攜帶了一些實用工具方法,執行獲取 wiki 標記的 HTML 版本這類任務。 ?Figure?9?WikiPageViewData Object 復制代碼 public class WikiPageViewData {
public string Name { get; set; }
public WikiPage Page { get; set; }
public int Version { get; set; }
public WikiPageViewData() {
Version = 0;
}
public string NewVersionUrl {
get {
return string.Format("/CreateNewVersion/{0}", Name);
}
}
public string Body {
get { return Page.Versions[Version].Body; }
}
public string HtmlBody {
get { return Page.Versions[Version].BodyAsHtml(); }
}
public string Creator {
get { return Page.Versions[Version].Creator; }
}
public string Tags {
get { return string.Join(",", Page.Versions[Version].Tags); }
}
}
現在,我已經定義了視圖數據,那么,我如何使用它呢?在 ShowPage.aspx.cs 中,您將看到以下內容: 復制代碼 namespace MiniWiki.Views.WikiPage {
public partial class ShowPage : ViewPage<WikiPageViewData>
{
}
}
請注意,我將基類類型定義為 ViewPage<WikiPageViewData>。這意味著頁面的 ViewData 屬性為 WikiPageViewData 類型,而不是象以前示例中的“Dictionary”。 .aspx 文件中的實際標記非常簡單: 復制代碼 <%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master"
AutoEventWireup="true" CodeBehind="ShowPage.aspx.cs"
Inherits="MiniWiki.Views.WikiPage.ShowPage" %>
<asp:Content
ID="Content1"
ContentPlaceHolderID="MainContentPlaceHolder"
runat="server">
<h1><%= ViewData.Name %></h1>
<div id="content" class="wikiContent">
<%= ViewData.HtmlBody %>
</div>
</asp:Content>
請注意,當引用 ViewData 時,我沒有使用索引操作符 []。由于我現在有強類型化的 ViewData,我可以直接訪問該屬性。不需要進行任何計算,而 Visual Studio 會提供 IntelliSense。 目光敏銳的讀者將會注意到此文件中的 <asp:Content> 標記。沒錯,“母版頁”確實可以與 MVC 視圖配合使用。并且“母版頁”還可以成為視圖。讓我們看看“母版頁”的源代碼: 復制代碼 namespace MiniWiki.Views.Layouts
{
public partial class Site :
System.Web.Mvc.ViewMasterPage<WikiPageViewData>
{
}
}
相關標記如圖 10 中所示。現在,“母版頁”將獲得與視圖完全相同的 ViewData 對象。我已經將“母版頁”的基類聲明為 ViewMasterPage<WikiPageViewData>,因此,我擁有了正確類型的 ViewData。我會在那里設置各種 DIV 標記以對頁面進行布局,填寫版本列表,然后以常用內容占位符收尾。 ?Figure?10?Site.Master 復制代碼 <%@ Master Language="C#" AutoEventWireup="true" CodeBehind="Site.master.cs" Inherits="MiniWiki.Views.Layouts.Site" %> <%@ Import Namespace="MiniWiki.Controllers" %> <%@ Import Namespace="MiniWiki.DomainModel" %> <%@ Import Namespace="System.Web.Mvc" %> <html > <head runat="server"> <title><%= ViewData.Name %></title> <link href="http://http://www.cnblogs.com/Content/Site.css" rel="stylesheet" type="text/css" /> </head> <body> <div id="inner"> <div id="top"> <div id="header"> <h1><%= ViewData.Name %></h1> </div> <div id="menu"> <ul> <li><a href="http://Home">Home</a></li> <li> <%= Html.ActionLink("Edit this page", new { controller = "WikiPage", action = "EditPage", pageName = ViewData.Name })%> </ul> </div> </div> <div id="main"> <div id="revisions"> Revision history: <ul> <% int i = 0; foreach (WikiPageVersion version in ViewData.Page.Versions) { %> <li> <a href="http://<%= ViewData.Name %>?version=<%= i %>"> <%= version.CreatedOn %> by <%= version.Creator %> </a> </li> <% ++i; } %> </ul> </div> <div id="maincontent"> <asp:ContentPlaceHolder ID="MainContentPlaceHolder" runat="server"> </asp:ContentPlaceHolder> </div> </div> </div> </body> </html> 另一件需要注意的事是對 Html.ActionLink 的調用。以下是呈現幫助程序的一個示例。各種視圖類均具有兩種屬性,Html 和 Url。每種均有輸出 HTML 代碼塊的有用方法。在本例中,Html.ActionLink 獲取一個對象(此處為匿名類型)并通過路由系統將其返回。這將會生成一個 URL,該 URL 將路由至我指定的控制器和操作。這樣一來,無論我如何更改路由,“編輯此頁面”鏈接將始終指向正確的位置。 您可能還注意到,我還不得不依靠手動構建鏈接(到先前頁面版本的鏈接)。遺憾的是,當前 的路由系統在涉及查詢字符串時生成 URL 的功能不是十分完善。這應會在框架的后續版本中得到修復。
創建表單和回發 現在,讓我們看看控制器上的 EditPage 操作: 復制代碼 [ControllerAction]
public void EditPage(string pageName)
{
WikiSpace space = new WikiSpace(Repository);
WikiPage page = space.GetPage(pageName);
RenderView("editpage",
new WikiPageViewData {
Name = pageName,
Page = page });
}
同樣,該操作所做的不多—它只是呈現指定頁面的視圖。視圖中的內容變得更加有趣,如圖 11 中所示。此文件構建了一個 HTML 表單,但沒有出現 Runat="server"。Url.Action helper 用于生成表單回發的 URL。其中還使用了幾種不同的 HTML 幫助程序(如 TextBox、TextArea 和 SubmitButton)。它們會出色完成您的預期目標:為各種輸入字段生成 HTML。 ?Figure?11?EditPage.aspx 復制代碼 <%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master" AutoEventWireup="true" CodeBehind="EditPage.aspx.cs" Inherits="MiniWiki.Views.WikiPage.EditPage" %> <%@ Import Namespace="System.Web.Mvc" %> <%@ Import Namespace="MiniWiki.Controllers" %> <asp:Content ID="Content1" ContentPlaceHolderID="MainContentPlaceHolder" runat="server"> <form action="<%= Url.Action( new { controller = "WikiPage", action = "NewVersion", pageName = ViewData.Name })%>" method=post> <% if (ViewContext.TempData.ContainsKey("errors")) { %> <div id="errorlist"> <ul> <% foreach (string error in (string[])ViewContext.TempData["errors"]) { %> <li><%= error%></li> <% } %> </ul> </div> <% } %> Your name: <%= Html.TextBox("Creator", ViewContext.TempData.ContainsKey("creator") ? (string)ViewContext.TempData["creator"] : ViewData.Creator)%> <br /> Please enter your updates here:<br /> <%= Html.TextArea("Body", ViewContext.TempData.ContainsKey("body") ? (string)ViewContext.TempData["body"] : ViewData.Body, 30, 65)%> <br /> Tags: <%= Html.TextBox( "Tags", ViewContext.TempData.ContainsKey("tags") ? (string)ViewContext.TempData["tags"] : ViewData.Tags)%> <br /> <%= Html.SubmitButton("SubmitAction", "OK")%> <%= Html.SubmitButton("SubmitAction", "Cancel")%> </form> </asp:Content> 處理 Web 編程最頭疼的事情之一就是表單中的錯誤。更確切地說,您想要顯示錯誤信息,但同時想要保留原來輸入的數據。我們都有過那種經歷,在填寫一張有 35 個字段的表單時出現一個錯誤,程序卻只是提供一堆錯誤信息和一張新的空白表單。MVC Framework 使用 TempData 存儲以前輸入信息,以便可以重新填入表單。這是 ViewState 實際上在 Web 窗體中變得非常簡單的原因,因為保存控件的內容幾乎是自動的。 我想在 MVC 中如法炮制,因此引入了 TempData。TempData 是一種詞典,與非類型化的 ViewData 很相似。不過,TempData 的內容僅針對單一請求存在,隨后就會被刪除。要了解如何使用此方法,請參閱圖 12,NewVersion 操作。 ?Figure?12?NewVersion Action 復制代碼 [ControllerAction] public void NewVersion(string pageName) { NewVersionPostData postData = new NewVersionPostData(); postData.UpdateFrom(Request.Form); if (postData.SubmitAction == "OK") { if (postData.Errors.Length == 0) { WikiSpace space = new WikiSpace(Repository); WikiPage page = space.GetPage(pageName); WikiPageVersion newVersion = new WikiPageVersion( postData.Body, postData.Creator, postData.TagList); page.Add(newVersion); } else { TempData["creator"] = postData.Creator; TempData["body"] = postData.Body; TempData["tags"] = postData.Tags; TempData["errors"] = postData.Errors; RedirectToAction(new { controller = "WikiPage", action = "EditPage", pageName = pageName }); return; } } RedirectToAction(new { controller = "WikiPage", action = "ShowPage", pageName = pageName }); } 首先,它創建一個 NewVersionPostData 對象。這是另一個幫助程序對象,具有存儲記入的內容和進行某些驗證的屬性和方法。為加載 postData 對象,我將使用 MVC 工具包的幫助程序。UpdateFrom 實際上是工具包提供的擴展方法,它使用反射將表單字段的名稱與我的對象中屬性的名稱相對映。最終結果是,所有字段值均載入到我的 postData 對象中。不過,UpdateFrom 使用起來確實有缺點,由于它直接從 HttpRequest 獲取表單數據,使單元測試變得更為困難。 NewVersion 檢查的第一項是 SubmitAction。如果用戶單擊“確定”按鈕并確實想要發布編輯的頁面,則此項檢查將通過。如果此處有任何其它值,操作會重定向回 ShowPage,只是重新顯示原來的頁面。 如果用戶確實單擊了“確定”,則檢查 postData.Errors 屬性。這將在記入內容上運行一些簡單的驗證。如果沒有任何錯誤,我會將新版本的頁面重新寫入 wiki。不過,如果出現錯誤,情況會變得饒有趣味。 如果出現錯誤,我會設置 TempData 詞典的各個字段,以便其包含 PostData 的內容。然后,我會重定向回“編輯”頁面。現在,由于已設置 TempData,頁面將重新顯示以用戶上次記入的值初始化的表單。 處理記入、驗證和 TempData 的這個過程現在變得有些煩瑣,并且需要多做一些手動工作。將來發行的版本應包括至少會將一些 TempData 檢查自動化的幫助程序方法。關于 TempData 的最后一個注意事項是:TempData 的內容存儲在用戶的服務器端會話中。如果您關閉會話,TempData 將無法正常工作。
創建控制器 現在,wiki 的基礎已在發揮功效,但繼續進行之前,我想要明確實現中的以下幾個要點。例如,Repository 屬性用于分離 wiki 的邏輯與物理存儲。您可以提供在文件系統(正如我所做的那樣)、數據庫或您想要的任何位置中存儲內容的存儲庫。遺憾的是,我需要解決兩個問題。 首先,我的控制器類與具體的 FileBasedSpaceRepository 類緊密地連在一起。我需要一個默認值,以便在屬性沒有設置時,也能合理使用。更糟的是,磁盤上文件的路徑在這里也是硬編碼的。最起碼,它應取自配置。 其次,我的對象必須依賴存儲庫,否則無法運行。對于良好的設計,存儲庫實際應為構造函數 參數,而不是屬性。但我無法將其添加到構造函數中,因為 MVC Framework 要求控制器上的構造函數不能有參數。 幸運的是,我可以通過一個擴展性掛接擺脫此限制:即控制器工廠??刂破鞴S的功能正如其 名稱所指:它創建 Controller 實例。您只需要創建一個類實現 IControllerFactory 接口并向 MVC 系統注冊即可。您可以為所有控制器或僅為指定的類型注冊控制器工廠。圖 13 所示為 WikiPageController 的控制器工廠,其現在將存儲庫作為構造函數參數傳遞。在這種情況下,實現非常煩瑣,但它可以創建能使用更強大工具(特別是依賴關系注入容器)的控制器。 無論如何,現在我擁有了將控制器依賴關系分離到對象中(易于管理和維護)的所有詳細信息。 ?Figure?13?Controller Factory 復制代碼 public class WikiPageControllerFactory : IControllerFactory { public IController CreateController(RequestContext context, Type controllerType) { return new WikiPageController( GetConfiguredRepository(context.HttpContext.Request)); } private ISpaceRepository GetConfiguredRepository(IHttpRequest request) { return new FileBasedSpaceRepository(request.MapPath("~/WikiPages")); } } 此工作的最后一步是向框架注冊工廠。通過 ControllerBuilder 類可進行此操作,方法是將以下行添加到 Application_Start 方法中的 Global.asax.cs(路由前后均可): 復制代碼 ControllerBuilder.Current.SetControllerFactory(
typeof(WikiPageController), typeof(WiliPageControllerFactory));
這將注冊 WikiPageController 的工廠。如果此項目中有其他控制器,它們不會使用此工廠,因為此工廠僅針對 WikiPageController 類型進行了注冊。如果您想要將工廠設置為供所有控制器使用,還可以調用 SetDefaultControllerFactory。
其他擴展點 控制器工廠只是框架擴展性的起點。本文中無法詳述所有的細節,因此我將僅僅說明要點。首 先,如果您想要輸出的內容不是 HTML,或想要使用其他模板引擎而不是 Web 窗體,可將控制器的 ViewFactory 設為其他項。您可以實現 IviewFactory 界面,然后即可完全控制如何生成輸出。這對于生成 RSS、XML 或圖形非常有用。 正如您所見到的,路由系統非常靈活。但路由系統中沒有任何內容是 MVC 專用的。每個路由均有一個 RouteHandler 屬性;目前為止,我始終將其設為 MvcRouteHandler。但可以實現 IRouteHandler 界面并將路由系統與其他 Web 技術掛接。將來推出的框架將附帶 WebFormsRouteHandler,并且其他技術也會在將來利用通用路由系統的優勢。 控制器并非必須從 System.Web.Mvc.Controller 衍生??刂破餍枰龅膬H僅是實現 IController 界面,該界面只有稱為 Execute 的一種方法。您可以從中進行任何操作。另一方面,如果您想將 Controller 基類的幾種行為組合在一起,您可以覆蓋 Controller 的許多虛擬函數:
  • OnPreAction、OnPostAction 和 OnError 可讓您將每個已執行操作上的預處理和后處理連接起來。OnError 為您提供在控制器內處理錯誤的機制。
  • 當 URL 路由到控制器但控制器沒有實現路由中請求的操作時,會調用 HandleUnknownAction。默認情況下,此方法會拋出一個異常,但您可以用所需的操作覆蓋默認值。
  • InvokeAction 是一種方法,它負責解決調用何種操作方法并進行調用。如果您想要自定義過程(例如,除去 [ControllerAction] 屬性的要求),應使用該方法。
還有其他幾種針對 Controller 的虛擬方法,但這些方法主要是測試掛接而不是作為擴展點。例如,RedirectToAction 是虛擬的,因此您可以創建實際并不進行重定向的衍生類。這樣,您不需要完全運行 Web 服務器就能測試重定向操作。
要告別 Web 窗體嗎? 現在您可能在想:“Web 窗體會面臨怎樣的命運?MVC 會取代它嗎?”答案是否定的!Web 窗體是一種普及技術,Microsoft 將繼續支持并改進它。它在許多應用程序中發揮著重要的作用;例如,可使用 Web 窗體創建典型的 Intranet 數據庫報表應用程序,所花的時間比使用 MVC 編寫短得多。此外,Web 窗體支持大量的控件,許多控件均具備非常先進的功能,可以大大提高效率。 那么,什么時候應該選擇 MVC 呢?這主要取決于您的要求和喜好。您是否正在為獲得想要的 URL 格式而煩惱?您是否想要對 UI 進行單元測試?以上情況均需要依靠 MVC。反之,如果您要顯示許多數據,提供可編輯的網格和優良的樹形視圖控件?那么,您暫時最好還是使用 Web 窗體。 今后,MVC Framework 很可能在 UI 控制部分有所改進,但在便利性上,它可能始終不及 Web 窗體,因為后者具備大量拖曳功能。同時,ASP.NET MVC Framework 為 Web 開發人員提供了一種在 Microsoft .NET Framework 中構建 Web 應用程序的新方法。Framework 針對可測試性設計、推倡使用 HTTP 并且幾乎在每個點均可擴展。對于那些想要完全控制其 Web 應用程序的開發人員來說,這是一個對 Web 窗體的誘人補充。
Chris Tavares 是 Microsoft 模式和實施方案小組的一名開發人員,他致力于幫助開發社區了解在 Microsoft 平臺上構建系統的最佳實踐。他還是 ASP.NET MVC 小組的虛擬成員,幫助您設計新的框架??梢酝ㄟ^ cct@tavaresstudios.com 與 Chris 取得聯系。 我 從事專業開發迄今為止已有 15 年,在此之前,我利用業余時間從事開發至少也有 10 年了。與我這一代的大多數人一樣,我是從 8 位計算機起步,然后轉用 PC 平臺的。隨著計算機的復雜性日益增加,我編寫的應用程序涵蓋了從小型游戲到個人數據管理再到控制外部硬件的各項功能。 不過,在我職業生涯的前半段,我編寫的所有軟件都有一個共同點:即,都是運行在用戶桌面 上的本地應用程序。我最早是在 90 年代初期聽說萬維網這件新生事物。那時我發現,通過構建 Web 應用程序,可以讓我輸入我的考勤卡信息而不必再費時費力從工作場所趕回辦公室。 一言以蔽之,我感覺很是困惑。我當時滿腦子是面向桌面的理念,很難接納這種無狀態的 Web。要添加很多讓人頭疼的調試、我沒有 UNIX 服務器的超級用戶訪問權限,再加上這個奇怪的角括號,這些因素使年輕時的我止步不前,又重返桌面開發渡過了幾年時光。 我遠離了 Web 開發領域,雖然這領域顯然很重要,但我并沒有真正理解其編程模型。然后,Microsoft? .NET Framework 和 ASP.NET 發行了。盡管它與桌面應用程序編程有許多相似之處,但終于有了可以讓我從事 Web 應用程序編程的框架。我可以構建窗口(頁面),將控件與事件掛鉤,而設計器使我不必處理那些討厭的角括號。最妙的是,ASP.NET 會通過查看狀態自動為我處理 Web 的無狀態性質!我又重新找回了程序員的快樂 ... 至少在一段時間內是如此。 隨著經驗的增加,我的設計內容也隨之豐富。我早已掌握了幾種最佳實踐,并將其應用到桌面 應用程序編程。其中的兩種就是:
  • 分離關注點:不要將 UI 邏輯與基礎行為混合在一起。
  • 自動單元測試:編寫自動測試以驗證您的代碼是否按預期執行。
這些是適用于任何技術的基本原則。分離關注點是一項可幫助您處理復雜問題的基本原則。在 同一個對象內混合多種責任(如計算剩余的工時、設置數據格式并繪圖)會給維護帶來很大的負擔。而自動測試對于獲得生產質量的代碼同時仍保持條理性至關重 要,尤其是當您更新現有項目時更是如此。 ASP.NET Web 窗體使入門變得非常簡單,但另一方面,要將我的設計理念應用到 Web 應用程序卻并非易事。Web 窗體堅持以 UI 為側重點;其基本單位為頁面。首先設計 UI 并拖曳控件。只需將應用程序邏輯融入頁面的事件處理程序(與為 Windows? 應用程序啟用的 Visual Basic? 非常相似)就萬事大吉,這一點非常吸引人。 但進一步的頁面單元測試常常有很大困難。您必須先啟動所有 ASP.NET,然后才能在“頁面”對象的生命周期內運行該對象。盡管可以通過發送 HTTP請求到服務器或自動化瀏覽器來測試 Web 應用程序,但這類測試非常脆弱(更換一個控制 ID 測試就會中斷)、難以設置(您必須以完全相同的方式在每位開發人員的計算機上設置該服務器)并且運行緩慢。 當我開始構建更復雜的 Web 應用程序時,Web 窗體提供的抽象概念(如控件、視圖狀態和頁面生命周期)就開始添亂而不是幫忙了。我需要花越來越多的時間來配置數據綁定(并編寫大量的事件處理程序對其進 行正確配置)。我不得不想辦法縮減視圖狀態的大小以便更快加載我的頁面。Web 窗體要求每個 URL 均存在物理文件,這對于動態站點(例如 wiki)非常困難。而成功編寫一個自定義的 WebControl 是一個非常復雜的過程,需要全面了解頁面生命周期和 Visual Studio? 設計器。 自從在 Microsoft 工作開始,我就一直與其他人分享關于各種 .NET 難題的體驗并希望可以解決一些難題。最近,作為開發人員參加有關模式與實踐的 Web 客戶端軟件工廠項目 (codeplex.com/websf) 時,我遇到了一個這樣的機會。特別是,模式與實踐交付的內容之一就是自動單元測試。在 Web 客戶端軟件工廠中,我們建議使用 Model View Presenter (MVP) 模式構建可測試的 Web 窗體。 簡而言之,MVP 并非將您的邏輯放入頁面中,而是讓您構建自己的頁面,頁面 (View) 只需調用單獨的對象,即 Presenter。Presenter 對象隨即執行響應視圖上活動必需的任何邏輯,通常通過使用其它對象 (Model) 訪問數據庫、執行業務邏輯等。一旦這些步驟完成后,Presenter 會更新視圖。這種方法提供了可測試性,因為表示器從 ASP.NET 管道中隔離出來;它與視圖通過界面進行通信并可脫離頁面獨立進行測試。 MVP 的這種功能實現有點笨;您需要單獨的視圖界面,并且您必須在源代碼文件中編寫許多事件轉發函數。但如果您想要在 Web 窗體應用程序中得到可測試的 UI,這差不多是最佳途徑。任何改進均需要在基礎平臺中做出更改。
模型視圖控制器模式 幸運的是,ASP.NET 團隊聽取了象我這樣的開發人員的意見,并且已經著手開發一種新的 Web 應用程序框架,該框架與您所熟知并喜愛的 Web 窗體處于同一層級,但采用一組完全不同的設計目標:
  • 使用 HTTP 和 HTML—不隱藏。
  • 可測試性貫穿整個框架之內。
  • 幾乎在每個點均可擴展。
  • 對輸出進行總體控制。
由于此新框架基于模型視圖控制器 (MVC) 模式,因此其名稱為 ASP.NET MVC。MVC 模式最初在 70 年代發明,是 Smalltalk 技術的一部分。正如我將在本文中所展示的,它實際上非常適合 Web 的性質。MVC 將您的 UI 分為三種不同的對象:用于接收和處理輸入的控制器;包含您域邏輯的模型;以及用于生成輸出的視圖。在 Web 環境中,輸入為 HTTP 請求,而請求流程與圖 1 類似。 Figure 1?MVC 模式請求流程?(單 擊該圖像獲得較大視圖) 這實際上與 Web 窗體中的過程完全不同。在 Web 窗體模型中,輸入進入頁面(視圖),然后視圖負責處理輸入并生成輸出。而 MVC 中這些責任是分開的。 因此,您可能立即會產生以下一種想法:“嘿,這太好了。我應該如何使用它?”或“為什么 我要編寫這些對象,以前只需要編寫一個對象?”這兩個問題都問得很好,最好通過示例來進行解釋。因此,我將使用 MVC Framework 編寫一個小型 Web 應用程序以說明其優點。
創建控制器 要繼續進行,您將需要安裝 Visual Studio 2008 并獲得 MVC Framework 的副本。在撰寫本文時,ASP.NET 擴展的 2007 年 12 月社區技術預覽 (CTP) 中已提供了這些內容 (asp.net/downloads/3.5-extensions)。您可能想要獲取擴展 CTP 和 MVC 工具包,其中包括一些非常有用的幫助程序對象。一旦下載并安裝 CTP 后,您將在“新建項目”對話框中獲得名為“ASP.NET MVC Web 應用程序”的新項目類型。 選擇“MVC Web 應用程序”項目后,會為您提供一個與常用網站或應用程序稍有不同的解決方案。該解決方案模板會創建一個帶有一些新目錄的 Web 應用程序(如圖 2 中所示)。特別是 Controllers 目錄包含各種控制器類,而 Views 目錄(及其所有子目錄)包含了各種視圖。 Figure 2?MVC 項目結構? 我將會編寫一個非常簡單的控制器,返回 URL 中傳遞的名稱。右鍵單擊 Controllers 文件夾并選擇“添加項目”以顯示常用的“添加項目”對話框以及一些新增加的內容,包括 MVC 控制器類和幾個 MVC 視圖組件。在此例中,我將添加一個非常富有想象力、名為 HelloController 的類: 復制代碼 using System;
using System.Web;
using System.Web.Mvc;
namespace HelloFromMVC.Controllers
{
public class HelloController : Controller
{
[ControllerAction]
public void Index()
{
...
}
}
}
控制器類比頁面簡單得多。實際上,唯一真正必需做的就是從 System.Web.Mvc.Controller 中衍生并將 [ControllerAction] 屬性置于您的操作方法中。操作是調用以響應特定 URL 請求的一種方法。操作負責執行所需的一切處理,然后呈現一個視圖。我將通過編寫一個將名稱傳遞到視圖的簡單操作著手,如下所示: 復制代碼 [ControllerAction]
public void HiThere(string id)
{
ViewData["Name"] = id;
RenderView("HiThere");
}
操作方法會通過 ID 參數從 URL 接收該名稱(稍后會介紹方法),將其存儲在 ViewData 集合中,然后呈現名為 HiThere 的視圖。 在討論如何調用此方法,或該視圖的顯示內容之前,我希望說一說可測試性。還記得我之前關 于測試 Web 窗體頁面類有多難的評論嗎?控制器的測試簡單得多。實際上,控制器可以直接實例化,而調用操作方法無需任何附加的基礎結構。您不需要 HTTP 上下文,也不需要服務器,只要測試工具即可。作為示例,我在圖 3 中為此類包括了 Visual Studio Team System (VSTS) 測試單元。 ?Figure?3?Controller Unit Test 復制代碼 namespace HelloFromMVC.Tests
{
[TestClass]
public class HelloControllerFixture
{
[TestMethod]
public void HiThereShouldRenderCorrectView()
{
TestableHelloController controller = new
TestableHelloController();
controller.HiThere("Chris");
Assert.AreEqual("Chris", controller.Name);
Assert.AreEqual("HiThere", controller.ViewName);
}
}
class TestableHelloController : HelloController
{
public string Name;
public string ViewName;
protected override void RenderView(
string viewName, string master, object data)
{
this.ViewName = viewName;
this.Name = (string)ViewData["Name"];
}
}
}
下面將進行幾項操作。實際的測試相當簡單:實例化該控制器,使用預期的數據調用該方法, 然后檢查呈現的視圖是否正確。我通過創建測試專用的子類覆蓋 RenderView 方法進行檢查。這可以縮短實際創建 HTML 的時間。我只關心是否將正確的數據發送到視圖以及是否呈現了正確的視圖。我不關心此測試視圖本身的底層詳細信息。
創建視圖 當然,最終我必須生成一些 HTML,因此,讓我們創建該 HiThere 視圖。要進行此操作,首先,我將在解決方案中的 Views 文件夾下創建名為 Hello 的新文件夾。默認情況下,控制器將在 Views\<控制器前綴> 文件夾(控制器前綴為控制器類的名稱去掉 "Controller" 字樣)中查找視圖。因此,對于 HelloController 呈現的視圖,它會在 Views\Hello 中查找。解決方案的查找結果如圖 4 所示。 Figure 4?將視圖添加到項目中?(單 擊該圖像獲得較大視圖) 視圖的 HTML 如下所示: 復制代碼 <html >
<head runat="server">
<title>Hi There!</title>
</head>
<body>
<div>
<h1>Hello, <%= ViewData["Name"] %></h1>
</div>
</body>
</html>
應注意以下幾件事。沒有 runat="server" 標記。沒有 form 標記。沒有控件聲明。實際上,這看起來更象傳統的 ASP 而不是 ASP.NET。請注意,MVC 視圖僅負責生成輸出,因此其不需要任何 Web 窗體頁面所需的事件處理或復雜控件。 MVC Framework 借用了 .aspx 文件格式作為一種有用的文本模板語言。如果需要,甚至可以使用源代碼,但默認情況下,源代碼文件如下所示: 復制代碼 using System;
using System.Web;
using System.Web.Mvc;
namespace HelloFromMVC.Views.Hello
{
public partial class HiThere : ViewPage
{
}
}
沒有頁面初始化或加載方法,沒有事件處理程序,除了基類聲明以外沒有任何內容,基類聲明 為 ViewPage 而不是 Page。這就是 MVC 視圖所需的一切。運行該應用程序,導航至 http://localhost:<端口>/Hello/HiThere/Chris,您將看到如圖 5 所示的內容。 Figure 5?成功的 MVC 視圖?(單 擊該圖像獲得較大視圖) 如果您看到的并非如圖 5 所示,而是難以理解的意外情況,請不要驚慌。如果您將 HiThere.aspx 文件設置為 Visual Studio 中的活動文檔,則當按 F5 后,Visual Studio 將嘗試直接訪問 .aspx 文件。由于 MVC 視圖要求控制器在顯示前運行,因此嘗試直接導航至該頁面將不起作用。只需將該 URL 編輯為與圖 5 中所示的內容相匹配,即可正常工作。 MVC Framework 如何知道調用我的操作方法?該 URL 甚至沒有文件擴展名。答案是 URL 路由。如果您仔細查看 global.asax.cs 文件,則會看到如圖 6 所示的代碼段。全局 RouteTable 會存儲 Route 對象的集合。每個 Route 說明一個 URL 窗體以及對其進行何種操作。默認情況下,會向該表中添加兩個路由。第一個是該方法的內容。它說明每個 URL 在服務器名后均由三部分組成,第一部分應為控制器名,第二部分為操作名稱,而第三部分為 ID 參數。 ?Figure?6?Route Table 復制代碼 public class Global : System.Web.HttpApplication
{
protected void Application_Start(object sender, EventArgs e)
{
// Change Url= to Url="[controller].mvc/[action]/[id]"
// to enable automatic support on IIS6
RouteTable.Routes.Add(new Route
{
Url = "[controller]/[action]/[id]",
Defaults = new { action = "Index", id = (string)null },
RouteHandler = typeof(MvcRouteHandler)
});
RouteTable.Routes.Add(new Route
{
Url = "Default.aspx",
Defaults = new {
controller = "Home",
action = "Index",
id = (string)null },
RouteHandler = typeof(MvcRouteHandler)
});
}
}
復制代碼 Url = "[controller]/[action]/[id]"
此默認路由是能讓我的 HiThere 方法得以調用的路由。請記住此 URL:http://localhost/Hello/HiThere/Chris?此路由將 Hello 與控制器、HiThere 與操作以及 Chris 與 ID 一一對應。MVC Framework 隨即創建 HelloController 實例,調用 HiThere 方法,然后將 Chris 作為 ID 參數的值傳遞。 此默認路由為您提供了許多功能,但您也可以添加自己的路由。例如,我想要一個真正友好的 站點,好友們只需輸入他們的姓名即可獲得個性化的問候。如果我在路由表的頂部添加以下路由 復制代碼 RouteTable.Routes.Add(new Route
{
Url = "[id]",
Defaults = new {
controller = "Hello",
action = "HiThere" },
RouteHandler = typeof(MvcRouteHandler)
});
隨后,我只需訪問 ,我的操作仍處于調用狀態,而我將會看到熟悉的友好問候。 系統如何知道調用哪個控制器和操作?答案是 Defaults 參數。它利用新的 C# 3.0 匿名類型語法來創建一個偽詞典。Route 上的 Defaults 對象可包含任意附加的信息,對于 MVC,它還可以包含一些眾所周知的條目:即控制器和操作。如果 URL 中沒有指定控制器或操作,則其將使用 Defaults 中的名稱。這就是為什么即使我在 URL 中忽略它們,但仍可以將我的請求映射到正確的控制器和操作。 還有一件事需要注意:還記得我說過“添加到表格的頂部”嗎?如果您將其置于底部,將會出 現錯誤。路由根據先到先得的原則進行工作。當處理 URL 時,路由系統會自上至下瀏覽表格,并且使用第一個匹配的路由。在本例中,默認路由 "[controller]/[action]/[id]" 匹配,因為它們是操作和 ID 的默認值。這樣,它會繼續查找 ChrisController,但我沒有控制器,因此會出現錯誤。
稍大的示例 現在,我已經說明了 MVC Framework 的基礎知識,將為您展示一個更大的示例,實現比僅顯示字符串更多的功能。wiki 是一種可以在瀏覽器中進行編輯的網站。可以輕松地添加或編輯頁面。我使用 MVC Framework 編寫了一個小型的示例 wiki?!熬庉嫶隧撁妗逼聊蝗?strong>圖 7 所示。 Figure 7?編輯主頁?(單擊該 圖像獲得較大視圖) 您可以檢查本文的代碼下載以查看如何實現底層 wiki 邏輯。現在我想重點說明 MVC Framework 如何使 Web 上的 wiki 獲取變得簡單。讓我們先設計 URL 結構。我想要以下各項:
  • /[pagename] 顯示該名稱的頁面。
  • /[pagename]?version=n 顯示頁面的請求版本,其中 0 = 當前版本,1 = 以前的版本,以此類推。
  • /Edit/[pagename] 打開該頁的編輯屏幕。
  • /CreateNewVersion/[pagename] 是為提交編輯而傳入的 URL。
讓我們從 wiki 頁面的基本顯示開始。我為它創建了一個名為 WikiPageController 的新類。接下來,我會添加一個名為 ShowPage 的操作。啟動的 WikiPageController 如圖 8 所示。ShowPage 方法相當簡單。WikiSpace 和 WikiPage 類分別表示一組 wiki 頁面和特定的頁面(及其修訂)。此操作只需加載模型并調用 RenderView。但此處的 "new WikiPageViewData" 行是什么意思? ?Figure?8?WikiPageController Implementation of ShowPage 復制代碼 public class WikiPageController : Controller
{
ISpaceRepository repository;
public ISpaceRepository Repository
{
get {
if (repository == null)
{
repository = new FileBasedSpaceRepository(
Request.MapPath("~/WikiPages"));
}
return repository;
}
set { repository = value; }
}
[ControllerAction]
public void ShowPage(string pageName, int? version)
{
WikiSpace space = new WikiSpace(Repository);
WikiPage page = space.GetPage(pageName);
RenderView("showpage",
new WikiPageViewData
{
Name = pageName,
Page = page,
Version = version ?? 0
});
}
}
我前面的示例說明了一種將數據從控制器傳遞到視圖的方法:即 ViewData 詞典。詞典非常方便,但也很危險。它們幾乎包含一切內容,您不能獲取內容的任何 IntelliSense?,并且由于 ViewData 詞典屬于 Dictionary<string, object> 類型,它將消耗內容,您必須計算所有一切。 當您了解在視圖中將需要什么數據后,就可以傳遞強類型化的 ViewData 對象。在我的示例中,我創建了一個簡單的對象 (WikiPageViewData),如圖 9 中所示。此對象將 wiki 頁面信息帶到視圖,同時還攜帶了一些實用工具方法,執行獲取 wiki 標記的 HTML 版本這類任務。 ?Figure?9?WikiPageViewData Object 復制代碼 public class WikiPageViewData {
public string Name { get; set; }
public WikiPage Page { get; set; }
public int Version { get; set; }
public WikiPageViewData() {
Version = 0;
}
public string NewVersionUrl {
get {
return string.Format("/CreateNewVersion/{0}", Name);
}
}
public string Body {
get { return Page.Versions[Version].Body; }
}
public string HtmlBody {
get { return Page.Versions[Version].BodyAsHtml(); }
}
public string Creator {
get { return Page.Versions[Version].Creator; }
}
public string Tags {
get { return string.Join(",", Page.Versions[Version].Tags); }
}
}
現在,我已經定義了視圖數據,那么,我如何使用它呢?在 ShowPage.aspx.cs 中,您將看到以下內容: 復制代碼 namespace MiniWiki.Views.WikiPage {
public partial class ShowPage : ViewPage<WikiPageViewData>
{
}
}
請注意,我將基類類型定義為 ViewPage<WikiPageViewData>。這意味著頁面的 ViewData 屬性為 WikiPageViewData 類型,而不是象以前示例中的“Dictionary”。 .aspx 文件中的實際標記非常簡單: 復制代碼 <%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master"
AutoEventWireup="true" CodeBehind="ShowPage.aspx.cs"
Inherits="MiniWiki.Views.WikiPage.ShowPage" %>
<asp:Content
ID="Content1"
ContentPlaceHolderID="MainContentPlaceHolder"
runat="server">
<h1><%= ViewData.Name %></h1>
<div id="content" class="wikiContent">
<%= ViewData.HtmlBody %>
</div>
</asp:Content>
請注意,當引用 ViewData 時,我沒有使用索引操作符 []。由于我現在有強類型化的 ViewData,我可以直接訪問該屬性。不需要進行任何計算,而 Visual Studio 會提供 IntelliSense。 目光敏銳的讀者將會注意到此文件中的 <asp:Content> 標記。沒錯,“母版頁”確實可以與 MVC 視圖配合使用。并且“母版頁”還可以成為視圖。讓我們看看“母版頁”的源代碼: 復制代碼 namespace MiniWiki.Views.Layouts
{
public partial class Site :
System.Web.Mvc.ViewMasterPage<WikiPageViewData>
{
}
}
相關標記如圖 10 中所示?,F在,“母版頁”將獲得與視圖完全相同的 ViewData 對象。我已經將“母版頁”的基類聲明為 ViewMasterPage<WikiPageViewData>,因此,我擁有了正確類型的 ViewData。我會在那里設置各種 DIV 標記以對頁面進行布局,填寫版本列表,然后以常用內容占位符收尾。 ?Figure?10?Site.Master 復制代碼 <%@ Master Language="C#" AutoEventWireup="true" CodeBehind="Site.master.cs" Inherits="MiniWiki.Views.Layouts.Site" %> <%@ Import Namespace="MiniWiki.Controllers" %> <%@ Import Namespace="MiniWiki.DomainModel" %> <%@ Import Namespace="System.Web.Mvc" %> <html > <head runat="server"> <title><%= ViewData.Name %></title> <link href="http://http://www.cnblogs.com/Content/Site.css" rel="stylesheet" type="text/css" /> </head> <body> <div id="inner"> <div id="top"> <div id="header"> <h1><%= ViewData.Name %></h1> </div> <div id="menu"> <ul> <li><a href="http://Home">Home</a></li> <li> <%= Html.ActionLink("Edit this page", new { controller = "WikiPage", action = "EditPage", pageName = ViewData.Name })%> </ul> </div> </div> <div id="main"> <div id="revisions"> Revision history: <ul> <% int i = 0; foreach (WikiPageVersion version in ViewData.Page.Versions) { %> <li> <a href="http://<%= ViewData.Name %>?version=<%= i %>"> <%= version.CreatedOn %> by <%= version.Creator %> </a> </li> <% ++i; } %> </ul> </div> <div id="maincontent"> <asp:ContentPlaceHolder ID="MainContentPlaceHolder" runat="server"> </asp:ContentPlaceHolder> </div> </div> </div> </body> </html> 另一件需要注意的事是對 Html.ActionLink 的調用。以下是呈現幫助程序的一個示例。各種視圖類均具有兩種屬性,Html 和 Url。每種均有輸出 HTML 代碼塊的有用方法。在本例中,Html.ActionLink 獲取一個對象(此處為匿名類型)并通過路由系統將其返回。這將會生成一個 URL,該 URL 將路由至我指定的控制器和操作。這樣一來,無論我如何更改路由,“編輯此頁面”鏈接將始終指向正確的位置。 您可能還注意到,我還不得不依靠手動構建鏈接(到先前頁面版本的鏈接)。遺憾的是,當前 的路由系統在涉及查詢字符串時生成 URL 的功能不是十分完善。這應會在框架的后續版本中得到修復。
創建表單和回發 現在,讓我們看看控制器上的 EditPage 操作: 復制代碼 [ControllerAction]
public void EditPage(string pageName)
{
WikiSpace space = new WikiSpace(Repository);
WikiPage page = space.GetPage(pageName);
RenderView("editpage",
new WikiPageViewData {
Name = pageName,
Page = page });
}
同樣,該操作所做的不多—它只是呈現指定頁面的視圖。視圖中的內容變得更加有趣,如圖 11 中所示。此文件構建了一個 HTML 表單,但沒有出現 Runat="server"。Url.Action helper 用于生成表單回發的 URL。其中還使用了幾種不同的 HTML 幫助程序(如 TextBox、TextArea 和 SubmitButton)。它們會出色完成您的預期目標:為各種輸入字段生成 HTML。 ?Figure?11?EditPage.aspx 復制代碼 <%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master" AutoEventWireup="true" CodeBehind="EditPage.aspx.cs" Inherits="MiniWiki.Views.WikiPage.EditPage" %> <%@ Import Namespace="System.Web.Mvc" %> <%@ Import Namespace="MiniWiki.Controllers" %> <asp:Content ID="Content1" ContentPlaceHolderID="MainContentPlaceHolder" runat="server"> <form action="<%= Url.Action( new { controller = "WikiPage", action = "NewVersion", pageName = ViewData.Name })%>" method=post> <% if (ViewContext.TempData.ContainsKey("errors")) { %> <div id="errorlist"> <ul> <% foreach (string error in (string[])ViewContext.TempData["errors"]) { %> <li><%= error%></li> <% } %> </ul> </div> <% } %> Your name: <%= Html.TextBox("Creator", ViewContext.TempData.ContainsKey("creator") ? (string)ViewContext.TempData["creator"] : ViewData.Creator)%> <br /> Please enter your updates here:<br /> <%= Html.TextArea("Body", ViewContext.TempData.ContainsKey("body") ? (string)ViewContext.TempData["body"] : ViewData.Body, 30, 65)%> <br /> Tags: <%= Html.TextBox( "Tags", ViewContext.TempData.ContainsKey("tags") ? (string)ViewContext.TempData["tags"] : ViewData.Tags)%> <br /> <%= Html.SubmitButton("SubmitAction", "OK")%> <%= Html.SubmitButton("SubmitAction", "Cancel")%> </form> </asp:Content> 處理 Web 編程最頭疼的事情之一就是表單中的錯誤。更確切地說,您想要顯示錯誤信息,但同時想要保留原來輸入的數據。我們都有過那種經歷,在填寫一張有 35 個字段的表單時出現一個錯誤,程序卻只是提供一堆錯誤信息和一張新的空白表單。MVC Framework 使用 TempData 存儲以前輸入信息,以便可以重新填入表單。這是 ViewState 實際上在 Web 窗體中變得非常簡單的原因,因為保存控件的內容幾乎是自動的。 我想在 MVC 中如法炮制,因此引入了 TempData。TempData 是一種詞典,與非類型化的 ViewData 很相似。不過,TempData 的內容僅針對單一請求存在,隨后就會被刪除。要了解如何使用此方法,請參閱圖 12,NewVersion 操作。 ?Figure?12?NewVersion Action 復制代碼 [ControllerAction] public void NewVersion(string pageName) { NewVersionPostData postData = new NewVersionPostData(); postData.UpdateFrom(Request.Form); if (postData.SubmitAction == "OK") { if (postData.Errors.Length == 0) { WikiSpace space = new WikiSpace(Repository); WikiPage page = space.GetPage(pageName); WikiPageVersion newVersion = new WikiPageVersion( postData.Body, postData.Creator, postData.TagList); page.Add(newVersion); } else { TempData["creator"] = postData.Creator; TempData["body"] = postData.Body; TempData["tags"] = postData.Tags; TempData["errors"] = postData.Errors; RedirectToAction(new { controller = "WikiPage", action = "EditPage", pageName = pageName }); return; } } RedirectToAction(new { controller = "WikiPage", action = "ShowPage", pageName = pageName }); } 首先,它創建一個 NewVersionPostData 對象。這是另一個幫助程序對象,具有存儲記入的內容和進行某些驗證的屬性和方法。為加載 postData 對象,我將使用 MVC 工具包的幫助程序。UpdateFrom 實際上是工具包提供的擴展方法,它使用反射將表單字段的名稱與我的對象中屬性的名稱相對映。最終結果是,所有字段值均載入到我的 postData 對象中。不過,UpdateFrom 使用起來確實有缺點,由于它直接從 HttpRequest 獲取表單數據,使單元測試變得更為困難。 NewVersion 檢查的第一項是 SubmitAction。如果用戶單擊“確定”按鈕并確實想要發布編輯的頁面,則此項檢查將通過。如果此處有任何其它值,操作會重定向回 ShowPage,只是重新顯示原來的頁面。 如果用戶確實單擊了“確定”,則檢查 postData.Errors 屬性。這將在記入內容上運行一些簡單的驗證。如果沒有任何錯誤,我會將新版本的頁面重新寫入 wiki。不過,如果出現錯誤,情況會變得饒有趣味。 如果出現錯誤,我會設置 TempData 詞典的各個字段,以便其包含 PostData 的內容。然后,我會重定向回“編輯”頁面。現在,由于已設置 TempData,頁面將重新顯示以用戶上次記入的值初始化的表單。 處理記入、驗證和 TempData 的這個過程現在變得有些煩瑣,并且需要多做一些手動工作。將來發行的版本應包括至少會將一些 TempData 檢查自動化的幫助程序方法。關于 TempData 的最后一個注意事項是:TempData 的內容存儲在用戶的服務器端會話中。如果您關閉會話,TempData 將無法正常工作。
創建控制器 現在,wiki 的基礎已在發揮功效,但繼續進行之前,我想要明確實現中的以下幾個要點。例如,Repository 屬性用于分離 wiki 的邏輯與物理存儲。您可以提供在文件系統(正如我所做的那樣)、數據庫或您想要的任何位置中存儲內容的存儲庫。遺憾的是,我需要解決兩個問題。 首先,我的控制器類與具體的 FileBasedSpaceRepository 類緊密地連在一起。我需要一個默認值,以便在屬性沒有設置時,也能合理使用。更糟的是,磁盤上文件的路徑在這里也是硬編碼的。最起碼,它應取自配置。 其次,我的對象必須依賴存儲庫,否則無法運行。對于良好的設計,存儲庫實際應為構造函數 參數,而不是屬性。但我無法將其添加到構造函數中,因為 MVC Framework 要求控制器上的構造函數不能有參數。 幸運的是,我可以通過一個擴展性掛接擺脫此限制:即控制器工廠??刂破鞴S的功能正如其 名稱所指:它創建 Controller 實例。您只需要創建一個類實現 IControllerFactory 接口并向 MVC 系統注冊即可。您可以為所有控制器或僅為指定的類型注冊控制器工廠。圖 13 所示為 WikiPageController 的控制器工廠,其現在將存儲庫作為構造函數參數傳遞。在這種情況下,實現非常煩瑣,但它可以創建能使用更強大工具(特別是依賴關系注入容器)的控制器。 無論如何,現在我擁有了將控制器依賴關系分離到對象中(易于管理和維護)的所有詳細信息。 ?Figure?13?Controller Factory 復制代碼 public class WikiPageControllerFactory : IControllerFactory { public IController CreateController(RequestContext context, Type controllerType) { return new WikiPageController( GetConfiguredRepository(context.HttpContext.Request)); } private ISpaceRepository GetConfiguredRepository(IHttpRequest request) { return new FileBasedSpaceRepository(request.MapPath("~/WikiPages")); } } 此工作的最后一步是向框架注冊工廠。通過 ControllerBuilder 類可進行此操作,方法是將以下行添加到 Application_Start 方法中的 Global.asax.cs(路由前后均可): 復制代碼 ControllerBuilder.Current.SetControllerFactory(
typeof(WikiPageController), typeof(WiliPageControllerFactory));
這將注冊 WikiPageController 的工廠。如果此項目中有其他控制器,它們不會使用此工廠,因為此工廠僅針對 WikiPageController 類型進行了注冊。如果您想要將工廠設置為供所有控制器使用,還可以調用 SetDefaultControllerFactory。
其他擴展點 控制器工廠只是框架擴展性的起點。本文中無法詳述所有的細節,因此我將僅僅說明要點。首 先,如果您想要輸出的內容不是 HTML,或想要使用其他模板引擎而不是 Web 窗體,可將控制器的 ViewFactory 設為其他項。您可以實現 IviewFactory 界面,然后即可完全控制如何生成輸出。這對于生成 RSS、XML 或圖形非常有用。 正如您所見到的,路由系統非常靈活。但路由系統中沒有任何內容是 MVC 專用的。每個路由均有一個 RouteHandler 屬性;目前為止,我始終將其設為 MvcRouteHandler。但可以實現 IRouteHandler 界面并將路由系統與其他 Web 技術掛接。將來推出的框架將附帶 WebFormsRouteHandler,并且其他技術也會在將來利用通用路由系統的優勢。 控制器并非必須從 System.Web.Mvc.Controller 衍生??刂破餍枰龅膬H僅是實現 IController 界面,該界面只有稱為 Execute 的一種方法。您可以從中進行任何操作。另一方面,如果您想將 Controller 基類的幾種行為組合在一起,您可以覆蓋 Controller 的許多虛擬函數:
  • OnPreAction、OnPostAction 和 OnError 可讓您將每個已執行操作上的預處理和后處理連接起來。OnError 為您提供在控制器內處理錯誤的機制。
  • 當 URL 路由到控制器但控制器沒有實現路由中請求的操作時,會調用 HandleUnknownAction。默認情況下,此方法會拋出一個異常,但您可以用所需的操作覆蓋默認值。
  • InvokeAction 是一種方法,它負責解決調用何種操作方法并進行調用。如果您想要自定義過程(例如,除去 [ControllerAction] 屬性的要求),應使用該方法。
還有其他幾種針對 Controller 的虛擬方法,但這些方法主要是測試掛接而不是作為擴展點。例如,RedirectToAction 是虛擬的,因此您可以創建實際并不進行重定向的衍生類。這樣,您不需要完全運行 Web 服務器就能測試重定向操作。
要告別 Web 窗體嗎? 現在您可能在想:“Web 窗體會面臨怎樣的命運?MVC 會取代它嗎?”答案是否定的!Web 窗體是一種普及技術,Microsoft 將繼續支持并改進它。它在許多應用程序中發揮著重要的作用;例如,可使用 Web 窗體創建典型的 Intranet 數據庫報表應用程序,所花的時間比使用 MVC 編寫短得多。此外,Web 窗體支持大量的控件,許多控件均具備非常先進的功能,可以大大提高效率。 那么,什么時候應該選擇 MVC 呢?這主要取決于您的要求和喜好。您是否正在為獲得想要的 URL 格式而煩惱?您是否想要對 UI 進行單元測試?以上情況均需要依靠 MVC。反之,如果您要顯示許多數據,提供可編輯的網格和優良的樹形視圖控件?那么,您暫時最好還是使用 Web 窗體。 今后,MVC Framework 很可能在 UI 控制部分有所改進,但在便利性上,它可能始終不及 Web 窗體,因為后者具備大量拖曳功能。同時,ASP.NET MVC Framework 為 Web 開發人員提供了一種在 Microsoft .NET Framework 中構建 Web 應用程序的新方法。Framework 針對可測試性設計、推倡使用 HTTP 并且幾乎在每個點均可擴展。對于那些想要完全控制其 Web 應用程序的開發人員來說,這是一個對 Web 窗體的誘人補充。
Chris Tavares 是 Microsoft 模式和實施方案小組的一名開發人員,他致力于幫助開發社區了解在 Microsoft 平臺上構建系統的最佳實踐。他還是 ASP.NET MVC 小組的虛擬成員,幫助您設計新的框架??梢酝ㄟ^ cct@tavaresstudios.com 與 Chris 取得聯系。

轉載于:https://www.cnblogs.com/bndy/archive/2010/07/19/1780491.html

總結

以上是生活随笔為你收集整理的ASP.NET MVC: 构建不带 Web 窗体的 Web 应用程序(转载)的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。

主站蜘蛛池模板: 亚洲天堂一区二区在线观看 | 轻轻草在线视频 | 亚洲综合黄色 | 欧美高清视频在线观看 | av久色 | 老司机午夜免费精品视频 | 伊人avav| 无码人妻精品一区二区三区不卡 | 自拍偷拍亚洲视频 | 黄色亚洲网站 | 欧美乱妇视频 | 超碰在线观看97 | 中文字幕av久久 | 欧美精品偷拍 | 在线看免费毛片 | 美女黄污网站 | 国产精品二区一区 | www.爱爱.com| 午夜精品视频在线观看 | 日韩一级网站 | 欧美成人三级视频 | 国产毛片91 | 亚洲午夜精品一区二区三区 | 国产精品无码电影 | 亚洲伊人av | 日韩成人在线视频观看 | 在线欧美亚洲 | 亚洲激情二区 | 91天堂在线观看 | 九一亚洲精品 | 日本精品一区在线观看 | 香蕉视频链接 | 欧美成人一区二免费视频软件 | 国产农村妇女aaaaa视频 | 夜夜骑天天操 | 免费av毛片| 无码人妻精品一区二区三区99日韩 | 美女视频91 | 插插插网站| 中文字幕欧美一区 | 成人av网址在线观看 | av影视网 | 九九热在线播放 | 自拍偷拍校园春色 | 开心激情五月婷婷 | 亚洲美女精品 | 狠狠躁18三区二区一区传媒剧情 | 欧美日韩激情一区二区 | 久色亚洲 | 欧美亚洲天堂 | av毛片在线播放 | 超级碰在线观看 | 春闺艳妇(h)高h产乳 | 亚洲午夜视频 | 美女十八毛片 | 欧美国产一区二区三区 | 在线观看99 | 精品成在人线av无码免费看 | sese国产| 久久av资源 | 欧美又粗又深又猛又爽啪啪九色 | 狠狠搞av | 91免费国产 | 欧美色综合网站 | 免费毛片在线播放免费 | 成人一区二区三区四区 | 最新av导航 | 国产一区二区三区四区五区在线 | 国产成人精品一区二区三区福利 | 一级片久久久久 | 亚洲欧美一区二区在线观看 | www.日韩在线观看 | 在线看毛片网站 | 欧美精品video | 96国产视频| 人妻少妇精品无码专区 | 蜜桃视频成人 | 亚洲国产精品视频一区 | 欧美一区二区福利视频 | 久久久久久久九九九九 | 国产黄a三级三级三级看三级男男 | 欧美人xxxx | 亚洲激情一区二区三区 | av免费资源| 一区二区三区视频在线观看免费 | av影院在线观看 | 少妇激情一区二区三区 | 99热18| a级在线看 | 福利二区三区 | 91超薄丝袜肉丝一区二区 | 少妇情理伦片丰满午夜在线观看 | 黄色av导航| 欧美区一区 | 综合人人 | 午夜插插 | 免费国产一级 | 欧美一区二区久久久 | 成年人在线免费观看 |