by Joche Ojeda | Dec 2, 2024 | Blazor
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.
by Joche Ojeda | Jun 1, 2024 | Data Synchronization, EfCore, SyncFrameworkV2
In modern software development, extending the functionality of a framework while maintaining its integrity and usability can be a complex task. One common scenario involves extending interfaces to add new events or methods. In this post, we’ll explore the impact of extending interfaces within the Sync Framework, specifically looking at IDeltaStore and IDeltaProcessor interfaces to include SavingDelta and SavedDelta events, as well as ProcessingDelta and ProcessedDelta events. We’ll discuss the options available—extending existing interfaces versus adding new interfaces—and examine the side effects of each approach.
Background
The Sync Framework is designed to synchronize data across different data stores, ensuring consistency and integrity. The IDeltaStore interface typically handles delta storage operations, while the IDeltaProcessor interface manages delta (change) processing. To enhance the functionality, you might want to add events such as SavingDelta, SavedDelta, ProcessingDelta, and ProcessedDelta to these interfaces.
Extending Existing Interfaces
Extending existing interfaces involves directly adding new events or methods to the current interface definitions. Here’s an example:
public interface IDeltaStore {
void SaveData(Data data);
// New events
event EventHandler<DeltaEventArgs> SavingDelta;
event EventHandler<DeltaEventArgs> SavedDelta;
}
public interface IDeltaProcessor {
void ProcessDelta(Delta delta);
// New events
event EventHandler<DeltaEventArgs> ProcessingDelta;
event EventHandler<DeltaEventArgs> ProcessedDelta;
}
Pros of Extending Existing Interfaces
- Simplicity: The existing implementations need to be updated to include the new functionality, making the overall design simpler.
- Direct Integration: The new events are directly available in the existing interface, making them easy to use and understand within the current framework.
Cons of Extending Existing Interfaces
- Breaks Existing Implementations: All existing classes implementing these interfaces must be updated to handle the new events. This can lead to significant refactoring, especially in large codebases.
- Violates SOLID Principles: Adding new responsibilities to existing interfaces can violate the Single Responsibility Principle (SRP) and Interface Segregation Principle (ISP), leading to bloated interfaces.
- Potential for Bugs: The necessity to modify all implementing classes increases the risk of introducing bugs and inconsistencies.
Adding New Interfaces
An alternative approach is to create new interfaces that extend the existing ones, encapsulating the new events. Here’s how you can do it:
public interface IDeltaStore {
void SaveData(Data data);
}
public interface IDeltaStoreWithEvents : IDeltaStore {
event EventHandler<DeltaEventArgs> SavingDelta;
event EventHandler<DeltaEventArgs> SavedDelta;
}
public interface IDeltaProcessor {
void ProcessDelta(Delta delta);
}
public interface IDeltaProcessorWithEvents : IDeltaProcessor {
event EventHandler<DeltaEventArgs> ProcessingDelta;
event EventHandler<DeltaEventArgs> ProcessedDelta;
}
Pros of Adding New Interfaces
- Adheres to SOLID Principles: This approach keeps the existing interfaces clean and focused, adhering to the SRP and ISP.
- Backward Compatibility: Existing implementations remain functional without modification, ensuring backward compatibility.
- Flexibility: New functionality can be selectively adopted by implementing the new interfaces where needed.
Cons of Adding New Interfaces
- Complexity: Introducing new interfaces can increase the complexity of the codebase, as developers need to understand and manage multiple interfaces.
- Redundancy: There can be redundancy in code, where some classes might need to implement both the original and new interfaces.
- Learning Curve: Developers need to be aware of and understand the new interfaces, which might require additional documentation and training.
Conclusion
Deciding between extending existing interfaces and adding new ones depends on your specific context and priorities. Extending interfaces can simplify the design but at the cost of violating SOLID principles and potentially breaking existing code. On the other hand, adding new interfaces preserves existing functionality and adheres to best practices but can introduce additional complexity.
In general, if maintaining backward compatibility and adhering to SOLID principles are high priorities, adding new interfaces is the preferred approach. However, if you are working within a controlled environment where updating existing implementations is manageable, extending the interfaces might be a viable option.
By carefully considering the trade-offs and understanding the implications of each approach, you can make an informed decision that best suits your project’s needs.
by Joche Ojeda | May 14, 2024 | C#, Data Synchronization, dotnet
Hello there! Today, we’re going to delve into the fascinating world of design patterns. Don’t worry if you’re not a tech whiz – we’ll keep things simple and relatable. We’ll use the SyncFramework as an example, but our main focus will be on the design patterns themselves. So, let’s get started!
What are Design Patterns?
Design patterns are like blueprints – they provide solutions to common problems that occur in software design. They’re not ready-made code that you can directly insert into your program. Instead, they’re guidelines you can follow to solve a particular problem in a specific context.
SOLID Design Principles
One of the most popular sets of design principles is SOLID. It’s an acronym that stands for five principles that help make software designs more understandable, flexible, and maintainable. Let’s break it down:
- Single Responsibility Principle: A class should have only one reason to change. In other words, it should have only one job.
- Open-Closed Principle: Software entities should be open for extension but closed for modification. This means we should be able to add new features or functionality without changing the existing code.
- Liskov Substitution Principle: Subtypes must be substitutable for their base types. This principle is about creating new derived classes that can replace the functionality of the base class without breaking the application.
- Interface Segregation Principle: Clients should not be forced to depend on interfaces they do not use. This principle is about reducing the side effects and frequency of required changes by splitting the software into multiple, independent parts.
- Dependency Inversion Principle: High-level modules should not depend on low-level modules. Both should depend on abstractions. This principle allows for decoupling.
Applying SOLID Principles in SyncFramework
The SyncFramework is a great example of how these principles can be applied. Here’s how:
- Single Responsibility Principle: Each component of the SyncFramework has a specific role. For instance, one component is responsible for tracking changes, while another handles conflict resolution.
- Open-Closed Principle: The SyncFramework is designed to be extensible. You can add new data sources or change the way data is synchronized without modifying the core framework.
- Liskov Substitution Principle: The SyncFramework uses base classes and interfaces that allow for substitutable components. This means you can replace or modify components without affecting the overall functionality.
- Interface Segregation Principle: The SyncFramework provides a range of interfaces, allowing you to choose the ones you need and ignore the ones you don’t.
- Dependency Inversion Principle: The SyncFramework depends on abstractions, not on concrete classes. This makes it more flexible and adaptable to changes.
And that’s a wrap for today! But don’t worry, this is just the beginning. In the upcoming series of articles, we’ll dive deeper into each of these principles. We’ll explore how they’re applied in the source code of the SyncFramework, providing real-world examples to help you understand these concepts better. So, stay tuned for more exciting insights into the world of design patterns! See you in the next article!
Related articles
If you want to learn more about data synchronization you can checkout the following blog posts:
- Data synchronization in a few words – https://www.jocheojeda.com/2021/10/10/data-synchronization-in-a-few-words/
- Parts of a Synchronization Framework – https://www.jocheojeda.com/2021/10/10/parts-of-a-synchronization-framework/
- Let’s write a Synchronization Framework in C# – https://www.jocheojeda.com/2021/10/11/lets-write-a-synchronization-framework-in-c/
- Synchronization Framework Base Classes – https://www.jocheojeda.com/2021/10/12/synchronization-framework-base-classes/
- Planning the first implementation – https://www.jocheojeda.com/2021/10/12/planning-the-first-implementation/
- Testing the first implementation – https://youtu.be/l2-yPlExSrg
- Adding network support – https://www.jocheojeda.com/2021/10/17/syncframework-adding-network-support/