Il presente articolo è stato tradotto automaticamente.

Windows Phone

Rimozione definitiva e lo Zen dell'asincronia per Windows Phone

Ben Day

Scarica il codice di esempio

Hai mai scritto un'applicazione e appena hai finito, che lei non aveva scritto il modo che hai fatto? È quella sensazione che qualcosa non è giusto con l'architettura. Modifiche che dovrebbero essere semplici sentono quasi impossibile, o almeno prendono molto più lunghe di quanto dovrebbero. E poi ci sono i bug. Oh, ci sono bug! Sei un programmatore decente. Gestione scrivere qualcosa con tanti bug?

Suona familiare? Beh, è successo a me quando ho scritto la mia prima applicazione Windows Phone, NPR Listener. NPR Listener parla ai servizi Web di Radio pubblica nazionale (npr.org/api/index.php) per ottenere l'elenco delle storie disponibili per i suoi programmi e quindi consente agli utenti ascoltare quelle storie sui loro dispositivi Windows Phone. Quando ho scritto, stavo facendo un sacco di sviluppo di Silverlight e sono stato molto felice come bene mie conoscenze e competenze portato a Windows Phone. Ho avuto la prima versione, fatta abbastanza velocemente e presentato al processo di certificazione di mercato. L'intero tempo stavo pensando, "Beh, è stato facile." E poi non sono riuscito certificazione. Qui è il caso che non è riuscita:

Passaggio 1: Eseguire l'applicazione.

Passaggio 2: Premere il tasto Start per andare alla pagina principale del tuo telefono.

Passaggio 3: Premere il pulsante indietro per tornare alla tua domanda.

Quando si preme il pulsante indietro, l'applicazione deve riprendere senza errori e, idealmente, dovrebbe mettere l'utente giusto indietro alla schermata dove ha terminato l'applicazione. Nel mio caso, il tester navigato per un programma di National Public Radio (come "All Things Considered"), in una delle storie di corrente e quindi premuto il tasto Start per andare alla schermata iniziale del dispositivo. Quando il tester viene premuto il pulsante indietro per tornare alla mia applicazione, l'applicazione è tornato, ed è stato un festival di NullReferenceExceptions. Non buono.

Ora, ti farò sapere un po ' su come progettare le applicazioni basate su XAML. Per me, è interamente circa il pattern Model-View-ViewModel e io lo scopo per una separazione quasi fanatica tra le pagine XAML e la logica dell'applicazione. Se ci sta per essere qualsiasi codice nella codebehinds (*. opererà) delle mie pagine, c'era meglio essere un motivo estremamente buono. Un sacco di questo è guidato dal mio vicino patologico bisogno di testabilità unità. Unit test sono vitali perché essi aiuto sapete quando l'applicazione viene utilizzata e, soprattutto, lo rendono facile effettuare il refactoring del codice e cambiare come funziona l'applicazione.

Quindi, se io sono così fanatico sugli unit test, perchè ho tutti quei NullReferenceExceptions? Il problema è che ho scritto la mia applicazione Windows Phone come un'applicazione Silverlight. Certo, Windows Phone è Silverlight, ma il ciclo di vita di un'applicazione Silverlight e un app di Windows Phone sono completamente diversi. In Silverlight, l'utente apre l'applicazione, interagisce con esso fino a quando non si è fatto e poi si chiude l'applicazione. In Windows Phone, al contrario, l'utente apre l'applicazione, funziona con esso e rimbalza avanti e indietro, nel sistema operativo o qualsiasi altra applicazione — ogni volta che vuole. Quando lei si allontana l'applicazione, l'applicazione è disattivata, o "definitivamente". Quando l'applicazione è stata definitivamente, è non è più in esecuzione, ma la sua navigazione "stack indietro" — le pagine dell'applicazione nell'ordine in cui essi sono stati visitati, è ancora disponibile sul dispositivo.

Potreste aver notato sul tuo dispositivo Windows Phone che può passare attraverso una serie di applicazioni e quindi premere il pulsante indietro più volte per tornare indietro attraverso le applicazioni in ordine inverso. Stack indietro di navigazione in azione, e ogni volta che si entra in un'altra applicazione, tale applicazione viene riattivato da dati persistenti lapide. Quando l'applicazione sta per essere definitivamente, riceve una notifica dal sistema operativo che sta per essere disattivato e di dovrebbe salvare lo stato di applicazione possono essere riattivato successivamente. Figura 1 mostra alcuni semplici codici per attivare e disattivare l'applicazione in App.xaml.cs.

Figura 1 lapide di semplice implementazione in App.xaml.cs

 

// Code to execute when the application is deactivated (sent to background).
// This code will not execute when the application is closing.
private void Application_Deactivated(object sender, DeactivatedEventArgs e)
{
  // Tombstone your application.
  IDictionary<string, object> stateCollection =
  PhoneApplicationService.Current.State;
  stateCollection.Add("VALUE_1", "the value");
}
// Code to execute when the application is activated
// (brought to foreground).
// This code will not execute when the application is first launched.
private void Application_Activated(object sender, ActivatedEventArgs e)
{
  // Un-tombstone your application.
  IDictionary<string, object> stateCollection =
  PhoneApplicationService.Current.State;
  var value = stateCollection["VALUE_1"];
}

Il mio problema NullReferenceException è stato causato da una totale mancanza di pianificazione — e codifica — per gestire gli eventi di rimozione definitiva. Che, oltre la mia implementazione di ViewModel completa, ricca e complessa, è una ricetta per il disastro. Pensate a cosa succede quando un utente fa clic sul pulsante indietro per reinserire l'applicazione. Che l'utente non fino a fine qualche pagina iniziale ma invece approda all'ultima pagina, che ha visitato nella tua applicazione. Nel caso il tester Windows Phone, quando l'utente riattivato la mia applicazione, entrò l'applicazione nel mezzo e l'interfaccia utente presuppone che il ViewModel era popolato e potrebbe sostenere quello schermo. Perché il ViewModel non risponde agli eventi di lapide, quasi ogni riferimento all'oggetto è null. Oops. Non ho unit test in questo caso, ho fatto? (Kaboom!)

La lezione è che è necessario pianificare le UI e ViewModels per la navigazione in avanti e a ritroso.

Aggiunta di rimozione definitiva dopo il fatto

Figura 2 Mostra la struttura dell'applicazione originale. Al fine di rendere la mia applicazione passare la certificazione, è necessario gestire tale caso di pulsante Start/Back. Ho potuto sia definitiva attuazione del progetto Windows Phone (Benday.Npr.Phone) o potrei vigore mio ViewModel (Benday.Npr.Presentation). Entrambi coinvolti alcuni compromessi architettonici a disagio. Se ho aggiunto la logica per il progetto Benday.Npr.Phone, mio UI sa troppo di come funziona il mio ViewModel. Se ho aggiunto la logica per il progetto di ViewModel, sarebbe necessario aggiungere un riferimento da Benday.Npr.Presentation a Microsoft.Phone.dll per ottenere l'accesso al dizionario di valore per la rimozione (PhoneApplicationService.Current.State) nello spazio dei nomi Microsoft.Phone.Shell. Che vuoi inquinare il mio progetto di ViewModel con dettagli di implementazione inutili e sarebbe una violazione del principio di separazione delle preoccupazioni (SoC).

The Structure of the Application
Figura 2 la struttura dell'applicazione

La mia scelta finale era di inserire la logica nel progetto telefono ma anche creare alcune classi che sanno come serializzare mio ViewModel in una stringa XML che potrei mettere nel dizionario di valore per la rimozione.jj658977 id="tgt69" runat="server" sentenceId="248c916d14faaccfe39762c576f5e8f5" >Questo approccio mi ha permesso di evitare il riferimento del progetto di presentazione a Microsoft.Phone.Shell mentre ancora mi dà il codice pulito che onorato l'unico principio di responsabilità.jj658977 id="tgt70" runat="server" sentenceId="491860692ce08917b2b63f081d248c2d" >Ho chiamato queste classi * ViewModelSerializer.jj658977 id="tgt71" runat="server" sentenceId="b2c9322420147c3676d28cf41f8ee835" >Figura 3 mostra alcune del codice necessario per attivare un'istanza di StoryListViewModel in XML.

Figura 3 codice in StoryListViewModelSerializer.cs di trasformare IStoryListViewModel in XML

 

private void Serialize(IStoryListViewModel fromValue)
{
  var document = XmlUtility.StringToXDocument("<stories />");
  WriteToDocument(document, fromValue);
  // Write the XML to the tombstone dictionary.
  SetStateValue(SERIALIZATION_KEY_STORY_LIST, document.ToString());
}
private void WriteToDocument(System.Xml.Linq.XDocument document,
  IStoryListViewModel storyList)
{
  var root = document.Root;
  root.SetElementValue("Id", storyList.Id);
  root.SetElementValue("Title", storyList.Title);
  root.SetElementValue("UrlToHtml", storyList.UrlToHtml);
  var storySerializer = new StoryViewModelSerializer();
  foreach (var fromValue in storyList.Stories)
  {
    root.Add(storySerializer.SerializeToElement(fromValue));
  }
}

Una volta ho avuto questi scritti di serializzatori, avevo bisogno di aggiungere la logica per App.xaml.cs per innescare questa serializzazione basati sullo schermo attualmente visualizzato (vedere Figura 4).

Figura 4 innescando i serializzatori ViewModel in App.xaml.cs

 

private void Application_Deactivated(object sender, 
  DeactivatedEventArgs e)
{
  ViewModelSerializerBase.ClearState();
  if (IsDisplayingStory() == true)
  {
    new StoryListViewModelSerializer().Serialize();
    new StoryViewModelSerializer().Serialize();
    ViewModelSerializerBase.SetResumeActionToStory();
  }
  else if (IsDisplayingProgram() == true)
  {
    new StoryListViewModelSerializer().Serialize();
    new ProgramViewModelSerializer().Serialize();
    ViewModelSerializerBase.SetResumeActionToProgram();
  }
  else if (IsDisplayingHourlyNews() == true)
  {
    new StoryListViewModelSerializer().Serialize();
    ViewModelSerializerBase.SetResumeActionToHourlyNews();
  }               
}

Alla fine ho ottenuto che funziona e ha ottenuto l'applicazione certificata ma, purtroppo, il codice è stato lento, brutto, fragile e buggy. Che cosa devo aver fatto era progettare il mio ViewModel così aveva meno dello stato che dovevano essere salvati e costruire così la stessa persisterebbe come corse, piuttosto che dover fare un evento di rimozione definitiva gigante alla fine. Come vuoi farlo?

La scuola di Freak di controllo della programmazione asincrona

Quindi, ecco una domanda per voi: Hai tendenze "control freak"? Avete lasciare problemi andare? Scegliete di ignorare la verità evidente e, attraverso la pura forza di volontà, risolvere i problemi in modo da ignora la realtà che è chiaro come il giorno e si guardare direttamente negli occhi? Yup... che è gestione chiamate asincrone nella prima versione di Listener di NPR. In particolare, che è come mi sono avvicinato la rete asincrona nella prima versione dell'applicazione.

In Silverlight, tutte le chiamate di rete devono essere asincrone. Codice avvia una chiamata di rete e restituisce immediatamente. Il risultato (o eccezione) viene consegnata successivamente tramite un callback asincrono. Ciò significa che sempre di rete logica consiste di due pezzi, la chiamata in uscita e il ritorno. Questa struttura ha conseguenze ed è un piccolo sporco segreto in Silverlight che qualsiasi metodo che si basa sui risultati di una chiamata di rete non può restituire un valore e deve restituire void. Questo ha un effetto collaterale: Qualsiasi metodo che chiama un altro metodo che si basa sui risultati di una chiamata di rete dovrà anche restituire void. Come potete immaginare, questo può essere assolutamente brutale per architetture a più livelli, perché le implementazioni tradizionali di livelli design pattern quali il livello di servizio, adattatore e Repository, fanno molto affidano sui valori restituiti dalle chiamate al metodo.

La mia soluzione è una classe denominata ReturnResult <T> (indicato in Figura5), che funge da collante tra il metodo che richiede la chiamata di rete e il metodo che gestisce i risultati della chiamata e fornisce un modo restituire valori utili il vostro codice. Figura 6 illustrato alcuni Repos­itory logica Pattern che effettua una chiamata a un servizio Windows Communication Foundation (WCF) e quindi restituisce un pop­u­calcolata istanza di IPerson. Utilizzando tale codice, è possibile chiamare LoadById (ReturnResult <IPerson>, int) e infine ricevere l'istanza popolata di IPerson quando client_LoadBy­IdCompleted (oggetto, LoadByIdCompleted­EventArgs) chiama uno dei metodi di notifica. In pratica consente di creare codice che è simile a quello che si avrebbe se è possibile utilizzare i valori restituiti. (Per ulteriori informazioni su ReturnResult <T>, vedere bit.ly/Q6dqIv.)

ReturnResult
Figura 5 ReturnResult <T>

Figura 6 utilizzando ReturnResult <T> per avviare una chiamata di rete e restituire un valore da evento completato

 

public void LoadById(ReturnResult<IPerson> callback, int id)
{
  // Create an instance of a WCF service proxy.
  var client = new PersonService.PersonServiceClient();
  // Subscribe to the "completed" event for the service method.
  client.LoadByIdCompleted +=
    new EventHandler<PersonService.LoadByIdCompletedEventArgs>(
      client_LoadByIdCompleted);
  // Call the service method.
  client.LoadByIdAsync(id, callback);
}
void client_LoadByIdCompleted(object sender,
  PersonService.LoadByIdCompletedEventArgs e)
{
  var callback = e.UserState as ReturnResult<IPerson>;
  if (e.Error != null)
  {
    // Pass the WCF exception to the original caller.
    callback.Notify(e.Error);
  }
  else
  {
    PersonService.PersonDto personReturnedByService = e.Result;
    var returnValue = new Person();
    var adapter = new PersonModelToServiceDtoAdapter();
    adapter.Adapt(personReturnedByService, returnValue);
    // Pass the populated model to the original caller.
    callback.Notify(returnValue);   
  }           
}

Quando ho finito di scrivere la prima versione di Listener di NPR, ho rapidamente capito che l'applicazione era lento (o almeno è apparso lento) perché non ho fatto alcuna memorizzazione nella cache. Che cosa veramente necessaria in app era un modo per chiamare un servizio Web di NPR, ottenere un elenco di storie per un dato programma e quindi memorizzare nella cache i dati quindi non devo tornare indietro al servizio, ogni volta che avevo bisogno di disegnare quello schermo. Aggiunta di funzionalità, tuttavia, era abbastanza difficile perché stavo tentando di far finta che non esistano le chiamate asincrone. Fondamentalmente, essendo un maniaco del controllo, e cercando di negare la struttura essenzialmente asincrona della mia domanda, stavo limitando le opzioni. Stavo combattendo la piattaforma e quindi contorcevo mia architettura dell'applicazione.

In un'applicazione sincrona, le cose cominciano ad accadere nell'interfaccia utente e il flusso di controllo passa attraverso gli strati di app, dati di ritorno dello stack viene rimossa. Tutto ciò che accade all'interno di una singola chiamata pila dove è iniziato il lavoro, elaborazione dati e backup dello stack viene restituito il valore. In un'applicazione asincrona, il processo è più simile a quattro chiamate tutti vagamente connesse: l'interfaccia utente richiede che qualcosa succede; alcune elaborazioni possono o non possono accadere; Se accade l'elaborazione e l'interfaccia utente ha sottoscritto l'evento, l'elaborazione notifica interfaccia utente che un'azione completata; e l'interfaccia utente aggiorna il display con i dati dall'azione asincrona.

Posso già foto me lezione alcuni giovani whippersnapper su quanta fatica abbiamo avuto nei giorni prima async e attendono. "Nel mio giorno, abbiamo dovuto gestire la nostra logica di rete asincrona e callback. È stato brutale, e ci è piaciuto! Ora scendere mio prato!" Beh, in verità, noi non piace così. E ' stato brutale.

Ecco un'altra lezione: L'architettura della piattaforma sottostante di combattimento sarà sempre causare problemi.

Riscrivere l'applicazione mediante archiviazione isolata

Ho scritto l'applicazione per Windows Phone 7 e ha fatto solo un aggiornamento minore per Windows Phone 7.1. In un'applicazione in cui tutta la missione è per lo streaming audio, era sempre stata una delusione che gli utenti non potevano ascoltare l'audio durante la navigazione in altre applicazioni. Quando Windows Phone 7.5 è uscito, ho voluto approfittare delle nuove funzionalità di streaming di sfondo. Anche voluto accelerare l'applicazione ed eliminare un sacco di inutili Web service denomina con l'aggiunta di qualche tipo di locali dei dati nella cache. Come iniziato a pensare di implementare queste caratteristiche, però, i limiti e la fragilità della mia implementazione definitiva, ViewModel e async divenne sempre più evidente. Era il momento di correggere i miei errori precedenti e riscrivere completamente l'applicazione.

Avendo imparato la lezione nella versione precedente dell'applicazione, deciso che stavo per iniziare a progettare per "lapide-possibilità" e anche completamente abbracciare la natura asincrona dell'applicazione. Perché ho voluto aggiungere la memorizzazione nella cache di dati locale, ho iniziato a cercare nell'utilizzo dell'archiviazione isolata. Archiviazione isolata è una posizione sul dispositivo, dove la tua applicazione può leggere e scrivere dati. Lavorare con esso è simile a lavorare con il sistema di file in qualsiasi applicazione .NET ordinaria.

Archiviazione isolata per le operazioni di rete semplificata e memorizzazione nella cache

Un enorme vantaggio di spazio di memorizzazione isolato è che queste chiamate, a differenza delle chiamate di rete, non devono essere asincrona. Ciò significa che posso usare un'architettura più convenzionale che si basa sui valori restituiti. Una volta ho pensato che questo fuori, iniziato a pensare come separare le operazioni che devono essere asincrona da quelli che possono essere sincroni. Chiamate di rete devono essere asincrona. Chiamate di archiviazione isolata possono essere sincrone. Cosa succede se scrivo sempre i risultati della rete chiama così all'archiviazione isolata prima di fare qualsiasi tipo di analisi? Questo mi permette di caricare i dati in modo sincrono e mi dà un modo facile ed economico fare la memorizzazione nella cache di dati locale. Archiviazione isolata mi aiuta a risolvere due problemi in una volta.

Iniziato da rielaborazione come fare le chiamate di rete, abbracciando il fatto che sono una serie di passaggi liberamente associati invece solo un grande passo in sincrono. Ad esempio, quando si desidera ottenere un elenco di storie per un dato programma NPR, ecco cosa fare (vedi Figura 7):

  1. Il ViewModel sottoscrive un evento StoryListRefreshed per la StoryRepository.
  2. Il ViewModel chiama StoryRepository per richiedere un aggiornamento dell'elenco per l'attuale programma di storia. Questa chiamata viene completato immediatamente e restituisce void.
  3. Il StoryRepository rilascia una chiamata asincrona della rete a un servizio Web NPR resto per ottenere l'elenco delle storie per il programma.
  4. A un certo punto, il metodo di callback viene attivato e il StoryRepository ora ha accesso ai dati dal servizio. I dati torna dal servizio come XML e, invece di trasformare questo in oggetti popolati che ottenere ritornò il ViewModel, il StoryRepository scrive immediatamente XML all'archiviazione isolata.
  5. La StoryRepository genera un evento StoryListRefreshed.
  6. Il ViewModel riceve l'evento StoryListRefreshed e chiama GetStories per ottenere l'elenco aggiornato delle storie. GetStories legge l'elenco di storia nella cache XML dall'archiviazione isolata, converte in oggetti che il ViewModel deve e restituisce gli oggetti popolati. Questo metodo può restituire oggetti popolati, perché è una chiamata sincrona che legge dall'archiviazione isolata.

Sequence Diagram for Refreshing and Loading the Story List
Figura 7 diagramma di sequenza per rinfrescanti e l'elenco della storia di carico

Il punto importante qui è che il metodo RefreshStories non restituisce alcun dato. Richiede solo l'aggiornamento dei dati nella cache della storia. Il metodo di GetStories accetta attualmente memorizzati nella cache dei dati XML e converte gli oggetti toria. Perché GetStories non deve chiamare tutti i servizi, è estremamente veloce, quindi la schermata di elenco storia popola rapidamente e l'applicazione sembra molto più veloce rispetto alla prima versione. Se non sono presenti dati memorizzati nella cache, GetStories semplicemente restituisce un elenco vuoto di oggetti toria. Ecco l'interfaccia di IStoryRepository:

 

public interface IStoryRepository
{
  event EventHandler<StoryListRefreshedEventArgs> StoryListRefreshed;
  IStoryList GetStories(string programId);
  void RefreshStories(string programId);
    ...
}

Un ulteriore punto di nascondere questa logica dietro un'interfaccia è che rende per codice pulito nel ViewModel e separa lo sforzo di sviluppo dei ViewModels dalla logica di archiviazione e di servizio. Questa separazione rende il codice più facile a unit test e più facile da mantenere.

Archiviazione isolata per la rimozione definitiva continua

Implementazione definitiva nella prima versione dell'applicazione ha preso il ViewModel e li convertito in XML memorizzato nel dizionario di valore del telefono lapide, PhoneApplicationService.Current.State. Piaceva l'idea XML, ma non come che la persistenza di ViewModel era la responsabilità del livello dell'interfaccia utente dell'applicazione telefono, piuttosto che del livello ViewModel stesso. Inoltre non piace che il livello di interfaccia utente aspettato fino a quando l'evento Deactivate lapide per mantenere il mio intero set di ViewModel. Quando l'applicazione è in esecuzione, solo una manciata di valori effettivamente bisogno di essere mantenuta, e cambiare molto gradualmente come l'utente si sposta da schermo a schermo. Perché non scrivere i valori all'archiviazione isolata come l'utente naviga attraverso l'app? In questo modo l'app è sempre pronto a essere disattivato e definitiva non è un grosso problema.

Inoltre, invece di persistenza l'intero stato dell'applicazione, perché non salvare solo il valore attualmente selezionato in ogni pagina? I dati vengono memorizzati nella cache locale così dovrebbe essere già sul dispositivo, e io posso facilmente ricaricare i dati dalla cache senza cambiare la logica dell'applicazione. Questo diminuisce il numero di valori che devono essere mantenuti dalle centinaia nella versione 1 di forse quattro o cinque nella versione 2. Che è molto meno dati di preoccuparsi, e tutto è molto più semplice.

La logica per tutto il codice di persistenza per la lettura e la scrittura per o dall'archiviazione isolata è incapsulata in una serie di oggetti di Repository. Per informazioni relative agli oggetti di storia, ci sarà una classe corrispondente di StoryRepository. Figura 8 illustrato il codice per prendere una storia Id, trasformandolo in un documento XML e il salvataggio in archiviazione isolata.

Figura 8 StoryRepository logica per salvare l'attuale storia Id

 

public void SaveCurrentStoryId(string currentId)
{
  var doc = XmlUtility.StringToXDocument("<info />");
  if (currentId == null)
  {
    currentId = String.Empty;
  }
  else
  {
    currentId = currentId.Trim();
  }
  XmlUtility.SetChildElement(doc.Root, "CurrentStoryId", currentId);
  DataAccessUtility.SaveFile(DataAccessConstants.FilenameStoryInformation, doc);
}

Avvolgendo la logica di persistenza all'interno dell'oggetto Repository mantiene la memorizzazione e recupero separato da qualsiasi logica di ViewModel e nasconde i dettagli di implementazione dalle classi ViewModel. Figura 9 Mostra il codice nella classe StoryListViewModel per salvare la storia corrente Id quando cambia la selezione della storia.

Figura 9 StoryListViewModel salva l'attuale storia Id quando cambia il valore

 

void m_Stories_OnItemSelected(object sender, EventArgs e)
{
  HandleStorySelected();
}
private void HandleStorySelected()
{
  if (Stories.SelectedItem == null)
  {
    CurrentStoryId = null;
    StoryRepositoryInstance.SaveCurrentStoryId(null);
  }
  else
  {
    CurrentStoryId = Stories.SelectedItem.Id;
    StoryRepositoryInstance.SaveCurrentStoryId(CurrentStoryId);
  }
}

E qui è il metodo di carico StoryListViewModel, che inverte il processo quando il StoryListViewModel ha bisogno di ripopolare stessa dal disco:

 

public void Load()
{
  // Get the current story Id.
  CurrentStoryId = StoryRepositoryInstance.GetCurrentStoryId();
  ...
  var stories = StoryRepositoryInstance.GetStories(CurrentProgramId);
  Populate(stories);
}

 

Pianificazione

In questo articolo, ho ho camminato attraverso alcune delle decisioni architettoniche e gli errori che ho fatto nella mia prima applicazione Windows Phone, NPR Listener. Ricordatevi di piano per la rimozione definitiva e di abbracciare — piuttosto che combattere — async nelle applicazioni Windows Phone. Se volete guardare il codice per entrambi la prima e dopo le versioni di NPR ascoltatore, si può scaricare da archive.msdn.microsoft.com/mag201209WP7.

Benjamin Day è un consulente e formatore specializzato in pratiche migliori di sviluppo software utilizzando strumenti di sviluppo Microsoft con un'enfasi su Visual Studio Team Foundation Server, mischia e Windows Azure. Egli è Microsoft Visual Studio ALM MVP, un trainer certificato mischia via Scrum.orge un altoparlante a conferenze quali TechEd, DevTeach e Visual Studio Live! Quando non lo sviluppo di software, il giorno è stato conosciuto per andare in esecuzione e kayak per bilanciare il suo amore per formaggi, salumi e champagne. Egli può essere contattato via benday.com.

Grazie ai seguenti esperti tecnici per la revisione di questo articolo: Jerri Chiu e David Starr