MVC模型以低耦合、可重用、可維護性高等眾多優點已逐漸代替了WebForm模型。能夠靈活使用MVC提供的擴展點可以達到事半功倍的效果,另一方面Asp.net MVC優秀的設計和高質量的代碼也值得我們去閱讀和學習。
本文將介紹Asp.net MVC中常用的八個擴展點並舉例說明。
一、ActionResult
ActionResult代表了每個Action的返回結果。asp.net mvc提供了眾多內置的ActionResult類型,如:ContentResult,ViewResult,JsonResult等,每一種類型都代表了一種服務端的Response類型。我們什么時候需要使用這個擴展點呢?
假如客戶端需要得到XML格式的數據列表:
public void GetUser() { var user = new UserViewModel() { Name = "richie", Age = 20, Email = "abc@126.com", Phone = "139********", Address = "my address" }; XmlSerializer serializer = new XmlSerializer(typeof(UserViewModel)); Response.ContentType = "text/xml"; serializer.Serialize(Response.Output, user); }
我們可以在Controller中定義一個這樣的方法,但是這個方法定義在Controller中有一點別扭,在MVC中每個Action通常都需要返回ActionResult類型,其次XML序列化這段代碼完全可以重用。經過分析我們可以自定義一個XmlResult類型:
public class XmlResult : ActionResult { private object _data; public XmlResult(object data) { _data = data; } public override void ExecuteResult(ControllerContext context) { var serializer = new XmlSerializer(_data.GetType()); var response = context.HttpContext.Response; response.ContentType = "text/xml"; serializer.Serialize(response.Output, _data); } }
這時候Action就可以返回這種類型了:
public XmlResult GetUser() { var user = new UserViewModel() { Name = "richie", Age = 20, Email = "abc@126.com", Phone = "139********", Address = "my address" }; return new XmlResult(user); }
同樣的道理,你可以定義出其他的ActionResult類型,例如:CsvResult等。
二、Filter
MVC中有四種類型的Filter:IAuthorizationFilter,IActionFilter,IResultFilter,IExceptionFilter
這四個接口有點攔截器的意思,例如:當有異常出現時會被IExceptionFilter類型的Filter攔截,當Action在執行前和執行結束會被IActionFilter類型的Filter攔截。
通過實現IExceptionFilter我們可以自定義一個用來記錄日志的Log4NetExceptionFilter:
public class Log4NetExceptionFilter : IExceptionFilter { private readonly ILog _logger; public Log4NetExceptionFilter() { _logger = LogManager.GetLogger(GetType()); } public void OnException(ExceptionContext context) { _logger.Error("Unhandled exception", context.Exception); } }
最后需要將自定義的Filter加入MVC的Filter列表中:
public class FilterConfig { public static void RegisterGlobalFilters(GlobalFilterCollection filters) { filters.Add(new Log4NetExceptionFilter()); } }
為了記錄Action的執行時間,我們可以在Action執行前計時,Action執行結束后記錄log:
public class StopwatchAttribute : ActionFilterAttribute { private const string StopwatchKey = "StopwatchFilter.Value"; private readonly ILog _logger= LogManager.GetLogger(typeof(StopwatchAttribute)); public override void OnActionExecuting(ActionExecutingContext filterContext) { filterContext.HttpContext.Items[StopwatchKey] = Stopwatch.StartNew(); } public override void OnActionExecuted(ActionExecutedContext filterContext) { var stopwatch = (Stopwatch)filterContext.HttpContext.Items[StopwatchKey]; stopwatch.Stop(); var log=string.Format("controller:{0},action:{1},execution time:{2}ms",filterContext.ActionDescriptor.ControllerDescriptor.ControllerName,filterContext.ActionDescriptor.ActionName,stopwatch.ElapsedMilliseconds) _logger.Info(log); } }
ActionFilterAttribute是一個抽象類,它不但繼承了IActionFilter, IResultFilter等Filter,還繼承了FilterAttribute類型,這意味着我們可以將這個自定義的類型當作Attribute來標記到某個Action或者Controller上,同時它還是一個Filter,仍然可以加在MVC的Filter中起到全局攔截的作用。
三、HtmlHelper
在Razor頁面中,如果需要寫一段公用的用來展示html元素的邏輯,你可以選擇使用@helper標記,例如:
@helper ShowProduct(List<ProductListViewModel.Product> products, string style) { <ul class="list-group"> @foreach (var product in products) { <li class="list-group-item @style"><a href="@product.Href" target="_blank">@product.Name</a></li> } </ul> }
這一段代碼有點像一個方法定義,只需要傳入一個list類型和字符串就會按照定義的邏輯輸出html:
<h2>Product list using helper</h2> <div class="row"> <div class="col-md-6">@ShowProduct(Model.SportProducts, "list-group-item-info")</div> <div class="col-md-6">@ShowProduct(Model.BookProducts, "list-group-item-warning")</div> </div> <div class="row"> <div class="col-md-6">@ShowProduct(Model.FoodProducts, "list-group-item-danger")</div> </div>
這樣抽取的邏輯只對當前頁面有效,如果我們想在不同的頁面公用這一邏輯如何做呢?
在Razor中輸入@Html即可得到HtmlHelper實例,例如我們可以這樣用:@Html.TextBox("name")。由此可見我們可以將公用的邏輯擴展在HtmlHelper上:
public static class HtmlHelperExtensions { public static ListGroup ListGroup(this HtmlHelper htmlHelper) { return new ListGroup(); } } public class ListGroup { public MvcHtmlString Info<T>(List<T> data, Func<T, string> getName) { return Show(data,getName, "list-group-item-info"); } public MvcHtmlString Warning<T>(List<T> data, Func<T, string> getName) { return Show(data,getName, "list-group-item-warning"); } public MvcHtmlString Danger<T>(List<T> data, Func<T, string> getName) { return Show(data,getName, "list-group-item-danger"); } public MvcHtmlString Show<T>(List<T> data, Func<T, string> getName, string style) { var ulBuilder = new TagBuilder("ul"); ulBuilder.AddCssClass("list-group"); foreach (T item in data) { var liBuilder = new TagBuilder("li"); liBuilder.AddCssClass("list-group-item"); liBuilder.AddCssClass(style); liBuilder.SetInnerText(getName(item)); ulBuilder.InnerHtml += liBuilder.ToString(); } return new MvcHtmlString(ulBuilder.ToString()); } }
有了上面的擴展,就可以這樣使用了:
<h2>Product list using htmlHelper</h2> <div class="row"> <div class="col-md-6">@Html.ListGroup().Info(Model.SportProducts,x=>x.Name)</div> <div class="col-md-6">@Html.ListGroup().Warning(Model.BookProducts,x => x.Name)</div> </div> <div class="row"> <div class="col-md-6">@Html.ListGroup().Danger(Model.FoodProducts,x => x.Name)</div> </div>
效果:
四、RazorViewEngine
通過自定義RazorViewEngine可以實現同一份后台代碼對應不同風格的View。利用這一擴展能夠實現不同的Theme風格切換。再比如站點可能需要在不同的語言環境下切換到不同的風格,也可以通過自定義RazorViewEngine來實現。
下面就讓我們來實現一個Theme切換的功能,首先自定義一個ViewEngine:
public class ThemeViewEngine: RazorViewEngine { public ThemeViewEngine(string theme) { ViewLocationFormats = new[] { "~/Views/Themes/" + theme + "/{1}/{0}.cshtml", "~/Views/Themes/" + theme + "/Shared/{0}.cshtml" }; PartialViewLocationFormats = new[] { "~/Views/Themes/" + theme + "/{1}/{0}.cshtml", "~/Views/Themes/" + theme + "/Shared/{0}.cshtml" }; AreaViewLocationFormats = new[] { "~Areas/{2}/Views/Themes/" + theme + "/{1}/{0}.cshtml", "~Areas/{2}/Views/Themes/" + theme + "/Shared/{0}.cshtml" }; AreaPartialViewLocationFormats = new[] { "~Areas/{2}/Views/Themes/" + theme + "/{1}/{0}.cshtml", "~Areas/{2}/Views/Themes/" + theme + "/Shared/{0}.cshtml" }; } }
當我們啟用這一ViewEngine時,Razor就會在/Views/Themes/文件夾下去找View文件。為了啟用自定義的ViewEngine,需要將ThemeViewEngine加入到ViewEngines
public class MvcApplication : System.Web.HttpApplication { protected void Application_Start() { if (!string.IsNullOrEmpty(ConfigurationManager.AppSettings["Theme"])) { var activeTheme = ConfigurationManager.AppSettings["Theme"]; ViewEngines.Engines.Insert(0, new ThemeViewEngine(activeTheme)); }; //... } }
接下來就開始編寫不同風格的View了,重點在於編寫的View文件夾組織方式要跟ThemeViewEngine中定義的路徑要一致,以ServiceController為例,我們編寫ocean和sky兩種風格的View:
最后在web.config制定一種Theme:<add key="Theme" value="ocean"/>,ocean文件夾下的View將會被優先采用:
五、Validator
通過在Model屬性上加Attribute的驗證方式是MVC提倡的數據驗證方式,一方面這種方式使用起來比較簡單和通用,另一方面這種統一的方式也使得代碼很整潔。使用ValidationAttribute需要引入System.ComponentModel.DataAnnotations命名空間。
但是有時候現有的ValidationAttribute可能會不能滿足我們的業務需求,這就需要我們自定義自己的Attribute,例如我們自定義一個AgeValidator:
public class AgeValidator: ValidationAttribute { public AgeValidator() { ErrorMessage = "Please enter the age>18"; } public override bool IsValid(object value) { if (value == null) return false; int age; if (int.TryParse(value.ToString(), out age)) { if (age > 18) return true; return false; } return false; } }
自定義的AgeValidator使用起來跟MVC內置的ValiatorAttribute沒什么區別:
[Required] [AgeValidator] public int? Age { get; set; }
不過我們有時候可能有這種需求:某個驗證規則要針對Model中多個屬性聯合起來判斷,所以上面的方案無法滿足需求。這時候只需Model實現IValidatableObject接口即可:
public class UserViewModel:IValidatableObject { public string Name { get; set; } [Required] [AgeValidator] public int? Age { get; set; } public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) { if(string.IsNullOrEmpty(Name)) yield return new ValidationResult("the name can not be empty"); if (Name.Equals("lucy")) { if(Age.Value<25) yield return new ValidationResult("lucy's age must greater than 25"); } } }
六、ModelBinder
Model的綁定體現在從當前請求提取相應的數據綁定到目標Action方法的參數中。
public ActionResult InputAge(UserViewModel user) { //... return View(); }
對於這樣的一個Action,如果是Post請求,MVC會嘗試將Form中的值賦值到user參數中,如果是get請求,MVC會嘗試將QueryString的值賦值到user參數中。
假如我們跟客戶的有一個約定,客戶端會POST一個XML格式的數據到服務端,MVC並不能准確認識到這種數據請求,也就不能將客戶端的請求數據綁定到Action方法的參數中。所以我們可以實現一個XmlModelBinder:
public class XmlModelBinder:IModelBinder { public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) { try { var modelType = bindingContext.ModelType; var serializer = new XmlSerializer(modelType); var inputStream = controllerContext.HttpContext.Request.InputStream; return serializer.Deserialize(inputStream); } catch { bindingContext.ModelState.AddModelError("", "The item could not be serialized"); return null; } } }
有了這樣的自定義ModelBinder,還需要通過在參數上加Attribute的方式啟用這一ModelBinder:
public ActionResult PostXmlContent([ModelBinder(typeof(XmlModelBinder))]UserViewModel user) { return new XmlResult(user); }
我們使用PostMan發送個請求試試:
剛才我們顯示告訴MVC某個Action的參數需要使用XmlModelBinder。我們還可以自定義一個XmlModelBinderProvider,明確告訴MVC什么類型的請求應該使用XmlModelBinder:
public class XmlModelBinderProvider: IModelBinderProvider { public IModelBinder GetBinder(Type modelType) { var contentType = HttpContext.Current.Request.ContentType.ToLower(); if (contentType != "text/xml") { return null; } return new XmlModelBinder(); } }
這一Provider明確告知MVC當客戶的請求格式為text/xml時,應該使用XmlModelBinder。
public class MvcApplication : System.Web.HttpApplication { protected void Application_Start() { ModelBinderProviders.BinderProviders.Insert(0, new XmlModelBinderProvider()); //... } }
有了XmlModelBinderProvier,我們不再顯示標記某個Action中的參數應該使用何種ModelBinder:
public ActionResult PostXmlContent(UserViewModel user) { return new XmlResult(user); }
七、自定義ControllerFactory實現依賴注入
MVC默認的DefaultControllerFactory通過反射的方式創建Controller實例,從而調用Action方法。為了實現依賴注入,我們需要自定義ControllerFactory從而通過IOC容器來創建Controller實例。
以Castle為例,需要定義WindsorControllerFactory,另外還要創建ContainerInstaller文件,將組建注冊在容器中,最后通過ControllerBuilder.Current.SetControllerFactory(new WindsorControllerFactory(container));將MVC的ControllerFactory指定為我們自定義的WindsorControllerFactory。
為了簡單起見,這一Nuget包可以幫助我們完成這一系列任務:
Install-Package Castle.Windsor.Web.Mvc
上面提到的步驟都會自動完成,新注冊一個組件試試:
public class ProvidersInstaller:IWindsorInstaller { public void Install(IWindsorContainer container, IConfigurationStore store) { container.Register(Component.For<IUserProvider>().ImplementedBy<UserProvider>().LifestylePerWebRequest()); } }
Controller就可以進行構造器注入了:
private readonly IUserProvider _userProvider; public ServiceController(IUserProvider userProvider) { _userProvider = userProvider; } public ActionResult GetUserByIoc() { var user = _userProvider.GetUser(); return new XmlResult(user); }
八、使用Lambda Expression Tree擴展MVC方法
准確來說這並不是MVC提供的擴展點,是我們利用Lambda Expression Tree寫出強類型可重構的代碼。以ActionLink一個重載為例:
public static MvcHtmlString ActionLink(this HtmlHelper htmlHelper, string linkText, string actionName, object routeValues, object htmlAttributes);
在Razor頁面,通過@Html.ActionLink("Line item 1", "OrderLineItem", "Service", new { id = 1 })可以生成a標簽。這一代碼的缺點在於Controller和Action都以字符串的方式給出,這樣的代碼在大型的軟件項目中不利於重構,即便Controller和Action字符串編寫錯誤,編譯器也能成功編譯。
我們可以利用Lambda Expression Tree解析出Controller和Action的名稱。理論上所有需要填寫Controller和Action字符串的方法都可以通過這一方法來實現。具體實現步驟參考Expression Tree 擴展MVC中的 HtmlHelper 和 UrlHelper。下面給出兩種方法的使用對比:
<div class="row"> <h2>Mvc way</h2> <ul> <li>@Html.ActionLink("Line item 1", "OrderLineItem", "Service", new { id = 1 }) </li> <li>@Html.ActionLink("Line item 2", "OrderLineItem", "Service", new { id = 2 })</li> <li>@Url.Action("OrderLineItem","Service",new {id=1})</li> <li>@Url.Action("OrderLineItem","Service",new {id=2})</li> </ul> </div> <div class="row"> <h2>Lambda Expression tree</h2> <ul> <li>@Html.ActionLink("Line item 1", (ServiceController c) => c.OrderLineItem(1))</li> <li>@Html.ActionLink("Line item 2", (ServiceController c) => c.OrderLineItem(2))</li> <li>@Url.Action((ServiceController c)=>c.OrderLineItem(1))</li> <li>@Url.Action((ServiceController c)=>c.OrderLineItem(2))</li> </ul> </div>
本文Demo下載:https://git.oschina.net/richieyangs/MVCExtension.Points
祝大家春節快樂,猴年大吉!