Ir al contenido principal

Entity Framework Code First - 2ª parte

Bienvenidos de nuevo queridos desarrolladores. Si recuerdan, en una publicación anterior relativa a la estrategia Code First habíamos dejado algunas cuestiones en el tintero, que nos disponemos ahora a abordar.

La primera de ellas giraba en torno al tratamiento que, a raíz de la adopción de la estrategia Code First, teníamos que incorporar al respecto de las clases de dominio y los modelos. Más concretamente en este post vamos a explicar la incorporación en nuestra plataforma de la popular herramienta AutoMapper, que como veremos nos permitirá hacer de forma sencilla la transición de un objeto de dominio a un modelo de Entity Framework, y viceversa.

La segunda cuestión que vamos a abordar es la actualización de la base de datos en tiempo de ejecución mediante Entity Framework, a raíz de posibles cambio en los modelos, tales como la incorporación de nuevas propiedades, tablas nuevas, o la incorporación y actualización de los datos contenidos en éstas.

Antes de entrar en materia permítanme abordar las cuestiones de protocolo: como saben en GitHub pueden clonar la plataforma de test a su gusto, crear sus propios test, e incorporar cuantas mejoras les parezca oportuno. Aquí les dejo el acceso al proyecto: https://github.com/EnigmaLABS/EnigmaTestLabs

No duden en hacernos llegar sus propias iniciativas de desarrollo y con gusto las incorporaremos y publicaremos. Si además quieres participar activamente en este blog contacten con nosotros sin tapujos. Escucharemos sus propuestas con mucho gusto. Otros medios que ponemos a su disposición para ampliar información y contactar con nosotros son LA WEB que presenta otra de nuestras plataformas, InstaGrow Services, y el catálogo de productos de Enigma Software. Por último, puedes estar al tanto de nuestras actividades a través de las cuentas de Instagram @enigma_csharp_labs e @instagrow_services_burgos.

Sin más, vamos al lío:

Incorporando AutoMapper

Como introducción al uso de AutoMapper vamos a echar un vistazo a la estructura de nuestra solución.


La misma consta de cuatro proyectos. Es una estructura clásica de N capas cada una de las cuales tiene su propia misión.
  • LabsMonitor es una aplicación de formularios de Windows. Sería la capa de presentación. Sobre la misma solo cabe decir que no debe contener ninguna lógica de negocio, tan solo recibir los objetos con la información, y mostrarlos en pantalla. Poco más que decir.
  • LabsBusiness constituye la capa de negocio. Se trata de un proyecto de tipo biblioteca de clases, al igual que los proyectos restantes. Aquí es donde reside la lógica de nuestro negocio, cálculos, algoritmos, etc. En nuestro caso particular aquí están contenidos los test. Pero también las clases que acceden al registro de Windows, las que gestionan la creación inicial de la base de datos, o los algoritmos que vamos utilizando para realizar los test, que son hasta este momento el cálculo de la serie Fibonacci, y la clase que concatena distintos fragmentos de El Quijote. Esta capa sirve además como enlace con la capa de datos, aquella que obtiene y recibe información de la base de datos.
  • LabsData es nuestra capa de acceso a la base de datos. No hay lógica de negocio en ellas, tan solo acceso a datos. Contiene los modelos de Entity Framework, el Context, los repositorios...
  • LabsObjects: este conjunto de clases es especial. Son clases de dominio, es transversal a todas las demás. Contiene las unidades de información que usarán el resto de proyectos para cumplir sus distintos cometidos. Todos los flujos de datos que maneje nuestra solución, deberán estar contenidos y definidos en una clase dentro de este proyecto. Aquí podemos encontrarnos entidades de dominio, u objetos de transferencia de información conocidos como DTO. Pueden contener lógica de dominio
En cuanto a las dependencias entre unos y otros proyectos: todos los proyectos harán referencia al proyecto LabsObjects, de tal modo que los contenedores de información de todas las capas serán compartidos entre ellas. Así, por decirlo de una manera, todas se comunicarán "en un mismo idioma" Aparte de esta dependencia común, la capa de presentación solo accederá a la capa de negocio, nunca directamente a la de datos. La de negocio accederá a la capa de datos, y la de datos no tendrá visibilidad sobre ninguna capa anterior. Así se generará un fruir de la información siempre coherente, y cada una de las capas tratará o usará la información atendiendo a su cometido.

En la primera parte de este post dedicado a Entity Framework Code First ya les introduje la problemática que queremos ahora abordar. Se la recuerdo brevemente:

Cuando estábamos usando ADO.NET como método de acceso a la información teníamos la posibilidad de que el resultado de una consulta en BBDD quedara inmediatamente mapeado en una clase de dominio, tal como vemos en el siguiente fragmento de código:

        public List<Categories> getCategories()
        {
            List<Categories> res = new List<Categories>();

            try
            {
                SqlConnection cnx = new SqlConnection(str_cnx);
                SqlCommand cmd = new SqlCommand();

                cmd.Connection = cnx;
                cmd.CommandType = CommandType.StoredProcedure;
                cmd.CommandText = "GetCategories";

                cnx.Open();

                SqlDataReader reader = cmd.ExecuteReader();

                while (reader.Read())
                {
                    Categories _cat = new Categories()
                    {
                        id = int.Parse(reader["idCategorie"].ToString()),
                        Categorie = reader["Categorie"].ToString(),
                    };

                    if (reader["idCategorieNode"] != DBNull.Value)
                    {
                        _cat.Categorie_dad = new Categories()
                        {
                            id = int.Parse(reader["idCategorieNode"].ToString()) 
                        };
                    }

                    res.Add(_cat);
                }

                cnx.Close();
            }
            catch (Exception ex)
            {

            }

            return res;
        }

Pero a raíz de la implantación de Entity Framework la cosa cambia. Como ya quedó claro, EF utiliza unas clases denominadas modelos, para construir la estructura de la base de datos, y como contenedores para plasmar los resultados de las consultas.

Las clases contenidas en un DBContext serán aquellas que participen de los accesos a los datos. Les muestro a continuación el modelo Test (que se convierte y mapea contra la tabla homónima), y nuestro DBContext:

    public class Tests
    {
        public Int64 id { get; set; }

        [Required]
        [StringLength(255)]
        public string Test { get; set; }

        [StringLength(555)]
        public string Description { get; set; }

        [StringLength(255)]
        public string Classname { get; set; }

        [StringLength(555)]
        public string Url_Blog { get; set; }

        [StringLength(555)]
        public string Url_Git { get; set; }

        [StringLength(555)]
        public string Url_Stackoverflow { get; set; }

        public Categories Categorie { get; set; }

        [Required]
        [ForeignKey("Categorie")]
        public int idCategorie { get; set; }

        public List<TestCases> TestCases { get; set; }
    }

    public class LabsContext : DbContext
    {
        public LabsContext(string cnx_str) : base(cnx_str) {  }

        public DbSet<EFModels.Categories> Categories { get; set; }
        public DbSet<EFModels.Tests> Test { get; set; }
        public DbSet<EFModels.TestCases> TestCases { get; set; }
        public DbSet<EFModels.Executions> Executions { get; set; }
    }

EF usará estas dos clases para realizar accesos (SELECTs, INSERTs, UPDATEs y DELETES) a las distintas entidades. Las clases que se utilizan como canal para realizar dichos accesos son denominados los repositorios:

        public List<Categories> getCategories()
        {
            List<Categories> res = new List<Categories>();

            using (var db = new context.LabsContext(_str_cnx))
            {
                var res_model = db.Categories.ToList();

                res = mapper.Map(res_model, res);
            }

            return res;
        }

        public List<test_object> getTests()
        {
            List<test_object> res = new List<test_object>();

            using (var db = new context.LabsContext(_str_cnx))
            {
                var res_model = db.Test.Include(tc => tc.TestCases).Include(c => c.Categorie).ToList();
                res = mapper.Map(res_model, res);
            }

            return res;
        }

Observen bien el código del repositorio que les acabo de presentar. He querido que éste devuelva, tal como hacía antes, una entidad de dominio. En este caso son las entidades Categorias y Test. Pero vean también que el acceso a los datos que EF realiza devuelve clases de un tipo diferente, ya que devuelve la información en modelos.

Bien pues, he aquí nuestro dilema, y he aquí nuestra solución: los mapeos automáticos entre clases, que ya habrán podido apreciar en el código del repositorio.

Antes de explicar el funcionamiento de AutoMapper, una aclaración: aunque muchas veces los modelos de EF y las clases de dominio serán clases estructuralmente muy parecidas, no siempre tiene porque ser así. Ustedes pueden tener la tentación de arrastrar los modelos a través de todas las capas, máxime en un proyecto de las características del que nos ocupa. Decirles al respecto que no se lo aconsejo, por muy diversos motivos. Entiendan que el modelo ha de contener exclusivamente aquellas informaciones que deseemos consolidar en una base de datos, mientras que la entidad de dominio, o los DTO pueden contener informaciones adicionales, tales como campos calculados, o resultados de diversas operaciones de negocio. Cabría la posibilidad de detenerse y extenderse en este asunto, pero el tiempo apremia señores. Les dejo a Uds. la responsabilidad de buscar y encontrar sus propias respuestas.

Empezando con AutoMapper

