Table of Contents

UI

Where your feature UI lives

The SaaS template splits front-end work across the WebApp and AdminPanel hosts. Add Blazor UI components and UI pages (.razor) or, where that host uses them, Razor UI pages (.cshtml) under the areas that match your audience:

Area Template path Typical use
Admin panel src/2-Clients/AdminPanel/Pages Staff-only management UI pages
User panel src/2-Clients/WebApp/Pages/UserPanel Signed-in customer / tenant user experience inside WebApp
Public website src/2-Clients/WebApp/Pages/Website Marketing and anonymous-first content in WebApp

Create a folder per feature (like DemoThings) with a routable UI page and optional Components subfolder for child UI components. Use @page "/your-route" on Blazor UI pages and align routes with your menus or links.

How UI talks to your backend

UI pages should depend on your application services (for example IDemoThingService), not on use-case handler types or repositories. Call async service methods, inspect Result.IsSuccess, use Result.Value for data binding, and show Result.Errors with the same patterns the template uses (for example ShowErrorToast).

Register common injections in the host _Imports.razor (see the admin panel example below) or use @inject on individual UI components.

Example: DemoThings in the admin panel

The admin DemoThings feature under AdminPanel/Pages/DemoThings demonstrates search, table listing, pagination, create/update flows, and permission-gated access:

  • Index.razor — route, [Authorize(Permissions...)], loads data via IDemoThingService, coordinates child components.
  • Components/ — reusable child UI components such as SearchDemoThings, DemoThings, CreateDemoThing, UpdateDemoThing.

The list UI page is implemented in Index.razor.

@page "/demo-things"
@using CanBeYours.AdminPanel.Helpers
@using CanBeYours.Application.Dtos.DemoThings
@using CanBeYours.Application.Helpers
@implements IDisposable
@attribute [Authorize(Permissions.Demo.DEMO_THINGS)]

<PageTitle>@AdminPanelLocalizer[AdminPanelResource.DemoThings]</PageTitle>

<h1 class="page-title">
    @AdminPanelLocalizer[AdminPanelResource.DemoThings]
</h1>

<div class="alert alert-info">
    @AdminPanelLocalizer[AdminPanelResource.DemoImplementationInfo]
    <a href="https://docs.codeblock.dev/" target="_blank" rel="noopener noreferrer">@AdminPanelLocalizer[AdminPanelResource.SeeDocs]</a>.
</div>

<SearchDemoThings SearchChangedCallback="OnSearchChanged" />

@if (IsLoading)
{
    <ComponentLoading />
}
else
{
    <div class="fade-in-animation">
        <DemoThings Model="@SearchDemoThingsOutputDto.Items" />
        <Pagination RecordsPerPage="@SearchDemoThingsInputDto.RecordsPerPage" TotalRecords="@SearchDemoThingsOutputDto.TotalRecords" CurrentPage="@SearchDemoThingsInputDto.PageNumber" PageChangedCallback="OnPageChanged" />
    </div>
}

@code {
    protected SearchDemoThingsInputDto SearchDemoThingsInputDto = new();
    protected SearchOutputDto<GetDemoThingDto> SearchDemoThingsOutputDto = new();
    protected bool IsLoading = true;

    protected override async Task OnInitializedAsync()
    {
        MessageService.OnMessage += HandleReceivedMessage;
        await GetDemoThings();
    }

    protected virtual async Task GetDemoThings()
    {
        var result = await DemoThingService.SearchDemoThings(SearchDemoThingsInputDto);

        if (result.IsSuccess)
        {
            SearchDemoThingsOutputDto = result.Value;
        }
        else
        {
            result.ShowErrorToast(ToastService);
        }

        IsLoading = false;
        StateHasChanged();
    }

    protected virtual async Task OnPageChanged(int pageNumber)
    {
        IsLoading = true;
        StateHasChanged();
        SearchDemoThingsInputDto.PageNumber = pageNumber;
        await GetDemoThings();
    }

    protected virtual async Task OnSearchChanged(SearchDemoThingsInputDto searchDemoThingsInputDto)
    {
        IsLoading = true;
        StateHasChanged();
        SearchDemoThingsInputDto = searchDemoThingsInputDto;
        await GetDemoThings();
    }

    protected virtual async void HandleReceivedMessage(string messageKey)
    {
        if (messageKey==Constants.DEMO_THING_CREATED || messageKey==Constants.DEMO_THING_UPDATED)
        {
            IsLoading = true;
            StateHasChanged();
            await GetDemoThings();
        }
    }

    public void Dispose()
    {
        MessageService.OnMessage -= HandleReceivedMessage;
    }
} 

The admin app injects IDemoThingService for all UI pages in AdminPanel/_Imports.razor. Mirror that pattern for IYourFeatureService when you add a new feature.

Apply the same structure under WebApp/Pages/UserPanel or WebApp/Pages/Website: new folder, routable entry UI page, child UI components, and calls into the same application services you already registered for your feature.

Example: UI using a pre-built module (subscription)

Your Blazor UI pages can also inject DevKit module services directly when the UI page needs module behavior (not only data from your own application services). For example, the subscription module exposes ISubscriptionService so you can check whether the current user has an active subscription before showing content.

See Dependency injection for how DevKit registers module services and how the same interfaces are used from use cases.

SubscribedUsersOnly.razor is a minimal admin UI page: it injects ISubscriptionService, calls UserHasAnyActiveSubscription with CurrentUser.GetUserId(), and branches the markup on Result success and the boolean Value.

@page "/subscribedusers-only"
@using CodeBlock.DevKit.Subscription.Services.Subscriptions
@inject ISubscriptionService SubscriptionService

<PageTitle>@AdminPanelLocalizer[AdminPanelResource.SubscribedUsersOnly]</PageTitle>

<h1 class="page-title">
    @AdminPanelLocalizer[AdminPanelResource.SubscribedUsersOnly]
</h1>

<p class="mb-4 text-muted">
    @AdminPanelLocalizer[AdminPanelResource.SubscribedUsersOnlyInfo]
</p>

@if (UserHasAnyActiveSubscription)
{
    <div class="alert alert-success">
        @AdminPanelLocalizer[AdminPanelResource.ActiveSubscriptionMessage]
    </div>
}
else
{
    <div class="alert alert-danger">
        @AdminPanelLocalizer[AdminPanelResource.NoActiveSubscriptionMessage]
        <div class="mt-3">
            <a class="btn btn-success" href="/pricing/demo">@AdminPanelLocalizer[AdminPanelResource.ViewAvailablePlans]</a>
        </div>
    </div>
}

@code {
    private bool UserHasAnyActiveSubscription = false;

    protected override async Task OnInitializedAsync()
    {
        await CheckIfUserHasAnyActiveSubscription();
    }

    private async Task CheckIfUserHasAnyActiveSubscription()
    {
        var result = await SubscriptionService.UserHasAnyActiveSubscription(CurrentUser.GetUserId());

        if (result.IsSuccess)
            UserHasAnyActiveSubscription = result.Value;
        else
            result.ShowErrorToast(ToastService);
    }
}

DevKit web client modules

For host-specific UI layout, navigation, configuration, and built-in behaviors, use the module documentation:

Customization

Beyond adding new UI pages and UI components: