分享我 ASP.NET MVC 設計常用的小技巧一則。

假設有個網站版面要求如下,所有 View 上方統一放上黑底標題列,標題列左方為 View 標題,右上角則為使用者帳號及姓名,下方白色區域則為 View 的實際內容:

這類情境很適合用 Layout Page 處理。我們設計 ~/Views/Shared/_Layout.cshtml 如下:

<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>@ViewBag.Title</title>
    <style>
        html,body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            padding: 0; margin: 0;
        }
        header {
            background-color: #444; color: white; padding: 6px; position: relative;
        }
        header h3 {
            font-size: 20pt; margin: 3px 12px; text-shadow: 1px 1px 1px black;
        }
        header .user-info {
            font-size: 9pt; position: absolute; right: 6px; top: 6px;
            
        }
    </style>
</head>
<body>
    <header>
        <h3>@ViewBag.Title</h3>
        <div class="user-info">
            @ViewBag.UserName ( @ViewBag.UserId )
        </div>
    </header>
    <div>
        @RenderBody()
    </div>
</body>
</html>

/Views/Home/Index.cshtml 只需設定 ViewBag.Title 及用 <img> 嵌入圖案:(註:透過 _ViewStart.csthml 指定 Layout = "/Views/Shared/_Layout.cshtml")

@{
    ViewBag.Title = "Index";
}
    <p style="padding: 12px;">
        <img src="~/img/icon.png" />
    </p>

HomeController.cs 需提供 ViewBag.UserId 及 ViewBag.UserName 給 _Layout.cshtml。UserId 可透過 Request.LogonUserIdentity.Name 取得,UserName 則是傳入 UserId 向後端查詢對應姓名,邏輯不難,但每個 Controller 都要加入相同邏輯。寫個傳入 Request 解析姓名並注入 ViewBag 共用函式給每個 Controller 呼叫是個方法,然而,使用繼承簡化設計會是更好的主意。

宣告一個自訂的 Controller 基底類別 MyControllerBase.cs:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;

namespace MvcWeb.Models
{
    public class MyControllerBase : Controller
    {
        public string UserId => Request.LogonUserIdentity.Name.Split('\\').Last();

        public string UserName => BaseDataHelper.GetUserName(UserId);

        //... 其他共用邏輯 ...
    }
}

接著讓 HomeController 改繼承 MyControllerBase,即可直接由 UserId 及 UserName 屬性取得所需資訊:

using MvcWeb.Models;
using System.Web.Mvc;

namespace MvcWeb.Controllers
{
    public class HomeController : MyControllerBase
    {
        // GET: Home
        public ActionResult Index()
        {
            ViewBag.UserId = base.UserId;
            ViewBag.UserName = base.UserName;
            return View();
        }
    }
}

但是,在每個 Controller 裡寫 ViewBag.UserId = base.UserId、ViewBag.UserName = base.UserName 還是有點遜,我們可以善用 Controller 都繼承自 MyControllerBase 這點,直接在 _Layout.cshtml 存取 UserId/UserName,完全省掉在 Controller 設定 ViewBag 的工夫。做法是透過 ViewContext.Controller 存取 Controller 物件,若 Controller 繼承自 MyControllerBase,便將其轉型成 MyControllerBase 取回 UserId / UserName 屬性。程式範例如下:

@using MvcWeb.Models
@{ 
    MyControllerBase controller = ViewContext.Controller as MyControllerBase;
    if (controller == null)
    {
        throw new ApplicationException("Controller 必須繼承自 MyControllerBase.");
    }
    var userId = controller.UserId;
    var userName = controller.UserName;
}
<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>@ViewBag.Title</title>
    <style>
        /* 略 */
    </style>
</head>
<body>
    <header>
        <h3>@ViewBag.Title</h3>
        <div class="user-info">
            @userName ( @userId )
        </div>
    </header>
    <div>
        @RenderBody()
    </div>
</body>
</html>

如此,Controller 只需繼承 MyControllerBase,_Layout.cshtml 便能直接取回必要的資料,Controller 完全不需沾手 UserId/UserName 這些細節,不但能少寫些程式,還貫徹了觀注點分離(SoC)概念,是我偏好的簡潔優雅寫法之一,推薦給大家。

補充:這個範例的精神在於展示利用繼承共用程式碼,我的實際應用 UserId/UserName 不只用於 View,在各 Controller 另有記錄身分、決定流程、控制權限等用途,故用繼承實作還有其他效益。若只限於 View 應用,亦可考慮使用 HtmlHelper 等技巧;另外,範例只有單純文字顯示,若 UI 結構或邏輯更複雜時,切割成 Partial View 或 ChildAction 較方便管理。

Tips of how to use ASP.NET MVC layout page and inherited controller to build concise view page.


Comments

# by demo

類似的需求我通常都會抽成 ChildAction ,而且抽成 ChildAction 後甚至可以加上快取提高速度 。 在 RazorPage 的話黑大用的這種方法更是直覺(我現在越來越愛 RazorPage)

# by 凱大

我是覺得上方的 bar 與 下方的 page 不應該是緊密連動的 兩者應屬於各自獨立的 application 並且交由背景程式來進行配置 或是 IoC 提供介面來操作 繼承太過於緊密了

# by Jeffrey

to demo, 同意。UI 的複雜度更高時,改用 Partial View 或 ChildAction 較有效率,已補充到本文,感謝提醒。

# by CClemon

想請問黑大,假設如果使用partial view弄在一個view裡面但是兩個使用的model是不相同的,我可以使用何種方法來透過Ajax更新partial veiw的model目前一直想不到方法都是改用vue來處理這個部分。

# by Jeffrey

to CClemon, 「更新 Partial View 的 Model」是指在點選某個按鈕或元素後切換 Partial View 顯示另一個 Model 的內容嗎?

# by CClemon

to 黑大 是的目前想不到如何用model來做處理,都是把資料轉Json再用js產生需要的html,想說使用MVC就準備用razor來處理完

# by Jeffrey

to CClemon,由於 Razor 是伺服器端功能,若要從前端傳入 Model,等於點選網頁元素後要把已轉成 JavaScript 形式的 Model 再傳回後端以 Razor 語法產生 HTML,此時應用獨立 View 而非 Partial View。

# by Lawrence

黑大好,我在 asp.net core mvc 中嘗試使用ViewContext.Controller,不過似乎.net core的ViewContext沒有Controller可以使用,這部分有優雅的解決方案嗎

# by Jeffrey

to Lawrence,查了一下,ASP.NET Core 調整架構後,已無法透過 ViewContext 存取來源 Controller,我想到一個土方法是在後端 ViewBag.Controller = this,CSHTML 端再 MyController controller = [MyController]ViewBag.Controller,不知可行嗎?

# by Lawrence

to 黑大 這個方法後來我使用@User在_Layout樣板中取得帳號的資訊來處理了 試了一陣子發現沒辦法所以改方法,後來才發現黑大有回,找時間來試試黑大提供的方法,感謝黑大

Post a comment