MVC Core新增了ViewComponent的概念,直接强行理解为视图组件,用于在页面上显示可重用的内容,这部份内容包括逻辑和展现内容,并且定义为组件那么其一定是能够独立存在而且是高度可重用的。html
其实从概念上讲,在ASP.NET的历史版本中早已有实现,从最开始的WebForm版本就提供了ascx做为用户的自定义控件,以控件的方式将独立的功能封装起来。到了后来的MVC框架,提供了partial view,可是partial view就是提供视图的重用,业务数据仍是依赖于action提供。git
MVC还要一个更加独立的实现方式就是用Child Action,所谓Child Action就是一个Action定义在Controller里面,而后标记一个[ChildAction]属性标记。我在早期作MVC项目的时候,感受到Child Action的功能很不错,将一些每一个页面(但不是全部页面)须要用到的小部件都作成Child Action,好比登陆信息,左侧菜单栏等。一开始以为不错,后来发现了严重性能问题结果被吊打。问题的元凶是Child Action执行的时候会把Controller的那一套生命周期的环节再执行一遍,好比OnActionExecuting,OneActionExecuted等,也就是说致使了不少无用的重复操做(由于当时OnActionExecuting也被用得很泛滥)。以后复盘分析,Child Action执行动做的时候就比如在当前ActionExcuted以后再开出一个分支去执行其余任务,致使了Controller执行过程又嵌套了一个Controller执行过程,严重违背了扁平化的思想(当时公司的主流设计思想),因此后来都没用Child Action。github
简单回顾了过去版本的实现,咱们来看看MVC Core的ViewComponenet。从名称定义来看,是要真的把功能数据和页面都独立出来,要否则配不上组件二字。ViewComponent独立于其所在的View页面和Action,更不会跟当前的Controller有任何的瓜葛,也就是说不是Child Action了。固然ViewComponent也能够重用父页面的数据或者从而后用本身的View。固然ViewComponent是不能独立使用的,必须在一个页面内被调用。后端
接下来看看ViewComponent的几种建立方式框架
首先准备一个项目,这里使用基于Starter kit项目项目模板建立一个用于练习的项目前后端分离
(该项目模板能够在这里下载https://marketplace.visualstudio.com/items?itemName=junwenluo.ASPNETMVCCoreStarterKit)异步
运行后的初始界面是这样的async
方式一 建立POCO View Component函数
POCO估计写过程序的都知道是什么东西,能够简单理解为一个比较纯粹的类,不依赖于任何外部框架或者包含附加的行为,纯粹的只用CLR的语言建立。性能
用POCO方式建立的ViewComponent类必须用ViewComponent结尾,这个类能定义在任何地方(只要能被引用到),这里咱们新建一个ViewComponents目录,而后新建以下类
public class PocoViewComponent { private readonly IRepository _repository; public PocoViewComponent(IRepository repository) { _repository = repository; } public string Invoke() { return $"{_repository.Cities.Count()} cities, " + $"{_repository.Cities.Sum(c => c.Population)} people"; } }
这个类很简单,就是提供一个Invoke方法,而后返回城市的数量和总人口信息。
而后在页面中应用,咱们把调用语句放在_Layout.cshtml页面中
@await Component.InvokeAsync("Poco")
将右上角的City Placeholder替换掉,那么运行以后能够看到右上角的内容输出了咱们预期的内容
那么这个就是最简单的实现ViewComponent的方式,从这个简单的例子能够看到咱们只须要提供一个约定的Invoke方法便可。
从这个简单的Component能够看到有如下3个优点
1 ViewComponent支持经过构造函数注入参数,也就是支持常规的依赖注入。有了依赖注入的支持,那么Component就有了本身
独立的数据来源
2 支持依赖注入意味着能够进行独立的单元测试
3 因为Component的实现的高度独立性,其能够被应用于多处,固然不会跟任何一个Controller绑定(也就不会有ChildAction带来的麻烦)
方式二 基于ViewComponentBase建立
基于POCO方式建立的ViewComponent的好处是简单,不依赖于其余类。若是基于ViewComponentBase建立,那么就有了一个上下文,毕竟也是一个基础类,总会提供一些帮助方法吧,就跟Controller一个道理。
下面是基于ViewComponent做为基类建立一个ViewComponent
public class CitySummary : ViewComponent { private readonly IRepository _repository; public CitySummary(IRepository repository) { _repository = repository; } public string Invoke() { return $"{_repository.Cities.Count()} cities, " + $"{_repository.Cities.Sum(c => c.Population)} people"; } }
咋一看跟用POCO的方式没有什么区别,代码拷贝过来就能用,固然输出的内容也是一致的。
固然这个例子只是证实这种实现方式,还没体现出继承了ViewComponentBase的好处。下面来了解一下这种方式的优点
1.实际项目中使用deViewComponent哪有这么简单,仅仅输出一行字符串。若是遇到须要输出很复杂的页面,那岂不是要拼凑很复杂的字符串,这样不优雅,更不用说单元测试,先后端分离这样的高大上的想法。因此ViewComponentBase就提供了一个相似Controller那样的解决方法,提供了几个内置的ResultType,然你返回到结果符合面向对象的思想,一次过知足上述的要求,主要有如下三种结果类型
a.ViewVIewComponentResult
能够理解为Controller的ViewResult,就是结果是经过一个VIew视图来展现
b.ContentViewComponentResult
相似于Controller的ContentResult,返回的是Encode以后的文本结果
c.HtmlContentViewComponentResult
这个用于返回包含Html字符串的内容,也就是说这些Html内容须要直接显示,而不是Encode以后再显示。
有了上述的理解,借助ViewComponentBase提供的一些基类方法就能够轻松实现显示一个复杂的视图,跟Controller相似。
下面咱们改进一下CitySummary,改为输出一个ViewModel,并经过独立的View去定义Html内容。
public class CitySummary : ViewComponent { private readonly IRepository _repository; public CitySummary(IRepository repository) { _repository = repository; } public IViewComponentResult Invoke() { return View(new CityViewModel { Cities = _repository.Cities.Count(), Population = _repository.Cities.Sum(c => c.Population) }); } }
注意Invoke的实现代码,其中使用了View的方法。
这个View方法的实现逻辑相似Controller的View,可是寻找View页面的方式不一样,其寻找页面文件的路径规则以下
/Views/{CurrentControllerName}/Components/{ComponentName}/Default.cshtml
/Views/Shared/Components/{ComponentName}/Default.cshtml
根据这规则,在View/Shared/目录下建立一个Components目录,而后再建立CitySummary目录,接着新建一个Default.cshtml页面
@model CityViewModel <table class="table table-condensed table-bordered"> <tr> <td>Cities:</td> <td class="text-right"> @Model.Cities </td> </tr> <tr> <td>Population:</td> <td class="text-right"> @Model.Population.ToString("#,###") </td> </tr> </table>
尽管页面比较简单,可是比起以前拼字符串的方式更增强大了,下面是应用后右上角的变化效果
这就是使用View的方式,其余两种结果类型的使用方式跟Controller的相似。
除了调用View方法以外,经过ViewComponentBase还能够得到当前请求的上下文信息,好比路由参数。
好比读取请求id,而后加载对应Country的数据
public IViewComponentResult Invoke() { var target = RouteData.Values["id"] as string; var cities = _repository.Cities.Where( city => target == null || Compare(city.Country, target, StringComparison.OrdinalIgnoreCase) == 0).ToArray(); return View(new CityViewModel { Cities = cities.Count(), Population = cities.Sum(c => c.Population) }); }
固然也能够经过方法参数的形式传入id,好比咱们能够在页面调用的时候传入id参数,那么Invoke方法能够改为以下
public IViewComponentResult Invoke(string target) { target = target ?? RouteData.Values["id"] as string; var cities = _repository.Cities.Where( city => target == null || Compare(city.Country, target, StringComparison.OrdinalIgnoreCase) == 0).ToArray(); return View(new CityViewModel { Cities = cities.Count(), Population = cities.Sum(c => c.Population) }); }
而后在界面调用的时候
@await Component.InvokeAsync("CitySummary", new { target = "USA" }),传入target参数。
上面介绍的都是同步执行的ViewComponent,接下来咱们来看看支持异步操做的ViewComponent。
下面咱们建立一个WeatherViewComponent,获取城市的天气,这获取天气经过异步的方式从外部获取。
在Components文件夹建立一个CityWeather文件夹,而后建立一个Default.cshtml文件,内容以下
@model string <img src="http://@Model"/>
这个页面只是显示一个天气的图片,具体的值经过服务端返回。
而后在ViewComponents目录新建一个CityWeather类,以下
public class CityWeather : ViewComponent { private static readonly Regex WeatherRegex = new Regex(@"<img id=cur-weather class=mtt title="".+?"" src=""//(.+?.png)"" width=80 height=80>"); public async Task<IViewComponentResult> InvokeAsync(string country, string city) { city = city.Replace(" ", string.Empty); using (var client = new HttpClient()) { var response = await client.GetAsync($"https://www.timeanddate.com/weather/{country}/{city}"); var content = await response.Content.ReadAsStringAsync(); var match = WeatherRegex.Match(content); var imageUrl = match.Groups[1].Value; return View("Default", imageUrl); } } }
这个ViewComponent最大的特别之处是,它从外部获取城市的天气信息,这个过程使用的async的方法,异步从http下载获得内容后,解析返回当前天气的图片。
对于每个城市咱们均可以调用这个ViewComponent,在城市列表中增长一列显示当前的天气图片
最后一种建立方式:混杂在Controller中
听名字就以为不对劲了,难道又回到ChildAction的老路。其实不是,先看看定义。
就是说将ViewComponent的Invoke方法定义在Controller中,Invoke的方法签名跟以前两种方式相同。
那么这么作的目的其实是为了某些代码的共用,很少说先看看代码如何实现。
在HomeController加入以下方法
public IViewComponentResult Invoke() => new ViewViewComponentResult { ViewData = new ViewDataDictionary<IEnumerable<City>>(ViewData, _repository.Cities) };
这个Invoke方法就是普通的ViewComponent必须的方法,最关键是重用了这个Controller里面的_repository,固然实际代码会更有意义些。
而后给HomeController加入以下属性标记
[ViewComponent(Name = "ComboComponent")]
这里使用了ViewComponent这个属性标记在Controller上,一看就知道这是用来标记识别ViewComponent的。
接着建立视图,在Views/Shared/Components/下建立一个ComboComponent目录,并建立一个Default.cshtml文件
@model IEnumerable<City> <table class="table table-condensed table-bordered"> <tr> <td>Biggest City:</td> <td> @Model.OrderByDescending(c => c.Population).First().Name </td> </tr> </table>
而后调用跟其余方式同样,按名称去Invoke
@await Component.InvokeAsync("ComboComponent")
小结
OK,以上就是ViewComponent的三种建立方式,都比较简单易懂,推荐使用方式二。
示例代码:https://github.com/shenba2014/AspDotNetCoreMvcExamples/tree/master/CustomViewComponent