Il presente articolo è stato tradotto automaticamente.

MVVM

Utilizzo delle funzionalità di Windows 8 con MVVM

Brent Edwards

Scarica il codice di esempio

Windows 8 porta molte nuove funzionalità che gli sviluppatori possono sfruttare per creare applicazioni interessanti e un ricco UX.Purtroppo, queste funzionalità non sono sempre molto unit test amichevole.Tali caratteristiche come piastrelle condivisione e secondari possono rendere il vostro app più interattiva e divertente, ma anche meno verificabile.

In questo articolo, prenderò in esame diversi modi per consentire un'applicazione utilizzare funzioni quali condivisione, impostazioni, piastrelle secondari, le impostazioni dell'applicazione e application storage.Utilizzando il pattern Model-View-ViewModel (MVVM), dependency injection e qualche astrazione, ti mostrerò come sfruttare queste caratteristiche mantenendo il presentazione strato unit test amichevole.

Circa l'applicazione di esempio

Per illustrare i concetti che sarò parlando in questo articolo, ho utilizzato MVVM per scrivere un campione Windows Store app che consente a un utente vista blog post dal feed RSS di suo blog preferiti.L'app illustra come:

  • Condividere informazioni su un post del blog con altre applicazioni tramite il fascino di quota
  • Cambiare quale Blog l'utente vuole leggere con il fascino delle impostazioni
  • Appuntare un post sul blog preferiti alla schermata iniziale per la lettura più tardi con piastrelle secondari
  • Salva blog preferiti per visualizzare in tutti i dispositivi con le impostazioni di roaming

Oltre l'applicazione di esempio, ho preso la funzionalità specifica di Windows 8 io sarò parlando in questo articolo e astratto in una libreria open source chiamata Charmed.Streghe può essere utilizzato come libreria helper o solo come riferimento.L'obiettivo di streghe è di essere una libreria di supporto MVVM cross-platform per Windows 8 e Windows Phone 8.Io sarò parlando più del Windows Phone 8 lato della libreria in un articolo futuro.Controllare lo stato di avanzamento della biblioteca di streghe bit.ly/17AzFxW.

Il mio obiettivo con questo articolo e il codice di esempio è quello di dimostrare il mio approccio alle applicazioni testabili con MVVM, utilizzando alcune delle nuove caratteristiche che offre Windows 8.

Panoramica MVVM

Prima di tuffarsi nel codice e funzionalità specifiche di Windows 8, mi prendo un rapido sguardo a MVVM.MVVM è un modello di progettazione che ha guadagnato la popolarità enorme negli ultimi anni per le tecnologie basate su XAML, come ad esempio Windows Presentation Foundation (WPF), Silverlight, Windows Phone 7, Windows Phone 8 e Windows 8 (Windows Runtime o WinRT).MVVM rompe l'architettura di un'applicazione in tre livelli logici: Modello, modello di visualizzazione e vista, come mostrato Figura 1.

The Three Logical Layers of Model-­View-ViewModelFigura 1 i tre livelli logici del modello -­View-ViewModel

Il modello strato è dove vive la logica aziendale dell'applicazione — business objects, convalida dei dati, accesso ai dati e così via.In realtà, il modello strato è tipicamente suddiviso in più strati e possibilmente anche più livelli.Come Figura 1 illustrato, il modello strato è il logico inferiore, o Fondazione, dell'applicazione.

Il livello del modello di visualizzazione contiene la logica di presentazione delle applicazioni­cazione, che include i dati da visualizzare, le proprietà per attivare gli elementi dell'interfaccia utente o renderle visibili e metodi che interagiranno con il modello e i livelli di visualizzazione.Fondamentalmente, il livello del modello di visualizzazione è una rappresentazione di visualizzazione indipendente dello stato corrente dell'interfaccia utente.Dico "vista-agnostic" perché semplicemente fornisce dati e metodi per la visualizzazione di interagire con, ma esso non detta come la vista sarà rappresentano i dati o consentire all'utente di interagire con tali metodi.Come Figura 1illustrato, il livello del modello di vista logicamente si siede tra il modello strato e lo strato di View e può interagire con entrambi.Il livello del modello di visualizzazione contiene codice che sarebbe in precedenza nel codebehind di livello della vista.

Lo strato di View contiene l'effettiva presentazione della domanda.Per le applicazioni basate su XAML, come quelli per il Runtime di Windows, lo strato di View consiste principalmente, se non interamente, di XAML.Lo strato di View sfrutta il potente motore di associazione dati XAML da associare alle proprietà sul modello di visualizzazione, applicando un look-and-feel ai dati che altrimenti non avrebbero alcuna rappresentazione visiva.Come Figura 1 illustrato, lo strato di View è il logico superiore dell'applicazione.Lo strato di View interagisce direttamente con il livello del modello di visualizzazione, ma non è a conoscenza del livello del modello.

Lo scopo principale del pattern MVVM è quello di separare la presentazione di un'applicazione dalla sua funzionalità.In questo modo rende l'applicazione più favorevole allo unit test perché la funzionalità ora vive in Plain Old CLR Objects (POCOs), piuttosto che nelle viste che hanno i propri cicli di vita.

Contracts

