Create master detail components in Blazor Server (Project Structure)
In the previous part
of this series you were introduced to the UI and overall functioning of
the master-detail Blazor Server app. You also created Team and TeamMembers
tables and EF Core model. You have already created the Blazor Server project.
Now it's time to kick start the component development.
To build the master detail UI you could have but all the markup and the code
in a single Razor component. However, for the sake of better code organization
you will divide the overall UI into a set of components. There will be a
container component that will load the master and detail tables. The CRUD
operations are taken care by individual Razor components. The container
component is made available at URL - /masterdetail.
The following figure shows the Razor components and auxiliary classes you
will add as you progress through the example.
Observe the above figure carefully. The Pages folder contains the container
Razor component MasterDetailContainer.razor. Since this component is assigned
some URL it's put in the Pages folder.
The Shared folder contains two subfolders namely Teams and TeamMembers. The
Teams folder contains four Razor components namely ListTeam.razor,
ShowTeam.razor, InsertTeam.razor, and UpdateTeam.razor. These four components
are responsible for CRUD operations on the Teams table.
The TeamMembers folder also conatins four Razor components namely
ListTeamMembers.razor, ShowTeamMember.razor, InsertTeamMember.razor, and
UpdateTeamMember.razor. These four components are responsible for CRUD
operations on the TeamMembers table.
If you see the master detail UI, there are several buttons such as Insert,
Edit, Update, Delete, Save, and Cancel. In your code you need to detect which of
these buttons were clicked by the user. Depending on the button clicked your
course of action changes. For example, if a user clicks on Edit button then you
need to load the component that renders the edit UI whereas if a user clicks on
Cancel button, you need to cancel the ingoing operation and discard the changes.
To help you in this process you create DataButton enumeration.
Upon clicking on any of the data buttons mentioned above you need to handle
that button's click event from the parent Razor component. You accomplish this
by creating custom events and callbacks in your components. The Team related
Razor components use Action based callbacks whereas TeamMember related
components use EventCallbacks. You can stick to either of the techniques but I
am showing both for the sake of your learning and understanding. The
TeamMemberEventArgs class shown in the figure is used by the EventCallback
technique (it will be clear when you develop TeamMember related components).
There is also DynamicPlaceHolder.razor component. The CRUD components
discussed above are loaded dynamically depending on the user's action. Initially
when no action is taken by the user (app has just started) you need to load this
empty placeholder component.
Now that you are aware of the overall project structure and various pieces
need by the app, let's create these pieces one-by-one.
Add a new enumeration named DataButtons (you can place it in the project's
root folder) as shown below:
public enum DataButton
{
Insert,
Edit,
Update,
Delete,
CancelReadMode,
CancelEditMode,
CancelInsertMode
}
The DataButton enumeration contains a set of items that indicate a button
being clicked by the user. They include Insert, Edit, Update, Delete, and Cancel
button from read / edit / insert mode UI respectively.
Now add Teams subfolder under Shared folder and add four Razor components
namely ListTeams.razor, ShowTeam.razor, InsertTeam.razor, and UpdateTeam.razor
in it.
The ListTeams component displays a list of teams in a table like this:
Since you need to fetch the teams data from the Teams table you need to
inject the AppDbContext in the component (you could have created a repository or
service but to keep things simple you will inject AppDbContext directly). So,
write the following code at the top of the ListTeams.razor component.
@inject AppDbContext db
Then add Items and SelectedItem properties in the @code block as shown below:
@code {
public List<Team> Items { get; set; }
public Team SelectedItem { get; set; }
}
These properties are assigned in the OnInitialized life cycle method as shown
below:
protected override void OnInitialized()
{
Items = db.Teams.ToList();
this.StateHasChanged();
}
The SelectedItem property is used while managing the TeamMembers and it is
assigned in the click event handler of Manager Members button.
To render the Teams table you need to iterate through the Items and render
the table rows as shown below:
<h3>List of Teams</h3>
<br />
<button @onclick="OnInsertClick">Insert</button>
<br /><br />
<table border="1" cellpadding="10">
<tr>
<th>Team ID</th>
<th>Name</th>
<th>Description</th>
<th colspan="2" align="center"></th>
</tr>
@foreach(var item in Items)
{
@if(item.TeamID == SelectedItem?.TeamID)
{
<tr class="SelectedRow">
<td>@item.TeamID</td>
<td>@item.Name</td>
<td>@item.Description</td>
<td><button @onclick="()=>
OnManageTeamClick(item)">Manage Team</button></td>
<td><button @onclick="()=>
OnManageMembersClick(item)">Manage Members</button></td>
</tr>
}
else
{
<tr>
<td>@item.TeamID</td>
<td>@item.Name</td>
<td>@item.Description</td>
<td><button @onclick="()=>
OnManageTeamClick(item)">Manage Team</button></td>
<td><button @onclick="()=>
OnManageMembersClick(item)">Manage Members</button></td>
</tr>
}
}
</table>
As you can see, at the top there is Insert button and its click event is
handled by the OnInsertClick() method. You will write this method shortly.
A foreach loop iterates through the Items and displays TeamID, Name, and
Description. Two buttons Manage Team and Manage Members are also rendered. The
click event of the Manage Team button is handled by the OnManageTeamClick()
method. and the click event of the Manage Members is handled by the
OnManageMembersClick() method. The relevant Team object is also passed to these
event handlers so that the event handlers know which Team is under
consideration.
Notice that the code checks the item's TeamID with the SelectedTeam's TeamID.
If they match that table row is displayed with a highlight (using SelectedRow
CSS class).
The ShowTeam, InsertTeam, and UpdateTeam components are loaded dynamically
rather than placing them in ListTeams at the development time. This is done
using <DynamicComponent> component of Blazor. The <DynamicComponent> is placed
below the table by adding this markup:
<DynamicComponent
Type="@DynamicComponentType"
Parameters="@DynamicComponentParams" />
The <DynamicComponent> allows you to load a component dynamically by
specifying a component type using the Type property. In this example the Type
comes from DynamicComponentType property of ListTeams component. You will write
this property shortly. A dynamically loaded component may need parameters for
its functioning. These parameters are passed using the Parameters dictionary. In
this example, you will create a DynamicComponentParams property that serves this
purpose.
The DynamicComponentType and DynamicComponentParams properties are
shown below:
@code {
public List<Team> Items { get; set; }
public Team SelectedItem { get; set; }
public Type DynamicComponentType { get; set; }
public Dictionary<string, object>
DynamicComponentParams { get; set; }
...
...
}
When you click on any of the DataButtons such as Insert, Edit, Save, Delete,
or Cancel an event is raised by the dynamically loaded component that is handled
by ListTeams component. To handle that event ListTeams contains a function with
this signature:
public void OnDataButtonClick(DataButton button, Team item)
{
}
The first parameter is DataButton that tells you which button was clicked
(Insert, Update, Cancel etc.) and the second parameter tells you which Team
object is to be used for the corresponding operation. So, add this
function inside the code block of ListTeams. Keep it empty for time being. You
will add code to it later.
This OnDataButtonClick() handler function is to be passed as a parameter to
the dynamically loaded component. So, we create a property as follows:
public Action<DataButton,Team> DataButtonClickHandler
{ get; set; }
And you initialize it to point to OnDataButtonClick() in the OnInitialized
life cycle method:
protected override void OnInitialized()
{
Items = db.Teams.ToList();
DataButtonClickHandler = new Action<DataButton, Team>
(OnDataButtonClick);
this.StateHasChanged();
}
When the application runs for the first time, no DataButton is clicked and
hence no dynamic component will be loaded. However, you can't keep <DynamicComponent>
uninitialized. So, you will add an empty placeholder component called
DynamicPlaceHolder.razor inside the Shared folder.
Then you load this empty placeholder in the OnInitialized() method.
protected override void OnInitialized()
{
DynamicComponentType = typeof(DynamicPlaceHolder);
Items = db.Teams.ToList();
DataButtonClickHandler = new Action<DataButton, Team>
(OnDataButtonClick);
this.StateHasChanged();
}
Next, you need to write the three click event handlers namely OnInsertClick(),
OnManageTeamClick(), OnManageMembersClick().
The OnInsertClick() event handler is called when you click on the Insert
button and is shown below:
public void OnInsertClick()
{
DynamicComponentType = typeof(InsertTeam);
DynamicComponentParams = new Dictionary<string, object>()
{
{"DataButtonClick",DataButtonClickHandler }
};
}
Inside the OnInsertClick() event handler you want to load InsertTeam
component that displays a UI for adding a new Team. Therefore, you set the
DynamicComponentType property to the type of InsertTeam. Upon saving the newly
added Team in the database the InsertTeam would want to notify ListTeams that
the operation has been completed. To accomplish this the InsertTeam component
has DataButtonClick event. So, you also need to pass the DataButtonClick handler
function to the InsertTeam component using DynamicComponentParams dictionary.
Recollect that you have added InsertTeam component already but it's empty as of
now. You will complete it as you proceed with this example.
The OnManageTeamClick() event handler does something similar but there are a
few differences as shown below:
public void OnManageTeamClick(Team item)
{
DynamicComponentType = typeof(ShowTeam);
DynamicComponentParams = new Dictionary<string, object>()
{
{"SelectedTeam",item },
{"DataButtonClick",DataButtonClickHandler}
};
this.StateHasChanged();
}
As you can see, OnManageTeamClick() receives a Team object for which the
button has been clicked. Inside, you set the DynamicComponentType property to
the type of ShowTeam. The ShowTeam component is responsible for displaying the
Team in read only table. You can then edit or delete that Team. The
DynamicComponentParams dictionary contains two items - SelectedTeam and
DataButtonClick. You pass SelectedTeam so that ShowTeam can display its details.
The OnManageMembersClick() is quite straightforward and simply sets the
SelectedItem property as shown below:
public void OnManageMembersClick(Team item)
{
SelectedItem = item;
this.StateHasChanged();
}
This completes ListTeams.razor component.
In the next part of this series you will develop ShowTeam, InsertTeam, and
UpdateTeam components.
That's it for now! Keep coding!!