OrchardCore 中的 插件开发/ Shape / DisplayDriver / 视图扩展 / Razor代码注入
请注意该文章 仅限于 OrchardCore项目中的 DisplayDriver 扩展机制,ASP.NET CORE MVC 自身并没有对应功能,如果需要可以将相关的OrchardCore模块添加到项目中也可以实现相应功能
背景
最近一个功能需求,需要使用其它用户模拟身份,所以计划在用户列表页面 扩展 按钮组功能
那么开始看代码,找 用户列表的源码, 当然也可以右键查看页面元素根据关键词到源代码里搜索,
但这里为了研究插件扩展机制如何使用(不含底层实现,感兴趣可以去研究源代码)
源码定位 (这一段有点强行关联的意思。。可以直接看总结)
首先定位到打开用户列表 它对应的路由地址是 Admin/Users/Index
源码位置:OrchardCore\src\OrchardCore.Modules\OrchardCore.Users\Controllers\AdminController.cs
找到控制器,它返回的是
public async Task<ActionResult> Index([ModelBinder(BinderType = typeof(UserFilterEngineModelBinder), Name = "q")] QueryFilterResult<User> queryFilterResult, PagerParameters pagerParameters)
{
//....其它代码
var shapeViewModel = await _shapeFactory.CreateAsync<UsersIndexViewModel>("UsersAdminList", viewModel =>
{
viewModel.Users = userEntries;
viewModel.Pager = pagerShape;
viewModel.Options = options;
viewModel.Header = header;
});
return View(shapeViewModel);
}
对应的视图 文件 OrchardCore\src\OrchardCore.Modules\OrchardCore.Users\Views\Admin\Index.cshtml
这里只有寥寥几行代码。。
@model UsersIndexViewModel
<script asp-name="bootstrap-select" depends-on="admin" at="Foot"></script>
<zone Name="Title"><h1>@RenderTitleSegments(T["Users"])</h1></zone>
<form asp-action="Index" method="post" class="no-multisubmit">
@await DisplayAsync(Model)
</form>
接下来怎么走?貌似跟不下去了啊,再回去看看,遗漏了什么
回到控制器代码 ,注意其返回值 ,它调用了下面的方法生产的一个ViewModel
await _shapeFactory.CreateAsync<UsersIndexViewModel>("UsersAdminList", viewModel =>...
注意到 第一个参数 "UsersAdminList" , 此时先要理解一点:OC中约定的所有Shape 对应的Razor文件都是在 Views 文件夹的根目录下
此时应该去找 对应这个名称的视图文件UsersAdminList.cshtml
负责渲染列表部分的代码如下
@if (Model.Users.Count > 0)
{
@foreach (var entry in Model.Users)
{
<li class="list-group-item">
<div class="form-check float-start">
<input type="checkbox" class="form-check-input" value="@entry.UserId" name="itemIds" id="itemIds-@entry.UserId">
<label class="form-check-label" for="itemIds-@entry.UserId"></label>
</div>
@await DisplayAsync(entry.Shape)
</li>
}
}
else
{
<li class="list-group-item">
<div class="alert alert-info mb-0">
@T["No results found."]
</div>
</li>
}
这里又来了个Shape~ 各种代码跟踪又走不下去了?
肯定有线索。。
Razor 的循环中遍历的是Model的Users 属性,我们看看 Users属性是怎么来的
再回到 Index 代码
有这么几行
var userEntries = new List<UserEntry>();
foreach (var user in results)
{
userEntries.Add(new UserEntry
{
UserId = user.UserId,
Shape = await _userDisplayManager.BuildDisplayAsync(user, updater: _updateModelAccessor.ModelUpdater, displayType: "SummaryAdmin")
});
}
BuildDisplayAsync 是一个泛型方法,第一个参数接收的类型是 User
那么此时烧脑的事情来了。。。根据一个类型去查找它的Razor视图。。有点难搞
分析下我们现在得到的信息 :
- 第一个入参是一个 强类型
User
- 另一个参数 DisplayType:"SummaryAdmin"
- _userDisplayManager 的类型为 IDisplayManager
要使用IDisplayManager<User>
,就要先注册它对应的 IDisplayType
我们全局查找下 关键词 services.AddScoped<IDisplayDriver<User>
然后在查找结果中逐个排查哪个实现中注册了 SummaryAdmin
(最好是结合网页右键查看源码,根据关键代码定位)
最终定位下来只有 UserDisplayDriver
是比较像的。还有个UserRoleDisplayDriver
看名字就不像。。先不理会
还记得 DisplayType 对应的名字是SummaryAdmin
最终对应的 razor 页面
User.SummaryAdmin.cshtml
打开看看,里面有这样一段
@if (Model.Actions != null)
{
<div>
@await DisplayAsync(Model.Actions)
</div>
}
另外在UserDisplayDriver
的 Display
方法中 有这样一行代码
Initialize<SummaryAdminUserViewModel>("UserButtons", model => model.User = user).Location("SummaryAdmin", "Actions:1")
再去找 对应的Razor文件 UserButtons.cshtml
好吧 ,总算是找到了,就它
开始我简单的按照文档将这个文件复制到我的项目中,然后增加了一段html ,发现并没有什么用。。
然后再看上面 的 Initialize 代码,做了个尝试
- 在我的项目中注入
services.AddScoped<IDisplayDriver<User>, MockLoginUserDisplayDriver>();
- 将复制过来的 razor文件 重命名为
UserButtons.MockUserLogin.cshtml
- 添加 MockLoginUserDisplayDriver
public class MockLoginUserDisplayDriver : DisplayDriver<User>
{
public override IDisplayResult Display(User user)
{
return Combine(
Initialize<SummaryAdminUserViewModel>("UserButtons_MockUserLogin", model => model.User = user).Location("SummaryAdmin", "Actions:2")
);
}
}
然后运行。果然生效了,只不过代码渲染了两次,修改 UserButtons.MockUserLogin.cshtml
代码
@if (!isCurrentUser && await AuthorizationService.AuthorizeAsync(User, PermissionsProvider.MockUserLoginPermission, Model.User))
{
<a asp-action="MockLogin" asp-controller="MockUserLogin" asp-route-id="@Model.User.UserId" class="btn btn-secondary btn-sm">@T["Mock Login"]</a>
}
总算是出来了!!
原文连接:https://www.cnblogs.com/Qbit/p/17376516.html
总结
核心代码截个图。。
第一个参数 UserButtons_MockUserLogin 这种格式不是必须的,但是 razor 文件名中的 .替换为下划线是必须的
也可以写成
Initialize<SummaryAdminUserViewModel>("MockUserLoginButtons", model => model.User = user).Location("SummaryAdmin", "Actions:2")
对应razor 文件 MockUserLoginButtons.cshtml
主要是 DisplayDriver
我的这个场景可以认时当在Admin后台渲染User类型的列表时(SummaryAdmin ) 将此代码注入到 Actions 位置的 第二个位置