Il presente articolo è stato tradotto automaticamente.

MVVM

Creazione di un livello di presentazione multipiattaforma con MVVM

Brent Edwards

Scaricare il codice di esempio

Con il rilascio di Windows 8 e Windows Phone 8, Microsoft ha preso un grande passo verso un vero sviluppo cross-platform.Entrambi eseguire il kernel stesso ora, che significa con un po ' di pianificazione, gran parte del codice dell'applicazione può essere riutilizzato in entrambi.Sfruttando il pattern Model-View-ViewModel (MVVM), alcuni altri modelli di progettazione comuni e alcuni trucchi, è possibile scrivere uno strato di presentazione multipiattaforma che funziona su entrambi Windows 8 e Windows Phone 8.

In questo articolo, prenderò uno sguardo ad alcune sfide specifiche piattaforme affrontato e parlare di soluzioni che possono essere applicati che permettono la mia app mantenere la netta separazione delle preoccupazioni senza sacrificare la capacità di scrivere buoni unit test per esso.

Circa l'applicazione di esempio

Nel numero di luglio 2013 di MSDN Magazine, presentato il codice da un'applicazione di esempio Windows Store, e l'inizio di un open source, cross-piattaforma quadro sviluppato, chiamato Charmed ("Sfruttando Windows 8 funzioni con MVVM," msdn.microsoft.com/magazine/dn296512).In questo articolo vi mostrerò come ho preso quella applicazione di esempio e il quadro e si è evoluta per essere più cross -­piattaforma.Ho anche sviluppato un compagno Windows Phone 8 app con le stesse funzionalità di base, sfruttando lo stesso quadro.Le applicazioni di esempio e quadro sono disponibili su GitHub a github.com/brentedwards/Charmed.Il codice continuerà a evolversi come mi muovo verso l'ultimo articolo nella mia serie MVVM, che approfondiranno effettivamente testare il livello di presentazione e considerazioni aggiuntive intorno codice testabile.

L'app è un lettore di blog semplice, chiamato lettore incantato.Versione su ogni piattaforma dell'app ha appena abbastanza funzionalità per illustrare alcuni concetti chiave relativi a sviluppo cross-platform.Entrambe le versioni sono simili in UX essi forniscono, ma ancora in forma l'aspetto del loro rispettivo sistema operativo.

Struttura della soluzione

Qualsiasi discussione adeguata di sviluppo cross-platform con Visual Studio 2012 deve cominciare dall'inizio: struttura della soluzione.Mentre Windows 8 e Windows Phone 8 eseguire sul kernel stesso, loro apps ancora compilare in modo diverso e hanno tipi differenti del progetto.Ci sono vari modi per affrontare la creazione di una soluzione con tipi diversi di progetto, ma io preferisco avere una soluzione con tutti i miei progetti specifici della piattaforma inclusi in esso.Figura 1 Mostra la struttura della soluzione per le applicazioni di campione sarà discuteremo.

Cross-Platform Charmed Reader Solution StructureFigura 1 struttura della soluzione multipiattaforma lettore incantato

Visual Studio permette di avere più di un file di progetto fanno riferimento a un file singolo classe fisico, consentendo di aggiungere un file di classe esistente selezionando Aggiungi come Link, come mostrato Figura 2.

Adding an Existing Item with Add As LinkFigura 2 l'aggiunta di un elemento esistente con aggiungere come Link

Sfruttando la funzione di aggiungere come Link­nalità, posso scrivere gran parte del mio codice una volta e utilizzarlo sia per Windows 8 e Windows Phone 8.Tuttavia, non voglio fare questo per ogni file di classe.Come dimostrerò, ci sono situazioni dove ogni piattaforma avrà la propria implementazione.

Mostra differenze

Anche se sarò in grado di riutilizzare gran parte del codice che contiene la mia logica di presentazione, che è scritto in c#, non sarò in grado di riutilizzare il codice effettivo di presentazione, che è scritto in XAML.Questo è perché Windows 8 e Windows Phone 8 sono leggermente diversi gusti di XAML che non gioca abbastanza bene con l'altro per essere intercambiabili.Parte del problema è sintassi, particolarmente le dichiarazioni dello spazio dei nomi, ma la maggior parte di esso è dovuto a diversi controlli, essendo disponibile per piattaforme e lo styling diversi concetti in fase di attuazione.Ad esempio, GridView e ListView utilizza Windows 8 abbastanza pesantemente, ma questi controlli non sono disponibili per Windows Phone 8.Il rovescio, Windows Phone 8 ha il controllo di rotazione e il LongListSelector, che non sono disponibili in Windows 8.

Nonostante questa mancanza di riusabilità del codice XAML, è ancora possibile utilizzare alcune risorse di progettazione in entrambe le piattaforme, particolarmente il design dell'interfaccia utente di Windows Phone.Questo è perché Windows 8 ha il concetto di vista Snap, che ha una larghezza fissa di 320 pixel.Scatto vista utilizza 320 pixel perché mobile designer hanno iniziato a progettare per larghezze di 320 pixel di schermo per anni.Nello sviluppo di piattaforme, questo funziona in mio favore perché non devo venire con un design nuovo di zecca per scattare vista — io solo posso adattare il mio progetto Windows Phone.Naturalmente, devo tenere a mente che ogni piattaforma hanno i propri principi di design unico, così potrei dover variare un po ' per rendere ogni app sentire naturale alla sua piattaforma.

