Nueva aplicación de Skydrive y actualización de Espacio

Tal y como me comentaba mi amigo Danann, recientemente ha sido publicada en Internet una nueva aplicación para Skydrive (la podéis encontrar aquí). La versión para escritorio te crea una carpeta en “Favoritos” donde puedes sincronizar tus carpetas de Skydrive como si fuera una carpeta en PC (aplicación que, por otro lado, ya estaba presente en otras soluciones como Dropbox).

image

Hasta el momento yo utilizaba LiveMesh (parte del paquete Essentials) y, seguiré usándolo porque me parece (al menos de momento, más flexible, aunque menos intuitiva). LiveMesh me permite sincronizar no sólo en Skydrive, también entre diferentes máquinas así cómo acceder por escritorio remoto fácilmente entre ellas.

Bien, en cualquier caso, vamos al motivo de este post. Cuando mi aplicación de Windows Phone se actualizó, entré para echarle un vistazo a los cambios y, desde el menú de configuración pude ver que disponía de 7GB de espacio en Skydrive. ¡¡¿¿7GB, pero no eran 25GB??!!

Es el momento de echarle un vistazo a tu cuenta de Live! y acceder a Skydrive

image

En la parte inferior izquierda podrás ver el menú de administrar almacenamiento

image

Una vez accedas, actualiza gratuitamente tu espacio en cuenta de 7GB a 25GB. Microsoft dice que es por tiempo limitado, asi que no esperes… No vaya a ser.

image

Empezando con Windows Phone 7.1 (y XI) – Un repaso a MVVM y el formulario de Nuevo Lugar

Para terminar de cerrar la funcionalidad básica de la aplicación de “Mis Lugares Favoritos” es necesario proporcionar al usuario la posibilidad de dar de alta un nuevo lugar.

Para ello bastará con hacer un formulario sencillo donde el usuario pueda rellenar los campos mínimos del nuevo lugar (me apunto la posibilidad de sugerir lugares próximos a la posición actual). En gran medida este formulario podrá utilizarse para modificar lugares existentes también.

Antes de lanzarme a construir dicho formulario me gustaría repasar un poco el patrón Modelo Vista Vista-Modelo.

El Patrón Modelo Vista VistaModelo

image

Como se puede ver en la figura superior el patrón es muy parecido al Modelo Vista Presentación.

  1. Vista: Representa lo que el usuario ve en pantalla. Idealmente, el código que contiene es un constructor y una llamada a InitializeComponents. La vista conoce al componente VistaModelo (normalmente a través de su DataContext). Esto se hace, idealmente, a través de una interfaz que evite el acoplamiento. Normalmente la forma en que  la Vista conoce a la VistaModelo es mediante el DataBinding y mediante Comandos. Hemos visto ya algo del DataBinding y, espero, en este blog veremos algo sobre los comandos.
  2. VistaModelo: Envía notificaciones a la Vista. Para evitar acoplamiento y que la VistaModelo sepa nada de la Vista, las notificaciones se envían implementando la interfaz INotifyPropertyChanged o INotifyCollectionChanged. A su vez, conoce el Modelo ya que lo actualiza. En general se encarga de sincronizar el trabajo entre Modelo y Vista, transformando o validando el Modelo antes de proporcionárselo a la Vista.
  3. Modelo: Envía notificaciones a la VistaModelo, de nuevo mediante la interfaz INotifyPropertyChanged o INotifyCollectionChanged. El Modelo no “conoce” a ninguno de los demás componentes. Suele encapsular la lógica de negocio y los datos.

¿Cómo se comunican los componentes?

Por un lado tenemos la forma en que comunicamos que una propiedad o una colección ha cambiado. Para ello disponemos de las interfaces INotifyPropertyChanged e INotifyCollectionChanged.

Por otro lado, es muy común (y es la razón por la que escribo este post) que la vista deba comunicar a la VistaModelo que ha de realizar algún tipo de acción sobre el modelo (lo común es que sea a petición del usuario) como añadir un elemento al modelo, modificar o borrar uno existente, etc… Para ello se hace uso de los Commands.

Commands

Como decía, es normal que el usuario quiera realizar algún tipo de operación sobre el modelo. Para ello se hace uso de Comandos. Los comandos son una abstracción que permite asociar una operación a un control en la Vista. Encapsulan el código relacionado con la operación de forma que se facilita el desacoplamiento de su representación en la vista (un botón, un control para ordenar. Vaya, multitud de posibilidades…).

