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="[email protected]">
                    <label class="form-check-label" for="[email protected]"></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视图。。有点难搞
分析下我们现在得到的信息 :

  1. 第一个入参是一个 强类型 User
  2. 另一个参数 DisplayType:"SummaryAdmin"
  3. _userDisplayManager 的类型为 IDisplayManager

要使用IDisplayManager<User> ,就要先注册它对应的 IDisplayType
我们全局查找下 关键词 services.AddScoped<IDisplayDriver<User>
image

然后在查找结果中逐个排查哪个实现中注册了 SummaryAdmin(最好是结合网页右键查看源码,根据关键代码定位)
最终定位下来只有 UserDisplayDriver 是比较像的。还有个UserRoleDisplayDriver 看名字就不像。。先不理会
还记得 DisplayType 对应的名字是SummaryAdmin 最终对应的 razor 页面
User.SummaryAdmin.cshtml
打开看看,里面有这样一段

    @if (Model.Actions != null)
    {
        <div>
            @await DisplayAsync(Model.Actions)
        </div>
    }

另外在UserDisplayDriverDisplay 方法中 有这样一行代码

Initialize<SummaryAdminUserViewModel>("UserButtons", model => model.User = user).Location("SummaryAdmin", "Actions:1")

再去找 对应的Razor文件 UserButtons.cshtml
好吧 ,总算是找到了,就它

开始我简单的按照文档将这个文件复制到我的项目中,然后增加了一段html ,发现并没有什么用。。
然后再看上面 的 Initialize 代码,做了个尝试

  1. 在我的项目中注入services.AddScoped<IDisplayDriver<User>, MockLoginUserDisplayDriver>();
  2. 将复制过来的 razor文件 重命名为 UserButtons.MockUserLogin.cshtml
  3. 添加 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>
}

总算是出来了!!

image

原文连接:https://www.cnblogs.com/Qbit/p/17376516.html

总结

核心代码截个图。。

image

第一个参数 UserButtons_MockUserLogin 这种格式不是必须的,但是 razor 文件名中的 .替换为下划线是必须的
也可以写成
Initialize<SummaryAdminUserViewModel>("MockUserLoginButtons", model => model.User = user).Location("SummaryAdmin", "Actions:2")
对应razor 文件 MockUserLoginButtons.cshtml
主要是 DisplayDriver 和 Location方法, 第一个 决定渲染类型,第二个参数决定渲染在页面上的位置
我的这个场景可以认时当在Admin后台渲染User类型的列表时(SummaryAdmin ) 将此代码注入到 Actions 位置的 第二个位置

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章