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 theLength
property of themessage
.required
KeywordStarting with C# 11, we can utilize the
required
keyword to compel developers to initialize a property. Learn more aboutrequired
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:
The
OnGet
method serves to manage HTTP GET requests to the page. It endeavours to locate a tenant based on theId
. In the event that it's not found, it redirects to theIndex
page.The
OnPost
method servers to manage HTTP POST requests to the page. Once more, it attempts to locate a tenant based on theId
. Should it be found, the tenant will be updated with new values, and a success message will be stored inTempData
for display on theIndex
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:
asp-page="Delete"
The target page to which we will redirect the user.
asp-page-handler="Delete"
The name of the handler method that will process the request, calling
OnGetDelete
method as previously mentioned.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, whileid
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.