Building a Hotel Management System With ASP.NET Core(#6) - Displaying and Updating Related Data

in #utopian-io3 years ago (edited)

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

img8.PNG

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 FeaturesControllertakes 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 the checked property of the checkbox. However, our RoomFeature 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 to false.

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, our Selected 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 of SelectedFeatureIDs 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, we SaveChanges() 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

Proof of Work Done

Github Repo for the tutorial solution:
https://github.com/Johnesan/TheHotelApplication

Sort: