BookStore示例项目---菜单栏UI分析

部署

参照 ABP示例项目BookStore搭建部署

项目解构

1)、动态脚本代理

启动项目时,默认会调用两个接口

/Abp/ApplicationConfigurationScript
/Abp/ServiceProxyScript

ServiceProxyScript会解析项目路由,动态生成api路径。此两个接口封装在了 Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic 程序集中。一旦引用该程序集便会自动调用接口。

1.1)、虚拟文件系统

说到虚拟文件系统,先要了解嵌入资源文件。简而言之,就是以程序调用的形式访问文件。对于虚拟文件系统的了解,可以参考:

基于ASP.NET Core的模块化设计: 虚拟文件系统

ABP虚拟文件系统(VirtualFileSystem)实例------定制菜单栏显示用户姓名

1.2)、小结

上面说到的动态脚本代理是如何调用的?在模块 Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared 中有一类cshtml,它是嵌入式资源文件,以Page\Account文件夹下_ViewStart.cshtml为例:

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

在这里调用 Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic 中的GetApplicationLayout方法:

public virtual string GetLayout(string name, bool fallbackToDefault = true)
{
    switch (name)
    {
        case StandardLayouts.Application:
            return "~/Themes/Basic/Layouts/Application.cshtml";
        case StandardLayouts.Account:
            return "~/Themes/Basic/Layouts/Account.cshtml";
        case StandardLayouts.Empty:
            return "~/Themes/Basic/Layouts/Empty.cshtml";
        default:
            return fallbackToDefault ? "~/Themes/Basic/Layouts/Application.cshtml" : null;
    }
}

而这三个cshtml视图文件都包含了这么一段脚本:

<script src="~/Abp/ApplicationConfigurationScript"></script>
<script src="~/Abp/ServiceProxyScript"></script>

如此便调用了后端方法生成动态脚本,同时我们可以改造这里的视图,用来定制网站的菜单栏等UI界面。

2)、UI界面菜单栏分析

2.1)、ABP UI界面单测项目分析

ABP简单菜单栏分析,项目源码: https://github.com/abpframework/abp/tree/dev/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo

如图:

由上面得知,开始调用layout下的视图文件,用以加载动态js代理,但是同时还会去渲染菜单导航栏。

<body class="abp-application-layout bg-light">
    @await Component.InvokeLayoutHookAsync(LayoutHooks.Body.First, StandardLayouts.Application)

    @(await Component.InvokeAsync<MainNavbarViewComponent>())

    <div class="@containerClass">
        @(await Component.InvokeAsync<PageAlertsViewComponent>())
        <div id="AbpContentToolbar">
            <div class="text-right mb-2">
                @RenderSection("content_toolbar", false)
            </div>
        </div>
        @RenderBody()
    </div>

    <abp-script-bundle name="@BasicThemeBundles.Scripts.Global" />

    <script src="~/Abp/ApplicationConfigurationScript"></script>
    <script src="~/Abp/ServiceProxyScript"></script>

    @await Component.InvokeAsync(typeof(WidgetScriptsViewComponent))

    @await RenderSectionAsync("scripts", false)

    @await Component.InvokeLayoutHookAsync(LayoutHooks.Body.Last, StandardLayouts.Application)
</body>

MainNavbarViewComponent类会加载一个视图,此视图渲染整个导航栏。

<nav class="navbar navbar-expand-md navbar-dark bg-dark shadow-sm flex-column flex-md-row mb-4" id="main-navbar" style="min-height: 4rem;">
    <div class="container">
        @(await Component.InvokeAsync<MainNavbarBrandViewComponent>())
        <button class="navbar-toggler" type="button" data-toggle="collapse"
                data-target="#main-navbar-collapse" aria-controls="main-navbar-collapse"
                aria-expanded="false" aria-label="Toggle navigation">
            <span class="navbar-toggler-icon"></span>
        </button>
        <div class="collapse navbar-collapse" id="main-navbar-collapse">
            <ul class="navbar-nav mx-auto">
                @(await Component.InvokeAsync<MainNavbarMenuViewComponent>())
            </ul> 
            <ul class="navbar-nav"> 
                @(await Component.InvokeAsync<MainNavbarToolbarViewComponent>())
            </ul>
        </div>
    </div>