La VistaModelo puede implementar dichos comandos de varias formas, pero resumiento, expondrá objetos que implementen la interfaz ICommand. De esta forma, la manera en que la Vista interactúa con el comando definido en la VistaModelo puede definirse de manera declarativa, eliminando así la necesidad de escribir código.

Nada de todo esto es un invento de Microsoft. Se trata de, sencillamente, la aplicación de patrones de diseño existentes a una tecnología (Silverlight) de manera que pueda sacársele el mayor partido.

¿Por qué?

Bueno, como ya hemos dicho. La implementación de patrones de diseño no sólo permite aplicar soluciones ya probadas a problemas concretos. En este caso, mediante el desacoplamiento y evitando el solapamiento de responsabilidades dispondremos de un código más fácil de mantener y probar.

¿Qué componentes puedo cambiar y de qué forma afectan a los demás componentes? Bueno, como se deduce del diagrama de componentes, el más delicado es el Mdelo, ya que cambios en el mismo van a obligarme a cambiar la VistaModelo. A su vez, cambios en la VistaModelo, pueden obligarme a cambiar la Vista. Si el “conocimiento” que cada componente tiene sobre otro es a través de una interfaz, entonces evitaré que cambios en la implementación de un componente puedan llegar a afectar a otro.

Al lío.

Bien, todo esta parrafada previa era necesaria para poder realizar la pantalla de creación de nuevo Lugar. La razón es evidente: necesito montar un comando que permita crear un nuevo lugar.

Para ello vamos a modificar la interfaz que implementa mi VistaModelo y vamos a crear una nueva propiedad que devuelva una instancia de ICommand y que nos permita crear un nuevo lugar. Cualquier clase que implemente esta interfaz tiene a su cargo dos responsabilidades:

  1. Definir una función específica (el método ICommand.Execute)
  2. Definir el estado de un componente visual que, normalmente, será el encargado de lanzar dicha función (la propiedad ICommand.CanExecute)

Por ejemplo, un botón que nos permita crear un nuevo lugar estará o no habilitado dependiendo de la propiedad CanExecute y, cuando sea pulsado por el usuario, invocará el método Execute. Para cumplir religiosamente con el patrón MVVM, se implementa también un evento, CanExecuteChanged, que indica si la propiedad CanExecute ha cambiado de estado o no.

Para ello, voy a crear primero un método dentro de mi VistaModelo que me permita crear un nuevo lugar. Este método no será invocado directamente desde la vista, así que no estará presente en la interfaz de mi VistaModelo.

image

Por otro lado, imaginad que creo otro dos o tres comandos (por ejemplo, ActualizarLugar, BorrarLugar y CompartirLugar). Esto significaría crear los correspondientes métodos en la VistaModelo y otras tres clases que implementen ICommand.

La solución está en la red (en docenas de sitios la solución aportada es más o menos la misma) yo no voy a ser menos y voy a montar una clase que implemente ICommand y que, a la vez, me permita delegar fácilmente la lógica en la VistaModelo sin hacer que el número de clases en mi proyecto crezca de forma descontrolada.

   1: public class DelegateCommand : ICommand

   2: {

   3:     private Func<object, bool> _puedeEjecutarAccion;

   4:     private Action<object> _accion;

   5:  

   6:     public DelegateCommand(Action<object> accion)

   7:         : this(accion, null)

   8:     {

   9:     }

  10:  

  11:     public DelegateCommand(Action<object> accion, Func<object, bool> puedeEjecutarAccion)

  12:     {

  13:         if (accion == null)

  14:         {

  15:             throw new ArgumentNullException(Resources.Jdmveira_WindowsPhone_Util.AccionParameterNull);

  16:         }

  17:  

  18:         _accion = accion;

  19:         _puedeEjecutarAccion = puedeEjecutarAccion;

  20:     }

  21:  

  22:     public event EventHandler CanExecuteChanged;

  23:  

  24:     public bool CanExecute(object parameter)

  25:     {

  26:         bool result = false;

  27:  

  28:         if (_puedeEjecutarAccion != null)

  29:             result = _puedeEjecutarAccion(parameter);

  30:  

  31:         return result;

  32:     }

  33:  

  34:     public void Execute(object parameter)

  35:     {

  36:         _accion(parameter);

  37:     }

  38:  

  39:     public void RaiseCanExecuteChanged(EventArgs args)

  40:     {

  41:         if (CanExecuteChanged != null)

  42:             CanExecuteChanged(this, args);

  43:     }

  44: }

Si tengo que ponerle una pega a la clase que podéis ver arriba es que no es muy limpia la forma en que se comunica que la propiedad CanExecute ha cambiado. Como la clase DelegateCommand delega la funcionalidad en métodos externos, no tiene control sobre cuándo ha cambiado el valor de CanExecute así que ha de “confiar” en que la clase externa que lo utiliza se acordará de invocar el evento correspondiente. Mientras intento dar con una forma más limpia y completa tendrá que valer. ¿No?
 
Ahora queda preparar el comando desde la VistaModelo para que pueda ser invocado fácilmente desde la vista (utilizando DataBinding).
   1: public class MainViewModel : IMainViewModel

   2: {

   3:     #region Constantes

   4:  

   5:     private const string PropNameItems = "Items";

   6:     private DataHelper _dataHelper;

   7:     private CollectionViewSource _favoritos;

   8:     private ObservableCollection<MiLugar> _items;

   9:     private ICommand _comandoCrearLugar;

  10:  

  11:     #endregion Constantes

  12:  

  13:     #region Constructores

  14:  

  15:     public MainViewModel()

  16:     {

  17:         // Si estamos en modo diseño no tiene sentido cargar la base de datos

  18:         if (!DesignerProperties.IsInDesignTool)

  19:             _dataHelper = new DataHelper();

  20:  

  21:         _items = new ObservableCollection<MiLugar>();

  22:         _favoritos = new CollectionViewSource();

  23:         _comandoCrearLugar = new DelegateCommand(AccionCrearNuevoLugar, PuedeCrearNuevoLugar);

  24:  

  25:         ActualizarVistas();

  26:     }

  27:  

  28:     public ICommand CrearNuevoLugar    

  29:     {

  30:         get { return _comandoCrearLugar; }

  31:     }

  32:  

  33:     private void AccionCrearNuevoLugar(object nuevo)

  34:     {

  35:         throw new NotImplementedException();

  36:     }

  37:  

  38:     private bool PuedeCrearNuevoLugar(object nuevo)

  39:     {

  40:         throw new NotImplementedException();

  41:     }

  42:  

  43:     ...

Por último, habrá que ir creando una nueva interfaz para que el usuario introduzca los datos del nuevo lugar. Aprovecharé una nueva entrada en el blog para este tema, ya que parece un buen momento para sacar información de localización del usuario (al fin y al cabo podemos asumir que está dando de alta un nuevo lugar cuando se encuentra en el mismo… Probablemente).

Empezando con Windows Phone 7.1 (y X) – Haciendo DataBinding sobre Colecciones

Tres modos de ver Mis Lugares

Como habéis visto, mi intención es que el control Panorama disponga de varias vistas sobre la misma colección de datos (Items).

El siguiente esquema muestra las clases que estoy utilizando en la capa de presentación.

Mis Lugares Favoritos: Clases MVVM
Mis Lugares Favoritos: Clases MVVM

Si le echáis un vistazo a la interfaz que declaro hay varias propiedades públicas que hacen referencia a diferentes filtros que voy a utilizar a la hora de mostrar mi conjunto de datos (Mis Lugares de Interés):

  • Items: Colección observable (ObservableCollection<T>) que contiene los lugares de interés del usuario.
  • Favoritos: Colección observable de aquellos lugares de interés que están marcados como favoritos.
  • Recientes: Colección observable de aquellos lugares de interés que se han creado recientemente.
  • Cercanos: Colección observable de aquellos lugares de interés que se encuentran próximos a la ubicación actual del usuario.

Como podéis ver, se trata de una coleccíon padre y diferentes subconjuntos de dicha colección. Uno podría suponer que algo así no nos debería de provocar demasiados problemas. Pongamos un ejemplo:

 1: public class MainViewModel: IMainViewModel {

 2:

 3:     public MainViewModel() {

 4:         Items = new ObservableCollection<MiLugar> ();

 5:     }

 6:

 7:     /// <summary>

 8:     /// La colección principal de lugares

 9:     /// </summary>

 10:     public ObservableCollection<MiLugar> Items { get; private set; }

 11:

 12:     /// <summary>

 13:     /// Filtra la colección principal por la propiedad Favorito

 14:     /// </summary>

 15:     public ObservableCollection<MiLugar> Favoritos {

 16:         get {

 17:

 18:             var result = from i en Items

 19:                          where i.Favorito == true

 20:                          select i;

 21:

 22:             return new ObservableCollection<MiLugar> (result);

 23:         }

 24:     }

 25:

 26:     /// <summary>

 27:     /// Ordena la colección principal mostrando primero los lugares más recientes

 28:     /// </summary>

 29:     public ObservableCollection<MiLugar> Recientes {

 30:         get {

 31:

 32:             var result = from i en Items

 33:                          orderby i.Creado descending

 34:                          select i;

 35:

 36:             return new ObservableCollection<MiLugar> (result);

 37:         }

 38:     }

 39:

 40:     /// <summary>

 41:     /// Ordena la colección principal mostrando primero los lugares más cercanos

 42:     /// </summary>

 43:     public ObservableCollection<MiLugar> Cercanos {

 44:         get {

 45:

 46:             // TODO: Mmmm... Aquí hay trabajo por hacer para ver cómo averiguamos los más "cercanos". De momento devolveremos todo

 47:             var result = from i en Items

 48:                          select i;

 49:

 50:             return new ObservableCollection<MiLugar> (result);

 51:         }

 52:     }

 53: }

Parece que no va a ser tan fácil

Una colección principal, dos o tres consultas Linq y devolvemos el resultado. ¿Sencillo, no?

Pues me temo que, además, completamente inútil: el código que veis arriba no nos sirve. En cualquier caso, dejad que os cuente alguna cosilla sobre el DataBinding y el ObservableCollection<T>. Cuando la página se carga, la infraestructura de DataBinding se subscribe a cambios en las colecciones. La idea es buena: mi vista expone unas pocas colecciones de datos que implementan el patrón Observable (no confundir con el patrón Observer), cuando cargo mis vistas con datos filtrados, la infraestructura de DataBinding se da cuenta y voilá, muestra automáticamente los datos actualizados. Lástima que, cada vez que realizo un filtrado de datos creo una nueva colección observable. La infraestructura de DataBinding estaba suscrita a los cambios de la anterior, con lo que no será capaz de darse cuenta de que ha habido cambios.

¿Qué puedo hacer que no sea extremadamente ineficiente? (Como vaciar la colección inicial e ir llenándola con los datos filtrados, que provocaría una notificación de cambio para el borrado y una nueva notificación por cada elemento nuevo que insertara)

CollectionViewSource al rescate

Pues existe un tipo que nos puede resultar de mucha ayuda en este aspecto: CollectionViewSource. Esta clase actúa como proxy que muestra vistas de una colección permitiendo Filtros, Ordenaciones y Agrupaciones.

Una forma de hacer uso de este control, muy cómoda todo sea dicho, pero no me termina de convencer desde el punto de vista de patrones de diseño (aunque todo es discutible).

Como habéis podido ver más arriba, mi ModelView expone una propiedad que representa la colección de todos “Mis Lugares”. Exponiendo tan solo esta colección, puedo montar todos los CollectionViewSource que quiera en la vista de manera declarativa. Veamos un ejemplo (he eliminado mucho código para centrarnos en lo importante)

 1: <phone:PhoneApplicationPage

 2:

 3:     xmlns:compMod="clr-namespace:System.ComponentModel;assembly=System.Windows"

 4:

 5:     <phone:PhoneApplicationPage.Resources>

 6:         <CollectionViewSource x:Key="filtroRecientes" Source="{Binding Path=Items}">

 7:             <CollectionViewSource.SortDescriptions>

 8:                 <compMod:SortDescription PropertyName="Modificado" Direction="Descending"></compMod:SortDescription>

 9:             </CollectionViewSource.SortDescriptions>

 10:         </CollectionViewSource>

 11:     </phone:PhoneApplicationPage.Resources>

 12:

 13:         <!--LayoutRoot is the root grid where all page content is placed-->

 14:      <Grid x:Name="LayoutRoot" Background="Transparent">

 15:         <!--Panorama control-->

 16:         <controls:Panorama Title="{Binding Source={StaticResource LocalizationHelper}, Path=LocalizedResources.TituloPanorama}" Foreground="{StaticResource PhoneTextBoxForegroundBrush}" >

 17:             <controls:Panorama.Background>

 18:                 <ImageBrush ImageSource="Resources/images/Panorama Green.png"/>

 19:             </controls:Panorama.Background>

 20:

 21:             <controls:PanoramaItem Header="{Binding Source={StaticResource LocalizationHelper}, Path=LocalizedResources.TituloTodos}" Orientation="Horizontal">

 22:                 <ListBox ItemsSource="{Binding Path=Items}" Foreground="{StaticResource PhoneTextBoxForegroundBrush}" />

 23:             </controls:PanoramaItem>

 24:

 25:             <controls:PanoramaItem Header="{Binding Source={StaticResource LocalizationHelper}, Path=LocalizedResources.TituloRecientes}" Orientation="Horizontal">

 26:                 <ListBox ItemsSource="{Binding Source={StaticResource filtroRecientes}}" Foreground="{StaticResource PhoneTextBoxForegroundBrush}" />

 27:             </controls:PanoramaItem>

 28:         </controls:Panorama>

 29:     </Grid>

 30: </phone:PhoneApplicationPage>

Lo primero de todo, he incluido una referencia al espacio de nombres System.ComponentModel. A continuación, he creado un nuevo recurso que será mi CollectionViewSource. Como podéis ver, dicha vista apunta a la propiedad Items en mi ModelView e incluye la descripción de una ordenación basada en una propiedad de mi Modelo, concretamente la propiedad Modificado que indica la fecha de la última modificación de los datos de un lugar.

 

image
Modelo MiLugar: Representa un Lugar de Interés

A continuación, el ListBox incluido dentro del PanoramaItem que muestra los lugares recientes. Simplemente tiene como fuente de datos el CollectionViewSource que acabamos de crear.

Sin embargo, no es ésta la forma en la que más me gusta hacer uso de este componente. Parece más adecuado exponer el CollectionViewSource como una propiedad más del ModelView.

 1: public class MainViewModel : IMainViewModel

 2: {

 3:     #region Miembros privados

 4:

 5:     private ObservableCollection<MiLugar> _items;

 6:     private CollectionViewSource _favoritos;

 7:     #endregion

 8:

 9:     #region Propiedades Públicas

 10:

 11:     /// <summary>

 12:     /// Una colección de lugares

 13:     /// </summary>

 14:     public ObservableCollection<MiLugar> Items

 15:     {

 16:         get { return _items; }

 17:         private set

 18:         {

 19:             if (_items != value)

 20:             {

 21:                 _items = value;

 22:                 NotifyPropertyChanged(PropNameItems);

 23:

 24:                 ActualizarVistas();

 25:             }

 26:         }

 27:     }

 28:

 29:

 30:     /// <summary>

 31:     /// Un subconjunto de lugares favoritos

 32:     /// </summary>

 33:     public CollectionViewSource Favoritos

 34:     {

 35:         get { return _favoritos; }

 36:     }

 37:     #endregion

 38:

 39:     /// <summary>

 40:     /// Actualiza las diferentes vistas expuestas sobre el conjunto de lugares favoritos.

 41:     /// </summary>

 42:     private void ActualizarVistas()

 43:     {

 44:         Favoritos.Source = Items;

 45:

 46:         Favoritos.View.Filter = new Predicate<object>(FiltroFavoritos);

 47:     }

 48:

 49:     /// <summary>

 50:     /// Establece como filtro para favoritos aquellos lugares marcados con el parámetro Favorito a true

 51:     /// </summary>

 52:     /// <param name="obj">Instancia sobre la que se establece el filtro</param>

 53:     /// <returns>true si el lugar es favorito, false en c.c.</returns>

 54:     private bool FiltroFavoritos(object obj)

 55:     {

 56:         var lugar = obj as MiLugar;

 57:         if (lugar == null)

 58:         {

 59:             return false;

 60:         }

 61:

 62:         return (lugar.Favorito == true);

 63:     }

 64: }

Ahora no hay más que hacer el DataBinding en la Vista XAML para que apunte a la nueva propiedad. ¿Correcto? Bueno, un matiz. la clase CollectionViewSource tiene una propiedad llamada CollectionViewSource.View que es la propiedad que realmente muestra la vista con el filtro.

 1: <controls:PanoramaItem Header="{Binding Source={StaticResource LocalizationHelper}, Path=LocalizedResources.TituloFavoritos}" Orientation="Horizontal" Foreground="{StaticResource PhoneBackgroundBrush}">

 2:     <ListBox ItemsSource="{Binding Path=Favoritos.View}" Foreground="{StaticResource PhoneTextBoxForegroundBrush}" />

 3: </controls:PanoramaItem>

El siguiente paso: añadir un formulario para el alta de lugares y tendremos nuestra primera versión operativa de la aplicación. Algo que poder enseñar y revisar. En el próximo post.