ASP.NET Razor Pages: Structure, PageModel, and Data Handling

Understanding Razor Pages

Index.cshtml is often the application’s default page.

  • A Razor page is essentially an HTML file annotated with server-side code.
  • File extensions are .cshtml for C# annotations or .vbhtml for VB annotations.
  • The ASP.NET Razor view engine processes these pages to produce the final HTML content sent to clients.
  • It allows inserting dynamic content into the output sent to the browser.

Project Structure Essentials

The Pages Folder

  • Contains the application’s Razor pages and supporting files.
  • Each Razor page typically consists of a pair of files:
    • A .cshtml file: Contains HTML markup mixed with C# code using Razor syntax.
    • A .cshtml.cs file (code-behind): Contains the C# class (PageModel) that handles page events and logic.
  • Supporting files often have names starting with an underscore (e.g., _Layout.cshtml).
  • _Layout.cshtml configures UI elements common across multiple pages.

The wwwroot Folder

  • Contains static assets accessible directly by browsers, such as HTML files, JavaScript files, and CSS files.

Configuration and Entry Point

  • appsettings.json: Contains configuration data for the application, like database connection strings.
  • Program.cs: Contains the application’s entry point, setting up the web host and services.

The PageModel Class

  • The PageModel class is usually declared in a separate code-behind file (.cs).
  • Convention: The code-behind file is named the same as the Razor page (e.g., Index.cshtml has Index.cshtml.cs).
  • Convention: The PageModel class name matches the page file name with ‘Model’ appended (e.g., the PageModel for Index.cshtml is named IndexModel).
  • It encapsulates the data properties and logic associated with its Razor page.
  • Data properties on the PageModel hold dynamic content used when the Razor Engine generates the HTML page.

Handler Methods

  • Handler methods within the PageModel process incoming HTTP requests for the page.
  • They are typically named by prefixing the HTTP request method with On.
  • An HTTP GET request is handled by the OnGet() method (or OnGetAsync()).
  • An HTTP POST request is handled by the OnPost() method (or OnPostAsync()).

Common Handler Methods

  • OnGet(): Primarily used for page initialization when a page is requested (e.g., clicking a hyperlink). It’s similar in purpose to the Page_Load() event in classic ASP.NET Web Forms.
  • OnPost(): Typically used for processing data submitted from the page via a form (e.g., clicking a submit button).

Object-Oriented Concepts

Inheritance (“Is-A” Relationship)

  • Captures an “Is-A” relationship between classes (e.g., a Car is a Vehicle).
  • Child classes (derived classes) inherit properties and methods from their parent class (base class).
  • Child classes can add their own unique properties and methods.
  • In UML diagrams, inheritance is represented by a triangle-headed line pointing to the parent class.

Composition (“Has-A” Relationship)

  • In contrast to “Is-A”, this captures relationships like ownership, containment, or whole-parts (e.g., a Car has an Engine).
  • Represents a strong correlation between two classes.
  • In UML diagrams, composition/aggregation is often represented by a diamond-headed line.
  • In C#, a “has-a” relationship is often implemented using fields or properties, frequently of a collection type:

class Course
{
  private string CourseCode;
  private string CourseName;
  // Course "has-a" list of Students
  private List<Student> students = new List<Student>();

  public List<Student> GetStudents()
  {
    return students;
  }
}
  

Polymorphism

  • Means “many forms”. Allows objects of different types (derived from the same base class or implementing the same interface) to respond to the same method call in their own specific ways.
  • Enables programmers to work with objects at a more general level (using the base class or interface type), letting the runtime environment (.NET) handle the specific implementation details.
  • Facilitates writing generic code that processes objects based on their superclass or interface.
  • Supports key Software Engineering Principles like:
    • Separation of Concerns: Different implementations are handled by different classes.
    • Open/Closed Principle: Systems can be extended (by adding new derived classes) without modifying existing code.

The Razor Engine

  • The view engine used by ASP.NET Core to process Razor files (.cshtml).
  • It parses the annotated HTML files, executes the embedded C# code (Razor expressions), and generates the final HTML response sent to the client.
  • Its primary role is to insert dynamic content into the static HTML structure.
  • Razor expressions are evaluated server-side to generate the response.

Razor Expressions and Syntax

  • Used to embed C# code within HTML markup.
  • Razor expressions start with the @ symbol.
  • Uses:
    • Inserting variable values: <p>Product Name: @Model.Name</p>
    • Accessing ViewData: <p>Stock Level: @ViewData["StockLevel"]</p>
    • Adding conditional HTML elements (e.g., using @if).
    • Executing blocks of C# code using @{ ... }.
  • The Razor engine intelligently switches between HTML and C# modes. Standard HTML tags are treated as content.
  • Use @: to explicitly indicate that a line within a code block should be treated as content/markup.
  • Razor expressions can contain almost any valid C# statement.
  • Best Practice: Use Razor expressions primarily for presentation logic. Complex business logic should reside in the PageModel (code-behind).

C# String Interpolation in Razor

  • C# provides a convenient syntax (string interpolation) to embed expression values directly within a string literal.
  • Start the string with a $ symbol.
  • Place expressions inside curly braces {} within the string.

@{
  Product aProduct = new Product();
  aProduct.Price = 55.5;
}
...
<p>@($"The product's price is {aProduct.Price:C2}")</p>
  

Interpolated String Format

  • General format: $"<text> {<interpolated-expression> [,<field-width>] [:<format-string>] } <text> ..."
  • There should be no whitespace between the $ and the opening double quote ".
  • field-width: A signed integer specifying the minimum number of characters. Positive values right-align the result; negative values left-align it.
  • format-string: A standard .NET format string (e.g., C2 for currency, N0 for number with no decimals).
  • To include literal curly braces in the output, use double braces: {{ or }}.

Passing Data with ViewData

  • ViewData is a dictionary-like object (specifically, ViewDataDictionary) used to pass data from the PageModel (code-behind) to the Razor page (.cshtml).
  • It stores data as key-value pairs.
  • Values are stored as type object.
  • When retrieving a value from ViewData in the Razor page, you must cast it back to its original type.
  • Example (Code-behind): ViewData["StockLevel"] = 100;
  • Example (Razor Page): <p>Stock: @((int)ViewData["StockLevel"])</p>

Model Binding

  • A powerful ASP.NET Core feature that automatically maps incoming request data (from forms, route data, query strings) to PageModel properties or handler method parameters.
  • To bind an HTML form control on a Razor page to a property of the page’s PageModel class:
  • The HTML control must have a name attribute whose value matches the name of the PageModel property (case-insensitive by default, but matching case is recommended).
  • The corresponding property in the PageModel class must be decorated with the [BindProperty] attribute. It also typically needs a public getter and setter.

Passing Data via Query String

  • Data can be passed to handler methods via the URL’s query string.
  • Example URL: http://localhost:60624/index?lowPrice=20&highPrice=100
  • To receive these values in a handler method (like OnGet), declare parameters whose names match the query string keys (case-insensitive matching).
  • Example Handler Signature: public void OnGet(int? lowPrice, int? highPrice) { ... }
  • Query string keys are optional. If a key is missing in the URL, the corresponding parameter will typically be assigned its default value (e.g., null for nullable types, 0 for int).
  • The Razor engine attempts to convert the query string value (which is a string) to the parameter’s declared type. If conversion fails, model validation errors may occur, or the parameter might receive a default value.
  • It’s often recommended to use nullable types (like int?, string) for parameters bound to query strings to handle missing values gracefully.
  • HTML form data submitted using the GET method is appended to the URL as a query string.
  • While primarily associated with GET requests, parameters in any handler method (including OnPost) can potentially be bound from query string values if present in the request URL.