Condividi tramite


architettura e concetti di Razor Pages in ASP.NET Core

Di Rick Anderson, Dave Brock e Kirk Larkin

Note

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

Warning

Questa versione di ASP.NET Core non è più supportata. Per altre informazioni, vedere .NET e .NET Core Support Policy. Per la versione corrente, vedere la versione .NET 10 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.

Per un'esercitazione che usa l'approccio Model-View-Controller, vedere Iniziare con ASP.NET Core MVC.

Questo articolo illustra l'architettura, i concetti e i modelli che rendono Razor le pagine efficaci per la creazione di applicazioni Web incentrate sulle pagine. Illustra il Razor funzionamento di Pages, i relativi componenti chiave e le procedure consigliate per l'implementazione. Se si preferisce l'apprendimento pratico con istruzioni dettagliate, vedere Tutorial: Creare un'app Web Razor Pages con ASP.NET Core. Per una panoramica delle ASP.NET Core, vedere Introduzione a ASP.NET Core.

Prerequisites

Creare un progetto Pages Razor

Vedere Inizia con Razor Pages per istruzioni dettagliate su come creare un progetto Razor Pages.

Razor Pagine

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 è molto 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. 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:

Nome file e percorso 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

Notes:

  • 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 modulo 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, il 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 ha un OnPostAsyncmetodo del gestore, che viene eseguito sulle POST richieste (quando un utente pubblica il modulo). È 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 Create.cshtmlRazor.
  • 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 usando 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 inviato:

  • 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 handler. Un metodo gestore che restituisce void esegue il rendering della pagina.
    • Nell'esempio precedente, la pubblicazione del modulo senza alcun valore causa ModelState.IsValid a restituire 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 sul 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. Binding riduce il codice utilizzando la stessa proprietà per eseguire il rendering dei campi del modulo (<input asp-for="Customer.Name">) e accettare l'input.

Warning

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à. Scegliere di utilizzare l'associazione GET è utile in scenari che si basano su valori di stringa di query o di rotta.

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

[BindProperty(SupportsGet = true)]

Per altre informazioni, vedi 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 <input asp-for="Customer.Name" /> tag di input associa l'elemento HTML <input> all'espressione Customer.Name del modello.
  • @addTagHelper rende disponibili i Tag Helpers.

Pagina iniziale

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 <a /a>Tag di ancoraggio ha usato l'attributo asp-route-{value} per generare un collegamento alla pagina Modifica. Il collegamento contiene i dati del percorso con l'ID del 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>

L'HTML renderizzato:

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

Metodo OnPostDeleteAsync:

  • Ottiene il id dalla stringa di query.
  • Interroga il database per il contatto 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?}"

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

Validation

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:

  • Includi gli script jQuery e quelli di convalida jQuery.

  • Usa i <div /><span />Tag Helpers per abilitare:

    • 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 contenere 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 un "Genere".
  • 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 Crea per il modello Movie mostra che ci sono errori con valori non validi:

Modulo di visualizzazione del film con più errori di convalida lato client con 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 Pagine) 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 reso dalle pagine o dalle visualizzazioni dell’applicazione. 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.

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

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

Note

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 project 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. Ad esempio, usando Visual Studio, configurare la compilazione del preprocessore esistente come attività Before Build all'interno di Visual Studio Task Runner Explorer.

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 project in 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 project seguente un file BaseView.cshtml.css contiene stili comuni tra le 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 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 specificato viene generato nella directory principale dell'app. Nel file project usare la proprietà StaticWebAssetBasePath per modificare 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

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 <link> del tag href 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. Se l'identificatore del pacchetto non è specificato nel file di progetto, per impostazione predefinita viene impostato sul nome dell'assembly del 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 Pagine) 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 ASP.NET Core Blazor isolamento CSS.

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 OnHead viene creato e chiamato un gestore HEAD:

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

Razor Pages ritorna a chiamare il gestore OnGet se non è definito un gestore OnHead.

XSRF/CSRF e Razor Pagine

Razor Le pagine sono protette dalla convalida antifalsificazione. FormTagHelper inserisce token antifalsificazione negli elementi del modulo HTML.

Uso di layout, partiali, modelli e Tag Helper con Razor Pages

Le pagine sono compatibili con tutte le funzionalità del motore di visualizzazione Razor. I layout, le viste parziali, i modelli, i Tag Helper, _ViewStart.cshtml e _ViewImports.cshtml funzionano esattamente come nelle viste convenzionali 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>

Struttura:

  • Controlla il layout di ogni pagina, a meno che la pagina non scelga di non utilizzare il layout.
  • Importa le strutture HTML, ad esempio JavaScript e i fogli di stile.
  • Il contenuto della Razor pagina viene visualizzato 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, template, parziali) 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. I layout, i template e i partiali usati con i controller MVC e le visualizzazioni convenzionali 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 da punti 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 Pages/Customers/Edit.cshtmlRazor corrisponde alla classe PageModel.

@namespace funziona anche con le visualizzazioni convenzionaliRazor.

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 di tag. Il file di layout ha importato i file JavaScript.

Il Razor Pages starter project contiene l'Pages/_ValidationScriptsPartial.cshtml, che collega la convalida lato client.

Per altre informazioni sulle visualizzazioni parziali, vedere Partial views 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. Per 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. Per 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 precedenti di generazione di URL offrono opzioni e funzionalità avanzate rispetto alla codifica manuale degli URL. 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) Page
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 tramite nomi relativi è utile quando si costruiscono 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 Areas in ASP.NET Core e Razor Pages route and app conventions 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 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 il TempData. Questa proprietà memorizza i dati fino a quando non vengono letti. 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 il markup per due gestori utilizzando il Tag Helper asp-page-handler:

@page
@model CreateFATHModel

<html>
<body>
    <p>
        Enter your name.
    </p>
    <div asp-validation-summary="All"></div>
    <form method="POST">
        <div><label>Name: <input asp-for="Customer.Name" /></label></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 inviano a ciascuno dei metodi gestori 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 gestori 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 verso una pagina. Ad esempio, è possibile impostare la route alla pagina About 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 root directory indicato all'inizio del percorso da una tilde (~) è supportato. Ad esempio, @page "~/Some/Other/Path" è uguale a @page "/Some/Other/Path".

Se non ti piace la stringa di query ?handler=JoinList nell'URL, modifica la route per 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><label>Name: <input asp-for="Customer.Name" /></label></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.

Collocare i file JS seguendo le convenzioni per le estensioni dei nomi dei file.

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

I file collocati JS sono indirizzabili 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 di 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>
    }
    

Il layout Pages/Shared/_Layout.cshtml predefinito può essere configurato per includere i file collocati JS , eliminando la necessità di configurare ogni pagina singolarmente:

<script asp-src-include="@(ViewContext.View.Path).js"></script>

Il download sample usa il frammento di codice precedente per includere i file JS collocati nel layout predefinito.

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 Target Framework Moniker (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 del 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 precompilare le visualizzazioni, vedere la Razor compilazione delle visualizzazioni.

Specificare che Razor Pages si trova nella radice del contenuto

Per impostazione predefinita, le Razor Pages sono nella directory /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 Pages Razor

Vedere Inizia con Razor Pages per istruzioni dettagliate su come creare un progetto Razor Pages.

Razor Pagine

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 è molto 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. 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:

Nome file e percorso 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

Notes:

  • 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 modulo 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, il 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 ha un OnPostAsyncmetodo del gestore, che viene eseguito sulle POST richieste (quando un utente pubblica il modulo). È 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 CreateModel.cshtmlRazor.
  • 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 usando 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 inviato:

  • 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, l'invio del modulo senza alcun valore fa sì che ModelState.IsValid restituisca 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 sul 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. Il binding riduce il codice utilizzando la stessa proprietà per eseguire il rendering dei campi del modulo (<input asp-for="Customer.Name">) e accettare l'input.

Warning

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à. Optare per l'associazione GET è utile quando si affrontano scenari che si basano su valori del percorso o stringa della query.

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

[BindProperty(SupportsGet = true)]

Per altre informazioni, vedi 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 <input asp-for="Customer.Name" /> tag di input associa l'elemento HTML <input> all'espressione Customer.Name del modello.
  • @addTagHelper rende disponibili i Tag Helper.

Pagina iniziale

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 <a /a>tag di ancoraggio ha usato l'attributo asp-route-{value} per generare un collegamento alla pagina Modifica. Il collegamento contiene i dati del percorso con l'ID del 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>

L'HTML renderizzato:

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

Metodo OnPostDeleteAsync:

  • Ottiene 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?}"

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

}

Validation

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:

  • Includi gli script jQuery e quelli di convalida jQuery.

  • Usa i <div /><span />Tag Helpers per abilitare:

    • 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 contenere solo lettere.
    • La prima lettera deve essere maiuscola. Gli spazi, i numeri e i caratteri speciali non sono consentiti.
  • RegularExpression "Valutazione"

    • 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 un "Genere".
  • 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 errori con valori non validi.

Modulo di visualizzazione del film con più errori di convalida lato client di 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 OnHead viene creato e chiamato un gestore HEAD:

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

Razor Pages torna a chiamare il gestore OnGet se nessun gestore OnHead è definito.

XSRF/CSRF e Razor Pagine

Razor Le pagine sono protette dalla validazione antifalsificazione. FormTagHelper inserisce token antifalsificazione negli elementi del modulo HTML.

Uso di layout, partiali, modelli e Tag Helpers con Razor Pages

Le pagine sono compatibili con tutte le funzionalità del motore di visualizzazione Razor. I layout, i parziali, i modelli, gli helper di tag, _ViewStart.cshtml e _ViewImports.cshtml funzionano esattamente come nelle normali viste 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>

Disposizione:

  • Controlla il layout di ogni pagina, a meno che la pagina non scelga di non utilizzare il layout.
  • Importa le strutture HTML, ad esempio JavaScript e i fogli di stile.
  • Il rendering del contenuto della pagina Razor viene effettuato 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, template, parziali) 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. I layout, i modelli e i parziali utilizzati con i controller MVC e le visualizzazioni Razor tradizionali funzionano perfettamente.

Aggiungere un file Pages/_ViewImports.cshtml:

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

@namespace sarà spiegato più avanti nel tutorial. 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 da punti 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 Pages/Customers/Edit.cshtmlRazor corrisponde alla classe PageModel.

@namespace funziona anche con le visualizzazioni convenzionaliRazor.

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 i Tag Helpers. Il file di layout ha importato i file JavaScript.

Il Razor Pages starter project contiene l'Pages/_ValidationScriptsPartial.cshtml, che collega la convalida lato client.

Per altre informazioni sulle visualizzazioni parziali, vedere Partial views 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. Per 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. Per esempio:

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

Il nome della pagina è il percorso della pagina dalla cartella radice /Pages, inclusa una barra iniziale /, ad esempio /Index. Gli esempi precedenti di generazione di URL offrono opzioni e funzionalità avanzate rispetto alla codifica manuale degli URL. 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) Page
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 tramite nomi relativi è utile quando si costruiscono 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 Areas in ASP.NET Core e Razor Pages route and app conventions 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 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 il TempData. Questa proprietà memorizza i dati fino a quando non vengono letti. 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 il markup per due gestori utilizzando il Tag Helper asp-page-handler:

@page
@model CreateFATHModel

<html>
<body>
    <p>
        Enter your name.
    </p>
    <div asp-validation-summary="All"></div>
    <form method="POST">
        <div><label>Name: <input asp-for="Customer.Name" /></label></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 inviano a ciascuno dei metodi gestori 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 gestori 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 verso una pagina. Ad esempio, è possibile impostare la route alla pagina About 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 radice indicato da una tilde (~) all'inizio del percorso è supportato. Ad esempio, @page "~/Some/Other/Path" è uguale a @page "/Some/Other/Path".

Se non ti piace la stringa di query ?handler=JoinList nell'URL, cambia la route per inserire il nome del gestore nella parte del 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><label>Name: <input asp-for="Customer.Name" /></label></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 la compilazione delle visualizzazioniRazor.

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 le pagine Razor si trovano nella radice del contenuto (ContentRootPath) dell'applicazione:

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

Specificare che le pagine Razor si trovano in una directory radice personalizzata

Aggiungere WithRazorPagesRoot per specificare che le pagine Razor sono collocate in una directory radice personalizzata nell'applicazione (fornire un percorso relativo):

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

Risorse aggiuntive