Come Figura 3 illustrato, ho implementato l'interfaccia utente per Windows 8 Snap vista per essere molto simile, ma non abbastanza identico all'interfaccia utente per Windows Phone 8.Naturalmente, io non sono un designer, e Mostra nel mio design dell'interfaccia utente completamente priva di interesse.Ma spero che questo illustra come simile Windows Vista Snap 8 e Windows Phone 8 può essere nei loro progetti di interfaccia utente.

Sample App UI for Windows 8 Snap View (left) and Windows Phone 8 (right)Figura 3 esempio App UI per Windows 8 a scatto (a sinistra) e Windows Phone 8 (a destra)

Differenze di codice

Una delle sfide interessanti ho affrontato come intrapreso cross -­sviluppo di piattaforma con Windows 8 e Windows Phone 8 è che ogni piattaforma gestisce determinati compiti in modo diverso.Per esempio, mentre entrambe le piattaforme seguono uno schema di navigazione basata su URI, variano nei parametri che prendono.Creazione di riquadri secondari è anche diverso.Anche se entrambe le piattaforme per sostenerli, cosa succede su ogni piattaforma quando un riquadro secondario è tappato è fondamentalmente diversa.Ogni piattaforma ha anche il suo modo di trattare con le impostazioni dell'applicazione, e hanno diverse classi per l'interazione con queste impostazioni.

Infine, ci sono caratteristiche di che Windows 8 è che non Windows Phone 8.I concetti principali che sfrutta la mia app di Windows 8 che non supporta Windows Phone 8 sono contratti e il menu di fascino.Questo significa che Windows Phone non supporta il fascino Condividi o impostazioni.

Così, come si fa a trattare con queste differenze fondamentali del codice?Ci sono un certo numero di tecniche che si possono impiegare per farlo.

Direttive del compilatore quando Visual Studio crea tipi di progetto di Windows 8 e Windows Phone 8, definisce automaticamente le direttive del compilatore specifico della piattaforma nelle impostazioni del progetto —­NETFX_CORE per Windows 8 e WINDOWS_PHONE per Windows Phone 8.Sfruttando queste direttive del compilatore, si può dire Visual Studio cosa compilare per ogni piattaforma.Delle tecniche che possono essere impiegati, questo è il più fondamentale, ma è anche la disordinata.Si traduce in codice che è un po' come formaggio svizzero: pieno di buchi.Mentre questo è a volte un male necessario, ci sono tecniche migliori che lavorano in molti casi.

Astrazione questa è la tecnica più pulita che uso per trattare con differenze di piattaforma, e coinvolge l'astrazione della piattaforma -­funzionalità specifiche in un'interfaccia o una classe astratta.Questa tecnica consente di fornire implementazioni specifiche della piattaforma per le interfacce e, allo stesso tempo, un'interfaccia coerenza per l'uso in tutto il codebase.Nei casi dove c'è il codice di supporto può utilizzare ogni implementazione specifica della piattaforma, è possibile implementare una classe astratta con questo codice di supporto comuni, quindi fornire implementazioni specifiche della piattaforma.Questa tecnica richiede che l'interfaccia o una classe abstract siano disponibili in entrambi i progetti, tramite la funzionalità di aggiungere come Link citato in precedenza.

Direttive del compilatore Plus di astrazione la tecnica finale che può essere utilizzata è una combinazione delle due precedenti.È possibile astrarre le differenze di piattaforma in un'interfaccia o una classe astratta, quindi sfruttare le direttive del compilatore nell'effettiva attuazione.Questo è utile per i casi in cui le differenze di piattaforma sono abbastanza lievi che non vale che li separa per ogni tipo di progetto.

In pratica, ho trovato che uso raramente direttive del compilatore in proprio, soprattutto nei miei modelli vista.Io preferisco tenere il mio modelli vista pulito quando possibile.Così, quando le direttive del compilatore sono la soluzione migliore, io solitamente anche scivolare in qualche astrazione per mantenere il formaggio svizzero un po' più nascosto.

Esplorazione

Una delle prime sfide che incontrato nei miei viaggi multipiattaforma era la navigazione.Navigazione non è lo stesso in Windows 8 e Windows Phone 8, ma esso è vicino.Windows 8 ora utilizza la navigazione basata su URI, che ha utilizzato Windows Phone tutti insieme.La differenza sta nel come vengono passati parametri.Windows 8 prende un singolo oggetto come parametro mentre Windows Phone 8 prende come molti parametri come volete, ma attraverso la stringa di query.Poiché Windows Phone utilizza la stringa di query, tutti i parametri devono essere serializzati in una stringa.Come si scopre, Windows 8 non è così diverso al riguardo.

Anche se Windows 8 prende un singolo oggetto come parametro, tale oggetto in qualche modo deve essere serializzato quando un'altra app prende il centro della scena e viene disattivato il mio app.Il sistema operativo prende il percorso facile e chiama ToString tale parametro, che non è tutto ciò che è utile a me quando il mio app ottiene attivato di nuovo.Perché voglio portare queste due piattaforme insieme che quanto più possibile in termini di mio sforzo di sviluppo, ha senso per serializzare il mio parametro come una stringa prima di navigazione, quindi deserializzare dopo spostamento è completano.Posso anche aiutare a facilitare questo processo con l'attuazione del mio navigatore.

