5. Actions for Organizations (Tenants)

5. Actions for Organizations (Tenants)

·

12 min read

In our previous post, we introduced a Razor Page tailored for tenants, featuring three essential action buttons: Add, Edit and Delete. In this post, we'll delve into the implementation of these buttons' functionalities.

Non-null Value Warning

Before we begin, it's important to mention that Visual Studio emits a few warnings when we open the project.

Let's open one of the classes, UserViewModel which shows the warnings.

At first glance, the class UserViewModel appears to be error-free, with no obvious issues such as missing semicolons (;) or incorrect keyword names.

Nevertheless, since the release of C# 8, Microsoft introduced a feature known as nullable reference types, which can result in these warnings. The objective of this feature is to reduce the likelihood of our code triggering a NullReferenceException, which happens to be the most frequent exception when null values are inadvertently accessed. This feature aims to make null-related issues easier to identify and less troublesome to rectify.

Consider the following example which is a code excerpt from the Microsoft website.

string message = null;

// warning: dereference null.
Console.WriteLine($"The length of the message is {message.Length}");

var originalMessage = message;
message = "Hello, World!";

// No warning. Analysis determined "message" is not null.
Console.WriteLine($"The length of the message is {message.Length}");

// warning!
Console.WriteLine(originalMessage.Length);

If you create a console application with the code provided in Visual Studio, it will build without any issues.

However, when you run the project, you will get a NullReferenceException error!

This is precisely why the Nullable Reference Types feature is essential. It conducts null-state analysis and issues warnings to developers like you and me. You can find further information about it here.

There are a few ways we can address this warning.

  • Initialize non-nullable properties. We can just assign a default value to it.

      //string message = null;
      string message = "First Message";
    
  • Change to a nullable type

      //string message = null;
      string? message = null;
    

    This applies to the variable that we know might be null. In this case, additional checks are necessary, as illustrated below:

      if (message != null)
      {
          Console.WriteLine($"The length of the message is {message.Length}");
      }
    
  • Disable the warning

      #nullable disable
          string message = null;
      #nullable restore
    

    We can suppress the warning by using preprocessor directives.

    If we intend to deactivate these warnings at the project level, we can include or set <Nullable>disable</Nullable> in our .csproj file.

  • Null Forgiving Operator !

    Null-forgiving operator ! can help suppress all nullable warnings like the below:

      string message = null!;
    

    However, our code will still throw exceptions.

    A more common usage of this operator is to evaluate the result of the underlying expression. For example:

      Console.WriteLine($"The length of the message is {message!.Length}");
    

    At runtime, the message will be evaluated to determine its actual value before accessing the Length property of the message.

  • required Keyword

    Starting with C# 11, we can utilize the required keyword to compel developers to initialize a property. Learn more about required keyword here.

      public class People
      {
          public required string FirstName { get; set; }
      }
    

    However, there is a caveat: the required keyword is only accessible from C# 11. In our current project, we're targeting .NET 6.0, and the default C# language version is C# 10. You can verify the default C# language versions for other frameworks by checking here.

Returning to our project, we can resolve these warnings by modifying properties to be nullable.

Make sure to address the remaining warnings found in various parts of the project as well.

Adding New Tenant

With the code cleanup completed, let's proceed to the next phase.

Let's assume that we store all tenants in a static variable storage. This way, we can add a new tenant to that static variable and retrieve it later for displaying all tenants, including the new one, in the table.

Now, let's make modifications to the code in the Index.cshtml.cs file located in the Pages\Tenants folder.

using Microsoft.AspNetCore.Mvc.RazorPages;
using Portal.Models;

namespace Portal.Pages.Tenants;

public class IndexModel : PageModel
{
    public List<OrganizationViewModel> Organizations { get; set; } = OrganizationsData;

    public static List<OrganizationViewModel> OrganizationsData = new()
    {
        new OrganizationViewModel
        {
            Id = 1,
            Identifier = Guid.NewGuid(),
            Name = "Acme"
        },
        new OrganizationViewModel
        {
            Id = 2,
            Identifier = Guid.NewGuid(),
            Name = "Contoso"
        }
    };
}

Within the IndexModel class, we create a public static variable named OrganizationsData for the purpose of storing all tenant data. To initiate testing, I have included some tenants as a starting point.

Following that, let's create a new view model for adding a new tenant.

Please create a new AddOrganizationViewModel class in the Models folder, as demonstrated below:

namespace Portal.Models;

public class AddOrganizationViewModel
{
    public string? Name { get; set; }
}

Create a new Razor Page named Add in Pages\Tenants folder.

Replace the content of Add.cshtml.cs as shown below:

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Portal.Models;

namespace Portal.Pages.Tenants;

public class AddModel : PageModel
{
    [BindProperty]
    public AddOrganizationViewModel Organization { get; set; } = new();

    public void OnPost()
    {
        if (!ModelState.IsValid)
        {
            return;
        }

        var organization = new OrganizationViewModel
        {
            Id = IndexModel.OrganizationsData.Count + 1,
            Identifier = Guid.NewGuid(),
            Name = Organization.Name!
        };

        IndexModel.OrganizationsData.Add(organization);
    }
}

Please note that once a new tenant is created, it will be added to the OrganizationsData property within the IndexModel (located in Pages\Tenants\Index.cshtml.cs).

Replace the content of Add.cshtml as outlined below:

@page
@model AddModel
@{
    ViewData["Title"] = "Add Tenant";
}

<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
    <h1 class="h2">@ViewData["Title"]</h1>
    <div class="btn-toolbar mb-2 mb-md-0">
        <div class="btn-group me-2">
            <a class="btn btn-sm btn-outline-secondary" asp-page="Index">Back</a>
        </div>
    </div>
</div>

<form id="addOrgForm" method="post">
    <div class="mb-3 col-4">
        <label asp-for="Organization.Name">Name</label>
        <input class="form-control" asp-for="Organization.Name">
        <span asp-validation-for="Organization.Name" class="text-danger"></span>
    </div>
    <div class="col-6 text-right">
        <input id="addOrg" type="submit" class="btn btn-sm btn-outline-primary" value="Save" />
    </div>
</form>

@section Scripts {
    <partial name="_ValidationScriptsPartial" />

    <script>
        $(function () {
            $('#addOrg').on('click', function () {
                $('#addOrgForm').validate();
                if (($('#addOrgForm').valid() === false)
                        return false;

                return true;
            });
        });
    </script>
}

Press F5 to run the project. Click the Add button to create a new tenant, and then click Save afterward.

However, you will notice that it still displays the same Add Tenant page. How do we confirm that a tenant has been added?

Click the Back button on the Add Tenant page, and you will observe a new tenant added to the table.

To resolve the problem of the page displaying the same content after adding a tenant, let's include a success message to indicate that the tenant has been successfully added. We can utilize TempData to temporarily store this message and then display it on another page. Once the data is read from TempData, it will be deleted and won't be available for subsequent requests.

Let's incorporate this code into our AddModel class located in Add.cshtml.cs file:

In Index.cshtml file, add the following code to display the message from TempData:

@if (TempData["Message"] != null)
{
    <div class="alert alert-primary alert-dismissible fade show" role="alert">
        @TempData["Message"]
        <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
    </div>
}

The final code for Index.cshtml is as follows:

So, we are now capable of displaying the message if there is data available in TempData.

Press F5 to run the project, add a new tenant, and afterward, you should see the success message, providing a better user experience.

Updating Tenant

The Razor Page for updating a tenant is quite similar to the page for adding a new tenant.

Let's create a new view model class for updating tenants.

Please create a new EditOrganizationViewModel class in the Models folder, as demonstrated below:

using System.ComponentModel.DataAnnotations;

namespace Portal.Models;

public class EditOrganizationViewModel
{
    [Required]
    public int Id { get; set; }

    [Required]
    public string? Name { get; set; }
}

Next, create a new Razor Page called Edit in Tenants folder.

Replace the content of Edit.cshtml.cs as outlined below:

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Portal.Models;

namespace Portal.Pages.Tenants;

public class EditModel : PageModel
{
    [BindProperty(SupportsGet = true)]
    public int Id { get; set; }

    [BindProperty]
    public EditOrganizationViewModel Organization { get; set; } = new();

    public void OnGet()
    {
        var organization = IndexModel.OrganizationsData.FirstOrDefault(o => o.Id == Id);

        if (organization is null)
        {
            RedirectToPage("Index");
        }

        Organization = new EditOrganizationViewModel
        {
            Id = organization!.Id,
            Name = organization.Name
        };
    }

    public IActionResult OnPost()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        var organization = IndexModel.OrganizationsData.FirstOrDefault(o => o.Id == Id);

        if (organization is null)
        {
            RedirectToPage("Index");
        }

        organization!.Name = Organization.Name!;

        TempData["Message"] = $"Organization {organization.Name} updated successfully.";

        return RedirectToPage("Index");
    }
}

It is quite similar to the AddModel class, with the exception that it accepts the Id parameter through the Id property. The Id property needs to be sourced from a query parameter obtained from an HTTP GET request. Therefore, we need to set SupportsGet = true for the BindProperty attribute. Learn more about BindProperty attribute here. We will explore how to retrieve the Id query parameter from an HTTP GET request.

[BindProperty(SupportsGet = true)]
public int Id { get; set; }

Within the EditModel class, there are 2 methods:

  1. The OnGet method serves to manage HTTP GET requests to the page. It endeavours to locate a tenant based on the Id. In the event that it's not found, it redirects to the Index page.

  2. The OnPost method servers to manage HTTP POST requests to the page. Once more, it attempts to locate a tenant based on the Id. Should it be found, the tenant will be updated with new values, and a success message will be stored in TempData for display on the Index page.

Replace the content of Edit.cshtml as shown below:

@page "{id:int}"
@model EditModel
@{
    ViewData["Title"] = "Edit Tenant";
}

<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
    <h1 class="h2">@ViewData["Title"]</h1>
    <div class="btn-toolbar mb-2 mb-md-0">
        <div class="btn-group me-2">
            <a class="btn btn-sm btn-outline-secondary" asp-page="Index">Back</a>
        </div>
    </div>
</div>

<form id="editOrgForm" method="post">
    <div class="mb-3 col-4">
        <label asp-for="Organization.Name">Name</label>
        <input class="form-control" asp-for="Organization.Name">
        <span asp-validation-for="Organization.Name" class="text-danger"></span>
    </div>
    <div class="col-6 text-right">
        <input id="saveOrg" type="submit" class="btn btn-sm btn-outline-primary" value="Save" />
    </div>
</form>

