DSL con diseñador WPF

 

En los últimos años la comunidad NetFxFactory ha venido desarrollando artículos muy influyentes relacionados con las DSL Tools de Microsoft. Han aportado excelentes soluciones en temas como el multidiagrama o el uso de patrones en modelos.

En Septiembre de 2009, publicaron “Provide an improved user experience to your DSL’s” en el que anticipaban una de las principales características  incorporadas en la versión de DSL Tools de 2010: los diseñadores basados en WPF; dando una primera aproximación para Visual Studio 2008. Una vez lanzado Visual Studio 2010 publicaron “Magnify your diagram”, artículo en el que migraban su prototipo de 2008 haciendo uso de todas las características lanzadas en la nueva versión de DSL Tools.  

Como bien dice Pascal Recchia, autor de estos artículos, a pesar de que entre estas nuevas características no se propone ninguna mejora directa sobre los mecanismos de layout de los diseñadores generados (que supone, sin lugar a dudas, un de lde la herramienta) se puede entrever que la estrategia que se quiere seguir para afrontarlos es precisamente el uso de WPF.

En el ejemplo que se propone en dichos artículos se desarrolla un DSL para modelar una red de aeropuertos y vuelos entre aeropuertos.

image

 

 

Un ejemplo bastante simple y “específico del dominio”. Indudablemente de eso se trata. No obstante, si lo que queremos es desarrollar un diseñador basado en WPF algo más completo y genérico, que nos sirva de referencia tanto para crear un diseñador de clases, como para un entidad-relación o un CUIP, la cosa se nos queda algo corta.

Tampoco nos sirve de mucho la plantilla de DSL con diseñador WPF que viene incluida en la instalación de las DSL Tools de VS2010.

 

image

 

En este caso, el diseñador que se genera desde nuestro metamodelo es, en efecto, un formulario WPF… pero bastante discreto:

 

image

 

 

 

Por tanto, para poder empezar a crear DSLs “con sustancia” usando esta prometedora característica, hay que hacerse con un buen ejemplo de diseñadores de diagramas en WPF.

En CodeProject hay una serie de cuatro artículos, con su correspondiente código, que desgranan paso a paso el desarrollo de un diseñador de diagramas basado en WPF. Si descargamos el código de ejemplo podemos ejecutar la aplicación y veremos algo como esto:

 

 

image

 

Nos decidimos a integrar estos ejemplos de CodeProject sobre un pequeño DSL de entidades con atributos generado con las DSL Tools.

A la hora de crearlo seleccionamos la plantilla “Minimal Language”.

 

image

 

 ¿Y por qué no usar la plantilla “Minimal WPF Designer” si lo que queremos es desarrollar un diseñador WPF? Pues porque con la plantilla WPF no se nos permite definir, y por tanto generar, desde el metamodelo, lo relativo al diseñador y al mapeo entre sus elementos y los correspondientes elementos del modelo. Esto merece una explicación un poco más en profundidad.

Como vemos en la siguiente imagen, un metamodelo en DSL Tools está representado por dos partes: Elementos del modelo (Classes and relationships) y Elementos del diagrama (Diagram Elements).

 

image

 

Mientras los primeros definen el dominio: entidades y atributos en nuestro ejemplo, aeropuertos y vuelos en el de NetFxFactory; los segundos definen la representación en un diseñador de cada una de las instancias de los elementos de ese dominio, tanto en forma visual, como en posición, tamaño o Zindex. Los pequeños conectores que relacionan ambas partes marcan el mapeo entre los elementos de las respectivas regiones, es decir, qué elemento de diseñador representa a cada elemento del dominio.

Con la plantilla de diseñador de WPF por defecto, que es la que se usa en el ejemplo de NetFxFactory, la parte de diseñador esta deshabilitada, y no se nos permite mapear elementos de diseñador a elementos de dominio. Esto implica que nuestro modelo sólo mantendrá información sobre los elementos del dominio y no de cómo se representan los mismos en el diseñador. Asimismo el mecanismo de persistencia de esta información de diseñador queda deshabilitada, es decir, no tendremos el archivo “.diagram” en nuestro modelo y, por tanto, no se guardarán, entre otras cosas, las posiciones de los elementos en el diseñador.

Por todas estas cosas utilizamos la plantilla “Minimal Language”  y creamos nuestro DSL de entidades (Podéis ver el metamodelo en la imagen anterior).

A la solución creada le agregamos el proyecto que comentamos de los artículos de CodeProject. En este caso usamos el código que acompaña al artículo 4. Previamente lo convertimos en biblioteca de clases y eliminamos el App.xaml que ya no necesitaremos.

 

image

 

 

En este punto, si ejecutamos la solución actual, obviamente vemos el diseñador generado por defecto, basado en WinForms:

 

 

image

 

Para usar nuestro nuevo diseñador, basado en WPF, tenemos que añadir algo de código “custom”. La responsabilidad del diseñador recae sobre tres objetos: EditorFactory, DocData y DocView (se encuentran en la carpeta GeneratedCode del proyecto DslPackage). El objeto DocView corresponde a la interfaz del diseñador, el DocData contiene la representación en memoria del documento y maneja el guardado y cargado del mismo, mientras el EditorFactory es el encargado de construir objetos de las dos anteriores clases.

En este caso creamos una clase parcial del DocView para directamente cambiar la interfaz del diseñador al Designer de WPF.

 

image

 

 

En la clase parcial básicamente sobrescribimos la carga de la vista y su contenido para albergar una instancia del WPF Designer.

        public override object Content

        {

            get

            {

                if (content == null)

                {

                    content = this.CreateControl();

                }

                return content;

            }

        }

 

        protected Wpf::ContentControl WpfViewControl

        {

            get

            {

                if (this.Content != null)

                {

                    return this.Content as Wpf::ContentControl;

                }

                else

                {

                    return null;

                }

            }

        }

            

        protected global::System.Windows.UIElement CreateControl()

        {

            return new DiagramDesigner.Designer();

        }

 

        protected override bool LoadView()

        {

            base.LoadView();

 

            global::System.Diagnostics.Debug.Assert(this.DocData.RootElement != null);

 

            bool returnValue = false;

 

            if (this.DocData.RootElement != null)

            {

                returnValue = true;

                WpfViewControl.DataContext = this.DocData.RootElement;

                SelectionPusher.Add(WpfViewControl);

            }

            return returnValue;

        }

 

 

 

 

Podemos comprobar el resultado si ejecutamos la instancia experimental y observamos el proyecto Debugging. Queda curioso, pero realmente aún no sirve para nada.

 

image

 

 

Para sacarle partido hay que meterse de lleno en el XAML de nuestro Designer y darle un par de vueltas. Pero por esto pasaré a muy alto nivel, que para enseñar XAML ya hay blogs mejores que éste. La base del diseñador la podemos ver en el siguiente esquema:

 

image

 

 

La parte azulada corresponde a la interfaz del diseñador, mientras la parte anaranjada corresponde a la infraestructura generada por las DSL Tools que lo sustenta.

El resultado es un DSL totalmente funcional con un diseñador en WPF y del que, por ejemplo, podemos generar código mediante plantillas T4:

 

 

image

image

 

 

El siguiente paso sería sacar factor común y obtener un conjunto de clases base más un conjunto de plantillas para que, dada una definición de un metamodelo cualquiera en un archivo DslDefinition, obtengamos el correspondiente diseñador basado en WPF automáticamente, tal y como se hace actualmente con los diseñadores basados en WinForms.

Este creo que es el camino en el que se están centrando desde NetFxFactory… estaremos atentos a sus resultados!

Generando búsquedas mediante el patrón especificación

Como ya comenté en mi anterior post, llevo un tiempo trabajando sobre la arquitectura DDD NLayer, con el objetivo de generar gran parte de los artefactos que la componen a partir de modelos. En la solución de ejemplo ya se adjuntan ciertas plantillas T4 para generar las self-tracking entities y el context de EF. Sin embargo, hay bastante más código “generable” que nos podría servir de soporte y que nos permitiría rápidamente centrarnos en los procesos de negocio realmente complejos. Una de estas partes con posibilidad de ser fácilmente generable es la de las búsquedas basadas en el patrón especificación. Para entender su implementación en DDD NLayer sólo hay que leer el post de su autor Unai.

La piedra angular de dicha implementación se basa en las clases Specification, que contienen los criterios de búsqueda (método SatisfiedBy), junto a los parámetros que se usan en dicha búsqueda. Por ejemplo:

public class BankAccountSearchSpecification
        :Specification<BankAccount>
    {
        string _CustomerName;
        string _BankAccountNumber;

        public BankAccountSearchSpecification(string bankAccountNumber, string customerName)
        {
            _CustomerName = customerName;
            _BankAccountNumber = bankAccountNumber;
        }

        public override System.Linq.Expressions.Expression<Func<BankAccount, bool>> SatisfiedBy()
        {
            Specification<BankAccount> spec = new TrueSpecification<BankAccount>();

            if (!String.IsNullOrEmpty(_BankAccountNumber) &&
                !String.IsNullOrWhiteSpace(_BankAccountNumber))
            {
                spec &= new BankAccountNumberSpecification(_BankAccountNumber);
            }
            if (!String.IsNullOrEmpty(_CustomerName) &&
                !String.IsNullOrWhiteSpace(_CustomerName))
            {
                spec &= new DirectSpecification<BankAccount>(ba => ba.Customer.ContactName.ToLower().Contains(_CustomerName.ToLower()));
            }

            return spec.SatisfiedBy();
        }
    }

Nuestro objetivo es generar una clase Specification por cada entidad, que nos proporcione las búsquedas parametrizadas básicas sobre sus propiedades y relaciones. El problema es que en el caso de entidades con un número alto de propiedades, los constructores de estas clases Specification se llenarían de parámetros. Una primera opción sería usar información del modelo para obtener grupos de propiedades que formen especificaciones autónomas dentro de cada entidad. Así obtendríamos conjuntos de especificaciones como los que encontramos en la solución de DDD NLayer. Por ejemplo, para la entidad Order podemos encontrar OrderDateSpecification, OrderFromCustomerDateRangeSpecification, OrderFromCustomerSpecification u OrderShippingSpecification. Con esta solución ganamos en la ubicuidad del lenguaje que tanto buscamos en DDD, pero no conseguimos nuestro objetivo de definir búsquedas de manera estándar y simple si no poseemos el nivel de expresividad adecuado en nuestros modelos.

Ese era nuestro caso, ya que partíamos de un modelo similar al de Entity Framework o al modelo de clases de UML. Para estos casos podemos introducir un nuevo artefacto por cada entidad al que llamamos SearchValues, y que tiene la única responsabilidad de encapsular los parámetros de búsqueda. De este modo, la clase Specification sólo tiene la responsabilidad de definir los criterios delegando el contenido de los parámetros, como hemos dicho, a la clase SearchValues. En el constructor recibirá un objeto de este tipo y en el método SatisfiedBy decidirá si los parámetros contenidos en ese objeto SearchValues cumplen o no con los criterios definidos.

De modo que para cada entidad, por un lado generamos una clase SearchValues, con una propiedad por cada propiedad de la entidad (strings para tipos string, rangos para decimal, int o datetime, e int para filtrar por claves en relaciones), ademas de otra propiedad de tipo SearchValues para hacer búsquedas por elementos que “no cumplan los criterios de ese otro SearchValues”, propiedad a la que llamamos NotInSearch. Y por otro lado generamos una clase Specification que contiene la correspondiente propiedad de tipo SearchValues y define los criterios de búsqueda en el método SatisfiedBy a partir de los valores de esa propiedad. En código queda más claro:

public class BankAccountSearchSpecification
        :Specification<BankAccount>
    {
		public BankAccountSearchValues SearchValues { get; set; }
				
                public BankAccountSearchSpecification(BankAccountSearchValues searchValues)
                { 
			SearchValues=searchValues;
		}

               public override Expression<Func<BankAccount, bool>> SatisfiedBy()
               {
 			Specification<BankAccount> spec = new TrueSpecification<BankAccount>();
				if(SearchValues.CustomerName != string.Empty)
				{
					spec &= new DirectSpecification<BankAccount>(element => element.CustomerName == (SearchValues.CustomerName));
				}
				if(SearchValues.BankAccountNumber != string.Empty)
				{
					spec &= new DirectSpecification<BankAccount>(element => element.BankAccountNumber == (SearchValues.BankAccountNumber));
				}
				if (SearchValues.NotInSearch != null)
                               {
                                       spec &= new NotSpecification<BankAccount>(new BankAccountSearchSpecification(SearchValues.NotInSearch));
                               }  
			
	        return spec.SatisfiedBy();
        }
    }
    public class BankAccountSearchValues
    {	
	public string CustomerName { get; set;	}
	public string BankAccountNumber	{ get; set; }
	public BankAccountSearchValues NotInSearch { get; set; }
    }

Las expresiones que usamos para las búsquedas por propiedades de tipos básicos son triviales. En el código anterior se pueden ver para tipos string por criterio de “igual a”. Otros criterios como “contenida en”, “menor que”, etc. son igualmente triviales, así como el uso de otros tipos como rangos de datetime, int, etc. En cuanto a las relaciones, la cosa se complica un poco cuando intentamos filtrar por extremos con cardinalidad “*” (Many). Por inercia la tendencia es usar una expresion del tipo:

spec &= new DirectSpecification<BankAccount>(element => element.BankTransferFromThis.Contains(SearchValues.BankTransfer));

Sin embargo, nos encontraremos con el error: “No se pudo crear un valor de constante de tipo ‘Namespace.Entities.Oferta’. Sólo se admiten los tipos primitivos (‘como Int32, String y Guid’) en este contexto”.

La tendencia entonces cambia a crear el Join con Linq para filtrar por los elementos relacionados. El problema es que implicaría un severo golpe al diseño y la simplicidad de este motor de búsquedas.

La solución óptima que encontramos fue usar Any(), con lo que se obtiene una forma elegante y simple de buscar en este tipo de relaciones, además de ser independiente de si se trata de “One to Many” o “Many to Many”. La sentencia quedaría así:

spec &= new DirectSpecification<BankAccount>(element => element.BankTransferFromThis.Any(bt=>bt.BankTransferId==SearchValues.BankTransferId));

Y la sentencia SQL que genera es algo como esto (un poco simplificada):

SELECT * FROM  BankAccount

LEFT OUTER JOIN BankTransfer ON BankTransfer.BankAccountId = BankAccount.BankAccountId

WHERE  EXISTS (SELECT 1 FROM BankTransfer WHERE (BankTransfer.BankAccountId = BankAccount.BankAccountId) AND (BankTransfer.BankAccountId = @p__linq__0)

Una vez que hemos contemplado los casos de cada tipo diferente de propiedad, tanto si es escalar, como si es de navegación, sólo nos queda generar la plantilla que recorra las clases o entidades del modelo y vaya tratando cada uno de los casos comentados. Gracias a las T4, nos resultará fácil y sólo dependerá del metamodelo que estemos usando. En el caso de usar UML podemos seguir aquí una guía de cómo hacerlo, si queremos usar el modelo de EF podemos usar la guía que suponen las plantillas de T4 para STE o POCO, y en el caso de que usemos modelos de DSL Tools simplemente al crear el proyecto de DSL Designer se nos creará un proyecto Debugging con ejemplos de dichas plantillas. No obstante, espero escribir algún ejemplo de estas aproximaciones en próximos posts. 

Algo que creo que deberíamos tener en cuenta (y que siempre se ha de aplicar cuando usamos código generado) es que ni todas las búsquedas se deberían de usar tal como nos la genera la plantilla desde el modelo, ni tampoco, por ello, deberíamos crearlas todas “a mano”. Habrá siempre zonas en nuestra aplicación, más o menos amplias, que puedan ser automatizadas. La cuestión es encontrar la frontera correcta y no dejar de tener el control de qué está haciendo nuestro código por mucho que se haya generado automáticamente. Por ejemplo, en soluciones basadas en self-tracking en que no disponemos de lazy loading, el hecho de cargar todas las colecciones (por medio de Include()) de todas las relaciones de nuestras entidades para luego poder usar este tipo de búsquedas ya generadas automáticamente mediante plantillas, nos tiraría abajo el rendimiento de ciertas partes de nuestra aplicación. Ni por ello debemos descartar usar las búsquedas generadas para algunas otras partes de la aplicación, ni tampoco sacrificar al usuario por el hecho de usar ese código generado en todos sitios. De nuevo, como en muchas otras cosas, es cuestión de sensatez y ponderación…