Windows 8 introduce il concetto di contratti, che sono accordi tra due o più applicazioni nel sistema di un utente.Questi contratti forniscono coerenza tra tutte le applicazioni, consentendo agli sviluppatori di sfruttare le funzionalità da qualsiasi app che li supporta.Un'app può dichiarare quali contratti di supporti nel file Package.appxmanifest, come mostrato Figura 2.

Contracts in the Package.appxmanifest FileFigura 2 contratti nel File Package.appxmanifest

Mentre è facoltativo per contratti di supporto, è generalmente una buona idea.Ci sono tre contratti in particolare che un'applicazione deve supportare — Condividi, impostazioni e ricerca — perché sono sempre disponibili tramite il menu di fascino, mostrato Figura 3.

The Charms MenuFigura 3 il Menu Charms

Mi concentrerò su due tipi di contratto: Condivisione e impostazioni.

Condivisione

Il contratto di quota permette un'app condividere i dati specifici di un contesto con altre applicazioni sul sistema dell'utente.Ci sono due parti del contratto di quota: l'origine e la destinazione.La fonte è l'applicazione che sta facendo la condivisione.Esso fornisce alcuni dati di essere condivisi, in qualsiasi formato è necessario.L'obiettivo è l'applicazione che riceve i dati condivisi.Perché il fascino di condivisione è sempre disponibile all'utente tramite il menu di fascino, voglio che l'applicazione di esempio per essere una fonte di condivisione, perlomeno.Non ogni app ha bisogno di essere una destinazione quota perché non ogni app ha un bisogno di accettare input da altre fonti.Tuttavia, c'è una probabilità abbastanza buona che qualsiasi dato app avrà almeno una cosa che vale la pena di condivisione con altre applicazioni.Così, una maggioranza di apps sarà probabile trovare esso utile per essere una fonte di condivisione.

Quando l'utente preme il fascino di quota, un oggetto chiamato il Broker condividere inizia il processo di prendere i dati di che un app condivide (se presente) e inviarlo alla destinazione quota come specificato dall'utente.C'è un oggetto chiamato DataTransferManager che posso usare per condividere dati durante tale processo.Il DataTransferManager ha un evento chiamato DataRequested, che viene generato quando l'utente preme il fascino di quota.Il codice riportato di seguito viene illustrato come ottenere un riferimento alla DataTransferManager e iscriversi all'evento DataRequested:

public void Initialize()
{
  this.DataTransferManager = DataTransferManager.GetForCurrentView();
  this.DataTransferManager.DataRequested += 
    this.DataTransferManager_DataRequested;
}
private void DataTransferManager_DataRequested(
  DataTransferManager sender, DataRequestedEventArgs args)
{
  // Do stuff ...
}

DataTransferManager.GetForCurrentView la chiamata restituisce un riferimento al DataTransferManager attivo per la visualizzazione corrente. Mentre è possibile mettere questo codice in un modello di visualizzazione, crea una dipendenza difficile su DataTransferManager, una classe sealed non può essere deriso negli unit test. Perché voglio davvero la mia app per rimanere come verificabile come possibile, questo non è ideale. Una soluzione migliore è quello di astrarre l'interazione DataTransferManager fuori in una classe helper e definire un'interfaccia per la classe helper implementare.

Prima di sottrarre questa interazione, devo decidere quali parti veramente importante. Ci sono tre parti di interazione con il DataTransferManager mi preoccupo:

  1. La sottoscrizione all'evento DataRequested quando viene attivato il mio punto di vista.
  2. Annullamento dell'iscrizione dall'evento DataRequested quando la mia vista è disattivato.
  3. Essere in grado di aggiungere i dati condivisi DataPackage.

Con quei tre punti in mente, si materializza la mia interfaccia:

public interface IShareManager
{
  void Initialize();
  void Cleanup();
  Action<DataPackage> OnShareRequested { get; set; }
}

Initialize dovrebbe ottenere un riferimento alla DataTransferManager e sottoscrivere l'evento DataRequested. Pulitura deve disdire l'evento DataRequested. OnShareRequested è dove posso definire quale metodo viene chiamato quando è stato generato l'evento DataRequested. Ora posso implementare la IShareManager, come mostrato Figura 4.

Figura 4 implementazione IShareManager

public sealed class ShareManager : IShareManager
{
  private DataTransferManager DataTransferManager { get; set; }
  public void Initialize()
  {
    this.DataTransferManager = DataTransferManager.GetForCurrentView();
    this.DataTransferManager.DataRequested +=
      this.DataTransferManager_DataRequested;
  }
  public void Cleanup()
  {
    this.DataTransferManager.DataRequested -=
      this.DataTransferManager_DataRequested;
  }
  private void DataTransferManager_DataRequested(
    DataTransferManager sender, DataRequestedEventArgs args)
  {
    if (this.OnShareRequested != null)
    {
      this.OnShareRequested(args.Request.Data);
    }
  }
  public Action<DataPackage> OnShareRequested { get; set; }
}

Quando viene generato l'evento DataRequested, l'evento args che vengono attraverso contengono un DataPackage. Quello DataPackage è dove i dati condivisi effettivi devono essere inserito, motivo per cui l'azione per OnShareRequested prende un DataPackage come parametro. Con il mio IShareManager interfaccia definita e ShareManager attuazione, ora sono pronto per includere la condivisione nel mio modello di visualizzazione, senza sacrificare la testabilità unità per la quale io sto puntando.

Una volta ho usato il mio contenitore di Inversion of Control (IoC) di scelta per inserire un'istanza di IShareManager nel mio modello di visualizzazione, posso mettere a utilizzare, come mostrato Figura 5.

Figura 5 cablaggio fino IShareManager

public FeedItemViewModel(IShareManager shareManager)
{
  this.shareManager = shareManager;
}
public override void LoadState(
  FeedItem navigationParameter, Dictionary<string, 
  object> pageState)
{
  this.shareManager.Initialize();
  this.shareManager.OnShareRequested = ShareRequested;
}
public override void SaveState(Dictionary<string, 
  object> pageState)
{
  this.shareManager.Cleanup();
}

LoadState viene chiamato quando il modello di pagina e di visualizzazione sono attivati e SaveState viene chiamato quando il modello di pagina e la vista sono disattivate. Ora che il ShareManager è tutto impostato su e pronto a gestire la condivisione, hanno bisogno di implementare il metodo ShareRequested che verrà chiamato quando l'utente avvia condivisione. Voglio condividere alcune informazioni su un particolare post (FeedItem), come mostrato Figura 6.

Figura 6 popolamento DataPackage su ShareRequested

private void ShareRequested(DataPackage dataPackage)
{
  // Set as many data types as possible.
dataPackage.Properties.Title = this.FeedItem.Title;
  // Add a Uri.
dataPackage.SetUri(this.FeedItem.Link);
  // Add a text-only version.
var text = string.Format(
    "Check this out!
{0} ({1})", 
    this.FeedItem.Title, this.FeedItem.Link);
  dataPackage.SetText(text);
  // Add an HTML version.
var htmlBuilder = new StringBuilder();
  htmlBuilder.AppendFormat("<p>Check this out!</p>", 
    this.FeedItem.Author);
  htmlBuilder.AppendFormat(
    "<p><a href='{0}'>{1}</a></p>", 
    this.FeedItem.Link, this.FeedItem.Title);
  var html = HtmlFormatHelper.CreateHtmlFormat(htmlBuilder.ToString());
  dataPackage.SetHtmlFormat(html);
}

Ho scelto di condividere diversi tipi di dati diversi. Questa è generalmente una buona idea perché non si dispone di alcun controllo su ciò che un utente ha il suo sistema di apps o quali dati tipi di tali applicazioni il supporto. È importante ricordare che la condivisione è essenzialmente uno scenario fire-and-forget. Non avete idea cosa app l'utente sceglie di condividere e quali app che farà con i dati condivisi. Per condividere con il più ampio pubblico possibile, fornire un titolo, un URI, una versione solo testo e una versione HTML.

Le impostazioni

Il contratto di impostazioni permette all'utente di modificare le impostazioni specifiche del contesto in un'app. Queste possono essere delle impostazioni che possono influenzare l'applicazione come un intero, o solo specifici elementi che si riferiscono al contesto corrente. Gli utenti di Windows 8 saranno diventato condizionati a usando il fascino di impostazioni per apportare modifiche alle app, e voglio che l'applicazione di esempio per sostenerlo perché è sempre disponibile all'utente tramite il menu di fascino. Infatti, se un app dichiara la capacità di Internet tramite il file Package.appxmanifest, deve implementare il contratto impostazioni fornendo un link ad una politica sulla privacy basata sul Web da qualche parte nel menu impostazioni. Perché applicazioni utilizzando Visual Studio 2012 modelli automaticamente dichiarano destra capacità Internet fuori dalla scatola, questo è qualcosa che non dovrebbe essere trascurato.

Quando un utente preme il fascino di impostazioni, il sistema operativo inizia costruendo dinamicamente dal menu che verrà visualizzato. Il menu e le icone a comparsa associate sono controllate dal sistema operativo. Non riesco a controllare il menu e le icone a comparsa aspetto, ma posso aggiungere opzioni di menu. Un oggetto chiamato SettingsPane me verrai avvisato quando l'utente seleziona il fascino delle impostazioni tramite l'evento CommandsRequested. Ottenere un riferimento alla SettingsPane e la sottoscrizione all'evento CommandsRequested è abbastanza semplice:

public void Initialize()
{
  this.SettingsPane = SettingsPane.GetForCurrentView();
  this.SettingsPane.CommandsRequested += 
    SettingsPane_CommandsRequested;
}
private void SettingsPane_CommandsRequested(
  SettingsPane sender, 
  SettingsPaneCommandsRequestedEventArgs args)
{
  // Do stuff ...
}

La cattura con questo è un altro duro dipendenza. Questa volta la dipendenza è SettingsPane, che è un'altra classe che non può essere deriso. Perché voglio essere in grado di unit test il modello di visualizzazione che utilizza SettingsPane, ho bisogno di astrarre i riferimenti a esso, proprio come ho fatto per i riferimenti a DataTransferManager. Come si scopre, le mie interazioni con SettingsPane sono molto simili alle mie interazioni con DataTransferManager:

  1. La sottoscrizione all'evento CommandsRequested per la visualizzazione corrente.
  2. Annullamento dell'iscrizione dall'evento CommandsRequested per la visualizzazione corrente.
  3. Aggiunta di oggetti my own SettingsCommand quando viene generato l'evento.

