Silverlight MVVM + WCF Ria Services: Una combinación poderosa

enero 18, 2010
By

En este artículo se propone una solución para integrar en el patrón M-V-VM (Model-View-ViewModel) de Silverlight los controles RAD (Rapid Application Development) brindados por los WCF Ria Services y el Silverlight Toolkit, dígase DomainDataSourceDataGrid, DataPager y DataForm. Además se muestran algunas consideraciones para resolver problemas comunes que se encuentran al desarrollar el patrón Maestro-Detalle utilizando el DataGrid/DataForm con los controles ComboBox/ListBox.

Introducción

Existen varios frameworks para Silverlight que facilitan la aplicación del patrón MVVM aunque este patrón por si mismo puede aplicarse directamente gracias a que WPF/Silverlight tiene las bases necesarias creadas (Por ejemplo, propagación del DataContext, Encaminamiento de Eventos y Comandos). Para aprender de forma rápida y sencilla cómo funciona el patrón MVVM sin necesidad de asociarse a un framework en particular, recomiendo la lectura del artículo WPF Apps With The Model-View-ViewModel Design Pattern.

De manera general el patrón MVVM permite la creación de grandes aplicaciones compuestas en Silverlight y como framework de ejemplo pudiera citar Microsoft Composite Application Guidance for WPF and Silverlight (Prism). Pero como vacío (en mi opinión) en este y otros frameworks que he visto puedo nombrar la falta de ViewModels predefinidos que realicen las tareas comunes existentes en aplicaciones de negocios tales como filtrar, ordenar y paginar (Server-Side para el caso de Silverlight). Y digo vacío y no deficiencia porque MVVM es un patrón de diseño, no una herramienta y la implementación de estas funcionalidades comunes es posible realizarla, pero requiere de que el desarrollador implemente el comportamiento para cada ViewModel.

En cambio el WCF Ria Services nos brinda de manera predeterminada un control (DomainDataSource) que implementa toda la plomería de filtrado, búsqueda y paginación de manera predeterminada; solamente nosotros como desarrolladores debemos configurar qué es lo que queremos y dicho control se encarga de realizar el cómo, gestionar la caché y optimizar los pedidos al servidor. El problema de este control es que como todo control RAD está muy ligado a la Vista (XAML View) para como es lógico permitir la configuración del mismo mediante la creación de ligaduras (Bindings) declarativas en el XAML View.  Esta relación entre el DomainDataSource y la Vista viola la regla dorada del patrón MVVM que fuerza a que el Modelo (Model, por ejemplo LINQ to SQL, ADO.NET Data Services o WCF RIA Services) esté separado totalmente de la Vista (View) y su relación sea a través de lo que se conoce como el VewModel (Estrictamente ninguna clase definida en el modelo es accesible desde la vista y viceversa).

Entonces ¿Cómo hacer para unir los beneficios de MVVM y el control DomainDataSource?

La solución

Empecemos por el final mostrando la funcionalidad de la aplicación Silverlight de ejemplo construida utilizando la propuesta que más adelante se presentará (Figura 1).

ejemplo-aplicacion

Figura 1. Aplicación de Prueba. MVVM + WCF Ria Services

El diseño de la base de datos es muy simple. Está conformado por dos Tablas, una llamada Tareas y otra de Categorías. Existe una relación de cascada entre el campo Categoría definido en la tabla Tareas y el identificador de la tabla Categorías.

La aplicación de ejemplo se divide en dos regiones, superior e inferior. La región superior muestra la inclusión del ComboBox tanto el el DataGrid (a la izquierda) como en el DataForm (a la derecha). Como se puede apreciar en la Figura 1, existe soporte para paginación del DataGrid utilizando un control DataPager y la posibilidad de filtrar los elementos por Nombre. Note que además se permiten las operaciones CRUD (Create, Retrieve, Update y Delete) y se obtienen las características de validación automática del DataForm que posteriormente veremos que se configuran por anotación en el ViewModel y no en el Modelo como es usual.

La región inferior, muestra un escenario Maestro-Detalle sencillo donde al cambiar el valor del ComboBox de Categoría pues se actualizan los datos del GridView inferior.

Hay que resaltar (no se ve en la imagen) que los cambios que se realicen en cualquier GridView o DataForm se propagan a los restantes controles en tiempo real gracias a las ligaduras de WPF (Bindings) brindando una experiencia de usuario agradable.

mvvm-view-dissection

Figura 2. Relación entre los ViewModels de la aplicación y la interfaz de usuario.

La interfaz de la aplicación que se muestra en la Figura 1 está dividida en varios ViewModels según su comportamiento. En la Figura 2, se puede apreciar la relación entre las diferentes partes de la interfaz de usuario y los respectivos ViewModels.

El ViewModel principal se llama PrincipalViewModel y es quien controla el comportamiento de la pantalla. El PrincipalViewModel publica dos propiedades llamadas Categorías y Tareas que son de tipo CategoriasCollectionViewModel y TareasCollectionViewViewModel respectivamente. Estas clases son de tipo colección y contienen los ViewModels que mapean los datos directamente (CategoriaViewModel y TareaViewModel).

La idea general de la solución que se propone en este artículo es la creación de dos tipos de ViewModel predeterminados, uno que tenga implementado un comportamiento de colección que que defina las operaciones de filtrado, paginado y ordenación utilizando las interfaces predefinidas en Silverlight para ese fin y otro que defina un comportamiento de Entidad para mapear los datos obtenidos desde el modelo.

Los controles RAD de manejo de datos en Silverlight utilizan las interfaces ICollection, ICollectionView, IPagedCollectionView, IEditableCollectionView, INotifyPropertyChanged e INotifyCollectionChanged en dependencia de la operación que deseen realizar. Por tanto, si se desarrollara un ViewModel que implemente estas interfaces entonces se garantizaría el funcionamiento OOTB (Out-Of-The-Box) de los controles Silverlight a través de ligaduras (Bindings).

Por supuesto, el corazón de esta propuesta es el cómo de la implementación de las interfaces anteriores ya que si se realiza manualmente para cada combinación ViewModel-Model pues sería tan engorroso que no tendría ningún sentido. Es aquí donde entra a jugar el papel del DataSourceView del WCF Ria Services. La clase que define el ViewModel con comportamiento de colección, llamémosla CollectionViewViewModel, implementa las interfaces anteriores pero encapsula un DataSourceView y hace de Wrapper con la propiedad Data del DataSourceView que ya implementa las interfaces de manejo de datos de Silverlight, por lo que no tenemos que implementar nosotros la lógica de las operaciones, simplemente actuamos de Proxy entre el DataSourceView y el consumidor que utiliza una instancia de tipo CollectionViewViewModel.

Solo falta un detalle más. Para no violar la regla de oro del patrón MVVM no podemos devolver directamente las Entidades que nos trae del servidor el DataSourceView (para así garantizar la separación View-Model). Entonces cada Entidad del Modelo devuelta por el DataSourceView hay que encapsularla dentro de un ViewModel especial que le llamaremos DataViewModel. Para ello, se desarrolló una clase especial llamada ModelProxy que se encarga de mantener la relación entre una entidad del Modelo y su ViewModel correspondiente. Esto es necesario para permitir el manejo de relaciones, implementadas por el Modelo WCF Ria Services como  EntityRef<T> y EntitySet<T>. La infraestructura WCF Ria Services rastrea los cambios realizados a las entidades e incorpora un sistema de cache para consultar el servidor solo en caso necesario. El sistema de rastreo diferencia las inserciones de las actualizaciones en un escenario maestro-detalle comparando con su cache interna. Es decir, si a un EntityRef<T> se le asigna una Entidad de tipo T, esta última se agrega al contexto como InsertOnSubmit si el elemento no aparece en la cache de dicho contexto (note que cada vez que se realiza una operación de carga, las entidades se almacenan en cache). Por ende, nuestro ModelProxy tiene que llevar la relación de los elementos en cache para garantizar el correcto funcionamiento del WCF Ria Services. Esto pudiera presuponer un costo elevado de memoria adicional, pero realmente en el DataViewModel no se almacena información; simplemente es un Wrapper del modelo.

MVVM-base

Figura 3. Diagrama de clases de los ViewModels especializados.

En la Figura 3, se muestra las clases que constituyen la propuesta de este artículo. Hay que señalar que para utilizar esta propuesta dentro de un framework MVVM como Prism, habría que sustituir la clase ViewModelBase por aquella que represente el ViewModel Base en el framework seleccionado y teóricamente todo debe funcionar correctamente. En la aplicación de ejemplo no se utiliza ningún framework MVVM.

Diferencias entre CollectionViewModel y CollectionViewViewModel

La clase CollectionViewModel implementa únicamente la interface ICollection y la clase CollectionViewViewModel implementa ICollectionView, IPagedCollectionView, IEditableCollectionView entre otras.

Esta diferenciación es necesaria para poder hacer funcionar dentro de un GridView o FormView, aquellos controles que sincronicen la posición de la fuente de datos con el elemento seleccionado. Un ejemplo es el ComboBox y el ListBox.

IMPORTANTE! Si agregamos un ComboBox dentro de un DataForm y la propiedad ItemSource del ComboBox está asociada a una colección que implemente ICollectionView se obtendrá un comportamiento indeseado que consiste en cambiar al mismo tiempo en todos los registros del DataForm el valor del campo asociado al SelectedItem del ComboBox.

Orquestación MVVM en la aplicación de ejemplo

En esta sección se muestran algunos detalles de implementación/utilización de las clases propuestas en la sección anterior.

Como se muestra en el Listado 1, para definir el ViewModel que identifica una Tarea, simplemente hay que heredar de la clase DataViewModel e implementar las propiedades que se quieren hacer accesibles en la vista. Como se puede apreciar, cada a propiedad se le anota como atributo información sobre el comportamiento que debe realizar la Vista. Por ejemplo el atributo Display determina la etiqueta que muestra el DataForm y el atributo Required informa al validador del DataForm que dicho atributo se requiere por lo que no puede estar en blanco (El DataForm mostrará un mensaje de validación gráfico).

public class TareaViewModel : DataViewModel
{
    [Display(Name = "Descripción")]
    public string Descripcion
    {
        get
        {
            return ((Tarea)Model).Descripcion;
        }
        set
        {
            ((Tarea)Model).Descripcion = value;
        }
    }

    [Display(Name = "Nombre")]
    [Required]
    public string Nombre
    {
        get
        {
            return ((Tarea)Model).Nombre;
        }
        set
        {
            ((Tarea)Model).Nombre = value;
        }
    }

    [Display(Name = "Categoría")]
    [Required]
    public CategoriaViewModel Categoria
    {
        get
        {
            var model = Model as Tarea;
            if (model.Categoria != null)
            {
                return (CategoriaViewModel)MemoizedProxy.GetViewModel(                                  model.Categoria, typeof(CategoriaViewModel));
            }
            return null;
        }
        set
        {
            var model = Model as Tarea;
            model.Categoria = (Categoria)value.GetModel();
        }
    }
}

Listado 1. ViewModel que define una Tarea

La propiedad Categoria, que aparece en el Listado 1 es de tipo CategoriaViewModel que es el ViewModel que mapea la categoría. La clase base DataViewModel se encargará de hacer posible el funcionamiento correcto del EntityRef<T> en el modelo.

Hay que señalar que de la propagación del cambio de valor en las propiedades (INotifyPropertyChanged) se encarga la clase base DataViewModel, por lo que no es necesario tenerlo en cuenta a este nivel.

public class PrincipalViewModel: ViewModelBase
{
    public TareasDomainContext DomainContext {get;set;}

    TareasCollectionViewViewModel _tareas;
    public TareasCollectionViewViewModel Tareas
    {
        get
        {
            return _tareas;
        }
    }

    CategoriasCollectionViewModel _categorias;
    public CategoriasCollectionViewModel Categorias
    {
        get
        {
            return _categorias;
        }
    }

    public PrincipalViewModel()
    {
        DomainContext = new TareasDomainContext();
        MemoizedProxy = new ModelProxy();

        _tareas = new TareasCollectionViewViewModel(DomainContext, MemoizedProxy);
        _categorias = new CategoriasCollectionViewModel(DomainContext, MemoizedProxy);
    }
}

Listado 2. ViewModel principal

En el Listado 2, se muestra el ViewModel principal de la aplicación de ejemplo. Este se vincula declarativamente al DataContext que define la página XAML. Como se puede apreciar a este nivel se define el DomainContext para el trabajo con los WCF Ria Services.

En el Listado 3, se define el ViewModel TareasCollectionViewViewModel que hereda el comportamiento de colección desde CollectionViewViewModel. En este ViewModel se encapsula el objeto DomainDataSource y se define un filtro de ejemplo.

public class TareasCollectionViewViewModel: CollectionViewViewModel
{
    public TareasDomainContext DomainContext {get;set;}
    DomainDataSource ddsTareas;

    public override Type ElementType
    {
        get
        {
            return typeof(TareaViewModel);
        }
    }

    string _filtrarPorNombre;
    public string FiltrarPorNombre
    {
        get
        {
            return _filtrarPorNombre;
        }
        set
        {
            _filtrarPorNombre = value;
            OnPropertyChanged("FiltrarPorNombre");
            if (ddsTareas.CanLoad)
            {
                if (ddsTareas.FilterDescriptors == null)
                    ddsTareas.FilterDescriptors = new System.Windows.Data.FilterDescriptorCollection();
                ddsTareas.FilterDescriptors.Clear();
                ddsTareas.FilterDescriptors.Add(new System.Windows.Data.FilterDescriptor ("Nombre",  System.Windows.Data.FilterOperator.Contains,value));
                ddsTareas.Load();
            }
        }
    }

    public TareasCollectionViewViewModel(TareasDomainContext context,                                          ModelProxy proxy)
    {
        this.MemoizedProxy = proxy;
        this.DomainContext = context;
        ddsTareas = new DomainDataSource();
        ddsTareas.AutoLoad = true;
        ddsTareas.QueryName = "GetTareasQuery";
        ddsTareas.LoadSize = 5;
        ddsTareas.PageSize = 5;
        ddsTareas.DomainContext = context;
        ddsTareas.Load();
        Data = ddsTareas.DataView;
    }
}

Listado 3. ViewModel Colección de Tareas

La relación entre estas clases y la vista (página XAML de Silverlight) se realiza vinculando al DataContext de la página una instancia de PrincipalViewModel y para cada control en el árbol visual de dicha página, aplicando la ligadura correspondiente.

Las ventajas de este acercamiento son simples: Ventajas de MVVM + Ventajas de WCF RIA Services. Espero que les sea de utilidad :)

Para descargar la aplicación de ejemplo clic en: Descargar Aplicación de Ejemplo

Tags: , ,

One Response to Silverlight MVVM + WCF Ria Services: Una combinación poderosa

  1. Sergio on febrero 10, 2010 at 8:47 am

    Muy buen ejemplo, es justo lo que estaba buscando…
    Voy a echarle un vistazo y les daré mi feeback.

    Muy bueno el blog.
    Un abrazo grande desde Argentina.

    –Sergio

Acerca del autor...

Alejandro Tamayo

Web: http://www.linkedin.com/in/atamayocastillo
Alejandro Tamayo
Professor, Researcher, Developer, Consultant and technology enthusiast. Master of Science (MSc) in Computer Science and member of Weboo Research Group.Leer completo