Asp.Net MVC4 系列--進階篇之View

mybwu_com發表於2014-04-12

自定義一個ViewEngine

一般的,不需要自定義建立一個View Engine,但為了說明View部分的全貌,先從自定義ViewEngine開始,逐漸全面瞭解MVCFramework的View部分實現。

需要實現的介面

public interface IViewEngine {
ViewEngineResult  FindPartialView(ControllerContextcontrollerContext,
string partialViewName, bool useCache);
ViewEngineResult  FindView(ControllerContext controllerContext,
string viewName, string masterName, bool useCache);
void ReleaseView(ControllerContext controllerContext, IViewview);
}


介面說明:

1.查詢PartialView

2.查詢View

3.釋放View

除了最後一個函式返回值為空外,前兩個函式都需要返回一個ViewEngineResult ,程式碼如下:

public class ViewEngineResult {
public  ViewEngineResult(IEnumerable<string> searchedLocations) {
if (searchedLocations == null) {
throw  new  ArgumentNullException("searchedLocations");
}
SearchedLocations  =  searchedLocations;
}
public  ViewEngineResult(IView view ,  IViewEngine  viewEngine) {
if (view == null) { throw newArgumentNullException("view");}
if (viewEngine == null) { throw  new  ArgumentNullException("viewEngine");}
View = view;
ViewEngine = viewEngine;
}
public IEnumerable<string> SearchedLocations { get;private set; }
public IView View { get; private set; }
public IViewEngine ViewEngine { get; private set; }
}


可以看到,ViewEngineResult的構造可以通過兩種建構函式:

1.如果找不到View,那麼給一個已經找了哪些路徑(最後會show給user,如果找了所有的路徑,到最後都沒有找到)

2.如果找到了,那麼給這個view物件和它的ViewEngine(由誰來render)

IView介面:

public interface IView {
void  Render(ViewContext  viewContext, TextWriter  writer);
}


一個函式:一手拿著ViewContext(包含了這次請求的資訊),一手拿著writer,把資訊寫成html,render到客戶端,推送到瀏覽器直接消費。

示例實現:

1.Controller

  public class TestController : Controller
{

        public ActionResult Index()
        {
            ViewData["Key1"] ="Value1";
            ViewData["Key2"] =DateTime.Now;
            ViewData["Key3"] = 3;
            return View("Test");
        }
}


實現了TestController,裡面有一個Index Action,返回了一個Test View(在Viewsfolder中不存在)。

2.Customize View

public class ViewDataPrinter : IView
    {
       public void Render(ViewContext viewContext, TextWriter writer)
       {
           Write(writer, "---View Data---");
           foreach (string key in viewContext.ViewData.Keys)
           {
                Write(writer, "Key: {0},Value: {1}", key,
                viewContext.ViewData[key]);
           }
       }
       private void  Write(TextWriter writer, string  template, params object[] values)
       {
           writer.Write(string.Format(template, values) + "<p/>");
       }
}


功能:把viewContext中的ViewData列印出來

3.Customize View Engine 實現

public class TestViewEngine : IViewEngine
    {
       Public  ViewEngineResult  FindView(ControllerContext  controllerContext,
       String  viewName, string  masterName, bool  useCache)
       {
           if (viewName == "Test")
           {
                return new ViewEngineResult(new ViewDataPrinter(), this);
           }
           return new  ViewEngineResult(new [] { "Sorry , Only service for TestView" });
       }
 
       public ViewEngineResult  FindPartialView(ControllerContext controllerContext,
       string partialViewName, bool useCache)
       {
           return new  ViewEngineResult(new[] { "Sorry , Not Support ParcialView Currently" });
       }
       public void ReleaseView(ControllerContext controllerContext, IView view)
       {
           
       }
}


功能:

我們只實現了FindView,不支援FindPartialView,FindView中只支援名字為“Test”的View,如果名字匹配,返回一個ViewDataPrinter例項,把自己也傳進去。

4.註冊ViewEngine

MVC Framework中支援多個ViewEngine,如果只希望新增到ViewEngine中的最後一個,那麼直接用Add就可以了:

ViewEngines.Engines.Add(new TestViewEngine());


當然,如果考慮到Apply的順序,那麼也可以insert到第一個:

ViewEngines.Engines.Insert(0, new TestViewEngine());

如果想把預設的ViewEngine刪掉,只保留自己customize的:

ViewEngines.Engines.Clear();

ViewEngines.Engines.Add(new TestViewEngine());

5.測試


訪問TestController的Index Action,會呼叫剛才customize的ViewEngine,建立一個ViewDataResult,裡面傳遞的是ViewDataPrinter,render時列印ViewData的資訊。

測試2:

在 Test Controller 新增一個Action,指向一個不存在的View

 public ActionResult NoExist()
       {
           return View("NoExist");
       }

public ActionResult NoExist()

{

return View("NoExist");

}

訪問這個Action :

<2>

可以看到,出現了”Sorry , Only Service For TestView “ ,是我們Customize的ViewEngine列印的資訊。

注意,如果試圖訪問一個根本不存在的Controller,或者是Action,會得到404。

Razor

透過一個例子初探語法:



Controller :


public class HomeController : Controller { 
public  ActionResult Index() { 
string[]  names = { "Apple", "Orange", "Pear" }; 
return View(names); 
} 
}



View:


@model string[] 
@{ 
ViewBag.Title = "Index"; 
} 
This is a list of fruit names: 
@foreach (string name in Model) { 
<span><b>@name</b></span> 
}


測試:


Razor的工作原理:

1.搜尋View,順序:

Views/<ControllerName>/<ViewName>.cshtml
Views/Shared/<ViewName>.cshtml

對於AreaView,順序為:

Area/<AreaName>/Views/<ControllerName>/<ViewName>.cshtml
Area/<AreaName>/Views/Shared/<ViewName>.cshtml

2.解析View程式碼,生成臨時c#code,存在臨時目錄,生成程式碼取樣:

public class _Page_Views_Home_Index_cshtml :System.Web.Mvc.WebViewPage<string[]> {
public _Page_Views_Home_Index_cshtml() {
}
public override void Execute() {
ViewBag.Title = "Index";
WriteLiteral("\r\n\r\nThis is a list of fruit names:\r\n\r\n");
foreach (string name in Model) {
WriteLiteral(" <span><b>");
Write(name);
WriteLiteral("</b></span>\r\n");
}
}
}


可見,Razor把我們的frontend程式碼完全解析為了c#。

3.當瀏覽器開始Render View時,會動態編譯這個臨時檔案,把內容寫成html傳送到瀏覽器。


Customize Razor

我們可以改變Razor哪些行為?搜尋範圍。例子如下:

Customize Razor實現

public class CustomizeRazor : RazorViewEngine
    {
        public CustomizeRazor()
        {
            ViewLocationFormats =new[] { "~/Views/{1}/{0}.cshtml","~/Views/Common/{0}.cshtml" };
           
        }
}


程式碼說明: 希望razor的查詢範圍和順序為:

Views/<ControllerName>/<ViewName>.cshtml
Views/Common/<ViewName>.cshtml


在global檔案中註冊

ViewEngines.Engines.Add(new CustomizeRazor());


1.新增Common資料夾

2.新增View ,名稱為NoExist.cshtml在Common資料夾

View 程式碼:

<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title></title>
</head>
<body>
    <div>
    This View is located under common folder , which is supposed be found by Custmized Razor
    </div>
</body>
</html>


3.在Test Controller 新增Action:

   public ActionResult NoExist()
        {
            return View("NoExist");
        }


4.測試:


按照Razor預設的行為,會去:

Views/<ControllerName>/<ViewName>.cshtml
Views/<ControllerName>/Shared/<ViewName>.cshtml

由於我們都沒有提供,只是把NoExistView放在了Common,我們在global檔案註冊了我們的CustomizeRazor,Razor的查詢行為成功的被customize了。

Razor dynamic Content

目前支援:

Inline Code

Html Helper Method

Sections

Partial view

Child Actions

