分享最近學到的小技巧,在 ASP.NET MVC Action 透過 return View() 或 return PartialView() 控制是否要顯示 Layout 部分。

直接使用 VS2019 預設的 ASP.NET Core MVC 專案範本當範例,它的 Index.cshtml 長這樣:

@{
    ViewData["Title"] = "Home Page";
}


<div class="text-center">
    <h1 class="display-4">Welcome</h1>
    <p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
</div>

但實際呈現畫面還有頁首、頁尾(黃底標示處):

原因是在 Views 目錄下有個 _ViewStart.csthml

_ViewStart.cshtml 的內容只有三行,設定 Layout = "_Layout" 指向 Views/Shared/_Layout.cshtml (完整版面設計,包含頁首頁尾):

@{
    Layout = "_Layout";
}

_ViewStart.cshtml 會每個完整 View (註:不含 Layout 頁面及 Partial View) 顯示時被執行,方便放入共用邏輯。同時它具有階層性,除了放在 Views 資料夾的根目錄,子資料夾也可以有自己的 _ViewStart.cshtml,只套用隸屬該子資料夾的 View。參考 由於 _ViewStart.cshtml 不會套用在 PartialView 上,利用這個特性可以玩出一些花招。

假設有某個網頁有時需要獨立顯示,有時會被內嵌在其他網頁的區塊中。獨立顯示時需帶出頁首頁尾,被內嵌時不用。要停用 Layout 可以透過 Layout = null 控制,除此之外還有一種簡便做法,以下測試把 Action 的 return View() 改成 return PartialView():

public IActionResult Index()
{
    return PartialView();
    //return View();
}

如上圖,同樣是 Index.cshtml,return PartialView() 省略執行 _ViewStart.cshtml,故不會套用 Layout = "_Layaout" 只顯示內容。(註:此做法可用於 ASP.NET MVC 3/4/5 及 ASP.NET Core ) 要應用在前述的內嵌時不顯示 Layout 情境,簡單做法是新增 ?embedded=Y 參數,Action 寫成 return Request["embedded"] == "Y" ? PartialView() : View();

前篇文章學會用 VS2019 查看 ASP.NET Core 原始碼,不鑽一下原始碼手會癢,練習從原始碼找出 PartialView 會略過 _ViewStart.csthml 的依據:

PartialViewResultExecutor 尋找 View 時,ViewEngine.GetView() isMainPage 參數為 false:

public virtual ViewEngineResult FindView(ActionContext actionContext, PartialViewResult viewResult) 
{
    //...略
    var result = viewEngine.GetView(executingFilePath: null, viewPath: viewName, isMainPage: false);

RazorViewEngine.cs GetView() 將 isMainPage 傳給 LocatePageFromPath() 再傳給 CreateCacheResult(),在 CreateCacheResult 中 isMainPage 決定是否套用 GetViewStartPages():

public ViewEngineResult GetView(string executingFilePath, string viewPath, bool isMainPage)
{
    //...略
    var cacheResult = LocatePageFromPath(executingFilePath, viewPath, isMainPage);
    return CreateViewEngineResult(cacheResult, viewPath);
}

private ViewLocationCacheResult LocatePageFromPath(string executingFilePath, string pagePath, bool isMainPage)
{
    //...略
    cacheResult = CreateCacheResult(expirationTokens, applicationRelativePath, isMainPage);
    //...略
}

internal ViewLocationCacheResult CreateCacheResult(
    HashSet<IChangeToken> expirationTokens, string relativePath, bool isMainPage) 
{
    //...略
        var viewStartPages = isMainPage ?
            GetViewStartPages(viewDescriptor.RelativePath, expirationTokens) :
            Array.Empty<ViewLocationCacheItem>();
    //...略
}

完成程式碼追查練習,謎底也揭曉了,我充實而欣慰。唉,程式魔人的快樂,往往就是這麼樸實無華,且枯燥。

Tips of using View() or PartialView() to control whether the .cshtml include the layout page or not.


Comments

# by Eric Tsao

可以再配合著 Request.IsAjaxRequest() 玩出更多花樣唷~~

# by Jeffrey

to Eric Tsao,感謝,再學到一招!

# by Ming-hung Tsao

黑大別客氣,我是看您的部落格長大的XDDD

# by JackyLiang

讚捏,剛好我困惑的地方

# by Alex

謝謝版主 剛好用到

# by linus lin

請問版主 我的問題剛好想法 頁面想吃Layout 已經設定好 @{ ViewBag.Title = "Index"; Layout = "~/Views/Shared/_Layout.cshtml"; 卻還是吃不到Layout的畫面

# by Jeffrey

to linus lin, 聽描述很難推測問題點,能做成可重現問題的簡單範例專案放上 github 嗎?這樣大家比較能幫忙。

Post a comment