Introduzione a Razor Pages in ASP.NET Core

Di Rick Anderson, Dave Brock e Kirk Larkin

Nota

Questa non è la versione più recente di questo articolo. Per la versione corrente, vedere la versione .NET 8 di questo articolo.

Importante

Queste informazioni si riferiscono a un prodotto non definitive che può essere modificato in modo sostanziale prima che venga rilasciato commercialmente. Microsoft non riconosce alcuna garanzia, espressa o implicita, in merito alle informazioni qui fornite.

Per la versione corrente, vedere la versione .NET 8 di questo articolo.

Razor Pages può rendere più semplice e produttiva la scrittura di codice per scenari incentrati sulle pagine rispetto all'uso di controller e viste.

Se si sta cercando un'esercitazione in cui si usa l'approccio Model-View-Controller, vedere Introduzione ad ASP.NET Core MVC.

Questo documento offre un'introduzione a Razor Pages. Non è un'esercitazione dettagliata. Se alcune sezioni risultano troppo avanzate, vedere Introduzione a Razor Pages. Per una panoramica di ASP.NET Core, vedere Introduzione a ASP.NET Core.

Prerequisiti

Creare un progetto Razor Pages

Vedere Introduzione a Razor Pages per istruzioni dettagliate su come creare un progetto Razor Pages.

Razor Pages

Razor Pages è abilitato in 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();

Nel codice precedente:

Si consideri una pagina di base:

@page

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

Il codice precedente è simile a un file di visualizzazione Razor usato in un'app ASP.NET Core con controller e visualizzazioni. Ciò che lo differenzia è la direttiva @page. @page trasforma il file in un'azione MVC, ovvero gestisce le richieste direttamente, senza passare attraverso un controller. @page deve essere la prima direttiva Razor in una pagina. @page influisce sul comportamento di altri costrutti Razor. I nomi dei file di Razor Pages hanno un suffisso .cshtml.

Nei due file seguenti viene visualizzata una pagina simile che usa una classe PageModel. Ecco il file Pages/Index2.cshtml:

@page
@using RazorPagesIntro.Pages
@model Index2Model

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

Il modello di pagina 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 }";
        }
    }
}

Per convenzione, il file di classe PageModel ha lo stesso nome del file della pagina Razor con l'aggiunta di .cs. Ad esempio, la pagina Razor precedente è Pages/Index2.cshtml. Il file contenente la classe PageModel è denominato Pages/Index2.cshtml.cs.

Le associazioni dei percorsi URL alle pagine sono determinate dalla posizione della pagina nel file system. La tabella seguente illustra un percorso di pagina Razor e gli URL corrispondenti:

Percorso e nome file URL corrispondente
/Pages/Index.cshtml / oppure /Index
/Pages/Contact.cshtml /Contact
/Pages/Store/Contact.cshtml /Store/Contact
/Pages/Store/Index.cshtml /Store oppure /Store/Index

Note:

  • Il runtime cerca i file di Razor Pages nella cartella Pages per impostazione predefinita.
  • Index è la pagina predefinita quando un URL non include una pagina.

Scrivere un form di base

Razor Pages semplifica l'implementazione dei modelli normalmente usati con i Web browser durante la creazione di un'app. L'associazione di modelli, gli helper tag e gli helper HTML funzionano tutti con le proprietà definite in una classe di Razor Pages. Si consideri una pagina che implementa un form "contact us" di base per il modello Contact:

Per gli esempi in questo documento, l'oggetto DbContext viene inizializzato nel file Program.cs .

Il database in memoria richiede il pacchetto 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();

Il modello di dati:

using System.ComponentModel.DataAnnotations;

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

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

Il contesto del database:

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>();
    }
}

Il file di visualizzazione 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>

Il modello di pagina 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");
    }
}

Per convenzione, la classe PageModel è denominata <PageName>Model e si trova nello stesso spazio dei nomi della pagina.

La classe PageModel consente la separazione della logica di una pagina dalla relativa presentazione. Definisce i gestori di pagina per le richieste inviate alla pagina e i dati usati per il rendering della pagina. Questa separazione consente:

La pagina contiene un oggetto OnPostAsync, ovvero un metodo gestore che viene eseguito per le richieste POST, quando un utente invia il form. È possibile aggiungere metodi gestore per qualsiasi verbo HTTP. I gestori più comuni sono i seguenti:

  • OnGet per inizializzare lo stato necessario per la pagina. Nel codice precedente il metodo OnGet visualizza la pagina RazorCreateModel.cshtml.
  • OnPost per gestire gli invii del modulo.

Il suffisso di denominazione Async è facoltativo ma viene spesso usato per convenzione per le funzioni asincrone. Il codice precedente è tipico di Razor Pages.

Se si ha familiarità con le app ASP.NET e con l'uso di controller e visualizzazioni:

  • Il codice OnPostAsync nell'esempio precedente è simile al tipico codice dei controller.
  • La maggior parte delle primitive MVC, ad esempio associazione di modelli, convalida e risultati delle azioni, funzionano allo stesso modo con controller e Razor Pages.

Il metodo OnPostAsync precedente:

[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");
}

Il flusso di base di OnPostAsync:

Verificare se sono presenti errori di convalida.

  • Se non sono presenti errori, salvare i dati e reindirizzare.
  • Se sono presenti errori, visualizzare di nuovo la pagina con i messaggi di convalida. In molti casi, gli errori di convalida vengono rilevati nel client e non vengono mai inviati al server.

Il file di visualizzazione 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>

Il codice HTML di cui è stato eseguito il rendering da 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>

Nel codice precedente il modulo viene pubblicato:

  • Con dati validi:

    • Il metodo gestore OnPostAsync chiama il metodo helper RedirectToPage. RedirectToPage restituisce un'istanza di RedirectToPageResult. RedirectToPage:

      • È un risultato dell'azione.
      • È simile a RedirectToAction o RedirectToRoute (usati in controller e visualizzazioni).
      • È personalizzato per le pagine. Nell'esempio precedente viene reindirizzato alla pagina di indice radice (/Index). Il metodo RedirectToPage è descritto in dettaglio nella sezione Generazione di URL per le pagine.
  • Con errori di convalida passati al server:

    • Il metodo gestore OnPostAsync chiama il metodo helper Page. Page restituisce un'istanza di PageResult. La restituzione di Page è simile al modo in cui le azioni nel controller restituiscono View. PageResult è il tipo restituito predefinito per un metodo gestore. Un metodo gestore che restituisce void esegue il rendering della pagina.
    • Nell'esempio precedente la pubblicazione del modulo senza alcun valore restituisce ModelState.IsValid che restituisce false. In questo esempio non vengono visualizzati errori di convalida nel client. La gestione degli errori di convalida viene illustrata più avanti in questo 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 errori di convalida rilevati dal lato client:

    • I dati non vengono pubblicati nel server.
    • La convalida lato client viene illustrata più avanti in questo documento.

La proprietà Customer usa l'attributo [BindProperty] per acconsentire esplicitamente all'associazione di modelli:

[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]non deve essere usato con modelli contenenti proprietà che non devono essere cambiate dal client. Per altre informazioni, vedere Overposting.

Razor Pages, per impostazione predefinita, associa solo proprietà con verbi diversi da GET. L'associazione alle proprietà elimina la necessità di scrivere codice per convertire i dati HTTP nel tipo di modello. Riduce il codice usando la stessa proprietà per il eseguire il rendering dei campi del form (<input asp-for="Customer.Name">) e accettare l'input.

Avviso

Per motivi di sicurezza, è necessario acconsentire esplicitamente all'associazione dei dati della richiesta GET alle proprietà del modello di pagina. Verificare l'input dell'utente prima di eseguirne il mapping alle proprietà. Acconsentire esplicitamente all'associazione GET è utile in scenari basati su valori route o stringa di query.

Per associare una proprietà nelle richieste GET, impostare la proprietà SupportsGet dell'attributo [BindProperty] su true:

[BindProperty(SupportsGet = true)]

Per altre informazioni, vedere ASP.NET Core Community Standup: Bind on GET discussion (YouTube).

Revisione del file di visualizzazione 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>
  • Nel codice precedente l'helper tag di input<input asp-for="Customer.Name" /> associa l'elemento HTML <input> all'espressione del modello Customer.Name.
  • @addTagHelper rende disponibili gli helper tag.

La home page

Index.cshtml è la home page:

@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>

La classe PageModel associata (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();
    }
}

Il file Index.cshtml contiene il markup seguente:

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

L'helper tag di ancoraggio <a /a> ha usato l'attributo asp-route-{value} per generare un collegamento alla pagina di modifica. Il collegamento contiene i dati della route con l'ID contatto. Ad esempio: https://localhost:5001/Edit/1. Gli helper tag consentono al codice lato server di partecipare alla creazione e al rendering di elementi HTML nei file Razor.

Il file Index.cshtml contiene il markup per creare un pulsante di eliminazione per ogni contatto cliente:

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

Il tag HTML sottoposto a rendering:

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

Quando viene eseguito il rendering del pulsante di eliminazione in HTML, il relativo oggetto formaction include i parametri per:

  • L'ID di contatto cliente, specificato dall'attributo asp-route-id.
  • L'oggetto handler, specificato dall'attributo asp-page-handler.

Quando il pulsante è selezionato, viene inviata una richiesta POST di modulo al server. Per convenzione, il nome del metodo del gestore viene selezionato in base al valore del parametro handler in base allo schema OnPost[handler]Async.

Poiché in questo esempio l'handler è delete, il metodo OnPostDeleteAsync viene usato per elaborare la richiesta POST. Se asp-page-handler viene impostato su un valore diverso, ad esempio remove, viene selezionato un metodo gestore con il nome 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();
}

Il metodo OnPostDeleteAsync:

  • Accetta l'oggetto id dalla stringa di query.
  • Interroga il database in merito al contatto del cliente con FindAsync.
  • Se il contatto cliente viene trovato, viene rimosso e il database viene aggiornato.
  • Chiama RedirectToPage per reindirizzare alla pagina di indice radice (/Index).

Il file 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 prima riga contiene la direttiva @page "{id:int}". Il vincolo di routing "{id:int}" indica alla pagina di accettare le richieste ricevute che contengono i dati della route int. Se una richiesta inviata alla pagina non contiene dati della route che possono essere convertiti in un oggetto int, il runtime restituisce un errore HTTP 404 (non trovato). Per rendere l'ID facoltativo, aggiungere ? al vincolo di route:

@page "{id:int?}"

Ecco il file 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);
    }
}

Convalida

Le regole di convalida:

  • Vengono specificate in modo dichiarativo nella classe del modello.
  • Vengono applicate ovunque nell'app.

Lo spazio dei nomi System.ComponentModel.DataAnnotations offre un set di attributi di convalida predefiniti che vengono applicati in modo dichiarativo a una classe o una proprietà. DataAnnotations contiene anche gli attributi di formattazione come [DataType] che guidano nella formattazione e non offrono alcuna convalida.

Si consideri il modello Customer:

using System.ComponentModel.DataAnnotations;

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

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

Usando il file di visualizzazione Create.cshtml seguente:

@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>

Il codice precedente:

  • Include gli script di convalida jQuery e jQuery.

  • Usa glihelper tag<div /> e <span /> per abilitare:

    • La convalida lato client.
    • Il rendering degli errori di convalida.
  • Viene generato il codice HTML seguente:

    <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>
    

Se si pubblica il modulo di creazione senza un valore per il nome, viene visualizzato il messaggio di errore "Il campo Nome è obbligatorio". Se nel client è abilitato JavaScript, il browser visualizza l'errore senza pubblicare nel server.

L'attributo [StringLength(10)] genera data-val-length-max="10" nel codice HTML sottoposto a rendering. data-val-length-max impedisce ai browser di immettere stringhe con una lunghezza maggiore di quella massima specificata. Se viene usato uno strumento come Fiddler per modificare e riprodurre il post:

  • Con il nome composto da più di 10 caratteri.
  • Viene restituito il messaggio di errore "Il campo Nome deve essere una stringa con una lunghezza massima di 10".

Si consideri il modello Movie seguente:

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; }
    }
}

Gli attributi di convalida specificano il comportamento da imporre nelle proprietà del modello a cui vengono applicati:

  • Gli attributi Required e MinimumLength indicano che una proprietà deve avere un valore, ma nulla impedisce all'utente di inserire spazi vuoti per soddisfare questa convalida.

  • L'attributo RegularExpression viene usato per limitare i caratteri che possono essere inseriti. Nel codice precedente "Genre":

    • Deve includere solo lettere.
    • La prima lettera deve essere maiuscola. Gli spazi, i numeri e i caratteri speciali non sono consentiti.
  • RegularExpression "Rating":

    • Richiede che il primo carattere sia una lettera maiuscola.
    • Consente i caratteri speciali e i numeri negli spazi successivi. "PG-13" è valido per una classificazione, ma non per "Genre".
  • L'attributo Range vincola un valore all'interno di un intervallo specificato.

  • L'attributo StringLength imposta la lunghezza massima di una proprietà stringa e, facoltativamente, la lunghezza minima.

  • I tipi di valore, ad esempio decimal, int, float e DateTime, sono intrinsecamente necessari e non richiedono l'attributo [Required].

La pagina di creazione per il modello Movie mostra gli errori con i valori non validi:

Il modulo di vista del film con più errori di convalida del lato client jQuery

Per altre informazioni, vedi:

Isolamento CSS

Isolare gli stili CSS in singole pagine, visualizzazioni e componenti per ridurre o evitare:

  • Dipendenze da stili globali che possono risultare difficili da gestire.
  • Conflitti di stile nel contenuto annidato.

Per aggiungere un file CSS con ambito per una pagina o una visualizzazione, inserire gli stili CSS in un file .cshtml.css complementare corrispondente al nome del file .cshtml. Nell'esempio seguente un file Index.cshtml.css fornisce stili CSS applicati solo alla pagina o alla visualizzazione Index.cshtml.

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

h1 {
    color: red;
}

L'isolamento CSS si verifica in fase di compilazione. Il framework riscrive i selettori CSS in modo che corrispondano al markup sottoposto a rendering dalle pagine o dalle visualizzazioni dell'app. Gli stili CSS riscritti vengono raggruppati in bundle e prodotti come asset statici, {APP ASSEMBLY}.styles.css. Il segnaposto {APP ASSEMBLY} è il nome dell'assembly del progetto. Nel layout dell'app viene inserito un collegamento agli stili CSS in bundle.

Nel contenuto <head> del file Pages/Shared/_Layout.cshtml (RazorPages) o Views/Shared/_Layout.cshtml (MVC) dell'app aggiungere o confermare la presenza del collegamento agli stili CSS in bundle:

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

Nell'esempio seguente il nome dell'assembly dell'app è WebApp:

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

Gli stili definiti in un file CSS con ambito vengono applicati solo all'output sottoposto a rendering del file corrispondente. Nell'esempio precedente tutte le dichiarazioni CSS h1 definite altrove nell'app non sono in conflitto con lo stile dell'intestazione di Index. Le regole di cascata o ereditarietà degli stili CSS rimangono effettive per i file CSS con ambito. Ad esempio, gli stili applicati direttamente a un elemento <h1> nel file Index.cshtml sostituiscono gli stili del file CSS con ambito in Index.cshtml.css.

Nota

Per garantire l'isolamento degli stili CSS quando si verifica il raggruppamento in bundle, l'importazione di CSS nei blocchi di codiceRazor non è supportata.

L'isolamento CSS si applica solo agli elementi HTML. L'isolamento CSS non è supportato per gli helper tag.

All'interno del file CSS in bundle, ogni pagina, visualizzazione o componente Razor è associato a un identificatore di ambito nel formato b-{STRING}, dove il segnaposto {STRING} è una stringa di dieci caratteri generata dal framework. L'esempio seguente mostra lo stile dell'elemento <h1> precedente nella pagina Index di un'app Razor Pages:

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

Nella pagina Index in cui viene applicato lo stile CSS dal file in bundle l'identificatore di ambito viene aggiunto come attributo HTML:

<h1 b-3xxtam6d07>

L'identificatore è univoco per un'app. In fase di compilazione viene creato un bundle di progetto con la convenzione {STATIC WEB ASSETS BASE PATH}/Project.lib.scp.css, dove il segnaposto {STATIC WEB ASSETS BASE PATH} è il percorso di base degli asset Web statici.

Se vengono utilizzati altri progetti, ad esempio pacchetti NuGet o librerie di classi Razor, il file in bundle:

  • Fa riferimento agli stili usando le importazioni CSS.
  • Non viene pubblicato come asset Web statico dell'app che utilizza gli stili.

Supporto dei preprocessori CSS

I preprocessori CSS sono utili per migliorare lo sviluppo di CSS tramite l'utilizzo di funzionalità come variabili, annidamenti, moduli, mixin ed ereditarietà. Anche se l'isolamento CSS non supporta in modo nativo i preprocessori CSS, ad esempio Sass o Less, l'integrazione dei preprocessori CSS è facile purché la relativa compilazione si verifichi prima che il framework riscriva i selettori CSS durante il processo di compilazione. Usando Visual Studio, ad esempio, configurare la compilazione del preprocessore esistente come attività Prima di compilare in Esplora esecuzione attività.

Molti pacchetti NuGet di terze parti, ad esempio AspNetCore.SassCompiler, possono compilare file SASS/SCSS all'inizio del processo di compilazione prima che si verifichi l'isolamento CSS, quindi non è necessaria alcuna configurazione aggiuntiva.

Configurazione dell'isolamento CSS

L'isolamento CSS consente di configurare alcuni scenari avanzati, ad esempio quando sono presenti dipendenze da strumenti o flussi di lavoro esistenti.

Personalizzare il formato dell'identificatore di ambito

In questa sezione il segnaposto {Pages|Views} corrisponde a Pages per le app Razor Pages o a Views per le app MVC.

Per impostazione predefinita, gli identificatori di ambito usano il formato b-{STRING}, dove il segnaposto {STRING} è una stringa di dieci caratteri generata dal framework. Per personalizzare il formato dell'identificatore di ambito, aggiornare il file di progetto a un modello desiderato:

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

Nell'esempio precedente il file CSS generato per Index.cshtml.css cambia il relativo identificatore di ambito da b-{STRING} a custom-scope-identifier.

Usare gli identificatori di ambito per ottenere l'ereditarietà con i file CSS con ambito. Nell'esempio di file di progetto seguente un file BaseView.cshtml.css contiene stili comuni tra visualizzazioni. Un file DerivedView.cshtml.css eredita questi stili.

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

Usare l'operatore carattere jolly (*) per condividere gli identificatori di ambito tra più file:

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

Cambiare il percorso di base per gli asset Web statici

Il file CSS con ambito viene generato alla radice dell'app. Nel file di progetto usare la proprietà StaticWebAssetBasePath per cambiare il percorso predefinito. L'esempio seguente inserisce il file CSS con ambito e il resto degli asset dell'app nel percorso _content:

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

Disabilitare il raggruppamento automatico in bundle

Per rifiutare esplicitamente il modo in cui il framework pubblica e carica i file con ambito in fase di esecuzione, usare la proprietà DisableScopedCssBundling. Quando si usa questa proprietà, altri strumenti o processi sono responsabili dell'acquisizione dei file CSS isolati dalla directory obj, oltre che delle relative operazioni di pubblicazione e caricamento in fase di esecuzione:

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

Supporto della libreria di classi Razor (RCL)

Quando una libreria di classi Razor (RCL) fornisce stili isolati, l'attributo href del tag <link> punta a {STATIC WEB ASSET BASE PATH}/{PACKAGE ID}.bundle.scp.css, dove i segnaposto sono:

  • {STATIC WEB ASSET BASE PATH}: il percorso di base degli asset Web statici.
  • {PACKAGE ID}: l'identificatore del pacchetto della libreria. L'identificatore del pacchetto viene impostato per impostazione predefinita sul nome dell'assembly del progetto se non viene specificato nel file di progetto.

Nell'esempio seguente :

  • Il percorso di base degli asset Web statici è _content/ClassLib.
  • Il nome dell'assembly della libreria di classi è ClassLib.

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

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

Per altre informazioni sulle librerie di classi Razor (RCL), vedere gli articoli seguenti:

Per informazioni sull'isolamento CSS Blazor, vedere Isolamento CSS di Blazor in ASP.NET Core.

Gestire le richieste HEAD con un fallback del gestore OnGet

Le richieste HEAD consentono di recuperare le intestazioni per una risorsa specifica. A differenza delle richieste GET, le richieste HEAD non restituiscono un corpo della risposta.

In genere, per le richieste HEAD viene creato e chiamato un gestore OnHead:

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

Razor Pages esegue un fallback per chiamare il gestore OnGet se non vengono definiti gestori OnHead.

XSRF/CSRF e Razor Pages

Razor Pages è protetto dalla convalida antifalsificazione. FormTagHelper inserisce token antifalsificazione negli elementi del modulo HTML.

Uso di layout, righe parzialmente eseguite, modelli e helper tag con Razor Pages

Le pagine sono compatibili con tutte le funzionalità del motore di visualizzazione Razor. I layout, le righe parzialmente eseguite, i modelli, gli helper tag, _ViewStart.cshtml e _ViewImports.cshtml funzionano esattamente come nelle normali visualizzazioni Razor.

La pagina verrà ora riorganizzata in modo da usufruire di alcune di queste funzionalità.

Aggiungere una pagina layout 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>

Il Layout:

  • Controlla il layout di ogni pagina, a meno che la pagina non accetti il layout.
  • Importa le strutture HTML, ad esempio JavaScript e i fogli di stile.
  • Il contenuto della pagina Razor viene sottoposto a rendering dove viene chiamato @RenderBody().

Per altre informazioni, vedere pagina layout.

La proprietà Layout è impostata in Pages/_ViewStart.cshtml:

@{
    Layout = "_Layout";
}

Il layout è nella cartella Pages/Shared. Le pagine cercano altre visualizzazioni (layout, modelli, righe parzialmente eseguite) in modo gerarchico, partendo dalla stessa cartella della pagina corrente. Un layout nella cartella Pages/Shared può essere usato da qualsiasi pagina Razor della cartella Pages.

Il file di layout dovrebbe andare nella cartella Pages/Shared.

Si consiglia di non inserire il file di layout nella cartella Views/Shared. Views/Shared è un modello destinato alle visualizzazioni MVC. Razor Pages deve basarsi sulla gerarchia di cartelle, non su convenzioni di percorso.

La ricerca delle visualizzazioni da una pagina Razor include la cartella Pages. Il layout, i modelli e le righe parzialmente eseguite in uso con i controller MVC e le visualizzazioni Razor tradizionali funzionano normalmente.

Aggiungere un file Pages/_ViewImports.cshtml:

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

@namespace viene spiegato in seguito nell'esercitazione. La direttiva @addTagHelper inserisce gli helper tag predefiniti in tutte le pagine presenti nella cartella Pages.

La direttiva @namespace impostata in una pagina:

@page
@namespace RazorPagesIntro.Pages.Customers

@model NameSpaceModel

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

La direttiva @namespace imposta lo spazio dei nomi per la pagina. La direttiva @model non deve necessariamente includere lo spazio dei nomi.

Se la direttiva @namespace è contenuta in _ViewImports.cshtml, lo spazio dei nomi specificato indica il prefisso per lo spazio dei nomi generato nella pagina che importa la direttiva @namespace. Il resto dello spazio dei nomi generato (la parte del suffisso) è il percorso relativo separato dal punto tra la cartella contenente _ViewImports.cshtml e la cartella contenente la pagina.

Ad esempio, la classe PageModelPages/Customers/Edit.cshtml.cs imposta esplicitamente lo spazio dei nomi:

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

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

        // Code removed for brevity.

Il file Pages/_ViewImports.cshtml imposta lo spazio dei nomi seguente:

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

Lo spazio dei nomi generato per la pagina RazorPages/Customers/Edit.cshtml corrisponde alla classe PageModel.

@namespaceè compatibile anche con le visualizzazioni Razor tradizionali.

Si consideri il file di visualizzazione 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>

Il file di visualizzazione Pages/Customers/Create.cshtml aggiornato con _ViewImports.cshtml e il file di layout precedente:

@page
@model CreateModel

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

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

Nel codice precedente _ViewImports.cshtml ha importato lo spazio dei nomi e gli helper tag. Il file di layout ha importato i file JavaScript.

Il progetto di avvio di Razor Pages contiene Pages/_ValidationScriptsPartial.cshtml, che associa la convalida lato client.

Per altre informazioni sulle visualizzazioni parziali, vedere Visualizzazioni parziali in ASP.NET Core.

Generazione di URL per le pagine

La pagina Create, riportata in precedenza, usa RedirectToPage:

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");
    }
}

L'applicazione ha la struttura di file o cartella seguente:

  • Pages/

    • Index.cshtml

    • Privacy.cshtml

    • /Customers

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

Le pagine Pages/Customers/Create.cshtml e Pages/Customers/Edit.cshtml reindirizzano a Pages/Customers/Index.cshtml dopo il completamento dell'operazione. La stringa ./Index è un nome di pagina relativo usato per accedere alla pagina precedente. Viene usato per generare URL della pagina Pages/Customers/Index.cshtml. Ad esempio:

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

Il nome di pagina assoluto /Index viene usato per generare gli URL della pagina Pages/Index.cshtml. Ad esempio:

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

Il nome della pagina è il percorso alla pagina dalla cartella radice /Pages, inclusa una barra iniziale /, ad esempio /Index. Gli esempi di generazione di URL precedenti offrono opzioni e caratteristiche funzionali avanzate rispetto agli URL hardcoded. La generazione di URL usa il routing ed è in grado di generare e codificare i parametri in base al modo in cui la route è definita nel percorso di destinazione.

La generazione di URL per le pagine supporta i nomi relativi. La tabella seguente indica quale pagina di indice viene selezionata con diversi parametri RedirectToPage in Pages/Customers/Create.cshtml.

RedirectToPage(x) Pagina
RedirectToPage("/Index") Pages/Index
RedirectToPage("./Index"); Pages/Customers/Index
RedirectToPage("../Index") Pages/Index
RedirectToPage("Index") Pages/Customers/Index

RedirectToPage("Index"), RedirectToPage("./Index") e RedirectToPage("../Index") sono nomi relativi. Il parametro RedirectToPage è combinato con il percorso della pagina corrente per calcolare il nome della pagina di destinazione.

Il collegamento dei nomi relativi è utile quando si compilano siti con una struttura complessa. Quando vengono usati nomi relativi per il collegamento tra pagine in una cartella:

  • La ridenominazione di una cartella non interrompe i collegamenti relativi.
  • I collegamenti non vengono interrotti perché non includono il nome della cartella.

Per reindirizzare la pagina a un'Area diversa, specificare l'area:

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

Per altre informazioni, vedere Aree in ASP.NET Core e Convenzioni di route e app di Razor Pages in ASP.NET Core.

Attributo ViewData

I dati possono essere passati a una pagina con ViewDataAttribute. I valori delle proprietà con l'attributo [ViewData] vengono archiviati e caricati da ViewDataDictionary.

Nell'esempio seguente AboutModel applica l'attributo [ViewData] alla proprietà Title:

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

    public void OnGet()
    {
    }
}

Nella pagina About (Informazioni) accedere alla proprietà Title come proprietà del modello:

<h1>@Model.Title</h1>

Nel layout il titolo viene letto dal dizionario ViewData:

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

TempData

ASP.NET Core espone TempData. Questa proprietà archivia i dati finché non viene letta. I metodi Keep e Peek possono essere usati per esaminare i dati senza eliminazione. TempData è utile per il reindirizzamento quando i dati sono necessari per più di una singola richiesta.

Il codice seguente imposta il valore di Message usando 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");
    }
}

Il markup seguente nel file Pages/Customers/Index.cshtml visualizza il valore di Message usando TempData.

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

Il modello di pagina Pages/Customers/Index.cshtml.cs applica l'attributo [TempData] alla proprietà Message.

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

Per altre informazioni, vedere TempData.

Più gestori per pagina

La pagina seguente genera markup per due gestori usando l'helper tag asp-page-handler:

@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>

Il form nell'esempio precedente contiene due pulsanti di invio, ognuno dei quali usa FormActionTagHelper per l'invio a un URL diverso. L'attributo asp-page-handler è correlato a asp-page. asp-page-handler genera gli URL che indirizzano a ognuno dei metodi gestore definiti da una pagina. asp-page non è specificato poiché l'esempio è collegato alla pagina corrente.

Il modello di pagina :

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();
        }
    }
}

Il codice precedente usa metodi gestore denominati. I metodi gestore denominati vengono creati usando il testo che nel nome segue On<HTTP Verb> e precede Async (se presente). Nell'esempio precedente i metodi della pagina sono OnPostJoinListAsync e OnPostJoinListUCAsync. Rimuovendo OnPost e Async, i nomi dei gestori sono JoinList e JoinListUC.

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

Usando il codice precedente, il percorso URL che indirizza a OnPostJoinListAsync è https://localhost:5001/Customers/CreateFATH?handler=JoinList. Il percorso URL che indirizza a OnPostJoinListUCAsync è https://localhost:5001/Customers/CreateFATH?handler=JoinListUC.

Route personalizzate

Usare la direttiva @page per:

  • Specificare una route personalizzata a una pagina. Ad esempio, è possibile impostare la route alla pagina About (Informazioni) su /Some/Other/Path con @page "/Some/Other/Path".
  • Aggiungere segmenti alla route predefinita di una pagina. Ad esempio, è possibile aggiungere un segmento "item" alla route predefinita di una pagina con @page "item".
  • Aggiungere parametri alla route predefinita di una pagina. Ad esempio, un parametro ID, id, può essere necessario per una pagina con @page "{id}".

Un percorso relativo alla directory radice designato da una tilde (~) all'inizio del percorso è supportato. Ad esempio, @page "~/Some/Other/Path" è identico a @page "/Some/Other/Path".

Se si vuole omettere la stringa di query ?handler=JoinList dall'URL, cambiare la route in modo da inserire il nome del gestore nella parte di percorso dell'URL. È possibile personalizzare la route aggiungendo un modello di route racchiuso tra virgolette doppie dopo la direttiva @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>

Usando il codice precedente, il percorso URL che indirizza a OnPostJoinListAsync è https://localhost:5001/Customers/CreateFATH/JoinList. Il percorso URL che indirizza a OnPostJoinListUCAsync è https://localhost:5001/Customers/CreateFATH/JoinListUC.

? che segue handler indica che il parametro di route è facoltativo.

Collocazione dei file JavaScript (JS)

La collocazione di file JavaScript (JS) per pagine e visualizzazioni è un modo pratico per organizzare gli script in un'app.

Condividere i percorsi dei file JS usando le convenzioni per le estensioni di nome file:

  • Pagine delle app Razor Pages e viste delle app MVC: .cshtml.js. Esempi:
    • Pages/Index.cshtml.js per la pagina Index di un'app Razor in Pages/Index.cshtml.
    • Views/Home/Index.cshtml.js per la vista Index di un'app MVC in Views/Home/Index.cshtml.

I file JS con il percorso condiviso sono raggiungibili pubblicamente usando il percorso del file nel progetto:

  • Pagine e visualizzazioni da un file di script collocato nell'app:

    {PATH}/{PAGE, VIEW, OR COMPONENT}.{EXTENSION}.js

    • Il segnaposto {PATH} è il percorso della pagina, della vista o del componente.
    • Il segnaposto {PAGE, VIEW, OR COMPONENT} è la pagina, la vista o il componente.
    • Il segnaposto {EXTENSION} corrisponde all'estensione della pagina, della vista o del componente, razor o cshtml.

    Esempio per Razor Pages:

    Un file JS per la pagina Index viene inserito nella cartella Pages (Pages/Index.cshtml.js) accanto alla pagina Index (Pages/Index.cshtml). Nella pagina Index viene fatto riferimento allo script nel percorso della cartella Pages:

    @section Scripts {
      <script src="~/Pages/Index.cshtml.js"></script>
    }
    

    Quando l'app viene pubblicata, il framework sposta automaticamente lo script nella radice Web. Nell'esempio precedente, lo script viene spostato in bin\Release\{TARGET FRAMEWORK MONIKER}\publish\wwwroot\Pages\Index.cshtml.js, dove il segnaposto {TARGET FRAMEWORK MONIKER} è il moniker framework di destinazione (TFM). Non è necessaria alcuna modifica all'URL relativo dello script nella pagina Index.

    Quando l'app viene pubblicata, il framework sposta automaticamente lo script nella radice Web. Nell'esempio precedente, lo script viene spostato in bin\Release\{TARGET FRAMEWORK MONIKER}\publish\wwwroot\Components\Pages\Index.razor.js, dove il segnaposto {TARGET FRAMEWORK MONIKER} è il moniker framework di destinazione (TFM). Non è necessaria alcuna modifica all'URL relativo dello script nel componente Index.

  • Per gli script forniti da una libreria di classi Razor (RCL):

    _content/{PACKAGE ID}/{PATH}/{PAGE, VIEW, OR COMPONENT}.{EXTENSION}.js

    • Il segnaposto {PACKAGE ID} è l'identificatore del pacchetto della libreria RCL (o il nome della libreria per una libreria di classi a cui fa riferimento l'app).
    • Il segnaposto {PATH} è il percorso della pagina, della vista o del componente. Se un componente Razor si trova nella radice della libreria RCL, il segmento di percorso non è incluso.
    • Il segnaposto {PAGE, VIEW, OR COMPONENT} è la pagina, la vista o il componente.
    • Il segnaposto {EXTENSION} corrisponde all'estensione della pagina, della vista o del componente, razor o cshtml.

Configurazione e impostazioni avanzate

La configurazione e le impostazioni descritte nelle sezioni seguenti non sono richieste dalla maggior parte delle app.

Per configurare le opzioni avanzate, usare l'overload AddRazorPages che 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();

Usare RazorPagesOptions per impostare la directory radice per le pagine o aggiungere convenzioni di modello applicativo per le pagine. Per altre informazioni sulle convenzioni, vedere Convenzioni di autorizzazione di Razor Pages.

Per la precompilazione delle visualizzazioni, vedere l'articolo sulla compilazione delle visualizzazioni di Razor.

Specificare che Razor Pages si trova nella radice del contenuto

Per impostazione predefinita, la directory radice di Razor Pages è /Pages. Aggiungere WithRazorPagesAtContentRoot per specificare che Razor Pages si trova nella radice del contenuto (ContentRootPath) dell'app:

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();

Specificare che Razor Pages si trova in una directory radice personalizzata

Aggiungere WithRazorPagesRoot per specificare che Razor Pages si trova in una directory radice personalizzata nell'app (fornire un percorso relativo):

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();

Risorse aggiuntive

Creare un progetto Razor Pages

Vedere Introduzione a Razor Pages per istruzioni dettagliate su come creare un progetto Razor Pages.

Razor Pages

Razor Pages è abilitato in 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();
        });
    }
}

Si consideri una pagina di base:

@page

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

Il codice precedente è simile a un file di visualizzazione Razor usato in un'app ASP.NET Core con controller e visualizzazioni. Ciò che lo differenzia è la direttiva @page. @page trasforma il file in un'azione MVC, ovvero gestisce le richieste direttamente, senza passare attraverso un controller. @page deve essere la prima direttiva Razor in una pagina. @page influisce sul comportamento di altri costrutti Razor. I nomi dei file di Razor Pages hanno un suffisso .cshtml.

Nei due file seguenti viene visualizzata una pagina simile che usa una classe PageModel. Ecco il file Pages/Index2.cshtml:

@page
@using RazorPagesIntro.Pages
@model Index2Model

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

Il modello di pagina 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 }";
        }
    }
}

Per convenzione, il file di classe PageModel ha lo stesso nome del file della pagina Razor con l'aggiunta di .cs. Ad esempio, la pagina Razor precedente è Pages/Index2.cshtml. Il file contenente la classe PageModel è denominato Pages/Index2.cshtml.cs.

Le associazioni dei percorsi URL alle pagine sono determinate dalla posizione della pagina nel file system. La tabella seguente illustra un percorso di pagina Razor e gli URL corrispondenti:

Percorso e nome file URL corrispondente
/Pages/Index.cshtml / oppure /Index
/Pages/Contact.cshtml /Contact
/Pages/Store/Contact.cshtml /Store/Contact
/Pages/Store/Index.cshtml /Store oppure /Store/Index

Note:

  • Il runtime cerca i file di Razor Pages nella cartella Pages per impostazione predefinita.
  • Index è la pagina predefinita quando un URL non include una pagina.

Scrivere un form di base

Razor Pages semplifica l'implementazione dei modelli normalmente usati con i Web browser durante la creazione di un'app. L'associazione di modelli, gli helper tag e gli helper HTML funzionano tutti con le proprietà definite in una classe di Razor Pages. Si consideri una pagina che implementa un form "contact us" di base per il modello Contact:

Per gli esempi riportati in questo documento, DbContext viene inizializzato nel file Startup.cs.

Il database in memoria richiede il pacchetto NuGet Microsoft.EntityFrameworkCore.InMemory.

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

Il modello di dati:

using System.ComponentModel.DataAnnotations;

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

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

Il contesto del database:

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; }
    }
}

Il file di visualizzazione 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>

Il modello di pagina 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");
        }
    }
}

Per convenzione, la classe PageModel è denominata <PageName>Model e si trova nello stesso spazio dei nomi della pagina.

La classe PageModel consente la separazione della logica di una pagina dalla relativa presentazione. Definisce i gestori di pagina per le richieste inviate alla pagina e i dati usati per il rendering della pagina. Questa separazione consente:

La pagina contiene un oggetto OnPostAsync, ovvero un metodo gestore che viene eseguito per le richieste POST, quando un utente invia il form. È possibile aggiungere metodi gestore per qualsiasi verbo HTTP. I gestori più comuni sono i seguenti:

  • OnGet per inizializzare lo stato necessario per la pagina. Nel codice precedente il metodo OnGet visualizza la pagina RazorCreateModel.cshtml.
  • OnPost per gestire gli invii del modulo.

Il suffisso di denominazione Async è facoltativo ma viene spesso usato per convenzione per le funzioni asincrone. Il codice precedente è tipico di Razor Pages.

Se si ha familiarità con le app ASP.NET e con l'uso di controller e visualizzazioni:

  • Il codice OnPostAsync nell'esempio precedente è simile al tipico codice dei controller.
  • La maggior parte delle primitive MVC, ad esempio associazione di modelli, convalida e risultati delle azioni, funzionano allo stesso modo con controller e Razor Pages.

Il metodo OnPostAsync precedente:

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

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

    return RedirectToPage("./Index");
}

Il flusso di base di OnPostAsync:

Verificare se sono presenti errori di convalida.

  • Se non sono presenti errori, salvare i dati e reindirizzare.
  • Se sono presenti errori, visualizzare di nuovo la pagina con i messaggi di convalida. In molti casi, gli errori di convalida vengono rilevati nel client e non vengono mai inviati al server.

Il file di visualizzazione 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>

Il codice HTML di cui è stato eseguito il rendering da 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>

Nel codice precedente il modulo viene pubblicato:

  • Con dati validi:

    • Il metodo gestore OnPostAsync chiama il metodo helper RedirectToPage. RedirectToPage restituisce un'istanza di RedirectToPageResult. RedirectToPage:

      • È un risultato dell'azione.
      • È simile a RedirectToAction o RedirectToRoute (usati in controller e visualizzazioni).
      • È personalizzato per le pagine. Nell'esempio precedente viene reindirizzato alla pagina di indice radice (/Index). Il metodo RedirectToPage è descritto in dettaglio nella sezione Generazione di URL per le pagine.
  • Con errori di convalida passati al server:

    • Il metodo gestore OnPostAsync chiama il metodo helper Page. Page restituisce un'istanza di PageResult. La restituzione di Page è simile al modo in cui le azioni nel controller restituiscono View. PageResult è il tipo restituito predefinito per un metodo gestore. Un metodo gestore che restituisce void esegue il rendering della pagina.
    • Nell'esempio precedente la pubblicazione del modulo senza alcun valore restituisce ModelState.IsValid che restituisce false. In questo esempio non vengono visualizzati errori di convalida nel client. La gestione degli errori di convalida viene illustrata più avanti in questo documento.
    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }
    
        _context.Customers.Add(Customer);
        await _context.SaveChangesAsync();
    
        return RedirectToPage("./Index");
    }
    
  • Con errori di convalida rilevati dal lato client:

    • I dati non vengono pubblicati nel server.
    • La convalida lato client viene illustrata più avanti in questo documento.

La proprietà Customer usa l'attributo [BindProperty] per acconsentire esplicitamente all'associazione di modelli:

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]non deve essere usato con modelli contenenti proprietà che non devono essere cambiate dal client. Per altre informazioni, vedere Overposting.

Razor Pages, per impostazione predefinita, associa solo proprietà con verbi diversi da GET. L'associazione alle proprietà elimina la necessità di scrivere codice per convertire i dati HTTP nel tipo di modello. Riduce il codice usando la stessa proprietà per il eseguire il rendering dei campi del form (<input asp-for="Customer.Name">) e accettare l'input.

Avviso

Per motivi di sicurezza, è necessario acconsentire esplicitamente all'associazione dei dati della richiesta GET alle proprietà del modello di pagina. Verificare l'input dell'utente prima di eseguirne il mapping alle proprietà. Acconsentire esplicitamente all'associazione GET è utile in scenari basati su valori route o stringa di query.

Per associare una proprietà nelle richieste GET, impostare la proprietà SupportsGet dell'attributo [BindProperty] su true:

[BindProperty(SupportsGet = true)]

Per altre informazioni, vedere ASP.NET Core Community Standup: Bind on GET discussion (YouTube).

Revisione del file di visualizzazione 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>
  • Nel codice precedente l'helper tag di input<input asp-for="Customer.Name" /> associa l'elemento HTML <input> all'espressione del modello Customer.Name.
  • @addTagHelper rende disponibili gli helper tag.

La home page

Index.cshtml è la home page:

@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>

La classe PageModel associata (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();
    }
}

Il file Index.cshtml contiene il markup seguente:

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

L'helper tag di ancoraggio <a /a> ha usato l'attributo asp-route-{value} per generare un collegamento alla pagina di modifica. Il collegamento contiene i dati della route con l'ID contatto. Ad esempio: https://localhost:5001/Edit/1. Gli helper tag consentono al codice lato server di partecipare alla creazione e al rendering di elementi HTML nei file Razor.

Il file Index.cshtml contiene il markup per creare un pulsante di eliminazione per ogni contatto cliente:

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

Il tag HTML sottoposto a rendering:

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

Quando viene eseguito il rendering del pulsante di eliminazione in HTML, il relativo oggetto formaction include i parametri per:

  • L'ID di contatto cliente, specificato dall'attributo asp-route-id.
  • L'oggetto handler, specificato dall'attributo asp-page-handler.

Quando il pulsante è selezionato, viene inviata una richiesta POST di modulo al server. Per convenzione, il nome del metodo del gestore viene selezionato in base al valore del parametro handler in base allo schema OnPost[handler]Async.

Poiché in questo esempio l'handler è delete, il metodo OnPostDeleteAsync viene usato per elaborare la richiesta POST. Se asp-page-handler viene impostato su un valore diverso, ad esempio remove, viene selezionato un metodo gestore con il nome 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();
}

Il metodo OnPostDeleteAsync:

  • Accetta l'oggetto id dalla stringa di query.
  • Interroga il database in merito al contatto del cliente con FindAsync.
  • Se il contatto cliente viene trovato, viene rimosso e il database viene aggiornato.
  • Chiama RedirectToPage per reindirizzare alla pagina di indice radice (/Index).

Il file 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 prima riga contiene la direttiva @page "{id:int}". Il vincolo di routing "{id:int}" indica alla pagina di accettare le richieste ricevute che contengono i dati della route int. Se una richiesta inviata alla pagina non contiene dati della route che possono essere convertiti in un oggetto int, il runtime restituisce un errore HTTP 404 (non trovato). Per rendere l'ID facoltativo, aggiungere ? al vincolo di route:

@page "{id:int?}"

Ecco il file 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");
    }

}

Convalida

Le regole di convalida:

  • Vengono specificate in modo dichiarativo nella classe del modello.
  • Vengono applicate ovunque nell'app.

Lo spazio dei nomi System.ComponentModel.DataAnnotations offre un set di attributi di convalida predefiniti che vengono applicati in modo dichiarativo a una classe o una proprietà. DataAnnotations contiene anche gli attributi di formattazione come [DataType] che guidano nella formattazione e non offrono alcuna convalida.

Si consideri il modello Customer:

using System.ComponentModel.DataAnnotations;

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

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

Usando il file di visualizzazione Create.cshtml seguente:

@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>

Il codice precedente:

  • Include gli script di convalida jQuery e jQuery.

  • Usa glihelper tag<div /> e <span /> per abilitare:

    • La convalida lato client.
    • Il rendering degli errori di convalida.
  • Viene generato il codice HTML seguente:

    <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>
    

Se si pubblica il modulo di creazione senza un valore per il nome, viene visualizzato il messaggio di errore "Il campo Nome è obbligatorio". Se nel client è abilitato JavaScript, il browser visualizza l'errore senza pubblicare nel server.

L'attributo [StringLength(10)] genera data-val-length-max="10" nel codice HTML sottoposto a rendering. data-val-length-max impedisce ai browser di immettere stringhe con una lunghezza maggiore di quella massima specificata. Se viene usato uno strumento come Fiddler per modificare e riprodurre il post:

  • Con il nome composto da più di 10 caratteri.
  • Viene restituito il messaggio di errore "Il campo Nome deve essere una stringa con una lunghezza massima di 10".

Si consideri il modello Movie seguente:

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; }
    }
}

Gli attributi di convalida specificano il comportamento da imporre nelle proprietà del modello a cui vengono applicati:

  • Gli attributi Required e MinimumLength indicano che una proprietà deve avere un valore, ma nulla impedisce all'utente di inserire spazi vuoti per soddisfare questa convalida.

  • L'attributo RegularExpression viene usato per limitare i caratteri che possono essere inseriti. Nel codice precedente "Genre":

    • Deve includere solo lettere.
    • La prima lettera deve essere maiuscola. Gli spazi, i numeri e i caratteri speciali non sono consentiti.
  • RegularExpression "Rating":

    • Richiede che il primo carattere sia una lettera maiuscola.
    • Consente i caratteri speciali e i numeri negli spazi successivi. "PG-13" è valido per una classificazione, ma non per "Genre".
  • L'attributo Range vincola un valore all'interno di un intervallo specificato.

  • L'attributo StringLength imposta la lunghezza massima di una proprietà stringa e, facoltativamente, la lunghezza minima.

  • I tipi di valore, ad esempio decimal, int, float e DateTime, sono intrinsecamente necessari e non richiedono l'attributo [Required].

La pagina di creazione per il modello Movie mostra gli errori con i valori non validi:

Il modulo di vista del film con più errori di convalida del lato client jQuery

Per altre informazioni, vedi:

Gestire le richieste HEAD con un fallback del gestore OnGet

Le richieste HEAD consentono di recuperare le intestazioni per una risorsa specifica. A differenza delle richieste GET, le richieste HEAD non restituiscono un corpo della risposta.

In genere, per le richieste HEAD viene creato e chiamato un gestore OnHead:

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

Razor Pages esegue un fallback per chiamare il gestore OnGet se non vengono definiti gestori OnHead.

XSRF/CSRF e Razor Pages

Razor Pages è protetto dalla convalida antifalsificazione. FormTagHelper inserisce token antifalsificazione negli elementi del modulo HTML.

Uso di layout, righe parzialmente eseguite, modelli e helper tag con Razor Pages

Le pagine sono compatibili con tutte le funzionalità del motore di visualizzazione Razor. I layout, le righe parzialmente eseguite, i modelli, gli helper tag, _ViewStart.cshtml e _ViewImports.cshtml funzionano esattamente come nelle normali visualizzazioni Razor.

La pagina verrà ora riorganizzata in modo da usufruire di alcune di queste funzionalità.

Aggiungere una pagina layout 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>

Il Layout:

  • Controlla il layout di ogni pagina, a meno che la pagina non accetti il layout.
  • Importa le strutture HTML, ad esempio JavaScript e i fogli di stile.
  • Il contenuto della pagina Razor viene sottoposto a rendering dove viene chiamato @RenderBody().

Per altre informazioni, vedere pagina layout.

La proprietà Layout è impostata in Pages/_ViewStart.cshtml:

@{
    Layout = "_Layout";
}

Il layout è nella cartella Pages/Shared. Le pagine cercano altre visualizzazioni (layout, modelli, righe parzialmente eseguite) in modo gerarchico, partendo dalla stessa cartella della pagina corrente. Un layout nella cartella Pages/Shared può essere usato da qualsiasi pagina Razor della cartella Pages.

Il file di layout dovrebbe andare nella cartella Pages/Shared.

Si consiglia di non inserire il file di layout nella cartella Views/Shared. Views/Shared è un modello destinato alle visualizzazioni MVC. Razor Pages deve basarsi sulla gerarchia di cartelle, non su convenzioni di percorso.

La ricerca delle visualizzazioni da una pagina Razor include la cartella Pages. Il layout, i modelli e le righe parzialmente eseguite in uso con i controller MVC e le visualizzazioni Razor tradizionali funzionano normalmente.

Aggiungere un file Pages/_ViewImports.cshtml:

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

@namespace viene spiegato in seguito nell'esercitazione. La direttiva @addTagHelper inserisce gli helper tag predefiniti in tutte le pagine presenti nella cartella Pages.

La direttiva @namespace impostata in una pagina:

@page
@namespace RazorPagesIntro.Pages.Customers

@model NameSpaceModel

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

La direttiva @namespace imposta lo spazio dei nomi per la pagina. La direttiva @model non deve necessariamente includere lo spazio dei nomi.

Se la direttiva @namespace è contenuta in _ViewImports.cshtml, lo spazio dei nomi specificato indica il prefisso per lo spazio dei nomi generato nella pagina che importa la direttiva @namespace. Il resto dello spazio dei nomi generato (la parte del suffisso) è il percorso relativo separato dal punto tra la cartella contenente _ViewImports.cshtml e la cartella contenente la pagina.

Ad esempio, la classe PageModelPages/Customers/Edit.cshtml.cs imposta esplicitamente lo spazio dei nomi:

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

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

        // Code removed for brevity.

Il file Pages/_ViewImports.cshtml imposta lo spazio dei nomi seguente:

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

Lo spazio dei nomi generato per la pagina RazorPages/Customers/Edit.cshtml corrisponde alla classe PageModel.

@namespaceè compatibile anche con le visualizzazioni Razor tradizionali.

Si consideri il file di visualizzazione 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>

Il file di visualizzazione Pages/Create.cshtml aggiornato con _ViewImports.cshtml e il file di layout precedente:

@page
@model CreateModel

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

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

Nel codice precedente _ViewImports.cshtml ha importato lo spazio dei nomi e gli helper tag. Il file di layout ha importato i file JavaScript.

Il progetto di avvio di Razor Pages contiene Pages/_ValidationScriptsPartial.cshtml, che associa la convalida lato client.

Per altre informazioni sulle visualizzazioni parziali, vedere Visualizzazioni parziali in ASP.NET Core.

Generazione di URL per le pagine

La pagina Create, riportata in precedenza, 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");
    }
}

L'applicazione ha la struttura di file o cartella seguente:

  • Pages/

    • Index.cshtml

    • Privacy.cshtml

    • /Customers

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

Le pagine Pages/Customers/Create.cshtml e Pages/Customers/Edit.cshtml reindirizzano a Pages/Customers/Index.cshtml dopo il completamento dell'operazione. La stringa ./Index è un nome di pagina relativo usato per accedere alla pagina precedente. Viene usato per generare URL della pagina Pages/Customers/Index.cshtml. Ad esempio:

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

Il nome di pagina assoluto /Index viene usato per generare gli URL della pagina Pages/Index.cshtml. Ad esempio:

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

Il nome della pagina è il percorso alla pagina dalla cartella radice /Pages, inclusa una barra iniziale /, ad esempio /Index. Gli esempi di generazione di URL precedenti offrono opzioni e caratteristiche funzionali avanzate rispetto agli URL hardcoded. La generazione di URL usa il routing ed è in grado di generare e codificare i parametri in base al modo in cui la route è definita nel percorso di destinazione.

La generazione di URL per le pagine supporta i nomi relativi. La tabella seguente indica quale pagina di indice viene selezionata con diversi parametri RedirectToPage in Pages/Customers/Create.cshtml.

RedirectToPage(x) Pagina
RedirectToPage("/Index") Pages/Index
RedirectToPage("./Index"); Pages/Customers/Index
RedirectToPage("../Index") Pages/Index
RedirectToPage("Index") Pages/Customers/Index

RedirectToPage("Index"), RedirectToPage("./Index") e RedirectToPage("../Index") sono nomi relativi. Il parametro RedirectToPage è combinato con il percorso della pagina corrente per calcolare il nome della pagina di destinazione.

Il collegamento dei nomi relativi è utile quando si compilano siti con una struttura complessa. Quando vengono usati nomi relativi per il collegamento tra pagine in una cartella:

  • La ridenominazione di una cartella non interrompe i collegamenti relativi.
  • I collegamenti non vengono interrotti perché non includono il nome della cartella.

Per reindirizzare la pagina a un'Area diversa, specificare l'area:

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

Per altre informazioni, vedere Aree in ASP.NET Core e Convenzioni di route e app di Razor Pages in ASP.NET Core.

Attributo ViewData

I dati possono essere passati a una pagina con ViewDataAttribute. I valori delle proprietà con l'attributo [ViewData] vengono archiviati e caricati da ViewDataDictionary.

Nell'esempio seguente AboutModel applica l'attributo [ViewData] alla proprietà Title:

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

    public void OnGet()
    {
    }
}

Nella pagina About (Informazioni) accedere alla proprietà Title come proprietà del modello:

<h1>@Model.Title</h1>

Nel layout il titolo viene letto dal dizionario ViewData:

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

TempData

ASP.NET Core espone TempData. Questa proprietà archivia i dati finché non viene letta. I metodi Keep e Peek possono essere usati per esaminare i dati senza eliminazione. TempData è utile per il reindirizzamento quando i dati sono necessari per più di una singola richiesta.

Il codice seguente imposta il valore di Message usando 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");
    }
}

Il markup seguente nel file Pages/Customers/Index.cshtml visualizza il valore di Message usando TempData.

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

Il modello di pagina Pages/Customers/Index.cshtml.cs applica l'attributo [TempData] alla proprietà Message.

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

Per altre informazioni, vedere TempData.

Più gestori per pagina

La pagina seguente genera markup per due gestori usando l'helper tag asp-page-handler:

@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>

Il form nell'esempio precedente contiene due pulsanti di invio, ognuno dei quali usa FormActionTagHelper per l'invio a un URL diverso. L'attributo asp-page-handler è correlato a asp-page. asp-page-handler genera gli URL che indirizzano a ognuno dei metodi gestore definiti da una pagina. asp-page non è specificato poiché l'esempio è collegato alla pagina corrente.

Il modello di pagina :

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();
        }
    }
}

Il codice precedente usa metodi gestore denominati. I metodi gestore denominati vengono creati usando il testo che nel nome segue On<HTTP Verb> e precede Async (se presente). Nell'esempio precedente i metodi della pagina sono OnPostJoinListAsync e OnPostJoinListUCAsync. Rimuovendo OnPost e Async, i nomi dei gestori sono JoinList e JoinListUC.

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

Usando il codice precedente, il percorso URL che indirizza a OnPostJoinListAsync è https://localhost:5001/Customers/CreateFATH?handler=JoinList. Il percorso URL che indirizza a OnPostJoinListUCAsync è https://localhost:5001/Customers/CreateFATH?handler=JoinListUC.

Route personalizzate

Usare la direttiva @page per:

  • Specificare una route personalizzata a una pagina. Ad esempio, è possibile impostare la route alla pagina About (Informazioni) su /Some/Other/Path con @page "/Some/Other/Path".
  • Aggiungere segmenti alla route predefinita di una pagina. Ad esempio, è possibile aggiungere un segmento "item" alla route predefinita di una pagina con @page "item".
  • Aggiungere parametri alla route predefinita di una pagina. Ad esempio, un parametro ID, id, può essere necessario per una pagina con @page "{id}".

Un percorso relativo alla directory radice designato da una tilde (~) all'inizio del percorso è supportato. Ad esempio, @page "~/Some/Other/Path" è identico a @page "/Some/Other/Path".

Se si vuole omettere la stringa di query ?handler=JoinList dall'URL, cambiare la route in modo da inserire il nome del gestore nella parte di percorso dell'URL. È possibile personalizzare la route aggiungendo un modello di route racchiuso tra virgolette doppie dopo la direttiva @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>

Usando il codice precedente, il percorso URL che indirizza a OnPostJoinListAsync è https://localhost:5001/Customers/CreateFATH/JoinList. Il percorso URL che indirizza a OnPostJoinListUCAsync è https://localhost:5001/Customers/CreateFATH/JoinListUC.

? che segue handler indica che il parametro di route è facoltativo.

Configurazione e impostazioni avanzate

La configurazione e le impostazioni descritte nelle sezioni seguenti non sono richieste dalla maggior parte delle app.

Per configurare le opzioni avanzate, usare l'overload AddRazorPages che configura RazorPagesOptions:

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

Usare RazorPagesOptions per impostare la directory radice per le pagine o aggiungere convenzioni di modello applicativo per le pagine. Per altre informazioni sulle convenzioni, vedere Convenzioni di autorizzazione di Razor Pages.

Per la precompilazione delle visualizzazioni, vedere l'articolo sulla compilazione delle visualizzazioni di Razor.

Specificare che Razor Pages si trova nella radice del contenuto

Per impostazione predefinita, la directory radice di Razor Pages è /Pages. Aggiungere WithRazorPagesAtContentRoot per specificare che Razor Pages si trova nella radice del contenuto (ContentRootPath) dell'app:

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

Specificare che Razor Pages si trova in una directory radice personalizzata

Aggiungere WithRazorPagesRoot per specificare che Razor Pages si trova in una directory radice personalizzata nell'app (fornire un percorso relativo):

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

Risorse aggiuntive