Building a Hotel Management System With ASP.NET Core(#6) - Displaying and Updating Related Data
Repository
https://github.com/dotnet/core
What Will I Learn?
- You will learn how to display related data of one entity in another.
- You will learn how to Edit/Update related data of one entity in another.
- You will learn how to display Selected features of a Room in our Hotel Management System
- You will learn about
HashSets
in C# - You will learn why and when to use View Models
- You will learn about the importance of persisting a DbContext's changes to the changes to the Database when executing several write actions in a method.
Requirements
- Basic knowledge of C# programming language
- Visual Studio 2017/ VS code/ Any suitable code editor
- Previous Tutorial
Difficulty
- Intermediate/Advanced
Tutorial Contents
In the previous tutorial, we learnt how to link two entities with ManyToMany relationship, specifically, the Room and Feature Entities. Today we are going leverage on our table relationships to display and update related features data in our room views.
Creating Our FeaturesController
Prior to now, we have only implemented the RoomTypes
and Rooms
Controllers. Creating the FeaturesController
takes a similar fashion as that of the RoomTypesController
. Refer to this tutorial to see how the controller was implemented.
View the Complete Controller Implementation here - https://github.com/Johnesan/TheHotelApplication/blob/master/TheHotelApp/Controllers/FeaturesController.cs
Confirm that your controller is all set up, by running the application and trying to perform a few CRUD operations on the controller actions. If all was implemented correctly, you should be able to create, edit, view and delete features. Here, I have added three features in my application
Displaying Related Features in RoomsController
Our aim here is to allow the administrator to add appropriate features to the ICollection<RoomFeature> Features
navigation property in the Room model entity as well as display this data when requested.
Creating a ViewModel
- Why Do we need a ViewModel?
In the Create view, we are going to present the administrator a list of checkboxes representing all the available features in our application. Depending on the features he checks, we update the RoomFeatureRelationship table with the selected features and the base room instance.
To determine whether or not a particular feature is related to the room in question, we need a boolean property that will be bound to thechecked
property of the checkbox. However, ourRoomFeature
Entity does not contain a definition for any such boolean property. For this purpose, we need a ViewModel.
In your ViewModels Folder, create a new class --> SelectedRoomFeatureViewModel.cs
:
public class SelectedRoomFeatureViewModel
{
public string FeatureID { get; set; }
public virtual Feature Feature { get; set; }
public bool Selected { get; set; }
}
Modifying Our Create and Edit Methods [GET]
To serve our view this list of features to populate the checkboxes, we send this list via a ViewData
.
To perform this task of populating the Room.Features
with the right selected items, we implement a new method in our service class.
In our Services folder, Open the IGenericHotelService.cs
class and add this code:
List<SelectedRoomFeatureViewModel> PopulateSelectedFeaturesForRoom(Room room);
- This implementation simply takes in a room entity and returns a list of
SelectedRoomFeatureViewModel
type, which of course has a boolean property specifying if that particular feature is related to the room entity in question.
Implementing this method in our GenericHotelService.cs
class:
GenericHotelService.cs
public List<SelectedRoomFeatureViewModel> PopulateSelectedFeaturesForRoom(Room room)
{
var viewModel = new List<SelectedRoomFeatureViewModel>();
var allFeatures = _context.Features;
if (room.ID == "" || room.ID == null)
{
foreach(var feature in allFeatures)
{
viewModel.Add(new SelectedRoomFeatureViewModel
{
FeatureID = feature.ID,
Feature = feature,
Selected = false
});
}
}
else
{
var roomFeatures = _context.RoomFeatureRelationships.Where(x => x.RoomID == room.ID);
var roomFeatureIDs = new HashSet<string>(roomFeatures.Select(x => x.FeatureID));
foreach (var feature in allFeatures)
{
viewModel.Add(new SelectedRoomFeatureViewModel
{
FeatureID = feature.ID,
Feature = feature,
Selected = roomFeatureIDs.Contains(feature.ID)
});
}
}
return viewModel;
}
Explanation for the Code Block Above
The first line creates a new instance of a List of SelectedRoomFeatureViewModel -->
var viewModel = new List<SelectedRoomFeatureViewModel>();
. This instance is what we are going to modify and thereafter return.The next line fetches all the features available in our database via the ApplicationDbContext instance.
var allFeatures = _context.Features;
. Each feature in this list is going to be modified and placed inside the viewModel to be sent to the view.Next, we perform a check to ascertain if the room's ID is null(i.e if the room is a new room entity whose ID hasn't been set).
If the condition is true, we loop through allFeatures, create a new viewModel using each of the features and set their
Selected
property tofalse
.
if (room.ID == "" || room.ID == null)
{
foreach(var feature in allFeatures)
{
viewModel.Add(new SelectedRoomFeatureViewModel
{
FeatureID = feature.ID,
Feature = feature,
Selected = false
});
}
}
If the condition fails, then we make a call to the
RoomFeatureRelationships
DbSet and select rows related to just the room in question.var roomFeatures = _context.RoomFeatureRelationships.Where(x => x.RoomID == room.ID);
We then create a new HashSet of these rows, selecting the FeatureID.
var roomFeatureIDs = new HashSet<string>(roomFeatures.Select(x => x.FeatureID));
A HashSet is an unordered Collection that holds a set of objects, but in a way that it allows you to easily and quickly determine whether an object is already in the set or not. It achieves this by internally managing an array and storing the object using an index which is calculated from the hashcode of the object.
- Next, we loop through allFeatures and create a viewModel for each instance of feature. However, unlike the first loop, here we check if the feature's ID is present in the HashSet of our
roomFeatureIDs
. If present, ourSelected
property is set to true, otherwise false.
foreach (var feature in allFeatures)
{
viewModel.Add(new SelectedRoomFeatureViewModel
{
FeatureID = feature.ID,
Feature = feature,
Selected = roomFeatureIDs.Contains(feature.ID)
});
}
Calling the PopulateSelectedFeaturesForRoom()
Method in Our Controller Action
Now that we have a method to populate the list of features for a room entity, all that is left is to pass it to our view for use. To do this, we use a ViewData
or ViewBag
- In our Create and Edit Methods [GET], just before returning the view, put the following code
ViewData["Features"] = _hotelService.PopulateSelectedFeaturesForRoom(room);
Note that the room argument supplied is the current room being considered.
Displaying The Data in Our View
Now to finally display this data, we go to our Create(and Edit) view. Just before the submit button input, add the following:
<div class="form-group">
<label asp-for="Features" class="control-label"></label>
@foreach (var roomFeature in ViewBag.Features as IEnumerable<SelectedRoomFeatureViewModel>)
{
<p>
<input type="checkbox" name="SelectedFeatureIDs" value="@roomFeature.FeatureID" @Html.Raw(roomFeature.Selected ? "checked=\"checked\"" : "") /> @roomFeature.Feature.Name
</p>
}
</div>
- Here, the View accesses the ViewBag.Features (same thing as ViewData["Features"]) we sent from the Controller action. It casts it to the appropriate type -
List<SelectedRoomFeatureViewModel>
and iterates through the list. - For each item in the list, it creates a checkbox and binds the "check" property to the
Selected
property of the feature.
Run the project and you should see the following View
Modifying Our Create and Edit Methods [POST]
We have successfully provided the admin an interface to select and alter a room's features. Now we have to write actions that will be performed to persist our user's preference in the database.
Just like we defined the PopulateSelectedFeaturesForRoom(Room room)
method to handle the viewModel population, we are going to define another method to handle updating the features for a particular room.
Like before, we first define the implementation in our interface(IGenericHotelService.cs). Then we implement it in our GenericHotelService.cs class. Below is the implementation:
public void UpdateRoomFeaturesList(Room room, string[] SelectedFeatureIDs)
{
var PreviouslySelectedFeatures = _context.RoomFeatureRelationships.Where(x => x.RoomID == room.ID);
_context.RoomFeatureRelationships.RemoveRange(PreviouslySelectedFeatures);
_context.SaveChanges();
if (SelectedFeatureIDs != null)
{
foreach (var featureID in SelectedFeatureIDs)
{
var AllFeatureIDs = new HashSet<string>(_context.Features.Select(x => x.ID));
if (AllFeatureIDs.Contains(featureID))
{
_context.RoomFeatureRelationships.Add(new RoomFeature
{
FeatureID = featureID,
RoomID = room.ID
});
}
}
_context.SaveChanges();
}
}
Explanation for the Code Block Above
- Firstly, we notice that this method takes in two parameters: the
room
to be updated and the list ofSelectedFeatureIDs
to be related with it.
We get this list of strings from the Action parameters:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(string id, [Bind("ID,Number,RoomTypeID,Price,Available,Description,MaximumGuests")] Room room, string[] SelectedFeatureIDs)
{
.
.
.
}
This array of strings is passed from the view's checkbox properties.
- The next set of statement gets the previously linked Features to the room and removes those relationship from the
RoomFeatureRelationships
Table.
var PreviouslySelectedFeatures = _context.RoomFeatureRelationships.Where(x => x.RoomID == room.ID);
_context.RoomFeatureRelationships.RemoveRange(PreviouslySelectedFeatures);
_context.SaveChanges();
Note that we call the SaveChanges()
method after altering the table. Failure to do this might result in an error similar to this
The reason for this error is due to the fact that we are going to make overriding changes to this same instance of the _context
while there is pending action on it that hasn't been persisted on the database.
- Next, we check if the
SelectedFeatureIDs
array is null. If not, we loop through all the featureIDs in the array, check our Features DbSet to ascertain if it is a valid ID linked to a feature in the database, if yes, we create a relationship between the featureID and the roomID. Thereafter, weSaveChanges()
to persist our changes to the database.
if (SelectedFeatureIDs != null)
{
foreach (var featureID in SelectedFeatureIDs)
{
var AllFeatureIDs = new HashSet<string>(_context.Features.Select(x => x.ID));
if (AllFeatureIDs.Contains(featureID))
{
_context.RoomFeatureRelationships.Add(new RoomFeature
{
FeatureID = featureID,
RoomID = room.ID
});
}
}
_context.SaveChanges();
}
Now Run the application and Try to Edit a Room Entity's Features, then Save it. Go back to the edit page again and check to see that it has been updated. Make changes to the selected features, save and come back again. You find that everything works as expected.
Curriculum
- Building a Hotel Management System With ASP.NET Core(#1) - Introduction
- Building a Hotel Management System With ASP.NET Core(#2) - Building the Service Layer
- Building a Hotel Management System With ASP.NET Core(#3) - Building the RoomType Controller Logic
- Building a Hotel Management System With ASP.NET Core(#4) - Building the Room Controller Logic
- Building a Hotel Management System With ASP.NET Core(#5) - Complex Data Relationships(ManyToMany Relationship Linking)
Proof of Work Done
Github Repo for the tutorial solution:
https://github.com/Johnesan/TheHotelApplication
Thank you for your contribution.
While I liked the content of your contribution, I would still like to extend few advices for your upcoming contributions:
Looking forward to your upcoming tutorials.
Your contribution has been evaluated according to Utopian policies and guidelines, as well as a predefined set of questions pertaining to the category.
To view those questions and the relevant answers related to your post, click here.
Need help? Write a ticket on https://support.utopian.io/.
Chat with us on Discord.
[utopian-moderator]
Hey @johnesan
Thanks for contributing on Utopian.
We’re already looking forward to your next contribution!
Contributing on Utopian
Learn how to contribute on our website or by watching this tutorial on Youtube.
Want to chat? Join us on Discord https://discord.gg/h52nFrV.
Vote for Utopian Witness!
great work!