使用Section

1.在View中定義section

@{
    ViewBag.Title ="View1";
    Layout ="~/Views/Shared/_LayoutPage1.cshtml";
}
 
@model string[]
@{
ViewBag.Title = "Index";
}
 
@section Header {
<div class="view">
@foreach (string str in new [] {"Home", "List","Edit"}) {
@Html.ActionLink(str, str, null, new { style = "margin: 5px" })
}
</div>
}
 
<!--
    Suppose to be caught as body ,Body Start
    -->
<div class="view">
This is a list of fruit names(let Razor Catch Body part):
@foreach (string name in Model) {
<span><b>@name</b></span>
}
</div>
 
<!--   
    Body end
-->
 
 
@section Footer {
<div class="view">
This is the footer
</div>
}


程式碼說明:定義了兩個action:header 和footer, 並期望中間部分被razor成功的解析為 body部分。

2.新增一個layout(就是上面View指向的_LayoutPage1):

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width"/>
<style type="text/css">
div.layout { background-color: lightgray;}
div.view { border: thin solid black; margin: 10px 0;}
</style>
<title>@ViewBag.Title</title>
</head>
<body>
@RenderSection("Header")
<div class="layout">
This is part of the layout
</div>
    @RenderBody()
   
    <div class="layout">
This is part of the layout
</div>
@RenderSection("Footer")
 
<div class="layout">
This is part of the layout
</div>
</body>
</html>


4.測試


可以看到,razor已經成功的把header,footer解析了,並且把我們期望它解析的body部分也捕捉到了,但是通常我們為了穩妥,可以顯示的把body部分也定義為section裡面:

@section Body {
<div class="view">
This is a list of fruit names(Body As Section,recommended):
@foreach (string name in Model) {
<span><b>@name</b></span>
}
</div>
}


在Layout的Render程式碼相應改為:

@RenderSection("Body")


Optional Section

考慮場景:

1.如果View中定義了Section : Footer才顯示footer,否則顯示一個預設的footer:

@if (IsSectionDefined("Footer")) {
@RenderSection("Footer")
} else {
<h4>This is the default footer</h4>
}


2.如果View中定義了scripts則render,否則不要render:

@RenderSection("scripts",false)


Partial View


定義一個PartialView

<div>
This is the message from the partial view.
@Html.ActionLink("This is a link to the Index action", "Index")
</div>
 


Consume partial view

@{
ViewBag.Title = "List";
Layout = null;
}
<h3>This is the/Views/Common/List.cshtml View</h3>
@Html.Partial("MyPartial")


語法很簡單,就是使用html helper method中的renderpartial,關於htmlhelper method,下一章會講。

Strong type的partialview 也類似:

1.定義

@model IEnumerable<string> 
<div> 
This is the message from the partial view. 
<ul> 
@foreach (string str in Model) { 
<li>@str</li> 
} 
</ul> 
</div> 


2.在View中消費

@{
ViewBag.Title = "List";
Layout = null;
}
<h3>This is the /Views/Common/List.cshtml View</h3>
@Html.Partial("MyStronglyTypedPartial", new []{"Apple", "Orange", "Pear"})


View在消費時主要注意引數要給對。

Child Action

1.定義Action:

[ChildActionOnly]
public  ActionResult Time() {
return  PartialView(DateTime.Now);
}


2.定義配套的PartialView:

<p>The time is: @Model.ToShortTimeString()</p>


3.消費child action

@{
ViewBag.Title = "List";
Layout = null;
}
<h3>This is the /Views/Common/List.cshtml View</h3>
@Html.Partial("MyStronglyTypedPartial", new []{"Apple", "Orange", "Pear"})
@Html.Action("Time")


這樣,Action會被觸發,並render出partialView 。

與PartialView不同,這裡HtmlHelper觸發的是Action,而PartialView那裡是直接render一個PartialView。


Action通常和partialView一起使用的,主要針對一些場景,希望不僅僅封裝View,並希望把Action這部分也重用了,這時考慮使用child Action。但是child Action 是不能被請求消費的。




相關文章