Create master detail components in Blazor Server (Master CRUD Components)
In the previous part of
this article series you developed the List component that displays a list of
Teams in a table. In this part you will create ShowTeam, InsertTeam, and
UpdateTeam components to perform the CRUD operations on the Teams table.
The following figure shows how the ShowTeam component displays a Team in the
browser:
Open ShowTeam.razor component added from the previous part and inject
AppDbContext in it as shown below:
@inject AppDbContext db
In the @code block add two parameter properties called SelectedTeam and
DataButtonClick. These properties are shown below:
[Parameter]
public Team SelectedTeam { get; set; }
[Parameter]
public Action<DataButton,Team> DataButtonClick { get; set; }
Recollect that ShowTeam is loaded dynamically by the parent List component.
And the SelectedItem property is assigned a value by the parent based on user's
selection in the table. The DataButtonClick is a callback function and it's also
assigned by the ent List component.
When a user clicks on the Edit or Cancel button the DataButtonClick callback
needs to be invoked. This is done in RaiseDataButtonClick() event handler method
as shown below:
public void RaiseDataButtonClick(DataButton operation)
{
DataButtonClick?.Invoke(operation,SelectedTeam);
}
The DataButtonClick callback is triggered using the Invoke() method. The
RaiseDataButtonClick() is called when Edit or Cancel button is clicked.
Upon clicking the Delete button you need to delete the Team from the
database. This is done in the click event handler of the Delete button as shown
below:
private void OnDeleteClick()
{
db.Teams.Remove(SelectedTeam);
db.SaveChanges();
DataButtonClick?.Invoke(DataButton.Delete,SelectedTeam);
}
As you can see, this code deletes the SelectedTeam from the database and also
raises the DataButtonClick callback.
You could have combined the RaiseDataButtonClick() and OnDeleteClick() but I
am keeping them separate here for the sake of easy understanding.
This completes the @code block. The following code shows the completed @code
block for your quick checking:
@code {
[Parameter]
public Team SelectedTeam { get; set; }
[Parameter]
public Action<DataButton,Team> DataButtonClick { get; set; }
public void RaiseDataButtonClick(DataButton operation)
{
DataButtonClick?.Invoke(operation,SelectedTeam);
}
private void OnDeleteClick()
{
db.Teams.Remove(SelectedTeam);
db.SaveChanges();
DataButtonClick?.Invoke(DataButton.Delete,SelectedTeam);
}
}
Now let's add the markup responsible for showing the Team record and the
buttons.
<h2>Edit Team : @SelectedTeam.Name</h2>
<table border="1" cellpadding="10">
<tr>
<td>Team ID :</td>
<td>@SelectedTeam.TeamID</td>
</tr>
<tr>
<td>Name :</td>
<td>@SelectedTeam.Name</td>
</tr>
<tr>
<td>Description :</td>
<td>@SelectedTeam.Description</td>
</tr>
<tr>
<td colspan="2">
<button @onclick="()=>RaiseDataButtonClick
(DataButton.Edit)">Edit</button>
<button @onclick="OnDeleteClick">Delete</button>
<button @onclick="()=>RaiseDataButtonClick
(DataButton.CancelReadMode)">Cancel</button>
</td>
</tr>
</table>
This markup is quite straightforward. Notice the lines marked in bold
letters. The Edit and Cancel buttons wire the onclick event handler to the
RaiseDataButtonClick() method. They also pass the DataButton value of Edit and
CancelReadMode to the handler. The Delete button onclick event calls the
OnDeleteClick() method.
This complete the ShowTeam.razor component.
Next, open the ListTeams.razor component and write the following code in the
OnDataButtonClick() method (currently it's empty).
public void OnDataButtonClick(DataButton button,
Team item)
{
if (button == DataButton.CancelReadMode)
{
DynamicComponentType = typeof(DynamicPlaceHolder);
DynamicComponentParams = null;
SelectedItem = null;
}
if (button == DataButton.Edit)
{
DynamicComponentType = typeof(UpdateTeam);
DynamicComponentParams = new
Dictionary<string, object>()
{
{"SelectedTeam",item },
{"DataButtonClick",DataButtonClickHandler}
};
}
if (button == DataButton.Delete)
{
DynamicComponentType = typeof(DynamicPlaceHolder);
DynamicComponentParams = null;
}
Items = db.Teams.ToList();
this.StateHasChanged();
}
Here, you need to decide the next dynamic component to be loaded depending on
the button clicked in the ShowTeam component.
If the Cancel button was clicked you simply load the empty DynamicPlaceHolder
component. This way the ShowList will be replaced by DynamicPlaceHolder. Since
clicking on the Cancel buttons means that the current selection is cancelled,
you also set SelectedItem to null.
If the Edit button was clicked, you need to load the UpdateTeam component
that displays the selected Team for modification. So, ShowList will be replaced
by UpdateTeam component.
Similarly, if the Delete button was clicked you need to load
DynamicPlaceHolder because the record has already been deleted and there is
nothing to show.
Now let's turn our attention to InsertTeam.razor component.
The InsertTeam component is loaded upon clicking the Insert button from the
List component and is shown below:
Open InsertTeam.razor and write the following code in it.
@inject AppDbContext db
@code {
[Parameter]
public Action<DataButton,Team>
DataButtonClick { get; set; }
public Team NewTeam { get; set; } = new Team();
public void RaiseDataButtonClick
(DataButton operation)
{
DataButtonClick?.Invoke(operation,NewTeam);
}
private void OnSaveClick()
{
db.Teams.Add(NewTeam);
db.SaveChanges();
DataButtonClick?.Invoke
(DataButton.Insert,NewTeam);
}
}
This code should look familiar to you because it's similar to ShowTeam
component.
Since it's an insert operation the parent component doesn't supply any Team
to InsertTeam component. The NewTeam property is a new Team entity. The
RaiseDataButtonClick() is called when you click on the Cancel button and raises
the DataButtonClick callback.
The OnSaveClick() is called when you click on the Save button.This event
handler adds the new Team into the database and raises the DataButtonClick
callback.
The following markup is responsible for displaying the UI of the InsertTeam
component.
<h2>Insert New Team</h2>
<EditForm Model="NewTeam" OnValidSubmit="OnSaveClick">
<DataAnnotationsValidator></DataAnnotationsValidator>
<table border="0" cellpadding="10">
<tr>
<td class="right">
<label for="FirstName">Name :</label>
</td>
<td>
<InputText id="Name" @bind-Value="NewTeam.Name" />
<ValidationMessage For="(() => NewTeam.Name)" />
</td>
</tr>
<tr>
<td class="right">
<label for="Description">Description :</label>
</td>
<td>
<InputText id="Description"
@bind-Value="NewTeam.Description" />
<ValidationMessage
For="(() => NewTeam.Description)" />
</td>
</tr>
<tr>
<td colspan="2">
<button type="submit">Save</button>
<button type="button" @onclick="()=>
RaiseDataButtonClick(DataButton.CancelInsertMode)">Cancel</button>
</td>
</tr>
</table>
<ValidationSummary></ValidationSummary>
</EditForm>
To render the data entry form you use Blazor's EditForm component. You also
enable data annotation based validations using DataAnnotationsValidator
component. The validation errors are displayed using ValidationMessage and
ValidationSummary components. The textboxes accepting Team Name and Description
are displayed using InputText component.
Upon clicking the Save button the OnSaveClick() event handler is called and
clicking the Cancel button triggers the RaiseDataButtonClick() method. Note that
you pass Insert and CancelInsertMode in these methods. I won't go in the details
of Blazor's form components here. You can learn more about them in the
Microsoft's official documentation.
This completes InsertTeam.razor component.
Let's write the code necessary to deal with Insert and CancelInsertMode
DataButton values.
Open ListTeams.razor and go to OnDataButtonClick() callback method. Add the
following code to this method:
if (button == DataButton.CancelInsertMode)
{
DynamicComponentType = typeof(DynamicPlaceHolder);
DynamicComponentParams = null;
}
And
if (button == DataButton.Insert)
{
DynamicComponentType = typeof(ShowTeam);
DynamicComponentParams = new Dictionary<string, object>()
{
{"SelectedTeam",item },
{"DataButtonClick",DataButtonClickHandler}
};
}
When a user clicks the Cancel button the InsertTeam component is replaced
with DynamicPlaceHolder component. Similarly, when a new Team is successfully
added, InsertTeam is replaced with ShowTeam component.
Now let's complete the UpdateTeam.razor component.
When you click on the Edit button in the ShowTeam component, the UpdateTeam
component gets loaded and looks like this:
Open UpdateTeam.razor and add the following code:
@inject AppDbContext db
@code {
[Parameter]
public Team SelectedTeam { get; set; }
[Parameter]
public Action<DataButton,Team>
DataButtonClick { get; set; }
public void RaiseDataButtonClick(DataButton operation)
{
DataButtonClick?.Invoke(operation,SelectedTeam);
}
private void OnSaveClick()
{
db.SaveChanges();
DataButtonClick?.Invoke
(DataButton.Update,SelectedTeam);
}
}
This code is similar to ShowTeam.razor and InsertTeam.razor components.
The RaiseDataButtonClick() sends CancelEditMode to the DataButtonClick
callback whereas OnSaveClick() sends Update to the callback.
The markup responsible for rendering the update form is shown below:
<h2>Edit Team : @SelectedTeam.Name</h2>
<EditForm Model="SelectedTeam" OnValidSubmit="OnSaveClick">
<DataAnnotationsValidator></DataAnnotationsValidator>
<table border="0" cellpadding="10">
<tr>
<td class="right">
<label>Team ID :</label>
</td>
<td>
@SelectedTeam.TeamID
</td>
</tr>
<tr>
<td class="right">
<label for="FirstName">Name :</label>
</td>
<td>
<InputText id="Name"
@bind-Value="SelectedTeam.Name" />
<ValidationMessage
For="(() => SelectedTeam.Name)" />
</td>
</tr>
<tr>
<td class="right">
<label for="Description">Description :</label>
</td>
<td>
<InputText id="Description"
@bind-Value="SelectedTeam.Description" />
<ValidationMessage For="(() =>
SelectedTeam.Description)" />
</td>
</tr>
<tr>
<td colspan="2">
<button type="submit">Save</button>
<button type="button" @onclick="()=>
RaiseDataButtonClick(DataButton.CancelEditMode)">Cancel</button>
</td>
</tr>
</table>
<ValidationSummary></ValidationSummary>
</EditForm>
This time model for the EditForm is SelectedTeam object and OnValidClick
points to the OnSaveClick() handler function. The Cancel button triggers the
RaiseDataButtonClick() method.
This completes the UpdateTeam.razor component.
Now go back to the ListTeams.razor and add this code to the OnDataButtonClick()
callback function:
if (button == DataButton.CancelEditMode)
{
DynamicComponentType = typeof(ShowTeam);
DynamicComponentParams = new Dictionary<string, object>()
{
{"SelectedTeam",item },
{"DataButtonClick",DataButtonClickHandler}
};
}
And
if (button == DataButton.Update)
{
DynamicComponentType = typeof(ShowTeam);
DynamicComponentParams = new Dictionary<string, object>()
{
{"SelectedTeam",item },
{"DataButtonClick",DataButtonClickHandler}
};
}
When you click on the Cancel button in the UpdateTeam component, the callback
function replaces UpdateTeam with ShowTeam to reflect the read-only display of
the selected Team. When you click on the Save button on the UpdateTeam.razor
component, the callback function loads the ShowTeam component and will reflect
the changes made by the user. Although these pieces of code are identical, it's
better to keep them separate in case you need to plug-in some different logic
for the Cancel and Save buttons.
This completes all the master components.
Open the MasterDetailContainer.razor component that resides in the Pages
folder (you added it in the previous part of this article series). And add the
following code:
@page "/masterdetail"
<h1>Master Detail Demo</h1>
@code {
}
<ListTeams></ListTeams>
As you can see the MasterDetailContainer component is associated with /masterdetail
route. And it houses the ListTeams component you created earlier.
You can now run the application and navigate to /masterdetail.
You should see the Teams listing as before. The following figure shows Team 1
in read-only mode:
And the following figure shows Team 1 in edit mode:
Finally, the following figure shows the modified Team details reflected in
the table:
In this part of this article series you completed the master CRUD components.
In the next part of this series you will begin developing the detail components.
That's it for now! Keep coding!!