Si noti che voglio evitare direttamente riferimento alle viste dai miei modelli vista, quindi voglio la navigazione per essere vista model driven.La mia soluzione è di impiegare una convenzione in cui viste sono collocate in una cartella dello spazio dei nomi viste e modelli di visualizzazione vengono inseriti in una cartella dello spazio dei nomi ViewModels.Inoltre mi assicurerò che mia viste sono denominate {qualcosa} Page e miei modelli vista sono denominati {qualcosa} ViewModel.Con questa convenzione in atto, posso fornire qualche semplice logica per risolvere un'istanza di una vista, basata sul tipo di modello di visualizzazione.

Ora ho bisogno di decidere quali altre funzionalità ho bisogno per la navigazione:

  • Navigazione a vista model driven
  • La possibilità di tornare indietro
  • Per Windows Phone 8, la capacità di rimuovere una voce di stack indietro

I primi due sono semplici.Mi spiego la necessità di rimuovere una voce di stack indietro un po' più tardi, ma questa capacità è incorporata in Windows Phone 8 e non di Windows 8.

Sia Windows 8 e Windows Phone 8 sfruttare le classi per la loro navigazione che non sono facilmente deriso.Perché uno dei miei obiettivi principali con queste applicazioni è quello di tenerli testabile, voglio astratto questo codice dietro un'interfaccia mockable.Pertanto, il mio navigatore impiegherà una combinazione delle direttive del compilatore e astrazione.Questo mi lascia con la seguente interfaccia:

public interface INavigator
{
  bool CanGoBack { get; }
  void GoBack();
  void NavigateToViewModel<TViewModel>(object parameter = null);
#if WINDOWS_PHONE
  void RemoveBackEntry();
#endif // WINDOWS_PHONE
}

Si noti l'utilizzo del #if WINDOWS_PHONE. Questo dice al compilatore compilare il RemoveBackEntry nella definizione dell'interfaccia solo quando è definita la direttiva del compilatore WINDOWS_PHONE, come sarà per progetti Windows Phone 8. Ora ecco la mia implementazione, come mostrato Figura 4.

Figura 4 attuazione INavigator

public sealed class Navigator : INavigator
{
  private readonly ISerializer serializer;
  private readonly IContainer container;
#if WINDOWS_PHONE
  private readonly Microsoft.Phone.Controls.PhoneApplicationFrame frame;
#endif // WINDOWS_PHONE
  public Navigator(
    ISerializer serializer,
    IContainer container
#if WINDOWS_PHONE
    , Microsoft.Phone.Controls.PhoneApplicationFrame frame
#endif // WINDOWS_PHONE
    )
  {
    this.serializer = serializer;
    this.container = container;
#if WINDOWS_PHONE
    this.frame = frame;
#endif // WINDOWS_PHONE
  }
  public void NavigateToViewModel<TViewModel>(object parameter = null)
  {
    var viewType = ResolveViewType<TViewModel>();
#if NETFX_CORE
    var frame = (Frame)Window.Current.Content;
#endif // NETFX_CORE
      if (parameter != null)
                             {
#if WINDOWS_PHONE
      this.frame.Navigate(ResolveViewUri(viewType, parameter));
#else
      frame.Navigate(viewType, this.serializer.Serialize(parameter));
#endif // WINDOWS_PHONE
    }
    else
    {
#if WINDOWS_PHONE
      this.frame.Navigate(ResolveViewUri(viewType));
#else
      frame.Navigate(viewType);
#endif // WINDOWS_PHONE
    }
  }
  public void GoBack()
  {
#if WINDOWS_PHONE
    this.frame.GoBack();
#else
    ((Frame)Window.Current.Content).GoBack();
#endif // WINDOWS_PHONE
  }
  public bool CanGoBack
  {
    get
    {
#if WINDOWS_PHONE
      return this.frame.CanGoBack;
#else
      return ((Frame)Window.Current.Content).CanGoBack;
#endif // WINDOWS_PHONE
    }
  }
  private static Type ResolveViewType<TViewModel>()
  {
    var viewModelType = typeof(TViewModel);
    var viewName = viewModelType.AssemblyQualifiedName.Replace(
      viewModelType.Name,
      viewModelType.Name.Replace("ViewModel", "Page"));
    return Type.GetType(viewName.Replace("Model", string.Empty));
  }
  private Uri ResolveViewUri(Type viewType, object parameter = null)
  {
    var queryString = string.Empty;
    if (parameter != null)
    {
      var serializedParameter = this.serializer.Serialize(parameter);
      queryString = string.Format("?parameter={0}", serializedParameter);
    }
    var match = System.Text.RegularExpressions.Regex.Match(
      viewType.FullName, @"\.Views.*");
    if (match == null || match.Captures.Count == 0)
    {
      throw new ArgumentException("Views must exist in Views namespace.");
    }
    var path = match.Captures[0].Value.Replace('.', '/');
    return new Uri(string.Format("{0}.xaml{1}", path, queryString),
      UriKind.Relative);
  }
#if WINDOWS_PHONE
  public void RemoveBackEntry()
  {
    this.frame.RemoveBackEntry();
  }
#endif // WINDOWS_PHONE
}

