前言

以前写过 Asp.net core 学习笔记 ( ViewComponent 组件 ), 这篇作为翻新版.

参考

Docs – View components in ASP.NET Core

Don't replace your View Components with Razor Components (Razor Component 无法替代 View Component)

介绍

View Component 是 Partial View 的升级版本. 区别就是它多了一个 .cs 可以写逻辑.

Overview

View Component 是由一个 class (.cs) 和一个 View (.cshtml) 组成的.

Component 有名字, View 也有名字, 在某个 Page/View 中想使用 Component 就通过名字召唤它

然后 Component 又会去找寻它的 View 做渲染.

ASP.NET Core 有一套命名机制去声明是否一个 class 属于 View Component, 也有一套机制去寻找 Component 的 View.

我呢是很讨厌这些机制的, 因为它们设计的不直观. 经常莫名其妙的找不到, 需要死背它那不直观的设计才能用好. 但幸好我们可以通过一些 best practice 去规定使用的方式, 这样就不会老是搞错了.

Create View Component Class

两个点需要注意,

第一点是如何告诉 ASP.NET Core 这个 class 是一个 ViewComponent

第二点是这个 Component 的名字是什么.

第一种: 继承 ViewComponent class

public class CallToAction : ViewComponent

class name CallToAction 自动成为 component name, 如果 class name ends with "ViewComponent" 则会被忽略

比如: CallToActionViewComponent 最终的 component name 依然是 CallToAction.

第二种: class name ends with "ViewComponent"

public class CallToActionViewComponent

component name = CallToAction

例外

由于第二种方式是通过命名, 这就导致会出现一些例外. 比如

[NonViewComponent]
public class ReviewComponent

这时需要通过 NonViewComponentAttribute 声明这不是一个 ViewComponent

第三种: 通过 ViewComponentAttribute

[ViewComponent(Name = "CallToAction")]
public class CallToAction

这也是唯一一个自定义 component name 的方式, 如果没有定义, 那么它依然是通过 class name auto become component name.

如果 Name = "CallToActionViewComponent" 那么最终就是 "CallToActionViewComponent" ViewComponent 只有在 class name auto become component name 时会被忽视.

Best Practice

上面这些方式是可以混用的哦, 但是搞那么多方除了乱以外没有任何好处.

我的建议:

1. 通过继承 ViewComponent 来让 ASP.NET Core 知道这个 class 是 View Component, 继承的好处是可以用 build-in 的功能, 比如 return View()

2. class name 命名规范最好是 CallToActionViewComponent, 最终 component name 是 CallToAction

Implement Invoke Method

ViewComponent class 必须实现 Invoke 或 InvokeAsync 方法, 不然会报错

很奇葩的地方是它并不是 interface of ViewComponent class 哦, 所以不是 override method, 它就是一个你要自己知道的方法. 也只有在 runtime 会报错. 设计的真烂.

public class CallToActionViewComponent : ViewComponent
{
public IViewComponentResult Invoke()
{
return View();
}
}

View (.cshtml) 查询

下面 3 个是默认查询的地方.

Component name 上面提到了, view name 默认是 "Default" (不要问我为什么不是 Index)

所以上面 Component 对应的 View Localtion 是

Views/Shared/Components/CallToAction/Default.cshtml

可以通过 options 添加更多匹配的路径.

{0} 就是最终的 view name

查看 ViewViewComponentResult.cs 源码, 可以看到它 hardcore 了 format "Components/{0}/{1}", 0 = component name, 1 = view name.

然后通过 viewEngine 去找. 而 Options 的 ViewLocationFormats 是添加给 viewEngine 用的. 但是无论如何你都避不开它 hardcode 的 "Components/{0}/{1}"

viewEngine 的所有 format 可以通过 RazorViewEngineOptions 查看

var razorViewEngineOptions = app.Services.GetRequiredService<IOptions<RazorViewEngineOptions>>().Value;

下面是 MVC, RazorPages 的所有默认 format

它的玩法是定义一些 format, 然后 viewEngine.FindView 的时候会把 parameter 丢进去成为最终的路径.

ViewLocationExpanders 是一个动态 generate format 的方式. 几年前有写过一篇, 大概长这样:

context 可以看到什么 Page 调用了这个 Component, 然后 viewName 就是 hardcode 的 component format.

viewLocations 是默认的 3 个 formats.

虽然 ViewLocationExpanders 比 ViewLocationFormats dynamic 多一些, 但是很多时候还是不够用的, 因为它也拿不到 component 的 physical file path.

比如, 我的 folder 结构是 MyViewComponent > MyViewComponent.cs + Index.cshtml

同一个 folder 里面有 component class 和 view

由于我拿不到 component 的 physical 路径, 也就没有办法设定成 "{MyViewComponent.cs 路径}Index.cshtml"

MVC 其实也是同样的局限. Router 找到 Controller 以后, Controller 也只能通过 ControllerName 找到 View 没办法依据 physical file path.

Best Practice

我的建议是直接写绝对路径, 或者固定好 component 的 folder 位置. 然后写一个 ViewLocationExpanders 去扩展它.

public class PageTitleSectionComponent : ViewComponent
{
public IViewComponentResult Invoke(PageTitleSectionComponentViewModel viewModel)
{
return View(
$"~/Web/Shared/Component/PageTitleSection/Index.cshtml", viewModel
);
}
}

调用

有 3 种调用方式

@await Component.InvokeAsync("CallToAction", new { myName = "test1" })
@await Component.InvokeAsync(typeof(CallToActionViewComponent)) <!-- 推荐 -->
<vc:call-to-action my-name="test2"></vc:call-to-action>

一个通过 component name (string), 一个用 type, 一个用 tag helper (记得要 @addTagHelper 哦)

也是一样, 搞一堆来乱而已. 我个人的建议是使用 Type 的方式. 这样可以避开 PascalCase convert kebab-case 的问题.

想传入 parameter 就通过匿名对象, 它这个方式也是很烂, 类型在 compile 时无法检测.

public IViewComponentResult Invoke(string myName = "abc")

另外还有一些奇葩

如果是 string 要传变量的话, 记得加上 @. 其它类似就不需要 (统一加上会比较好)

Passing HTML Template

想 passing HTML template 也是可以, 类似 Angular 的 ng-template

定义 Templated Razor delegates

@{
Func<string, object> content =
@<div>
<p>@item</p>
</div>
;
}
@await Component.InvokeAsync(typeof(CallToActionViewComponent), new { content = content })

Func<string, object> string 是 @item 的类型.

把委托当普通参数传进去 View Component.

public class CallToActionViewComponent : ViewComponent
{
public IViewComponentResult Invoke(Func<string, object> content)
{
return View(content);
}
}

在通过 ViewModel 传给 View.

最后在 View 执行就可以了

@model Func<string, object>

@Model("item1")
<h1>Hello World</h1>

提醒:

1. Templated Razor delegates 只能一个 tag, 如果想传入多个 tag 可以用 List<Func<string, object>> 来装, 然后传进去, 虽然很麻烦但是勉强能用, 或者 wrap 一层 div 也是可以破, 只是对 HTML 结构有点乱而已, 有个相关的 Issue 但没人理了。

还有一个方法就是做一个 root-element,然后利用 tag helper 在最终把它 remove 掉。

@{
var title = "Derrick"; Func<dynamic?, IHtmlContent> template = @<stg-root>
<h1>Hello World @title</h1>
@await Component.InvokeAsync(typeof(ItemComponent))
</stg-root>;
} @template(null)

stg-root 就是一个特别的 element,最后会被 remove 掉。

[HtmlTargetElement(tag: "stg-root")]
public class StgRootTagHelper : TagHelper
{
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
output.TagName = null;
var childContent = await output.GetChildContentAsync();
output.Content.AppendHtml(childContent.GetContent());
}
}

注:当涉及到与其它 tag helper 一起操作时,要记得它一开始是存在的,到了 tag helper 的时候才被 remove 掉哦,忘记容易出 bug。

2. 委托最多传 1 个参数 , 像这样 Func<string, int, object> 2个参数是不 ok 的, 会报错 error CS1593: Delegate 'Func<string, int, object>' does not take 1 arguments

因为 @item 只有一个丫. 所以要嘛传入一个对象, 或者 Tuple 也可以.

public Func<(string Value, int Number), object> BodyTemplate // view model

BodyTemplate = @<div>@item.Value - @item.Number</div> // template

@Model.BodyTemplate(("string value", 11)) // generate template

另外最少也要传一个, 可以像下面这样传 null

@{
Func<dynamic?, object> value =
@<h1>Hello World</h1>
;
}
<div class="text-center">
@value(null)
</div>

3. dynamic 的对象不可以不一致

