玩轉Asp.net MVC 的八個擴展點


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

祝大家春節快樂,猴年大吉!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM