Introducción a Razor Pages en ASP.NET Core

Por Rick Anderson, Dave Brock y Kirk Larkin

Razor Pages facilita la programación de escenarios centrados en páginas y hace que resulte más productiva que con controladores y vistas.

Si busca un tutorial que use el enfoque Model-View-Controller, consulte Introducción a ASP.NET Core MVC.

En este documento se proporciona una introducción a Razor Pages. No es un tutorial paso a paso. Si encuentra que alguna sección es demasiado avanzada, consulte Introducción a Razor Pages. Para obtener información general de ASP.NET Core, vea Introducción a ASP.NET Core.

Requisitos previos

Crear un proyecto de Razor Pages

Vea Introducción a Razor Pages para obtener instrucciones detalladas sobre cómo crear un proyecto Razor Pages.

Razor Pages

Razor Pages está habilitado en Program.cs:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

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

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();

En el código anterior:

Considere la posibilidad de una página básica:

@page

<h1>Hello, world!</h1>
<h2>The time on the server is @DateTime.Now</h2>

El código anterior se parece mucho a un archivo de vista de Razor que se utiliza en una aplicación ASP.NET Core con controladores y vistas. La directiva @page lo hace diferente. @page transforma el archivo en una acción de MVC, lo que significa que administra las solicitudes directamente, sin tener que pasar a través de un controlador. @page debe ser la primera directiva de Razor de una página. @page afecta al comportamiento de otras construcciones de Razor. Los nombres de archivo de Razor tienen un sufijo .cshtml.

Una página similar, con una clase PageModel, se muestra en los dos archivos siguientes. El archivo Pages/Index2.cshtml:

@page
@using RazorPagesIntro.Pages
@model Index2Model

<h2>Separate page model</h2>
<p>
    @Model.Message
</p>

Modelo de página Pages/Index2.cshtml.cs:

using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;
using System;

namespace RazorPagesIntro.Pages
{
    public class Index2Model : PageModel
    {
        public string Message { get; private set; } = "PageModel in C#";

        public void OnGet()
        {
            Message += $" Server time is { DateTime.Now }";
        }
    }
}

Por convención, el archivo de clase PageModel tiene el mismo nombre que el archivo de Razor Pages con .cs anexado. Por ejemplo, la página de Razor anterior es Pages/Index2.cshtml. El archivo que contiene la clase PageModel se denomina Pages/Index2.cshtml.cs.

Las asociaciones de rutas de dirección URL a páginas se determinan según la ubicación de la página en el sistema de archivos. En la tabla siguiente, se muestra una ruta de acceso Razor Pages y la dirección URL correspondiente:

Ruta de acceso y nombre de archivo URL correspondiente
/Pages/Index.cshtml / o /Index
/Pages/Contact.cshtml /Contact
/Pages/Store/Contact.cshtml /Store/Contact
/Pages/Store/Index.cshtml /Store o /Store/Index

Notas:

  • El entorno de ejecución busca archivos de páginas de Razor en la carpeta Pages de forma predeterminada.
  • Index es la página predeterminada cuando una URL no incluye una página.

Escritura de un formulario básico

Razor Pages está diseñado para facilitar la implementación de patrones comunes que se usan con exploradores web al compilar una aplicación. Los enlaces de modelos, los asistentes de etiquetas y los asistentes de HTML funcionan con las propiedades definidas en una clase de Razor Pages. Considere la posibilidad de una página que implementa un formulario básico del estilo "Póngase en contacto con nosotros" para el modelo Contact:

Para los ejemplos de este documento, DbContext se inicializa en el archivo Startup.cs.

La base de datos en memoria requiere el paquete NuGet Microsoft.EntityFrameworkCore.InMemory.

using Microsoft.EntityFrameworkCore;
using RazorPagesContacts.Data;
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddDbContext<CustomerDbContext>(options =>
    options.UseInMemoryDatabase("name"));

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

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

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();

El modelo de datos:

using System.ComponentModel.DataAnnotations;

namespace RazorPagesContacts.Models
{
    public class Customer
    {
        public int Id { get; set; }

        [Required, StringLength(10)]
        public string? Name { get; set; }
    }
}

El contexto de la base de datos:

using Microsoft.EntityFrameworkCore;

namespace RazorPagesContacts.Data
{
    public class CustomerDbContext : DbContext
    {
        public CustomerDbContext (DbContextOptions<CustomerDbContext> options)
            : base(options)
        {
        }

        public DbSet<RazorPagesContacts.Models.Customer> Customer => Set<RazorPagesContacts.Models.Customer>();
    }
}

Archivo de vista Pages/Customers/Create.cshtml:

@page
@model RazorPagesContacts.Pages.Customers.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<p>Enter a customer name:</p>

<form method="post">
    Name:
    <input asp-for="Customer!.Name" />
    <input type="submit" />
</form>

Modelo de página Pages/Customers/Create.cshtml.cs:

public class CreateModel : PageModel
{
    private readonly Data.CustomerDbContext _context;

    public CreateModel(Data.CustomerDbContext context)
    {
        _context = context;
    }

    public IActionResult OnGet()
    {
        return Page();
    }

    [BindProperty]
    public Customer? Customer { get; set; }

    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        if (Customer != null) _context.Customer.Add(Customer);
        await _context.SaveChangesAsync();

        return RedirectToPage("./Index");
    }
}

Por convención, la clase PageModel se denomina <PageName>Model y se encuentra en el mismo espacio de nombres que la página.

La clase PageModel permite la separación de la lógica de una página de su presentación. Define los controladores de página para solicitudes que se envían a la página y los datos que usan para representar la página. Esta separación permite lo siguiente:

La página tiene un método de controladorOnPostAsync, que se ejecuta en solicitudes POST (cuando un usuario envía el formulario). Se pueden agregar métodos de controlador para cualquier verbo HTTP. Los controladores más comunes son:

  • OnGet para inicializar el estado necesario para la página. En el código anterior, el método OnGet muestra la instancia de la página de CreateModel.cshtmlRazor.
  • OnPost para controlar los envíos del formulario.

El sufijo de nombre Async es opcional, pero se usa a menudo por convención para funciones asincrónicas. El código anterior es típico de Razor Pages.

Si está familiarizado con las aplicaciones de ASP.NET con controladores y vistas:

  • El código OnPostAsync del ejemplo anterior es similar al típico código de controlador.
  • La mayoría de los primitivos de MVC, como el enlace de modelos, la validación y los resultados de acciones, funcionan del mismo modo con los controladores y Razor Pages.

El método OnPostAsync anterior:

[BindProperty]
public Customer? Customer { get; set; }

public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    if (Customer != null) _context.Customer.Add(Customer);
    await _context.SaveChangesAsync();

    return RedirectToPage("./Index");
}

El flujo básico de OnPostAsync:

Compruebe los errores de validación.

  • Si no hay ningún error, guarde los datos y redirija.
  • Si hay errores, muestre la página de nuevo con mensajes de validación. En muchos casos, los errores de validación se detectan en el cliente y nunca se envían al servidor.

Archivo de vista Pages/Customers/Create.cshtml:

@page
@model RazorPagesContacts.Pages.Customers.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<p>Enter a customer name:</p>

<form method="post">
    Name:
    <input asp-for="Customer!.Name" />
    <input type="submit" />
</form>

El código HTML representado de Pages/Customers/Create.cshtml:

<p>Enter a customer name:</p>

<form method="post">
    Name:
    <input type="text" data-val="true"
           data-val-length="The field Name must be a string with a maximum length of 10."
           data-val-length-max="10" data-val-required="The Name field is required."
           id="Customer_Name" maxlength="10" name="Customer.Name" value="" />
    <input type="submit" />
    <input name="__RequestVerificationToken" type="hidden"
           value="<Antiforgery token here>" />
</form>

En el código anterior, la publicación del formulario:

  • Con datos válidos:

    • El método del controlador OnPostAsync llama al método auxiliar RedirectToPage. RedirectToPage devuelve una instancia de RedirectToPageResult. RedirectToPage:

      • Es el resultado de una acción.
      • Es similar a RedirectToAction o RedirectToRoute (se usa en controladores y vistas).
      • Se ha personalizado para las páginas. En el ejemplo anterior, redirige a la página de índice raíz (/Index). RedirectToPage se detalla en la sección Generación de direcciones URL para las páginas.
  • Con errores de validación que se pasan al servidor:

    • El método del controlador OnPostAsync llama al método auxiliar Page. Page devuelve una instancia de PageResult. Devolver Page es similar a cómo las acciones en los controladores devuelven View. PageResult es el tipo de valor devuelto predeterminado para un método de controlador. Un método de controlador que devuelve void representa la página.
    • En el ejemplo anterior, la publicación del formulario sin valores hace que se devuelva ModelState.IsValid como false. En este ejemplo, no se muestra ningún error de validación en el cliente. La entrega de errores de validación se trata más adelante en este documento.
    [BindProperty]
    public Customer? Customer { get; set; }
    
    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }
    
        if (Customer != null) _context.Customer.Add(Customer);
        await _context.SaveChangesAsync();
    
        return RedirectToPage("./Index");
    }
    
  • Con errores de validación detectados mediante la validación del lado cliente:

    • Los datos no se publican en el servidor.
    • La validación del lado cliente se explica más adelante en este documento.

La propiedad Customer usa el atributo [BindProperty] para participar en el enlace de modelos:

public class CreateModel : PageModel
{
    private readonly Data.CustomerDbContext _context;

    public CreateModel(Data.CustomerDbContext context)
    {
        _context = context;
    }

    public IActionResult OnGet()
    {
        return Page();
    }

    [BindProperty]
    public Customer? Customer { get; set; }

    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        if (Customer != null) _context.Customer.Add(Customer);
        await _context.SaveChangesAsync();

        return RedirectToPage("./Index");
    }
}

[BindProperty]no debe usarse en modelos que contengan propiedades que el cliente no debe cambiar. Para más información, consulte Publicación excesiva.

De forma predeterminada, Razor Pages enlaza propiedades solo con verbos que no sean GET. El enlace a propiedades elimina la necesidad de escribir código para convertir los datos HTTP en el tipo de modelo. Enlazar reduce el código al usar la misma propiedad para representar los campos de formulario (<input asp-for="Customer.Name">) y aceptar la entrada.

Advertencia

Por motivos de seguridad, debe participar en el enlace de datos de solicitud GET con las propiedades del modelo de página. Compruebe las entradas de los usuarios antes de asignarlas a las propiedades. Si participa en el enlace de GET, le puede ser útil al trabajar con escenarios que dependan de cadenas de consultas o valores de rutas.

Para enlazar una propiedad en solicitudes GET, establezca la propiedad SupportsGet del atributo [BindProperty] en true:

[BindProperty(SupportsGet = true)]

Para obtener más información, vea ASP.NET Core Community Standup: Bind on GET discussion (YouTube).

Revisión del archivo de vista Pages/Customers/Create.cshtml:

@page
@model RazorPagesContacts.Pages.Customers.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<p>Enter a customer name:</p>

<form method="post">
    Name:
    <input asp-for="Customer!.Name" />
    <input type="submit" />
</form>
  • En el código anterior, el asistente de etiquetas de entrada<input asp-for="Customer.Name" /> enlaza el elemento <input> HTML con la expresión del modelo Customer.Name.
  • @addTagHelper hace que los asistentes de etiquetas estén disponibles.

La página principal

Index.cshtml es la página principal:

@page
@model RazorPagesContacts.Pages.Customers.IndexModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<h1>Contacts home page</h1>
<form method="post">
    <table class="table">
        <thead>
            <tr>
                <th>ID</th>
                <th>Name</th>
                <th></th>
            </tr>
        </thead>
        <tbody>
        @if (Model.Customers != null)
        {
            foreach (var contact in Model.Customers)
            {
                <tr>
                    <td> @contact.Id </td>
                    <td>@contact.Name</td>
                    <td>
                        <!-- <snippet_Edit> -->
                        <a asp-page="./Edit" asp-route-id="@contact.Id">Edit</a> |
                        <!-- </snippet_Edit> -->
                        <!-- <snippet_Delete> -->
                        <button type="submit" asp-page-handler="delete" asp-route-id="@contact.Id">delete</button>
                        <!-- </snippet_Delete> -->
                    </td>
                </tr>
            }
        }
        </tbody>
    </table>
    <a asp-page="Create">Create New</a>
</form>

Clase PageModel asociada (Index.cshtml.cs):

public class IndexModel : PageModel
{
    private readonly Data.CustomerDbContext _context;
    public IndexModel(Data.CustomerDbContext context)
    {
        _context = context;
    }

    public IList<Customer>? Customers { get; set; }

    public async Task OnGetAsync()
    {
        Customers = await _context.Customer.ToListAsync();
    }

    public async Task<IActionResult> OnPostDeleteAsync(int id)
    {
        var contact = await _context.Customer.FindAsync(id);

        if (contact != null)
        {
            _context.Customer.Remove(contact);
            await _context.SaveChangesAsync();
        }

        return RedirectToPage();
    }
}

El archivo Index.cshtml contiene el siguiente marcado:

<a asp-page="./Edit" asp-route-id="@contact.Id">Edit</a> |

El <a /a>asistente de etiquetas delimitadoras ha usado el atributo asp-route-{value} para generar un vínculo a la página de edición. El vínculo contiene datos de ruta con el identificador del contacto. Por ejemplo: https://localhost:5001/Edit/1. Las aplicaciones auxiliares de etiquetas permiten que el código de servidor participe en la creación y la representación de elementos HTML en archivos de Razor.

El archivo Index.cshtml contiene marcado para crear un botón de eliminar para cada contacto de cliente:

<button type="submit" asp-page-handler="delete" asp-route-id="@contact.Id">delete</button>

El código HTML representado:

<button type="submit" formaction="/Customers?id=1&amp;handler=delete">delete</button>

Al representar el botón de eliminar en HTML, el elemento formaction incluye parámetros para los siguientes elementos:

  • Id. de contacto de cliente especificado mediante el atributo asp-route-id.
  • handler especificado mediante el atributo asp-page-handler.

Al seleccionar el botón, se envía una solicitud de formulario POST al servidor. De forma predeterminada, el nombre del método de control se selecciona de acuerdo con el valor del parámetro handler y según el esquema OnPost[handler]Async.

Como en este ejemplo handler es delete, el método de control OnPostDeleteAsync se usa para procesar la solicitud POST. Si asp-page-handler se establece en otro valor, como remove, se seleccionará un método de controlador llamado OnPostRemoveAsync.

public async Task<IActionResult> OnPostDeleteAsync(int id)
{
    var contact = await _context.Customer.FindAsync(id);

    if (contact != null)
    {
        _context.Customer.Remove(contact);
        await _context.SaveChangesAsync();
    }

    return RedirectToPage();
}

El método OnPostDeleteAsync realiza las acciones siguientes:

  • Obtiene el elemento id de la cadena de consulta.
  • Realiza una consulta a la base de datos del contacto de cliente con FindAsync.
  • Si se encuentra el contacto del cliente, este se quita y se actualiza la base de datos.
  • Llama a RedirectToPage para redirigir la página Index raíz (/Index).

El archivo Edit.cshtml

@page "{id:int}"
@model RazorPagesContacts.Pages.Customers.EditModel

@{
    ViewData["Title"] = "Edit";
}

<h1>Edit</h1>

<h4>Customer</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form method="post">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <input type="hidden" asp-for="Customer!.Id" />
            <div class="form-group">
                <label asp-for="Customer!.Name" class="control-label"></label>
                <input asp-for="Customer!.Name" class="form-control" />
                <span asp-validation-for="Customer!.Name" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Save" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-page="./Index">Back to List</a>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

La primera línea contiene la directiva @page "{id:int}". La restricción de enrutamiento "{id:int}" indica a la página que acepte las solicitudes a la página que contienen datos de ruta int. Si una solicitud a la página no contiene datos de ruta que se puedan convertir en int, el tiempo de ejecución devuelve un error HTTP 404 (no encontrado). Para que el identificador sea opcional, anexe ? a la restricción de ruta:

@page "{id:int?}"

El archivo Edit.cshtml.cs:

public class EditModel : PageModel
{
    private readonly RazorPagesContacts.Data.CustomerDbContext _context;

    public EditModel(RazorPagesContacts.Data.CustomerDbContext context)
    {
        _context = context;
    }

    [BindProperty]
    public Customer? Customer { get; set; }

    public async Task<IActionResult> OnGetAsync(int? id)
    {
        if (id == null)
        {
            return NotFound();
        }

        Customer = await _context.Customer.FirstOrDefaultAsync(m => m.Id == id);
        
        if (Customer == null)
        {
            return NotFound();
        }
        return Page();
    }

    // To protect from overposting attacks, enable the specific properties you want to bind to.
    // For more details, see https://aka.ms/RazorPagesCRUD.
    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        if (Customer != null)
        {
            _context.Attach(Customer).State = EntityState.Modified;

            try
            {
                await _context.SaveChangesAsync();
            }
            catch (DbUpdateConcurrencyException)
            {
                if (!CustomerExists(Customer.Id))
                {
                    return NotFound();
                }
                else
                {
                    throw;
                }
            }
        }

        return RedirectToPage("./Index");
    }

    private bool CustomerExists(int id)
    {
        return _context.Customer.Any(e => e.Id == id);
    }
}

Validación

Reglas de validación:

  • Se especifican mediante declaración en la clase de modelo.
  • Se aplican en toda la aplicación.

El espacio de nombres System.ComponentModel.DataAnnotations proporciona un conjunto de atributos de validación integrados que se aplican mediante declaración a una clase o propiedad. DataAnnotations también contiene atributos de formato como [DataType] que ayudan a aplicar formato y no proporcionan ninguna validación.

Considere el modelo Customer:

using System.ComponentModel.DataAnnotations;

namespace RazorPagesContacts.Models
{
    public class Customer
    {
        public int Id { get; set; }

        [Required, StringLength(10)]
        public string? Name { get; set; }
    }
}

Con el siguiente archivo de vista Create.cshtml:

@page
@model RazorPagesContacts.Pages.Customers.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<p>Validation: customer name:</p>

<form method="post">
    <div asp-validation-summary="ModelOnly"></div>
    <span asp-validation-for="Customer.Name"></span>
    Name:
    <input asp-for="Customer.Name" />
    <input type="submit" />
</form>

<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/jquery-validation/dist/jquery.validate.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script>

El código anterior:

  • Incluye scripts de validación de jQuery y jQuery.

  • Usa los asistentes de etiquetas<div /> y <span /> para habilitar lo siguiente:

    • Validación del lado cliente.
    • Representación del error de validación.
  • Se genera el siguiente código HTML:

    <p>Enter a customer name:</p>
    
    <form method="post">
        Name:
        <input type="text" data-val="true"
               data-val-length="The field Name must be a string with a maximum length of 10."
               data-val-length-max="10" data-val-required="The Name field is required."
               id="Customer_Name" maxlength="10" name="Customer.Name" value="" />
        <input type="submit" />
        <input name="__RequestVerificationToken" type="hidden"
               value="<Antiforgery token here>" />
    </form>
    
    <script src="/lib/jquery/dist/jquery.js"></script>
    <script src="/lib/jquery-validation/dist/jquery.validate.js"></script>
    <script src="/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script>
    

Al publicar el formulario de creación sin un valor de nombre, se muestra el mensaje de error "El campo Nombre es obligatorio" en el formulario. Si JavaScript está habilitado en el cliente, el explorador muestra el error sin realizar la publicación en el servidor.

El atributo [StringLength(10)] genera data-val-length-max="10" en el código HTML representado. data-val-length-max impide que los exploradores superen la longitud máxima especificada al escribir. Si se usa una herramienta como Fiddler para editar y reproducir la publicación:

  • Con el nombre de más de 10 caracteres.
  • Se devuelve el mensaje de error "El nombre del campo debe ser una cadena con una longitud máxima de 10 caracteres".

Considere el modelo Movie siguiente:

using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace RazorPagesMovie.Models
{
    public class Movie
    {
        public int ID { get; set; }

        [StringLength(60, MinimumLength = 3)]
        [Required]
        public string Title { get; set; }

        [Display(Name = "Release Date")]
        [DataType(DataType.Date)]
        public DateTime ReleaseDate { get; set; }

        [Range(1, 100)]
        [DataType(DataType.Currency)]
        [Column(TypeName = "decimal(18, 2)")]
        public decimal Price { get; set; }

        [RegularExpression(@"^[A-Z]+[a-zA-Z\s]*$")]
        [Required]
        [StringLength(30)]
        public string Genre { get; set; }

        [RegularExpression(@"^[A-Z]+[a-zA-Z0-9""'\s-]*$")]
        [StringLength(5)]
        [Required]
        public string Rating { get; set; }
    }
}

Los atributos de validación especifican el comportamiento que se exigirá a las propiedades del modelo al que se aplican:

  • Los atributos Required y MinimumLength indican que una propiedad debe tener un valor, pero nada impide al usuario escribir espacios en blanco para satisfacer esta validación.

  • El atributo RegularExpression se usa para limitar los caracteres que se pueden escribir. En el código anterior, "Género":

    • Solo debe usar letras.
    • La primera letra debe estar en mayúsculas. No se permiten espacios en blanco, números ni caracteres especiales.
  • La "Clasificación" de RegularExpression:

    • Requiere que el primer carácter sea una letra mayúscula.
    • Permite caracteres especiales y números en los espacios posteriores. "PG-13" es válido para una "Clasificación", pero se produce un error en un "Género".
  • El atributo Range restringe un valor a un intervalo determinado.

  • El atributo StringLength establece la longitud máxima de una propiedad de cadena y, opcionalmente, su longitud mínima.

  • Los tipos de valor (como decimal, int, float, DateTime) son intrínsecamente necesarios y no necesitan el atributo [Required].

La página de creación del modelo Movie muestra errores de visualización con valores no válidos:

Formulario de vista de película con varios errores de validación de cliente de jQuery

Para obtener más información, consulte:

Aislamiento de CSS

Aísle los estilos CSS a páginas, vistas y componentes individuales para reducir o evitar lo siguiente:

  • Dependencias de estilos globales que pueden ser difíciles de mantener.
  • Conflictos de estilo en el contenido anidado.

Para agregar un archivo CSS con ámbito para una página o vista, coloque los estilos CSS en un archivo .cshtml.css complementario que coincida con el nombre del archivo .cshtml. En el ejemplo siguiente, un archivo Index.cshtml.css proporciona estilos CSS que solo se aplican a la página o vista de Index.cshtml.

Pages/Index.cshtml.css (Razor Pages) o Views/Index.cshtml.css (MVC):

h1 {
    color: red;
}

El aislamiento de CSS se produce en tiempo de compilación. El marco reescribe selectores CSS para que coincidan con el marcado representado por las páginas o vistas de la aplicación. Estos estilos de CSS reescritos se unen y se generan como un recurso estático, {APP ASSEMBLY}.styles.css. El nombre del ensamblado del proyecto es el marcador de posición {APP ASSEMBLY}. Se coloca un vínculo a los estilos CSS agrupados en el diseño de la aplicación.

En el contenido de <head> del archivo Pages/Shared/_Layout.cshtml de la aplicación (Razor Pages) o de Views/Shared/_Layout.cshtml (MVC), agregue o confirme la presencia del vínculo a los estilos CSS agrupados:

<link rel="stylesheet" href="~/{APP ASSEMBLY}.styles.css" />

En el ejemplo siguiente, el nombre del ensamblado de la aplicación es WebApp:

<link rel="stylesheet" href="WebApp.styles.css" />

Los estilos definidos en un archivo CSS con ámbito solo se aplican a la salida representada del archivo coincidente. En el ejemplo anterior, cualquier declaración CSS h1 definida en otra parte de la aplicación no entra en conflicto con el estilo de encabezado de Index. Las reglas heredadas y en cascada de los estilos CSS permanecen en vigor para los archivos CSS con ámbito. Por ejemplo, los estilos aplicados directamente a un elemento <h1> del archivo Index.cshtml invalidan los estilos del archivo CSS con ámbito en Index.cshtml.css.

Nota

Para garantizar el aislamiento de estilos CSS en el momento de la unión, no se admite la importación de CSS en bloques de código Razor.

El aislamiento de CSS solo se aplica a los elementos HTML. El aislamiento de CSS no se admite para los asistentes de etiquetas.

Dentro del archivo CSS agrupado, cada página, vista o componente Razor está asociado a un identificador de ámbito en el formato b-{STRING}, donde el marcador de posición {STRING} es una cadena de diez caracteres generada por el marco. En el ejemplo siguiente se proporciona el estilo del elemento <h1> anterior en la página Index de una aplicación de Razor Pages:

/* /Pages/Index.cshtml.rz.scp.css */
h1[b-3xxtam6d07] {
    color: red;
}

En la página Index donde se aplica el estilo CSS desde el archivo agrupado, el identificador de ámbito se anexa como un atributo HTML:

<h1 b-3xxtam6d07>

El identificador es único para una aplicación. En tiempo de compilación, se crea un conjunto del proyecto con la convención {STATIC WEB ASSETS BASE PATH}/Project.lib.scp.css, donde el marcador de posición {STATIC WEB ASSETS BASE PATH} es la ruta de acceso base a los recursos web estáticos.

Si se usan otros proyectos, como paquetes NuGet o bibliotecas de clases de Razor, el archivo unido:

  • Hace referencia a los estilos mediante importaciones de CSS.
  • No se publica como recurso web estático de la aplicación que consume los estilos.

Compatibilidad del preprocesador de CSS

Los preprocesadores de CSS son útiles para mejorar el desarrollo de CSS mediante el uso de características como variables, anidamiento, módulos, mixins y herencia. Aunque el aislamiento de CSS no admite de forma nativa preprocesadores de CSS como Sass o Less, la integración de preprocesadores de CSS se realiza sin problemas siempre que se produzca la compilación del preprocesador antes de que el marco reescriba los selectores de CSS durante el proceso de compilación. Con Visual Studio, por ejemplo, configure la compilación del preprocesador existente como una tarea Antes de la compilación en el Explorador del Ejecutor de tareas de Visual Studio.

Muchos paquetes NuGet de terceros, como Delegate.SassBuilder, pueden compilar archivos SASS/SCSS al principio del proceso de compilación antes de que se produzca el aislamiento de CSS y no requieren ninguna configuración adicional.

Configuración del aislamiento de CSS

El aislamiento de CSS permite la configuración de algunos escenarios avanzados, como cuando hay dependencias en herramientas o flujos de trabajo existentes.

Personalización del formato del identificador de ámbito

En esta sección, el marcador de posición {Pages|Views} es Pages para aplicaciones Razor Pages o Views para aplicaciones MVC.

De forma predeterminada, los identificadores de ámbito usan el formato b-{STRING}, donde el marcador de posición {STRING} es una cadena de diez caracteres generada por el marco. Para personalizar el formato de un identificador de ámbito, actualice el archivo del proyecto a un patrón deseado:

<ItemGroup>
  <None Update="{Pages|Views}/Index.cshtml.css" CssScope="custom-scope-identifier" />
</ItemGroup>

En el ejemplo anterior, el CSS generado para Index.cshtml.css cambia su identificador de ámbito de b-{STRING} a custom-scope-identifier.

Use identificadores de ámbito para lograr la herencia con archivos CSS de ámbito. En el siguiente ejemplo del archivo del proyecto, un archivo BaseView.cshtml.css contiene estilos comunes en todas las vistas. Un archivo DerivedView.cshtml.css hereda estos estilos.

<ItemGroup>
  <None Update="{Pages|Views}/BaseView.cshtml.css" CssScope="custom-scope-identifier" />
  <None Update="{Pages|Views}/DerivedView.cshtml.css" CssScope="custom-scope-identifier" />
</ItemGroup>

Use el operador comodín (*) para compartir los identificadores de ámbito en varios archivos:

<ItemGroup>
  <None Update="{Pages|Views}/*.cshtml.css" CssScope="custom-scope-identifier" />
</ItemGroup>

Cambio de la ruta de acceso base de recursos web estáticos

El archivo CSS con ámbito se genera en la raíz de la aplicación. En el archivo del proyecto, use la propiedad StaticWebAssetBasePath para cambiar la ruta de acceso predeterminada. En el ejemplo siguiente se coloca el archivo CSS con ámbito, y el resto de los recursos de la aplicación, en la ruta de acceso _content:

<PropertyGroup>
  <StaticWebAssetBasePath>_content/$(PackageId)</StaticWebAssetBasePath>
</PropertyGroup>

Deshabilitación de la unión automática

Para no usar el modo en que el marco publica y carga archivos con ámbito en tiempo de ejecución, use la propiedad DisableScopedCssBundling. Al usar esta propiedad, otras herramientas o procesos son responsables de tomar los archivos CSS aislados del directorio obj y de publicarlos y cargarlos en tiempo de ejecución:

<PropertyGroup>
  <DisableScopedCssBundling>true</DisableScopedCssBundling>
</PropertyGroup>

Razor Compatibilidad con la biblioteca de clases (RCL)

Cuando una Razor biblioteca de clases (RCL) proporciona estilos aislados, el atributo href de la etiqueta <link> apunta a {STATIC WEB ASSET BASE PATH}/{PACKAGE ID}.bundle.scp.css, donde los marcadores de posición son los siguientes:

  • {STATIC WEB ASSET BASE PATH}: ruta de acceso base del recurso web estático.
  • {PACKAGE ID}: el identificador del paquete de la biblioteca. El identificador del paquete tiene como valor predeterminado el nombre del ensamblado del proyecto si el identificador del paquete no se especifica en el archivo del proyecto.

En el ejemplo siguiente:

  • La ruta de acceso base del recurso web estático es _content/ClassLib.
  • El nombre del ensamblado de la biblioteca de clases es ClassLib.

Pages/Shared/_Layout.cshtml (Razor Pages) o Views/Shared/_Layout.cshtml (MVC):

<link href="_content/ClassLib/ClassLib.bundle.scp.css" rel="stylesheet">

Para obtener más información sobre RCL, vea los siguientes artículos:

Para obtener información sobre el aislamiento de CSS de Blazor, consulte Aislamiento de CSS de Blazor de ASP.NET Core.

Control de solicitudes HEAD con un controlador OnGet de reserva

Las solicitudes HEAD permiten recuperar los encabezados de un recurso específico. A diferencia de las solicitudes GET, las solicitudes HEAD no devuelven un cuerpo de respuesta.

Normalmente, se crea un controlador OnHead al que se llama para las solicitudes HEAD:

public void OnHead()
{
    HttpContext.Response.Headers.Add("Head Test", "Handled by OnHead!");
}

Razor Pages recurre a una llamada al controlador OnGet si no se define ningún controlador OnHead.

XSRF/CSRF y Razor Pages

Razor Pages está protegido mediante validación antifalsificación. El elemento FormTagHelper inserta tokens antifalsificación en los elementos de formulario HTML.

Usar diseños, parciales, plantillas y asistentes de etiquetas con Razor Pages

Las páginas funcionan con todas las características del motor de vista de Razor. Los diseños, parciales, plantillas, asistentes de etiquetas, _ViewStart.cshtml y _ViewImports.cshtml funcionan de la misma manera que lo hacen con las vistas de Razor convencionales.

Para simplificar esta página, aprovecharemos algunas de esas características.

Agregue una página de diseño a Pages/Shared/_Layout.cshtml:

<!DOCTYPE html>
<html>
<head>
    <title>RP Sample</title>
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
</head>
<body>
    <a asp-page="/Index">Home</a>
    <a asp-page="/Customers/Create">Create</a>
    <a asp-page="/Customers/Index">Customers</a> <br />

    @RenderBody()
    <script src="~/lib/jquery/dist/jquery.js"></script>
    <script src="~/lib/jquery-validation/dist/jquery.validate.js"></script>
    <script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script>
</body>
</html>

El diseño:

  • Controla el diseño de cada página (a no ser que la página no tenga diseño).
  • Importa las estructuras HTML como JavaScript y hojas de estilos.
  • El contenido de la página de Razor se representa donde se llama a @RenderBody().

Para más información, consulte la página de diseño.

La propiedad Layout se establece en Pages/_ViewStart.cshtml:

@{
    Layout = "_Layout";
}

El diseño está en la carpeta Pages/Shared. Las páginas buscan otras vistas (diseños, plantillas, parciales) de forma jerárquica, a partir de la misma carpeta que la página actual. Un diseño en la carpeta Pages/Shared se puede usar desde cualquier página de Razor en la carpeta Pages.

El archivo de diseño debería ir en la carpeta Pages/Shared.

Le recomendamos que no coloque el archivo de diseño en la carpeta Views/Shared. Views/Shared es un patrón de vistas de MVC. Razor Pages está diseñado para basarse en la jerarquía de carpetas, no en las convenciones de ruta de acceso.

La búsqueda de vistas de una instancia de Razor Pages incluye la carpeta Pages. Los diseños, plantillas y parciales que se usan con los controladores de MVC y las vistas de Razor convencionales simplemente funcionan.

Agregue un archivo Pages/_ViewImports.cshtml:

@namespace RazorPagesContacts.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

@namespace se explica más adelante en el tutorial. La directiva @addTagHelper pone los asistentes de etiquetas integradas en todas las páginas de la carpeta Pages.

La directiva @namespace establecida en una página:

@page
@namespace RazorPagesIntro.Pages.Customers

@model NameSpaceModel

<h2>Name space</h2>
<p>
    @Model.Message
</p>

La directiva @namespace establece el espacio de nombres de la página. La directiva @model no necesita incluir el espacio de nombres.

Cuando la directiva @namespace se encuentra en _ViewImports.cshtml, el espacio de nombres especificado proporciona el prefijo del espacio de nombres generado en la página que importa la directiva @namespace. El resto del espacio de nombres generado (la parte del sufijo) es la ruta de acceso relativa separada por puntos entre la carpeta que contiene _ViewImports.cshtml y la carpeta que contiene la página.

Por ejemplo, la clase PageModelPages/Customers/Edit.cshtml.cs establece explícitamente el espacio de nombres:

namespace RazorPagesContacts.Pages
{
    public class EditModel : PageModel
    {
        private readonly AppDbContext _db;

        public EditModel(AppDbContext db)
        {
            _db = db;
        }

        // Code removed for brevity.

El archivo Pages/_ViewImports.cshtml establece el espacio de nombres siguiente:

@namespace RazorPagesContacts.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

El espacio de nombres generado para la página de Pages/Customers/Edit.cshtmlRazor es el mismo que la clase PageModel.

@namespacetambién funciona con vistas de Razor convencionales.

Considere el archivo de vista Pages/Customers/Create.cshtml:

@page
@model RazorPagesContacts.Pages.Customers.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<p>Validation: customer name:</p>

<form method="post">
    <div asp-validation-summary="ModelOnly"></div>
    <span asp-validation-for="Customer.Name"></span>
    Name:
    <input asp-for="Customer.Name" />
    <input type="submit" />
</form>

<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/jquery-validation/dist/jquery.validate.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script>

Archivo de vista actualizado Pages/Customers/Create.cshtml con _ViewImports.cshtml y el archivo de distribución anterior:

@page
@model CreateModel

<p>Enter a customer name:</p>

<form method="post">
    Name:
    <input asp-for="Customer.Name" />
    <input type="submit" />
</form>

En el código anterior, el elemento _ViewImports.cshtml importó el espacio de nombres y los asistentes de etiquetas. El archivo de distribución importó los archivos JavaScript.

El proyecto de inicio de Razor Pages contiene Pages/_ValidationScriptsPartial.cshtml, que enlaza la validación del lado cliente.

Para más información sobre las vistas parciales, vea Vistas parciales en ASP.NET Core.

Generación de direcciones URL para las páginas

La página Create, mostrada anteriormente, usa RedirectToPage:

public class CreateModel : PageModel
{
    private readonly CustomerDbContext _context;

    public CreateModel(CustomerDbContext context)
    {
        _context = context;
    }

    public IActionResult OnGet()
    {
        return Page();
    }

    [BindProperty]
    public Customer Customer { get; set; }

    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        _context.Customers.Add(Customer);
        await _context.SaveChangesAsync();

        return RedirectToPage("./Index");
    }
}

La aplicación tiene la siguiente estructura de archivos o carpetas:

  • /Pages

    • Index.cshtml

    • Privacy.cshtml

    • /Customers

      • Create.cshtml
      • Edit.cshtml
      • Index.cshtml

Las páginas Pages/Customers/Create.cshtml y Pages/Customers/Edit.cshtml redirigen a Pages/Customers/Index.cshtml si la operación se realiza correctamente. La cadena ./Index es un nombre de página relativo que se usa para acceder a la página anterior. Se usa para generar direcciones URL a la página Pages/Customers/Index.cshtml. Por ejemplo:

  • Url.Page("./Index", ...)
  • <a asp-page="./Index">Customers Index Page</a>
  • RedirectToPage("./Index")

El nombre de página absoluto /Index se usa para generar direcciones URL en la página Pages/Index.cshtml. Por ejemplo:

  • Url.Page("/Index", ...)
  • <a asp-page="/Index">Home Index Page</a>
  • RedirectToPage("/Index")

El nombre de página es la ruta de acceso a la página de la carpeta raíz /Pages, incluido un / inicial, por ejemplo /Index. Los ejemplos anteriores de generación de URL ofrecen opciones mejoradas y capacidades funcionales en comparación con la escritura a mano de estas. La generación de direcciones URL usa el enrutamiento y puede generar y codificar parámetros según cómo se defina la ruta en la ruta de acceso de destino.

La generación de direcciones URL para las páginas admite nombres relativos. En la siguiente tabla, se muestra qué página de índice está seleccionada usando distintos parámetros RedirectToPage en Pages/Customers/Create.cshtml.

RedirectToPage(x) Página
RedirectToPage("/Index") Pages/Index
RedirectToPage("./Index"); Pages/Customers/Index
RedirectToPage("../Index") Pages/Index
RedirectToPage("Index") Pages/Customers/Index

RedirectToPage("Index"), RedirectToPage("./Index") y RedirectToPage("../Index") son nombres relativos. El parámetro RedirectToPage se combina con la ruta de acceso de la página actual para calcular el nombre de la página de destino.

Vincular el nombre relativo es útil al crear sitios con una estructura compleja. Cuando se usan nombres relativos para el vínculo entre las páginas de una carpeta:

  • Al cambiar el nombre de una carpeta, no se rompen los vínculos relativos.
  • Los vínculos no se rompen porque no incluyen el nombre de la carpeta.

Para redirigir a una página en otra área, especifique el área:

RedirectToPage("/Index", new { area = "Services" });

Para obtener más información, consulte Areas de ASP.NET Core y Convenciones de aplicación y de ruta de Razor Pages en ASP.NET Core.

Atributo ViewData

Se pueden pasar datos a una página con ViewDataAttribute. Las propiedades con el atributo [ViewData] tienen sus valores almacenados y cargados desde el elemento ViewDataDictionary.

En el ejemplo siguiente, el elemento AboutModel aplica el atributo [ViewData] a la propiedad Title:

public class AboutModel : PageModel
{
    [ViewData]
    public string Title { get; } = "About";

    public void OnGet()
    {
    }
}

En la página Acerca de, acceda a la propiedad Title como propiedad de modelo:

<h1>@Model.Title</h1>

En el diseño, el título se lee desde el diccionario ViewData:

<!DOCTYPE html>
<html lang="en">
<head>
    <title>@ViewData["Title"] - WebApplication</title>
    ...

TempData

ASP.NET Core expone el elemento TempData. Esta propiedad almacena datos hasta que se leen. Los métodos Keep y Peek se pueden usar para examinar los datos sin que se eliminen. TempData es útil para el redireccionamiento cuando se necesitan los datos de más de una única solicitud.

El siguiente código establece el valor de Message mediante TempData:

public class CreateDotModel : PageModel
{
    private readonly AppDbContext _db;

    public CreateDotModel(AppDbContext db)
    {
        _db = db;
    }

    [TempData]
    public string Message { get; set; }

    [BindProperty]
    public Customer Customer { get; set; }

    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        _db.Customers.Add(Customer);
        await _db.SaveChangesAsync();
        Message = $"Customer {Customer.Name} added";
        return RedirectToPage("./Index");
    }
}

El siguiente marcado en el archivo Pages/Customers/Index.cshtml muestra el valor de Message mediante TempData.

<h3>Msg: @Model.Message</h3>

El modelo de página Pages/Customers/Index.cshtml.cs aplica el atributo [TempData] a la propiedad Message.

[TempData]
public string Message { get; set; }

Para más información, consulte TempData.

Varios controladores por página

En la página siguiente se usa el asistente de etiquetas asp-page-handler para generar marcado para dos controladores:

@page
@model CreateFATHModel

<html>
<body>
    <p>
        Enter your name.
    </p>
    <div asp-validation-summary="All"></div>
    <form method="POST">
        <div>Name: <input asp-for="Customer.Name" /></div>
        <!-- <snippet_Handlers> -->
        <input type="submit" asp-page-handler="JoinList" value="Join" />
        <input type="submit" asp-page-handler="JoinListUC" value="JOIN UC" />
        <!-- </snippet_Handlers> -->
    </form>
</body>
</html>

El formulario del ejemplo anterior tiene dos botones de envío, y cada uno de ellos usa FormActionTagHelper para enviar a una dirección URL diferente. El atributo asp-page-handler es un complemento de asp-page. asp-page-handler genera direcciones URL que envían a cada uno de los métodos de controlador definidos por una página. asp-page no se especifica porque el ejemplo se vincula a la página actual.

Modelo de página:

using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RazorPagesContacts.Data;

namespace RazorPagesContacts.Pages.Customers
{
    public class CreateFATHModel : PageModel
    {
        private readonly AppDbContext _db;

        public CreateFATHModel(AppDbContext db)
        {
            _db = db;
        }

        [BindProperty]
        public Customer Customer { get; set; }

        public async Task<IActionResult> OnPostJoinListAsync()
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }

            _db.Customers.Add(Customer);
            await _db.SaveChangesAsync();
            return RedirectToPage("/Index");
        }

        public async Task<IActionResult> OnPostJoinListUCAsync()
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }
            Customer.Name = Customer.Name?.ToUpperInvariant();
            return await OnPostJoinListAsync();
        }
    }
}

El código anterior usa métodos de controlador con nombre. Los métodos de controlador con nombre se crean tomando el texto en el nombre después de On<HTTP Verb> y antes de Async (si existe). En el ejemplo anterior, los métodos de página son OnPostJoinListAsync y OnPostJoinListUCAsync. Quitando OnPost y Async, los nombres de controlador son JoinList y JoinListUC.

<input type="submit" asp-page-handler="JoinList" value="Join" />
<input type="submit" asp-page-handler="JoinListUC" value="JOIN UC" />

Al usar el código anterior, la ruta de dirección URL que envía a OnPostJoinListAsync es https://localhost:5001/Customers/CreateFATH?handler=JoinList. La ruta de dirección URL que envía a OnPostJoinListUCAsync es https://localhost:5001/Customers/CreateFATH?handler=JoinListUC.

Rutas personalizadas

Use la directiva @page para:

  • Especificar una ruta personalizada a una página. Por ejemplo, la ruta a la página Acerca de se puede establecer en /Some/Other/Path con @page "/Some/Other/Path".
  • Anexar segmentos a la ruta predeterminada de una página. Por ejemplo, se puede agregar un segmento "item" a la ruta predeterminada de una página con @page "item".
  • Anexar parámetros a la ruta predeterminada de una página. Por ejemplo, un parámetro de identificador, id, puede ser necesario para una página con @page "{id}".

Se admite una ruta de acceso relativa raíz designada por una tilde (~) al principio de la ruta de acceso. Por ejemplo, @page "~/Some/Other/Path" es lo mismo que @page "/Some/Other/Path".

Si no le gusta la cadena de consulta ?handler=JoinList en la dirección URL, puede cambiar la ruta para poner el nombre del controlador en la parte de la ruta de la dirección URL. Para personalizar la ruta, se puede agregar una plantilla de ruta entre comillas dobles después de la directiva @page.

@page "{handler?}"
@model CreateRouteModel

<html>
<body>
    <p>
        Enter your name.
    </p>
    <div asp-validation-summary="All"></div>
    <form method="POST">
        <div>Name: <input asp-for="Customer.Name" /></div>
        <input type="submit" asp-page-handler="JoinList" value="Join" />
        <input type="submit" asp-page-handler="JoinListUC" value="JOIN UC" />
    </form>
</body>
</html>

Al usar el código anterior, la ruta de dirección URL que envía a OnPostJoinListAsync es https://localhost:5001/Customers/CreateFATH/JoinList. La ruta de dirección URL que envía a OnPostJoinListUCAsync es https://localhost:5001/Customers/CreateFATH/JoinListUC.

El signo ? que sigue a handler significa que el parámetro de ruta es opcional.

Valores de configuración avanzados

La mayoría de las aplicaciones no requieren la configuración de las siguientes secciones.

Para configurar las opciones avanzadas, use la sobrecarga AddRazorPages que configura RazorPagesOptions:

using Microsoft.EntityFrameworkCore;
using RazorPagesContacts.Data;
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages(options =>
{
    options.RootDirectory = "/MyPages";
    options.Conventions.AuthorizeFolder("/MyPages/Admin");
});

builder.Services.AddDbContext<CustomerDbContext>(options =>
    options.UseInMemoryDatabase("name"));

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

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

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();

Use el elemento RazorPagesOptions para establecer el directorio raíz de páginas o agregar convenciones de modelo de aplicación para las páginas. Para obtener más información sobre las convenciones, vea Convenciones de autorización de Razor Pages.

Para precompilar vistas, consulte la sección sobre la compilación de vistas de Razor.

Especificación de Razor Pages en la raíz del contenido

De forma predeterminada, Razor Pages se encuentra en la raíz del directorio /Pages. Agregue WithRazorPagesAtContentRoot para especificar que sus instancias de Razor Pages se encuentran en la raíz de contenido (ContentRootPath) de la aplicación:

using Microsoft.EntityFrameworkCore;
using RazorPagesContacts.Data;
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages(options =>
{
    options.Conventions.AuthorizeFolder("/MyPages/Admin");
})
  .WithRazorPagesAtContentRoot();

builder.Services.AddDbContext<CustomerDbContext>(options =>
    options.UseInMemoryDatabase("name"));

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

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

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();

Especificación de Razor Pages en un directorio raíz personalizado

Agregue WithRazorPagesRoot para especificar que Razor Pages se encuentra en un directorio raíz personalizado en la aplicación (proporcione una ruta de acceso relativa):

using Microsoft.EntityFrameworkCore;
using RazorPagesContacts.Data;
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages(options =>
{
    options.Conventions.AuthorizeFolder("/MyPages/Admin");
})
  .WithRazorPagesRoot("/path/to/razor/pages");

builder.Services.AddDbContext<CustomerDbContext>(options =>
    options.UseInMemoryDatabase("name"));

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

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

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();

Recursos adicionales

Crear un proyecto de Razor Pages

Vea Introducción a Razor Pages para obtener instrucciones detalladas sobre cómo crear un proyecto Razor Pages.

Razor Pages

Razor Pages está habilitado en Startup.cs:

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddRazorPages();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseExceptionHandler("/Error");
            app.UseHsts();
        }

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

        app.UseRouting();

        app.UseAuthorization();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapRazorPages();
        });
    }
}

Considere la posibilidad de una página básica:

@page

<h1>Hello, world!</h1>
<h2>The time on the server is @DateTime.Now</h2>

El código anterior se parece mucho a un archivo de vista de Razor que se utiliza en una aplicación ASP.NET Core con controladores y vistas. La directiva @page lo hace diferente. @page transforma el archivo en una acción de MVC, lo que significa que administra las solicitudes directamente, sin tener que pasar a través de un controlador. @page debe ser la primera directiva de Razor de una página. @page afecta al comportamiento de otras construcciones de Razor. Los nombres de archivo de Razor tienen un sufijo .cshtml.

Una página similar, con una clase PageModel, se muestra en los dos archivos siguientes. El archivo Pages/Index2.cshtml:

@page
@using RazorPagesIntro.Pages
@model Index2Model

<h2>Separate page model</h2>
<p>
    @Model.Message
</p>

Modelo de página Pages/Index2.cshtml.cs:

using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;
using System;

namespace RazorPagesIntro.Pages
{
    public class Index2Model : PageModel
    {
        public string Message { get; private set; } = "PageModel in C#";

        public void OnGet()
        {
            Message += $" Server time is { DateTime.Now }";
        }
    }
}

Por convención, el archivo de clase PageModel tiene el mismo nombre que el archivo de Razor Pages con .cs anexado. Por ejemplo, la página de Razor anterior es Pages/Index2.cshtml. El archivo que contiene la clase PageModel se denomina Pages/Index2.cshtml.cs.

Las asociaciones de rutas de dirección URL a páginas se determinan según la ubicación de la página en el sistema de archivos. En la tabla siguiente, se muestra una ruta de acceso Razor Pages y la dirección URL correspondiente:

Ruta de acceso y nombre de archivo URL correspondiente
/Pages/Index.cshtml / o /Index
/Pages/Contact.cshtml /Contact
/Pages/Store/Contact.cshtml /Store/Contact
/Pages/Store/Index.cshtml /Store o /Store/Index

Notas:

  • El entorno de ejecución busca archivos de páginas de Razor en la carpeta Pages de forma predeterminada.
  • Index es la página predeterminada cuando una URL no incluye una página.

Escritura de un formulario básico

Razor Pages está diseñado para facilitar la implementación de patrones comunes que se usan con exploradores web al compilar una aplicación. Los enlaces de modelos, los asistentes de etiquetas y los asistentes de HTML simplemente funcionan con las propiedades definidas en una clase de Razor Pages. Considere la posibilidad de una página que implementa un formulario básico del estilo "Póngase en contacto con nosotros" para el modelo Contact:

Para los ejemplos de este documento, DbContext se inicializa en el archivo Startup.cs.

La base de datos en memoria requiere el paquete NuGet Microsoft.EntityFrameworkCore.InMemory.

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<CustomerDbContext>(options =>
                      options.UseInMemoryDatabase("name"));
    services.AddRazorPages();
}

El modelo de datos:

using System.ComponentModel.DataAnnotations;

namespace RazorPagesContacts.Models
{
    public class Customer
    {
        public int Id { get; set; }

        [Required, StringLength(10)]
        public string Name { get; set; }
    }
}

El contexto de la base de datos:

using Microsoft.EntityFrameworkCore;
using RazorPagesContacts.Models;

namespace RazorPagesContacts.Data
{
    public class CustomerDbContext : DbContext
    {
        public CustomerDbContext(DbContextOptions options)
            : base(options)
        {
        }

        public DbSet<Customer> Customers { get; set; }
    }
}

Archivo de vista Pages/Create.cshtml:

@page
@model RazorPagesContacts.Pages.Customers.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<p>Enter a customer name:</p>

<form method="post">
    Name:
    <input asp-for="Customer.Name" />
    <input type="submit" />
</form>

Modelo de página Pages/Create.cshtml.cs:

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RazorPagesContacts.Data;
using RazorPagesContacts.Models;
using System.Threading.Tasks;

namespace RazorPagesContacts.Pages.Customers
{
    public class CreateModel : PageModel
    {
        private readonly CustomerDbContext _context;

        public CreateModel(CustomerDbContext context)
        {
            _context = context;
        }

        public IActionResult OnGet()
        {
            return Page();
        }

        [BindProperty]
        public Customer Customer { get; set; }

        public async Task<IActionResult> OnPostAsync()
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }

            _context.Customers.Add(Customer);
            await _context.SaveChangesAsync();

            return RedirectToPage("./Index");
        }
    }
}

Por convención, la clase PageModel se denomina <PageName>Model y se encuentra en el mismo espacio de nombres que la página.

La clase PageModel permite la separación de la lógica de una página de su presentación. Define los controladores de página para solicitudes que se envían a la página y los datos que usan para representar la página. Esta separación permite lo siguiente:

La página tiene un método de controladorOnPostAsync, que se ejecuta en solicitudes POST (cuando un usuario envía el formulario). Se pueden agregar métodos de controlador para cualquier verbo HTTP. Los controladores más comunes son:

  • OnGet para inicializar el estado necesario para la página. En el código anterior, el método OnGet muestra la instancia de la página de CreateModel.cshtmlRazor.
  • OnPost para controlar los envíos del formulario.

El sufijo de nombre Async es opcional, pero se usa a menudo por convención para funciones asincrónicas. El código anterior es típico de Razor Pages.

Si está familiarizado con las aplicaciones de ASP.NET con controladores y vistas:

  • El código OnPostAsync del ejemplo anterior es similar al típico código de controlador.
  • La mayoría de los primitivos de MVC, como el enlace de modelos, la validación y los resultados de acciones, funcionan del mismo modo con los controladores y Razor Pages.

El método OnPostAsync anterior:

public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    _context.Customers.Add(Customer);
    await _context.SaveChangesAsync();

    return RedirectToPage("./Index");
}

El flujo básico de OnPostAsync:

Compruebe los errores de validación.

  • Si no hay ningún error, guarde los datos y redirija.
  • Si hay errores, muestre la página de nuevo con mensajes de validación. En muchos casos, los errores de validación se detectan en el cliente y nunca se envían al servidor.

Archivo de vista Pages/Create.cshtml:

@page
@model RazorPagesContacts.Pages.Customers.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<p>Enter a customer name:</p>

<form method="post">
    Name:
    <input asp-for="Customer.Name" />
    <input type="submit" />
</form>

El código HTML representado de Pages/Create.cshtml:

<p>Enter a customer name:</p>

<form method="post">
    Name:
    <input type="text" data-val="true"
           data-val-length="The field Name must be a string with a maximum length of 10."
           data-val-length-max="10" data-val-required="The Name field is required."
           id="Customer_Name" maxlength="10" name="Customer.Name" value="" />
    <input type="submit" />
    <input name="__RequestVerificationToken" type="hidden"
           value="<Antiforgery token here>" />
</form>

En el código anterior, la publicación del formulario:

  • Con datos válidos:

    • El método del controlador OnPostAsync llama al método auxiliar RedirectToPage. RedirectToPage devuelve una instancia de RedirectToPageResult. RedirectToPage:

      • Es el resultado de una acción.
      • Es similar a RedirectToAction o RedirectToRoute (se usa en controladores y vistas).
      • Se ha personalizado para las páginas. En el ejemplo anterior, redirige a la página de índice raíz (/Index). RedirectToPage se detalla en la sección Generación de direcciones URL para las páginas.
  • Con errores de validación que se pasan al servidor:

    • El método del controlador OnPostAsync llama al método auxiliar Page. Page devuelve una instancia de PageResult. Devolver Page es similar a cómo las acciones en los controladores devuelven View. PageResult es el tipo de valor devuelto predeterminado para un método de controlador. Un método de controlador que devuelve void representa la página.
    • En el ejemplo anterior, la publicación del formulario sin valores hace que se devuelva ModelState.IsValid como false. En este ejemplo, no se muestra ningún error de validación en el cliente. La entrega de errores de validación se trata más adelante en este documento.
    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }
    
        _context.Customers.Add(Customer);
        await _context.SaveChangesAsync();
    
        return RedirectToPage("./Index");
    }
    
  • Con errores de validación detectados mediante la validación del lado cliente:

    • Los datos no se publican en el servidor.
    • La validación del lado cliente se explica más adelante en este documento.

La propiedad Customer usa el atributo [BindProperty] para participar en el enlace de modelos:

public class CreateModel : PageModel
{
    private readonly CustomerDbContext _context;

    public CreateModel(CustomerDbContext context)
    {
        _context = context;
    }

    public IActionResult OnGet()
    {
        return Page();
    }

    [BindProperty]
    public Customer Customer { get; set; }

    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        _context.Customers.Add(Customer);
        await _context.SaveChangesAsync();

        return RedirectToPage("./Index");
    }
}

[BindProperty]no debe usarse en modelos que contengan propiedades que el cliente no debe cambiar. Para más información, consulte Publicación excesiva.

De forma predeterminada, Razor Pages enlaza propiedades solo con verbos que no sean GET. El enlace a propiedades elimina la necesidad de escribir código para convertir los datos HTTP en el tipo de modelo. Enlazar reduce el código al usar la misma propiedad para representar los campos de formulario (<input asp-for="Customer.Name">) y aceptar la entrada.

Advertencia

Por motivos de seguridad, debe participar en el enlace de datos de solicitud GET con las propiedades del modelo de página. Compruebe las entradas de los usuarios antes de asignarlas a las propiedades. Si participa en el enlace de GET, le puede ser útil al trabajar con escenarios que dependan de cadenas de consultas o valores de rutas.

Para enlazar una propiedad en solicitudes GET, establezca la propiedad SupportsGet del atributo [BindProperty] en true:

[BindProperty(SupportsGet = true)]

Para obtener más información, vea ASP.NET Core Community Standup: Bind on GET discussion (YouTube).

Revisión del archivo de vista Pages/Create.cshtml:

@page
@model RazorPagesContacts.Pages.Customers.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<p>Enter a customer name:</p>

<form method="post">
    Name:
    <input asp-for="Customer.Name" />
    <input type="submit" />
</form>
  • En el código anterior, el asistente de etiquetas de entrada<input asp-for="Customer.Name" /> enlaza el elemento <input> HTML con la expresión del modelo Customer.Name.
  • @addTagHelper hace que los asistentes de etiquetas estén disponibles.

La página principal

Index.cshtml es la página principal:

@page
@model RazorPagesContacts.Pages.Customers.IndexModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<h1>Contacts home page</h1>
<form method="post">
    <table class="table">
        <thead>
            <tr>
                <th>ID</th>
                <th>Name</th>
                <th></th>
            </tr>
        </thead>
        <tbody>
            @foreach (var contact in Model.Customer)
            {
                <tr>
                    <td> @contact.Id  </td>
                    <td>@contact.Name</td>
                    <td>
                        <!-- <snippet_Edit> -->
                        <a asp-page="./Edit" asp-route-id="@contact.Id">Edit</a> |
                        <!-- </snippet_Edit> -->
                        <!-- <snippet_Delete> -->
                        <button type="submit" asp-page-handler="delete" asp-route-id="@contact.Id">delete</button>
                        <!-- </snippet_Delete> -->
                    </td>
                </tr>
            }
        </tbody>
    </table>
    <a asp-page="Create">Create New</a>
</form>

Clase PageModel asociada (Index.cshtml.cs):

public class IndexModel : PageModel
{
    private readonly CustomerDbContext _context;

    public IndexModel(CustomerDbContext context)
    {
        _context = context;
    }

    public IList<Customer> Customer { get; set; }

    public async Task OnGetAsync()
    {
        Customer = await _context.Customers.ToListAsync();
    }

    public async Task<IActionResult> OnPostDeleteAsync(int id)
    {
        var contact = await _context.Customers.FindAsync(id);

        if (contact != null)
        {
            _context.Customers.Remove(contact);
            await _context.SaveChangesAsync();
        }

        return RedirectToPage();
    }
}

El archivo Index.cshtml contiene el siguiente marcado:

<a asp-page="./Edit" asp-route-id="@contact.Id">Edit</a> |

El <a /a>asistente de etiquetas delimitadoras ha usado el atributo asp-route-{value} para generar un vínculo a la página de edición. El vínculo contiene datos de ruta con el identificador del contacto. Por ejemplo: https://localhost:5001/Edit/1. Las aplicaciones auxiliares de etiquetas permiten que el código de servidor participe en la creación y la representación de elementos HTML en archivos de Razor.

El archivo Index.cshtml contiene marcado para crear un botón de eliminar para cada contacto de cliente:

<button type="submit" asp-page-handler="delete" asp-route-id="@contact.Id">delete</button>

El código HTML representado:

<button type="submit" formaction="/Customers?id=1&amp;handler=delete">delete</button>

Al representar el botón de eliminar en HTML, el elemento formaction incluye parámetros para los siguientes elementos:

  • Id. de contacto de cliente especificado mediante el atributo asp-route-id.
  • handler especificado mediante el atributo asp-page-handler.

Al seleccionar el botón, se envía una solicitud de formulario POST al servidor. De forma predeterminada, el nombre del método de control se selecciona de acuerdo con el valor del parámetro handler y según el esquema OnPost[handler]Async.

Como en este ejemplo handler es delete, el método de control OnPostDeleteAsync se usa para procesar la solicitud POST. Si asp-page-handler se establece en otro valor, como remove, se seleccionará un método de controlador llamado OnPostRemoveAsync.

public async Task<IActionResult> OnPostDeleteAsync(int id)
{
    var contact = await _context.Customers.FindAsync(id);

    if (contact != null)
    {
        _context.Customers.Remove(contact);
        await _context.SaveChangesAsync();
    }

    return RedirectToPage();
}

El método OnPostDeleteAsync realiza las acciones siguientes:

  • Obtiene el elemento id de la cadena de consulta.
  • Realiza una consulta a la base de datos del contacto de cliente con FindAsync.
  • Si se encuentra el contacto del cliente, este se quita y se actualiza la base de datos.
  • Llama a RedirectToPage para redirigir la página Index raíz (/Index).

El archivo Edit.cshtml

@page "{id:int}"
@model RazorPagesContacts.Pages.Customers.EditModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers


<h1>Edit Customer - @Model.Customer.Id</h1>
<form method="post">
    <div asp-validation-summary="All"></div>
    <input asp-for="Customer.Id" type="hidden" />
    <div>
        <label asp-for="Customer.Name"></label>
        <div>
            <input asp-for="Customer.Name" />
            <span asp-validation-for="Customer.Name"></span>
        </div>
    </div>

    <div>
        <button type="submit">Save</button>
    </div>
</form>

La primera línea contiene la directiva @page "{id:int}". La restricción de enrutamiento "{id:int}" indica a la página que acepte las solicitudes a la página que contienen datos de ruta int. Si una solicitud a la página no contiene datos de ruta que se puedan convertir en int, el tiempo de ejecución devuelve un error HTTP 404 (no encontrado). Para que el identificador sea opcional, anexe ? a la restricción de ruta:

@page "{id:int?}"

El archivo Edit.cshtml.cs:

public class EditModel : PageModel
{
    private readonly CustomerDbContext _context;

    public EditModel(CustomerDbContext context)
    {
        _context = context;
    }

    [BindProperty]
    public Customer Customer { get; set; }

    public async Task<IActionResult> OnGetAsync(int id)
    {
        Customer = await _context.Customers.FindAsync(id);

        if (Customer == null)
        {
            return RedirectToPage("./Index");
        }

        return Page();
    }

    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        _context.Attach(Customer).State = EntityState.Modified;

        try
        {
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            throw new Exception($"Customer {Customer.Id} not found!");
        }

        return RedirectToPage("./Index");
    }

}

Validación

Reglas de validación:

  • Se especifican mediante declaración en la clase de modelo.
  • Se aplican en toda la aplicación.

El espacio de nombres System.ComponentModel.DataAnnotations proporciona un conjunto de atributos de validación integrados que se aplican mediante declaración a una clase o propiedad. DataAnnotations también contiene atributos de formato como [DataType] que ayudan a aplicar formato y no proporcionan ninguna validación.

Considere el modelo Customer:

using System.ComponentModel.DataAnnotations;

namespace RazorPagesContacts.Models
{
    public class Customer
    {
        public int Id { get; set; }

        [Required, StringLength(10)]
        public string Name { get; set; }
    }
}

Con el siguiente archivo de vista Create.cshtml:

@page
@model RazorPagesContacts.Pages.Customers.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<p>Validation: customer name:</p>

<form method="post">
    <div asp-validation-summary="ModelOnly"></div>
    <span asp-validation-for="Customer.Name"></span>
    Name:
    <input asp-for="Customer.Name" />
    <input type="submit" />
</form>

<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/jquery-validation/dist/jquery.validate.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script>

El código anterior:

  • Incluye scripts de validación de jQuery y jQuery.

  • Usa los asistentes de etiquetas<div /> y <span /> para habilitar lo siguiente:

    • Validación del lado cliente.
    • Representación del error de validación.
  • Se genera el siguiente código HTML:

    <p>Enter a customer name:</p>
    
    <form method="post">
        Name:
        <input type="text" data-val="true"
               data-val-length="The field Name must be a string with a maximum length of 10."
               data-val-length-max="10" data-val-required="The Name field is required."
               id="Customer_Name" maxlength="10" name="Customer.Name" value="" />
        <input type="submit" />
        <input name="__RequestVerificationToken" type="hidden"
               value="<Antiforgery token here>" />
    </form>
    
    <script src="/lib/jquery/dist/jquery.js"></script>
    <script src="/lib/jquery-validation/dist/jquery.validate.js"></script>
    <script src="/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script>
    

Al publicar el formulario de creación sin un valor de nombre, se muestra el mensaje de error "El campo Nombre es obligatorio" en el formulario. Si JavaScript está habilitado en el cliente, el explorador muestra el error sin realizar la publicación en el servidor.

El atributo [StringLength(10)] genera data-val-length-max="10" en el código HTML representado. data-val-length-max impide que los exploradores superen la longitud máxima especificada al escribir. Si se usa una herramienta como Fiddler para editar y reproducir la publicación:

  • Con el nombre de más de 10 caracteres.
  • Se devuelve el mensaje de error "El nombre del campo debe ser una cadena con una longitud máxima de 10 caracteres".

Considere el modelo Movie siguiente:

using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace RazorPagesMovie.Models
{
    public class Movie
    {
        public int ID { get; set; }

        [StringLength(60, MinimumLength = 3)]
        [Required]
        public string Title { get; set; }

        [Display(Name = "Release Date")]
        [DataType(DataType.Date)]
        public DateTime ReleaseDate { get; set; }

        [Range(1, 100)]
        [DataType(DataType.Currency)]
        [Column(TypeName = "decimal(18, 2)")]
        public decimal Price { get; set; }

        [RegularExpression(@"^[A-Z]+[a-zA-Z\s]*$")]
        [Required]
        [StringLength(30)]
        public string Genre { get; set; }

        [RegularExpression(@"^[A-Z]+[a-zA-Z0-9""'\s-]*$")]
        [StringLength(5)]
        [Required]
        public string Rating { get; set; }
    }
}

Los atributos de validación especifican el comportamiento que se exigirá a las propiedades del modelo al que se aplican:

  • Los atributos Required y MinimumLength indican que una propiedad debe tener un valor, pero nada impide al usuario escribir espacios en blanco para satisfacer esta validación.

  • El atributo RegularExpression se usa para limitar los caracteres que se pueden escribir. En el código anterior, "Género":

    • Solo debe usar letras.
    • La primera letra debe estar en mayúsculas. No se permiten espacios en blanco, números ni caracteres especiales.
  • La "Clasificación" de RegularExpression:

    • Requiere que el primer carácter sea una letra mayúscula.
    • Permite caracteres especiales y números en los espacios posteriores. "PG-13" es válido para una "Clasificación", pero se produce un error en un "Género".
  • El atributo Range restringe un valor a un intervalo determinado.

  • El atributo StringLength establece la longitud máxima de una propiedad de cadena y, opcionalmente, su longitud mínima.

  • Los tipos de valor (como decimal, int, float, DateTime) son intrínsecamente necesarios y no necesitan el atributo [Required].

La página de creación del modelo Movie muestra errores de visualización con valores no válidos:

Formulario de vista de película con varios errores de validación de cliente de jQuery

Para obtener más información, consulte:

Control de solicitudes HEAD con un controlador OnGet de reserva

Las solicitudes HEAD permiten recuperar los encabezados de un recurso específico. A diferencia de las solicitudes GET, las solicitudes HEAD no devuelven un cuerpo de respuesta.

Normalmente, se crea un controlador OnHead al que se llama para las solicitudes HEAD:

public void OnHead()
{
    HttpContext.Response.Headers.Add("Head Test", "Handled by OnHead!");
}

Razor Pages recurre a una llamada al controlador OnGet si no se define ningún controlador OnHead.

XSRF/CSRF y Razor Pages

Razor Pages está protegido mediante validación antifalsificación. El elemento FormTagHelper inserta tokens antifalsificación en los elementos de formulario HTML.

Usar diseños, parciales, plantillas y asistentes de etiquetas con Razor Pages

Las páginas funcionan con todas las características del motor de vista de Razor. Los diseños, parciales, plantillas, asistentes de etiquetas, _ViewStart.cshtml y _ViewImports.cshtml funcionan de la misma manera que lo hacen con las vistas de Razor convencionales.

Para simplificar esta página, aprovecharemos algunas de esas características.

Agregue una página de diseño a Pages/Shared/_Layout.cshtml:

<!DOCTYPE html>
<html>
<head>
    <title>RP Sample</title>
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
</head>
<body>
    <a asp-page="/Index">Home</a>
    <a asp-page="/Customers/Create">Create</a>
    <a asp-page="/Customers/Index">Customers</a> <br />

    @RenderBody()
    <script src="~/lib/jquery/dist/jquery.js"></script>
    <script src="~/lib/jquery-validation/dist/jquery.validate.js"></script>
    <script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script>
</body>
</html>

El diseño:

  • Controla el diseño de cada página (a no ser que la página no tenga diseño).
  • Importa las estructuras HTML como JavaScript y hojas de estilos.
  • El contenido de la página de Razor se representa donde se llama a @RenderBody().

Para más información, consulte la página de diseño.

La propiedad Layout se establece en Pages/_ViewStart.cshtml:

@{
    Layout = "_Layout";
}

El diseño está en la carpeta Pages/Shared. Las páginas buscan otras vistas (diseños, plantillas, parciales) de forma jerárquica, a partir de la misma carpeta que la página actual. Un diseño en la carpeta Pages/Shared se puede usar desde cualquier página de Razor en la carpeta Pages.

El archivo de diseño debería ir en la carpeta Pages/Shared.

Le recomendamos que no coloque el archivo de diseño en la carpeta Views/Shared. Views/Shared es un patrón de vistas de MVC. Razor Pages está diseñado para basarse en la jerarquía de carpetas, no en las convenciones de ruta de acceso.

La búsqueda de vistas de una instancia de Razor Pages incluye la carpeta Pages. Los diseños, plantillas y parciales que se usan con los controladores de MVC y las vistas de Razor convencionales simplemente funcionan.

Agregue un archivo Pages/_ViewImports.cshtml:

@namespace RazorPagesContacts.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

@namespace se explica más adelante en el tutorial. La directiva @addTagHelper pone los asistentes de etiquetas integradas en todas las páginas de la carpeta Pages.

La directiva @namespace establecida en una página:

@page
@namespace RazorPagesIntro.Pages.Customers

@model NameSpaceModel

<h2>Name space</h2>
<p>
    @Model.Message
</p>

La directiva @namespace establece el espacio de nombres de la página. La directiva @model no necesita incluir el espacio de nombres.

Cuando la directiva @namespace se encuentra en _ViewImports.cshtml, el espacio de nombres especificado proporciona el prefijo del espacio de nombres generado en la página que importa la directiva @namespace. El resto del espacio de nombres generado (la parte del sufijo) es la ruta de acceso relativa separada por puntos entre la carpeta que contiene _ViewImports.cshtml y la carpeta que contiene la página.

Por ejemplo, la clase PageModelPages/Customers/Edit.cshtml.cs establece explícitamente el espacio de nombres:

namespace RazorPagesContacts.Pages
{
    public class EditModel : PageModel
    {
        private readonly AppDbContext _db;

        public EditModel(AppDbContext db)
        {
            _db = db;
        }

        // Code removed for brevity.

El archivo Pages/_ViewImports.cshtml establece el espacio de nombres siguiente:

@namespace RazorPagesContacts.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

El espacio de nombres generado para la página de Pages/Customers/Edit.cshtmlRazor es el mismo que la clase PageModel.

@namespacetambién funciona con vistas de Razor convencionales.

Considere el archivo de vista Pages/Create.cshtml:

@page
@model RazorPagesContacts.Pages.Customers.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<p>Validation: customer name:</p>

<form method="post">
    <div asp-validation-summary="ModelOnly"></div>
    <span asp-validation-for="Customer.Name"></span>
    Name:
    <input asp-for="Customer.Name" />
    <input type="submit" />
</form>

<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/jquery-validation/dist/jquery.validate.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script>

Archivo de vista actualizado Pages/Create.cshtml con _ViewImports.cshtml y el archivo de distribución anterior:

@page
@model CreateModel

<p>Enter a customer name:</p>

<form method="post">
    Name:
    <input asp-for="Customer.Name" />
    <input type="submit" />
</form>

En el código anterior, el elemento _ViewImports.cshtml importó el espacio de nombres y los asistentes de etiquetas. El archivo de distribución importó los archivos JavaScript.

El proyecto de inicio de Razor Pages contiene Pages/_ValidationScriptsPartial.cshtml, que enlaza la validación del lado cliente.

Para más información sobre las vistas parciales, vea Vistas parciales en ASP.NET Core.

Generación de direcciones URL para las páginas

La página Create, mostrada anteriormente, usa RedirectToPage:

public class CreateModel : PageModel
{
    private readonly CustomerDbContext _context;

    public CreateModel(CustomerDbContext context)
    {
        _context = context;
    }

    public IActionResult OnGet()
    {
        return Page();
    }

    [BindProperty]
    public Customer Customer { get; set; }

    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        _context.Customers.Add(Customer);
        await _context.SaveChangesAsync();

        return RedirectToPage("./Index");
    }
}

La aplicación tiene la siguiente estructura de archivos o carpetas:

  • /Pages

    • Index.cshtml

    • Privacy.cshtml

    • /Customers

      • Create.cshtml
      • Edit.cshtml
      • Index.cshtml

Las páginas Pages/Customers/Create.cshtml y Pages/Customers/Edit.cshtml redirigen a Pages/Customers/Index.cshtml si la operación se realiza correctamente. La cadena ./Index es un nombre de página relativo que se usa para acceder a la página anterior. Se usa para generar direcciones URL a la página Pages/Customers/Index.cshtml. Por ejemplo:

  • Url.Page("./Index", ...)
  • <a asp-page="./Index">Customers Index Page</a>
  • RedirectToPage("./Index")

El nombre de página absoluto /Index se usa para generar direcciones URL en la página Pages/Index.cshtml. Por ejemplo:

  • Url.Page("/Index", ...)
  • <a asp-page="/Index">Home Index Page</a>
  • RedirectToPage("/Index")

El nombre de página es la ruta de acceso a la página de la carpeta raíz /Pages, incluido un / inicial, por ejemplo /Index. Los ejemplos anteriores de generación de URL ofrecen opciones mejoradas y capacidades funcionales en comparación con la escritura a mano de estas. La generación de direcciones URL usa el enrutamiento y puede generar y codificar parámetros según cómo se defina la ruta en la ruta de acceso de destino.

La generación de direcciones URL para las páginas admite nombres relativos. En la siguiente tabla, se muestra qué página de índice está seleccionada usando distintos parámetros RedirectToPage en Pages/Customers/Create.cshtml.

RedirectToPage(x) Página
RedirectToPage("/Index") Pages/Index
RedirectToPage("./Index"); Pages/Customers/Index
RedirectToPage("../Index") Pages/Index
RedirectToPage("Index") Pages/Customers/Index

RedirectToPage("Index"), RedirectToPage("./Index") y RedirectToPage("../Index") son nombres relativos. El parámetro RedirectToPage se combina con la ruta de acceso de la página actual para calcular el nombre de la página de destino.

Vincular el nombre relativo es útil al crear sitios con una estructura compleja. Cuando se usan nombres relativos para el vínculo entre las páginas de una carpeta:

  • Al cambiar el nombre de una carpeta, no se rompen los vínculos relativos.
  • Los vínculos no se rompen porque no incluyen el nombre de la carpeta.

Para redirigir a una página en otra área, especifique el área:

RedirectToPage("/Index", new { area = "Services" });

Para obtener más información, consulte Areas de ASP.NET Core y Convenciones de aplicación y de ruta de Razor Pages en ASP.NET Core.

Atributo ViewData

Se pueden pasar datos a una página con ViewDataAttribute. Las propiedades con el atributo [ViewData] tienen sus valores almacenados y cargados desde el elemento ViewDataDictionary.

En el ejemplo siguiente, el elemento AboutModel aplica el atributo [ViewData] a la propiedad Title:

public class AboutModel : PageModel
{
    [ViewData]
    public string Title { get; } = "About";

    public void OnGet()
    {
    }
}

En la página Acerca de, acceda a la propiedad Title como propiedad de modelo:

<h1>@Model.Title</h1>

En el diseño, el título se lee desde el diccionario ViewData:

<!DOCTYPE html>
<html lang="en">
<head>
    <title>@ViewData["Title"] - WebApplication</title>
    ...

TempData

ASP.NET Core expone el elemento TempData. Esta propiedad almacena datos hasta que se leen. Los métodos Keep y Peek se pueden usar para examinar los datos sin que se eliminen. TempData es útil para el redireccionamiento cuando se necesitan los datos de más de una única solicitud.

El siguiente código establece el valor de Message mediante TempData:

public class CreateDotModel : PageModel
{
    private readonly AppDbContext _db;

    public CreateDotModel(AppDbContext db)
    {
        _db = db;
    }

    [TempData]
    public string Message { get; set; }

    [BindProperty]
    public Customer Customer { get; set; }

    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        _db.Customers.Add(Customer);
        await _db.SaveChangesAsync();
        Message = $"Customer {Customer.Name} added";
        return RedirectToPage("./Index");
    }
}

El siguiente marcado en el archivo Pages/Customers/Index.cshtml muestra el valor de Message mediante TempData.

<h3>Msg: @Model.Message</h3>

El modelo de página Pages/Customers/Index.cshtml.cs aplica el atributo [TempData] a la propiedad Message.

[TempData]
public string Message { get; set; }

Para más información, consulte TempData.

Varios controladores por página

En la página siguiente se usa el asistente de etiquetas asp-page-handler para generar marcado para dos controladores:

@page
@model CreateFATHModel

<html>
<body>
    <p>
        Enter your name.
    </p>
    <div asp-validation-summary="All"></div>
    <form method="POST">
        <div>Name: <input asp-for="Customer.Name" /></div>
        <!-- <snippet_Handlers> -->
        <input type="submit" asp-page-handler="JoinList" value="Join" />
        <input type="submit" asp-page-handler="JoinListUC" value="JOIN UC" />
        <!-- </snippet_Handlers> -->
    </form>
</body>
</html>

El formulario del ejemplo anterior tiene dos botones de envío, y cada uno de ellos usa FormActionTagHelper para enviar a una dirección URL diferente. El atributo asp-page-handler es un complemento de asp-page. asp-page-handler genera direcciones URL que envían a cada uno de los métodos de controlador definidos por una página. asp-page no se especifica porque el ejemplo se vincula a la página actual.

Modelo de página:

using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RazorPagesContacts.Data;

namespace RazorPagesContacts.Pages.Customers
{
    public class CreateFATHModel : PageModel
    {
        private readonly AppDbContext _db;

        public CreateFATHModel(AppDbContext db)
        {
            _db = db;
        }

        [BindProperty]
        public Customer Customer { get; set; }

        public async Task<IActionResult> OnPostJoinListAsync()
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }

            _db.Customers.Add(Customer);
            await _db.SaveChangesAsync();
            return RedirectToPage("/Index");
        }

        public async Task<IActionResult> OnPostJoinListUCAsync()
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }
            Customer.Name = Customer.Name?.ToUpperInvariant();
            return await OnPostJoinListAsync();
        }
    }
}

El código anterior usa métodos de controlador con nombre. Los métodos de controlador con nombre se crean tomando el texto en el nombre después de On<HTTP Verb> y antes de Async (si existe). En el ejemplo anterior, los métodos de página son OnPostJoinListAsync y OnPostJoinListUCAsync. Quitando OnPost y Async, los nombres de controlador son JoinList y JoinListUC.

<input type="submit" asp-page-handler="JoinList" value="Join" />
<input type="submit" asp-page-handler="JoinListUC" value="JOIN UC" />

Al usar el código anterior, la ruta de dirección URL que envía a OnPostJoinListAsync es https://localhost:5001/Customers/CreateFATH?handler=JoinList. La ruta de dirección URL que envía a OnPostJoinListUCAsync es https://localhost:5001/Customers/CreateFATH?handler=JoinListUC.

Rutas personalizadas

Use la directiva @page para:

  • Especificar una ruta personalizada a una página. Por ejemplo, la ruta a la página Acerca de se puede establecer en /Some/Other/Path con @page "/Some/Other/Path".
  • Anexar segmentos a la ruta predeterminada de una página. Por ejemplo, se puede agregar un segmento "item" a la ruta predeterminada de una página con @page "item".
  • Anexar parámetros a la ruta predeterminada de una página. Por ejemplo, un parámetro de identificador, id, puede ser necesario para una página con @page "{id}".

Se admite una ruta de acceso relativa raíz designada por una tilde (~) al principio de la ruta de acceso. Por ejemplo, @page "~/Some/Other/Path" es lo mismo que @page "/Some/Other/Path".

Si no le gusta la cadena de consulta ?handler=JoinList en la dirección URL, puede cambiar la ruta para poner el nombre del controlador en la parte de la ruta de la dirección URL. Para personalizar la ruta, se puede agregar una plantilla de ruta entre comillas dobles después de la directiva @page.

@page "{handler?}"
@model CreateRouteModel

<html>
<body>
    <p>
        Enter your name.
    </p>
    <div asp-validation-summary="All"></div>
    <form method="POST">
        <div>Name: <input asp-for="Customer.Name" /></div>
        <input type="submit" asp-page-handler="JoinList" value="Join" />
        <input type="submit" asp-page-handler="JoinListUC" value="JOIN UC" />
    </form>
</body>
</html>

Al usar el código anterior, la ruta de dirección URL que envía a OnPostJoinListAsync es https://localhost:5001/Customers/CreateFATH/JoinList. La ruta de dirección URL que envía a OnPostJoinListUCAsync es https://localhost:5001/Customers/CreateFATH/JoinListUC.

El signo ? que sigue a handler significa que el parámetro de ruta es opcional.

Valores de configuración avanzados

La mayoría de las aplicaciones no requieren la configuración de las siguientes secciones.

Para configurar las opciones avanzadas, use la sobrecarga AddRazorPages que configura RazorPagesOptions:

public void ConfigureServices(IServiceCollection services)
{            
    services.AddRazorPages(options =>
    {
        options.RootDirectory = "/MyPages";
        options.Conventions.AuthorizeFolder("/MyPages/Admin");
    });
}

Use el elemento RazorPagesOptions para establecer el directorio raíz de páginas o agregar convenciones de modelo de aplicación para las páginas. Para obtener más información sobre las convenciones, vea Convenciones de autorización de Razor Pages.

Para precompilar vistas, consulte la sección sobre la compilación de vistas de Razor.

Especificación de Razor Pages en la raíz del contenido

De forma predeterminada, Razor Pages se encuentra en la raíz del directorio /Pages. Agregue WithRazorPagesAtContentRoot para especificar que sus instancias de Razor Pages se encuentran en la raíz de contenido (ContentRootPath) de la aplicación:

public void ConfigureServices(IServiceCollection services)
{            
    services.AddRazorPages(options =>
        {
            options.Conventions.AuthorizeFolder("/MyPages/Admin");
        })
        .WithRazorPagesAtContentRoot();
}

Especificación de Razor Pages en un directorio raíz personalizado

Agregue WithRazorPagesRoot para especificar que Razor Pages se encuentra en un directorio raíz personalizado en la aplicación (proporcione una ruta de acceso relativa):

public void ConfigureServices(IServiceCollection services)
{            
    services.AddRazorPages(options =>
        {
            options.Conventions.AuthorizeFolder("/MyPages/Admin");
        })
        .WithRazorPagesRoot("/path/to/razor/pages");
}

Recursos adicionales