</nav>

2.2)、BookStore示例项目应用的UI扩展点

在上面的代码中,涉及到了两个类: MainNavbarBrandViewComponentMainNavbarMenuViewComponent 。如此这里便有两个扩展点,首先就是 IBrandingProvider 接口。在 MainNavbarBrandViewComponent 源码中会这么调用该接口:

@using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Components
@inject IBrandingProvider BrandingProvider
<a class="navbar-brand" href="~/">@BrandingProvider.AppName</a>

ABP源码有一个继承自该接口的默认类:

public class DefaultBrandingProvider : IBrandingProvider, ITransientDependency
{
    public virtual string AppName => "MyApplication";

    public virtual string LogoUrl => null;
}

BookStore项目中的扩展点:

namespace Acme.BookStore.Web
{
    [Dependency(ReplaceServices = true)]
    public class BookStoreBrandingProvider : DefaultBrandingProvider
    {
        public override string AppName => "BookStore";
    }
}

MainNavbarMenuViewComponent 类源码中会调用一个视图:

@using Volo.Abp.UI.Navigation
@model ApplicationMenu
@foreach (var menuItem in Model.Items)
{
    var elementId = string.IsNullOrEmpty(menuItem.ElementId) ? string.Empty : $"id=\"{menuItem.ElementId}\"";
    var cssClass = string.IsNullOrEmpty(menuItem.CssClass) ? string.Empty : menuItem.CssClass;
    var disabled = menuItem.IsDisabled ? "disabled" : string.Empty;
    if (menuItem.IsLeaf)
    {
        if (menuItem.Url != null)
        {
            <li class="nav-item @cssClass @disabled" @elementId>
                <a class="nav-link" href="@(menuItem.Url ?? "#")">
                    @if (menuItem.Icon != null)
                    {
                        if (menuItem.Icon.StartsWith("fa"))
                        {
                            <i class="@menuItem.Icon"></i>
                        }
                    }
                    @menuItem.DisplayName
                </a>
            </li>
        }
    }
    else
    {
        <li class="nav-item">
            <div class="dropdown">
                <a class="nav-link dropdown-toggle" href="#" id="Menu_@(menuItem.Name)" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
                    @if (menuItem.Icon != null)
                    {
                        if (menuItem.Icon.StartsWith("fa"))
                        {
                            <i class="@menuItem.Icon"></i>
                        }
                    }
                    @menuItem.DisplayName
                </a>
                <div class="dropdown-menu border-0 shadow-sm" aria-labelledby="Menu_@(menuItem.Name)">
                    @foreach (var childMenuItem in menuItem.Items)
                    {
                        @await Html.PartialAsync("~/Themes/Basic/Components/Menu/_MenuItem.cshtml", childMenuItem)
                    }
                </div>
            </div>
        </li>
    }
}

在这里就会显示菜单栏及其子菜单。那么这么的扩展点在哪里呢?在模块类中有这么一个配置菜单的方法:

Configure<AbpNavigationOptions>(options =>
{
    options.MenuContributors.Add(new DefaultMenuContributor());
});

如果我们可以参考 DefaultMenuContributor 类的实现,扩展自己的菜单。

BookStore示例项目的扩展点:

public class BookStoreMenuContributor : IMenuContributor
{
    public async Task ConfigureMenuAsync(MenuConfigurationContext context)
    {
        if (context.Menu.Name == StandardMenus.Main)
        {
            await ConfigureMainMenuAsync(context);
        }
    }

    // 配置菜单栏的 显示
    private async Task ConfigureMainMenuAsync(MenuConfigurationContext context)
    {
        if (!MultiTenancyConsts.IsEnabled)
        {
            var administration = context.Menu.GetAdministration();
            administration.TryRemoveMenuItem(TenantManagementMenuNames.GroupName);
        }

        var l = context.ServiceProvider.GetRequiredService<IStringLocalizer<BookStoreResource>>();

        context.Menu.Items.Insert(0, new ApplicationMenuItem("BookStore.Home", l["Menu:Home"], "/"));

        context.Menu.AddItem(
            new ApplicationMenuItem("BooksStore", l["Menu:BookStore"])
                .AddItem(new ApplicationMenuItem("BooksStore.Books", l["Menu:Books"], url: "/Books"))
        );
    }
}

3)、菜单栏多语言显示

这是ABP示例项目BookStore的菜单栏,前面两个在上面已经有了描述,而多语言的显示是怎么渲染加载出来的呢?

在ABP的源码中,有多个模块专门处理UI界面。其中,有一个基础的模块,就是我们前面提到的

Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic 模块。在这里处理基本的一些UI主题界面,比如,菜单栏,工具栏等。

namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic
{
    [DependsOn(
        typeof(AbpAspNetCoreMvcUiThemeSharedModule),
        typeof(AbpAspNetCoreMvcUiMultiTenancyModule)
        )]
    public class AbpAspNetCoreMvcUiBasicThemeModule : AbpModule
    {
        public override void ConfigureServices(ServiceConfigurationContext context)
        {
            // 添加基础 主题
            Configure<AbpThemingOptions>(options =>
            {
                options.Themes.Add<BasicTheme>();

                if (options.DefaultThemeName == null)
                {
                    options.DefaultThemeName = BasicTheme.Name;
                }
            });

            // 添加嵌入资源文件
            Configure<AbpVirtualFileSystemOptions>(options =>
            {
                options.FileSets.AddEmbedded<AbpAspNetCoreMvcUiBasicThemeModule>("Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic");
            });

            // 添加工具栏 (多语言)
            Configure<AbpToolbarOptions>(options =>
            {
                options.Contributors.Add(new BasicThemeMainTopToolbarContributor());
            });

            // 样式及脚本捆绑
            Configure<AbpBundlingOptions>(options =>
            {
                options
                    .StyleBundles
                    .Add(BasicThemeBundles.Styles.Global, bundle =>
                    {
                        bundle
                            .AddBaseBundles(StandardBundles.Styles.Global)
                            .AddContributors(typeof(BasicThemeGlobalStyleContributor));
                    });

                options
                    .ScriptBundles
                    .Add(BasicThemeBundles.Scripts.Global, bundle =>
                    {
                        bundle
                            .AddBaseBundles(StandardBundles.Scripts.Global)
                            .AddContributors(typeof(BasicThemeGlobalScriptContributor));
                    });
            });
        }
    }
}

我们看看工具栏的处理类 BasicThemeMainTopToolbarContributor

public class BasicThemeMainTopToolbarContributor : IToolbarContributor
{
    public async Task ConfigureToolbarAsync(IToolbarConfigurationContext context)
    {
        if (context.Toolbar.Name != StandardToolbars.Main)
        {
            return;
        }

        if (!(context.Theme is BasicTheme))
        {
            return;
        }

        var languageProvider = context.ServiceProvider.GetService<ILanguageProvider>();

        //TODO: This duplicates GetLanguages() usage. Can we eleminate this?
        var languages = await languageProvider.GetLanguagesAsync();
        if (languages.Count > 1)
        {
            context.Toolbar.Items.Add(new ToolbarItem(typeof(LanguageSwitchViewComponent)));
        }

        if (context.ServiceProvider.GetRequiredService<ICurrentUser>().IsAuthenticated)
        {
            context.Toolbar.Items.Add(new ToolbarItem(typeof(UserMenuViewComponent)));
        }
    }
}

