zoukankan      html  css  js  c++  java
  • ASP.NET MVC Core的ViewComponent

    转:https://www.cnblogs.com/shenba/p/6629212.html

    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

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

    image

    方式一 创建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替换掉,那么运行之后可以看到右上角的内容输出了我们预期的内容

    image

    那么这个就是最简单的实现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>
    复制代码

    尽管页面比较简单,但是比起之前拼字符串的方式更加强大了,下面是应用后右上角的变化效果

    image

    这就是使用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,在城市列表中增加一列显示当前的天气图片

    image

    最后一种创建方式:混杂在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

  • 相关阅读:
    模板 无源汇上下界可行流 loj115
    ICPC2018JiaozuoE Resistors in Parallel 高精度 数论
    hdu 2255 奔小康赚大钱 最佳匹配 KM算法
    ICPC2018Beijing 现场赛D Frog and Portal 构造
    codeforce 1175E Minimal Segment Cover ST表 倍增思想
    ICPC2018Jiaozuo 现场赛H Can You Solve the Harder Problem? 后缀数组 树上差分 ST表 口胡题解
    luogu P1966 火柴排队 树状数组 逆序对 离散化
    luogu P1970 花匠 贪心
    luogu P1967 货车运输 最大生成树 倍增LCA
    luogu P1315 观光公交 贪心
  • 原文地址:https://www.cnblogs.com/feihusurfer/p/14198085.html
Copyright © 2011-2022 走看看