MVC Core新增了ViewComponent的概念,直接强行理解为视图组件,用于在页面上显示可重用的内容,这部分内容包括逻辑和展示内容,而且定义为组件那么其必定是可以独立存在并且是高度可重用的。

其实从概念上讲,在ASP.NET的历史版本中早已有实现,从最开始的WebForm版本就提供了ascx作为用户的自定义控件,以控件的方式将独立的功能封装起来。到了后来的MVC框架,提供了partial view,但是partial view就是提供视图的重用,业务数据还是依赖于action提供。

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。

简单回顾了过去版本的实现,我们来看看MVC Core的ViewComponenet。从名称定义来看,是要真的把功能数据和页面都独立出来,要不然配不上组件二字。ViewComponent独立于其所在的View页面和Action,更不会跟当前的Controller有任何的瓜葛,也就是说不是Child Action了。当然ViewComponent也可以重用父页面的数据或者从然后用自己的View。当然ViewComponent是不能独立使用的,必须在一个页面内被调用。

接下来看看ViewComponent的几种创建方式

首先准备一个项目,这里使用基于Starter kit项目项目模板创建一个用于练习的项目

(该项目模板可以在这里下载https://marketplace.visualstudio.com/items?itemName=junwenluo.ASPNETMVCCoreStarterKit

运行后的初始界面是这样的

方式一 创建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) == ).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) == ).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[].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

ASP.NET MVC Core的ViewComponent的更多相关文章

  1. .NET CORE学习笔记系列(1)——ASP.NET MVC Core 介绍和项目解读

    ASP.NET MVC Core 项目文件夹解读 一.项目文件夹总览 1.1.Properties——launchSettings.json 启动配置文件,你可以在项目中“Properties”文件夹 ...

  2. ASP.NET Core 1.0、ASP.NET MVC Core 1.0和Entity Framework Core 1.0

    ASP.NET 5.0 将改名为 ASP.NET Core 1.0 ASP.NET MVC 6  将改名为 ASP.NET MVC Core 1.0 Entity Framework 7.0    将 ...

  3. [转帖]2016年时的新闻:ASP.NET Core 1.0、ASP.NET MVC Core 1.0和Entity Framework Core 1.0

    ASP.NET Core 1.0.ASP.NET MVC Core 1.0和Entity Framework Core 1.0 http://www.cnblogs.com/webapi/p/5673 ...

  4. 简述C#中IO的应用 RabbitMQ安装笔记 一次线上问题引发的对于C#中相等判断的思考 ef和mysql使用(一) ASP.NET/MVC/Core的HTTP请求流程

    简述C#中IO的应用   在.NET Framework 中. System.IO 命名空间主要包含基于文件(和基于内存)的输入输出(I/O)服务的相关基础类库.和其他命名空间一样. System.I ...

  5. ASP.NET MVC Core的TagHelper (高级特性)

    这篇博文ASP.NET MVC Core的TagHelper(基础篇)介绍了TagHelper的基本概念和创建自定义TagHelper的方式,接着继续介绍一些新的看起来比较高级的特性.(示例代码紧接着 ...

  6. ASP.NET MVC Core的TagHelper(基础篇)

    TagHelper又是一个新的名词,它替代了自之前MVC版本的HtmlHelper,专注于在cshmlt中辅助生成html标记. 通过使用自定义的TagHelper可以提供自定义的Html属性或元素, ...

  7. ASP.NET MVC Core Starter Kit

    上一篇博文<创建.NET Core程序的Nuget Package>提到准备创建一个Nuget包,用于自动生成一个简单的ASP.NET MVC Core的示例项目.本来是打算用Nuget实 ...

  8. ASP.NET MVC/Core表单提交后台模型二级属性验证问题

    起因 这个是网友在官网论坛的提问:https://fineui.com/bbs/forum.php?mod=viewthread&tid=22237 重新问题 本着务实求真的态度,我们先来复现 ...

  9. ASP.NET/MVC/Core的HTTP请求流程

    ASP.NET HTTP管道(Pipeline)模型 1. 先讲一点,再深刻思考 一般我们都在写业务代码,优化页面,优化逻辑之间内徘徊.也许我们懂得HTTP,HTTPS的GET,POST,但是我们大部 ...

随机推荐

  1. Python2 to python3

    概述 几乎所有的Python 2程序都需要一些修改才能正常地运行在Python 3的环境下.为了简化这个转换过程,Python 3自带了一个叫做2to3的实用脚本(Utility Script),这个 ...

  2. 使用Toolbar + DrawerLayou实现菜单侧滑,改变toolbar左上角图标

    侧边栏具体实现可以参照http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0303/2522.html getSupportActio ...

  3. 开源一款强大的文件服务组件(QJ_FileCenter)(系列三 访问接口与项目集成)

    系列文章 1. 开源一款强大的文件服务组件(QJ_FileCenter)(系列一) 2. 开源一款强大的文件服务组件(QJ_FileCenter)(系列二 安装说明) 3. 开源一款强大的文件服务组件 ...

  4. C++并发多线程(一)

    并发:两个或者更多的任务同时发生,一个程序同时执行多个独立的任务. 以往计算机 单核CPU 某一个时刻只能执行一个任务 由操作系统调度 每秒钟进行多次所谓的任务切换并发的假象(不是真正的并发),这种切 ...

  5. WPF 分享一种设置程序保存配置文件的方法

    最近需要做一个配置程序,主要给其他程序做相关配置的小工具. 配置项蛮多的,一般我们都是将各个配置项写到配置文件的节点中,比如App.config文件或者自定义的xml文件. 因为我用的是wpf,MVV ...

  6. pageadmin CMS 如何添加自定义页面

    理论上网站上的所有页面都可以通过栏目管理来添加,那自定义页面的意义是什么呢? 网站的需求是很多样化的,比如需要制作一个对外提供数据的api,甚至制作一个搜索页面,或者制作一些数据和栏目没有对应关系的页 ...

  7. LOJ#2076. 「JSOI2016」炸弹攻击(模拟退火)

    题面 传送门 题解 退火就好了 记得因为答案比较小,但是温度比较高,所以在算\(\exp\)的时候最好把相差的点数乘上一个常数来让选取更劣解的概率降低 话虽如此然而我自己打的退火答案永远是\(0\)- ...

  8. AtcoderExaWizards 2019题解

    传送门 \(A\ Regular\ Triangle\) 咕咕 \(B\ Red\ or\ Blue\) 咕咕咕 \(C\ Snuke\ the\ Wizard\) 我可能脑子真的坏掉了-- 容易发现 ...

  9. 理解 atime,ctime,mtime (上)

    理解 atime,ctime,mtime (上) Unix文件系统会为每个文件存储大量时间戳.这意味着您可以使用这些时间戳来查找任意时间访问到的任何文件或目录(读取或写入),更改(文件访问权限更改)或 ...

  10. 架构师养成记--32.Redis高级(安全 主从复制)

    Redis高级命令及特性 keys * 返回满足的所有键值(*表示模糊匹配) exists 是否存在指定的key(返回1表示存在,0表示不存在) expire 设置某个key的过期时间,使用ttl查看 ...