Understanding Blazor Components Every Razor file in Blazor represents a component. That's the fundamental concept! A Blazor Razor file contains markup and can include C# code within an @code section. Each page in our application is essentially a component, and these components can be composed by nesting other components within them.
Any class that derives from the ComponentBase class becomes a Blazor component. When you use Razer files, the generated class automatically inherits from ComponentBase. Let's explore this by examining components in a sample project.
Examining Component Structure Open the Index.razor file in your project. This component demonstrates how a parent component can pass parameters to child components. The SurveyPrompt component requires a Title parameter that we can set when using the component.
Creating a Simple Alert Component Let's build a reusible alert component that can be conditionally displayed. Create a new Razor component named Alert.razor and implement it with the following code:
@if (IsVisible)
{
<div class="alert alert-info mt-3" role="alert">
@ChildContent
</div>
}
[Parameter]
public bool IsVisible { get; set; }
[Parameter]
public RenderFragment ChildContent { get; set; } = default!;
This component uses the @if directive to conditionally render its content. The IsVisible property determines whether the alert should be shown. The ChildContent parameter allows us to pass HTML content from the parent component.
Now, let's use this component in our Index page:
<Alert IsVisible="@showAlert">
<strong>Blazor is amazing!</strong>
</Alert>
<button @onclick="ToggleAlert" class="btn btn-primary">Toggle Alert</button>
@code {
private bool showAlert = true;
private void ToggleAlert()
{
showAlert = !showAlert;
}
}
Separating View and ViewModel For better organization, you can separate your component's markup from its logic by using partial classes. Create a DismissibleAlert.razor file and a corresponding DismissibleAlert.razor.cs file.
The markup for the DismissibleAlert component:
@if (IsVisible)
{
<div class="alert alert-warning alert-dismissible fade show mt-3" role="alert">
@ChildContent
<button type="button" class="close" @onclick="Dismiss">
<span>×</span>
</button>
</div>
}
The C# code in DismissibleAlert.razor.cs:
using Microsoft.AspNetCore.Components;
public partial class DismissibleAlert
{
private bool isVisible;
[Parameter]
public bool IsVisible
{
get => isVisible;
set
{
if (value != isVisible)
{
isVisible = value;
IsVisibleChanged?.InvokeAsync(isVisible);
}
}
}
[Parameter]
public EventCallback<bool> IsVisibleChanged { get; set; }
[Parameter]
public RenderFragment ChildContent { get; set; } = default!;
public void Dismiss() => IsVisible = false;
}
Parent-Child Communication Patterns Components communicate primarily through data binding. For two-way binding, use the @bind syntax:
<DismissibleAlert @bind-IsVisible="showAlert">
<strong>Important notification!</strong>
</DismissibleAlert>
When using asynchronous operations like timers, you need to manually trigger UI updates:
public void ToggleAlert()
{
showAlert = !showAlert;
StateHasChanged(); // Manually trigger UI update
}
Using Cascading Values For sharing data across deeply nested components, Blazor provides cascading values. Let's create a shared data context:
public class SharedData
{
private int sharedValue;
public int SharedValue
{
get => sharedValue;
set
{
if (value != sharedValue)
{
sharedValue = value;
ValueChanged?.Invoke(sharedValue);
}
}
}
public Action<int>? ValueChanged { get; set; }
}
Then use the CascadingValue component to provide this data to descendant components:
<CascadingValue Value="@sharedData">
@ChildContent
</CascadingValue>
Component Lifecycle Methods Blazor components have a lifecycle with several methods you can override:
protected override void OnInitialized() { /* Called when component is initialized */ }
protected override void OnParametersSet() { /* Called when parameters are set */ }
protected override bool ShouldRender() { /* Return true to render, false to skip */ }
protected override void OnAfterRender(bool firstRender) { /* Called after rendering */ }
public void Dispose() { /* Cleanup resources */ }
Refactoring a Pizza Ordering Application Let's apply these concepts to refactor a pizza ordering application. We'll create reusable components for displaying pizzas and handling orders.
Pizza Display Component Create a PizzaItem component to display individual pizza details:
<div class="row">
<div class="col">@Pizza.Name</div>
<div class="col text-right">@Pizza.Price.ToString("C")</div>
<div class="col">
<img src="@GetSpicinessImage(Pizza.Spiciness)" alt="@Pizza.Spiciness" />
</div>
<div class="col">
<button class="@ButtonClass" @onclick="@( () => OnSelected.InvokeAsync(Pizza) )">
@ButtonText
</button>
</div>
</div>
[Parameter]
public Pizza Pizza { get; set; } = default!;
[Parameter]
public string ButtonText { get; set; } = "Order";
[Parameter]
public string ButtonClass { get; set; } = "btn btn-primary";
[Parameter]
public EventCallback<Pizza> OnSelected { get; set; }
Pizza List Component Create a PizzaList component to display a collection of pizzas:
@if (Pizzas is null || !Pizzas.Any())
{
<div>Loading pizzas...</div>
}
else
{
<h2>@Title</h2>
@foreach (var pizza in Pizzas)
{
<PizzaItem Pizza="@pizza"
ButtonText="@ButtonText"
ButtonClass="@ButtonClass"
OnSelected="@OnSelected" />
}
}
[Parameter]
public string Title { get; set; } = "Pizza Menu";
[Parameter]
public IEnumerable<Pizza> Pizzas { get; set; } = default!;
[Parameter]
public string ButtonText { get; set; } = "Order";
[Parameter]
public string ButtonClass { get; set; } = "btn btn-primary";
[Parameter]
public EventCallback<Pizza> OnSelected { get; set; }
Shopping Basket Component Create a ShoppingBasket component to manage ordered items:
@if (OrderItems is not null && OrderItems.Any())
{
<h2>Your Order</h2>
@foreach (var item in OrderItems)
{
<div class="row mb-2">
<div class="col">@item.Name</div>
<div class="col text-right">@item.Price.ToString("C")</div>
<div class="col">
<button class="btn btn-danger" @onclick="@( () => OnRemove.InvokeAsync(item.Id) )">
Remove
</button>
</div>
</div>
}
<div class="row">
<div class="col font-weight-bold">Total: @TotalPrice.ToString("C")</div>
</div>
}
[Parameter]
public IEnumerable<OrderItem> OrderItems { get; set; } = default!;
[Parameter]
public EventCallback<int> OnRemove { get; set; }
[Parameter]
public Func<int, Pizza> GetPizzaDetails { get; set; } = default!;
private decimal TotalPrice => OrderItems?.Sum(item => item.Price) ?? 0;
Customer Entry Component Create a CustomerEntry component for user information:
<EditForm Model="@Customer" OnValidSubmit="@HandleValidSubmit">
<DataAnnotationsValidator />
<InputText @bind-Value="@Customer.Name" />
<InputText @bind-Value="@Customer.Email" />
<button type="submit" class="btn btn-success">Place Order</button>
</EditForm>
[Parameter]
public Customer Customer { get; set; } = default!;
[Parameter]
public EventCallback<Customer> OnValidSubmit { get; set; }
Putting It All Together Now we can compose our main page using these components:
<PizzaList Title="Available Pizzas"
Pizzas="@menuItems"
ButtonText="Add to Cart"
OnSelected="@AddToCart" />
<ShoppingBasket OrderItems="@cartItems"
OnRemove="@RemoveFromCart" />
<CustomerEntry Customer="@customer"
OnValidSubmit="@PlaceOrder" />
This refactored approach provides better separation of concerns, reusability, and maintainability for our application.