Les diré a modo introductorio que AutoMapper es una librería de gran popularidad, motivo por el cual está extensamente documentada. Les remito a su página oficial para conocer su funcionamiento mucho más en detalle (https://automapper.org/). Su uso es gratuito y su instalación vía NuGet sumamente sencilla.

En este artículo vamos a mostrar los primeros pasos a seguir al incorporar mapeos entre clases, pero sepan que el uso que se le puede dar a la librería es mucho más extenso de lo que aquí vamos a ver, y les remito de nuevo a la web oficial si desean profundizar en esta interesante herramienta.

Una vez instalada la librería, lo primero que debemos hacer es configurar los mapeos que después necesitemos ejecutar. Vamos a utilizar la clase TestCase como ejemplo. Queremos mapear el modelo TestCase con la entidad de dominio TestCase, así que les voy a mostrar ambos objetos:

    public class TestCases
    {
        public Int64 id { get; set; }

        [Required]
        [StringLength(255)]
        public string Function { get; set; }

        [StringLength(555)]
        public string Description { get; set; }

        [Required]
        [ForeignKey("Test")]
        public Int64 idTest { get; set; }

        public EFModels.Tests Test { get; set; }
    }

    public class TestCases
    {
        public Int64 id { get; set; }
        public string Function { get; set; }
        public string Description { get; set; }

        public Int64 idTest { get; set; }
    }

Lo primero que observamos es que, tal y como acabamos de comentar, las estructuras son sumamente similares, cosa que nos será de ayuda a la hora de mapear.

Tan solo hay una propiedad de diferencia, la llamada de navegación Test, que se encuentra en el modelo y no en la entidad. Esto nos importa poco, ya que AutoMapper hace una gestión automática de los valores nulos, de modo que mapea las propiedades coincidentes, y obvia el resto. Igual ocurriría si hubiera alguna propiedad en la entidad que no existiera en el modelo.

Segunda observación: los nombres de las propiedades son idénticos. Esto nos facilitará el trabajo, ya que son los nombres de las propiedades lo que usa AutoMapper para inferir las equivalencias automáticas, y mapear los datos de una clase a otra. Si deseásemos mapear propiedades con nombres diferentes, igual podríamos hacerlo, pero indicando explícitamente esta relación en la configuración.

Bueno, vamos a ver como le indicamos a la librería qué mapeos vamos a querer realizar. En este caso, puesto que nuestras propiedades tienen nombres iguales en ambas clases, no tendremos más que indicar el tipo de origen y destino:

private static MapperConfiguration config_map;
private Mapper mapper;

public labs_repos(string p_str_cnx)
{
    _str_cnx = p_str_cnx;

    if (config_map == null)
    {
        config_map = new MapperConfiguration(cfg => {
            cfg.CreateMap<EFModels.Categories, Categories>().ReverseMap();
            cfg.CreateMap<EFModels.Tests, test_object>().ReverseMap();
            cfg.CreateMap<EFModels.TestCases, TestCases>().ReverseMap();
            cfg.CreateMap<EFModels.Executions, TestCaseExecutions>().ReverseMap();
        });
    }

    mapper = new Mapper(config_map);
}

Vean que el modificador ReverseMap nos sirve para indicar que necesitaremos realizar estos mapeos en ambas direcciones.

Importante señalar que una vez definidas las tipologías de origen y destino, el mapeo se podrá realizar clase contra clase, o también en colecciones de objetos tales como listas.

Otro dato importante es que este código de configuración de los mapeos ha de ejecutarse una única vez, o bien nos generará una excepción. Yo he optado por incluirlo como variable estática en el repositorio para evitar mover la variable MapperConfiguration a través de todas las capas, hasta el punto en que tenga que ser utilizada, pero un uso clásico situaría este código en un fichero de inicialización tal como un Program.cs o un Gobal.asax en el caso de una solución web.

También hay que enfrentarse a la disyuntiva de dónde realizar el mapeo. Podríamos optar por que nuestra capa de datos devolviera un modelo y éste fuera mapeado contra la entidad de dominio como parte de la lógica de negocio. Ésta me parece una buena solución de cara al correcto control de errores. Yo sin embargo he optado por que la capa de datos devuelva ya una entidad de dominio, realizando por tanto en ésta misma el mapeo. El motivo para hacerlo así es mi deseo de compatibilizar los accesos a datos mediante ADO.NET, y mediante EF, para que posteriormente podamos desarrollar algún Test comparativo.

Bueno, pues una vez los mapeos han sido configurados, y hemos decidido que será la capa de datos el lugar donde los ejecutaremos (en concreto, en el propio repositorio), vamos a ver por fin como ejecutarlos:

public List<test_object> getTests()
{
    List<test_object> res = new List<test_object>();

    using (var db = new context.LabsContext(_str_cnx))
    {
        var res_model = db.Test.Include(tc => tc.TestCases).Include(c => c.Categorie).ToList();
        res = mapper.Map(res_model, res);
    }

    return res;
}

¿No íbamos a ver el mapeo de la entidad TestCase? ¿Porque les muestro entonces el método GetTests?

Como podrán ver, este método que devuelve nuestro conjunto de tests incluye la información de los TestCases de cada uno de los test resultantes. Es así que, al mapear la entidad Test, estamos también implícita y automáticamente mapeando la entidad TestCase dependiente. Eso sí, para que esto funcione, en la configuración hemos debido indicarle a AutoMapper la correspondencia entre las clases Test (Modelo <=> Entidad), pero también entre las clases TestCases (Modelo <=> Entidad).

Veamos un ejemplo más. ¿Recuerdan el modificador ReverseMap que añadimos en la configuración de los mapeos? ¿En qué situación nos interesará hacer un mapeo inverso, es decir, recibir una entidad de dominio y convertirla en un modelo EF? Pues la respuesta es bien sencilla: en un método de inserción, borrado o eliminación. Por ejemplo el método InsertTestCase que les muestro a continuación:

public bool insertTestCase(ref TestCases TestCase)
{
    try
    {
        EFModels.TestCases _testcasemodel = mapper.Map<EFModels.TestCases>(TestCase);

        using (var db = new context.LabsContext(_str_cnx))
        {
            db.TestCases.Add(_testcasemodel);
            db.SaveChanges();
        }
    }
    catch (Exception ex)
    {
        return false;
    }

    return true;
}

Un apunte arquitectónico. Sobre los principios SOLID

Ustedes conocerán, o al menos habrán oído hablar de los principios SOLID. Se trata de 5 normas de arquitectura de software encaminadas a estructurar adecuadamente cada una de las piezas que componen una solución. Como todo en la vida, tiene sus seguidores fervorosos y sus detractores. Si bien parece cierto que los mismos tienen una utilidad más obvia en soluciones de gran tamaño, o que tienen previsión de crecer, no seré yo quien entre en polémicas con Pepito, el arquitecto de software que vive en mi cabeza.

En este momento muy probablemente nuestra plataforma de test se encuentre incumpliendo todos y cada uno de estos principio. Bueno, no se alarmen. Iremos como siempre les digo, mejorando juntos. Próximamente explicaremos e implantaremos el último de estos principios (D: Inversión de dependencias) que me parece uno de los más interesantes. No se lo pierdan.

Si desean ir estudiando este asunto les propongo un artículo ilustrado con ejemplos y explicado con gran claridad: Principios SOLID.

CodeFirst: actualizando la base de datos tras un cambio en el modelo EF (en tiempo de ejecución)

En la anterior publicación acerca de la estrategia CodeFirst vimos como crear en tiempo de ejecución una base de datos SQL Server, a partir de la definición de un modelo. Pues bien, ¿qué pasa si aplicamos algún cambio sobre este modelo? Necesitamos actualizar la base de datos, y tal vez también los datos de la misma. Vamos a ver como abordar esta problemática.

Para ilustrarlo, he añadido en el modelo Tests una propiedad de tipo texto a la que he llamado Url_Stackoverflow cuyo cometido creo es bastante obvio. Necesitamos que en la primera ejecución tras la inclusión de este campo en el modelo, la base de datos lo refleje creando un campo nuevo en la correspondiente tabla. Además, acto seguido ejecutaremos la carga de datos, con el fin de informar un valor en este nuevo campo, para el primero de nuestros test (Test 1: Multithreading Vs Singlethreading)

En nuestro formulario inicial, llamado frmMonitor, en el evento Load  habíamos ya desarrollado una comprobación de si la base de datos existe, y si no existe, crearla. Sobre ese código lanzaremos la comprobación para las subsiguientes actualizaciones:

        private void frmMonitor_Load(object sender, EventArgs e)
        {
            registry_functions _reg = new registry_functions();

            bool existeBBDD = _reg.ExisteBBDD();

            //primero comprobamos si existe la BBDD
            if (!existeBBDD)
            {
                frmStart _frm = new frmStart(this, _DataSystem);
                _frm.ShowDialog();
            }
            else
            {
                data_functions _df = new data_functions();
                _df.UpdateDatabaseEF(_reg.GetRegisteredServer());

                GetCategories();
            }
        }

Inspeccionemos el código del método UpdateDatabaseEF:

public bool UpdateDatabaseEF(string Server)
{
    try
    {
        ZMLabsData.Migrations.Configuration _confDB = new ZMLabsData.Migrations.Configuration();
        _confDB.CreateOrUpdateDataBase(true, GetCnxEF(Server));
    }
    catch (Exception ex)
    {
        return false;
    }

    return true;
}

Nada relevante de momento. Aquí solo encontramos la instancia del objeto Migrations.Configuration y la llamada al método createOrUpdateDataBase. Esperemos que éste sea el bueno. Recuerden que la clase Configuration es creada automáticamente por EF al habilitar las migraciones. Su finalidad principal es encargarse de la carga de datos inicial, y posteriores actualizaciones.

public Configuration()
{
    AutomaticMigrationsEnabled = true;
    AutomaticMigrationDataLossAllowed = true;
}

public void CreateOrUpdateDataBase(bool Update, string cnx_str)
{
    context.LabsContext _myContext = new context.LabsContext(cnx_str);

    if (Update)
    {
        if (!_myContext.Database.CompatibleWithModel(false))
        {
            Database.SetInitializer(new MigrateDatabaseToLatestVersion<context.LabsContext, Configuration>());

            using (var ctx = new context.LabsContext(cnx_str))
            {
                ctx.Database.Initialize(true);
            }
        }
    }
    else
    {
        _myContext.Database.Create();
        Seed(_myContext);
    }
}

Bien, en este método ya encontramos cositas interesantes: este método existía previamente. Anteriormente se llamaba CreateDatabaseEF y ejecutaba la parte que ahora está en el else, es decir, la creación inicial.

En la parte que corresponde a la actualización, primero hace una llamada al método CompatibleWithModel perteneciente al contexto instanciado previamente. Esto provocará una comparación entre los modelos locales y el esquema de la base de datos, indicándonos si poseen la misma estructura.

Si se detecta algún cambio procedemos con la actualización. Para ello creamos una implementación de IDatabaseInitializer<TContext> donde hemos de indicar qué clase de contexto queremos utilizar, y qué clase contiene las configuraciones a aplicar. Hecho lo cual podremos invocar al método Initialize del context, que definitivamente volcará en base de datos los cambios detectados.

Dos apuntes de enorme importancia. Uno: observen el constructor de la clase Configuration. Hemos establecido la propiedad AutomaticMigrationDataLossAllowed a true. Así, si en lugar de añadir un campo lo que hacemos es suprimirlo, EF realizará igualmente el cambio a pesar de los riesgos de pérdida de información. De lo contrario devolvería un error.

Dos: el método SetInitializer, si no hacemos nada para evitarlo, devolverá un error indicando que el contexto no dispone de un constructor. Para atajar este error lo que tenemos que hacer es implementar el interfaz IDbContextFactory en el context del siguiente modo:

public class MyContextFactory : IDbContextFactory<LabsContext>
{
    public LabsContext Create()
    {
        return new LabsContext(MyCnxBuilder.GetCnx());
    }
}

public class LabsContext : DbContext
{
    public LabsContext(string cnx_str) : base(cnx_str) { }

    public DbSet<EFModels.Categories> Categories { get; set; }
    public DbSet<EFModels.Tests> Test { get; set; }
    public DbSet<EFModels.TestCases> TestCases { get; set; }
    public DbSet<EFModels.Executions> Executions { get; set; }
}

Después de ejecutarse el método Initialize, el sistema de forma automática invoca al método Seed de la clase Configurations para las labores de carga de información. Para este caso he añadido información en la nueva propiedad del modelo para comprobar que efectivamente, el resto de la información queda intacta, y se actualiza la información del nuevo campo en base de datos:

//Tests
EFModels.Tests _test1 = new EFModels.Tests()
{
   id = 1,
   Test = "Multithreading vs Singlethreading",
   Classname = "test1_multithreading_vs_singlethreading",
   Description = "Calcula 500 veces 200 elementos de la serie fibonacci",
   Url_Blog = @"https://enigmasoftwarelabs.blogspot.com/2020/04/test-1-multithreading-vs-singlethreading.html",
   Url_Git = "",
   Url_Stackoverflow = @"https://stackoverflow.com/questions/12390468/multithreading-slower-than-singlethreading",

   Categorie = _cat_Multithreading,
   idCategorie = _cat_Multithreading.id
};

Una vez vemos que todo ha funcionado podemos jugar y poner a prueba el sistema, comentando de nuevo nuestro nuevo campo en el modelo, para comprobar que tras la ejecución éste queda eliminado de la base de datos.

Ahora amigos míos ya no tienen excusa para clonar el proyecto (Github - Enigma Test Labs)  y empezar a producir sus propios test. Si lo hacen por favor no olviden compartirlos. Espero les resultase interesante esta extensa publicación.




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