Open Closed

Blazor Server Side Login Page Customization #9283


User avatar
0
imagedeveloper created

How can one remove the input and switch option on the login page and instead have a dropdown list of tenants? I have tried https://github.com/abpframework/abp/blob/dev/framework/src/Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy/Pages/Abp/MultiTenancy/TenantSwitchModal.cshtml but this seems to only override the tenant switch modal.


5 Answer(s)
  • User Avatar
    0
    enisn created
    Support Team .NET Developer

    Hi,

    It's located in in the Account layout, and it's coming from the theme itself. It seems you're useing LeptonX theme, you can override the following page:

    • Create the following file in your project in the exact same folder structure: Themes/LeptonX/Layouts/Account/Default.cshtml
    @using Microsoft.Extensions.Localization
    @using Microsoft.Extensions.Options
    @using Volo.Abp.AspNetCore.Mvc.UI.Theme.LeptonX
    @using Volo.Abp.LeptonX.Shared.Localization;
    @using Volo.Abp.Localization
    @using Volo.Abp.AspNetCore.Mvc.UI.Components.LayoutHook
    @using Volo.Abp.AspNetCore.Mvc.UI.Theme.LeptonX.Bundling
    @using Volo.Abp.AspNetCore.Mvc.UI.Theming
    @using Volo.Abp.AspNetCore.Mvc.UI.Widgets.Components.WidgetScripts
    @using Volo.Abp.AspNetCore.Mvc.UI.Widgets.Components.WidgetStyles
    @using Volo.Abp.Ui.Branding
    @using Volo.Abp.AspNetCore.Mvc.AntiForgery
    @using Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy.Localization
    @using Volo.Abp.AspNetCore.MultiTenancy
    @using Volo.Abp.MultiTenancy
    @using Volo.Abp.AspNetCore.Mvc.UI.Theme.LeptonX.Themes.LeptonX.Components.Common.PageAlerts
    @using Volo.Abp.AspNetCore.Mvc.UI.Theme.LeptonX.Themes.LeptonX.Components.SideMenu.Toolbar.LanguageSwitch
    @using Microsoft.AspNetCore.Http.Extensions
    @using Volo.Abp.Ui.LayoutHooks
    @using Volo.Abp.LeptonX.Shared
    @inject IAbpAntiForgeryManager AbpAntiForgeryManager
    @inject IBrandingProvider BrandingProvider
    @inject IOptions<LeptonXThemeOptions> LeptonXThemeOptions
    @inject IOptions<LeptonXThemeMvcOptions> LeptonXThemeMvcOptions
    @inject LeptonXStyleProvider LeptonXStyleProvider
    @inject IStringLocalizer<AbpUiMultiTenancyResource> MultiTenancyStringLocalizer
    @inject IStringLocalizer<LeptonXResource> L
    @inject ITenantResolveResultAccessor TenantResolveResultAccessor
    @inject IOptions<AbpMultiTenancyOptions> MultiTenancyOptions
    @inject ICurrentTenant CurrentTenant
    @inject ThemeLanguageInfoProvider ThemeLanguageInfoProvider
    @inject Volo.Abp.AspNetCore.Mvc.UI.Layout.IPageLayout PageLayout
    @inject IOptions<AbpThemingOptions> ThemingOptions
    
    @{
        AbpAntiForgeryManager.SetCookie();
        var langDir = CultureHelper.IsRtl ? "rtl" : string.Empty;
        var title = $"{ViewBag.Title ?? PageLayout.Content.Title} | {BrandingProvider.AppName}".Trim('|', ' ');
    
    
        var languageInfo = await ThemeLanguageInfoProvider.GetLanguageSwitchViewComponentModel();
        var returnUrl = System.Net.WebUtility.UrlEncode(Context.Request.GetEncodedPathAndQuery());
    
        var logoUrl = BrandingProvider.LogoUrl == null ? null : "--lpx-logo: url(" + Url.Content(BrandingProvider.LogoUrl.EnsureStartsWith('/').EnsureStartsWith('~')) + ");";
        var logoReverseUrl = BrandingProvider.LogoReverseUrl == null ? null : "--lpx-logo: url(" + Url.Content(BrandingProvider.LogoReverseUrl.EnsureStartsWith('/').EnsureStartsWith('~')) + ");";
        var selectedStyle = await LeptonXStyleProvider.GetSelectedStyleAsync();
    
        var selectedStyleFileName = CultureHelper.IsRtl ? selectedStyle + ".rtl" : selectedStyle;
    
        var accountLayoutBackgroundStyle = LeptonXThemeMvcOptions.Value.AccountLayoutBackgroundStyle ?? $"background-image: url('{Url.Content($"~/LeptonX/images/login-pages/login-bg-img-{selectedStyle}.svg")}') !important;";
    }
    <!DOCTYPE html>
    <html lang="@CultureInfo.CurrentCulture.Name" dir="@langDir">
    
    <head>
    
        @await Component.InvokeLayoutHookAsync(LayoutHooks.Head.First, StandardLayouts.Account)
    
        @if (!ThemingOptions.Value.BaseUrl.IsNullOrWhiteSpace())
        {
            <base href="@ThemingOptions.Value.BaseUrl" />
        }
    
        <title>@title</title>
    
        <meta name="viewport" content="width=device-width,initial-scale=1.0" />
        <meta charset="UTF-8" />
        <meta name="description" content="@ViewBag.MetaDescription">
    
        @if (LeptonXThemeOptions.Value.Favicon != null)
        {
            <link rel="icon" href="@Url.Content(LeptonXThemeOptions.Value.Favicon.Url)" type="@LeptonXThemeOptions.Value.Favicon.Type">
        }
    
        <abp-style-bundle name="@LeptonXThemeBundles.Styles.Global" />
    
        <link href="~/Themes/LeptonX/Global/side-menu/css/bootstrap-@(selectedStyleFileName).css" type="text/css"
              rel="stylesheet" id="lpx-theme-bootstrap-@selectedStyle" />
        <link href="~/Themes/LeptonX/Global/side-menu/css/@(selectedStyleFileName).css" type="text/css" rel="stylesheet"
              id="lpx-theme-color-@selectedStyle" />
    
        @await Component.InvokeAsync(typeof(WidgetStylesViewComponent))
        @await RenderSectionAsync("styles", false)
        @await Component.InvokeLayoutHookAsync(LayoutHooks.Head.Last, StandardLayouts.Account)
    
        <style>
            .lpx-login-bg {
                @Html.Raw(accountLayoutBackgroundStyle)
            }
    
            :root .lpx-theme-light {
                @logoUrl
            }
            :root .lpx-theme-dark {
                @logoReverseUrl
            }
    
            :root .lpx-theme-dim {
                @logoReverseUrl
            }
        </style>
    </head>
    
    <body class="abp-account-layout lpx-theme-@selectedStyle">
    
        @await Component.InvokeLayoutHookAsync(LayoutHooks.Body.First, StandardLayouts.Account)
    
    
        <div class="container-fluid p-0" style="overflow-x: hidden">
            @await Component.InvokeLayoutHookAsync(LayoutHooks.PageContent.First, StandardLayouts.Account)
            <div class="lpx-login-area">
                <div class="lpx-login-bg">
                    <div class="d-flex flex-column justify-content-center min-vh-100">
    
                        <div class="row">
                            <div class="col-xxl-5 col-lg-7 col-md-8 col-11 mx-auto position-relative py-4">
    
                                @if (BrandingProvider.LogoUrl.IsNullOrEmpty())
                                {
                                    <div class="lpx-logo-container lpx-login-brand-text">
                                        <div class="lpx-brand-logo lpx-login-logo mx-auto"></div>
                                        <div class="lpx-brand-name lpx-login-name mx-auto">@BrandingProvider.AppName</div>
                                    </div>
                                }
                                else
                                {
                                    <div class="lpx-brand-logo lpx-login-logo mb-3 mx-auto"></div>
                                }
    
                                <div class="card mx-auto" style="max-width: 30rem;">
                                    <div class="card-body p-3 p-sm-4">
                                        @if (languageInfo.Languages.Count > 1)
                                        {
                                            <div class="align-items-start d-flex justify-content-between mb-2">
                                                <h2 class="lpx-main-title lpx-login-title m-0 me-auto"> @PageLayout.Content.Title @* TODO: Find a better text here. *@</h2>
                                                <div class="dropdown btn-group ms-auto" aria-labelledby="languageDropdown">
    
                                                    <button class="btn btn-sm btn-light dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
                                                        <i class="bi bi-translate me-1"></i> @languageInfo.CurrentLanguage.DisplayName
                                                    </button>
    
                                                    <ul class="dropdown-menu dropdown-menu-end" aria-labelledby="dropdownMenuButton1" style="max-height: 60vh; overflow-y: auto;">
                                                        @foreach (var language in languageInfo.Languages)
                                                        {
                                                            var twoLetterLanguageName = new CultureInfo(language.CultureName).TwoLetterISOLanguageName.ToUpperInvariant();
                                                            var url = Url.Content($"~/Abp/Languages/Switch?culture={language.CultureName}&uiCulture={language.UiCultureName}&returnUrl={returnUrl}");
                                                            <li>
                                                                <a href="@url" class="dropdown-item" data-lpx-language-option="@twoLetterLanguageName">@language.DisplayName / @twoLetterLanguageName</a>
                                                            </li>
                                                        }
                                                    </ul>
                                                </div>
                                            </div>
    
                                            <hr/>
                                        }
    
                                        @await Component.InvokeAsync(typeof(PageAlertsViewComponent))
    
                                        @if (MultiTenancyOptions.Value.IsEnabled &&
                                             (TenantResolveResultAccessor.Result?.AppliedResolvers?.Contains(CookieTenantResolveContributor.ContributorName)
                                              == true ||
                                              TenantResolveResultAccessor.Result?.AppliedResolvers?.Contains(QueryStringTenantResolveContributor.ContributorName)
                                              == true))
                                        {
                                            <div>
                                                <div class="row">
                                                    <div class="col">
                                                        <span style="font-size: .8em;"
                                                              class="text-uppercase text-muted">@MultiTenancyStringLocalizer["Tenant"]</span><br/>
                                                        <h6 class="m-0 d-inline-block">
                                                            @if (CurrentTenant.Id == null)
                                                            {
                                                                <span>
                                                                    @MultiTenancyStringLocalizer["NotSelected"]
                                                                </span>
                                                            }
                                                            else
                                                            {
                                                                <strong>
                                                                    @(CurrentTenant.Name ??
                                                                      CurrentTenant.Id.Value.ToString())
                                                                </strong>
                                                            }
                                                        </h6>
                                                    </div>
                                                    <div class="col-auto">
                                                        <a id="AbpTenantSwitchLink" href="javascript:;"
                                                           class="btn btn-sm btn-outline-primary">@MultiTenancyStringLocalizer["Switch"]</a>
                                                    </div>
                                                </div>
                                            </div>
                                            <hr/>
    
                                        }
    
                                        @RenderBody()
    
                                    </div>
                                    @* @await Html.PartialAsync("~/Themes/LeptonX/Layouts/Account/_Footer.cshtml") *@
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
    
            @await Component.InvokeLayoutHookAsync(LayoutHooks.PageContent.Last, StandardLayouts.Account)
        </div>
    
        <abp-script-bundle name="@LeptonXThemeBundles.Scripts.Global" />
        <script src="~/Abp/ApplicationLocalizationScript?cultureName=@CultureInfo.CurrentUICulture.Name"></script>
        <script type="text/javascript" src="~/Abp/ApplicationConfigurationScript"></script>
        <script type="text/javascript" src="~/Abp/ServiceProxyScript"></script>
        @await Component.InvokeAsync(typeof(WidgetScriptsViewComponent))
        @await RenderSectionAsync("scripts", false)
        @await Component.InvokeLayoutHookAsync(LayoutHooks.Body.Last, StandardLayouts.Account)
    </body>
    
    </html>
    

    This was the original content of the Account layout in v4.1

    You should find the following section:

    <div class="row">
        <div class="col">
            <span style="font-size: .8em;"
                  class="text-uppercase text-muted">@MultiTenancyStringLocalizer["Tenant"]</span><br/>
            <h6 class="m-0 d-inline-block">
                @if (CurrentTenant.Id == null)
                {
                    <span>
                        @MultiTenancyStringLocalizer["NotSelected"]
                    </span>
                }
                else
                {
                    <strong>
                        @(CurrentTenant.Name ??
                          CurrentTenant.Id.Value.ToString())
                    </strong>
                }
            </h6>
        </div>
        <div class="col-auto">
            <a id="AbpTenantSwitchLink" href="javascript:;"
               class="btn btn-sm btn-outline-primary">@MultiTenancyStringLocalizer["Switch"]</a>
        </div>
    </div>
    

    And update it with the following code:

    <div class="row">
        <form method="post" asp-page="/Abp/MultiTenancy/TenantSwitchModal">
                <select name="Input.Name"
                    class="auto-complete-select"
                    data-autocomplete-api-url="/api/app/tenant-lookup"
                    data-autocomplete-display-property="name"
                    data-autocomplete-value-property="name"
                    data-autocomplete-items-property="items"
                    data-autocomplete-filter-param-name="filter"
                    data-autocomplete-allow-clear="true"
                    >
                    @if (CurrentTenant.Id != null)
                    {
                        @* Tenant resolvers works with name, not id. So value must be name. *@
                        <option value="@CurrentTenant.Name">@CurrentTenant.Name</option>
                    }
        </select>
    
         <button type="submit"
                    class="btn btn-sm btn-outline-primary mt-2 w-100">@MultiTenancyStringLocalizer["Switch"]    </button>
        </form>
    </div>
    

    This will look something like that:

    Let's implement lookup endpoint, we cannot use Saas endpoints since they require authorization.

    • Create ITenantLookupAppService.cs in your Application.Contracts layer:
    public interface ITenantLookupAppService : IApplicationService
    {
        Task<PagedResultDto<TenantLookupDto>> GetListAsync(TenantLookupPagedResultRequestDto input);
    }
    
    public class TenantLookupDto
    {
        public Guid Id { get; set; }
        public string Name { get; set; }
    }
    
    public class TenantLookupPagedResultRequestDto : PagedAndSortedResultRequestDto
    {
        public string? Filter { get; set; }
    }
    
    
    
    
    - Create `TenantLookupAppService.cs` in your **Application** layer:
    
    ```cs
    public class TenantLookupAppService : ApplicationService, ITenantLookupAppService
    {
        private readonly ITenantRepository _tenantRepository;
    
        public TenantLookupAppService(ITenantRepository tenantRepository)
        {
            _tenantRepository = tenantRepository;
        }
    
        public async Task&lt;PagedResultDto&lt;TenantLookupDto&gt;> GetListAsync(TenantLookupPagedResultRequestDto input)
        {
            var tenants = await _tenantRepository.GetListAsync(skipCount: input.SkipCount, maxResultCount: input.MaxResultCount, filter: input.Filter);
            var count = await _tenantRepository.GetCountAsync(filter: input.Filter);
            return new PagedResultDto&lt;TenantLookupDto&gt;(count, tenants.Select(t => new TenantLookupDto { Id = t.Id, Name = t.Name }).ToList());
        }
    }
    
    • Now tenants will be listed and searchable here no matter how many you're, this ABP's Auto-Complete-Select implementation supports lazy-loading and searching, so it doesn't load all of them at the same time to the UI.

    As a note: Submitting directly to a SwitchTenantModal endpoint, returns the switchtenant modal when succeeded, you can prevent it by submiting form by javascript instead page-submit directly.

  • User Avatar
    0
    imagedeveloper created

    Thank you so much for the solution

  • User Avatar
    0
    imagedeveloper created

    Do you have a sample of this script.."you can prevent it by submiting form by javascript instead page-submit directly"?

  • User Avatar
    0
    enisn created
    Support Team .NET Developer
    • Create a file named account.js in the same folder alongside Account/Default.cshtml file.

    • Set the content:

    $(function () {
      var $form = $('#tenant-switch-form');
      if ($form.length > 0) {
        $form.on('submit', function (e) {
          e.preventDefault();
          var $this = $(this);
          var formData = $this.serialize();
          var action = $this.attr('action') || window.location.pathname;
          var method = $this.attr('method') || 'post';
          $.ajax({
            url: action,
            type: method.toUpperCase(),
            data: formData,
            success: function (data, status, xhr) {
              // On success, reload the page
              window.location.reload();
            },
            error: function (xhr, status, error) {
              alert('Tenant switch failed: ' + (xhr.responseText || error));
            }
          });
        });
      }
    });
    
    • Add it at the bottom of the page in Account/Default.cshtml
        <script src="~/Themes/LeptonX/Layouts/Account/account.js"></script>
    
    • Add id="tenant-switch-form" to the tenant switch form. It should be looking like that:
     <form id="tenant-switch-form" method="post" asp-page="/Abp/MultiTenancy/TenantSwitchModal">
        <select name="Input.Name"
            class="auto-complete-select"
            <!-- 
            ...
            -->
    
  • User Avatar
    0
    imagedeveloper created

    Works great! Thanks again

Boost Your Development
ABP Live Training
Packages
See Trainings
Mastering ABP Framework Book
The Official Guide
Mastering
ABP Framework
Learn More
Mastering ABP Framework Book
Made with ❤️ on ABP v9.3.0-preview. Updated on June 13, 2025, 11:37