项目

ASP.NET Core (MVC / Razor Pages) 用户界面自定义指南

本文档解释了如何为 ASP.NET Core MVC / Razor Page 应用程序重写所依赖的 应用模块主题 的用户界面。

重写页面

本节涵盖 Razor Pages 开发,这是为 ASP.NET Core 创建服务器渲染用户界面的推荐方法。预构建的模块通常使用 Razor Pages 方法而非经典的 MVC 模式(后续章节也会涵盖 MVC 模式)。

对于一个页面,通常有三种重写需求:

  • 仅重写页面模型 (C#) 部分,以在不更改页面 UI 的情况下执行附加逻辑。
  • 仅重写 Razor 页面 (.cshtml 文件),以在不更改页面背后 C# 代码的情况下更改 UI。
  • 完全重写页面。

重写页面模型 (C#)

using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Identity;
using Volo.Abp.Identity.Web.Pages.Identity.Users;

namespace Acme.BookStore.Web.Pages.Identity.Users
{
    [Dependency(ReplaceServices = true)]
    [ExposeServices(typeof(EditModalModel))]
    public class MyEditModalModel : EditModalModel
    {
        public MyEditModalModel(
            IIdentityUserAppService identityUserAppService,
            IIdentityRoleAppService identityRoleAppService
            ) : base(
                identityUserAppService,
                identityRoleAppService)
        {
        }

        public async override Task<IActionResult> OnPostAsync()
        {
            //TODO: 附加逻辑
            await base.OnPostAsync();
            //TODO: 附加逻辑
        }
    }
}
  • 该类继承并替换了用户的 EditModalModel,并重写了 OnPostAsync 方法,以便在底层代码执行前后执行附加逻辑。
  • 它使用 ExposeServicesDependency 属性来替换该类。

重写 Razor 页面 (.CSHTML)

通过在相同路径下创建相同的 .cshtml 文件,可以重写 .cshtml 文件(Razor 页面、Razor 视图、视图组件等)。

示例

本示例重写了账户模块定义的登录页面 UI。

账户模块在 Pages/Account 文件夹下定义了一个 Login.cshtml 文件。因此,您可以通过在相同路径下创建一个文件来重写它:

overriding-login-cshtml

您通常希望复制模块的原始 .cshtml 文件,然后进行必要的更改。您可以在这里找到原始文件。请不要复制 Login.cshtml.cs 文件,那是 Razor 页面的代码隐藏文件,我们目前还不想重写它(参见下一节)。

如果要重写的页面包含 ABP 标签助手,请记得添加 _ViewImports.cshtml

@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI
@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI.Bootstrap
@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI.Bundling

就是这样,您可以按需更改文件内容。

完全重写 Razor 页面

您可能希望完全重写一个页面;包括与该页面相关的 Razor 文件和 C# 文件。

在这种情况下:

  1. 按照上述方法重写 C# 页面模型类,但不要替换现有的页面模型类。
  2. 按照上述方法重写 Razor 页面,但还需将 @model 指令指向您的新页面模型。

示例

本示例重写了账户模块定义的登录页面

创建一个派生自 LoginModel(定义在 Volo.Abp.Account.Web.Pages.Account 命名空间中)的页面模型类:

如果您使用的是 AbpAccountWebOpenIddictModuleAbpAccountPublicWebOpenIddictModule,则基类是 OpenIddictSupportedLoginModel 而不是 LoginModel。并且您应将 ExposeServices 属性更改为 [ExposeServices(typeof (MyLoginModel), typeof(OpenIddictSupportedLoginModel), typeof(LoginModel))]

[ExposeServices(typeof (MyLoginModel), typeof(LoginModel))]
public class MyLoginModel : LoginModel
{
    public MyLoginModel(
        IAuthenticationSchemeProvider schemeProvider,
        IOptions<AbpAccountOptions> accountOptions
        ) : base(
        schemeProvider,
        accountOptions)
    {

    }

    public override Task<IActionResult> OnPostAsync(string action)
    {
        //TODO: 添加逻辑
        return base.OnPostAsync(action);
    }

    //TODO: 添加新方法和属性...
}

如果需要,您可以重写任何方法或添加新的属性/方法。

Login.cshtml 文件复制到您的解决方案中,如上所述。将 @model 指令更改为指向 MyLoginModel

@page
...
@model Acme.BookStore.Web.Pages.Account.MyLoginModel
...

完成!现在可以在视图中进行任何更改并运行您的应用程序了。

不通过继承替换页面模型

您不必从原始页面模型类继承(如前一示例所示)。相反,您可以完全自行重新实现该页面。在这种情况下,只需从 PageModelAbpPageModel 或任何您需要的合适基类派生即可。

重写视图组件

ABP、预构建的主题和模块定义了一些可重用的视图组件。这些视图组件可以像上面描述的页面一样被替换。

示例

下面的截图取自应用程序启动模板附带的基本主题

bookstore-brand-area-highlighted

基本主题为布局定义了一些视图组件。例如,上图中用红色矩形高亮显示的区域称为品牌组件。您可能希望通过添加自己的应用程序徽标来自定义此组件。让我们看看如何操作。

首先,创建您的徽标并将其放在 Web 应用程序中的某个文件夹下。我们使用了 wwwroot/logos/bookstore-logo.png 路径。然后,从基本主题文件中复制品牌组件的视图(从此处),放在 Themes/Basic/Components/Brand 文件夹下。结果应类似于下图:

bookstore-added-brand-files

然后按需更改 Default.cshtml。示例内容如下:

<a href="/">
    <img src="https://koudingke.oss-cn-hangzhou.aliyuncs.com/docs/abp-docs/zh-Hans/framework/ui/mvc-razor-pages/~/logos/bookstore-logo.png"/>
</a>

现在,您可以运行应用程序查看结果:

bookstore-added-logo

如果需要,您也可以通过依赖注入系统替换组件的背后 C# 代码类

重写主题

如上所述,您可以替换所用主题的任何组件、布局或 C# 类。有关主题系统的更多信息,请参阅主题文档

重写静态资源

重写模块的嵌入式静态资源(如 JavaScript、Css 或图像文件)非常简单。只需在您的解决方案中的相同路径下放置一个文件,然后让虚拟文件系统处理它即可。

操作捆绑包

捆绑与压缩系统提供了一个可扩展的动态系统来创建脚本样式捆绑包。它允许您扩展和操作现有的捆绑包。

示例:添加全局 CSS 文件

例如,ABP 定义了一个全局样式捆绑包,该捆绑包会添加到每个页面(实际上是由主题添加到布局中)。让我们向捆绑包文件的末尾添加一个自定义样式文件,以便我们可以覆盖任何全局样式。

首先,创建一个 CSS 文件,并将其放在 wwwroot 内的一个文件夹中:

bookstore-global-css-file

在文件中定义一些自定义 CSS 规则。示例:

.card-title {
    color: orange;
    font-size: 2em;
    text-decoration: underline;
}

.btn-primary {
    background-color: red;
}

然后,在模块的 ConfigureServices 方法中,将此文件添加到标准全局样式捆绑包:

Configure<AbpBundlingOptions>(options =>
{
    options.StyleBundles.Configure(
        StandardBundles.Styles.Global, //捆绑包名称!
        bundleConfiguration =>
        {
            bundleConfiguration.AddFiles("/styles/my-global-styles.css");
        }
    );
});

全局脚本捆绑包

类似于 StandardBundles.Styles.Global,还有一个 StandardBundles.Scripts.Global,您可以添加文件或操作现有文件。

示例:操作捆绑包文件

上面的示例向捆绑包添加了一个新文件。如果您创建一个捆绑包贡献者类,可以做更多事情。示例:

public class MyGlobalStyleBundleContributor : BundleContributor
{
    public override void ConfigureBundle(BundleConfigurationContext context)
    {
        context.Files.Clear();
        context.Files.Add("/styles/my-global-styles.css");
    }
}

然后您可以将贡献者添加到现有的捆绑包:

Configure<AbpBundlingOptions>(options =>
{
    options.StyleBundles.Configure(
        StandardBundles.Styles.Global,
        bundleConfiguration =>
        {
            bundleConfiguration.AddContributors(typeof(MyGlobalStyleBundleContributor));
        }
    );
});

清除所有 CSS 文件并不是一个好主意。在实际场景中,您可以找到特定文件并用您自己的文件替换它。

示例:为特定页面添加 JavaScript 文件

上面的示例适用于添加到布局的全局捆绑包。如果您想为依赖模块内定义的特定页面添加(或替换)CSS/JavaScript 文件该怎么办?

假设您希望在用户进入 Identity 模块的角色管理页面时运行一段 JavaScript 代码

首先,在 wwwrootPagesViews 文件夹下创建一个标准的 JavaScript 文件(ABP 默认支持在这些文件夹内添加静态资源)。我们倾向于使用 Pages/Identity/Roles 文件夹以遵循约定:

bookstore-added-role-js-file

文件内容很简单:

$(function() {
    abp.log.info('My custom role script file has been loaded!');
});

然后将此文件添加到角色管理页面的捆绑包:

Configure<AbpBundlingOptions>(options =>
{
    options.ScriptBundles
        .Configure(
            typeof(Volo.Abp.Identity.Web.Pages.Identity.Roles.IndexModel).FullName,
            bundleConfig =>
            {
                bundleConfig.AddFiles("/Pages/Identity/Roles/my-role-script.js");
            });
});

typeof(Volo.Abp.Identity.Web.Pages.Identity.Roles.IndexModel).FullName 是安全获取角色管理页面捆绑包名称的方法。

注意,并非每个页面都定义此类页面捆绑包。它们仅在需要时定义。

除了向页面添加新的 CSS/JavaScript 文件外,您还可以替换现有的文件(通过定义捆绑包贡献者)。

布局自定义

布局在设计中是由主题定义的(参见主题化)。它们不包含在下载的应用程序解决方案中。这样,您可以轻松地升级主题并获得新功能。除非您用自己的布局替换(将在下一节中说明),否则您无法在应用程序中直接更改布局代码。

有一些自定义布局的常见方法,将在后续章节中描述。

菜单贡献者

ABP 定义了两个标准菜单

bookstore-menus-highlighted

  • StandardMenus.Main: 应用程序的主菜单。
  • StandardMenus.User: 用户菜单(通常位于屏幕右上角)。

渲染菜单是主题的职责,但菜单项由模块和您的应用程序代码决定。只需实现 IMenuContributor 接口,并在 ConfigureMenuAsync 方法中操作菜单项

菜单贡献者会在需要渲染菜单时执行。应用程序启动模板中已经定义了一个菜单贡献者,您可以将其作为示例并根据需要进行改进。更多信息请参阅导航菜单文档。

工具栏贡献者

工具栏系统用于在用户界面上定义工具栏。模块(或您的应用程序)可以向工具栏添加,然后主题在布局上渲染工具栏。

只有一个标准工具栏(名为“Main” - 定义为常量:StandardToolbars.Main)。对于基本主题,其渲染效果如下: bookstore-toolbar-highlighted

在上面的截图中,有两个项目被添加到了主工具栏:语言切换组件和用户菜单。您可以在此处添加自己的项目。

示例:添加通知图标

在本示例中,我们将在语言切换项目的左侧添加一个通知(铃铛)图标。工具栏中的项应该是一个视图组件。因此,首先在您的项目中创建一个新的视图组件:

bookstore-notification-view-component

NotificationViewComponent.cs

public class NotificationViewComponent : AbpViewComponent
{
    public async Task<IViewComponentResult> InvokeAsync()
    {
        return View("/Pages/Shared/Components/Notification/Default.cshtml");
    }
}

Default.cshtml

<div id="MainNotificationIcon" style="color: white; margin: 8px;">
    <i class="far fa-bell"></i>
</div>

现在,我们可以创建一个实现 IToolbarContributor 接口的类:

public class MyToolbarContributor : IToolbarContributor
{
    public Task ConfigureToolbarAsync(IToolbarConfigurationContext context)
    {
        if (context.Toolbar.Name == StandardToolbars.Main)
        {
            context.Toolbar.Items
                .Insert(0, new ToolbarItem(typeof(NotificationViewComponent)));
        }

        return Task.CompletedTask;
    }
}

该类将 NotificationViewComponent 作为第一个项添加到 Main 工具栏中。

最后,您需要在模块的 ConfigureServices 中,将此贡献者添加到 AbpToolbarOptions

Configure<AbpToolbarOptions>(options =>
{
    options.Contributors.Add(new MyToolbarContributor());
});

完成,运行应用程序时,您将在工具栏上看到通知图标:

bookstore-notification-icon-on-toolbar

此示例中的 NotificationViewComponent 只是返回一个视图而不带任何数据。在实际应用中,您可能希望查询数据库(或调用 HTTP API)以获取通知并传递给视图。如果需要,可以为您的工具栏项添加 JavaScriptCSS 文件到全局捆绑包(如前所述)。

有关工具栏系统的更多信息,请参阅工具栏文档

布局钩子

布局钩子系统允许您在布局的某些特定部分添加代码。所有主题的所有布局都应实现这些钩子。然后,您可以将视图组件添加到钩子点。

示例:添加谷歌分析脚本

假设您需要向布局添加谷歌分析脚本(该脚本将对所有页面可用)。首先,在您的项目中创建一个视图组件

bookstore-google-analytics-view-component

GoogleAnalyticsViewComponent.cs

public class GoogleAnalyticsViewComponent : AbpViewComponent
{
    public IViewComponentResult Invoke()
    {
        return View("/Pages/Shared/Components/GoogleAnalytics/Default.cshtml");
    }
}

Default.cshtml

<script>
    (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
            (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
            m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
    })(window,document,'script','//www.google-analytics.com/analytics.js','ga');

    ga('create', 'UA-xxxxxx-1', 'auto');
    ga('send', 'pageview');
</script>

UA-xxxxxx-1 替换为您自己的代码。

然后,您可以在模块的 ConfigureServices 中,将此组件添加到任何钩子点:

Configure<AbpLayoutHookOptions>(options =>
{
    options.Add(
        LayoutHooks.Head.Last, //钩子名称
        typeof(GoogleAnalyticsViewComponent) //要添加的组件
    );
});

现在,GA 代码将作为最后一项插入到页面的 head 中。您(或您使用的模块)可以向同一钩子添加多个项。所有这些项都将被添加到布局中。

上面的配置将 GoogleAnalyticsViewComponent 添加到了所有布局。您可能希望仅添加到特定布局:

Configure<AbpLayoutHookOptions>(options =>
{
    options.Add(
        LayoutHooks.Head.Last,
        typeof(GoogleAnalyticsViewComponent),
        layout: StandardLayouts.Application //设置要添加的布局
    );
});

有关布局系统的更多信息,请参阅下面的布局部分。

布局

布局系统允许主题定义标准的、命名的布局,并允许任何页面为其目的选择合适的布局。有三个预定义的布局:

  • "Application": 应用程序的主布局(也是默认布局)。它通常包含页眉、菜单(侧边栏)、页脚、工具栏等。
  • "Account": 此布局用于登录、注册和其他类似页面。默认情况下,用于 /Pages/Account 文件夹下的页面。
  • "Empty": 空的、最小化的布局。

这些名称在 StandardLayouts 类中定义为常量。您当然可以创建自己的布局,但这些都是标准的布局名称,并且所有主题都开箱即用地实现了它们。

布局位置

您可以在这里找到基本主题的布局文件。您可以将它们作为参考来构建自己的布局,或者在必要时重写它们。

ITheme

ABP 使用 ITheme 服务来按布局名称获取布局位置。您可以替换此服务以动态选择布局位置。

IThemeManager

IThemeManager 用于获取当前主题并获取布局路径。任何页面都可以确定自己的布局。示例:

@using Volo.Abp.AspNetCore.Mvc.UI.Theming
@inject IThemeManager ThemeManager
@{
    Layout = ThemeManager.CurrentTheme.GetLayout(StandardLayouts.Empty);
}

此页面将使用空布局。您可以使用 ThemeManager.CurrentTheme.GetEmptyLayout(); 扩展方法作为快捷方式。

如果希望为特定文件夹下的所有页面设置布局,则可以在该文件夹下的 _ViewStart.cshtml 文件中编写上述代码。

在本文档中