Windows Phone 8.1. Primeras Best Practices con MVVM y GalaSoft.MvvmLight

image_thumb12Muy buenas,

En breve recibiremos la actualización oficial de este nuevo SO. No obstante, hace ya una semana que Windows Phone 8.1 está entre nosotros gracias a la aplicación “Preview for Developers”. Nuevas características y opciones nos tienen entretenidos. Noticias cada día sobre cómo configurarlo o cómo utilizar cada una de sus nuevas opciones. Josue Yeray y otros compañeros ya han posteado a este respecto como podemos ver aquí.

En cuanto al desarrollo, también nos encontramos con novedades:

  • Nuevos Emuladores y otras herramientas. Principalmente “Windows Phone Power Tools.”
  • Tareas de background y triggers: Notificaciones push, geofencing, etc.
  • Convergencia de APIs con Windows Store (aproximadamente un 90%).
  • Aplicaciones Universales para C# y WinJS (HTML 5 y JS).
  • Nuevas APIs para para la edición de vídeo y audio.
  • Etc.

Para no escribir más sobre estos temas, podemos echar un vistazo a este enlace.

Aunque existen compatibilidad total con proyectos Windows Phone 8.0, así como la posibilidad de migrar los proyectos de Windows Phone 8.0 a 8.1 (Silverlight), lo recomendable es comenzar a desarrollar proyectos WinRT basados en los nuevos modelos de aplicación para Windows Phone 8.1 y Windows. Modelos que podemos ver en la siguiente imagen:

image_thumb2

En cuanto al patrón MVVM, como siempre, Microsoft presenta su manera de implementarlo. Sin embargo, si al igual que yo, usas “GalaSoft.MvvmLight”, entonces tendrás que seguir algunas pautas, teniendo en cuenta que por el momento, no tenemos las plantillas que nos automatizan el trabajo.

Los siguientes pasos son los siguientes:

  • Crear un nuevo Proyecto Universal o Windows Phone 8.1 de tipo Hub App desde Visual Studio 2013 Update 2.
  • Añadir las referencias o instalar desde nuget el paquete MVVM Light Libraries only (PCL) 4.3.31.2.
  • Añadir la referencia a la DLL “Behaviors SDK (XAML)”. Es importante incluir ésta para poder hacer uso de triggers (“<i:Interaction.Behaviors>”)  desde el XAML y vincular así cada View con su ViewModel.

image_thumb4

  • Dentro del proyecto Shared, crear la carpeta ViewModel y añadir en su interior las clases ViewModel tal y como ya estábamos acostumbrados. De ésta manera tendremos algo como lo siguiente:

image_thumb10

Nota: Los interfaces son opcionales y, dependerán de hasta que punto queramos testear nuestra aplicación. Sin embargo, la interfaz IViewModelBase si que es necesaria. De la misma manera, como puede verse, la carpeta Common sólo contiene lo que vamos a necesitar además de una nueva clase, PageBase. Ambas, Interfaz y clase se muestran a continuación:

PageBase.cs

1 public class PageBase : Page 2 { 3 private readonly NavigationHelper navigationHelper; 4 private IViewModelBase viewModel; 5 6 public PageBase() : base() 7 { 8 this.NavigationCacheMode = NavigationCacheMode.Required; 9 10 this.navigationHelper = new NavigationHelper(this); 11 this.navigationHelper.LoadState += this.NavigationHelper_LoadState; 12 this.navigationHelper.SaveState += this.NavigationHelper_SaveState; 13 } 14 15 16 /// <summary> 17 /// Gets the <see cref="NavigationHelper"/> associated with this <see cref="Page"/>. 18 /// </summary> 19 public NavigationHelper NavigationHelper 20 { 21 get { return this.navigationHelper; } 22 } 23 24 ///// <summary> 25 ///// Populates the page with content passed during navigation. Any saved state is also 26 ///// provided when recreating a page from a prior session. 27 ///// </summary> 28 ///// <param name="sender"> 29 ///// The source of the event; typically <see cref="NavigationHelper"/> 30 ///// </param> 31 ///// <param name="e">Event data that provides both the navigation parameter passed to 32 ///// <see cref="Frame.Navigate(Type, object)"/> when this page was initially requested and 33 ///// a dictionary of state preserved by this page during an earlier 34 ///// session. The state will be null the first time a page is visited.</param> 35 private async void NavigationHelper_LoadState(object sender, LoadStateEventArgs e) 36 { 37 this.viewModel.Load(); 38 39 //BackgroundAccessStatus status = BackgroundExecutionManager.GetAccessStatus(); 40 //if (status == BackgroundAccessStatus.Denied || status == BackgroundAccessStatus.Unspecified) 41 //{ 42 // await BackgroundExecutionManager.RequestAccessAsync(); 43 //} 44 } 45 46 ///// <summary> 47 ///// Preserves state associated with this page in case the application is suspended or the 48 ///// page is discarded from the navigation cache. Values must conform to the serialization 49 ///// requirements of <see cref="SuspensionManager.SessionState"/>. 50 ///// </summary> 51 ///// <param name="sender">The source of the event; typically <see cref="NavigationHelper"/></param> 52 ///// <param name="e">Event data that provides an empty dictionary to be populated with 53 ///// serializable state.</param> 54 private void NavigationHelper_SaveState(object sender, SaveStateEventArgs e) 55 { 56 // TODO: Save the unique state of the page here. 57 } 58 59 60 #region NavigationHelper registration 61 62 ///// <summary> 63 ///// The methods provided in this section are simply used to allow 64 ///// NavigationHelper to respond to the page's navigation methods. 65 ///// <para> 66 ///// Page specific logic should be placed in event handlers for the 67 ///// <see cref="NavigationHelper.LoadState"/> 68 ///// and <see cref="NavigationHelper.SaveState"/>. 69 ///// The navigation parameter is available in the LoadState method 70 ///// in addition to page state preserved during an earlier session. 71 ///// </para> 72 ///// </summary> 73 ///// <param name="e">Event data that describes how this page was reached.</param> 74 protected override void OnNavigatedTo(NavigationEventArgs e) 75 { 76 base.OnNavigatedTo(e); 77 78 this.viewModel = (IViewModelBase)this.DataContext; 79 80 this.navigationHelper.OnNavigatedTo(e); 81 } 82 83 protected override void OnNavigatedFrom(NavigationEventArgs e) 84 { 85 base.OnNavigatedFrom(e); 86 this.navigationHelper.OnNavigatedFrom(e); 87 } 88 89 #endregion 90 }

IViewModelBase.cs

1 public interface IViewModelBase 2 { 3 void Load(); 4 }

  • La clase PageBase permite la navegación entre páginas (NavigationHelper) que a su vez hace uso de la clase (SuspensionManager) y que van a permitir a nuestra aplicación:
    1. Mayor rapidez en la carga de cada página. Evitaremos incluir en el constructor de cada ViewModel la carga de datos. Ésta pasará a implementarse en el método “Load” indicado por el interface IViewModelBase. Por ello, cada ViewModel que creemos deberá implementar esta interfaz. Puede verse como la clase PageBase, en las líneas 37 y 78 se encargan de llevar a cabo todo esta labor.
    2. Mantener el estado de cada página para que en caso de que la aplicación quede suspendida pueda volver a ser activada en su estado anterior. Recordemos que ahora Windows Phone 8.1 no cierra las aplicaciones al volver hacia atrás (con el botón Back).
  • Modificar la página MainPage.xaml como se muestra a continuación para que pase a hacer uso de la clase PageBase:
1 <common:PageBase 2 x:Class="elGuerre.MainPage" 3 xmlns:common="using:elGuerre.Common" 4 ... 5 > 6 </common:PageBase>

  • Con todos estos cambios, la clase MainPage.xaml.cs quedará totalmente limpia. Simplemente heredando de PageBase.
1 public sealed partial class MainPage : PageBase 2 { 3 public MainPage() 4 { 5 this.InitializeComponent(); 6 } 7 }

 

  • Modificar la página MainPage.xaml nuevamente para hacer uso de los behaviors y vincular los eventos con los eventos RelayCommand de nuestro ViewModel:
1 ... 2 xmlns:i="using:Microsoft.Xaml.Interactivity" 3 xmlns:core="using:Microsoft.Xaml.Interactions.Core" 4 ... 5 <HubSection x:Uid="HubSection1" Header="SECTION 1" > 6 <DataTemplate> 7 <ListView 8 ItemsSource="{Binding Groups}" 9 IsItemClickEnabled="True" 10 ContinuumNavigationTransitionInfo.ExitElementContainer="True" 11 > 12 <ListView.ItemTemplate> 13 ... 14 </ListView.ItemTemplate> 15 16 <i:Interaction.Behaviors> 17 <i:BehaviorCollection> 18 <core:EventTriggerBehavior EventName="ItemClick"> 19 <core:InvokeCommandAction Command="{Binding ClickSectionCommand}" /> 20 <!--<core:CallMethodAction MethodName="Method1" TargetObject="{Binding Mode=OneWay}" />--> 21 </core:EventTriggerBehavior> 22 </i:BehaviorCollection> 23 </i:Interaction.Behaviors> 24 25 </ListView> 26 </DataTemplate> 27 </HubSection> 28 29 ... 30 31 <HubSection x:Uid="HubSection2" Header="SECTION 2" Width="Auto" > 32 <DataTemplate> 33 <GridView 34 ItemsSource="{Binding Groups[0].Items}" 35 AutomationProperties.AutomationId="ItemGridView" 36 AutomationProperties.Name="Items In Group" 37 ItemTemplate="{StaticResource Standard200x180TileItemTemplate}" 38 SelectionMode="None" 39 IsItemClickEnabled="True" 40 ContinuumNavigationTransitionInfo.ExitElementContainer="True"> 41 <GridView.ItemsPanel> 42 <ItemsPanelTemplate> 43 <ItemsWrapGrid /> 44 </ItemsPanelTemplate> 45 </GridView.ItemsPanel> 46 47 <i:Interaction.Behaviors> 48 <i:BehaviorCollection> 49 <core:EventTriggerBehavior EventName="ItemClick"> 50 <core:InvokeCommandAction Command="{Binding ClickItemCommand}" /> 51 </core:EventTriggerBehavior> 52 </i:BehaviorCollection> 53 </i:Interaction.Behaviors> 54 55 </GridView> 56 </DataTemplate> 57 </HubSection>

  • Nota: Se ha eliminado de los controles HubSection el DataContex con objeto de que los behabiors encuentren los RelayCommand en su ViewModel. Por lo que el DataContext es el siguiente teniendo como Source al contenedor de dependencias (SimpleIoC). De igual forma los ItemSource han sido modificados por el mismo motivo.
DataContext="{Binding Main, Mode=OneWay, Source={StaticResource Locator}}"

 

  • Como punto final. La implementación de MainViewModel es la siguiente:
1 public class MainViewModel : ViewModelBase, IMainViewModel 2 { 3 public const string TitlePropertyName = "Title"; 4 5 private string title = string.Empty; 6 private readonly IDataService dataService; 7 private NavigationHelper navHelper; 8 private ObservableCollection<SampleDataGroup> groups; 9 10 /// <summary> 11 /// Initializes a new instance of the MainViewModel class. 12 /// </summary> 13 public MainViewModel(IDataService dataService) 14 { 15 16 this.dataService = dataService; 17 18 // Tiempo de diseño 19 if (this.IsInDesignMode) 20 this.Load(); 21 } 22 23 public async void Load() 24 { 25 var data = await this.dataService.GetData(); 26 this.Groups = new ObservableCollection<SampleDataGroup>(data); 27 } 28 29 public ObservableCollection<SampleDataGroup> Groups 30 { 31 get { return this.groups; } 32 set { Set(() => Groups, ref this.groups, value); } 33 } 34 35 public string Title 36 { 37 get{ return title; } 38 set { Set(() => Title, ref this.title, value); } 39 } 40 41 public void Method1() 42 { 43 44 } 45 46 private RelayCommand<ItemClickEventArgs> clickSectionCommand; 47 public RelayCommand<ItemClickEventArgs> ClickSectionCommand 48 { 49 get 50 { 51 return clickSectionCommand 52 ?? (clickSectionCommand = new RelayCommand<ItemClickEventArgs>( 53 (e) => 54 { 55 var groupId = ((SampleDataGroup)e.ClickedItem).UniqueId; 56 this.navHelper = new NavigationHelper(); 57 this.navHelper.NavigateTo<SectionPage>(groupId); 58 })); 59 } 60 } 61 62 63 private RelayCommand<ItemClickEventArgs> clickItemCommand; 64 public RelayCommand<ItemClickEventArgs> ClickItemCommand 65 { 66 get 67 { 68 return clickItemCommand 69 ?? (clickItemCommand = new RelayCommand<ItemClickEventArgs>( 70 (e) => 71 { 72 var itemId = ((SampleDataItem)e.ClickedItem).UniqueId; 73 this.navHelper = new NavigationHelper(); 74 this.navHelper.NavigateTo<ItemPage>(itemId); 75 })); 76 } 77 } 78 }

  • Modificar las páginas SectionPage e ItemPage siguiendo estos mismo pasos creando igualmente los ViewModel correspondientes.
  • Si hemos optado por una aplicación Universal, sería necesario repetir los pasos únicamente para las páginas .XAML.

Tras estos pasos y algunos comentarios que encontraremos al analizar el código, no nos será nada complicado tener nuestro primer conjunto de buenas prácticas para la implementación de aplicaciones Universales y/o aplicaciones Windows Phone 8.1.  ¡Espero haber conseguido ese objetivo!

Saludos @Home

@JuanluElGuerre