With the previous episodes in this series we have put together a solid architecture to work with our data. In this episode you will see how we can reduce even more duplicate code (in the future) by creating a Blazor Component which we can use to manage all entities in the database in a similar way. How cool is that?
Creating a Blazor Control with Generic Parameters
If you take a good look in the Customers.razor file, you might already notice that if we want to duplicate the same behavior with other entities in the Database, the only things that need to be changed are:
- The UI Model and key
- The Columns in the DxGrid
- The EditForm in the DxGrid
Blazor has some very cool tricks to accomplish just this – yet another feature I like about Blazor.
For this, let’s create a new Razor control called Shared\BrowseAndEditCtrl.razor in the DxBlazorChinook.csproj.
The first 3 lines of the file will look like this:
@typeparam TKey where TKey : IEquatable<TKey>
@typeparam TModel where TModel : class, new()
@inject IDataStore<TKey, TModel> Store
This means that the control has two generic parameters with the same restrictions on them as the IDataStore interface. Therefore, we can inject a IDataStore with the same generic parameters. We’ll see a bit later how to use them in the Blazor markup.
Next, we can copy over the entire DxGrid declaration, as well as the @code { } section. We'll need to replace CustomerModel with TModel.
Now we need to add some [Parameter] decorated properties to make this a proper reusable component.
@code {
private FluentValidationValidator? fluentValidator;
private ValidationSummary? validationSummary;
object Data { get; set; } = default!;
[Parameter]
public bool PaginateViaPrimaryKey { get; set; }
[Parameter]
public RenderFragment GridColumns { get; set; } = default!;
[Parameter]
public RenderFragment<GridEditFormTemplateContext> EditFormLayoutItems { get; set; } = default!;
// ... code left out for clearity
With the PaginateViaPrimaryKey parameter, we can control per entity whether we want to use this feature. To make this behavior work in the control, we can change the OnInitialized() method to look like this:
protected override void OnInitialized()
{
var dataSource = new GridDevExtremeDataSource<TModel>(Store.Query());
if (PaginateViaPrimaryKey)
{
dataSource.CustomizeLoadOptions = (loadOptions) =>
{
// If underlying data is a large SQL table, specify PrimaryKey and PaginateViaPrimaryKey.
// This can make SQL execution plans more efficient.
loadOptions.PrimaryKey = new[] { Store.KeyField };
loadOptions.PaginateViaPrimaryKey = PaginateViaPrimaryKey;
};
}
Data = dataSource;
}
The GridColumns property allows us to include a list of DxGridDataColumn columns in the markup containing the control. For this property to work, we’ll need to change something in the DxGrid markup.
The EditFormLayoutItems is another RenderFragment with a minor difference which is the generic type specified. By doing so, we can pass in the GridEditFormTemplateContextwhich is passed in by the DxGrid. Now we can change the DxGrid declaration to look like this::
<DxGrid Data="@Data"
CustomizeEditModel="Grid_CustomizeEditModel"
EditModelSaving="Grid_EditModelSaving"
DataItemDeleting="Grid_DataItemDeleting"
EditMode="GridEditMode.PopupEditForm"
ShowSearchBox="true"
KeyFieldName="@Store.KeyField">
<Columns>
<DxGridCommandColumn Width="160px" />
@GridColumns
</Columns>
<EditFormTemplate Context="EditFormContext">
<DxFormLayout CssClass="w-100">
@EditFormLayoutItems(EditFormContext)
<DxFormLayoutItem Caption="" ColSpanSm="12" BeginRow="true"
CssClass="formlayout-validation-summary">
<Template>
<div class="validation-container" style="color:red;">
<ValidationSummary @ref="validationSummary"></ValidationSummary>
<ul class="validation-errors">
@if (serverError != null)
{
@foreach (var e in serverError.Errors)
{
<li>@e.ErrorMessage</li>
}
}
</ul>
</div>
</Template>
</DxFormLayoutItem>
</DxFormLayout>
</EditFormTemplate>
</dxGrid>
As you can see, I have left DxGridCommandColumn in, so we don’t need to specify that in components containing this control. I have also left the DxFormLayout and the DxFormLayoutItem containing the validation message in, so we only need to specify the actual edit items. Please note how I pass in the EditFormContext into the EditFormLayoutItems.
To complete this control, there is one thing we need to change since the control is unaware of the model being used. This is in the Grid_CustomizeEditModel event. The TModel is unaware of properties FirstName and LastName. It could well be possible that the TModel doesn’t have those and we also didn’t put in any restrictions on the @typeparam TModel which tells the compiler that it should have those.
We can overcome this by invoking a callback function (a bit like an event). To make this work, we’ll add another property to the control:
[Parameter]
public Action<TModel> InitNewItemAction { get; set; } = default!;
We can now change the Grid_CustomizeEditModel code to look like this:
void Grid_CustomizeEditModel(GridCustomizeEditModelEventArgs e)
{
if (e.IsNew && InitNewItemAction != null)
{
var item = (TModel)e.EditModel;
InitNewItemAction(item);
}
}
The remaining Grid_EditModelSaving and Grid_DataItemDeleting methods should look like this:
async Task Grid_EditModelSaving(GridEditModelSavingEventArgs e)
{
serverError = null;
var item = (TModel)e.EditModel;
var result = (e.IsNew)
? await Store.CreateAsync(item)
: await Store.UpdateAsync(item);
if (!result.Success)
{
e.Cancel = true;
serverError = result.Exception as ValidationException;
}
}
async Task Grid_DataItemDeleting(GridDataItemDeletingEventArgs e)
{
serverError = null;
var item = (TModel)e.DataItem;
var result = await Store.DeleteAsync(Store.ModelKey(item));
if (!result.Success)
{
e.Cancel = true;
serverError = result.Exception as ValidationException;
}
}
Ready to slice code from Customers.razor?
Now we’re ready to unleash some serious Blazor power! If we go back to the Customers.razor file, first take a look at the amount of code . . . which we'll replace with only:
@page "/customers"
<BrowseAndEditCtrl TKey="int" TModel="CustomerModel">
<GridColumns>
<DxGridDataColumn FieldName="@nameof(CustomerModel.FirstName)" Caption="LastName" />
<DxGridDataColumn FieldName="@nameof(CustomerModel.LastName)" Caption="LastName" />
<DxGridDataColumn FieldName="@nameof(CustomerModel.Company)" Caption="Company" />
<DxGridDataColumn FieldName="@nameof(CustomerModel.Address)" Caption="Address" />
<DxGridDataColumn FieldName="@nameof(CustomerModel.City)" Caption="City" />
<DxGridDataColumn FieldName="@nameof(CustomerModel.State)" Caption="State" />
<DxGridDataColumn FieldName="@nameof(CustomerModel.Country)" Caption="Country" />
<DxGridDataColumn FieldName="@nameof(CustomerModel.PostalCode)" Caption="PostalCode" />
<DxGridDataColumn FieldName="@nameof(CustomerModel.Phone)" Caption="Phone" />
<DxGridDataColumn FieldName="@nameof(CustomerModel.Fax)" Caption="Fax" />
<DxGridDataColumn FieldName="@nameof(CustomerModel.Email)" Caption="Email" />
</GridColumns>
<EditFormLayoutItems Context="ctx">
@{
var item = (CustomerModel)ctx.EditModel;
}
<DxFormLayoutItem Caption="FirstName" ColSpanMd="6"><DxTextBox @bind-Text="item.FirstName" /></DxFormLayoutItem>
<DxFormLayoutItem Caption="LastName" ColSpanMd="6"><DxTextBox @bind-Text="item.LastName" /></DxFormLayoutItem>
<DxFormLayoutItem Caption="Company" ColSpanMd="6"><DxTextBox @bind-Text="item.Company" /></DxFormLayoutItem>
<DxFormLayoutItem Caption="Address" ColSpanMd="6"><DxTextBox @bind-Text="item.Address" /></DxFormLayoutItem>
<DxFormLayoutItem Caption="City" ColSpanMd="6"><DxTextBox @bind-Text="item.City" /></DxFormLayoutItem>
<DxFormLayoutItem Caption="State" ColSpanMd="6"><DxTextBox @bind-Text="item.State" /></DxFormLayoutItem>
<DxFormLayoutItem Caption="Country" ColSpanMd="6"><DxTextBox @bind-Text="item.Country" /></DxFormLayoutItem>
<DxFormLayoutItem Caption="PostalCode" ColSpanMd="6"><DxTextBox @bind-Text="item.PostalCode" /></DxFormLayoutItem>
<DxFormLayoutItem Caption="Phone" ColSpanMd="6"><DxTextBox @bind-Text="item.Phone" /></DxFormLayoutItem>
<DxFormLayoutItem Caption="Fax" ColSpanMd="6"><DxTextBox @bind-Text="item.Fax" /></DxFormLayoutItem>
<DxFormLayoutItem Caption="Email" ColSpanMd="6"><DxTextBox @bind-Text="item.Email" /></DxFormLayoutItem>
</EditFormLayoutItems>
</BrowseAndEditCtrl>
@code{
// Look mom, no code behind :-)
}
Please take a second to digest what the markup and the BrowseAndEditCtrl do. The TKey and TModel determine which IDataStore is injected into the control, and the control will just work with that.
You can now imagine that if we want the exact same UI with the same behavior for another entity in the database, the only things we'll need to do are:
- A UI Model class + Validator
- Configure AutoMapper to map from the EF model to the UI model and back.
- Implement and register a server-side Validator and a new DataStore derived from EFDataStore.
- Create a component containing the BrowseAndEditCtrl with the TKey / TModel properties set, and add theGridColumns and EditLayoutItems.
Please don’t take my word on this but try it yourself!
Just head over to the repo and clone this branch.
Try adding an Artist page to manage them, and let me know how long it took you!
(Don't forget to add a link to it in the Shared\NavMenu.razor)