@section Scripts {
    <partial name="_ValidationScriptsPartial" />

    <script>
        $(function () {
            $('#saveOrg').on('click', function () {
                $('#editOrgForm').validate();
                if (($('#editOrgForm').valid() === false)
                    return false;

                return true;
            });
        });
    </script>
}

On the first line of the code, @page "{id:int}" illustrates how we capture the Id query parameter from an HTTP GET request. @page is a directive that allows us to define a Route Template. The {id} part of the template serves as a placeholder representing any value added to the URL following Tenant/Edit/. By default, the parameter {id} is treated as a string type. However, we know that id should be of integer data type. Hence, we need to constrain the parameter value to an int data type. You can delve deeper into Routing in ASP.NET Core by referring to this resource.

@page "{id:int}"

Press F5 to run the project. Go to Tenants page and select one of the tenants to edit.

Change the name of the tenant and click Save button.

You are now able to see the success message after editing the tenant as shown below:

Deleting Tenant

Up to this point, we've successfully implemented functionality for the Add and Edit buttons. Both actions share a similar code structure and logic flow.

Regarding the Delete action, users can initiate deletions from two locations: the Index and Edit pages. Let's delve into how to implement the Delete button on both pages.

Edit page

So far, we only have Save button to save changes. Let's add another Delete button next to it.

<a class="btn btn-sm btn-outline-secondary" onclick="return confirm('Are you sure?')">Delete</a>

It's an HTML <a> tag that prompts the user for confirmation before proceeding with the deletion, enhancing the user experience.

Next, let's create a Razor Page named Delete.

Modify @page directive to capture Id parameter as shown below:

@page "{id:int}"

Replace the content of Delete.cshtml.cs as demonstrated below:

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace Portal.Pages.Tenants;

public class DeleteModel : PageModel
{
    [BindProperty(SupportsGet = true)]
    public int Id { get; set; }

    public IActionResult OnGetDelete()
    {
        IndexModel.OrganizationsData.RemoveAll(o => o.Id == Id);

        TempData["Message"] = "Organization deleted successfully.";

        return RedirectToPage("Index");
    }
}

We bind the Id parameter to the Id property using the BindProperty attribute, as shown above.

Take note that the name Delete is appended to OnGet, resulting in OnGetDelete. OnGet indicates that this method will manage HTTP GET requests. Occasionally, you may encounter OnPost, which handles HTTP POST requests. Both methods are the most commonly used handlers. Delete is merely a name you define as a handler name.

It's important to note that by default, Razor Pages do not support HTTP DELETE requests due to this limitation.

To address this limitation, the recommended approaches are to either use OnGet or OnPost with a handler name appended to either of them, for instance, OnGet<Handler> or OnPost<Handler>.

In our code, OnGetDelete indicates that we will handle deletions by sending an HTTP GET request to this Razor Page. Later on, we'll learn how to write the corresponding code asp-page-handler="Delete" to invoke the OnGetDelete method.

Within the OnGetDelete method, we remove tenants based on their respective Id. Following a successful operation, we redirect the user back to the Index page.

Returning to the Edit.cshtml, let's modify the code for the Delete button.

<a class="btn btn-sm btn-outline-secondary" asp-page="Delete" asp-page-handler="Delete" asp-route-id="@Model.Id" onclick="return confirm('Are you sure?')">Delete</a>

There are 3 additional attributes on the <a> HTML tag:

  1. asp-page="Delete"

    The target page to which we will redirect the user.

  2. asp-page-handler="Delete"

    The name of the handler method that will process the request, calling OnGetDelete method as previously mentioned.

  3. asp-route-id="@Model.Id"

    This attribute is composed of two parts. asp-route- is a tag helper that enables us to specify any route data parameter name, while id is the parameter we added after the hyphen.

With that said, let's press F5 to run the project.

Edit one of the tenants, and then click the Delete button.

At this stage, you will be prompted for confirmation.

Click OK button to confirm and you will see the output as shown below:

Please note that the browser did not display the Delete page at all because we immediately redirected the user back to the Index page upon completion.

Index page

The next page where we will implement the Delete action is the Index page, as demonstrated earlier.

Since we have already created a Delete Razor Page, we can simply adjust the code for the Delete button as shown below:

<a class="btn btn-outline-info" asp-page="Delete" asp-page-handler="Delete" asp-route-id="@organization.Id" onclick="return confirm('Are you sure?')">
    Delete
</a>

Press F5 to run the project. Click Delete button on one of the tenant rows.

You will be asked for confirmation as shown below:

Click OK button to confirm and a success message will be displayed.

Conclusion

In this post, we've successfully implemented functionality for the Add, Edit, and Delete actions. We've employed TempData to display success messages following our interactions with tenants. By passing the Id parameter in the Route Template, we can precisely locate specific tenants within our code. Since Razor Pages do not inherently support HTTP DELETE requests, we've learned to utilize the OnGet handler method for deletions. Additionally, we've explored the use of Null Reference Types to enhance the quality of our code.

Currently, we are utilizing a public static variable OrganizationsData to store tenant data. However, any changes made in this manner will be lost once the project is not running. In the next post, we will delve into persistently storing the data.

Download source code

Github repository

Did you find this article valuable?

Support Han Chee by becoming a sponsor. Any amount is appreciated!