Così, l'interfaccia è necessario astrarre assomiglia molto al IShare­interfaccia Manager:

public interface ISettingsManager
{
  void Initialize();
  void Cleanup();
  Action<IList<SettingsCommand>> OnSettingsRequested { get; set; }
}

Initialize dovrebbe ottenere un riferimento alla SettingsPane e sottoscrivere l'evento CommandsRequested. Pulitura deve disdire l'evento CommandsRequested. OnSettingsRequested è dove posso definire quale metodo viene chiamato quando è stato generato l'evento CommandsRequested. Ora è possibile implementare ISettings­Manager, come illustrato nel Figura 7.

Figura 7 implementazione ISettingsManager

public sealed class SettingsManager : ISettingsManager
{
  private SettingsPane SettingsPane { get; set; }
  public void Initialize()
  {
    this.SettingsPane = SettingsPane.GetForCurrentView();
    this.SettingsPane.CommandsRequested += 
      SettingsPane_CommandsRequested;
  }
  public void Cleanup()
  {
    this.SettingsPane.CommandsRequested -= 
      SettingsPane_CommandsRequested;
  }
  private void SettingsPane_CommandsRequested(
    SettingsPane sender, SettingsPaneCommandsRequestedEventArgs args)
  {
    if (this.OnSettingsRequested != null)
    {
      this.OnSettingsRequested(args.Request.ApplicationCommands);
    }
  }
  public Action<IList<SettingsCommand>> OnSettingsRequested { get; set; }
}

Quando viene generato l'evento CommandsRequested, l'evento args alla fine mi danno accesso all'elenco di oggetti SettingsCommand che rappresentano le opzioni del menu impostazioni. Per aggiungere le mie opzioni del menu impostazioni, ho solo bisogno di aggiungere un'istanza di SettingsCommand a tale elenco. Un oggetto SettingsCommand non chiede molto, solo un identificatore univoco, il testo dell'etichetta e codice da eseguire quando l'utente seleziona l'opzione.

Io uso il mio contenitore IoC per inserire un'istanza di ISettingsManager per il mio modello di visualizzazione, quindi impostarlo per inizializzare e pulire, come indicato nel Figura 8.

Figura 8 cablaggio fino ISettingsManager

public ShellViewModel(ISettingsManager settingsManager)
{
  this.settingsManager = settingsManager;
}
public void Initialize()
{
  this.settingsManager.Initialize();
  this.settingsManager.OnSettingsRequested = 
    OnSettingsRequested;
}
public void Cleanup()
{
  this.settingsManager.Cleanup();
}

Sarò utilizzando le impostazioni per consentire agli utenti di cambiare che RSS feed che possono visualizzare con l'applicazione di esempio. Questo è qualcosa che voglio l'utente sia in grado di cambiare da ovunque in app, così ho incluso il ShellViewModel, che viene creata un'istanza quando si avvia l'applicazione. Se volevo che RSS feed per essere cambiato solo da una delle altre viste, includerei il codice impostazioni nel modello di visualizzazione associate.

Funzionalità incorporate per creare e mantenere un riquadro a comparsa per impostazioni è carente nel Runtime di Windows. C'è un sacco di codifica manuale più necessaria che ci dovrebbe essere per ottenere funzionalità che ha dovuto per essere coerente in tutte le applicazioni. Per fortuna, non sono l'unico che si sente in questo modo. Tim Heuer, program manager del team Microsoft XAML, ha creato un quadro eccellente chiamato Callisto, che aiuta con questo punto di dolore. Callisto è disponibile su GitHub (bit.ly/Kijr1S) e su NuGet (bit.ly/112ehch). Io lo uso nell'applicazione di esempio e mi raccomando e check-out.

Perché ho il SettingsManager tutti cablate fino nel mio modello di visualizzazione, ho solo bisogno di fornire il codice da eseguire quando le impostazioni sono richiesti, come mostrato Figura 9.

Figura 9 mostrando il SettingsView su SettingsRequested con Callisto

private void OnSettingsRequested(IList<SettingsCommand> commands)
{
  SettingsCommand settingsCommand =
    new SettingsCommand("FeedsSetting", "Feeds", (x) =>
  {
    SettingsFlyout settings = new Callisto.Controls.SettingsFlyout();
    settings.FlyoutWidth =
      Callisto.Controls.SettingsFlyout.SettingsFlyoutWidth.Wide;
    settings.HeaderText = "Feeds";
    var view = new SettingsView();
    settings.Content = view;
    settings.HorizontalContentAlignment = 
      HorizontalAlignment.Stretch;
    settings.VerticalContentAlignment = 
      VerticalAlignment.Stretch;
    settings.IsOpen = true;
  });
  commands.Add(settingsCommand);
}

Creare un nuovo SettingsCommand, dando l'id "FeedsSetting" e il testo dell'etichetta "Feed". La lambda che uso per il callback, che viene chiamato quando l'utente seleziona la voce di menu "Feed", sfrutta il controllo SettingsFlyout di Callisto. Il controllo di SettingsFlyout fa il sollevamento di carichi pesanti di dove mettere le icone a comparsa, come ampia per farlo e quando aprire e chiudere. Tutto quello che devo fare è dirlo se desidera la versione larga o stretta, dargli qualche testo di intestazione e il contenuto, quindi impostare IsOpen su true per aprirlo. Raccomando anche impostando la proprietà HorizontalContentAlignment e il VerticalContent­allineamento al tratto. In caso contrario, il contenuto non corrisponde alla dimensione della SettingsFlyout.

Messaggio Bus

Un punto importante quando si tratta con il contratto di impostazioni è che qualsiasi modifica alle impostazioni è dovrebbe essere applicato a riflette immediatamente in app. Ci sono un certo numero di modi che è possibile realizzare trasmettendo le modifiche alle impostazioni effettuate dall'utente. Io preferisco usare il metodo è un bus di messaggi (anche conosciuto come un aggregatore di evento). Un bus di messaggi è un sistema di pubblicazione di messaggi app-wide. Il concetto di autobus il messaggio non è costruito al Runtime di Windows, che significa che devo creare uno oppure utilizzare uno da un altro quadro. Ho incluso un'implementazione di autobus del messaggio che ho usato su diversi progetti con il quadro di streghe. È possibile trovare l'origine a bit.ly/12EBHrb. Ci sono parecchie altre implementazioni buone pure. Caliburn. micro ha la EventAggregator e MVVM Light ha il Messenger. Tutte le implementazioni in genere seguono lo stesso modello, fornendo un modo per sottoscrivere, disdire e pubblicare messaggi.

Utilizzando il bus dei messaggi streghe nello scenario delle impostazioni, configurare il mio MainViewModel (quella che consente di visualizzare i feed) per sottoscrivere un FeedsChangedMessage:

this.messageBus.Subscribe<FeedsChangedMessage>((message) =>
  {
    LoadFeedData();
  });

Una volta che MainViewModel è impostato per ascoltare le modifiche per il feed, configurare SettingsViewModel per pubblicare il FeedsChanged­messaggio quando l'utente aggiunge o rimuove un feed RSS:

this.messageBus.Publish<FeedsChangedMessage>(new FeedsChangedMessage());

Ogni volta che è coinvolto un autobus di messaggio, è importante che ogni parte dell'app utilizza la stessa istanza di autobus del messaggio. Così, ho fatto in modo di configurare il mio contenitore IoC per dare un'istanza singleton per ogni richiesta per risolvere un IMessageBus.

Ora l'applicazione di esempio è impostato per consentire all'utente di apportare modifiche per i feed RSS visualizzati tramite il fascino di impostazioni e aggiornare la vista principale per riflettere questi cambiamenti.

Le impostazioni di roaming

Un'altra cosa interessante che introdotto Windows 8 è il concetto di impostazioni di roaming. Le impostazioni di roaming consentono agli sviluppatori di app per la transizione di piccole quantità di dati tra tutti i dispositivi dell'utente. Questi dati devono essere meno di 100KB e devono essere limitati a bit di informazioni necessarie per creare un UX persistente, personalizzabili in tutti i dispositivi. Nel caso l'applicazione di esempio, voglio essere in grado di mantenere il feed RSS che l'utente vuole leggere in tutti i suoi dispositivi.

Il contratto di impostazioni ho parlato all'inizio in genere va mano nella mano con le impostazioni di roaming. Ha senso solo che le personalizzazioni che consentono all'utente di fare utilizzando le impostazioni contratto essere mantenuta tra i dispositivi con le impostazioni di roaming.

L'accesso alle impostazioni di roaming, come le altre questioni che ho guardato finora, è piuttosto semplice. La classe ApplicationData dà accesso a entrambi LocalSettings e RoamingSettings. Mettere qualcosa in RoamingSettings è semplice come fornendo una chiave e un oggetto:

ApplicationData.Current.RoamingSettings.Values[key] = value;

Mentre ApplicationData è facile da lavorare, è un'altra classe sealed non può essere deriso negli unit test. Così, nell'interesse di mantenendo il mio modelli vista come verificabile come posso, ho bisogno di astrarre l'interazione con ApplicationData. Prima di definire un'interfaccia per astrarre le funzionalità di roaming impostazioni dietro, ho bisogno di decidere che cosa voglio fare con esso:

  1. Vedere se esiste una chiave.
  2. Aggiungere o aggiornare un'impostazione.
  3. Rimuovere un'impostazione.
  4. Ottenere un'impostazione.

Ora ho quello che ho bisogno di creare un'interfaccia che chiamerò ISettings:

public interface ISettings
{
  void AddOrUpdate(string key, object value);
  bool TryGetValue<T>(string key, out T value);
  bool Remove(string key);
  bool ContainsKey(string key);
}

Con la mia interfaccia definita, ho bisogno di implementare, come Figura 10 illustrato.

Figura 10 implementazione ISettings

public sealed class Settings : ISettings
{
  public void AddOrUpdate(string key, object value)
  {
    ApplicationData.Current.RoamingSettings.Values[key] = value;
  }
  public bool TryGetValue<T>(string key, out T value)
  {
    var result = false;
    if (ApplicationData.Current.RoamingSettings.Values.ContainsKey(key))
    {
      value = (T)ApplicationData.Current.RoamingSettings.Values[key];
      result = true;
    }
    else
    {
      value = default(T);
    }
    return result;
  }
  public bool Remove(string key)
  {
    return ApplicationData.Current.RoamingSettings.Values.Remove(key);
  }
  public bool ContainsKey(string key)
  {
    return ApplicationData.Current.RoamingSettings.Values.ContainsKey(key);
  }
}

TryGetValue prima controlla se una determinata chiave esiste e assegnare il valore per il parametro out se lo fa. Piuttosto che generare un'eccezione se la chiave non viene trovata, restituisce un Boolean che indica se la chiave è stata trovata. Il resto dei metodi sono abbastanza auto-esplicativo.

Ora posso lasciare che il mio contenitore IoC risolvere ISettings e dare alla mia SettingsViewModel. Una volta che lo faccio, il modello di visualizzazione utilizzerà le impostazioni per caricare il feed dell'utente per essere modificato, come mostrato Figura 11.

Figura 11 caricamento e il salvataggio feed dell'utente

public SettingsViewModel(
  ISettings settings,
  IMessageBus messageBus)
{
  this.settings = settings;
  this.messageBus = messageBus;
  this.Feeds = new ObservableCollection<string>();
  string[] feedData;
  if (this.settings.TryGetValue<string[]>(Constants.FeedsKey, out feedData))
  {
    foreach (var feed in feedData)
    {
      this.Feeds.Add(feed);
    }
  }
}
public void AddFeed()
{
  this.Feeds.Add(this.NewFeed);
  this.NewFeed = string.Empty;
  SaveFeeds();
}
public void RemoveFeed(string feed)
{
  this.Feeds.Remove(feed);
  SaveFeeds();
}
private void SaveFeeds()
{
  this.settings.AddOrUpdate(Constants.FeedsKey, this.Feeds.ToArray());
  this.messageBus.Publish<FeedsChangedMessage>(new FeedsChangedMessage());
}

Una cosa da notare circa il codice in Figura 11 è che i dati effettivamente salvare nelle impostazioni sono una matrice di stringhe. Poiché le impostazioni di roaming sono limitate a 100KB, ho bisogno di mantenere le cose semplici e bastone di tipi primitivi.

Piastrelle secondari

Lo sviluppo di applicazioni che coinvolgere gli utenti può essere abbastanza una sfida. Ma come si fanno a mantenere utenti provenienti indietro una volta che si installa l'app? Una cosa che può aiutare con questa sfida è secondari piastrelle. Una tegola secondaria fornisce la possibilità di collegamento profondo in un'applicazione, lasciando l'utente ignorare il resto delle app e andare dritto a ciò che egli si preoccupa di più. Una tegola secondaria ottiene inchiodata alla schermata home dell'utente, con un'icona di tua scelta. Una volta sfruttato, la piastrella secondaria lancia il vostro app con argomenti che raccontano l'app esattamente dove andare e cosa caricare. Che fornisce funzionalità di tile secondaria agli utenti è un buon modo per farli personalizzare la loro esperienza, facendo loro voglia di tornare per ulteriori informazioni.

Piastrelle secondarie sono più complicati di altri argomenti che coprono in questo articolo, perché ci sono diverse cose che devono essere implementati prima la piena esperienza di utilizzo di piastrelle secondari funzionerà correttamente.

Appuntare una tegola secondaria comporta un'istanza della classe SecondaryTile. Il SecondaryTile prende di Costruttore diversi parametri che aiutano lo determinano ciò che la piastrella sarà simile, tra cui un nome, un URI per il file di immagine del logo da utilizzare per la piastrella e string argomenti che verranno assegnati al app quando la piastrella è premuta. Una volta che la SecondaryTile è stata creata un'istanza, io devo chiamare un metodo che in definitiva Visualizza una piccola finestra pop-up l'utente chiedendo il permesso al pin la piastrella, come mostrato Figura 12.

SecondaryTile Requesting Permission to Pin a Tile to the Start Screen
Figura 12 SecondaryTile richiede l'autorizzazione al Pin una tegola alla schermata iniziale

Una volta che l'utente ha premuto Pin to Start, la prima metà del lavoro è fatto. La seconda metà è configurare l'app effettivamente sostenere il deep linking utilizzando gli argomenti della piastrella fornisce quando viene premuto. Prima di entrare nella seconda metà, permettetemi di parlare di come potrai realizzare il primo semestre in modo verificabile.

Perché SecondaryTile utilizza i metodi che interagiscono direttamente con il sistema operativo — che, a sua volta, Mostra componenti UI — non posso usarlo direttamente dal mio modelli vista senza sacrificare la testabilità. Così, io sarò astratto fuori un'altra interfaccia, che chiamerò ISecondaryPinner (dovrebbe permettere me pin e sbloccare una tegola e verificare se una tegola sia già stata bloccata):

public interface ISecondaryPinner
{
  Task<bool> Pin(FrameworkElement anchorElement,
    Placement requestPlacement, TileInfo tileInfo);
  Task<bool> Unpin(FrameworkElement anchorElement,
    Placement requestPlacement, string tileId);
  bool IsPinned(string tileId);
}

Si noti che entrambi i Pin e Rimuovi ritorno attività <bool>. Ecco perché la SecondaryTile utilizza le attività asincrone per richiedere all'utente di bloccare o sbloccare una tegola. Significa anche che i miei metodi ISecondaryPinner Pin e Rimuovi possono essere atteso.

Si noti inoltre che le sia Pin e Rimuovi prendere un oggetto FrameworkElement e un valore di enumerazione di posizionamento come parametri. Il motivo è che il SecondaryTile ha bisogno di un rettangolo e un posizionamento per dirgli dove mettere la richiesta pin pop-up. Ho intenzione di avere la mia implementazione di SecondaryPinner calcolare quel rettangolo basato su FrameworkElement che è passato in.

Infine, creare una classe helper, TileInfo, da passare intorno i parametri obbligatori e facoltativi utilizzati da SecondaryTile, come mostrato Figura 13.

Figura 13 la classe TileInfo Helper

public sealed class TileInfo
{
  public TileInfo(
    string tileId,
    string shortName,
    string displayName,
    TileOptions tileOptions,
    Uri logoUri,
    string arguments = null)
  {
    this.TileId = tileId;
    this.ShortName = shortName;
    this.DisplayName = displayName;
    this.Arguments = arguments;
    this.TileOptions = tileOptions;
    this.LogoUri = logoUri;
    this.Arguments = arguments;
  }
  public TileInfo(
    string tileId,
    string shortName,
    string displayName,
    TileOptions tileOptions,
    Uri logoUri,
    Uri wideLogoUri,
    string arguments = null)
  {
    this.TileId = tileId;
    this.ShortName = shortName;
    this.DisplayName = displayName;
    this.Arguments = arguments;
    this.TileOptions = tileOptions;
    this.LogoUri = logoUri;
    this.WideLogoUri = wideLogoUri;
    this.Arguments = arguments;
  }
  public string TileId { get; set; }
  public string ShortName { get; set; }
  public string DisplayName { get; set; }
  public string Arguments { get; set; }
  public TileOptions TileOptions { get; set; }
  public Uri LogoUri { get; set; }
  public Uri WideLogoUri { get; set; }
}

TileInfo dispone di due costruttori che possono essere utilizzati, a seconda dei dati. Ora, implementare ISecondaryPinner, come mostrato Figura 14.

Figura 14 attuazione ISecondaryPinner

public sealed class SecondaryPinner : ISecondaryPinner
{
  public async Task<bool> Pin(
    FrameworkElement anchorElement,
    Placement requestPlacement,
    TileInfo tileInfo)
  {
    if (anchorElement == null)
    {
      throw new ArgumentNullException("anchorElement");
    }
    if (tileInfo == null)
    {
      throw new ArgumentNullException("tileInfo");
    }
    var isPinned = false;
    if (!SecondaryTile.Exists(tileInfo.TileId))
    {
      var secondaryTile = new SecondaryTile(
        tileInfo.TileId,
        tileInfo.ShortName,
        tileInfo.DisplayName,
        tileInfo.Arguments,
        tileInfo.TileOptions,
        tileInfo.LogoUri);
      if (tileInfo.WideLogoUri != null)
      {
        secondaryTile.WideLogo = tileInfo.WideLogoUri;
      }
      isPinned = await secondaryTile.RequestCreateForSelectionAsync(
        GetElementRect(anchorElement), requestPlacement);
    }
    return isPinned;
  }
  public async Task<bool> Unpin(
    FrameworkElement anchorElement,
    Placement requestPlacement,
    string tileId)
  {
    var wasUnpinned = false;
    if (SecondaryTile.Exists(tileId))
    {
      var secondaryTile = new SecondaryTile(tileId);
      wasUnpinned = await secondaryTile.RequestDeleteForSelectionAsync(
        GetElementRect(anchorElement), requestPlacement);
    }
    return wasUnpinned;
  }
  public bool IsPinned(string tileId)
  {
    return SecondaryTile.Exists(tileId);
  }
  private static Rect GetElementRect(FrameworkElement element)
  {
    GeneralTransform buttonTransform =
      element.TransformToVisual(null);
    Point point = buttonTransform.TransformPoint(new Point());
    return new Rect(point, new Size(
      element.ActualWidth, element.ActualHeight));
  }
}

Assicurati pin prima piastrella richiesta non esiste già, quindi richiederà all'utente di pin. Sbloccare la volontà prima assicurati che la piastrella richiesta esiste, quindi richiederà all'utente di sbloccare lo. Entrambi restituisce un Boolean che indica se il perno o sbloccare era successo.

Ora riesco a iniettare un'istanza di ISecondaryPinner nel mio modello di visualizzazione e metterlo in uso, come mostrato Figura 15.

Figura 15 Pinning e Unpinning con ISecondaryPinner

public FeedItemViewModel(
  IShareManager shareManager,
  ISecondaryPinner secondaryPinner)
{
  this.shareManager = shareManager;
  this.secondaryPinner = secondaryPinner;
}
public async Task Pin(FrameworkElement anchorElement)
{
  var tileInfo = new TileInfo(
    FormatSecondaryTileId(),
    this.FeedItem.Title,
    this.FeedItem.Title,
    TileOptions.ShowNameOnLogo | TileOptions.ShowNameOnWideLogo,
    new Uri("ms-appx:///Assets/Logo.png"),
    new Uri("ms-appx:///Assets/WideLogo.png"),
    this.FeedItem.Id.ToString());
    this.IsFeedItemPinned = await this.secondaryPinner.Pin(
    anchorElement,
    Windows.UI.Popups.Placement.Above,
    tileInfo);
}
public async Task Unpin(FrameworkElement anchorElement)
{
  this.IsFeedItemPinned = !await this.secondaryPinner.Unpin(
    anchorElement,
    Windows.UI.Popups.Placement.Above,
    this.FormatSecondaryTileId());
}

Pin, crea un'istanza di supporto TileInfo, dandogli un id univoco formattato, il titolo feed, Uri per il logo e il logo ampia e l'id di mangime come argomento di lancio. Pin prende il pulsante che è stato scelto come l'elemento di ancoraggio per il percorso della richiesta pin pop-up di base. Io uso il risultato del metodo SecondaryPinner.Pin per determinare se l'elemento feed sia stato bloccato.

In Rimuovi, dò l'id univoco formattato della piastrella, usando l'inverso del risultato per determinare se l'elemento feed è ancora bloccato. Ancora una volta, il pulsante che è stato fatto clic viene passato al Sblocca come l'elemento di ancoraggio per la richiesta di Sblocca popup.

Dopo avere questo posto e lo uso per appuntare un post sul blog (FeedItem) alla schermata iniziale, ho posso toccare la piastrella appena creata per lanciare l'app. Tuttavia, lancerà l'applicazione nello stesso modo come prima, prendendo me alla pagina principale, visualizzare tutti i post del blog. Voglio che mi prendono per il post sul blog specifico che appuntato. Che è dove la seconda metà della funzionalità entra in gioco.

La seconda metà della funzionalità va in app.xaml.cs, da cui viene lanciato l'app, come mostrato Figura 16.

Figura 16 lanciando l'App

protected override async void OnLaunched(LaunchActivatedEventArgs args)
{
  Frame rootFrame = Window.Current.Content as Frame;
  if (rootFrame.Content == null)
  {
    Ioc.Container.Resolve<INavigator>().
NavigateToViewModel<MainViewModel>();
  }
  if (!string.IsNullOrWhiteSpace(args.Arguments))
  {
    var storage = Ioc.Container.Resolve<IStorage>();
    List<FeedItem> pinnedFeedItems =
      await storage.LoadAsync<List<FeedItem>>(Constants.PinnedFeedItemsKey);
    if (pinnedFeedItems != null)
    {
      int id;
      if (int.TryParse(args.Arguments, out id))
      {
        var pinnedFeedItem = pinnedFeedItems.FirstOrDefault(fi => fi.Id == id);
        if (pinnedFeedItem != null)
        {
          Ioc.Container.Resolve<INavigator>().
NavigateToViewModel<FeedItemViewModel>(
            pinnedFeedItem);
        }
      }
    }
  }
  Window.Current.Activate();
}

Aggiungere codice alla fine del metodo sottoposto a override OnLaunched per verificare se gli argomenti sono stati passati durante il lancio. Se gli argomenti sono stati passati, analizzare gli argomenti in un int per essere utilizzati come mangimi id. Io ottenere il feed con quell'id dal mio feed salvati e passarlo a FeedItemViewModel per essere visualizzato. Una cosa da notare è che assicurarsi che l'app è già visualizzata la pagina principale, e a navigare ad esso in primo luogo se non è stato visualizzato. In questo modo l'utente può premere il pulsante ' indietro ' e terra sulla pagina principale o meno l'app era già in esecuzione.

Conclusioni

In questo articolo, ho parlato il mio approccio all'implementazione di un app Store Windows testabile utilizzando il pattern MVVM, sfruttando ancora alcune delle nuove caratteristiche fredde che Windows 8 porta alla tabella. In particolare, ho guardato astrarre le impostazioni, di condivisione, piastrelle secondari e le impostazioni di roaming in classi helper che implementano le interfacce mockable. Utilizzando questa tecnica, sono in grado di unit test come gran parte della mia vista modello funzionalità possibile.

In futuro gli articoli, ti immergermi in più delle specifiche di come posso effettivamente scrivere unit test per questi modelli vista ora che ho impostato li a essere più verificabili. Potrai esplorare anche come queste stesse tecniche possono essere applicate per rendere il mio vista modelli cross-platform con Windows Phone 8, pur mantenendoli testabile.

Con un po' di pianificazione, è possibile creare un'applicazione interessante con un UX innovativo che sfrutta le nuove caratteristiche chiave di Windows 8 — e farlo senza sacrificare le migliori pratiche o unit test.

Brent Edwards è un consulente principale associato per Magenic, una società di sviluppo di applicazioni personalizzate che si concentra sulla catasta di Microsoft e lo sviluppo di applicazioni mobile. È anche cofondatore dell'utente Twin città Windows 8 gruppo in Minneapolis, Minn. Contattarlo al brente@magenic.com.

Grazie all'esperto tecnica seguente per la revisione di questo articolo: Rocky Lhotka (Magenic)
Rockford Lhotka è il CTO di Magenic ed è il creatore del quadro sviluppo .NET CSLA ampiamente utilizzato. Egli è autore di numerosi libri e parla regolarmente alle principali conferenze in giro per il mondo. Rockford è un MVP Microsoft Regional Director. Magenic (www.magenic.com) è una società specializzata nella pianificazione, progettazione, costruzione e mantenimento la maggior parte dei sistemi critici dell'organizzazione. Per ulteriori informazioni vai a www.lhotka.net.