umbrella-blazor-scaffold-index-page
Scaffold a Blazor index/listing page (.razor + .razor.cs) for a feature, following the Umbrella UmbrellaGrid pattern with breadcrumb, auth policy, and action column.
SKILL.md.ai-shared/skills/umbrella-blazor-scaffold-index-page/SKILL.md
GitHubScaffold Blazor Index Page
Purpose
Add a Blazor index page that renders a paginated, sortable, filterable grid for a feature. The page uses UmbrellaGrid via the project-specific <AppName>RemoteDataAccessGridComponentBase base class, which handles all data fetching, sorting, filtering, and delete wiring automatically.
Prerequisite: A client data service interface (I<Name>Service) must exist — either from umbrella-dotnet-scaffold-api-data-service-controller + umbrella-dotnet-scaffold-client-data, or from umbrella-dotnet-rename-client-repository-to-service. The slim model (Slim<Name>Model) and paginated result model must also exist. The slim record must have a record-level [Display(Name = "<Friendly Singular>")] attribute so inherited grid actions and dialogs use the entity's friendly name.
Discovery (read these before writing anything)
- Read 2–3 existing index pages under
Web\<AppName>.Web.Client\Pages\Admin\to confirm folder naming conventions, route patterns, breadcrumb usage, grid column patterns, and action column structure. - Confirm the project-specific grid component base class name (e.g.
IndyRecordsRemoteDataAccessGridComponentBase). - Read
Web\<AppName>.Web.Shared\Security\Policies\<AppName>PolicyNames.csorSharedPolicyNames.csfor the correct auth policy constant. - Confirm the feature's index route (e.g.
/admin/industries) and the manage route (e.g./admin/industries/manage) by checking an analogous existing feature. - Check whether the target feature folder needs a local
_Imports.razorfor its model namespace or other feature-specific imports. Reuse the nearest page-folder pattern; Razor markup does not automatically inherit code-behindusingdirectives. - Confirm the manage page already exists or is being scaffolded in the same feature workflow before adding Create/Edit links. Do not leave permanent navigation to a route that is not implemented.
- Confirm
Slim<Name>Modeldeclares the friendly singular record name with[Display(Name = "<Friendly Singular>")]; add it throughumbrella-dotnet-scaffold-api-server-modelsif missing.
Request failure handling
The grid base owns the request lifecycle in the minimal scaffold below. If the page adds custom loading or other request handlers, read Blazor request failures and component cancellation. Use the inherited lifetime token, distinguish navigation cancellation from timeouts, and complete failed loads with an error view and retry. Follow the installed base implementation before adding custom handlers.
Step 1 -- Create the folder
Folder: Web\<AppName>.Web.Client\Pages\Admin\<Name>Management\
Step 2 -- Create Index.razor
File: Web\<AppName>.Web.Client\Pages\Admin\<Name>Management\Index.razor
@inherits IndexBase
@page "/admin/<route-plural>"
@{
string title = "Manage <Names>";
}
<<AppName>PageTitle>@title</<AppName>PageTitle>
<UmbrellaBreadcrumb>
<UmbrellaBreadcrumbItem Name="@title" />
</UmbrellaBreadcrumb>
<div class="listing-page">
<div class="listing-page__header">
<h1>@title</h1>
<div>
<a class="btn btn-primary" href="/admin/<route-plural>/manage">Create <i class="fas fa-plus-circle"></i></a>
</div>
</div>
<UmbrellaGrid @ref="GridInstance" TItem="Slim<Name>Model" InitialSortProperty="x => x.CreatedDateUtc" OnDataRequestedAsync="OnGridDataRequestAsync">
<Columns>
<UmbrellaColumn Property="x => x.CreatedDateUtc" Sortable="true">@context.CreatedDateUtc.ToString("d")</UmbrellaColumn>
<UmbrellaColumn Property="x => x.Name" Sortable="true" Filterable="true" />
<UmbrellaActionsColumn>
<a class="btn btn-primary btn-sm" href="/admin/<route-plural>/manage/@context.Id" title="Edit">
<i class="fas fa-edit" aria-hidden="true"></i>
</a>
<button class="btn btn-danger btn-sm" @onclick="_ => DeleteItemClickAsync(context)" title="Delete">
<i class="fas fa-trash" aria-hidden="true"></i>
</button>
</UmbrellaActionsColumn>
</Columns>
</UmbrellaGrid>
</div>
Rules:
@inherits IndexBaseonly — no@pagelogic or C# in the.razorfile beyond@{...}for local variables.- Route uses lowercase, hyphenated, plural form:
/admin/career-quiz-questions,/admin/industries. InitialSortPropertydefaults tox => x.CreatedDateUtc— change to a more meaningful property if the entity doesn't have a creation date or if another sort makes more sense for the feature.- Columns: always include
CreatedDateUtcfirst (if available), then any key display fields. Check theSlim<Name>Modelproperties to know what's available. UmbrellaActionsColumn: include an Edit link when the manage route exists. Include a View link only if a public-facing detail page exists. Include Delete only after confirming the service exposes delete, the controller endpoint is enabled, and the selected policy/resource-authorization behavior permits it; if those signals disagree, omit the action and ask.- A "Create" button in the header links to the manage page route with no ID segment.
Optional: public view link in actions column
If a public detail page exists for this entity:
<a class="btn btn-secondary btn-sm" href="/<public-route>/@context.Id" title="View">
<i class="fas fa-eye" aria-hidden="true"></i>
</a>
Step 3 -- Create Index.razor.cs
File: Web\<AppName>.Web.Client\Pages\Admin\<Name>Management\Index.razor.cs
using <AppName>.Web.Client.Data.Services.Abstractions;
using <AppName>.Web.Shared.Models.Api.<Feature>;
namespace <AppName>.Web.Client.Pages.Admin.<Name>Management;
[Authorize(<AppName>PolicyNames.<Policy>)]
public abstract class IndexBase : <AppName>RemoteDataAccessGridComponentBase<Slim<Name>Model, int, PaginatedResultModel<Slim<Name>Model>, I<Name>Service>;
Rules:
public abstract class— the.razorfile inherits from it via@inherits.[Authorize(PolicyName)]on the class, not in the.razorfile.- The four generic type params match the service interface's
TSlimModel,TIdentifier,TPaginatedResultModel, and the service interface itself. - No body needed — the base class provides
GridInstance,OnGridDataRequestAsync, andDeleteItemClickAsyncautomatically. - No SCSS file unless the feature requires custom page-level styles. Check existing pages — most have none.
If discovery showed that sibling feature folders use a local _Imports.razor, add or update it with the feature model namespace required by the markup. Keep it limited to imports actually needed by pages in that folder.
Verification
- The
.razorfile contains only@inherits IndexBase,@page, optional@{...}variable blocks, and HTML/component markup — no C# logic. - The code-behind is
public abstract classwith[Authorize]and the correct 4 generic params. - The route is lowercase, hyphenated, and plural.
- The grid
TItemmatches the slim model used in the base class generic params. DeleteItemClickAsyncis called on the delete button — not a custom method.- The "Create" button href matches the manage page's create route.
- Any model-bound Dynamic Image usage passes the matching version token and uses only catalog-discoverable static variant inputs.
- Read
.ai-shared\bundles\umbrella\analyzer-compatibility.mdand build with the installed analyzers enabled. - Any required feature-local
_Imports.razorexists, and Create/Edit links resolve to an implemented or concurrently scaffolded manage route. Slim<Name>Modelhas a record-level[Display(Name = "<Friendly Singular>")]attribute for friendly inherited UI text.- Custom request handlers, if added, follow the shared cancellation/error guidance and verify the relevant timeout, retry, disposal, and missing-resource behaviours. Keep the minimal grid scaffold delegated to its framework base.