在这里有一个处理语言转换视图组件 LanguageSwitchViewComponent 和用户菜单视图组件 UserMenuViewComponent 。ILanguageProvider接口有一个默认实现类:

public class DefaultLanguageProvider : ILanguageProvider, ITransientDependency
{
    protected AbpLocalizationOptions Options { get; }

    public DefaultLanguageProvider(IOptions<AbpLocalizationOptions> options)
    {
        Options = options.Value;
    }

    public Task<IReadOnlyList<LanguageInfo>> GetLanguagesAsync()
    {
        return Task.FromResult((IReadOnlyList<LanguageInfo>)Options.Languages);
    }
}

这里的 GetLanguagesAsync 方法直接返回选项类AbpLocalizationOptions的Languages属性。而ABP开放出来的多语言配置接口就是这个属性,我们将多语言添加到这个属性中,ABP就会加载出来所有的多语言。

BookStore项目的扩展:

Configure<AbpLocalizationOptions>(options =>
{
    options.Resources
        .Get<BookStoreResource>()
        .AddBaseTypes(
            typeof(AbpUiResource)
        );

    options.Languages.Add(new LanguageInfo("cs", "cs", "Čeština"));
    options.Languages.Add(new LanguageInfo("en", "en", "English"));
    options.Languages.Add(new LanguageInfo("pt-BR", "pt-BR", "Português"));
    options.Languages.Add(new LanguageInfo("tr", "tr", "Türkçe"));
    options.Languages.Add(new LanguageInfo("zh-Hans", "zh-Hans", "简体中文"));
});

ABP是如何加载渲染出来视图的呢?有这么一个类 LanguageSwitchViewComponent ,这个类在上面也有调用,前提就是要在选项类中添加多语言。源码如下:

public class LanguageSwitchViewComponent : AbpViewComponent
{
    private readonly ILanguageProvider _languageProvider;

    public LanguageSwitchViewComponent(ILanguageProvider languageProvider)
    {
        _languageProvider = languageProvider;
    }

    public async Task<IViewComponentResult> InvokeAsync()
    {
        var languages = await _languageProvider.GetLanguagesAsync();
        var currentLanguage = languages.FindByCulture(
            CultureInfo.CurrentCulture.Name,
            CultureInfo.CurrentUICulture.Name
        );

        var model = new LanguageSwitchViewComponentModel
        {
            CurrentLanguage = currentLanguage,
            OtherLanguages = languages.Where(l => l != currentLanguage).ToList()
        };
        
        return View("~/Themes/Basic/Components/Toolbar/LanguageSwitch/Default.cshtml", model);
    }
}

Default.cshtml视图:

@using System.Linq
@using Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Themes.Basic.Components.Toolbar.LanguageSwitch
@model LanguageSwitchViewComponentModel
@if (Model.OtherLanguages.Any())
{
    <div class="dropdown">
        <a class="nav-link dropdown-toggle" href="#" role="button" id="dropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
            @Model.CurrentLanguage.DisplayName
        </a>

        <div class="dropdown-menu dropdown-menu-right border-0 shadow-sm" aria-labelledby="dropdownMenuLink">
            @foreach (var language in Model.OtherLanguages)
            {
                <a class="dropdown-item" href="/Abp/Languages/Switch?culture=@(language.CultureName)&uiCulture=@(language.UiCultureName)&returnUrl=@Context.Request.Path">@language.DisplayName</a>
            }
        </div>
    </div> 
}

还有一个扩展点,也可以通过 扩展 IToolbarContributor 接口。可以参考 BasicThemeMainTopToolbarContributor 类。

ABP中处理菜单栏视图主要是在 Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic 模块中,涉及的文件如下:

如此,BookStore项目的菜单栏UI便分析完了。

我来评几句
登录后评论

已发表评论数()

相关站点

热门文章