Ir al contenido principal

Proyecto GEOW. Implementando el patrón CQRS

Me siento muy feliz de poder ofrecerles esta nueva publicación y este nuevo proyecto Open Source. Naturalmente seguiremos evolucionando el proyecto de tests de c#, pero he querido hacer este paréntesis para hablarles del patrón CQRS, o lo que es lo mismo, Command Query Responsibility Segregation.

Una primera versión plenamente operativa del proyecto GEOW está ya disponible para todos Uds. en GitHub. Les recomiendo encarecidamente que la clonen y jueguen con ella. No solo tiene un elevado potencial pedagógico, además sus propiedades visuales son altamente hipnóticas, por lo que les pido mucha precaución a la hora de ponerlo a funcionar :) 

Por esta misma razón me he decidido a crear un canal en YouTube para irles mostrando videitos con los resultados de los dos proyectos que nos traemos entre manos. Espero lo disfruten.

Son muchos los detalles técnicos que se desprenden en esta publicación, además de la propia implementación del patrón CQRS. Por ejemplo:
  • Aprenderás a manejar proyectos de base de datos SQL Server con Visual Studio y a usar la herramienta de comparación de esquemas.
  • Seguirán profundizando y practicando los fundamentos básicos de la programación orientada a objetos.
  • Darás unos interesantes primeros pasos con la clase System.Drawing.
Solemos pensar que las bases de datos adquieren complejidad cuando crecen, cuando se vuelven voluminosas. No deja de ser cierto. En mi trayectoria como DBA me he encontrado con bases de datos que concentraban grandísimas cantidades de información en una sola tabla. Bueno, con una función de partición y una buena estrategia de historificación la cosa quedaba más o menos controlada. Otro elemento de estrés para una base de datos es, ya no el tamaño de la misma, sino el número de usuarios que de forma concurrente accede a la información. Todos nos hemos enfrentado alguna vez a situaciones de bloqueos prolongados en una base de datos con muchos usuarios. En este sentido que las transacciones que se gestionan en el motor de base de datos sean de poca duración es fundamental. Pero la cosa se complica in extremis cuando además de muchos usuarios concurrentes, nos encontramos con elevadas exigencias simultáneas de lectura y escritura. Este es sin duda el escenario más hostil para una base de datos, y es en este escenario donde operar con el patrón CQRS nos puede resultar provechoso.


En esencia el patrón CQRS consiste en crear dos modelos separados para lecturas y escrituras, lo cual en realidad puede materializarse de formas bien distintas. Se consigue poder optimizar cada uno de los modelos atendiendo a sus necesidades específicas. Como todo, existen pros y contras a valorar. El incremento de la complejidad a la hora de implementar este patrón en nuestros proyectos es inevitable. Necesitaremos además implementar una pieza para sincronizar los modelos de escritura y lectura, pieza que será clave para el buen rendimiento del sistema. 

Sin más vamos a introducirnos en las vísceras del proyecto. Veamos primero sus componentes:


La arquitectura, como podrán apreciar no puede ser más sencilla e intuitiva. GEOW es el proyecto principal que contiene un Front-End consistente en formularios de Windows. Y por lo demás, capa de dominio, negocio, y datos.

Trabajando con proyectos de bases de datos SQL Server

Permítanme detenerme un momento en el proyecto GEOWDB, ya que les será necesario manipularlo para poner a funcionar el proyecto en sus máquinas. 

Se trata de un proyecto de tipo SQL Server, y el mismo contiene el esquema de una base de datos. Les muestro como crear un nuevo proyecto de este tipo:


Una vez creado el proyecto pueden Uds. hacer click derecho sobre el proyecto, y elegir la opción Importar -> Base de datos. En la imagen la opción no está habilitada porque es una operación que puede hacerse una sola vez.


El resultado es que se vuelca el esquema de su base de datos en un proyecto. Esta es la mejor solución para gestionar el control de código fuente sobre el código contenido en las bases de datos, como por ejemplo el de los procedimientos almacenados. También sirve para controlar la evolución del diseño de todos los objetos como tablas, vistas, etc.


Como pueden apreciar el proceso de importación crea una carpeta por cada esquema de base de datos, y en su interior un fichero para cada objeto perteneciente a ese esquema, a saber, tablas, procedimientos almacenados, etc. 

Ud. puede añadir elementos al proyecto. Yo he añadido aquí dos carpetas, comparer, y scripts. Luego les explicaré sobre el contenido de estas dos carpetas.

¿Cómo pueden Uds., a partir de este proyecto, crear sus propias bases de datos GEOWDB? Mediante la herramienta de comparación de esquemas. 

 
Esta herramienta súper útil toma un origen y un destino, compara esquemas, y muestra los cambios al usuario. Se usa bastante para realizar despliegues de bases de datos en distintos entornos. Tenga en cuenta que el origen y destino de la comparación puede ser tanto una base de datos, como un proyecto de base de datos, así, si toman Uds. como origen de la comparación este proyecto, y lo comparan contra una base de datos en blanco, el sistema le devolverá todos los cambios necesarios para crear el esquema del proyecto.

La herramienta, tras la comparación le mostrará los cambios de forma individualizada por objeto, para que Ud. pueda seleccionar y des-seleccionar los cambios que desea ejecutar. Por último encontrará en la parte superior un botón Update Target, que realizará en el destino todas las operaciones necesarias para equipararlo al esquema de origen.


Los parámetros de la comparación pueden ser guardados en un fichero con extensión .scmp, para que Ud. pueda repetir fácilmente esa comparación. Cuando digo los parámetros de la comparación me refiero a que quedan guardados el origen y el destino, y la configuración de la comparación, esto es, qué objetos del esquema desean ser comparados. 


En la carpeta comparer he dejado un ejemplo; se trata del comparador que he usado, con origen en mi base de datos y destino en el proyecto GEOWDB, para ir manteniendo el proyecto actualizado con los cambios que voy practicando en la base de datos. Así puedo ir creando versiones del esquema y código de la base de datos en el mismo repositorio Git o TFS.

Una vez creada la base de datos, la misma necesita un conjunto de datos maestros para funcionar. La carga de los mismos está en un script, en la carpeta scripts, llamado cargainicial.sql que no contiene más que unas pocas instrucciones Insert que Ud. deberá ejecutar. Puede hacerlo igual desde el propio IDE de Visual Studio, o bien desde SQL Server Management Studio.


Finalmente revise la cadena de conexión en el archivo app.config, para que apunte a su nueva base de datos, y todo estará listo para funcionar.

GEOW Proyect - Conociendo el proyecto: primeras ejecuciones

Una vez tenemos montada y funcionando la base de datos, podemos entrar de lleno en el proyecto que implementa el patrón CQRS

Si ejecutamos el proyecto nos encontramos con esta interfaz gráfica:


Observen que hay una lista de objetos, cada uno de los cuales tiene asociadas unas coordenadas, que son sus puntos de inicio. Los objetos son rectángulos de un alto y ancho pre-determinados, que se irán moviendo por la parte vacía de la pantalla, que es en realidad un control PictureBox. Si marcan Uds. el check Activo verán que los objetos se pintan en el control e inician su movimiento.

Se puede eliminar la lista inicial de objetos con el botón "Eliminar objetos" y crear los suyos propios, informando de sus características con los controles que hay debajo de la lista. Defina por ejemplo 100 objetos de tamaño 10x10 con una posición y dirección iniciales, seleccione un color y pulse el botón Añadir Objetos

Vamos a ir viendo los resultados:


Aquí he añadido 10 cuadrados negros de 10x10 y los he puesto a moverse. Sin embargo, como todos los cuadrados tienen la misma posición inicial, aparenta haber uno solo. 

Los cuadrados se moverán sin variar su dirección hasta que lleguen al borde del PictureBox. Al llegar al borde cambiarán de dirección. En cambio si Ud. activa el check Cambios de dirección, aleatoriamente los objetos modificarán su dirección, de modo que los 10 objetos se irán separando unos de otros:

Experimente con más o menos objetos, de distintos tamaños y colores. Pruebe a marcar y des-marcar el check de traza, añada nuevos objetos en tiempo real... los resultados son visualmente hermosos, pero no es éste el propósito final de nuestros bonitos rectángulos.


GEOW Proyect - Conociendo el proyecto: adentrándonos en el código

Los fundamentos de la programación orientada a objetos se ha tenido presente durante el desarrollo del proyecto. Observen por ejemplo la belleza intrínseca de la clase PointObj


