講解分為三個部分
1、MVC的基本開發流程
2、webform和MVC的選擇
3、MVC的內部過程
1、MVC的開發流程
MVC的出現時微軟在2009年左右開始提出的網站開發的新的發展方向,這個方面的開發官方解釋是可以比較好的實現三層分離,而且分離之后,可以實現復用等相關好處,通常人們列舉的例子是ui方面可以同時支持HTML網絡或者WAP網絡。但是其實個人的理解是,動態網站的開發經過不斷地證實和發展,java的struts模型,可以提高開發速度,也可以降低差多,是比較好的框架,而微軟也需要提供自己的開發框架。不能夠只是一個界面一個界面的設計方式,設計模式逐步進入到了web開發的領域。
MVC使用vs2010進行開發時(這里介紹的是MVC2),首先需要選在一個模板,然后vs2010會幫忙創建好對應的目錄結構。
每個目錄的基本功能:
Content:主要用於描述css之類的資源
Controllers:主要就是controller的存放位置,創建controller時,都是需要在該目錄創建的。
Models:主要就是entity的具體位置,以及跟entity相關的操作類
Scripts:javascript腳本存放的位置
Views:該部分主要是放置view顯示部分的
Global.asax:目前來看,該部分主要就是路由設置
Web.config:該配置文件而已
從開發的流程方面來看,MVC的開發方式,或者說思考的方式出現了變化,在MVC當中,需要理解一個重要的點是:
Controller才是系統的中心,一切圍繞Controller展開。
Model:所謂模型,可以理解為數據,這個數據可以是數據庫中對應的表中的數據,這種數據是只有屬性,而沒有動作的,這種數據通常也被稱之為Entity,即實體,除了這種數據之外,MODLE起始還要包括Interface,這些接口的目標是提供可以控制Entity的接口標准,然后在提供實現的載體,通常我們稱之為Mock類,為了方便,可能我們還會在Model當中創建各種factory,從而簡化對象的創建。
Controller:這個部分就是核心了,其實所謂核心,是說所有的處理,全部圍繞着Controller展開,它的主要工作是訪問model,獲取數據后,將參數轉發給view,然后讓view表現出來。在這里主要完成的工作有兩點:
1、在客戶端訪問一個頁面后,需要跳轉到Controller對應的action中去,然后在action中處理對應的view顯示出來。
2、 完成客戶的表單提交相應處理,也就是Form表單處理。(還記得之前講過,對於HTML而言,只有Form表單實現了客戶端的信息發送給服務端,然后由服務端處理相關的相應,因為MVC的設計目標就是放棄了微軟原有的服務器控件,因此一切回歸原始,采用HTML的form表單方式實現提交和相關的控制處理。)
View:顧名思義,該部分就是現實的部分,這個部分需要時刻記住的是,這個view雖然也是aspx的頁面,但是已經發生了根本性的變化,不再有所謂的codebeind代碼了,這個view的所有變成將采用混合式的變成,你會注意到這個部分的變成變為HTML與C#的混合,會出現很多的<%%><%=%>類似的代碼。很多人起始對這個部分有不同的開發,混合代碼對於分層不利,但是在MVC中,因為不涉及邏輯,所以view的表現變得簡單,混合編程會變為可以接受的處理方式。另外,這種方式帶來的好處是,美工可以介入了,他們的修改對於程序員來說,沒有什么特別,也是非常容易直接引入的。帶來的壞處是Gridview這種強大的服務器控件被丟棄了。雖然是這樣,但是我個人覺得,這是回到了web開發的本質。他的思想,與JSP,PHP等等變為一致。
Golabal.asax:路由表,這個部分就是所謂的全局路由表。在MVC框架中,之所以實現了MVC功能,一個重要的概念是路由表,該部分實現了地址訪問的動態,不再提供具體頁面的訪問模式。
注意:給我的感覺是,記住在view目錄和model目錄中,添加子目錄,每個controller對應的view,都是一個目錄下的view。這個是MVC框架查找時自動搜索的。
2、webform和MVC的選擇
這個部分的爭論,我想從微軟開始推出MVC框架后,大家就在不間斷的討論着,不同的人,給出的看法也是不同,就我個人而言,我覺得MVC才是未來趨勢,是世界最后大同的根本。盡管web form的模式,是微軟開創性的創造,但是畢竟web開發不是微軟首創,很多時候,大勢所趨而已。我這里只是想談談兩者的思想出發點的差別:
webform模式,這個模式的思維基礎,是微軟在桌面開發中取得了前所未有的成功,這些成功,微軟希望復制到網絡開發中,何為form,就是窗口開發,這種框架的邏輯是所見即所得+事件處理,微軟希望可以將web實現為桌面開發的模式,但是網絡開發的基礎是HTML和HTTP協議,這兩個部分帶來的問題是HTML表現元素有限,並且只能夠通過form與后台服務器通信。另外,HTTP協議無狀態,無法實現消息機制,為了解決這些問題,微軟創造了新的開發模式,引入ASP.NET服務器控件,實現了豐富的控件,通過postback回傳機制,實現了事件模型,通過codebehind技術實現web頁面與C#代碼的分離。上述技術,的確非常成功,也確實很大程度上簡化了web的開發,但是隨着發展,帶來了問題,就是webform的開發基礎是頁面化的,這種思維模式是說你開一個頁面,然后在這個頁面寫響應事件,但是這種模式對於頻繁變化的web程序缺乏良好的復用性,並且,前端人員開發的界面,往往在合成時,需要重做,這是微軟自己創造的困難,這是一種以Page為中心的開發思想。
MVC模式,這個模式的思維基礎,是分工清晰,以Controller為核心,在開發時可以先做model再做Controller,最后做view,通過使用demo view實現,最后再替換美工的view。這種模式變成了以數據為中心的開發思想。最大的好處是,這種模式中每個部分都可以靈活復用,最大限度的實現現在的各種網絡需要,比如互聯網和移動互聯網。而且,其他的變成語言,在思想方面也基本采用這種模式,這種模式最終被時間證明,成為了標准思考方式和開發方式。微軟提倡的桌面化開發,漸漸退卻往日之光芒。
3、MVC的內部過程
這個部分是個非常核心的問題,本文除了自己理解,還大量引用了其他相關的文章。嘗試講解清楚MVC的基本運轉流程。
MVC的主體過程:
問題:
1、 瀏覽器請求的地址,並不是具體的某個頁面,如1234.aspx頁面,而是controller/action方法,這是如何做到的?
2、 Controller被訪問到以后,如何找到具體的view進行返回的?
我個人的理解就是回答了上述的問題,也就解釋清楚了MVC的基本框架。
第一個問題,瀏覽器請求地址的問題,MVC之所以能夠找到具體的Controller是因為有一個route組件,實現了路由處理的功能,將請求轉化為Controller的具體方法。需要注意該組件居然是個獨立組件。
Routing的作用:
1、 解析URL,識別當中的參數
2、 解析之后,調用具體的controller和action
比如首頁地址是: localhost/home/index
我們發現訪問上面的地址, 最后會傳遞給 HomeController中名為index的action(即HomeController類中的index方法).
當然服務器端不會自己去實現這個功能, 關鍵點就是在Global.asax.cs文件中的下列代碼:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = "" } // Parameter defaults
);
}
protected void Application_Start()
{
RegisterRoutes(RouteTable.Routes);
}
回來看我們的Url: localhost/home/index
localhost是域名, 所以首先要去掉域名部分: home/index
對應了上面代碼中的這種URL結構:{controller}/{action}/{id}
因為我們建立了這種Url結構的識別規則, 所以能夠識別出 Controller是home,action是index,id沒有則為默認值"".
上述功能之所以能夠實現,關鍵在MapRoute方法,雖然MapRoute方法是RouteCollection對象的方法,但是卻被放置在System.Web.Mvc程序集中, 如果你的程序只引用了System.Web.Routing, 那么RouteCollection對象是不會有MapRoute方法的. 但是如果你同又引用了System.Web.Mvc, 則在mvc的dll中為RouteCollection對象添加了擴展方法:
public static void IgnoreRoute(this RouteCollection routes, string url);
public static void IgnoreRoute(this RouteCollection routes, string url, object constraints);
public static Route MapRoute(this RouteCollection routes, string name, string url);
public static Route MapRoute(this RouteCollection routes, string name, string url, object defaults);
public static Route MapRoute(this RouteCollection routes, string name, string url, string[] namespaces);
public static Route MapRoute(this RouteCollection routes, string name, string url, object defaults, object constraints);
public static Route MapRoute(this RouteCollection routes, string name, string url, object defaults, string[] namespaces);
public static Route MapRoute(this RouteCollection routes, string name, string url, object defaults, object constraints, string[] namespaces);
RouteCollection是一個集合,他的每一項應該是一個Route對象. 但是我們使用MapRoute時並沒有創建這個對象, 這是因為當我們將MapRoute方法需要的參數傳入時, 在方法內部會根據參數創建一個Route對象:
public static Route MapRoute(this RouteCollection routes, string name, string url, object defaults, object constraints, string[] namespaces) {
if (routes == null) {
throw new ArgumentNullException("routes");
}
if (url == null) {
throw new ArgumentNullException("url");
}
Route route = new Route(url, new MvcRouteHandler()) {
Defaults = new RouteValueDictionary(defaults),
Constraints = new RouteValueDictionary(constraints)
};
if ((namespaces != null) && (namespaces.Length > 0)) {
route.DataTokens = new RouteValueDictionary();
route.DataTokens["Namespaces"] = namespaces;
}
routes.Add(name, route);
return route;
}
上面就是MapRoute方法的實現, 至於在創建Route對象時第二個參數是一個MvcRouteHandler, 它是一個實現了IRouteHandler接口的類. IRouteHandler十分簡單只有一個方法:
IHttpHandler GetHttpHandler(RequestContext requestContext);
參數是一個RequestContext 類實例, 這個類的結構也很簡單:
public class RequestContext
{
public RequestContext(HttpContextBase httpContext, RouteData routeData);
public HttpContextBase HttpContext { get; }
public RouteData RouteData { get; }
}
其中的一個屬性RouteData就包含了Routing根據Url識別出來各種參數的值, 其中就有Controller和Action的值.
歸根結底, ASP.NET MVC最后還是使用HttpHandler處理請求. ASP.NET MVC定義了自己的實現了IHttpHandler接口的Handler:MvcHandler, 因為MvcRouteHandler的GetHttpHandler方法最后返回的就是MvcHandler.
MvcHandler的構造函數需要傳入RequestContext 對象, 也就是傳入了所有的所有需要的數據, 所以最后可以找到對應的Controller和Action, 已經各種參數.
(引用參考:http://www.cnblogs.com/zhangziqiu/archive/2009/02/28/ASPNET-MVC-2.html)
(引用參考:http://www.cnblogs.com/zhangziqiu/archive/2009/03/11/Aspnet-MVC-3.html)
第二個問題:Controller找到了,Action也找到了,此時如何哪?
下面分層次的總結Controller處理流程:
1. 頁面處理流程
發送請求 –> UrlRoutingModule捕獲請求 –>MvcRouteHandler.GetHttpHandler() –> MvcHandler.ProcessRequest()
2.MvcHandler.ProcessRequest() 處理流程:
使用工廠方法獲取具體的Controller –> Controller.Execute() –> 釋放Controller對象
3.Controller.Execute() 處理流程
獲取Action –> 調用Action方法獲取返回的ActionResult –> 調用ActionResult.ExecuteResult() 方法
4.ActionResult.ExecuteResult() 處理流程
獲取IView對象-> 根據IView對象中的頁面路徑獲取Page類-> 調用IView.RenderView() 方法(內部調用Page.RenderView方法)
通過對MVC源代碼的分析,我們了解到Controller對象的職責是傳遞數據,獲取View對象(實現了IView接口的類),通知View對象顯示.View對象的作用是顯示.雖然顯示的方法RenderView()是由Controller調用的,但是Controller僅僅是一個"指揮官"的作用, 具體的顯示邏輯仍然在View對象中.需要注意IView接口與具體的ViewPage之間的聯系.在Controller和View之間還存在着IView對象.對於ASP.NET程序提供了WebFormView對象實現了IView接口.WebFormView負責根據虛擬目錄獲取具體的Page類,然后調用Page.RenderView().
引用參考:(Http://www.cnblogs.com/zhangziqiu/archive/2009/03/11/Aspnet-MVC-3.html)
講到這里,相信很多人開始似乎明白了,又似乎不明白了,下面我做進一步的講解,先看一下,通常Controller的實現如下:
public class HomeController:Controller
{
public ActionResult Index()
{
Return View(“Index”);
}
}
先看看關鍵類ActionResult,這個返回值,體現了微軟精心設計,為什么做這么個類,其實本質而言,微軟希望這個action可以返回更多內容,而不僅僅是view。
類名
抽象類
父類
功能
ContentResult
根據內容的類型和編碼,數據內容.
EmptyResult
空方法.
FileResult
abstract
寫入文件內容,具體的寫入方式在派生類中.
FileContentResult
FileResult
通過文件byte[] 寫入文件.
FilePathResult
FileResult
通過文件路徑寫入文件.
FileStreamResult
FileResult
通過文件Stream 寫入文件.
HttpUnauthorizedResult
拋出401錯誤
JavaScriptResult
返回javascript文件
JsonResult
返回Json格式的數據
RedirectResult
使用Response.Redirect重定向頁面
RedirectToRouteResult
根據Route規則重定向頁面
ViewResultBase
abstract
調用IView.Render()
PartialViewResult
ViewResultBase
調用父類ViewResultBase 的ExecuteResult方法.
重寫了父類的FindView方法.
尋找用戶控件.ascx文件
ViewResult
ViewResultBase
調用父類ViewResultBase 的ExecuteResult方法.
重寫了父類的FindView方法.
尋找頁面.aspx文件
這里我們主要講解viewResult的作用。
在ASP.NETMVC中,ViewResult用的最多,Controller有一個View方法,它來實例化一個ViewResult對象,並返回。
下面是View方法:
protected internal virtual ViewResult View(string viewName, string masterName, object model) {
if (model != null) {
ViewData.Model = model;
}
return new ViewResult {
ViewName = viewName,
MasterName = masterName,
ViewData = ViewData,
TempData = TempData
};
}
ViewResult類的ExecuteResult方法的具體參考如下:
public override void ExecuteResult(ControllerContext context) {
if (context == null) {
throw new ArgumentNullException("context");
}
if (String.IsNullOrEmpty(ViewName)) {
ViewName = context.RouteData.GetRequiredString("action");
}
ViewEngineResult result = null;
if (View == null) {
result = FindView(context); // 很關鍵,找到具體的view
View = result.View;
}
ViewContext viewContext = new ViewContext(context, View, ViewData, TempData);
// 很關鍵,渲染自己
View.Render(viewContext, context.HttpContext.Response.Output);
if (result != null) {
result.ViewEngine.ReleaseView(context, View);
}
}
那么如何FindView哪?具體如下:
rotectedoverrideViewEngineResult FindView(ControllerContext context) {
ViewEngineResult result =ViewEngineCollection.FindView(context, ViewName, MasterName);
if (result.View != null) {
return result;
}
//we need to generate an exception containing all the locations we searched
StringBuilder locationsText = new StringBuilder();
foreach (string location in result.SearchedLocations) {
locationsText.AppendLine();
locationsText.Append(location);
}
throw new InvalidOperationException(String.Format(CultureInfo.CurrentUICulture,
MvcResources.Common_ViewNotFound,ViewName, locationsText));
}
從ViewResult類的FindView方法中,得知ViewEngineResult是通過ViewEngineCollection的FindView得到的,而ViewEngineCollection正是ViewEngines的靜態屬性Engines,Engines返回一個只有一個WebFormViewEngine類型實例的一個集合。所以,ViewEngineResult會是調用WebFormViewEngine類的FindView方法返回的結果。如果ViewEngins的靜態屬性Engines有多個ViewEngine提供,那么就依次遍歷它們直到找到第一個不為空的ViewEngineResult為止。這樣我們就可以在同一個MVC網站中使用多種視圖引擎了。
靜態類ViewEngines的描述如下:
public static class ViewEngines
{
private static readonly ViewEngineCollection _engines = new ViewEngineCollection { new WebFormViewEngine(), new RazorViewEngine() };
public static ViewEngineCollection Engines
{
get { return _engines;}
}
}
public class ViewEngineCollection : Collection<IViewEngine>
{
//其他成員
public virtual ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName);
public virtual ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName);
}
從上述例子可以看出,起始微軟為我們提供了兩個ViewEngine, WebFormViewEngine和RazorViewEngine,WebFormViewEngine對應的是ASPX界面,RazorViewEngine對應的是.cshtml/.vbhtml引擎
此外,這里有一個隱藏很深的概念,似乎很多書都沒講清楚,每一個引擎都會對應一個view,作為頁面渲染使用,對於viewengine做進一步的解釋,就是說,為什么一個view當中的<%%>之類的界面元素最后可以變成html,就是這些引擎在起作用,他們的內部實現,可以提供類似正則表達式或者是頁面parsing的方法,完成頁面字符串解析,從而轉換為對應的html,再response給客戶端瀏覽器。
微軟提供的兩種viewengine和view如下:
RazorViewEngine和Razorview
(參考引用:http://www.cnblogs.com/artech/archive/2012/09/05/razor-view-engine-02.html)
WebformViewengine和Webformview
通過上邊的講述,基本的概念已經講清楚了,如果希望實現自己的viewengine,可以查看一下微軟參考實現是如何做到的,然后我們就可以防治自己的viewengine了。這里便需要進一步說明的是,為什么MVC需要viewengine,而WEBFORM不需要,是因為,微軟的webform是直接通過IHttphandler處理了,也就是說ASP.NETWEBFORM模式中的HTTPHANDLER完成了事件處理和頁面顯示的雙重功能。而ASP.NETMVC沒有事件處理,因此頁面顯示被划到了viewengine當中實現了。事件處理,被controller替換處理了。
微軟的Razorview和Webformview本質上是實現於IView接口,而WebformViewengine和Webformview本質上是實現於IViewEngine
下面介紹他們的實現邏輯:
public interface IView
2: {
3: void Render(ViewContext viewContext, TextWriter writer);
4: }
5:
6: public class ViewContext : ControllerContext
7: {
8: //其他成員
9: public virtual bool ClientValidationEnabled { get; set; }
10: public virtual bool UnobtrusiveJavaScriptEnabled { get; set; }
11:
12: public virtual TempDataDictionary TempData { get; set; }
13: [Dynamic]
14: public object ViewBag { [return: Dynamic] get; }
15: public virtual ViewDataDictionary ViewData { get; set; }
16: public virtual IView View { get; set; }
17: public virtual TextWriter Writer { get; set; }
18: }
19:
20: public abstract class HttpResponseBase
21: {
22: //其他成員
23: public virtual TextWriter Output { get; set; }
24: }
1: public interface IViewEngine
2: {
3: ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache);
4: ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache);
5: void ReleaseView(ControllerContext controllerContext, IView view);
6: }
1: public class ViewEngineResult
2: {
3: public ViewEngineResult(IEnumerable<string> searchedLocations);
4: public ViewEngineResult(IView view, IViewEngine viewEngine);
5:
6: public IEnumerable<string> SearchedLocations { get; }
7: public IView View { get; }
8: public IViewEngine ViewEngine { get; }
9: }
1: public class ViewResult : ViewResultBase
2: {
3: protected override ViewEngineResult FindView(ControllerContext context);
4: public string MasterName { get; set; }
5: }
6:
7: public abstract class ViewResultBase : ActionResult
8: {
9: public override void ExecuteResult(ControllerContext context);
10: protected abstract ViewEngineResult FindView(ControllerContext context);
11:
12: public object Model { get; }
13: public TempDataDictionary TempData { get; set; }
14: [Dynamic]
15: public object ViewBag { [return: Dynamic] get; }
16: public ViewDataDictionary ViewData { get; set; }
17: public string ViewName { get; set; }
18: public ViewEngineCollection ViewEngineCollection { get; set; }
19: public IView View { get; set; }
20: }
(參考引用:http://www.cnblogs.com/artech/archive/2012/08/22/view-engine-01.html)
自定義的View以及相關的Viewengine,參考如下:
public class StaticFileView:IView
2: {
3: public string FileName { get; private set; }
4: public StaticFileView(string fileName)
5: {
6: this.FileName = fileName;
7: }
8: public void Render(ViewContext viewContext, TextWriter writer)
9: {
10: byte[] buffer;
11: using (FileStream fs = new FileStream(this.FileName, FileMode.Open))
12: {
13: buffer = new byte[fs.Length];
14: fs.Read(buffer, 0, buffer.Length);
15: }
16: writer.Write(Encoding.UTF8.GetString(buffer));
17: }
18: }
internal class ViewEngineResultCacheKey
2: {
3: public string ControllerName { get; private set; }
4: public string ViewName { get; private set; }
5:
6: public ViewEngineResultCacheKey(string controllerName, string viewName)
7: {
8: this.ControllerName = controllerName ?? string.Empty;
9: this.ViewName = viewName ?? string.Empty;
10: }
11: public override int GetHashCode()
12: {
13: return this.ControllerName.ToLower().GetHashCode() ^ this.ViewName.ToLower().GetHashCode();
14: }
15:
16: public override bool Equals(object obj)
17: {
18: ViewEngineResultCacheKey key = obj as ViewEngineResultCacheKey;
19: if (null == key)
20: {
21: return false;
22: }
23: return key.GetHashCode() == this.GetHashCode();
24: }
25: }
1: public class StaticFileViewEngine : IViewEngine
2: {
3: private Dictionary<ViewEngineResultCacheKey, ViewEngineResult> viewEngineResults = new Dictionary<ViewEngineResultCacheKey, ViewEngineResult>();
4: private object syncHelper = new object();
5: public ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache)
6: {
7: return this.FindView(controllerContext, partialViewName, null, useCache);
8: }
9:
10: public ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
11: {
12: string controllerName = controllerContext.RouteData.GetRequiredString("controller");
13: ViewEngineResultCacheKey key = new ViewEngineResultCacheKey(controllerName, viewName);
14: ViewEngineResult result;
15: if (!useCache)
16: {
17: result = InternalFindView(controllerContext, viewName, controllerName);
18: viewEngineResults[key] = result;
19: return result;
20: }
21: if(viewEngineResults.TryGetValue(key, out result))
22: {
23: return result;
24: }
25: lock (syncHelper)
26: {
27: if (viewEngineResults.TryGetValue(key, out result))
28: {
29: return result;
30: }
31:
32: result = InternalFindView(controllerContext, viewName, controllerName);
33: viewEngineResults[key] = result;
34: return result;
35: }
36: }
37:
38: private ViewEngineResult InternalFindView(ControllerContext controllerContext, string viewName, string controllerName)
39: {
40: string[] searchLocations = new string[]
41: {
42: string.Format( "~/views/{0}/{1}.shtml", controllerName, viewName),
43: string.Format( "~/views/Shared/{0}.shtml", viewName)
44: };
45:
46: string fileName = controllerContext.HttpContext.Request.MapPath(searchLocations[0]);
47: if (File.Exists(fileName))
48: {
49: return new ViewEngineResult(new StaticFileView(fileName), this);
50: }
51: fileName = string.Format(@"\views\Shared\{0}.shtml", viewName);
52: if (File.Exists(fileName))
53: {
54: return new ViewEngineResult(new StaticFileView(fileName), this);
55: }
56: return new ViewEngineResult(searchLocations);
57: }
58:
59: public void ReleaseView(ControllerContext controllerContext, IView view)
60: { }
61: }
1: public class MvcApplication : System.Web.HttpApplication
2: {
3: protected void Application_Start()
4: {
5: //其他操作
6: ViewEngines.Engines.Insert(0,
new StaticFileViewEngine());
7: }
8: }
1: public class HomeController : Controller
2: {
3: public ActionResult ShowNonExistentView()
4: {
5: return View("NonExistentView");
6: }
7:
8: public ActionResult ShowStaticFileView()
9: {
10: return View();
11: }
12: }
我們為Action方法ShowStaticFileView創建一個StaticFileView類型的View文件ShowStaticFileView.shtml(該View文件保存在“~/Views/Home”目錄下,擴展名不是.cshtml,而是shtml),其內容就是如下一段完整的HTML。
1: <!DOCTYPE html>
2: <html>
3: <head>
4: <title>Static File View</title>
5: </head>
6: <body>
7: 這是一個自定義的StaticFileView!
8: </body>
9: </html>
轉自CSDN,原文鏈接:https://blog.csdn.net/dongdongdongjl/article/details/38070333