[MVC]專案客製化方式

[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 應用程式,專案範本選取「網際網路應用程式」,這樣一開始就有基本的程式可以來測試看看。

image

 

一開始就是新增一個名稱為Customized的Area,如下,

image

image

 

再來就是在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" }
            );
        }
    }
}

 

重新執行,就可以正常運作。如下,

image

上圖是預設的Home/Index

 

image

上圖是客製化的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();
        }
    }
}

 

重新執行程式,就會發現,如果有客製化程式,就會導到客製化程式去執行,如下圖所示,

image

 

雖然Home/Index,有順利導向到了Customized/Home/Index,但是如果按了畫面上的「關於」,卻會連到Customized/Home/About,但我們並沒有客製Customized/Home/About,所以會出現404的錯誤。

image

為什麼它會導到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>&copy; @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。

如果大家有其他方式,請跟我分享,謝謝!