MVC 功能可攜元件再進化 - 封裝 cshtml
0 |
上週的 Coding4Fun 專案,我寫了一個 ASP.NET MVC (對,還不是 .NET Core,請體諒老人動作慢,勿心急敲碗) 小功能可以顯示 CPU 及記憶體使用狀況,後面又做了一些改良,將 Model、View、Controller 都搬進獨立類別程式庫,還把 js/css/ttf 等靜態檔一併包進去。這樣子任何 MVC 網站只要參照一個 .dll,在網頁掛一個 IFrame,就能擁有新功能,非常方便。
但有個美中不足之處 - 為了順利內嵌成組件資源,我將 View 的 .cshtml 轉成靜態 .html。若原本 View 內含較複雜的 Razor 語法、HtmlHelper 功能,硬要改寫成 HTML 有點不切實際,所以這裡有個好問題 - 我能不能將 cshtml 也存成 Embedded Resource 讓 ASP.NET MVC 載入?
答案是可行,關鍵是 VirtualPathProvider。
ASP.NET MVC 允許我們自訂 VirtualPathProvider,解析 ~/folder/filename,用 VirtualFile 類別傳回 Stream。換言之,我們可以實作一個類別,從 Embedded Resource、資料庫、WebAPI 或任何你想得到的來源取出對映檔案內容。(將 cshtml 存在資料庫這點很酷,我腦中有浮現一些有趣應用)
我先調整專案檔案配置,新開 Views 資料夾,把 View.html 轉成 Index.cshtml 跟 _Script.cshtml 拆成主 View 跟 Partial View (嚴格來說無此必要,純粹是想測試較複雜的情境),_Script.cshtml 還刻意放到 Partials 子資料夾,以模擬多層資料夾結構。而最大重點莫過於加入 EmbResVirtualPathProvider.cs 及 EmbResVirtualFile.cs 兩個類別:
EmbResVirtualPathProvider.cs 如下,重點是要繼承 VirtualPathProvider 類別,並實作 FileExists() 以測試路徑是否存在,GetFile() 則要傳回一個 VirtualFile 類別。為了加快處理效率,我事先查出組件所有內嵌資源名稱,建立一張虛擬路徑與含命名空間資源名稱的對應表(如:~/EmbViews/Partial/_Script.cshtml 對映成 WebStatsMonitor.Views.Partials._Script.cshtml)。虛擬路徑則要用 VirtualPathUtility.ToAppRelative() 方法轉成 ~ 起首路徑以求一致。
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using System.Web;
using System.Web.Caching;
using System.Web.Hosting;
namespace WebStatsMonitor
{
public class EmbResVirtualPathProvider : VirtualPathProvider
{
static Dictionary<string, string> virPathToEmbRes =
typeof(EmbResVirtualPathProvider).Assembly.GetManifestResourceNames()
.ToDictionary(
o => o.Replace("WebStatsMonitor.Views", "~.EmbViews").ToLower(),
o => o
);
public static bool IsEmbResPath(string virtualPath) =>
VirtualPathUtility.ToAppRelative(virtualPath).StartsWith("~/EmbViews/");
public static string FindEmbResFullName(string virtualPath)
{
if (!IsEmbResPath(virtualPath)) return null;
virtualPath = VirtualPathUtility.ToAppRelative(virtualPath);
var mappedName = virtualPath.Replace("/", ".").ToLower();
if (virPathToEmbRes.ContainsKey(mappedName))
return virPathToEmbRes[mappedName];
return null;
}
public override bool FileExists(string virtualPath)
{
return
(IsEmbResPath(virtualPath) && FindEmbResFullName(virtualPath) != null) ||
base.FileExists(virtualPath);
}
public override VirtualFile GetFile(string virtualPath)
{
if (!IsEmbResPath(virtualPath))
return base.GetFile(virtualPath);
var embResFullName = FindEmbResFullName(virtualPath);
if (embResFullName == null)
throw new HttpException((int)HttpStatusCode.NotFound,
"Rebedded Resource not found");
return new EmbResVirtualFile(virtualPath, FindEmbResFullName(virtualPath));
}
public override CacheDependency GetCacheDependency(string virtualPath, IEnumerable virtualPathDependencies, DateTime utcStart)
{
if (IsEmbResPath(virtualPath)) return null;
return base.GetCacheDependency(virtualPath, virtualPathDependencies, utcStart);
}
}
}
EmbResVirtualFile.cs 的內容很簡單,接收資源全名,呼叫 Assembly.GetManifestResourceStream() 傳回 Stream:
using System.IO;
using System.Web.Hosting;
namespace WebStatsMonitor
{
public class EmbResVirtualFile : VirtualFile
{
private readonly string embResFullName;
public EmbResVirtualFile(string virtualPath, string embResFullName) : base(virtualPath)
{
this.embResFullName = embResFullName;
}
public override Stream Open()
{
return this.GetType().Assembly.GetManifestResourceStream(embResFullName);
}
}
}
Index.cshtml 有個地方要說一下,跟一般 MVC 專案不同,最上方要加入 @inherits System.Web.Mvc.WebViewPage 以及必要的 @using,其餘 Razor 語法跟 HtmlHelper 依與一般寫法相同:
@inherits System.Web.Mvc.WebViewPage
@using System.Web.Mvc
@using System.Web.Mvc.Html
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>Web CPU & Memory</title>
<link href="~/WebStatsMonitor/Resource?resName=webstats.css" rel="stylesheet" />
</head>
<body>
<div class="frame">
<div class="row">
<span class="hdr">CPU</span> <span class="field" id="cpuLoad"></span>
</div>
<div class="row">
<span class="hdr">RAM</span> <span class="field" id="memUsage"></span>
</div>
</div>
<script src="~/WebStatsMonitor/Resource?resName=jquery.min.js"></script>
@*沒什麼意義的 PartialView 拆分,只是為了證明存成內嵌資源也辦得到*@
@Html.Partial("~/EmbViews/Partials/_Script.cshtml")
</body>
</html>
要加 inherits 跟 using 的理由是標準 MVC 有宣告在 Views/web.config 裡才不用加,放 dll 時得自己來。(若不想每個 .cshtml 手動加,在 VirutalFile.Open() 傳回內容自動補上也是解法)
WebStatsMonitorController 的 Index() 則要小改,原本 RedirectToAction 改為 return View("~/EmbViews/Index.cshtml"):
最後,引用元件的 MVC 網站需要註冊我們自訂的 EmbResVirtualPathProvider,最簡單的做法是加在 Global.asax.cs Application_Start():
public class MvcApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
RouteConfig.RegisterRoutes(RouteTable.Routes);
HostingEnvironment.RegisterVirtualPathProvider(
new WebStatsMonitor.EmbResVirtualPathProvider());
}
}
這樣子就能把 cshtml 內嵌成 Embedded Resource 跑囉~
有個我沒能克服的困擾是 Visual Studio 2019 編譯環境會出現大量內嵌 cshtml 編譯錯誤,有點惱人,但並不影響編譯跟執行就是了。
老樣子,完整程式範例我送上 Github 了,放在 emb-cshtml 分支,有興趣的同學請自取。
Example of how to embed .cshtml as embedded resource via customized VirtualPathProvider and VirtualFile.
Comments
Be the first to post a comment