Monday, October 28, 2024

ASP.NET Core - Basics of Razor Page

Objectives
  • To learn about Basics of Razor Page
  • Get handler method to get list of products
  • Get handler method to get details of a product
  •  Handler methods naming conventions
Project:
  1. Open the Visual Studio. 
  2. Create web project using ASP.NET Core Razor Page template.
  3. Project name: ReadJsonData
  4. Solution name: ReadJsonData
  5. .NET Core version: 8.0
  6. Add the AddRazorPages() service in Program class.
  7. Use MapRazorPages() as terminal middleware in Program class.
Look at the below updated code in Program class.
namespace ReadJsonData
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);

            // Add services to the container.
            builder.Services.AddRazorPages();

            var app = builder.Build();

            // Configure the HTTP request pipeline.
            if (!app.Environment.IsDevelopment())
            {
                app.UseExceptionHandler("/Error");
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }

            app.UseHttpsRedirection();
            app.UseStaticFiles();

            app.UseRouting();

            app.UseAuthorization();

            app.MapRazorPages();

            app.Run();
        }
    }
}
wwwroot/files/data.json:
Add files folder in wwwroot folder and add a data.json file in it:

[
  {
    "id": 1,
    "name": "Item 1",
    "description": "Description of Item 1"
  },
  {
    "id": 2,
    "name": "Item 2",
    "description": "Description of Item 2"
  }
]
Product Model class:
To read data from the data.json  file, we create a model class called Product as given below which maps the keys of the JSON file.

using System.Text.Json.Serialization;
namespace ReadJsonData.Models;
public class Product
{
    [JsonPropertyName("id")]
    public int Id { get; set; }

    [JsonPropertyName("name")]
    public string? Name { get; set; }

    [JsonPropertyName("description")]
    public string? Description { get; set; }
}
Note that we have used [JsonPropertyName] attribute so that Id and id etc. remain immaterial when model properties are mapped to data.json keys.

Index Razor Page:
The objective is to display the list of products in Index.cshtml file. For this we write the following code:
@page
@model IndexModel
@{
    ViewData["Title"] = "Home page";
}

<div>
    <ul>
        @foreach(var item in Model.Items)
        {
            <li>
                <strong>@item.Name</strong> - @item.Description
            </li>

        }
    </ul>
</div>

Get Handler method to list products:
Now we look at the logic of Get handler method in Index.cshtml.cs file.

using Microsoft.AspNetCore.Mvc.RazorPages;
using ReadJsonData.Models;
using System.Text.Json;

namespace ReadJsonData.Pages
{
    public class IndexModel : PageModel
    {
        private readonly IWebHostEnvironment _environment;
        public List<Product>? Items { get; private set; }
        public Product? ItemDetails { get; private set; }

        public IndexModel(IWebHostEnvironment env)
        {
            _environment = env;
        }

        // The OnGetAsync method fetches data and returns
        // which is displayed in razor page. Note that, here in this example, it returns a List<T>
        public async Task OnGetAsync()
        {
          var filePath =  Path.Combine(_environment.WebRootPath, "files", "data.json");
            if (System.IO.File.Exists(filePath))
            {
                var jsonData = await System.IO.File.ReadAllTextAsync(filePath);
                Items = JsonSerializer.Deserialize<List<Product>>(jsonData);
            }
            else
            {
                Items = new List<Product>(); // Empty list if file is not found
            }
        }
    }
}

Important Notes:
If you rename OnGetAsync() to OnGetByIdAsync(), the page will no longer automatically handle the GET request when you navigate to it in your browser. In Razor Pages, the naming convention for handler methods is critical. Razor Pages uses specific method names for each HTTP verb to determine which handler to call when a request is received:
  • OnGetAsync or OnGet for GET requests
  • OnPostAsync or OnPostAsync for POST requests
  • OnPutAsync or OnPutAsync for PUT requests, and so on
You can use Named handler but then you will have to make some changes in your razor page. Click this link for details about this.

Get Handler method to get details of a product:
To get Information about a product, we can either modify the existing code as done below:

using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;

public class IndexModel : PageModel
{
    private readonly IWebHostEnvironment _environment;

    public IndexModel(IWebHostEnvironment environment)
    {
        _environment = environment;
    }

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

    public Item ItemDetails { get; private set; }

    public async Task<IActionResult> OnGetAsync()
    {
        var filePath = Path.Combine(_environment.WebRootPath, "files", "data.json");

        if (System.IO.File.Exists(filePath))
        {
            var jsonData = await System.IO.File.ReadAllTextAsync(filePath);
            var items = JsonSerializer.Deserialize<List<Item>>(jsonData);

            // Find the item with the specified Id
            ItemDetails = items?.FirstOrDefault(i => i.Id == Id);
        }

        if (ItemDetails == null)
        {
            return NotFound(); // Return 404 if the item is not found
        }

        return Page();
    }
}
Since in same Index.cshtml.cs file both - Get all products and Get a product - logic is implemented, we will have to use its corresponding front page i.e. Index.cshtml for both cases in a way so that onlyone case is displayed at a time:

@page "{Id:int}"
@model IndexModel

<h2>Item Details</h2>

@if (Model.ItemDetails != null)
{
    <p><strong>ID:</strong> @Model.ItemDetails.Id</p>
    <p><strong>Name:</strong> @Model.ItemDetails.Name</p>
    <p><strong>Description:</strong> @Model.ItemDetails.Description</p>
}
else
{
    <p>Item not found.</p>
}
Note that if you prefer to pass the Id as a query string (e.g., /Index?Id=1), change the Razor Page directive to:
@page
@model IndexModel
This way, you can access the item using /Index?Id=1 instead of /Index/1.

Another Approach

You can also use a separate Razor page for Get Product by Id. Using a separate .cshtml file for displaying individual item details is often a good idea, as it:
  • Keeps Code Organized: Separates the list view (all items) from the detail view (individual item), making each page more focused.
  • Improves Readability and Maintainability: You avoid complex conditional logic in a single Razor Page to handle both list and detail views.
  • Enables Different Layouts: Allows you to use different layouts or styles for the list and detail views if needed.
Here’s how you can set up a separate .cshtml file for displaying individual item details.

Step 1: Create a New Razor Page for Product Details
  • Add a New Razor Page in your Pages folder, e.g., Details.cshtml.
  • Set up the New Page Model: In the code-behind file (Details.cshtml.cs), set it up to accept an Id parameter, load the JSON data, and filter it for the specific item.

using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;

public class DetailsModel : PageModel
{
    private readonly IWebHostEnvironment _environment;

    public DetailsModel(IWebHostEnvironment environment)
    {
        _environment = environment;
    }

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

    public Item ItemDetails { get; private set; }

    public async Task<IActionResult> OnGetAsync()
    {
        var filePath = Path.Combine(_environment.WebRootPath, "files", "data.json");

        if (System.IO.File.Exists(filePath))
        {
            var jsonData = await System.IO.File.ReadAllTextAsync(filePath);
            var items = JsonSerializer.Deserialize<List<Item>>(jsonData);

            // Find the item with the specified Id
            ItemDetails = items?.FirstOrDefault(i => i.Id == Id);
        }

        if (ItemDetails == null)
        {
            return NotFound();
        }

        return Page();
    }
}
Step 2: Design the Details.cshtml Page
In Details.cshtml, display the item’s properties:

@page "{Id:int}"
@model DetailsModel

<h2>Item Details</h2>

@if (Model.ItemDetails != null)
{
    <p><strong>ID:</strong> @Model.ItemDetails.Id</p>
    <p><strong>Name:</strong> @Model.ItemDetails.Name</p>
    <p><strong>Description:</strong> @Model.ItemDetails.Description</p>
}
else
{
    <p>Item not found.</p>
}
Step 3: Update the List Page to Link to the Details Page
In your main list page (e.g., Index.cshtml), add a link to the Details page for each item:

@page
@model IndexModel

<h2>Items</h2>
<ul>
    @foreach (var item in Model.Items)
    {
        <li>
            <strong>@item.Name</strong> - @item.Description
            <a asp-page="./Details" asp-route-id="@item.Id">View Details</a>
        </li>
    }
</ul>
Here, asp-page="./Details" and asp-route-id="@item.Id" will create a link to Details for each item by passing the Id.

Benefits of This Approach
  • Separation of Concerns: The list view (Index.cshtml) and detail view (Details.cshtml) are separated, making each view simpler and more focused.
  • Customizability: You can apply distinct layouts, styles, or additional functionality to the detail page without affecting the list page.
This setup will keep your Razor Pages more maintainable and user-friendly.
What will be if you use a custom handler name e.g. OnGetByIdAsync(int Id):
public async Task OnGetByIdAsync(int Id)
{
    // Your code
}
If you want to use a custom handler name, you can use the @page directive to specify a handler parameter in the URL, like so:
@page "{Id:int}/by-id"
@model ReadJsonData.Pages.DetailsModel
You’d then navigate to /Details/1/by-id to trigger the OnGetByIdAsync handler for an item with Id = 1. Alternatively, if you keep the method name as OnGetAsync, the default routing will continue to work without any additional changes.

Another example:
Let we have the handler in Razor page as given below:
public void OnGetChangedCountry()
{
    // Logic specific to changing the country, like loading cities for the selected country
}
Then the handler will be ChangedCountry as given below:
<form method="get">
    <select name="country" asp-page-handler="ChangedCountry">
        <option value="USA">USA</option>
        <option value="Canada">Canada</option>
    </select>
    <button type="submit">Submit</button>
</form>

To call this handler, the request URL would need to include ?handler=ChangedCountry (or it could be called from a form submission or JavaScript).

No comments:

Post a Comment

Hot Topics