Devo evidenziare un paio parti dell'implementazione nel navigatore Figura 4, in particolare l'uso delle direttive del compilatore il WINDOWS_PHONE e il NETFX_CORE. Questo mi permette di mantenere il codice specifico della piattaforma separati all'interno dello stesso file di codice. Voglio anche sottolineare il metodo ResolveViewUri, soprattutto come parametro di stringa di query è definito. Per mantenere le cose così consistente come possibile tra le due piattaforme, sto permettendo un solo parametro da passare. Un parametro sarà poi essere serializzato e passato lungo nella navigazione specifica per la piattaforma. Nel caso di Windows Phone 8, tale parametro viene passato tramite una variabile "parametro" nella stringa di query.

Naturalmente, la mia implementazione del navigatore è piuttosto limitato, particolarmente dovuto l'aspettativa eccessivamente semplice convenzione. Se si utilizza una libreria MVVM, quali frutta, può gestire la navigazione reale per voi in un modo più robusto. Tuttavia, nella propria navigazione, si può ancora desidera applicare questa tecnica di astrazione-plus-direttive del compilatore per appianare le differenze di piattaforma che esistono nelle librerie stessi.

Impostazioni applicazione

Le impostazioni dell'applicazione sono un altro settore dove Windows 8 e Windows Phone 8 divergono. Ogni piattaforma ha la possibilità di salvare le impostazioni dell'applicazione abbastanza facilmente, e le loro implementazioni sono pure abbastanza simile. Differiscono per le classi che utilizzano, ma entrambi utilizzano le classi che non sono facilmente mockable, che romperebbe la testabilità del mio modello di visualizzazione. Così, ancora una volta, sto andando di optare per l'astrazione, compilatore più direttive. Prima devo decidere che cosa la mia interfaccia dovrebbe essere simile. L'interfaccia deve:

  • Aggiungere o aggiornare un'impostazione
  • Cercare di ottenere un'impostazione, senza generare un'eccezione in caso di errore
  • Rimuovere un'impostazione
  • Determinare se un'impostazione esiste per una data chiave

Roba abbastanza semplice, così la mia interfaccia sarà piuttosto semplice:

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

Perché entrambe le piattaforme avranno la stessa funzionalità, non devo perdere tempo con le direttive del compilatore nell'interfaccia, mantenendo le cose bello e pulito per i miei modelli di vista. Figura 5 dimostra la mia implementazione dell'interfaccia ISettings.

Figura 5 implementazione ISettings

public sealed class Settings : ISettings
{
  public void AddOrUpdate(string key, object value)
  {
#if WINDOWS_PHONE
    IsolatedStorageSettings.ApplicationSettings[key] = value;
    IsolatedStorageSettings.ApplicationSettings.Save();
#else
    ApplicationData.Current.RoamingSettings.Values[key] = value;
#endif // WINDOWS_PHONE
  }
  public bool TryGetValue<T>(string key, out T value)
  {
#if WINDOWS_PHONE
    return IsolatedStorageSettings.ApplicationSettings.TryGetValue<T>(
      key, out value);
#else
    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;
#endif // WINDOWS_PHONE
  }
  public bool Remove(string key)
  {
#if WINDOWS_PHONE
    var result = IsolatedStorageSettings.ApplicationSettings.Remove(key);
    IsolatedStorageSettings.ApplicationSettings.Save();
    return result;
#else
    return ApplicationData.Current.RoamingSettings.Values.Remove(key);
#endif // WINDOWS_PHONE
  }
  public bool ContainsKey(string key)
  {
#if WINDOWS_PHONE
    return IsolatedStorageSettings.ApplicationSettings.Contains(key);
#else
    return ApplicationData.Current.RoamingSettings.Values.ContainsKey(key);
#endif // WINDOWS_PHONE
  }
}

L'implementazione mostrato Figura 5 è piuttosto semplice, proprio come l'interfaccia ISettings stessa. Esso illustra come ogni piattaforma è solo leggermente diverso e richiede solo leggermente diverso codice aggiungere, recuperare e rimuovere le impostazioni dell'applicazione. L'unica cosa che vorrei far notare è che la versione di Windows 8 del codice sfrutta le impostazioni di roaming-funzionalità di Windows 8-specifiche che consente un'app memorizzare le impostazioni nella nube, consentendo agli utenti di aprire l'app su un altro dispositivo Windows 8 e vedere le stesse impostazioni applicate.

Riquadri secondari

Come detto in precedenza, sia Windows 8 e Windows Phone 8 supporta la creazione di riquadri secondari — piastrelle che vengono creati a livello di codice e appuntati alla schermata home dell'utente. Riquadri secondari forniscono funzionalità di deep-linking, ovvero l'utente può toccare un riquadro secondario e saltare direttamente a una parte specifica di un'applicazione. Questo risulta particolarmente utile per gli utenti perché essi possono essenzialmente segnalibro parti della tua app e saltare direttamente a loro senza perdere tempo. Nel caso del mio esempio apps, voglio che l'utente sia in grado di segnalibro un singolo post (FeedItem) e vai a destra ad esso dalla schermata iniziale.

La cosa interessante con riquadri secondari è che, mentre li supportano entrambe le piattaforme, il modo li devo implementano per ogni piattaforma è abbastanza differente. Questo è un caso perfetto per esplorare un esempio più complesso di astrazione.

Se letto il mio articolo di luglio 2013, forse ricorderete che ho parlato di come astratto riquadri secondari per lo sviluppo di MVVM con Windows 8. La soluzione presentata funziona perfettamente bene per Windows 8, ma non compila anche per Windows Phone 8. Quello che presento qui è la naturale evoluzione di quella soluzione Windows 8, che funziona bene sia su Windows 8 e Windows Phone 8.

Ecco cosa l'interfaccia sembrava in Windows 8:

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

Come accennato, non compila questa interfaccia per Windows Phone 8. Particolarmente problematica è l'utilizzo di FrameworkElement e l'enumerazione di posizionamento. Nessuno di questi sono gli stessi in Windows Phone 8 come in Windows 8. Il mio obiettivo è quello di modificare questa interfaccia un po', quindi può essere utilizzato da entrambe le piattaforme senza alcun problema. Come potete vedere, il metodo ISecondaryPinner.Pin accetta come parametro un oggetto TileInfo. TileInfo è un oggetto di trasferimento di dati semplici (DTO) ho creato contenente le informazioni pertinenti necessarie per la creazione di un riquadro secondario. La cosa più facile per me fare è spostare i parametri Windows 8 versione bisogni nella classe TileInfo, quindi utilizzare direttive del compilatore per compilarli nella versione di Windows 8 della classe TileInfo. In questo modo cambia la mia interfaccia ISecondaryPinner al seguente:

public interface ISecondaryPinner
{
  Task<bool> Pin(TileInfo tileInfo);
  Task<bool> Unpin(TileInfo tileInfo);
  bool IsPinned(string tileId);
}

Si può vedere che i metodi sono tutti ancora lo stesso, ma i parametri per Pin e sblocca sono cambiati leggermente. Di conseguenza, la classe TileInfo ha anche cambiato da quello che era in precedenza e ora assomiglia a questo:

public sealed class TileInfo
{
  public string TileId { get; set; }
  public string ShortName { get; set; }
  public string DisplayName { get; set; }
  public string Arguments { get; set; }
  public Uri LogoUri { get; set; }
  public Uri WideLogoUri { get; set; }
  public string AppName { get; set; }
  public int?
Count { get; set; }
#if NETFX_CORE
  public Windows.UI.StartScreen.TileOptions TileOptions { get; set; }
  public Windows.UI.Xaml.FrameworkElement AnchorElement { get; set; }
  public Placement RequestPlacement { get; set; }
#endif // NETFX_CORE
}

In realtà, preferisco fornire costruttori per ogni scenario nel quale aiutante si abitua DTOs come questo, al fine di essere più esplicito su quali parametri sono necessari in quale momento. Per ragioni di brevità, ho lasciato i diversi costruttori fuori il frammento di codice TileInfo, ma si può vederli in tutta la loro gloria nel codice di esempio.

TileInfo ora ha tutte le proprietà necessarie da Windows 8 e Windows Phone 8, così la prossima cosa da fare è implementare l'interfaccia ISecondaryPinner. Perché l'attuazione sarà molto diversa per ogni piattaforma, potrai utilizzare la stessa interfaccia in entrambi i tipi di progetto ma fornire implementazioni specifiche della piattaforma in ogni progetto. Questo sarà ridurre l'effetto di formaggio svizzero che le direttive del compilatore avrebbe causato in questo caso. Figura 6 viene illustrata l'implementazione di Windows 8 di ISecondaryPinner, ora con le firme aggiornate.

Figura 6 attuazione ISecondaryPinner per Windows 8

public sealed class Win8SecondaryPinner : ISecondaryPinner
{
  public async Task<bool> Pin(TileInfo tileInfo)
  {
    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(tileInfo.AnchorElement), tileInfo.RequestPlacement);
    }
    return isPinned;
  }
  public async Task<bool> Unpin(TileInfo tileInfo)
  {
    var wasUnpinned = false;
    if (SecondaryTile.Exists(tileInfo.TileId))
    {
      var secondaryTile = new SecondaryTile(tileInfo.TileId);
      wasUnpinned = await secondaryTile.RequestDeleteForSelectionAsync(
        GetElementRect(tileInfo.AnchorElement), tileInfo.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));
  }
}

È importante notare che in Windows 8 non può tranquillamente creare un riquadro secondario a livello di codice senza l'approvazione da parte dell'utente. Questo è diverso da Windows Phone 8, che ti permettono di farlo. Quindi, devo creare un'istanza di SecondaryTile e chiamare RequestCreateForSelectionAsync, che si aprirà una finestra di dialogo nella posizione di fornire, inducendo l'utente ad approvare la creazione (o eliminazione) del riquadro secondario. Il metodo di supporto GetElementRect prende in FrameworkElement — che sarà il pulsante l'utente preme appuntare il riquadro secondario — e quindi calcola il rettangolo da utilizzare per il posizionamento della finestra di dialogo richiesta.

Figura 7 viene illustrata l'implementazione di Windows Phone 8 di ISecondaryPinner.

Figura 7 implementazione ISecondaryPinner per Windows Phone 8

public sealed class WP8SecondaryPinner : ISecondaryPinner
{
  public Task<bool> Pin(TileInfo tileInfo)
  {
    var result = false;
    if (!this.IsPinned(tileInfo.TileId))
    {
      var tileData = new StandardTileData
      {
        Title = tileInfo.DisplayName,
        BackgroundImage = tileInfo.LogoUri,
        Count = tileInfo.Count,
        BackTitle = tileInfo.AppName,
        BackBackgroundImage = new Uri("", UriKind.Relative),
        BackContent = tileInfo.DisplayName
      };
      ShellTile.Create(new Uri(tileInfo.TileId, UriKind.Relative), 
        tileData);
      result = true;
    }
  return Task.FromResult<bool>(result);
  }
  public Task<bool> Unpin(TileInfo tileInfo)
  {
    ShellTile tile = this.FindTile(tileInfo.TileId);
    if (tile != null)
    {
      tile.Delete();
    }
    return Task.FromResult<bool>(true);
  }
  public bool IsPinned(string tileId)
  {
    return FindTile(tileId) != null;
  }
  private ShellTile FindTile(string uri)
  {
    return ShellTile.ActiveTiles.FirstOrDefault(
      tile => tile.NavigationUri.ToString() == uri);
  }
}

Ci sono un paio di cose che vorrei sottolineare nell'implementazione Windows Phone 8. Il primo è come riquadro secondario viene creato utilizzando la classe StandardTileData e il metodo statico ShellTile.Create. La seconda è che l'implementazione di Windows Phone 8 per la creazione di riquadri secondari non è asincrona. Poiché l'implementazione di Windows 8 è asincrona, ho dovuto fare l'interfaccia supportano il pattern di async/attendono. Per fortuna, è molto facile fare ciò che altrimenti sarebbe un supporto non asincrona di metodo async/attendono modello sfruttando il metodo statico, Generico Metodo Task. FromResult. I modelli di visualizzazione che utilizzano l'interfaccia ISecondaryPinner non devono preoccuparsi per il fatto che Windows 8 è intrinsecamente asincrona e Windows Phone 8 non è.

Ora è possibile vedere come Windows 8 (Figura 6) e Windows Phone 8 (Figura 7) variano nella loro implementazione di riquadri secondari. Che non è la fine della storia per riquadri secondari, tuttavia. Ho mostrato solo l'implementazione dell'interfaccia ISecondaryPinner. Perché ogni piattaforma è diversa e deve fornire i valori per diverse proprietà della classe TileInfo, devo anche fornire implementazioni specifiche della piattaforma dei modelli vista che li utilizzano. Per la mia applicazione di esempio, fornisco la capacità di bloccare un singolo post, o FeedItem, quindi il modello di visualizzazione in questione è il FeedItemViewModel.

Alcune funzionalità comuni esiste in Windows 8 e Windows Phone 8, dal punto di vista del modello di visualizzazione. Quando l'utente pin un FeedItem, voglio entrambe le piattaforme per salvare quel FeedItem localmente, quindi può essere ricaricato quando l'utente tocca il riquadro secondario. Il rovescio, quando l'utente rimuove un FeedItem, voglio entrambe le piattaforme per eliminare quel FeedItem da archiviazione locale. Entrambe le piattaforme dovranno implementare questa funzionalità comune, ancora espandere su di esso con implementazioni specifiche della piattaforma per la funzionalità del riquadro secondario. Quindi, ha senso per fornire una classe base che implementa le funzionalità comuni e rendere disponibile tale classe su entrambe le piattaforme. Quindi, ogni piattaforma può ereditare tale classe con classi specifiche della piattaforma che forniscono implementazioni specifiche della piattaforma per appuntare e rimozione riquadri secondari.

Figura 8 Mostra il FeedItemViewModel, che è la classe base per essere ereditato da entrambe le piattaforme. FeedItemViewModel contiene tutta la roba che è comune a entrambe le piattaforme.

Figura 8 classe Base FeedItemViewModel

public abstract class FeedItemViewModel : ViewModelBase<FeedItem>
{
  private readonly IStorage storage;
  protected readonly ISecondaryPinner secondaryPinner;
  public FeedItemViewModel(
    ISerializer serializer,   
    IStorage storage,
    ISecondaryPinner secondaryPinner)
    : base(serializer)
  {
    this.storage = storage;
    this.secondaryPinner = secondaryPinner;
  }
  public override void LoadState(FeedItem navigationParameter,
    Dictionary<string, object> pageState)
  {
    this.FeedItem = navigationParameter;
  }
  protected async Task SavePinnedFeedItem()
  {
    var pinnedFeedItems =
      await this.storage.LoadAsync<List<FeedItem>>(
      Constants.PinnedFeedItemsKey);
    if (pinnedFeedItems == null)
    {
      pinnedFeedItems = new List<FeedItem>();
    }
    pinnedFeedItems.Add(feedItem);
    await this.storage.SaveAsync(Constants.PinnedFeedItemsKey, 
      pinnedFeedItems);
  }
  protected async Task RemovePinnedFeedItem()
  {
    var pinnedFeedItems =
      await this.storage.LoadAsync<List<FeedItem>>(
      Constants.PinnedFeedItemsKey);
    if (pinnedFeedItems != null)
    {
       var pinnedFeedItem = pinnedFeedItems.FirstOrDefault(fi => fi.Id ==
         this.FeedItem.Id);
       if (pinnedFeedItem != null)
       {
         pinnedFeedItems.Remove(pinnedFeedItem);
       }
      await this.storage.SaveAsync(Constants.PinnedFeedItemsKey, 
        pinnedFeedItems);
    }
  }
  private FeedItem feedItem;
  public FeedItem FeedItem
  {
    get { return this.feedItem; }
    set { this.SetProperty(ref this.feedItem, value); }
  }
  private bool isFeedItemPinned;
  public bool IsFeedItemPinned
  {
    get { return this.isFeedItemPinned; }
    set { this.SetProperty(ref this.isFeedItemPinned, value); }
  }
}

Con una classe di base in atto per facilitare FeedItems appuntato salvifica e l'eliminazione, posso passare a implementazioni specifiche della piattaforma. L'attuazione concreta di FeedItemViewModel per Windows 8 e l'utilizzo della classe TileInfo con le proprietà che si preoccupa di Windows 8 è mostrati Figura 9.

Figura 9 l'implementazione concreta di FeedItemViewModel per Windows 8

public sealed class Win8FeedItemViewModel : FeedItemViewModel
{
  private readonly IShareManager shareManager;
  public Win8FeedItemViewModel(
    ISerializer serializer,
    IStorage storage,
    ISecondaryPinner secondaryPinner,
    IShareManager shareManager)
    : base(serializer, storage, secondaryPinner)
  {
  this.shareManager = shareManager;
  }
  public override void LoadState(Models.FeedItem navigationParameter,
    Dictionary<string, object> pageState)
  {
    base.LoadState(navigationParameter, pageState);
    this.IsFeedItemPinned =
      this.secondaryPinner.IsPinned(FormatSecondaryTileId());
  }
  public override void SaveState(Dictionary<string, object> pageState)
  {
    base.SaveState(pageState);
    this.shareManager.Cleanup();
  }
  public async Task Pin(Windows.UI.Xaml.FrameworkElement anchorElement)
  {
    // Pin the feed item, then save it locally to make sure it is still
    // available when they return.
var tileInfo = new TileInfo(
      this.FormatSecondaryTileId(),
      this.FeedItem.Title,
      this.FeedItem.Title,
      Windows.UI.StartScreen.TileOptions.ShowNameOnLogo |
        Windows.UI.StartScreen.TileOptions.ShowNameOnWideLogo,
      new Uri("ms-appx:///Assets/Logo.png"),
      new Uri("ms-appx:///Assets/WideLogo.png"),
      anchorElement,
      Windows.UI.Popups.Placement.Above,
      this.FeedItem.Id.ToString());
    this.IsFeedItemPinned = await this.secondaryPinner.Pin(tileInfo);
    if (this.IsFeedItemPinned)
    {
       await SavePinnedFeedItem();
    }
  }
  public async Task Unpin(Windows.UI.Xaml.FrameworkElement anchorElement)
  {
    // Unpin, then delete the feed item locally.
var tileInfo = new TileInfo(this.FormatSecondaryTileId(), anchorElement,
    Windows.UI.Popups.Placement.Above);
    this.IsFeedItemPinned = !await this.secondaryPinner.Unpin(tileInfo);
    if (!this.IsFeedItemPinned)
    {
      await RemovePinnedFeedItem();
    }
  }
  private string FormatSecondaryTileId()
  {
    return string.Format(Constants.SecondaryIdFormat, this.FeedItem.Id);
  }
}

Figura 10 Mostra l'implementazione concreta di FeedItemViewModel per Windows Phone 8. Utilizzando TileInfo per Windows Phone 8 richiede meno proprietà rispetto per Windows 8.

Figura 10 implementazione concreta di FeedItemViewModel per Windows Phone 8

public sealed class WP8FeedItemViewModel : FeedItemViewModel
{
  public WP8FeedItemViewModel(
    ISerializer serializer,
    IStorage storage,
    ISecondaryPinner secondaryPinner)
    : base(serializer, storage, secondaryPinner)
  {
  }
  public async Task Pin()
  {
    // Pin the feed item, then save it locally to make sure it is still
    // available when they return.
var tileInfo = new TileInfo(
      this.FormatTileIdUrl(),
      this.FeedItem.Title,
      Constants.AppName,
      new Uri("/Assets/ApplicationIcon.png", UriKind.Relative));
    this.IsFeedItemPinned = await this.secondaryPinner.Pin(tileInfo);
    if (this.IsFeedItemPinned)
    {
      await this.SavePinnedFeedItem();
    }
  }
  public async Task Unpin()
  {
    // Unpin, then delete the feed item locally.
var tileInfo = new TileInfo(this.FormatTileIdUrl());
    this.IsFeedItemPinned = !await this.secondaryPinner.Unpin(tileInfo);
    if (!this.IsFeedItemPinned)
    {
      await this.RemovePinnedFeedItem();
    }
  }
  private string FormatTileIdUrl()
  {
    var queryString = string.Format("parameter={0}", FeedItem.Id);
    return string.Format(Constants.SecondaryUriFormat, queryString);
  }
}

Dopo Windows 8 (Figura 9) e Windows Phone 8 (Figura 10) implementazioni per FeedItemViewModel, miei apps sono impostati per bloccare i riquadri secondari alle loro rispettive schermate home. L'unica cosa rimanente per chiudere il ciclo sulla funzionalità è gestire quando l'utente tocca realmente i riquadri secondari bloccati. Il mio obiettivo per entrambe le applicazioni è quello di lanciare l'app direttamente in qualunque post del blog rappresenta il riquadro secondario, ma permettono all'utente di premere il pulsante indietro ed essere portati alla pagina principale di elencare tutti i Blog, piuttosto che indietro dall'app stessa.

Dal punto di vista di Windows 8, nulla cambia da quello che ho parlato in questo articolo di luglio 2013. Figura 11 Mostra il codice di Windows 8 invariato per movimentazione quando viene avviata l'applicazione, che è un frammento del file App.xaml.cs classe.

Figura 11 lanciando l'App da Windows 8

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

È un po' più complicato con Windows Phone 8. In questo caso, il riquadro secondario prende un uri, piuttosto che pochi parametri. Ciò significa che Window Phone 8 può avviare automaticamente il mio app in qualsiasi pagina che voglio, senza passare attraverso un punto centralizzato di lancio in primo luogo, come fa Windows 8. Perché voglio fornire un'esperienza coerente su entrambe le piattaforme, ho creato un punto di lancio centralizzato del mio proprio per Windows Phone 8, che ho chiamato SplashViewModel, mostrato Figura 12. Ho creato il mio progetto di lanciare attraverso di essa, ogni volta che si lancia l'applicazione, tramite un riquadro secondario o non.

Figura 12 SplashViewModel per Windows Phone 8

public sealed class SplashViewModel : ViewModelBase<int?>
{
  private readonly IStorage storage;
  private readonly INavigator navigator;
  public SplashViewModel(
    IStorage storage,
    INavigator navigator,
    ISerializer serializer)
    : base(serializer)
  {
    this.storage = storage;
    this.
navigator = navigator;
  }
  public override async void LoadState(
    int?
navigationParameter, Dictionary<string, object> pageState)
  {
    this.
navigator.NavigateToViewModel<MainViewModel>();
    this.
navigator.RemoveBackEntry();
    if (navigationParameter.HasValue)
    {
      List<FeedItem> pinnedFeedItems =
        await storage.LoadAsync<List<FeedItem>>(Constants.PinnedFeedItemsKey);
      if (pinnedFeedItems != null)
      {
        var pinnedFeedItem =
          pinnedFeedItems.FirstOrDefault(fi => 
            fi.Id == navigationParameter.Value);
        if (pinnedFeedItem != null)
        {
  this.
navigator.NavigateToViewModel<FeedItemViewModel>(pinnedFeedItem);
        }
      }
    }
  }
}

Il codice per SplashViewModel è piuttosto semplice. L'unica cosa che vorrei far notare è che voglio assicurarsi di rimuovere questa pagina da stack indietro, altrimenti sarebbe mantenere gli utenti da poter mai indietro dall'app. Sarebbe costantemente essere inviati nuovamente dentro l'app ogni volta che hanno sostenuto la a questa pagina. Questo è dove la preziosa aggiunta alla INavigator per Windows Phone 8 entra in gioco: RemoveBackEntry. Dopo la navigazione verso il MainViewModel, chiamo RemoveBackEntry per prendere la pagina di splash fuori stack indietro. Ora è una pagina sola usa solo per quando viene lanciato l'app.

Conclusioni

In questo articolo, ho parlato di sviluppo cross-platform con Windows 8 e Windows Phone 8. Ho parlato di ciò che può essere riutilizzato tra piattaforme (disegni e qualche codice) e ciò che non può (XAML). Ho parlato anche di alcune delle sfide affrontate dagli sviluppatori che lavorano su applicazioni multipiattaforma, e presentato diverse soluzioni a queste sfide. La mia speranza è che queste soluzioni possono essere applicate a più appena i casi specifici della navigazione, impostazioni applicazione e riquadri secondari che parlato. Queste soluzioni possono aiutare a mantenere i vostri modelli vista testabile fornendo la capacità di astrarre alcune delle interazioni OS e utilizzare interfacce per rendere queste interazioni mockable.

Nel mio prossimo articolo, guarderò più specificamente l'unità reale test di queste applicazioni cross-platform, ora che sono tutti impostati fino a essere testato. Parlerò più circa perché ho fatto alcune decisioni che ho fatto, come si riferiscono alla sperimentazione, nonché come effettivamente andare a unit test apps.

Avvicinandosi sviluppo cross-platform con l'intento di fornire UXs simili su entrambe le piattaforme e facendo un po' di pianificazione in anticipo, posso scrivere applicazioni che massimizzano il codice riutilizzare e promuovono gli unit test. Posso sfruttare la funzionalità specifiche della piattaforma, ad esempio il menu di Windows 8 Charms, senza sacrificare l'esperienza che ogni piattaforma offre.

Brent Edwards è un consulente principale associato per Magenic, una società di sviluppo di applicazioni personalizzate che si concentra sulla pila di Microsoft e lo sviluppo di applicazioni mobile. Egli è 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: Jason Bock (Magenic)