ASP.NET MVC 學習筆記(十四)-利用Attribute設定頁面的Title

ASP.NET MVC 學習筆記(十四)-利用Attribute設定頁面的Title

前情提要..在MVC要動態設定Title時,很自然地會利用一個ViewData來傳遞

請參考Demo這兩篇文章

ASP.NET MVC 設定Title 的技巧

ASP.NET MVC 設定Title 的技巧(二)

 

這樣的方法也用了一陣子了,但一直覺得下面這樣的寫法不太順眼

image

把Title設定放在 { } Block區塊中,會讓我覺得這是Action內要執行的一個部分

注意力總是會被分散掉一些些,所以一直希望將他移到Attribute去。

今天來介紹一下如何實作的

 

最重要的觀念就是MVC在執行Action時,有一個類別是繼承 IActionInvoker

image

 

而預設使用的實作類別是這一個

image

主要的想法就是在執行CreateActionResult的時候,偷偷加一些東西上去

因此我們自訂一個類別CustomCreateActionInvoker,然後繼承ControllerActionInvoker

接著override CreateActionResult這個方法。

繼承ControllerActionInvoker的原因是我們不需要實作太多東西,基本上還是利用Base的Method來執行。

    //自訂一個類別繼承ControllerActionInvoker
    public class CustomCreateActionInvoker : ControllerActionInvoker
    {
        protected override ActionResult CreateActionResult(ControllerContext controllerContext, ActionDescriptor actionDescriptor, object actionReturnValue)
        {
            //還是用Base的方法去獲得ActionResult
            var result = base.CreateActionResult(controllerContext, actionDescriptor, actionReturnValue);

            /*
             * 1. 當不是ChildAction時(這邊可以視情況改變)
             * 2. 當型態是ViewResult(排除JsonResult、FileResult..等狀況),也可用ViewResultBase
             *    才開始抓取Title 
             */
            if (!controllerContext.IsChildAction && result is ViewResult)
            {
                var viewResult = result as ViewResult;

                if (!HasPageTitleAttribute(viewResult, actionDescriptor))
                {
                    HasPageTitleAttribute(viewResult, controllerContext.Controller.GetType());
                }
            }
            return result;
        }

        private bool HasPageTitleAttribute(ViewResult viewResult, ICustomAttributeProvider attrubteProvider)
        {
            //當有抓到自訂的Attribute [PageTitleAttribute] 時,就將它設定到ViewData
            var attr = attrubteProvider.GetCustomAttributes(typeof(PageTitleAttribute), true).FirstOrDefault() 
                       as PageTitleAttribute;

            if (attr != null)
            {
                //使用自訂Attribute的GetTitle取得Title
                viewResult.ViewData["PageTitle"] = attr.GetTitle(viewResult.ViewData.Model);
                return true;
            }
            return false;
        }
    }

接著要將原先執行Action使用的ControllerActionInvoker類別換成我們自己

實作的CustomCreateActionInvoker,在BaseController內替換掉它。

    public class BaseController : Controller
    {
        /// <summary>
        /// 還是保留PageTitle的設定,以應付無法處理的情況
        /// </summary>
        public string PageTitle
        {
            set
            {
                ViewData["PageTitle"] = value;
            }
        }

        /// <summary>
        /// 將CreateActionInvoker替換成自訂的CustomCreateActionInvoker
        /// </summary>
        /// <returns></returns>
        protected override IActionInvoker CreateActionInvoker()
        {
            return new CustomCreateActionInvoker();
        }
    }

自訂的Attribute

    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
    public class PageTitleAttribute : Attribute
    {
        /// <summary> 設定Title </summary>
        private string Title { get; set; }

        /// <summary> 取得頁面Model內的任一屬性 </summary>
        private string PropertyName { get; set; }

        /// <summary> Format格式 </summary>
        private string Format { get; set; }

        private static BindingFlags _bindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.Static;

        /// <summary>
        /// 直接設定Title
        /// </summary>
        public PageTitleAttribute(string title)
        {
            this.Title = title;
        }

        /// <summary>
        /// 取得頁面的Model的屬性值,加上Format設定
        /// </summary>
        /// <param name="propertyName">屬性名稱</param>
        /// <param name="format">Format設定。範例:產品-{0} 介紹</param>
        public PageTitleAttribute(string propertyName, string format)
        {
            this.PropertyName = propertyName;
            this.Format = format;
        }

        /// <summary>
        /// 取得Title
        /// </summary>
        /// <param name="model">頁面的Model</param>
        public string GetTitle(object model)
        {
            if (this.Title != null)
                return this.Title;

            var title = String.Empty;

            if (model != null)
            {
                var prop = model.GetType().GetProperty(PropertyName, _bindingFlags);
                if (prop == null) throw new Exception("找不到屬性[" + PropertyName + "]");
                title = Convert.ToString(prop.GetValue(model, null));
            }
            return String.Format(this.Format, title);
        }
    }

來看看使用的方式

    [PageTitle("文章頁面")]
    public class HomeController : BaseController
    {
        [PageTitle("首頁")]
        public ActionResult Index()
        {
            //取得Action上設定的Title
            return View();
        }

        public ActionResult List()
        {
            //Children Action不執行取得Title
            return View();
        }

        [PageTitle("Name", "{0} | 文章")]
        public ActionResult Detail()
        {
            //取得Page的Model [Name]屬性的值,加上Format。
            
            //測試資料
            DemoClass model = new DemoClass()
            {
                ID = "1",
                Name = "ASP.NET MVC PageTitle設定",
                Content = "299"
            };

            return View(model);
        }

        public ActionResult About()
        {
            //沒有設定Title的Action,會去抓Controller設定的Title
            //如果此Controller內也沒有設定Title,則會抓BaseController的
            return View();
        }

        public ActionResult Json()
        {
            //除了ViewResult之外的Result型態都不抓Title
            return Json(new { test = "JsonResult不抓取Title" }, JsonRequestBehavior.AllowGet);
        }
    }

MasterPage的設定

<title><%=ViewData["PageTitle"] %><asp:ContentPlaceHolder ID="TitleContent" runat="server" /></title>

大概是這樣子。

其實還有幾種實作方式,例如在BaseController覆寫OnActionExecuted

        protected override void OnActionExecuted(ActionExecutedContext filterContext)
        {
            base.OnActionExecuted(filterContext);

            if (!filterContext.IsChildAction && filterContext.Result is ViewResult)
            {
                var viewResult = filterContext.Result as ViewResult;
                if (!HasPageTitleAttribute(viewResult, filterContext.ActionDescriptor))
                {
                    HasPageTitleAttribute(viewResult, filterContext.Controller.GetType());
                }
            }
        }

或者是自訂一個Filter,然後在Global註冊全域的Filter

(MVC 3才有提供,MVC 2的話就加在BaseController上吧)。

等方法...就看實際的狀況來選擇適合的方式吧。

 

備註:這篇實作的東西很少,所以使用上可能會不方便,如果有別的需求可以在自己擴展囉。

***範例下載***