Blazor Component Lifecycle
1. Understanding Component Lifecycle Methods in Blazor
Blazor components have a lifecycle that determines when they are
initialized, rendered, updated, and removed. One commonly used lifecycle
method is OnInitializedAsync(), which is useful when a component needs
to load data as it initializes.
Blazor Component Lifecycle
The Blazor Component Lifecycle describes the different stages a component goes through, from its creation and initialization to rendering, updating, and removal. Blazor provides lifecycle methods such as OnInitialized(), OnInitializedAsync(), OnParametersSet(), and OnAfterRenderAsync() to execute code at specific stages. For example, OnInitializedAsync() is commonly used to fetch data from an API when a component loads, while OnAfterRenderAsync() can be useful for JavaScript interop after rendering. Understanding these methods helps developers manage data loading, UI updates, and component behavior efficiently.
Example:
@page "/lifecycle-demo"
Component Lifecycle Demo
Welcome, @userName!
@code {
private string userName = "Guest";
protected override void OnInitialized() { userName = "Ayush"; } }
Welcome to the Portal
@code {
private string message = "Loading...";
protected override async Task OnInitializedAsync() { await Task.Delay(1000); } }
The component first displays a loading message. After the asynchronous operation finishes, Blazor updates the UI. This lifecycle method is useful when a page needs to retrieve records from an API. ## 2. Understanding Data Binding in Blazor Data binding connects UI elements with C# properties. The `@bind` directive keeps an input and a property connected. ### Demo 2: Using Two-Way Data Binding
razor
Search
@bind="searchText"
@bind:event="oninput"
placeholder="Enter the name" />
Searching for: @searchText
@code {
private string searchText = string.Empty;
}
The `@bind:event="oninput"` modifier updates the property as the user types. This is useful for live-search features. ## 3. Complete Management Implementation The following three files combine data binding, lifecycle methods, reusable components, event callbacks, Bootstrap, and custom CSS.
razor
@page "/admin/gyms"
@using System.Net.Http.Json
@using System.Net.Http
@using System.ComponentModel.DataAnnotations
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.JSInterop
@using Ayush_Assignment_11.Components.Admin
@attribute [Authorize(Roles = "Admin")]
@inject HttpClient Http
@inject IJSRuntime JS
Manage Gyms
Manage Gyms
Add, edit, search, and manage gym information.
Add Gym @if (!string.IsNullOrWhiteSpace(errorMessage)) { @errorMessage } Search gyms Sort by Gym name City Status Refresh @if (isLoading) { Loading...Loading gyms...
} else if (FilteredGyms.Count == 0) {No gyms found
Try another search or add a new gym.
} else {All Gyms
@FilteredGyms.Count result(s) @foreach (var gym in FilteredGyms) { } }@if (showForm)
{
role="dialog" aria-modal="true" aria-labelledby="gymFormTitle"
@onclick:stopPropagation="true">
@(isEditMode ? "Edit Gym" : "Add Gym")
@onclick="CloseForm" disabled="@isSaving">
Gym name
City
Address
Phone
Opening time
Closing time
Total class capacity
Status
disabled="@(!isEditMode)">
Active
Inactive
Locker facility available
Parking available
@onclick="CloseForm" disabled="@isSaving">Cancel
@if (isSaving)
{
}
@(isEditMode ? "Update Gym" : "Save Gym")
}
@code {
// In a standard Blazor project, move these three classes into separate .cs files.
public class GymCardModel
{
public Guid Id { get; set; }
public string GymName { get; set; } = string.Empty;
public string Address { get; set; } = string.Empty;
public string CityName { get; set; } = string.Empty;
public string Phone { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public TimeSpan OpeningTime { get; set; }
public TimeSpan ClosingTime { get; set; }
public int TotalClassCapacity { get; set; }
public bool LockerFacility { get; set; }
public bool ParkingAvailable { get; set; }
public string Status { get; set; } = "Active";
}
public class GymFormModel { public Guid Id { get; set; } [Required(ErrorMessage = "Gym name is required.")] public string GymName { get; set; } = string.Empty; [Required(ErrorMessage = "Address is required.")] public string Address { get; set; } = string.Empty; [Required(ErrorMessage = "City is required.")] public string CityName { get; set; } = string.Empty; [Required(ErrorMessage = "Phone is required.")] public string Phone { get; set; } = string.Empty; [Required(ErrorMessage = "Email is required.")] [EmailAddress(ErrorMessage = "Enter a valid email address.")] public string Email { get; set; } = string.Empty; public string OpeningTime { get; set; } = "06:00"; public string ClosingTime { get; set; } = "22:00"; [Range(1, int.MaxValue, ErrorMessage = "Class capacity must be greater than zero.")] public int TotalClassCapacity { get; set; } = 20; public bool LockerFacility { get; set; } public bool ParkingAvailable { get; set; } public string Status { get; set; } = "Active"; } public class OperationResponse { public bool Success { get; set; } public string Message { get; set; } = string.Empty; } private List<GymCardModel> gyms = new(); private GymFormModel formModel = new(); private bool isLoading; private bool isSaving; private bool showForm; private bool isEditMode; private string searchText = string.Empty; private string sortBy = "name"; private string errorMessage = string.Empty; private List<GymCardModel> FilteredGyms => gyms .Where(g => string.IsNullOrWhiteSpace(searchText) || g.GymName.Contains(searchText, StringComparison.OrdinalIgnoreCase) || g.CityName.Contains(searchText, StringComparison.OrdinalIgnoreCase) || g.Phone.Contains(searchText, StringComparison.OrdinalIgnoreCase) || g.Email.Contains(searchText, StringComparison.OrdinalIgnoreCase)) .OrderBy(g => sortBy switch { "city" => g.CityName, "status" => g.Status, _ => g.GymName }) .ToList(); protected override async Task OnInitializedAsync() { await LoadGymsAsync(); } private async Task LoadGymsAsync() { isLoading = true; errorMessage = string.Empty; try { var result = await Http.GetFromJsonAsync<List<GymCardModel>>("api/GymCenter"); gyms = result ?? new List<GymCardModel>(); } catch (Exception) { errorMessage = "Unable to load gyms. Check your API connection and endpoint."; } finally { isLoading = false; } } private void OpenAddForm() { formModel = new GymFormModel { OpeningTime = "06:00", ClosingTime = "22:00", TotalClassCapacity = 20, Status = "Active" }; isEditMode = false; showForm = true; } private async Task OpenEditForm(Guid id) { var gym = gyms.FirstOrDefault(g => g.Id == id); if (gym is null) { await ShowMessageAsync("Error", "Gym not found.", false); return; } formModel = new GymFormModel { Id = gym.Id, GymName = gym.GymName, Address = gym.Address, CityName = gym.CityName, Phone = gym.Phone, Email = gym.Email, OpeningTime = gym.OpeningTime.ToString(@"hh\:mm"), ClosingTime = gym.ClosingTime.ToString(@"hh\:mm"), TotalClassCapacity = gym.TotalClassCapacity, LockerFacility = gym.LockerFacility, ParkingAvailable = gym.ParkingAvailable, Status = gym.Status }; isEditMode = true; showForm = true; } private async Task SaveGymAsync() { if (!TimeSpan.TryParse(formModel.OpeningTime, out var openingTime) || !TimeSpan.TryParse(formModel.ClosingTime, out var closingTime)) { await ShowMessageAsync("Validation error", "Please enter valid opening and closing times.", false); return; } if (closingTime <= openingTime) { await ShowMessageAsync("Validation error", "Closing time must be later than opening time.", false); return; } isSaving = true; try { HttpResponseMessage response; if (isEditMode) { var requestBody = new { Id = formModel.Id, formModel.GymName, formModel.Address, formModel.CityName, formModel.Phone, formModel.Email, OpeningTime = openingTime, ClosingTime = closingTime, formModel.TotalClassCapacity, formModel.LockerFacility, formModel.ParkingAvailable, formModel.Status }; response = await Http.PutAsJsonAsync($"api/GymCenter/{formModel.Id}", requestBody); } else { var requestBody = new { formModel.GymName, formModel.Address, formModel.CityName, formModel.Phone, formModel.Email, OpeningTime = openingTime, ClosingTime = closingTime, formModel.TotalClassCapacity, formModel.LockerFacility, formModel.ParkingAvailable }; response = await Http.PostAsJsonAsync("api/GymCenter", requestBody); } var result = await response.Content.ReadFromJsonAsync<OperationResponse>(); if (!response.IsSuccessStatusCode || result?.Success != true) { await ShowMessageAsync("Operation failed", result?.Message ?? "The request could not be completed.", false); return; } showForm = false; await LoadGymsAsync(); await ShowMessageAsync("Success", result.Message, true); } catch (Exception) { await ShowMessageAsync("Error", "An error occurred while saving the gym.", false); } finally { isSaving = false; } } private async Task DeleteGymAsync(Guid id) { var confirmed = await JS.InvokeAsync<bool>( "gymUi.confirmDelete", "Are you sure?", "This record will be soft-deleted."); if (!confirmed) return; try { var response = await Http.DeleteAsync($"api/GymCenter/{id}"); var result = await response.Content.ReadFromJsonAsync<OperationResponse>(); if (!response.IsSuccessStatusCode || result?.Success != true) { await ShowMessageAsync("Unable to delete", result?.Message ?? "The deletion request failed.", false); return; } await LoadGymsAsync(); await ShowMessageAsync("Success", result.Message, true); } catch (Exception) { await ShowMessageAsync("Error", "An error occurred while deleting the gym.", false); } } private void CloseForm() { if (!isSaving) showForm = false; } private async Task ShowMessageAsync(string title, string message, bool success) { await JS.InvokeVoidAsync("gymUi.showMessage", title, message, success); } }
### Explanation of ManageGyms.razor - `[Authorize(Roles = "Admin")]` restricts the page to users in the Admin role, assuming authorization is configured. - `OnInitializedAsync()` loads the gyms when the component initializes. - `FilteredGyms` filters by name, city, phone, and email, then sorts by the selected option. - `EditForm` binds the form controls to `formModel` and enables data-annotation validation. - `SaveGymAsync()` sends POST for adding and PUT for editing. - `DeleteGymAsync()` asks for confirmation before sending a DELETE request. - `ShowMessageAsync()` calls JavaScript to display operation messages. The endpoint paths and JSON response shape must match the backend API. The API responses are expected to contain `Success` and `Message`. **Compilation note:** In a standard Blazor project, C# class declarations cannot be placed directly inside the component's `@code` block. Move `GymCardModel`, `GymFormModel`, and `OperationResponse` into separate `.cs` files and import their namespace. ### File 2: GymCard.razor
razor
@GymName
@Status@Address, @CityName
@OpeningTime.ToString(@"hh\:mm") - @ClosingTime.ToString(@"hh\:mm")
Class capacity: @TotalClassCapacity
Locker: @(LockerFacility ? "Yes" : "No") Parking: @(ParkingAvailable ? "Yes" : "No") Edit Delete@code {
[Parameter]
public Guid Id { get; set; }
[Parameter] public string GymName { get; set; } = string.Empty; [Parameter] public string Address { get; set; } = string.Empty; [Parameter] public string CityName { get; set; } = string.Empty; [Parameter] public string Phone { get; set; } = string.Empty; [Parameter] public string Email { get; set; } = string.Empty; [Parameter] public TimeSpan OpeningTime { get; set; } [Parameter] public TimeSpan ClosingTime { get; set; } [Parameter] public int TotalClassCapacity { get; set; } [Parameter] public bool LockerFacility { get; set; } [Parameter] public bool ParkingAvailable { get; set; } [Parameter] public string Status { get; set; } = "Active"; [Parameter] public EventCallback<Guid> OnEdit { get; set; } [Parameter] public EventCallback<Guid> OnDelete { get; set; } private Task EditGym() { return OnEdit.InvokeAsync(Id); } private Task DeleteGym() { return OnDelete.InvokeAsync(Id); } }
### Explanation of GymCard.razor The `[Parameter]` properties receive the gym's data from the parent. The `EventCallback<Guid>` properties allow the child to send the selected gym's ID to the parent when Edit or Delete is clicked. The card handles presentation and events, while the parent handles API calls and business operations. ### File 3: app.css
css
/* Manage Gyms page */
.gym-card {
border: 1px solid #e9ecef;
border-radius: 14px;
overflow: hidden;
background: #fff;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.gym-card:hover {
transform: translateY(-3px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
}
.gym-card .card-body {
padding: 1.25rem;
}
.gym-card .card-title {
font-size: 1.1rem;
overflow-wrap: anywhere;
}
.gym-detail {
color: #5f6b7a;
font-size: 0.92rem;
margin-bottom: 0.75rem;
overflow-wrap: anywhere;
}
.gym-detail i {
color: #0d6efd;
margin-right: 0.45rem;
}
.facility-badge {
display: inline-block;
padding: 0.4rem 0.6rem;
border-radius: 6px;
background: #f1f5f9;
color: #334155;
font-size: 0.8rem;
}
.gym-card .btn {
border-radius: 7px;
}
/* Add/Edit modal */
.modal-backdrop-custom {
position: fixed;
inset: 0;
z-index: 1050;
display: flex;
justify-content: center;
align-items: center;
padding: 1rem;
background: rgba(15, 23, 42, 0.6);
overflow-y: auto;
}
.form-modal {
width: 100%;
max-width: 850px;
max-height: 90vh;
overflow-y: auto;
border-radius: 12px;
}
.form-modal .card-header {
padding: 1rem 1.25rem;
}
.form-modal .card-body {
padding: 1.5rem;
}
/* Empty grid state */
.empty-state {
background: #fff;
border: 1px dashed #ced4da;
border-radius: 12px;
}
/* Responsive adjustments */
@media (max-width: 576px) {
.form-modal {
max-height: 95vh;
}
.form-modal .card-body { padding: 1rem; } .gym-card .card-body { padding: 1rem; } }
### Explanation of app.css - `.gym-card` defines the card border, background, and rounded corners. - `.gym-card:hover` adds a subtle lift and shadow. - `.gym-detail` styles the contact and operating information. - `.facility-badge` styles locker and parking details. - `.modal-backdrop-custom` creates the overlay behind the form. - `.form-modal` controls modal dimensions and scrolling. - The media query adjusts spacing for small screens. Bootstrap CSS and Bootstrap Icons must also be available in the application. The custom stylesheet complements Bootstrap rather than replacing it. ## 4. How the Three Files Work Together 1. `ManageGyms.razor` loads gym records from the API. 2. It passes each gym's properties to `GymCard.razor`. 3. The child displays the details and raises Edit/Delete events. 4. The parent opens the edit form or confirms deletion. 5. The parent sends the API request and reloads the list after success. 6. `app.css` styles the cards and modal. The backend must enforce the business rules, including preventing subscriptions for inactive or soft-deleted gyms and preventing deletion when active subscriptions exist. ## 5. Ending Demo: Parent-Child Communication with EventCallback A child component can notify its parent that an action occurred by using `EventCallback`. ### Child component: NotifyButton.razor
razor
Show Notification
@code {
[Parameter]
public EventCallback OnNotify { get; set; }
private Task NotifyParent() { return OnNotify.InvokeAsync(); } }
### Parent component
razor
Notifications
@code {
private string message = "No notification yet.";
private void HandleNotification() { message = "The child component sent a notification!"; } }
When the button is clicked, the child invokes `OnNotify`, and the parent updates the message. This is the same communication pattern used by the gym cards: the child raises an event, and the parent decides how to respond. ## Conclusion This implementation combines Blazor lifecycle methods, data binding, reusable components, event callbacks, Bootstrap, and custom CSS. `ManageGyms.razor` manages data and operations, `GymCard.razor` displays each gym, and `app.css` controls the visual design. Keeping these responsibilities separate makes the interface easier to maintain and extend as the gym management application grows.