using System;
using System.Drawing;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace GEOWObj
{
    public class PointObj
    {
        public enum enumDireccion { Derecha = 1, Izquierda = 2, Arriba = 3, Abajo = 4 }
        public Pen PenObj;

        public int Alto;
        public int Ancho;
        public Color ColorFigura;

        public List<PointObj> Followers;

        //para el modo persecución
        public bool EsLider = false;
        public bool EsPerseguidor = false;

        //dependencias
        private contracts.INeg_BufferPositions _negObj;
        
        #region privadas - propiedades

        private int _x;
        private int _y;
        private int AnchoLimite;
        private int AltoLimite;
        private enumDireccion _direccion;
        private string guidObject;
        private string nombreobjeto;
        private Int64 _idJourney;

        #endregion

        //comportamiento estandar
        public PointObj(Color PenColor, int X_Ini, int Y_ini, int Alto, int Ancho, enumDireccion DireccionInicial, int AnchoLienzo, int AltoLienzo, string Nombre, contracts.INeg_BufferPositions p_negObj)
        {
            this.PenObj = new Pen(PenColor);

            _x = X_Ini;
            _y = Y_ini;
            _direccion = DireccionInicial;

            this.Alto = Alto;
            this.Ancho = Ancho;
            this.ColorFigura = PenColor;

            NombreObjeto = Nombre;
            AnchoLimite = AnchoLienzo - Ancho - 5;
            AltoLimite = AltoLienzo - Alto - 55;

            _negObj = p_negObj;
        }

        //comportamiento Persecucion 
        public PointObj(Color PenColor, int X_Ini, int Y_ini, int Alto, int Ancho, enumDireccion DireccionInicial, int AnchoLienzo, int AltoLienzo, string Nombre, bool esLider, bool esPerseguidor, contracts.INeg_BufferPositions p_negObj)
        {
            this.PenObj = new Pen(PenColor);

            _x = X_Ini;
            _y = Y_ini;
            _direccion = DireccionInicial;

            this.Alto = Alto;
            this.Ancho = Ancho;
            this.ColorFigura = PenColor;

            NombreObjeto = Nombre;
            AnchoLimite = AnchoLienzo - Ancho - 5;
            AltoLimite = AltoLienzo - Alto - 55;

            this.EsLider = esLider;
            this.EsPerseguidor = esPerseguidor;

            _negObj = p_negObj;
        }

        public enumDireccion Direccion
        {
            get
            {
                return _direccion;
            }
            set
            {
                bool permitido = true;
                switch (value)
                {
                    case enumDireccion.Arriba:

                        permitido = !(_y <= 10);
                        break;

                    case enumDireccion.Abajo:

                        permitido = !(_y >= AltoLimite);
                        break;

                    case enumDireccion.Izquierda:

                        permitido = !(_x <= 10);
                        break;

                    case enumDireccion.Derecha:

                        permitido = !(_x >= AnchoLimite);
                        break;

                }

                if (permitido)
                {
                    _direccion = value;

                    if (this.EsLider)
                    {
                        //informa del cambio de dirección a los perseguidores
                        if (this.Followers != null)
                        {
                            foreach (var follower in this.Followers)
                            {
                                Task resprocess = new Task(() => follower.GetCoordenadasLider(this.X, this.Y));
                                resprocess.Start();
                            }
                        }
                    }
                }
            }
        }

        public int X
        {
            get
            {
                return _x;
            }
            set
            {
                if (value >= AnchoLimite || value < 10)
                {
                    if (this.Y > (AltoLimite / 2))
                    {
                        this.Direccion = enumDireccion.Arriba;
                    }
                    else
                    {
                        this.Direccion = enumDireccion.Abajo;
                    }

                }
                else
                {
                    _x = value;

                    //Graba la posición
                    DTO.InsertPositionDTO _pos = new DTO.InsertPositionDTO()
                    {
                        GUIDObject = this.GUIDObject,
                        PointDesc = this.nombreobjeto,
                        X = _x,
                        Y = this.Y,
                        Height = this.Alto,
                        Width = this.Ancho,
                        Color = this.ColorFigura.Name,
                        idJourney = _idJourney
                    };

                    Task _recordposition = new Task(() => _negObj.InsertPosition(_pos));
                    _recordposition.Start();
                }
            }
        }

        public int Y
        {
            get
            {
                return _y;
            }
            set
            {
                if (value >= AltoLimite || value < 10)
                {
                    if (this.X > AnchoLimite / 2)
                    {
                        this.Direccion = enumDireccion.Izquierda;
                    }
                    else
                    {
                        this.Direccion = enumDireccion.Derecha;
                    }
                }
                else
                {
                    _y = value;

                    //Graba la posición
                    DTO.InsertPositionDTO _pos = new DTO.InsertPositionDTO()
                    {
                        GUIDObject = this.GUIDObject,
                        PointDesc = this.nombreobjeto,
                        X = this.X,
                        Y = _y,
                        Height = this.Alto,
                        Width = this.Ancho,
                        Color = this.ColorFigura.Name,
                        idJourney = _idJourney
                    };

                    Task _recordposition = new Task(() => _negObj.InsertPosition(_pos));
                    _recordposition.Start();

                }
            }
        }

        public string GUIDObject
        {
            get
            {
                return getASCIICode(this.NombreObjeto) + this.Alto.ToString() + this.Ancho.ToString() + getASCIICode(this.ColorFigura.Name);
            }
        }

        public string NombreObjeto {

            get { return nombreobjeto; }

            set { nombreobjeto=value.ToLower().Trim(); }
        }

        public void SetIdJourney(Int64 val)
        {
            _idJourney = val;
        }

        //-->>
        #region funciones de movimiento

        public void Muevelo()
        {
            MovimientoEstandar();

            //switch (this.Comportamiento)
            //{
            //    case enumComportamiento.Estandar:

            //        MovimientoEstandar();
            //        break;

            //    case enumComportamiento.Persecucion:

            //        MovimientoEstandar();
            //        break;
            //}
        }

        private void MovimientoEstandar()
        {
            switch (this.Direccion)
            {
                case PointObj.enumDireccion.Derecha:

                    this.X += 10;
                    break;

                case PointObj.enumDireccion.Izquierda:

                    this.X -= 10;
                    break;

                case PointObj.enumDireccion.Arriba:

                    this.Y -= 10;
                    break;

                case PointObj.enumDireccion.Abajo:

                    this.Y += 10;
                    break;
            }
        }

        #endregion


        //-->>
        #region funciones de comunicación

        public void GetCoordenadasLider(int X_Lider, int Y_Lider)
        {
            int distanciaX = X_Lider - this.X;
            int distanciaY = Y_Lider - this.Y;

            int distanciaAbsolutaX = distanciaX < 0 ? distanciaX * -1 : distanciaX;
            int distanciaAbsolutaY = distanciaX < 0 ? distanciaX * -1 : distanciaX;

            if (distanciaAbsolutaX > distanciaAbsolutaY)
            {
                //la prioridad es recortar en el eje X, pero solo se puede hacer si la dirección actual es arriba o abajo
                if (this.Direccion == enumDireccion.Arriba || this.Direccion == enumDireccion.Abajo)
                {
                    //Recortamos en el eje X
                    if (X_Lider < this.X)
                    {
                        this.Direccion = enumDireccion.Izquierda;
                    }
                    else
                    {
                        this.Direccion = enumDireccion.Derecha;
                    }
                }
                else
                {
                    //recortamos en eje Y aunque no sea la prioridad
                    //a menos que la distancia sea "pequeña" en el eje Y en cuyo caso mantenemos la dirección

                    if (distanciaAbsolutaY > 100)
                    {
                        if (Y_Lider < this.Y)
                        {

                            this.Direccion = enumDireccion.Arriba;
                        }
                        else
                        {
                            this.Direccion = enumDireccion.Abajo;
                        }
                    }
                }
            }
            else
            {
                //la prioridad es recortar en el eje Y, pero solo se puede si la dirección actual es izquierda o derecha
                if (this.Direccion == enumDireccion.Izquierda || this.Direccion == enumDireccion.Derecha)
                {
                    //recortamos en eje Y 
                    if (Y_Lider < this.Y)
                    {
                        this.Direccion = enumDireccion.Arriba;
                    }
                    else
                    {
                        this.Direccion = enumDireccion.Abajo;
                    }
                }
                else
                {
                    //recortamos en eje X aunque no sea la prioridad
                    //a menos que la distancia sea "pequeña" en el eje X en cuyo caso mantenemos la dirección
                    if (distanciaAbsolutaX > 100)
                    {
                        {
                            if (X_Lider < this.X)
                            {
                                this.Direccion = enumDireccion.Izquierda;
                            }
                            else
                            {
                                this.Direccion = enumDireccion.Derecha;
                            }
                        }
                    }
                }
            }

        }

        #endregion


        //-->>
        #region Métodos privados

        private string getASCIICode(string texto)
        {
            string res = "";

            for (int cont = 0; cont < texto.Length; cont++)
            {
                res += Encoding.ASCII.GetBytes(texto)[cont].ToString();
            }

            return res;
        }

        #endregion
    }
}

Esta clase es la representación lógica del concepto teórico de una figura geométrica. En este caso son cuadrados dentro de un plano, pero bien podríamos incorporar el concepto de cualquier otro polígono.

Se han implementado dos comportamientos en lo que se refiere al movimiento. Uno, denominado estándar, consistente en que los cuadrados cambian de dirección en tres circunstancias. (i) Si el check de cambio de dirección está desmarcado, cambia la dirección del cuadrado solo cuando  llega al extremo de su contenedor, digamos, el área de dibujo. Este valor, el tamaño del lienzo (o lo que es lo mismo, del control PictureBox), pasa como parámetro en el constructor del objeto. (ii) Si el check de cambios de dirección está marcado los cuadrados además de rebotar en los extremos del lienzo, por cada nuevo movimiento tienen un cierto número de probabilidades aleatorio de llevar a cabo un cambio de dirección.

(iii) Por último, se puede tomar control de una figura en concreto, y determinar su movimiento de forma "manual". Esto se hace seleccionando en la lista de objetos un elemento. Al hacerlo aparecer 4 botones de dirección en la parte inferior del formulario. Además  se pueden usar las teclas de dirección del teclado (flechas arriba, abajo izquierda y derecha) para tomar control manual de la dirección de una figura en concreto (la seleccionada en el ListView).

Se ha programado un segundo comportamiento, denominado modo persecución, para lo que usamos el segundo constructor de la clase PointObj. Este constructor le añade una propiedad al objeto, la propiedad EsLider. En cada ciclo de movimiento, las figuras líderes informaran a las figuras no líder (perseguidores) si cambiaron de dirección. Pudieron cambiar de dirección por cualquiera de las 3 causas anteriormente explicadas. Si hay cambio de dirección en una figura líder, los perseguidores reaccionaran con un cambio de dirección orientado a "cazar" a las figuras líderes, pero no sin cierta "torpeza". Mi intención es progresar con mejoras en el algoritmo de caza al líder. De momento el actual está programado en la función GetCoordenadasLider:

        public void GetCoordenadasLider(int X_Lider, int Y_Lider)
        {
            int distanciaX = X_Lider - this.X;
            int distanciaY = Y_Lider - this.Y;

            int distanciaAbsolutaX = distanciaX < 0 ? distanciaX * -1 : distanciaX;
            int distanciaAbsolutaY = distanciaX < 0 ? distanciaX * -1 : distanciaX;

            if (distanciaAbsolutaX > distanciaAbsolutaY)
            {
                //la prioridad es recortar en el eje X, pero solo se puede hacer si la dirección actual es arriba o abajo
                if (this.Direccion == enumDireccion.Arriba || this.Direccion == enumDireccion.Abajo)
                {
                    //Recortamos en el eje X
                    if (X_Lider < this.X)
                    {
                        this.Direccion = enumDireccion.Izquierda;
                    }
                    else
                    {
                        this.Direccion = enumDireccion.Derecha;
                    }
                }
                else
                {
                    //recortamos en eje Y aunque no sea la prioridad
                    //a menos que la distancia sea "pequeña" en el eje Y en cuyo caso mantenemos la dirección

                    if (distanciaAbsolutaY > 100)
                    {
                        if (Y_Lider < this.Y)
                        {

                            this.Direccion = enumDireccion.Arriba;
                        }
                        else
                        {
                            this.Direccion = enumDireccion.Abajo;
                        }
                    }
                }
            }
            else
            {
                //la prioridad es recortar en el eje Y, pero solo se puede si la dirección actual es izquierda o derecha
                if (this.Direccion == enumDireccion.Izquierda || this.Direccion == enumDireccion.Derecha)
                {
                    //recortamos en eje Y 
                    if (Y_Lider < this.Y)
                    {
                        this.Direccion = enumDireccion.Arriba;
                    }
                    else
                    {
                        this.Direccion = enumDireccion.Abajo;
                    }
                }
                else
                {
                    //recortamos en eje X aunque no sea la prioridad
                    //a menos que la distancia sea "pequeña" en el eje X en cuyo caso mantenemos la dirección
                    if (distanciaAbsolutaX > 100)
                    {
                        {
                            if (X_Lider < this.X)
                            {
                                this.Direccion = enumDireccion.Izquierda;
                            }
                            else
                            {
                                this.Direccion = enumDireccion.Derecha;
                            }
                        }
                    }
                }
            }
        }

Centremos un momento nuestra atención en el SET de la propiedad X de un rectángulo (una de las dos coordenadas X e Y que definen su posición actual dentro del PictureBox). Aquí encontramos el código que provoca el "rebote" en los bordes del lienzo. Y encontramos también algo si cabe más interesante. Una llamada asíncrona a un método llamado InsertPosition.

        public int X
        {
            get
            {
                return _x;
            }
            set
            {
                if (value >= AnchoLimite || value < 10)
                {
                    if (this.Y > (AltoLimite / 2))
                    {
                        this.Direccion = enumDireccion.Arriba;
                    }
                    else
                    {
                        this.Direccion = enumDireccion.Abajo;
                    }

                }
                else
                {
                    _x = value;

                    //Graba la posición
                    DTO.InsertPositionDTO _pos = new DTO.InsertPositionDTO()
                    {
                        GUIDObject = this.GUIDObject,
                        PointDesc = this.nombreobjeto,
                        X = _x,
                        Y = this.Y,
                        Height = this.Alto,
                        Width = this.Ancho,
                        Color = this.ColorFigura.Name,
                        idJourney = _idJourney
                    };

                    Task _recordposition = new Task(() => _negObj.InsertPosition(_pos));
                    _recordposition.Start();
                }
            }
        }







Comentarios

Entradas populares de este blog

Test 3: ¿Son eficientes los ORMs?

Bienvenidos amigos. Me complace anunciar que por fin estrenamos la categoría " SQL Server Tips ", y lo hacemos por todo lo alto, entrando de lleno en un aspecto altamente polémico entre programadores. ¿Es eficiente un ORM en los accesos a datos? Ya conocen la filosofía de nuestros Tests, no vamos a teorizar demasiado, pero sí una pequeña base va a ser necesaria para conseguir una buena respuesta a nuestra pregunta. He leído un interesante  artículo de nuestros súper amigos de Deloitte ( cuando usar ORM ) argumentando que el uso o no de un ORM hay que decidirlo en relación a la complejidad de nuestro modelo de datos, y al rendimiento que requeriremos en nuestras soluciones, pero, ¿cuándo no deseamos el mejor rendimiento para nuestro software? Lo cierto es que, como ya hemos visto, el ORM facilita mucho las cosas, aporta claridad al código, de eso no cabe duda, pero, ¿es eficiente? He ahí la cuestión. Sobre este asunto vamos a poner a funcionar nuestros apreciados Test. C

Proyecto GEOW. Implementando el patrón CQRS. 2ª parte

En el anterior post ( Proyecto GEOW. Implementando el patrón CQRS ) nos adentramos en el funcionamiento del proyecto GEOW que nos va a servir de base para implementar un patrón arquitectónico, CQRS , pensado para dar respuesta a sistemas con alta exigencia de lecturas y escrituras simultáneas. Para ello hemos creado una interfaz gráfica con una serie de figuras geométricas en movimiento. Ahora vamos a ir a la parte de EL DATO. Cada vez que uno de estos cuadrados cambia de posición envía una trama con sus propias características, y sus nuevas coordenadas. Cada uno de los cuadrados realiza un movimiento cada 300 milisegundos, y he llegado a probar con hasta unas 700 figuras. En estos niveles el software empieza a sufrir, pero más la parte gráfica. Aparentemente el sistema de grabación de coordenadas se mantiene en buena forma. Vamos a ver, precisamente, este sistema de grabación: Grabando lotes de coordenadas en BBDD En el objeto PointObj que representa cada una de las figuras, en el

Primeros pasos en Enigma Software Labs

En pocas palabras: vamos a probar y estresar distintas soluciones de software para un mismo problema, en busca de realidades matemáticas. Para ello, lo primero que necesitamos en averiguar el modo de publicar código aquí en Blogger . Para ello voy a usar un sistema súper sencillo y eficiente. Ahí va un ejemplo de código C# private void frmMonitor_Load ( object sender, EventArgs e) { TreeNode _tr = new TreeNode( "SQL Server Tips" ); TreeNode _tr2 = new TreeNode( "C# Code" ); treeCatalogo.Nodes.Add(_tr); treeCatalogo.Nodes.Add(_tr2); //splitContainer.Panel1.Focus(); //lstMonitor.Focus(); } Hilarante ¿verdad?   hilite.me  lo hace posible. Bien, paso 2, fácil y sencillo: vamos a crear una plataforma de testeo para C# en una bonita solución que compartiré en el repositorio GIT, para disfrute de todos. Enigma Software - ZM LABS Como posteriormente me dive