Over time, I transitioned to using the first versions of my beloved framework, XAF. As you might know, XAF generates a polished and functional UI out of the box. Using XAF made me more of a backend developer since most of the development work wasn’t visual—especially in the early versions, where the model designer was rudimentary (it’s much better now).
Eventually, I moved on to developing .NET libraries and NuGet packages, diving deep into SOLID design principles. Fun fact: I actually learned about SOLID from DevExpress TV. Yes, there was a time before YouTube when DevExpress posted videos on technical tasks!
Nowadays, I feel confident creating and publishing my own libraries as NuGet packages. However, my “old monster” was still lurking in the shadows: UI components. I finally decided it was time to conquer it, but first, I needed to choose a platform. Here were my options:
- Windows Forms: A robust and mature platform but limited to desktop applications.
- WPF: A great option with some excellent UI frameworks that I love, but it still feels a bit “Windows Forms-ish” to me.
- Xamarin/Maui: I’m a big fan of Xamarin Forms and Xamarin/Maui XAML, but they’re primarily focused on device-specific applications.
- Blazor: This was the clear winner because it allows me to create desktop applications using Electron, embed components into Windows Forms, or even integrate with MAUI.
Recently, I’ve been helping my brother with a project in Blazor. (He’s not a programmer, but I am.) This gave me an opportunity to experiment with design patterns to get the most out of my components, which started as plain HTML5 pages.
Without further ado, here are the key insights I’ve gained so far.
Building high-quality Blazor components requires attention to both the C# implementation and Razor markup patterns. This guide combines architectural best practices with practical implementation patterns to create robust, reusable components.
1. Component Architecture and Organization
Parameter Organization
Start by organizing parameters into logical groups for better maintainability:
public class CustomForm : ComponentBase { // Layout Parameters [Parameter] public string Width { get; set; } [Parameter] public string Margin { get; set; } [Parameter] public string Padding { get; set; } // Validation Parameters [Parameter] public bool EnableValidation { get; set; } [Parameter] public string ValidationMessage { get; set; } // Event Callbacks [Parameter] public EventCallback<bool> OnValidationComplete { get; set; } [Parameter] public EventCallback<string> OnSubmit { get; set; } }
Corresponding Razor Template
<div class="form-container" style="width: @Width; margin: @Margin; padding: @Padding"> <form @onsubmit="HandleSubmit"> @if (EnableValidation) { <div class="validation-message"> @ValidationMessage </div> } @ChildContent </form> </div>
2. Smart Default Values and Template Composition
Component Implementation
public class DataTable<T> : ComponentBase { [Parameter] public int PageSize { get; set; } = 10; [Parameter] public bool ShowPagination { get; set; } = true; [Parameter] public string EmptyMessage { get; set; } = "No data available"; [Parameter] public IEnumerable<T> Items { get; set; } = Array.Empty<T>(); [Parameter] public RenderFragment HeaderTemplate { get; set; } [Parameter] public RenderFragment<T> RowTemplate { get; set; } [Parameter] public RenderFragment FooterTemplate { get; set; } }
Razor Implementation
<div class="table-container"> @if (HeaderTemplate != null) { <header class="table-header"> @HeaderTemplate </header> } <div class="table-content"> @if (!Items.Any()) { <div class="empty-state">@EmptyMessage</div> } else { @foreach (var item in Items) { @RowTemplate(item) } } </div> @if (ShowPagination) { <div class="pagination"> <!-- Pagination implementation --> </div> } </div>
3. Accessibility and Unique IDs
Component Implementation
public class FormField : ComponentBase { private string fieldId = $"field-{Guid.NewGuid():N}"; private string labelId = $"label-{Guid.NewGuid():N}"; private string errorId = $"error-{Guid.NewGuid():N}"; [Parameter] public string Label { get; set; } [Parameter] public string Error { get; set; } [Parameter] public bool Required { get; set; } }
Razor Implementation
<div class="form-field"> <label id="@labelId" for="@fieldId"> @Label @if (Required) { <span class="required" aria-label="required">*</span> } </label> <input id="@fieldId" aria-labelledby="@labelId" aria-describedby="@errorId" aria-required="@Required" /> @if (!string.IsNullOrEmpty(Error)) { <div id="@errorId" class="error-message" role="alert"> @Error </div> } </div>
4. Virtualization and Performance
Component Implementation
public class VirtualizedList<T> : ComponentBase { [Parameter] public IEnumerable<T> Items { get; set; } [Parameter] public RenderFragment<T> ItemTemplate { get; set; } [Parameter] public int ItemHeight { get; set; } = 50; [Parameter] public Func<ItemsProviderRequest, ValueTask<ItemsProviderResult<T>>> ItemsProvider { get; set; } }
Razor Implementation
<div class="virtualized-container" style="height: 500px; overflow-y: auto;"> <Virtualize Items="@Items" ItemSize="@ItemHeight" ItemsProvider="@ItemsProvider" Context="item"> <ItemContent> <div class="list-item" style="height: @(ItemHeight)px"> @ItemTemplate(item) </div> </ItemContent> <Placeholder> <div class="loading-placeholder" style="height: @(ItemHeight)px"> <div class="loading-animation"></div> </div> </Placeholder> </Virtualize> </div>
Best Practices Summary
1. Parameter Organization
- Group related parameters with clear comments
- Provide meaningful default values
- Use parameter validation where appropriate
2. Template Composition
- Use RenderFragment for customizable sections
- Provide default templates when needed
- Enable granular control over component appearance
3. Accessibility
- Generate unique IDs for form elements
- Include proper ARIA attributes
- Support keyboard navigation
4. Performance
- Implement virtualization for large datasets
- Use loading states and placeholders
- Optimize rendering with appropriate conditions
Conclusion
Building effective Blazor components requires attention to both the C# implementation and Razor markup. By following these patterns and practices, you can create components that are:
- Highly reusable
- Performant
- Accessible
- Easy to maintain
- Flexible for different use cases
Remember to adapt these practices to your specific needs while maintaining clean component design principles.