CRMSeptember 2026 · 5 min read

Building a CRM Interface with Blazor and MudBlazor

By go2ismail · Published · .NET 10

At a glance

Blazor Server pages switch between table, create, update, and read-only views. MudBlazor forms bind request DTOs, load lookup options from the API, validate per field, and call a typed service that reports success through snackbars.

Host the pipeline in a tab page

The CRM interface in Indotalent’s Blazor CRM Source Code is a Blazor Server application styled with MudBlazor 9. PipelinePage.razor at Features/Pipeline/ renders a MudTabs shell whose panels open the eight pipeline modules. The active tab is reflected in the query string, and the page itself is protected with [Authorize] for the Admin and Member roles.

Each module page sits at its own route, such as LeadPage.razor at /pipeline/lead. Because the app hosts Razor pages from the /Features root, the route and the folder stay close to each other.

Keep one page and switch views

LeadPage does not navigate to separate create or edit routes. It keeps a small view-state enum and swaps child components:

private enum ViewMode { Table, Create, Update, View }
private ViewMode _currentView = ViewMode.Table;

private void ShowUpdate(UpdateLeadRequest data, bool isReadOnly)
{
    _selectedData = data;
    _currentView = isReadOnly ? ViewMode.View : ViewMode.Update;
}

Child forms communicate only through EventCallback OnCancel and EventCallback OnSuccess. After a successful save the page returns to the table state, and the table reloads its data itself in OnInitializedAsync. The same pattern repeats for campaigns, budgets, expenses, contacts, activities, teams, and representatives.

Bind the create form to MudBlazor

_LeadCreateForm.razor binds a CreateLeadRequest instance to MudBlazor inputs inside a MudForm. Text fields use MudTextField with @bind-Value, and enums and lookups use MudSelect. Every validated field points to a FluentValidation helper:

<MudTextField @bind-Value="_model.Title"
              For="@(() => _model.Title)"
              Validation="@(_validator.ValidateValue())"
              Variant="Variant.Outlined" />

The validator exposes ValidateValue(), a per-property validation function, so MudBlazor can validate a single field as the user leaves it and validate the whole form on submit. This keeps the rules defined once in the validator and reused by the MudForm.

Fill dropdowns from a lookup service

On initialization the form calls the lookup endpoint and fills its dropdowns:

var res = await LeadService.GetLeadLookupAsync();
if (res != null && res.IsSuccess) _lookup = res.Value ?? new();

LeadLookupResponse carries campaigns, sales teams, pipeline stages, and closing statuses. The form renders a MudSelectItem for each campaign and team and for each enum value of stage and status. The update form loads the same lookups and also copies the selected row into its model, including the audit fields so they survive the round trip.

Submit through a typed service

Submitting calls the typed LeadService, which builds a RestSharp request and executes it through the shared BaseService. Create posts the DTO as JSON to api/lead; update posts to api/lead/update. The client never talks to EF Core or to the database directly, which means the same API contract stays available to other clients.

var res = await LeadService.CreateLeadAsync(_model);
if (res != null && res.IsSuccess)
{
    Snackbar.Add("Created successfully", Severity.Success);
    await OnSuccess.InvokeAsync();
}

Show progress and feedback

Each form guards against double submission with a processing flag that is raised before the request and cleared in a finally block. The button and inputs react to that state while the HTTP call is in flight. Feedback comes from the MudBlazor Snackbar: a success message after a successful create or update, and error details surfaced by BaseService when the response envelope reports IsSuccess = false. The service also intercepts 401 responses, tries the refresh-token endpoint, and redirects to the login page when the session cannot be renewed.

Load, search, and page the data table

_LeadDataTable.razor loads the full lead list through GetLeadListAsync and manages search, sorting, and paging on the client. A refresh flag and StateHasChanged update the grid while the list is reloaded, and the table shows stage chips and closing status for each row. Selecting a row enables the Edit and View toolbar actions, which fetch the full detail with GetLeadByIdAsync before handing an UpdateLeadRequest to the page.

Confirm deletes in a dialog

Deleting opens the shared _DeleteConfirmation MudDialog with the selected lead title as context. Only a confirmed dialog result calls DeleteLeadByIdAsync, and on success the table reloads and shows a snackbar. This pattern keeps destructive actions explicit without adding per-page confirmation markup.

Extend the UI flow without breaking it

To add a field, update the request DTO, add a MudBlazor input bound to it, and keep the validator rule. For a lookup change, adjust the lookup handler response and the form’s select. The surrounding flow, from view switching to service calls to snackbar feedback, stays untouched. Trace the full request in CRM Vertical Slice Architecture, and compare the service contract in Blazor CRM REST API Design. Start from the complete application on the Blazor CRM product page rather than a fragment of a demo. See this architecture implemented in a complete Blazor CRM application.