会出现这样的 error

Passing HTML Content

参考 :

Stack Overflow – How to pass csHtml to ViewComponent in .Net Core MVC?

Github Issue – Feature Request: View Component Slots (很遗憾, 目前没有支持)

类似 Angular 的 ng-content。content 和 template 不同哦,content 是 build 好了 HTML 丢进去,里面只是显示而已。

template 是丢进去后才 build HTML。用函数比喻的话,一个是传 string 进去。一个是传 () => string 函数进去。

1. 通过 Html.Raw 传入 IHtmlContent

2. 直接渲染对象就可以了

Build HTML content from template

直接写 Raw HTML 开发体验很差,我们可以尝试搭配 template 手法来实现。

比如我有个组件,需要 content 一个 item list 进去。

首先做一个 HTML builder

@functions {
public static IHtmlContent Repeat(
List<dynamic> items, Func<dynamic, IHtmlContent> template
)
{
var html = new HtmlContentBuilder(); foreach (var item in items)
{
html.AppendHtml(template(item));
} return html;
}
}

使用

@await Component.InvokeAsync(
typeof(TestComponent),
new { itemList = Repeat(new List<dynamic> { "a", "b", "c" }, @<h1>Item @item</h1>)}
)

因为 template 执行后就是返回 HtmlContent,所以配上一个 HtmlContentBuilder 就做到了。虽然手法感觉有点取巧,但这个是官方教的方式哦。

再多一个 build from 组件的 sample:

有 TestComponent 和 ItemComponent

TestComponent 内部需要一个 list

我们 HtmlContentBuilder for loop build 出 ItemComponent HTML 最后 passing 进去给 TestComponent。

@{
var items = new List<string> { "value1", "value2", "value3" };
var itemList = new HtmlContentBuilder();
foreach (var item in items)
{
itemList.AppendHtml(await Component.InvokeAsync(typeof(ItemComponent), new { value = item }));
}
} @await Component.InvokeAsync(
typeof(TestComponent),
new { itemList = itemList}
)

不支持泛型 Generic

ViewComponent class 和参数是不可以放泛型的哦.

参考

Github – Invoke MVC ViewComponent with generic type parameter

stackoverflow – Using a generic model in ASP.NET MVC Razor

Github – Supporting open generic models in Views

不支持 @section Inside View Component

需求是这样的, 有一个 View Component 封装了一个 modal 功能. 这个 modal element 需要放到 body 这样 CSS 才容易定位.

如果在 Page, 我们可以通过 @section 把 modal pass to layout 然后放到 body 的最尾部. 但如果不在 Page 而是在 View Component @section 就不可用了.

参考:

Stack Overflow – Where should I include a script for a view component?

Github Issus – Use Section From Layout Inside View Component

目前最好是避开这样的场景. 如果真的避不开可以考虑利用 JS 把 modal element append 到 body.

ASP.NET Core – View Component的更多相关文章

  1. 移花接木:借助 IViewLocationExpander 更换 ASP.NET Core View Component 视图路径

    端午节在家将一个 asp.net 项目向 asp.net core 迁移时遇到了一个问题,用 view component 取代 Html.RenderAction 之后,运行时 view compo ...

  2. 【WPF】【UWP】借鉴 asp.net core 管道处理模型打造图片缓存控件 ImageEx

    在 Web 开发中,img 标签用来呈现图片,而且一般来说,浏览器是会对这些图片进行缓存的. 比如访问百度,我们可以发现,图片.脚本这种都是从缓存(内存缓存/磁盘缓存)中加载的,而不是再去访问一次百度 ...

  3. ASP.NET Core MVC 之视图组件(View Component)

    1.视图组件介绍 视图组件是 ASP.NET Core MVC 的新特性,类似于局部视图,但它更强大.视图组件不使用模型绑定,并且仅依赖于调用它时所提供的数据. 视图组件特点: 呈块状,而不是整个响应 ...

  4. asp.net core mvc View Component 应用

    ViewComponent 1.View 组件介绍 在ASP.NET CORE MVC中,View组件有点类似于partial views,但是他们更强大,View组件不能使用model bindin ...

  5. [转]asp.net core中的View Component

    解读ASP.NET 5 & MVC6系列(14):View Component http://www.cnblogs.com/TomXu/p/4496486.html

  6. ASP.NET Core MVC 2.x 全面教程_ASP.NET Core MVC 23. 继续讲Tag Helpers 和复习View Component

    当条件为true就渲染,否则就不渲染 ‘ 判断用户的登陆 更好的一点是做一个TagHelper.把这些明显的C#代码都去掉.最终都是用html和属性的形式来组成一个最终的代码 属性名称等于Confit ...

  7. 解读ASP.NET 5 & MVC6系列(14):View Component

    在之前的MVC中,我们经常需要类似一种小部件的功能,通常我们都是使用Partial View来实现,因为MVC中没有类似Web Forms中的WebControl的功能.但在MVC6中,这一功能得到了 ...

  8. 创建ASP.NET Core MVC应用程序(1)-添加Controller和View

    创建ASP.NET Core MVC应用程序(1)-添加Controller和View 参考文档:Getting started with ASP.NET Core MVC and Visual St ...

  9. ASP.NET Core开发-MVC 使用dotnet 命令创建Controller和View

    使用dotnet 命令在ASP.NET Core MVC 中创建Controller和View,之前讲解过使用yo 来创建Controller和View. 下面来了解dotnet 命令来创建Contr ...

  10. 007.Adding a view to an ASP.NET Core MVC app -- 【在asp.net core mvc中添加视图】

    Adding a view to an ASP.NET Core MVC app 在asp.net core mvc中添加视图 2017-3-4 7 分钟阅读时长 本文内容 1.Changing vi ...

随机推荐

  1. Vue bug from backend

    一个后端引发前端的BUG 使用的框架是vue 代码里面有一个组件 <table :data="data"/> 获取后台数据 this.data = await fetc ...

  2. [oeasy]python0097_苹果诞生_史蒂夫_乔布斯_沃兹尼亚克_apple_I

    苹果诞生 回忆上次内容 上次时代华纳公司 凭借手中的影视ip和资本 吞并了雅达利公司 此时 雅达利公司 曾经开发过pong的 优秀员工 乔布斯 还在 印度禅修 寻找自我 看到游戏行业 蓬勃发展 乔布斯 ...

  3. oeasy教您玩转vim - 67 - # 批量替换

    ​ 批量替换 回忆上次 我们可以用vimdiff快速的比较文件 这很实用!!! 实用的一些跳转方式 遍历所有的修改change ]c 下一条修改 [c 上一条修改 遍历所有的函数method ]m 下 ...

  4. Netcode for Entities如何添加自定义序列化,让GhostField支持任意类型?以int3为例(1.2.3版本)

    一句话省流:很麻烦也很抽象,能用内置支持的类型就尽量用. 首先看文档.官方文档里一开头就列出了所有内置的支持的类型:Ghost Type Templates 其中Entity类型需要特别注意一下:在同 ...

  5. SQL Server 图解备份(完全备份、差异备份、增量备份)和还原

    常用的数据备份方式有完全备份.差异备份以及增量备份,那么这三种备份方式有什么区别,在具体应用中又该如何选择呢? 1.三种备份方式 完全备份(Full Backup):备份全部选中的文件夹,并不依赖文件 ...

  6. C#从6.0~9.0都更新了什么?

    一.C#6中新增的功能 get 只读属性 简洁的语法来创建不可变类型,仅有get访问器: public string FirstName { get; } public string LastName ...

  7. 写写Redis十大类型hyperloglog(基数统计)的常用命令

    hyperloglog处理问题的关键所在和bitmap差不多,都是为了减少对sql的写操作,提高性能,用于基数统计的算法.基数就是一种数据集,用于收集去重后内容的数量.会有0.81%的误差 hyper ...

  8. 【MySQL】重装Win10系统后恢复MySQL

    因为种种原因把系统重装了,安装的MySQL不在C盘中,所以数据不会被系统格式化掉 但是重装系统就把之前CMD声明的MySQL服务给删除了 要让MySQL重新跑起来,就需要重新安装服务 恢复MYSQL博 ...

  9. 推荐一款好用的PDF转换工具,可以拆分、合并,亲测好用!!!

    推荐一款好用的PDF转换工具,可以拆分.合并,等等操作,亲测好用. PS. 因为经常会遇到PDF的拆分需要,以前在网上的都是免费的,后来的也都变成付费的无水印的了,再然后就变成全都要收费了.尴尬的是, ...

  10. 【转载】手动DIY制作机械臂

    相关链接: https://news.cnblogs.com/n/703664/ https://www.bilibili.com/video/BV12341117rG https://www.cnb ...