[MVC]專案客製化方式
前言
相信各軟體公司都有開發產品,將產品賣給客戶後,難免需要客製一些程式,如果將來產品版本升級後,客製化過的專案需要如何升級到產品的版本呢?
之前針對ASP.NET WEB FORM規劃了「Asp.NET專案客製化方式」,那如果是使用ASP.NET MVC要如何做呢?
研究及實作
環境:VS2012, ASP.NET MVC 4.0
想法是使用Area來放客製化的程式,一開始一樣是走Root的Controller,如果客製化Area中有相同的Controller(Namespace不同),就導到客製化Area中的Controller。
接下來建立一個ASP.NET MVC 4 Web 應用程式,專案範本選取「網際網路應用程式」,這樣一開始就有基本的程式可以來測試看看。
一開始就是新增一個名稱為Customized的Area,如下,
再來就是在Customized Area中建立一個Home Controller,如下,
namespace MvcApplication1.Areas.Customized.Controllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
ViewBag.Message = "客製化 的 Action-Index";
return View();
}
}
}
並建立對應的View,可從原本的View Copy過來修改(包括_ViewStart.cshtml)。然後執行下去,馬上得到以下的錯誤,
找到多個與名稱為 'Home' 的控制器相符的型別。如果服務此要求 ('{controller}/{action}/{id}') 的路由沒有指定命名空間以搜尋符合該要求的控制器,就會發生這個情況。在這種情況下,請呼叫可接受 'namespaces' 參數的 'MapRoute' 方法的多載來註冊此路由。
'Home' 的要求找到以下符合的控制器:
MvcApplication1.Areas.Customized.Controllers.HomeController
MvcApplication1.Controllers.HomeController
這是因為有相同的Home Controller,它不知要Run那一個,所以我們修改App_Start目錄中的RouteConfig.cs,在routes.MapRoute加入namespaces的設定,如下,
namespace MvcApplication1
{
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional}
, namespaces: new[] { "MvcApplication1.Controllers" }
);
}
}
}
重新執行,就可以正常運作。如下,
上圖是預設的Home/Index
上圖是客製化的Home/Index
那接下來就可以在產品執行Controller的Action時,判斷是否有對應的Controller及Action,有的話,就導到客製化的Action去,而且只處理HTTP GET的Action。
所以可以透過ActionFilterAttribute覆寫OnActionExecuting來處理,如下我們建立一個叫CustomizedCheckFilterAttribute的Class,並繼承ActionFilterAttribute,如下,
using System;
using System.Reflection;
using System.Web.Mvc;
using System.Web.Routing;
using System.Linq;
namespace MvcApplication1.Filters
{
public class CustomizedCheckFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
const string customizedAreaName = "Customized";
var controllerName = filterContext.Controller.GetType().Name;
var actionName = filterContext.ActionDescriptor.ActionName;
var controllerShortName = filterContext.ActionDescriptor.ControllerDescriptor.ControllerName;
var isNeedRedirect = false;
if (IsHttpGet(filterContext))
{
isNeedRedirect = IsHaveCustomizedAction(customizedAreaName, controllerName, actionName);
}
if (isNeedRedirect)
{
filterContext.Result = new RedirectToRouteResult(
new RouteValueDictionary { { "Controller", controllerShortName },
{ "Action", actionName }, { "Area", customizedAreaName } });
}
else
{
base.OnActionExecuting(filterContext);
}
}
private static bool IsHaveCustomizedAction(string customizedAreaName, string controllerName, string actionName)
{
bool result = false;
var types = System.Reflection.Assembly.GetExecutingAssembly().GetTypes();
var customizedControllerTypeName = string.Format("{0}.Controllers.{1}", customizedAreaName, controllerName);
var customizedType = types.Where(t => t.FullName.EndsWith(customizedControllerTypeName, StringComparison.InvariantCultureIgnoreCase)).SingleOrDefault();
if (customizedType != null)
{
MethodInfo actionMethodInfo;
try
{
actionMethodInfo = customizedType.GetMethod(actionName, BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.IgnoreCase);
result = (actionMethodInfo != null);
}
catch (AmbiguousMatchException)
{
result = true;
}
}
return result;
}
private static bool IsHttpGet(ActionExecutingContext filterContext)
{
return filterContext.HttpContext.Request.HttpMethod.Equals("GET", StringComparison.InvariantCultureIgnoreCase);
}
}
}
以上透過Reflection來判斷是否有客製化的Type,同時透過Type取得是否有相對應的Method,而如果有AmbiguousMatchException的話,表示可能分別有GET/POST的Action,所以也算有找到!
然後在產品的HomeController Class上加上CustomizedCheckFilter屬性,如下,
using System.Web.Mvc;
using MvcApplication1.Filters;
namespace MvcApplication1.Controllers
{
[CustomizedCheckFilter]
public class HomeController : Controller
{
public ActionResult Index()
{
ViewBag.Message = "Modify this template to jump-start your ASP.NET MVC application.";
return View();
}
public ActionResult About()
{
ViewBag.Message = "Your app description page.";
return View();
}
public ActionResult Contact()
{
ViewBag.Message = "Your contact page.";
return View();
}
}
}
重新執行程式,就會發現,如果有客製化程式,就會導到客製化程式去執行,如下圖所示,
雖然Home/Index,有順利導向到了Customized/Home/Index,但是如果按了畫面上的「關於」,卻會連到Customized/Home/About,但我們並沒有客製Customized/Home/About,所以會出現404的錯誤。
為什麼它會導到Customized/Home/About呢?
那是因為RouteData中的Area已經改成了Customized,所以就會造成錯誤。
目前筆者想到的作法是在Shared\_Layout.cshtml中,需要將ViewContext.RouteData.DataTokens["area"]改成string.Empty,所以一開始,就將Area清成空白,以避免影響共用的Menu,如下,
<!DOCTYPE html>
<html lang="zh">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<meta charset="utf-8" />
<title>@ViewBag.Title - 我的 ASP.NET MVC 應用程式</title>
<link href="~/favicon.ico" rel="shortcut icon" type="image/x-icon" />
<meta name="viewport" content="width=device-width" />
@Styles.Render("~/Content/css")
@Scripts.Render("~/bundles/modernizr")
</head>
<body>
@{
ViewContext.RouteData.DataTokens["area"] = string.Empty;
}
<header>
<div class="content-wrapper">
<div class="float-left">
<p class="site-title">@Html.ActionLink("您標誌的位置", "Index", "Home")</p>
</div>
<div class="float-right">
<section id="login">
@Html.Partial("_LoginPartial")
</section>
<nav>
<ul id="menu">
<li>@Html.ActionLink("首頁", "Index", "Home")</li>
<li>@Html.ActionLink("關於", "About", "Home")</li>
<li>@Html.ActionLink("連絡", "Contact", "Home")</li>
</ul>
</nav>
</div>
</div>
</header>
<div id="body">
@RenderSection("featured", required: false)
<section class="content-wrapper main-content clear-fix">
@RenderBody()
</section>
</div>
<footer>
<div class="content-wrapper">
<div class="float-left">
<p>© @DateTime.Now.Year - 我的 ASP.NET MVC 應用程式</p>
</div>
</div>
</footer>
@Scripts.Render("~/bundles/jquery")
@RenderSection("scripts", required: false)
</body>
</html>
結論
以上透過ActionFilterAttribute來處理專案客製化的方式,並且在Shared\_Layout.cshtml中,將ViewContext.RouteData.DataTokens["area"]改成string.Empty,以避免影響其他共用的的Link。
如果大家有其他方式,請跟我分享,謝謝!
Hi,
亂馬客Blog已移到了 「亂馬客 : Re:從零開始的軟體開發生活」
請大家繼續支持 ^_^