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 viaIDemoThingService, coordinates child components.Components/— reusable child UI components such asSearchDemoThings,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:
- Customizing Blazor UI —
.razorpages, components, and layouts (ReplaceBaseComponent). - Customizing Razor UI —
.cshtmlpath overrides and Razor UI layouts. - UI components — paths, types, and embedded source for each shared item.
- CSS and JavaScript — Branding and static assets.