Condividi tramite


Il presente articolo è stato tradotto automaticamente.

ASP.NET

Applicazioni a pagina singola: creazione di applicazioni Web moderne e reattive con ASP.NET

Mike Wasson

Scaricare il codice di esempio

Applicazioni a pagina singola (ZPS) sono applicazioni Web che caricare una singola pagina HTML e aggiornare dinamicamente quella pagina come l'utente interagisce con l'app.

Terme utilizzano AJAX e HTML5 per creare applicazioni Web fluidi e reattivi, senza ricarica pagina costante. Tuttavia, questo significa che gran parte del lavoro avviene sul lato client, in JavaScript. Per lo sviluppatore ASP.NET tradizionale, può essere difficile fare il salto. Fortunatamente, ci sono molti open source framework JavaScript che lo rendono più facile creare centri termali.

In questo articolo, sarò a piedi attraverso la creazione di una semplice app SPA. Lungo la strada, I'll introdurre alcuni concetti fondamentali per la costruzione di stabilimenti termali, compreso il Model-View-Controller (MVC) e pattern Model-View-ViewModel (MVVM), associazione dati e il routing.

Circa l'applicazione di esempio

L'applicazione di esempio che ho creato è un database di film semplice, mostrato Figura 1. La colonna di estrema sinistra della pagina viene visualizzato un elenco di generi. Cliccando su un genere fa apparire una lista di film all'interno di quel genere. Facendo clic sul pulsante Modifica accanto a una voce consente di modificare tale voce. Dopo aver apportato le modifiche, scegliere Salva a presentare l'aggiornamento per il server, o Annulla per annullare le modifiche.

The Single-Page Application Movie Database App
Figura 1 la pagina singola applicazione Movie Database App

Ho creato due versioni diverse dell'app, uno che utilizza la libreria di Knockout.js e l'altro utilizzando la libreria Ember.js. Queste due librerie hanno approcci diversi, è istruttivo confrontare loro. In entrambi i casi, l'applicazione client è stato meno di 150 righe di JavaScript. Sul lato server, ho usato ASP.NET Web API per servire JSON al client. È possibile trovare il codice sorgente per entrambe le versioni dell'app a github.com/MikeWasson/MoviesSPA.

Nota: Creato l'app utilizzando la versione finale candidata [RC] di Visual Studio 2013. Alcune cose potrebbero cambiare per il rilasciato al manufacturing [RTM] versione, ma essi non dovrebbe influenzare il codice.)

Introduzione

Un tradizionale Web app, ogni volta che l'app chiama il server, il server di rendering di una nuova pagina HTML. Questo innesca un refresh della pagina nel browser. Se hai mai scritto un'applicazione Web Form o applicazione PHP, questo ciclo di vita di pagina dovrebbe apparire familiare.

In un centro benessere, dopo i primi carichi di pagina, tutte le interazioni con il server avviene tramite chiamate AJAX. Questi dati di ritorno di chiamate AJAX — non il markup — solitamente in formato JSON. L'app utilizza i dati JSON per aggiornare la pagina in modo dinamico, senza ricaricare la pagina. Figura 2 illustra la differenza tra i due approcci.

The Traditional Page Lifecycle vs. the SPA Lifecycle
Figura 2 vs tradizionale Page The Lifecycle. il ciclo di vita SPA

Uno dei vantaggi delle Terme è ovvio: Le applicazioni sono più fluido e reattivo, senza l'effetto stridente di ricarico e re-rendering della pagina. Un altro vantaggio potrebbe essere meno evidente e riguarda come l'architetto una Web app. Inviando i dati di app come JSON crea una separazione tra la presentazione (codice HTML) e la logica dell'applicazione (richieste AJAX più risposte JSON).

Questa separazione rende più facile progettare ed evolvere ogni strato. In un centro benessere ben progettato, è possibile modificare il codice HTML senza toccare il codice che implementa la logica di applicazione (almeno, questo è l'ideale). Vedrete questo in azione quando discutere più tardi di associazione dati.

In un centro di benessere puro, tutti interazione UI si verifica sul lato client, tramite JavaScript e CSS. Dopo il caricamento della pagina iniziale, il server agisce esclusivamente come un livello di servizio. Il cliente ha solo bisogno di sapere che cosa le richieste HTTP per inviare. Esso non si preoccupa come server implementa cose sul back-end.

Con questa architettura, il client e il servizio sono indipendenti. Si potrebbe sostituire l'intero back-end che esegue il servizio, e finchè non cambiano le API, non infrangi il client. È anche vero il contrario, è possibile sostituire l'intero client app senza cambiare il livello di servizio. Ad esempio, si potrebbe scrivere un client nativo mobile che utilizza il servizio.

Creazione del progetto di Visual Studio

Visual Studio 2013 ha un solo tipo di progetto di applicazione Web ASP.NET . Project wizard consente di selezionare i componenti ASP.NET da includere nel tuo progetto. Ho iniziato con il modello vuoto e poi aggiunto ASP.NET Web API al progetto selezionando Web API sotto "aggiungono cartelle e fondamentali riferimenti per:" come mostrato Figura 3.

Creating a New ASP.NET Project in Visual Studio 2013
Figura 3 creazione di un nuovo progetto ASP.NET in Visual Studio 2013

Il nuovo progetto ha tutte le librerie necessarie per Web API, oltre a qualche codice di configurazione Web API. Non ho preso alcuna dipendenza Web Form o ASP.NET MVC.

Si noti che nel Figura 3 che Visual Studio 2013 include un modello di pagina singola applicazione. Questo modello consente di installare uno scheletro SPA costruita su Knockout.js. Supporta log in utilizzando un database di appartenenza o il provider di autenticazione esterna. Non ho usato il modello nel mio app perché volevo mostrare un esempio più semplice, partendo da zero. Il modello SPA è una grande risorsa, però, soprattutto se si desidera aggiungere l'autenticazione all'app.

Creando il livello di servizio

Ho usato ASP.NET Web API per creare una semplice API REST per l'app. Io non entrerò nei dettagli sull'API Web qui — si può leggere di più a asp. NET/web-api.

In primo luogo, ho creato una classe di film che rappresenta un film. Questa classe fa due cose:

  • Dice Entity Framework (EF) come creare le tabelle di database per memorizzare i dati del film.
  • Racconta Web API come formattare il payload JSON.

Non devi utilizzare lo stesso modello per entrambi. Ad esempio, è possibile lo schema del database per un aspetto diverso dal tuo payload JSON. Per questa applicazione, ho mantenuto le cose semplici:

namespace MoviesSPA.Models
{
  public class Movie
  {
    public int ID { get; set; }
    public string Title { get; set; }
    public int Year { get; set; }
    public string Genre { get; set; }
    public string Rating { get; set; }
  }
}

Successivamente, ho usato ponteggio Visual Studio per creare un controller di Web API che utilizza EF come lo strato di dati. Per utilizzare il ponteggio, tasto destro del mouse la cartella Controllers in Esplora soluzioni e scegliere Aggiungi | Nuovo elemento messo. Nella procedura guidata Aggiungi impalcatura, seleziona "Web API 2 Controller con azioni, utilizzando il Entity Framework," come mostrato Figura 4.

Adding a Web API Controller
Figura 4 l'aggiunta di un Controller di API Web

Figura 5 illustra la procedura guidata Aggiungi Controller. Chiamato il controller MoviesController. Il nome conta, perché gli URI per l'API REST sono basati sul nome del controller. Ho anche controllato "Azioni del controller uso async" per sfruttare la nuova funzionalità di async in EF 6. Ho selezionato la classe di film per il modello e selezionate "nuovo contesto dati" per creare un nuovo contesto dati EF.

The Add Controller Wizard
Figura 5 l'installazione guidata di Controller

La procedura guidata aggiunge due file:

  • MoviesController.cs definisce il controller API Web che implementa l'API REST per l'app.
  • MovieSPAContext.cs è fondamentalmente colla EF che fornisce metodi per interrogare il database sottostante.

Figura 6 Mostra il default API REST ponteggio crea.

Figura 6 l'API REST di Default creato da ponteggio Web API

Verbo HTTP URI Descrizione
GET / api/film Ottenere un elenco di tutti i film
GET /API/film / {id} Ottenere film con ID uguale a {id}
PUT /API/film / {id} Aggiornare il film con ID uguale a {id}
POST / api/film Aggiungere un nuovo film nel database
CANC /API/film / {id} Eliminare un filmato dal database

I valori in parentesi graffe sono segnaposti. Ad esempio, per ottenere un filmato con ID uguale a 5, l'URI è /api/movies/5.

Esteso questa API con l'aggiunta di un metodo che rileva tutti i film in un genere specificato:

public class MoviesController : ApiController
{
  public IQueryable<Movie> GetMoviesByGenre(string genre)
  {
    return db.Movies.Where(m =>
      m.Genre.Equals(genre, StringComparison.OrdinalIgnoreCase));
  }
  // Other code not shown

Il cliente mette il genere nella stringa di query dell'URI. Ad esempio, per ottenere tutti i film del genere drammatico, il client invia una richiesta GET al/api/film? genere = dramma. Web API associa automaticamente il parametro query al parametro nel metodo GetMoviesByGenre genere.

Creazione di Client Web

Finora, ho appena creato una API REST. Se si invia una richiesta GET a/api/film? genere = dramma, il crudo HTTP risposta assomiglia a questo:

HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Type: application/json; charset=utf-8
Date: Tue, 10 Sep 2013 15:20:59 GMT
Content-Length: 240
[{"ID":5,"Title":"Forgotten Doors","Year":2009,"Genre":"Drama","Rating":"R"}, {"ID":6,"Title":"Blue Moon June","Year":1998,"Genre":"Drama","Rating":"PG-13"},{"ID":7,"Title":"The Edge of the Sun","Year":1977,"Genre":"Drama","Rating":"PG-13"}]

Ora ho bisogno di scrivere un'applicazione client che fa qualcosa di significativo con questo. Il flusso di lavoro di base è:

  • UI innesca una richiesta AJAX
  • Aggiornare il codice HTML per visualizzare il payload di risposta
  • Gestire gli errori di AJAX

Tutto questo potrebbe codice a mano. Ad esempio, ecco alcuni codice jQuery che crea un elenco di titoli di film:

$.getJSON(url)
  .done(function (data) {
    // On success, "data" contains a list of movies
    var ul = $("<ul></ul>")
    $.each(data, function (key, item) {
      // Add a list item
      $('<li>', { text: item.Title }).appendTo(ul);
    });
  $('#movies').html(ul);
});

Questo codice ha alcuni problemi. Mescola la logica dell'applicazione con la logica di presentazione, ed è strettamente associato al codice HTML. Inoltre, è noioso da scrivere. Invece di concentrarsi su app, trascorrere il vostro tempo a scrivere gestori eventi e codice per manipolare il Dom.

La soluzione è quello di costruire un framework JavaScript. Fortuna­ily, è possibile scegliere tra molti open source framework JavaScript. Alcuni di quelli più popolari includono il Backbone, angolare, Ember, Knockout, Dojo e JavaScriptMVC. La maggior parte utilizza alcune variazioni dei pattern MVC o MVVM, quindi potrebbe essere utile rivedere quei modelli.

Il pattern MVVM e MVC

Il MVC modello risale al 1980 e la precoce interfacce utente grafiche. L'obiettivo di MVC è fattore il codice in tre distinte responsabilità, mostrata Figura 7. Ecco cosa fanno:

  • Il modello rappresenta la logica di dominio dati e business.
  • La vista viene visualizzato il modello.
  • Il controller riceve l'input dell'utente e aggiorna il modello.

The MVC Pattern
Figura 7 il Pattern MVC

Una variante più recente di MVC è il MVVM (vedere Figura 8). In MVVM:

  • Il modello rappresenta ancora i dati del dominio.
  • Il modello di visualizzazione è una rappresentazione astratta della vista.
  • La vista viene visualizzato il modello di visualizzazione e invia l'input dell'utente per il modello di visualizzazione.

The MVVM Pattern
Figura 8 il MVVM

In un quadro di JavaScript MVVM, la vista è il markup e il modello di visualizzazione è codice.

MVC ha molte varianti, e la letteratura su MVC è spesso confusa e contraddittoria. Forse non è sorprendente per un modello di progettazione che è iniziato con Smalltalk-76 ed è ancora utilizzato nelle moderne applicazioni Web. Così, anche se è bene sapere la teoria, la cosa principale è capire il particolare framework MVC che stai usando.

Del client Web con Knockout.js

Per la prima versione del mio app, ho utilizzato la libreria Knockout.js. Eliminazione diretta segue il pattern MVVM, tramite l'associazione dati per collegare la vista con il modello di visualizzazione.

Per creare associazioni dati, si aggiunge un attributo speciale di associazione dati agli elementi HTML. Ad esempio, il markup seguente associa l'elemento span per una proprietà denominata genere sul modello di visualizzazione. Ogni volta che cambia il valore del genere, Knockout aggiorna automaticamente il codice HTML:

<h1><span data-bind="text: genre"></span></h1>

Associazioni possono funzionare anche in altro senso — ad esempio, se l'utente immette il testo in una casella di testo, Knockout aggiorna la proprietà corrispondente nel modello di visualizzazione.

La parte bella è che l'associazione dati è dichiarativa. Non hai a filo il modello di visualizzazione per gli elementi di pagina HTML. Basta aggiungere l'attributo di associazione dati e Knockout fa il resto.

Ho iniziato con la creazione di una pagina HTML con il layout di base, con nessuna associazione di dati, come mostrato Figura 9.

Nota: Utilizzato la libreria Bootstrap stile app, così l'app reale ha un sacco di extra < div > elementi e classi CSS per controllare la formattazione. Ho lasciato questi fuori gli esempi di codice per chiarezza.)

Figura 9 iniziale HTML Layout

<!DOCTYPE html>
<html>
<head>
  <title>Movies SPA</title>
</head>
<body>
  <ul>
    <li><a href="#"><!-- Genre --></a></li>
  </ul>
  <table>
    <thead>
      <tr><th>Title</th><th>Year</th><th>Rating</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td><!-- Title --></td>
        <td><!-- Year --></td>
        <td><!-- Rating --></td></tr>
    </tbody>
  </table>
  <p><!-- Error message --></p>
  <p>No records found.</p>
</body>
</html>

Creazione del modello di visualizzazione

Osservabili sono il fulcro del sistema di associazione dati Knockout. Un'osservabile è un oggetto che memorizza un valore e può informare gli abbonati quando cambia il valore. Il codice seguente converte la rappresentazione JSON di un film in oggetto equivalente con grandezze fisiche:

function movie(data) {
  var self = this;
  data = data || {};
  // Data from model
  self.ID = data.ID;
  self.Title = ko.observable(data.Title);
  self.Year = ko.observable(data.Year);
  self.Rating = ko.observable(data.Rating);
  self.Genre = ko.observable(data.Genre);
};

Figura 10 dimostra la mia implementazione iniziale del modello di visualizzazione. Questa versione supporta solo ottenere l'elenco dei film. Io aggiungere la funzionalità di modifica più tardi. Il modello di visualizzazione contiene osservabili per l'elenco dei film, una stringa di errore e il genere attuale.

Figura 10 il modello di visualizzazione

var ViewModel = function () {           
  var self = this;
  // View model observables
  self.movies = ko.observableArray();
  self.error = ko.observable();
  self.genre = ko.observable();  // Genre the user is currently browsing
  // Available genres
  self.genres = ['Action', 'Drama', 'Fantasy', 'Horror', 'Romantic Comedy'];
  // Adds a JSON array of movies to the view model
  function addMovies(data) {
    var mapped = ko.utils.arrayMap(data, function (item) {
      return new movie(item);
    });
    self.movies(mapped);
  }
  // Callback for error responses from the server
  function onError(error) {
    self.error('Error: ' + error.status + ' ' + error.statusText);
  }
  // Fetches a list of movies by genre and updates the view model
  self.getByGenre = function (genre) {
    self.error(''); // Clear the error
    self.genre(genre);
    app.service.byGenre(genre).then(addMovies, onError);
  };
  // Initialize the app by getting the first genre
  self.getByGenre(self.genres[0]);
}
// Create the view model instance and pass it to Knockout
ko.applyBindings(new ViewModel());

Si noti che il film è un observableArray. Come suggerisce il nome, un observableArray agisce come una matrice che notifica ai sottoscrittori quando cambia il contenuto della matrice.

La funzione di getByGenre fa una richiesta AJAX al server per l'elenco dei film e poi compila la matrice di self.movies con i risultati.

Quando si consumano una API REST, una delle parti più delicate è manipolazione della natura asincrona di HTTP. La funzione ajax di jQuery restituisce un oggetto che implementa l'API di promesse. È possibile utilizzare un oggetto promessa poi il metodo per impostare un callback richiamato quando la chiamata AJAX viene completata con successo e un altro callback richiamato se la chiamata AJAX non riesce:

app.service.byGenre(genre).then(addMovies, onError);

Associazioni dati

Ora che ho un modello di visualizzazione, posso associare dati HTML ad esso. Per l'elenco dei generi che appare nella parte sinistra dello schermo, ho usato le seguenti associazioni di dati:

<ul data-bind="foreach: genres">
  <li><a href="#"><span data-bind="text: $data"></span></a></li>
</ul>

L'attributo di associare dati contiene uno o più dichiarazioni di associazione, dove ogni associazione ha la forma "associazione: espressione." In questo esempio, l'associazione foreach racconta Knockout per scorrere il contenuto della matrice generi nel modello di visualizzazione. Per ogni elemento della matrice, Knockout crea un nuovo < li > elemento. L'associazione del testo in < span > imposta il testo span uguale al valore dell'elemento di matrice, che in questo caso è il nome del genere.

Destra ora, cliccando sui nomi genere non fa nulla, così ho aggiunto un clic in associazione a gestire eventi click:

<li><a href="#" data-bind="click: $parent.getByGenre">
  <span data-bind="text: $data"></span></a></li>

Questo associa l'evento click per la funzione getByGenre sul modello di visualizzazione. Avevo bisogno di usare il $parent qui, perché questa associazione si verifica nel contesto di foreach. Per impostazione predefinita, associazioni all'interno di un ciclo foreach fare riferimento all'elemento corrente nel ciclo.

Per visualizzare l'elenco dei film, aggiunto associazioni alla tabella, come mostrato Figura 11.

Figura 11 associazioni l'aggiunta alla tabella per visualizzare un elenco di film

<table data-bind="visible: movies().length > 0">
  <thead>
    <tr><th>Title</th><th>Year</th><th>Rating</th><th></th></tr>
  </thead>
  <tbody data-bind="foreach: movies">
    <tr>
      <td><span data-bind="text: Title"></span></td>
      <td><span data-bind="text: Year"></span></td>
      <td><span data-bind="text: Rating"></span></td>
      <td><!-- Edit button will go here --></td>
    </tr>
  </tbody>
</table>

In Figura 11, l'associazione loop su una matrice di oggetti film foreach. All'interno il foreach, le associazioni di testo si riferiscono alle proprietà dell'oggetto corrente.

L'associazione visibile sul tavolo < > elemento controlla se la tabella viene eseguito il rendering. In questo modo nascondere la tabella se la matrice del film è vuota.

Infine, qui ci sono le associazioni per il messaggio di errore e il messaggio "Nessun record trovato" (notare che è possibile inserire espressioni complesse in un'associazione):

<p data-bind="visible: error, text: error"></p>
<p data-bind="visible: !error() && movies().length == 0">No records found.</p>

Rendendo i record modificabili

L'ultima parte di questa applicazione sta dando all'utente la possibilità di modificare i record nella tabella. Questo comporta diversi bit di funzionalità:

  • Commutazione tra modalità (testo normale) visualizzazione e modifica la modalità (controlli di input).
  • Presentazione di aggiornamenti al server.
  • Lasciando l'utente annullare una modifica e ripristinare i dati originali.

Per monitorare la modalità di visualizzazione/modifica, aggiunto un Boolean bandiera all'oggetto film, come un'osservabile:

function movie(data) {
  // Other properties not shown
  self.editing = ko.observable(false);
};

Volevo la tabella del film per visualizzare il testo quando si modifica la proprietà è false, ma vale per interruttore per controlli di input quando si modifica. A tale scopo, utilizzate il KO se e ifnot associazioni, come mostrato Figura 12. Il "<!---> ko" se nonviene associazioni senza inserirli all'interno di un elemento contenitore HTML e permette di sintassi includono se.

Figura 12 che consente la modifica dei record di film

<tr>
  <!-- ko if: editing -->
  <td><input data-bind="value: Title" /></td>
  <td><input type="number" class="input-small" data-bind="value: Year" /></td>
  <td><select class="input-small"
    data-bind="options: $parent.ratings, value: Rating"></select></td>
  <td>
    <button class="btn" data-bind="click: $parent.save">Save</button>
    <button class="btn" data-bind="click: $parent.cancel">Cancel</button>
  </td>
  <!-- /ko -->
  <!-- ko ifnot: editing -->
  <td><span data-bind="text: Title"></span></td>
  <td><span data-bind="text: Year"></span></td>
  <td><span data-bind="text: Rating"></span></td>
  <td><button class="btn" data-bind="click: $parent.edit">Edit</button></td>
  <!-- /ko -->
</tr>

L'associazione valore imposta il valore di un controllo di input. Questo è un associazione bidirezionale, così quando l'utente digita qualcosa nel campo di testo o modifica la selezione a discesa, il cambiamento si propaga automaticamente per il modello di visualizzazione.

Associato i gestori click pulsante per funzioni denominate Salva, annullare e modificare il modello di visualizzazione.

La funzione di modifica è facile. Basta impostare il flag di modifica su true:

self.edit = function (item) {
  item.editing(true);
};

Salva e Annulla sono stati un po' più complicati. Al fine di supportare Annulla, mi serviva un modo per memorizzare nella cache il valore originale durante la modifica. Fortunatamente, Knockout rende facile estendere il comportamento di osservabili. Il codice in Figura 13 aggiunge una funzione di archivio alla classe osservabile. Chiamare la funzione memorizza su un'osservabile fornisce osservabili due nuove funzioni: ripristinare e commettere.

Figura 13 estensibile ko.observable con Revert e Commit

Ora posso chiamare la funzione di archivio per aggiungere questa funzionalità al modello:

function movie(data) {
  // ...
// New code:
  self.Title = ko.observable(data.Title).store();
  self.Year = ko.observable(data.Year).store();
  self.Rating = ko.observable(data.Rating).store();
  self.Genre = ko.observable(data.Genre).store();
};

Figura 14 Mostra Salva e annulla le funzioni sul modello di visualizzazione.

Figura 14 aggiungendo Salva e annulla le funzioni

self.cancel = function (item) {
  revertChanges(item);
  item.editing(false);
};
self.save = function (item) {
  app.service.update(item).then(
    function () {
      commitChanges(item);
    },
    function (error) {
      onError(error);
      revertChanges(item);
    }).always(function () {
      item.editing(false);
  });
}
function commitChanges(item) {
  for (var prop in item) {
    if (item.hasOwnProperty(prop) && item[prop].commit) {
      item[prop].commit();
    }
  }
}
function revertChanges(item) {
  for (var prop in item) {
    if (item.hasOwnProperty(prop) && item[prop].revert) {
      item[prop].revert();
    }
  }
}

Del client Web con Ember

Per confronto, ho scritto un'altra versione della mia app utilizzando la libreria Ember.js.

Un'app di Ember inizia con una tabella di routing, che definisce come l'utente si sposterà attraverso l'app:

window.App = Ember.Application.create();
App.Router.map(function () {
  this.route('about');
  this.resource('genres', function () {
    this.route('movies', { path: '/:genre_name' });
  });
});

La prima riga di codice crea un'applicazione di Ember. La chiamata a Router.map crea tre percorsi. Ogni percorso corrisponde a un URI o URI modello:

/#/about
/#/genres
/#/genres/genre_name

Per ogni itinerario, si crea un modello HTML utilizzando la libreria di modelli di manubri.

Brace ha un modello di primo livello per l'intera applicazione. Questo modello il rendering per ogni itinerario. Figura 15 Mostra il modello di domanda per la mia applicazione. Come potete vedere, il modello è fondamentalmente HTML, collocato all'interno di un tag script con tipo = "text/x-manubri." Il modello contiene speciali manubri markup interno doppie parentesi graffe: {{ }}. Questo markup serve uno scopo simile come l'attributo di associare in Knockout. Ad esempio, {{#linkTo}} crea un collegamento a un percorso.

Figura 15 livello di applicazione manubrio modello

ko.observable.fn.store = function () {
  var self = this;
  var oldValue = self();
  var observable = ko.computed({
    read: function () {
      return self();
    },
    write: function (value) {
      oldValue = self();
      self(value);
    }
  });
  this.revert = function () {
    self(oldValue);
  }
  this.commit = function () {
    oldValue = self();
  }
  return this;
}
<script type="text/x-handlebars" data-template-name="application">
  <div class="container">
    <div class="page-header">
      <h1>Movies</h1>
    </div>
    <div class="well">
      <div class="navbar navbar-static-top">
        <div class="navbar-inner">
          <ul class="nav nav-tabs">
            <li>{{#linkTo 'genres'}}Genres{{/linkTo}} </li>
            <li>{{#linkTo 'about'}}About{{/linkTo}} </li>
          </ul>
        </div>
      </div>
    </div>
    <div class="container">
      <div class="row">{{outlet}}</div>
    </div>
  </div>
  <div class="container"><p>&copy;2013 Mike Wasson</p></div>
</script>

Ora supponiamo che l'utente si sposta a / #/ circa. Ciò richiama l'itinerario «informazioni». Brace prima esegue il rendering del modello di applicazione di primo livello. Poi rende sul modello di {{uscita}} del modello di applicazione. Ecco sul modello:

 

<script type="text/x-handlebars" data-template-name="about">
  <h2>Movies App</h2>
  <h3>About this app...</h3>
</script>

Figura 16 dimostra come sul modello viene sottoposto a rendering all'interno del modello di applicazione.

Rendering the About Template
Figura 16 Rendering sul modello

Perché ogni itinerario ha un URI, è conservata la cronologia del browser. L'utente può navigare con il pulsante indietro. L'utente può anche aggiornare la pagina senza perdere il contesto, o segnalibro e ricaricare la pagina stessa.

Modelli e i controller di ember

In Ember, ogni itinerario ha un modello e un controller. Il modello contiene i dati del dominio. Il controller funge da proxy per il modello e memorizza i dati di stato qualsiasi applicazione per la visualizzazione. (Questo non corrisponde esattamente alla definizione classica di MVC. In qualche modo, il controller è più simile a un modello vista.)

Ecco come definito il modello del film:

App.Movie = DS.Model.extend({
  Title: DS.attr(),
  Genre: DS.attr(),
  Year: DS.attr(),
  Rating: DS.attr(),
});

Il controller deriva da Ember.ObjectController, come mostrato Figura 17.

Figura 17 che il Controller film deriva da Ember.ObjectController

App.MovieController = Ember.ObjectController.extend({
  isEditing: false,
  actions: {
    edit: function () {
      this.set('isEditing', true);
    },
    save: function () {
      this.content.save();
      this.set('isEditing', false);
    },
    cancel: function () {
      this.set('isEditing', false);
      this.content.rollback();
    }
  }
});

Ci sono alcune cose interessanti qui in corso. In primo luogo, non specificare il modello nella classe controller. Per impostazione predefinita, il percorso imposta automaticamente il modello del controller. In secondo luogo, Salva e Annulla funzioni utilizzano le funzionalità di transazione costruite in DS.Classe modello. Per ripristinare le modifiche, basta chiamare la funzione di rollback sul modello.

Ember utilizza un sacco di convenzioni di denominazione per collegare i diversi componenti. I generi strada colloqui per la GenresController, che rende il modello di generi. Infatti, Ember crea automaticamente un oggetto GenresController se uno non definite. Tuttavia, è possibile ignorare le impostazioni predefinite.

Nella mia app, ho configurato il percorso di generi/film per utilizzare un controller diverso implementando il gancio renderTemplate. In questo modo, diversi itinerari possono condividere lo stesso controller (vedere Figura 18).

Figura 18 itinerari diversi possono condividere lo stesso Controller

App.GenresMoviesRoute = Ember.Route.extend({
  serialize: function (model) {
    return { genre_name: model.get('name') };
  },
  renderTemplate: function () {
    this.render({ controller: 'movies' });
  },
  afterModel: function (genre) {
    var controller = this.controllerFor('movies');
    var store = controller.store;
    return store.findQuery('movie', { genre: genre.get('name') })
    .then(function (data) {
      controller.set('model', data);
  });
  }
});

Una cosa bella di Ember è che si possono fare cose con pochissimo codice. Mia applicazione di esempio è circa 110 righe di JavaScript. Che è più corta rispetto alla versione di Knockout e ottenere gratuitamente la cronologia del browser. D'altra parte, la brace è anche un quadro altamente "supponente". Se non scrivete il codice "via Ember", è molto probabile colpire alcuni blocchi stradali. Quando si sceglie un quadro, si dovrebbe considerare se il set di funzionalità e il design complessivo del quadro corrispondono alle vostre esigenze e stile di codifica.

Ulteriori informazioni

In questo articolo, ho mostrato come framework JavaScript rendono più facile creare centri termali. Lungo la strada, introdotto alcune caratteristiche comuni di queste librerie, tra cui l'associazione dati, routing e il pattern MVC e MVVM. Per ulteriori informazioni sulla creazione di centri termali con ASP.NET a applicazione asp.net/single-pagina.

Mike Wasson è un programmatore Microsoft. Per molti anni ha documentato le API di Win32 multimediali. Attualmente scrive su ASP.NET, concentrandosi su Web API. È possibile contattarlo al mwasson@microsoft.com.

Grazie all'esperto tecnica seguente per la revisione di questo articolo: Xinyang Qiu (Microsoft)
Xinyang Qiu è un senior Software Design Engineer in Test sul team Microsoft ASP.NET e un blogger attivi per blogs.msdn.com/b/webdev. Lui è felice di rispondere alle domande di ASP.NET esperti diretti per rispondere alle vostre domande. Contattarlo al xinqiu@microsoft.com.