玩转Asp.netMVC的八个扩展点
2016-02-15 16:55:33 | 来源:玩转帮会 | 投稿:佚名 | 编辑:dations

原标题:玩转Asp.netMVC的八个扩展点

  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 = "[email protected]",
                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 = "[email protected]",
                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.Application[StopwatchKey] = Stopwatch.StartNew();
        }
        public override void OnActionExecuted(ActionExecutedContext filterContext)
        {
            var stopwatch = (Stopwatch)filterContext.HttpContext.Application[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>
<p class="row">
    <p class="col-md-6">@ShowProduct(Model.SportProducts, "list-group-item-info")</p>
    <p class="col-md-6">@ShowProduct(Model.BookProducts, "list-group-item-warning")</p>
</p>
<p class="row">
    <p class="col-md-6">@ShowProduct(Model.FoodProducts, "list-group-item-danger")</p>
</p>

  这样抽取的逻辑只对当前页面有效,如果我们想在不同的页面公用这一逻辑如何做呢?

  在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-info");
        }
        public MvcHtmlString Danger<T>(List<T> data, Func<T, string> getName)
        {
            return Show(data,getName, "list-group-item-info");
        }
        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>
<p class="row">
    <p class="col-md-6">@Html.ListGroup().Info(Model.SportProducts,x=>x.Name)</p>
    <p class="col-md-6">@Html.ListGroup().Warning(Model.BookProducts,x => x.Name)</p>
</p>
<p class="row">
    <p class="col-md-6">@Html.ListGroup().Danger(Model.FoodProducts,x => x.Name)</p>
</p>

  效果:

  四、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。下面给出两种方法的使用对比:

<p 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>
</p>
<p 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>
</p>

  本文Demo预告:https://git.oschina.net/richieyangs/MVCExtension.Points

tags:

上一篇  下一篇

相关:

.NETCore1.0、ASP.NETCore1.0和EFCore1.0简介

  新版本的ASP.NET和Entity Framework有一个严重的问题,就是它们同以前的版本不兼容。这不只是行为或AP

南台强震引发需求 钢铁水泥股劲扬

台南强震,除维冠金龙大楼倒塌外,房屋多有毁损,产生重建需求,再加上大陆对水泥、钢铁等过剩产能已启动汰

物联网防灾新应用,App让大家的手机能协助侦测地震提早预警

图片来源: Google Play-MyShake 当地震发生时,如果能够早期预警就能避免造成人员及财产损害。加州大学柏克

Apple Music上线半年订阅用户突破1100万

台湾的Apple Music服务已在农历年前推出。 图片来源: Apple 苹果软件工程资深副总裁Craig Federighi及网路

兴奋的小记——IPSA我的第一个美白routine

说起美白,我是一肚子委屈要说呀。我的脸一直是全身最黑的地方……(自拍不可信,毕竟我一般自拍的时候都

神隆去年EPS为0.87元 年增26%

提供原料药及制剂开发制造的台湾神隆公布2015年合并营收为新台币39.55亿元,合并税后净利6.35亿元,年增31%

AppSo 年度榜单转发抽奖中奖名单公布

中奖名单:
PILO 蓝牙枕头:@唐浩益, @doublemarkKindle 6:@Elaine闫姝颐Sphero BB-8 机器人:@蒙娜理纱“

网路经济蔚为风潮 巨量资料、行动商务人才需求大增

记者曹逸雯/台北报导根据国发会报告指出,网路经济蔚为全球经济风潮,欧美日等近年来都积极强化网路经济的

住房城乡建设部关于2015年第三十二批一级建造师注册人员名单的公告

根据《注册建造师管理规定》(建设部令第153号)、《一级建造师注册实施办法》和《关于建设部机关直接实施的

住房城乡建设部关于2015年第三十三批一级建造师注册人员名单的公告

根据《注册建造师管理规定》(建设部令第153号)、《一级建造师注册实施办法》和《关于建设部机关直接实施的

站长推荐: